深入介紹Linux內核(三)續篇
深入介紹Linux內核
第三章 (續篇)
3.3.3 小括號中的組合語(yǔ)句
括弧對“{…}”用於把變數宣告和語(yǔ)句組合成一個(gè)復合語(yǔ)句(組合語(yǔ)句)或一個(gè)語(yǔ)句塊,這樣在語(yǔ)義上這些語(yǔ)句就等同於一條語(yǔ)句。組合語(yǔ)句的右大括弧后面不需要使用分號。小括號中的組合語(yǔ)句,即形如“({…})”的語(yǔ)句,可以在GNU C中用作一個(gè)運算式使用。 這樣就可以在運算式中使用loop、switch語(yǔ)句和區域變數,因此這種形式的語(yǔ)句通常稱(chēng)為語(yǔ)句運算式。語(yǔ)句運算式具有如下示例的形式:
({int y = foo ( ); int z;
if (y > 0) z = y;
else z = -y;
3 + z ; })
其中組合語(yǔ)句中最后一條語(yǔ)句必須是后面跟隨一個(gè)分號的運算式。這個(gè)運算式(“3+Z”)的值即用作整個(gè)小括號括住語(yǔ)句的值。如果最后一條語(yǔ)句不是運算式,那麼整個(gè)語(yǔ)句運算式就具有void類(lèi)型,因此沒(méi)有值。另外,這種運算式中語(yǔ)句宣告的任何區域變數都會(huì )在整塊語(yǔ)句結束后失效。這個(gè)示例語(yǔ)句可以像如下形式的代入語(yǔ)句來(lái)使用:
res = x + ({略…})+ b;
當然,人們通常不會(huì )象上面這樣寫(xiě)語(yǔ)句,這種語(yǔ)句運算式通常都用來(lái)定義巨集,例如內核原始碼init/main.c程式中讀取CMOS時(shí)鐘資訊的巨集定義:
3.3.4 寄存器變數
GNU對C語(yǔ)言的另一個(gè)擴充是允許我們把一些變數值放到CPU寄存器中,即所謂寄存器變數。這樣CPU就不用經(jīng)?;ㄙM較長(cháng)時(shí)間存取記憶體去取值。寄存器變數可以分為2種:全域寄存器變數和區域寄存器變數。全域寄存器變數會(huì )在程式的整個(gè)執行過(guò)程中保留寄存器專(zhuān)門(mén)用於幾個(gè)全域變數。相反,區域寄存器變數不會(huì )保留指定的寄存器,而僅在內嵌asm組合語(yǔ)句中作為輸入或輸出操作數時(shí)使用專(zhuān)門(mén)的寄存器。gcc編譯器的資料流程分析功能本身有能力確定指定的寄存器何時(shí)含有正在使用的值,何時(shí)可派上其他用場(chǎng)。當gcc資料流程分析功能認為儲存在某個(gè)區域寄存器變數值無(wú)用時(shí)就可能會(huì )刪除之,並且對區域寄存器變數的引用也可能被刪除、移動(dòng)或簡(jiǎn)化。因此,若不想讓gcc作這些最佳化改動(dòng),最好在asm語(yǔ)句中加上volatile關(guān)鍵字。
如果想在嵌入組合語(yǔ)句中把組合指令的輸出直接寫(xiě)到指定的寄存器中,那么此時(shí)使用區域寄存器變數就很方便。由於Linux內核中通常只使用區域寄存器變數,因此這裡我們只對區域寄存器變數的使用方法進(jìn)行討論。在GNU C程式中我們可以在函數中用如下形式定義一個(gè)區域寄存器變數:
register int res__asm__(“ax”);
這裡ax是變數res所希望使用的寄存器。定義這樣一個(gè)寄存器變數並不會(huì )專(zhuān)門(mén)保留這個(gè)寄存器不派其他用途。在程式編譯過(guò)程中,當gcc資料流程控制確定變數值已經(jīng)不用時(shí)就可能將該寄存器派作其他用途,而且對它的引用可能會(huì )被刪除、移動(dòng)或被簡(jiǎn)化。另外,gcc並不保證所編譯出的代碼會(huì )把變數一直放在指定的寄存器中。因此在嵌入組合的指令部分最好不要明確地引用該暫存器並且假設該寄存器肯定引用的是該變數值。然而把該變數用作為asm 的運算元還是能夠保證指定的寄存器被用作該運算元。
3.3.5行內函數
在程式中,透過(guò)把一個(gè)函數宣告為行內(inline)函數,就可以讓gcc把函數的代碼整合到呼叫該函數的代碼中去。這樣處理經(jīng)驗去掉函數呼叫進(jìn)入/退出時(shí)間開(kāi)銷(xiāo),從而肯定能夠加快執行速度。 因此把一個(gè)函數宣告為行內函數的主要目的就是能夠盡量快速的執行函數體。另外,如果行內函數中有常數值,那么在編譯期間gcc就可能用它來(lái)進(jìn)行一些簡(jiǎn)化操作,因此并非所有行內所有行內函數的代碼都會(huì )被嵌入進(jìn)去。行內函數方法對程式碼的長(cháng)度影響并不明顯。使用行內函數的程式編譯產(chǎn)生的目標代碼可能會(huì )長(cháng)一些也可能會(huì )短一些,這需要根據具體情況來(lái)定。
行內函數嵌入呼叫者代碼中的操作是一種最佳化操作,因此只有進(jìn)行最佳化編譯是才回執行代碼嵌入處理。 若編譯過(guò)程中沒(méi)有使用最佳化選項“-O”,那么行內函數的代碼就不會(huì )被真正地嵌入到呼叫者代碼中,而是只作為普通函數呼叫來(lái)處理。 把一個(gè)函數宣告為行內函數的方法是在函數宣告中使用關(guān)鍵字“inline”,例如內核檔fs/inode.c中的如下函數:
01 inline int inc(int *a)
02 {
03 (*a)+ +;
04 }
函數中的某些語(yǔ)句用法可能會(huì )使得行內函數的替換操作無(wú)法正常進(jìn)行,或者甲適合進(jìn)行替換操作。例如使用了可變參數、記憶體分配函數malloca( ),可變長(cháng)度資料類(lèi)型變數、非區域goto語(yǔ)句、以及遞回函數。編譯時(shí)可以使用選項-Winline讓gcc對旗標成inline但不能被替換的函數給出警告資訊以及不能替換的原因。
當一個(gè)函數像下面內核檔fs/inode.c中的函數定義一樣既使用inline又使用static關(guān)鍵字,那麼如果所有對該行內函數的呼叫都被替換而整合在呼叫者中,並且沒(méi)有引用過(guò)行內函數的位址,那麼這個(gè)行內函數自身的組合代碼就不會(huì )被引用過(guò)。因此在這種情況下,除非我們使用選項-fkeep-inline-functions,否則gcc就不會(huì )再為這個(gè)函數自身生成實(shí)際組合代碼。由於某些原因,一些對行內函數的呼叫並不能被整合到函數中去。特別是在行內函數定義之前的呼叫不會(huì )被替換整合,並且也都不能是遞回定義的函數。如果存在一個(gè)不能被替換整合的呼叫,那麼行內函數就會(huì )像平常一樣被編譯成組合代碼。當然,如果程式中有引用行內函數位址的語(yǔ)句,那麼行內函數也會(huì )像平常一樣被編譯成組合代碼。因為對行內函數位址的引用不能被替換嵌入。
20 static inline void wait_on_inode (struct m_inode * inode)
21 {
22 cli ( );
23 while (inode->i_lock)
24 sleep_on(&inode->i_wait);
25 sti ( );
26 }
請注意,行內函數功能已經(jīng)被包括在ISO標準C99中,但是該標準定義的行內函數與gcc定義的有較大區別。ISO標準C99的行內函數語(yǔ)義定義等同于這裡使用組合關(guān)鍵字inline和static的定義,即“省略”了關(guān)鍵字static。若在程式中需要使用C99標準的語(yǔ)義,那麼就需要使用編譯選項 -std=gnu89。不過(guò)為了相容起見(jiàn),在這種情況下還是最好使用inline和static組合。以后gcc將最終預設使用C99的定義,在希望仍然使用這裡定義的語(yǔ)義時(shí),就需要使用選項 -std=gnu89來(lái)指定。
若一個(gè)行內函數的定義沒(méi)有使用關(guān)鍵字static那麼gcc就會(huì )假設其他程式檔中也對這個(gè)函數有呼叫,因為一個(gè)全域符號只能被定義一次,所以該函數就不能再在其他原始檔案中進(jìn)行定義。因此這裡對行內函數的呼叫就不能被替換整合。因此,一個(gè)非靜態(tài)的行內函數總是會(huì )被編譯出自己的組合代碼來(lái)。在這方面ISO標準C99對不使用static關(guān)鍵字的行內函數定義等同於這裡使用static關(guān)鍵字的定義。
如果在定義一個(gè)函數時(shí)同時(shí)指定了inline和extern關(guān)鍵字,那麼該函數定義僅用於行內整合,並且在任何情況下都不會(huì )單獨產(chǎn)生該函數自身的組合代碼,即使明確引用了該函數的位址也不會(huì )產(chǎn)生。這樣的一個(gè)位址會(huì )變成一個(gè)外部參照引用,就好像你僅僅宣告了函數而沒(méi)有定義函數一樣。
關(guān)鍵字inline和extern組合在一起的作用幾乎雷同一個(gè)巨集定義。使用這種組合的方式是把帶有組合關(guān)鍵字的一個(gè)函數定義放在標頭檔中,並且把不含關(guān)鍵字的另一個(gè)同樣定義放在一個(gè)程式庫檔案中。此時(shí)標頭檔中的定義會(huì )讓絕大多數對該函數的呼叫被替換嵌入。如果還有沒(méi)有被替換的對該函數的呼叫,那么就會(huì )使用(引用)程式庫中的復制。Linux 0.1x內核原始碼中檔案include/string.h、lib/string.c就是這種使用方式的一個(gè)例子。例如string.h中定義了如下函數:
而在內核函數庫目錄中,lib/string.c檔把關(guān)鍵字inline和extern都定義為空,見(jiàn)如下所示。因此實(shí)際上就在內核函數庫中又包含了string.h紡所有這類(lèi)函數的一個(gè)復制,即又對這些函數重新定義了一次。
11 #define extern // 定義為空。
12 #define inline // 定義為空。
13 #define __LIBRARY__
14 #define
15
此時(shí)程式庫函數中重新定義的上述strcpy( )函數變成如下形式:
3.4 C與組合語(yǔ)言程式的相互呼叫
為了提高代碼執行效率,內核原始碼中有地方直接使用了組合語(yǔ)言編制。就會(huì )涉及到在兩種語(yǔ)言編制的程式之間的相互呼叫問(wèn)題。本節首先說(shuō)明C語(yǔ)言函數的呼叫機制,然后使用示例來(lái)說(shuō)明兩者函數之間的呼叫方法。
3.4.1 C函數呼叫機制
在linux內核程式boot/head.s執行完基本初始化操作之后,就會(huì )跳轉去執行init/main.lc程式。那麼head.s程式是如何把執行控制轉交給init/main.c程式的呢? 即組合語(yǔ)言程式是如何呼叫執行C語(yǔ)言程式的? 這裡我們首先描述一下函數的呼叫機制、控制權傳遞方式,然后說(shuō)明head.s程式跳轉到C程式的方法。
函數呼叫操作包括從一塊代碼到另一塊代碼之間的雙向資料傳遞和執行控制轉移。資料傳遞透過(guò)函數參數和返回值來(lái)進(jìn)行。另外,我們還需要在進(jìn)入函數時(shí)為函數的區域變數分配儲存空問(wèn),並且在退出函數時(shí)收回這部分空間。Intel80x86 CPU為控制傳遞提供了簡(jiǎn)單的指令,而資料的傳遞和區域變數儲存空間的分配與回收則透過(guò)堆疊操作來(lái)實(shí)現。
堆??蚪Y構和控制轉移權方式
大多數CPU上的程式實(shí)現使用堆棧來(lái)支援函數呼叫操作。堆棧被用來(lái)傳遞函數參數、儲存返回資訊、臨時(shí)保存暫存器原有值以備恢復以及用來(lái)儲存區域資料。單個(gè)函數呼叫操作所使用的堆棧部分被稱(chēng)為堆???Stack frame)結構,其通常結構見(jiàn)圖3-4所示。堆??蚪Y構的兩端由兩個(gè)指標來(lái)指定。暫存器ebp通常用作框架指標(frame pointer) ,而esp則用作堆棧指標(stack pointer)。在函數執行過(guò)程中,堆棧指標esp會(huì )隨著(zhù)資料的入堆棧和出堆棧而移動(dòng),因此函數中對大部分資料的存取都基於框架指標ebp進(jìn)行。
對於函數A呼叫函數B的情況,傳遞給B的參數包含在A(yíng)的堆??蛑?,當A呼叫B時(shí),函數A的返回位址(呼叫返回后繼續執行的指令位址)被壓入堆棧中,堆棧中該位置也明確指明了A堆??虻慕Y束處。而B(niǎo)的堆??騽t從隨后的堆棧部分開(kāi)始,即圖中保存框架指標(ebp)的地方開(kāi)始。再隨后則用於存放任何保存的寄存器值以及函數的臨時(shí)值。
B函數同樣也使用堆棧來(lái)保存不能放在基礎寄存器中的區域變數值。例如由於通常CPU的寄存器數量有限而不能夠存放函數的所有區域資料,或者有些區域變數是陣列或結構,因此必須使用陣列或結構引用來(lái)存取。還有就是C語(yǔ)言的位址操作符‘&’被應用到一個(gè)區域變數上時(shí),我們就需要為該變數生成一個(gè)位址,即為變數的位址指標分配一空問(wèn)。最后,B函數會(huì )使用堆棧來(lái)保存呼叫任何其他函數的參數。
堆棧是往低(小)位址方向擴展的,而esp指向當前堆棧頂端的元素。透過(guò)使用push和pop指令我們可以把資料壓入堆棧中或從堆棧中彈出。對於沒(méi)有指定初始值的資料所需要的儲存空間,我們可以透過(guò)把堆棧指標遞減適當的值來(lái)做到。類(lèi)似地,透過(guò)增加堆棧指標值我們可以回收堆棧中已分配的空間。
指令CALL和RET用於處理函數呼叫和返回操作。呼叫指令CALL的作理是把返回位址壓入堆棧中並且跳轉到被呼叫函數開(kāi)始處執行。返回位址是程式中緊隨呼叫指令CALL后面一條指令的位址。因此當被調函數返回時(shí)就會(huì )從該位置繼續執行。返回指令RET用於彈出堆棧最上方的位址並跳轉到該地址處。在使用該指令之前,應該先正確處理堆棧中內容,使得當前堆棧指標所指位置內容正是先前CALL指令保存的返回位址。另外,若返回值是一個(gè)整數或一個(gè)指標,那么寄存器eax將被預設用來(lái)傳遞返回值。
盡管某一時(shí)刻只有一個(gè)函數在執行,但我們還是需要確定在一個(gè)函數(呼叫者)呼叫其他函數(被呼叫者)時(shí),被呼叫者不會(huì )修改或覆蓋掉呼叫者今后要用到的寄存器內容。因此Intel CPU採用了所有函數必須遵守的寄存器用法統習慣例。該慣例指明,寄存器cax、edx和ecx的內容必須由呼叫者自己負責保存。當函數B被A呼叫時(shí),函數B可以在不用保存這些寄存器內容的情況下任意使用它們而不會(huì )毀壞函數A所需要的任何資料。另外,寄存器ebx、esi和edi的內容則必須由被呼叫者B來(lái)保護。當被呼叫者需要使用這些寄存器中的任意一個(gè)時(shí),必須首先在堆棧中保存其內容,並在退出時(shí)恢復這些寄存器的內容。因為呼叫者A(或者一些更高層的函數)並不負責保存這些寄存器內容,但可能在以后的操作中還需要用到原先的值。還有寄存器ebp和esp也必須遵守第二個(gè)慣例用法。
函數呼叫舉例
作為一個(gè)例子,我們來(lái)觀(guān)察下面C程式exch.c中函數呼叫的處理過(guò)程。該程式交換兩個(gè)變數中的值,並返回它們的差值。
1 void swap (int * a, int *b)
2 {
3 int c;
4 c = *a; *a = *b; *b = c;
5 }
6
7 int main ( )
8 {
9 int a, b;
10 a = 16; b = 32;
11 exchange (&a, &b);
12 return (a – b);
13 }
其中函數swap ()用于交換兩個(gè)變數的值。C程式中的主程序main()也是一個(gè)函數(將在下面說(shuō)明),它在呼叫了swap()之后返回交換后的結果。這兩個(gè)函數的堆疊框結構見(jiàn)圖3-5所示??梢钥闯?,函數swap()從呼叫者(main())的堆??蛑蝎@取其參數。圖中的位置資訊相對于寄存器ebp中的框架指標。堆??蜃筮叺臄底种赋隽讼鄬τ诳蚣苤笜说牡刂菲浦?。在象gdb這樣的除錯器中,這些數值都用2的補數表示。例如‘-4’被表示‘0xFFFFFFFC’,‘-12’會(huì )被表示成‘0xFFFFFFF4’。
呼叫者main()的堆??蚪Y構中包括區域變數a和b的儲存空間,相對于框架指標位于-4和-8偏移處。由于我們需要為這兩個(gè)區域變數生成位址,因此它們必須保存在堆棧中而非簡(jiǎn)單地存放在寄存器中。
使用命令“gcc -Wall -S -oexch.s cxch.c”可以生成該C語(yǔ)言程式的組合語(yǔ)旨程式exch.s代碼,見(jiàn)如下所示(刪除了幾行與討論無(wú)關(guān)的虛擬指令)。
1 .text
2 _swap:
3 pushl %ebp # 保存原ebp值,設置當前函數的框架指標。
4 mov1 %esp,%ebp
5 subl $4,%esp # 為區域變數c在堆棧內分配空間。
6 movl 8(%ebp),%eax # 取函數第l個(gè)參數,該參數是一個(gè)整數類(lèi)型值的指標。
7 movl (%eax),%ecx # 取該指標位置的內容,並保存到區域變數c中。
8 inovl %ecx,-4(%ebp)
9 movl 8 (%ebp),%eax # 再次取第1個(gè)參數,然后取第2個(gè)參數。
10 movl 12 (%cbp),%edz
11 movl (%edx),%ecx # 把第2個(gè)參數所指內容放到第1個(gè)參數所指的位置。
12 movl %ecx,(%cax)
13 movl 12 (%ebp),%eax # 再次取第2個(gè)參數
14 mov1 -4 (%ebp), %ecx # 然後把區域變數c中的內容放到這個(gè)指標所指位置處。
15 mov1 %ecx, (%eax)
16 leave # 恢復原ebp、esp值(即movl %ebp,%esp;pop1 %ebp;)。
17 ret
18 _main :
19 pushl %ebp # 保存原ebp值,設置當前函數的框架指標。
20 movl %esp, %ebp
21 subl $8, %esp # 為整型區域變數a和b在堆疊中分配空間。
22 movl $16,-4(%ebp) # 為區域變數代入初始值(a=16,b=32)。
23 movl $32,-8(%ebp)
24 leal -8 (%ebp), %eax # 為呼叫swap( )函數作準備,取區域變數b的位址。
25 pushl %eax # 作為呼叫的參數並壓入堆棧中.即先壓入第2個(gè)參數。
26 leal -4(%ebp),%eax # 再取區域變數a的位址,作為第l個(gè)參數人堆疊。
27 pushl %eax
28 call _swap # 什呼叫函數swap( )。
29 movl -4(%ebp), %eax # 取第l個(gè)區域變數a的值,減去第2個(gè)變數b的值。
30 subl -8(%ebp), %eax
31 leave # 恢復原ebp、esp值(即movl %ebp,%esp; pop1 %ebp;)。
32 ret
這兩個(gè)函數均可以劃分成三個(gè)部分:“設置”,初始化堆??蚪Y構;“主體執行函數的實(shí)際計算操作;“結束”,恢復堆棧狀態(tài)並從函數中返回。對於swap()函數,其設置部分代碼是3--5行。前兩行用來(lái)設置保存呼叫者的框架指標和設置本函數的堆??蛑笜?,第5行透過(guò)把堆棧指標esp下移4位元組為區域變數c分配空間。行6--15足swap函數的主體部分。第6--8行用於取呼叫者的第l個(gè)參數&a,並以該參數作為位址取所存內容到ecx寄存器中,然后保存到為區域變數分配的空間中(-4(%ebp)) 。第9—12行用於取第2個(gè)參數&b,並以該參數值作為位址取其內容放到第l個(gè)參數指定的位址處。第13--15行把保存在臨時(shí)域變數c中的值存放到第2個(gè)參數指定的地址處。最后16--17行是函數結束部分。leave指令用於處理堆棧內容以準備返回,它的作用等價(jià)於下面兩個(gè)指令:
movl %ebp,%esp #恢復原esp的值(指向堆??蜷_(kāi)始處)。
popl %ebp #恢復原ebp的值(通常是呼叫者的框架指標)。
這部分代碼恢復了在進(jìn)入swap()函數時(shí)暫存器esp和ebp的原有值,並執行返回指令ret。
第19--2l行是main ( )函數的設置部分,在保存和重新設置框架指標之后main()為區域變數a和b在堆棧中分配了空間。第22--23行為這兩個(gè)區域變數代入。從2--28行可以看出main()中足如何呼叫swap( )函數的。其中首先使用leal指令(取有效位址)獲得變數b和a的位址並分別壓入堆棧中,然后呼叫swap()函數。變數位址壓入堆棧中的順序正好與函數申明的參數順序相反。即函數最后一個(gè)參數首先壓入堆棧中,而函數的第l個(gè)參數則是最后一個(gè)在呼叫函數指令call之前壓入堆棧中的。第29--30兩行將兩個(gè)已經(jīng)交換過(guò)的數字相減,並放在eax寄存器中作為返回值。
從以上分析可知,C語(yǔ)言在呼叫函數時(shí)是在堆棧上臨時(shí)存放被調函數參數的值,即C語(yǔ)言是傳值類(lèi)語(yǔ)言,沒(méi)有直接的方式在被呼叫函數中修改呼叫者一個(gè)變數的值。因此為了達到修改的目的就需要向函數傳遞變數的指標(即變數的位址)。
Main ( )也是一個(gè)函數
上面這段組合語(yǔ)言程式足使用gcc 1.40編譯產(chǎn)生的,可以看出其中有幾行多余的代碼??梢?jiàn)當時(shí)的gcc編譯器還不能產(chǎn)生最高效率的代碼,這也是為什麼某些關(guān)鍵代碼需要直接使用組合語(yǔ)言編制的原因之…另外,上面提到C程式的程式main( )也是一個(gè)函數。這是因為在編譯鏈結時(shí)它將會(huì )作為crt0.s組合語(yǔ)言程式的函數被呼叫。crt0.s是一個(gè)樁(stub)程式,它被鏈結在每個(gè)用戶(hù)執行程的開(kāi)始部分,主要用於設置一些初始化全域變數等。 Linux 0.12中crt0.s組合語(yǔ)言程式見(jiàn)如下所示。其中建立並初始化全域變數_environ供程式中其他模組使用。
1 .text
2 .globl_environ # 宣告全域變數_environ(對應C程式中的environ變數)。
3
4 __entry: # 代碼人口標號。
5 movl 8(%esp), %eaxt # 取程式的環(huán)境變數指標envp並保存在_environ中。
6 movl %eax, _environ # envp是execve( )函數在載入執行檔時(shí)設置的。
7 call _main # 呼叫我們的主程序”其返回狀態(tài)值在eax暫存器中。
8 pushl %eax # 壓入返回值作為exit( )函數的參數並呼叫該函數。
9 1: call _exit
l0 jmp 1b # 控制應該不會(huì )到達這里。若到達這里則繼續執行exit()。
11 .data
12 _environ: # 定義變數_environ,為其分配一個(gè)長(cháng)字空間。
13 .long 0
通常使用gcc編譯鏈結生成執行檔時(shí),gcc會(huì )自動(dòng)把該檔案的代碼作為第一個(gè)模組鏈結在可執行程式中。在編譯時(shí)使用顯示詳細資訊選項‘V’就可以明顯地看出這個(gè)鏈結操作過(guò)程:
[/usr/root] #gcc -v -o exch exch.s
gcc version 1.40
/usr/local/lib/gcc-as -o exch.o exch.s
/usr/local/lib/gcc-ld -o exch/usr/local/lib/crt0.o exch.o/usr/local/lib/gnulib -lc
/usr/local/lib/gnulib
[/usr/root] #
因此在通常的編譯過(guò)程中我們無(wú)需特別指定stub模組crt0.o,但是若想從上面給出的組合語(yǔ)言程式手工使用ld (gld)從exch.o模組鏈結產(chǎn)生可執行檔exch,那麼我們就需要在命令行上特別指明crt0.o這個(gè)模組,並且鏈結的順序應該是“crt0.o、所有程式模組、程式庫檔案”。
為了使用ELF格式的目標檔以及建立共用程式%B