从0开始开发8086虚拟机

前言

虚拟机,自我高中以来就一直很想要了解其原理,那时候也有尝试过去开发8086虚拟机(因为看了那位川合秀实的30天自制操作系统,让自己变得雄心勃勃?),因其体量之大,疯狂被劝退,我现在所开发的8086虚拟机,还远远没到达完善的地步,自己研究一直到现在的不完整版,前前后后开发了3个月,几乎是坐在电脑前就是在研究这玩意。那个时候,一步一步看到它显示出来DOS系统的各种信息,一直激励着我。

从0开始开发8086虚拟机_第1张图片 这是刚刚成功显示Starting MS-DOS字样的时候,激动万分

这篇文章,主要是以详细解释为主(主要是因为我代码写得很烂hhh),这也是我的第一篇博客,写得不好,不对的地方希望能够被指正。噢对了,这篇文章主要是以运行MS-DOS操作系统为最终目标。

准备工作

书籍方面:(主要是我看的比较多的)

王爽经典的《汇编语言》

必备的《Intel 64 and IA-32 Architectures Software Developer’s Manual》就是常说的英特尔白皮书,它的Instruction Set Reference那一部分,里面有全部所需的指令,以及对应指令的伪代码,巨有用。

也是必备的《PC技术内幕》,这本书对后面的IO,中断,BIOS等有很大的作用。

还有各种操作系统相关的书籍,参考的书籍有点多,一时说不上来。

我这里如果要照顾到还不会汇编的人的话,可能篇幅会非常非常的长,所以很多有关汇编的知识可能只是一笔带过。也请见谅。

 

参考的源码和成品:

EasyVM,LightMachine,NXVM等

 

软件方面:

我之前开发的主要还是Qt下开发的,个人感觉Qt没啥缺点,也相对来说用得比较多。

这篇文章的话估计不怎么涉及Qt,主要讲原理,会一点C++就行毕竟我专业C with Class多年(xs

准备一手VirtualBox,可以方便单步调试DOS系统,然后虚拟机跑到对应的地址,对比一下内存信息,寄存器数据这些,方便查错,而且还能通过它反汇编出指令对应的字节码。emu8086那个软件也可以用来调试指令,它还有一个手册,上面有大部分指令的解析。

 

从开机开始

开机按下电源键,CPU先跳到BIOS对应的内存地址,既然说到了CPU,就简单描述一下8086的各种寄存器吧。

8086寄存器

AX 累加器(Accumulator),使用频率很高。

BX 基址寄存器(Base Register),一般用于存放存储器地址。

CX 计数器(Count Register),经常用来做计数器,一般都是CX--,一直到0

DX 数据寄存器(Data Register),存放数据

SI 源变址寄存器(Source Index),常保存存储单元地址

DI 目的变址寄存器(Destination Index),常保存存储单元地址

BP 基址指针寄存器(Base Pointer),指向堆栈区域中的基地址

SP 堆栈指针寄存器(Stack Pointer),指向堆栈区域的栈顶地址

IP 指令指针寄存器(Instruction Pointer),指向要执行指令所在存储单元的地址。IP寄存器是一个专用寄存器。

CS 代码段寄存器(Code Segment),(以前的程序猿为了拓宽内存寻址费尽心思)

Flag 标志寄存器 ,顾名思义就是标志用的

 

回到开机部分,这里我们需要讲到CS和IP,CPU的指令寄存器需要指向内存里面约定好的(BIOS程序区)0xFFFF0的这片区域,让CPU从这里开始取指令,指向的工作由CS和IP组合完成,(CS 左移4位) + IP =当前取指令的地址。

在8086呢CS为0xFFFF IP为0 所以CS:IP为0xFFFF0

在80286的时候CS就是0xF000 IP为0xFFF0

80386也是

 

BIOS程序区

开机的时候,BIOS的程序会装载到内存0xF0000一直到0xFFFFF的位置,而CPU从0xFFFF0位置开始运行,只剩下16个字节的位置,放不了几条指令的了,所以通常来说BIOS程序会进行一次大跳转,跳到F0000这块内存地址。然后从头开始执行BIOS程序。我上面那个截图的hackeriOS Dev就是BIOS程序所打印出来的。

从0开始开发8086虚拟机_第2张图片 我的BIOS程序最后是这个亚子的

 

EA 00 00 00 F0 反汇编其实就是JMP 0xF000:0000 跳转到了BIOS程序开头的位置

 

最终目的都是先在0xFFFF0取指令。

(实际上我用的也是CS=0xF000 IP=0xFFF0,参考了EasyVM的做法)

uint16_t CS_REG = 0xF000;
uint16_t IP_REG = 0xFFF0;

这里用到了uint16_t 实际上它就是一个无符号的short,在8086虚拟机开发过程中使用无符号的变量,会让你虚拟机写起来方便不少。

其他寄存器开机置0就行,标志寄存器比较特殊设置为0x0202 即中断允许标志位,置为1,还有我是为了方便后面对照VirtualBox把那个无定义的第二位也置1了(在VBox里面也是置1的)。

union U_AX_REG
{
	uint16_t data;
	uint8_t HL[2];
}AX_REG;

union U_BX_REG
{
	uint16_t data;
	uint8_t HL[2];
}BX_REG;

union U_CX_REG
{
	uint16_t data;
	uint8_t HL[2];
}CX_REG;

union U_DX_REG
{
	uint16_t data;
	uint8_t HL[2];
}DX_REG;

uint16_t SI_REG = 0;
uint16_t DI_REG = 0;
uint16_t DS_REG = 0;
uint16_t IP_REG = 0;
uint16_t SP_REG = 0;
uint16_t BP_REG = 0;
uint16_t CS_REG = 0;
uint16_t ES_REG = 0;
uint16_t SS_REG = 0;
uint16_t FLAG_REG = 0x202;

上面用到union联合体,是为了方便后面出现AH,AL这类寄存器,可以快速通过下标获得对应高低8位,0为低8位,1为高8位。

如AX_REG.HL[0];就能获得低8位的数据。

好了寄存器准备完毕,开始进入执行指令的阶段,前面说了,剩余16字节放不了几条指令了,那就放一个跳转,为了兼顾CS=0xFFFF 的情况,所以用一条远跳转即可

JMP 0xF000:0x0000

下一章讲取出JMP操作码,然后正式开始指令的解析工作。

 

 

你可能感兴趣的:(cpu,c++,经验分享,程序人生,其他)