背景 最近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程序. 留给我们在计算机科学领域上可以奋发的时间不多了.