C语言拾遗
编写可读代码
不可读:void (*signal(int sig, void (*func)(int)))(int);
可读:
typedef void (*sighandler_t)(int);
sighandler_t signal(int, sighandler_t);
例子:实现数字逻辑电路模拟器
#define FORALL_REGS(_) _(X) _(Y) #define LOGIC X1 = !X && Y; \ Y1 = !X && !Y; #define DEFINE(X) static int X, X##1; #define UPDATE(X) X = X##1; #define PRINT(X) printf(#X " = %d; ", X); int main() { FORALL_REGS(DEFINE); while (1) { // clock FORALL_REGS(PRINT); putchar ( '\n' ); sleep(1); LOGIC; FORALL_REGS(UPDATE); } }
使用预编译的利与弊
优点:
1. 增加/删除寄存器只要改一个地方
2. 阻止了一些编程错误,比如忘记更新寄存器,忘记打印寄存器
缺点:
1. 可读性变差 (更不像 C 代码了)
2. 给 IDE 解析带来一些困难
例子:实现 YEMU 全系统模拟器
存储系统
寄存器: PC, R0 (RA), R1, R2, R3 (8-bit)
内存: 16个字节 (按字节访问)
指令集
7 6 5 4 3 2 1 0
mov [0 0 0 0] [ rt] [ rs]
add [0 0 0 1] [ rt] [ rs]
load [1 1 1 0] [ addr ]
store [1 1 1 1] [ addr ]
存储模型:内存 + 寄存器 (包含 PC)
16 + 5 = 21 bytes = 168 bits
总共有2168种不同的取值,任给一个状态,我们都能计算出 PC 处的指令,从而计算出下一个状态
理论上,任何计算机系统都是这样的状态机
1. (M,R)构成了计算机系统的状态
2. 32 GiB 内存有2274877906944种不同的状态……
3. 每个时钟周期,取出M[R[PC]]的指令;执行;写回
受制于物理实现 (和功耗) 的限制,通常每个时钟周期只能改变少量寄存器和内存的状态
(量子计算机颠覆了这个模型:同一时刻可以处于多个状态)
YEMU: 模拟存储
存储是计算机能实现 “计算” 的重要基础
寄存器 (PC)、内存
这简单,用全局变量就好了!
#include <stdint.h> #define NREG 4 #define NMEM 16 typedef uint8_t u8; // 没用过 uint8_t? u8 pc = 0, R[NREG], M[NMEM] = { ... };
建议 STFW (C 标准库) → bool 有没有?
现代计算机系统:uint8_t == unsigned char
C Tips: 使用 unsigned int 避免潜在的 UB
-fwrapv 可以强制有符号整数溢出为 wraparound
C Quiz: 把指针转换成整数,应该用什么类型?
提升代码质量
给寄存器名字?
#define NREG 4 u8 R[NREG], pc; // 有些指令是用寄存器名描述的 #define RA 1 // BUG: 数组下标从0开始 ...
enum { RA, R1, ..., PC }; u8 R[] = { [RA] = 0, // 这是什么语法?? [R1] = 0, ... [PC] = init_pc, }; #define pc (R[PC]) // 把 PC 也作为寄存器的一部分 #define NREG (sizeof(R) / sizeof(u8))
从一小段代码看软件设计
软件里有很多隐藏的 dependencies (一些额外的、代码中没有体现和约束的 “规则”)
- 一处改了,另一处忘了 (例如加了一个寄存器忘记更新
NREG
...) - 减少 dependencies → 降低代码耦合程度
// breaks when adding a register #define NREG 5 // 隐藏假设max{RA, RB, ... PC} == (NREG - 1) // breaks when changing register size #define NREG (sizeof(R) / sizeof(u8)) // 隐藏假设寄存器是8-bit // never breaks #define NREG (sizeof(R) / sizeof(R[0])) // 但需要R的定义 // even better (why?) enum { RA, ... , PC, NREG }
YEMU: 模拟指令执行
在时钟信号驱动下,根据(M,R)更新系统的状态
RISC 处理器 (以及实际的 CISC 处理器实现):
- 取指令 (fetch): 读出
M[R[PC]]
的一条指令 - 译码 (decode): 根据指令集规范解析指令的语义 (顺便取出操作数)
- 执行 (execute): 执行指令、运算后写回寄存器或内存
最重要的就是实现 idex()
- 这就是 PA 里你们最挣扎的地方 (囊括了整个手册)
int main() { while (!is_halt(M[pc])) { idex(); } }
代码例子 1
void idex() { if ((M[pc] >> 4) == 0) { R[(M[pc] >> 2) & 3] = R[M[pc] & 3]; pc++; } else if ((M[pc] >> 4) == 1) { R[(M[pc] >> 2) & 3] += R[M[pc] & 3]; pc++; } else if ((M[pc] >> 4) == 14) { R[0] = M[M[pc] & 0xf]; pc++; } else if ((M[pc] >> 4) == 15) { M[M[pc] & 0xf] = R[0]; pc++; } }
代码例子 2
void idex() { u8 inst = M[pc++]; u8 op = inst >> 4; if (op == 0x0 || op == 0x1) { int rt = (inst >> 2) & 3, rs = (inst & 3); if (op == 0x0) R[rt] = R[rs]; else if (op == 0x1) R[rt] += R[rs]; } if (op == 0xe || op == 0xf) { int addr = inst & 0xf; if (op == 0xe) R[0] = M[addr]; else if (op == 0xf) M[addr] = R[0]; } }
代码例子 3 (YEMU 代码)
typedef union inst { struct { u8 rs : 2, rt: 2, op: 4; } rtype; struct { u8 addr: 4, op: 4; } mtype; } inst_t; #define RTYPE(i) u8 rt = (i)->rtype.rt, rs = (i)->rtype.rs; #define MTYPE(i) u8 addr = (i)->mtype.addr; void idex() { inst_t *cur = (inst_t *)&M[pc]; switch (cur->rtype.op) { case 0b0000: { RTYPE(cur); R[rt] = R[rs]; pc++; break; } case 0b0001: { RTYPE(cur); R[rt] += R[rs]; pc++; break; } case 0b1110: { MTYPE(cur); R[RA] = M[addr]; pc++; break; } case 0b1111: { MTYPE(cur); M[addr] = R[RA]; pc++; break; } default: panic("invalid instruction at PC = %x", pc); } }
有用的 C 语言特性
Union / bit fields
typedef union inst { struct { u8 rs : 2, rt: 2, op: 4; } rtype; struct { u8 addr: 4, op: 4; } mtype; } inst_t;
指针
内存只是个字节序列
无论何种类型的指针都只是地址 + 对指向内存的解读
inst_t *cur = (inst_t *)&M[pc]; // cur->rtype.op // cur->mtype.addr // ...
如何管理 “更大” 的项目 (YEMU)?
- 我们分多个文件管理它
yemu.h
- 寄存器名;必要的声明yemu.c
- 数据定义、主函数idex.c
- 译码执行Makefile
- 编译脚本 (能实现增量编译)
- 使用合理的编程模式
- 减少模块之间的依赖
enum { RA, ... , NREG }
- 合理使用语言特性,编写可读、可证明的代码
inst_t *cur = (inst_t *)&M[pc]
- 减少模块之间的依赖
- NEMU 就是加强版的 YEMU
更多的计算机系统模拟器
am-kernels/litenes
- 一个 “最小” 的 NES 模拟器
- 自带硬编码的 ROM 文件
fceux-am
- 一个非常完整的高性能 NES 模拟器
- 包含对卡带定制芯片的模拟 (
src/boards
)
QEMU
- 工业级的全系统模拟器
- 2011 年发布 1.0 版本
- 有兴趣的同学可以 RTFSC
- 作者:传奇黑客 Fabrice Bellard
本文内容源自:南京大学计算机系统基础习题课 http://jyywiki.cn/ICS/2020/slides/3.slides
2022/4/5