前言
虚拟机,自我高中以来就一直很想要了解其原理,那时候也有尝试过去开发8086虚拟机(因为看了那位川合秀实的30天自制操作系统,让自己变得雄心勃勃?),因其体量之大,疯狂被劝退,我现在所开发的8086虚拟机,还远远没到达完善的地步,自己研究一直到现在的不完整版,前前后后开发了3个月,几乎是坐在电脑前就是在研究这玩意。那个时候,一步一步看到它显示出来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程序所打印出来的。
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操作码,然后正式开始指令的解析工作。