Colin's Blog

Recent content on Colin's Blog

马上订阅 Colin's Blog RSS 更新: https://blog.oyyko.com/index.xml

链接 Note 1

finalwind42@gmail.com (Oyyko)
2022年3月24日 08:00

链接的笔记

本文部分信息来自网络

.bss段不占用存储空间

为什么要有.bss段?因为已经初始化的全局变量,需要在目标文件里面占用空间来存储它们被初始化的值。例如int a=3,b=2;我们需要存下3和2. 但是如果是没有初始化的全局变量,只需要记录一下长度就会好了。例如两个int,记录一些.bss段的长度为8字节即可。

那么,这个长度总要占地方的吧?

.bss段占据的大小存放在ELF文件格式中的段表(Section Table)中,段表存放了各个段的各种信息,比如段的名字、段的类型、段在elf文件中的偏移、段的大小等信息。同时符号存放在符号表.symtab中。

.bss不占据实际的磁盘空间,只在段表中记录大小,在符号表中记录符号。当文件加载运行时,才分配空间以及初始化。

程序为什么要分成数据段和代码段

数据和指令被映射到两个虚拟内存区域,数据段对进程来说可读写,代码段是只读,这样可以防止程序的指令被有意无意的改写。

有利于提高程序局部性,现代CPU缓存一般被设计成数据缓存和指令缓存分离,分开对CPU缓存命中率有好处。

代码段是可以共享的,数据段是私有的,当运行多个程序的副本时,只需要保存一份代码段部分。

链接器通过什么进行的链接

链接的接口是符号,在链接中,将函数和变量统称为符号,函数名和变量名统称为符号名。链接过程的本质就是把多个不同的目标文件之间相互“粘”到一起,像玩具积木一样各有凹凸部分,有固定的规则可以拼成一个整体。

可以将符号看作是链接中的粘合剂,整个链接过程基于符号才可以正确完成,符号有很多类型,主要有局部符号和外部符号,局部符号只在编译单元内部可见,对于链接过程没有作用,在目标文件中引用的全局符号,却没有在本目标文件中被定义的叫做外部符号,以及定义在本目标文件中的可以被其它目标文件引用的全局符号,在链接过程中发挥重要作用。

为什么需要extern “C”

C语言函数和变量的符号名基本就是函数名字变量名字,不同模块如果有相同的函数或变量名字就会产生符号冲突无法链接成功的问题,所以C++引入了命名空间来解决这种符号冲突问题。同时为了支持函数重载C++也会根据函数名字以及命名空间以及参数类型生成特殊的符号名称。

由于C语言和C++的符号修饰方式不同,C语言和C++的目标文件在链接时可能会报错说找不到符号,所以为了C++和C兼容,引入了extern “C”,当引用某个C语言的函数时加extern “C"告诉编译器对此函数使用C语言的方式来链接,如果C++的函数用extern “C"声明,则此函数的符号就是按C语言方式生成的。

以memset函数举例,C语言中以C语言方式来链接,但是在C++中以C++方式来链接就会找不到这个memset的符号,所以需要使用extern “C"方式来声明这个函数,为了兼容C和C++,可以使用宏来判断,用条件宏判断当前是不是C++代码,如果是C++代码则extern “C”。

1#ifdef __cplusplus2extern "C" {3#endif45void *memset(void *, int, size_t);67#ifdef __cplusplus8}9#endif

强符号和弱符号

我们经常编程中遇到的multiple definition of ‘xxx’,指的是多个目标中有相同名字的全局符号的定义,产生了冲突,这种符号的定义指的是强符号。有强符号自然就有弱符号,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。attribute((weak))可以定义弱符号。

1extern int ext;23int weak; // 弱符号4int strong = 1; // 强符号5__attribute__((weak)) int weak2 = 2; // 弱符号67int main() {8    return 0;9}

链接器规则:

不允许强符号被多次定义,多次定义就会multiple definition of ‘xxx’一个符号在一个目标文件中是强符号,在其它目标文件中是弱符号,选择强符号一个符号在所有目标文件中都是弱符号,选择占用空间最大的符号,int类型和double类型选double类型

强引用和弱引用

一般引用了某个函数符号,而这个函数在任何地方都没有被定义,则会报错error: undefined reference to ‘xxx’,这种符号引用称为强引用。与此对应的则有弱引用,链接器对强引用弱引用的处理过程几乎一样,只是对于未定义的弱引用,链接器不会报错,而是默认其是一个特殊的值。

 1#include <cstdio> 2 3__attribute__((weak)) void foo(); 4 5int main() 6{ 7    printf("%d\n", &foo); // 0 8    foo(); 9    return 0;10}

这里foo的地址是0

则可以改为

1__attribute__ ((weak)) void foo();23int main() {4    if (foo) {5        foo();6    }7    return 0;8}

这种强引用弱引用对于库来说十分有用,库中的弱引用可以被用户定义的强引用所覆盖,这样程序就可以使用自定义版本的库函数,可以将引用定义为弱引用,如果去掉了某个功能,也可以正常连接接,想增加相应功能还可以直接增加强引用,方便程序的裁剪和组合。

例如再加一个文件

1void foo()2{3    printf("foo2\n");4}

这个时候就可以g++ 1.cpp 2.cpp从而得到一个可以输出foo2的可执行文件。

弱符号的出现主要是为了解决宏条件编译问题的,宏条件编译对于长期维护的代码是个灾难。

程序首先执行的代码并不是main开始的代码,也就是程序的真正入口不是main函数,而是运行库的入口函数;运行库会 先把main函数需要的参数、环境变量等准备好;然后将标准输入输出文件描述符打开,这样才能保证main函数开始就可以使用printf;然后把堆初始化,这样才能保证程序可以自由地执行malloc、new等;还有就是把全局变量初始化、全局构造函数执行完;做完这么多工作之后运行库就执行回调函数main,这时候程序才开始进入人们常说的函数入口。程序的运行环境组成是:程序本身逻辑代码、运行库、系统内核、内存;内存分为用户空间和内核空间,只要内核才使用内核空间,其它的包括运行库都是使用用户空间;程序运行空间分为栈空间和堆空间,函数运行的环境就是栈空间,栈空间都是有固定大小的,一般是2M,地址增加方向是向低地址扩张;堆空间比较大,使用也很灵活,这个堆空间一般都是运行库在帮你管理,堆分配算中中最简单的就是空闲链表算法;程序运行完之后,运行库还要帮你把所有的后事处理掉,释放堆空间、关闭所有打开的文件描述符、释放所有的进程资源等,这就是进程关闭内存泄漏的那些空间能够得到回收的原因;从程序的整个过程可以看书,main函数只不过是运行库的一个回调函数,不是真正的函数入口,当然我们也可以自己写一个运行库,这样就可以直接运行在系统内核之上了,运行库主要部分就是标准c接口的实现,听上去并不复杂。

剩余内容已隐藏

查看完整文章以阅读更多