通常程序的编译中,或多或少会调用其它库中的函数接口,本篇blog就是讲静态库的调用流程。通常我们知道编译一个可执行程序会有这四个过程:预处理、编译、汇编以及链接。前面三步就是产生目标文件.o的过程,链接就是把各个.o文件粘在一起,构成一个可执行文件。而链接主要分为两步:第一是空间和地址的分配,第二是符号解析与重定位
1.空间和地址的分配
每个.o文件都有自己的段属性,比如.text、.data等等这些,链接的第一步就是将这些段属性合并在一起。下面将以a.c和b.c为示范说明链接中空间和地址的分配。
/*a.c*/
extern int shared;
int main(void)
{
int a = 100;
swap(&a, &shared);
return 0;
}
/*b.c*/
int shared = 1;
void swap(int* a, int* b)
{
*a ^= *b ^= *a ^= *b;
}
1.1 相似段的合并
使用objdump -h 来查看每个.o文件的段属性,并且使用链接器ld将a.o和b.o合并成可执行文件ab:ld a.o b.o -e main -o ab,采用相似段合并的方法,这里主要看.text和.data的段,.text一般是程序代码,.data是数据存放。从下面可以看出:.text ab(size 0x66)=a.o(size 0x2c) + b.o(size 0x3a) .data ab(size 0x4)=a.o(size 0x0) + b.o(size 0x4)
a.o: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000002c 00000000 00000000 00000034 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 00000000 00000000 00000060 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 00000000 00000000 00000060 2**0
ALLOC
3 .comment 0000002c 00000000 00000000 00000060 2**0
CONTENTS, READONLY
4 .note.GNU-stack 00000000 00000000 00000000 0000008c 2**0
CONTENTS, READONLY
5 .eh_frame 00000038 00000000 00000000 0000008c 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
b.o: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000003a 00000000 00000000 00000034 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000004 00000000 00000000 00000070 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 00000000 00000000 00000074 2**0
ALLOC
3 .comment 0000002c 00000000 00000000 00000074 2**0
CONTENTS, READONLY
4 .note.GNU-stack 00000000 00000000 00000000 000000a0 2**0
CONTENTS, READONLY
5 .eh_frame 0000003c 00000000 00000000 000000a0 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
ab: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000066 08048094 08048094 00000094 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .eh_frame 0000005c 080480fc 080480fc 000000fc 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .data 00000004 08049158 08049158 00000158 2**2
CONTENTS, ALLOC, LOAD, DATA
3 .comment 0000002b 00000000 00000000 0000015c 2**0
CONTENTS, READONLY
1.2 符号地址的确定
在上面的段属性合并之后,那么符号地址也将差不多确定好了,也就是上面的VMA(Virtual Memory Address)。这里为啥可以将main函数和swap的地址区分开来了?这是因为编译中以main为程序的执行入口,所以main的地址比swap要先,又根据前面目标文件中的函数的大小,可以将main和swap的地址确定下来。
符号 |
类型 |
虚拟地址 |
main |
函数 |
0x08048094 |
swap |
函数 |
0x080480c0 |
shared |
变量 |
0x08048158 |
2.符号解析与重定位
2.1重定位
前面知道a.c中引用了b.c中的函数,那么肯定在链接过程中需要将a.o中的swap和shared重定位的。先看a.o文件的反汇编objdump -d a.o ,可以看出在11处,使用shared的变量,在20处引用外部函数swap。本来是想shared的值赋值到寄存器%esp中,但是不知道shared的地址,所以使用0x00000000来代替。同理,在20处调用swap,可以看出e8 fc ff ff ff,其中e8是操作码,是一个近址相对位移调用指令,也就是下一条指令的地址0x25+0xfffffffc(-4)=0x21。
在ab中可以看出,这两处的地址都被修正了,重定位到相应的地址上去了。
a.o: file format elf32-i386
Disassembly of section .text:
00000000 <main>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 e4 f0 and $0xfffffff0,%esp
6: 83 ec 20 sub $0x20,%esp
9: c7 44 24 1c 64 00 00 movl $0x64,0x1c(%esp)
10: 00
11: c7 44 24 04 00 00 00 movl $0x0,0x4(%esp)
18: 00
19: 8d 44 24 1c lea 0x1c(%esp),%eax
1d: 89 04 24 mov %eax,(%esp)
20: e8 fc ff ff ff call 21 <main+0x21>
25: b8 00 00 00 00 mov $0x0,%eax
2a: c9
ab: file format elf32-i386
Disassembly of section .text:
08048094 <main>:
8048094: 55 push %ebp
8048095: 89 e5 mov %esp,%ebp
8048097: 83 e4 f0 and $0xfffffff0,%esp
804809a: 83 ec 20 sub $0x20,%esp
804809d: c7 44 24 1c 64 00 00 movl $0x64,0x1c(%esp)
80480a4: 00
80480a5: c7 44 24 04 58 91 04 movl $0x8049158,0x4(%esp)
80480ac: 08
80480ad: 8d 44 24 1c lea 0x1c(%esp),%eax
80480b1: 89 04 24 mov %eax,(%esp)
80480b4: e8 07 00 00 00 call 80480c0 <swap>
80480b9: b8 00 00 00 00 mov $0x0,%eax
80480be: c9 leave
80480bf: c3 ret
2.2 重定位表
那么在合并ab时候,怎么会知道那个文件中需要重定位了?这个时候,需要重定位的文件中会有重定位表,使用如下的命令来查看重定位表:objdump -r a.o。其中R_386_32是表示绝对地址修正,R_386_PC32是表示相对地址修正。
a.o: file format elf32-i386
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
00000015 R_386_32 shared
00000021 R_386_PC32 swap
其实我们也可以通过符号表来查看哪些函数或者变量在本文件中没有定义:readelf -s a.o,比如奥看下表就知道其中的shared和swap没有在a.o中定义,需要重定。位到其他的文件中
Symbol table '.symtab' contains 11 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FILE LOCAL DEFAULT ABS a.c
2: 00000000 0 SECTION LOCAL DEFAULT 1
3: 00000000 0 SECTION LOCAL DEFAULT 3
4: 00000000 0 SECTION LOCAL DEFAULT 4
5: 00000000 0 SECTION LOCAL DEFAULT 6
6: 00000000 0 SECTION LOCAL DEFAULT 7
7: 00000000 0 SECTION LOCAL DEFAULT 5
8: 00000000 44 FUNC GLOBAL DEFAULT 1 main
9: 00000000 0 NOTYPE GLOBAL DEFAULT UND shared
10: 00000000 0 NOTYPE GLOBAL DEFAULT UND swap
3.链接脚本
一般程序的运行时在操作系统中运行,那么可以使用编译器中默认的链接脚本,一般放在/usr/lib/ldscripts/下。但是想kernel、BootLoader等这些程序怎么办,因为他们本身就是操作系统,所在这些程序的编译需要指定链接脚本,当然,自己编译一些程序的时候,也可以不使用编译器提供的链接脚本,而使用自己的,使用如下的命令:ld -T link.script
比如:下面的一个简单的链接脚本,其中ENTRY是表示程序的入口是main函数,后面的SECTIONS命令一般是链接脚本的主体,这个命令指定了各种输入段到输出段的变换,SECTIONS后面紧跟一对大括号,里面包括了SECTIONS的变换规则,其中有三条语句,每条语句一行。第一条是赋值语句,确定当前虚拟地址;第二条是转换规则,将所有输入文件中的名字为.text、.data、.rodata都合并到输出文件的tinytext中;第三条规则是将输入文件中.comment的段都丢弃,不保存到输出文件中。
ENTRY(main)
SECTIONS
{
. = 0x0804800 + SIZEOF_HEADERS;
tinytext : { *(.text) *(.data) *(.rodate) }
/DISCARD/ : { *(.comment) }
}