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

posted @ 2022-04-05 22:10  One7  阅读(193)  评论(0编辑  收藏  举报