(上接“GCC編譯的背后(第一部分:預(yù)處理和編譯)”)
3、匯編
開篇:這里實際上還是翻譯過程,只不過把作為中間結(jié)果的匯編代碼翻譯成了機器代碼,即目標(biāo)代碼,不過它還不可以運行。如果要產(chǎn)生這一中間結(jié)果,可用gcc的-c選項,當(dāng)然,也可通過as命令_匯編_匯編語言源文件來產(chǎn)生。
匯編是把匯編語言翻譯成目標(biāo)代碼的過程,在學(xué)習(xí)匯編語言開發(fā)時,大家應(yīng)該比較熟悉nasm匯編工具(支持Intel格式的匯編語言)了,不過這里主要用
as匯編工具來匯編AT&T格式的匯編語言,因為gcc產(chǎn)生的中間代碼就是AT&T格式的。下面來演示分別通過gcc的-c選項和as來
產(chǎn)生
目標(biāo)代碼。
Quote: |
$ file hello.s hello.s: ASCII assembler program text $ gcc -c hello.s #用gcc把匯編語言編譯成目標(biāo)代碼 $
file hello.o
#file命令可以用來查看文件的類型,這個目標(biāo)代碼是可重定位的(relocatable),需
#要通過ld進行進一步的鏈接成可執(zhí)行程序(executable)和共享庫(shared) hello.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped $ as -o hello.o hello.s #用as把匯編語言編譯成目標(biāo)代碼 $ file hello.o hello.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
|
gcc和as默認產(chǎn)生的目標(biāo)代碼都是ELF格式[6]的,因此這里主要討論ELF格式的目標(biāo)代碼(如果
有時間再回顧一下a.out和coff格式,當(dāng)然你也可以參考資料[15],自己先了解一下,并結(jié)合objcopy來轉(zhuǎn)換它們,比較異同)。
目標(biāo)代碼不再是普通的文本格式,無法直接通過文本編輯器瀏覽,需要一些專門的工具。如果想了解更多目標(biāo)代碼的細節(jié),區(qū)分relocatable(可重定
位)、executable(可執(zhí)行)、shared
libarary(共享庫)的不同,我們得設(shè)法了解目標(biāo)代碼的組織方式和相關(guān)的閱讀和分析工具。下面我們主要介紹這部分內(nèi)容。
"BFD is a package which allows applications to use the same routines to
operate on object files whatever the object file format. A new object
file format can be supported simply by creating a new BFD back end and
adding it to the library."[24][25]。
binutils(GNU Binary
Utilities)的很多工具都采用這個庫來操作目標(biāo)文件,這類工具有objdump,objcopy,nm,strip等(當(dāng)然,你也可以利用它。如
果你深入了解ELF格式,那么通過它來分析和編寫Virus程序?qū)臃奖?,不過另外一款非常優(yōu)秀的分析工具readelf并不是
基于這個庫,所以你也應(yīng)該可以直接用elf.h頭文件中定義的相關(guān)結(jié)構(gòu)來操作ELF文件。
下面將通過這些輔助工具(主要是readelf和objdump,可參考本節(jié)最后列出的資料[4]),結(jié)合ELF手冊[6](建議看第三篇中文版)來分析它們。
下面大概介紹ELF文件的結(jié)構(gòu)和三種不同類型ELF文件的區(qū)別。
ELF文件的結(jié)構(gòu):
ELF Header(ELF文件頭)
Porgram Headers Table(程序頭表,實際上叫段表好一些,用于描述可執(zhí)行文件和可共享庫)
Section 1
Section 2
Section 3
...
Section Headers Table(節(jié)區(qū)頭部表,用于鏈接可重定位文件成可執(zhí)行文件或共享庫)
對于可重定位文件,程序頭是可選的,而對于可執(zhí)行文件和共享庫文件(動態(tài)連接庫),節(jié)區(qū)表則是可選的。這里的可選是指沒有也可以。可以分別通過
readelf文件的-h,-l和-S參數(shù)查看ELF文件頭(ELF Header)、程序頭部表(Program Headers
Table,段表)和節(jié)區(qū)表(Section Headers Table)。
文件頭說明了文件的類型,大小,運行平臺,節(jié)區(qū)數(shù)目等。先來通過文件頭看看不同ELF的類型。為了說明問題,先來幾段代碼吧。
Code:[Ctrl+A Select All]Code:[Ctrl+A Select All]Code:[Ctrl+A Select All] 下面通過這幾段代碼來演示通過readelf -h參數(shù)查看ELF的不同類型。期間將演示如何創(chuàng)建動態(tài)連接庫(即可共享文件)、靜態(tài)連接庫,并比較它們的異同。
Quote: |
$ gcc -c myprintf.c test.c #編譯產(chǎn)生兩個目標(biāo)文件myprintf.o和test.o,它們都是可重定位文件(REL) $ readelf -h test.o | grep Type Type: REL (Relocatable file) $ readelf -h myprintf.o | grep Type Type: REL (Relocatable file) $ gcc -o test myprintf.o test.o #根據(jù)目標(biāo)代碼連接產(chǎn)生可執(zhí)行文件,這里的文件類型是可執(zhí)行的(EXEC) $ readelf -h test | grep Type Type: EXEC (Executable file) $ ar rcsv libmyprintf.a myprintf.o #用ar命令創(chuàng)建一個靜態(tài)連接庫,靜態(tài)連接庫也是可重定位文件(REL) $ readelf -h libmyprintf.a | grep Type #因此,使用靜態(tài)連接庫和可重定位文件一樣,它們之間唯一不 #同是前者可以是多個可重定位文件的“集合”。 Type: REL (Relocatable file) $ gcc -o test test.o -llib -L./ #可以直接連接進去,也可以使用-l參數(shù),-L指定庫的搜索路徑 $ gcc -Wall myprintf.o -shared -Wl,-soname,libmyprintf.so.0 -o libmyprintf.so.0.0 #編譯產(chǎn)生動態(tài)鏈接庫,并支持major和minor版本號,動態(tài)鏈接庫類型為DYN $ ln -sf libmyprintf.so.0.0 libmyprintf.so.0 $ ln -sf libmyprintf.so.0 libmyprintf.so $ readelf -h libmyprintf.so | grep Type Type: DYN (Shared object file) $ gcc -o test test.o -llib -L./ #編譯時和靜態(tài)連接庫類似,但是執(zhí)行時需要指定動態(tài)連接庫的搜索路徑 $ LD_LIBRARY_PATH=./ ./test #LD_LIBRARY_PATH為動態(tài)鏈接庫的搜索路徑 $ gcc -static -o test test.o -llib -L./ #在不指定static時會優(yōu)先使用動態(tài)鏈接庫,指定時則阻止使用動態(tài)連接庫 #這個時候會把所有靜態(tài)連接庫文件加入到可執(zhí)行文件中,使得執(zhí)行文件很大 #而且加載到內(nèi)存以后會浪費內(nèi)存空間,因此不建議這么做
|
經(jīng)過上面的演示基本可以看出它們之間的不同。可重定位文件本身不可以運行,僅僅是作為可執(zhí)行文件、靜態(tài)連接庫(也是可重定位文件)、動態(tài)連接庫的
“組件”。靜態(tài)連接庫和動態(tài)連接庫本身也不可以執(zhí)行,作為可執(zhí)行文件的“組件”,它們兩者也不同,前者也是可重定位文件(只不過可能是多個可重定位文件的
集合),并且在連接時加入到可執(zhí)行文件中去;而動態(tài)連接庫在連接時,庫文件本身并沒有添加到可執(zhí)行文件中,只是在可執(zhí)行文件中加入了該庫的名字等信息,以
便在可執(zhí)行文件運行過程中引用庫中的函數(shù)時由動態(tài)連接器去查找相關(guān)函數(shù)的地址,并調(diào)用它們。從這個意義上說,動態(tài)連接庫本身也具有可重定位的特征,含有可
重定位的信息。對于什么是重定位?如何進行靜態(tài)符號和動態(tài)符號的重定位,我們將在鏈接部分和《
動態(tài)符號鏈接的細節(jié)》一節(jié)介紹。
下面來看看ELF文件的主體內(nèi)容,節(jié)區(qū)(Section)。ELF文件具有很大的靈活性,它通過文件頭組織整個文件的總體結(jié)構(gòu),通過節(jié)區(qū)表
(Section Headers Table)和程序頭(Program Headers Table或者叫段表)來分別描述可重定位文件和可執(zhí)行文件。但不管是哪種類型,它們都需要它們的主體,即各種節(jié)區(qū)。在可重定位文件中,節(jié)區(qū)
表描述的就是各種節(jié)區(qū)本身;而在可執(zhí)行文件中,程序頭描述的是由各個節(jié)區(qū)組成的段(Segment),以便程序運行時動態(tài)裝載器知道如何對它們進行內(nèi)存映像,從而方便程序加載和運行。
下面先來看看
一些常見的節(jié)區(qū),而關(guān)于這些節(jié)區(qū)(section)如何通過重定位構(gòu)成成不同的段(Segments),以及有哪些常規(guī)的段,我們將在鏈接部分進一步介紹。
可以通過readelf的-S參數(shù)查看ELF的節(jié)區(qū)。(建議一邊操作一邊看文檔,以便加深對ELF文件結(jié)構(gòu)的理解)先來看看可重定位文件的節(jié)區(qū)信息,通過節(jié)區(qū)表來查看:
Quote: |
$ gcc -c myprintf.c #默認編譯好myprintf.c,將產(chǎn)生一個可重定位的文件myprintf.o $ readelf -S myprintf.o #通過查看myprintf.o的節(jié)區(qū)表查看節(jié)區(qū)信息 There are 11 section headers, starting at offset 0xc0:
Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .text PROGBITS 00000000 000034 000018 00 AX 0 0 4 [ 2] .rel.text REL 00000000 000334 000010 08 9 1 4 [ 3] .data PROGBITS 00000000 00004c 000000 00 WA 0 0 4 [ 4] .bss NOBITS 00000000 00004c 000000 00 WA 0 0 4 [ 5] .rodata PROGBITS 00000000 00004c 00000e 00 A 0 0 1 [ 6] .comment PROGBITS 00000000 00005a 000012 00 0 0 1 [ 7] .note.GNU-stack PROGBITS 00000000 00006c 000000 00 0 0 1 [ 8] .shstrtab STRTAB 00000000 00006c 000051 00 0 0 1 [ 9] .symtab SYMTAB 00000000 000278 0000a0 10 10 8 4 [10] .strtab STRTAB 00000000 000318 00001a 00 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) I (info), L (link order), G (group), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific) $ objdump -d -j .text myprintf.o #這里是程序指令部分,用objdump的-d選項可以看到反編譯的結(jié)果, #-j指定需要查看的節(jié)區(qū) myprintf.o: file format elf32-i386
Disassembly of section .text:
00000000 <myprintf>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 08 sub $0x8,%esp 6: 83 ec 0c sub $0xc,%esp 9: 68 00 00 00 00 push $0x0 e: e8 fc ff ff ff call f <myprintf+0xf> 13: 83 c4 10 add $0x10,%esp 16: c9 leave 17: c3 ret $ readelf -r myprintf.o #用-r選項可以看到有關(guān)重定位的信息,這里有兩部分需要重定位
Relocation section '.rel.text' at offset 0x334 contains 2 entries: Offset Info Type Sym.Value Sym. Name 0000000a 00000501 R_386_32 00000000 .rodata 0000000f 00000902 R_386_PC32 00000000 puts $ readelf -x .rodata myprintf.o #.rodata節(jié)區(qū)包含只讀數(shù)據(jù),即我們要打印的hello, world!.
Hex dump of section '.rodata': 0x00000000 68656c6c 6f2c2077 6f726c64 2100 hello, world!.
$ readelf -x .data myprintf.o #沒有這個節(jié)區(qū),.data應(yīng)該包含一些初始化的數(shù)據(jù)
Section '.data' has no data to dump. $ readelf -x .bss mmyprintf.o #也沒有這個節(jié)區(qū),.bss應(yīng)該包含一些未初始化的數(shù)據(jù),程序默認初始為0
Section '.bss' has no data to dump. $ readelf -x .comment myprintf.o #是一些注釋,可以看到是是GCC的版本信息
Hex dump of section '.comment': 0x00000000 00474343 3a202847 4e552920 342e312e .GCC: (GNU) 4.1. 0x00000010 3200 2. $ readelf -x .note.GNU-stack myprintf.o #這個也沒有內(nèi)容
Section '.note.GNU-stack' has no data to dump. $ readelf -x .shstrtab myprintf.o #包括所有節(jié)區(qū)的名字
Hex dump of section '.shstrtab': 0x00000000 002e7379 6d746162 002e7374 72746162 ..symtab..strtab 0x00000010 002e7368 73747274 6162002e 72656c2e ..shstrtab..rel. 0x00000020 74657874 002e6461 7461002e 62737300 text..data..bss. 0x00000030 2e726f64 61746100 2e636f6d 6d656e74 .rodata..comment 0x00000040 002e6e6f 74652e47 4e552d73 7461636b ..note.GNU-stack 0x00000050 00 .
$ readelf -symtab myprintf.o #符號表,包括所有用到的相關(guān)符號信息,如函數(shù)名、變量名
Symbol table '.symtab' contains 10 entries: Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 00000000 0 FILE LOCAL DEFAULT ABS myprintf.c 2: 00000000 0 SECTION LOCAL DEFAULT 1 3: 00000000 0 SECTION LOCAL DEFAULT 3 4: 00000000 0 SECTION LOCAL DEFAULT 4 5: 00000000 0 SECTION LOCAL DEFAULT 5 6: 00000000 0 SECTION LOCAL DEFAULT 7 7: 00000000 0 SECTION LOCAL DEFAULT 6 8: 00000000 24 FUNC GLOBAL DEFAULT 1 myprintf 9: 00000000 0 NOTYPE GLOBAL DEFAULT UND puts $ readelf -x .strtab myprintf.o #字符串表,用到的字符串,包括文件名、函數(shù)名、變量名等。
Hex dump of section '.strtab': 0x00000000 006d7970 72696e74 662e6300 6d797072 .myprintf.c.mypr 0x00000010 696e7466 00707574 7300 intf.puts.
|
從上表可以看出,對于可重定位文件,會包含這些基本節(jié)區(qū).text, .rel.text, .data, .bss, .rodata,
.comment, .note.GNU-stack, .shstrtab,
.symtab和.strtab。為了進一步理解這些節(jié)區(qū)和源代碼的關(guān)系,這里來看一看myprintf.c產(chǎn)生的匯編代碼。
Quote: |
$ gcc -S myprintf.c $ cat myprintf.s .file "myprintf.c" .section .rodata .LC0: .string "hello, world!" .text .globl myprintf .type myprintf, @function myprintf: pushl %ebp movl %esp, %ebp subl $8, %esp subl $12, %esp pushl $.LC0 call puts addl $16, %esp leave ret .size myprintf, .-myprintf .ident "GCC: (GNU) 4.1.2" .section .note.GNU-stack,"",@progbits
|
是不是可以從中看出可重定位文件中的那些節(jié)區(qū)和匯編語言代碼之間的關(guān)系?在上面的可重定位文件,可以看到有一個可重定位的節(jié)區(qū),即.
rel.text,它標(biāo)記了兩個需要重定位的項,.rodata和puts。這個節(jié)區(qū)將告訴編譯器這兩個信息在鏈接或者動態(tài)鏈接的過程中需要重定位,
具體如何重定位?將根據(jù)重定位項的類型,比如上面的R_386_32和R_386_PC32(關(guān)于這些類型的更多細節(jié),請查看ELF手冊[6])。
到這里,對可重定位文件應(yīng)該有了一個基本的了解,下面將介紹什么是可重定位,可重定位文件到底是如何被鏈接生成可執(zhí)行文件和動態(tài)連接庫的,這個過程除了進行了一些符號的重定位外,還進行了哪些工作呢?
本節(jié)參考資料:
[1] 了解編譯程序的過程
http://9iyou.com/Program_Data/linuxunix-3125.htmlhttp://www.host01.com/article/server/00070002/0621409075078127.htm[2] C track: compiling C programs.
http://www.cs.caltech.edu/courses/cs11/material/c/mike/misc/compiling_c.html[3] Dissecting shared libraries
http://www.ibm.com/developerworks/linux/library/l-shlibs.html4、鏈接
開篇:重定位是將符號引用與符號定義進行鏈接的過程。因此鏈接是處理可重定位文件,把它們的各種符號引用和符號定義轉(zhuǎn)換為可執(zhí)行文件中的合適信息(一般是虛擬內(nèi)存地址)的過程。鏈接又
分為靜態(tài)鏈接和動態(tài)鏈接,前者是程序開發(fā)階段程序員用ld(gcc實際上在后臺調(diào)用了ld)靜態(tài)鏈接器手動鏈接的過程,而動態(tài)鏈接則是程序運行期間系
統(tǒng)調(diào)用動態(tài)鏈接器(ld-linux.so)自動鏈接的過程。比如,如果鏈接到可執(zhí)行文件中的是靜態(tài)連接庫libmyprintf.a,那么.
rodata節(jié)區(qū)在鏈接后需要被重定位到一個絕對的虛擬內(nèi)存地址,以便程序運行時能夠正確訪問該節(jié)區(qū)中的字符串信息。而對于puts,因為它是動態(tài)連接庫libc.so中定義的函數(shù),所
以會在程序運行時通過動態(tài)符號鏈接找出puts函數(shù)在內(nèi)存中的地址,以便程序調(diào)用該函數(shù)。在這里主要討論靜態(tài)鏈接過程,動態(tài)鏈接過程見《
動態(tài)符號鏈接的細節(jié)》。
靜態(tài)鏈接過程主要是把可重定位文件依次讀入,分析各個文件的文件頭,進而依次讀入各個文件的節(jié)區(qū),并計算各個節(jié)區(qū)的虛擬內(nèi)存位置,對一些需要重定位的符號
進
行處理,設(shè)定它們的虛擬內(nèi)存地址等,并最終產(chǎn)生一個可執(zhí)行文件或者是動態(tài)鏈接庫。這個鏈接過程是通過ld來完成的,ld在鏈接時使用了一個鏈接腳本
(linker script),
該鏈接腳本處理鏈接的具體細節(jié)。由于靜態(tài)符號鏈接過程非常復(fù)雜,特別是計算符號地址的過程,考慮到時間關(guān)系,相關(guān)細節(jié)請參考ELF手冊[6]。這里主要介
紹可重定位文件中的節(jié)區(qū)(節(jié)區(qū)表描述的)和可執(zhí)行文件中段(程序頭描述的)的對應(yīng)關(guān)系以及gcc編譯時采用的一些默認鏈接選項。
下面先來看看可執(zhí)行文件的節(jié)區(qū)信息,通過程序頭(段表)來查看:
Quote: |
$ readelf -S test.o #為了比較,先把test.o的節(jié)區(qū)表也列出 There are 10 section headers, starting at offset 0xb4:
Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .text PROGBITS 00000000 000034 000024 00 AX 0 0 4 [ 2] .rel.text REL 00000000 0002ec 000008 08 8 1 4 [ 3] .data PROGBITS 00000000 000058 000000 00 WA 0 0 4 [ 4] .bss NOBITS 00000000 000058 000000 00 WA 0 0 4 [ 5] .comment PROGBITS 00000000 000058 000012 00 0 0 1 [ 6] .note.GNU-stack PROGBITS 00000000 00006a 000000 00 0 0 1 [ 7] .shstrtab STRTAB 00000000 00006a 000049 00 0 0 1 [ 8] .symtab SYMTAB 00000000 000244 000090 10 9 7 4 [ 9] .strtab STRTAB 00000000 0002d4 000016 00 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) I (info), L (link order), G (group), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific) $ gcc -o test test.o libmyprintf.o $ readelf -l test #我們發(fā)現(xiàn),test和test.o,libmyprintf.o相比,多了很多節(jié)區(qū),如.interp和.init等
Elf file type is EXEC (Executable file) Entry point 0x80482b0 There are 7 program headers, starting at offset 52
Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align PHDR 0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4 INTERP 0x000114 0x08048114 0x08048114 0x00013 0x00013 R 0x1 [Requesting program interpreter: /lib/ld-linux.so.2] LOAD 0x000000 0x08048000 0x08048000 0x0047c 0x0047c R E 0x1000 LOAD 0x00047c 0x0804947c 0x0804947c 0x00104 0x00108 RW 0x1000 DYNAMIC 0x000490 0x08049490 0x08049490 0x000c8 0x000c8 RW 0x4 NOTE 0x000128 0x08048128 0x08048128 0x00020 0x00020 R 0x4 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
Section to Segment mapping: Segment Sections... 00 01 .interp
02 .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version
.gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata
.eh_frame 03 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss 04 .dynamic 05 .note.ABI-tag 06
|
上表給出了可執(zhí)行文件的如下幾個段(segment),
PHDR: 給出了程序表自身的大小和位置,不能出現(xiàn)一次以上。
INTERP: 因為程序中調(diào)用了puts(在動態(tài)鏈接庫中定義),使用了動態(tài)連接庫,因此需要動態(tài)裝載器/鏈接器(ld-linux.so)
LOAD: 包括程序的指令,.text等節(jié)區(qū)都映射在該段,只讀(R)
LOAD: 包括程序的數(shù)據(jù),.data, .bss等節(jié)區(qū)都映射在該段,可讀寫(RW)
DYNAMIC: 動態(tài)鏈接相關(guān)的信息,比如包含有引用的動態(tài)連接庫名字等信息
NOTE: 給出一些附加信息的位置和大小
GNU_STACK: 這里為空,應(yīng)該是和GNU相關(guān)的一些信息
這里的段可能包括之前的一個或者多個節(jié)區(qū),也就是說經(jīng)過鏈接之后原來的節(jié)區(qū)被重排了,并映射到了不同的段,這些段將告訴系統(tǒng)應(yīng)該如何把它加載到內(nèi)存中。
從上表中,通過比較可執(zhí)行文件(test)中擁有的節(jié)區(qū)和可重定位文件(test.o和myprintf.o)中擁有的節(jié)區(qū)后發(fā)現(xiàn),鏈接之后多了一些之前沒有的節(jié)區(qū),這些新的節(jié)區(qū)來自哪里?它們的作用是什么呢?先來通過gcc的-v參數(shù)看看它的后臺鏈接過程。
Quote: |
$ gcc -v -o test test.o myprintf.o #把可重定位文件鏈接成可執(zhí)行文件 Reading specs from /usr/lib/gcc/i486-slackware-linux/4.1.2/specs Target: i486-slackware-linux Configured
with: ../gcc-4.1.2/configure --prefix=/usr --enable-shared
--enable-languages=ada,c,c++,fortran,java,objc --enable-threads=posix
--enable-__cxa_atexit --disable-checking --with-gnu-ld --verbose
--with-arch=i486 --target=i486-slackware-linux
--host=i486-slackware-linux Thread model: posix gcc version 4.1.2 /usr/libexec/gcc/i486-slackware-linux/4.1.2/collect2
--eh-frame-hdr -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test
/usr/lib/gcc/i486-slackware-linux/4.1.2/../../../crt1.o
/usr/lib/gcc/i486-slackware-linux/4.1.2/../../../crti.o
/usr/lib/gcc/i486-slackware-linux/4.1.2/crtbegin.o
-L/usr/lib/gcc/i486-slackware-linux/4.1.2
-L/usr/lib/gcc/i486-slackware-linux/4.1.2
-L/usr/lib/gcc/i486-slackware-linux/4.1.2/../../../../i486-slackware-linux/lib
-L/usr/lib/gcc/i486-slackware-linux/4.1.2/../../.. test.o myprintf.o
-lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s
--no-as-needed /usr/lib/gcc/i486-slackware-linux/4.1.2/crtend.o
/usr/lib/gcc/i486-slackware-linux/4.1.2/../../../crtn.o
|
從上邊的演示看出,gcc在連接了我們自己的目標(biāo)文件test.o和myprintf.o之外,還連接了crt1.o,crtbegin.o等額外的目標(biāo)文件,難道那些新的節(jié)區(qū)就來自這些文件?
另外gcc在進行了相關(guān)配置(./configure)后,調(diào)用了collect2,卻并沒有調(diào)用ld,通過查找gcc文檔中和collect2相關(guān)的部
分發(fā)現(xiàn)collect2在后臺實際上還是去尋找ld命令的。為了理解gcc默認連接的后臺細節(jié),這里直接把collect2替換成ld,并把一些路徑換成
絕對路徑或者簡化,得到如下的ld命令以及執(zhí)行的效果。
Quote: |
$ ld --eh-frame-hdr \ -m elf_i386 \ -dynamic-linker /lib/ld-linux.so.2 \ -o test \ /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/gcc/i486-slackware-linux/4.1.2/crtbegin.o \ test.o myprintf.o \ -L/usr/lib/gcc/i486-slackware-linux/4.1.2
-L/usr/i486-slackware-linux/lib -L/usr/lib/ -lgcc --as-needed -lgcc_s
--no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed \ /usr/lib/gcc/i486-slackware-linux/4.1.2/crtend.o /usr/lib/crtn.o $ ./test hello, world!
|
不出我們所料,它完美的運行了。下面通過ld的手冊(man ld)來分析一下這幾個參數(shù)。
--eh-frame-hdr
要求創(chuàng)建一個.eh_frame_hdr節(jié)區(qū)(貌似目標(biāo)文件test中并沒有這個節(jié)區(qū),所以不關(guān)心它)。
-m elf_i386
這
里指定不同平臺上的鏈接腳本,可以通過--verbose命令查看腳本的具體內(nèi)容,如ld -m elf_i386
--verbose,它實際上被存放在一個文件中(/usr/lib/ldscripts目錄下),你可以去修改這個腳本,具體如何做?請參考ld的手冊。
在后面我們將簡要提到鏈接腳本中是如何預(yù)定義變量的,以及這些預(yù)定義變量如何在我們的程序中使用。需要提到的是,如果不是交叉編譯,那么無須指定該選項。
-dynamic-linker /lib/ld-linux.so.2
指定動態(tài)裝載器/鏈接器,即程序中的INTERP段中的內(nèi)容。動態(tài)裝載器/連接器負責(zé)連接有可共享庫的可執(zhí)行文件的裝載和動態(tài)符號連接。
-o test
指定輸出文件,即可執(zhí)行文件名的名字
/usr/lib/crt1.o /usr/lib/crti.o /usr/lib/gcc/i486-slackware-linux/4.1.2/crtbegin.o
鏈
接到test文件開頭的一些內(nèi)容,這里實際上就包含了.init等節(jié)區(qū)。.init節(jié)區(qū)包含一些可執(zhí)行代碼,在main函數(shù)之前被調(diào)用,以便進行一些初始化操
作,在C++中完成構(gòu)造函數(shù)功能,更多細節(jié)請參考資料[9]
test.o myprintf.o
鏈接我們自己的可重定位文件
-L/usr/lib/gcc/i486-slackware-linux/4.1.2
-L/usr/i486-slackware-linux/lib -L/usr/lib/ -lgcc --as-needed -lgcc_s
--no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed
鏈接libgcc庫和libc庫,后者定義有我們需要的puts函數(shù)
/usr/lib/gcc/i486-slackware-linux/4.1.2/crtend.o /usr/lib/crtn.o
鏈接到test文件末尾的一些內(nèi)容,這里實際上包含了.fini等節(jié)區(qū)。.fini節(jié)區(qū)包含了一些可執(zhí)行代碼,在程序退出時被執(zhí)行,作一些清理工作,在C++中完成析構(gòu)造函數(shù)功能。我們往往可以通過atexit來注冊那些需要在程序退出時才執(zhí)行的函數(shù)。
對于crtbegin.o和crtend.o這兩個文件,貌似完全是用來支持C++的構(gòu)造和析構(gòu)工作的[9],所以可以不鏈接到我們的可執(zhí)行文件中,鏈接時把它們?nèi)サ艨纯矗?br>
Quote: |
$
ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test
/usr/lib/crt1.o /usr/lib/crti.o test.o myprintf.o -L/usr/lib -lc
/usr/lib/crtn.o #后面發(fā)現(xiàn)不用鏈接libgcc,也不用--eh-frame-hdr參數(shù) $ readelf -l test
Elf file type is EXEC (Executable file) Entry point 0x80482b0 There are 7 program headers, starting at offset 52
Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align PHDR 0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4 INTERP 0x000114 0x08048114 0x08048114 0x00013 0x00013 R 0x1 [Requesting program interpreter: /lib/ld-linux.so.2] LOAD 0x000000 0x08048000 0x08048000 0x003ea 0x003ea R E 0x1000 LOAD 0x0003ec 0x080493ec 0x080493ec 0x000e8 0x000e8 RW 0x1000 DYNAMIC 0x0003ec 0x080493ec 0x080493ec 0x000c8 0x000c8 RW 0x4 NOTE 0x000128 0x08048128 0x08048128 0x00020 0x00020 R 0x4 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
Section to Segment mapping: Segment Sections... 00 01 .interp
02 .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version
.gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata 03 .dynamic .got .got.plt .data 04 .dynamic 05 .note.ABI-tag 06 $ ./test hello, world!
|
完全可以工作,而且發(fā)現(xiàn).ctors(保存著程序中全局構(gòu)造函數(shù)的指針數(shù)組),
.dtors(保存著程序中全局析構(gòu)函數(shù)的指針數(shù)組),.jcr(未知),.eh_frame節(jié)區(qū)都沒有了,所以crtbegin.o和crtend.o應(yīng)該包含了這些節(jié)區(qū)。
而對于另外兩個文件crti.o和crtn.o,通過readelf
-S查看后發(fā)現(xiàn)它們都有.init和.fini節(jié)區(qū),如果我們不需要讓程序進行一些初始化和清理工作呢?是不是就可以不
鏈接這個兩個文件?試試看。
Quote: |
$ ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test /usr/lib/crt1.o test.o myprintf.o -L/usr/lib/ -lc /usr/lib/libc_nonshared.a(elf-init.oS): In function `__libc_csu_init': (.text+0x25): undefined reference to `_init'
|
貌似不行,竟然有人調(diào)用了__libc_csu_init函數(shù),而這個函數(shù)引用了_init。這兩個符號都在哪里呢?
Quote: |
$ readelf -s /usr/lib/crt1.o | grep __libc_csu_init 18: 00000000 0 NOTYPE GLOBAL DEFAULT UND __libc_csu_init $ readelf -s /usr/lib/crti.o | grep _init 17: 00000000 0 FUNC GLOBAL DEFAULT 5 _init
|
竟然是crt1.o調(diào)用了__libc_csu_init函數(shù),而該函數(shù)卻引用了我們沒有鏈接的crti.o文件中定義的_init符號。這樣的話不鏈接
crti.o和crtn.o文件就不成了羅?不對吧,要不干脆不用crt1.o算了,看看gcc額外連接進去的最后一個文件crt1.o到底干了個啥子?
Quote: |
$ ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test test.o myprintf.o -L/usr/lib/ -lc ld: warning: cannot find entry symbol _start; defaulting to 00000000080481a4
|
這樣卻說沒有找到入口符號_start,難道crt1.o中定義了這個符號?不過它給默認設(shè)置了一個地址,只是個警告,說明test已經(jīng)生成,不管怎樣先運行看看再說。
Quote: |
$ ./test hello, world! Segmentation fault
|
貌似程序運行完了,不過結(jié)束時冒出個段錯誤?可能是程序結(jié)束時有問題,用gdb調(diào)試看看:
Quote: |
$ gcc -g -c test.c myprintf.c #產(chǎn)生目標(biāo)代碼, 非交叉編譯,不指定-m也可以鏈接成功,所以下面可以去掉-m參數(shù) $ ld -dynamic-linker /lib/ld-linux.so.2 -o test test.o myprintf.o -L/usr/lib -lc ld: warning: cannot find entry symbol _start; defaulting to 00000000080481d8 $ ./test hello, world! Segmentation fault $ gdb ./test ... (gdb) l 1 #include "test.h" 2 3 int main() 4 { 5 myprintf(); 6 return 0; 7 } (gdb) break 7 #在程序的末尾設(shè)置一個斷點 Breakpoint 1 at 0x80481bf: file test.c, line 7. (gdb) r #程序都快結(jié)束了都沒問題,怎么會到最后出個問題呢? Starting program: /mnt/hda8/Temp/c/program/test hello, world!
Breakpoint 1, main () at test.c:7 7 } (gdb) n #單步執(zhí)行看看,怎么下面一條指令是0x00000001,肯定是程序退出以后出了問題 0x00000001 in ?? () (gdb) n #誒,當(dāng)然找不到邊了,都跑到0x00000001了 Cannot find bounds of current function (gdb) c #原來是這么回事,估計是return 0返回之后出問題了,看看它的匯編去。 Continuing.
Program received signal SIGSEGV, Segmentation fault. 0x00000001 in ?? () $ gcc -S test.c #產(chǎn)生匯編代碼 $ cat test.s #后面就這么幾條指令,難不成ret返回有問題,不讓它ret返回,把return改成_exit直接進入內(nèi)核退出 ... call myprintf movl $0, %eax addl $4, %esp popl %ecx popl %ebp leal -4(%ecx), %esp ret ... $ vim test.c $ cat test.c #就把return語句修改成_exit了。 #include "test.h" #include <unistd.h> /* _exit */
int main() { myprintf(); _exit(0); } $ gcc -g -c test.c myprintf.c $ ld -dynamic-linker /lib/ld-linux.so.2 -o test test.o myprintf.o -L/usr/lib -lc ld: warning: cannot find entry symbol _start; defaulting to 00000000080481d8 $ ./test #竟然好了,再看看匯編有什么不同 hello, world! $ gcc -S test.c $ cat test.s #貌似就把ret指令替換成了_exit函數(shù)調(diào)用,直接進入內(nèi)核,然內(nèi)核讓處理了,那為什么ret有問題呢? ... call myprintf subl $12, %esp pushl $0 call _exit ... $ gdb ./test #把代碼改回去(改成return 0;),再調(diào)試看看調(diào)用main函數(shù)返回時的下一條指令地址eip ... (gdb) l warning: Source file is more recent than executable. 1 #include "test.h" 2 3 int main() 4 { 5 myprintf(); 6 return 0; 7 } (gdb) break 5 Breakpoint 1 at 0x80481b5: file test.c, line 5. (gdb) break 7 Breakpoint 2 at 0x80481bc: file test.c, line 7. (gdb) r Starting program: /mnt/hda8/Temp/c/program/test
Breakpoint 1, main () at test.c:5 5 myprintf(); (gdb) x/8x $esp #發(fā)現(xiàn)0x00000001剛好是之前我們調(diào)試時看到的程序返回后的位置,即eip,說明程序在初始化的時候 #這個eip就是錯誤的。為什么呢?因為我們根本沒有鏈接進來初始化的代碼,而是在編譯器自己給我們 #初始化了一個程序入口即00000000080481d8,也就是說,沒有任何人調(diào)用main,main不知道返回哪里去 #所以,我們直接讓main結(jié)束時進入內(nèi)核調(diào)用_exit而退出則不會有問題 0xbf929510: 0xbf92953c 0x080481a4 0x00000000 0xb7eea84f 0xbf929520: 0xbf92953c 0xbf929534 0x00000000 0x00000001
|
通過上面的演示和解釋發(fā)現(xiàn)只要把return語句修改為_exit語句,程序即使不鏈接任何額外的目標(biāo)代碼都可以正常運行(原因是不連接那些額外的文件時
相當(dāng)于沒有進行初始化操作,如果在程序的最后執(zhí)行ret匯編指令,程序?qū)o法獲得正確的eip,從而無法進行后續(xù)的動作)。但是為什么會有“找不到
_start符號”的警告呢?通過readelf
-s查看crt1.o發(fā)現(xiàn)里頭有這個符號,并且crt1.o引用了main這個符號,是不是意味著會從_start進入main呢?是不是程序入口是
_start,而并非main呢?
先來看看剛才提到的鏈接器的默認鏈接腳本(ld -m elf_386
--verbose),它告訴我們程序的入口(entry)是_start,而一個可執(zhí)行文件必須有一個入口地址才能運行,所以這就是說明了為什么ld一
定要提示我們“_start找不到”,找不到以后就給默認設(shè)置了一個地址。
Quote: |
$ ld --verbose | grep ^ENTRY #非交叉編譯,可不用-m參數(shù);ld默認找_start入口,并不是main哦! ENTRY(_start)
|
原來是這樣,程序的入口(entry)竟然不是main函數(shù),而是_start。那干脆把匯編里頭的main給改掉算了,看行不行?
Quote: |
$ cat test.c #include "test.h" #include <unistd.h> /* _exit */
int main() { myprintf(); _exit(0); } $ gcc -S test.c $ sed -i -e "s#main#_start#g" test.s #把匯編中的main全部修改為_start,即修改程序入口為_start $ gcc -c test.s myprintf.c $ ld -dynamic-linker /lib/ld-linux.so.2 -o test test.o myprintf.o -L/usr/lib/ -lc #果然沒問題了 :-) $ ./test hello, world!
|
_start竟然是真正的程序入口,那在有main的情況下呢?為什么在_start之后能夠找到main呢?這個看看alert7大叔的"Before
main分析"[5]吧,這里不再深入介紹。總之呢,通過修改程序的return語句為_exit(0)和修改程序的入口為_start,我們的代碼不鏈接gcc默認鏈
接的那些額外的文件同樣可以工作得很好。并且打破了一個學(xué)習(xí)C語言以來的常識:main函數(shù)作為程序的主函數(shù),是程序的入口,實際上則不然。
再補充一點內(nèi)容,在ld的鏈接腳本中,有一個特別的關(guān)鍵字PROVIDE,由這個關(guān)鍵字定義的符號是ld的預(yù)定義字符,我們可以在C語言函數(shù)中擴展它們后直接使用。這些特別的符號可以通過下面的方法獲取,
Quote: |
$ ld --verbose | grep PROVIDE | grep -v HIDDEN PROVIDE (__executable_start = 0x08048000); . = 0x08048000 + SIZEOF_HEADERS; PROVIDE (__etext = .); PROVIDE (_etext = .); PROVIDE (etext = .); _edata = .; PROVIDE (edata = .); _end = .; PROVIDE (end = .);
|
這里面有幾個我們比較關(guān)心的,第一個是程序的入口地址__executable_start,另外三個是etext,edata,end,分別對應(yīng)程序的
代碼段(text)、初始化數(shù)據(jù)(data)和未初始化的數(shù)據(jù)(bss)(可以參考資料[6]和man
etext),如何引用這些變量呢?看看這個例子。
Code:
[Ctrl+A Select All]
到這里,程序鏈接過程的一些細節(jié)都介紹得差不多了。在《動態(tài)符號鏈接的細節(jié)》中將主要介紹ELF文件的動態(tài)符號鏈接過程。
本節(jié)參考資料
[1] An beginners guide to compiling programs under Linux.
http://www.luv.asn.au/overheads/compile.html
[2] gcc manual
http://gcc.gnu.org/onlinedocs/gcc-4.2.2/gcc/
[3] A Quick Tour of Compiling, Linking, Loading, and Handling Libraries on Unix
http://efrw01.frascati.enea.it/Software/Unix/IstrFTU/cern-cnl-2001-003-25-link.html
[4] Unix 目標(biāo)文件初探
http://www.ibm.com/developerworks/cn/aix/library/au-unixtools.html
[5] Before main()分析
http://www.xfocus.net/articles/200109/269.html
[6] A Process Viewing Its Own /proc/<PID>/map Information
http://www.linuxforums.org/forum/linux-kernel/51790-process-viewing-its-own-proc-pid-map-information.html