简介:《计算机系统基础(第二版)》第三章课后习题参考答案以PDF形式提供,面向正在学习处理器指令系统、机器级表示与底层程序执行的高校学生及备考读者,可用于核对解题思路、定位知识盲区。内容逐题覆盖寻址方式判别、指令格式合法性分析、数据类型转换、函数参数栈布局、算术与逻辑运算后的标志位变化,以及指令执行结果推导等核心知识点,并给出典型题目的参考答案与关键过程。资源包共1个PDF文件,大小约3.15MB,结构紧凑,便于按题号检索、打印批注和课后复盘。已有7738人学习或下载,适合在完成第三章练习后对照查漏补缺,也可作为理解IA-32汇编、寻址模式与标志位管理的辅助材料。
1. 一条 mov 指令背后:第三章的知识地图
复习《计算机系统基础》第三章,最常见的走弯路方式是把它当成一堆课后题答案逐条背。但把题目按题型摊开就会发现,整章只回答一个问题:一句 C 赋值语句,怎么一步步变成机器码里的那串字节。这条链路上有五个固定零件——长度后缀决定操作数占几个字节,寻址方式决定数据来自寄存器、立即数还是内存,扩展指令决定窄类型如何变宽,标志位决定条件跳转走不走,栈帧决定函数参数和局部变量待在哪儿。五个零件串起来,就是从寻址方式一路走到过程调用的完整地图。这份梳理适合已经学过一遍、正在准备期末或考研复习的人,拿它自查哪一环断掉,比从头再读一遍教材省时间。
2. 寻址方式与指令后缀:一条指令的五个零件怎么拼
第三章第一节到第三节的内容,本质是在训练一种“读指令像读公式”的能力。给你一行movl 8(%ebp), %eax,你要能立刻说出:搬 4 个字节,源在内存,地址是 ebp 里的值加 8,目的是 32 位寄存器 eax。这个反应速度决定了后面所有题能不能做下去。
2.1 后缀 b/w/l 与操作数的四类出身
AT&T 格式的汇编里,指令助记符后面那个字母不是随便加的,它直接规定了这次搬动或运算的宽度。
| 后缀 | 全称 | 宽度 | 对应寄存器举例 | 常见用途 |
|---|---|---|---|---|
| b | byte | 8 位 | %al %bl %cl %dl | char、字节读写 |
| w | word | 16 位 | %ax %bx %cx %dx | short、16 位参数 |
| l | long | 32 位 | %eax %ebx %ecx %edx | int、指针、地址 |
没有后缀的指令在 IA-32 里基本不存在,写mov %eax, %ebx汇编器会直接报错,这一点在判断题里出现频率很高。
操作数的“出身”只有四类,第三章反复考的就是这四类的组合。寄存器直接写%eax;立即数前面必须带$,写成$0xFF;内存操作数写成disp(base, index, scale),展开后是M[R[base] + R[index] × scale + disp];栈上的数据本质上也是内存操作数,只是基址固定用 ebp 或 esp。比例因子 scale 只能取 1、2、4、8,这条限制在判断非法指令时会用到。
# 1) 基址 + 偏移:取 ebp+8 处的 4 字节送 eax movl 8(%ebp), %eax # 2) 基址 + 比例变址 + 偏移:地址 = eax + ecx*4 + 12 movl 12(%eax, %ecx, 4), %edx # 3) 纯绝对地址:直接访问 0x8049300 movl 0x8049300, %ebx # 4) 立即数送寄存器:$ 不能省 movl $0xFF, %ecx上面四条里,第一条和第二条是函数参数、数组元素的标准访问姿势;第三条常见于全局变量;第四条是常量赋值。判断一条指令合法与否,先看后缀宽度和操作数宽度是否一致,再看目的操作数是不是可写位置,最后看地址表达式里的寄存器是否承担了合法角色。
提示:
leal是唯一一个“看着像取内存、实际只算地址”的指令。leal 8(%eax,%edx,8), %ecx执行后 ecx = eax + edx×8 + 8,根本不去访问内存。第三章里凡是看到 leal 出现在算术题中,一律按加法器理解,别去找内存。
2.2 六类非法指令:把错误当场判死
课后题里那八小问,其实是同一套检查规则的不同触发方式。把规则背成清单,比逐题记答案管用。
| 错误类型 | 典型写法 | 为什么非法 |
|---|---|---|
立即数缺$ | movl 0xFF, %eax | 被解释成地址 0xFF 的内存读,不是常数 |
| 后缀与宽度不符 | movb $0x1234, %al | 16 位立即数塞不进 8 位目的 |
| 目的是立即数 | movl %eax, $0x10 | 立即数不可写 |
| 立即数超范围 | movw $0x12345, %ax | 超过 16 位却用 w 后缀 |
| 8 位寄存器当地址 | movl (%bl), %eax | 地址寄存器必须是 16/32 位 |
| 源目宽度不一致 | movl %ax, %ebx | 16 位源配 32 位目 |
| 寄存器名不存在 | movl %esx, %eax | IA-32 只有 esi/edi/esp/ebp |
| 变址缺寄存器 | movl (,%eax,,4), %ebx | 用了比例因子却没写变址寄存器 |
这张表的价值在于:考试里给你一条指令问“是否正确”,你按顺序过一遍——后缀对不对、立即数有没有$、目的可不可写、地址寄存器宽度够不够、比例因子有没有配套变址——五秒钟能出结论。做题时很多人卡住,不是不会,是检查顺序乱了,看到$就放过,结果栽在宽度上。
第三条“目的不能是立即数”最容易记混,因为源可以是立即数。记法很简单:数据流向是从源到目的,立即数是一个写在指令里的常量,它没有存放位置,所以只能当源。
2.3 从地址表达式反推寻址方式
题目还有一种常见形式:给你一串 leal 或 mov,让你写出它算出的表达式。比如leal 8(%eax,%edx,8), %ecx对应x + 8*y,leal 12(%eax,%edx,2), %ecx对应y + 2*x + 12。这类题不需要理解语义,只要机械代入公式:基址寄存器乘 1,变址寄存器乘比例因子,再加上偏移。
练熟的标志是能双向翻译。看到4*y,脑子里自动浮现leal 0(,%edx,4), %eax或者leal (%edx,%edx,3), %eax这类写法;看到x + y,知道编译器可能用leal (%eax,%ebx), %ecx一条搞定,而不是先 mov 再 add。第三章里数组下标访问a[i]编译成(%eax,%ecx,4),本质就是这条公式的直接应用——int 数组元素 4 字节,所以比例因子是 4。
3. 数据类型转换与参数定位:从指令宽度反推 C 原型
第三章后半段的难点从“指令对不对”转向了“这段汇编对应什么 C 代码”。这类题有固定解法,核心是两条线索:指令宽度暴露类型宽度,扩展方式暴露符号性。
3.1 源类型 × 目的类型:一张转换矩阵
C 语言里的隐式转换到了机器级,只剩两种动作:截断和扩展。截断取低位,扩展分符号扩展(高位补符号位)和零扩展(高位补 0)。指令助记符的设计正好反映这一点:movs开头是 sign extend,movz开头是 zero extend,后面的两个字母分别是源宽度和目的宽度。
| 源类型 | 目的类型 | 机器级表示 | 语义 |
|---|---|---|---|
| char | int | movsbl %al, (%edx) | 8 位符号扩展到 32 位后存内存 |
| int | char | movb %al, (%edx) | 只取低 8 位,高位丢弃 |
| int | unsigned | movl %eax, (%edx) | 位模式不变,只是重新解释 |
| short | unsigned | movswl %ax, (%edx) | 16 位符号扩展到 32 位 |
| unsigned char | unsigned int | movzbl %al, (%edx) | 8 位零扩展到 32 位 |
| char | int | movsbl %al, (%edx) | 同第一行,符号位决定高位 |
| int | int | movl %eax, (%edx) | 宽度一致,直接搬 |
表里最容易出错的是第四行:short 转 unsigned,很多人以为该用零扩展,其实 C 的整型提升规则是先按自身类型做符号扩展升到 int,再转成 unsigned,位模式上就是movswl。判断口诀是:只要源是有符号窄类型,无论目的是不是无符号,扩展动作都按源来。
movsbl这类双后缀指令要会拆:s 表示符号扩展,b 是源宽度 8 位,l 是目的宽度 32 位。反过来movzbl是零扩展。至于movb、movw、movl这类单后缀指令,宽度相同直接搬,不涉及扩展。
3.2 参数在栈上的坐标:ebp+8 起的对齐规则
IA-32 的过程调用约定里,栈向低地址增长。call 指令先把返回地址压栈,进入被调函数后push %ebp再压一次,mov %esp, %ebp建立当前栈帧。于是 ebp+4 是返回地址,ebp+8 是第一个参数,ebp+12 是第二个,依次往后。
题 6 里三个指针参数落在 ebp+8、ebp+12、ebp+16,正好符合这个规律。把它写成 C 就非常直接:
// 根据 ebp+8 / +12 / +16 三个槽定位到 xptr、yptr、zptr void func(int *xptr, int *yptr, int *zptr) { int tempx = *xptr; // 先取出三个值,避免交换过程中被覆盖 int tempy = *yptr; int tempz = *zptr; *yptr = tempx; // 循环右移一位的赋值 *zptr = tempy; *xptr = tempz; }这里的关键不是代码本身,而是“先全部取出来再写回”的顺序。如果按*xptr = *yptr这样直接写,第一个赋值就把后续要用的值改掉了,这是从汇编反推 C 时最容易看漏的地方——汇编里寄存器充当了临时变量,翻译回 C 时必须自己补上 temp。
参数槽的第二个坑是 4 字节对齐。即使参数是 short,入栈时也占满 4 个字节,所以题 14 里 x、y、k 三个 16 位参数的偏移是 8、12、16,而不是 8、10、12。
3.3 从指令宽度反推 C 函数原型
这类题的解法是给每个参数槽贴标签。看到movb 8(%ebp), %dl,说明第一个参数是 1 字节,char 或者 unsigned char;分不清有无符号就退回看它的使用方式,比如有没有movsbl后续扩展,或者有没有testb $0x80测符号位。看到movl 12(%ebp), %eax而后续用它做(%eax)寻址,那是 32 位指针。
题 17 就是这样拼出来的:movb定第一个参数为 char,两条movw定第二、三个是 16 位,最后返回movl且做零扩展,定返回类型为 unsigned int,于是原型是unsigned int test(char a, unsigned short b, unsigned short c, short *p)。
遇到 8 字节参数要格外小心。题 10 里 y 是unsigned long long,占两个槽:ebp+16 放低 32 位,ebp+20 放高 32 位,取的时候先用movl 20(%ebp), %ecx拿高位,再用mull 16(%ebp)做无符号乘,最后把高位乘积和低位进位加到一起。判断依据是那条mull——无符号乘法,且操作数是内存里的低半部分,说明原始类型宽度是 64 位。
4. 标志位与控制转移:OF/SF/ZF/CF 与跳转地址的算法
第三章第三节之后,题目的重心转到“运算之后发生了什么”和“程序下一步去哪”。这两件事其实绑在一起:运算改标志,标志决定跳转。
4.1 四类指令对标志位的影响
四个标志的分工要先分清。CF 是进位/借位,看的是无符号数最高位有没有往外冒;OF 是溢出,看的是有符号数结果有没有超出表示范围;SF 取结果的最高位,表示正负;ZF 看结果是不是全零。
| 指令类别 | OF | SF | ZF | CF | 备注 |
|---|---|---|---|---|---|
| add / sub | 影响 | 影响 | 影响 | 影响 | 四个标志全改 |
| and / or / xor | 清 0 | 影响 | 影响 | 清 0 | 逻辑运算固定 OF=CF=0 |
| test | 清 0 | 影响 | 影响 | 清 0 | 与 and 相同,但不写回结果 |
| imul / mul | 不改 | 不改 | 不改 | 不改 | 这是最容易记错的一条 |
| inc / dec | 影响 | 影响 | 影响 | 不改 | 专门为了配合循环里的加法 |
| shl / sar | 影响 | 影响 | 影响 | 影响 | 移出的最后一位进 CF |
拿题 8 的第一个小问走一遍:addl (%eax), %edx,eax 指向的内存里是 0xFFFFFFF0,edx 里是 0x00000080。两数相加等于 0x00000070,最高位没有向外进位,所以 CF 这里其实要按无符号加法看:0x00000080 + 0xFFFFFFF0 = 0x100000070,超出 32 位,CF=1。结果 0x70 不为零,ZF=0;最高位是 0,SF=0;有符号视角下 128 + (-16) = 112,没溢出,OF=0。
减法的例子更绕。0x00000010 减 0x80000008,等价于加 0x80000008 的补码 0x7FFFFFF8,得到 0x80000008。无符号减法里被减数小于减数,产生借位,CF=1;结果最高位是 1,SF=1;结果非零,ZF=0;有符号角度看 16 减一个很大的负数,结果超出正数范围,OF=1。
逻辑运算就简单了。or之后 OF=CF=0 是硬规定,SF 和 ZF 按结果算。题里 0x0100 or 0xFF00 = 0xFF00,最高位 1 所以 SF=1,非零所以 ZF=0。
乘法这条要单独记:imul和mul不修改任何标志位。题 8 第五问里 0x908f12a8 左移 5 位得到 0x11e25500,明显溢出,但因为用的是乘法指令,OF 根本不会置 1。这个点在判断题里几乎是必考项,很多人凭直觉认为“溢出就该 OF=1”,结果翻车。
除法还有一个隐藏前提:idiv的被除数必须是除数的两倍宽,所以 16 位除法前必须把 AX 符号扩展到 DX:AX。题 14 里的sarw $15, %dx就是在做这件事——算术右移 15 位,把 AX 的符号位铺满 DX,等价于cwd指令的手写版。
4.2 相对转移:目标地址 = 下条指令地址 + 偏移量
条件转移和 jmp 在 IA-32 里都用相对转移,指令里存的是偏移量,不是绝对地址。计算公式只有一个:
目标地址 = 转移指令地址 + 该指令的字节长度 + 偏移量等价写法是“下条指令地址 + 偏移量”,两种说法一回事,但用“下条指令地址”不容易错,因为你不需要单独记指令长度,只要知道下一条指令从哪开始。
# 机器码 74 08:je 指令,操作码 74,偏移量 08 # 指令位于 0x804838c,长度 2 字节 # 目标 = 0x804838c + 2 + 0x08 = 0x8048396 je 0x8048396 # 机器码 72 f6:jb 指令,偏移量是负数 0xf6 = -10 # 指令位于 0x8048390,长度 2 字节 # 目标 = 0x8048390 + 2 + (-10) = 0x8048388 jb 0x8048388两点必须注意。第一,偏移量是有符号数,0xf6要按补码解释成 -10,不是 246。第二,call 和 jmp 的偏移量是 4 字节,且按小端存放,从机器码里抠出来要倒着拼。题 11 里的call指令机器码后四个字节是1e 00 00 00,拼成 0x0000001E,再拿 0x804838e + 5 + 0x1e 得到 0x80483b1。
反向题(给目标地址求指令地址)就是解方程:题里 jle 的目标是 0x80492e0,偏移 0x16,那么下条指令地址 x 满足 0x80492e0 = x + 0x16,x = 0x80492ca,也就是 jle 指令的结束位置。这类题只要公式写对,剩下都是加减法。
4.3 switch 跳转表与循环结构的还原
switch 编译成跳转表时,典型结构是先用减法把 case 值压到从 0 开始的连续区间,再判断是否越界,最后用jmp *表(,%eax,4)间接跳转。题 16 里x+3说明编译器做的变换是eax = x - (-3),也就是把最小值 -3 映射到下标 0。表里 x+3 = 0 和 6 都指向 .L7,说明 case -3 和 case 3 共用 default 分支。
还原技巧是把下标和标号列成一张对应表,标号相同的合并成同一个 case 组。写 C 的时候注意 fall through:相邻两个标号之间没有 break,就说明是故意贯穿的,比如case -2: case -1:共用一个代码块。
循环的还原靠三件事定位:初始化在哪、判断条件在哪、步进在哪。题 31 的循环里movl $-2147483648, %edi把 i 初始化成 0x80000000,循环体最后shrl %bl, %edi把 i 逻辑右移 k 位,testl %edi, %edi加jne判断 i 是否为零。逻辑右移说明 i 在 C 里必须声明成无符号,否则编译器会用sarl。这一点是判断有符号/无符号的关键证据,比看变量名字靠谱得多。
题 19 的递归更好认:函数开头判x == 0就返回,否则算x >> 1递归调用,返回(x & 1) + rv。三个动作拼起来就是“统计二进制里 1 的个数”。递归题的套路是先找基准情形(返回常量的那个分支),再找递归式(调用自己的那几行),最后看返回值怎么和参数组合。
5. 易错点排查:五个高频翻车现场和补丁
这一章是整份笔记里最该反复看的部分。下面五条都是把习题做错一遍之后才记住的,每一条按“错在哪、为什么错、怎么补”三段写。
5.1 易错点一:转移目标地址少加了指令长度
易漏点:拿到je的机器码,直接用“指令地址 + 偏移量”算目标,忘了中间还隔着指令本身的字节数。
为什么漏:教材里“PC 相对寻址”的公式写的是 (PC) + 偏移量,而这里的 PC 指的是取下这条指令之后的值,不是当前指令地址。看书时一眼扫过去很容易理解成当前地址。
怎么补:做题时强制写成三行——先写指令地址,加上指令字节长度得到下条指令地址,再加偏移量。条件转移指令固定 2 字节,call 和 jmp 是 5 字节(1 字节操作码加 4 字节偏移)。养成写中间步骤的习惯,比在脑子里默算可靠。
5.2 易错点二:后缀宽度和操作数宽度对不上
易漏点:看到movb $0x1234, %al只注意目的操作数是 8 位寄存器,忘了源是 16 位立即数,判成合法。
为什么漏:检查时只扫了一眼右边的寄存器,没回头核对左边的立即数位数。十六进制位数和二进制位数的换算在紧张的考试里容易短路。
怎么补:把常见宽度的十六进制位数背下来——8 位对应 2 个十六进制位,16 位对应 4 个,32 位对应 8 个。判题时先数位数再看后缀,形成固定顺序,不要凭感觉。
5.3 易错点三:参数偏移硬套 ebp+8、+12、+16
易漏点:不管参数实际类型,一律按 4 字节间隔往后排;遇到 long long 或者 double 也不拆成两个槽。
为什么漏:教材例题里前几个参数恰好都是 32 位,看起来像固定规律。而“每个参数至少占 4 字节、8 字节类型占两个槽”这条规则藏在调用约定的细节里,不做题不会主动去想。
怎么补:拿到参数列表先标宽度,char 和 short 各占一个 4 字节槽(高位闲置),int 和指针占一个,long long 和 double 占两个。排完再写下每个槽的偏移。题 10 就是靠这个办法看出 y 占了 ebp+16 和 ebp+20 两个位置。
5.4 易错点四:结构体大小按“成员宽度相加”算
易漏点:算出成员偏移后直接相加当结构体大小,忘了末尾还要补齐到最大对齐边界。
为什么漏:对齐规则里“起始偏移要是成员大小的整数倍”这一半大家记得住,“结构体总大小要是最大成员对齐数的整数倍”这一半经常漏掉。另外 IA-32 下 long double 分配 12 字节但实际只用 10 字节,Windows 平台又要求按 8 字节对齐,跨平台差异不专门记就会混。
怎么补:算完偏移后强制加一步——取所有成员里最大的对齐要求,把总大小向上取整到它的倍数。题 28 里 Windows 平台那个结构体算出 44 字节,因为 double 要求 8 字节对齐,所以补到 48;把成员按长度从大到小重排后降到 40 字节,这个对比正好说明顺序会影响大小。
5.5 易错点五:标志位凭直觉判断
易漏点:认为乘法溢出会置 OF=1,认为 test 会修改操作数,认为 inc/dec 会影响 CF。
为什么漏:加减法用得多,形成了“运算就改标志”的惯性思维。而乘法、逻辑指令、自增自减各有各的特殊规定,分散在教材不同段落,不集中整理就会串。
怎么补:记三条硬规则——乘除指令不改任何标志;逻辑指令和 test 固定把 OF 和 CF 清 0,只按结果设 SF 和 ZF;inc/dec 只改 OF、SF、ZF,特意保留 CF 以便和 add/adc 配合做多字长加法。做题时遇到这三类指令,先在草稿上标一句“不改 CF”再往下算。
6. 复习清单:把第三章压成一张自测表
复习到收尾阶段,最有用的不是再刷一遍题,而是拿一张表逐项打勾。下面这张自测表按“能不能在 30 秒内说清楚”为标准,说不清的那一行就是你要回去补的模块。
| 自测项 | 达标标准 | 对应题型 |
|---|---|---|
| 后缀与宽度 | 看到 b/w/l 立刻说出字节数和常见寄存器 | 判断题、指令改错 |
| 寻址公式 | 能把disp(base,index,scale)口算成表达式 | leal 计算、数组寻址 |
| 非法指令 | 五步检查顺序能背出来并套用 | 指令合法性判断 |
| 扩展指令 | 分得清 movs 和 movz,知道源类型决定扩展方式 | 类型转换表填空 |
| 参数定位 | 会按宽度排参数槽,8 字节类型拆两个槽 | 从汇编反推 C 原型 |
| 标志位 | 六类指令对四个标志的影响不混 | 运算结果与标志判断 |
| 转移地址 | 公式写得出,补码偏移量会解释 | 跳转目标计算 |
| 控制结构 | 能从跳转表还原 switch,从判断位置还原循环 | C 与汇编互译 |
| 栈帧 | 会算 ebp、esp 在 push/pop 后的值 | 栈帧地址计算题 |
| 数据布局 | 数组大小、结构体对齐、成员偏移能一次算对 | 结构体与数组布局 |
打勾的方式建议用三色:完全说得清算过,说得磕巴标黄,完全想不起来标红。红黄两项当天再过一遍,第二天只看红项。这个方法比从头重读教材快得多,因为教材里 70% 的内容你其实已经会了,真正拖分的就是那几行红项。
还有一个具体的查漏技巧:把第三章所有出现过的寄存器名和指令名抄在一张纸上,每个后面写一句“它改哪些标志、宽度多少、常见搭配是什么”。写不出来的当场翻书补上。这张纸写完,基本覆盖了整章的选择题和判断题考点。
再补一个手算练习的建议。标志位那部分光看答案没用,必须自己动手把二进制竖式写出来,尤其是减法转补码那一步。我在最开始复习的时候,sub 的 CF 判断连错三题,原因就是直接拿十进制结果去推标志,后来强制自己每道题都写出“被减数 + 减数补码”的竖式,错误率才降下来。这个习惯一直保留到现在,凡是涉及借位和进位的判断,都要在纸上落一遍。
第三章的题量大,但题型收敛得很厉害,翻来覆去就是寻址、转换、标志、跳转、栈帧、布局这六类。把这六类的判断流程固化成条件反射,剩下的只是细心问题。希望帮到你。
本文还有配套的精品资源,点击获取