程序一定要從main函數(shù)開始運(yùn)行嗎?
對于靜態(tài)鏈接先提出兩個問題:
對于那些需要重定位的符號,都會放在重定位表里,也叫重定位段,即.rel.data、.rel.text等,如果.text段有被重定位的地方,就有.rel.text段,如果.data段有被重定位的地方,就有.rel.data段。
可以使用objdump查看目標(biāo)文件的重定位表。
源代碼:
int main() {printf("程序喵\n");return 0;}gcc -c test
objdump -r test.otest.o: file format elf64-x86-64RELOCATION RECORDS FOR [.text]:OFFSET TYPE VALUE0000000000000007 R_X86_64_PC32 .rodata-0x0000000000000004000000000000000c R_X86_64_PLT32 puts-0x0000000000000004RELOCATION RECORDS FOR [.eh_frame]:OFFSET TYPE VALUE0000000000000020 R_X86_64_PC32 .text
使用nm也可以查看需要重定位的符號:
nm -u test.oU _GLOBAL_OFFSET_TABLE_U puts
對于UND類型,這種未定義的符號都是因?yàn)樵撃繕?biāo)文件中有關(guān)于他們的重定位項(xiàng),在鏈接器掃描完所有的輸入目標(biāo)文件后,所有這種未定義的符號都應(yīng)該能在全局符號表中找到,否則報(bào)符號未定義錯誤。
注意:我們代碼里明明用的是printf,為什么它卻引用了puts的符號呢,因?yàn)榫幾g器默認(rèn)情況下會把只用一個字符串參數(shù)的printf替換成puts, 可以節(jié)省格式解析的時間,使用-fno-builtin會關(guān)閉這個內(nèi)置函數(shù)優(yōu)化選項(xiàng),如下:
~/test$ gcc -c -fno-builtin testlink.cc -o test.o~/test$ nm test.oU _GLOBAL_OFFSET_TABLE_0000000000000000 T mainU printf
現(xiàn)在的程序和庫通常來講都很大,一個目標(biāo)文件可能包含成百上千個函數(shù)或變量,當(dāng)需要用到某個目標(biāo)文件的任意一個函數(shù)或變量時,就需要把它整個目標(biāo)文件都鏈接進(jìn)來,也就是說那些沒有用到的函數(shù)也會被鏈接進(jìn)去,這會導(dǎo)致鏈接輸出文件變的很大,造成空間浪費(fèi)。
-ffunction-sections-fdata-sections
可能很多人都會以為程序都是由main函數(shù)開始執(zhí)行和結(jié)束的,但其實(shí)不是,在main函數(shù)調(diào)用之前,為了保證程序可以順利進(jìn)行,要先初始化進(jìn)程執(zhí)行環(huán)境,如堆分配初始化、線程子系統(tǒng)等,C++的全局對象構(gòu)造函數(shù)也是這一時期被執(zhí)行的,全局析構(gòu)函數(shù)是main之后執(zhí)行的。
Linux一般程序的入口是__start函數(shù),程序有兩個相關(guān)的段:
init段:進(jìn)程的初始化代碼,一個程序開始運(yùn)行時,在main函數(shù)調(diào)用之前,會先運(yùn)行.init段中的代碼。
fini段:進(jìn)程終止代碼,當(dāng)main函數(shù)正常退出后,glibc會安排執(zhí)行該段代碼。
如何指定程序入口
在ld鏈接過程中使用-e參數(shù)可以指定程序入口,由于一段簡短的printf函數(shù)其實(shí)都依賴了好多個鏈接庫,我們也不太方便使用鏈接腳本將目標(biāo)文件與所有這些依賴庫進(jìn)行鏈接,所以使用下面這段內(nèi)嵌匯編的程序來打印一段字符串,這段程序不依賴任何鏈接庫就可以打印出字符串內(nèi)容,讀者如果不懂其中的含義也不用擔(dān)心,只需要了解下面介紹的鏈接知識就好。
代碼如下:
const char* str = "hello";void print() {asm("movl $13,%%edx \n\t""movl str,%%ecx \n\t""movl $0,%%ebx \n\t""movl $4,%%eax \n\t""int $0x80 \n\t"::"r"(str):"edx", "ecx", "ebx");}void exit() {asm("movl $42,%ebx \n\t""movl $1,%eax \n\t""int $0x80 \n\t");}void nomain() {print();exit();}
使用如下命令生成目標(biāo)文件:
gcc -c -fno-builtin test.cc
看下輸出的test.o的符號:
~/test$ nm -a test.o0000000000000000 b .bss0000000000000000 n .comment0000000000000000 d .data0000000000000000 d .data.rel.local0000000000000000 r .eh_frame0000000000000000 n .note.GNU-stack0000000000000000 r .rodata0000000000000000 t .text0000000000000026 T _Z4exitv0000000000000000 T _Z5printv0000000000000039 T _Z6nomainv0000000000000000 D str0000000000000000 a test.cc
這里由于我的源文件是.cc結(jié)尾,所以是以c++方式編譯的,所以符號變成了上面的形式,如果變成了test.c,符號如下:
~/test$ gcc -c -fno-builtin test.c -o test.o~/test$ nm -a test.o0000000000000000 b .bss0000000000000000 n .comment0000000000000000 d .data0000000000000000 d .data.rel.local0000000000000000 r .eh_frame0000000000000000 n .note.GNU-stack0000000000000000 r .rodata0000000000000000 t .text0000000000000026 T exit0000000000000039 T nomain0000000000000000 T print0000000000000000 D str0000000000000000 a test.c
再使用-e指定入口函數(shù)符號:
~/test$ ld -static -e nomain -o test test.o~/test$ ./testhello
如何使用自定義鏈接腳本實(shí)現(xiàn)自定義段的功能
在ld鏈接過程中使用-T參數(shù)可以指定鏈接腳本,通過ld -verbose可以查看默認(rèn)的鏈接腳本,原文太長,這里簡單截取了一部分:
$ ld -verboseGNU ld (GNU Binutils for Ubuntu) 2.30Supported emulations:elf_x86_64elf32_x86_64elf_i386elf_iamcui386linuxelf_l1omelf_k1omi386pepi386peusing internal linker script:==================================================/* Script for -z combreloc: combine and sort reloc sections *//* Copyright (C) 2014-2018 Free Software Foundation, Inc.Copying and distribution of this script, with or without modification,are permitted in any medium without royalty provided the copyrightnotice and this notice are preserved. */OUTPUT_FORMAT("elf64-x86-64", "elf64-x86-64","elf64-x86-64")OUTPUT_ARCH(i386:x86-64)ENTRY(_start)SEARCH_DIR("=/usr/local/lib/x86_64-linux-gnu"); SEARCH_DIR("=/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/lib/x86_64-linux-gnu64"); SEARCH_DIR("=/usr/local/lib64"); SEARCH_DIR("=/lib64"); SEARCH_DIR("=/usr/lib64"); SEARCH_DIR("=/usr/local/lib"); SEARCH_DIR("=/lib"); SEARCH_DIR("=/usr/lib"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib64"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib");SECTIONS{/* Read-only sections, merged into text segment: */PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;.init :{KEEP (*(SORT_NONE(.init)))}.plt : { *(.plt) *(.iplt) }.plt.got : { *(.plt.got) }.plt.sec : { *(.plt.sec) }.text :{*(.text.unlikely .text.*_unlikely .text.unlikely.*)*(.text.exit .text.exit.*)*(.text.startup .text.startup.*)*(.text.hot .text.hot.*)*(.text .stub .text.* .gnu.linkonce.t.*)/* .gnu.warning sections are handled specially by elf32.em. */*(.gnu.warning)}.fini :{KEEP (*(SORT_NONE(.fini)))}.rodata : { *(.rodata .rodata.* .gnu.linkonce.r.*) }/DISCARD/ : { *(.note.GNU-stack) *(.gnu_debuglink) *(.gnu.lto_*) }}
這里自定義一個簡單的鏈接腳本test.lds
ENTRY(nomain)SECTIONS{. = 0x8048000 + SIZEOF_HEADERS;tinytext : { *(.text) *(.data) *(.rodata) }/DISCARD/ : { *(.comment) }}
再使用-T指定鏈接腳本:
~/test$ ld -static -T test.lds -e nomain -o test test.o~/test$ ./testhello
上面的tinytext一行是指將.text段、.data段、.rodata段的內(nèi)容都合并到tinytext段中,使用readelf查看段的信息。
~/test$ readelf -S test~/test$ There are 6 section headers, starting at offset 0x482a0:Section Headers:[Nr] Name Type Address OffsetSize EntSize Flags Link Info Align[ 0] NULL 0000000000000000 000000000000000000000000 0000000000000000 0 0 0[ 1] .eh_frame PROGBITS 00000000080480b0 000480b00000000000000078 0000000000000000 A 0 0 8[ 2] tinytext PROGBITS 0000000008048128 000481280000000000000066 0000000000000000 WAX 0 0 8[ 3] .shstrtab STRTAB 0000000000000000 0004826e000000000000002e 0000000000000000 0 0 1[ 4] .symtab SYMTAB 0000000000000000 0004819000000000000000c0 0000000000000018 5 4 8[ 5] .strtab STRTAB 0000000000000000 00048250000000000000001e 0000000000000000 0 0 1Key to Flags:W (write), A (alloc), X (execute), M (merge), S (strings), l (large)I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)O (extra OS processing required) o (OS specific), p (processor specific)
工具小貼士
關(guān)于靜態(tài)鏈接庫:
ar rcs libxxx.a xx1.o xx2.o 打包靜態(tài)鏈接庫ar -t libc.a 查看靜態(tài)鏈接庫里都有什么目標(biāo)文件ar -x libc.a 會解壓所有的目標(biāo)文件到當(dāng)前目錄gcc --verbose 可以查看整個編譯鏈接步驟
關(guān)于objdump:
objdump -i 查看本機(jī)目標(biāo)架構(gòu)objdump -f 顯示文件頭信息objdump -d 反匯編程序objdump -t 顯示符號表入口,每個目標(biāo)文件都有什么符號objdump -r 顯示文件的重定位入口,重定位表objdump -x 顯示所有可用的頭信息,等于-a -f -h -r -tobjdump -H 幫助
關(guān)于分析ELF文件格式:
readelf -h 列出文件頭readelf -S 列出每個段readelf -r 列出重定位表readelf -d 列出動態(tài)段
關(guān)于查看目標(biāo)文件符號信息:
nm -a 顯示所有的符號nm -D 顯示動態(tài)符號nm -u 僅顯示沒有定義的外部符號nm -defined-only 僅顯示定義的符號
關(guān)于符號的說明:
如果符號類型是小寫的,表明符號是局部符號,大寫表示符號是全局符號。
A:該符號的值是絕對的,在以后的鏈接過程中,不允許進(jìn)行改變。這樣的符號值,常常出現(xiàn)在中斷向量表中,例如用符號來表示各個中斷向量函數(shù)在中斷向量表中的位置。
B:該符號的值出現(xiàn)在.bss段中,未初始化的全局和靜態(tài)變量。
C:該符號的值在COMMON段中,里面的都是弱符號。
D:該符號位于數(shù)據(jù)段中。
I:該符號對另一個符號的間接引用
N:debug符號
R:該符號位于只讀數(shù)據(jù)區(qū)
T:該符號位于代碼段
U:該符號在當(dāng)前文件未定義,定義在別的文件中
?:該符號類型沒有定義
參考資料
https://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/
《程序員的自我修養(yǎng)》
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點(diǎn),不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!





