Skip to content
Published at:

02. 数制与数据表示

汇编程序员必须对底层数据表示有精确的理解。寄存器中的位模式(bit pattern)本身没有类型——是加法指令把它当作整数,是浮点指令把它当作 IEEE 754 浮点数,是 load 指令把它从内存原样搬来。本章建立理解这些数值表示所需的基础。

二进制与十六进制

二进制是 CPU 唯一识别的语言。数字电路中只有两个稳定状态(高电平/低电平),对应 1 和 0。数据的粒度从小到大:

单位位数值范围(无符号)
bit10-1
nibble40-15
byte80-255
halfword160-65535
word320-约 43 亿
doubleword640-约 1.8e19

RISC-V 中:

  • RV32 的 word = 32 位,doubleword = 64 位(基础指令操作 32 位)
  • RV64 的 word = 32 位,doubleword = 64 位(基础指令操作 64 位)
  • 本教程以 RV64I 为基准,xl = 64 位

十六进制(hexadecimal) 是二进制的人肉缩写。一个 hex 位正好代表 4 个二进制位:

HexBinDecHexBinDec
000000810008
100011910019
200102A101010
300113B101111
401004C110012
501015D110113
601106E111014
701117F111115

你很快就会发现自己能用眼睛完成 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)。

asm
# RV64 中 -5 的补码计算
 5 = 0x0000_0000_0000_0005
~5 = 0xFFFF_FFFF_FFFF_FFFA   # 按位取反
+1 = 0xFFFF_FFFF_FFFF_FFFB   # = -5,验证:5 + (-5) = 0

补码的核心优势——加法共用硬件。这是 RISC-V 不区分 addaddu 的根本原因:

asm
# 同一段位模式,用同一套加法器硬件
# 无符号视角: 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 通过分支指令比较操作数符号来间接判断。

符号扩展。这是汇编中最重要的概念之一。当窄位宽的值被加载到宽位宽寄存器时,高位填充什么决定了语义:

asm
# 假设内存地址 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 的基础。理解它们不只是为了写代码,更是为了读懂编译器生成的优化——编译器用位运算替代乘除法是常规操作。

基本逻辑运算的真值表

ABANDORXORNOT A
000001
010111
100110
111100

移位的两种语义

  • 逻辑左移(SLL):左移 n 位,低位填 0 → 等价于乘 $2^n$
  • 逻辑右移(SRL):右移 n 位,高位填 0 → 无符号数除 $2^n$
  • 算术右移(SRA):右移 n 位,高位填符号位 → 有符号数除 $2^n$(向下取整!)
asm
# 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) & 1srli + andi
乘 2^nx << nslli x, x, n
除 2^n(无符号)x >> nsrli 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 零扩展的选择(lb vs lbu)是最常见的 assembly bug
  • RISC-V 小端序,低字节在低地址,网络序为大端需要注意转换
  • AND/OR/XOR/移位直接对应 RISC-V 基础指令,编译器用位运算替代乘除法是常规优化