失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > C++程序编译-链接-加载过程初探-符号表

C++程序编译-链接-加载过程初探-符号表

时间:2021-01-28 06:58:11

相关推荐

C++程序编译-链接-加载过程初探-符号表

文章目录

前言符号表nm指令看一下hello world变量和函数在符号表中的位置看看类的定义把类和主程序一起打包测试符号表角度看编译与链接加载程序到内存

前言

从自己接触计算机程序设计开始,就是从C程序入手的。在多年的从业经验中,慢慢积攒了一些经验,也没有好好梳理一下。

一直觉得一个好的程序员必须了解c或者c++,因为这两者是和操作系统打交道比较多的语言,使用过程中可以更好的了解操作系统,对程序优化和整个计算机的体系结构的了解有很好的帮助。

符号表

c代码经过编译和链接后,会生成可执行文件。而可执行文件中一般是带了一份“程序地图”的,这份地图中很详细的标记了程序内部的函数,变量的地址和相关属性。这份“地图”就是符号表。

我们可以通过nm指令来读取符号表和显示出来。

我使用的是mac环境,和linux类似,在windows下也有这个指令,可能使用方式有所不同吧,没有尝试过。

nm指令

可以通过man nm指令来查看具体的使用方法,各位可以自己去试一下。

这篇文章通过写几个基本的例子来说明nm指令和符号表的基本内容。

看一下hello world

先看一下最基本的hello world程序:

int main(){printf("hello world");return 0;}

使用 gcc -o demo demo.cpp来生成可执行文件demo

然后使用命令 nm demo就可以看到符号表内容了:

0000000100008008 d __dyld_private0000000100000000 T __mh_execute_header0000000100003f60 T _mainU _printfU dyld_stub_binder

nm的三个基本内容:

地址,不是内存中的绝对地址,而是一个虚拟地址,有兴趣的朋友可以去了解下操作系统原理。第二个算是一个flag,表示是什么样的内容,有下面几种取值:

A :符号的值是绝对值,不会被更改

B或b :未被初始化的全局数据,放在.bss段

D或d :已经初始化的全局数据

G或g :指被初始化的数据,特指small objects

I :另一个符号的间接参考

N :debugging 符号

p :位于堆栈展开部分

R或r :属于只读存储区

S或s :指为初始化的全局数据,特指small objects

T或t :代码段的数据,.test段

U :符号未定义

W或w :符号为弱符号,当系统有定义符号时,使用定义符号,当系统未定义符号且定义了弱符号时,使用弱符号。

? :unknown符号符号名称

再看看都有一些什么阳的符号:

mh_execute_header,这个是程序的入口地址,在mac的程序中都会有这么一个东西。这个符号位于程序内存的代码段(程序内存分段详见上一篇文章)__dyld_private 和dyld_stub_binder这两个符号是程序里面的一些特殊的符号,具体是啥不太清楚,dyld_stub_binder好像是一个什么懒加载函数,不是我最近了解的重点,先不管。在这个符号表中,有两个关键符号,也就是main函数和printf外部函数两个符号,一般从头文件里引入的库函数一般都是U的flag,而且是没有地址的,我估计是因为在执行的时候再加载dll进来,所以现在是没有地址的。main函数就分配在代码段(code area)

变量和函数在符号表中的位置

我们在基本的helloworld函数中增加两个变量:

int x = 1;int func_a(int x){return 1;}int main(){int y = 2;printf("hello world");return 0;}

比起上面的例子,在符号表中就增加了两条记录:

0000000100003f40 T __Z6func_ai0000000100008010 D _x

x全局变量分配在数据区,flag标记为初始化了的全局变量从上面的例子可以看出,局部变量是不会在符号表中存在的。我理解是这部分变量是在堆栈中分配的,在程序载入的时候是不需要分配地址的,在函数被调用的时候再入栈和分配地址的,所以在符号表中不会有记录。函数作为一个符号在符号表中有一条记录,标记为T,分配在代码段。

看看类的定义

我们定义一个简单的类:

class testclass{private:int x = 1;public:int addx();};int testclass::addx(){return x+1;}

通过gcc -c testclass.cpp生成testclass.o

再通过nm testclass.o来查看符号表,发现只有一条记录:

0000000000000000 T __ZN9testclass4addxEv

只有方法函数addx的符号,这个符号是带了类名称做为前缀的,位于代码区。

这里有个有意思的事情必须要提一下,就是这个函数的符号名称,这个名称被成为“函数签名”。因为程序会分布在不同的文件,不同的类中,不同的文件和类中都可以定义相同名字的函数,也就是作用域的概念。不同文件和类中的函数(或者是函数的重载)都会在这个函数签名上得到体现。

GCC的基本C++名称修饰方法如下:所有的符号都以"_Z"开头,对于嵌套的名字(在名称空间或在类里面的),后面紧跟"N",如果是全局函数,后面跟的是“6g”;

然后是各个名称空间和类的名字,每个名字前是名字字符串长度,再以"E"结尾。

对于一个函数来说,它的参数列表紧跟在"E"后面,对于int类型来说,就是字母"i"。基本上都是每个类型的第一个字符,如果为没有参数列表的话,就是字母“v”。

我们来看一下上面的符号名称是恰好符号这个规则的。

把类和主程序一起打包测试

在main函数中使用类:

int x = 1;testclass g_a;int g_func(double d){return 1;}int main(){testclass a;printf("hello world: %d\n", a.addx());return 0;}

通过gcc -L ./ -ltestclass.o demo_mem.cpp命令,把程序一起打包。然后在通过nm命令观察输出:

0000000100003f70 t __GLOBAL__sub_I_demo_mem.cpp0000000100003ee0 T __Z6g_funcd0000000100003ea0 T __ZN9testclass4addxEv0000000100003ec0 t __ZN9testclassC1Ev0000000100003f30 t __ZN9testclassC2Ev0000000100003f50 t ___cxx_global_var_init0000000100008008 d __dyld_private0000000100000000 T __mh_execute_header0000000100008014 S _g_a0000000100003ef0 T _mainU _printf0000000100008010 D _xU dyld_stub_binder

首先,再次确认下函数签名,我定义了一个全局函数func,签名为__Z6g_funcd,因为是全局的,所以没有类的前缀。也是在code area。

符号表中多出了___cxx_global_var_init函数,这个函数是用于初始化全局类变量的,如果是基础类型,是没有这个函数的。

_ZN9testclassC1Ev和_ZN9testclassC2Ev分别表示类的构造函数和析构函数。这两个函数是组队出现的,也就是如果有一个__ZN9testclassC1Ei,就会有一个__ZN9testclassC2Ei。

也就是说,单独的类的编译文件.o中是没有构造函数和析构函数的,在链接成可执行文件的时候,并且这个类被声明和定义了变量的时候才会在符号表中添加。

关于函数签名,这里还可以补充一种情况,如果函数参数是类的话,签名的参数就是类的名字。

int g_func(testclass d){return 1;}

nm的输出就会是:

0000000100003ef0 T __Z6g_func9testclass

符号表角度看编译与链接

从符号表的角度来说的话,编译过程就是把一个个的cpp或者c文件编译成对象文件:.o文件。这个.o文件记录了当前类或者文件中的各种符号,就和上面的例子一样。

然后链接就是把各个.o目标文件的符号表进行合并,并产生一些其他的符号,比如构造和析构函数等等。

最后执行的时候由操作系统分配内存并根据符号表把这些关键符号加载到相应的内存结构中。

加载程序到内存

程序在没有运行之前,也就是说程序没有被加载到内存前,可执行程序内部已经分好3段信息,分别是代码区(text)、数据区(data)和未初始化数据区(bss)三个部分。

程序在加载到内存前,代码区和全局区(data和bss)的大小就是固定的,程序运行期间不能改变,data段和bss区中的数据的生存周期为整个程序运行过程。

可以直接通过size命令看到:

__TEXT __DATA __OBJCothersdec hex16384 16384 0 42950000644295032832100010000

运行可执行程序,系统把程序加载到内存,除了根据可执行程序的信息分出代码区、数据区和未初始化数据区之外,还额外增加了栈区和堆区(这两个区)。

代码区​存放可执行文件的二进制代码,CPU 执行的机器指令。通常代码区是可共享的(即另外的执行程序可以调用它),使其可共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。代码区通常是只读的,使其只读的原因是防止程序意外地修改了它的指令。生存周期为整个程序运行过程。

全局初始化数据区/静态数据区/只读常量区(data段),该区包含了在程序中明确被初始化且不为0的全局变量、已经初始化且不为0的静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量、全局常量(const))。注意:局部常量位于栈区 生存周期为整个程序运行过程。

未初始化数据区(又叫 bss 区), 该区包含全局未初始化变量和未初始化静态变量,局部未初始化静态变量。未初始化数据区的数据在程序开始执行之前被内核初始化为 0 或者空(NULL)。 生存周期为整个程序运行过程。

函数中的局部变量在栈区分配内存,new和malloc分配的内存就存在于堆区。

如果觉得《C++程序编译-链接-加载过程初探-符号表》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。