一、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; |
| uint8_t M[64]; |
为什么我们不适用int32_t和int8_t?
8位最大是127,如果内存值为127,在+1后溢出,溢出值为undefine behavior,但是无符号数不会溢出

模拟器本质上执行的就是上述几条行为,正如右下角程序所呈现的,如果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) { |
| if (((inst >> 7) & 0x1f) != 0) { |
| R[(inst >> 7) & 0x1f] = R[(inst >> 15) & 0x1f] + |
| (((inst >> 20) & 0x7ff) - ((inst & 0x80000000) ? 4096 : 0)); |
| } |
| } else if (inst == 0x00100073) { |
| 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) { |
| if (((inst >> 7) & 0x1f) != 0) { |
| R[(inst >> 7) & 0x1f] = R[(inst >> 15) & 0x1f] + |
| (((inst >> 20) & 0x7ff) - ((inst & 0x80000000) ? 4096 : 0)); |
| } |
| } else if (inst == 0x00100073) { |
| 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; |
| 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; |
| } |
根据读入的文件再次进行模拟,如果我们把可执行文件(prog)的指令序列抽取到prog.bin中,可以看到.bin的内容和反汇编内容是指令序列
三、编写可读可维护的代码
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); |
| int ret = fseek(fp, 0, SEEK_END); |
| assert(ret != -1); |
| long fsize = ftell(fp); |
| assert(fsize != -1); |
| rewind(fp); |
| assert(fsize < 1024); |
| ret = fread(M, 1, 1024, fp); |
| assert(ret == fsize); |
| fclose(fp); |
| while (!halt) { inst_cycle(); } |
| return 0; |
| } |
在每一个容易出错的地方都插入assert,即使会出现错误也会非常清晰的呈现:
| ./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]); |
| int ret = fseek(fp, 0, SEEK_END); |
| Assert(ret != -1, "Fail to seek the end of the file"); |
| long fsize = ftell(fp); |
| Assert(fsize != -1, "Fail to return the file position"); |
| rewind(fp); |
| Assert(fsize < 1024, "Program size exceeds 1024 Bytes"); |
| ret = fread(M, 1, 1024, fp); |
| Assert(ret == fsize, "Fail to load the whole program"); |
| fclose(fp); |
| while (!halt) { inst_cycle(); } |
| return 0; |
| } |
但是要注意Assert中有if ,而编译器中else会匹配最近的if,所以用宏的时候要注意最近匹配的原则。
2、减少代码中的隐含依赖
这里举了一个例子
| uint8_t M[512]; |
| Assert(fsize < 1024, "Program size exceeds 1024 Bytes"); |
| ret = fread(M, 1, 1024, fp); |
例如我们的代码原来是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 (...) { ... } |
这其中其实是有错误的,但是明显很难找出。所以我们需要把它简化,可以让代码进行复用,such as
点击查看代码
| 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; } |
| else if (funct3 == 0x4) { R[rd] = R[rs1] ^ imm; } |
| else if (funct3 == 0x6) { R[rd] = R[rs1] | imm; } |
| else if (funct3 == 0x7) { R[rd] = R[rs1] & imm; } |
| else { panic("Unsupported funct3 = %d", funct3); } |
| R[0] = 0; |
| } else if (...) { ... } |
| PC += 4; |
把代码进行模块化,需要的话直接调用即可,对齐的代码更容易阅读并发现错误
4、使用合适语言特性
其实上述代码还可以进行更标准的写法,如我们所知RISCV有很多类型例如I,J等。那么我们可以定义合适的结构体,such as
点击查看代码
| 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; |
| case 0b100: R[rd] = R[rs1] ^ imm; break; |
| case 0b110: R[rd] = R[rs1] | imm; break; |
| case 0b111: R[rd] = R[rs1] & imm; break; |
| default: panic("Unsupported funct3 = %d", inst->I.funct3); |
| } |
| R[0] = 0; |
| } else if (inst->bytes == 0x00100073) { ... } |
后续可以仿照I类型的把其余J S等类型补齐。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 提示词工程——AI应用必不可少的技术
· 地球OL攻略 —— 某应届生求职总结
· 字符编码:从基础到乱码解决
· SpringCloud带你走进微服务的世界