我通常使用c++,所以它可能不是c语言的最佳实践。
这是一个基于堆栈的玩具vm,因此它非常原始,并且没有按位排列的指令。
对于一个玩具vm来说,64k可能有点过分了。
编码很难
#include <stdio.h>
#include "vm.h"
int main() {
Vm *vm = new_vm();
i32 buffer[] = {
0x00000A01, /* push 0x0A(\n) */
0x00004301, /* push 0x43(C) */
0x00004201, /* push 0x42(B) */
0x00004101, /* push 0x41(A) */
0x00000009, /* output */
0x00000002, /* pop */
0x00000009,
0x00000002,
0x00000009,
0x00000002,
0x00000009,
0x00000000 /* halt */
};
for (int i = 0; i < sizeof(buffer); i++) {
vm->mem[vm->pc+i] = buffer[i];
}
run_vm(vm);
free_vm(vm);
return 0;
}vm.h
#ifndef VM_H_
#define VM_H_
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
typedef uint32_t i32; /* other numbers */
typedef int32_t si32; /* stack pointer */
typedef unsigned char byte;
typedef struct {
i32 mem[0xffff]; /* approx. 64k */
si32 sp;
i32 pc;
i32 raw;
byte opc;
i32 param;
bool running;
} Vm;
Vm *new_vm();
void reset_vm(Vm *vm);
void free_vm(Vm *vm);
void run_vm(Vm *vm);
#endifvm.c
#include <stdio.h>
#include "vm.h"
Vm *new_vm() {
Vm *ret = (Vm*)malloc(sizeof(Vm));
ret->pc = 1024; /* add space for the stack */
ret->sp = -1;
ret->running = true;
return ret;
}
void reset_vm(Vm *vm) {
vm->running = true;
for (int i = 0; i < 0xffff; i++) {
vm->mem[i] = 0;
}
vm->sp = -1;
vm->pc = 1024;
}
void free_vm(Vm *vm) {
free(vm);
vm = NULL;
}
static void fetch(Vm *vm) {
vm->raw = vm->mem[vm->pc++];
}
static void decode(Vm *vm) {
/* style of opcode
* 24 bits for parameter
* a byte for the opcode
*/
vm->opc = vm->raw & 0xff;
vm->param = (vm->raw & 0xffffff00) >> 8;
}
static void execute(Vm *vm) {
switch(vm->opc) {
case 0x00: /* halt */
vm->running = false;
printf("Halt\n");
break;
case 0x01: /* push */
vm->mem[++vm->sp] = vm->param;
break;
case 0x02: /* pop */
vm->mem[vm->sp--] = 0;
break;
case 0x03: /* store */
vm->mem[ vm->mem[vm->sp - 1] ] = vm->mem[vm->sp];
break;
case 0x04: /* load */
vm->mem[vm->sp + 1] = vm->mem[ vm->mem[vm->sp] ];
++vm->sp;
break;
case 0x05: /* add */
vm->mem[vm->sp + 1] = vm->mem[vm->sp] + vm->mem[vm->sp - 1];
++vm->sp;
break;
case 0x06: /* sub */
vm->mem[vm->sp + 1] = vm->mem[vm->sp - 1] - vm->mem[vm->sp];
++vm->sp;
break;
case 0x07: /* mul */
vm->mem[vm->sp + 1] = vm->mem[vm->sp] * vm->mem[vm->sp - 1];
++vm->sp;
break;
case 0x08: /* div */
vm->mem[vm->sp + 1] = vm->mem[vm->sp - 1] / vm->mem[vm->sp];
++vm->sp;
break;
case 0x09: /* outc */
printf("%c", vm->mem[vm->sp]);
break;
case 0x0A: /* inpc */
vm->mem[++vm->sp] = getchar();
break;
}
}
void run_vm(Vm *vm) {
while(vm->running) {
fetch(vm);
decode(vm);
execute(vm);
}
}发布于 2020-06-26 14:51:58
我通常使用c++,所以它可能不是c语言的最佳实践。
在C++中,当内存分配在new中失败时,抛出异常时,在C编程语言中使用malloc()、calloc()或realloc()时情况并非如此。在任何内存分配调用之后都需要进行额外的检查。检查的目的是查看返回的内存是否为NULL,是否分配失败,通过指针引用是否为未知行为。
Vm *new_vm() {
Vm *ret = (Vm*)malloc(sizeof(Vm));
if (!ret)
{
fprintf(stderr, "Allocation of the Virtual Machine failed.\n");
return ret;
}
ret->pc = 1024; /* add space for the stack */
ret->sp = -1;
ret->running = true;
return ret;
}在main()中:
#include <stdio.h>
#include <stdlib.h>
#include "vm.h"
int main() {
Vm *vm = new_vm();
if (vm == NULL)
{
fprintf(stderr, "Exiting Toy Virtual Machine.\n");
return EXIT_FAILURE;
}
i32 buffer[] = {
0x00000A01, /* push 0x0A(\n) */
0x00004301, /* push 0x43(C) */
0x00004201, /* push 0x42(B) */
0x00004101, /* push 0x41(A) */
0x00000009, /* output */
0x00000002, /* pop */
0x00000009,
0x00000002,
0x00000009,
0x00000002,
0x00000009,
0x00000000 /* halt */
};
for (int i = 0; i < sizeof(buffer); i++) {
vm->mem[vm->pc+i] = buffer[i];
}
run_vm(vm);
free_vm(vm);
return EXIT_SUCCESS;
}只包括必要的标头
在发布的代码中,stdlib.h包含在vm.h中,stdlib.h在main()中没有必要,只有在vm.c中。为了缩小头文件和源文件的范围,只能包括所需的内容。在其他方面,这将减少编译时间,也可能减少链接问题。
开关语句中缺少默认大小写
在开关语句中使用default : case语句来处理尚未指定的情况通常是一种很好的编程实践:
static void execute(Vm *vm) {
switch(vm->opc) {
default:
fprintf(stderr, "Unknown Opcode in execute(). 0x%x\n", vm->opc);
return;
case 0x00: /* halt */
vm->running = false;
printf("Halt\n");
break;
...
}然后实现了通过该函数的所有可能的路径。这在C或C++以及大多数具有switch语句的其他编程语言中都是正确的。
对操作码使用ENUM
如果使用较少的数值常量和更多的符号常量,则代码将更具可读性。在C中,有两种方法可以做到这一点,要创建单个符号常量,请使用宏定义
#define SYMBOL VALUE或者使用枚举
typedef enum {
HALT = 0x00,
PUSH = 0x01,
POP = 0x02,
...
INPUTCHAR = 0x0A
} OPCODE;
typedef struct {
i32 mem[0xffff]; /* approx. 64k */
si32 sp;
i32 pc;
i32 raw;
OPCODE opc;
i32 param;
bool running;
} Vm;使用无符号类型作为索引
堆栈指针索引当前是一个有符号整数,并被初始化为-1,这是我认为不好的做法,因为堆栈-1会导致未知的行为。最好使用size_t或unsigned作为堆栈指针索引。这将迫使一些领域发生变化,但以下是我的建议:
running初始化为false而不是true。running为真,则增加堆栈指针run_vm(Vm *vm)开头,将running设置为truereset_vm(Vm *vm)的实现以匹配上述所有内容类型名称和变量名称
一开始,我对Vm感到困惑,不管它是虚拟机还是虚拟机,其他变量和类型也是如此。编写良好的代码是自文档化的,不需要大量注释,类型名称和变量名称在这方面起了很大作用。在我看来,Vm应该改名为VirtualMachine,sp应该改名为StackPointer,pc应该改名为ProgramCounter等等。
在可用的地方使用库函数
C++包含std::memset(),C中的memset()先于C++。函数reset_vm()应该使用memset()而不是用于重置内存的循环。函数memset()应该比当前循环更快。
void reset_vm(Vm *vm) {
vm->running = true;
memset(&vm->mem[0], 0, sizeof(*vm->mem[0]) * 0xffff);
vm->sp = -1;
vm->pc = 1024;
}https://codereview.stackexchange.com/questions/244566
复制相似问题