汇编指令编码纠错:长度后缀、寻址方式与栈帧

发布时间:2026/10/29 8:53:02
汇编指令编码纠错:长度后缀、寻址方式与栈帧 简介面向计算机系统基础课程学习者的第三章课后习题参考答案对应袁春风《计算机系统基础第二版》教材聚焦处理器指令系统、寻址方式、操作数格式与程序机器级表示等核心内容。答案按题号逐条整理覆盖寻址模式判定w、b、l 后缀与源、目的操作数的组合、指令格式错误辨析、数据类型转换表、函数参数在栈中的存放位置如 R[ebp]8、12、16以及 add、sub、or、test 等指令对 OF、SF、ZF、CF 标志位的影响与十六进制运算过程并给出把机器级代码还原为 C 语言函数的思路。资源包共 1 个 PDF 文件约 3.15MB页面按习题顺序编排便于对照教材边做边核对。已有 7738 人学习下载适合课后自测、期末复习与考研专业课查漏补缺也可作为理解程序底层执行过程的练习参照。1. 长度后缀写错报错的不是语义而是编码表翻开计算机系统基础第三章的课后习题第 4 题有 8 个小问全是判断汇编指令哪里写错了。多数人第一反应是「汇编器在帮我检查语义」——源操作数 16 位而后缀是 b宽度对不上所以报错。这个理解反了。汇编器并不懂你的语义它只是去查一张编码表movb对应哪个操作码、后面跟几个字节的立即数、ModR/M 里的寄存器字段怎么解释。当源和目的宽度拼不出表里的任何一行它才拒绝。真正值得搞懂的是那张表长什么样、为什么有些组合压根没有格子可放。搞清这一点寻址方式、符号扩展、标志位这三块知识会自己串起来。2. 长度后缀与寻址方式一条汇编指令里到底编码了什么2.1 b/w/l 不是注释它决定了操作码和立即数字节数在 ATT 语法里movb、movw、movl看着只差一个字母落到机器码上是三个不同的操作码族。以 mov 为例寄存器到内存这一族里字节操作是0x88字和长字共用0x89再由前缀区分立即数到寄存器这一族里0xB0r是 movb imm80xB8r是 movl imm32。后缀同时钉死三件事操作数宽度、立即数字段占几个字节、寄存器编号怎么解释。movl $1, (%eax) # C7 00 01 00 00 00 6 字节opcode ModR/M imm32 movb $1, (%eax) # C6 00 01 3 字节opcode ModR/M imm8第二行如果写成movl $1, (%eax)立即数字段就必须撑到 4 字节反过来把 32 位立即数塞进movb编码器要么截断要么报错。习题 4 的第2问「源操作数是 16 位而长度后缀是字节 b」本质上就是这个矛盾mov这类指令的源和目的必须同宽后缀只说一次源目的共享。只有movs/movz这种带两个宽度字母的指令才允许源窄目的宽。由此可以推出一条实用判断看到一条mov类指令先数后缀再看两个操作数是不是都符合这个宽度。不符合就是编码层的问题不是逻辑层的问题。2.2 源与目的的组合为什么目的操作数不能是立即数ATT 的书写顺序是opcode src, dst源在左目的在右和 Intel 语法正好相反。操作数有三种形态立即数以$开头寄存器以%开头内存写成disp(base, index, scale)。习题 4 第1问「源操作数是立即数 0xFF需在前面加$」说的就是少了这个前缀汇编器会把0xFF当成内存地址 255 去解析。真正有意思的是第3问目的操作数不能是立即数寻址。这不是「语义上没意义」被拒绝而是编码格式里根本没有这个位置。IA-32 的指令由操作码后面跟 ModR/M 字节构成ModR/M 拆成三段mod2 位区分寄存器和各种内存寻址、reg3 位通常是一个寄存器操作数、r/m3 位配合 mod 表示另一个寄存器或内存。立即数不走 ModR/M它单独占操作码后面的 imm8/imm16/imm32 字段。也就是说一条指令最多只有一个「立即数字段」的位置它天然只能当源。想把立即数当目的编码器找不到格子放它。同样的逻辑解释了另外两问。第5问「不能用 8 位寄存器作为目的操作数地址所在寄存器」ModR/M 和 SIB 里的 3 位寄存器字段在 32 位模式下解释成 EAX 到 EDI没有 8 位的解释方式地址运算本身也是在 32 位 ALU 里做的。第7问「不存在 ESX 寄存器」段寄存器只有 ES、CS、SS、DS、FS、GS 六个把 ES 加上 32 位寄存器的X后缀拼成ESX是把两套命名体系混在了一起。第8问「源操作数地址中缺少变址寄存器」指向的是 SIB 字节的结构问题。当内存操作数写成disp(base, index, scale)时编码里必须有 base 和 index 两个字段缺了 index剩下那部分就不能按比例变址来解释。这类错误在手工反汇编时特别容易看走眼——机器码里字段是全的是人的书写漏了。2.3 比例变址数组下标怎么被压进一条指令SIB 字节的结构是 scale2 位、index3 位、base3 位。scale 两位只能表示四种比例00 是 1 倍01 是 2 倍10 是 4 倍11 是 8 倍。这不是随便挑的数字它精确对应 C 语言里最常见的访存模式元素大小 × 下标。char 数组比例 1short 数组比例 2int 和指针数组比例 4double 或含 8 字节成员的结构体数组比例 8。习题 21 给了一张表达式与汇编的对照表挑三行看最清楚。设 S 是short *首地址在 EDX下标 i 在 ECXleal (%edx, %ecx, 2), %eax # Si → R[eax] AS 2*i movw (%edx, %ecx, 2), %ax # S[i] → 取值 M[AS 2*i] movw 8(%edx, %ecx, 8), %ax # S[4*i4] → M[AS 8*i 8]第三行是整张表里最能说明问题的一行。S[4*i4]的地址按定义是AS 2*(4i4)展开成AS 8i 8。编译器没有生成「先乘 4 再乘 2」的两步而是把2×48折进了 scale 字段把2×48折进了 disp 字段。一条指令、一次地址计算比例和偏移都在编码里。对照表里还有一行值得注意S[i] - S的值是 i汇编只有一条movl %ecx, %eax。按定义这是(AS2i-AS)/2除以 2 在编译期就完成了运行时不需要任何运算。这说明指针减法在机器级之所以能直接得到元素个数靠的是编译器知道元素大小而不是 CPU 有什么特殊能力。多维数组把这套机制用得更满。习题 23 里a[i][j][k]的地址被算成a 4*(63*i 9*j k)反推维度内层步长 1 对应 k中层步长 9 对应 j 所以 M9外层步长 63 9×7 所以 N7数组总字节数 4536 4×18×9×7 所以 L18。三个维度、一次 lea 配两次 imul就把三维下标压成了一个线性偏移。提示手工反汇编看到(%eax,%ebx,4)这种形式先确认元素类型是不是 4 字节再看 base 和 index 谁对应首地址、谁对应下标。顺序搞反算出来的地址会差一个数量级。3. 符号扩展与标志位同一条 ALU 的三种态度3.1 movs 与 movzC 语言整型提升的机器语义习题 5 那张表列了七组类型转换核心只有两条指令movs符号扩展和movz零扩展。它们的助记符带两个宽度字母前一个说源多宽后一个说目的多宽。movsbl就是把 8 位源符号扩展到 32 位目的。movsbl %al, (%edx) # char → int把 AL 的符号位铺满高 24 位后存 4 字节 movzbl %al, (%edx) # unsigned char → unsigned int高 24 位补 0 movb %al, (%edx) # int → char只取低 8 位直接截断 movswl %ax, (%edx) # short → unsigned仍然是符号扩展第四行是表里最容易做错的一行。short转unsigned看着像应该零扩展实际用的是movswl。理由在 C 语言的整型提升规则里short先提升为int值保持不变再转成unsigned int。如果源值是 -1正确结果是 0xFFFFFFFF用零扩展会得到 0x0000FFFF值完全变了。所以决定用 s 还是 z 的不是目的类型有没有符号而是源值在拓宽过程中要不要保号。反过来int → char用movb而不是movsb因为这是窄化只保留低 8 位高位直接丢弃没有「扩展」这回事。int → unsigned int则是同宽搬运movl一条搞定位模式一个不变。这里有个常被忽略的细节扩展是「取出—扩展—写入」三步里的中间一步不是独立指令。movsbl %al, (%edx)一条指令完成了读 8 位、扩到 32 位、写 4 字节。所以扩展指令的两个操作数宽度不必相同这也是mov系列做不到的。3.2 32×32 到 64EDX:EAX 与高低位拼装习题 10 那段代码是在算一个 64 位无符号数的乘法被乘数 x 是 32 位乘数 y 是 64 位拆成 yh高 32 位和 yl低 32 位两半。movl 12(%ebp), %eax # EAX x movl 20(%ebp), %ecx # ECX yh imull %eax, %ecx # ECX yh * x 的低 32 位双操作数 imul 只取低位 mull 16(%ebp) # EDX:EAX yl * x无符号 64 位积 addl %ecx, %edx # EDX (yl*x)高32位 (yh*x)低32位 movl 8(%ebp), %ecx # ECX d movl %eax, (%ecx) # d 的低 32 位 积的低 32 位 movl %edx, 4(%ecx) # d 的高 32 位 拼出来的高位原理展开成公式就一句话x*y x*yl (x*yh) 32。低 32 位完全由x*yl决定x*yh只贡献高 32 位而x*yh自身的低 32 位正好落在结果的第 32 到 63 位所以要把它的低 32 位加到x*yl的高 32 位上。x*yh的高 32 位超出 64 位范围直接丢弃。mull是单操作数形式隐式使用 EAX 作被乘数结果放 EDX:EAX正好给了 64 位空间。imull %eax, %ecx是双操作数形式只保留低 32 位。两者混用不是随意搭配而是「需要完整的 64 位积时用单操作数只需要低 32 位时用双操作数」。这也解释了为什么第 4 行选无符号的mully 的类型是unsigned long long用有符号版本在高位修正上会多出几条指令。3.3 标志位add、or、test、dec、shl 各管一摊EFLAGS 里的 CF、OF、SF、ZF 不是每条指令都全管。加法减法最老实四个标志全按定义算位运算偷懒CF 和 OF 直接清零test做完 and 不写回结果只留标志inc/dec干脆不碰 CF。拿习题 8 的几个具体数字过一遍最直观。第一问0x00000080 0xFFFFFFF0无符号看是进位有符号看是一个正数加一个负数0000 0000 0000 0000 0000 0000 1000 0000 1111 1111 1111 1111 1111 1111 1111 0000 1 0000 0000 0000 0000 0000 0000 0111 0000截断到 32 位是 0x00000070最高位是 0 所以 SF0结果非零所以 ZF0。最高位产生了进位所以 CF1。两个操作数一正一负异号相加永不溢出所以 OF0。第二问0x00000010 - 0x80000008减法在硬件里是加补码。被减数 0x10 是正的小数减数 0x80000008 当作有符号数是 -2147483640正数减负数结果应该是正的 2147483656超出 int 范围结果变负因此 OF1、SF1。无符号角度看是小数减大数需要借位CF1。第三问0x0100 or 0xFF00得 0xFF00。位运算没有进位和溢出概念硬件干脆把 CF 和 OF 强制清 0只根据结果设置 SF 和 ZF最高位是 1 所以 SF1结果非零所以 ZF0。第四问testb $0x80, %dl做的是0x80 and 0x80得 0x80但结果不写回 DL。它的用途是只看最高位位 7 为 1 说明这个字节按有符号解释是负数。所以习题 12 里判断x 0用的是testb $0x80, %dl而不是cmpb $0, %dl少一次比较、少一个立即数。第六问decw %cxCX 从 0x0010 变 0x000F。正数减 1OF0结果最高位 0SF0非零ZF0。注意 CF 在这里原封不动——inc/dec的设计就是不更新 CF所以在多字节减法里 CF 存着上一字节的借位不能被循环计数器的自减冲掉。第五问最值得琢磨M[0x8049380] * 32被编译成左移 5 位0x908f12a8 5得0x11e25500显然溢出但没有任何标志位报告这件事。原因有两层移位指令的 CF 只装最后一次移出的那一位OF 只在移 1 位时有定义移多位时 OF 无意义而教材口径里乘法指令本身不设置 SF/ZF只用 CF/OF 表示高位是否有有效信息。所以「乘出界了」这件事机器级不会给你一个 ZF1 这样的后悔药只能靠程序员自己判断。4. 栈帧与过程调用EBP 链、参数偏移与返回地址4.1 R[ebp]8 起的参数布局IA-32 的过程调用靠栈传递参数。call指令先把返回地址压栈进入被调用函数后第一条push %ebp把调用者的帧指针也压进去然后mov %esp, %ebp建立新帧。于是从 EBP 往上数0 是调用者的 EBP4 是返回地址8 开始才是第一个参数。习题 6 里func(int *xptr, int *yptr, int *zptr)的三个指针分别落在R[ebp]8、R[ebp]12、R[ebp]16。每个指针 4 字节边界自然对齐没有填充。参数从右往左压栈所以最后压进去的第一个参数反而在最低地址。知道了参数位置函数体几乎可以逐行还原movl 8(%ebp), %eax # 取 xptr movl (%eax), %ecx # tempx *xptr movl 12(%ebp), %edx # 取 yptr movl (%edx), %eax # tempy *yptr movl %eax, (%edx) # *yptr tempx这段对应的是三个指针指向的值轮换*yptr 旧 *xptr*zptr 旧 *yptr*xptr 旧 *zptr。所以要先把三个值都读进临时变量再依次写回否则第二次写会覆盖掉还没用到的旧值。参数偏移这个知识点看着简单翻车的人不少。指针参数占 4 字节int占 4 字节short占 4 字节按 4 字节边界对齐char也占 4 字节long long占 8 字节并对齐到 4 的倍数。习题 10 里 y 是 64 位所以R[ebp]16是低 32 位、R[ebp]20是高 32 位两个槽位紧挨着。推算偏移时先画一张从 8 开始的槽位表比凭感觉加要可靠得多。4.2 局部变量的位置sub 之后偏移是负的函数开场做三件事保存旧 EBP、建立新 EBP、把 ESP 往下挪给局部变量腾地方。sub $0x28, %esp就是挪出 40 字节。此后局部变量全部通过R[ebp]-k访问。习题 18 给了一串栈指针变化进入函数时 ESP 是 0xbc000020返回地址已经压好push %ebp后变 0xbc00001cmov %esp,%ebp让 EBP 也等于 0xbc00001csub $0x28,%esp后 ESP 变成 0xbbfffff4。于是ebp-4是 0xbc000018ebp-8是 0xbc000014。leal -4(%ebp), %eax # x ebp-4 pushl %eax # 第二个参数入栈 leal -8(%ebp), %eax # y ebp-8 pushl %eax # 第一个参数入栈 call scanf这里能看出一个规律局部变量的地址是「EBP 减去偏移」参数的地址是「EBP 加上偏移」方向相反中间隔着返回地址和旧 EBP。计算任何变量的地址前先确认它在 EBP 的哪一侧。还有一个容易忽略的点每次push让 ESP 减 4call再减 4压返回地址。所以「调用前 ESP 是多少」和「进入被调用函数时 ESP 是多少」差 4。习题 18 里那句「未跳转到 scanf 时 ESP 是 0xbbfffff0从 scanf 返回后是 0xbbfffff4」差的正是这一层。4.3 缓冲区溢出返回地址被改写那一刻习题 29 给了一个带gets的函数。buf的首地址是EBP - 0x14也就是说从 buf 到 EBP 之间只隔了 0x14 20 字节。这 20 字节的布局是8 字节的 buf接着是被调用者保存寄存器 EBX、ESI、EDI 各 4 字节共 12 字节再往上 4 字节是旧 EBP再往上 4 字节是返回地址。gets不做边界检查输入超过 8 字节就开始写穿。写满 20 字节后接下来 4 字节覆盖旧 EBP再 4 字节覆盖返回地址。函数执行ret时CPU 从栈顶弹出这个被污染的地址当跳转目标。如果攻击者精心构造输入让这 4 字节指向一段可执行的代码控制流就被劫持了如果随便填比如让地址变成0x08413938落在未映射或不可执行的段上就会收到 segmentation fault。这段代码还埋了两个 bugmalloc(strlen(buf)1)少了1字符串结尾的\0没地方放返回的指针没判 NULL 就直接用。这两处和溢出是独立的错误但经常一起出现。注意EBP - 0x14这种「数组到帧指针的距离」是判断溢出风险最快的指标。距离越小可写的安全空间越窄。5. 常见排查那些「答案对不上」的时刻5.1 相对转移的偏移量基准是下条指令现象手工算跳转目标结果和答案差 2 或差 5。原因IA-32 的条件转移和 call 都用相对寻址偏移量是相对「下一条指令的地址」而不是当前指令地址。je是 2 字节指令机器码74 08里的 08 要加在当前地址 2 的位置上call是 5 字节指令后面的 4 字节偏移要加在当前地址 5 的位置上。解决先确定指令长度再用「当前地址 指令长度 偏移」算目标。教学材料里0x804838c处的je目标算成0x804838c280x8048396同一个材料里0x804838e处的call目标算成0x804838e50x1e0x80483b1两处用的都是这个规则。8 位和 32 位偏移都是补码0xFFFFFF00要按 -256 解释往前跳而不是往后跳 40 多亿字节。5.2 小端序把立即数读反了现象从机器码里抠出的立即数是0x00A80408但汇编语句里写的是0x804a800。原因IA-32 是小端多字节数据低字节存低地址。movl $0x804a800, (%ecx)的机器码里立即数字段按字节是00 A8 04 08。从头按大端拼就得到错误的数。解决从机器码反推立即数时按字节序号倒着拼或者先标出每个字节的地址再按地址从高到低读。这条规则对所有多字节字段都成立包括偏移量和地址。5.3 结构体大小按成员长度累加永远算不对现象一堆成员加起来 44 字节实际sizeof是 48换个平台编译又变成 40。原因成员之间要按各自的对齐要求填充结构体总大小还要补齐到最大成员对齐值的整数倍。Windows 上double和long long按 8 字节对齐Linux 32 位下double按 4 字节对齐。同一个结构体定义char、double、int、short、char*、long、void*、long long排下来Windows 上偏移是 0、8、16、20、24、28、32、40末尾还要补 4 字节凑到 8 的倍数共 48 字节。解决按「偏移量 上一个成员结束位置向上取整到本成员对齐值」逐个算最后把总大小向上取整到最大对齐值。想省空间就把成员按长度从大到小重排上面那个结构体能从 48 压到 40。别在不同平台上假设sizeof一样尤其是涉及序列化和文件格式的时候。5.4 有符号除法忘了符号扩展或者把 sar 写成 shr现象y % k在 y 为负数时结果离谱或者 idiv 直接触发除法异常。原因idiv的被除数是 DX:AX 组成的 32 位值做 16 位除法前必须把 AX 的符号位铺满 DX。教材里用sarw $15, %dx手工完成这一步算术右移 15 位正数得 0负数得 0xFFFF。如果误用shrw负数的高位变成 0被除数就从负数变成了一个大正数商和余数全错。反过来如果 DX 里残留了上一次运算的值而没做扩展idiv可能因为商超出 16 位范围而抛出异常。解决有符号除法前用cwd16 位或cdq32 位做符号扩展或者手写sar到符号位。无符号除法前把 DX 清零。看到sar和shr混用基本可以判定是符号处理的 bug。5.5 跳转表忘了减偏置case 全部错位现象switch反汇编出来的跳转表查得到下标但跳到的分支和预期差几位。原因跳转表是从 0 开始索引的数组而 case 值可以是负数或从任意数开始。编译器会先做一个平移把[min_case, max_case]映射到[0, 表长-1]。习题 16 里先算x3再查表说明最小 case 是 -3 或者表的下标从 -3 开始对齐表里下标 0 和 6 都指向 default 分支说明 case -3、3 没有对应处理。解决看跳转表前先找那个加法或减法把偏置补回去再确认表项数和 default 的位置。表里多个下标指向同一个标号通常意味着那几个 case 共享一段代码或者落在 default 上。6. 设计取舍为什么宽度和寻址要压进操作码回过头看这套编码会发现一个贯穿全局的取舍IA-32 把「多宽、怎么寻址、要不要扩展」全部塞进了操作码和 ModR/M 里而不是交给多条指令组合。一条movl 8(%edx,%ecx,4), %eax同时完成了基址加变址乘比例加偏移、32 位读、写寄存器三件事。代价是指令长度从 1 字节到 15 字节不等解码器必须先找到指令边界才能知道下一条从哪开始。这个代价在乱序执行的时代被放大了。现代处理器前端要把变长指令切成微操作做法是预解码阶段先扫一遍字节流标记出每条指令的起止位置再送进真正的解码器。同一段逻辑RISC 风格需要三条定长指令算地址、load、写回但每条都是本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询