程序运行之静态链接一
Posted 一张红枫叶
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了程序运行之静态链接一相关的知识,希望对你有一定的参考价值。
前面介绍了单个.o文件的格式以及里面的内容。那么如果我们有多个目标文件,如何将它们链接成一个可执行的文件呢。多个目标文件就涉及到了链接。我们首先介绍静态链接。
有如下2个文件,test.c/test1.c
test.c
#include<stdlib.h>
#include<stdio.h>
extern int shared;
int main(){
int a=100;
swap(&a,&shared);
}
test1.c
#include <stdio.h>
int shared=1;
void swap(int *a,int *b){
int temp;
temp=*a;
*a=*b;
*b=temp;
}
首先我们通过gcc分别得到2个文件的.o文件。 gcc -c test.c test1.c
从代码中可以看到,test1.c总共定义了两个全局符号,一个是变量shared,一个是函数swap。test.c里面引用到了test1.c中的shared和swap。接下里的工作就是要把test.o和test1.o这两个目标文件链接在一起形成一个可执行的文件。
前面介绍过每个目标文件都有代码段,数据段等等。那么多个目标文件链接在一起的话,这些段该如何组合呢。
一个最简单的方法就是叠加,如下图所示,在最终的目标文件中,把所有的目标文件都依次叠加。这样做的确很简单明了。但是会造成一个问题,在有很多输入文件的情况下,输出文件将会有很多零散的段。比如一个规模稍大的工程就有好几百个文件。而且每个文件都有.data,.text.bss段。那么最后输出的文件将会有成百上千个零散的段。这样非常浪费空间,而且每个段都必须要用一定的地址和空间对齐要求。比如X86的硬件,段的装载地址和空间的对齐单位是页,也就是4096字节。如果一个段的长度是1个字节。在内存中就要占用4096字节。多个这样的段将会极大的浪费内存。
那么第二种方法就是相似段合并了。我们把目标文件的.data,.text,.bss段分别整合在一起。如下图所示。
这种链接方法内部分为两个步骤:
第一步: 空间与地址分配
扫描所有的输入目标文件,获得它们的各个段的长度,属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来。统一放到一个全局符号表。这一步中,链接器能够所有输入目标文件的段长度,并且将它们合并,计算出输出文件中各个段合并后的长度与位置,并建立映射关系。
第二步:符号解析与重定位
使用上面收集到的所有信息,读取输入文件中段的数据,重定位消息,并且进行符合解析与重定位,调整代码中的地址等
比如我们将test.o和test1.o链接起来生成一个可执行的文件
gcc test.o test1.o -o ab
首先来看test.o的文件
root@zhf-maple:/home/zhf/c_prj# objdump -h test.o
test.o: 文件格式 elf64-x86-64
节:
Idx Name Size VMA LMA File off Algn
0 .text 00000051 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 0000000000000000 0000000000000000 00000091 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000091 2**0
ALLOC
objdump -h test1.o
test1.o: 文件格式 elf64-x86-64
节:
Idx Name Size VMA LMA File off Algn
0 .text 0000002d 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000004 0000000000000000 0000000000000000 00000070 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000074 2**0
ALLOC
objdump -h ab
ab: 文件格式 elf64-x86-64
节:
Idx Name Size VMA LMA File off Algn
CONTENTS, ALLOC, LOAD, READONLY, CODE
13 .text 00000202 0000000000000560 0000000000000560 00000560 2**4
22 .data 00000014 0000000000201000 0000000000201000 00001000 2**3
CONTENTS, ALLOC, LOAD, DATA
23 .bss 00000004 0000000000201014 0000000000201014 00001014 2**0
ALLOC
24 .comment 00000023 0000000000000000 0000000000000000 00001014 2**0
CONTENTS, READONLY
从上面的结果可以看到,在链接之前,VMA的地址都是0,这是因为虚拟空间还没有分配。等到链接之后,可执行文件ab中的各个段都被分配到了相应的虚拟地址。在ab中.text段被分配到了0000000000000560,大小为0x202字节。.data段从地址0000000000201000开始。大小为14字节。
完成空间和地址的分配步骤以后,链接器就进入了符号解析与重定位的步骤。这也是静态链接的核心。在分析符号解析和重定位之前。我们首先看下test.o里面是怎么使用这两个外部符号的。
objdump -d test.o
test.o: 文件格式 elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
f: 00 00
11: 48 89 45 f8 mov %rax,-0x8(%rbp)
15: 31 c0 xor %eax,%eax
17: c7 45 f4 64 00 00 00 movl $0x64,-0xc(%rbp)
1e: 48 8d 45 f4 lea -0xc(%rbp),%rax
22:48 8d 35 00 00 00 00 lea 0x0(%rip),%rsi # 29 <main+0x29>
29: 48 89 c7 mov %rax,%rdi
2c: b8 00 00 00 00 mov $0x0,%eax
31:e8 00 00 00 00 callq 36 <main+0x36>
36: b8 00 00 00 00 mov $0x0,%eax
3b: 48 8b 55 f8 mov -0x8(%rbp),%rdx
3f: 64 48 33 14 25 28 00 xor %fs:0x28,%rdx
46: 00 00
48: 74 05 je 4f <main+0x4f>
4a: e8 00 00 00 00 callq 4f <main+0x4f>
4f: c9 leaveq
50: c3 retq
上面2个标红加粗的部分分别是引用shared和swap的位置。对于shared的应用是一个lea指令,前面3个字节是指令码,后面4个字节是shared的地址。对于swap的应用是callq指令。前面1个字节是指令码,后面4个字节是swap的调用地址。可以看到shared和swap的地址都是0.这是因为它们定义在其他目标文件中,所以编译器就暂时把地址0看做是shared的地址
再来看下ab.out中的命令
objdump -d ab
000000000000066a <main>:
66a: 55 push %rbp
66b: 48 89 e5 mov %rsp,%rbp
66e: 48 83 ec 10 sub $0x10,%rsp
672: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
679: 00 00
67b: 48 89 45 f8 mov %rax,-0x8(%rbp)
67f: 31 c0 xor %eax,%eax
681: c7 45 f4 64 00 00 00 movl $0x64,-0xc(%rbp)
688: 48 8d 45 f4 lea -0xc(%rbp),%rax
68c:48 8d 35 7d 09 20 00 lea 0x20097d(%rip),%rsi # 201010 <shared>
693: 48 89 c7 mov %rax,%rdi
696: b8 00 00 00 00 mov $0x0,%eax
69b:e8 1b 00 00 00 callq 6bb <swap>
6a0: b8 00 00 00 00 mov $0x0,%eax
6a5: 48 8b 55 f8 mov -0x8(%rbp),%rdx
6a9: 64 48 33 14 25 28 00 xor %fs:0x28,%rdx
6b0: 00 00
6b2: 74 05 je 6b9 <main+0x4f>
6b4: e8 87 fe ff ff callq 540 <__stack_chk_fail@plt>
6b9: c9 leaveq
6ba: c3 retq
可以看到main函数中shared和swap都已经被修正到正确的位置。经过修正以后,shared和swap的位置分别是0x20097d和6bb
那么链接器是如何知道那些指令是需要被调整的呢。在ELF文件中, 有一个叫重定位表的结构专门用来保存这些与重定位相关的信息。我们使用objdump来查看目标文件的重定位表。
root@zhf-maple:/home/zhf/c_prj# objdump -r test.o
test.o: 文件格式 elf64-x86-64
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000025 R_X86_64_PC32 shared-0x0000000000000004
0000000000000032 R_X86_64_PLT32 swap-0x0000000000000004
RELOCATION RECORDS FOR [.text] 表示这个重定位是代码段的重定位表。0x25和0x32分别是lea指令和callq指令的地址部分。具体的地址寻址方式需要查看对应的指令手册,这里就不再介绍
以上是关于程序运行之静态链接一的主要内容,如果未能解决你的问题,请参考以下文章