一、freestanding
在之前的学习中都是在linux进行编译。那么从学习的角度看还是freestanding比较简单,图中_start这个程序是作为死循环的,输出一个A后处于while(1)的循环状态。 那么如果我们在freestanding中进行编译呢?要在freestanding中编译首先要明白一个问题,程序如何结束运行? 通过yzh视频中得知,C99的手册中写到freetanding environment 的结束是一个未定义行为,那么怎么解决这个问题呢?就要在exit地址写暗号~,如果把//*exit注释,则会出现 死循环,不断输出A的情况。 正如C99手册所说,freestanding在手册中很多都是处于未定义的状态,在上篇博客中我们也提到未定义状态可能引起的问题,所以我们最好自己设计一个!二、YEMU (ysyx EMU)
要实现YEMU,也就是实现指令模拟,我们回顾上次课的“状态机”相关知识
正如图片中所描述的,我们只要把这个状态机实现出来,就可以来执行指令。那么又在上节课介绍:C程序是个状态机,那么我们是不是可以用C语言来实现这个状态机,从而实现指令运行。 如何用C语言实现寄存器和内存,例如在RISCV32中有32个32位的寄存器,那么我们定义一个数组就可以了,每个元素都是uint32_t,也就是32位 32个寄存器了。特殊的是用PC,还有内存#include <stdint.h>
uint32_t R[32], PC; // according to the RISC-V manual
uint8_t M[64]; // 64-Byte memory
为什么我们不适用int32_t和int8_t?
8位最大是127,如果内存值为127,在+1后溢出,溢出值为undefine behavior,但是无符号数不会溢出 ![image](/i/l/?n=24&i=blog/3412936/202404/3412936-20240419113907886-619491592.png) 模拟器本质上执行的就是上述几条行为,正如右下角程序所呈现的,如果halt为false,那么while循环会一直执行inst_cycle(), 这个cycle就是 指令周期。所以本质上我们要做的就是把 inst_cycle给实现出来,这样就是一个模拟器。但是在进行模拟前要先清楚inst的语义,也就是每个指令是什么意思。搞清楚 每个指令的意思,就需要去查手册,这个行为就称作“译码”。如下是用C实现一个模拟器点击查看代码
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 (inst == 0x00100073) { // ebreak
if (R[10] == 0) { putchar(R[11] & 0xff); }
else if (R[10] == 1) { halt = true; }
else { printf("Unsupported ebreak command\n"); }
} else { printf("Unsupported instuction\n"); }
PC += 4;
}
可以看到对指令进行分析,如果为addi指令,则进行xxx操作,如果位ebreak指令,则进行xxx操作。在指令执行完毕后需要把pc + 4,这对应这我们的"更新pc"。这里值得注意的是R[(inst >> 7) & 0x1f] = R[(inst >> 15) & 0x1f] +...这条指令是把立即数和rs寄存器的值相加,存到rd寄存器中。但是可以看到他后面还有个减法 -((inst & 0x80000000) ? 4096 : 0)。具体解释如下
最高位通常用于表示一个立即数是否为负。如果最高位是1,这通常意味着立即数是负的,需要进行符号扩展;如果最高位是0,则表示立即数是非负的。
inst & 0x80000000 这个表达式通过和 0x80000000 进行位与操作来检查 inst 的最高位是否设置(即为1)。这里的 0x80000000 是一个只有最高位为1的32位整数。如果 inst 的最高位是1,这个操作的结果将不为0,这表示立即数是负的,否则为正。
当确定立即数为负时,代码使用 4096(或者 0x1000)来进行符号扩展。在这种情况下,负的立即数将从12位扩展到32位,而12位立即数的表示方法是使用补码。在补码中,一个负数可以通过从2的指数次方中减去其绝对值来表示。由于立即数是12位,所以2的指数次方就是 2^12,也就是 4096。
因此,当你看到 ((inst & 0x80000000) ? 4096 : 0) 这段代码时,它是在做这样的操作:如果立即数是负的,就从 4096 减去其绝对值来得到32位的负立即数;如果是正数,就直接使用该值。这是RISC-V和其他许多处理器架构在处理立即数时常用的一种技巧。
指令部分分析结束后,我们还需要设定初始值,也就是rest:
riscv手册中定义 pc set 一个 0,0号寄存器恒为0,其他state 是unspecified的,也就是自己说了算的。 综合上述信息,我们就可以得到YEMY(Ysyx EMUlator )1.0版本点击查看代码
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
uint32_t R[32], PC;
uint8_t M[64] = {
0x13, 0x05, 0x00, 0x00, 0x93, 0x05, 0x10, 0x04, 0x73, 0x00, 0x10, 0x00,
0x13, 0x05, 0x10, 0x00, 0x93, 0x05, 0x00, 0x00, 0x73, 0x00, 0x10, 0x00,
0x6f, 0x00, 0x00, 0x00,
};
bool halt = false;
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 (inst == 0x00100073) { // ebreak
if (R[10] == 0) { putchar(R[11] & 0xff); }
else if (R[10] == 1) { halt = true; }
else { printf("Unsupported ebreak command\n"); }
} else { printf("Unsupported instuction\n"); }
PC += 4;
}
int main() {
PC = 0; R[0] = 0; // can be omitted since uninitialized global variables are initialized with 0
while (!halt) { inst_cycle(); }
return 0;
}
点击查看代码
uint8_t M[1024];
int main(int argc, char *argv[]) {
PC = 0; R[0] = 0;
FILE *fp = fopen(argv[1], "r");
fread(M, 1, 1024, fp);
fclose(fp);
while (!halt) { inst_cycle(); }
return 0;
}
三、编写可读可维护的代码
1、写assert,assert可以避免出现 Segmentation Fault 等类似无法调试的错误,例如
点击查看代码
#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;
}
./yemu not-exist.bin
yemu: yemu.c:27: main: Assertion `fp != NULL' failed.
同时也可以利用宏,来让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;
}
uint8_t M[512];
Assert(fsize < 1024, "Program size exceeds 1024 Bytes");
ret = fread(M, 1, 1024, fp); // BUG: 忘了改, 可能发生缓冲区溢出!
例如我们的代码原来是uint8_t M[1024],但是在后期调试的时候进行更改,改为了512,那么就会发生溢出。后续的内容收到影响进而出现非常难调试的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);
如果把这些参数定义为宏,那么消灭了上述的代码依赖。
3、编写可复用代码
未来可能一块代码复用多次,不可以直接copy paste,(容易出现一处出现错误,所有的copy-paste都需要进行修改的情况)
例如:
点击查看代码
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 (...) { ... }
点击查看代码
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 (inst->bytes == 0x00100073) { ... }