深度剖析编译链接原理

一、请问一个或多个c/c++源文件是如何生成一个可执行的二进制文件呢?

在我们写好代码后,源文件需要经过:预编译、编译、汇编、链接四个步骤对我们的源文件进行加工处理,才变成可执行二进制文件。
在这里插入图片描述

下面我对这四个阶段进行一一解释:

  1. 预编译(预编译器:简单的增删、替换)

    • 头文件递归展开:把#include<头文件>在当前文件递归(该头文件包含其他头文件)展开

    • 宏替换:#define替换

    • 删除预处理指令:删除下表指令

    指令 说明
    # 空指令,无任何效果
    #undef 取消已定义的宏
    #if 如果给定条件为真,则编译下面代码
    #ifdef 如果宏已经定义,则编译下面代码
    #ifndef 如果宏没有定义,则编译下面代码
    #elif 如果前面的#if给定条件不为真,当前条件为真,则编译下面代码
    #endif 结束一个#if……#else条件编译块
    • 删除注释: // 、/**/
    • 添加行号,文件标识:添加行号是为了为编译阶段进行语法复分析出现错误提供行号
    • 保留#pragma:为编译器保留的
  2. 编译(编译器)

    • 词法分析:分析标识符是否合法int 1a = 10;(err)
    • 语法分析:后期补充
    • 语义分析:后期补充
    • 代码优化:后期补充
    • 生成汇编指令
  3. 汇编(汇编器)

    • 翻译指令:将汇编指令翻译成二进制指令,生成.o/.obj文件
  4. 链接(链接器)

    • 合并各个段(链接器完成)

    • 符号重定位

接下来让我们分析一段代码:

#include <stdio.h>
/*
.text段存放指令

静态变量和全局变量都存在数据区
数据区有.data和.bss,
		.data段已初始化且不为零的数据(先不考虑.rodata段)
		.bss段存放未初始化或初始化为零的数据

下面代码数据存放应如下:					
*/
int ga = 10;		//.data
int gb = 0;			//.bss
int gc;				//.bss

static int gd = 20; //.data
static int ge = 0;	//.bss
static int gf;		//.bss

int main()
{
    
    
	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;
}

现在我们将其汇编生成.o文件看看
在这里插入图片描述

使用file -h 文件名:看到是可重定位的ELF格式的文件
在这里插入图片描述

使用objdump -h elf.o查看目标文件的段信息
在这里插入图片描述

可以通过计算得出.data段中存储了12字节的数据 == 0x0000 000c

问题一:但是.bss段应该存储6*4 = 24字节的数据,但是实际只存储了20字节 == 0x0000 0014,为什么少了4字节数据呢?

这个需要根据符号表.symtab来解释,后面会讲解。

.o文件中是section段形式,在虚拟机中段空间是segement段的形式;两者方便映射;

下面看看该文件的各个段section在虚拟地址空间对应的关系如下图
在这里插入图片描述

问题二:那么,既然.bss段是虚拟的,不存在,那初始化为0和未初始化的数据是怎么存放的呢?

使用readelf -h elf.o查看ELF文件格式的头部信息:
在这里插入图片描述

我们再来看看section header中具体存了什么内容吧

使用readelf -S 文件名.o
在这里插入图片描述

解答二:现在来解释为什么.bss不存在但能够保存初始化为0和未初始化的数据,程序在运行的时候调用头部里面.bss段的描述信息进行初始化,也就是说将.bss的信息简略的存放在了头部section header中

至于第一个问题:为什么.bss的字节大小少了4字节?

这就要讲到符号表.symtab

该程序中除了指令都会生成符号存放在符号表中,比如自身文件名、main等函数、核心的段、存放在数据区的变量等都会生成符号;

符号表如下:
在这里插入图片描述

那什么是强弱符号呢?

强弱符号:只有global属性的变量存在强弱符号

  • 强符号:已初始化(0也算)的全局变量

  • 弱符号:未初始化的全局变量

注:(C语言有强弱符号区分,c++没有)

强弱符号规则:

  1. 两个强符号:编译器报错,该变量重复定义
  2. 一个强符号,一个弱符号:选择强符号
  3. 两个弱符号:不同编译器不同结果,有的直接报错,有的选择以编译的第一个文件中定义的变量

简单看个程序就明白了,下面这是一个工程下的两个文件

第一种情况:

文件一:

int a = 20;		//强符号//global .data段 
void set_a()	//global .text段 
{
    
    
    a = 30;		
}

文件二:

#include <stdio.h>

short a;		//弱符号//global .bss段 *COM*块中 
short b = 10;	//强符号//global .data段
int main()
{
    
    
    set_a();
    printf("a = %d, b = %d\n", a, b);
    return 0;
}

结果: a = 30, b = 10

链接阶段,选择强符号int a = 20,此时这个a类型确定为int类型4字节,所以给a开辟的空间是4字节;运行时调用set_a()时 a = 30,没有产生问题,第二种情况就产生了问题。

第二种情况:

文件一:

int a;			// 弱符号//global .bss段 *COM*块中
void set_a()	//global .text段 
{
    
    
    a = 30;		
}

文件二:

#include <stdio.h>

short a = 10;	//强符号//global .bss段 
short b = 10;	//global .data段
int main()
{
    
    
    set_a();
    printf("a = %d, b = %d\n", a, b);
    return 0;
}

结果:a = 30, b = 0

分析一下第二种情况:

文件一:set_a()中的语句

 a = 30; 

这句其对应编译后的汇编指令是

mov dword ptr[a], 1e
//但是符号还未确定,在链接阶段进行符号确定(符号重定位)

*解释:给四字节(dword)的a赋值0x1e(十进制30)

文件二:在链接阶段,确定(short)类型主函数中的a来执行mov dword ptr[a], 1e指令;此时4字节的1e赋值给2字节的a,因为2字节的a和2字节的b空间是连续的(如下图),并且以小端的方式进行存储。

在这里插入图片描述

这里不懂小端存储可以看这篇blog:

(判断计算机是大端存储还是小端存储(两种方法)_xiaoxiaoguailou的博客-CSDN博客

从图中看出a的值是0x001e,b的值被0x0000覆盖,所以得到a = 30, b = 0;(这里赋值越界但是可以运行是因为b的空间是合法已分配的空间)

总结:

解答一:.bss段缺少四字节原因:

int gc;

编译阶段变量gc未初始化是一个弱符号,汇编后暂时存放在*COM*模块中,不确定其他文件中强符号的位置。在链接阶段根据强弱符号规则,确定该符号是放在.bss段还是放在*COM*模块中;所以该程序的.bss段缺少该变量的大小4字节;

解答二:那么,既然.bss段是虚拟的,不存在,那初始化为0和未初始化的数据是怎么存放的呢?

程序在运行的时候调用头部里面.bss段的描述信息进行初始化,也就是说将.bss的信息简略的存放在了头部section header中;

结论三:在链接阶段合并各个段后,符号重定位发生,此时根据强弱符号规则,将弱符号存放在*COM*模块中;

-----------------------------------------------------------------END --------------------------------------------------------------

这块的知识比较抽象,上机实操的理解会更容易。

后期有时间会对编译阶段的词法分析、语句分析、语义分析和链接阶段的合并各个段的详细内容进行补全。。。。。。。。。。

Guess you like

Origin blog.csdn.net/xiaoxiaoguailou/article/details/120999122