程序一定要从main函数开始运行吗?

编码之外

共 8072字,需浏览 17分钟

 ·

2021-01-21 16:25


对于静态链接先提出两个问题:


Q:

每个目标文件都有好多个段,目标文件在被链接成可执行文件时,输入目标文件中的各个段如何被合并到输出文件?


A:

合并相似的段,将所有的.text段合并到输出文件的.text段,将所有的.data段合并到输出文件的.data段。




Q:

链接器如何为他们分配在输出文件中的空间和地址?


A:

这里涉及到程序链接的两个步骤:


  1. 空间与地址分配:扫描所有的输入目标文件,获得它们每个段的长度属性和位置,收集输入目标文件中的符号表中的所有符号定义和符号引用,统一放到一个全局符号表中,合并所有的段,计算出输出文件中各个段合并后的长度和位置,并建立映射关系。


  2. 符号解析与重定位:使用第一步收集到的所有信息,读取输入文件中段的数据及重定位信息,进行符号解析和重定位,调整代码中的地址,将每个段中需要重定位的指令和数据进行“修补”,使他们都指向正确的位置。




Tips:



外部符号指的是目标文件需要引用的符号,但是定义在其它目标文件中,链接前外部符号地址都是000000之类,链接后的可执行文件就可以看见这些外部符号都是有地址的。链接就是把相似的段放在一起,先找到段的偏移地址,再找出符号在段中的偏移,这样可以确定符号在整个可执行程序中的地址。


对于那些需要重定位的符号,都会放在重定位表里,也叫重定位段,即.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 VALUE0000000000000007 R_X86_64_PC32 .rodata-0x0000000000000004000000000000000c R_X86_64_PLT32 puts-0x0000000000000004

RELOCATION RECORDS FOR [.eh_frame]:OFFSET TYPE VALUE0000000000000020 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


Tips:



现在的程序和库通常来讲都很大,一个目标文件可能包含成百上千个函数或变量,当需要用到某个目标文件的任意一个函数或变量时,就需要把它整个目标文件都链接进来,也就是说那些没有用到的函数也会被链接进去,这会导致链接输出文件变的很大,造成空间浪费。



有一个编译选项叫函数级别链接,可以使得某个函数或变量单独保存在一个段里面,都链接器需要用到某个函数时,就将它合并到输出文件中,对于没用到的函数则将他们抛弃,减少空间浪费,但这会减慢编译和链接过程,GCC编译器的编译选项是:
-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.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结尾,所以是以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指定入口函数符号:

~/test$ ld -static -e nomain -o test test.o~/test$ ./testhello

如何使用自定义链接脚本实现自定义段的功能
在ld链接过程中使用-T参数可以指定链接脚本,通过ld -verbose可以查看默认的链接脚本,原文太长,这里简单截取了一部分:

$ ld -verboseGNU 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   i386peusing 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$ ./testhello

上面的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 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)


工具小贴士

关于静态链接库:

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 -tobjdump -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/

《程序员的自我修养》

浏览 21
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报