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

前言

  • 先了解一下内存布局;
  • 用户空间是独立的,内核空间是共享的;
    在这里插入图片描述

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的工具;
    在这里插入图片描述

【语义分析】

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

在这里插入图片描述

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寄存器里面;

在这里插入图片描述

3.3 追踪程序查看

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
程序员自我修养:链接,装载与库》是一本由林锐、郭晓东、郑蕾等人合著的计算机技术书籍,在该书中,作者从程序员的视角出发,对链接、装载与库等概念进行了深入的阐述和解析。 在计算机编程中,链接是指将各个源文件中的代码模块组合成一个可执行的程序的过程链接可以分为静态链接和动态链接两种方式。静态链接是在编译时将所有代码模块合并成一个独立的可执行文件,而动态链接是在运行时根据需要加载相应的代码模块。 装载是指将一个程序从磁盘上加载到内存中准备执行的过程。在装载过程中,操作系统会为程序分配内存空间,并将程序中的各个模块加载到相应的内存地址上。装载过程中还包括解析模块之间的引用关系,以及进行地址重定位等操作。 库是指一组可重用的代码模块,通过链接和装载的方式被程序调用。库可以分为静态库和动态库。静态库是在编译时将库的代码链接到程序中,使程序与库的代码合并为一个可执行文件。动态库则是在运行时通过动态链接的方式加载并调用。 《程序员自我修养:链接,装载与库》对于理解链接、装载和库的原理和机制具有极大的帮助。通过学习这些概念,程序员可以更好地优化代码结构和组织,提高程序的性能和可维护性。同时,了解链接、装载和库的工作原理也对于进行调试和故障排除具有重要意义。 总之,链接、装载与库是计算机编程中的重要概念,对于程序员来说掌握这些知识是非常必要的。《程序员自我修养:链接,装载与库》这本书提供了深入浅出的解释和实例,对于想要学习和掌握这些知识的程序员来说是一本非常有价值的参考书籍。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jxiepc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值