程序一定要從main函數(shù)開始運(yùn)行嗎?
對(duì)于靜態(tài)鏈接先提出兩個(gè)問題:
對(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.o
test.o: file format elf64-x86-64
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000007 R_X86_64_PC32 .rodata-0x0000000000000004
000000000000000c R_X86_64_PLT32 puts-0x0000000000000004
RELOCATION RECORDS FOR [.eh_frame]:
OFFSET TYPE VALUE
0000000000000020 R_X86_64_PC32 .text
使用nm也可以查看需要重定位的符號(hào):
nm -u test.o
U _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.o
U _GLOBAL_OFFSET_TABLE_
0000000000000000 T main
U printf
現(xiàn)在的程序和庫通常來講都很大,一個(gè)目標(biāo)文件可能包含成百上千個(gè)函數(shù)或變量,當(dāng)需要用到某個(gè)目標(biāo)文件的任意一個(gè)函數(shù)或變量時(shí),就需要把它整個(gè)目標(biāo)文件都鏈接進(jìn)來,也就是說那些沒有用到的函數(shù)也會(huì)被鏈接進(jìn)去,這會(huì)導(dǎo)致鏈接輸出文件變的很大,造成空間浪費(fèi)。
-ffunction-sections
-fdata-sections
可能很多人都會(huì)以為程序都是由main函數(shù)開始執(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è)程序開始運(yùn)行時(shí),在main函數(shù)調(diào)用之前,會(huì)先運(yùn)行.init段中的代碼。
fini段:進(jìn)程終止代碼,當(dāng)main函數(shù)正常退出后,glibc會(huì)安排執(zhí)行該段代碼。
如何指定程序入口
在ld鏈接過程中使用-e參數(shù)可以指定程序入口,由于一段簡(jiǎn)短的printf函數(shù)其實(shí)都依賴了好多個(gè)鏈接庫,我們也不太方便使用鏈接腳本將目標(biāo)文件與所有這些依賴庫進(jìn)行鏈接,所以使用下面這段內(nèi)嵌匯編的程序來打印一段字符串,這段程序不依賴任何鏈接庫就可以打印出字符串內(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.o
0000000000000000 b .bss
0000000000000000 n .comment
0000000000000000 d .data
0000000000000000 d .data.rel.local
0000000000000000 r .eh_frame
0000000000000000 n .note.GNU-stack
0000000000000000 r .rodata
0000000000000000 t .text
0000000000000026 T _Z4exitv
0000000000000000 T _Z5printv
0000000000000039 T _Z6nomainv
0000000000000000 D str
0000000000000000 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.o
0000000000000000 b .bss
0000000000000000 n .comment
0000000000000000 d .data
0000000000000000 d .data.rel.local
0000000000000000 r .eh_frame
0000000000000000 n .note.GNU-stack
0000000000000000 r .rodata
0000000000000000 t .text
0000000000000026 T exit
0000000000000039 T nomain
0000000000000000 T print
0000000000000000 D str
0000000000000000 a test.c
再使用-e指定入口函數(shù)符號(hào):
~/test$ ld -static -e nomain -o test test.o
~/test$ ./test
hello
如何使用自定義鏈接腳本實(shí)現(xiàn)自定義段的功能
在ld鏈接過程中使用-T參數(shù)可以指定鏈接腳本,通過ld -verbose可以查看默認(rèn)的鏈接腳本,原文太長(zhǎng),這里簡(jiǎn)單截取了一部分:
$ ld -verbose
GNU ld (GNU Binutils for Ubuntu) 2.30
Supported emulations:
elf_x86_64
elf32_x86_64
elf_i386
elf_iamcu
i386linux
elf_l1om
elf_k1om
i386pep
i386pe
using 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 copyright
notice 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$ ./test
hello
上面的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 Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .eh_frame PROGBITS 00000000080480b0 000480b0
0000000000000078 0000000000000000 A 0 0 8
[ 2] tinytext PROGBITS 0000000008048128 00048128
0000000000000066 0000000000000000 WAX 0 0 8
[ 3] .shstrtab STRTAB 0000000000000000 0004826e
000000000000002e 0000000000000000 0 0 1
[ 4] .symtab SYMTAB 0000000000000000 00048190
00000000000000c0 0000000000000018 5 4 8
[ 5] .strtab STRTAB 0000000000000000 00048250
000000000000001e 0000000000000000 0 0 1
Key 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 會(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 -t
objdump -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 僅顯示沒有定義的外部符號(hào)
nm -defined-only 僅顯示定義的符號(hào)
關(guān)于符號(hào)的說明:
如果符號(hào)類型是小寫的,表明符號(hào)是局部符號(hào),大寫表示符號(hào)是全局符號(hào)。
A:該符號(hào)的值是絕對(duì)的,在以后的鏈接過程中,不允許進(jìn)行改變。這樣的符號(hào)值,常常出現(xiàn)在中斷向量表中,例如用符號(hào)來表示各個(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)類型沒有定義
參考資料
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),如有問題,請(qǐng)聯(lián)系我們,謝謝!