程序员的自我修养 您所在的位置:网站首页 angular动态编译html 程序员的自我修养

程序员的自我修养

2023-03-26 20:10| 来源: 网络整理| 查看: 265

目录常见场景编译过程ELF文件格式.text Section.data和.rodat Section.symtab Section.rel.text Section静态链接为什么需要链接?链接后达到什么效果?怎么链接?符号重定位依赖重定位表+符号表动态链接静态链接VS动态链接动态链接效果静态共享库动态共享库

常见场景

你是在工作中遇到如下问题或者疑问:

undefined reference to “function”。链接过程中出现未定义引用。 .a和.so文件分别是什么?什么情况下使用? extern "C"有什么作用? 等等... 编译过程

我们平时编译,如果没有加任何编译参数将默认执行预处理,编译,汇编,链接等步骤。

ELF文件格式

每一个cpp文件会生成一个.o文件。.o文件里面有什么信息?多个.o文件如何合并成一个可执行文件。可执行文件的文件里有都有什么信息? 看下下面的例子:

int global_init_var = 84; int global_uninit_var; void func1(int i) { printf("%d\n", i); } int main() { static int static_var = 85; static int static_var2; int a = 1; int b; func1(static_var + static_var2 + a + b); return 0; }

为了探究.o文件内容,只编译不链接gcc -c whats_in_elf.c -o whats_in_elf.o ELF可以用objdump,readelf等工具查看内容。这里用readelf -S whats_in_elf.o查看section headers:

# daihaonan link_load $ readelf -S whats_in_elf.o There are 11 section headers, starting at offset 0x114: Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .text PROGBITS 00000000 000034 000051 00 AX 0 0 4 [ 2] .rel.text REL 00000000 000424 000028 08 9 1 4 [ 3] .data PROGBITS 00000000 000088 000008 00 WA 0 0 4 [ 4] .bss NOBITS 00000000 000090 000004 00 WA 0 0 4 [ 5] .rodata PROGBITS 00000000 000090 000004 00 A 0 0 1 [ 6] .comment PROGBITS 00000000 000094 00002d 01 MS 0 0 1 [ 7] .note.GNU-stack PROGBITS 00000000 0000c1 000000 00 0 0 1 [ 8] .shstrtab STRTAB 00000000 0000c1 000051 00 0 0 1 [ 9] .symtab SYMTAB 00000000 0002cc 0000f0 10 10 10 4 [10] .strtab STRTAB 00000000 0003bc 000065 00 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) I (info), L (link order), G (group), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific)

可以看到.o文件由很多section组成,每个section都有size, file off等描述其在文件内位置的属性。元信息记录在File header中,其中有e_shoff字段指向Section Header Table,Section Header Table是个数组结构保存每个Section信息。 查看Header:

.o文件总体格式如下:

当然还有很多其它section,.text,.data,.rodata,.symtab,.rel.text段是最主要的段,分别保存代码信息,全局数据,全局只读数据,符号表,代码段重定位表。

.text Section

将代码反汇编objdump -s -d whats_in_elf.o

# daihaonan link_load $ objdump -d whats_in_elf.o whats_in_elf.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 : 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 4: 48 83 ec 10 sub $0x10,%rsp 8: 89 7d fc mov %edi,-0x4(%rbp) b: 8b 45 fc mov -0x4(%rbp),%eax e: 89 c6 mov %eax,%esi 10: bf 00 00 00 00 mov $0x0,%edi 15: b8 00 00 00 00 mov $0x0,%eax 1a: e8 00 00 00 00 callq 1f 1f: c9 leaveq 20: c3 retq 0000000000000021 : 21: 55 push %rbp 22: 48 89 e5 mov %rsp,%rbp 25: 48 83 ec 10 sub $0x10,%rsp 29: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp) 30: 8b 15 00 00 00 00 mov 0x0(%rip),%edx # 36 36: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 3c 3c: 8d 04 02 lea (%rdx,%rax,1),%eax 3f: 03 45 f8 add -0x8(%rbp),%eax 42: 03 45 fc add -0x4(%rbp),%eax 45: 89 c7 mov %eax,%edi 47: e8 00 00 00 00 callq 4c 4c: b8 00 00 00 00 mov $0x0,%eax 51: c9 leaveq 52: c3 retq

可以看到func1和main两个函数的反汇编代码。 顺便可以了解下gcc函数调用约定。

规则如下:

执行call指令前,函数调用者将参数入栈,按照函数列表从右到左的顺序入栈。 call指令会自动将当前eip入栈,ret指令将自动从栈中弹出该值到eip寄存器。 被调用函数负责:将ebp入栈,esp的值赋给ebp。所以反汇编一个函数会发现开头两个指令都是push %ebp, mov %esp,%ebp 一个例子: .data和.rodat Section Contents of section .data: 0000 54000000 55000000 T...U... Contents of section .rodata: 0000 25640a00 %d..

可以看到.data Section有8个字节,分别是0x54和0x55对应全局变量static_var和global_init_var。 .rodata Section只有4个字节保存%d\n三个字符。

从这里可以直观看到全局有初值的变量是会在ELF文件中分配空间的,而a,b这种栈上分配的变量不会ELF文件中分配空间,只会在运行到该函数的是在栈上动态分配。

.symtab Section

可以用readelf -s whats_in_elf.o查看符号表

# daihaonan link_load $ readelf -s whats_in_elf.o Symbol table '.symtab' contains 16 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS whats_in_elf.c 2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 3: 0000000000000000 0 SECTION LOCAL DEFAULT 3 4: 0000000000000000 0 SECTION LOCAL DEFAULT 4 5: 0000000000000000 0 SECTION LOCAL DEFAULT 5 6: 0000000000000004 4 OBJECT LOCAL DEFAULT 3 static_var.1600 7: 0000000000000000 4 OBJECT LOCAL DEFAULT 4 static_var2.1601 8: 0000000000000000 0 SECTION LOCAL DEFAULT 7 9: 0000000000000000 0 SECTION LOCAL DEFAULT 8 10: 0000000000000000 0 SECTION LOCAL DEFAULT 6 11: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 global_init_var 12: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM global_uninit_var 13: 0000000000000000 33 FUNC GLOBAL DEFAULT 1 func1 14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf 15: 0000000000000021 50 FUNC GLOBAL DEFAULT 1 main

从上面可以得到如下信息:

该.o文件中有static_var.1600,static_var2.1601,global_init_var,global_uninit_var,func1,printf,main等符号 每个符号在.o文件中的位置,比如func1,Ndx是1,对应.text Section,Value为0,Size为33,说明func1从.text Section起始字节开始,占了33个字节。 printf这个符号在.o文件中并没有定义,所以它的Ndx是UND

用g++ whats_in_elf.c -o whats_in_elf2.o重新编译,会发现

# daihaonan link_load $ readelf -s whats_in_elf2.o Symbol table '.symtab' contains 17 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS whats_in_elf.c 2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 3: 0000000000000000 0 SECTION LOCAL DEFAULT 3 4: 0000000000000000 0 SECTION LOCAL DEFAULT 4 5: 0000000000000000 0 SECTION LOCAL DEFAULT 5 6: 0000000000000004 4 OBJECT LOCAL DEFAULT 3 _ZZ4mainE10static_var 7: 0000000000000004 4 OBJECT LOCAL DEFAULT 4 _ZZ4mainE11static_var2 8: 0000000000000000 0 SECTION LOCAL DEFAULT 7 9: 0000000000000000 0 SECTION LOCAL DEFAULT 8 10: 0000000000000000 0 SECTION LOCAL DEFAULT 6 11: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 global_init_var 12: 0000000000000000 4 OBJECT GLOBAL DEFAULT 4 global_uninit_var 13: 0000000000000000 33 FUNC GLOBAL DEFAULT 1 _Z5func1i 14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND __gxx_personality_v0 15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _Z6printfPKcz 16: 0000000000000021 50 FUNC GLOBAL DEFAULT 1 main

原来的func1变成了_Z5func1i,为了防止符号冲突,C++引入了符号修饰的概念。 所以在C++里如果希望动态库中某个函数能被正确加载,需要加上extern "C"方式符号被修饰,比如:

extern "C" { ProcessorBase* create_processor(const std::string& processor_name) { ... } }

加载该符号的地方才能正确找到create_processor这个符号。(PFUNC_CREATE_PROCESSOR_CALL)dlsym(handle,"create_processor");

.rel.text Section

对于可重定位的ELF文件,必须包含重定位Section,一个ELF文件中可能有多个重定位Section,比如.text有需要重定位的地方,那么会有一个.rel.text表,详细见下文。

静态链接 为什么需要链接?

考虑如下程序: a.c

extern int shared; int main() { int a = 100; swap(&a, &shared); }

b.c

int shared = 1; void swap(int* a, int* b) { *a ^= *b ^= *a ^= *b; }

分别将a.c和b.c进行编译,然后查看代码段反汇编。

# daihaonan link_load $ objdump -d a.o a.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 : 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 4: 48 83 ec 10 sub $0x10,%rsp 8: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp) f: 8b 15 00 00 00 00 mov 0x0(%rip),%edx # 15 15: 8b 45 fc mov -0x4(%rbp),%eax 18: 89 d6 mov %edx,%esi 1a: 89 c7 mov %eax,%edi 1c: b8 00 00 00 00 mov $0x0,%eax 21: e8 00 00 00 00 callq 26 26: c9 leaveq 27: c3 retq

main中会引用全局变量shared,调用swap函数,但是shared和swap都不是定义在a.o中的,而是定义在b.o中。所以a.o中对shared的引用为0x0(%rip),%rip寄存器中保存的是当前执行指令的地址,对swap调用为e8 00 00 00 00,这是一条近址相对位移调用指令,e8是指令码,00 00 00 00是操作数,也就是被调用函数相对于调用指令的下一条指令的偏移量。这里因为不知道swap函数在哪,所以暂时用00 00 00 00来代替。

所以我们可以得出链接的一个主要作用是对一些全局变量,函数引用指令进行修正。

链接后达到什么效果?

将a.o和b.o链接在一起。ld a.o b.o -e main -o ab 然后再来看下ab中main的反汇编代码

# daihaonan link_load $ objdump -S ab ab: file format elf64-x86-64 Disassembly of section .text: 00000000004000e8 : 4000e8: 55 push %rbp 4000e9: 48 89 e5 mov %rsp,%rbp 4000ec: 48 83 ec 10 sub $0x10,%rsp 4000f0: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp) 4000f7: 8b 15 bb 00 20 00 mov 0x2000bb(%rip),%edx # 6001b8 4000fd: 8b 45 fc mov -0x4(%rbp),%eax 400100: 89 d6 mov %edx,%esi 400102: 89 c7 mov %eax,%edi 400104: b8 00 00 00 00 mov $0x0,%eax 400109: e8 02 00 00 00 callq 400110 40010e: c9 leaveq 40010f: c3 retq

链接后再来看main函数的反汇编代码。有三个地方变动了mov 0x0(%rip),%edx变成了mov 0x2000bb(%rip),%edx,e8 00 00 00 00变成了e8 02 00 00 00。最左侧的地址变成了全局的虚拟地址,这说明链接还会分配虚拟地址空间,链接结束,每个函数,每个全局变量在虚拟地址空间内的地址就确定了。 callq下一条指令地址为0x40010e再加上0x02,等于0x400110。所以swap函数代码起始地址应该是0x400110。用objdump -S ab来验证下。

0000000000400110 : 400110: 55 push %rbp 400111: 48 89 e5 mov %rsp,%rbp 400114: 53 push %rbx 400115: 48 89 7d f0 mov %rdi,-0x10(%rbp) 400119: 48 89 75 e8 mov %rsi,-0x18(%rbp) 40011d: 48 8b 45 f0 mov -0x10(%rbp),%rax 400121: 8b 10 mov (%rax),%edx 400123: 48 8b 45 e8 mov -0x18(%rbp),%rax 400127: 8b 08 mov (%rax),%ecx 400129: 48 8b 45 f0 mov -0x10(%rbp),%rax 40012d: 8b 18 mov (%rax),%ebx 40012f: 48 8b 45 e8 mov -0x18(%rbp),%rax 400133: 8b 00 mov (%rax),%eax 400135: 31 c3 xor %eax,%ebx 400137: 48 8b 45 f0 mov -0x10(%rbp),%rax 40013b: 89 18 mov %ebx,(%rax) 40013d: 48 8b 45 f0 mov -0x10(%rbp),%rax 400141: 8b 00 mov (%rax),%eax 400143: 31 c1 xor %eax,%ecx 400145: 48 8b 45 e8 mov -0x18(%rbp),%rax 400149: 89 08 mov %ecx,(%rax) 40014b: 48 8b 45 e8 mov -0x18(%rbp),%rax 40014f: 8b 00 mov (%rax),%eax 400151: 31 c2 xor %eax,%edx 400153: 48 8b 45 f0 mov -0x10(%rbp),%rax 400157: 89 10 mov %edx,(%rax) 400159: 5b pop %rbx 40015a: c9 leaveq 40015b: c3 retq

果然swap起始地址是0x400110。

a.o+b.o到ab的过程大致如下图:

第一步对a.o和b.o相同的Section进行并合。 第二步将ab映射到进行虚拟地址空间,并确定各符号在进行虚拟地址空间中的地址。 第三步修正各符号引用,使其指向符号最终的地址。

怎么链接?

链接一般分为两步:

空间和地址分配。扫码所有输入目标文件,搜集符号定义和引用,放到全局符号表,并对Section进行并合。 符号解析和重定位。 符号重定位依赖重定位表+符号表 # daihaonan link_load $ objdump -r a.o a.o: file format elf64-x86-64 RELOCATION RECORDS FOR [.text]: OFFSET TYPE VALUE 0000000000000011 R_X86_64_PC32 shared-0x0000000000000004 0000000000000022 R_X86_64_PC32 swap-0x0000000000000004

重定位表中记录了哪些地方需要修正,这里可以看到.text的0x11偏移处引用了shared变量,所以需要修正,.text的0x22偏移处引用了swap函数,也需要修正, 而.symtab Section记录了符号所在的位置。

# daihaonan link_load $ readelf -s b.o Symbol table '.symtab' contains 10 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS b.c 2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 3: 0000000000000000 0 SECTION LOCAL DEFAULT 2 4: 0000000000000000 0 SECTION LOCAL DEFAULT 3 5: 0000000000000000 0 SECTION LOCAL DEFAULT 5 6: 0000000000000000 0 SECTION LOCAL DEFAULT 6 7: 0000000000000000 0 SECTION LOCAL DEFAULT 4 8: 0000000000000000 4 OBJECT GLOBAL DEFAULT 2 shared 9: 0000000000000000 76 FUNC GLOBAL DEFAULT 1 swap

链接器有了这俩信息,可以轻而易举完成符号重定位。

动态链接 静态链接VS动态链接

动态链接优点:

方便发布。模块A依赖模块B,如果模块B实现发生了改变,在静态链接的情况下,模块A需要重新编译。 内存占用。模块A和模块B都依赖模块C中的某个函数func,在果静态链接的情况下,模块A/B同时运行时,func需要在内存中存在两份。

动态链接缺点:

执行效率不如静态链接高。 动态链接效果 静态共享库

如图假设A.so又依赖B.so中的a变量和foo函数,当调用foo的时候,动态链接器会将B.so加载到内存load_address处,foo在B.so内是固定的y字节偏移出。所以foo在进程内的虚拟地址就是load_address+y。然后动态链接器修改A.so中call foo指令出代码,将foo地址修改为load_address+y。至此动态链接完成。和静态链接的区别在于动态链接将地址重定位推迟到了运行时。

动态共享库

上面这种静态共享库有个问题,就是指令部分没法在多个进程之间共享,从而失去了节省内存的优点。 假设有两个进程,做的事情都是A.so中调用B.so中的foo函数和引用a变量。 进程1A.so被加载到a0虚拟地址,进程2中A.so被动态加载到a1虚拟地址,静态共享库的虚拟内存分布如下:

A.so中的代码会被重定位,并且重定位值不一样,进程1中a变量在虚拟地址load_address1+x处,而在进程2中a变量在虚拟地址load_address2+x处。所以A.so的代码在内存中需要保存多份。

如果我们把需要重定位的地方单独抽出来放到数据区,这样a变量被加载到哪个地址,代码部分都不需要变动,那么两个进程可以只在物理内存中加载一份代码。使用这种机制的共享库叫做动态共享库。 相同的动态共享库的虚拟内存分布如下:

这种模式下,代码中需要被重定位的地方被放到了GOT中,动态加载重定位的时候只需要修改GOT就可以了,代码部分不需要被修改。缺点也很明显就是多了一层索引。

这就是-fPIC链接选项的作用。该链接选项指定生成的动态库为动态共享库。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有