记录学习自考课程编码13015学习过程,献给每一位拥有梦想的"带专人"
ps:有不正确的地方麻烦更新在评论区,我会一一修复 😅
第四章 可执行文件的生成与加载执行
可执行文件的生成
- 预处理 预处理器为
cpp- gcc -E hello.c -o hello1.i
- cpp hello1.c -o hello1.i
- 编译 编译器为
ccl- gcc -S hello.i -o hello1.s
- ccl hello1.i -o hello1.s
- 汇编 汇编器是
as- gcc -c hello1.s -o hello1.o
- as hello1.s -o hello1.o
- 通常把汇编生成的机器语言目标文件称为可重定位目标文件
- 链接 链接器是
ld- gcc helllo1.o hello2.o -o hello
- ld -o hello hello1.o hello2.o
汇编与链接的区别:
二者虽然生成的都是二进制文件,但是汇编生成的是可重定位文件,而链接生成的是可执行文件,所不同的是前者单个模块生成的,而后者是多个模块组合而成的。对于汇编代码总是从0开始,对于链接代码在ABI规范规定的虚拟地址空间中产生。
上述给出通过objdump -d test.o反汇编命令输出的结果包括指令的地址、机器代码和反汇编出来的汇编代码。可以看出,在可重定位文件 test.o中 add 函数的起始地址为 0,而在可执行文件 test 中 add 函数的起始地址是080483d4
每一个信息区称为一个节(sec-tion)、如代码节(.test)、只读数据节(.rodata)、已初始化全局数据节(.data)和未初始化全局数据节(.bss)
符号解析:符号解析的目的是将每个符号的引用与一个确定的符号定义建立关联。符号包括全局静态变量名和函数名,而非静态局部变量名则不是符号
重定位:可重定位文件中的代码区和数据区都是从地址 0 开始的,链接器需要将不同模块中相同的节合并起来生成一个新的单独的节,并将合并后的代码区和数据区按照 ABI 规范确定的虚拟地址空间划分(也称存储器映像)来重新确定位置。这种重新确定代码和数据的地址并更新指令中被引用的符号地址的操作称为重定位
目标文件格式
目标代码:编译器或汇编器处理源代码后生成的机器语言目标代码
目标文件:指存放目标代码的文件
目标文件格式:
- 通用目标文件格式 (COFF)unix 早期使用
- 可移植可执行格式(PE)Windows 系统使用
- 可执行可链接格式(ELF)Linux、BSD、现代Unix 使用
ELF 视图
- 链接视图(节):可重定位目标文件;主要由不同的
节组成,节是 ELF 文件中具有相同特征的最小可处理信息单元,不同的节描述了目标文件中不同类型的信息及其特征 - 执行视图(段):可执行目标文件;主要由不同的
段组成,描述了目标文件中的节如何映射到存储空间的段中,可以将多个节合并后映射到同一个段
可重定位目标文件格式
- ELF 头:位于目标文件的起始位置,包含文件结构说明信息
- .text:目标代码部分
- .rodata:只读数据,如 print 语句中的格式串
- .data:已初始化且初值不为 0 的全局变量和静态变量
- .bss:所有未初始化或初始化为 0 的全局变量和静态变量
- .symtab:符号表
- .rel.text:
.text节相关的可重定位信息 - .rel.data:
.data节相关的可重定位信息 - 节头表:由若干表项组成,每个表项描述相应节的节名、在文件中的偏移、大小、访问属性、对齐方式等,目标文件中每个节都有一个表项与之对应
可执行目标文件格式
与 ELF 可重定位文件格式相比,ELF 可执行文件的不同点主要有以下几个方面
ELF头中字段e_entry给出程序执行入口地址,可重定位文件中此字段为0.init和.fini节,其中.init节定义一个_init函数,用于可执行文件开始执行时的初始化工作;.fini节中包含进程终止时要执行的指令代码- 少了
.rel .text和.rel.data等重定位信息节。因为可执行文件中的指令和数据已被重定位,故可去掉用于重定位的节- 多了一个
程序头表,也称段头表,它是一个结构数组。可执行文件中所有代码位置连续,所有只读数据位置连续,所有可读可写数据位置连续。因而在可执行文件中,ELF 头、程序头表、.init节、.fini节、.text节和.rodata节合起来可构成一个只读代码段。.data节和.bss节合起来可以构成一个可读/写数据段。显然,在可执行文件启动运行时,这两个段必须分配存储空间并装入内存,因而称为可装入段
- 多了一个
可执行文件存储映像
可执行文件与虚拟地址空间之间的存储器映像
- 运行时堆:在可读/写数据段后面 4kb 对齐的高地址处,通过调用malloc()库函数动态向高地址分配空间
- 用户栈:从用户空间最大地址往低地址方向增长
- 共享库区域:堆区和栈区中间有一块空间保留给共享库目标代码
- 虚拟存储区:用户栈区以上的高地址区是操作系统内核的虚拟存储区
符号与符号表
- 全局符号:非静态的函数名和全局变量名
- 外部符号:由其他模块定义并被当前模块引用,包括外部函数名、外部变量名
- 本地符号:当前模块定义和使用,静态函数名和全局变量名(
静态数据区域,在.data或.bss节中分配空间) - 局部变量不是符号,不会记录在符号表中
表项
| 字段 | 字段说明 |
|---|---|
| st_name | 给出符号在字符串表中的索引(字节偏移量),指向在字符串表(.strtab节)中的一个以 null 结尾的字符串,即符号 |
| st_value | 给出符号的值,在可重定位文件中,是指符号所在位置相对于所在节起始位置的字节偏移量 在可执行目标文件和共享目标文件中,st_value 则是符号所在的虚拟地址 |
| st_size | 给出符号所表示对象的字节个数 |
| st_info | 符号类型(低四位) 未指定(NOTYPE);变量(OBJECT);函数(FUNC);节(SECTION) 和绑定属性(高四位) 本地(LOCAL);全局(GLOBAL);弱(WEAK) |
| st_other | 符号的可见性 |
| st_shndx | 符号所在节的节头表索引,其中有三种特殊伪节 不重定位 (ABS);未定义 (UNDEF);未初始化变量 COMMON |
例题:
代码和表格的对应关系
// main.c
extern void swap(void);
int buf[2] = {1,2};
int main() {
swap();
return 0;
}
// swap.c
extern int buf[];
int *bufp0 = &buf[0];
int *bufp1;
void swap() {
// 局部变量链接器不需要信息不会记录在符号表中
int temp;
bufp1 = &buf[1];
temp = *bufp0;
*bufp0 = *bufp1;
*bufp1 = temp;
}
| 符号(main.c) | 符号类型 | 绑定属性 | 所在节 |
|---|---|---|---|
| buf | OBJECT | GLOBAL | .data |
| main | FUNC | GLOBAL | .text |
| swap | NOTYPE | GlOBAL | |
| 符号(swap.c) | |||
| bufp0 | OBJECT | GLOBAL | .data |
| buf | OBJECT | GLOBAL | |
| swap | FUNC | GLOBAL | .text |
| bufp1 | OBJECT | GLOBAL | .bss |
符号解析和静态链接
符号解析的目的是将每个符号的引用与一个确定的符号定义建立关联。符号包括全局静态变量名和函数名,而非静态局部变量名则不是符号
全局符号包括:
- 强符号:
- 函数
.data节中有初始值的全局变量.bss节中初始化为 0 的全局变量
COMMON伪节的未初始化全局变量- 绑定属性为
WEAK的弱符号
同名全局符号处理规则
强符号不能多次定义,否则链接报错- 一次
强符号,多次COMMON符号或弱符号,以强符号为准 - 同时出现
COMMON和弱符号,以COMMON符号为准 - 一个
COMMON符号出现多次,以所占空间最大的为准 - 若是用
-fno-common选项,则将COMMON符号当做强符号
例题:
定义两个强符号的例子
main.c 中的 x 有初始值是
强符号,p1.c 中的 x 也有初始值是强符号,所以链接报错main.c 中的 y 没有初始值是
common符号,p1.c 中的 y 有初始值是 0 在.bss中,是强符号,所以以 p1.c 中的 y 为准
// main.c
int x=10; // 有初值是强符号
y; // y 没有初始值所以是 common 符号
int p1(void);
int main() {
x = p1();
return x;
}
// p1.c
int x = 20; // 有初值是强符号
int y = 0; // y 有初始值所以是强符号,在.bss 中 以p1.c 中为准
int p1() {
return x;
}
COMMON 符号定义的例子
main.c 中的 y 有初值,是
强符号,p1.c 中的 y 是全局变量但没有初值是common符号,所有以强符号为准main.c 中的 z 没有初值,是
common符号,在 p1.c 中也没有初值是common符号, 出现多个common符号以所占空间大的为准,int 为 32 位,short 是 8 位 所以以 main.c 中的为准
// main.c
#include <stdio.h>
int y=100,z;
void p1(void);
int main() {
z=1000;
p1();
printf("y=%d,z=%d\n",y,z);
return 0;
}
// p1.c
int y;
short z;
void p1() {
y = 200;
z = 2000;
}
解决同名全局符号引起的问题
- 避免使用全局变量,必须用的话,定义为
static静态变量,如果定义为静态变量,即使重名会将两个变量记录在不同的符号表,变为两个不同的符号 - 给全局符号赋初值,变成强符号
- 外部全局变量尽量使用
extern
静态链接
- 将多个目标模块以及其依赖的库在编译时进行合并过程,程序所必须的库函数和代码被打包到一个单独的可执行文件中,使得生成的可执行文件不需要依赖任何外部库文件
- 多个目标模块打包成一个单独的库文件,称为静态库
- 在类 Unix 系统中,静态库文件使用
存档档案的文件格式,用.a后缀 - 在 windows 系统中使用
.lib后缀表示静态库文件
动态链接:
在程序运行时将可执行文件与所需的库进行链接的过程,动态链接不会将所需的外部库文件嵌入在可执行程序中,而是保留这些库的引用,运行时,操作系统加载这些库并进行链接
前面说了可重定位和可执行两种目标文件,还有一类目标文件是共享目标文件,也称共享库文件
共享库文件:一种特殊的可重定位目标文件,其中记录了相应的代码、数据、重定位和符号信息表
- 在可执行文件装入或运行时,由
动态链接器把共享库文件动态装入内存并链接,这个过程叫动态链接 - 类 Unix 系统中共享库的扩展名是
.so,在 Windows 中是.dll
动态链接与静态链接的区别
- 静态库浪费主存和磁盘空间:静态链接的代码始终被合并到可执行文件
- 静态库更新困难,使用不便:静态链接需要定期维护更新静态库
- 动态库的共享性:代码在内存只有一个副本
- 动态库的动态性:使用的程序在执行时才会加载到内存
- 动态库的链接方式:程序加载过程中加载和链接、程序执行时加载和链接
重定位过程
在符号解析的基础上将所有关联的目标合并,并确定每个符号在虚拟地址空间中的地址,在引用处重定位引用地址
- 节和定义符号的重定位
- 引用处符号的重定位
程序和进程的概念
-
程序:代码+数据,
静态概念 -
进程:程序的运行过程,
动态概念 -
计算机处理的所有任务都是由
进程完成的 -
一个可执行文件可以多次加载,一个程序可以对应多个进程
-
可执行文件是通过加载器来启动的,Unix 通过
execve函数启动加载器 -
操作系统把进程中的所有存储区域信息记录在
进程描述符中 -
父进程通过
fork函数创建一个子进程 -
唯一的
正整数标识一个进程,叫做PID
在 Shell 输入可执行文件名a.out进行程序加载的过程
Shell输入提示符,接受用户输入命令- 用户输入
./a.out回车之后,Shell对命令进行解析,获取参数 - 调用
fork函数,创建子进程 - 调用
execve,在当前进程中加载并运行a.out
程序的执行和中央处理器
CPU执行指令的过程(CPU 取出并执行一条指令的时间称为指令周期)
- 取指令:即将要指令的指令地址在
程序计数器PC中 - 译码:对
指令寄存器 IR中的指令操作码进行译码 - 源操作数地址计算并取出操作数
- 执行数据操作(对操作数进行运算)
- 目的操作数地址计算并存结果
- 指令地址计算并将其送到
PC中
指令功能的基本操作
- 读取存储单元内容(内存),并将其装入某个寄存器
读内存 - 把寄存器中的数据送到给定的存储单元
写内存 - 把数据从一个寄存器送到另外一个寄存器
传送指令 - 在算数逻辑部件 ALU 中进行算数/逻辑计算,并把结果送到某个寄存器
CPU 基本组成
- 程序计数器 PC:又称
指令计数器或指令指针寄存器(IP),用来存放即将执行指令的地址 - 指令寄存器 IR:用来存放现行指令
- 指令译码器 ID:ID 对 IR 中操作码部分进行译码,产生的译码信号要提供给操作控制信号形成以产生控制信号
- 启停控制逻辑:脉冲源产生一定频率的脉冲信号作为 CPU 的
时钟信号。启停控制逻辑在需要时能保证可靠地开放或封锁时钟信号,实现对机器的启动与停机 - 时序信号产生部件:以
时钟信号为基础,产生不同指令对应的时序信号,以实现机器指令执行时的时序控制 - 操作控制信号形成部件:该部件综合时序信号、指令译码信号和执行部件反馈的条件标志(如 CF、SF、ZF 和 OF)等,形成不同指令操作所需要的控制信号
- 总线控制逻辑:实现对总线传输的控制
- 中断机构:实现对异常情况和外部中断请求的处理
打断程序正常执行的事件
从开机后 CPU 被加电开始,到断电为止,CPU 自始至终就一直在重复一件事:读出 PC 所指存储单元的指令并执行它。每条指令的执行都会改变 PC 的内容,因而 CPU 能够不断地执行新的指令
正常情况下,CPU 按部就班地按照程序规定的顺序一条指令接着一条指令执行,或者按顺序执行,或者跳转到跳转类指令设定的跳转目标指令处执行,这两种情况都属于正常执行顺序
以下事件会打断程序的正常执行
- 非法操作码:对指令操作进行译码时,发现是不存在的
非法操作码,因此,CPU 不知道如何实现当前指令而无法继续执行 - 页故障:在访问指令或数据时,发现
页故障,如段错误、缺页,因此,CPU 没有访问到正确的指令或数据无法继续执行当前指令 - 运算结果溢出:在 ALU 中运算的结果发生
溢出,或者整数除法指令的除数为 0等,因此,CPU 发现运算结果不正确而无法继续执行程序 - 收到中断请求信号:程序在执行过程中,CPU 接收到外部发送来的中断请求信号
CPU 除了能够正常地不断执行指令以外,还必须具有程序正常执行被打断时的处理机制,这种机制称为异常控制也称中断机制,CPU 中相应的异常和中断处理逻辑被称为中断机构
打断程序正常执行的事件被分为两大类:
- 内部异常:是指由 CPU 在执行某条指令时引起的与该指令相关的意外事件。如除数为 0、结果溢出、断点、单步跟踪、寻址错、访问超时、非法操作码、栈溢出、缺页、地址越界(段错误)等
- 外部中断:程序执行过程中,若 CPU外部发生了采样计时时间到、网络数据包到达、用户按下
Ctrl + C等外部事件,要求 CPU 中止当前程序的执行,则会向 CPU 发中断请求信号,要求 CPU 对这些情况进行处理。通常,每条指令执行完后,CPU 都会主动去查询有没有中断请求,有的话,则将下一条指令地址作为断点保存,然后转到用来处理相应中断事件的中断服务程序去执行,结束后回到断点继续执行。这类事件与执行的指令无关,由 CPU 外部的I/O子系统发出,所以,称为I/O中断或外部中断,需要通过外部中断请求线向 CPU 发请求信号
CPU对异常和中断的响应过程可分为以下步骤
-
保护断点和程序状态:异常/中断处理后可能要回到原被中断的程序继续执行,因此必须保存并恢复到中断时原程序的状态。每个正在运行程序的状态信息称为
程序状态字通常存放在程序状态字寄存器如果 IA-32 中程序状态字寄存器就是
标志寄存器 EFLAGS -
关中断:如果中断处理程序在保存原被打断程序现场的过程中又发生了新的中断,那么,就会因为要处理新的中断,而破坏原被打断程序的现场以及已保存的断点和程序状态等,因此需要一种机制来禁止在处理中断时再响应新的中断。通常通过设置
中断使能位来实现。当中断使能位被置1,则为开中断,表示允许相应中断;若中断使能位被清0,则为关中断,表示不允许响应中断 -
识别异常和中断事件并转相应的处理程序
指令流水线的基本概念
CPU 设计中最关键的思路之一是让指令在 CPU 中按流水线方式执行
指令流水线:将 CPU 执行指令的各个阶段看成相应的流水段,指令执行过程就构成了指令流水线
指令流水线的四个阶段:
- 取指令并使 PC 加 1(IF):根据 PC 的值从存储器取出指令,并 PC PC + 1
- 译码并读寄存器(ID):对指令操作码进行译码并生成控制信号,同时读取寄存器 rs 和 rt 的内容
- 运算或读存储器(EX):在 ALU 中对寄存器操作数进行运算,或者根据 addr 读存储器
- 结果写回(WB):将结果写入摸底存储器 rt,或写入主存储单元 addr 中
进入流水线的指令流,由于后一条指令的第 i 步与前一条指令的第 i+1 步同时进行,从而使一串指令的总处理时间大为缩短。
在理想状态下,完成 4 条指令的执行只用了 7 个时钟周期,若采用非流水方式的串行执行处理,则最多需要 16 个时钟周期
若流水段数为 M,每个流水段的执行时间为 T,则理想情况下,N 条指令的执行总时间为
例题:
设译码时间为 60、存储器读写为 200、PC+1 为 40、寄存器读写为 50、ALU 为 100,分别计算串行执行和流水线执行下指令的时间
add r0,r1;
mov r1,r0;
load ro,6#;
store 8#,r0;
串行:
- 加法指令:
- r0 与 r1 相加结果存在 r1 中
- 200 + 60 + 100 + 50 = 410
- 取指令 + 译码并读寄存器 + 运算或读存储器 + 寄存器读写
- 传送指令:
- 将 r0 传到 r1
- 200 + 60 + 50 = 310
- 取指令 + 译码并读寄存器 + 寄存器读写
- 读数据:
- 从 6 号主存单元读数据到 r0 中
- 200 + 60 + 200 + 50 = 510
- 取指令 + 译码并读寄存器 + 取内存 + 寄存器读写
- 存指令:
- 将 r0 的数存到内存中第 8 号主存单元
- 200 + 60 + 200 = 460
- 取指令 + 译码并读寄存器 + 写 8 号存储单元
410 + 310 + 510 + 460 = 1690
流水线:
段数:以最复杂指令为准,这里最复杂指令为 load 指令 所以是 4 段
每段时间:以最复杂的操作需要的时间为准,这里最复杂的是存储器读写操作 所以每段时间 200
练习
-
名词解释:
- 符号解析:符号解析的目的是将每个符号的引用与一个确定的符号定义建立关联。符号包括全局静态变量名和函数名,而非静态局部变量名则不是符号
- 重定位:重新确定代码和数据的地址并更新指令中被引用符号地址的操作
- 静态链接:将多个目标模块以及其依赖的库在编译时进行合并过程,程序所必须的库函数和代码被打包到一个单独的可执行文件中,使得生成的可执行文件不需要依赖任何外部库文件
- 动态链接:在程序运行时将可执行文件与所需的库进行链接的过程,动态链接不会将所需的外部库文件嵌在可执行程序中,而是保留这些库的引用,运行时,操作系统加载这些库并进行链接
- 共享库文件:一种特殊的可重定位文件其中记录了相应的代码、数据、重定位和符号信息表
- 指令周期:CPU 取出一条指令的时间
- 进程:程序的一次运行过程,进程有自己的生命周期,由任务的启动而创建,随着任务的完成而终止,所使用资源也随着进程的终止而释放
- 中断:程序正常执行被打断时的处理机制
- 指令流水线:如果将 CPU 执行指令的各个阶段看成流水段,那么指令执行过程就构成了指令的流水线
-
简述静态链接和动态链接的区别:
- 静态库浪费主存和磁盘空间,静态链接会将外部库文件与源代码统一合并到一个可执行文件中
- 静态库更新困难,使用不便,静态链接需要定期维护
- 动态库的共享性,代码在内存只有一个副本
- 动态库的动态性,使用的程序在执行时才会加载到内存
- 动态库的链接方式,程序加载过程中加载和链接,程序执行时加载和链接
-
简述通过 shell 命令行解释程序加载过程
- shell 输入提示符,接受用户输入的命令
- 用户输入命令回车后,shell 对命令进行解析,获取参数
- 调用 fork 函数,创建子进程
- 调用execve,在当前进程上下文中运行程序
-
假设一个 C 语言源程序有两个源文件:main.c 和 swap.c,其中,main.c 和 swap.c 的内容如下
// main.c
extern void swap();
int buf[2] = {1,2};
int main() {
swap();
return 0;
}
// swap.c
extern int buf[];
int *bufp0 = &buf[0];
int *bufp1;
static void incr() {
static int count = 0;
count++;
}
void swap() {
int temp;
incr();
bufp1 = &bufp[1];
temp = *bufp0;
*bufp0 = *bufp1;
*bufp1 = temp;
}
对于编译生成的可重定位目标文件 swap.o,填写表中各符号的情况,说明每个符号是否出现在swap.0的符号表(.symtab节)中,如果是,定义该符号的模块是main.o还是 swap.o、该符号的类型相对于swap.o是全局、外部还是本地符号、该符号出现在swap.o中的哪个节(.text、.data、.bss)或哪个特殊伪节(ABS、UNDEF 或 COMMON)中
| 符号 | swap.o的符号表中 | 定义模块 | 符号类型 | 节 | 解读 |
|---|---|---|---|---|---|
| buf | 在 | main.o | extern | undef | 在swap.c中声明为extern,表示在其他模块中定义,在 swap.o中未定义 UNDEF |
| bufp0 | 在 | swap.o | GLOBAL | .data | 在 swap.c中定义为全局指针,并且初始化所以在.data节 |
| bufp1 | 在 | swap.o | GLOBAL | COMMON | 在swap.c中定义为全局指针,没有初始化,所以是.bss,但是题目中说了特殊伪节所以是COMMON |
| incr | 在 | swap.o | LOCAL | .text | 有 static 标识是静态函数,所以是 LOCAL 本地符号,属于代码部分 .text节 |
| count | 否 | swap.o | LOCAL | .bss | 由于它是静态局部变量,因此确实会出现在符号表中,但是作为本地符号(本地于incr函数),虽然初始化,但是初始化值为 0所在存在于.bss节 |
| swap | 在 | swap.o | GLOBAL | .text | 在 swap.c 中定义的全局函数,是GLOBAL,属于代码部分 .text节 |
| temp | 否 | \ | \ | \ | 在 swap 函数内定义为静态变量,不出现在符号表中。 |
-
给出了两个源程序文件,它们被分别编译生成可重定位目标模块m1.o和m2.o。在模块 mj 中对符号 x 的任意引用与模块 mi 中定义的符号 x 关联记为REF(mj.x) DEF(mi.x)。请在下列空格处填写模块名和符号名,以说明给出的引用符号所关联的定义符号,若发生链接错误则说明其原因;若从多个定义符号中任选则给出全部可能的定义符号,若是局部变量则说明不存在关联
// m1.c int p1(void); int main() { int p1 = p1(); return p1; }// m2.c static int main = 1; int p1() { main++; return main; }-
REF(m1.main) DEF(不存在关联)
m1中的 main 函数与m2中的main变量不产生关联
-
REF(m2.main) DEF(m2.main)
m2中的 main 关联的是m2中的静态变量main
-
REF(m1.p1) DEF(m2.p1)
p1 函数在m1中声明并引用,在m2中定义,所以m1中的p1函数关联的是m2中定义的p1函数
-
REF(m2.p1) DEF(不存在关联)
m2中没有使用p1
-
捏捏捏捏捏捏捏捏捏捏捏
由于后续视频课程为付费课程,观看方式为萝卜 Bro 发送的腾讯会议链接无法分享给大家,所以还是建议大家去购买正版课程 😊😊