欢迎访问我的博客新地址:www.heybrt.tk
从helloworld回顾程序的编译过程之三——静态链接1
本文关于静态链接库的链接过程分析是对《程序员的自我修养——链接、装载与库》这本书的一点学习总结,另外,本文是在linux操作系统下进行验证和测试,所使用的测试文件为:main.c和add.c,其内容如下:
---------------文件main.c---------------
- //main.c
- int g_iMainValue = 35;
- double g_dMainValue = 10.25;
- extern int g_iAddValue2;
- int main()
- {
- int iTmpNum1= 13;
- int iTmpNum2= 26;
- iTmpNum1 = iTmpNum1 + iTmpNum2 + g_iMainValue;
- add(iTmpNum1,g_dMainValue,g_iAddValue2);
- return 0;
- }
---------------文件add.c---------------
- //add.c
- int g_iAddValue1 = 50;
- int g_iAddValue2 = 33;
- int add(int iNum1, int iNum2, int iNum3)
- {
- return iNum1 + iNum2 + iNum3 + g_iAddValue1;
- }
1、编译和链接上述两个源文件,
1)将上述两文件编译成目标文件main.o和add.o,可使用如下命令:
# gcc -c add.c main.c
2)将目标文件main.o和add.o链接成可执行文件:test,可使用如下命令:
# ld main.o add.o -e main -o test
其中:-e main表示要将函数main作为程序的入口
2、查看和分析目标文件main.o、add.o和test,这三个都是ELF文件格式的目标文件,这里需要看下它们之间有哪些区别,
1)回顾一下目标文件main.o的内容,使用命令:objdump -s -d main.o
- # objdump -s -d main.o
- main.o: file format elf32-i386
- Contents of section .text:
- 0000 5589e583 e4f083ec 20c74424 1c0d0000 U....... .D$....
- 0010 00c74424 181a0000 008b4424 188b5424 ..D$......D$..T$
- 0020 1c01c2a1 00000000 8d040289 44241ca1 ............D$..
- 0030 00000000 dd050000 00008944 240cdd5c ...........D$..\
- 0040 24048b44 241c8904 24e8fcff ffffb800 $..D$...$.......
- 0050 000000c9 c3 .....
- Contents of section .data:
- 0000 23000000 00000000 00000000 00802440 #.............$@
- Contents of section .comment:
- 0000 00474343 3a202855 62756e74 7520342e .GCC: (Ubuntu 4.
- 0010 342e332d 34756275 6e747535 2e312920 4.3-4ubuntu5.1)
- 0020 342e342e 3300 4.4.3.
- 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 0d 00 00 movl $0xd,0x1c(%esp)
- 10: 00
- 11: c7 44 24 18 1a 00 00 movl $0x1a,0x18(%esp)
- 18: 00
- 19: 8b 44 24 18 mov 0x18(%esp),%eax
- 1d: 8b 54 24 1c mov 0x1c(%esp),%edx
- 21: 01 c2 add %eax,%edx
- 23: a1 00 00 00 00 mov 0x0,%eax
- 28: 8d 04 02 lea (%edx,%eax,1),%eax
- 2b: 89 44 24 1c mov %eax,0x1c(%esp)
- 2f: a1 00 00 00 00 mov 0x0,%eax
- 34: dd 05 00 00 00 00 fldl 0x0
- 3a: 89 44 24 0c mov %eax,0xc(%esp)
- 3e: dd 5c 24 04 fstpl 0x4(%esp)
- 42: 8b 44 24 1c mov 0x1c(%esp),%eax
- 46: 89 04 24 mov %eax,(%esp)
- 49: e8 fc ff ff ff call 4a <main+0x4a>
- 4e: b8 00 00 00 00 mov $0x0,%eax
- 53: c9 leave
- 54: c3 ret
从上面的输出的内容中可以看到:
(1).text段的内容,以及对其反汇编之后生成的汇编代码,留心反汇编之后的第3行,这里2f行的00 00 00 00和49行的fc ff ff ff都是指一个暂时未确定的空地址,后面在3将会对其位置确定进行描述:
- 3: 83 e4 f0 and $0xfffffff0,%esp
(2).data段的内容为:
0000 23000000 00000000 00000000 00802440
最前面的0000为段内偏移量,即main.c中定义的两个全局变量的值:23000000 00000000表示整数35;00000000 00802440表示浮点数10.25,这里需要留心两个问题:
[1] 数据的大小端存储的问题;在本人的机子中数据35表示为:23000000 00000000,即从低到高位:0x23 0x00 0x00 0x00 0x00 0x00 0x00 0x00,这是由于大小端的问题,某些机子可能采用小端的方式存储数据时就会将35表示成 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x23;
[2] 内存对齐的问题,本来只用4个字节的整形数据g_iMainValue却占用了8个字节,高位被补充了8个字节的0;这是因为我们在main.c程序中先定义了4字节整形变量g_iMainValue,后定义了8字节双精度变量g_dMainValue,如果反过来先定义g_dMainValue,后定义g_iMainValue的话上述.data段的内容就会变成下面的形式:
(3)局部变量会在栈中分配内存,因此编译的时候,就没有对main.c中的局部变量iTmpNum1和iTmpNum2分配空间。
2)查看这三个目标文件的段表信息,使用命令:objdump -h文件名
main.o的段表内容为:
- # objdump -h main.o
- main.o: file format elf32-i386
- Sections:
- Idx Name Size VMA LMA File off Algn
- 0 .text 00000055 00000000 00000000 00000034 2**2
- CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
- 1 .data 00000010 00000000 00000000 00000090 2**3
- CONTENTS, ALLOC, LOAD, DATA
- 2 .bss 00000000 00000000 00000000 000000a0 2**2
- ALLOC
- 3 .comment 00000026 00000000 00000000 000000a0 2**0
- CONTENTS, READONLY
- 4 .note.GNU-stack 00000000 00000000 00000000 000000c6 2**0
- CONTENTS, READONLY
可以看到源文件main.c被编译成目标main.o之后,
(1) 目标文件的代码段.text的大小为00000055;
(2) 其虚地址空间(VMA)和加载地址空间(LMA)都是空;
(3) 数据段.data的大小为00000010,即10进制的16,这就是我们在main.c中定义的两个全局变量:8字节double类型变量和8字节的int变量(内存对齐补充了4字节)所占用的总内存大小。
同样步骤也可以看到add.o的段表内容为:
- # objdump -h add.o
- add.o: file format elf32-i386
- Sections:
- Idx Name Size VMA LMA File off Algn
- 0 .text 0000001b 00000000 00000000 00000034 2**2
- CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
- 1 .data 00000008 00000000 00000000 00000050 2**2
- CONTENTS, ALLOC, LOAD, DATA
- 2 .bss 00000000 00000000 00000000 00000058 2**2
- ALLOC
- 3 .comment 00000026 00000000 00000000 00000058 2**0
- CONTENTS, READONLY
- 4 .note.GNU-stack 00000000 00000000 00000000 0000007e 2**0
- CONTENTS, READONLY
test的段表内容为:
- # objdump -h test
- test: file format elf32-i386
- Sections:
- Idx Name Size VMA LMA File off Algn
- 0 .rel.plt 00000000 08048094 08048094 00000094 2**2
- CONTENTS, ALLOC, LOAD, READONLY, DATA
- 1 .plt 00000000 08048094 08048094 00000094 2**2
- CONTENTS, ALLOC, LOAD, READONLY, CODE
- 2 .text 00000071 08048094 08048094 00000094 2**2
- CONTENTS, ALLOC, LOAD, READONLY, CODE
- 3 .got.plt 00000000 08049108 08049108 00000108 2**2
- CONTENTS, ALLOC, LOAD, DATA
- 4 .data 00000018 08049108 08049108 00000108 2**3
- CONTENTS, ALLOC, LOAD, DATA
- 5 .comment 00000025 00000000 00000000 00000120 2**0
- CONTENTS, READONLY
对比分析main.o、add.o和test三个目标文件的段表可以看出三者的关系:
(1)test对应段的是对main.o、add.o对应段的综合,例如:数据段中main.o的.data段大小为00000010,add.o的.data段大小为00000008,而test的.data段的大小为00000018 = 00000010 + 00000008;
(2)test目标文件中虚地址空间(VMA)和加载地址空间(LMA)都已经被填充了地址,而main.o和add.o中的这些地址都是空的。
3)查看三个目标文件的符号表,使用命令:readelf -s目标文件名,分析如下:
main.o的符号表为:
- # readelf -s main.o
- Symbol table '.symtab' contains 12 entries:
- Num: Value Size Type Bind Vis Ndx Name
- 0: 00000000 0 NOTYPE LOCAL DEFAULT UND
- 1: 00000000 0 FILE LOCAL DEFAULT ABS main.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 5
- 7: 00000000 4 OBJECT GLOBAL DEFAULT 3 g_iMainValue
- 8: 00000008 8 OBJECT GLOBAL DEFAULT 3 g_dMainValue
- 9: 00000000 85 FUNC GLOBAL DEFAULT 1 main
- 10: 00000000 0 NOTYPE GLOBAL DEFAULT UND g_iAddValue2
- 11: 00000000 0 NOTYPE GLOBAL DEFAULT UND add
分析上述main.o的符号表,可以看到:
(1) main.o目标文件*有下列符号:g_iMainValue、g_dMainValue、main、g_iAddValue2、add,
(2) 变量符号g_iMainValue和g_dMainValue的为全局变量,类型为OBJECT,g_iMainValue的大小为4,g_dMainValue的大小为8,至于索引值,这里看到的情况就与《程序员的自我修养》一书不一致,这里看到的是3,无法与2)中看到的的main.o中的段表对应上,
(3) 函数符号main在目标文件main.o第1个段.text中,而add没有在本目标文件中定义,因此其段索引为UND
(4) 外部符号g_iAddValue2和add的索引都没有确定,暂时被标识为UND。
add.o的符号表为:
- # readelf -s add.o
- Symbol table '.symtab' contains 10 entries:
- Num: Value Size Type Bind Vis Ndx Name
- 0: 00000000 0 NOTYPE LOCAL DEFAULT UND
- 1: 00000000 0 FILE LOCAL DEFAULT ABS add.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 5
- 7: 00000000 4 OBJECT GLOBAL DEFAULT 3 g_iAddValue1
- 8: 00000004 4 OBJECT GLOBAL DEFAULT 3 g_iAddValue2
- 9: 00000000 27 FUNC GLOBAL DEFAULT 1 add
分析上述add.o的符号表,可以看到:
(1) 目标文件add.o*有三个符号,其中变量名符号为g_iAddValue1、g_iAddValue2和函数名符号add,在Ndx一列中也可以看到变量符号g_iAddValue1、g_iAddValue2所处段的下标为3,
(2) 变量符号为全局变量g_iAddValue1和g_iAddValue2,它们的所在段的索引值也与《程序员的自我修养》一书中所述不一致,还需后续研究。
(3) 函数符号为add
test的符号表为:
- # readelf -s test
- Symbol table '.symtab' contains 18 entries:
- Num: Value Size Type Bind Vis Ndx Name
- 0: 00000000 0 NOTYPE LOCAL DEFAULT UND
- 1: 08048094 0 SECTION LOCAL DEFAULT 1
- 2: 08048094 0 SECTION LOCAL DEFAULT 2
- 3: 08048094 0 SECTION LOCAL DEFAULT 3
- 4: 08049108 0 SECTION LOCAL DEFAULT 4
- 5: 08049108 0 SECTION LOCAL DEFAULT 5
- 6: 00000000 0 SECTION LOCAL DEFAULT 6
- 7: 00000000 0 FILE LOCAL DEFAULT ABS add.c
- 8: 00000000 0 FILE LOCAL DEFAULT ABS main.c
- 9: 0804910c 4 OBJECT GLOBAL DEFAULT 5 g_iAddValue2
- 10: 08049110 4 OBJECT GLOBAL DEFAULT 5 g_iMainValue
- 11: 08048094 27 FUNC GLOBAL DEFAULT 3 add
- 12: 08049120 0 NOTYPE GLOBAL DEFAULT ABS __bss_start
- 13: 080480b0 85 FUNC GLOBAL DEFAULT 3 main
- 14: 08049108 4 OBJECT GLOBAL DEFAULT 5 g_iAddValue1
- 15: 08049118 8 OBJECT GLOBAL DEFAULT 5 g_dMainValue
- 16: 08049120 0 NOTYPE GLOBAL DEFAULT ABS _edata
- 17: 08049120 0 NOTYPE GLOBAL DEFAULT ABS _end
分析上述test的符号表,可以看到:
(1) 目标文件test中的符号包含了它的链接来源目标文件main.o和add.o中的符号,也即:test的符号表就是main.o和add.o两个文件符号表的整合。
(2) 确定所有外部符号的段索引,可以看到在main.o中未确定段索引的外部分符号g_iAddValue2和add在test中都已经被填充,
4)综合上面的分析可以看到链接过程主要完成了下面的几件事情:
(1) 将各目标文件合并,主要是将各自的对应的段进行合并,生成一个目标文件。
1)扫描所有输入的目标文件,获得各目标文件的各个段的长度、属性和位置,并将它们合并,计算出合并后的段的长度和位置;
2)将各目标文件(例如main.o、add.o)符号表中的所有符号定义和符号引用收集起来统一放到链接后目标文件test的符号表中。
(2) 对合并后的各个段进行空间和地址的分配,即为链接后的目标文件test的各个段分配地址空间。
从2中也可以看到链接之前main.o和add.o中的虚地址空间(VMA)和加载地址空间(LMA)都是空,但是到test中都被填充了值,也就是进行了地址空间的分配。
(3) 对各个符号进行解析和重新定位。
例如,在扫描test合并之后的符号表,查找其中位置未定的引用符号add(从main.o中合并过来的符号引用add)等,该add的定义位置也被从add.o中合并到了test的符号表,因此其位置也可以确定了。
如果在test合并之后的符号表中有查找不到定义位置的符号,则会报出链接错误。
其中最重要步骤就是符号的解析和重定位。
转自网易名为“逍遥子”的博客