【程序员的自我修养06】静态链接过程的思考
绪论
大家好,欢迎来到【程序员的自我修养】专栏。正如其专栏名,本专栏主要分享学习《程序员的自我修养——链接、装载与库》的知识点以及结合自己的工作经验以及思考。编译原理相关知识本身就比较有难度,我会尽自己最大的努力,争取深入浅出。若你希望与一群志同道合的朋友一起学习,也希望加入到我们的学习群中。文末有加入方式。
介绍
经过前两章的介绍,我们应该大致掌握了.o
文件静态链接为可执行文件的过程。但是没有介绍一些特殊的场景与应用,比如:
- 相同的符号定义在多个源文件中,符号解析流程是如何处理方式的呢?
- 无用代码是如何处理的呢?
- 链接的过程,可以控制函数初始化的过程吗?
本文则进一步分析,探讨上述几个问题,希望能够给你的工作带来帮助。
符号重定义
我们知道不同的.o
文件在链接之前是不知道其它.o
文件的内容,因此它们之间若存在相同的符号,也是只有在链接阶段才能发现。若链接器发现同一个符号有多处定义,它又是如何处理的呢?我们可以分为三种情况讨论:
- 两个或两个以上强符号;
- 一个强符号,与其它多个弱符号,出现类型不一致;
- 两个或两个以上弱符号类型不一致。
分析:对于强符号,弱符号的定义,可以参考我的另一篇文章C语言中弱符号与弱引用的实际应用。
对于第一种情况是无须额外处理的。因为多个强符号的定义本身就是非法的,链接器会报符号重复定义错误,如下:
/tmp/ccaOZiSD.o:(.data+0x0): multiple definition of `strong_symble'
/tmp/ccDOL5Yz.o:(.data+0x0): first defined here
collect2: error: ld returned 1 exit status
对于第二种情况,链接器的处理方式是:全局符号表中保留的符号与强符号所占空间相同。注:链接器是不知道符号的类型,它只能通过符号表信息,知道符号所占空间大小。示例如下:
//a.c
#include<stdio.h>
int strong_symble;
int main()
{
return 0;
}
//b.c
char strong_symble = 1;
编译:
yihua@ubuntu:~/test/init$ gcc a.c b.c -o ab
/usr/local/bin/ld: Warning: alignment 1 of symbol `strong_symble' in /tmp/ccDCLDu5.o is smaller than 4 in /tmp/ccmdUaIz.o
/usr/local/bin/ld: Warning: size of symbol `strong_symble' changed from 4 in /tmp/ccmdUaIz.o to 1 in /tmp/ccDCLDu5.o
如上,链接器在链接时会发现警告,提示strong_symble
从4Byte 变为 1Byte。查看ab
符号表,strong_symble
的确仅占1Byte。
yihua@ubuntu:~/test/init$ readelf -s ab | grep strong
47: 0000000000201010 1 OBJECT GLOBAL DEFAULT 22 strong_symble
第三种情况,链接器的处理方式:全局符号表保留的符号为所有弱符号占空间最大的。示例如下:
//a.c
#include<stdio.h>
int week_symble;
int main()
{
return 0;
}
//b.c
char week_symble;
//c.c
double week_symble;
编译:gcc a.c b.c c.c -o abc
查看符号信息:
yihua@ubuntu:~/test/init$ readelf -s abc | grep week
51: 0000000000201018 8 OBJECT GLOBAL DEFAULT 23 week_symble
可知,week_symble
的大小最终为double数据类型大小。
总结:对于上述三种情况:
场景一,会在编译阶段体现。我们按需解决即可。
场景三,似乎也没有问题,符号最终采用的空间最大的定义,似乎也不会出现越界情况。
但是场景二,虽然会提示警告,但是依然会采用强符号的所占空间。这样就容易导致其它符号引用的地方越界。因此我们需要关注编译过程的警告,这也是我们要求工程编译0警告的原因之一。
面试题:为什么.o
文件中并没有将未初始化的全局变量保存在.bss段?
答:首先未初始化的全局变量属于弱符号,编译器并不能确定它的最终占用空间大小。及时此时将其保存在.bss
段,链接阶段也可能会改变。因此是在编译阶段保存到.bss
段是没有意义的。最终链接阶段,确认了符号的最终所占大小。会再保存到.bss
段。
无用代码处理方式
从【程序员的自我修养04】目标文件生成可执行文件过程中我们知道,链接器将所有.o
文件的相似段进行合并输入到可执行文件中。那么就存在一种情况:
- 一个
.o
文件可能包含成千上百个函数或变量,当我们需要用到某个.o
文件种的任意一个函数或变量时,就需要把它整个链接进来,也就是没有用到的函数、变量也一起被链接进来了。
这样的情况容易造成以下缺陷:
- 空间浪费。一个工程中有成千上万的
.o
文件,若大量存在该现象,则会导致最终生成的可执行程序体积很大。 - 降低cache命中率,导致运行效率低。
在如今的高性能计算机上,一般不需要担心这类问题。不过我们也可以通过编译的选项参数,解决这个问题。比如GCC编译器提供了类似的机制,它提供了-ffunction-sections
和-fdata-section
参数。它表示将所有的函数和变量单独保存到一个段中,当链接器需要用到某一个函数、变量时,将其合并到输出文件中。没有引用的就抛弃。
- 优点:很大程度上减小了输出文件的长度,减少空间浪费;提高运行效率。
- 缺点:减慢编译和链接过程;
示例:
//a.c
#include<stdio.h>
int week_symble;
extern int b(void);
int main()
{
b();
return 0;
}
//b.c
char week_symble;
int b()
{
return 0;
}
int c()
{
return 0;
}
int d()
{
return 0;
}
编译:
gcc -c b.c a.c -ffunction-sections
ld a.o b.o --gc-sections -e main -o ab
-ffunction-sections
告诉编译器将函数按段分配。--gc-sections
告诉链接器删除那些未使用的段。
分析:我们首先查看b.o
的段表信息,的确多了新的函数段.test.b
、.text.c
、.text.d
,如下:
yihua@ubuntu:~/test/init$ readelf -S b.o
There are 14 section headers, starting at offset 0x340:
Section Headers:
[Nr] Name Type Address Offset Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040 0000000000000000 0000000000000000 AX 0 0 1
[ 2] .data PROGBITS 0000000000000000 00000040 0000000000000000 0000000000000000 WA 0 0 1
[ 3] .bss NOBITS 0000000000000000 00000040 0000000000000000 0000000000000000 WA 0 0 1
[ 4] .text.b PROGBITS 0000000000000000 00000040 000000000000000b 0000000000000000 AX 0 0 1
[ 5] .text.c PROGBITS 0000000000000000 0000004b 000000000000000b 0000000000000000 AX 0 0 1
[ 6] .text.d PROGBITS 0000000000000000 00000056 000000000000000b 0000000000000000 AX 0 0 1
[ 7] .comment PROGBITS 0000000000000000 00000061 000000000000002a 0000000000000001 MS 0 0 1
[ 8] .note.GNU-stack PROGBITS 0000000000000000 0000008b 0000000000000000 0000000000000000 0 0 1
[ 9] .eh_frame PROGBITS 0000000000000000 00000090 0000000000000078 0000000000000000 A 0 0 8
[10] .rela.eh_frame RELA 0000000000000000 00000288 0000000000000048 0000000000000018 I 11 9 8
[11] .symtab SYMTAB 0000000000000000 00000108 0000000000000168 0000000000000018 12 11 8
[12] .strtab STRTAB 0000000000000000 00000270 0000000000000015 0000000000000000 0 0 1
[13] .shstrtab STRTAB 0000000000000000 000002d0 000000000000006c 0000000000000000 0 0 1
最终可执行文件中,也没有函数c
和d
。如下:
yihua@ubuntu:~/test/init$ readelf -s ab
Symbol table '.symtab' contains 14 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000004000e8 0 SECTION LOCAL DEFAULT 1
2: 0000000000400108 0 SECTION LOCAL DEFAULT 2
3: 0000000000601000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 FILE LOCAL DEFAULT ABS a.c
6: 0000000000000000 0 FILE LOCAL DEFAULT ABS b.c
7: 0000000000000000 0 FILE LOCAL DEFAULT ABS
8: 0000000000601000 0 OBJECT LOCAL DEFAULT 3 _GLOBAL_OFFSET_TABLE_
9: 00000000004000f8 11 FUNC GLOBAL DEFAULT 1 b
10: 0000000000601018 0 NOTYPE GLOBAL DEFAULT 3 __bss_start
11: 00000000004000e8 16 FUNC GLOBAL DEFAULT 1 main
12: 0000000000601018 0 NOTYPE GLOBAL DEFAULT 3 _edata
13: 0000000000601018 0 NOTYPE GLOBAL DEFAULT 3 _end
总结:当我们在资源很紧张的平台中,急需做一些优化,不妨尝试一下该方法。
函数初始化流程控制
我们一般知道C/C++程序是从main开始执行的,随着main函数的结束而结束,但实际上,在main函数执行之气那,为了程序能够顺利执行,要初始化进程的执行环境,比如堆分配初始化、线程子系统等。C++全局对象构造函数也是这一时期被执行的,析构函数也是在main结束后执行的。那么C 函数也可以实现类似的逻辑吗?
C语言提供了两个函数属性,__attribute__((constructor))
、__attribute__((destructor))
分配可以设置函数在main之前执行,main之后执行。并且修饰的函数需要满足三个要求:
- 函数必须是公开的,不能用static 修饰
- 返回类型必须是void
- 不能有入参
示例如下:
//initial.c
#include<stdio.h>
__attribute__((constructor)) void before_main(void)
{
printf("i'm before main \n");
return;
}
__attribute__((destructor)) void after_main(void)
{
printf("i'm after main \n");
return;
}
int main()
{
printf("i'm main \n");
return 0;
}
编译:gcc initial.c -o initial
运行输出:
yihua@ubuntu:~/test/init$ ./initial
i'm before main
i'm main
i'm after main
总结:C语言也可以实现类似C++的构造与析构函数,可以根据自身业务进行应用。
总结
本文介绍了静态链接过程一些特殊场景和优化项。希望大家能够结合自己的实际工作,应用起来。
静态链接章节到此为止,后续我会陆续分享动态链接过程及应用。有兴趣的朋友,还请关注,若有任何问题,意见,都可以在评论区留言,我会及时回复。
若我的内容对您有所帮助,还请关注我的公众号。不定期分享干活,剖析案例,也可以一起讨论分享。
我的宗旨:
踩完您工作中的所有坑并分享给您,让你的工作无bug,人生尽是坦途。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!