Android的init過程(二);初始化語言(init.rc)解析
本文使用的軟體版本
Android:4.2.2
Linux核心:3.1.10
本文及後續幾篇文章將對Android的初始化(init)過程進行詳細地、剝絲抽繭式地分析,並且在其中穿插了大量的知識,希望對讀者瞭解Android的啟動過程又所協助。本章主要介紹了與硬體相關初始設定檔案名的確定以及屬性服務的原理和實現。
Android本質上就是一個基於Linux核心的作業系統。與Ubuntu Linux、Fedora Linux類似。只是Android在應用程式層專門為行動裝置添加了一些特有的支援。既然Android是Linux核心的系統,那麼基本的啟動過程也應符合Linux的規則。如果研究過其他Linux系統應該瞭解,一個完整的Linux系統首先會將一個Linux核心裝載到記憶體,也就是編譯Linux核心原始碼產生的bzImage檔案,對於為Android最佳化的Linux核心原始碼會產生zImage檔案。該檔案就是Linux核心的二進位版本。由於zImage在核心空間運行,而我們平常使用的軟體都是在應用空間運行(關於核心空間和應用空間的詳細描述,可以參考《Android深度探索(卷1):HAL與驅動開發》一書的內容,在後續的各卷中將會對Android的整體體系進行全方位的剖析)。核心空間和應用空間是不能直接通過記憶體位址層級訪問的,所以就需要建立某種通訊機制。
目前Linux有很多通訊機制可以在使用者空間和核心空間之間互動,例如裝置驅動檔案(位於/dev目錄中)、記憶體檔案(/proc、/sys目錄等)。瞭解Linux的同學都應該知道Linux的重要特徵之一就是一切都是以檔案的形式存在的,例如,一個裝置通常與一個或多個裝置檔案對應。這些與核心空間互動的檔案都在使用者空間,所以在Linux核心裝載完,需要首先建立這些檔案所在的目錄。而完成這些工作的程式就是本文要介紹的init。Init是一個命令列程式。其主要工作之一就是建立這些與核心空間互動的檔案所在的目錄。當Linux核心載入完後,要做的第一件事就是調用init程式,也就是說,init是使用者空間執行的第一個程式。
在分析init的核心代碼之前,還需要初步瞭解init除了建立一些目錄外,還做了如下的工作
1. 初始化屬性
2. 處理設定檔的命令(主要是init.rc檔案),包括處理各種Action。
3. 效能分析(使用bootchart工具)。
4. 無限迴圈執行command(啟動其他的進程)。
儘管init完成的工作不算很多,不過代碼還是非常複雜的。Init程式並不是由一個原始碼檔案組成的,而是由一組原始碼檔案的目標檔案連結而成的。這些檔案位於如下的目錄。
<Android原始碼本目錄>/system/core/init
其中init.c是init的主檔案,現在開啟該檔案,看看其中的內容。由於init是命令列程式,所以分析init.c首先應從main函數開始,現在好到main函數,代碼如下:
int main(int argc, char **argv){ int fd_count = 0; struct pollfd ufds[4]; char *tmpdev; char* debuggable; char tmp[32]; int property_set_fd_init = 0; int signal_fd_init = 0; int keychord_fd_init = 0; bool is_charger = false; if (!strcmp(basename(argv[0]), "ueventd")) return ueventd_main(argc, argv); if (!strcmp(basename(argv[0]), "watchdogd")) return watchdogd_main(argc, argv); /* clear the umask */ umask(0); // 下面的代碼開始建立各種使用者空間的目錄,如/dev、/proc、/sys等 mkdir("/dev", 0755); mkdir("/proc", 0755); mkdir("/sys", 0755); mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755"); mkdir("/dev/pts", 0755); mkdir("/dev/socket", 0755); mount("devpts", "/dev/pts", "devpts", 0, NULL); mount("proc", "/proc", "proc", 0, NULL); mount("sysfs", "/sys", "sysfs", 0, NULL); /* 檢測/dev/.booting檔案是否可讀寫和建立*/ close(open("/dev/.booting", O_WRONLY | O_CREAT, 0000)); open_devnull_stdio(); klog_init(); // 初始化屬性 property_init(); get_hardware_name(hardware, &revision); // 處理核心命令列 process_kernel_cmdline(); … … is_charger = !strcmp(bootmode, "charger"); INFO("property init\n"); if (!is_charger) property_load_boot_defaults(); INFO("reading config file\n"); // 分析/init.rc檔案的內容 init_parse_config_file("/init.rc"); … …// 執行初始設定檔案中的動作 action_for_each_trigger("init", action_add_queue_tail); // 在charger模式下略過mount檔案系統的工作 if (!is_charger) { action_for_each_trigger("early-fs", action_add_queue_tail); action_for_each_trigger("fs", action_add_queue_tail); action_for_each_trigger("post-fs", action_add_queue_tail); action_for_each_trigger("post-fs-data", action_add_queue_tail); } queue_builtin_action(property_service_init_action, "property_service_init"); queue_builtin_action(signal_init_action, "signal_init"); queue_builtin_action(check_startup_action, "check_startup"); if (is_charger) { action_for_each_trigger("charger", action_add_queue_tail); } else { action_for_each_trigger("early-boot", action_add_queue_tail); action_for_each_trigger("boot", action_add_queue_tail); } /* run all property triggers based on current state of the properties */ queue_builtin_action(queue_property_triggers_action, "queue_property_triggers");#if BOOTCHART queue_builtin_action(bootchart_init_action, "bootchart_init");#endif // 進入無限迴圈,建立init的子進程(init是所有進程的父進程) for(;;) { int nr, i, timeout = -1; // 執行命令(子進程對應的命令) execute_one_command(); restart_processes(); if (!property_set_fd_init && get_property_set_fd() > 0) { ufds[fd_count].fd = get_property_set_fd(); ufds[fd_count].events = POLLIN; ufds[fd_count].revents = 0; fd_count++; property_set_fd_init = 1; } if (!signal_fd_init && get_signal_fd() > 0) { ufds[fd_count].fd = get_signal_fd(); ufds[fd_count].events = POLLIN; ufds[fd_count].revents = 0; fd_count++; signal_fd_init = 1; } if (!keychord_fd_init && get_keychord_fd() > 0) { ufds[fd_count].fd = get_keychord_fd(); ufds[fd_count].events = POLLIN; ufds[fd_count].revents = 0; fd_count++; keychord_fd_init = 1; } if (process_needs_restart) { timeout = (process_needs_restart - gettime()) * 1000; if (timeout < 0) timeout = 0; } if (!action_queue_empty() || cur_action) timeout = 0;// bootchart是一個效能統計工具,用於搜集硬體和系統的資訊,並將其寫入磁碟,以便其// 他程式使用#if BOOTCHART if (bootchart_count > 0) { if (timeout < 0 || timeout > BOOTCHART_POLLING_MS) timeout = BOOTCHART_POLLING_MS; if (bootchart_step() < 0 || --bootchart_count == 0) { bootchart_finish(); bootchart_count = 0; } }#endif // 等待下一個命令的提交 nr = poll(ufds, fd_count, timeout); if (nr <= 0) continue; for (i = 0; i < fd_count; i++) { if (ufds[i].revents == POLLIN) { if (ufds[i].fd == get_property_set_fd()) handle_property_set_fd(); else if (ufds[i].fd == get_keychord_fd()) handle_keychord(); else if (ufds[i].fd == get_signal_fd()) handle_signal(); } } } return 0;}
我們可以看到main函數是非常複雜的,不過我們也不需要每條語句都弄得非常清楚(因為這樣弄是非常困難的),通常只需要瞭解init的主線即可。其實從init的main函數可以看出。Init實際上就分為如下兩部分。
1. 初始化(包括建立/dev、/proc等目錄、初始化屬性、執行init.rc等初始設定檔案中的action等)。
2. 使用for迴圈無限迴圈建立子進程。
第一項工作很好理解。而第二項工作是init中的核心。在Linux系統中init是一切應用空間進程的父進程。所以我們平常在Linux終端執行的命令,並建立進程。實際上都是在這個無限的for迴圈中完成的。也就是說,在Linux終端執行ps –e 命令後,看到的所有除了init外的其他進程,都是由init負責建立的。而且init也會常駐內容。當然,如果init掛了,Linux系統基本上就崩潰了。
由於init比較複雜,所以本文只分析其中的一部分,在後續文章中將詳細分析init的各個核心組成部分。
對於main函數最開始完成的建立目錄的工作比較簡單,這部分也沒什麼可以分析的。就是調用了一些普通的API(mkdir)建立一些目錄。現在說一些題外話,由於Android的底層原始碼(包括init)實際上是屬於Linux應用編程領域,所以要想充分理解Android原始碼,除了Linux的基本結構要瞭解外,Linux應用程式層的API需要熟悉。為了滿足這些讀者的需要,後續我會寫一些關於Linux應用編程的文章。Ok,現在言歸正傳,接下來分析一個比較重要的部分:設定檔的解析。
這裡的設定檔主要指init.rc。讀者可以進到Android的shell,會看到根目錄有一個init.rc檔案。該檔案是唯讀,即使有了root許可權,可以修改該檔案也沒有。因為我們在根目錄看到的檔案只是記憶體檔案的鏡像。也就是說,android啟動後,會將init.rc檔案裝載到記憶體。而修改init.rc檔案的內容實際上只是修改記憶體中的init.rc檔案的內容。一旦重啟android,init.rc檔案的內容又會恢複到最初的裝載。想徹底修改init.rc檔案內容的唯一方式是修改Android的ROM中的核心鏡像(boot.img)。其實boot.img名曰核心鏡像,不過該檔案除了包含完整的Linux核心檔案(zImage)外,還包括另外一個鏡像檔案(ramdisk.img)。ramdisk.img就包含了init.rc檔案和init命令。所以只有修改ramdisk.img檔案中的init.rc檔案,並且重新打包boot.img檔案,並刷機,才能徹底修改init.rc檔案。如果讀者有Android原始碼,編譯後,就會看到out目錄中的相關子目錄會產生一個root目錄,該目錄實際上就是ramdisk.img解壓後的內容。會看到有init命令和init.rc檔案。在後續的文章中將會討論具體如何修改init.rc檔案,如何刷機。不過這些內容與本文關係不大,所以不做詳細的討論。
現在回到main函數,在建立完目錄後,會看到執行了如下3個函數。
property_init();
get_hardware_name(hardware, &revision);
process_kernel_cmdline();
其中property_init主要是為屬性分配一些儲存空間,該函數並不是核心。不過當我們查看init.rc檔案時會發現該檔案開始部分用一些import語句匯入了其他的設定檔,例如,/init.usb.rc。大多數設定檔都直接使用了確定的檔案名稱,只有如下的代碼使用了一個變數(${ro.hardware})執行了設定檔名的一部分。那麼這個變數值是從哪獲得的呢?
import /init.${ro.hardware}.rc
首先要瞭解init.${ro.hardware}.rc設定檔的內容通常與當前的硬體有關。現在我們先來關注get_hardware_name函數,代碼如下:
void get_hardware_name(char *hardware, unsigned int *revision){ char data[1024]; int fd, n; char *x, *hw, *rev; /* 如果hardware已經有值了,說明hardware通過核心命令列提供,直接返回 */ if (hardware[0]) return; // 開啟/proc/cpuinfo檔案 fd = open("/proc/cpuinfo", O_RDONLY); if (fd < 0) return; // 讀取/proc/cpuinfo檔案的內容 n = read(fd, data, 1023); close(fd); if (n < 0) return; data[n] = 0; // 從/proc/cpuinfo檔案中擷取Hardware欄位的值 hw = strstr(data, "\nHardware"); rev = strstr(data, "\nRevision"); // 成功擷取Hardware欄位的值 if (hw) { x = strstr(hw, ": "); if (x) { x += 2; n = 0; while (*x && *x != '\n') { if (!isspace(*x)) // 將Hardware欄位的值都轉換為小寫,並更新hardware參數的值 // hardware也就是在init.c檔案中定義的hardware數組 hardware[n++] = tolower(*x); x++; if (n == 31) break; } hardware[n] = 0; } } if (rev) { x = strstr(rev, ": "); if (x) { *revision = strtoul(x + 2, 0, 16); } }}
從get_hardware_name方法的代碼可以得知,該方法主要用於確定hardware和revision的變數的值。Revision這裡先不討論,只要研究hardware。擷取hardware的來源是從Linux核心命令列或/proc/cpuinfo檔案中的內容。Linux核心命令列暫且先不討論(因為很少傳遞該值),先看看/proc/cpuinfo,該檔案是虛擬檔案(記憶體檔案),執行cat /proc/cpuinfo命令會看到該檔案中的內容,1所示。在白框中就是Hardware欄位的值。由於該裝置是Nexus 7,所以值為grouper。如果程式就到此位置,那麼與硬體有關的設定檔名是init.grouper.rc。有Nexus 7的讀者會看到在根目錄下確實有一個init.grouper.rc檔案。說明Nexus 7的原生ROM並沒有在其他的地方設定設定檔名,所以設定檔名就是從/proc/cpuinfo檔案的Hardware欄位中取的值。
圖1
現在來看在get_hardware_name函數後面調用的process_kernel_cmdline函數,代碼如下:
static void process_kernel_cmdline(void){ /* don't expose the raw commandline to nonpriv processes */ chmod("/proc/cmdline", 0440); // 匯入核心命令列參數 import_kernel_cmdline(0, import_kernel_nv); if (qemu[0]) import_kernel_cmdline(1, import_kernel_nv); // 用屬性值設定核心變數 export_kernel_boot_props();}
在process_kernel_cmdline函數中除了使用import_kernel_cmdline函數匯入核心變數外,主要的功能就是調用export_kernel_boot_props函數通過屬性設定核心變數,例如,通過ro.boot.hardware屬性設定hardware變數,也就是說可以通過ro.boot.hardware屬性值可以修改get_hardware_name函數中從/proc/cpuinfo檔案中得到的hardware欄位值。下面看一下export_kernel_boot_props函數的代碼。
static void export_kernel_boot_props(void){ char tmp[PROP_VALUE_MAX]; const char *pval; unsigned i; struct { const char *src_prop; const char *dest_prop; const char *def_val; } prop_map[] = { { "ro.boot.serialno", "ro.serialno", "", }, { "ro.boot.mode", "ro.bootmode", "unknown", }, { "ro.boot.baseband", "ro.baseband", "unknown", }, { "ro.boot.bootloader", "ro.bootloader", "unknown", }, }; // 通過核心的屬性設定應用程式層設定檔的屬性 for (i = 0; i < ARRAY_SIZE(prop_map); i++) { pval = property_get(prop_map[i].src_prop); property_set(prop_map[i].dest_prop, pval ?: prop_map[i].def_val); } // 根據ro.boot.console屬性的值設定console變數 pval = property_get("ro.boot.console"); if (pval) strlcpy(console, pval, sizeof(console)); /* save a copy for init's usage during boot */ strlcpy(bootmode, property_get("ro.bootmode"), sizeof(bootmode)); /* if this was given on kernel command line, override what we read * before (e.g. from /proc/cpuinfo), if anything */ // 擷取ro.boot.hardware屬性的值 pval = property_get("ro.boot.hardware"); if (pval) // 這裡通過ro.boot.hardware屬性再次改變hardware變數的值 strlcpy(hardware, pval, sizeof(hardware)); // 利用hardware變數的值設定設定ro.hardware屬性 // 這個屬性就是前面提到的設定初始設定檔案名的屬性,實際上是通過hardware變數設定的 property_set("ro.hardware", hardware); snprintf(tmp, PROP_VALUE_MAX, "%d", revision); property_set("ro.revision", tmp); /* TODO: these are obsolete. We should delete them */ if (!strcmp(bootmode,"factory")) property_set("ro.factorytest", "1"); else if (!strcmp(bootmode,"factory2")) property_set("ro.factorytest", "2"); else property_set("ro.factorytest", "0");}
從export_kernel_boot_props函數的代碼可以看出,該函數實際上就是來回設定一些屬性值,並且利用某些屬性值修改console、hardware等變數。其中hardware變數(就是一個長度為32的字元數組)在get_hardware_name函數中已經從/proc/cpuinfo檔案中獲得過一次值了,在export_kernel_boot_props函數中又通過ro.boot.hardware屬性設定了一次值,不過在Nexus 7中並沒有設定該屬性,所以hardware的值仍為grouper。最後用hardware變數設定ro.hardware屬性,所以最後的初始設定檔案名為init.grouper.rc。
這裡還有一個問題,前面多次提到屬性或屬性檔案,那麼這些屬性檔案指的是什麼呢?是init.rc?當然不是。實際上這些屬性檔案是一些列位於不同目錄,系統依次讀取的設定檔。
屬性服務(Property Service)
在研究這些設定檔之前應先瞭解init是如何處理這些屬性的。編寫過Windows本地應用的讀者都應瞭解,在windows中有一個註冊表機制,在註冊表中提供了大量的屬性。在Linux中也有類似的機制,這就是屬性服務。init在啟動的過程中會啟動屬性服務(Socket服務),並且在記憶體中建立一Block Storage地區,用來儲存這些屬性。當讀取這些屬性時,直接從這一記憶體地區讀取,如果修改屬性值,需要通過Socket串連屬性服務完成。在init.c檔案中的一個action函數中調用了start_property_service函數來啟動屬性服務,action是init.rc及其類似檔案中的一種執行機制,由於內容比較多,所以關於init.rc檔案中的執行機制將在下一篇文章中詳細討論。
現在順藤摸瓜,找到start_property_service函數,該函數在Property_service.c檔案中,該檔案與init.c檔案中同一個目錄。
void start_property_service(void){ int fd; // 裝載不同的屬性檔案 load_properties_from_file(PROP_PATH_SYSTEM_BUILD); load_properties_from_file(PROP_PATH_SYSTEM_DEFAULT); load_override_properties(); /* Read persistent properties after all default values have been loaded. */ load_persistent_properties(); // 建立socket服務(屬性服務) fd = create_socket(PROP_SERVICE_NAME, SOCK_STREAM, 0666, 0, 0); if(fd < 0) return; fcntl(fd, F_SETFD, FD_CLOEXEC); fcntl(fd, F_SETFL, O_NONBLOCK); // 開始服務監聽 listen(fd, 8); property_set_fd = fd;}
現在我們已經知道屬性服務的啟動方式了,那麼在start_property_service函數中還涉及到如下兩個宏。
PROP_PATH_SYSTEM_BUILD
PROP_PATH_SYSTEM_DEFAULT
這兩個宏都是系統預定義的屬性檔案名稱的路徑。為了擷取這些宏的定義,我們先進行另外一個函數的分析。
在前面讀取屬性值時使用過一個property_get函數,該函數在Property_service.c中實現,代碼如下:
const char* property_get(const char *name){ prop_info *pi; if(strlen(name) >= PROP_NAME_MAX) return 0; pi = (prop_info*) __system_property_find(name); if(pi != 0) { return pi->value; } else { return 0; }}
可以看到,在property_get函數中調用了一個核心函數__system_property_find,該函數真正實現了擷取屬性值的功能。該函數屬於bionic的一個library,在system_properties.c檔案中實現,讀者可以在如下的目錄找到該檔案。
<Android原始碼根目錄>/bionic/libc/bionic
__system_property_find函數的代碼如下:
const prop_info *__system_property_find(const char *name){ // 擷取屬性儲存區記憶體地區的首地址 prop_area *pa = __system_property_area__; unsigned count = pa->count; unsigned *toc = pa->toc; unsigned len = strlen(name); prop_info *pi; while(count--) { unsigned entry = *toc++; if(TOC_NAME_LEN(entry) != len) continue; pi = TOC_TO_INFO(pa, entry); if(memcmp(name, pi->name, len)) continue; return pi; } return 0;}
從__system_property_find函數的代碼很容易看出,第一行使用了一個__system_property_area__變數,該變數是全域的。在前面分析main函數時涉及到一個property_init函數,該函數調用了init_property_area函數,該函數用於初始化屬性記憶體地區,也就是__system_property_area__變數。
static int init_property_area(void){ prop_area *pa; if(pa_info_array) return -1; if(init_workspace(&pa_workspace, PA_SIZE)) return -1; fcntl(pa_workspace.fd, F_SETFD, FD_CLOEXEC); pa_info_array = (void*) (((char*) pa_workspace.data) + PA_INFO_START); pa = pa_workspace.data; memset(pa, 0, PA_SIZE); pa->magic = PROP_AREA_MAGIC; pa->version = PROP_AREA_VERSION; /* 初始化屬性記憶體地區,屬性服務會使用該地區 */ __system_property_area__ = pa; property_area_inited = 1; return 0;}
在前面涉及到的system_properties.c檔案對應的標頭檔system_properties.h中定義了前面提到的兩個表示屬性檔案路徑的宏,其實還有另外兩個表示路徑的宏,一共4個屬性檔案。system_properties.h檔案可以在<Android原始碼根目錄>/bionic/libc/include/sys目錄中找到。這4個宏定義如下:
#define PROP_PATH_RAMDISK_DEFAULT "/default.prop"#define PROP_PATH_SYSTEM_BUILD "/system/build.prop"#define PROP_PATH_SYSTEM_DEFAULT "/system/default.prop"#define PROP_PATH_LOCAL_OVERRIDE "/data/local.prop"
現在讀者可以進入Android裝置的相應目錄,通常可以找到上述4個檔案,如一般會在根目錄,會發現一個default.prop檔案,cat default.prop會看到該檔案的內容。而屬性服務就是裝載所有這4個屬性檔案中的所有屬性以及使用property_set設定的屬性。在Android裝置的終端可以直接使用getprop命令從屬性服務擷取所有的屬性值。2所示。getprop命令還可以直接根屬性名稱還擷取具體的屬性值,例如,getprop ro.build.product。
圖2
如果讀者感興趣,可以看一下getprop是如何通過屬性服務讀寫屬性的。getprop命令的原始碼檔案是getprop.c。讀者可以在<Android原始碼根目錄>/system/core/toolbox目錄中找到該檔案。實際上,getprop擷取屬性值也是通過property_get函數完成的。在前面分析過該函數,實際上調用了__system_property_find函數從__system_property_area__變數指定的記憶體地區擷取相應的屬性值。
此外在system_properties.c檔案中還有如下兩個函數用於通過屬性服務修改或添加某個屬性的值。
static int send_prop_msg(prop_msg *msg){ struct pollfd pollfds[1]; struct sockaddr_un addr; socklen_t alen; size_t namelen; int s; int r; int result = -1; // 建立用於串連屬性服務的socket s = socket(AF_LOCAL, SOCK_STREAM, 0); if(s < 0) { return result; } memset(&addr, 0, sizeof(addr)); // property_service_socket是Socket裝置檔案名稱 namelen = strlen(property_service_socket); strlcpy(addr.sun_path, property_service_socket, sizeof addr.sun_path); addr.sun_family = AF_LOCAL; alen = namelen + offsetof(struct sockaddr_un, sun_path) + 1; if(TEMP_FAILURE_RETRY(connect(s, (struct sockaddr *) &addr, alen)) < 0) { close(s); return result; } r = TEMP_FAILURE_RETRY(send(s, msg, sizeof(prop_msg), 0)); if(r == sizeof(prop_msg)) { pollfds[0].fd = s; pollfds[0].events = 0; r = TEMP_FAILURE_RETRY(poll(pollfds, 1, 250 /* ms */)); if (r == 1 && (pollfds[0].revents & POLLHUP) != 0) { result = 0; } else { result = 0; } } close(s); return result;}// 使用者可以直接調用該函數設定屬性值int __system_property_set(const char *key, const char *value){ int err; int tries = 0; int update_seen = 0; prop_msg msg; if(key == 0) return -1; if(value == 0) value = ""; if(strlen(key) >= PROP_NAME_MAX) return -1; if(strlen(value) >= PROP_VALUE_MAX) return -1; memset(&msg, 0, sizeof msg); msg.cmd = PROP_MSG_SETPROP; strlcpy(msg.name, key, sizeof msg.name); strlcpy(msg.value, value, sizeof msg.value); // 設定屬性值 err = send_prop_msg(&msg); if(err < 0) { return err; } return 0;}
在send_prop_msg函數中涉及到一個property_service_socket變數,定義如下:
static const char property_service_socket[] = "/dev/socket/" PROP_SERVICE_NAME;
實際上,send_prop_msg通過這個裝置檔案與屬性服務通訊的。讀者可以在Android裝置的終端進入/dev/socket目錄,通常會看到一個property_service檔案,該檔案就是屬性服務對應的裝置檔案。
現在已經分析完了init如何確定與硬體相關的初始設定檔案名(init.grouper.rc),並且討論了4個屬性檔案及其裝載過程,以及屬性服務實現的基本原理。在下一篇文章中將討論更深入的內容,例如,init.rc檔案中提供了很多action,那麼什麼是aciton呢,init有是如何解析init.rc檔案呢?這些內容都將在下一篇文章中揭曉。