1 防御性编程
不相信外界的输入/其他函数传递的参数, 通过断言提前拦截非预期情况(c语言也有断言)。
#include <assert.h>
// ...
int main(int argc, char *argv[]) {
PC = 0; R[0] = 0;
assert(argc >= 2); // 要求至少包含一个参数
FILE *fp = fopen(argv[1], "r");
assert(fp != NULL); // 要求argv[1]是一个可以成功打开的文件
int ret = fseek(fp, 0, SEEK_END);
assert(ret != -1); // 要求fseek()成功
long fsize = ftell(fp);
assert(fsize != -1); // 要求ftell()成功
rewind(fp);
assert(fsize < 1024); // 要求程序大小不超过1024字节
ret = fread(M, 1, 1024, fp);
assert(ret == fsize); // 要求完全读出程序的内容
fclose(fp);
while (!halt) { inst_cycle(); }
return 0;
}
1.1 改进1: 让断言失败时输出更多信息
定义Assert,实现assert被触发时,可以打印一句话信息出来。
#define Assert(cond, format, ...) \
do { \
if (!(cond)) { \
fprintf(stderr, format "\n", ## __VA_ARGS__); \
assert(cond); \
} \
} while (0)
int main(int argc, char *argv[]) {
PC = 0; R[0] = 0;
Assert(argc >= 2, "Program is not given"); // 要求至少包含一个参数
FILE *fp = fopen(argv[1], "r");
Assert(fp != NULL, "Fail to open %s", argv[1]); // 要求argv[1]是一个可以成功打开的文件
int ret = fseek(fp, 0, SEEK_END);
Assert(ret != -1, "Fail to seek the end of the file"); // 要求fseek()成功
long fsize = ftell(fp);
Assert(fsize != -1, "Fail to return the file position"); // 要求ftell()成功
rewind(fp);
Assert(fsize < 1024, "Program size exceeds 1024 Bytes"); // 要求程序大小不超过1024字节
ret = fread(M, 1, 1024, fp);
Assert(ret == fsize, "Fail to load the whole program"); // 要求完全读出程序的内容
fclose(fp);
while (!halt) { inst_cycle(); }
return 0;
}
1.2 改进2: 输出库函数错误原因
#include <string.h>
#include <errno.h>
#define Perror(cond, format, ...) \
Assert(cond, format ": %s", ## __VA_ARGS__, strerror(errno))
int main(int argc, char *argv[]) {
PC = 0; R[0] = 0;
Assert(argc >= 2, "Program is not given"); // 要求至少包含一个参数
FILE *fp = fopen(argv[1], "r");
Perror(fp != NULL, "Fail to open %s", argv[1]); // 要求argv[1]是一个可以成功打开的文件
int ret = fseek(fp, 0, SEEK_END);
Perror(ret != -1, "Fail to seek the end of the file"); // 要求fseek()成功
long fsize = ftell(fp);
Perror(fsize != -1, "Fail to return the file position"); // 要求ftell()成功
rewind(fp);
Assert(fsize < 1024, "Program size exceeds 1024 Bytes"); // 要求程序大小不超过1024字节
ret = fread(M, 1, 1024, fp);
Assert(ret == fsize, "Fail to load the whole program"); // 要求完全读出程序的内容
fclose(fp);
while (!halt) { inst_cycle(); }
return 0;
}
errno.h:报告库函数执行错误的原因。
2 减少代码中的隐含依赖
破坏隐藏依赖会出现bug(例如这里改了,那里忘记修改)
uint8_t M[512];
Assert(fsize < 1024, "Program size exceeds 1024 Bytes");
ret = fread(M, 1, 1024, fp); // BUG: 忘了改, 可能发生缓冲区溢出!
第二行的1024忘记修改为512,导致fsize数据大于512,存到内存时候,会出现溢出,出现不确定性问题。
不要让上面的代码有相关性。
#define MSIZE 1024
uint8_t M[MSIZE];
// 另一种方式
uint8_t M[1024];
#define MSIZE (sizeof(M) / sizeof(M[0]))
Assert(fsize < MSIZE, "Program size exceeds %d Bytes", MSIZE);
ret = fread(M, 1, MSIZE, fp);
- 不言自明 ✅ - 代码中可能还有其他1024
- 不言自证 ✅ - 不要自信地认为“改的时候我会记得”
- 面对几十个文件, 几千行代码, 你不会记得的
3 将定义放在头文件
随着项目规模增长, 需要分成多个文件来管理
// main.c
#define MSIZE 512
ret = fread(M, 1, MSIZE, fp);
// inst.c
#define MSIZE 1024 // BUG: 这里忘了改
assert(PC < MSIZE);
uint32_t inst = *(uint32_t *)&M[PC];
更好的代码:
// yemu.h
#define MSIZE 512 // 一改全改
// main.c
#include "yemu.h"
ret = fread(M, 1, MSIZE, fp);
// inst.c
#include "yemu.h"
assert(PC < MSIZE);
uint32_t inst = *(uint32_t *)&M[PC];
4 拒绝复制粘贴
4.1 反面案例
指令模拟器,在addi基础上,新加了xori、ori和andi三条指令。从addi复制过来,然后修改。出现问题很难发现。
if (((inst & 0x7f) == 0x13) && ((inst >> 12) & 0x7) == 0) { // addi
if (((inst >> 7) & 0x1f) != 0) {
R[(inst >> 7) & 0x1f] = R[(inst >> 15) & 0x1f] +
(((inst >> 20) & 0x7ff) - ((inst & 0x80000000) ? 4096 : 0));
}
} else if (((inst & 0x7f) == 0x13) && ((inst >> 12) & 0x7) == 0x4) { // xori
if (((inst >> 7) & 0x1f) != 0) {
R[(inst >> 7) & 0x1f] = R[(inst >> 15) & 0x1f] ^
(((inst >> 20) & 0x7ff) - ((inst & 0x80000000) ? 4096 : 0));
}
} else if (((inst & 0x7f) == 0x13) && ((inst >> 12) & 0x7) == 0x6) { // ori
if (((inst >> 7) & 0x1f) != 0) {
R[(inst >> 7) & 0x1f] = R[(inst >> 15) & 0x1f] |
(((inst >> 20) & 0x7ff) - ((inst & 0x80000000) ? 4096 : 0));
}
} else if (((inst & 0x7f) == 0x13) && ((inst >> 12) & 0x7) == 0x4) { // andi
if (((inst >> 7) & 0x1f) != 0) {
R[(inst >> 7) & 0x1f] = R[(inst >> 15) & 0x1f] &
(((inst >> 20) & 0x7ff) - ((inst & 0x80000000) ? 4096 : 0));
}
} else if (...) { ... }
Copy-Paste = 编写相似代码时, 复制旧代码并稍作修改
- 开发效率++, 维护难度+++++
上述代码不言自明本身就不怎么样, 不言自证就更难了
- 需要看很久的代码, 基本上都很难做到不言自证
- 当你粘贴出上百行这样的代码, 你很可能会改漏几处
- 哪天你发现了一个共性的问题(例如立即数忘记符号扩展), 所有粘贴的代码都要修改
- 改漏了 = bug
- 而且你基本上没有耐心仔细看的 😂
粘贴一时爽, 调试火葬场 😈
4.2 编写可复用的代码
通过变量, 函数, 宏等消除重复/相似的代码
uint32_t inst = *(uint32_t *)&M[PC];
uint32_t opcode = inst & 0x7f;
uint32_t funct3 = (inst >> 12) & 0x7;
uint32_t rd = (inst >> 7 ) & 0x1f;
uint32_t rs1 = (inst >> 15) & 0x1f;
uint64_t imm = ((inst >> 20) & 0x7ff) - ((inst & 0x80000000) ? 4096 : 0);
if (opcode == 0x13) {
if (funct3 == 0x0) { R[rd] = R[rs1] + imm; } // addi
else if (funct3 == 0x4) { R[rd] = R[rs1] ^ imm; } // xori
else if (funct3 == 0x6) { R[rd] = R[rs1] | imm; } // ori
else if (funct3 == 0x7) { R[rd] = R[rs1] & imm; } // andi
else { panic("Unsupported funct3 = %d", funct3); }
R[0] = 0; // 若指令写入了R[0], 此处将其重置为0
} else if (...) { ... }
PC += 4;
- 引入中间变量, 不言自明 ✅
- 对齐的代码更容易阅读并发现错误, 不言自证 ✅
5 使用合适的语言特性
依据指令结构,采用结构体进行实现,减少发生错误的可能性。
typedef union {
struct {
uint32_t opcode : 7;
uint32_t rd : 5;
uint32_t funct3 : 3;
uint32_t rs1 : 5;
int64_t imm11_0 : 12;
} I;
struct { /* ... */ } R;
uint32_t bytes;
} inst_t;
inst_t *inst = (inst_t *)&M[PC];
uint32_t rd = inst->I.rd;
uint32_t rs1 = inst->I.rs1;
uint64_t imm = (int64_t)inst->I.imm11_0;
if (inst->I.opcode == 0b0010011) {
switch (inst->I.funct3) {
case 0b000: R[rd] = R[rs1] + imm; break; // addi
case 0b100: R[rd] = R[rs1] ^ imm; break; // xori
case 0b110: R[rd] = R[rs1] | imm; break; // ori
case 0b111: R[rd] = R[rs1] & imm; break; // andi
default: panic("Unsupported funct3 = %d", inst->I.funct3);
}
R[0] = 0; // 若指令写入了R[0], 此处将其重置为0
} else if (inst->bytes == 0x00100073) { ... }
- struct和位域(bit field)
把位抽取操作交给编译器 - union
可对数据解释成不同类型 - 指针
按指针类型对内存地址中的数据进行解释 - switch-case语句
替代对同一个变量的连续判断 - 二进制常数(GNU dialect)
可以直接抄手册了
6 什么是好代码(总结)
正确的代码 != 好代码
- 好代码更大概率是正确的
好代码的两条重要准则 - 不言自明 - 仅看代码就能明白是做什么的(specification)
- 不言自证 - 仅看代码就能验证实现是对的(verification)
使用正确的编程模式写出好代码 - 防御性编程 - 通过assert检查非预期行为
- 减少代码中的隐含依赖 - 使得“打破依赖”不会发生
- 头文件 + 源文件
- 编写可复用的代码 - 不要Copy-Paste
- 使用合适的语言特性 - 把细节交给语言规范和编译器