简介:《计算机系统基础(第二版)》第三章课后习题参考答案,面向使用袁春风教材的本科生、考研复习者,以及需要补强汇编语言与机器级表示基础的读者,聚焦处理器指令系统、寻址方式、操作数格式和程序的机器级表示。答案按题号逐条展开,涉及长度后缀 w/b/l 与源、目的操作数组合判断,基址加比例变址加偏移等寻址形式,立即数作目的操作数、后缀与操作数长度不匹配等指令格式错误辨析,char、int、short 等类型转换的机器级表示,函数参数在栈中 R[ebp]+8、+12、+16 的定位,以及加减、or、test、dec 等指令对 OF、SF、ZF、CF 标志和寄存器、存储单元结果的影响,便于对照订正作业、梳理第三章考点与考前查漏补缺。压缩包共 1 个 PDF 文件,约 3.15MB,单文件结构便于按题号检索。目前已有 7738 人学习下载,可作为课堂作业核对和汇编入门练习的参考。
1. 从一串看不懂的 mov 指令说起:答案到底在讲什么
第一次翻到「计算机系统基础」第三章的课后习题答案,很多人会盯着movsbl %al, (%edx)发呆——字母都认识,连起来完全不知道在干什么。其实它描述的是一件很朴素的事:把一小盒东西换个更大的盒子装,再送到指定的地方。第三章整章都在做这种翻译工作,把 C 语言里一行赋值、一个函数调用、一次数组访问,翻译成寄存器、内存和标志位之间的你来我往。搞不懂这套翻译规则,反汇编在眼里就是一屏乱码;搞懂了,编译器吐出来的每一行都能读成一句人话。下面顺着第三章课后习题里那些典型题目,把这套规则按「找操作数、算结果、传参数」的顺序一层层拆开。
2. 一条指令怎么找到它的操作数:寻址方式的分类逻辑
寄快递要先填面单:东西从哪个地址取,送到哪个地址。一条机器指令也一样,动手之前必须先回答两个问题——源操作数在哪儿,目的操作数放在哪儿。这个「在哪儿、怎么算出来」的规则,就是寻址方式。第三章习题里大量的判断、挑错题,考的其实都是这一套。搞不清寻址方式,后面所有关于指令行为的推导都会崩,因为你连「这条指令到底动了哪块内存」都说不出来。
2.1 操作数从哪儿来:寄存器、立即数、内存三档
先建立一个最粗的分法:一条指令的操作数,来源只有三档。
第一档是寄存器,直接写名字,比如%eax、%dl、%bx。CPU 内部伸手就能拿到,速度最快,指令里只要几个比特就能指定是哪一个。
第二档是立即数,就是写死在指令里的常数。这里有个新手最容易翻车的地方——AT&T 语法的立即数前面必须加美元符号$。习题 4 的第(1)小题就是这个坑:源操作数是立即数0xFF,正确写法是$0xFF,少了那个$,汇编器会把0xFF理解成一个地址,转身去内存地址0x000000FF里取数。
提示:这类错误最阴险的地方在于,它往往不报错。汇编器认为你就是要访问那块内存,语法上完全合法,跑出来的结果却南辕北辙。
第三档是内存,答案里统一写成M[...]的形式,方括号里那一坨不是数值,而是「地址怎么算出来」的算式。这是三档里唯一需要现场计算的,也是第三章出题密度最高的地方。
区分这三档有个简单办法:看有没有$,看有没有%,看有没有括号。有$是立即数,有%是寄存器,带括号的是内存。习题里那些「目的操作数不能是立即数寻址」的判断,就是拿这条规则去卡——立即数是写死在指令里的常数,它没有「地址」这个概念,自然也就没法当目的地被写入。
2.2 基址+变址+比例+偏移:一个通用的算地址公式
内存寻址的花样看着多,其实都能塞进同一个模板:
偏移量(基址寄存器, 比例变址寄存器, 比例因子)最终地址 = 基址寄存器的值 + 比例变址寄存器的值 × 比例因子 + 偏移量。比例因子只能是 1、2、4、8,分别对应 1 字节、2 字节、4 字节、8 字节的元素大小——这不是随便定的,正是因为 C 语言里常见的元素宽度就这几种,硬件才专门把这四个系数做成可选。
为什么要设计得这么绕?因为它恰好能一步算出一个数组元素的地址。习题 21 那张表就是在演示这件事。假设S是一个short型数组,首地址放在%edx里,下标i放在%ecx里:
S+i → leal (%edx, %ecx, 2), %eax &S[i+2] → leal 4(%edx, %ecx, 2), %eax第一行里那个2就是比例因子,因为short占 2 个字节,第i个元素距离首地址正好是i*2。第二行多出来的偏移量4,是「再往后挪两个元素」的意思,两个元素乘 2 字节等于 4 字节,所以写成常数 4。整条指令一次算完,不需要先算乘法再算加法。
多维数组更能看出这套设计的价值。习题 23 里,a[i][j][k]的地址被算成a + 4*(63*i + 9*j + k),一眼就能反推出三件事:元素占 4 字节,所以整体乘 4;中间那一维长度是 9,所以j前面系数是 9;最外维的步长是 63,而 63 除以 9 等于 7,所以中间维长度是 7。原本在 C 语言里要写三重下标的表达式,到了机器级就被压缩成「几个常数乘一乘再加起来」的一维算式。编译器干的就是这个活,把高维的直觉翻译成一维的算术。
顺带说一个习题里常出现的兄弟指令leal。名字里带le,意思是「加载有效地址」——它只算地址,不访问内存。所以leal 4(%edx, %ecx, 2), %eax做完之后,%eax里装的是一个地址值,那块内存里的东西一点没动。它和movl 4(%edx, %ecx, 2), %eax的区别,就是「算地址」和「按地址取数」的区别。这两条指令长得极像,考试里也是最常见的失分点之一。
2.3 后缀 b、w、l 不是装饰:它决定这条指令搬多少字节
AT&T 语法规定,指令助记符后面要跟一个单字母后缀表示操作宽度:b是 byte,1 个字节;w是 word,2 个字节;l是 long word,4 个字节。
注意这里的w是 16 位,不是「一个字长」这种含糊说法。在 IA-32 这套语境里,「字」固定指 16 位,「长字」固定指 32 位。这个命名习惯来自更早的 16 位时代,沿用下来之后确实容易让人误会,但它就是规矩。
习题 4 那 8 条错误里,起码有一半跟后缀有关:
- 第(2)条,源操作数是 16 位,长度后缀却写成了
b,宽度对不上; - 第(4)条,操作数位数超过 16 位,长度后缀却是
w,装不下; - 第(5)条,目的操作数的地址所在寄存器用了 8 位寄存器,地址是 32 位的,8 位寄存器根本装不下;
- 第(6)条,源操作数寄存器和目的操作数寄存器长度不一致。
把这四条归到一起,能提炼出一句话:后缀描述的是这条指令搬运的宽度,不是数据「本来」有多宽。数据本身没有宽度标签,是你在指令里指定了这次搬运按多宽处理。同一个%al和%eax,其实是同一个物理寄存器的低 8 位和低 32 位,指令选了哪个名字,就决定了这次读写碰几个字节。
这个逻辑和第 2.2 节的地址计算是配套的:地址算式决定了「从哪儿开始」,后缀决定了「往后读几格」。两者合起来,一条内存访问指令的全部信息就齐了。
2.4 这套寻址设计到底在省什么
回到「为什么」这个问题上。如果只是想表达「取第 i 个元素」,理论上可以设计成「先算乘法指令,再算加法指令,最后访存」三步。但硬件没有这么做,而是把乘加合并进寻址硬件里,具体有三笔账。
第一笔是代码体积。一条leal (%edx, %ecx, 2), %eax只有几个字节,换成三条独立指令,体积翻几倍。早期的机器内存极其紧张,指令编码每省一位都是实打实的收益。
第二笔是执行次数。地址计算在访存单元里顺手就做了,不占用额外的指令周期,也不用占用通用寄存器去存中间结果。对循环里的数组遍历来说,这个差距会被放大成非常可观的性能差别。
第三笔是通用性。基址可以是任意寄存器,变址可以是任意寄存器,比例因子四选一,偏移量是个常数——这四个自由度组合起来,几乎能覆盖 C 语言里所有「按下标算地址」的场景,一维、二维、指针遍历、结构体数组都能套进去。
理解了这三笔账,再看习题里那些「源操作数地址中缺少变址寄存器」的错误,就不只是背一条规则了:你是在检查一个本该由硬件一步完成的地址计算,有没有缺胳膊少腿。
3. 标志位这台黑匣子:运算指令到底留下了什么痕迹
机器级代码里有一个特别容易被忽略的东西:CPU 每算一次,除了把结果写回寄存器或内存,还会顺手在一组叫 EFLAGS 的寄存器里点亮几盏灯。这些灯就是标志位。后面的if、while、循环退出、循环继续,全靠看这几盏灯——不是看结果本身。这就是为什么很多人明明算对了结果,却读不懂后面的跳转:他盯着结果,而程序盯着灯。
3.1 OF、SF、ZF、CF 四盏灯分别在说什么
四个最常用的标志位,各管一摊:
| 标志位 | 名字 | 什么时候点亮 |
|---|---|---|
| CF | 进位/借位标志 | 把操作数当无符号数时,结果超出表示范围 |
| OF | 溢出标志 | 把操作数当有符号数时,结果超出表示范围 |
| ZF | 零标志 | 运算结果的所有位全为 0 |
| SF | 符号标志 | 结果的最高位是 1 |
关键在于,CF 和 OF 是同一堆二进制、两种解读的产物。CPU 不会问「这串数到底是有符号还是无符号」——它不知道,也不需要知道。它只算一遍,然后把两种解读下可能出现的「异常」分别挂到 CF 和 OF 上。你在写 C 代码的时候,编译器知道变量类型,所以它会在后面的条件跳转里挑对应的那一盏灯来看。你手写汇编的时候没有这层保护,就得自己记住该看哪一盏。
打个比方:一台车同时装了速度表和水温表,发动机只转一次,两块表各看各的。你不会因为速度表正常就认为水温没问题,同理,也不能因为 CF 是 0 就断定 OF 也是 0。
3.2 加法和减法为什么同时动四盏灯
拿习题 8 的第(1)小题走一遍。指令是addl (%eax), %edx,含义是R[edx] ← R[edx] + M[R[eax]]。假设%edx里是0x00000080,%eax指向的内存单元里是0xFFFFFFF0,两个 32 位数相加:
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结果是 33 位,最高位那个进位在写回 32 位寄存器时被丢掉,保留下来的低 32 位是0x00000070。四盏灯逐一看:
- CF:无符号视角下,
0x00000080 + 0xFFFFFFF0远超0xFFFFFFFF,进位溢出了,CF = 1; - OF:有符号视角下,
0x80是 +128,0xFFFFFFF0是 −16,加起来是 +112,落在 32 位有符号范围内,OF = 0; - ZF:结果
0x70不是 0,ZF = 0; - SF:结果最高位是 0,SF = 0。
再看第(2)小题的减法,subl (%eax, %ebx), %ecx。%ecx里是0x00000010,内存里是0x80000008。减法在硬件里通常转成
本文还有配套的精品资源,点击获取