原作者的E-mail是zhanglei@sict.ac.cn 0. Contents
IDTH: 100%; HEIGHT: 2px"> 每一個(gè)鏈接過程都由鏈接腳本(linker script, 一般以lds作為文件的后綴名)控制. 鏈接腳本主要用于規(guī)定如何把輸入文件內(nèi)的section放入輸出文件內(nèi), 并控制輸出文件內(nèi)各部分在程序地址空間內(nèi)的布局. 但你也可以用連接命令做一些其他事情.
鏈接器把一個(gè)或多個(gè)輸入文件合成一個(gè)輸出文件.
有時(shí)把輸入文件內(nèi)的section稱為輸入section(input section), 把輸出文件內(nèi)的section稱為輸出section(output sectin). 目標(biāo)文件的每個(gè)section至少包含兩個(gè)信息: 名字和大小. 大部分section還包含與它相關(guān)聯(lián)的一塊數(shù)據(jù), 稱為section contents(section內(nèi)容). 一個(gè)section可被標(biāo)記為“l(fā)oadable(可加載的)”或“allocatable(可分配的)”. loadable section: 在輸出文件運(yùn)行時(shí), 相應(yīng)的section內(nèi)容將被載入進(jìn)程地址空間中. allocatable section: 內(nèi)容為空的section可被標(biāo)記為“可分配的”. 在輸出文件運(yùn)行時(shí), 在進(jìn)程地址空間中空出大小同section指定大小的部分. 某些情況下, 這塊內(nèi)存必須被置零. 如果一個(gè)section不是“可加載的”或“可分配的”, 那么該section通常包含了調(diào)試信息. 可用objdump -h命令查看相關(guān)信息. 每個(gè)“可加載的”或“可分配的”輸出section通常包含兩個(gè)地址: VMA(virtual memory address虛擬內(nèi)存地址或程序地址空間地址)和LMA(load memory address加載內(nèi)存地址或進(jìn)程地址空間地址). 通常VMA和LMA是相同的.
可這樣來理解VMA和LMA, 假設(shè): (1) .data section對應(yīng)的VMA地址是0x08050000, 該section內(nèi)包含了3個(gè)32位全局變量, i、j和k, 分別為1,2,3. (2) .text section內(nèi)包含由"printf( "j=%d ", j );"程序片段產(chǎn)生的代碼. 連接時(shí)指定.data section的VMA為0x08050000, 產(chǎn)生的printf指令是將地址為0x08050004處的4字節(jié)內(nèi)容作為一個(gè)整數(shù)打印出來。 如果.data section的LMA為0x08050000,顯然結(jié)果是j=2 如果.data section的LMA為0x08050004,顯然結(jié)果是j=1 還可這樣理解LMA: .text section內(nèi)容的開始處包含如下兩條指令(intel i386指令是10字節(jié),每行對應(yīng)5字節(jié)): jmp 0x08048285 movl $0x1,%eax 如果.text section的LMA為0x08048280, 那么在進(jìn)程地址空間內(nèi)0x08048280處為“jmp 0x08048285”指令, 0x08048285處為movl $0x1,%eax指令. 假設(shè)某指令跳轉(zhuǎn)到地址0x08048280, 顯然它的執(zhí)行將導(dǎo)致%eax寄存器被賦值為1. 如果.text section的LMA為0x08048285, 那么在進(jìn)程地址空間內(nèi)0x08048285處為“jmp 0x08048285”指令, 0x0804828a處為movl $0x1,%eax指令. 假設(shè)某指令跳轉(zhuǎn)到地址0x08048285, 顯然它的執(zhí)行又跳轉(zhuǎn)到進(jìn)程地址空間內(nèi)0x08048285處, 造成死循環(huán). 符號(symbol): 每個(gè)目標(biāo)文件都有符號表(SYMBOL TABLE), 包含已定義的符號(對應(yīng)全局變量和static變量和定義的函數(shù)的名字)和未定義符號(未定義的函數(shù)的名字和引用但沒定義的符號)信息. 符號值: 每個(gè)符號對應(yīng)一個(gè)地址, 即符號值(這與c程序內(nèi)變量的值不一樣, 某種情況下可以把它看成變量的地址). 可用nm命令查看它們. (nm的使用方法可參考本blog的GNU binutils筆記) 3. 腳本格式 鏈接腳本由一系列命令組成, 每個(gè)命令由一個(gè)關(guān)鍵字(一般在其后緊跟相關(guān)參數(shù))或一條對符號的賦值語句組成. 命令由分號‘;’分隔開. 文件名或格式名內(nèi)如果包含分號';'或其他分隔符, 則要用引號‘"’將名字全稱引用起來. 無法處理含引號的文件名. /* */之間的是注釋。 4. 簡單例子 在介紹鏈接描述文件的命令之前, 先看看下述的簡單例子: 以下腳本將輸出文件的text section定位在0x10000, data section定位在0x8000000: SECTIONS { . = 0x10000; .text : { *(.text) } . = 0x8000000; .data : { *(.data) } .bss : { *(.bss) } } 解釋一下上述的例子: . = 0x10000 : 把定位器符號置為0x10000 (若不指定, 則該符號的初始值為0). .text : { *(.text) } : 將所有(*符號代表任意輸入文件)輸入文件的.text section合并成一個(gè).text section, 該section的地址由定位器符號的值指定, 即0x10000. . = 0x8000000 :把定位器符號置為0x8000000 .data : { *(.data) } : 將所有輸入文件的.text section合并成一個(gè).data section, 該section的地址被置為0x8000000. .bss : { *(.bss) } : 將所有輸入文件的.bss section合并成一個(gè).bss section,該section的地址被置為0x8000000+.data section的大小. 連接器每讀完一個(gè)section描述后, 將定位器符號的值*增加*該section的大小. 注意: 此處沒有考慮對齊約束. 5. 簡單腳本命令 - 1 -
入口地址(entry point): 進(jìn)程執(zhí)行的第一條用戶空間的指令在進(jìn)程地址空間的地址) ld有多種方法設(shè)置進(jìn)程入口地址, 按一下順序: (編號越前, 優(yōu)先級越高) 1, ld命令行的-e選項(xiàng) 2, 連接腳本的ENTRY(SYMBOL)命令 3, 如果定義了start符號, 使用start符號值 4, 如果存在.text section, 使用.text section的第一字節(jié)的位置值 5, 使用值0 - 2 -
相當(dāng)于c程序內(nèi)的的#include指令, 用以包含另一個(gè)鏈接腳本. 腳本搜索路徑由-L選項(xiàng)指定. INCLUDE指令可以嵌套使用, 最大深度為10. 即: 文件1內(nèi)INCLUDE文件2, 文件2內(nèi)INCLUDE文件3... , 文件10內(nèi)INCLUDE文件11. 那么文件11內(nèi)不能再出現(xiàn) INCLUDE指令了. - 3 -
ld首先在當(dāng)前目錄下尋找該文件, 如果沒找到, 則在由-L指定的搜索路徑下搜索. file可以為 -lfile形式,就象命令行的-l選項(xiàng)一樣. 如果該命令出現(xiàn)在暗含的腳本內(nèi), 則該命令內(nèi)的file在鏈接過程中的順序由該暗含的腳本在命令行內(nèi)的順序決定. - 4 -
file必須是庫文件, 且file文件作為一組被ld重復(fù)掃描,直到不在有新的未定義的引用出現(xiàn)。 - 5 -
同ld的-o選項(xiàng), 不過-o選項(xiàng)的優(yōu)先級更高. 所以它可以用來定義默認(rèn)的輸出文件名. 如a.out - 6 -
同ld的-L選項(xiàng), 不過由-L指定的路徑要比它定義的優(yōu)先被搜索。 - 7 -
在鏈接過程中, 每個(gè)輸入文件是有順序的. 此命令設(shè)置文件filename為第一個(gè)輸入文件。 - 8 -
同ld選項(xiàng)-o format BFDNAME, 不過ld選項(xiàng)優(yōu)先級更高. - 9 -
若有命令行選項(xiàng)-EB, 則使用第2個(gè)BFD格式; 若有命令行選項(xiàng)-EL,則使用第3個(gè)BFD格式.否則默認(rèn)選第一個(gè)BFD格式.
同ld選項(xiàng)-b BFDNAME. 若使用了TARGET命令, 但未使用OUTPUT_FORMAT命令, 則最用一個(gè)TARGET命令設(shè)置的BFD格式將被作為輸出文件的BFD格式. 另外還有一些: ASSERT(EXP, MESSAGE):如果EXP不為真,終止連接過程 EXTERN(SYMBOL SYMBOL ...):在輸出文件中增加未定義的符號,如同連接器選項(xiàng)-u FORCE_COMMON_ALLOCATION:為common symbol(通用符號)分配空間,即使用了-r連接選項(xiàng)也為其分配 NOCROSSREFS(SECTION SECTION ...):檢查列出的輸出section,如果發(fā)現(xiàn)他們之間有相互引用,則報(bào)錯(cuò)。對于某些系統(tǒng),特別是內(nèi)存較緊張的嵌入式系統(tǒng),某些section是不能同時(shí)存在內(nèi)存中的,所以他們之間不能相互引用。 OUTPUT_ARCH(BFDARCH):設(shè)置輸出文件的machine architecture(體系結(jié)構(gòu)),BFDARCH為被BFD庫使用的名字之一。可以用命令objdump -f查看。 可通過 man -S 1 ld查看ld的聯(lián)機(jī)幫助, 里面也包括了對這些命令的介紹. 6. 對符號的賦值 在目標(biāo)文件內(nèi)定義的符號可以在鏈接腳本內(nèi)被賦值. (注意和C語言中賦值的不同!) 此時(shí)該符號被定義為全局的. 每個(gè)符號都對應(yīng)了一個(gè)地址, 此處的賦值是更改這個(gè)符號對應(yīng)的地址. e.g. 通過下面的程序查看變量a的地址: /* a.c */ #include <stdio.h> int a = 100; int main(void) { printf( "&a=0x%p ", &a ); return 0; } /* a.lds */ a = 3; $ gcc -Wall -o a-without-lds a.c &a = 0x8049598 $ gcc -Wall -o a-with-lds a.c a.lds &a = 0x3
一些簡單的賦值語句 能使用任何c語言內(nèi)的賦值操作: SYMBOL = EXPRESSION ; SYMBOL += EXPRESSION ; SYMBOL -= EXPRESSION ; SYMBOL *= EXPRESSION ; SYMBOL /= EXPRESSION ; SYMBOL <<= EXPRESSION ; SYMBOL >>= EXPRESSION ; SYMBOL &= EXPRESSION ; SYMBOL |= EXPRESSION ; 除了第一類表達(dá)式外, 使用其他表達(dá)式需要SYMBOL被定義于某目標(biāo)文件。 . 是一個(gè)特殊的符號,它是定位器,一個(gè)位置指針,指向程序地址空間內(nèi)的某位置(或某section內(nèi)的偏移,如果它在SECTIONS命令內(nèi)的某section描述內(nèi)),該符號只能在SECTIONS命令內(nèi)使用。 注意:賦值語句包含4個(gè)語法元素:符號名、操作符、表達(dá)式、分號;一個(gè)也不能少。 被賦值后,符號所屬的section被設(shè)值為表達(dá)式EXPRESSION所屬的SECTION(參看11. 腳本內(nèi)的表達(dá)式) 賦值語句可以出現(xiàn)在連接腳本的三處地方:SECTIONS命令內(nèi),SECTIONS命令內(nèi)的section描述內(nèi)和全局位置;如下, floating_point = 0; /* 全局位置 */ SECTIONS { .text : { *(.text) _etext = .; /* section描述內(nèi) */ } _bdata = (. + 3) & ~ 4; /* SECTIONS命令內(nèi) */ .data : { *(.data) } } PROVIDE關(guān)鍵字 該關(guān)鍵字用于定義這類符號:在目標(biāo)文件內(nèi)被引用,但沒有在任何目標(biāo)文件內(nèi)被定義的符號。 例子: SECTIONS { .text : { *(.text) _etext = .; PROVIDE(etext = .); } } 當(dāng)目標(biāo)文件內(nèi)引用了etext符號,確沒有定義它時(shí),etext符號對應(yīng)的地址被定義為.text section之后的第一個(gè)字節(jié)的地址。 7. SECTIONS命令 SECTIONS命令告訴ld如何把輸入文件的sections映射到輸出文件的各個(gè)section: 如何將輸入section合為輸出section; 如何把輸出section放入程序地址空間(VMA)和進(jìn)程地址空間(LMA).該命令格式如下: SECTIONS { SECTIONS-COMMAND SECTIONS-COMMAND ... } SECTION-COMMAND有四種: (1) ENTRY命令 (2) 符號賦值語句 (3) 一個(gè)輸出section的描述(output section description) (4) 一個(gè)section疊加描述(overlay description) 如果整個(gè)連接腳本內(nèi)沒有SECTIONS命令, 那么ld將所有同名輸入section合成為一個(gè)輸出section內(nèi), 各輸入section的順序?yàn)樗鼈儽贿B接器發(fā)現(xiàn)的順序. 如果某輸入section沒有在SECTIONS命令中提到, 那么該section將被直接拷貝成輸出section。 輸出section描述 輸出section描述具有如下格式: SECTION [ADDRESS] [(TYPE)] : [AT(LMA)] { OUTPUT-SECTION-COMMAND OUTPUT-SECTION-COMMAND ... } [>REGION] [AT>LMA_REGION] [ ![]() ![]() [ ]內(nèi)的內(nèi)容為可選選項(xiàng), 一般不需要. SECTION:section名字 SECTION左右的空白、圓括號、冒號是必須的,換行符和其他空格是可選的。 每個(gè)OUTPUT-SECTION-COMMAND為以下四種之一, 符號賦值語句 一個(gè)輸入section描述 直接包含的數(shù)據(jù)值 一個(gè)特殊的輸出section關(guān)鍵字 輸出section名字(SECTION): 輸出section名字必須符合輸出文件格式要求,比如:a.out格式的文件只允許存在.text、.data和.bss section名。而有的格式只允許存在數(shù)字名字,那么此時(shí)應(yīng)該用引號將所有名字內(nèi)的數(shù)字組合在一起;另外,還有一些格式允許任何序列的字符存在于 section名字內(nèi),此時(shí)如果名字內(nèi)包含特殊字符(比如空格、逗號等),那么需要用引號將其組合在一起。 輸出section地址(ADDRESS): ADDRESS是一個(gè)表達(dá)式,它的值用于設(shè)置VMA。如果沒有該選項(xiàng)且有REGION選項(xiàng),那么連接器將根據(jù)REGION設(shè)置VMA;如果也沒有 REGION選項(xiàng),那么連接器將根據(jù)定位符號‘.’的值設(shè)置該section的VMA,將定位符號的值調(diào)整到滿足輸出section對齊要求后的值,輸出 section的對齊要求為:該輸出section描述內(nèi)用到的所有輸入section的對齊要求中最嚴(yán)格的。 例子: .text . : { *(.text) } 和 .text : { *(.text) } 這兩個(gè)描述是截然不同的,第一個(gè)將.text section的VMA設(shè)置為定位符號的值,而第二個(gè)則是設(shè)置成定位符號的修調(diào)值,滿足對齊要求后的。 ADDRESS可以是一個(gè)任意表達(dá)式,比如ALIGN(0x10)這將把該section的VMA設(shè)置成定位符號的修調(diào)值,滿足16字節(jié)對齊后的。 注意:設(shè)置ADDRESS值,將更改定位符號的值。 輸入section描述: 最常見的輸出section描述命令是輸入section描述。 輸入section描述是最基本的連接腳本描述。 輸入section描述基礎(chǔ): 基本語法:FILENAME([EXCLUDE_FILE (FILENAME1 FILENAME2 ...) SECTION1 SECTION2 ...) FILENAME文件名,可以是一個(gè)特定的文件的名字,也可以是一個(gè)字符串模式。 SECTION名字,可以是一個(gè)特定的section名字,也可以是一個(gè)字符串模式 例子是最能說明問題的, *(.text) :表示所有輸入文件的.text section (*(EXCLUDE_FILE (*crtend.o *otherfile.o) .ctors)) :表示除crtend.o、otherfile.o文件外的所有輸入文件的.ctors section。 data.o(.data) :表示data.o文件的.data section data.o :表示data.o文件的所有section *(.text .data) :表示所有文件的.text section和.data section,順序是:第一個(gè)文件的.text section,第一個(gè)文件的.data section,第二個(gè)文件的.text section,第二個(gè)文件的.data section,... *(.text) *(.data) :表示所有文件的.text section和.data section,順序是:第一個(gè)文件的.text section,第二個(gè)文件的.text section,...,最后一個(gè)文件的.text section,第一個(gè)文件的.data section,第二個(gè)文件的.data section,...,最后一個(gè)文件的.data section 下面看連接器是如何找到對應(yīng)的文件的。 當(dāng)FILENAME是一個(gè)特定的文件名時(shí),連接器會(huì)查看它是否在連接命令行內(nèi)出現(xiàn)或在INPUT命令中出現(xiàn)。 當(dāng)FILENAME是一個(gè)字符串模式時(shí),連接器僅僅只查看它是否在連接命令行內(nèi)出現(xiàn)。 注意:如果連接器發(fā)現(xiàn)某文件在INPUT命令內(nèi)出現(xiàn),那么它會(huì)在-L指定的路徑內(nèi)搜尋該文件。 字符串模式內(nèi)可存在以下通配符: * :表示任意多個(gè)字符 ? :表示任意一個(gè)字符 [CHARS] :表示任意一個(gè)CHARS內(nèi)的字符,可用-號表示范圍,如:a-z :表示引用下一個(gè)緊跟的字符 在文件名內(nèi),通配符不匹配文件夾分隔符/,但當(dāng)字符串模式僅包含通配符*時(shí)除外。 任何一個(gè)文件的任意section只能在SECTIONS命令內(nèi)出現(xiàn)一次。看如下例子, SECTIONS { .data : { *(.data) } .data1 : { data.o(.data) } } data.o文件的.data section在第一個(gè)OUTPUT-SECTION-COMMAND命令內(nèi)被使用了,那么在第二個(gè)OUTPUT-SECTION-COMMAND命令內(nèi)將不會(huì)再被使用,也就是說即使連接器不報(bào)錯(cuò),輸出文件的.data1 section的內(nèi)容也是空的。 再次強(qiáng)調(diào):連接器依次掃描每個(gè)OUTPUT-SECTION-COMMAND命令內(nèi)的文件名,任何一個(gè)文件的任何一個(gè)section都只能使用一次。 讀者可以用-M連接命令選項(xiàng)來產(chǎn)生一個(gè)map文件,它包含了所有輸入section到輸出section的組合信息。 再看個(gè)例子, SECTIONS { .text : { *(.text) } .DATA : { [A-Z]*(.data) } .data : { *(.data) } .bss : { *(.bss) } } 這個(gè)例子中說明,所有文件的輸入.text section組成輸出.text section;所有以大寫字母開頭的文件的.data section組成輸出.DATA section,其他文件的.data section組成輸出.data section;所有文件的輸入.bss section組成輸出.bss section。 可以用SORT()關(guān)鍵字對滿足字符串模式的所有名字進(jìn)行遞增排序,如SORT(.text*)。 通用符號(common symbol)的輸入section: 在許多目標(biāo)文件格式中,通用符號并沒有占用一個(gè)section。連接器認(rèn)為:輸入文件的所有通用符號在名為COMMON的section內(nèi)。 例子, .bss { *(.bss) *(COMMON) } 這個(gè)例子中將所有輸入文件的所有通用符號放入輸出.bss section內(nèi)。可以看到COMMOM section的使用方法跟其他section的使用方法是一樣的。 有些目標(biāo)文件格式把通用符號分成幾類。例如,在MIPS elf目標(biāo)文件格式中,把通用符號分成standard common symbols(標(biāo)準(zhǔn)通用符號)和small common symbols(微通用符號,不知道這么譯對不對?),此時(shí)連接器認(rèn)為所有standard common symbols在COMMON section內(nèi),而small common symbols在.scommon section內(nèi)。 在一些以前的連接腳本內(nèi)可以看見[COMMON],相當(dāng)于*(COMMON),不建議繼續(xù)使用這種陳舊的方式。 輸入section和垃圾回收: 在連接命令行內(nèi)使用了選項(xiàng)--gc-sections后,連接器可能將某些它認(rèn)為沒用的section過濾掉,此時(shí)就有必要強(qiáng)制連接器保留一些特定的 section,可用KEEP()關(guān)鍵字達(dá)此目的。如KEEP(*(.text))或KEEP(SORT(*)(.text)) 最后看個(gè)簡單的輸入section相關(guān)例子: SECTIONS { outputa 0x10000 : { all.o foo.o (.input1) } outputb : { foo.o (.input2) foo1.o (.input1) } outputc : { *(.input1) *(.input2) } } 本例中,將all.o文件的所有section和foo.o文件的所有(一個(gè)文件內(nèi)可以有多個(gè)同名section).input1 section依次放入輸出outputa section內(nèi),該section的VMA是0x10000;將foo.o文件的所有.input2 section和foo1.o文件的所有.input1 section依次放入輸出outputb section內(nèi),該section的VMA是當(dāng)前定位器符號的修調(diào)值(對齊后);將其他文件(非all.o、foo.o、foo1.o)文件的. input1 section和.input2 section放入輸出outputc section內(nèi)。 在輸出section存放數(shù)據(jù)命令: 能夠顯示地在輸出section內(nèi)填入你想要填入的信息(這樣是不是可以自己通過連接腳本寫程序?當(dāng)然是簡單的程序)。 BYTE(EXPRESSION) 1 字節(jié) SHORT(EXPRESSION) 2 字節(jié) LOGN(EXPRESSION) 4 字節(jié) QUAD(EXPRESSION) 8 字節(jié) SQUAD(EXPRESSION) 64位處理器的代碼時(shí),8 字節(jié) 輸出文件的字節(jié)順序big endianness 或little endianness,可以由輸出目標(biāo)文件的格式?jīng)Q定;如果輸出目標(biāo)文件的格式不能決定字節(jié)順序,那么字節(jié)順序與第一個(gè)輸入文件的字節(jié)順序相同。 如:BYTE(1)、LANG(addr)。 注意,這些命令只能放在輸出section描述內(nèi),其他地方不行。 錯(cuò)誤:SECTIONS { .text : { *(.text) } LONG(1) .data : { *(.data) } } 正確:SECTIONS { .text : { *(.text) LONG(1) } .data : { *(.data) } } 在當(dāng)前輸出section內(nèi)可能存在未描述的存儲區(qū)域(比如由于對齊造成的空隙),可以用FILL(EXPRESSION)命令決定這些存儲區(qū)域的內(nèi)容, EXPRESSION的前兩字節(jié)有效,這兩字節(jié)在必要時(shí)可以重復(fù)被使用以填充這類存儲區(qū)域。如FILE(0x9090)。在輸出section描述中可以有=FILEEXP屬性,它的作用如同F(xiàn)ILE()命令,但是FILE命令只作用于該FILE指令之后的section區(qū)域,而=FILEEXP屬性作用于整個(gè)輸出section區(qū)域,且FILE命令的優(yōu)先級更高!!! 輸出section內(nèi)命令的關(guān)鍵字: CREATE_OBJECT_SYMBOLS :為每個(gè)輸入文件建立一個(gè)符號,符號名為輸入文件的名字。每個(gè)符號所在的section是出現(xiàn)該關(guān)鍵字的section。 CONSTRUCTORS :與c++內(nèi)的(全局對象的)構(gòu)造函數(shù)和(全局對像的)析構(gòu)函數(shù)相關(guān),下面將它們簡稱為全局構(gòu)造和全局析構(gòu)。 對于a.out目標(biāo)文件格式,連接器用一些不尋常的方法實(shí)現(xiàn)c++的全局構(gòu)造和全局析構(gòu)。當(dāng)連接器生成的目標(biāo)文件格式不支持任意section名字時(shí),比如說ECOFF、XCOFF格式,連接器將通過名字來識別全局構(gòu)造和全局析構(gòu),對于這些文件格式,連接器把與全局構(gòu)造和全局析構(gòu)的相關(guān)信息放入出現(xiàn) CONSTRUCTORS關(guān)鍵字的輸出section內(nèi)。 符號__CTORS_LIST__表示全局構(gòu)造信息的的開始處,__CTORS_END__表示全局構(gòu)造信息的結(jié)束處。 符號__DTORS_LIST__表示全局構(gòu)造信息的的開始處,__DTORS_END__表示全局構(gòu)造信息的結(jié)束處。 這兩塊信息的開始處是一字長的信息,表示該塊信息有多少項(xiàng)數(shù)據(jù),然后以值為零的一字長數(shù)據(jù)結(jié)束。 一般來說,GNU C++在函數(shù)__main內(nèi)安排全局構(gòu)造代碼的運(yùn)行,而__main函數(shù)被初始化代碼(在main函數(shù)調(diào)用之前執(zhí)行)調(diào)用。是不是對于某些目標(biāo)文件格式才這樣??? 對于支持任意section名的目標(biāo)文件格式,比如COFF、ELF格式,GNU C++將全局構(gòu)造和全局析構(gòu)信息分別放入.ctors section和.dtors section內(nèi),然后在連接腳本內(nèi)加入如下, __CTOR_LIST__ = .; LONG((__CTOR_END__ - __CTOR_LIST__) / 4 - 2) *(.ctors) LONG(0) __CTOR_END__ = .; __DTOR_LIST__ = .; LONG((__DTOR_END__ - __DTOR_LIST__) / 4 - 2) *(.dtors) LONG(0) __DTOR_END__ = .; 如果使用GNU C++提供的初始化優(yōu)先級支持(它能控制每個(gè)全局構(gòu)造函數(shù)調(diào)用的先后順序),那么請?jiān)谶B接腳本內(nèi)把CONSTRUCTORS替換成SORT (CONSTRUCTS),把*(.ctors)換成*(SORT(.ctors)),把*(.dtors)換成*(SORT(.dtors))。一般來說,默認(rèn)的連接腳本已作好的這些工作。 輸出section的丟棄: 例子,.foo { *(.foo) },如果沒有任何一個(gè)輸入文件包含.foo section,那么連接器將不會(huì)創(chuàng)建.foo輸出section。但是如果在這些輸出section描述內(nèi)包含了非輸入section描述命令(如符號賦值語句),那么連接器將總是創(chuàng)建該輸出section。 有一個(gè)特殊的輸出section,名為/DISCARD/,被該section引用的任何輸入section將不會(huì)出現(xiàn)在輸出文件內(nèi),這就是DISCARD的意思吧。如果/DISCARD/ section被它自己引用呢?想想看。 輸出section屬性: 終于講到這里了,呵呵。 我們再回顧以下輸出section描述的文法: SECTION [ADDRESS] [(TYPE)] : [AT(LMA)] { OUTPUT-SECTION-COMMAND OUTPUT-SECTION-COMMAND ... } [>REGION] [AT>LMA_REGION] [ ![]() ![]() 前面我們?yōu)g覽了SECTION、ADDRESS、OUTPUT-SECTION-COMMAND相關(guān)信息,下面我們將瀏覽其他屬性。 TYPE :每個(gè)輸出section都有一個(gè)類型,如果沒有指定TYPE類型,那么連接器根據(jù)輸出section引用的輸入section的類型設(shè)置該輸出section的類型。它可以為以下五種值, NOLOAD :該section在程序運(yùn)行時(shí),不被載入內(nèi)存。 DSECT,COPY,INFO,OVERLAY :這些類型很少被使用,為了向后兼容才被保留下來。這種類型的section必須被標(biāo)記為“不可加載的”,以便在程序運(yùn)行不為它們分配內(nèi)存。 輸出section的LMA :默認(rèn)情況下,LMA等于VMA,但可以通過關(guān)鍵字AT()指定LMA。 用關(guān)鍵字AT()指定,括號內(nèi)包含表達(dá)式,表達(dá)式的值用于設(shè)置LMA。如果不用AT()關(guān)鍵字,那么可用AT>LMA_REGION表達(dá)式設(shè)置指定該section加載地址的范圍。 這個(gè)屬性主要用于構(gòu)件ROM境象。 例子, SECTIONS { .text 0x1000 : { *(.text) _etext = . ; } .mdata 0x2000 : AT ( ADDR (.text) + SIZEOF (.text) ) { _data = . ; *(.data); _edata = . ; } .bss 0x3000 : { _bstart = . ; *(.bss) *(COMMON) ; _bend = . ;} } 程序如下, extern char _etext, _data, _edata, _bstart, _bend; char *src = &_etext; char *dst = &_data; /* ROM has data at end of text; copy it. */ while (dst < &_edata) { *dst++ = *src++; } /* Zero bss */ for (dst = &_bstart; dst< &_bend; dst++) *dst = 0; 此程序?qū)⑻幱赗OM內(nèi)的已初始化數(shù)據(jù)拷貝到該數(shù)據(jù)應(yīng)在的位置(VMA地址),并將為初始化數(shù)據(jù)置零。 讀者應(yīng)該認(rèn)真的自己分析以上連接腳本和程序的作用。 輸出section區(qū)域:可以將輸出section放入預(yù)先定義的內(nèi)存區(qū)域內(nèi),例子, MEMORY { rom : RIGIN = 0x1000, LENGTH = 0x1000 } SECTIONS { ROM : { *(.text) } >rom } 輸出section所在的程序段:可以將輸出section放入預(yù)先定義的程序段(program segment)內(nèi)。如果某個(gè)輸出section設(shè)置了它所在的一個(gè)或多個(gè)程序段,那么接下來定義的輸出section的默認(rèn)程序段與該輸出 section的相同。除非再次顯示地指定。例子, PHDRS { text PT_LOAD ; } SECTIONS { .text : { *(.text) } :text } 可以通過:NONE指定連接器不把該section放入任何程序段內(nèi)。詳情請查看PHDRS命令 輸出section的填充模版:這個(gè)在前面提到過,任何輸出section描述內(nèi)的未指定的內(nèi)存區(qū)域,連接器用該模版填充該區(qū)域。用法:=FILEEXP,前兩字節(jié)有效,當(dāng)區(qū)域大于兩字節(jié)時(shí),重復(fù)使用這兩字節(jié)以將其填滿。例子, SECTIONS { .text : { *(.text) } =0x9090 } 覆蓋圖(overlay)描述: 覆蓋圖描述使兩個(gè)或多個(gè)不同的section占用同一塊程序地址空間。覆蓋圖管理代碼負(fù)責(zé)將section的拷入和拷出。考慮這種情況,當(dāng)某存儲塊的訪問速度比其他存儲塊要快時(shí),那么如果將section拷到該存儲塊來執(zhí)行或訪問,那么速度將會(huì)有所提高,覆蓋圖描述就很適合這種情形。文法如下, SECTIONS { ... OVERLAY [START] : [NOCROSSREFS] [AT ( LDADDR )] { SECNAME1 { OUTPUT-SECTION-COMMAND OUTPUT-SECTION-COMMAND ... } [ ![]() SECNAME2 { OUTPUT-SECTION-COMMAND OUTPUT-SECTION-COMMAND ... } [ ![]() ... } [>REGION] [ ![]() ... } 由以上文法可以看出,同一覆蓋圖內(nèi)的section具有相同的VMA。SECNAME2的LMA為SECTNAME1的LMA加上SECNAME1的大小,同理計(jì)算SECNAME2,3,4...的LMA。SECNAME1的LMA由LDADDR決定,如果它沒有被指定,那么由START決定,如果它也沒有被指定,那么由當(dāng)前定位符號的值決定。 NOCROSSREFS關(guān)鍵字指定各section之間不能交叉引用,否則報(bào)錯(cuò)。 對于OVERLAY描述的每個(gè)section,連接器將定義兩個(gè)符號__load_start_SECNAME和__load_stop_SECNAME,這兩個(gè)符號的值分別代表SECNAME section的LMA地址的開始和結(jié)束。 連接器處理完OVERLAY描述語句后,將定位符號的值加上所有覆蓋圖內(nèi)section大小的最大值。 看個(gè)例子吧, SECTIONS{ ... OVERLAY 0x1000 : AT (0x4000) { .text0 { o1/*.o(.text) } .text1 { o2/*.o(.text) } } ... } .text0 section和.text1 section的VMA地址是0x1000,.text0 section加載于地址0x4000,.text1 section緊跟在其后。 程序代碼,拷貝.text1 section代碼, extern char __load_start_text1, __load_stop_text1; memcpy ((char *) 0x1000, &__load_start_text1, &__load_stop_text1 - &__load_start_text1); 8. 內(nèi)存區(qū)域命令 --------------- 注意:以下存儲區(qū)域指的是在程序地址空間內(nèi)的。 在默認(rèn)情形下,連接器可以為section分配任意位置的存儲區(qū)域。你也可以用MEMORY命令定義存儲區(qū)域,并通過輸出section描述的> REGION屬性顯示地將該輸出section限定于某塊存儲區(qū)域,當(dāng)存儲區(qū)域大小不能滿足要求時(shí),連接器會(huì)報(bào)告該錯(cuò)誤。 MEMORY命令的文法如下, MEMORY { NAME1 [(ATTR)] : RIGIN = ORIGIN1, LENGTH = LEN2 NAME2 [(ATTR)] : RIGIN = ORIGIN2, LENGTH = LEN2 ... } NAME :存儲區(qū)域的名字,這個(gè)名字可以與符號名、文件名、section名重復(fù),因?yàn)樗幱谝粋(gè)獨(dú)立的名字空間。 ATTR :定義該存儲區(qū)域的屬性,在講述SECTIONS命令時(shí)提到,當(dāng)某輸入section沒有在SECTIONS命令內(nèi)引用時(shí),連接器會(huì)把該輸入 section直接拷貝成輸出section,然后將該輸出section放入內(nèi)存區(qū)域內(nèi)。如果設(shè)置了內(nèi)存區(qū)域設(shè)置了ATTR屬性,那么該區(qū)域只接受滿足該屬性的section(怎么判斷該section是否滿足?輸出section描述內(nèi)好象沒有記錄該section的讀寫執(zhí)行屬性)。ATTR屬性內(nèi)可以出現(xiàn)以下7個(gè)字符, R 只讀section W 讀/寫section X 可執(zhí)行section A ‘可分配的’section I 初始化了的section L 同I ! 不滿足該字符之后的任何一個(gè)屬性的section ORIGIN :關(guān)鍵字,區(qū)域的開始地址,可簡寫成org或o LENGTH :關(guān)鍵字,區(qū)域的大小,可簡寫成len或l 例子, MEMORY { rom (rx) : RIGIN = 0, LENGTH = 256K ram (!rx) : rg = 0x40000000, l = 4M } 此例中,把在SECTIONS命令內(nèi)*未*引用的且具有讀屬性或?qū)憣傩缘妮斎雜ection放入rom區(qū)域內(nèi),把其他未引用的輸入section放入 ram。如果某輸出section要被放入某內(nèi)存區(qū)域內(nèi),而該輸出section又沒有指明ADDRESS屬性,那么連接器將該輸出section放在該區(qū)域內(nèi)下一個(gè)能使用位置。 9. PHDRS命令 ------------ 該命令僅在產(chǎn)生ELF目標(biāo)文件時(shí)有效。 ELF目標(biāo)文件格式用program headers程序頭(程序頭內(nèi)包含一個(gè)或多個(gè)segment程序段描述)來描述程序如何被載入內(nèi)存。可以用objdump -p命令查看。 當(dāng)在本地ELF系統(tǒng)運(yùn)行ELF目標(biāo)文件格式的程序時(shí),系統(tǒng)加載器通過讀取程序頭信息以知道如何將程序加載到內(nèi)存。要了解系統(tǒng)加載器如何解析程序頭,請參考ELF ABI文檔。 在連接腳本內(nèi)不指定PHDRS命令時(shí),連接器能夠很好的創(chuàng)建程序頭,但是有時(shí)需要更精確的描述程序頭,那么PAHDRS命令就派上用場了。 注意:一旦在連接腳本內(nèi)使用了PHDRS命令,那么連接器**僅會(huì)**創(chuàng)建PHDRS命令指定的信息,所以使用時(shí)須謹(jǐn)慎。 PHDRS命令文法如下, PHDRS { NAME TYPE [ FILEHDR ] [ PHDRS ] [ AT ( ADDRESS ) ] [ FLAGS ( FLAGS ) ] ; } 其中FILEHDR、PHDRS、AT、FLAGS為關(guān)鍵字。 NAME :為程序段名,此名字可以與符號名、section名、文件名重復(fù),因?yàn)樗谝粋(gè)獨(dú)立的名字空間內(nèi)。此名字只能在SECTIONS命令內(nèi)使用。 一個(gè)程序段可以由多個(gè)‘可加載’的section組成。通過輸出section描述的屬性 ![]() ![]() 如果在一個(gè)輸出section描述內(nèi)指定了 ![]() 在TYPE屬性后存在FILEHDR關(guān)鍵字,表示該段包含ELF文件頭信息;存在PHDRS關(guān)鍵字,表示該段包含ELF程序頭信息。 TYPE可以是以下八種形式, PT_NULL 0 表示未被使用的程序段 PT_LOAD 1 表示該程序段在程序運(yùn)行時(shí)應(yīng)該被加載 PT_DYNAMIC 2 表示該程序段包含動(dòng)態(tài)連接信息 PT_INTERP 3 表示該程序段內(nèi)包含程序加載器的名字,在linux下常見的程序加載器是ld-linux.so.2 PT_NOTE 4 表示該程序段內(nèi)包含程序的說明信息 PT_SHLIB 5 一個(gè)保留的程序頭類型,沒有在ELF ABI文檔內(nèi)定義 PT_PHDR 6 表示該程序段包含程序頭信息。 EXPRESSION 表達(dá)式值 以上每個(gè)類型都對應(yīng)一個(gè)數(shù)字,該表達(dá)式定義一個(gè)用戶自定的程序頭。 AT(ADDRESS)屬性定義該程序段的加載位置(LMA),該屬性將**覆蓋**該程序段內(nèi)的section的AT()屬性。 默認(rèn)情況下,連接器會(huì)根據(jù)該程序段包含的section的屬性(什么屬性?好象在輸出section描述內(nèi)沒有看到)設(shè)置FLAGS標(biāo)志,該標(biāo)志用于設(shè)置程序段描述的p_flags域。 下面看一個(gè)典型的PHDRS設(shè)置, PHDRS { headers PT_PHDR PHDRS ; interp PT_INTERP ; text PT_LOAD FILEHDR PHDRS ; data PT_LOAD ; dynamic PT_DYNAMIC ; } SECTIONS { . = SIZEOF_HEADERS; .interp : { *(.interp) } :text :interp .text : { *(.text) } :text .rodata : { *(.rodata) } /* defaults to :text */ ... . = . + 0x1000; /* move to a new page in memory */ .data : { *(.data) } :data .dynamic : { *(.dynamic) } :data :dynamic ... } 10. 版本號命令 -------------- 當(dāng)使用ELF目標(biāo)文件格式時(shí),連接器支持帶版本號的符號。 讀者可以發(fā)現(xiàn)僅僅在共享庫中,符號的版本號屬性才有意義。 動(dòng)態(tài)加載器使用符號的版本號為應(yīng)用程序選擇共享庫內(nèi)的一個(gè)函數(shù)的特定實(shí)現(xiàn)版本。 可以在連接腳本內(nèi)直接使用版本號命令,也可以將版本號命令實(shí)現(xiàn)于一個(gè)特定版本號描述文件(用連接選項(xiàng)--version-script指定該文件)。 該命令的文法如下, VERSION { version-script-commands } 以下內(nèi)容直接拷貝于以前的文檔, ===================== 開始 ================================== 內(nèi)容簡介 --------- 0 前提 1 帶版本號的符號的定義 2 連接到帶版本的符號 3 GNU擴(kuò)充 4 我的疑問 5 英文搜索關(guān)鍵字 6 我的參考 0. 前提 -- 只限于ELF文件格式 -- 以下討論用gcc 1. 帶版本號的符號的定義(共享庫內(nèi)) 文件b.c內(nèi)容如下, int old_true() { return 1; } int new_true() { return 2; } 寫連接器的版本控制腳本,本例中為b.lds,內(nèi)容如下 VER1.0{ new_true; }; VER2.0{ }; $gcc -c b.c $gcc -shared -Wl,--version-script=b.lds -o libb.so b.o 可以在{}內(nèi)填入要綁定的符號,本例中new_true符號就與VER1.0綁定了。 那么如果有一個(gè)應(yīng)用程序連接到該庫的new_true符號,那么它連接的就是VER1.0版本的new_true符號 如果把b.lds更改為, VER1.0{ }; VER2.0{ new_true; }; 然后在生成libb.so文件,在運(yùn)行那個(gè)連接到VER1.0版本的new_true符號的應(yīng)用程序,可以發(fā)現(xiàn)該應(yīng)用程序不能運(yùn)行了, 因?yàn)閹靸?nèi)沒有VER1.0版本的new_true,只有VER2.0版本的new_true。 2. 連接到帶版本的符號 寫一個(gè)簡單的應(yīng)用(名為app)連接到libb.so,應(yīng)用符號new_true 假設(shè)libb.so的版本控制文件為, VER1.0{ }; VER2.0{ new_true; }; $ nm app | grep new_true U new_true@@VER1.0 $ 用nm命令發(fā)現(xiàn)app連接到VER1.0版本的new_true 3. GNU的擴(kuò)充 它允許在程序文件內(nèi)綁定 *符號* 到 *帶版本號的別名符號* 文件b.c內(nèi)容如下, int old_true() { return 1; } int new_true() { return 2; } __asm__( ".symver old_true,true@VER1.0" ); __asm__( ".symver new_true,true@@VER2.0" ); 其中,帶版本號的別名符號是true,其默認(rèn)的版本號為VER2.0 供連接器用的版本控制腳本b.lds內(nèi)容如下, VER1.0{ }; VER2.0{ }; 版本控制文件內(nèi)必須包含版本VER1.0和版本VER2.0的定義,因?yàn)樵赽.c文件內(nèi)有對他們的引用 ****** 假定libb.so與app.c在同一目錄下 ******** 以下應(yīng)用程序app.c連接到該庫, int true(); int main() { printf( "%d ", true ); } $ gcc app.c libb.so $ LD_LIBRARY_PATH=. ./app 2 $ nm app | grep true U true@@VER2.0 $ 很明顯,程序app使用的是VER2.0版本的別名符號true,如果在b.c內(nèi)沒有指明別名符號true的默認(rèn)版本, 那么gcc app.c libb.so將出現(xiàn)連接錯(cuò)誤,提示true沒有定義。 也可以在程序內(nèi)指定特定版本的別名符號true,程序如下, __asm__( ".symver true,true@VER1.0" ); int true(); int main() { printf( "%d ", true ); } $ gcc app.c libb.so $ LD_LIBRARY_PATH=. ./app 1 $ nm app | grep true U true@VER1.0 $ 顯然,連接到了版本號為VER1.0的別名符號true。其中只有一個(gè)@表示,該版本不是默認(rèn)的版本 我的疑問: 版本控制腳本文件中,各版本號節(jié)點(diǎn)之間的依賴關(guān)系 英文搜索關(guān)鍵字: .symver versioned symbol version a shared library 參考: info ld, Scripts node ===================== 結(jié)束 ================================== 11. 表達(dá)式 ---------- 表達(dá)式的文法與C語言的表達(dá)式文法一致,表達(dá)式的值都是整型,如果ld的運(yùn)行主機(jī)和生成文件的目標(biāo)機(jī)都是32位,則表達(dá)式是32位數(shù)據(jù),否則是64位數(shù)據(jù)。 能夠在表達(dá)式內(nèi)使用符號的值,設(shè)置符號的值。 下面看六項(xiàng)表達(dá)式相關(guān)內(nèi)容, 常表達(dá)式: _fourk_1 = 4K; /* K、M單位 */ _fourk_2 = 4096; /* 整數(shù) */ _fourk_3 = 0x1000; /* 16 進(jìn)位 */ _fourk_4 = 01000; /* 8 進(jìn)位 */ 1K=1024 1M=1024*1024 符號名: 沒有被引號""包圍的符號,以字母、下劃線或'.'開頭,可包含字母、下劃線、'.'和'-'。當(dāng)符號名被引號包圍時(shí),符號名可以與關(guān)鍵字相同。如, "SECTION"=9 "with a space" = "also with a space" + 10; 定位符號'.': 只在SECTIONS命令內(nèi)有效,代表一個(gè)程序地址空間內(nèi)的地址。 注意:當(dāng)定位符用在SECTIONS命令的輸出section描述內(nèi)時(shí),它代表的是該section的當(dāng)前**偏移**,而不是程序地址空間的絕對地址。 先看個(gè)例子, SECTIONS { output : { file1(.text) . = . + 1000; file2(.text) . += 1000; file3(.text) } = 0x1234; } 其中由于對定位符的賦值而產(chǎn)生的空隙由0x1234填充。其他的內(nèi)容應(yīng)該容易理解吧。 再看個(gè)例子, SECTIONS { . = 0x100 .text: { *(.text) . = 0x200 } . = 0x500 .data: { *(.data) . += 0x600 } } .text section在程序地址空間的開始位置是0x 表達(dá)式的操作符: 與C語言一致。 優(yōu)先級 結(jié)合順序 操作符 1 left ! - ~ (1) 2 left * / % 3 left + - 4 left >> << 5 left == != > < <= >= 6 left & 7 left | 8 left && 9 left || 10 right ? : 11 right &= += -= *= /= (2) (1)表示前綴符,(2)表示賦值符。 表達(dá)式的計(jì)算: 連接器延遲計(jì)算大部分表達(dá)式的值。 但是,對待與連接過程緊密相關(guān)的表達(dá)式,連接器會(huì)立即計(jì)算表達(dá)式,如果不能計(jì)算則報(bào)錯(cuò)。比如,對于section的VMA地址、內(nèi)存區(qū)域塊的開始地址和大小,與其相關(guān)的表達(dá)式應(yīng)該立即被計(jì)算。 例子, SECTIONS { .text 9+this_isnt_constant : { *(.text) } } 這個(gè)例子中,9+this_isnt_constant表達(dá)式的值用于設(shè)置.text section的VMA地址,因此需要立即運(yùn)算,但是由于this_isnt_constant變量的值不確定,所以此時(shí)連接器無法確立表達(dá)式的值,此時(shí)連接器會(huì)報(bào)錯(cuò)。 相對值與絕對值: 在輸出section描述內(nèi)的表達(dá)式,連接器取其相對值,相對與該section的開始位置的偏移 在SECTIONS命令內(nèi)且非輸出section描述內(nèi)的表達(dá)式,連接器取其絕對值 通過ABSOLUTE關(guān)鍵字可以將相對值轉(zhuǎn)化成絕對值,即在原來值的基礎(chǔ)上加上表達(dá)式所在section的VMA值。 例子, SECTIONS { .data : { *(.data) _edata = ABSOLUTE(.); } } 該例子中,_edata符號的值是.data section的末尾位置(絕對值,在程序地址空間內(nèi))。 內(nèi)建函數(shù): ABSOLUTE(EXP) :轉(zhuǎn)換成絕對值 ADDR(SECTION) :返回某section的VMA值。 ALIGN(EXP) :返回定位符'.'的修調(diào)值,對齊后的值,(. + EXP - 1) & ~(EXP - 1) BLOCK(EXP) :如同ALIGN(EXP),為了向前兼容。 DEFINED(SYMBOL) :如果符號SYMBOL在全局符號表內(nèi),且被定義了,那么返回1,否則返回0。例子, SECTIONS { ... .text : { begin = DEFINED(begin) ? begin : . ; ... } ... } LOADADDR(SECTION) :返回三SECTION的LMA MAX(EXP1,EXP2) :返回大者 MIN(EXP1,EXP2) :返回小者 NEXT(EXP) :返回下一個(gè)能被使用的地址,該地址是EXP的倍數(shù),類似于ALIGN(EXP)。除非使用了MEMORY命令定義了一些非連續(xù)的內(nèi)存塊,否則NEXT(EXP)與ALIGH(EXP)一定相同。 SIZEOF(SECTION) :返回SECTION的大小。當(dāng)SECTION沒有被分配時(shí),即此時(shí)SECTION的大小還不能確定時(shí),連接器會(huì)報(bào)錯(cuò)。 SIZEOF_HEADERS : sizeof_headers :返回輸出文件的文件頭大小(還是程序頭大小),用以確定第一個(gè)section的開始地址(在文件內(nèi))。??? 12. 暗含的連接腳本 輸入文件可以是目標(biāo)文件,也可以是連接腳本,此時(shí)的連接腳本被稱為 暗含的連接腳本 如果連接器不認(rèn)識某個(gè)輸入文件,那么該文件被當(dāng)作連接腳本被解析。更進(jìn)一步,如果發(fā)現(xiàn)它的格式又不是連接腳本的格式,那么連接器報(bào)錯(cuò)。 一個(gè)暗含的連接腳本不會(huì)替換默認(rèn)的連接腳本,僅僅是增加新的連接而已。 一般來說,暗含的連接腳本符號分配命令,或INPUT、GROUP、VERSION命令。 在連接命令行中,每個(gè)輸入文件的順序都被固定好了,暗含的連接腳本在連接命令行內(nèi)占住一個(gè)位置,這個(gè)位置決定了由該連接腳本指定的輸入文件在連接過程中的順序。 典型的暗含的連接腳本是libc.so文件,在GNU/linux內(nèi)一般存在/usr/lib目錄下。 References 1, gnu ld在線手冊 2, 程序的鏈接和裝入及Linux下動(dòng)態(tài)鏈接的實(shí)現(xiàn) 3, UNIX/Linux平臺可執(zhí)行文件格式分析 4, John R. Levine.《Linkers & Loaders》 |