
上次课内容: C程序如何执行
本次课内容:
学习处理器设计, 为什么要了解这些?
我们之前接触的是宿主操作系统(Linux)之上的运行时环境
printf()的代码在哪里 😂
RTFM: C99
5.1.2.1 Freestanding environment
2 The effect of program termination in a freestanding environment is
implementation-defined.
QEMU虽然是个开源项目, 但还挺复杂, 不利于我们理解细节
我们还是考虑用sISA计算1+2+...+10时提供的运行时环境
bner0指令实现| C程序 | ISA | |
|---|---|---|
| 状态 | \(\{PC, V\}\) | \(\{PC, R, M\}\) |
| 激励事件 | 执行语句 | 执行指令 |
| 状态转移规则 | 语句的语义 | 指令的语义 |
回顾ISA模型机执行sISA指令的过程:
PC索引内存M, 读取一条指令opcode字段查看指令类型;
然后根据指令格式解析出操作数bner0, 则将结果写回目的寄存器;
否则, 根据判断情况决定是否进行跳转PC加1
实现sEMU = 用C代码实现inst_cycle()
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;
}#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;
}数列求和程序很简单, 不需要复杂运行时环境
添加输出一个整数的功能
out rsout rs指令后,
将R[rs]输出到终端
从命令行输入参数n,
让数列求和程序计算1+2+...+n
n放在一个约定的位置
r0寄存器r0中r0中获取数列的末项Mr0寄存器中存放程序的参数
out指令以整数方式输出指定寄存器的值
bner0指令实现#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;
}
代码不多, 很快定位问题; 但如何从大项目中存活?
调试的最高境界: 不用调试
诀窍: 编写可读可维护的代码
#\
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;
}将预期的正确行为直接写到程序中
segmentation fault ->
minirvemu.c:27: main: ...
程序中的断言足够多 -> 近似于证明了程序的正确性
IC验证教大家写SVA(SystemVerilog Assertion), 也是类似的道理
程序正确用代码语言表述出来
#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是宏, 而不是函数? (动手试试吧!)
#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 (例如这里改了, 那里忘了改):
随着项目规模增长, 需要分成多个文件来管理
// 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];
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 = 编写相似代码时, 复制旧代码并稍作修改
上述代码不言自明本身就不怎么样, 不言自证就更难了
来自学长的肺腑之言: 粘贴一时爽, 调试火葬场 😈
通过变量, 函数, 宏等方式消除重复/相似的代码
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 (...)struct和位域(bit field)
union
switch-case语句
正确的代码 != 好代码
好代码的两条重要准则
使用正确的编程模式写出好代码
assert检查非预期行为打破依赖不会发生
C++的继承功能很好地解决了Copy-Paste问题
补充: 对大家来说, 需要先意识到Copy-Paste不是好的编程风格
现在的代码足够好吗?
然后才是尝试使用合适的语言特性写出好代码
编写可读可维护的代码
使用正确的编程模式写出好代码