深入理解编译、链接和运行(obj文件组成格式分析,可执行文件组成格式分析) | 您所在的位置:网站首页 › 输出文件是什么文件 › 深入理解编译、链接和运行(obj文件组成格式分析,可执行文件组成格式分析) |
一、简单的CS历史 现代大多数计算机都是基于冯.诺伊曼提出的存储程序原理采用冯.诺伊曼架构,即由运算器、控制器、存储器和输入输出设备组成。 为了屏蔽I/O设备的底层差异,产生虚拟文件系统virtual file system(VFS)。为了屏蔽内存和I/O的差异产生了虚拟存储器(虚拟内存),而为了屏蔽CPU、I/O和内存的差异进而产生进程的概念。 虚拟的概念是由大名鼎鼎的计算机公司IBM提出的,为了方便理解虚拟,IBM写出下面的几句话: 1、看得见 存在 物理 2、看得见 不存在 虚拟 3、看不见 存在 透明 二、程序是怎么执行的 我们或多或少都有疑问,这些看似平常的由字符组成的文章是通过怎样的过程可最终以在计算机上执行,高级语言如pascal、c、c++、java等,尽管语言不相同,持有各自的特性,但其最终生成的无非就是指令和数据,毫不夸张的讲程序其实就是指令和数据。所以计算机是在做运算,处理指令和数据。那么我们用高级语言编写的程序最终是怎么成为计算机可以识别的机器语言的。在linux系统上,当我们输入./a.out并进行回车时发生了什么。作为一名程序员,这是我们需要知道的。 下边的代码,用于分析编译、链接过程。 #include int gdata1 = 10; int gdata2 = 0; int gdata3; static int gdata4 = 20; static int gdata5 = 0; static int gdata6; int main(){ int a = 30; int b = 0; int c; static int d = 40; static int e = 0; static int f; return 0; }上边共定义的12个变量,其中哪些是指令,哪些是数据。都分布在内存的哪些区域是我们所需清楚的。 为探究上述问题,首先我们需要清楚的了解虚拟地址空间的内存布局。操作系统为每一个进程分配虚拟地址空间,而虚拟地址空间的大小,取决于CPU的位数,更具体的说是ALU(算术逻辑运算单元的宽度)即一次可以处理最长整数的宽度,同时也可以理解为数据总线的条数。在32bit的linux内核,也可以理解为地址总线的条数,因为地址总线的条数和数据总线的条数相同。总而言之,在32bit操作系统下,虚拟地址空间的大小为2^32即4G的大小的虚拟地址空间。 三、虚拟地址空间 由上边分析可知,在32bit的CPU架构下地址总线的条数为32条,所以其寻址能力为2^32个,按字节编址,所以虚拟地址空间的大小为4GB。下面以图示的方式说明这4GB的虚拟地址空间布局是什么样的。 用户空间的分析: (1)保留区:很多情况下,正是由于我们对虚拟地址空间布局不熟悉所以编写出错误的程序。如果熟悉虚拟地址空间的内存布局,大可避免这些不必要的错误。如下边的小程序正是许多许多新手程序员经常犯的错误: #include #include int main(){ char* p = NULL; int len = strlen(p); printf("len = %d\n",len); return 0; }显然程序中并没有给指针变量p分配合理的内存,就对p进行了访问,此时p所指向的内存区域正是虚拟地址空间中128MB的保留区,所以会出现段错误。 (2).data、.bss和.text (3)共享库 如果程序中用到了库函数,如printf、scanf、puts、gets等。则在共享库中包含了这些函数的定义。 (4)栈 函数运行用到的栈 内核空间的分析: (1)ZONE_DMA:直接内存访问,正常情况下,磁盘中的数据到达主存需要进过存储器的层次结构,需要经过CPU。若这些数据不需要CPU处理,则浪费了大量的CPU时间。如果在主存和辅存之间之间开辟一条数据通路,则可提高CPU的使用率,同时可以加快主存和辅存之间交换数据的速度,进而提高整机的性能。DMA直接内存访问正是提供了这样的一种机制。 (2)ZONE_NORMAL (3)ZONE_HEIGHMEM:主要用于在32bit的linux系统中在内核映射高于1GB的物理内存时会用到高端内存。 四、深入编译和链接过程。 下面详细分析由源文件是如何经过编译和链接过程最终生成可执行文件。 测试环境:ubuntu18.04 + gcc 测试工具:逆向和反汇编工具 objdump和readelf 测试代码: int gdata1 = 10; //.data int gdata2 = 0; //.bss int gdata3; //.bss static int gdata4 = 20; //.data static int gdata5 = 0; //.bss static int gdata6; //.bss int main(){ //.text int a = 30; //.text int b = 0; //.text int c; //.text static int d = 40; //.data static int e = 0; //.bss static int f; //.bss return 0; //.text } //.bss共占24个字节 .data共占12个字节编译生成可重定位的二进制文件: (2)编译:gcc -S *.i -o *.s 词法分析、语法分析和语义分析、代码的优化、编译、汇总所有的所有的符号 (3)汇编:gcc -c *.s -o *.o 将汇编指令转换为特定平台下的机器语言、构建*.o文件组成格式。 链接 (1)合并所有obj文件的段,并调整段偏移和段长度,合并符号表,进行符号解析,分配内存地址(虚拟地址)。 (2)链接的核心:符号的重定位。 针对编译和链接过程,提出以下需要解决的问题: (1)编译的过程是怎么样的? (2)obj文件的组成格式是什么,它为什么不能执行? 1.readelf -h main.o输出obj文件头部,可以查看到obj文件一些重要信息。 下面分析.obj文件的组成格式 3.objdump -s main.o
.obj文件组成格式的分析,着重看 现在新的问题出现了: (2.1)既然.obj文件中都没有存储.bss段的信息,那么在程序中那些初始化为0的全局变量和未初始化的局部变量它是怎么识别它们的? 答:由于.bss段中都是0,所以不需要记录。只需要记录其大小即可,所以通过段表即可找到。 (2.2)分析测试代码,得出由六个变量位于.bss段,但实际上在.bss中大小只有20个字节即只记录了5个变量,那么还有一个变量为什么不记录,它在哪里? 答:这里涉及到强弱符号,我会单独写出来。浅显的可以这样理解,由于全局变量gdata3是一个弱符号,而未经链接。并不知道是否有强符号的存在,所以在.bss段中并为记录。而gdata6虽然未经初始化,但由于其经static关键字修饰,本文件可见,所以不存在强弱符号之分。 (3)链接的第二步具体做了哪些事情,什么是符号重定位? 链接器只对所有.obj文件的global符号进行处理,对local的符号不做任何处理。如static生成的符号就是local的符号。 objdump -t main.o查看生成的符号表 分别查看main.o和sum.o所生成的符号表:
(4)可执行文件的组成格式是什么?它为什么可以执行?它从哪开始执行? CPU怎么知道它从哪开始执行? 分析可执行文件run的组成格式 readelf -h run获取可执行文件的头部信息 可执行文件的组成格式 gcc -o run *.c ./run &放到后台执行 |
CopyRight 2018-2019 实验室设备网 版权所有 |