本章我们要构建一台虚拟的电脑,设计我们自己的指令集,运行我们的指令集,说得通俗一点就是自己实现一套汇编语言。它们将作为我们的编译器最终输出的目标代码。
计算机的内部工作原理
我们关心计算机的三个基本部件:CPU、寄存器及内存。代码(汇编指令)以二进制的形式保存在内存中,CPU 从中一条条地加载指令执行。程序运行的状态保存在寄存器中。
内存
我们从内存开始说起。现代的操作系统都不直接使用内存,而是使用虚拟内存。虚拟内存可以理解为一种映射,在我们的程序眼中,我们可以使用全部的内存地址,而操作系统需要将它映射到实际的内存上。当然,这些并不重要,重要的是一般而言,进程的内存会被分成几个段:
1.代码段(text)用于存放代码(指令)。
2.数据段(data)用于存放初始化了的数据,如int i = 10;,就需要存放到数据段中。
3.未初始化数据段(bss)用于存放未初始化的数据,如int i[1000];,因为不关心其中的真正数值,所以单独存放可以节省空间,减少程序的体积。
4.栈(stack)用于处理函数调用相关的数据,如调用帧(calling frame)或是函数的局部变量等。
5.堆(heap)用于为程序动态分配内存。
它们在内存中的位置类似于下图:
推荐C语言编程学习交流群:941636044,大量学习资料免费下载!
但我们的虚拟机并不模拟完整的计算机,我们只关心三个内容:代码段、数据段以及栈。其中的数据段我们只存放字符串,因为我们的编译器并不支持初始化变量,因此我们也不需要未初始化数据段。理论上我们的虚拟器需要维护自己的堆用于内存分配,但实际实现上较为复杂且与编译无关,故我们引入一个指令MSET,使我们能直接使用编译器(解释器)中的内存。
综上,我们需要首先在全局添加如下代码:
int *text, // text segment
*old_text, // for dump text segment
*stack; // stack
char *data; // data segment
注意这里的类型,虽然是int型,但理解起来应该作为无符号的整型,因为我们会在代码段(text)中存放如指针/内存地址的数据,它们就是无符号的。其中数据段(data)由于只存放字符串,所以是char *型的
接着,在main函数中加入初始化代码,真正为其分配内存:
int main() {
close(fd);
...
// allocate memory for virtual machine
if (!(text = old_text = malloc(poolsize))) {
printf("could not malloc(%d) for text area\n", poolsize);
return -1;
}
if (!(data = malloc(poolsize))) {
printf("could not malloc(%d) for data area\n", poolsize);
return -1;
}
if (!(stack = malloc(poolsize))) {
printf("could not malloc(%d) for stack area\n", poolsize);
return -1;
}
memset(text, 0, poolsize);
memset(data, 0, poolsize);
memset(stack, 0, poolsize);
...
program();
寄存器
计算机中的寄存器用于存放计算机的运行状态,真正的计算机中有许多不同种类的寄存器,但我们的虚拟机中只使用 4 个寄存器,分别如下:
1.PC程序计数器,它存放的是一个内存地址,该地址中存放着下一条要执行的计算机指令。
2.SP指针寄存器,永远指向当前的栈顶。注意的是由于栈是位于高地址并向低地址增长的,所以入栈时SP的值减小。
3.BP基址指针。也是用于指向栈的某些位置,在调用函数时会使用到它。
4.AX通用寄存器,我们的虚拟机中,它用于存放一条指令执行后的结果。
要理解这些寄存器的作用,需要去理解程序运行中会有哪些状态。而这些寄存器只是用于保存这些状态的。
在全局中加入如下定义: