计算机系统基础第三章习题解析:补码、溢出与IEEE 754浮点数验证

发布时间:2026/10/15 23:11:13
计算机系统基础第三章习题解析:补码、溢出与IEEE 754浮点数验证
简介这份资源是袁春风《计算机系统基础第二版》第三章课后习题的参考答案面向正在学习计算机组成与系统底层原理的高校学生及考研复习者帮助解决指令系统、寻址方式与机器级表示等章节练习中的疑难。压缩包内共1个PDF文件约3.15MB内容按题号逐题给出解答涵盖寻址方式判定、指令格式错误分析、数据类型转换表、函数参数栈布局、算术与逻辑运算标志位变化以及程序执行后寄存器取值等核心考点。每道题均附有详细推导过程例如加法与减法指令对OF、SF、ZF、CF的影响OR与TEST指令的标志位差异以及乘法指令不影响标志位等易错细节便于读者对照教材自查并巩固机器级代码的编写思路。目前已有7731人学习下载适合需要系统核对答案、理清底层运作逻辑的读者参考使用。1. 从一道“看起来会做、一跑就错”的习题说起《计算机系统基础第二版》第三章的课后习题是很多人从“会写 C 代码”到“理解机器到底怎么跑代码”的分水岭。这一章通常围绕数据的机器级表示展开——整数补码、无符号与有符号的转换、溢出判断、浮点数的 IEEE 754 编码、类型转换与位扩展。标题里说的“课后习题答案”真正有价值的部分不是那几行最终数字而是每道题背后那套可复现的推导路径给你一个十进制数你怎么一步步写出它的补码给你一段位模式你怎么判断它当有符号数、无符号数、浮点数分别是什么。我见过太多人对着答案抄一遍就过去了结果一到自己写代码验证就翻车。原因很简单习题答案只给了结果没给“为什么是这个结果”的中间过程。这篇笔记就按一线做题和验证的思路把第三章习题里最常见的几类问题拆开给出可以自己动手复现的推导方法和验证代码。适合正在学这门课、准备考试或者想补一补底层数据表示这块短板的同学。2. 整数表示与转换补码、无符号和溢出的手算路径2.1 为什么补码能把减法变成加法第三章习题里最高频的一类就是给一个十进制负整数要求写出它的 n 位补码表示。很多人背了“取反加一”但遇到边界值就懵。补码的本质是模运算n 位二进制能表示 2^n 个状态把减法 a - b 变成 a (2^n - b)而 2^n - b 正好就是 b 的补码。理解这一点后面所有转换都不会乱。手算路径我一般固定成三步。第一步写出该数绝对值的 n 位原码。第二步如果原码最高位是 0正数补码就是它本身如果是负数从右往左找到第一个 1这个 1 及其右边不变左边全部取反。第三步检查位数是否够不够就说明发生了溢出这道题大概率在考你这个边界。以 8 位为例-128 的补码是 10000000而 128 在 8 位有符号里根本表示不了。习题里经常出“下列哪个数不能用 8 位补码表示”考的就是这个范围-2^(n-1) 到 2^(n-1) - 1。2.2 用 Python 验证每一道转换题手算完一定要验证否则你永远不知道自己错在哪一步。Python 的整数是任意精度的所以需要手动截断到 n 位这恰好能帮你暴露溢出问题。def to_twos_complement(value, bits): 把十进制整数转成 bits 位补码的二进制字符串 mask (1 bits) - 1 # 生成 bits 位全 1 的掩码 raw value mask # 按位与自动完成模 2^bits 运算 return format(raw, f0{bits}b) def from_twos_complement(bit_str): 把补码二进制字符串还原成有符号十进制数 bits len(bit_str) value int(bit_str, 2) if value (1 (bits - 1)): # 最高位为 1说明是负数 value - (1 bits) # 减去 2^bits 得到负值 return value # 验证几个典型值 for v in [-128, -1, 0, 127, 128]: if -128 v 127: s to_twos_complement(v, 8) print(f{v:5} - {s} - {from_twos_complement(s)}) else: print(f{v:5} - 超出 8 位补码范围)这段代码的关键在value mask。Python 里负数做按位与会先按无限位补码展开再截断结果正好等于模 2^bits 的余数所以不需要你手动写“取反加一”。from_twos_complement里的判断是补码转十进制的标准做法最高位权值是 -2^(n-1)其余位是正的。参数说明bits就是题目要求的位数常见是 8、16、32。如果你把bits改成 4就能验证 -8 到 7 的范围习题里的小位数题用这个最方便。2.3 无符号与有符号互转的习题怎么不丢分第三章必考的另一类题是给一个位模式问它当无符号数和有符号数分别是多少。这类题丢分通常不是不会算而是忘了“位模式本身不变变的只是解释方式”。我的做法是先写出二进制然后套两个公式。无符号值 各位按 2 的幂次求和。有符号值 最高位如果是 1就用无符号值减去 2^n如果是 0就等于无符号值。这两个公式覆盖所有情况不需要分正负讨论。def interpret(bit_str): bits len(bit_str) unsigned int(bit_str, 2) signed unsigned - (1 bits) if bit_str[0] 1 else unsigned return unsigned, signed # 习题常见位模式 for s in [11111111, 10000000, 01111111, 10101010]: u, si interpret(s) print(f{s}: 无符号{u:3}, 有符号{si:4})跑一遍就能看到11111111 当无符号是 255当有符号是 -110000000 当无符号是 128当有符号是 -128。这两个是习题里出现频率最高的边界值记住它们能帮你快速自查。2.4 溢出判断习题里最爱设的陷阱溢出题通常给两个数相加问结果是否溢出、溢出后是什么。判断规则要分情况无符号加法如果结果小于任一加数就溢出了有符号加法如果两个正数相加得负数或两个负数相加得正数就溢出了。同号相加才可能溢出异号相加永远不会。def add_overflow(a, b, bits): mask (1 bits) - 1 result (a b) mask # 无符号溢出判断 unsigned_overflow result (a mask) or result (b mask) # 有符号溢出判断 sa a if a (1 (bits-1)) else a - (1 bits) sb b if b (1 (bits-1)) else b - (1 bits) sr result if result (1 (bits-1)) else result - (1 bits) signed_overflow (sa 0 and sb 0 and sr 0) or (sa 0 and sb 0 and sr 0) return result, unsigned_overflow, signed_overflow print(add_overflow(127, 1, 8)) # 有符号溢出 print(add_overflow(255, 1, 8)) # 无符号溢出这道验证代码的价值在于它把习题里“判断是否溢出”和“写出结果”两步合并了。你手算完对照输出哪一步错了立刻能定位。3. 浮点数 IEEE 754 编码从习题到能自己拆位3.1 单精度浮点的三个字段怎么切第三章浮点部分习题基本围绕 IEEE 754 单精度32 位展开1 位符号、8 位阶码、23 位尾数。很多人卡在“阶码为什么要加偏置值 127”。原因是阶码字段要同时表示正指数和负指数用移码表示后比较大小可以直接按无符号比硬件实现简单。习题里让你写某个浮点数的机器表示你就按这个顺序切。以 -12.5 为例。先转二进制12.5 1100.1。规格化后是 1.1001 × 2^3。符号位 1阶码 3 127 130 10000010尾数去掉隐含的 1取 1001 后面补零到 23 位。拼起来就是 1 10000010 10010000000000000000000。3.2 手算转浮点的固定流程我一般固定成五步习题再变也不离这个框架。第一步把十进制转成二进制整数部分除二取余小数部分乘二取整。第二步规格化成 1.xxxx × 2^e 的形式。第三步算阶码字段 e 127。第四步尾数字段 小数点后的位补零到 23 位。第五步拼接三个字段。import struct def float_to_bits(f): 把 Python 浮点数转成 32 位 IEEE 754 二进制字符串 packed struct.pack(f, f) # 按大端打包成 4 字节 integer struct.unpack(I, packed)[0] # 读成 32 位无符号整数 return format(integer, 032b) def bits_to_float(bit_str): integer int(bit_str, 2) packed struct.pack(I, integer) return struct.unpack(f, packed)[0] for v in [-12.5, 0.15625, 1.0, -0.0]: b float_to_bits(v) sign, exp, frac b[0], b[1:9], b[9:] print(f{v:8} - {b} | 符号{sign} 阶码{exp} 尾数{frac})struct.pack(f, f)里的表示大端f表示单精度。这样得到的位模式和习题答案的书写顺序完全一致。bits_to_float是反向验证你手算完用这个跑一下对不上就说明某一步错了。3.3 特殊值0、无穷和 NaN 的习题套路习题里一定会考特殊值。阶码全 0 且尾数全 0 表示 ±0阶码全 1 且尾数全 0 表示 ±无穷阶码全 1 且尾数非 0 表示 NaN。这几个不用推导记住位模式即可。容易错的是非规格化数阶码全 0 但尾数非 0 时隐含位变成 0指数固定为 -126。习题里如果出现很小的数大概率在考这个。special [00000000000000000000000000000000, 10000000000000000000000000000000, 01111111100000000000000000000000, 01111111100000000000000000000001] for s in special: print(s, -, bits_to_float(s))跑出来你会看到 0、-0、正无穷、NaN。注意 NaN 打印出来就是 nan这是正常的不要以为代码错了。3.4 浮点加减法习题的简化处理第三章有些习题会给两个浮点数问相加结果。完整的手算流程是对阶、尾数相加、规格化、舍入。但习题通常选的是好算的数比如 1.5 2.5 这种。我的建议是先用上面的代码算出真实结果再反推位模式这样能验证你手算的每一步。如果题目要求展示过程就按对阶、相加、规格化三步写舍入部分如果尾数没超出 23 位就说明不用舍入。4. 避坑与排查做第三章习题最容易翻车的五个地方4.1 位数不够还硬写结果符号位被吃掉现象算 -128 的 8 位补码写成 110000000多出一位。原因没先确认表示范围-128 刚好是 8 位补码的最小值只有 10000000 这一种写法。解决每道题先写出范围 -2^(n-1) 到 2^(n-1)-1超出就直接判定无法表示不要硬凑位数。4.2 把无符号溢出和有符号溢出混为一谈现象两个正数相加得负数判成无符号溢出。原因无符号溢出看结果是否回绕变小有符号溢出看符号是否异常。解决分开判断先明确题目问的是哪种类型。代码里unsigned_overflow和signed_overflow两个变量就是为此准备的。4.3 浮点阶码偏置值记成 128现象算出来的阶码总是差 1。原因单精度偏置是 127双精度才是 1023有人把 127 记成 128。解决记住公式 e 127并且用代码验证。如果结果和答案差 1先查偏置值。4.4 尾数补零方向搞反现象位模式拼出来尾数部分不对。原因规格化后小数点后的位要从左往右填右边补零不是左边补零。解决写的时候先数清楚尾数有几位不足的在右边补 0补完再拼接。4.5 类型转换题忽略符号扩展和零扩展现象char 转 int 时负数变大正数。原因有符号转有符号要符号扩展无符号转有符号要零扩展规则不同。解决先看源类型和目标类型是否都有符号。有符号扩展是高位补符号位零扩展是高位补 0。用代码验证时Python 里手动模拟这两种扩展对照习题答案。5. 把习题变成可复用的验证脚本一个具体技巧做第三章习题最值得投入的不是把每道题答案抄一遍而是写一个自己的验证脚本库。我自己的习惯是建一个csapp_ch3_check.py把补码转换、无符号解释、溢出判断、浮点拆位这四个函数放进去。每做一道题先手算再调用对应函数验证。错的那道题把中间步骤打印出来看是哪一步和代码不一致。这个习惯的好处是考试时你没法跑代码但你脑子里已经跑过几百遍了。手算路径和代码路径重合度越高你出错概率越低。具体做法是给每个函数加一个verbose参数需要的时候打印中间值。def twos_complement_verbose(value, bits, verboseFalse): mask (1 bits) - 1 raw value mask if verbose: print(f 掩码: {format(mask, b)}) print(f 按位与结果: {format(raw, f0{bits}b)}) return format(raw, f0{bits}b) twos_complement_verbose(-12, 8, verboseTrue)跑一下就能看到掩码和中间结果。这个技巧对浮点尤其有用因为浮点步骤多中间任何一步错了最终位模式都会错打印中间值能快速定位。另一个进阶用法是把习题里的位模式批量喂给脚本。比如习题给了一列 8 位二进制问哪些当有符号数是负数。你可以写个循环一次输出所有结果比一道一道手算快得多也更容易发现规律。patterns [10000000, 11111111, 01111111, 10000001, 00000001] for p in patterns: u, s interpret(p) print(f{p}: 无符号{u:3}, 有符号{s:4}, {负数 if s 0 else 非负})最后说个我自己的教训。早年做这类题我总想心算觉得写代码验证太慢。结果考试时遇到 -128 的补码脑子里冒出 110000000 这个错误答案就是因为平时没用手算加验证的方式固化流程。后来我强迫自己每道题都走一遍“手算三步 代码验证”错题率才降下来。第三章的习题看着简单但边界值、特殊值、类型转换这几个坑不亲手跑一遍是真的记不住。希望帮到你。本文还有配套的精品资源点击获取