使用 LDD、Readelf 和 Objdump 的 GCC 鏈接過程
鏈接是 gcc 編譯過程的最后階段。

在鏈接過程中,目標文件被鏈接在一起,所有對外部符號的引用都被解析,最終地址被分配給函數調用等。
在本文中,我們將主要關注 gcc 鏈接過程的以下幾個方面:
目標文件以及它們如何鏈接在一起
代碼重定位
在閱讀本文之前,請確保您了解 C 程序在成為可執行文件之前必須經過的所有 4 個階段(預處理、編譯、匯編和鏈接)。
鏈接對象文件
讓我們通過一個例子來理解這第一步。首先創建以下 main.c 程序。
$ vi main.c #include
接下來創建以下 func.c 程序。在 main.c 文件中,我們通過關鍵字“extern”聲明了一個函數 func(),并在一個單獨的文件 func.c 中定義了這個函數
$ vi func.c void func(void) { printf("\n inside func()\n"); }
為 func.c 創建目標文件,如下所示。這將在當前目錄中創建文件 func.o。
$ gcc -c func.c
同樣為 main.c 創建目標文件,如下所示。這將在當前目錄中創建文件 main.o。
$ gcc -c main.c
現在執行以下命令來鏈接這兩個目標文件以生成最終的可執行文件。這將在當前目錄中創建文件“main”。
$ gcc func.o main.o -o main
當您執行此“主”程序時,您將看到以下輸出。
$ ./main Inside main() Inside func()
從上面的輸出中可以清楚地看出,我們能夠成功地將兩個目標文件鏈接到最終的可執行文件中。
當我們將函數 func() 從 main.c 中分離出來并寫在 func.c 中時,我們得到了什么?
答案是如果我們也將函數 func() 寫在同一個文件中,這可能無關緊要,但想想我們可能有數千行代碼的非常大的程序。對一行代碼的更改可能會導致重新編譯整個源代碼,這在大多數情況下是不可接受的。因此,非常大的程序有時會分成小塊,這些小塊最終鏈接在一起以生成可執行文件。
在大多數情況下,適用于 makefile的make 實用程序都會發揮作用,因為該實用程序知道哪些源文件已被更改以及哪些目標文件需要重新編譯。相應源文件未被更改的目標文件按原樣鏈接。這使得編譯過程非常容易和易于管理。
所以,現在我們明白了,當我們鏈接兩個目標文件 func.o 和 main.o 時,gcc 鏈接器能夠解析對 func() 的函數調用,并且當最終的可執行文件 main 執行時,我們會看到 printf()在正在執行的函數 func() 內部。
鏈接器在哪里找到函數 printf() 的定義?由于鏈接器沒有給出任何錯誤,這肯定意味著鏈接器找到了 printf() 的定義。printf() 是在 stdio.h 中聲明并定義為標準“C”共享庫 (libc.so) 的一部分的函數
我們沒有將此共享對象文件鏈接到我們的程序。那么,這是如何工作的呢?使用ldd工具一探究竟,它會打印出每個程序所需的共享庫或命令行指定的共享庫。
在“main”可執行文件上執行 ldd,它將顯示以下輸出。
$ ldd main linux-vdso.so.1 => (0x00007fff1c1ff000) libc.so.6 => /lib/libc.so.6 (0x00007f32fa6ad000) /lib64/ld-linux-x86-64.so.2 (0x00007f32faa4f000)
上面的輸出表明主可執行文件依賴于三個庫。上述輸出中的第二行是“libc.so.6”(標準“C”庫)。這就是 gcc 鏈接器能夠解析對 printf() 的函數調用的方式。
第一個庫是進行系統調用所必需的,而第三個共享庫是加載可執行文件所需的所有其他共享庫的庫。該庫將出現在每個依賴于任何其他共享庫執行的可執行文件中。
在鏈接過程中,gcc 內部使用的命令很長,但對于用戶而言,我們只需要編寫即可。
$ gcc
代碼重定位
重定位是二進制文件中的條目,它們在鏈接時或運行時被填充。一個典型的重定位條目說:找到'z'的值并將該值放入偏移量'x'的最終可執行文件中
為此示例創建以下 reloc.c。
$ vi reloc.c extern void func(void); void func1(void) { func(); }
在上面的 reloc.c 中,我們聲明了一個尚未提供定義的函數 func(),但我們在 func1() 中調用了該函數。
從 reloc.c 創建一個目標文件 reloc.o,如下所示。
$ gcc -c reloc.c -o reloc.o
使用 readelf 實用程序查看此目標文件中的重定位,如下所示。
$ readelf --relocs reloc.o Relocation section '.rela.text' at offset 0x510 contains 1 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000000005 000900000002 R_X86_64_PC32 0000000000000000 func - 4 ...
在我們制作 reloc.o 時,func() 的地址是未知的,因此編譯器會留下 R_X86_64_PC32 類型的重定位。這種重定位間接表示“在最終可執行文件中填充函數 func() 的地址,偏移量為 000000000005”。
上述重定位對應于目標文件 reloc.o 中的 .text 部分(再次需要了解 ELF 文件的結構才能理解各個部分),因此讓我們使用 objdump 實用程序反匯編 .text 部分:
$ objdump --disassemble reloc.o reloc.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000
在上面的輸出中,偏移量“5”(相對于起始地址 0000000000000000 的值為“4”的條目)有 4 個字節等待寫入函數 func() 的地址。
因此,函數 func() 有一個未決的重定位,當我們將 reloc.o 與包含函數 func() 定義的目標文件或庫鏈接時,它將得到解決。
讓我們試試看這個重定位是否得到解決。這是另一個提供 func() 定義的 main.c 文件:
$ vi main.c #include
從 main.c 創建 main.o 目標文件,如下所示。
$ gcc -c main.c -o main.o
將 reloc.o 與 main.o 鏈接并嘗試生成如下所示的可執行文件。
$ gcc reloc.o main.o -o reloc
再次執行objdump,看看重定位是否已經解決:
$ objdump --disassemble reloc > output.txt
我們重定向了輸出,因為可執行文件包含大量信息,我們不想迷失在標準輸出上。
查看 output.txt 文件的內容。
$ vi output.txt ... 0000000000400524
在第 4 行中,我們可以清楚地看到我們之前看到的空地址字節現在填充了函數 func() 的地址。
總而言之,gcc 編譯器鏈接是一片廣闊的海洋,無法在一篇文章中涵蓋。盡管如此,本文還是嘗試剝離鏈接過程的第一層,讓您了解在承諾鏈接不同目標文件以生成可執行文件的 gcc 命令下會發生什么。
gcc
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。