《程序員的自我修養(yǎng)》讀書筆記——靜態(tài)鏈接

上一篇介紹了目標(biāo)文件的格式,有了對(duì)結(jié)構(gòu)的認(rèn)識(shí),這篇講靜態(tài)鏈接,主要是關(guān)于目標(biāo)文件如何鏈接起來組成可執(zhí)行文件。筆記后面把ld鏈接腳本語法省略,暫時(shí)用不到這么牛逼的武器。

本文導(dǎo)圖


實(shí)驗(yàn)代碼

實(shí)驗(yàn)代碼

  • a.c
extern int shared;
int main() {
    int a = 100;
    swap(&a, &shared);
}
  • b.c
int shared = 1;
void swap(int* a, int* b) {
    *a ^= *b ^= *a ^= *b;
}

對(duì)上面內(nèi)容的解釋:

  • b中定義兩個(gè)全局符號(hào),變量shared和函數(shù)swap
  • a中定義了一個(gè)全局符號(hào)main
  • a中引用到了b中的swap和shared

空間地址分配

鏈接額過程就是將輸入的目標(biāo)文件合并為一個(gè)輸出的可執(zhí)行文件。如何將目標(biāo)文件的各個(gè)段合并到可執(zhí)行文件中,也就是空間如何分配,總體有如下兩種方式,按序疊加與相似段合并。——合并規(guī)則

按序疊加很簡單,就是按照目標(biāo)文件的順序疊加起來,可以用下圖說明:


  • 如果可執(zhí)行文件成千上百個(gè)目標(biāo)文件組成,就會(huì)出現(xiàn)很多零散的段,每個(gè)目標(biāo)文件都有三個(gè)最為核心的段,這樣就會(huì)非常浪費(fèi)空間,因?yàn)槊總€(gè)段都需要有一定的地址和空間對(duì)其要求。比如x86來說,段的裝載地址和空間對(duì)其單位是頁,也就是4096字節(jié)(一個(gè)頁大小是4096字節(jié)),會(huì)造成極大的內(nèi)存空間碎片。
  • 關(guān)于內(nèi)存地址對(duì)其可以看看內(nèi)存對(duì)齊規(guī)則之我見,簡單來講就是因?yàn)?strong>CPU是按字讀取內(nèi)存。所以內(nèi)存對(duì)齊的話,不會(huì)出現(xiàn)某個(gè)類型的數(shù)據(jù)讀一半的情況,需要再二次讀取內(nèi)存??梢蕴嵘L問效率。編譯器想通過空間換時(shí)間,通過適當(dāng)增加padding,使每個(gè)成員的訪問都在一個(gè)指令里完成,而不需要兩次訪問再拼接。

相似段合并這種方式更加實(shí)際,比如講.text段合并到可執(zhí)行文件的.text段,各個(gè)段一次合并。如下圖所示:


鏈接器為目標(biāo)文件分配地址空間有兩層含義:

  • 一個(gè)是在輸出可執(zhí)行文件中的空間
  • 另一個(gè)是裝載后的虛擬地址中的虛擬地址空間
  • 之前提到過.bbs段,在可執(zhí)行文件中并不占用文件空間,但是在占用虛擬地址空間,因?yàn)?bbs段在在文件中并沒有內(nèi)容。

目前只討論關(guān)于虛擬地址空間的分配

具體來講鏈接器空間分配策略都是用第二中,并且采用兩步鏈接。

  • 第一步:空間與地址分配——掃描所有目標(biāo)文件,得到各個(gè)段的長度,將所有目標(biāo)文件的符號(hào)表中的符號(hào)定義及引用信息統(tǒng)一放到一個(gè)全局符號(hào)表??梢愿鶕?jù)目標(biāo)文件的段長度,將他們合并,建立映射關(guān)系。
  • 第二步:符號(hào)解析與重定位——根據(jù)上面的信息,讀取文件中的段數(shù)據(jù),重定位信息,進(jìn)行符號(hào)解析與重定位,調(diào)整代碼地址。這一步才是狠心,尤其是重定位。

用ld將a.o、b.o連接起來

Linux

ld a.o b.o -e main -o ab

Mac

ld a.o b.o -e _main -o ab

ld命令的兩個(gè)參數(shù)含義是:

-o:指定輸出文件名;
-e:指定程序的入口符號(hào)。

鏈接之后各個(gè)段的屬性

Linux

Mac

$ objdump -h a.o

a.o:    file format Mach-O 64-bit x86-64

Sections:
Idx Name          Size      Address          Type
  0 __text        0000002e 0000000000000000 TEXT
  1 __compact_unwind 00000020 0000000000000030 DATA
  2 __eh_frame    00000040 0000000000000050 DATA

$ objdump -h b.o

b.o:    file format Mach-O 64-bit x86-64

Sections:
Idx Name          Size      Address          Type
  0 __text        0000002c 0000000000000000 TEXT
  1 __data        00000004 000000000000002c DATA
  2 __compact_unwind 00000020 0000000000000030 DATA
  3 __eh_frame    00000040 0000000000000050 DATA

$ objdump -h ab

ab: file format Mach-O 64-bit x86-64

Sections:
Idx Name          Size      Address          Type
  0 __text        0000005c 0000000000001f20 TEXT
  1 __eh_frame    00000080 0000000000001f80 DATA
  2 __data        00000004 0000000000002000 DATA

在Linux中VMA表示的是虛擬地址,LMA表示的是加載地址,一般這兩個(gè)值一樣,但是有些嵌入式系統(tǒng)中會(huì)不一樣。Mac中只有一個(gè)地址也就是虛擬地址。

現(xiàn)在直接看VMA和SIZE,暫時(shí)忽略文件偏移。在鏈接之前虛擬地址都是零(MAC上起始的.text段為0),因?yàn)樘摂M地址空間還沒有分配,所以默認(rèn)都是0,但是鏈接之后,可執(zhí)行文件ab各個(gè)段都分配了相應(yīng)的虛擬地址,所以可以看到text已經(jīng)分配到地址。

對(duì)應(yīng)到Linux中ELF文件,.text段分配到了0x08048094,大小是0x72字節(jié),.data段從地址0x08049108開始,大小為四字節(jié)。總體來說如下圖:

為什么不從虛擬地址的0地址開始分配呢。涉及到操作系統(tǒng)進(jìn)程虛擬地址的分配規(guī)則。Linux下ELF文件默認(rèn)從0x08048000開始分配的。

符號(hào)地址的確定

第一步過程中確定了在可執(zhí)行文件中的空間分布。比如.text其實(shí)段0x08040894,.data段其實(shí)地址0x08049108.

第一步完成之后,鏈接器就開始計(jì)算各個(gè)符號(hào)的虛擬地址。符號(hào)在段內(nèi)的位置是固定的,比如main、shared、wap地址已經(jīng)是確定的了,只不過需要鏈接器給每個(gè)符號(hào)添加一個(gè)偏移量。

比如a.o中的main函數(shù)相對(duì)于a.o的text偏移量是X,經(jīng)過鏈接之后a.o的text段位于虛擬地址0x08048094,那么main的地址就是0x08048094+X。從前面的objdump可以看到main位于a.o的text段偏移是0。所以main這個(gè)符號(hào)最終在可執(zhí)行文件中的地址是0x08048094+0。

符號(hào)解析、重定位

完成了空間和地址分配,鏈接器開始進(jìn)行符號(hào)解析及重定位。

使用objdump的參數(shù)d查看反匯編結(jié)果

未鏈接a.o反匯編結(jié)果

Mac 下

$ objdump -d a.o

a.o:    file format Mach-O 64-bit x86-64

Disassembly of section __TEXT,__text:
_main:
       0:   55  pushq   %rbp
       1:   48 89 e5    movq    %rsp, %rbp
       4:   48 83 ec 10     subq    $16, %rsp
       8:   48 8d 7d fc     leaq    -4(%rbp), %rdi
       c:   48 8b 35 00 00 00 00    movq    (%rip), %rsi
      13:   c7 45 fc 64 00 00 00    movl    $100, -4(%rbp)
      1a:   b0 00   movb    $0, %al
      1c:   e8 00 00 00 00  callq   0 <_main+0x21>
      21:   31 c9   xorl    %ecx, %ecx
      23:   89 45 f8    movl    %eax, -8(%rbp)
      26:   89 c8   movl    %ecx, %eax
      28:   48 83 c4 10     addq    $16, %rsp
      2c:   5d  popq    %rbp
      2d:   c3  retq

Linux下:


可執(zhí)行文件ab反匯編結(jié)果:
Mac 下:

objdump -d ab

ab: file format Mach-O 64-bit x86-64

Disassembly of section __TEXT,__text:
......

_main:
    1f20:   55  pushq   %rbp
    1f21:   48 89 e5    movq    %rsp, %rbp
    1f24:   48 83 ec 10     subq    $16, %rsp
    1f28:   48 8d 7d fc     leaq    -4(%rbp), %rdi
    1f2c:   48 8d 35 cd 00 00 00    leaq    205(%rip), %rsi
    1f33:   c7 45 fc 64 00 00 00    movl    $100, -4(%rbp)
    1f3a:   b0 00   movb    $0, %al
    1f3c:   e8 0f 00 00 00  callq   15 <_swap>
    1f41:   31 c9   xorl    %ecx, %ecx
    1f43:   89 45 f8    movl    %eax, -8(%rbp)
    1f46:   89 c8   movl    %ecx, %eax
    1f48:   48 83 c4 10     addq    $16, %rsp
    1f4c:   5d  popq    %rbp
    1f4d:   c3  retq
    1f4e:   90  nop
    1f4f:   90  nop

......

Linux下


需要懂點(diǎn)匯編才能理解上面的不同。主要是想說明,被引用的函數(shù)或者變量的地址,在鏈接之后被重新定位了。如上面的swap函數(shù)、shared變量。

關(guān)于匯編的學(xué)習(xí)后面會(huì)專門寫一篇!

重定位表

鏈接器通過重定位表才能知道哪些指令需要被調(diào)整,重定位表往往是一個(gè)或多個(gè)段。ELF必須包含重定位表來重新定位符號(hào)。

比如代碼段.text有符號(hào)需要重定位,則就會(huì)有一個(gè).rel.text的段保存了代碼段重定位的信息,如果.data段中有重定位的地方,就會(huì)有一個(gè)對(duì)應(yīng)的.rel.data段保存了數(shù)據(jù)端的重定位表??梢允褂胦bjdump的r參數(shù)查看。

Mac下:

objdump -r a.o

a.o:    file format Mach-O 64-bit x86-64

RELOCATION RECORDS FOR [__text]:
000000000000001d X86_64_RELOC_BRANCH _swap
000000000000000f X86_64_RELOC_GOT_LOAD _shared@GOTPCREL

RELOCATION RECORDS FOR [__compact_unwind]:
0000000000000000 X86_64_RELOC_UNSIGNED __text

Linux下:

可以看到a.o有兩個(gè)重定位入口,重定位入口的偏移表示該符號(hào)入口在重定位段中的位置。

對(duì)上面代碼的解析:因?yàn)檫€未進(jìn)行鏈接,首先main函數(shù)其實(shí)地址為0x00000000。這個(gè)main函數(shù)占用了0x33個(gè)字節(jié),17條指令。shared的引用是一條mov指令,一共8個(gè)字節(jié),將shared的地址復(fù)制到ESP寄存器+4的偏移地址中,前四個(gè)是指令碼,后面是shared的地址。暫時(shí)認(rèn)為shared的地址是0x00000000,并且是絕對(duì)地址指令。

其次對(duì)swap的調(diào)用的指令一個(gè)共5個(gè)字節(jié),0xE8是操作碼,這是個(gè)相對(duì)位移調(diào)用指令,后面四個(gè)字節(jié)就是函數(shù) 相對(duì)于調(diào)用指令的下一條指令的偏移量。沒重定位之前,相對(duì)偏移量為0xFFFFFFFC(小端),常量-4的補(bǔ)碼形式。

  • 第一行表示這個(gè)重定位表是對(duì)代碼段的重定位,所以偏移表示代碼段中需要被調(diào)整的位置。對(duì)照前面的反匯編結(jié)構(gòu)。這里的0x1c和0x27分別對(duì)應(yīng)了代碼段中的mov和call指令部分。

重定位表中保存的是一個(gè)ELF32_Rel(intelx86)結(jié)構(gòu)數(shù)組,表中每一個(gè)元素都對(duì)應(yīng)一個(gè)重定位入口

typedef struct {
    ELF32_Addr r_offset;
    ELF32_Word r_info;
}

具體含義如下:


重定位表項(xiàng)里面包含了如何定位該符號(hào)的所有信息,偏移位置,類型,符號(hào)等。

符號(hào)解析

符號(hào)未定義錯(cuò)誤在鏈接的時(shí)候經(jīng)常出現(xiàn)。對(duì)應(yīng)上面的例子,

ld a.o
ld: warning: -macosx_version_min not specified, assuming 10.11
ld: warning: object file (a.o) was built for newer OSX version (10.12) than being linked (10.11)
Undefined symbols for architecture x86_64:
  "_shared", referenced from:
      _main in a.o
  "_swap", referenced from:
      _main in a.o
  "start", referenced from:
     implicit entry/start for main executable
ld: symbol(s) not found for inferred architecture x86_64

導(dǎo)致符號(hào)未定義的原因很多(根本原因找不到符號(hào)),比如:

  • 鏈接時(shí)少了某個(gè)庫
  • 符號(hào)目標(biāo)文本路徑不正確

為什么缺少符號(hào)定義會(huì)導(dǎo)致鏈接錯(cuò)誤?其實(shí)重定位過程伴隨著符號(hào)解析的過程,每個(gè)目標(biāo)文件可能定義一些符號(hào),也可能引用的到其他文件中定義的符號(hào),每個(gè)重定位入口都是對(duì)一個(gè)符號(hào)的引用。當(dāng)鏈接器對(duì)某個(gè)符號(hào)進(jìn)行重定位的時(shí)候,就需要確定這個(gè)符號(hào)的目標(biāo)地址,這個(gè)時(shí)候就會(huì)去查找輸入目標(biāo)文件的符號(hào)表組成的全局符號(hào)表,找到這個(gè)符號(hào)然后重定位。

使用readelf -s查看符號(hào)表。

a.o中的符號(hào)表



注意上面的main、shared、swap都是GLOBAL。main函數(shù)定義在代碼段之外,shared和swap是UND為定義。因?yàn)樗麄兪嵌x哎其他目標(biāo)文件中的。穩(wěn)定一的都是需要在全局符號(hào)表中找到。

指令修正

這部分需要結(jié)合在重定位反匯編用到的偏移量。

不同處理器對(duì)于地址的格式和方式都不一樣。常見的有跳轉(zhuǎn)指令(jump 11種)、子程序調(diào)用指令(call 10種)、數(shù)據(jù)傳送指令(mov 34中)尋址千差萬別。差別如下:

  • 近址或遠(yuǎn)址尋址
  • 絕對(duì)與相對(duì)尋址
  • 尋址長度8、16、32、64位
  • 相對(duì)近址32位尋址
  • 絕對(duì)近址32位尋址

每個(gè)被修正的長度為32位,4字節(jié),都是近址尋址。區(qū)別就是相對(duì)或者絕對(duì)。之前說過,重定位入口r_info成員低八位表示重定位入口類型

對(duì)應(yīng)到上面的內(nèi)容。swap符號(hào)的引用類型為R_386_PC32,代表是相對(duì)位移調(diào)用指令,而shared是R_386_32類型,他修正的是一條傳輸指令的原,shared的絕對(duì)地址。

絕對(duì)尋址修正和相對(duì)尋址修正的區(qū)別就是前者修正后的地址就是該符號(hào)的實(shí)際地址,而后者修正后的地址為符號(hào)距離被修正位置的地址差。

現(xiàn)在來計(jì)算一下,假設(shè)鏈接之后main函數(shù)的虛擬地址為0x1000,swap函數(shù)的地址為0x2000,shared變量的虛擬地址為0x3000。現(xiàn)在開始修正這兩個(gè)重定位符號(hào)的地址。

shared

根據(jù)上面的分析,首先shared是一個(gè)絕對(duì)尋址修正,結(jié)果應(yīng)該是S(實(shí)際虛擬地址——假設(shè)的)+ A(修正位置的值——從符號(hào)解析中得到的value)

那么修正之后的地址就是:0x3000 + 0x0000000 = 0x3000。那么就應(yīng)該是:


swap

swap需要相對(duì)修正,器修正地址就是S + A - P(被修正的位置,當(dāng)鏈接的時(shí)候這個(gè)值就是被修正位置的虛擬地址 就為0x1000(main函數(shù)地址)+ 0x27)

對(duì)應(yīng)下來:0x2000 + (-4) - (0x1000 + 0x27) = 0xFD5:

那么這條相對(duì)唯一調(diào)用指令地址是改指令下一條指令其實(shí)地址加上偏移量。那就是0x1026 + 0xfd5 = 0x2000。這就是swap函數(shù)的地址。

COMMON塊

編譯器將未初始化的全局變量定義作為弱符號(hào),如上一篇的例子中g(shù)lobal_uninit_var。使用readelf -s查看


類型是一個(gè)SH_COMMON類型。

當(dāng)不同目標(biāo)文件需要的COMMON塊空間大小不一致的時(shí)候,以最大的那塊為準(zhǔn)。

需要使用COMMON機(jī)制的原因是編譯器和鏈接器允許不同類型的弱符號(hào)存在,但是最本質(zhì)的還是鏈接器不支持符號(hào)類型,也就是鏈接器無法判斷各個(gè)符號(hào)類型是否一致。

小結(jié):如果編譯單元包含了弱符號(hào)(比如未初始化的全局變量就是典型的),那么弱符號(hào)最終占有多大空間不知道,所以編譯器無法為改符號(hào)在BSS段分配空間。但是在鏈接過程中,弱符號(hào)大小可以確定了,所以最終在輸出可執(zhí)行文件的BBS段為弱符號(hào)分配空間。最終未初始化的全局變量還是放在BBS段

C++ 相關(guān)問題

C++語言特性太復(fù)雜,必須有編譯器和鏈接器共同支持才能完成工作。關(guān)鍵在于:

  • 重復(fù)代碼消除
  • 全局構(gòu)造與析構(gòu)
  • 特性:虛函數(shù)、函數(shù)重載、繼承、異常等。這些數(shù)據(jù)結(jié)構(gòu)復(fù)雜,往往在不同編譯器和鏈接器之間不能通用,二進(jìn)制兼容性很麻煩。

重復(fù)代碼消除

C++中的模板本質(zhì)來講很像宏,當(dāng)被實(shí)例化的時(shí)候,并不知道自己是否在別的編譯單元被實(shí)例化,所以必定出現(xiàn)重復(fù)代碼。如果不管這些重復(fù)代碼的話,主要會(huì)有如下問題:

  • 空間浪費(fèi)
  • 地址教易出錯(cuò):可能有兩個(gè)指向同一個(gè)函數(shù)的指針不相等。
  • 指令運(yùn)行效率低:因?yàn)楝F(xiàn)代CPU都會(huì)對(duì)指令和數(shù)據(jù)進(jìn)行緩存,同一份指令有多份副本,那么Cache的命中率就會(huì)很低。

比較現(xiàn)實(shí)的做法:每個(gè)目標(biāo)的實(shí)例代碼都單獨(dú)存放到一個(gè)段(段命名如.gnu.linkonce.name,name就是該函數(shù)模板修飾后的名稱)里,每個(gè)段只包含一個(gè)模板實(shí)例。當(dāng)別的編譯單元也有同樣模板實(shí)例的時(shí)候,就會(huì)生成相同的名字,最終鏈接器將他們合并到最后的代碼段。

函數(shù)級(jí)別鏈接

現(xiàn)在的程序和庫都非常龐大,一個(gè)目標(biāo)文件可能包含成千上百個(gè)函數(shù)或者變量,當(dāng)我們需要用到某個(gè)目標(biāo)文件的一個(gè)函數(shù)或變量的時(shí)候,需要把珍格格文件鏈接進(jìn)來,這樣導(dǎo)致輸出的文件也很多。

在C++編譯器中,有個(gè)編譯選項(xiàng)叫做函數(shù)幾倍鏈接,這個(gè)的作用就是讓所有的函數(shù)像前面的模板一樣,單獨(dú)保存到一個(gè)段里面。當(dāng)鏈接器需要用到這個(gè)函數(shù)的時(shí)候,就會(huì)將它合并到輸出文件,沒有用到的函數(shù)就會(huì)被拋棄。這樣的方式同樣有問題,雖然減少了輸出文件的長度,但是會(huì)減慢編譯和鏈接的過程,并且所有函數(shù)保存到獨(dú)立的短中,目標(biāo)函數(shù)的短數(shù)量增加,重定位會(huì)因?yàn)槎痰臄?shù)目增加而變得復(fù)雜,目標(biāo)文件也會(huì)變得相對(duì)較大。

全局構(gòu)造、析構(gòu)

C/C++程序都是從main函數(shù)開始執(zhí)行的,隨著main函數(shù)結(jié)束而結(jié)束。在main函數(shù)之前為了程序能夠順利執(zhí)行。需要初始化進(jìn)程執(zhí)行環(huán)境、如堆分配初始化、線程子系統(tǒng)等。C++的全局對(duì)象構(gòu)造函數(shù)在這一時(shí)期被執(zhí)行

Linux下一班程序的入口是_start,這個(gè)函數(shù)時(shí)Linux系統(tǒng)庫(Glibc)的一部分。當(dāng)程序與Glibc鏈接在一起形成可執(zhí)行文件之后,_start就是程序初始化入口。程序初始化之后,會(huì)調(diào)用main函數(shù)來執(zhí)行程序,main函數(shù)執(zhí)行之后就會(huì)進(jìn)行一些清理工作,然后結(jié)束進(jìn)程。

在ELF定義了兩個(gè)特殊的段,

  • .init段在main函數(shù)之前的可執(zhí)行指令。構(gòu)成進(jìn)程的初始化代碼。所以在main函數(shù)調(diào)用之前,Glibc初始化部分會(huì)執(zhí)行這個(gè)段的代碼。
  • .fini段保存著進(jìn)程的終止代碼指令。所以當(dāng)main函數(shù)正常退出時(shí),Glib會(huì)安排執(zhí)行這個(gè)段中的代碼。

C++、ABI

編譯器有很多種,那么不同編譯器產(chǎn)生的目標(biāo)文件可不可以進(jìn)鏈接呢?

如果兩個(gè)不同的編譯器想要產(chǎn)生的目標(biāo)文件能夠正確的鏈接起來,那么這兩個(gè)目標(biāo)文件必須滿足:

  • 采用相同的目標(biāo)文件格式
  • 擁有相同的符號(hào)修飾標(biāo)準(zhǔn)
  • 變量的內(nèi)存分配方式相同
  • 函數(shù)調(diào)用方式相同

把符號(hào)修飾標(biāo)準(zhǔn)、變量內(nèi)存布局、函數(shù)調(diào)用方式等這些跟可執(zhí)行的二進(jìn)制文件兼容性相關(guān)的內(nèi)容統(tǒng)稱為ABI(應(yīng)用程序二進(jìn)制接口)。如果想弄清API和ABI的區(qū)別,可以看最后的擴(kuò)展閱讀內(nèi)容呢。

  • 簡單來講就是API往往指源代碼級(jí)別的接口,如POSIX是一個(gè)API標(biāo)準(zhǔn),而ABI是二進(jìn)制層面的級(jí)別。比如C++對(duì)象內(nèi)存布局是C++ABI的一部分。
  • 就拿POSIX規(guī)定printf()函數(shù)的原型為例,POSIX保證這個(gè)函數(shù)所有遵循POSIX標(biāo)準(zhǔn)的系統(tǒng)之間都一樣,但是不保證printf在每個(gè)系統(tǒng)執(zhí)行時(shí),是否按照從右到左的參數(shù)壓入堆棧。參數(shù)如何在堆棧中分配等這些實(shí)際運(yùn)行時(shí)的二進(jìn)制級(jí)別問題。
  • 由于各大硬件拼圖,編程語言,編譯,鏈接器和操作系統(tǒng)之間的ABI互相不兼容,所以哥哥目標(biāo)文件之間無法互相鏈接。

實(shí)際例子

對(duì)于C語言的目標(biāo)來講,一下幾個(gè)方面會(huì)決定二進(jìn)制是否兼容:

API相同ABI不一定相同。

鏈接過程控制

一般情況下用鏈接器默認(rèn)的鏈接規(guī)則就可以了,但是在一些特殊的情況下就需要自定義一些參數(shù)了,比如引導(dǎo)程序、內(nèi)核驅(qū)動(dòng)程序就需要特殊的鏈接過程。

鏈接過程需要確定的內(nèi)容:

  • 使用哪些目標(biāo)文件
  • 使用安歇庫文件
  • 是否在最終的可執(zhí)行文件保留調(diào)試信息
  • 輸出文件格式(可執(zhí)行文件、動(dòng)態(tài)庫、靜態(tài)庫)
  • ....

鏈接控制腳本

鏈接控制腳本就是用來控制鏈接行為的

一般鏈接器有如下幾種方式控制鏈接行為:

  • 使用命令行:給鏈接器指定參數(shù),比如之前用的ld的-o、-e參數(shù)就屬于這類。
  • 鏈接指令存放到目標(biāo)文件里面:編譯器經(jīng)常會(huì)通過這種方式給鏈接器傳遞指令。具體來講比如在PE目標(biāo)文件的.drectve段用來鏈接傳遞參數(shù) 。
  • 鏈接控制腳本:最為靈活也是最為強(qiáng)大的控制方式

之前我們?cè)谑褂胠d命令鏈接的時(shí)候,沒有指定鏈接腳本,其實(shí)ld如果沒有指定鏈接腳本,則會(huì)使用默認(rèn)的鏈接腳本。在Linux上使用ld -verbose查看默認(rèn)鏈接腳本。為了更加精確的控制鏈接過程,可以自己寫一個(gè)鏈接腳本,然后指定該腳本控制腳本,比如ld -T link.script

一個(gè)例子

在這里例子中,作者沒有使用main函數(shù)和c中的printf函數(shù)來打印helloword。而是使用了自定義的一套方式,使用了GCC內(nèi)嵌匯編(不是很懂,沒弄過),并且自定義了將所有段合并到一個(gè)叫做tinytext段中。

TinyHelloWord.c源碼


看到后面懵逼了,暫時(shí)停下來去了解下匯編、復(fù)習(xí)下終端等知識(shí)。

  • 句柄與普通指針的區(qū)別:指針包含的是引用對(duì)象的內(nèi)存地址,而句柄則是由系統(tǒng)所管理的引用標(biāo)識(shí),該標(biāo)識(shí)可以被系統(tǒng)重新定位到一個(gè)內(nèi)存地址上。這種間接訪問對(duì)象的模式增強(qiáng)了系統(tǒng)對(duì)引用對(duì)象的控制。

使用ld鏈接腳本

無奈是輸出文件還是輸入文件,主要的數(shù)據(jù)就是文件中的各個(gè)段。它們中的段我們稱作輸入段、輸出段??刂奇溄庸褪前芽刂戚斎攵稳绾巫兂奢敵龆?,比如

  • 哪些輸入段要合并為一個(gè)輸出段
  • 哪些輸入段要丟棄
  • 指定輸出段的名字、裝載地址、屬性

比如上面TinyHelloword的鏈接腳本TinyHelloWorld.lds


  • 第一行是ENTRY(nomain)指定了程序入口為nomain()函數(shù)

  • SECTIONS是鏈接腳本的主體,指定了各個(gè)輸入段到輸出段的交換。里面的大括號(hào)包含的是SECTIONS的變化規(guī)則。

    • 第一條是賦值語句. = 0x08048000 + SIZEOF_HEADERS表示把當(dāng)前虛擬地址設(shè)置成為0x08048000 + SIZEOF_HEADERS
    • tinytext:{*(.text)*(.data)*(.rodata)}第二條是個(gè)段轉(zhuǎn)換規(guī)則,也就是后面的三個(gè)段合并輸出到文件tinytext中
    • /DISCARD/:{*{comment}}第三個(gè)意思就是丟棄所有輸入文件中的名字為.comment內(nèi)容,不保存到輸出文件中。

    默認(rèn)情況下,.shstrtab、.symtab、.strtab代表段名字符串表,符號(hào)表和字符串表,這三種表,鏈接器在產(chǎn)生可執(zhí)行文件的時(shí)候會(huì)自動(dòng)生成。——可執(zhí)行文件中,符號(hào)表和字符串是可選的,段名字符串表保存段名,必不可少。

ld鏈接腳本語法

這一小節(jié)直接看資料,ld鏈接腳本文件語法解析。平時(shí)很難有機(jī)會(huì)用到這塊知識(shí)。

擴(kuò)展閱讀

ABI-Application binary interface
GCC內(nèi)嵌匯編
Linux 內(nèi)核中斷內(nèi)幕
句柄是什么?
Purpose of memory alignment
內(nèi)存地址對(duì)齊提升程序性能

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容