引言

上次课内容: C程序如何执行

 

本次课内容:

  • 如何用程序实现ISA的状态机?
    • 如何实现一个可以执行指令的模拟器?

 

学习处理器设计, 为什么要了解这些?

  • 在工业界的处理器设计流程中, 模拟器起着非常重要的作用
  • 你将来也会使用模拟器帮助你完成处理器设计流程中的一些重要工作

freestanding运行时环境

编译到freestanding

我们之前接触的是宿主操作系统(Linux)之上的运行时环境

  • Linux的运行时环境并不简单
    • 我们目前还很难解释清楚printf()的代码在哪里 😂

 

从学习的角度来说, 还是freestanding环境更简单

  • 我们来编译一个最简单的程序
#include <stdint.h>
void _start() {
  volatile uint8_t *p = (uint8_t *)(uintptr_t)0x10000000;
  *p = 'A';
  while (1);
}

选择qemu-system-riscv64作为freestanding环境

  • 0x10000000是qemu-system-riscv64中virt机器模型的串口地址
riscv64-linux-gnu-gcc -ffreestanding -nostdlib -Wl,-Ttext=0x80000000 -O2 a.c
# QEMU emulator version 8.2.2 (Debian 1:8.2.2+ds-0ubuntu1.10)
qemu-system-riscv64 -nographic -M virt -bios none -kernel a.out

程序如何结束运行?

RTFM: C99

5.1.2.1 Freestanding environment

2 The effect of program termination in a freestanding environment is
implementation-defined.

 

在qemu-system-riscv64中的virt机器模型中, 往一个特殊的地址写入一个特殊的 “暗号”即可结束QEMU的运行

#include <stdint.h>
void _start() {
  volatile uint8_t *p = (uint8_t *)(uintptr_t)0x10000000;
  *p = 'A';
  volatile uint32_t *exit = (uint32_t *)(uintptr_t)0x100000;
  *exit = 0x5555; // magic number
}

自定义一个freestanding运行时环境

QEMU虽然是个开源项目, 但还挺复杂, 不利于我们理解细节

  • 6600+个源文件, 330000+行源代码
    • 去除测试代码和第三方库

 

我们还是考虑用sISA计算1+2+...+10时提供的运行时环境

  1. 在程序执行开始前
    • 加载程序
    • 暂时不支持程序参数的传递
  2. 在程序执行过程中
    • 暂时不支持库函数
  3. 在程序执行结束后
    • 通过死循环来指示程序结束 - 通过bner0指令实现

sEMU: 用C程序执行sISA的指令

指令集模拟器 = 用C程序实现ISA状态机

C程序 ISA
状态 \(\{PC, V\}\) \(\{PC, R, M\}\)
激励事件 执行语句 执行指令
状态转移规则 语句的语义 指令的语义
  • 用C程序的状态实现ISA的状态
    • 用C程序的变量实现ISA的PC, GPR和内存
  • 用C程序的状态转移规则实现ISA的状态转移规则
    • 用C语言语句实现指令的语义

 

  • 指令采用符号化表示 -> 汇编模拟器
    • 如MIPS模拟器SPIM, 一些编译原理课程实验用它执行MIPS汇编代码
  • 指令采用编码表示 -> 传统的(二进制)指令集模拟器
    • 包含大部分指令集模拟器, 如QEMU, Spike, FCEUX, NEMU等

用变量实现寄存器和内存

#include <stdint.h>
uint8_t PC = 0; // C语言中不存在4位的基础数据类型
uint8_t R[4];
uint8_t M[16];

 

Q: 为什么不使用int8_t?

A: C语言标准规定, 有符号数溢出是未定义行为, 但无符号数不会溢出

6.5 Expressions

5 If an exceptional condition occurs during the evaluation of an expression (that is,
if the result is not mathematically defined or not in the range of representable
values for its type), the behavior is undefined.
6.2.5 Types

9 A computation involving unsigned operands can never overflow, because a result that
cannot be represented by the resulting unsigned integer type is reduced modulo the
number that is one greater than the largest value that can be represented by the
resulting type.

用语句实现指令的语义

回顾ISA模型机执行sISA指令的过程:

  • 取指 - 根据PC索引内存M, 读取一条指令
  • 译码 - 通过指令的opcode字段查看指令类型; 然后根据指令格式解析出操作数
  • 执行 - 若执行的指令不是bner0, 则将结果写回目的寄存器; 否则, 根据判断情况决定是否进行跳转
  • 更新PC - 若不跳转, 则让PC加1

 

不断执行指令, 直到结束:

while (1) {
  inst_cycle();
}

实现sEMU = 用C代码实现inst_cycle()

用语句实现指令的语义(2)

 7  6 5  4 3   2 1   0
+----+----+-----+-----+
| 00 | rd | rs1 | rs2 | R[rd]=R[rs1]+R[rs2]       add指令, 寄存器相加
+----+----+-----+-----+
| 10 | rd |    imm    | R[rd]=imm                 li指令, 装入立即数, 高位补0
+----+----+-----+-----+
| 11 |   addr   | rs2 | if (R[0]!=R[rs2]) PC=addr bner0指令, 若不等于R[0]则跳转
+----+----------+-----+

一个简单的实现:

void inst_cycle() {
  uint8_t inst = *(uint8_t *)&M[PC];
  if (((inst >> 6) & 0x3) == 0) { // add
    R[(inst >> 4) & 0x3] = R[(inst >> 2) & 0x3] + R[inst & 0x3];
  } else if (((inst >> 6) & 0x3) == 2) { // li
    R[(inst >> 4) & 0x3] = inst & 0xf;
  } else if (((inst >> 6) & 0x3) == 3) { // bner0
    if (R[0] != R[inst & 0x3]) {
      PC = (inst >> 2) & 0xf;
      return;
    }
  } else { printf("Unsupported instuction\n"); }
  PC += 1;
}

sEMU

#include <stdint.h>
#include <stdio.h>
uint8_t PC = 0; // C语言中不存在4位的基础数据类型
uint8_t R[4];
uint8_t M[16] = {
  0b10001010, // li r0, 10
  0b10010000, // li r1, 0
  0b10100000, // li r2, 0
  0b10110001, // li r3, 1
  0b00010111, // add r1, r1, r3
  0b00101001, // add r2, r2, r1
  0b11010001, // bner0 r1, 4
  0b11011111, // bner0 r3, 7
};
void inst_cycle() {
  uint8_t inst = *(uint8_t *)&M[PC];
  if (((inst >> 6) & 0x3) == 0) { // add
    R[(inst >> 4) & 0x3] = R[(inst >> 2) & 0x3] + R[inst & 0x3];
  } else if (((inst >> 6) & 0x3) == 2) { // li
    R[(inst >> 4) & 0x3] = inst & 0xf;
  } else if (((inst >> 6) & 0x3) == 3) { // bner0
    if (R[0] != R[inst & 0x3]) { PC = (inst >> 2) & 0xf; return; }
  } else { printf("Unsupported instuction\n"); }
  PC += 1;
}
int main() {
  for (int i = 0; i < 100; i ++) { inst_cycle(); }
  printf("r2 = %d\n", R[2]);
  return 0;
}

从文件读入程序

// ...
int main(int argc, char *argv[]) {
  FILE *fp = fopen(argv[1], "r");
  fread(M, 1, 16, fp);
  fclose(fp);
  for (int i = 0; i < 100; i ++) { inst_cycle(); }
  printf("r2 = %d\n", R[2]);
  return 0;
}
grep -Eo "[01]{8}" a.txt | tr '\n' ' ' | sed -e 's/^/0: /' | xxd -r -b -c 8 > a.bin

强化运行时环境

数列求和程序很简单, 不需要复杂运行时环境

  • 换句话说, 想运行更复杂的程序, 需要先做好运行时环境的支撑

 

添加输出一个整数的功能

  • sISA - 添加一条新指令out rs
  • sEMU - 执行out rs指令后, 将R[rs]输出到终端
  • 程序 - 使用这条指令

 

从命令行输入参数n, 让数列求和程序计算1+2+...+n

  • 让运行时环境在程序开始执行之前将n放在一个约定的位置
    • 例如r0寄存器
  • sEMU - 将用户输入的参数放置在r0
  • 程序 - 从r0中获取数列的末项

sEMU提供的运行时环境

  • 在程序执行开始前
    • 加载程序: 将程序从文件中读入M
    • 传递参数: 在r0寄存器中存放程序的参数

 

  • 在程序执行过程中
    • 可通过out指令以整数方式输出指定寄存器的值

 

  • 在程序执行结束后
    • 通过死循环来指示程序结束 - 通过bner0指令实现

类似可实现minirvEMU

#include <stdio.h>
#include <stdint.h>
uint32_t R[16], PC;
uint8_t M[1024];

void inst_cycle() {
  uint32_t inst = *(uint32_t *)&M[PC];
  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 (...) { ... } */
  else { printf("Unsupported instuction\n"); }
  PC += 4;
}

int main(int argc, char *argv[]) {
  FILE *fp = fopen(argv[1], "r");
  fread(M, 1, 1024, fp);
  fclose(fp);
  for (int i = 0; i < 100; i ++) { inst_cycle(); }
  return 0;
}

编写可读可维护代码

换个输入就炸了

./minirvemu not-exist.bin
segmentation fault

代码不多, 很快定位问题; 但如何从大项目中存活?

  • 数十个源文件, 成千上万行代码
    • 这规模其实不算大, Linux有数千万行代码 😂

 

调试的最高境界: 不用调试

  • Programs are meant to be read by humans and only incidentally for computers to execute. — D. E. Knuth
  • 程序首先是拿给人读的, 其次才是被机器执行

诀窍: 编写可读可维护的代码

  • 不言自明 - 仅看代码就能明白是做什么的(specification)
  • 不言自证 - 仅看代码就能验证实现是对的(verification)

一个反例 - IOCCC 2020年的一份获奖代码

                                                       #\
                                define C(c           /**/)#c
                               /*size=3173*/#include<stdio.h>
                            /*crc=b7f9ecff.*/#include<stdlib.h>
                           /*Mile/Adele_von_Ascham*/#include<time.h>
                           typedef/**/int(I);I/*:3*/d,i,j,a,b,l,u[16],v
                           [18],w[36],x,y,z,k;char*P="\n\40(),",*p,*q,*t[18],m[4];
                          void/**/O(char*q){for(;*q;q++)*q>32?z=111-*q?z=(z+*q)%185,(k?
                          k--:(y=z%37,(x=z/37%7)?printf(*t,t[x],y?w[y-1]:95):y>14&&y<33?x
                          =y>15,printf(t[15+x],x?2<<y%16:l,x?(1<<y%16)-1:1):puts(t[y%28])))
                          ,0:z+82:0;}void/**/Q(I(p),I*q){for(x=0;x<p;x++){q[x]=x;}for(;--p
    >1;q[p]=y)y          =q[x=rand()%-~p],q[x]=q[p];}char/**/n[999]=C(Average?!nQVQd%R>Rd%
  R%          %RNIPRfi#VQ}R;TtuodtsRUd%RUd%RUOSetirwf!RnruterR{RTSniamRtniQ>h.oidts<edulc
 ni                   #V>rebmun<=NIPD-RhtiwRelipmocResaelPRrorre#QNIPRfednfi#V__ELIF__R_
Re               nifed#V~-VU0V;}V{R= R][ORrahcRdengisnuRtsnocRcitatsVesle#Vfidne#V53556
 .           .1RfoRegnarRehtRniRre   getniRnaRsiR]NIP[R erehwQQc.tuptuoR>Rtxt.tupniR
 <         R]NIP[R:egasuV_Redulcn i#VfednfiVfednuVenife dVfedfiVQc%Rs%#V);I/**/main(
  I(      f),char**e){if(f){for(i=    time(NULL),p=n,q=  n+997,x=18;x;p++){*p>32&&!(
         *--q=*p>80&&*p<87?P[*p-   81]:*     p)?t  [( --  x)]=q+1:q;}if(f-2||(d=atoi
        (e[1]))<1||65536<d){;O("   \"");             goto  O;}srand(i);Q(16,u);i=0;Q(
       36,w);for(;i<36; i++){w[i]   +=w           [i]<26 ? 97:39; }O(C(ouoo9oBotoo%]#
      ox^#oy_#ozoou#o{ a#o|b#o}c#                o~d#oo-e   #oo.  f#oo/g#oo0h#oo1i#oo
     2j#oo3k#oo4l#o   p));for(j                   =8;EOF   -(i=   getchar());l+=1){a=1+
    rand()%16;for(b  =0;b<a||i-                           main   (0,e);b++)x=d^d/4^d/8^d/
    32,d=  (d/  2|x<<15)&65535;                          b|=   !l<<17;Q(18,v);for(a=0;a<18;
    a++     ){if( (b&(1<<(i=v[a]      ))))*                 m=75+i,O(m),j=i<17&&j<i?i:j;}O(C(
    !)           ); }O(C(oqovoo97o    /n!));i=           0;for(;i<8;O(m))m[2]=35,*m=56+u[i],m[1
    ]=          75   +i++;O(C(oA!oro   oqoo9)          );k=112-j*7;O(C(6o.!Z!Z#5o-!Y!Y#4~!X!X#3}
     !W  !W     #2    |!V!V#1{!U!U#0z!            T!T#/y!S!S#.x!R!R#-w!Q!Q#ooAv!P!P#+o#!O!O#*t!N!
       N#      oo       >s!M!M#oo=r!L!L#oo<q!K!K#   &pIo@:;= oUm#oo98m##oo9=8m#oo9oUm###oo9;=8m#o
               o9   oUm##oo9=oUm#oo98m####          o09]    #o1:^#o2;_#o3<o  ou#o4=a#o5>b#o6?c#o
             7@d#o8A e#o    9B    f#o:Cg#o;          D     h#o<Ei #o=Fj#o>   Gk#o?Hl#oo9os#####
           ));d=0                                          ;}          O:    for(x=y=0;x<8;++
          x)y|=                                                               d&(1<<u[x])?
          1<<                                                               x:0;return
           /*                                                               :9    */
            y                                                                ;    }
  • 不言自明? 不言自证?
    • 上面的代码和原版相比有改动, 如何调试?
    • 如何添加一个新功能?

minirvEMU其实也做得不够好, 让我们来改进它

防御性编程

不相信外界的输入/其他函数传递的参数, 通过断言提前拦截非预期情况

#include <assert.h>
// ...
int main(int argc, char *argv[]) {
  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);
  for (int i = 0; i < 100; i ++) { inst_cycle(); }
  return 0;
}
./minirvemu not-exist.bin
minirvemu: minirvemu.c:27: main: Assertion `fp != NULL' failed.

防御性编程的意义

将预期的正确行为直接写到程序中

  • 不言自证 ✅
    • 如果违反断言, 程序马上终止
    • 避免非预期情况继续传播, 造成更难理解的错误
    • 能够大幅提升调试效率
      • segmentation fault -> minirvemu.c:27: main: ...
      • 别忘了来收拾残局的是你 😂

 

程序中的断言足够多 -> 近似于证明了程序的正确性

 

IC验证教大家写SVA(SystemVerilog Assertion), 也是类似的道理

  • 其实写assert不难, 难的是怎么把程序正确用代码语言表述出来
    • 但这是另一个话题了

改进1: 让断言失败时输出更多信息

#define Assert(cond, format, ...) \
  do { \
    if (!(cond)) { \
      fprintf(stderr, format "\n", ## __VA_ARGS__); \
      assert(cond); \
    } \
  } while (0)

int main(int argc, char *argv[]) {
  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);
  for (int i = 0; i < 100; i ++) { inst_cycle(); }
  return 0;
}

思考: 为什么Assert是宏, 而不是函数? (动手试试吧!)

改进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[]) {
  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);
  for (int i = 0; i < 100; i ++) { inst_cycle(); }
  return 0;
}

RTFM: man errno

减少代码中的隐含依赖

破坏隐含依赖 = bug (例如这里改了, 那里忘了改):

uint8_t M[512];

Assert(fsize < 1024, "Program size exceeds 1024 Bytes");
ret = fread(M, 1, 1024, fp);  // BUG: 忘了改, 可能发生缓冲区溢出!

 

更好的代码, 消灭了上述依赖:

#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
  • 不言自证 ✅ - 不要自信地认为改的时候我会记得
    • 面对几十个文件, 几千行代码, 你不会记得的

将定义放在头文件

随着项目规模增长, 需要分成多个文件来管理

// 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];

 

更好的代码:

// minirvemu.h
#define MSIZE 512 // 一改全改

// main.c
#include "minirvemu.h"
ret = fread(M, 1, MSIZE, fp);

// inst.c
#include "minirvemu.h"
assert(PC < MSIZE);
uint32_t inst = *(uint32_t *)&M[PC];

添加指令

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

Copy-Paste = 编写相似代码时, 复制旧代码并稍作修改

  • 开发效率++, 维护难度+++++

 

上述代码不言自明本身就不怎么样, 不言自证就更难了

  • 需要看很久的代码, 基本上都很难做到不言自证
    • 而且你基本上没有耐心仔细看的 😂
  • 当你粘贴出上百行这样的代码, 你很可能会改漏几处
    • 哪天你发现了一个共性的问题(例如立即数忘记符号扩展), 所有粘贴的代码都要修改
    • 改漏了 = bug

 

来自学长的肺腑之言: 粘贴一时爽, 调试火葬场 😈

编写可复用的代码

通过变量, 函数, 宏等方式消除重复/相似的代码

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;
uint32_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;

 

  • 引入中间变量, 不言自明 ✅
  • 对齐的代码更容易阅读并发现错误, 不言自证 ✅

使用合适的语言特性

typedef union {
  struct {
    uint32_t opcode  :  7;
    uint32_t rd      :  5;
    uint32_t funct3  :  3;
    uint32_t rs1     :  5;
     int32_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;
uint32_t imm = (int32_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 (...)

使用合适的语言特性(2)

  1. struct和位域(bit field)
    • 把位抽取操作交给编译器
  2. union
    • 可将数据解释成不同类型
  3. 指针
    • 按指针类型对内存地址中的数据进行解释
  4. switch-case语句
    • 替代对同一个变量的连续判断
  5. 二进制常数(GNU dialect)
    • 可以直接抄手册了

编写可读可维护代码

正确的代码 != 好代码

  • 好代码更大概率是正确的

 

好代码的两条重要准则

  • 不言自明 - 仅看代码就能明白是做什么的(specification)
  • 不言自证 - 仅看代码就能验证实现是对的(verification)

 

使用正确的编程模式写出好代码

  • 防御性编程 - 通过assert检查非预期行为
  • 减少代码中的隐含依赖 - 使得打破依赖不会发生
    • 头文件 + 源文件
  • 编写可复用的代码 - 不要Copy-Paste
  • 使用合适的语言特性 - 把细节交给语言规范和编译器

开放讨论: 思维 vs. 实现

C++的继承功能很好地解决了Copy-Paste问题

 

补充: 对大家来说, 需要先意识到Copy-Paste不是好的编程风格

  • 和具体使用哪种编程语言无关
  • 换句话说, 无论使用哪种语言, 都应该思考现在的代码足够好吗?

 

然后才是尝试使用合适的语言特性写出好代码

  • 一个事实: 如果C语言都写不好, 那么C++也很难写好
    • 语言决定上限, 思维决定下限

总结

指令集模拟器 = 用C程序实现ISA状态机

  • sEMU = 指令集模拟器 = 用C语言实现指令集手册定义的状态机
    • 自定义freestanding运行时环境
    • 支持指令周期: 取指, 译码, 执行, 更新PC

 

编写可读可维护的代码

  • 不言自明 - 仅看代码就能明白是做什么的(specification)
  • 不言自证 - 仅看代码就能验证实现是对的(verification)

 

使用正确的编程模式写出好代码

  • 防御性编程
  • 减少代码中的隐含依赖
  • 编写可复用的代码
  • 使用合适的语言特性