计算机体系结构 01:比特怎样表示数

同一串比特没有天然含义。0x3f800000 可以当作无符号整数 1065353216,也可以按 IEEE 风格单精度浮点格式解释为 1.0;0xffffffff 在无符号 32 位整数里是 4294967295,在二补码有符号解释里常被读作 -1。机器执行的是比特操作,程序语言和 ISA 给这些比特加上类型、宽度、对齐和舍入规则。

本篇回答三个问题:定宽整数为什么会回绕;字节序和对齐为什么影响内存解释;浮点加法为什么不能按实数代数随意换括号。证据等级为“手算”和“功能执行”。本篇的 C 程序只执行定义良好的 unsigned 运算、memcpy 对象表示复制、对齐查询和浮点分组;不执行有符号整数溢出。

整数:宽度先于意义

以 8 位为例,1111 1111 如果按无符号数解释,就是:

1
128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 = 255

如果按二补码有符号数解释,最高位权重是 -128,其余位仍为正:

1
-128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 = -1

同一比特串没有变,解释规则变了。C11 对 unsigned 运算给出明确规则:无符号运算不会溢出,超出可表示范围的结果按“最大值加一”的模数归约。WG14 的项目页把 C11 对应到 ISO/IEC 9899:2011,并列出相近草案 N1570;N1570 对 unsigned 运算的描述正是本文采用的依据。相反,C 表达式求值出现结果不在类型可表示范围内的异常条件时,行为可能是未定义;本篇不把“有符号溢出在某台机器上看起来回绕”写成 C 语言保证。

RISC-V 的整数指令又是另一层。RV32I 规范说整数运算在 XLEN 位寄存器上操作;ADDIADD 的溢出被忽略,低 XLEN 位写回目标寄存器。于是同一段二进制加法,在 C 的 signed 语义、C 的 unsigned 语义和 RV32I 指令语义中,边界并不完全一样。写文章和做实验时必须先说清楚是哪一层。

字节序:内存按字节编号

32 位字 0x01020304 如果保存在 4 个连续字节里,有两种常见顺序:

1
2
小端:低地址 04 03 02 01 高地址
大端:低地址 01 02 03 04 高地址

本机 C 程序用 memcpy 复制对象表示,输出:

1
host_byte_order_probe=0x04030201

这说明当前宿主机器上,这个 32 位对象的最低有效字节放在最低地址,是小端。这个输出只证明本机 C 对象表示的观察,不自动推出所有 RISC-V 平台、所有网络协议或所有文件格式都是小端。第 00 篇的教学执行器也按小端装入 RV32I 字;这是一条实验约定,后续模型都沿用它。

对齐:地址不是任意切分都等价

结构体:

1
2
3
4
struct Padded {
uint8_t tag;
uint32_t value;
};

第一个字段只需要 1 字节,但第二个字段是 32 位整数。当前本机输出:

1
struct_padded_size=8 align=4 offset_value=4

value 放在偏移 4,整个结构体大小是 8。中间 3 个字节不是程序员写出来的字段,而是实现为了满足对齐要求插入的填充。C 标准把对象对齐作为实现属性处理;ISA 和微架构则决定非对齐访问是否允许、是否陷入、是否被拆成多次访问。本系列的基础 RV32I 执行器把非对齐 lw/sw 作为错误处理,这不是完整硬件事实,而是教学模型边界。

浮点:近似值会改变代数直觉

浮点数不是任意精度实数。ISO/IEC 60559:2020 规定浮点格式和运算方法,目标是让给定输入下的结果可一致复现;C 标准也允许浮点表达式在实现定义条件下发生收缩。对程序员来说,最早要记住的事实是:每次浮点运算都可能舍入,括号改变舍入发生的位置。

本机程序计算:

1
2
(1e20 + -1e20) + 3.14
1e20 + (-1e20 + 3.14)

输出:

1
2
3
float_left=3.1400000000000001
float_right=0
float_non_associative=true

手算解释是:左边先把 1e20-1e20 抵消,再加 3.14;右边先算 -1e20 + 3.14,3.14 相对 1e20 太小,在该量级的表示中被舍入掉,再加 1e20 得 0。这里的 3.1400000000000001 是二进制浮点不能精确表示十进制 3.14 的显示结果。

核心案例

本篇附件 representation.c 的核心输出为:

1
2
3
4
5
6
7
8
host_byte_order_probe=0x04030201
uint32_bits_0x3f800000_as_float=1.0
uint32_max_plus_one=0
float_env=FLT_RADIX:2 FLT_MANT_DIG:24 DBL_MANT_DIG:53
struct_padded_size=8 align=4 offset_value=4
float_left=3.1400000000000001
float_right=0
float_non_associative=true

验收结论分层如下:

观察 证据等级 能支持的结论
UINT32_MAX + 1u = 0 功能执行 本 C 程序中的 unsigned 32 位运算按模数归约
0x3f800000memcpyfloat 得 1.0 功能执行 本机该表示与单精度 1.0 的常见编码一致
host_byte_order_probe=0x04030201 功能执行 本机对象字节序观察为小端
浮点左右分组不同 功能执行、手算 浮点加法不能按实数加法直接重排

程序包含 sizeof(float) == sizeof(uint32_t) 的静态断言;unsigned 回绕、0x3f800000float 的复制和浮点非结合性都有运行时校验,失败会非零退出。结构体大小和字段偏移是本机布局观察,不是 C 语言不变量。这些结论不包括:所有平台的 long double 格式、所有编译选项下的浮点收缩行为、硬件浮点单元内部实现、非对齐访问在商品 CPU 上的代价。

练习

练习 1:8 位 unsigned 中,250 + 10 的结果是多少?同一低 8 位按二补码 signed 解释是多少?

答案:250 + 10 = 260,按 256 取模得到 4。低 8 位是 0000 0100,按二补码 signed 解释仍是 4。

练习 2:0x00000001 在小端 32 位对象里按字节从低地址到高地址怎样排列?如果读作 32 位整数,数值是否变了?

答案:低地址到高地址是 01 00 00 00。只要读写双方同意这是同一个 32 位小端对象,数值仍是 1;如果把这 4 个字节按大端格式解释,就会得到不同数值。

参考资料