1、总结和开机流程
在第一节<(1)汇编写入引导区,虚拟机启动步骤>中讲解到一个简单屏幕显示一川字符串,第二节讲到BIOS启动过程!
那个汇编代码用nasm汇编器进行汇编成二进制,然后把这二进制文件写入模拟的软盘system.img[磁盘]的第0面0磁道第1扇区中!然后虚拟机加载此映射文件。
BIOS读取硬盘0盘面0磁道1扇区(C0-H0-S1)的MBR(主引导记录)到内存中指定区域(具体是BIOS提供的int 19中断例程加载MBR到RAM的0X00007C00H开始处),设置程序计数器到指定区域(EIP=0X00007C00),然后CPU开始执行MBR的指令(即CPU使用权交由MBR来掌控)。
加载别处指定硬盘位置数据到内存指定位置,然后跳转CPU到内存指定位置在执行指令,这样就跳出了512限制),然后CPU就可以执行boot loader代码命令了。
简单的说,整个开机流程到操作系统之前的动作应该是这样的:
1.BIOS:开机主动执行的韧体,会认识第一个开机的设备
2.MBR:第一个可开机设备的第一个扇区内的主引导分区块,内含引导加载程序。(MBR结构)
3.引导加载程序:一个可读取内核文件来执行的软件。
4.内核文件:开始操作系统的功能。
BIOS与MBR都是硬件本身会支持的功能,至于Boot loader(引导加载程序)则是操作系统安装在MBR上面的一套软件。由于MBR仅有466bytes而已,因此这个引导程序是非常小而完美的。这个boot loader的主要任务有下面几项:
- 提供菜单:用户可以选择不同的开机选项,这也是多重引导的重要功能呢。
- 载入内核文件:直接执行可开机的程序区段来开始操作系统。
- 转交其他loader:将引导加载功能转交给其他loader负责。
1)那么首先开始写主引导boot程序(作用:将硬盘的第0面0磁道2扇区[0磁头0柱面2扇区](C0-H0-S2)读取1个扇区的内容到内存中0X8000位置,然后跳转到这个位置执行指令)
实例一、
org 0x7c00 ;指定起始位置
LOAD_ADDR EQU 0X8000 ;加载地址位置
entry:
mov ax, 0 ;清0
mov ss, ax
mov ds, ax
mov es, ax
mov si, ax
readFloppy:
mov CH, 0 ;CH 用来存储柱面号
mov DH, 0 ;DH 用来存储磁头号
mov CL, 2 ;CL 用来存储扇区号
mov BX, LOAD_ADDR ; ES:BX 数据存储缓冲区
mov AH, 0x02 ; AH = 02 表示要做的是读盘操作
mov AL, 1 ; AL 表示要练习读取几个扇区
mov DL, 0 ;驱动器编号,一般我们只有一个软盘驱动器,所以写死为0
INT 0x13 ;调用BIOS中断实现磁盘读取功能
JC fin ;出错 跳转到fin
jmp LOAD_ADDR ;CPU-EIP跳转到0x8000位置
fin:
HLT
jmp fin
实例二、
org 0x7c00;
LOAD_ADDR EQU 0X8000
;MBR BIOS加载此二进制代码到内存0X8000地址上,CPU运行
entry:
mov ax, 0
mov ss, ax
mov ds, ax
mov es, ax
mov ss, ax
mov si, ax
mov sp, 0x7c00
mov ax, 0xb800
mov gs, ax
;设置显示器配置
mov ax, 0600h
mov bx, 0700h
mov cx, 0 ; 左上角: (0, 0)
mov dx, 184fh ; 右下角: (80,25),
; 因为VGA文本模式中,一行只能容纳80个字符,共25行。
; 下标从0开始,所以0x18=24,0x4f=79
int 10h ; int 10h
readSection1:
;读取0磁头0柱面2扇区数据到内存的BX地址处
mov BX, LOAD_ADDR ; BX = 0X8000 ES:BX数据存储缓冲区的地址
mov AH, 0x02 ; AH = 02表示要做的是读盘操作
mov AL, 1 ; AL表示要连续读取几个扇区
mov CH, 0 ;CH 用来存储柱面号
mov CL, 2 ;CL 用来存储扇区号
mov DH, 0 ;DH 用来存储磁头号
mov DL, 0 ;驱动器编号,一般我们只有一个软盘驱动器,所以写死为0
INT 0x13 ;调用BIOS中断实现磁盘读取功能
JC fin
jmp LOAD_ADDR ;EIP跳转到地址0x8000处
fin:
jmp $ ; 使程序悬停在此
2)CPU-EIP跳转到0x8000位置上(这里设置0x8000H上,当然 你可以自己设置未使用的内存地址上都是可以的),这时候我们可以写简单的一些系统内核,下面进行测试
该段汇编主要是向显卡循环显示一个一个字符,最后取值为0就跳转fin执行HLT让CPU睡眠,死循环!
要显示一个字符,int 0x10则满足条件
AH=0X0E;AL=需要显示的字符code;BH=0;BL=颜色code
实例一、利用BIOS中断int 0x10来显示文本
org 0x8000 ;起始地址0x8000
entry:
mov ax, 0 ;清理
mov ss, ax
mov ds, ax
mov es, ax
mov si, msg ;将msg地址给si
putloop:
mov al, [si] ;取si值给al
add si, 1
cmp al, 0 ;al与0比较
je fin ;al为0时跳转fin
; 以下三行是为了显示AL中保存的字符
mov ah, 0x0e ;AH必须为0x0e;在Teletype模式下显示字符
mov bx, 15 ;BH = 0, BL = 15,合起来就是BX=15,这个15是指颜色的编号为15
int 0x10 ;执行BIOS中段,简单理解一个函数,该函数的地址是0x10,该函数的作用是显示一个字符
jmp putloop
fin:
HLT
jmp fin
msg:
DB "jadeshu create OS kernel!"
实例二、利用BIOS中断int 0x10来显示文本
org 0x8000
entry:
mov ax, 0
mov ss, ax
mov ds, ax
mov es, ax
mov ss, ax
;在光标位置处打印字符.
mov ah, 3 ; 输入: 3号子功能是获取光标位置,需要存入ah寄存器
mov bh, 0 ; bh寄存器存储的是待获取光标的页号
int 0x10 ; 输出: ch=光标开始行,cl=光标结束行
; dh=光标所在行号,dl=光标所在列号
;; 打印字符串 ;;
mov ax, msg
mov bp, ax ; es:bp 为串首地址, es此时同cs一致,
; 开头时已经为sreg初始化
; 光标位置要用到dx寄存器中内容,cx中的光标位置可忽略
mov cx, 25 ; cx 为串长度,不包括结束符0的字符个数
mov ax, 0x1301 ; 子功能号13是显示字符及属性,要存入ah寄存器,
; al设置写字符方式 ah=01: 显示字符串,光标跟随移动
mov bx, 0x2 ; bh存储要显示的页号,此处是第0页,
; bl中是字符属性, 属性黑底绿字(bl = 02h)
int 0x10 ; 执行BIOS 0x10 号中断
jmp $ ; 使程序悬停在此
msg:
DB "jadeshu create OS kernel!"
分别将上面的两个汇编程序用nasm进行汇编成二进制!生成后命令为boot和kernel!
2、C++模拟软盘文件简单实现
虽然我们将引导区MBR和内核都简单写出来了,但是我们还无法运行,那么接下来就需要将这两个二进制文件写人我们自己用软件模拟的软盘内。
下面开始建立一个模拟软盘文件,模拟硬盘一个盘面(该盘面有两面,80个磁道,每个磁道有18个扇区)512*18*2*80=1474560约等于1.4M硬盘
HardDisk.h
#ifndef __HARDDISK_H__
#define __HARDDISK_H__
#define SECTOR_SIZE 512
#define CYLINDER_COUNT 80
#define SECTORS_COUNT 18
#include <string>
class CHardDisk
{
public:
CHardDisk();
~CHardDisk();
// 设置硬盘盘面、柱面、扇区
void setMagneticHead(int head) { this->head = head; }
void setCylinder(int cylinder) { this->current_cylinder = cylinder; }
void setSector(int sector) { this->current_sector = sector; }
// 获取扇区数据
char* getDiskBuffer(int head, int cylinder_num, int sector_num);
// 将buf数据写入指定扇区
void setDiskBuffer(int head, int cylinder_num, int sector_num, char* buf);
// 制作映像文件
void makeVirtualDisk(const char* name = "system.img");
void writeFileToDisk(const char* fileName, bool bootable, int cylinder, int beginSec);
private:
int head = 0; // 默认盘面
int current_cylinder = 0; // 当前磁道号
int current_sector = 0; // 当前扇区号
char* disk0[CYLINDER_COUNT][SECTORS_COUNT+1];
char* disk1[CYLINDER_COUNT][SECTORS_COUNT+1];
};
#endif
HardDisk.cpp
#include "HardDisk.h"
CHardDisk::CHardDisk()
{
char* buf = nullptr;
// 初始化硬盘,在分配和初始化内存中的数据
for (int i = 0; i < CYLINDER_COUNT; i++)
{
for (int j = 1; j < SECTORS_COUNT+1; j++)
{
buf = new char[SECTOR_SIZE]; // 效率低下
memset(buf, 0, SECTOR_SIZE);
this->disk0[i][j] = buf;
buf = new char[SECTOR_SIZE];
memset(buf, 0, SECTOR_SIZE);
this->disk1[i][j] = buf;
}
}
}
CHardDisk::~CHardDisk()
{
// 释放分配的内存
for (int i = 0; i < CYLINDER_COUNT; i++)
{
for (int j = 1; j < (SECTORS_COUNT + 1); j++)
{
delete[] this->disk0[i][j];
delete[] this->disk1[i][j];
}
}
}
char* CHardDisk::getDiskBuffer(int head, int cylinder_num, int sector_num)
{
this->setMagneticHead(head);
this->setCylinder(cylinder_num);
this->setSector(sector_num);
if (head == 0)
{
return this->disk0[cylinder_num][sector_num];
}
else if(head == 1)
{
return this->disk1[cylinder_num][sector_num];
}
return nullptr;
}
void CHardDisk::setDiskBuffer(int head, int cylinder_num, int sector_num, char* buf)
{
char* bufTmp = getDiskBuffer(head, cylinder_num, sector_num);
//memcpy_s(bufTmp, SECTOR_SIZE, buf, SECTOR_SIZE);
memcpy(bufTmp, buf, SECTOR_SIZE);
printf("已经写入到(磁头:%d - 柱面:%d - 扇区:%d)\n", head, cylinder_num, sector_num);
}
void CHardDisk::makeVirtualDisk(const char* name)
{
printf("准备开始打包......\r\n");
FILE* file = nullptr;
fopen_s(&file, name, "wb");
for (int cylinder = 0; cylinder < CYLINDER_COUNT; cylinder++)
{
// 读完0面就读同一位置的1面数据
for (int head = 0; head <= 1; head++)
{
for (int sector = 1; sector < (SECTORS_COUNT+1); sector++)
{
char* buf = getDiskBuffer(head, cylinder, sector);
// 将软件模拟的磁盘内容写入指定文件内
fwrite(buf, 1, SECTOR_SIZE, file);
}
}
}
fclose(file);
printf("打包成功\r\n");
}
void CHardDisk::writeFileToDisk(const char* fileName, bool bootable, int cylinder, int beginSec)
{
FILE* file = nullptr;
fopen_s(&file, fileName, "rb");
if (file == nullptr)
{
printf("读取文件不存在\r\n");
return;
}
char* buf = new char[512];
memset(buf, 0, 512);
if (bootable) {
buf[510] = (char)0x55;
buf[511] = (char)0xaa;
}
//求得文件的大小
fseek(file, 0, SEEK_END);
int size = ftell(file);
rewind(file);
if (size > SECTOR_SIZE)
{
int count_test = 0;
// 文件数据大于512字节,另作处理
while (!feof(file)) {
fread(buf, 1, SECTOR_SIZE, file);
setDiskBuffer(this->head, cylinder, beginSec, buf);
memset(buf, 0, SECTOR_SIZE);
beginSec++;
// 填充0面磁头扇区完后就填充1面同样柱面的扇区
//if (beginSec >= SECTORS_COUNT) {
// beginSec = 0;
// count_test++;
// if (count_test % 2 == 0)
// {
// cylinder++;
// }
// this->head == 0 ? this->head = 1 : this->head = 0;
// cylinder++;
//}
// 填充完整个0面在填充1面磁头
if (beginSec >= SECTORS_COUNT) {
beginSec = 0;
//count_test++;
//if (count_test % 2 == 0)
//{
// cylinder++;
//}
//this->head == 0 ? this->head = 1 : this->head = 0;
cylinder++;
}
if (cylinder >= CYLINDER_COUNT)
{
cylinder = 0;
this->head == 0 ? this->head = 1 : this->head = 0;
}
}
}
else
{
fread(buf, 1, size, file);
setDiskBuffer(0, cylinder, beginSec, buf);
}
fclose(file);
file = nullptr;
}
main.cpp
#include "HardDisk.h"
int main()
{
CHardDisk disk;
// 章节一案例
//disk.writeFileToDisk("test", true, 0, 1);
//disk.makeVirtualDisk("system01.img");
// 章节二案例
// 将boot二进制文件写入0柱面1扇区
disk.writeFileToDisk("boot", true, 0, 1);
// 将kernel二进制文件写入1柱面2扇区
disk.writeFileToDisk("kernel", false, 0, 2);
disk.makeVirtualDisk("system02.img");
system("pause");
return 0;
}
最后生成system02.img文件
用虚拟机打开,详情见第一节
实例一显示结果:
实例二显示结果:(黑底蓝字)