程序一定要从main函数开始运行吗?
对于静态链接先提出两个问题:
对于那些需要重定位的符号,都会放在重定位表里,也叫重定位段,即.rel.data、.rel.text等,如果.text段有被重定位的地方,就有.rel.text段,如果.data段有被重定位的地方,就有.rel.data段。
可以使用objdump查看目标文件的重定位表。
源代码:
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也可以查看需要重定位的符号:
nm -u test.o
U _GLOBAL_OFFSET_TABLE_
U puts
对于UND类型,这种未定义的符号都是因为该目标文件中有关于他们的重定位项,在链接器扫描完所有的输入目标文件后,所有这种未定义的符号都应该能在全局符号表中找到,否则报符号未定义错误。
注意:我们代码里明明用的是printf,为什么它却引用了puts的符号呢,因为编译器默认情况下会把只用一个字符串参数的printf替换成puts, 可以节省格式解析的时间,使用-fno-builtin会关闭这个内置函数优化选项,如下:
~/test$ gcc -c -fno-builtin testlink.cc -o test.o
~/test$ nm test.o
U _GLOBAL_OFFSET_TABLE_
0000000000000000 T main
U printf
现在的程序和库通常来讲都很大,一个目标文件可能包含成百上千个函数或变量,当需要用到某个目标文件的任意一个函数或变量时,就需要把它整个目标文件都链接进来,也就是说那些没有用到的函数也会被链接进去,这会导致链接输出文件变的很大,造成空间浪费。
-ffunction-sections
-fdata-sections
可能很多人都会以为程序都是由main函数开始执行和结束的,但其实不是,在main函数调用之前,为了保证程序可以顺利进行,要先初始化进程执行环境,如堆分配初始化、线程子系统等,C++的全局对象构造函数也是这一时期被执行的,全局析构函数是main之后执行的。
Linux一般程序的入口是__start函数,程序有两个相关的段:
init段:进程的初始化代码,一个程序开始运行时,在main函数调用之前,会先运行.init段中的代码。
fini段:进程终止代码,当main函数正常退出后,glibc会安排执行该段代码。
如何指定程序入口
在ld链接过程中使用-e参数可以指定程序入口,由于一段简短的printf函数其实都依赖了好多个链接库,我们也不太方便使用链接脚本将目标文件与所有这些依赖库进行链接,所以使用下面这段内嵌汇编的程序来打印一段字符串,这段程序不依赖任何链接库就可以打印出字符串内容,读者如果不懂其中的含义也不用担心,只需要了解下面介绍的链接知识就好。
代码如下:
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();
}
使用如下命令生成目标文件:
gcc -c -fno-builtin test.cc
看下输出的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 _Z4exitv
0000000000000000 T _Z5printv
0000000000000039 T _Z6nomainv
0000000000000000 D str
0000000000000000 a test.cc
这里由于我的源文件是.cc结尾,所以是以c++方式编译的,所以符号变成了上面的形式,如果变成了test.c,符号如下:
~/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指定入口函数符号:
~/test$ ld -static -e nomain -o test test.o
~/test$ ./test
hello
如何使用自定义链接脚本实现自定义段的功能
在ld链接过程中使用-T参数可以指定链接脚本,通过ld -verbose可以查看默认的链接脚本,原文太长,这里简单截取了一部分:
$ 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_*) }
}
这里自定义一个简单的链接脚本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段的内容都合并到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)
工具小贴士
关于静态链接库:
ar rcs libxxx.a xx1.o xx2.o 打包静态链接库
ar -t libc.a 查看静态链接库里都有什么目标文件
ar -x libc.a 会解压所有的目标文件到当前目录
gcc --verbose 可以查看整个编译链接步骤
关于objdump:
objdump -i 查看本机目标架构
objdump -f 显示文件头信息
objdump -d 反汇编程序
objdump -t 显示符号表入口,每个目标文件都有什么符号
objdump -r 显示文件的重定位入口,重定位表
objdump -x 显示所有可用的头信息,等于-a -f -h -r -t
objdump -H 帮助
关于分析ELF文件格式:
readelf -h 列出文件头
readelf -S 列出每个段
readelf -r 列出重定位表
readelf -d 列出动态段
关于查看目标文件符号信息:
nm -a 显示所有的符号
nm -D 显示动态符号
nm -u 仅显示没有定义的外部符号
nm -defined-only 仅显示定义的符号
关于符号的说明:
如果符号类型是小写的,表明符号是局部符号,大写表示符号是全局符号。
A:该符号的值是绝对的,在以后的链接过程中,不允许进行改变。这样的符号值,常常出现在中断向量表中,例如用符号来表示各个中断向量函数在中断向量表中的位置。
B:该符号的值出现在.bss段中,未初始化的全局和静态变量。
C:该符号的值在COMMON段中,里面的都是弱符号。
D:该符号位于数据段中。
I:该符号对另一个符号的间接引用
N:debug符号
R:该符号位于只读数据区
T:该符号位于代码段
U:该符号在当前文件未定义,定义在别的文件中
?:该符号类型没有定义
参考资料
https://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/
《程序员的自我修养》