【程序员的自我修养】|【02】编译链接过程、目标文件解析

发布于:2022-10-16 ⋅ 阅读:(520) ⋅ 点赞:(0)

1 编译和链接

构建编译链接合并的过程;

  • 一个gcc main.c可被分解为预处理、编译、汇编、链接;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qR5aPiAL-1665713974431)(C:\Users\YuFei\AppData\Roaming\Typora\typora-user-images\image-20221014093312872.png)]

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SrpfwZ2w-1665713974433)(C:\Users\YuFei\AppData\Roaming\Typora\typora-user-images\image-20221014101321120.png)]
【词法分析】

  • 源代码输入到扫描器中,通过类似有限状态机将源代码字符序列分割成一系列记号
  • 记号一般分为:关键字标识符数字字符串等和特殊符号
  • 通过lex的程序实现词法扫描;
  • 预处理的语言交给预处理器;

如:array[index] = (index + 4) * (2 + 6)

记号 类型
array 标识符
[ 左方括号
index 标识符
] 右方括号
= 赋值
( 左圆括号
index 标识符
+ 加号
4 数字
) 右圆括号
* 乘号
( 左圆括号
2 数字
+ 加号
6 数字
) 左圆括号

【语法分析】

  • 将产生的记号进行语法分析,产生语法树(以表达式为节点的树),采用上下文无关语法;
  • 使用yacc的工具;
    在这里插入图片描述

【语义分析】

  • 语义分析器来完成,对表达式的语法层面的分析,无法判断语法的合法性;
  • 静态语义:编译器确定的语义,通常包括声明和类型的匹配、类型的转换
  • 动态语义:只有在运行期才能确定的语义,一般在运行期出现的语义相关的问题;
  • 分析后,语法树的表达式都被标识了类型,若要隐式转换,则会在语法树中插入相应的转换节点

在这里插入图片描述
【中间语言生成】

  • 源码级优化器对源码进行优化,将语法树转成中间代码(语法树的顺序表示);
  • 中间代码有多种类型,一般使用三地址码
  • 中间代码让编译器分为前端后端,对于不同的平台可针对不同机器平台的多个后端;
    • 前端:负责产生机器无关的中间代码;
    • 后端:将中间代码转换成目标机器代码;
// 根据上述代码,三地址码(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.3 汇编

  • as main.s -o main.o/gcc -c main.s -o main.o将汇编代码转成机器指令,使用as来完成,输出目标文件;

1.4 链接

符号:用来表示一个地址,可能是一段子程序的起始地址,也可以是变量的起始地址;

程序被分割为多个模块后,模块间如何通信,如何形成单一程序

  • 模块间的函数调用
  • 模块间的变量访问
  • 访问必须要知道访问的目的地址——模块间符号的引用
  • 模块间依靠符号通信
    在这里插入图片描述
    【静态链接】
    链接:将每个源代码模块独立地编译;
  • 地址和空间分配、符号决议(地址绑定)、重定位;

在这里插入图片描述

  • 若main.c中使用到func.c中的foo函数,则在调用处必须知道函数foo的地址,但每个模块都是单独编译的,而在编译main.c中并不知道foo函数地址;所有他会暂时将foo的地址搁置,等最后链接的时候会根据所引用固定符号,自动取相应的模块中找到foo的地址并修正

重定位

  • 编译文件A时,不知道var的目标地址,故编译器无法确定地址,即先将其地址置为0,等待B(var在此定义)链接后,var的地址才被确定下来,在将其修改,该修改过程被称为重定位;

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:将所有包含指令的段反汇编;

提取代码段内容

在这里插入图片描述
数据段和只读数据段

global_init_varabal和static_var一共为8字节,故.data段为8字节;
call printf时用到"%d\n"是一个只读数据,放到.rodata段为4字节是该字符串常量的ASCII字节序;

在这里插入图片描述
BSS段

上述中的global_uninit_var和static_var2存放在该段中,预留了空间,但该段大小有8字节;
但有些全局未初始化的变量没有被存放在.bss段中,只是预留一个未定义的全局变量,等链接成功后在为其在该段中分配空间;

在这里插入图片描述
【注意】:以下情况下static变量一个在.data段一个在.bss段;
在这里插入图片描述
其他段

段名 说明
.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符号;

弱符号与强符号

  • 目标文件A、B都定义了全局同型变量,都被初始化,则A、B链接时会报错;
  • 强符号:编译器默认函数和初始化了全局变量;
  • 弱符号:为初始化的全局变量;可通过__attribute__((weak))来指定;
    • 弱符号被用户定义的强引用符号覆盖,从而使用自定义的库函数;
  • 强引用:链接器会报符号未定义错误;

全局符号

  • 不允许强符号被多次定义,若有多个强符号定义,则链接器会出现重复定义错误;
  • 若一个符号在某个目标文件中是强符号,在其他文件都是若符号,则下选择强符号;
  • 若一个符号在所有目标文件中都是弱符号,则选择其中占用空间最大的一个;
本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到