(4)打造简单OS-loader硬盘加载和C++写入文件
0.简要说明:
我们完全可以使用bochs创建映像文件,如https://blog.csdn.net/jadeshu/article/details/89046838 ,那么为什么还去用C++去模拟文件呢,主要更深刻的理解和自己动手,比直接创建的文件映像更深刻,了解的内容也更多!!!
当然如果想省事的话,您也可以直接用bochs直接创建映射软盘和硬盘文件,然后用dd ld 等命令把MBR Loader直接写入到该映射文件中对应的扇区中即可!!!
如下:
将mbr.bin文件写入虚拟磁盘boot.img,写入到第一个扇区中。
dd if=mbr.bin of=boot.img bs=512 count=1 conv=notrunc
CHS方式扇区从1开始索引,LBA方式扇区从0开始索引
第一扇区 0~512字节
第二扇区 512~1024字节
第三扇区 1024~1536字节
将loader.bin文件写入虚拟磁盘boot.img,写入偏移2个扇区(即第三个扇区),count写入4块大小文件
即512*2 = 1024
dd if=loader.bin of=boot.img bs=512 count=4 seek=2 conv=notrunc
建议初学者还是按照提供的内容进行学习!直接模拟硬盘!跟着一步一步做,不容易出错!
1.loader硬盘加载(文本模式)
后面会讲到,单独一节做个小实验,用图形模式去显示图形,我们还是先来看下下面的代码
图形模式小实验入口:https://blog.csdn.net/jadeshu/article/details/103092821
mbr.s
;主引导程序
;------------------------------------------------------------
SECTION MBR vstart=0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
mov ax,0xb800
mov gs,ax
; 清屏
;利用0x06号功能,上卷全部行,则可清屏。
; -----------------------------------------------------------
;INT 0x10 功能号:0x06 功能描述:上卷窗口
;------------------------------------------------------
;输入:
;AH 功能号= 0x06
;AL = 上卷的行数(如果为0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值:
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
mov eax, 0x2 ; 起始扇区lba地址,从间隔第二个扇区开始
mov bx, 0x900 ; 写入的地址
mov cx, 1 ; 待读入的扇区数,读取1个扇区内容
call rd_disk_m_16 ; 以下读取程序的起始部分(一个扇区)
jmp 0x900
;-------------------------------------------------------------------------------
;功能:读取硬盘n个扇区
rd_disk_m_16:
;-------------------------------------------------------------------------------
; eax=LBA扇区号
; ebx=将数据写入的内存地址
; ecx=读入的扇区数
mov esi,eax ;备份eax
mov di,cx ;备份cx
;读写硬盘:
;第1步:设置要读取的扇区数
mov dx,0x1f2
mov al,cl
out dx,al ;读取的扇区数
mov eax,esi ;恢复ax
;第2步:将LBA地址存入0x1f3 ~ 0x1f6
;LBA地址7~0位写入端口0x1f3
mov dx,0x1f3
out dx,al
;LBA地址15~8位写入端口0x1f4
mov cl,8
shr eax,cl
mov dx,0x1f4
out dx,al
;LBA地址23~16位写入端口0x1f5
shr eax,cl
mov dx,0x1f5
out dx,al
shr eax,cl
and al,0x0f ;lba第24~27位
or al,0xe0 ; 设置7~4位为1110,表示lba模式
mov dx,0x1f6
out dx,al
;第3步:向0x1f7端口写入读命令,0x20
mov dx,0x1f7
mov al,0x20
out dx,al
;第4步:检测硬盘状态
.not_ready:
;同一端口,写时表示写入命令字,读时表示读入硬盘状态
nop
in al,dx
and al,0x88 ;第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙
cmp al,0x08
jnz .not_ready ;若未准备好,继续等。
;第5步:从0x1f0端口读数据
mov ax, di
mov dx, 256
mul dx
mov cx, ax ; di为要读取的扇区数,一个扇区有512字节,每次读入一个字,
; 共需di*512/2次,所以di*256
mov dx, 0x1f0
.go_on_read:
in ax,dx
mov [bx],ax
add bx,2
loop .go_on_read
ret
; times 510-($-$$) db 0
; db 0x55,0xaa
其中代码大概意思是:
(1)首先初始化各个段寄存器
(2)清屏,设置为文本模式
(3)读取偏移第二个扇区(也就是后面C++写入文件偏移的2*512=1024位置)的512字节内容加载到0X900地址处,
(4)最后(jmp 0X900)让直接调转到0X900地址处执行
上面汇编代码应用到了LBA逻辑块知识,需要学习的进入https://blog.csdn.net/jadeshu/article/details/89072512
loader.s
section loader vstart=0x900
; 输出背景色绿色,前景色红色,并且跳动的字符串"Jade OS"
mov byte [gs:0x00],'J'
mov byte [gs:0x01],0xA4 ; A表示绿色背景闪烁,4表示前景色为红色
mov byte [gs:0x02],'a'
mov byte [gs:0x03],0xA4
mov byte [gs:0x04],'d'
mov byte [gs:0x05],0xA4
mov byte [gs:0x06],'e'
mov byte [gs:0x07],0xA4
mov byte [gs:0x08],' '
mov byte [gs:0x09],0xA4
mov byte [gs:0x0a],'O'
mov byte [gs:0x0b],0xA4
mov byte [gs:0x0c],'S'
mov byte [gs:0x0d],0xA4
jmp $ ; 通过死循环使程序悬停在此
程序说明图:
(1)是将硬盘中的MBR加载到内存0X7C00开始的位置,由BIOS完成
(2)mbr.s中将loader加载到内存0X900开始的位置
执行流程 BIOS[CS:IP 0XFFF0:FFF0]--》CS:IP[0:0X7C00]----(MBR将loader加载到0X900)--->CS:IP[0:0X900]---(loader中)
其中代码大概意思是:
起始地址为0x900,上面mbr.s汇编代码指定了文本模式显示,并指定的gs为0XB800,而0XB8000~0XB8FFF地址是在实模式下用于文本模式显示适配器,可以看下面实模式下的内存布局图!
往这段内存处赋值,即可显示相关的字符!
最后进行汇编编译
nasm mbr.S -o mbr 编译产生mbr文件
nasm loader.S -o loader 编译产生loader文件
2.C++写入文件并创建
HardDisk.h
#ifndef __HARDDISK_H__
#define __HARDDISK_H__
#define SECTOR_SIZE 512 // 1个扇区占字节大小
#define CYLINDER_COUNT 121 // 柱面
#define SECTORS_COUNT 63 // 1个柱面的扇区数
#include <string>
// 用数组存储
class CHardDisk
{
public:
CHardDisk();
~CHardDisk();
// 设置硬盘盘面、柱面、扇区
void setMagneticHead(unsigned int head) { this->head = head; }
void setCylinder(int cylinder) { this->current_cylinder = cylinder; }
void setSector(int sector) { this->current_sector = sector; }
// 获取扇区数据
char* getDiskBuffer(unsigned int head, int cylinder_num, int sector_num);
// 将buf数据写入指定扇区
void setDiskBuffer(unsigned 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:
unsigned int head = 0; // 默认盘面
int current_cylinder = 0; // 当前磁道号
int current_sector = 0; // 当前扇区号
char* disk0[CYLINDER_COUNT][SECTORS_COUNT];
char* disk1[CYLINDER_COUNT][SECTORS_COUNT];
};
#endif
HardDisk.cpp
#include "HardDisk.h"
CHardDisk::CHardDisk()
{
char* buf = nullptr;
// 初始化硬盘,在分配和初始化内存中的数据
for (int i = 0; i < CYLINDER_COUNT; i++)
{
for (int j = 0; j < SECTORS_COUNT; 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 = 0; j < SECTORS_COUNT; j++)
{
delete[] this->disk0[i][j];
delete[] this->disk1[i][j];
}
}
}
char* CHardDisk::getDiskBuffer(unsigned 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(unsigned 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 = 0; sector < SECTORS_COUNT; 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)
{
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++;
if (beginSec >= SECTORS_COUNT) {
beginSec = 0;
count_test++;
if (count_test % 2 == 0)
{
cylinder++;
}
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;
printf("==========mbr=========\n");
disk.writeFileToDisk("mbr", true, 0, 0); //0-512
// 将loader二进制文件写入0柱面偏移2扇区
printf("==========loader=========\n");
disk.writeFileToDisk("loader", false, 0, 2); // 1024-
disk.makeVirtualDisk("boot.img");
system("pause");
return 0;
}
编译结果如下:(我这里把loader文件做的大一点,是为了演示写入具体的过程!!如果仅编译上面的loader汇编代码不会有这么多文件写入),因机械磁盘效率问题,我们都是写入同一个柱面,这里有2个磁头,所以写完磁头0柱面0,然后直接写磁头1柱面0.。。随后磁头0柱面1,磁头1柱面1.。。随后磁头0柱面2 磁头1柱面2.。。。这样继续存储,读去也是,这样磁头不用倒来倒去,提高效率!
3.进入实验阶段
打开bochs模拟器,选择硬盘启动方式,更改配置文件,如https://blog.csdn.net/jadeshu/article/details/89046838 所示
在cmd中输入 bochs.exe -f bosh.src (其中bosh.src是配置文件,选择硬件方式boot:disk )
然后界面如下所示