【程序员的自我修养06】静态链接过程的思考

2023-12-14 17:03:49

绪论

大家好,欢迎来到【程序员的自我修养】专栏。正如其专栏名,本专栏主要分享学习《程序员的自我修养——链接、装载与库》的知识点以及结合自己的工作经验以及思考。编译原理相关知识本身就比较有难度,我会尽自己最大的努力,争取深入浅出。若你希望与一群志同道合的朋友一起学习,也希望加入到我们的学习群中。文末有加入方式。

介绍

经过前两章的介绍,我们应该大致掌握了.o文件静态链接为可执行文件的过程。但是没有介绍一些特殊的场景与应用,比如:

  • 相同的符号定义在多个源文件中,符号解析流程是如何处理方式的呢?
  • 无用代码是如何处理的呢?
  • 链接的过程,可以控制函数初始化的过程吗?

本文则进一步分析,探讨上述几个问题,希望能够给你的工作带来帮助。

符号重定义

我们知道不同的.o文件在链接之前是不知道其它.o文件的内容,因此它们之间若存在相同的符号,也是只有在链接阶段才能发现。若链接器发现同一个符号有多处定义,它又是如何处理的呢?我们可以分为三种情况讨论:

  1. 两个或两个以上强符号
  2. 一个强符号,与其它多个弱符号,出现类型不一致
  3. 两个或两个以上弱符号类型不一致

分析:对于强符号,弱符号的定义,可以参考我的另一篇文章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文件种的任意一个函数或变量时,就需要把它整个链接进来,也就是没有用到的函数、变量也一起被链接进来了。
    这样的情况容易造成以下缺陷:
  1. 空间浪费。一个工程中有成千上万的.o文件,若大量存在该现象,则会导致最终生成的可执行程序体积很大。
  2. 降低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

最终可执行文件中,也没有函数cd。如下:

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,人生尽是坦途

文章来源:https://blog.csdn.net/xieyihua1994/article/details/134997262
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。