26考研——指令系统_程序的机器级代码表示(4)

408答疑



三、程序的机器级代码表示

本节是 2022 年新增的考点,历年统考真题曾多次以综合题的形式考查过,难度较大。统考大纲没有指定具体指令集,但历年统考真题主要考查的是 x86 汇编指令,因此本节主要介绍 x86 汇编指令。

常用汇编指令介绍

相关寄存器

x86 处理器中有 8 个 32 位的通用寄存器,主要寄存器及说明如下图所示。
在这里插入图片描述

  • 为了向后兼容,EAX、EBX、ECX 和 EDX 的高两位字节和低两位字节可以独立使用,E 表示 Extended,表示 32 位的寄存器。
  • 例如,EAX 的低两位字节称为 AX,而 AX 的高低字节又可分别作为两个 8 位寄存器,分别称为 AH 和 AL。
  • 除 EBP 和 ESP 外,其他几个寄存器的用法是比较灵活的。

汇编指令格式

使用不同的编程工具开发程序时,用到的汇编程序也不同,一般有两种不同的汇编格式:AT&T 格式和 Intel 格式(统考要求掌握的是 Intel 格式)。它们的区别主要体现如下:

  1. 指令字母大小写:AT&T 格式的指令只能用小写字母,而 Intel 格式的指令对大小写不敏感。

  2. 操作数顺序

    • 在 AT&T 格式中,第一个为源操作数,第二个为目的操作数,方向从左到右,合乎自然;
    • 在 Intel 格式中,第一个为目的操作数,第二个为源操作数,方向从右向左。
  3. 寄存器和立即数前缀

    • 在 AT&T 格式中,寄存器需要加前缀“%”,立即数需要加前缀“$”;
    • 在 Intel 格式中,寄存器和立即数都不需要加前缀。
  4. 内存寻址:在内存寻址方面,AT&T 格式使用“(”和“)”,而 Intel 格式使用“[”和“]”。

  5. 复杂寻址方式:在处理复杂寻址方式时,例如 AT&T 格式的内存操作数“disp(base, index, scale)”分别表示偏移量、基址寄存器、变址寄存器和比例因子,如“8(%edx, %eax, 2)”表示操作数为 M [ R [ e d x ] + R [ e a x ] ∗ 2 + 8 ] M[R[edx] + R[eax]*2 + 8] M[R[edx]+R[eax]2+8],其对应的 Intel 格式的操作数为 [ e d x + e a x ∗ 2 + 8 ] [edx + eax*2 + 8] [edx+eax2+8]

  6. 数据长度:在指定数据长度方面,AT&T 格式指令操作码的后面紧跟一个字符,表明操作数大小,“b”表示 byte(字节)、“w”表示 word(字)或“l”表示 long(双字)。Intel 格式也有类似的语法,它在操作码后面显式地注明 byte ptr、word ptr 或 dword ptr。

32 或 64 位体系结构都是由 16 位扩展而来的,因此用 word(字)表示 16 位。

下表所示为 AT&T 格式指令和 Intel 格式指令的对比:

AT&T 格式指令Intel 格式指令含义
mov $100, %eaxmov eax, 100100→R[eax]
mov %eax, %ebxmov ebx, eaxR[eax]→R[ebx]
mov %eax, (%ebx)mov [ebx], eaxR[eax]→M[R[ebx]]
mov %eax, -8(%ebp)mov [ebp-8], eaxR[eax]→M[R[ebp]-8]
lea 8(%edx, %eax, 2), %eaxlea eax, [edx+eax*2+8]R[edx]R[eax]*2+8→R[eax]
movl %eax, %ebxmov dword ptr ebx, eax长度为 4 字节的 R[eax]→R[ebx]
  • mov 指令用于在内存和寄存器之间或者寄存器之间移动数据;
  • lea 指令用于将一个内存地址(而不是其所指的内容)加载到目的寄存器;
  • R[r] 表示寄存器 r 的内容,M[addr] 表示主存单元 addr 的内容,→ 或 ← 表示信息传送方向。

两种汇编格式的相互转换并不复杂,但历年统考真题采用的均是 Intel 格式。

常用指令

汇编指令通常可分为数据传送指令、算术和逻辑运算指令和控制流指令,下面以 Intel 格式为例,介绍一些常用的指令。以下用于操作数的标记分别表示寄存器、内存和常数。

寄存器标记:

  • <reg>:表示任意寄存器,若其后带有数字,则指定其位数。
  • <reg32>:表示 32 位寄存器(eax, ebx, ecx, edx, esi, edi, esp 或 ebp);
  • <reg16>:表示 16 位寄存器(ax, bx, cx 或 dx);
  • <reg8>:表示 8 位寄存器(ah, al, bh, bl, ch, cl, dh, dl)。

内存地址标记

  • <mem>:表示内存地址( [如eax]、[var + 4] 或 dword ptr [eax + ebx])。

常数标记

  • <con>:表示 8 位、16 位或 32 位常数。
  • <con8>:表示 8 位常数;
  • <con16>:表示 16 位常数;
  • <con32>:表示 32 位常数。

分析汇编指令对应的二进制代码
x86 中的指令机器码长度为 1 字节,对同一指令的不同用途有多种编码方式,比如 mov 指令就有 28 种机内编码,用于不同操作数类型或用于特定寄存器。例如,

mov ax, <con16>						#机器码为 B8H
mov al, <con8>						#机器码为 BOH
mov <reg16>, <regl6>/<mem16>		#机器码为 89H
mov <reg8>/<mem8>, <reg8>			#机器码为 8AH
moy <req16>/<mem16>, <req16>		#矶器码为 8BH
数据传送指令
mov 指令

将第二个操作数(寄存器的内容、内存中的内容或常数值)复制到第一个操作数(寄存器或内存)。

  • 语法
mov <reg>, <reg>
mov <reg>, <mem>
mov <mem>, <reg>
mov <reg>, <con>
mov <mem>, <con>
  • 举例
mov eax, ebx				# 将 ebx 值复制到 eax
mov byte ptr [var], 5		# 将 5 保存到 var 值指示的内存地址的一字节中
  • 双操作数指令的两个操作数不能都是内存,即 mov 指令不能用于直接从内存复制到内存,若需在内存之间复制,可先从内存复制到一个寄存器,再从这个寄存器复制到内存。
push 指令

将操作数压入内存的栈,常用于函数调用。ESP 是栈顶,入栈前先将 ESP 值减 4(栈增长方向与内存地址增长方向相反),然后将操作数压入 ESP 指示的地址。

  • 语法
push <reg32>
push <mem>
push <con32>
  • 举例
push eax		#将 eax 值入栈
push [var]		#将 var 值指示的内存地址的 4 字节值入栈

栈中元素固定为 32 位

pop 指令

push 指令相反,pop 指令执行的是出栈工作,出栈前先将 ESP 指示的地址中的内容出栈,然后将 ESP 值加 4。

  • 语法
pop eax			# 弹出栈顶元素送到 eax
pop [ebx]		# 弹出栈顶元素送到 ebx 值指示的内存地址的 4 字节中
算术和逻辑运算指令
add/sub 指令

add 指令将两个操作数相加,相加的结果保存到第一个操作数中。sub 指令用于两个操作数相减,相减的结果保存到第一个操作数中。

  • 语法
add <reg>, <reg>
add <reg>, <mem>
add <mem>, <reg>
add <reg>, <con>
add <mem>, <con>
sub <reg>, <reg>
sub <reg>, <mem>
sub <mem>, <reg>
sub <reg>, <con>
sub <mem>, <con>
  • 举例
sub eax, 10					# eax ← eax - 10
add byte ptr [var], 10		# 10 与 var 值指示的内存地址的一字节值相加,并将结果保存在 var 值指示的内存地址的字节中
inc/dec 指令

incdec 指令分别表示将操作数自加 1、自减 1。

  • 语法
inc <reg>
inc <mem>
dec <reg>
dec <mem>
  • 举例
dec eax					# eax 值自减 1
inc dword ptr [var]		# var 值指示的内存地址的 4 字节值自加 1
imul 指令

有符号整数乘法指令,有两种格式:

  1. 两个操作数,将两个操作数相乘,将结果保存在第一个操作数中,第一个操作数必须为寄存器;
  2. 三个操作数,将第二个和第三个操作数相乘,将结果保存在第一个操作数中,第一个操作数必须为寄存器。
  • 语法
imul <reg32>, <reg32>
imul <reg32>, <mem>
imul <reg32>, <reg32>, <con>
imul <reg32>, <mem>, <con>
  • 举例
imul eax, [var]			# eax ← eax * [var]
imul esi, edi, 25		# esi ← edi * 25

乘法操作结果可能溢出,则编译器置溢出标志 OF=1,以使 CPU 调出溢出异常处理程序。

idiv 指令

有符号整数除法指令,它只有一个操作数,即除数,而被除数则为 edx:eax 中的内容(共 64 位),操作结果有两部分:商和余数,商送到 eax,余数则送到 edx。

  • 语法
idiv <reg32>
idiv <mem>
  • 举例
idiv ebx					# 进行除法操作
idiv dword ptr [var]		# 进行内存地址的除法操作
and/or/xor 指令

andorxor 指令分别是逻辑与、逻辑或、逻辑异或操作指令,用于操作数的位操作,操作结果放在第一个操作数中。

  • 语法
and <reg>, <reg>
and <reg>, <mem>
and <mem>, <reg>
and <reg>, <con>
and <mem>, <con>
or <reg>, <reg>
or <reg>, <mem>
or <mem>, <reg>
or <reg>, <con>
or <mem>, <con>
xor <reg>, <reg>
xor <reg>, <mem>
xor <mem>, <reg>
xor <reg>, <con>
xor <mem>, <con>
  • 举例
and eax, 0fH		# 将 eax 中的前 28 位全部置为 0,最后 4 位保持不变
xor edx, edx		# 置 edx 中的内容为 0
not 指令

位翻转指令,将操作数中的每一位翻转,即 0→1、1→0。

  • 语法
not <reg>
not <mem>
  • 举例
not byte ptr [var]		# 将 var 值指示的内存地址的一字节的所有位翻转
neg 指令

取负指令。

  • 语法
neg <reg>
neg <mem>
  • 举例
neg eax		# eax ← -eax
shl/shr 指令

逻辑移位指令,shl 为逻辑左移,shr 为逻辑右移,第一个操作数表示被操作数,第二个操作数指示移位的位数。

  • 语法
shl <reg>, <con8>
shl <mem>, <con8>
shl <reg>, <cl>
shl <mem>, <cl>
shr <reg>, <con8>
shr <mem>, <con8>
shr <reg>, <cl>
shr <mem>, <cl>
  • 举例
shl eax, 1		# 将 eax 值左移 1 位
shr ebx, cl		# 将 ebx 值右移 n 位(n 为 cl 中的值)
控制流指令

x86 处理器维持着一个指示当前执行指令的指令指针(IP),当一条指令执行后,此指针自动指向下一条指令。IP 寄存器不能直接操作,但可以用控制流指令更新。通常用标签(label)指示程序中的指令地址,在 x86 汇编代码中,可在任何指令前加入标签。例如,

		mov esi,[ebp+8]
begin:	xor ecx, ecx
		mov eax,[esi]

这样就用 begin 指示了第二条指令,控制流指令通过标签就可以实现程序指令的跳转。

jmp 指令

jmp 指令控制 IP 转移到 label 所指示的地址(从 label 中取出指令执行)。

  • 语法
jmp <label>
  • 举例
jmp begin		# 跳转到 begin 标记的指令执行
jcondition 指令

条件转移指令,依据 CPU 状态字中的一系列条件状态转移。CPU 状态字中包括指示最后一个算术运算结果是否为 0,运算结果是否为负数等。

  • 语法
je <label>		(jump when equal)
jz <label>		(jump when last result was zero)
jne <label>		(jump when not equal)
jg <label>		(jump when greater than)
jge <label>		(jump when greater than or equal to)
jl <label>		(jump when less than)
jle <label>		(jump when less than or equal to)
  • 举例
cmp eax, ebx		# 比较 eax 和 ebx
jle done			# 若 eax 值 <= ebx 值,则跳转到 done 执行;否则执行下一条指令
cmp/test 指令

cmp 指令的功能相当于 sub 指令,用于比较两个操作数的值。test 指令的功能相当于 and 指令,对两个操作数进行逐位与运算。与 suband 指令不同的是,这两类指令都不保存操作结果,仅根据运算结果设置 CPU 状态字中的条件码。

  • 语法
cmp <reg>, <reg>
cmp <reg>, <mem>
cmp <mem>, <reg>
cmp <reg>, <con>
test <reg>, <reg>
test <reg>, <mem>
test <mem>, <reg>
test <reg>, <con>
  • 举例
cmp dword ptr [var], 10		# 将 var 指示的主存地址的 4 字节内容,与 10 比较
jne loop					# 若相等则继续顺序执行;否则跳转到 loop 处执行
test eax, eax				# 测试 eax 是否为零
jz xxxx						# 为零则置标志 ZF 为 1,跳转到 xxxx 处执行
call/ret 指令

分别用于实现子程序(过程、函数等)的调用及返回。

  • 语法
call <label>
ret
  • 举例
call label		# 调用 label 标记的子程序
  • call 指令将下一条指令的地址(返回地址)入栈,然后无条件转移到由标签指示的指令。与其他简单的跳转指令不同,call 指令保存该指令的下一条指令的地址(当 call 指令结束后,返回保存的地址)。
  • ret 指令实现子程序的返回机制,ret 指令弹出栈中保存的指令地址,然后无条件转移到保存的指令地址执行。callret 是程序(函数)调用中最关键的两条指令。

理解上述指令的语法和用途,可以更好地帮助读者解答相关题型。读者在上机调试 C 程序代码时,也可以尝试用编译器调试,以便更好地帮助理解机器指令的执行。

选择语句的机器级表示

  • 常见的选择结构语句有 if-thenif-then-else 等。编译器通过条件码(标志位)设置指令和各类转移指令来实现程序中的选择结构语句。
  • 条件码描述了最近的算术或逻辑运算操作的属性,可以检测这些寄存器来执行条件分支指令,最常用的条件码有 CF、ZF、SF 和 OF。
  • 常见的算术逻辑运算指令(add, sub, imul, or, and, shl, inc, dec, not, sal 等)会设置条件码,还有 cmp 和 test 指令只设置条件码而不改变任何其他寄存器。
  • 之前介绍的 jcondition 条件转移指令,就是根据条件码 ZF 和 SF 来实现跳转的。

if-else 语句

if-else 语句的通用形式如下:

if (test_expr)
    then_statement
else
    else_statement

这里的 test_expr 是一个整数表达式,它的取值为 0(假),或为非 0(真)。两个分支语句(then_statement 或 else_statement)中只会执行一个。

这种通用形式可以被翻译成如下所示的 goto 语句形式:

t = test_expr;
if (!t)
    goto false;
then_statement
goto done;
false:
else_statement
done:

C 语言函数

对于下面的 C 语言函数:

int get_cont(int *p1, int *p2) {
    if (p1 > p2)
        return *p2;
    else
        return *p1;
}

已知 p1 和 p2 对应的实参已被压入调用函数的栈帧,它们对应的存储地址分别为 R[ebp] + 8、R[ebp] + 12(EBP 指向当前栈帧底部),返回结果存放在 EAX 中。对应的汇编代码为:

mov eax, dword ptr [ebp+8]  	# R[eax] ← M[R[ebp]+8](将基址为 EBP 的偏移地址 8 处的值(即 p1)加载到 EAX 寄存器中),即 R[eax] = p1
mov edx, dword ptr [ebp+12] 	# R[edx] ← M[R[ebp]+12](将基址为 EBP 的偏移地址 12 处的值(即 p2)加载到 EDX 寄存器中),即 R[edx] = p2
cmp eax, edx               		# 比较 p1 和 p2(比较 EAX 和 EDX 寄存器中的值),即根据 p1-p2 的结果置标志
jbe .L1                    		# 若 p1 <= p2,则转标记 L1 处执行
mov eax, dword ptr [edx]   		# R[eax] ← M[R[edx]](将 EDX 寄存器指向的地址(即 p2 指向的值)加载到 EAX 寄存器中),即 R[eax] = M[p2]
jmp .L2                    		# 无条件跳转到标记 L2 执行
.L1:
mov eax, dword ptr [eax]   		# R[eax] ← M[R[eax]](将 EAX 寄存器指向的地址(即 p1 指向的值)加载到 EAX 寄存器中),即 R[eax] = M[p1]
.L2:

p1 和 p2 是指针型参数,所以在 32 位机中的长度是 dword ptr,比较指令 cmp 的两个操作数都应来自寄存器,因此应先将 p1 和 p2 对应的实参从栈中取到通用寄存器,比较指令执行后得到各个条件码,然后根据各条件码值的组合选择执行不同的指令,因此需要用到条件转移指令。

循环语句的机器级表示

循环语句在编程中非常常见,主要有 whilefordo-while 三种形式。汇编中没有直接对应的指令,但可以通过条件测试和跳转指令组合实现循环效果。大多数编译器将这三种循环结构转换为 do-while 形式来产生机器代码。

do-while 循环

do-while 循环的通用形式如下:

do {
    body_statement
} while (test_expr);

这种通用形式可以被翻译成条件和 goto 语句:

loop:
body_statement
t = test_expr;
if (t) 
	goto loop;
  • 每次循环,程序会执行循环体内的语句 body_statement 至少执行一次,然后执行测试表达式。若测试为真,则继续执行循环。

while 循环

while 循环的通用形式如下:

while (test_expr) {
    body_statement
}

与 do-while 的不同之处在于,第一次执行 body_statement 之前,就会测试 test_expr 的值,循环有可能中止。GCC 通常会将其翻译成条件分支加 do-while 循环的方式。

t = test_expr;
if (!t) 
	goto done;
do {
    body_statement
    t = test_expr;
} while (t);
done:

相应地,进一步将它翻译成 goto 语句:

t = test_expr;
if (!t) 
	goto done;
loop:
body_statement
t = test_expr;
if (t) 
	goto loop;
done:

for 循环

for 循环的通用形式如下:

for (init_expr; test_expr; update_expr) {
    body_statement
}

这个 for 循环的行为与下面的 while 循环代码的行为一样:

init_expr;
while (test_expr) {
    body_statement
    update_expr;
}

进一步将它翻译成 goto 语句:

init_expr;
t = test_expr;
if (!t)
	goto done;
loop:
body_statement
update_expr;
t = test_expr;
if (t)
	goto loop;
done:

C 语言函数

下面是一个用 for 循环写的自然数求和的函数:

int nsum_for(int n) {
    int i;
    int result = 0;
    for (i = 1; i <= n; i++)
        result += i;
    return result;
}

这段代码中的 for 循环的不同组成部分如下:

init_expr			i = 1
test_expr			i <= n
update_expr			i++
body_statement		result += i

通过替换前面给出的模板中的相应位置,很容易将 for 循环转换为 whiledo-while 循环。将这个函数翻译为 goto 语句代码后,不难得出其过程体的汇编代码:

mov ecx, dword ptr [ebp+8]  	# R[ecx] ← M[R[ebp]+8](将基址为 EBP 的偏移地址 8 处的值(即 n)加载到 ECX 寄存器中),即 R[ecx] = n
mov eax, 0                		# R[eax]0(R[eax] = 0, 即 result = 0
mov edx, 1                 		# R[edx]1(R[edx] = 1, 即 i = 1
cmp edx, ecx               		# Compare R[edx] : R[ecx], 即比较 i : n
jg .L2                    		# If greater, 转跳到 L2 执行
.L1:
add eax, edx               		# R[eax] ← R[eax]+R[edx](R[eax] = R[eax] + R[edx], 即 result += i
add edx, 1                 		# R[edx] ← R[edx]+1(R[edx] = R[edx] + 1, 即 i++
cmp edx, ecx              		# Compare R[edx] 和 R[ecx], 即比较 i : n
jle .L1                   		# If less or equal, 转跳到 L1 执行
.L2:

已知 n 对应的实参已被压入调用函数的栈帧,其对应的存储地址为 R[ebp]+8,过程 nsum_for 中的局部变量 i 和 result 被分别分配到寄存器 EDX 和 EAX 中,返回参数在 EAX 中。

过程调用的机器级表示

  • call/ret 指令主要用于过程调用,属于无条件转移指令。
  • 假定过程 P(调用者)调用过程 Q(被调用者),过程调用的执行步骤如下:
    1. 参数传递:调用者 P 将入口参数(实参)放到被调用者 Q 能访问到的地方。
    2. 返回地址保存:调用者 P 将返回地址存到特定的地方,然后将控制转移到被调用者 Q。
    3. 现场保存:被调用者 Q 保存调用者 P 的现场(通用寄存器的内容),并为自己的非静态局部变量分配空间。
    4. 执行过程:被调用者 Q 执行过程。
    5. 现场恢复:被调用者 Q 恢复调用者 P 的现场,将返回结果放到 P 能访问到的地方,并释放局部变量所占空间。
    6. 返回:被调用者 Q 取出返回地址,将控制转移到调用者 P。

步骤 2 由 call 指令实现的,步骤 6 通过 ret 指令返回到过程 P。

返回地址与现场的保存与恢复

  • 在上述步骤中,需要为入口参数、返回地址、过程 P 的现场、过程 Q 的局部变量、返回结果找到存放空间。
  • 用户可见寄存器数量有限,调用者和被调用者需共享寄存器,若直接覆盖对方的寄存器,则会导致程序出错。
  • 因此有如下规范:寄存器 EAX、ECX 和 EDX 是调用者保存寄存器,当 P 调用 Q 时,若 Q 需用到这些寄存器,则由 P 将这些寄存器的内容保存到栈中,并在返回后由 P 恢复它们的值。
  • 寄存器 EBX、ESI、EDI 是被调用者保存寄存器,当 P 调用 Q 时,Q 必须先将这些寄存器的内容保存在栈中才能使用它们,并在返回 P 之前先恢复它们的值。

栈帧结构

  • 每个过程都有自己的栈区,称为栈帧,因此,一个栈由若干栈帧组成,寄存器 EBP 指示栈帧的起始位置,寄存器 ESP 指示栈顶,栈从高地址向低地址增长。
  • 过程执行时,ESP 会随着数据的入栈而动态变化,而 EBP固定不变。
  • 当前栈帧的范围在 EBP 和 ESP 指向的区域之间。

C 语言函数

下面用一个简单的 C 语言程序来说明过程调用的机器级实现。

int add(int x, int y) {
    return x + y;
}
int caller() {
    int temp1 = 125;
    int temp2 = 80;
    int sum = add(temp1, temp2);
    return sum;
}

经 GCC 编译后,caller 和 add 过程对应的代码如下:

add:
    push   ebp
    mov    ebp, esp
    mov    eax, dword ptr [ebp+12]  	# R[eax] = x
    mov    edx, dword ptr [ebp+8]   	# R[edx] = y
    lea    eax, [edx+eax]           	# R[eax] = x + y
    pop    ebp
    ret

caller:
    push   ebp
    mov    ebp, esp
    sub    esp, 24
    mov    dword ptr [ebp-12], 125    	# M[R[ebp]-12]125,即 temp1 = 125
    mov    dword ptr [ebp-8], 80     	# M[R[ebp]-8]80,即 temp2 = 80
    mov    eax, dword ptr [ebp-8]    	# R[eax] ← M[R[ebp]-8],即 R[eax] = temp2
    mov    dword ptr [esp+4], eax       # M[R[esp]+4] ← R[eax],即 temp2 入栈
    mov    eax, dword ptr [ebp-12]   	# R[eax] ← M[R[ebp]-12],即 R[eax] = temp1
    mov    dword ptr [esp], eax         # M[R[esp]] ← R[eax],即 temp1 入栈
    call   add                       	# 调用 add 函数,将返回值保存在 eax 中
    mov    dword ptr [ebp-4], eax    	# M[R[ebp]-4] ← R[eax],即 add 返回值送 sum
    mov    eax, dword ptr [ebp-4]		# R[eax] ← M[R[ebp]-4],即 sum 作为返回值 
    leave
    ret
  • 下图给出了 caller 和 add 的栈帧,假定 caller 被过程 P 调用。
  • 执行 caller 函数部分第 4 行的指令后,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 4+12+8+4=28 4+12+8+4=28 字节。但是,caller 的栈帧共有 4 + 24 + 4 = 32 4+24+4=32 4+24+4=32 字节,其中浪费了 4 字节的空间(未使用)。这是因为 GCC 为保证数据的严格对齐而规定每个函数的栈帧大小必须是 16 字节的倍数。
  • call 指令执行后,add 函数的返回参数存放在 EAX 中,因此 call 指令后面的两条指令中,指令“mov [ebp-4], eax”将 add 的结果存入 sum 变量的存储空间,该变量的地址为 R[ebp]-4;指令“mov eax, dword ptr [ebp-4]”将 sum 变量的值作为返回值送到寄存器 EAX 中。
leave 指令功能

在执行 ret 指令之前,应将当前栈帧释放,并恢复旧 EBP 的值。上述 caller 函数部分第 14 行由 leave 指令实现了这个功能,leave 指令功能相当于以下两条指令的功能:

mov esp, ebp
pop ebp
  • 第一条指令使 ESP 指向当前 EBP 的位置。
  • 第二条指令执行后,EBP 恢复为 P 中的旧值,并使 ESP 指向返回地址。
ret 指令功能
  • 执行完 leave 指令后,ret 指令就可从 ESP 所指处取返回地址,以返回 P 执行。
  • 当然,编译器也可通过 pop 指令和对 ESP 的内容做加法来进行退栈操作,而不一定要使用 leave 指令。
过程调用的三个阶段(以 add 函数为例)

通常,一个过程调用对应的机器级代码都有三个部分:准备阶段、过程体和结束阶段。

准备阶段
  • 上述第 1、2 行的指令构成准备阶段的代码段,这是最简单的准备阶段代码段,它通过将当前栈指针 ESP 传送到 EBP 来完成将 EBP 指向当前栈帧底部的任务。
  • 如上图所示,EBP 指向 add 栈帧底部,从而可以通过 EBP 获取入口参数。这里 add 的入口参数 x 和 y 对应的值(125 和 80)分别在地址为 R [ e b p ] + 8 R[ebp]+8 R[ebp]+8 R [ e b p ] + 12 R[ebp]+12 R[ebp]+12 的存储单元中。
过程体
  • 上述第 3、4、5 行的指令序列是过程体的代码段,过程体的代码段将返回值放在 EAX 中。
  • 这里好像没有加法指令,实际上第 5 行 lea 指令执行的是加法运算 R [ e d x ] + R [ e a x ] = x + y R[edx] + R[eax] = x + y R[edx]+R[eax]=x+y
结束阶段
  • 上述第 6、7 行的指令序列是结束阶段的代码段,通过将 EBP 弹出栈帧来恢复 EBP 在 caller 过程中的值,并在栈中退出 add 过程的栈帧,使得执行到 ret 指令时栈顶中已经是返回地址。
  • 这里的返回地址应该是 caller 代码中第 12 行的指令 “mov [ebp-4], eax” 的地址。
add 函数的特点
  • add 过程中没有用到任何被调用者保存寄存器,没有局部变量。
  • 此外,add 是一个被调用过程,并且不再调用其他过程,因此也没有入口参数和返回地址要保存。
  • 因此,在 add 的栈帧中除了需要保存 EBP,无须保留其他任何信息。

五、参考资料

鲍鱼科技课件

b站免费王道课后题讲解:
在这里插入图片描述

网课全程班:
在这里插入图片描述

26王道考研书

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

408答疑+v:18675660929

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

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

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

打赏作者

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

抵扣说明:

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

余额充值