一、请问一个或多个c/c++源文件是如何生成一个可执行的二进制文件呢?
在我们写好代码后,源文件需要经过:预编译、编译、汇编、链接四个步骤对我们的源文件进行加工处理,才变成可执行二进制文件。
下面我对这四个阶段进行一一解释:
-
预编译(预编译器:简单的增删、替换)
-
头文件递归展开:把#include<头文件>在当前文件递归(该头文件包含其他头文件)展开
-
宏替换:#define替换
-
删除预处理指令:删除下表指令
指令 说明 # 空指令,无任何效果 #undef 取消已定义的宏 #if 如果给定条件为真,则编译下面代码 #ifdef 如果宏已经定义,则编译下面代码 #ifndef 如果宏没有定义,则编译下面代码 #elif 如果前面的#if给定条件不为真,当前条件为真,则编译下面代码 #endif 结束一个#if……#else条件编译块 - 删除注释: // 、/**/
- 添加行号,文件标识:添加行号是为了为编译阶段进行语法复分析出现错误提供行号
- 保留#pragma:为编译器保留的
-
-
编译(编译器)
- 词法分析:分析标识符是否合法int 1a = 10;(err)
- 语法分析:后期补充
- 语义分析:后期补充
- 代码优化:后期补充
- 生成汇编指令
-
汇编(汇编器)
- 翻译指令:将汇编指令翻译成二进制指令,生成.o/.obj文件
-
链接(链接器)
-
合并各个段(链接器完成)
-
符号重定位
-
接下来让我们分析一段代码:
#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++没有)
强弱符号规则:
- 两个强符号:编译器报错,该变量重复定义
- 一个强符号,一个弱符号:选择强符号
- 两个弱符号:不同编译器不同结果,有的直接报错,有的选择以编译的第一个文件中定义的变量
简单看个程序就明白了,下面这是一个工程下的两个文件
第一种情况:
文件一:
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 --------------------------------------------------------------
这块的知识比较抽象,上机实操的理解会更容易。
后期有时间会对编译阶段的词法分析、语句分析、语义分析和链接阶段的合并各个段的详细内容进行补全。。。。。。。。。。