C语言实现RISCV虚拟机with Chatgpt4

利用chatgpt4 实现RISCV虚拟机

背景

最近chatgpt4大火, 很多人包括我都能预感到chatgpt在不久的将来,将会对人类的生活造成巨大的影响, 因此我突发奇想能不能通过chatgpt让他来干一些我自己一个人干的不够好,但是有了他的帮助下比较容易的一件事情呢? 我想到了, 要不用C写一个RISCV虚拟机吧! Let’s do it!

写RISCV虚拟机之前的准备

首先,要明确的一点是,目前chatgpt4(截至2023/03/24)还不会比人类要强,只能说是人工智能这趟列车在逼近人类智慧的中后阶段, 不过这趟列车终将飞驰而过,把人类远远抛在后面. 但是chatgpt4 非常擅长于脏活累活, 我经常写好一个模块的增,然后让chatgpt根据我写的增,完善模块的删改查功能, 可以说chatgpt让整体的开发进度提速了300%-400%.

因此,在写RISCV虚拟机之前我们也是一样的,我们需要定义好我们想要的功能是什么样的,比如我们要支持32bits还是64bits的riscv指令? 我们只支持riscv的哪些指令的解析等等, 然后我们还需要定义我们虚拟机的结构,比如内存是怎么布局的?支持哪些特殊的virtual routine等等.

在本次的RISCV虚拟机中, 我们假定我们支持R/I/S/SB/U/UJ类型的RISC-V指令, 并且支持堆内存的分配,支持嵌套函数调用, 支持简单的通过读写特殊的内存地址转发到宿主机的stdin/stdout等, 同时我们还需要假定输入文件存放了符合要求的二进制RISCV指令, 注意并不是常见的ELF格式文件,而是直接将对应的汇编代码转译成二进制的文件.

chatgpt4 定义RISCV虚拟机所需的数据结构

首先我们需要明确我们需要哪些数据结构

  • 代表32bits的riscv指令的structure instruction_t , 以及支持的指令类型riscv_instruction_type_t
  • 代表读入的二进制指令文件布局的structure blob_t
  • 代表整个riscv的硬件状态的虚拟机structure cpu_t
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// RISC-V 指令类型枚举
typedef enum {
    R_TYPE,
    I_TYPE,
    S_TYPE,
    SB_TYPE,
    U_TYPE,
    UJ_TYPE
} riscv_instruction_type_t;

// RISC-V 指令结构体
typedef struct {
    riscv_instruction_type_t type; // 指令类型
    uint32_t opcode;              // 操作码
    uint32_t rd;                  // 目标寄存器
    uint32_t rs1;                 // 源寄存器 1
    uint32_t rs2;                 // 源寄存器 2
    uint32_t funct3;              // 功能码 3
    uint32_t funct7;              // 功能码 7
    int32_t imm;                  // 立即数
} riscv_instruction_t;

// 存储指令和数据内存的结构体 
struct blob {
    char inst_mem[INST_MEM_SIZE];
    char data_mem[DATA_MEM_SIZE];
} blob;

// 定义CPU结构体,包含寄存器、数据内存和程序计数器
typedef struct {
    uint32_t registers[NUM_REGISTERS];
    char data_mem[DATA_MEM_SIZE];
    uint32_t pc;
} cpu_t;

chatgpt4定义支持的RISCV指令

我们需要支持R/I/S/SB/U/UJ类型的指令, 那让我们看看这些指令的结构长什么样子

其实不难看出,最难的部分在于imm立即数的解析, 其实说实话我对于chatgpt4生成的instruction_t并不太满意, 理由是一个instruction 就要占8*4Bytes =32Bytes, 最理想的情况下,应该是一个instruction 就是4Bytes, 但是这样需要为每个类型的指令都新增一个structure,我觉得不太优雅就否了这个选项. 虽然他也给了我差强人意的8Bytes的选项, 他是这样定义的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef struct {
    uint32_t opcode : 7;
    uint32_t rd : 5;
    uint32_t funct3 : 3;
    uint32_t rs1 : 5;
    uint32_t rs2 : 5;
    uint32_t funct7 : 7;
    union {
        uint32_t imm : 12;
        uint32_t imm_hi : 20;
    };
} instruction_t;  // 每个instruction_t占8个字节
typedef struct {
    riscv_instruction_type_t type; // 指令类型
    uint32_t opcode;              // 操作码
    uint32_t rd;                  // 目标寄存器
    uint32_t rs1;                 // 源寄存器 1
    uint32_t rs2;                 // 源寄存器 2
    uint32_t funct3;              // 功能码 3
    uint32_t funct7;              // 功能码 7
    int32_t imm;                  // 立即数
} riscv_instruction_t; // 每个instruction_t占32个字节
  • 关于imm解析的事情,如果有仔细看RISCV指令组成会发现, 有些imm的下标不是从0开始的,这是因为要做硬件上的一些优化导致的

详情可以参考stackoverflow https://stackoverflow.com/questions/58414772/why-are-risc-v-s-b-and-u-j-instruction-types-encoded-in-this-way

  • 同时为了可读性 我们可以让chatgpt给我们生成对应的宏定义, 我就列几个举举例子, 虽然我们也可以做这件事情,但是如果我们来做的话,不仅特别繁琐,而且还容易弄错, 但是如果交给chatgpt的话, 它很快就给我们生成又快又好的宏定义了
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#define OPCODE_R_TYPE 0x33
#define OPCODE_I_TYPE1 0x03
#define OPCODE_I_TYPE2 0x13
#define OPCODE_S_TYPE 0x23
#define OPCODE_SB_TYPE 0x63
#define OPCODE_U_TYPE_LUI 0x37
#define OPCODE_U_TYPE2 0x17
#define OPCODE_UJ_TYPE_JAL 0x6F

// 宏定义:funct3 和 funct7 值
// ADD and SUB
#define FUNCT3_ADD 0x00
#define FUNCT7_ADD 0x00
#define FUNCT7_SUB 0x20

// SLL
#define FUNCT3_SLL 0x01
#define FUNCT7_SLL 0x00

// SLT and SLTU
#define FUNCT3_SLT 0x02
#define FUNCT3_SLTU 0x03
#define FUNCT7_SLT 0x00
#define FUNCT7_SLTU 0x00

// XOR
#define FUNCT3_XOR 0x04
#define FUNCT7_XOR 0x00

chatgpt4搭建起main函数框架

我们首先来看看chatgpt4为我们生成的main函数, 说实话没有什么可以挑剔的地方, 就算让我来写, 不仅花时间,而且也不会比他写的更好. 整体来说,看了整个main函数我们最关心的应该是execute_program这个函数的实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <binary_file>\n", argv[0]);
        return 1;
    }
    FILE *fp = fopen(argv[1], "rb");
    if (fp == NULL) {
        fprintf(stderr, "Failed to open file: %s\n", argv[1]);
        return 1;
    }
    size_t inst_mem_size = fread(blob.inst_mem, 1, INST_MEM_SIZE, fp);
    if (inst_mem_size == 0) {
        fprintf(stderr, "Failed to read instruction memory from file: %s\n", argv[1]);
        return 1;
    }
    fclose(fp);
    cpu_t cpu = {0};
    execute_program(&cpu, blob.inst_mem, inst_mem_size);
    return 0;
}

chatgpt4解析RISCV指令

现在来到我们的重头戏了,如何解析这些RISCV指令呢? 来看看chatgpt写的execute_program

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void execute_program(cpu_t *cpu, const char *inst_mem, uint32_t inst_mem_size) {
    cpu->pc = 0;
    while (cpu->pc < inst_mem_size) {
        uint32_t inst = *((uint32_t *)(inst_mem + cpu->pc));
        riscv_instruction_t decoded_inst;
        decode_instruction(inst, &decoded_inst);
        // 根据指令类型执行相应的操作
        switch (decoded_inst.type) {
            case R_TYPE:
                execute_r_type(cpu, &decoded_inst);
                break;
            case I_TYPE:
                execute_i_type(cpu, &decoded_inst);
                break;
            case S_TYPE:
                execute_s_type(cpu, &decoded_inst);
                break;
            case SB_TYPE:
                execute_sb_type(cpu, &decoded_inst);
                break;
            case U_TYPE:
                execute_u_type(cpu, &decoded_inst);
                break;
            case UJ_TYPE:
                execute_uj_type(cpu, &decoded_inst);
                break;
            default:
                fprintf(stderr, "Unknown instruction type encountered\n");
                return;
        }
        // 更新 pc
        if (decoded_inst.type != UJ_TYPE && decoded_inst.type != SB_TYPE) {
            cpu->pc += 4;
        }
    }
}

说实话, 写的也是很好,没有什么可以挑剔的话, 结构清晰,易于扩展, 以后我们要是需要支持别的指令只需要加一个对应的switch case就可以了,我们可以看到execute_program通过调用decode_instruction解析指令,然后根据不同的type, 去执行对应的类型的指令. 我们来看看decode_instruction的实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
void decode_instruction(uint32_t inst, riscv_instruction_t *decoded_inst) {
    // 从32位指令中提取各个字段
    decoded_inst->opcode = inst & 0x7F;
    decoded_inst->rd = (inst >> 7) & 0x1F;
    decoded_inst->funct3 = (inst >> 12) & 0x07;
    decoded_inst->rs1 = (inst >> 15) & 0x1F;
    decoded_inst->rs2 = (inst >> 20) & 0x1F;
    decoded_inst->funct7 = (inst >> 25) & 0x7F;

    // 根据opcode确定指令类型并解码立即数
    switch (decoded_inst->opcode) {
        case OPCODE_R_TYPE:
            decoded_inst->type = R_TYPE;
            decoded_inst->imm = 0;
            break;

        case OPCODE_I_TYPE1:
        case OPCODE_I_TYPE2:
            decoded_inst->type = I_TYPE;
            decoded_inst->imm = (inst >> 20) & 0xFFF;
            if (decoded_inst->imm & 0x800) { // sign-extend if necessary
                decoded_inst->imm |= 0xFFFFF000;
            }
            break;
            case OPCODE_S_TYPE:
        decoded_inst->type = S_TYPE;
        decoded_inst->imm = ((inst >> 7) & 0x1F) | ((inst >> 20) & 0xFE0);
        if (decoded_inst->imm & 0x800) { // sign-extend if necessary
            decoded_inst->imm |= 0xFFFFF000;
        }
        break;
        
        case OPCODE_SB_TYPE:
            decoded_inst->type = SB_TYPE;
            decoded_inst->imm = ((inst >> 7) & 0x1E) | ((inst >> 20) & 0x7E0) | ((inst << 4) & 0x800) | ((inst >> 19) & 0x1000);
            if (decoded_inst->imm & 0x1000) { // sign-extend if necessary
                decoded_inst->imm |= 0xFFFFE000;
            }
            break;

        case OPCODE_U_TYPE_LUI:
            break;
        case OPCODE_U_TYPE2:
            decoded_inst->type = U_TYPE;
            decoded_inst->imm = inst & 0xFFFFF000;
            break;

        case OPCODE_UJ_TYPE_JAL:
            decoded_inst->type = UJ_TYPE;
            decoded_inst->imm = ((inst >> 12) & 0xFF) | ((inst >> 20) & 0x7FE) | ((inst << 11) & 0x800) | ((inst >> 9) & 0x100000);
            if (decoded_inst->imm & 0x100000) { // sign-extend if necessary
                decoded_inst->imm |= 0xFFE00000;
            }
            break;

        default:
            // 未知指令类型,可以将类型设置为无效
            decoded_inst->type = -1;
            decoded_inst->imm = 0;
            break;
    }
}

说实话,我觉得很多人可能看这些代码可能似懂非懂, 包括刚开始的我也一样, 这个位运算是在干什么? 这个为什么要左移?等等, 但是记住,不懂得都可以问chatgpt 让他给你解释代码! 不过打铁还需自身硬,如果chatgpt生成的代码是错误的, 你还需要有能力发现,并且提醒他改正,如果它无法改正还需要你自己亲手给他改.

最后,我们看一下cpu是怎么执行指令然后更新自己的状态的,让我们浅看一个execute_sb_type函数, 其他函数其实也是大差不差

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void execute_s_type(cpu_t *cpu, riscv_instruction_t *inst) {
    uint32_t rs1 = inst->rs1;
    uint32_t rs2 = inst->rs2;
    uint32_t address = cpu->registers[rs1] + inst->imm;

    if (address >= DATA_MEM_SIZE) {
        fprintf(stderr, "Address out of range in S-type instruction\n");
        return;
    }

    switch (inst->funct3) {
        case FUNCT3_SB:
            cpu->data_mem[address] = (uint8_t)(cpu->registers[rs2] & 0xFF);
            break;

        case FUNCT3_SH:
            *((uint16_t *)&cpu->data_mem[address]) = (uint16_t)(cpu->registers[rs2] & 0xFFFF);
            break;

        case FUNCT3_SW:
            *((uint32_t *)&cpu->data_mem[address]) = cpu->registers[rs2];
            break;

        default:
            fprintf(stderr, "Unknown S-type instruction encountered\n");
            return;
    }
}

这个例子里面由于S类型的指令都是更新内存的所以看不到对寄存器的修改, 但是对寄存器的修改差不多也是类似的.

chatgpt4 实现对应的Virtual Routine

所谓的Virtual Routine 指的就是通过读写特殊的内存地址来达到实现一些目的, 类似于memory I/O ,

  • 比如我可以规定读写0x80ff 地址时 解释为malloc 行为, 分配一定的对内存;
  • 读写0xffff地址时,解释为register dump, 打印所有的寄存器和pc的值到控制台

使用chatgpt4 实现RISC-V虚拟机的感悟

其实说实话,这个项目弄了还是还是蛮久的大概是1-2天左右, 但是如果没有chatgpt的情况下,我可能得花费10倍的时间才能完成同样的工作量, 很多时间都需要花在指令解析这个部分上, 但是有了chatgpt之后 我其实就是写一些prompt,然后读懂chatgpt写的代码,看他有没有写错,当然有些时候也会问他为什么要这么写, 以及让他以更加优雅的方式实现我的想法.在这些方面chatgpt都做的非常棒, 可以预见的未来,chatgpt + 中高级程序员 就能一个打十个, 真的类似于10倍效率的提升.

然后说下我对chatgpt的看法, 或者对人工智能这个领域的看法, 其实本身我个人对AI这个领域并不太感冒,甚至可以说一无所知, 因为我觉得这东西太玄乎了, 不是最顶尖那一波科研人员 都很难在这个领域有真正的建树. 相比而言,我更喜欢工程化的项目,你的进步是看得见的,你的理解是确定真实的. 因为工程上的东西, 你只要花时间,只要你不太笨,你多多少少都能学到东西, 而AI领域的知识,需要很好的数学背景而且你的进步是无法具体量化的,他的结果是无法用逻辑解释的. 但是chatgpt的出现,让我改变了我的想法, 虽然我依旧认为我的智力不足以在AI有所建树, 但是我觉得最不容易被淘汰的方向应该是要往工程化+AI的结合这个方向走,因为所有AI项目的落地都需要结合工程化, 我目前急需培养的能力就是补齐深度学习相关的知识,如果做好这方面的知识储备的话,我相信不久的未来能占到一些先机.

现在虽然chatgpt4仍有一些缺陷, 比如我尝试给一段二进制riscv指令给chatgpt4, 让他给我反编译成riscv汇编代码, 他还是无法很好的胜任这个工作, 但是我相信在不久的将来,可能是3年内 可能是5-10年内, 如果我再让chatgpt给我反汇编, 我相信他很好的完成这个工作, 估计这个时候,人工智能这趟列车已经超过了人类科学发展的列车, 人类最终的宿命有可能只是充当人工智能程序的boot程序. 留给我们在计算机科学领域上可以奋发的时间不多了.

0%