C++:Linux下编译链接原理底层演示分析

Linux下编译链接原理演示:

我们一个可执行文件的产生其原理大致如图所示:
在这里插入图片描述
若想更加透彻了解编译链接原理,详情可参考该博客:
C++:一个C/C++源文件从文本变成可执行文件的过程

一、编译原理底层演示分析

.o文件:其实质是由各种各样的段组成。,分区域每一块存了不同的东西。
在这里插入图片描述
我们在这里是为了通过演示Linux下底层具体实现,更好的了解编译链接基本原理。
我们这里有两个文件,main.cpp与sum.cpp;
main.cpp:
在这里插入图片描述
sum.cpp:
在这里插入图片描述
我们来一起瞧一瞧:
①将main.cpp,sum.cpp编译生成.o文件
在这里插入图片描述
②那么.o文件组成格式是什么?我们无论什么程序,最终产生的结果为:指令或数据。因此,我们使用 objdump 命令可以查看.o文件和一些可执行文件详细信息。
查看main.o与sum.o的符号表:符号表是汇编器把汇编文件转成.o文件时候,会给文件生成符号表与各种段。可以使用readelf -h 文件名来查看文件头。

为了更好的对比,我们将代码与符号表一起来看。
mian.cpp: 定义了data,main函数以及引用了gdata与sum函数。
在这里插入图片描述
sum.cpp: 定义了一个全局变量gdata与全局函数sum。
在这里插入图片描述

问题一:如果当前文件引用外部文件函数或全局变量的符号时候,再当前main.cpp编译成的main.o文件中,会产生sum的符号与gdata的符号呢?
符号定义与符号引用: 这里会产生符号的,我们观察上图,main函数在main.cpp中定义,最后会放在.text段;data也是main.cpp定义的,为初始值不为0的全局变量,最终放在.data段,这是符号已经定义好的;那么gdata与sum呢?仔细观察上图,都产生符号了! 但是它们都在 * UND * 。 这是什么意思呢?这是未定义段,我们这个符号在代码中用到它们了,但暂时并不知道它们是如何定义的,因此在当前的符号表中只能将它们放入未定义;这段过程是符号的引用。

       local与global: l是locall局部符号,只能在当前文件中看见;g为global,在其他文件中也可以看到。链接时,所有obj文件在一起链接的,对于链接器来说它只能看见.o文件中的global符号,local符号看不见。例如:我们定义了一个静态全局变量或者静态函数,只能当前文件可见,其他文件不可见,因此可以在多个文件中定义名字相同的静态全局变量,因为它们只能在当前文件可见。
       我们来看看sum.cpp中gdata与sum函数,分别在.data段与.text段,说明这是它们定义的地方,它们都为global符号。
最终我们main.cpp中生成的符号为:
在这里插入图片描述
最终我们sum.cpp中生成的符号为:
在这里插入图片描述
这里还可以使用objdump -s 文件名直接查看常见的段:
在这里插入图片描述
.o文件由各种各样的段组成,那如何查看所有段呢?我们可以使用readelf -S main.o将其所有段也打印出来。
在这里插入图片描述
从上面我们还得出一个结论:编译过程中,符号是不分配虚拟地址的,只有在链接阶段才分配。

问题二:那为什么编译后的文件无法直接运行呢?
首先,我们带上调试信息执行g++ -c main.o -g,再执行objdump -S main.o;
在这里插入图片描述
      我们来看一看,不管是访问自己文件的.cpp还是其他文件的.cpp地址都是没有的。main函数从高级源代码转为汇编是在编译时候做的,这个指令无法执行,因为符号的地址不可能是零地址。编译过程中符号还没有分配地址,指令的产生在编译阶段会产生好。符号的地址不确定,会在指令上面,暂时将符号地址都填为0;这也是.obj文件无法运行的原因。
 

二、链接原理底层演示分析

符号解析: 所有.o文件进行合并,各个段进行合并了,包括段表符号表全都进行合并,进行符号解析:所有对符号的引用,都要找到该符号定义的地方。例如:* UND *就是对符号的引用。对符号的引用可以出现多次,但是定义只能出现一次,符号未定义或重定义都会出错。最终产生的可执行文件也是各种各
给所有的符号分配虚拟地址: 符号解析完成后,给所有符号分配虚拟地址。
符号的重定向: 符号具体的地址写到指令上。
我们来具体演示一下:
用ld命令进行相应的链接处理:ld -e main *.o进行连接,再使用objdump -t命令查看。
在这里插入图片描述
我们可以看到。所有的符号都分配了地址,使用objdump -S再来查看:
在这里插入图片描述
所以,符号分配地址是在链接过程的第一步,符号解析完成后分配虚拟地址,再将 * UND *改为符号正确的地址。

这里也解决了我们一个问题:为什么程序执行时会从main函数第一行指令开始运行
我们来看看可执行文件的文件头:之前为可重定向文件,此时变为可执行文件。
在这里插入图片描述
文件头记录了当前可执行程序的入口地址,所以会从main函数第一行指令开始执行。
在这里插入图片描述
 

三、可执行文件与可重定位文件区别

前面了解了,那么可执行文件与可重定位文件有什么区别?
基本结构都一样,为各种各样的段,但是可执行文件多了一个progrma headers段。progrma headers放了两个LOAD:加载。运行时候有那么多段,运行时不一定全都加载,系统查看progrma headers的两个LOAD,告诉系统运行这个程序的时候将哪些内容加载到内存当中。 需要加载的只有代码段和数据段。
在这里插入图片描述
可执行文件加载的大致过程:a.out在磁盘上,里面有各种各样的段,elf header里面存放了程序的入口地址,program headers将.text段,.data段加载到内存中,映射到到进程的虚拟地址空间中。
在这里插入图片描述