文章目录
前言
- 先了解一下内存布局;
- 用户空间是独立的,内核空间是共享的;
1 编译和链接
构建:编译
和链接
合并的过程;
- 一个
gcc main.c
可被分解为预处理、编译、汇编、链接;
1.1 预编译
gcc -E main.c -o main.i
使用-E
生成.i
文件;- 将所有
#define
删除,并展开宏定义; - 处理
条件预编译
指令; - 处理
#include
预编译指令,将包含的头文件插入(递归进行); - 删除所有
注释
; - 添加
行号
和文件名
标识,便于调试用; - 保留所有
#pragma
指令;
- 将所有
1.2 编译
gcc -S main.c -o main.s
将.i
文件进行词法
分析、语法
分析、语义
分析及优化产生汇编代码;- 现在版本GCC将预编译和编译合并在一起,为一个
cc1
的程序; - 汇编器
as
、链接器ld
、预编译编译程序cc1
;
【词法分析】
- 源代码输入到
扫描器
中,通过类似有限状态机
将源代码字符序列分割成一系列记号
; - 记号一般分为:
关键字
、标识符
、数字
、字符串
等和特殊符号
; - 通过
lex
的程序实现词法扫描; - 预处理的语言交给预处理器;
如:array[index] = (index + 4) * (2 + 6)
记号 | 类型 |
---|---|
array | 标识符 |
[ | 左方括号 |
index | 标识符 |
] | 右方括号 |
= | 赋值 |
( | 左圆括号 |
index | 标识符 |
+ | 加号 |
4 | 数字 |
) | 右圆括号 |
* | 乘号 |
( | 左圆括号 |
2 | 数字 |
+ | 加号 |
6 | 数字 |
) | 左圆括号 |
【语法分析】
- 将产生的记号进行语法分析,产生语法树(以表达式为节点的树),采用上下文
无关
语法; - 使用
yacc
的工具;
【语义分析】
- 由
语义
分析器来完成,对表达式的语法层面的分析,无法判断语法的合法性; - 静态语义:编译器确定的语义,通常包括
声明
和类型的匹配
、类型的转换
; - 动态语义:只有在
运行期
才能确定的语义,一般在运行期出现的语义相关的问题; - 分析后,语法树的表达式都被标识了类型,若要
隐式转换
,则会在语法树中插入相应的转换节点
;
1.3 汇编
as main.s -o main.o/gcc -c main.s -o main.o
将汇编代码转成机器指令,使用as来完成,输出目标文件;
【中间语言生成】
源码级优化器
对源码进行优化,将语法树转成中间代码
(语法树的顺序表示);- 中间代码有多种类型,一般使用
三地址码
; - 中间代码让编译器分为
前端
和后端
,对于不同的平台可针对不同机器平台的多个后端;- 前端:负责产生机器无关的中间代码;
- 后端:将中间代码转换成目标机器代码;
// 根据上述代码,三地址码(x = y op z)可翻译为
t1 = 2 + 6;
t2 = index + 4;
t3 = t2 * t1;
array[index] = t3;
// 优化后
t2 = index + 4;
t2 = t2 * 8;
array[index] = t2;
【目标代码生成与优化】
- 生成
中间代码
后,后续的过程都为编辑器后端操作; - 主要包括
代码生成器
和目标代码优化器
;- 代码生成器:将
中间代码
转换成目标机器
代码,该过程依赖于目标机器(不同的机器不同的寄存器、字长、数据类型等); - 目标代码优化器:对上述代码进行
优化
,选择合适的寻址
方式、位移
代替乘法等;
- 代码生成器:将
- 至此生成目标代码,但此时上述代码中的index、array还没有地址;
1.4 链接
作用:
- 合并所有的目标文件,并调整段的偏移和段长度,合并符号表,进行符号解析;
- 符号重定位;
符号:用来表示一个地址
,可能是一段子程序的起始地址,也可以是变量的起始地址;
重定位:编译文件A时,不知道var的目标地址,故编译器无法确定地址,即先将其地址置为0,等待B(var在此定义)链接后,var的地址才被确定下来,在将其修改,该修改过程被称为重定位;
链接解决程序被分割为多个模块后,模块间如何通信,如何形成单一程序
- 模块间的
函数调用
; - 模块间的
变量访问
; - 访问必须要知道访问的目的地址——模块间
符号的引用
; - 模块间依靠
符号
来通信
;
- 若main.c中使用到func.c中的foo函数,则在调用处必须知道函数
foo的地址
,但每个模块都是单独编译
的,而在编译main.c中并不知道foo函数地址;所有他会暂时将foo的地址搁置
,等最后链接
的时候会根据所引用固定符号
,自动取相应的模块中找到foo的地址并修正
;
2 目标文件
- 源码编译后但未进行链接的中间文件,包括代码,数据,链接时信息(符号表、调试信息、字符串等);
- 目标文件将信息按不同的属性以节(段)的形式存储,表示一定长度的区域;
ELF文件类型 | 说明 | 实例 |
---|---|---|
可重定位文件 | 包含文code和data,可被用来链接生成可执行文件或共享目标文件,也可未静态链接库 | Linux的.o win的.obj |
可执行文件 | 包含可执行程序 | /bin/bash文件 win的.exe |
共享目标文件 | 包含code和data,在链接器下可使用跟其他的可重定位文件和共享文件链接,产生新的目标文件或动态链接器可将几个这种共享目标文件与可执行文件结合,作为进程映像的一部分来运行 | Linux的.so win的DLL |
核心转储文件 | 当进程意外终止时,系统可将该进程的地址空间的内存及终止时的其他信息转储到文件 | Linux下的core dump |
2.1 格式
- win下的PE,
.obj
文件; - Linux下的ELF,
.o
文件; - 都是由COFF的变种;
- 在Linux下可使用file命令查看文件类型;
2.2 目标文件内部
- 源码存放在代码段
.code或.text
; - 全局变量和局部静态变量放在数据段
.data
和.bss
;
为什么将code和data分开存放
- 为了防止程序的指令被改写,code和data分别映射到两个虚存区域,数据区域为可读写,而指令区域为只读,分别设置为不同权限;
- 提高程序缓存的命中率,分开存放有利于提高程序的局部性;
- 共享指令,当多个程序运行时,可只使用该一份程序指令节省空间,而数据是私有的;
2.3 挖掘目标文件
【size查看ELF文件的大小】
2.4 代码段
【objdump】:
-s:将所有段的内容以十六进制打印出来;
-d:将所有包含指令的段反汇编;
提取代码段内容
2.4 数据段和只读数据段
global_init_varabal和static_var一共为8字节,故.data段为8字节;
call printf时用到"%d\n"是一个只读数据,放到.rodata段为4字节是该字符串常量的ASCII字节序;
2.4 BSS段
上述中的global_uninit_var和static_var2存放在该段中,预留了空间,但该段大小有8字节;
但有些全局未初始化的变量没有被存放在.bss段中,只是预留一个未定义的全局变量,等链接成功后在为其在该段中分配空间;
由于它是弱符号,可能是外部引用的变量,故等到链接的时候在分配空间;
【注意】:以下情况下static变量一个在.data段一个在.bss段;
2.4 其他段
段名 | 说明 |
---|---|
.rodata | 只读数据(字符串常量、全局const) |
.comment | 编译器版本信息 |
.debug | 调试信息 |
.dynamic | 动态链接信息 |
.hash | 符号哈希表 |
.line | 调试时的行号表 |
.note | 额外的编译器信息 |
.strtab | 字符串表 |
.symtab | 符号表 |
.shstrtab | 段名表 |
.plt .got | 动态链接的跳转表和全局入口表 |
.init .fini | 程序初始化与终结代码段 |
- 以
.
为前缀表示系统保留的,程序可使用非系统保留的名字作为段名
【如何将二进制文件作为目标文件中的一个段】
- 使用
objcopy
;
objcopy -I binary -O elf32-i386 -B i386 images.jpg main.o
2.5 ELF文件结构描述
2.5.1 文件头
- 用readelf来详细查看ELF文件,该结构一般被定义在/usr/include/elf.h;
【e_type】:文件类型;
- ET_REL(值1):可重位文件
.o
文件; - ER_EXEC(值2):可执行文件;
- ET_DYN(值3):共享目标文件
.so
文件;
2.5.2 段表
- 保存段的基本属性结构,描述各段的信息,编译器、链接器、装载器都由段来定位和访问各个段的属性;
- 段表在ELF文件中的位置由ELF文件头的e_shoff成员决定;
【段的类型】
【段的标志位】:该段在进程虚拟地址空间中的属性;
【段的链接信息】
2.5.3 字符串表
段名为.strtab或.shstrtab
保存段表中字符串常量;
2.6 链接的接口
- 在链接中,每个函数和变量都有自己独自的名字,将函数和变量统称为符号,函数名或变量名为符号名;
- 链接是基于符号完成的;
- 符号表:每个目标文件都有相应的,记录该目标文件中用到的所有符号;
- 符号值:每个符号都有相应值,即函数或变量的地址;
符号表类型分类
- 全局符号:可被其他文件引用;
- 外部符号:全局符号,但没有定义在本目标文件;
- 段名:编译器产生,值为该段的起始地址;
- 局部符号:只在编译单元内部可见;
- 行号信息:目标文件指令与源码中对应关系;
查看链接符号结果
2.7 ELF符号表结构
符号类型和绑定信息
- 低4位位符号的类型,高28位标识符号绑定信息;
符号所在段
- 若符号定义在本目标文件中,则该成员标识符号所在的段在段表中的下标,若不是,则是特殊值;
- 第一列为符号表数组的下标;第二列为符号值;第三列为符号大小;第五列为绑定信息;第七列为符号所属段,最后一列为俗符号名称;
2.8 符号修饰与函数签名
- 符号修饰:防止符号名冲突;
- 名称空间:防止多模块的符号冲突问题;
- 函数签名:包含一个函数信息(函数名、参数类型、所在的类、名称空间及其他信息);
C++符号修饰
- C++拥有类、继承、虚机制、重载、名称空间等;让符号管理更加复杂;
- 编译器在处理符号时,使用某个名称修饰,使得每个函数签名对应的一个修饰后名称;在编译成目标代码后,会将函数和变量的名字进行修饰,形成符号名;
- 不同编译器会采用不同的名字修饰;
1 int func(int);
2 float func(float);
3
4
5 class C {
6 int func(int);
7 class C2 {
8 int func(int);
9 };
10 };
11
12 namespace N {
13 int func(int);
14
15 class C{
16 int func(int);
17 };
18 }
2.9 extern “C”
- 使用该方法,C++的名称修饰机制不会起作用;
- 如:当使用到C文件的函数时,编译器会认为是一个C++函数,C++编译器会将该函数进行符号修饰,故编译器无法链接C语言库的memset符号;
2.10 弱符号与强符号
- 目标文件A、B都定义了全局同型变量,都被初始化,则A、B链接时会报错;
- 强符号:编译器默认函数和初始化了全局变量;
- 弱符号:为初始化的全局变量;可通过
__attribute__((weak))
来指定;- 弱符号被用户定义的强引用符号覆盖,从而使用自定义的库函数;
- 强引用:链接器会报符号未定义错误;
全局符号
不允许强符号被多次定义
,若有多个强符号定义,则链接器会出现重复定义错误;- 若一个符号在某个目标文件中是强符号,在其他文件都是弱符号,则下选择
强符号
; - 若一个符号在所有目标文件中都是弱符号,则选择其中
占用空间最大
的一个;
3 可执行文件
- 使用
ld -e main *.o
进行链接; - 查看符号表、各段信息;
3.1 重定位
3.2 程序运行的步骤
- 创建虚拟地址空间到物理内存的映射,创建页目录和页表;
- 加载代码段和数据段;
- 把可执行文件的入口地址写到CPU的pc寄存器里面;