linux平台的链接与加载
原文是上下兩篇
- 鏈接與加載(上) — 靜態(tài)鏈接
- 鏈接與加載(下) — 動(dòng)態(tài)鏈接
為觀看方便,現(xiàn)在合并起來(lái)。
?
一.靜態(tài)鏈接
示例程序
我們先看一個(gè)簡(jiǎn)單的示例程序,代碼如下:
/*main.c*/ int u = 333;int sum(int, int);int main(int argc, char* argv[]) {sum(argc, 1);return 0; }
?/*sum.c*/ #define pi 3int printf(const char*, ...);int x = 111; int y; const int w = 222; extern int u; int *ptr = &u;static int add(int a) {static int base = 99999;base--;return a + base; }int sum(int a, int b) {static int base = 88888;static int xx;base++;printf("%d\n", base);int t = add(a);return a + b + x + y + w + *ptr + t + base + pi; }
?編譯過(guò)程
通過(guò)一行命令(gcc -static sum.c main.c),即可編譯出一個(gè)可執(zhí)行程序a.out,看似簡(jiǎn)單,其實(shí)背后隱藏了一個(gè)復(fù)雜的過(guò)程:*.c文件經(jīng)過(guò)預(yù)處理器(cpp)、編譯器(cc1)、匯編器(as)生成可重定位目標(biāo)文件*.o,然后多個(gè)可重定位目標(biāo)文件經(jīng)過(guò)鏈接器(ld),生成一個(gè)可執(zhí)行目標(biāo)文件a.out,如下圖:
鏈接器所做的事情:鏈接。其主要需要解決下面兩個(gè)問(wèn)題:
1、符號(hào)解析:將符號(hào)引用與符號(hào)定義聯(lián)系起來(lái)。
2、重定位:將符號(hào)定義與虛擬地址關(guān)聯(lián)起來(lái),并修改所有對(duì)這些符號(hào)的引用。
可重定位目標(biāo)文件
一般來(lái)說(shuō),我們稱*.o這種文件為可重定位目標(biāo)文件,a.out稱為可執(zhí)行目標(biāo)文件,在靜態(tài)鏈接的情況下,我們可以認(rèn)為可重定位目標(biāo)文件為鏈接器的輸入,可執(zhí)行目標(biāo)文件為輸出。實(shí)際上,各種目標(biāo)文件在不同平臺(tái)上,有不同的格式,例如Windows的PE格式、Linux下的ELF格式。接下來(lái)通過(guò)分析一個(gè)典型ELF可重定位目標(biāo)文件,來(lái)看看鏈接器是如何解決上面提到的兩個(gè)問(wèn)題的。使用readelf -a sum.o及objdump -d sum.o可以看到sum.o的詳細(xì)信息。
一個(gè)ELF可重定位目標(biāo)文件,一般包含下面這些關(guān)鍵section:
像.text, .rodata, .data, .bss這些段,都比較好理解。鏈接器將多個(gè)可重定位目標(biāo)文件合并為可以可執(zhí)行程序時(shí),首先要做的就是要合并.text, .rodata, .data, .bss這些段,如下圖:
然后根據(jù).symtab, .rel.text, .rel.data, .strtab等section的信息,進(jìn)行符號(hào)解析及重定位的關(guān)鍵,下面詳解說(shuō)明一下這幾個(gè)段。
字符串表(.strtab)
一個(gè)字符串表,包括.symtab中的符號(hào)表的符號(hào)名,字符串表即以null結(jié)束得字符串序列,類似如下格式(可使用readelf -x獲取):
符號(hào)表(.symtab)
存放了程序中被定義或引用的函數(shù)和全局變量(包含局部靜態(tài)變量)的信息,包括符號(hào)的名字(記錄的是.strtab中的偏移量),地址、大小、類型(數(shù)據(jù)、函數(shù)、段、源文件)、綁定域(本地、全局)、所屬section等,示例程序sum.o的.symtab如下:
可以看到:
1、static修飾的局部變量,例如sum::z,add::base存放在.data section,而sum::xx放在.bss section,但是其符號(hào)名后面增加了一個(gè)數(shù)字后綴,已解決不同函數(shù)里使用相同名字的static變量,且其綁定域是LOCAL,即表示該符號(hào)是本地的,非全局可見(jiàn)。
2、TYPE字段,一般常見(jiàn)的是FUNC、OBJECT、NOTYPE,分別表示函數(shù)、變量、未知類型(例如引用的外部變量或函數(shù),如extern int u;以及在libc.a中定義的printf函數(shù))。
3、Ndx即為該符號(hào)所在的section 索引值,但有3個(gè)特殊值:ABS代表不應(yīng)該被重定位的符號(hào);UND表示未定義的符號(hào)(例如extern int u);COMMOM表示未被分配位置的未初始化數(shù)據(jù)(例如int y)。
重定位表(.rel.xxx)與重定位
重定位表,如rel.text為代碼段的重定位表,rel.data為數(shù)據(jù)段對(duì)應(yīng)的重定位表,一般而言,任何調(diào)用外部函數(shù)或全局變量的指令均需要修改。例如sum函數(shù)調(diào)用了printf函數(shù),在未鏈接之前,sum.o是不知道printf函數(shù)最終真實(shí)地址的,需經(jīng)過(guò)鏈接重定位,將printf函數(shù)關(guān)聯(lián)到一個(gè)地址后,再用該地址,修改call指令。下圖即為sum.o的.rel.text及.rel.data信息,為了更容易理解,將源代碼及匯編代碼放在一起。可以看到,sum.c編譯成sum.o,里面包括全局變量(含局部靜態(tài)變量)、外部函數(shù)等地址均需要重定位。
.rel.xxx的offset表示需要進(jìn)行重定位的位置(即在該section中的偏移量),info由兩部分組成:后8位表示類型,例如0x01表示R_386_32,0x02表示R_386_PC32;前24位為其在.symtab中的索引值。以sum中對(duì)全局變量y的引用為例,在匯編代碼中,看到在.text section的0x5d偏移處引用了y,則在.rel.text有一條對(duì)應(yīng)的記錄,其info信息為0x00001201,即其類型為R_386_32,前24位為0x000012,對(duì)應(yīng)是y在.symtab中的索引值。靜態(tài)局部變量略有不同,其在.symtab中的索引值均指向的是.data section。
R_386_32:重定位一個(gè)使用32位絕對(duì)地址的引用。其地址計(jì)算方法為.symtab中對(duì)應(yīng)的value值加上原始值,以.rel.text的第一條記錄為例,其計(jì)算方法是重定位后.data section地址加上0x00000008,即add函數(shù)里的static basic地址。
R_386_PC32:重定位一個(gè)使用32位PC相關(guān)的地址引用。其地址計(jì)算方法為用被重定位的符號(hào)的實(shí)際運(yùn)行時(shí)地址,加上原始值,減去重定位所影響到的地址單元的實(shí)際運(yùn)行時(shí)地址,最終算得的結(jié)果即得相對(duì)地址。例如重定位后,printf的地址是0x08048cc0,sum的地址是0x0804824c,需要重定位的地址在sum內(nèi)的偏移量為0x38-0x18 = 0x20,則計(jì)算后應(yīng)得的地址為0x08048cc0 - 0x0804824c - 0x20 + 0xfffffffc = 0x00000a50。sum函數(shù)中printf經(jīng)過(guò)重定位后,該語(yǔ)句將變成 call 50 0a 00 00。
靜態(tài)庫(kù)解析符號(hào)引用
接下來(lái),我們看看鏈接器是如何使用靜態(tài)庫(kù)來(lái)解析引用的。
在符號(hào)解析階段,鏈接器從左至右,依次掃描可重定位目標(biāo)文件(*.o)和靜態(tài)庫(kù)(*.a)。在這個(gè)過(guò)程中,鏈接器將維持三個(gè)集合:
集合E:可重定位目標(biāo)文件的集合。
集合U:未解析的符號(hào)集,即符號(hào)表中UNDEF的符號(hào)。
集合D:已定義的符號(hào)集。
初始情況下,E、U、D均為空。
1、對(duì)于每個(gè)輸入文件f,如果是目標(biāo)文件,則將f加入E,并用f中的符號(hào)表修改U、D,然后繼續(xù)下個(gè)文件。
2、如果f是一個(gè)靜態(tài)庫(kù),那么鏈接器將嘗試匹配U中未解析符號(hào)與靜態(tài)庫(kù)成員定義的符號(hào)。如果靜態(tài)庫(kù)中某個(gè)成員m,定義了一個(gè)符號(hào)來(lái)解析U中引用,那么將m加入E中,同時(shí)使用m的符號(hào)表,來(lái)更新U、D。對(duì)靜態(tài)庫(kù)中所有成員目標(biāo)文件反復(fù)進(jìn)行該過(guò)程,直至U和D不再發(fā)生變化。此時(shí),任何不包含在E中的成員目標(biāo)文件都將丟棄,鏈接器將繼續(xù)下一個(gè)文件。
3、當(dāng)所有輸入文件完成后,如果U非空,鏈接器則會(huì)報(bào)錯(cuò),否則合并和重定位E中目標(biāo)文件,構(gòu)建出可執(zhí)行文件。
對(duì)此,我們?cè)倩氐轿恼麻_(kāi)頭的那么問(wèn)題,就比較清晰了,因?yàn)閘ibmgwapi.a以來(lái)于libdnscli.a,但是libdnscli.a放在libmgwapi.a的左邊,導(dǎo)致libdnscli.a里的目標(biāo)文件根本就沒(méi)有加入集合E中。其解決辦法就是交換二者順序,當(dāng)然類似與gcc demo.c -ldnscli -lmgwapi -ldnscli也是可以的。
至此,靜態(tài)鏈接部分大致就這些內(nèi)容,下篇講介紹動(dòng)態(tài)鏈接與程序加載原理。
分析工具
1、readelf:顯示目標(biāo)文件的完整結(jié)構(gòu)。
2、objdump:顯示一個(gè)目標(biāo)文件中所有信息,可以反匯編.text。
3、nm:列出目標(biāo)文件的符號(hào)表中定義的符號(hào)。
?
二.動(dòng)態(tài)鏈接
靜態(tài)庫(kù)解決了程序模塊化、分離編譯、提升編譯效率等問(wèn)題,但是也有一些明顯的缺點(diǎn):1、更新及維護(hù)困難。例如需更新靜態(tài)庫(kù)版本,則需要將應(yīng)用程序與之重新鏈接。如果是一個(gè)基礎(chǔ)類庫(kù),被幾十上百個(gè)程序使用,其更新工作將是極其繁瑣的。
2、空間浪費(fèi)。幾乎每個(gè)進(jìn)程都會(huì)使用的標(biāo)準(zhǔn)I/O函數(shù),例如printf等,這些函數(shù)代碼將被復(fù)制到每個(gè)進(jìn)程的代碼段中,這將會(huì)造成內(nèi)存及磁盤空間的極大浪費(fèi)。
對(duì)此,動(dòng)態(tài)鏈接共享庫(kù)應(yīng)運(yùn)而生。
在正式討論動(dòng)態(tài)鏈接之前,先留一個(gè)小問(wèn)題:下面這兩個(gè)編譯命令有何區(qū)別?為何?
1: gcc -shared -o libxyz.so xyz.c2: gcc -shared -fPIC -o libxyz.so xyz.c?
示例程序 //xyz.c //gcc -shared -fPIC -o libxyz.so xyz.c int printf(const char*, ...); extern int errOffset; int errBase = 1; int setErr() { errOffset = 0x888; errBase = 0x999; printf("setErr\n"); return 0; }
動(dòng)態(tài)共享庫(kù)的難點(diǎn)
要想模塊的更新及維護(hù)更加方便,則必須更加徹底的模塊化,將程序的各個(gè)模塊合理分割,形成獨(dú)立的文件,并且在應(yīng)用程序加載執(zhí)行時(shí),才進(jìn)行鏈接操作。如此,只需模塊與模塊之間的接口保持兼容,則可以很方便的更新任意一個(gè)模塊,而不需要對(duì)應(yīng)用程序做任何操作。同時(shí)也能解決靜態(tài)庫(kù)磁盤空間浪費(fèi)的問(wèn)題,因?yàn)閼?yīng)用程序不需要再將庫(kù)復(fù)制一份。
但是內(nèi)存空間的浪費(fèi),則不是那么好解決。我們先看看一下共享庫(kù)中的全局變量,例如在某個(gè)共享庫(kù)中,申請(qǐng)了一個(gè)全局變量,同時(shí)A進(jìn)程與B進(jìn)程都鏈接了該庫(kù)。我們的實(shí)際使用過(guò)程中知道,A進(jìn)程與B進(jìn)程中這個(gè)全局變量是不會(huì)互相影響的。那么可以推斷,共享庫(kù)中的數(shù)據(jù)段,每個(gè)進(jìn)程都有一個(gè)副本,是不能共享的,示意圖如下:
一個(gè)共享庫(kù),最核心的兩個(gè)段是代碼段和數(shù)據(jù)段,既然數(shù)據(jù)段不能共享(這個(gè)是理所當(dāng)然的),那代碼段必須能共享,否則就節(jié)省不了任何內(nèi)存空間了。
地址無(wú)關(guān)代碼(PIC)
為何需要PIC技術(shù)?
上篇我們講到,鏈接要解決的一個(gè)重要問(wèn)題是重定位:即修改代碼段,將全局變量或外部函數(shù)的地址替換成運(yùn)行時(shí)的真實(shí)地址。如果我們僅僅是將鏈接過(guò)程推遲到運(yùn)行時(shí),那么這就有一個(gè)問(wèn)題:例如共享庫(kù)libxyz.so中有對(duì)errOffset變量的引用,且errOffset是在其他模塊定義的,假設(shè)進(jìn)程A中errOffset的地址是AddrA,進(jìn)程B的地址是AddrB,在進(jìn)程A和B的加載和重定位時(shí),需將代碼段中對(duì)errOffset的地址修改為進(jìn)程對(duì)應(yīng)的實(shí)際地址,而二個(gè)進(jìn)程中地址不一樣,則會(huì)導(dǎo)致二者代碼段不一樣,這會(huì)導(dǎo)致每個(gè)進(jìn)程均需要拷貝一個(gè)代碼段副本出來(lái)(注:如果編譯共享庫(kù)時(shí)沒(méi)有帶上-fPIC選項(xiàng),就是這種效果,跟靜態(tài)庫(kù)的差異僅僅是將鏈接延遲到加載)。這樣就達(dá)不到節(jié)省內(nèi)存空間的效果。而要讓代碼段能在多個(gè)進(jìn)程間共享,那必須保持代碼段在重定位時(shí),不需要被修改。于是,地址無(wú)關(guān)代碼(PIC,Position-independent Code)技術(shù)誕生。
PIC原理
該方案的主要思想是:把代碼段中跟地址相關(guān)的部分放到數(shù)據(jù)段中,使得重定位時(shí),代碼段不需要被修改。主要依賴下面兩個(gè)事實(shí):
1、無(wú)論在何處加載一個(gè)共享庫(kù),其數(shù)據(jù)段總是緊跟在代碼段之后的,即代碼段中任何指令和數(shù)據(jù)段中任何變量之間的距離都是一個(gè)常量,與代碼段和數(shù)據(jù)段的絕對(duì)地址無(wú)關(guān)。
2、目標(biāo)模塊的數(shù)據(jù)段,在各個(gè)進(jìn)程中都有對(duì)應(yīng)的副本,是可以被修改的。
于是編譯器在數(shù)據(jù)段開(kāi)始的地方,創(chuàng)建了一個(gè)表,叫做全局偏移量表(GOT,global offset table)。記錄了該模塊所有外部函數(shù)或全局變量的表目,同時(shí)為GOT中每個(gè)表目生成一個(gè)重定位記錄。在加載時(shí),動(dòng)態(tài)鏈接器更新GOT中表目的值,使得其值為該符號(hào)的運(yùn)行時(shí)絕對(duì)地址。
全局?jǐn)?shù)據(jù)訪問(wèn)的位置無(wú)關(guān)
我們先看看全局變量是如何通過(guò)GOT技術(shù)實(shí)現(xiàn)位置無(wú)關(guān)代碼的。還是以示例程序來(lái)分析,先看一下匯編代碼。
1: 0000055c <setErr>:
2: 55c: 55 push %ebp3: 55d: 89 e5 mov %esp,%ebp4: 55f: 53 push %ebx5: 560: 83 ec 04 sub $0x4,%esp6: 563: e8 00 00 00 00 call 568 <setErr+0xc>7: 568: 5b pop %ebx8: 569: 81 c3 94 11 00 00 add $0x1194,%ebx9: 56f: 8b 83 f8 ff ff ff mov 0xfffffff8(%ebx),%eax10: 575: c7 00 88 08 00 00 movl $0x888,(%eax)11: 57b: 8b 83 ec ff ff ff mov 0xffffffec(%ebx),%eax12: 581: c7 00 99 09 00 00 movl $0x999,(%eax)13: 587: 83 ec 0c sub $0xc,%esp14: 58a: 8d 83 0f ef ff ff lea 0xffffef0f(%ebx),%eax15: 590: 50 push %eax16: 591: e8 c2 fe ff ff call 458 <puts@plt>17: 596: 83 c4 10 add $0x10,%esp18: 599: b8 00 00 00 00 mov $0x0,%eax19: 59e: 8b 5d fc mov 0xfffffffc(%ebp),%ebx20: 5a1: c9 leave21: 5a2: c3 ret從匯編代碼看,第6、7行的作用是獲得當(dāng)前PC值,并將其值存入寄存器ebx,第8、9行,則是計(jì)算errOffset的GOT地址,不妨假設(shè)libxyz.so映射到地址0x40018000,則執(zhí)行完第7行指令后,ebx的值為0x40018000 + 0x568,執(zhí)行完第9行時(shí),eax的值為 0x40018000 + 0x568 + 0x1194 – 0x8 = 0x400196f4 = 0x40018000 + 0x16f4,而這正好就是GOT中errOffset所對(duì)應(yīng)的表目地址。同理可以計(jì)算全局變量errBase所對(duì)應(yīng)的GOT條目偏移量。通過(guò)下面指令,可以驗(yàn)證GOT中各個(gè)條目與計(jì)算是否一致:
注:call指令的效果即將下一條指令壓棧,并跳轉(zhuǎn)。上面的第6行匯編,獲得的效果是,0x40018000 + 0x568 被壓棧,并跳轉(zhuǎn)至第7行匯編(0x40018000 + 0x568),第7行的效果是,將0x40018000 + 0x568彈出,并賦給寄存器ebx,此時(shí)即完成了當(dāng)前PC值的獲取。
$ objdump –R libxyz.so
1: DYNAMIC RELOCATION RECORDS2: OFFSET TYPE VALUE3: ...4: 000016e8 R_386_GLOB_DAT errBase5: ...6: 000016f4 R_386_GLOB_DAT errOffset7: ...8: 00001708 R_386_JUMP_SLOT puts9: ...函數(shù)的延遲綁定
為何需要延遲綁定技術(shù)?
位置無(wú)關(guān)代碼,在性能上,比靜態(tài)鏈接要差一些,要訪問(wèn)一個(gè)全局變量,需要先定位到GOT地址,然后間接尋址。另外一個(gè)降低程序性能的因素是,動(dòng)態(tài)鏈接的工作是在加載時(shí)完成的,即程序啟動(dòng)后,動(dòng)態(tài)鏈接庫(kù)先要完成鏈接過(guò)程,即需尋找并加載所需的共享庫(kù),進(jìn)行符號(hào)搜索和重定位,這無(wú)疑會(huì)影響程序的啟動(dòng)速度,于是就有延遲綁定技術(shù)(PLT,Procedure Linkage Table)。
在動(dòng)態(tài)鏈接下,程序各個(gè)模塊之間包含了大量的函數(shù)調(diào)用(全局變量較少,否則大量全局變量會(huì)增加模塊之間的耦合度,范圍了模塊之間松耦合的原則)。但是往往有很多函數(shù),在整個(gè)程序執(zhí)行過(guò)程中,都不會(huì)被調(diào)用,例如一些錯(cuò)誤處理函數(shù),如果一開(kāi)始全部都在程序啟動(dòng)時(shí)進(jìn)行鏈接,則會(huì)造成浪費(fèi)。PLT技術(shù),則是當(dāng)函數(shù)第一次被使用時(shí)才進(jìn)行鏈接(符號(hào)解析、重定位),如果沒(méi)有用到則不進(jìn)行鏈接。
延遲綁定原理
參考上面匯編代碼中,對(duì)printf函數(shù)的調(diào)用,實(shí)際上是跳到puts@plt的地址,其匯編代碼如下:
1: Disassembly of section .plt:2: ...3: 00000458 <puts@plt>:4: 458: ff a3 0c 00 00 00 jmp *0xc(%ebx)5: 45e: 68 00 00 00 00 push $0x06: 463: e9 e0 ff ff ff jmp 448 <_init+0x18>7: ...在上面對(duì)全局變量的分析中,已知ebx的值為0x40018000 + 0x568 + 0x1194,上面的第4行匯編,即是跳轉(zhuǎn)至0x40018000 + 0x568 + 0x1194 + 0xc = 0x40018000 + 0x1708處(此即puts對(duì)應(yīng)的GOT地址)所存儲(chǔ)的地址。如果該函數(shù)還未被調(diào)用過(guò),則該GOT條目的值被初始化為0x40018000 + 0x45e,即上面的第5行匯編代碼地址,此時(shí)第4行的作用,就是跳轉(zhuǎn)到第5行。其中0x0表示puts這個(gè)符號(hào)在.rel.plt中的索引值,以此作為一個(gè)參數(shù),調(diào)用動(dòng)態(tài)鏈接器,完成符號(hào)解析和重定位操作,并將puts函數(shù)的真實(shí)地址填入puts@GOT中。后續(xù)再次調(diào)用puts函數(shù)時(shí),通過(guò)第4行即可直接跳轉(zhuǎn)到puts函數(shù)的入口。
總體來(lái)說(shuō),動(dòng)態(tài)鏈接的示意圖如下:
同名全局符號(hào)覆蓋(global symbol interpose)
在使用動(dòng)態(tài)鏈接庫(kù)時(shí),必須小心該問(wèn)題。例如下面這個(gè)main.c與libxyz.so編譯(gcc -o b.out -g main.c -L./ -lxyz)時(shí),不會(huì)報(bào)任何錯(cuò)誤。最終main函數(shù)中setErr函數(shù)調(diào)用的是main.c文件中定義的,而不是libxyz.so中定義的。
1: int errOffset = 333;
2: extern int errBase;
3:4: int setErr()
5: {6: errOffset = 888;7: errBase = 999;8: return 0;
9: }10:11: int main(int argc, char* argv[])
12: {13: setErr();14: printf("%d, %d\n", errOffset, errBase);
15: return 0;
16: }對(duì)于這個(gè)問(wèn)題,linux下動(dòng)態(tài)鏈接庫(kù)采用如下處理方式:當(dāng)一個(gè)符號(hào)需求被加入全局符號(hào)表時(shí),如果同名符號(hào)已存在,則忽略后加入的符號(hào)。一般動(dòng)態(tài)鏈接器的加載順序,是按廣度優(yōu)先順序進(jìn)行加載,首先是main,然后是libxyz.so,然后是libc.so等。
總結(jié)
現(xiàn)在我們簡(jiǎn)單回顧一下,鏈接與編譯等過(guò)程的分離,使得代碼的分離編譯,模塊化成為可能。而鏈接過(guò)程解決的兩個(gè)核心問(wèn)題是符號(hào)解析與重定位:符號(hào)解析即相當(dāng)于將符號(hào)與運(yùn)行時(shí)地址關(guān)聯(lián)起來(lái),而重定位則是修正代碼段或數(shù)據(jù)段中的全局變量或函數(shù)的地址。靜態(tài)鏈接庫(kù)在鏈接時(shí),將模塊中相應(yīng)目標(biāo)文件的代碼段和數(shù)據(jù)段直接修正后,拷貝到可執(zhí)行目標(biāo)文件中;而動(dòng)態(tài)鏈接共享庫(kù),與應(yīng)用程序進(jìn)一步分離,并將鏈接過(guò)程延遲到程序加載時(shí)進(jìn)行,同時(shí)通過(guò)PIC技術(shù),使得重定位時(shí),僅需要修改數(shù)據(jù)段,而無(wú)需修改代碼段,于是代碼段可以被多個(gè)進(jìn)程共享,以達(dá)到節(jié)省內(nèi)存空間效果。
無(wú)論是靜態(tài)鏈接還是動(dòng)態(tài)鏈接,拋開(kāi)這些技術(shù)細(xì)節(jié)本身,還有很多其他思想是值得借鑒的:
1、堅(jiān)決的模塊化。小到代碼級(jí)函數(shù),大到大規(guī)模系統(tǒng)的功能Service化,無(wú)一不是模塊化的體現(xiàn)。將C語(yǔ)言代碼轉(zhuǎn)換成可執(zhí)行程序,就要經(jīng)過(guò)預(yù)處理、編譯、匯編、鏈接等模塊的依此處理,這幾個(gè)模塊之間功能清晰,職責(zé)明確,使得這個(gè)復(fù)雜的過(guò)程變得清晰簡(jiǎn)潔,實(shí)為模塊化設(shè)計(jì)的典范。至于如何才能做到這種高內(nèi)聚松耦合的模塊化,可以參閱《Unix編程藝術(shù)》一書(shū),該書(shū)中有部分章節(jié)詳細(xì)談了模塊化的幾大原則:正交性、緊湊性等。
2、Don't Repeat yourself。不重復(fù)造輪子在不同層面有上不同的體現(xiàn),總的來(lái)說(shuō)就是不要讓相同或相似的東西重復(fù)出現(xiàn),如果幾行代碼經(jīng)常重復(fù)出現(xiàn),就應(yīng)該寫(xiě)成函數(shù);如果一個(gè)文件在不同程序中重復(fù)出現(xiàn),那就應(yīng)該編譯成庫(kù)文件。動(dòng)態(tài)共享庫(kù)相對(duì)于靜態(tài)庫(kù),在復(fù)用上更進(jìn)一步:不要讓類似的二進(jìn)制代碼重復(fù)出現(xiàn)在內(nèi)存里。
3、只有必須要做的時(shí)候才去做。動(dòng)態(tài)庫(kù)的延遲綁定技術(shù)、被廣泛應(yīng)用的copy on write(COW)技術(shù)都是這種思路,如果在合適的場(chǎng)景使用,不僅能提升效率,還能節(jié)省資源。
參考資料
1、《深入理解計(jì)算機(jī)系統(tǒng)》。
2、《程序員的自我修養(yǎng)》。
?
總結(jié)
以上是生活随笔為你收集整理的linux平台的链接与加载的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Linux环境下的堆栈--调试C程序
- 下一篇: 汇编包含C代码