本节是2022年新增考点,但相关知识早已多次以综合题形式出现在历年真题中,难度较大,不少跨考生感到无从下手。通过本节学习后,考生应能从容应对此类问题。统考大纲未指定具体指令集,但历年真题主要考查x86和MIPS汇编指令。其中,MIPS指令通常会在试题中附带功能说明,而x86指令则更常作为默认考查对象。因此,本节重点介绍x86汇编指令。
4.3.1 常用汇编指令介绍
1. 相关寄存器
x86处理器中有8个32位通用寄存器。为向后兼容早期的16位和8位架构,EAX、EBX、ECX和EDX的低16位可作为独立寄存器使用(分别记为AX、BX、CX、DX),而每个16位寄存器又可进一步拆分为两个8位寄存器(如AX分为AH和AL)。其中,E表示 Extended,用于标识32位寄存器。
除EBP(基址指针)和ESP(栈指针)外,其余通用寄存器的用途是比较灵活的。
2. 常用指令
汇编指令通常可分为数据传送指令、算术与逻辑运算指令和控制流指令,下面以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)因操作数类型或寄存器不同,可能对应多种机器码编码,例如,
mov ax, <con16> #机器码为B8Hmov al, <con8> #机器码为B0Hmov <reg16>, <reg16> / <mem16> #机器码为89Hmov <reg8>, <mem8> / <reg8> #机器码为8AHmov <reg16>, <mem16> / <reg16> #机器码为8BH(1) 数据传送指令
- mov指令。将第二个操作数(寄存器内容、内存内容或常数值)复制到第一个操作数(寄存器或内存)。其语法如下:
mov <reg>, <reg>mov <reg>, <mem>mov <mem>, <reg>mov <reg>, <con>mov <mem>, <con>举例:
mov eax, ebx #将ebx寄存器的值复制到eaxmov byte ptr [var], 5 #将常数5存入地址var处的1字节内存单元双操作数指令的两个操作数不能同时为内存,即mov指令不能用于直接从内存复制到内存。若需在内存之间复制,可先将源内存内容加载到寄存器,再从该寄存器写入目标内存。
- push 指令。将操作数压入栈中,常用于函数调用和现场保护。ESP是栈顶,入栈前先将ESP减4(栈向低地址方向增长),再将操作数压入ESP所指地址。其语法如下:
push <reg32>push <mem>push <con32>举例(注意,栈中元素固定为32位):
push eax #将eax的值压入栈push [var] #将地址var处的4字节内容压入栈- pop指令。与push 指令相反,从栈中弹出数据。出栈前先将ESP所指地址的内容读出,再将ESP加4。其语法如下:
pop <reg32>pop <mem>举例:
pop eax #弹出栈顶元素并存入eaxpop [ebx] #弹出栈顶元素并存入ebx所指的4字节内存地址(2) 算术和逻辑运算指令
- add/sub指令。add指令将两个操作数相加,sub指令将第一个操作数减去第二个操作数,结果均保存在第一个操作数中。其语法如下:
add <reg>, <reg> / sub <reg>, <reg>add <reg>, <mem> / sub <reg>, <mem>add <mem>, <reg> / sub <mem>, <reg>add <reg>, <con> / sub <reg>, <con>add <mem>, <con> / sub <mem>, <con>举例:
sub eax, 10 #eax ← eax-10add byte ptr [var], 10 #将地址var处的1字节内容与10相加,结果存回该地址- inc/dec指令。分别对操作数执行自增1或自减1操作。其语法如下:
inc <reg> / dec <reg>inc <mem> / dec <mem>举例:
dec eax #eax值自减1inc dword ptr [var] #将地址var处的4字节内容自增1-
imul指令。有符号整数乘法指令,支持两种格式:
- 双操作数,将第二、第三操作数相乘,结果存入第一个操作数(必须为寄存器);
- 三操作数,将第二、第三操作数相乘,结果存入第一个操作数(必须为寄存器)。
其语法如下:
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无符号整数乘法由mul指令实现,仅支持单操作数格式,被乘数隐含在eax中,乘积结果存放在edx
- idiv 指令。有符号整数除法指令,仅指定除数。被除数为 edx
组成的64位有符号数(edx为高32位,eax为低32位)。执行后,商存入eax,余数存入edx。其语法如下:
idiv <reg32>idiv <mem>举例:
idiv ebxidiv dword ptr [var]无符号整数除法指令div的格式与idiv的完全一致,仅对操作数的解释不同。
- and/or/xor指令。分别执行按位与、或、异或操作,结果存入第一个操作数。其语法如下:
and <reg>, <reg> / or <reg>, <reg> / xor <reg>, <reg>and <reg>, <mem> / or <reg>, <mem> / xor <reg>, <mem>and <mem>, <reg> / or <mem>, <reg> / xor <mem>, <reg>and <reg>, <con> / or <reg>, <con> / xor <reg>, <con>and <mem>, <con> / or <mem>, <con> / xor <mem>, <con>举例:
and eax, 0fH #将eax的高28位置0,低4位保持不变xor edx, edx #将edx清零- not指令。按位取反指令,将操作数的每一位0变1、1变0。其语法如下:
not <reg>not <mem>举例:
not byte ptr [var] #将地址var处的1字节内容按位取反- neg指令。取负指令,计算操作数的二进制补码(-x)。其语法如下:
neg <reg>neg <mem>举例:
neg eax- shl/shr 指令。逻辑移位指令:shl 为逻辑左移,shr 为逻辑右移,第一个操作数为被移位数,第二个操作数为移位位数。其语法如下:
shl <reg>, <con8> / shr <reg>, <con8>shl <mem>, <con8> / shr <mem>, <con8>shl <reg>, <cl> / shr <reg>, <cl>shl <mem>, <cl> / shr <mem>, <cl>举例:
shl eax, 1 #eax逻辑左移1位shr ebx, cl #ebx逻辑右移n位(n为cl中的值)(3) 控制流指令
x86处理器通过指令指针寄存器EIP(相当于程序计数器即PC)指示当前执行指令的地址。每条指令执行后,EIP自动指向下一条指令。EIP不能直接访问,但可通过控制流指令修改。程序中常用标签(label)标记指令地址,例如:
指令①begin: 指令② 指令③此处标签 begin指向第二条指令,控制流指令通过标签实现转移。
- jmp指令。无条件转移到label标签所指示的地址继续执行。其语法如下:
jmp <label>举例:
jmp begin- jcondition 指令。条件转移指令,根据程序状态字寄存器中的状态标志(如零标志 ZF、符号标志SF、溢出标志OF等)决定是否转移。常见指令包括:
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, ebxjle done #若 eax ≤ ebx,则转移到done;否则顺序执行下一条指令- cmp/test 指令。cmp指令执行减法运算但不保存结果,仅根据结果设置标志位;test指令执行按位与运算但不保存结果,仅根据结果设置标志位(特别是ZF)。其语法如下:
cmp <reg>, <reg> / test <reg>, <reg>cmp <mem>, <reg> / test <mem>, <reg>cmp <reg>, <con> / test <reg>, <con>cmp <mem>, <con> / test <mem>, <con>cmp 和 test 指令通常与 jcondition 指令配合使用。cmp指令举例:
cmp dword ptr [var], 10 #将var处的4字节内容,与10比较jne loop #若相等则顺序执行;否则转移到looptest 指令举例:
test eax, eax #测试eax是否为零jz xxxx #若eax为零(ZF=1),则转移到xxxx- call/ret指令。分别用于实现子程序调用与返回。其语法如下:
call <label>retcall 指令将下一条指令的地址(返回地址)压入栈,然后转移到 label处执行;ret 指令从栈顶弹出返回地址,并转移到该地址继续执行。call和ret是函数调用机制的核心指令。
掌握上述指令的语法与功能,有助于解答相关考题。建议读者在学习C语言程序时,结合调试工具(如GDB)查看其对应的汇编代码,以加深对机器级指令的理解。
3. 汇编指令格式
使用不同的编程工具开发程序时,所用的汇编器也不同,主要有 AT&T格式和 Intel格式两种(统考常涉及的是 Intel格式)。它们的主要区别如下:
- AT&T格式的指令名必须使用小写字母,Intel格式对大小写不敏感。
- 操作数顺序不同:AT&T格式为“源,目的”,Intel格式为“目的,源”。
- AT&T格式中,寄存器前加“%”,立即数前加“$”;Intel格式中两者均无前缀。
- 内存寻址符号不同:AT&T使用圆括号“()”,Intel使用方括号“[]”。
- 复杂寻址方式表示不同:AT&T格式的内存操作
8(%edx,%eax,2)分别表示偏移量、基址寄存器、变址寄存器和比例因子,表示操作数地址为R[edx]+R[eax]*2+8;对应的 Intel格式为[edx+eax*2+8]。 - 操作数长度指定方式不同:AT&T格式在指令助记符后加后缀,表明操作数大小,“b”表示 byte(字节)、“w”表示word(字)或“l”表示long(双字);Intel格式则在内存操作数前使用“byte ptr”、“word ptr”或“dword ptr”显式指定长度。
NOTE在x86体系结构中,32或64位都是由16位扩展来的,因此word(字)始终表示16位。
表 4.2所示为 AT&T 格式与 Intel 格式的指令对比。其中,mov 指令用于在寄存器与内存之间或寄存器之间传送数据;lea指令用于将有效地址(而非内存内容)加载到寄存器。
表4.2 AT&T格式指令和 Intel 格式指令的对比
| AT&T格式 | Intel 格式 | 含义 |
|---|---|---|
mov $100, %eax | mov eax, 100 | |
mov %eax, %ebx | mov ebx, eax | |
mov %eax, (%ebx) | mov [ebx], eax | |
mov %eax, -8(%ebp) | mov [ebp-8], eax | n |
lea 8(%edx,%eax,2), %eax | lea eax, [edx+eax*2+8] | |
movl %eax, %ebx | mov dword ptr [ebx], eax | 32位 |
注:R[r]表示寄存器r的内容,M[addr]表示主存单元addr的内容,→或←表示信息传送方向。
两种汇编格式的相互转换并不复杂,历年统考真题通常采用 Intel 格式。
4.3.2 选择语句的机器级表示
常见的选择结构语句有 if-then、if-then-else 等。编译器通过条件码(标志位)设置指令和各类条件转移指令来实现程序中的选择结构。条件码描述了最近算术或逻辑运算操作的结果属性,程序通过检测这些标志位来决定是否执行条件分支。常用的条件码有CF、ZF、SF和OF。
常见的算术逻辑运算指令(add, sub, imul, or, and, shl, inc, dec, not, sal等)在执行时会自动设置条件码。此外,cmp和 test指令专门用于设置条件码,它们执行相应的运算(cmp相当于sub,test 相当于 and),但不保存结果,仅根据运算结果更新标志位。
之前介绍的jcondition条件转移指令,就是根据条件码ZF、OF或 SF来实现转移的。
if-else 语句的通用形式如下:
if (test_expr) // test_expr为条件测试表达式 then_statement; // 当test_expr 为真时,执行then_statement 语句else else_statement; // 当test_expr为假时,执行else_statement语句在C语言中,test_expr是一个整数表达式,其值为0时表示“假”,非0(包括负数)时表示“真”。两个分支语句(then_statement 或 else_statement)中只会执行其中一个。
这种通用形式可以被翻译成如下等价的goto语句形式:
t = test_expr; // 暂存测试表达式的结果if (!t) // 若条件为假(t=0) goto false; // 转移至false标签,进入假分支then_statement; // 真分支:仅当 t≠0 时执行goto done; // 执行完真分支后,跳过假分支,转移至结束点false: // 假分支入口标签else_statement; // 假分支:仅当t=0时执行done: // 整个if-else结构的结束点下面以一个具体的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],即加载参数p1到eaxmov edx, dword ptr [ebp+12] # R[edx] ← M[R[ebp]+12],即加载参数p2到edxcmp eax, edx # 比较p1和p2,根据结果设置条件码jbe .L1 # 若 p1 <= p2,则转移到L1mov eax, dword ptr [edx] # R[eax] ← M[R[edx]],即取*p2作为返回值jmp .L2 # 无条件转移到L2,跳过else分支.L1:mov eax, dword ptr [eax] # R[eax] ← M[R[eax]],即取*p1作为返回值.L2:p1和p2是指针型参数,在32位机器中占4字节(一个 dword),比较指令cmp的两个操作数都应来自寄存器,因此需先将p1和p2对应的实参从栈中加载到寄存器。指针比较在C语言中按无符号整数处理,故使用jbe(无符号“小于等于”)进行条件转移。
4.3.3 循环语句的机器级表示
常见的循环结构语句有 while、for 和 do-while。x86 指令集中没有直接对应这些高级循环结构的指令,编译器通过条件测试(如 cmp、test)和条件转移(如 je、jne、jg、jle 等)指令的组合来实现循环逻辑。实际上,大多数编译器会将这三种循环统一转换为类似于 do-while形式。
(1) do-while循环
do-while 语句是一种“后测试”型循环,其通用形式如下:
do body_statement // body_statement为循环体执行语句 while (test_expr); // test_expr为循环继续的条件表达式这种结构可以直接翻译成如下所示的条件和 goto 语句组合:
loop: // 循环入口标签body_statement // 执行循环体语句(首次进入时必然执行)t = test_expr; // 计算循环条件if (t) // 若条件为真(t≠0) goto loop; // 跳回loop标签,继续下一轮迭代由于循环体在条件判断之前执行,因此无论 test_expr 的初始值是什么,body_statement 至少会被执行一次。每次执行结束时,程序会重新计算test_expr的值;如果结果为真(非零),就跳回循环开头继续执行;否则,顺序执行后续代码,退出循环。
(2) while循环
while 语句是一种“前测试”型循环,其通用形式如下:
while (test_expr) // test_expr为循环判断的条件表达式 body_statement; // 当test_expr 为真时重复执行 body_statement 语句与 do-while 不同,while 循环在第一次执行 body_statement 之前就会测试 test_expr 的值。如果初始条件为假(test_expr=0),那么循环体一次都不会执行。
为了用转移指令实现这一语义,编译器通常采用“先判断、再进入循环体”的策略。具体来说,可以将 while循环转换为一个带前置判断的 do-while式结构,如下所示:
t = test_expr; // 计算初始循环条件if (!t) // 若初始条件为假(t=0) goto done; // 跳过循环体,直接转至结束点do body_statement // 执行循环体 while (test_expr); // 重新测试条件done: // 循环结构的结束点进一步展开为纯 goto 语句的形式:
t = test_expr; // 计算初始循环条件if (!t) // 若为假 goto done; // 跳过循环loop:body_statement // 执行循环体t = test_expr; // 重新计算条件if (t) // 若仍为真 goto loop; // 继续下一轮done: // 循环结束这种转换确保了 while循环“先判断、后执行”的语义,同时复用了do-while的结构。
(3) for循环
for 循环是一种语法更紧凑的循环形式,其通用形式如下:
for (init_expr; test_expr; update_expr) body_statement // body_statement 为循环体执行语句init_expr 为初始化表达式,用于初始化循环计数器(如 i=0);test_expr为条件判断表达式,每次执行循环体前测试(如 i<10);update_expr为更新表达式,每次循环体执行完后执行(如i++)。
从执行逻辑上看,for循环完全等价于下面这段 while循环代码:
init_expr; // 初始化(只执行一次)while (test_expr) { // 每次循环前判断条件 body_statement // 执行循环体 update_expr; // 更新循环变量}因此,可以先将 for 循环转换为 while 形式,再按照前述方法转换为 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 = 1test_expr: i <= nupdate_expr: i++body_statement: result += i通过替换前面给出的模板中的相应位置,很容易将 for 循环转换为 while 或 do-while循环。将这个函数翻译为goto语句代码后,不难得出其过程体的汇编代码:
mov ecx, dword ptr [ebp+8] # R[ecx] ← M[R[ebp]+8],即加载参数n到ecxmov eax, 0 # R[eax] ← 0,即初始化result=0mov edx, 1 # R[edx] ← 1,即初始化i=1cmp edx, ecx # 比较i与n(R[edx] 与 R[ecx])并设置标志位jg .L2 # If greater,则转移到L2.L1: # 循环体入口add eax, edx # R[eax] ← R[eax] + R[edx],即result += iadd edx, 1 # R[edx] ← R[edx] + 1,即i++cmp edx, ecx # 再次比较i:njle .L1 # If less or equal,则转移到L1.L2: # 循环结束点已知实参n已被压入调用函数的栈帧,其对应的存储地址为 R[ebp]+8。编译器将频繁使用的局部变量优化到寄存器中:此处,result被分配到 eax(同时也是返回值寄存器),i被分配到 edx。由于i和n均为int型,判断条件 属于有符号比较,因此使用jle和jg等有符号转移指令。
4.3.4 过程调用的机器级表示
在程序执行过程中,当一个过程(函数)调用另一个过程时,需要完成参数传递、控制转移、现场保护与返回等一系列操作。x86架构通过call和ret指令支持这一机制,它们都属于无条件转移指令,但配合栈的使用,实现了完整的“调用-返回”语义。
假定过程P(调用者)调用过程Q(被调用者),整个调用过程的执行步骤如下:
- P将入口参数(实参)放到Q能访问的位置。
- P执行 call指令,将返回地址(call的下一条指令地址)压入栈中,并转移到Q。
- Q建立自己的栈帧,为局部变量分配空间,并在必要时保存某些寄存器。
- Q执行其主体代码。
- Q将返回结果放到约定位置,释放局部变量空间,并恢复之前保存的寄存器。
- Q执行 ret指令,从栈顶弹出返回地址,并转移回P继续执行。
上述过程中,入口参数、返回地址、局部变量、返回结果以及需保护的寄存器内容,都需要在内存中找到合适的存放位置。由于寄存器数量有限,且调用双方共享寄存器资源,若不加保护直接覆盖,将导致程序出错。因此,调用约定和栈的配合至关重要。规定:寄存器EAX、ECX和EDX是调用者保存寄存器:若P希望在调用后仍使用这些寄存器的值,则P必须在调用前自行保存(如压栈),并在返回后恢复。寄存器EBX、ESI、EDI是被调用者保存寄存器:若Q需要使用这些寄存器,则Q必须在使用前将其保存到栈中,并在返回前恢复。
每个过程在执行时,都会在运行时栈中分配一块专属区域,称为栈帧。整个运行时栈由多个连续的栈帧组成。EBP(基址指针)指向当前栈帧的基址(保存旧EBP的位置),ESP(栈指针)始终指向当前栈顶。栈从高地址向低地址增长;过程执行时,ESP会随数据入栈/出栈动态变化,而EBP在当前过程执行期间保持不变,便于通过固定偏移访问参数和局部变量。
下面用一个简单的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对应的汇编代码如下:
caller: push ebp # 保存调用者P的EBP mov ebp, esp # 建立新栈帧:EBP ← 当前栈顶 sub esp, 24 # 为局部变量和参数区分配24字节空间 mov [ebp-12], 125 # M[R[ebp]-12] ← 125,即temp1=125 mov [ebp-8], 80 # M[R[ebp]-8] ← 80,即temp2=80 mov eax, dword ptr [ebp-8] # R[eax] ← M[R[ebp]-8],加载temp2 mov [esp+4], eax # M[R[esp]+4] ← R[eax],将temp2放入参数区高地址 mov eax, dword ptr [ebp-12] # R[eax] ← M[R[ebp]-12],加载temp1 mov [esp], eax # M[R[esp]] ← R[eax],将temp1放入参数区低地址 call add # 调用add,返回值保存于eax mov [ebp-4], eax # M[R[ebp]-4] ← R[eax],将返回值存入sum mov eax, dword ptr [ebp-4] # R[eax] ← M[R[ebp]-4],将sum作为返回值 leave # 等价于 mov esp, ebp 和 pop ebp ret # 弹出返回地址并跳回假设 caller 被过程 P调用。在执行完 sub esp, 24 后,caller的栈帧已建立,ESP 指向新栈顶。GCC为 caller 分配了24字节的空间。从代码可见:
- caller 仅使用了EAX(属于调用者保存寄存器),没有使用任何被调用者保存寄存器。因此其栈帧中除了通过“push ebp”保存调用者P的旧EBP外,无须保存其他寄存器。
- caller的三个局部变量 temp1、temp2和sum皆被分配在栈帧中,占12字节。
- 在call指令调用 add之前,caller依次将入口参数 temp2和 temp1的值(80和125)保存到栈中(左参数位于较低地址,右参数位于较高地址),占8字节。
- 执行 call 指令时再把返回地址压入栈中,占4字节。
包括最初压入栈中的旧EBP值(4字节)在内,caller栈帧实际使用的空间是4+12+8+4=28字节。然而,由于GCC为了保证数据的严格对齐,规定每个函数的栈帧大小必须是16字节的倍数,最终分配的栈帧大小为32字节,这意味着有4字节(未使用)作为对齐填充。
call 指令执行后,add函数的返回值存放在 EAX中。因此,call 指令后面的两条指令分别完成:指令 mov [ebp-4], eax 将 add的结果存入变量 sum的存储位置,该变量位于地址 R[ebp]-4;指令 mov eax, dword ptr [ebp-4] 再将 sum的值加载到EAX,作为caller的返回值。
在执行 ret 指令之前,必须释放当前栈帧并恢复调用者(P)的基址指针。上述第14行 leave指令正是用于完成这一任务,其功能等价于以下两条指令:
mov esp, ebp # 将栈指针ESP设置为当前EBP的值,即指向保存旧EBP的位置pop ebp # 从栈中弹出旧EBP值,恢复过程P的基址指针执行完这两条指令后,EBP已恢复为过程P中的原始值,而ESP则指向栈顶的返回地址。此时,ret指令便可从ESP所指位置取出该返回地址,并转移回过程P继续执行。当然,编译器也可以不使用 leave指令,而是通过显式的 pop 操作配合对 ESP的调整来实现栈帧的回收。
add 函数编译并链接后,对应的机器代码如下:
8048469: 55 push ebp804846a: 89 e5 mov ebp, esp804846c: 8b 45 0c mov eax, dword ptr [ebp+12]804846f: 8b 55 08 mov edx, dword ptr [ebp+8]8048472: 8d 04 02 lea eax, [edx+eax]8048475: 5d pop ebp8048476: c3 ret通常,一个过程的机器级代码可分为三个部分:准备阶段、过程体和结束阶段。
准备阶段(第1、2行):“push ebp”将 caller的EBP值保存到栈中,随后“mov ebp,esp”使EBP指向当前栈帧的基址。如图4.12所示,EBP指向 add栈帧底部。这里 add的入口参数x和y对应的值(125和80)分别在地址为 R[ebp]+8、R[ebp]+12 的存储单元中。
过程体(第3、4、5行):第3行将 y([ebp+12]) 加载到EAX,第4行将 x([ebp+8]) 加载到EDX。第5行使用 lea指令计算 edx+eax,并将结果存回 EAX,作为函数的返回值。这里好像没有加法指令,实际上 lea 指令执行的是加法运算 R[edx]+R[eax]=x+y。
结束阶段(第6、7行):“pop ebp”恢复caller的EBP值,使栈帧指针回到调用前的状态;随后 ret 指令从栈顶弹出返回地址,并转移回调用点。此时栈顶正是 call 指令执行时压入的返回地址,对应 caller 中紧接在“call add”之后的那条指令 (mov [ebp-4],eax)。
由于add过程不包含局部变量、未使用任何被调用者保存的寄存器,且不再调用其他过程(没有入口参数和返回地址要保存)。因此,其栈帧结构极为简洁:仅需保存EBP以维持调用链的完整性,无须额外空间用于寄存器保存、局部变量或嵌套调用。
