C++代码的编译和链接原理
我们在64位的linux Ubuntu系统演示。
我们看下面例子:
我们看下面示意图
预编译阶段:
#开头的 都是在预编译阶段处理,进行展开的!!!
但是下面是特例:
#pragma lib的意思是当前程序运行时需要链接的库,必须存活在链接阶段
#pragma link的意思是程序运行以后直接以指定函数为入口函数,也是存活在链接阶段
编译阶段:语法语义词法分析,代码的优化,处理完之后,生成相应平台的汇编代码
汇编阶段: 生成.o可重定位二进制目标文件,也就是把汇编码转成相应平台的机器码
链接阶段:所有.o文件和静态库文件合在一块进行链接。
链接后,生成可执行文件
gcc -o可以指定程序的名称
linux示例
objdump可以查看.o文件和可执行文件的详细信息。
我们查看符号表:
对应的代码:
如果当前文件引用外部文件的函数或者全局变量的符号时,在当前main.cpp编译成main.o的文件,这个符号会不会产生呢?
是会产生符号的,因为不产生符号就使用不了。
符号就是下图中的最右边这一列:
最左边的UND就是这个符号现在在代码上用到它了,但是却不知道它们是怎么定义的,所以只能给UND,是对符号的引用,不是对符号的定义。
l是local的意思,在当前文件看得见,g是global的意思,在其他文件也看得见。
链接的时候是所有obj文件在一起链接的,所以对于链接器来说,只能看得见.o文件的g符号,对于l符号链接器看不见。
对于定义静态的全局变量或者静态函数,只能在当前文件可见,其他文件看不见。编译生成的.o文件里面,普通的都是global,静态的都是local。
所以,在多个文件可以定义名字相同的静态全局变量或者静态函数。
但是在多个文件不可以定义名字相同的普通全局变量或者普通函数,符号解析就有冲突了。
我们看看.o文件的组成:
这个文件头我们查看看:
我们注意:symtab存放的就是符号表
我们查看段:
我们把各个段都打印出来:
编译过程中,符号是不分配虚拟地址的(因为都不知道符号在哪里定义的)。
我们发现:
在编译过程中,符号没有分配虚拟地址,但是指令已经分配好了。指令把符号的地址都填充成0,以后改成正确的地址。
链接的步骤
如下图所示:
符号解析就是所有对符号的引用都要找到该符号定义的地方。
例如我们前面所述:
我们写的代码链接时,可能会报错(符号未定义,符号重定义)
因为符号定义只能在某个.o文件出现1次,但是可以多个地方引用
段的合并,符号表本来就是.o文件的一个段,符号表段在合并的时候,就是符号解析,最终放在可执行文件里,可执行文件就是各种各样的段组成的。
符号解析成功以后:
给所有的符号分配虚拟地址
分配完地址以后,所有符号都有地址了。
然后到代码段上,指令上,原来填的符号地址都是0,现在要重新写上去。
就是符号的重定向!!!
我们进行链接:
所有符号都有其区域的位置和地址了!!!
我们查看代码段:
对于sum来说,符号放的是偏移量
符号是在链接过程的第1步:符号解析完成后(对所有符号的引用都要找到其定义的地址),分配虚拟地址。
分配完地址,再跑到代码段的指令上把符号的地址填成符号的正确地址(符号的重定向)
可执行文件的查看
在文件头记录了当前可执行程序的入口地址:
和可重定位目标文件不同的地方,可执行文件多了这么个段:
当我们运行可执行文件的时候,系统看program headers
程序运行,符号表段,段表内容都不需要往内存上加载,只需要加载的只有代码段和数据段
Align是以页面大小对齐
程序的入口地址:文件头有入口地址。
运行的时候读文件头,从文件头的地址,加载的时候把这个地址加载到CPU的PC寄存器里,加载完成,CPU就从main函数的第一条指令地址开始执行了
可执行文件加载的过程示意图
详细的过程看我另外一篇博客《Linux内存地址映射》