记录学习自考课程编码13015学习过程,献给每一位拥有梦想的"带专人"
ps:有不正确的地方麻烦更新在评论区,我会一一修复 😅
第三章 程序的转换及机器级表示
程序转换
-
C 语言、汇编语言、机器语言的关系
- 指令的类型
- 伪指令:包含多个机器指令的一个序列属于软件范畴
- 机器指令:包含多个机器指令
- 微指令:属于硬件范畴
- 机器级指令
- 机器指令(0/1 序列)
- 汇编指令(符号表示、助记符)
- 指令的类型
-
机器指令的格式
-
操作码
-
寻址方式
-
寄存器编号
-
立即数(位移量,1B/2B/4B)
-
| 100010 D W | mod | reg | r/m | disp8 |
| 100010 0 0 | 01 001 001 | 11111010 | ||
寄存器传送语言
M[R[bx] + R[di]-6] R[cl]
将 Cl寄存器的内容传送到一个存储单元中,该存储单元的有效地址计算方法为 BX 和 DI 两个寄存器的内容相加再减6
汇编指令的表示
-
Intel 格式
mov [bx + di - 6], cl
-
AT&T格式(本课程使用)
movb %cl, -6(%bx , %di)
AT&T格式相对于 Inter 格式的区别
- 指令名后的字母表示操作数的大小
- 寄存器名前使用 % 前缀,立即数前使用$前缀
- 内存引用使用括号而不是方括号
- 在有偏移的内存引用中,基址、索引、比例和偏移量的顺序与 Intel 格式不同
- AT&T 格式源操作数在目标操作数之前
Intel 格式: 目标操作数 源操作数
指令集体系结构 ISA 规定的内容
- 指令格式、操作类型
- 操作数的类型
- 寄存器的名称、编号、长度、用途等
- 操作数能存放的存储空间大小和编址方式
- 大端序还是小端序
- 寻址方式
- 指令执行过程的控制方式,包括程序计数器等
高级语言转换成机器语言
- 预处理成hello.i(源码)
- 编译成汇编语言hello.s(源码)
- 汇编成可重定位目标文件(二进制)
- 链接成可执行目标文件(二进制)
gcc 生成机器代码
- 一步到位:gcc -o1 hello1.c hello2.c -o hello
- -o1 表示一级优化
- 将hello1.c hello2.c 一起编译为 可执行程序 hello
- 预处理:gcc -E hello1.c -o hello1.i
- 编译:gcc -S hello1.i -o hello1.s
- 汇编:gcc -c hello1.s -o hello1.o
- 链接:gcc hello1.o hello2.0 -o hello
指令系统风格
-
CISC 风格指令系统:随着
VLSI(超大规模集成电路)技术的迅速发展,计算机硬件成本不断下降,软件成本不断上升。为此,人们在设计指令系统时增加了越来越多功能强大的复杂指令,以使指令的风格接近高级语言语句的功能,这类计算机称为复杂指令集计算机 CISC,本课程的 Intel x86指令系统就是典型的CISC架构 -
RISC 风格指令系统:RISC 的着眼点不是简单地放在简化指令系统上,而是通过简化指令计算机结构更佳简单合理,从而提高机器的性能。
与
CISC相比- 指令数目少
- 指令格式规整,采用定长指令字方式,操作码和操作数地址等字段的长度固定
- 只有
Load/Store指令中的数据需要访存,这种称为Load/Store型指令风格 - 采用大量通用寄存器
汇编指令
AT&T:move %cl, -6(%bx,%di)
- 寄存器内容:%+名字,如 %ebp
- RTL:R[ebp]
- 存储器内容:偏移量(基址寄存器、变址寄存器、比例因子)
- 计算:基址寄存器 + 变址寄存器 * 比例因子 + 偏移量
- 例:100(%ebx, %esi, 4)
- 存储单元的地址为:寄存器ebx的内容加上(寄存器esi内容乘以 4),在加 100
- RTL:M[R[ebx] + 4xR[esi] + 100]
- 汇编指令格式:op src , dst,表示:dst dst op src
- 例如:addl (, %ebx, 2), %eax
- RTL:R[eax] R[eax] + M[2 * R[ebx]
- 解析:加 32 位,源操作数
(, %ebx, 2)目标操作数 %eax ,寄存器 eax 的内容与主存(, %ebx, 2)的内容相加,将结果放入 eax 中 M 表示主存 R 表示寄存器 (, %ebx, 2)分别对应 基址、变址、比例因子,基址为空,偏移量为空,变址ebx乘 2
- 解析:加 32 位,源操作数
- addl 中l 的含义
- b 8 位 字节传送
- w 16 位 字传送
- l 32 位 双字传送
- q 64 位
IA-32 指令系统
IA-32、x86-64
- Intel 8086、80286、i386等,架构称为x86
- 现在 32x86架构的名称x86-32改为 IA-32
- 由 16 位架构发展而来,规定一个字为 16 位,32 位是双字
- AMD 提出了兼容 IA-32指令集的64位架构版本
- AMD 称为 amd64
- Intel 称为Intel64
- 统称位x86-64
定点寄存器组
- 8 个通用寄存器
- EAX、EBX、ECX、EDX 用来存放操作数
- ESP、EBP、ESI、EDI 用来放变址或指针 ESP 栈指针寄存器 EBP 基址寄存器
- 两个专用寄存器
- EIP:指令指针寄存器
- EFLAGS:标志寄存器
- 6 个段寄存器
常用条件标志含义说明
- OF:溢出标志;反应
带符号数的操作结果是否超过相应的数值范围- 例如字节运算结果超过-128 ~ +127或字节运算结果超出-32768 ~ +32767 称为溢出。此时 OF=1,否则 OF=0
- SF:符号标志;反应带符号数运算结果的符号。负数时,SF=1,否则 SF=0
- ZF:零标志;反映运算结果是否为 0。若为 0,ZF=1,否则 ZF=0
- CF:进/借位标志:反应无符号整数加(减)运算后的进(借)位情况,有进(借)位 CF = 1,否则 CF = 0
控制标志含义说明
- DF:方向标志。用来确定串操作指令执行时变址寄存器
SI (ESI)和DI(EDI)中的内容是自动递增还是递减。若 DF = 1,则为递减,否则为递增 - IF:中断允许标志。IF = 1 允许中断,否则禁止中断
- TF:陷阱标志。用来控制单步执行操作TF = 1 时按单步方式执行指令
IA-32 寻址方式、指令的操作数类型
- 什么是寻址? 根据指令给定信息得到操作数或其地址
- 立即寻址:在指令中,无须指定其存放位置
- 寄存器寻址:需要指定操作数所在寄存器的编号
- 当操作数为存储单元内容时:需要指定操作数所在存储单元的地址
- 其他寻址:操作数在存储器中(存储器操作数)
- 实地址模式
- 保护模式
IA-32 常用指令类型及其操作
常用指令类型:
- 传送指令
- 通用数据传送指令:传送的是寄存器或存储器的数据
- MOV:一般的传送指令,包括``movb(b表示一个字节)
、movw、movl(l表示 32 位)` - MOVS:符号扩展传送指令,将短的源数据高位符号扩展后传送到目的地址,如
movsbw(b表示 8 位,w 表示 16 位)表示把一个字节进行符号扩展后送到一个 16 位寄存器中 - MOVZ:零扩展传送指令,将短的源数据高位零扩展后传送到目的地址,如
movzwl表示把一个字的高位进行零扩展后送到一个 32 位寄存器中 - XCHG:数据交换指令,将两个寄存器内容互换。例如,
xchgb表示字节交换 - PUSH:压栈指令,先执行 R[sp] R[sp] -2 或 R[esp] R[esp] -4,然后将一个字或双字从指定寄存器送到 sp 或 ESP 指示的栈单元中。
pushl表示双字压栈,pushw表示字压栈 - POP:将一个字或双字从
SP或ESP指示的单元送入指定寄存器,再执行 R[sp] R[sp] + 2或 R[esp] R[esp] + 4。如popl表示双字出站,popw表示字出栈
- MOV:一般的传送指令,包括``movb(b表示一个字节)
- 地址传送指令:传送的是操作数的存储地址
- 例:对赋值语句
x = i + j,编译器使用了指令leal(%edx,%eax),%eax。该指令中源操作数的有效地址为R[edx] + R[eax],顾指令的功能为R[eax]R[edx] + R[eax],该指令执行前,R[edx] = i,R[eax] = j,因此该指令执行后R[eax] = i+j
- 例:对赋值语句
- 输入/输出指令:输入/输出指令专门用于在累加寄存器
AL/AX/EAX和I/O端口之间进行数据传送 - 标志传送指令:对标志寄存器进行操作
- 通用数据传送指令:传送的是寄存器或存储器的数据
- 定点算数指令
- 位运算指令
- 执行流控制指令
栈是一种采用先进后出的方式进行方位的一块存储区,在处理过程中调用时非常有用。大多数情况下,栈是从高地址向低地址增长的。在 IA-32 中,用 ESP 寄存器指向当前栈顶,而栈底通常在一个高地址上。
在图 3.6 中给出了在 16 位架构下的 pushw和popw指令执行结果示意图。如图所示,在执行 pushw %ax指令之后,SP 指向存放有 AX 内容的单元,也即新栈顶指向了当前刚入栈的数据。若随后在执行 popw %ax 指令,则原先在栈顶的两个字节退出栈,栈顶向高地址移动两个单元,又回到 pushw %ax指令执行前的位置,因为 Intel 架构采用小端方式,所以 AL 在低地址上,AH 在高地址上
例题:
将以下 Intel 格式的指令转换为 AT&T 格式指令,并说明功能
- push ebp
- pushl %ebp
- 说明:
- R[esp] R[esp] -4 将栈指针寄存器中的内容腾出来;将栈指针下移
- M[R[esp]] R[ebp] 将基址寄存器中的内容放入内存中,内存地址为 R[esp]
- 解读:因为是基于 x86 格式是 32 位所以使用pushl,%ebp 表示
寄存器;将基址寄器内容压入栈中
- mov ebp,esp
- movl %esp,%ebp
- 说明:
- R[ebp] R[esp] 将栈指针寄存器内容移动到基指针寄存器
- 解读:由于是 Intel 格式操作数,目标操作数为 ebp, 源操作数为 esp,AT&T 格式与 Intel 格式正好相反
- mvo edx, DWORD PTR[ebp + 8]
- movl 8(%ebp),%edx
- 说明:
- R[edx] M[R[ebp] + 8 ] 将 ebp基址寄存器地址加 8 的双字长内容移动到 edx 寄存器
- 解读:DWORD 表示 double word 双字 l,源操作数
DWORD PTR[ebp + 8]目的操作数edx
- mov bl,255
- movl $255, %bl
- 说明:
- R[bl] 255 将一个立即数移动到 ebx 通用寄存器中
- 解读:bl 是
ebx 通用寄存器的低八位,255 是一个立即数也是一个常量,常量使用$
- mov ax, WORD PTR[ebp+edx*4 +8]
- movw 8(%ebp, edx,4),%ax
- 说明:
- R[ax] 将内存中一个 16 位 的 ebp 基址寄存器地址 加 edx 寄存器地址 乘比例因子 4 加 8 的内容移动到 ax 寄存器
- 解读:
WORD PTR指定数据大小为 16 位,4 是比例因子
- mov WORD PTR [ebp + 20], dx
- movw %dx, 20(%ebp)
- 说明:
- M[R[ebp] + 20] R[dx] 将内存中 ebp 基址寄存器地址 加 20 的内容移动到 dx 寄存器
- 解读:
WORD PTR指定数据大小为 16 位
-
- leal 8(%ecx,%edx,4) , %eax
- 说明:
- R[eax] 把 寄存器里的值写到 eax 寄存器
- 解读:
lea地址传送指令;因为是基于 x86 格式是 32 位所以使用pushl;lea 是计算地址的指令,不放在内存中所以不使用 M
定点算数运算指令
对于标志位的影响
- 加/减:影响
OF、ZF、SF、CF - 自加/自减:不影响
CF - 乘:影响
OF、CF - 除法:都不影响
例题:
假设 R[ax]=FFFAH,R[bx]=FFF0H,则执行 Intel 格式的指令 add ax,bx之后,AX 和 BX 中的内容各是什么?标志位是什么?分别将操作数作为无符号整数和带符号整数来解释
因为是 Intel 指令格式 将 bx 寄存器的值加到 ax 寄存器中
R[ax]=FFFAH,R[bx]=FFF0H
无符号整数
将FFFAH和FFF0H分别转换为十进制为65530和65520
执行加法操作
65530 + 65520 = 130150
ax 寄存器是 16 位的,结果将被截断到 16 位,最大能表示 65535
130150 转换16 (除16 倒取余或转化为000111111111110101010)进制 0x1FFEA
截断后,保留低16位:0xFFEA
因此执行 add ax,bx 后:
R[ax]=FFEA
R[bx]=FFF0
标志位
- CF 进位标志:结果超出 16 位所以 CF = 1
- ZF 零标志:结果不是 0,所以 ZF = 0
- SF 符号标志:最高位是 1是负数,所以 SF = 1
- OF 溢出标志:发生进位没有溢出 OF = 0
带符号整数
将 R[ax]和 R[bx]的值解释为有符号整数 补码形式
FFFAH(十六进制)= -6(十进制)
FFF0(十六进制) = -16(十进制)
执行加法操作
-6 + -16 = -22
将结果转回 16 进制 FFEA
标志位
- CF 进位标志:没有发生进位 CF = 0
- ZF 零标志: 结果不是 0 ZF = 0
- SF 符号标志: 负数,SF = 1
- OF 溢出标志:没有溢出 OF = 0
程序执行流控制指令
-
无条件跳转指令 JMP
无条件跳转指令 JMP 的执行结果就是直接跳转到目标地址处执行。例如,直接跳转方式下,汇编指令
jmp.L1的含义就是直接跳转到标号.L1处执行,在生成机器语言目标代码时,汇编器和链接器会根据跳转目标地址和当前 jmp 指令之间的相对距离,计算出 jmp 指令中的立即数(即偏移量)字段。间接跳转方式下,IA-32 中的汇编指令jmp * .L8(,%eax,4)的功能直接跳转到由存储地址.L8+R[eax] * 4中的内容所指出的目标地址处执行,即 R[eip] M[.L8 + R[eax] * 4]。这种间接跳转方式可用于利用跳转表进行 switch 语句实现的情形。eip:指令指针寄存器 -
条件跳转指令 Jcc
条件跳转指令 Jcc (其中 CC 为条件助记符)以标志位或标志位组合作为跳转依据。如果满足条件,则跳转到由标号
label确定的目标地址处执行;否则继续执行下一条指令
-
条件设置指令 SETcc
用来将条件标志组合得到的条件值设置到一个 8 位通用寄存器中
-
条件传送指令 CMOVcc
如果符合条件就进行传送操作,否则什么都不做
格式:
CMOVcc DST, SRC -
调用和返回指令 CALL/RET
为便于模块化程序设计,往往把程序中某些具有独立功能的部分编写成独立的程序模块,称之为
子程序子程序的使用主要是通过
过程调用和函数调用实现IA-32 提供的两条指令
- 调用指令:包含两个操作:将返回地址入栈(相当于
PUSH操作);跳转到指定地址处执行,CALL指令会修改指针ESP - 返回指令:返回指令 RET 也是一种无条件跳转指令,通常放在子程序的末尾,使子程序执行后返回主程序继续执行。该指令执行过程中,返回地址被从栈顶取出(相当于 POP 指令),并送到
EIP 寄存器(段内或段间调用时)和 CS 寄存器(仅段间调用),RET 指令会修改栈指针
- 调用指令:包含两个操作:将返回地址入栈(相当于
-
陷阱指令(中断/访管)
陷阱也称自陷或陷入,它是预先安排的一种异常事件就像预先设定的陷阱一样。当执行到陷阱指令(也称自陷指令)时,CPU 就调出特定的程序进行相应的处理,处理结束后返回到陷阱指令的下一条指令执行。陷阱的重要作用之一是在用户程序和操作系统内核之间提供一个类似过程调用的接口,称为系统调用
例题:
以下各种指令系列用于将变量 x 和 y 的某种比较结果记录到 CL 寄存器。根据以下各组指令序列,分别判断变量 x 和 y 在 C 语言程序中的数据类型,并说明指令序列的功能
-
第一组:cmpl %eax,%edx #R[eax] = x, R[edx] = y
setb %cl
cmp指令是比较指令,比较 eax 寄存器的内容和 edx 寄存器的内容,eax 寄存器内容为 x,edx 寄存器内容为 y
setb %cl 将结果设置到 cl 寄存器中
setb 指令对应条件跳转指令表第 11 条,表示无符号整数 A < B,跳转条件为 CF = 1 并且 ZF = 0 ,有进位并且不为 0
如果 x < y 则将值设置到 cl 寄存器中,eax 与 edx 寄存器都是 32 位寄存器,但是 setb 表示无符号,也就是说是 32 位无符号整数比较,因此 x 和 y 可能是
unsigned、unsigned long或指针型数据 -
第二组:cmpw %ax,%dx R[ax] = x, #R[ax] = x,R[dx] = y
setl %cl
setl 对应条件跳转指令表第 15 条,表示带符号整数A < B,跳转条件为SF != OF 并且 ZF = 0,w 表示 16 位数据,16 位带符号整数类型为
short -
第三组:cmpl %eax,%edx #R[eax] = x,R[edx] = y
setne %cl
setne 对应条件跳转指令表第4 条,表示不相等不等于 0,跳转条件为ZF = 0,l 表示 32 位数据 ,数据类型为 unsigned、int、unsigned long、指针型数据
-
第四组:cmpb %al,%dl #R[al] = x, R[dl] = y
setae %cl
setae 对应对应跳转指令表第 10 条,表示无符号整数A >= B,CF = 0, ZF = 1,数据类型可能为 unsigned char 或 char,因为 C 语言没有明确规定 char 是带符号整数还是无符号整数,因此,编译器可能将 char 类型变量作为无符号整数类型处理
C 语言类型表
| 数据类型 | 典型位数 | 典型字节数 | 描述 |
|---|---|---|---|
char | 8 位 | 1 字节 | 字符型 |
signed char | 8 位 | 1 字节 | 有符号字符型,范围 -128 到 127 |
unsigned char | 8 位 | 1 字节 | 无符号字符型,范围 0 到 255 |
short | 16 位 | 2 字节 | 有符号短整型,范围 -32768 到 32767 |
unsigned short | 16 位 | 2 字节 | 无符号短整型,范围 0 到 65535 |
int | 32 位 | 4 字节 | 有符号整型,范围 -2147483648 到 2147483647 |
unsigned int | 32 位 | 4 字节 | 无符号整型,范围 0 到 4294967295 |
long | 32 或 64 位 | 4 或 8 字节 | 有符号长整型,32位系统范围 -2147483648 到 2147483647,64位系统范围 -9223372036854775808 到 9223372036854775807 |
unsigned long | 32 或 64 位 | 4 或 8 字节 | 无符号长整型,32位系统范围 0 到 4294967295,64位系统范围 0 到 18446744073709551615 |
long long | 64 位 | 8 字节 | 有符号长长整型,范围 -9223372036854775808 到 9223372036854775807 |
unsigned long long | 64 位 | 8 字节 | 无符号长长整型,范围 0 到 18446744073709551615 |
float | 32 位 | 4 字节 | 单精度浮点型,符合 IEEE 754 标准 |
double | 64 位 | 8 字节 | 双精度浮点型,符合 IEEE 754 标准 |
long double | 80 或 128 位 | 10 或 16 字节 | 扩展精度浮点型,具体尺寸和精度因平台而异 |
_Bool | 1 或 8 位 | 1 字节 | 布尔类型,用于表示 true 和 false |
wchar_t | 16 或 32 位 | 2 或 4 字节 | 宽字符类型,用于表示更大范围的字符集 |
| 指针类型 | 32 或 64 位 | 4 或 8 字节 | 取决于系统位数(32位或64位) |
C语言程序的机器级表示
IA-32 中用于过程调用的指令
调用指令 CALL 和返回指令 RET 是用于过程调用的主要指令,它们都属于一种无条件跳转指令,都会改变程序执行的顺序。为了支持嵌套和递归调用,通常利用栈来保存返回地址、入口参数和过程内部定义的非静态局部变量,因此,CALL 指令在跳转到被调用过程执行之前先要把返回地址压栈,RET指令在返回调用过程之前要从栈中取出返回地址
过程调用的执行步骤
假定过程 P 调用过程 Q,则 P 称为调用者(Caller),Q 称为被调用者(callee)。过程调用的执行步骤如下
P将入口参数(实参)放到Q能访问到的地方P将返回地址存到特定的地方,然后将控制转移到QQ保存P的现场,并为自己的非静态局部变量分配空间- 执行
Q的过程体(函数体) Q恢复P的现场,并释放``局部变量所占空间Q取出返回地址,将控制转移到P
上述步骤中,第一步和第二步是在过程 P 中完成的,其中第二步是由 CALL 指令完成的,通过 CALL 指令,将控制过程从 P 转移到 Q。3-6 步都在背调用过程 Q 中完成,第三步称为准备阶段,用于保存 P 的现场并为 Q 的非静态局部变量分配空间。第五步称为结束阶段,用于恢复 P 的现场并释放 Q 的局部变量所占空间,第六步执行 RET指令返回到过程 P。每个过程的功能主要通过过程体的执行来完成。如果过程 Q 有嵌套调用的话,那么 Q 的过程体和被 Q 调用的过程体又会有上述 6 个步骤的执行过程
现场:当从调用过程跳转到被调用过程执行时,原来在通用寄存器中存放的调用过程中的内容,不能因为被调用过程要使用而将这些寄存器破坏掉,因此,在被调用过程使用这些寄存器之前,在准备阶段先将寄存器中的值保存到栈中,用完以后,在结束阶段再从栈中将这些值从新写回到寄存器中,这样,回到调用过程后,寄存器中存放的还是调用过程中的值,通常将这些寄存器中的值称为现场
IA-32的寄存器使用约定
I386 System V ABI 规范规定,寄存器 EAX、ECX、EDX是调用者保存寄存器。当过程 P 调用过程 Q 时,Q 可以直接使用这三个寄存器,不用将它们的值保存到栈中,这也意味着,如果 P 在从 Q 返回后还要用这三个寄存器中的值,P应该在转到 Q 之前先保存它们的值,并再从 Q 返回后先恢复它们的值再使用。寄存器 EBX、ESI、EDI 是被调用者保存寄存器,Q 必须先将它们的值保存到栈中再使用它们,并再返回 P 之前先恢复它们的值。还有两外两个寄存器 EBP 和 ESP 则分别是帧指针寄存器和栈指针寄存器,分别用来指向当前栈帧的底部和顶部
IA-32的栈、栈帧及其结构
栈帧:每个过程都有自己的栈区,称为栈帧,因此,一个栈由若干栈帧组成,每个栈帧用于专门的帧指针寄存器 EBP指定起始位置。因而,当前栈帧的范围在帧指针 EBP 和栈指针 ESP 指向区域之间。过程执行时,由于不断有数据入栈,所以栈指针会动态移动,而帧指针则固定不变。对程序来说,用固定的帧指针来访问变量要比用变化的栈指针方便得多,也不易出错,因此,在一个过程内对栈中信息的访问大多通过帧指针 EBP 进行。
在调用过程 P 中遇到一个函数调用(假定被调用函数为 Q)时,在调用过程 P 的栈帧中保存的内容如上图所示。首先,P 确定是否需要将某些调用者保存寄存器(如 EAX、ECX 和 EDX)保存到自己的栈帧中;然后,将入口参数按序保存到 P 的栈帧中,参数压栈的顺序是先右后左;最后执行 CALL 指令,先将返回地址保存到 P 的栈帧中,然后转去执行被调用过程 Q
在执行被调用函数 Q 的准备阶段,在 Q 的栈帧中保存的内容上图所示。首先,Q 将 EBP 的值保存到自己的栈帧(即被调用过程 Q 的栈帧)中,并设置 EBP 指向它,即 EBP 指向当前栈帧的底部;然后,根据需要确定是否将被调用者保存寄存器(EBX、ESI 和 EDI)保存到 Q 的栈帧中;最后在栈中为 Q 的非静态局部变量分配空间。通常,如果非静态局部变量为简单变量且有空闲的通用寄存器,则编译器会将通用寄存器分配给局部变量,但是,对于非静态局部变量是数组或结构复杂数据类型的情况,则只能在栈中为其分配空间
在 Q 过程体执行后的结束阶段,Q 会恢复被调用者保存寄存器和 EBP 寄存器的值,并使ESP 指向返回地址,这样,栈中的状态又回到了开始执行 Q 时的状态。这时执行 RET 指令便能取出返回地址,回到过程 P 继续执行
例题:
所定义的汇编代码:
caller 表示代码段开始的标签
pushl %ebp; 将当前基址指针 ebp 保存到栈中
movl %esp,%ebp; 将当前栈指针 esp 复制到基址指针 ebp 为新的栈帧建立基准 (是为了保存 caller 函数的栈帧地址,当被调用函数执行完需要利用该地址进行返回)
subl $24,%esp; 在栈上分配 24 字节空间
movl $125,-12(%ebp); M[R[ebp]-12] <- 125 将常数 125 存储到 ebp 基址-12 处,即 temp1 = 125
movl $80,-8(%ebp); M[R[ebp]-8] <- 80将常数 80 存储到 ebp 基址-8 处,即 temp2 = 80
mvol -8(%ebp),%eax; R[eax] <- M[R[ebp]-8] 将 ebp 基址-8 偏移处的值(即 temp2)加载到调用者寄存器 eax 中
movl %eax,4(%esp); M[R[esp]+4] <- R[eax] 将 eax 的值(temp2)存储到寄存器 esp 基址+4 偏移处
movl -12(%ebp),%eax; R[eax] <- M[R[ebp]-12] 将 ebp 基址-12 偏移处的值(即 temp1)加载到 eax
movl %eax,(%esp); M[R[esp]] <- R[eax] 将 eax 中的值(即 temp1)的值存储到 esp 基址偏移 0 处
call add; 调用add 函数,将函数返回值保存在 eax 中
movl %eax,-4(%ebp); M[R[ebp]-4] <- R[eax] 将 eax 中的返回值存入 ebp 基址-4 偏移处(保存为 sum)
movl -4(%ebp),%eax; R[eax] <- M[R[ebp]-4] 将 ebp 基址-4 偏移处的值(sum)加载到寄存器 eax 中
leave 恢复栈指针和基址指针
ret 从当前函数返回到调用者
上图给出了 caller 栈帧的状态,其中,假定 caller 被过程 P 调用。图中 ESP 的位置是执行了第7条 指令后 ESP 的值所指的位置,可以看出 GCC 为 caller 的参数分配了 24 字节的空间。从汇编代码中可以看出,caller 中只使用了调用者保存寄存器 EAX,没有使用任何被调用者保存寄存器,因而在 caller 栈中无须保存除 EBP 以外的任何寄存器的值;caller 有三个局部变量 temp1、temp2、sum,皆被分配在栈帧中;在用 call 指令调用 add 函数之前,caller 先将入口参数从右向左依次将 temp2和 temp1 的值即 80 和 125 保存到栈中。在执行call 指令时再把返回地址压入栈中。此外,在最初进入 caller 时,还将 EBP 的值压入了栈中,因此 caller 的栈帧中用到的空间占4+12+8+4 = 28字节。但是 caller 的栈帧共有4+24+4=32 字节,其中浪费了4 字节空间(未使用)。这是因为 GCC 为保证x86架构中的数据的严格对齐而规定的每个函数的栈帧大小必须是 16 字节的倍数。
call 指令执行后,add 函数的返回参数存放在 EAX 中,因而 call 指令后面的两条指令中,序号为 23 的 movl 指令用来将 add 的结果存入 sum 变量的存储空间,其变量的地址为 R[ebp]-4;序号为 25 的 movl 指令用来将 sum 变量的值送入返回值寄存器 EAX 中
在执行 ret 指令前,应将当前过程的栈帧释放掉,并恢复旧EBP 的值,上述序号为 17 的 leave 指令实现了这个功能,leave 指令功能相当于以下两条指令的功能。其中,其中第一条指令指向当前 EBP 的位置,第二条指令执行后,EBP 恢复为 P 中的旧值,并使ESP 指向返回地址
movl %ebp,%esp
popl %ebp
执行完 leave 指令后,ret 指令就可以从 ESP 所指处取返回地址,以返回 P 执行。当然,编译器也可以通过 pop 指令对 ESP 的内容做加法来进行退栈操作。
如果对栈帧不理解可以看 B站大佬木讷ne阿的对于栈帧的讲解,下面附上地址
从汇编角度深刻理解函数调用过程 (参数如何传递?函数如何返回?栈帧是什么?)
按值传参和按地址传参
- 按值传参:基本数据类型(整型、浮点型、字符型)
- 按地址传参:数组、结构体、指针
// 程序 1 输出
a = 15 b = 22
a = 22 b = 15
// 程序 2 输出
a = 15 b = 22
a = 15 b = 22
在给 swap() 过程传递参数时,程序一用了 leal指令,lea指令专门用来计算地址,而程序二用的是 mvol 指令,因而程序一传递的是地址,而程序二传送的是 a 和 b 的内容
选择语句的机器级表示
int get_lowaddr_content(int *p1, int *p2) {
if(p1 > p2) {
return *p2;
}else {
return *p1;
}
}
已知形式参数 p1 和 p2 对应的实参已压入调用过程的栈帧,p1和p2对应实参的存储地址分别为R[ebp] + 8、R[ebp] + 12。这里 EBP 指向栈帧底部,返回结果存放在 EAX 中,写出函数体对应的汇编代码,要求用 GCC 默认的 AT&T 格式书写
movl 8(%ebp),%eax; R[eax] <- M[R[ebp] + 8], 即 R[eax] = p1
movl 12(%ebp),%edx; R[edx] <- M[R[ebp] + 8], 即 R[edx] = p2
cmpl %edx,%eax; 比较p1和p2,即根据 p1 - p2 的结果设置标志
jbe .L1 # 若p1 <= p2 则转 L1 处执行
movl (%edx),%eax; R[eax] <- M[R[edx]], 即R[eax] = M[p2]
jmp .L2 # 无条件跳转到 L2 执行
.L1:
movl (%eax),%eax; R[eax] <- M[R[eax]], 即R[eax] = M[p1]
.L2
cmp 指令:cmp operand1, operand2,执行 operand1 - operand2
jbe:跳转指令表第 12 条,表示无符号整数 A <= B 跳转条件 CF = 1 或 ZF = 1 有进位或不为 0
解:因为p1和p2是指针类型参数,所以指令助记符中的长度后缀是 l,比较指令 cmpl 的两个操作数都来自寄存器,所以先将p1和p2对应的实参从栈中取到通用寄存器中,比较指令执行后得到的各个条件标志位,程序需要根据条件标志的组合选择执行不同的指令,因此需要用到条件跳转指令,跳转目标地址用标号.L1和.L2。
for 循环的机器级表示
for(begin_expr; cond_expr; update_expr)
loop_body_statement
for循环结构的执行过程大多可以用以下更近似于机器级语言的低级行为来描述
begin_expr; 初始表达式
c = cond_expr; 条件表达式
if(!c) goto done; 如果条件不满足跳过循环
loop:loop_body_statement; 如果条件满足到条件标签开始执行循环体
update_expr; 更新表达式
c = cond_expr; 重新计算表达式
if (c) goto loop; 如果表达式满足继续循环
done: # 条件表达式不满足跳出循环
例题:
根据汇编代码补全C 语言代码
movl 8(%ebp),%ebx; 将 ebp + 8 的值放入 ebx 中; ebx = x
movl $0,%eax; 将立即数 0 放入 eax 寄存器中; result eax = result
movl $0,%ecx; 将立即数 0 放入 ecx 存储器中; ecx = i
.L12: # 这是一个标签用于标记循环的开始位置
leal (%eax,%eax),%edx; eax + eax 放到 edx 中; edx = result * 2;
movl %ebx,%eax; 将 ebx 的放入 eax 中; eax = x
andl $1,%eax; eax = x & 1
orl %edx,%eax; eax = (x & 1) | (result * 2)
shrl %ebx; ebx 右移一位 x >> 1
addl $1,%ecx; i = i + 1
cmpl $32,%ecx; i 与 32 比较
jne .L12 # i != 32 循环,若相等则 跳出
int func_test(unsigned x) {
int result = 0;
int i;
for(i = 0;i != 32; i++) {
result = (result * 2) | (x & 0x01);
x = x >> 1;
}
return result;
}
练习
-
名词解释:基址寄存器、栈指针寄存器、立即寻址
- 基址寄存器:在计算机系统中用于存储基地址的寄存器,基址寄存器通常用于计算内存地址,尤其是在处理数组,记录或动态分配内存时,通常与变址寄存器,偏移量、比例因子一起计算一个有效的内容地址
- 栈指针寄存器:用于指向栈顶的寄存器。栈是一个先进后出的数据结构,栈指针寄存器用于保存函数调用的返回地址和局部变量、在函数的执行过程中,用于分配和释放局部变量的空间
- 立即寻址:是一种寻址方式,其中操作数是直接在指令中给出的常量值,这种寻址方式不需要访问内存或寄存器来获得操作数,因此执行速度快
-
高级语言转换成机器语言需要经历哪些步骤
- 处理成扩展名为 i 的文件
- 编译成扩展名位 s 的源码
- 汇编成可重定位目标文件二进制文件
- 链接成可执行目标文件二进制
-
Intel 和 AT&T 指令格式有哪些区别
- 指令名称后的字母表示操作数的大小
- 寄存器名称前使用 % 前缀,立即数前使用$前缀
- 内存引用使用括号而不是方括号
- 再有偏移的内存引用中,基址、变址、比例因子、偏移量的顺序与 Intel 不同
- AT&T 格式的源操作数在目标操作数之前
-
写出写列指令的功能,根据操作数的长度确定指令长度后缀
-
mov 8(%ebp,%ebx,4),%ax
- 将 8(%ebp,%ebx,4) 的值移动到 ax 寄存器,ax 寄存器是 16 位寄存器,所以指令后缀为
wword
- 将 8(%ebp,%ebx,4) 的值移动到 ax 寄存器,ax 寄存器是 16 位寄存器,所以指令后缀为
-
mov %al, 12(%ebp)
- 将 al 寄存器的值移入 12(%ebp) 中,al 是 8 位寄存器,所以指令后缀为
b
- 将 al 寄存器的值移入 12(%ebp) 中,al 是 8 位寄存器,所以指令后缀为
-
add (,%ebx,4),%ebx
- 从(,%ebx,4) 地址中取一个值与 ebx 寄存器中的值相加,将结果存回 ebx 寄存器,ebx 寄存器是 32 位寄存器所以指令后缀为
l
- 从(,%ebx,4) 地址中取一个值与 ebx 寄存器中的值相加,将结果存回 ebx 寄存器,ebx 寄存器是 32 位寄存器所以指令后缀为
-
push $0xF8
- $0xF8 是一个立即数,本不应该有指令后缀,但是本课程基于 x86 架构为 32 位,所以指令后缀为
l
- $0xF8 是一个立即数,本不应该有指令后缀,但是本课程基于 x86 架构为 32 位,所以指令后缀为
-
mov $0xFFF0,%eax
- eax寄存器是一个 32 位寄存器所以指令后缀为
l
- eax寄存器是一个 32 位寄存器所以指令后缀为
-
lea 8(%ebx,%esi),%eax
-
eax寄存器是一个 32 位寄存器所以指令后缀为
l
-
-
-
名词解释:CISC/RISC、调用者保存寄存器、栈帧、按地址传参
- CISC:随着超大规模集成电路技术的发展,计算机的硬件成本不断下降,软件成本不断上升。为此人们设计的指令系统也增加了很多功能强大的复杂指令,这些指令的功能也越来越接近高级语言的功能,给软件提供了更好的支持,这类计算机称为复杂指令计算机
- RISC:精简指令集计算机,不止简化了指令系统,提高了机器的性能。指令数目少,指令规格规整,只有 Load/Store 指令中的数据需要访存,采用了大量通用寄存器
- 调用者保存寄存器:寄存器 EAX、ECX、EDX、是调用者保存寄存器,当过程 P 调用 Q 时,Q 可以直接使用这三个寄存器,不用将他们的值保存到栈中,这也意味着,如果 P 再从 Q 返回后还要用这三个寄存器中的值,P 应该在转到 Q 之前先保存它们的值,并在从 Q 返回后先恢复它们的值再使用
- 栈帧:每个过程都有自己的栈区,称为栈帧,因此,一个栈由若干栈帧组成,每个栈帧用专门的帧指针寄存器 EBP 指定起始位置。因而,当前帧的范围在帧指针 EBP 和栈指针 ESP 指向区域之间。过程执行时,由于不断有数据入栈,所以栈指针会动态移动而帧指针固定不变。对于程序来说,用固定的帧指针来访问变量要比用变化的帧指针方便得多,也不易出错,因此,在一个过程内对栈中信息的访问大多要通过帧指针 EBP 进行
- 按地址传参:当形参是指针类型变量名或构造类型变量名时,采用按地址传递方式
-
按值传参和按地址传参有什么区别
按值传参是将实际参数的值传递给函数的形式参数。函数接受到的是实际参数的副本,任何对形式参数的修改都不会影响实际参数,按地址传递是将实际参数的地址传递给函数的形式参数。形式参数通过该地址来访问实际参数,任何对形式参数的修改都会影响实际参数
按值传参安全但有性能开销,适用于基本数据类型和不需要修改参数的场景
按地址传参灵活但是需要注意安全,适用于适用于需要修改参数或传递大数据结构的场景
-
无条件跳转指令和条件跳转指令有什么相同点和不同点
相同点:
- 两种指令都可以改变程序的执行顺序
- 无论是无条件跳转还是有天见跳转,都会跳转到一个指定的目标地址,这可以是一个标签或内存地址
- 两者都是控制流指令,常用于实现循环,分支和其他控制结构
不同点:
- 无条件跳转指令无需任何条件都可以跳转到目标位置
- 条件跳转指令只有满足特定条件才可以跳转
- 无条件跳转只需要写 label 名,条件跳转指令需要满足跳转条件才能跳转到对应的label
- 无条件跳转指令常用于无限循环、无条件分支、函数调用和返回
- 条件跳转指令常用于实现条件分支、循环条件判断等场景
-
找出下列 AT&T 格式代码的错误
- movl 0xFF,(%eax) 0xFF 是一个立即数需要在前面加$
- movb %ax,12(%ebp) ax寄存器是 16位的 b 是表示 8 位需要将 b 换位 w
- addl %ecx,$0xF0 目标地址不能是常数
- addl %esi,%esx 没有 esx 寄存器
- movw 8(%ebp,,4),%ax 少写了一个变址
-
假设变量 x 和 ptr 的类型声明如下
src_type x; x是 src_type 这个类型的值 dst_type * ptr; ptr 是 dst_type 这个类型的指针这里,src_type和 dst_type是用 typedef 声明的数据类型。有以下一个 C 语言赋值语句
*ptr = (dst_type) x; // 将 x 的值转换成dst_type类型 放入 prt 这个指针型变量若 x 存储在寄存器 EAX(32 位) 或 AX(16 位) 或 AL(8 位) 中,ptr 存储在寄存器 EDX (32 位)中,则对于表3.12中给出的src_type和 dst_type的类型组合,写出实现上述赋值语句的机器级代码。要求用 AT&T 格式表示机器级代码
src_type dst_type 机器级表示 char(8位) int(32位) movsbl %al,%eax movs符号位扩展指令;
mvol %ax,(%edx) 括号表示 edx 寄存器的值是一个内存地址int(32位) char(8位) movb %al,(%edx) int(32位) unsigned(32位) movl %eax,%edx short(16位) int(32位) movswl %ax,%eax
movl %eax,(%edx)unsigned char(8位) unsigned(32位) 无符号不需要进行符号位扩展,但是需要零扩展
movzbl %al,%eax
mvol %eax,(%edx)char(8位) unsigned(32位) movzbl %al,%eax
movl %eax,(%edx)int(32位) int(32位) movl %eax,(%edx) -
假设函数product 的 C 语言代码如下,其中 num_type 是用 typeef 声明的数据类型
void product(num_type * d, unsigned x, num_type y) { *d = x * y; }函数 product 的过程体对应的主要汇编代码如下
movl 12(%ebp),%eax; R[eax] <- M[R[ebp] + 12] mvol 20(%ebp),%ecx; R[ecx] <- M[R[ebp] + 20] imull %eax,%ecx; imull 表示有符号乘法 R[ecx] <- R[eax] * R[ecx] mull 16(%ebp); mull 表示无符号乘法 R[edx]R[eax] <- M[R[ebp] + 16] * R[eax] R[edx]R[eax] 高 32 位发放在 edx 低 32 位放在 eax 中 leal (%ecx,%edx),%edx; R[edx] <- R[ecx] + R[edx] movl 8(%ebp),%ecx; R[ecx] <- M[R[ebp] + 8] movl %eax,(%ecx); M[R[ecx]] <- R[eax] mvol %edx,4(%ecx); M[R[ecx] + 4] <- R[edx]请给出上述每条汇编指令的注释,并说明 num_type 是什么类型
因为 EBP 是基址寄存器存放的是该函数的返回值,ebp + 4 表示返回地址,ebp + 8 表示第一个形参的地址,ebp + 12 表示第二个形参的地址,ebp + 16 表示第三个形参的地址
movl 12(%ebp),%eax;表示的是第二个参数,mvol 20(%ebp),%ecx;表示的是第三个参数,因为第三个参数是 ebp + 16 但是汇编代码表示为 + 20,所以 y 的类型肯定比 32 位要大,16-20 字节是低 32 位,20-24 是高 32 位,并且第四步进行的是无符号乘法,这里没有数组与结构体以及浮点数操作所以只能是 long long int 类型 -
函数lproc 的过程体对应的汇编代码如下
回答下列问题或完成下列任务
-
给每条汇编指令添加注释
-
参数 x 和 k 分别存放在哪个寄存器中?举报变量 val 和 i 分别存放在哪个寄存器中
x 存放在 edx 中
k 存放在 ecx 中
val 存放在 esi 中
i 存放在 edi 中
-
局部变量 val 和 i 的初始值分别是什么
val 的初始值 255
i 的初始值 -2147483648
-
循环终止条件是什么?循环控制变量 i 是如何被修改的
循环的终止条件是 i 等于 0,i 是通过每次循环逻辑右移k 位来修改的
-
填写 C 代码中缺失部分
mvol 8(%ebp),%edx; R[edx] <- M[R[ebp] + 8] // 将参数 x 传递给 edx 寄存器 movl 12(%ebp),%ecx; R[ecx] <- M[R[ebp] + 12] // 将参数 k 传递给 ecx 寄存器 mvol $255,%esi; M[R[esi]] <- 255 // 将局部变量 val 初始化为 255 movl $-2147483648, %edi; M[R[edi]] <- -2147483648 // 将局部变量 i 初始化为 -2147483648 .L3: mvol %edi,%eax; R[eax] <- R[edi] // 将 i 的值复制到 eax 寄存器 andl %edx,%eax; R[eax] <- R[eax] & R[edx] // 执行 i & x 操作 xorl %eax,%esi; R[esi] <- R[eax] ^ R[esi] // 执行 val ^ (i & x) (^ 表示异或操作) movl %ecx,%ebx; R[ebx] <- R[ecx] // 将 k 的值复制到 ebx shrl %bl,%edi; R[edi] >> R[edi] >> R[bl] // 将 i 的值逻辑右移 k 位 # 右移操作分为逻辑右移与算数右移, # 算数右移 对有符号数进行操作的时候会保留符号位 # 逻辑右移 对无符号数进行右移操作时不会保留符号位 # 这里 如果 i 是有符号整数,直接右移会进行算数右移,这与汇编中的逻辑右移不一致,所以需要将 i 转换为无符号整数,以确保右移 操作是逻辑右移 testl %edi,%edi; // 设置标志寄存器 // 测试 i 是否为 0 jne .L3 # 如果 i 不为 0 则继续执行 L3 如果 i 为 0 则跳出循环 movl %esi,%eax; R[eax] <- R[esi] // 将 val 的值复制到 eax 寄存器int lproc(int x, int k) { int val = 255; int i; for(i= -2147483648;i != 0; i= (unsigned)i >> k) { val ^= (i & x); } return val; } -
捏捏捏捏捏捏捏捏捏捏捏
由于后续视频课程为付费课程,观看方式为萝卜 Bro 发送的腾讯会议链接无法分享给大家,所以还是建议大家去购买正版课程 😊😊