Appearance
题目
已知 ,计算 的 C 语言函数 f1 的源程序(阴影部分)及其在 32 位计算机 M 上的部分机器级代码如下:
int f1(int n) {
1 00401000 55 push ebp
... ... ...
if(n>1)
11 00401018 83 7D 08 01 cmp dword ptr [ebp+8],1
12 0040101C 7E 17 jle f1+35h (00401035)
return n*f1(n-1);
13 0040101E 8B 45 08 mov eax, dword ptr [ebp+8]
14 00401021 83 E8 01 sub eax, 1
15 00401024 50 push eax
16 00401025 E8 D6 FF FF FF call f1 ( 00401000)
... ... ...
19 00401030 0F AF C1 imul eax, ecx
20 00401033 EB 05 jmp f1+3Ah (0040103a)
else return 1;
}
21 00401035 B8 01 00 00 00 mov eax,1
... ... ...
26 00401040 3B EC cmp ebp, esp
... ... ...
30 0040104A C3 ret其中,机器级代码行包括行号、虚拟地址、机器指令和汇编指令,计算机 M 按字节编址,int 型数据占 32 位。请回答下列问题:
(1) 计算 f(10) 需要调用函数 f1 多少次?执行哪条指令会递归调用 f1?
(2) 上述代码中,哪条指令是条件转移指令?哪几条指令一定会使程序跳转执行?
(3) 根据第 16 行的 call 指令,第 17 行指令的虚拟地址应是多少?已知第 16 行的 call 指令采用相对寻址方式,该指令中的偏移量应是多少(给出计算过程)?已知第 16 行的 call 指令的后 4 字节为偏移量,M 是采用大端方式还是采用小端方式?
(4) f(13)=6227020800,但 f1(13) 的返回值为 1932053504,为什么两者不相等?要使 f1(13) 能返回正确的结果,应如何修改 f1 的源程序?
(5) 第 19 行的 imul 指令(带符号整数乘)的功能是 R[eax]←R[eax]×R[ecx],当乘法器输出的高、低 32 位乘积之间满足什么条件时,溢出标志 OF=1?要使 CPU 在发生溢出时转异常处理,编译器应在 imul 指令后应加一条什么指令?
解析
本题考查 递归函数的机器级理解:把汇编代码翻成"函数调用 + 跳转"流程,并涉及 call 指令的相对寻址、字节序、整数溢出、溢出陷阱 等几个细节。
阅读机器代码先抓 三类跳转:
jle / je / jne等条件转移;call / ret函数调用 / 返回;jmp无条件跳转。
(1) 调用次数与递归调用指令 [2 分]
f1(10) → f1(9) → ... → f1(1) → 返回 1。链条上一共调用 10 次 f1(最外层一次 + 内部递归 9 次)。
第 16 行 call f1 是递归调用指令——它在 f1 的代码体内又调用 f1 自身。
(2) 条件转移与必跳转的指令 [2 分]
条件转移指令: 第 12 行 jle —— 当上一个比较结果为"小于或等于"(即 ZF = 1 或 SF ≠ OF)时跳转。本题中 jle 跳到 00401035H(即 else return 1 分支),对应 n ≤ 1 的递归出口。
一定会跳转的指令(无条件改变 PC):
| 行 | 指令 | 含义 |
|---|---|---|
| 16 | call | 调用 f1 → PC 跳到被调函数地址 |
| 20 | jmp | 无条件跳过 else 分支到 0040103AH |
| 30 | ret | 子程序返回,PC 恢复为调用方下一条 |
(3) call 后续指令地址、call 偏移量、字节序 [4 分]
Step 1. 第 17 行虚拟地址。
第 16 行 call 的机器码 E8 D6 FF FF FF(5 字节),起始地址 00401025H:
Step 2. call 偏移量计算。
x86 的 call 是相对寻址,目标地址 = (call 后续指令地址) + 偏移量(即 PC 在 fetch call 之后已经指向第 17 行):
按 32 位补码减法:
Step 3. 判断字节序。
机器码中偏移字段是 D6 FF FF FF(按内存顺序读出,低字节在前)。如果是小端序,最低有效字节 D6 存放在最低地址,与 FFFF FFD6H 的低字节 D6 对应;如果是大端序,最低地址应存放最高字节 FF。
字段顺序 D6 FF FF FF → 最低地址放最低字节 → M 采用小端(little-endian)方式。
编者注(生僻术语): x86 / x86-64 / RISC-V 均为小端序;ARM 默认小端但可切换。区别只在多字节数据在内存中的字节顺序上,不影响算术运算结果。
(4) f1(13) 不正确的原因与修改方法 [2 分]
,超过 32 位 int 上限 。
f1 在递归乘法过程中发生了 溢出,结果按 mod 截断后再按补码解读 → 1932053504(注意:这是从下方截掉位之后再按 int 重新解读的"看起来正常但其实错"的数)。
修改方法: 把 f1 的返回类型改成 更宽的整数(long long,64 位) 或 浮点(double / long double),使范围足够容纳 13!。同时把内部变量 eax 等也对应宽化。
易错点: 不要简单改成
unsigned int——unsigned 范围 仍小于 。必须升到 64 位整数或浮点。
(5) imul 指令的溢出判定 + 加什么指令转异常 [3 分]
OF = 1 的条件: imul 是带符号整数乘法,输出 64 位结果(高 32 + 低 32)。仅取低 32 位作为返回值时,溢出判定为:
换言之,乘积高 33 位(高 32 位再加上低 32 位的最高位)既不全为 0 也不全为 1 时,OF = 1。原因:带符号截断不溢出当且仅当被截掉的高位是低位符号位的符号扩展。
应加的指令: 在 imul 后插入一条 溢出自陷指令(如 x86 中的 into,或 MIPS 的 tge / RISC-V 的"显式判 OF 后 trap")。它检测 OF 标志,OF = 1 时触发异常 → 进入溢出异常处理程序。
编者注(生僻术语): "自陷(trap)"是软件主动触发硬件异常的一种机制,与中断的区别在于:中断是异步的(外设来的),自陷是同步的(指令自己产生)。x86 的
into(INTerrupt on Overflow)就是 OF=1 时触发 4 号异常。