02. 数制与数据表示
汇编程序员必须对底层数据表示有精确的理解。寄存器中的位模式(bit pattern)本身没有类型——是加法指令把它当作整数,是浮点指令把它当作 IEEE 754 浮点数,是 load 指令把它从内存原样搬来。本章建立理解这些数值表示所需的基础。
二进制与十六进制
二进制是 CPU 唯一识别的语言。数字电路中只有两个稳定状态(高电平/低电平),对应 1 和 0。数据的粒度从小到大:
| 单位 | 位数 | 值范围(无符号) |
|---|---|---|
| bit | 1 | 0-1 |
| nibble | 4 | 0-15 |
| byte | 8 | 0-255 |
| halfword | 16 | 0-65535 |
| word | 32 | 0-约 43 亿 |
| doubleword | 64 | 0-约 1.8e19 |
RISC-V 中:
- RV32 的 word = 32 位,doubleword = 64 位(基础指令操作 32 位)
- RV64 的 word = 32 位,doubleword = 64 位(基础指令操作 64 位)
- 本教程以 RV64I 为基准,xl = 64 位
十六进制(hexadecimal) 是二进制的人肉缩写。一个 hex 位正好代表 4 个二进制位:
| Hex | Bin | Dec | Hex | Bin | Dec | |
|---|---|---|---|---|---|---|
| 0 | 0000 | 0 | 8 | 1000 | 8 | |
| 1 | 0001 | 1 | 9 | 1001 | 9 | |
| 2 | 0010 | 2 | A | 1010 | 10 | |
| 3 | 0011 | 3 | B | 1011 | 11 | |
| 4 | 0100 | 4 | C | 1100 | 12 | |
| 5 | 0101 | 5 | D | 1101 | 13 | |
| 6 | 0110 | 6 | E | 1110 | 14 | |
| 7 | 0111 | 7 | F | 1111 | 15 |
你很快就会发现自己能用眼睛完成 hex-bin 转换。例如 0x7A3 = 0111 1010 0011。RISC-V 汇编中立即数和地址几乎总是 hex 书写,这是汇编程序员的书写语言。
快速转换方法:
- Binary → Hex:从右往左每 4 位一组,查表转换
- Hex → Binary:每位 hex 展开为 4 位二进制
- Hex → Decimal:各数位乘 16 的幂再求和
- Decimal → Hex:反复除 16 取余数,逆序排列
64 位数值实例。在 RV64I 中,整数操作默认 64 位(有 addw 等变体操作 32 位):
64-bit 最大值(无符号): 0xFFFFFFFF_FFFFFFFF = 18,446,744,073,709,551,615
64-bit 最大值(有符号): 0x7FFF_FFFF_FFFF_FFFF = 9,223,372,036,854,775,807
64-bit 最小值(有符号): 0x8000_0000_0000_0000 = -9,223,372,036,854,775,808注意 RISC-V 汇编中常用下划线 _ 分隔 hex 的每 4 位,这只是可读性写法,汇编器会忽略下划线。
补码:有符号数的表示
如何用只有 0 和 1 的电路表示负数?直观方案是最高位做符号位,其余位存绝对值——这叫原码。但它有两个严重缺陷:零有两种表示(+0 = 0x0000,-0 = 0x8000),且加减法需要额外电路处理符号。现代计算机选择了一种更巧妙的方案:补码(Two's Complement)。
补码定义:$n$ 位补码中,最高位的权重是 $-2^{n-1}$(而不是 $2^{n-1}$),其余位权重照常。因此 $n$ 位补码范围是 $[-2^{n-1}, 2^{n-1}-1]$。
计算负数:对正数 $x$,其相反数 $-x$ 的补码 = ~x + 1(按位取反 + 1)。
# RV64 中 -5 的补码计算
5 = 0x0000_0000_0000_0005
~5 = 0xFFFF_FFFF_FFFF_FFFA # 按位取反
+1 = 0xFFFF_FFFF_FFFF_FFFB # = -5,验证:5 + (-5) = 0补码的核心优势——加法共用硬件。这是 RISC-V 不区分 add 和 addu 的根本原因:
# 同一段位模式,用同一套加法器硬件
# 无符号视角: 0xFFFF_FFFF_FFFF_FFFB = 18446744073709551611
# 有符号视角: 0xFFFF_FFFF_FFFF_FFFB = -5
# 无符号运算 5 + (-5):
# 0x0000_0000_0000_0005 + 0xFFFF_FFFF_FFFF_FFFB = 0x0000_0000_0000_0000 (进位 1 丢弃)
# 有符号运算 5 + (-5):
# 5 + (-5) = 0
# 同一套电路,同一段位模式,结果正确MIPS 有 add(溢出时 trap)和 addu(忽略溢出)两条指令,而 RISC-V 只有 add——更简洁。需要检测溢出时,RISC-V 通过分支指令比较操作数符号来间接判断。
符号扩展。这是汇编中最重要的概念之一。当窄位宽的值被加载到宽位宽寄存器时,高位填充什么决定了语义:
# 假设内存地址 0x1000 处有一个字节 0xFE(= -2 的 8 位补码)
lb x5, 0(x6) # byte load with sign-extension: x5 = 0xFFFF_FFFF_FFFF_FFFE (-2)
lbu x5, 0(x6) # byte load unsigned (zero-extension): x5 = 0x0000_0000_0000_00FE (254)RISC-V 的 lb/lh/lw(宽度后缀 + 无 u)做符号扩展,lbu/lhu/lwu 做零扩展。这不是语言细节——选错指令是最常见的 assembly bug 之一。
大小端
端序(endianness) 定义多字节值在内存中的字节排列顺序。RISC-V 采用小端序。
小端(Little-Endian):低字节在低地址。例如 0x12345678 作为 32-bit word 存储在地址 0x1000:
地址: 0x1000 0x1001 0x1002 0x1003
内容: 0x78 0x56 0x34 0x12
(LSB) (MSB)大端(Big-Endian):高字节在低地址。同样 0x12345678 存储为:
地址: 0x1000 0x1001 0x1002 0x1003
内容: 0x12 0x34 0x56 0x78
(MSB) (LSB)记忆方法:小端 = Little end(低位端)在前 → 低位字节在低地址。大端 = Big end(高位端)在前 → 高位字节在低地址。
实际影响:
- RISC-V 默认小端(LE),BE 在规范中为可选——几乎所有 RISC-V 实现都是 LE
- 网络字节序是大端(TCP/IP 头),网络编程中常用
htonl/ntohl转换 - x86 是小端,ARM 可配置但主流也是小端
- 查看内存 dump 时注意端序——同样的 bytes,不同端序解释为不同的整数
端序与汇编的关系:当用 ld 指令从内存读取 8 字节到一个 64 位寄存器时,CPU 自动按小端序重排字节。汇编程序员通常不需要手动处理端序,但需要意识到它存在——尤其在调试内存 dump 或编写 binary 解析代码时。
位运算基础
汇编层面操作的是位,位运算指令(AND/OR/XOR/移位)是 RISC-V 的基础。理解它们不只是为了写代码,更是为了读懂编译器生成的优化——编译器用位运算替代乘除法是常规操作。
基本逻辑运算的真值表:
| A | B | AND | OR | XOR | NOT A |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 1 |
| 0 | 1 | 0 | 1 | 1 | 1 |
| 1 | 0 | 0 | 1 | 1 | 0 |
| 1 | 1 | 1 | 1 | 0 | 0 |
移位的两种语义:
- 逻辑左移(SLL):左移 n 位,低位填 0 → 等价于乘 $2^n$
- 逻辑右移(SRL):右移 n 位,高位填 0 → 无符号数除 $2^n$
- 算术右移(SRA):右移 n 位,高位填符号位 → 有符号数除 $2^n$(向下取整!)
# RV64 移位示例
li x5, -16 # x5 = 0xFFFF_FFFF_FFFF_FFF0
srli x6, x5, 2 # 逻辑右移 2: x6 = 0x3FFF_FFFF_FFFF_FFFC
srai x7, x5, 2 # 算术右移 2: x7 = 0xFFFF_FFFF_FFFF_FFFC (-4)注意有符号数算术右移除 $2^n$ 时向下取整(向负无穷),而 C 语言的整数除法向零取整——这是汇编与高级语言之间需要注意的语义差异。
常见位操作技巧:
| 操作 | 位运算 | RISC-V 指令序列 |
|---|---|---|
| 清零第 k 位 | x & ~(1 << k) | andi x, x, mask |
| 置位第 k 位 | x | (1 << k) | ori x, x, mask |
| 翻转第 k 位 | x ^ (1 << k) | xori x, x, mask |
| 测试第 k 位 | (x >> k) & 1 | srli + andi |
| 乘 2^n | x << n | slli x, x, n |
| 除 2^n(无符号) | x >> n | srli x, x, n |
| 提取低 n 位 | x & ((1<<n)-1) | andi x, x, mask |
| 对齐至 2^n 边界 | x & ~(2^n - 1) | andi x, x, mask |
这些位操作在第 5-6 章会以 RISC-V 指令形式系统展开。
本章要点
- 十六进制是汇编程序员的书写语言,hex-bin 快速转换是基本功
- 补码让有/无符号加法共用同一套硬件——RISC-V 不区分
add/addu的根本原因 - 符号扩展 vs 零扩展的选择(
lbvslbu)是最常见的 assembly bug - RISC-V 小端序,低字节在低地址,网络序为大端需要注意转换
- AND/OR/XOR/移位直接对应 RISC-V 基础指令,编译器用位运算替代乘除法是常规优化