程序一定要從main函數(shù)開(kāi)始運(yùn)行嗎?
對(duì)于靜態(tài)鏈接先提出兩個(gè)問(wèn)題:
對(duì)于那些需要重定位的符號(hào),都會(huì)放在重定位表里,也叫重定位段,即.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也可以查看需要重定位的符號(hào):
nm -u test.oU _GLOBAL_OFFSET_TABLE_U puts
對(duì)于UND類型,這種未定義的符號(hào)都是因?yàn)樵撃繕?biāo)文件中有關(guān)于他們的重定位項(xiàng),在鏈接器掃描完所有的輸入目標(biāo)文件后,所有這種未定義的符號(hào)都應(yīng)該能在全局符號(hào)表中找到,否則報(bào)符號(hào)未定義錯(cuò)誤。
注意:我們代碼里明明用的是printf,為什么它卻引用了puts的符號(hào)呢,因?yàn)榫幾g器默認(rèn)情況下會(huì)把只用一個(gè)字符串參數(shù)的printf替換成puts, 可以節(jié)省格式解析的時(shí)間,使用-fno-builtin會(huì)關(guān)閉這個(gè)內(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)在的程序和庫(kù)通常來(lái)講都很大,一個(gè)目標(biāo)文件可能包含成百上千個(gè)函數(shù)或變量,當(dāng)需要用到某個(gè)目標(biāo)文件的任意一個(gè)函數(shù)或變量時(shí),就需要把它整個(gè)目標(biāo)文件都鏈接進(jìn)來(lái),也就是說(shuō)那些沒(méi)有用到的函數(shù)也會(huì)被鏈接進(jìn)去,這會(huì)導(dǎo)致鏈接輸出文件變的很大,造成空間浪費(fèi)。
-ffunction-sections-fdata-sections
可能很多人都會(huì)以為程序都是由main函數(shù)開(kāi)始執(zhí)行和結(jié)束的,但其實(shí)不是,在main函數(shù)調(diào)用之前,為了保證程序可以順利進(jìn)行,要先初始化進(jìn)程執(zhí)行環(huán)境,如堆分配初始化、線程子系統(tǒng)等,C++的全局對(duì)象構(gòu)造函數(shù)也是這一時(shí)期被執(zhí)行的,全局析構(gòu)函數(shù)是main之后執(zhí)行的。
Linux一般程序的入口是__start函數(shù),程序有兩個(gè)相關(guān)的段:
init段:進(jìn)程的初始化代碼,一個(gè)程序開(kāi)始運(yùn)行時(shí),在main函數(shù)調(diào)用之前,會(huì)先運(yùn)行.init段中的代碼。
fini段:進(jìn)程終止代碼,當(dāng)main函數(shù)正常退出后,glibc會(huì)安排執(zhí)行該段代碼。
如何指定程序入口
在ld鏈接過(guò)程中使用-e參數(shù)可以指定程序入口,由于一段簡(jiǎn)短的printf函數(shù)其實(shí)都依賴了好多個(gè)鏈接庫(kù),我們也不太方便使用鏈接腳本將目標(biāo)文件與所有這些依賴庫(kù)進(jìn)行鏈接,所以使用下面這段內(nèi)嵌匯編的程序來(lái)打印一段字符串,這段程序不依賴任何鏈接庫(kù)就可以打印出字符串內(nèi)容,讀者如果不懂其中的含義也不用擔(dān)心,只需要了解下面介紹的鏈接知識(shí)就好。
代碼如下:
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的符號(hà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++方式編譯的,所以符號(hào)變成了上面的形式,如果變成了test.c,符號(hào)如下:
~/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ù)符號(hào):
~/test$ ld -static -e nomain -o test test.o~/test$ ./testhello
如何使用自定義鏈接腳本實(shí)現(xiàn)自定義段的功能
在ld鏈接過(guò)程中使用-T參數(shù)可以指定鏈接腳本,通過(guò)ld -verbose可以查看默認(rèn)的鏈接腳本,原文太長(zhǎng),這里簡(jiǎ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_*) }}
這里自定義一個(gè)簡(jiǎn)單的鏈接腳本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)鏈接庫(kù):
ar rcs libxxx.a xx1.o xx2.o 打包靜態(tài)鏈接庫(kù)ar -t libc.a 查看靜態(tài)鏈接庫(kù)里都有什么目標(biāo)文件ar -x libc.a 會(huì)解壓所有的目標(biāo)文件到當(dāng)前目錄gcc --verbose 可以查看整個(gè)編譯鏈接步驟
關(guān)于objdump:
objdump -i 查看本機(jī)目標(biāo)架構(gòu)objdump -f 顯示文件頭信息objdump -d 反匯編程序objdump -t 顯示符號(hào)表入口,每個(gè)目標(biāo)文件都有什么符號(hào)objdump -r 顯示文件的重定位入口,重定位表objdump -x 顯示所有可用的頭信息,等于-a -f -h -r -tobjdump -H 幫助
關(guān)于分析ELF文件格式:
readelf -h 列出文件頭readelf -S 列出每個(gè)段readelf -r 列出重定位表readelf -d 列出動(dòng)態(tài)段
關(guān)于查看目標(biāo)文件符號(hào)信息:
nm -a 顯示所有的符號(hào)nm -D 顯示動(dòng)態(tài)符號(hào)nm -u 僅顯示沒(méi)有定義的外部符號(hào)nm -defined-only 僅顯示定義的符號(hào)
關(guān)于符號(hào)的說(shuō)明:
如果符號(hào)類型是小寫的,表明符號(hào)是局部符號(hào),大寫表示符號(hào)是全局符號(hào)。
A:該符號(hào)的值是絕對(duì)的,在以后的鏈接過(guò)程中,不允許進(jìn)行改變。這樣的符號(hào)值,常常出現(xiàn)在中斷向量表中,例如用符號(hào)來(lái)表示各個(gè)中斷向量函數(shù)在中斷向量表中的位置。
B:該符號(hào)的值出現(xiàn)在.bss段中,未初始化的全局和靜態(tài)變量。
C:該符號(hào)的值在COMMON段中,里面的都是弱符號(hào)。
D:該符號(hào)位于數(shù)據(jù)段中。
I:該符號(hào)對(duì)另一個(gè)符號(hào)的間接引用
N:debug符號(hào)
R:該符號(hào)位于只讀數(shù)據(jù)區(qū)
T:該符號(hào)位于代碼段
U:該符號(hào)在當(dāng)前文件未定義,定義在別的文件中
?:該符號(hào)類型沒(méi)有定義
參考資料
https://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/
《程序員的自我修養(yǎng)》
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問(wèn)題,請(qǐng)聯(lián)系我們,謝謝!





