参考文章:《操作系统真象还原》第三章 ---- 完善MBR 尝汇编先苦涩后甘甜而再战MBR!(内有闲聊)

一、地址、section、vstart(实模式汇编关键)
 
地址本质:程序中符号(指令/变量)相对于文件/段开头的偏移量,由编译器分配
section(段):汇编中划分代码/数据的逻辑块,用于组织程序、控制地址分配
vstart:指定段的虚拟起始地址,MBR被BIOS加载到 0x7c00 ,所以必须用 vstart=0x7c00 ,让编译出的偏移地址与实际内存地址一致,避免寻址错误
 
二、实模式CPU与内存(x86 16位)
 
CPU结构:控制单元(取指/译码)、运算单元(ALU)、存储单元(寄存器组)
核心寄存器:
通用寄存器:AX/BX/CX/DX、SI/DI、BP/SP(栈指针)
段寄存器:CS(存储代码段)、DS(存储数据段)、ES(额外附加的段寄存器)、SS(栈寄存器)
指令寄存器:IP
标志寄存器:FLAGS(用于表示是否进位,为0、溢出)
内存分段寻址:CS:IP取指实模式下地址=段基址<<4 + 偏,实际计算即为:cs的地址末尾加上0,在与IP地址相加,结果即为最终地址
栈操作:SS:SP指向栈顶, push 减SP、 pop 加SP


 
三、直接操作显卡(绕过BIOS中断)

BIOS只能在16为实模式下才能使用,在32位保护模式下不能直接调用BIOS中断,后面的学习都是基于保护模式下的,因此要绕过BIOS中断,直接对显卡进行读写来输出字符
原理:显卡显存被映射到CPU内存空间(实模式下文本模式默认 0xb8000 )CPU写显存=直接控制显示


显存格式(文本模式):每字符占2字节,其中低字节:表示要输出的字符,高字节表示所输出字符的属性(前景色+背景色+高亮/闪烁)


用到的代码:
用 mov ax,0xb800  →  mov es,ax  指向显存段
按 es:偏移 写入字符与属性,实现屏幕输出

四、MBR-->Loader

主引导记录 MBR最多只有512字节要留 2 字节做标识:0x55、0xaa ,真正能用的代码空间只有几百字节,而一个操作系统的内核所需要的空间大小远超几百字节,所以引入Loader这一个中间程序实现从“MBR-->内核”到“MBER-->Loader-->内核”的转变
 在加入了Loader后,操作系统的启动流程就变为了:
 
1. 开机 → CPU 复位 → 硬件找启动盘
2. 加载 MBR 到  0x7c00  并执行
3. MBR 把 Loader 从硬盘读到内存
4. 跳转到 Loader 执行
5. Loader 进行一系列操作
6. Loader 最后把内核加载进来
7. 跳转到内核,操作系统正式启动


 
五、代码实现--改进MBR,直接操作显卡来显示字符

将文件mbr.s中的代码修改成如下:

SECTION MBR vstart=0x7c00 ;起始地质编译在0x7c00
    mov ax,cs         ; CS = 代码段寄存器,此时 BIOS 刚进入 MBR,CS=0,把 CS 寄存器的值传给 AX
    mov ds,ax         ; 让数据段寄存器 DS、附加段寄存器 ES、栈段寄存器 SS、额外段寄存器 FS 指向和 CS 一样的段地址(统一段地址,避免出错)
    mov es,ax
    mov ss,ax
    mov fs,ax
    mov sp,0x7c00     ; 设置栈顶指针 sp = 0x7c00,为 MBR 留出安全的栈空间
    mov ax,0xb800     ; 文本模式显存起始地址是 0xB8000,因为段寄存器只能存“段基址”,段基址 << 4 才是真实地址,0xB800 << 4 = 0xB8000,正好是显存开始
    mov gs,ax         ; GS是通用段寄存器,一般可以任意使用,把 GS 指向显存段,之后用 gs:偏移来表示最终的地址从而正确定位到屏幕的位置

    
    mov ax,0600h      ;调用BIOS中断,AH=0x06:向上滚屏功能(等于清屏),AL=0x00:整屏清空
    mov bx,0700h      ;BH=0x07:清屏后默认黑底白字属性
    mov cx,0          ;左上角坐标 (0,0)
    mov dx,184fh      ;右下角坐标 (24,79) → 184f 是十六进制表示
    
    
    int 0x10          ;调用 BIOS 显示中断.执行清屏

    ;新增功能 直接操作显存部分
    ;预设输出LOVE6 OS
    
    mov byte [gs:0x00],'H'     ;低位字节储存要输出的字符
    mov byte [gs:0x01],0xA4    ;背景储存在第二个字节 含字符与背景属性:高亮粉字
    
    mov byte [gs:0x02],'E' 
    mov byte [gs:0x03],0xA4
    
    mov byte [gs:0x04],'L' 
    mov byte [gs:0x05],0xA4
    
    mov byte [gs:0x06],'L' 
    mov byte [gs:0x07],0xA4
    
    mov byte [gs:0x08],'0' 
    mov byte [gs:0x09],0xA4
    
    mov byte [gs:0x0A],' ' 
    mov byte [gs:0x0B],0xA4
    
    mov byte [gs:0x0C],'O' 
    mov byte [gs:0x0D],0xA4
    
    mov byte [gs:0x0E],'S' 
    mov byte [gs:0x0F],0xA4
    
    jmp $                     ;于此处死循环
    
    times 510 - ($ - $$) db 0 ;填充 0,直到代码长度 = 510 字节,$代表当前指令的地址, $$代表当前段的起始地址(也就是0x7c00 )
    db 0x55,0xaa              ;MBR 有效标志:BIOS 识别这是合法引导扇区

修改完成后,同上一章的操作一样,将mbr.s汇编生成二进制文件、将二进制文件写入虚拟硬盘、进入bochs运行仿真

在bochs内打开终端,输入以下命令

nasm -o mbr.bin mbr.s    ;编译生成二进制文件mbr.bin
dd if=“你的mbr.bin文件的位置” of=“你所创建的虚拟硬盘的位置” bs=512 count=1 conv=notrunc
bin/bochs -f bochsrc.disk ;启动bochs

成功运行如下图所示:

六、代码实现--从磁盘读入Loader

对mbr.s文件进行进一步的修改:

%include "boot.inc"
SECTION MBR vstart=0x7c00 ;起始地址编译在0x7c00
    mov ax,cs
    mov ds,ax
    mov es,ax
    mov ss,ax
    mov fs,ax
    mov sp,0x7c00
    mov ax,0xb800  ; ax为文本信号起始区
    mov gs,ax      ; gs = ax 充当段基址的作用
    
    		   ; ah = 0x06 al = 0x00 想要调用int 0x06的BIOS提供的中断对应的函数 
    		   ; 即向上移动即完成清屏功能
    		   ; cx dx 分别存储左上角与右下角的左边 详情看int 0x06函数调用
    mov ax,0600h 
    mov bx,0700h
    mov cx,0
    mov dx,184fh
    
    ;调用BIOS中断
    int 0x10 

    		
    		   
    mov byte [gs:0x00],'H' 
    mov byte [gs:0x01],0xA4    
    
    mov byte [gs:0x02],'E' 
    mov byte [gs:0x03],0xA4
    
    mov byte [gs:0x04],'L' 
    mov byte [gs:0x05],0xA4
    
    mov byte [gs:0x06],'L' 
    mov byte [gs:0x07],0xA4
    
    mov byte [gs:0x08],'O' 
    mov byte [gs:0x09],0xA4
    
    mov byte [gs:0x0A],' ' 
    mov byte [gs:0x0B],0xA4
    
    mov byte [gs:0x0C],'O' 
    mov byte [gs:0x0D],0xA4
    
    mov byte [gs:0x0E],'S' 
    mov byte [gs:0x0F],0xA4
    
                   
    mov eax,LOADER_START_SECTOR    ;把要读取的起始扇区号 2 存入 EAX
    mov bx,LOADER_BASE_ADDR        ;把内存目标地址 0x600 存入 BX,BX 用来存放数据写入的内存地址
    mov cx,1                       ;读取磁盘数设为1 
    
    call rd_disk_m_16              ;执行该函数:从硬盘读取Loader到内存,函数的具体代码在后面给出

    jmp LOADER_BASE_ADDR           ;跳转到 Loader 执行
    
   
;读取硬盘的基本流程:
;1 写入待操作磁盘数
;2 写入LBA 低24位寄存器 确认扇区(LBA:Logical Block Addressing, 逻辑块寻址现代LBA是48位的)
;3 device 寄存器 第4位主次盘 第6位LBA模式 改为1
;4 command 写指令
;5 读取status状态寄存器 判断是否完成工作
;6 完成工作 取出数据
  

rd_disk_m_16:
 

 ;;;;;;;;;;;;;;;;;;;; 1、写入待操作磁盘数
    mov esi,eax   ; 
    mov di,cx     ;备份 AX、CX,因为后面端口操作会覆盖这些寄存器
    
    mov dx,0x1F2  ;指定要操作的硬盘端口,0x1F2是IDE硬盘控制器的固定端口号,是硬盘“扇区计数”端口
                  ;操作 IO 端口时,端口号必须放在 dx 寄存器里
    mov al,cl     ;把要读取的扇区数,即“1”个存到 ,通过端口发送数据时,数据只能存在ax、al 里
    out dx,al     ;向硬盘的 0x1F2 端口,发送数值 1,即:读取一个扇区
    
    mov eax,esi   ;恢复 eax = 2(LBA起始扇区号)
    
    
    
 ;;;;;;;;;;;;;;;;;;;; 2、写入LBA 24位寄存器 确认扇区


    mov cl,0x8    ;准备 “右移 8 位” 的步数,后面要反复用 shr 右移 8 位,所以先把 8 存到 cl
                  ;操作 IO 端口时,端口号必须放在 dx 寄存器里
    mov dx,0x1F3  ;写0x1F3 端口,0x1F3 端口是 LBA low,即LBA的低8位,
    out dx,al     ;把 AL 里的值发给硬盘,此时 al = 0x02,所以低 8 位 = 0x02

    mov dx,0x1F4  ;写0x1F4 端口,0x1F4 端口是 LBA Mid,即LBA的中8位
    shr eax,cl    ;将eax右移8位,此时eax=0x0000 al=00
    out dx,al     ;所以中 8 位 = 0x00
    
    mov dx,0x1F5  ;写0x1F5 端口,0x1F5 端口是 LBA High,即LBA的高8位
    shr eax,cl    ;将eax右移8位,此时eax=0x0000 al=00
    out dx,al     ;所以高 8 位 = 0x00
                  ;因此,最终发送给硬盘的24位地址:0x000002
    

;;;;;;;;;;;;;;;;;;;;;;3、设置 Device 寄存器(开启 LBA 模式)

    shr eax,cl    ;把 EAX 右移 8 位
    and al,0x0f   ;与0x0f做与运算,相当于只保留AL低4位
    or al,0xe0    ;与0xe0做或运算,相当于AL高4位变为1110
    mov dx,0x1F6  ;写0x1F6 端口,0x1F6 端口是Device寄存器的端口号
    out dx,al     ;将0x1F6设置为1110 0000 ,代表开启LBA模式
                  ;位 7~5 = 111:固定值,必须这样写
                  ;位 4 = 0:表示主硬盘(1 是从盘)
                  ;位 3~0 = LBA 地址的第 24~27 位(我们这里是 0)

                  ;Device寄存器是用来////
                   

;;;;;;;;;;;;;;;;;;;;;4、发送读命令

    mov dx,0x1F7 ;写0x1F7 端口,0x1F5 端口是硬盘控制器的端口号 
    mov ax,0x20  ; 0x20是读命令,相当于告诉硬盘:要读取扇区
                 ;发给硬件的数据必须放在 al/ax/eax里
    out dx,al    ;将0x1F7设置为0x20 ,代表让硬盘开始读扇区



;;;;;;;;;;;;;;;;;;;;5、循环等待硬盘就绪
    
		  
  .not_ready:     
    nop           ;空操作,延时等待
    in al,dx      ;dx 此时的值 = 0x1F7,即将此时0x1F7的状态传给al
    and al,0x88   ;al与0100 0100 即0x88做与运算
                  ;即保留al第7位(BSY位,是状态位)
                  ;也保留al第3位(DRQ数据就绪位)
                  ;用于判断此时硬盘是否繁忙、数据是否准备好
    cmp al,0x08   ;比较al与0000 1000,若第7位为0 表明硬盘不繁忙
                  ;                   若第3位为1,表明数据已准备好

    jne .not_ready ; 若不相等,则重复循环
    

;;;;;;;;;;;;;;;;;;;;;6、循环读取数据(一次读 2 字节)

    mov ax,di      ;di里保存的是要读取的扇区数(之前备份的 cx=1),因此ax=1
    mov dx,256     ;1个扇区 = 512 字节,一次读 2 字节,要读256 次
    mul dx         ;ax = ax · dx=1x256=256
    mov cx,ax      ;cx 是 loop 循环专用计数器,cx == 0即跳出循环
    mov dx,0x1F0   ;写0x1F0 端口,0x1F0 端口是硬盘数据端口的端口号

 .go_read_loop:
    in ax,dx       ;两字节dx 一次读两字节存入 ax
    mov [bx],ax    ;bx转么用于存储目标内存地址,即把读到的2字节数据写入内存
                   ;bx = 寄存器里存的纯数字(比如 0x600)
                   ;[bx] = 以bx里的数字作为地址,去访问内存
                   ;bx的初始值我们前面设置了是0x600,是loader程序的起始地址
    add bx,2       ;内存地址向后移动2字节,准备存下一组数据

    loop .go_read_loop
    
    ret            ;函数返回,跳转到 call 的下一句
        
    times 510 - ($ - $$) db 0 
    db 0x55,0xaa

在bochs文件夹下新建include文件夹用于存放头文件

然后在include文件夹内新建头文件boot.inc

LOADER_START_SECTOR equ 2       ;把 LOADER_START_SECTOR 这个符号直接替换成数值 2
                                ;表明Loader 程序在磁盘上的起始扇区号是2扇区
                                ;扇区从0开始计数,第0扇区是MBR,第1扇区是1,所以2就是第3个扇区
                                
LOADER_BASE_ADDR equ 0x600      ;Loader 程序被加载到内存的起始地址是 0x600
;MBR 引导程序在调用 BIOS 中断读取磁盘时,会把 0x600 作为目标内存地址,把 Loader 读到这里,然后跳过去执行

然后将把库目录路径链接给MBR.S,生成二进制文件,并将二进制文件写入虚拟硬盘(仍然是在bochs下打开终端)

nasm -I include/ -o mbr.bin mbr.S    ;在include/这个路径里面去寻找头文件,然后进行编译
dd if=“你的mbr.bin文件的位置” of=“你所创建的虚拟硬盘的位置” bs=512 count=1 conv=notrunc

编写简单的loader程序进行验证

%include "boot.inc"
SECTION MBR vstart=LOADER_BASE_ADDR
    mov byte [gs:0x00],0x00             ;gs:0x00访问显存第0个位置(屏幕左上角第一个字符)
    mov byte [gs:0x01],0xA4             ;这里是写入空字符,即不显示任何字符
                                        ;0xA4 = 二进制 1010 0100
                                        ;高位 1010:浅绿色背景
                                        ;低位 0100:红色前景
    
    mov byte [gs:0x02],'H' 
    mov byte [gs:0x03],0xA4
    
    mov byte [gs:0x04],'E' 
    mov byte [gs:0x05],0xA4
    
    mov byte [gs:0x06],'L' 
    mov byte [gs:0x07],0xA4
    
    mov byte [gs:0x08],'L' 
    mov byte [gs:0x09],0xA4
    
    mov byte [gs:0x0A],' ' 
    mov byte [gs:0x0B],0xA4
    
    mov byte [gs:0x0C],'L' 
    mov byte [gs:0x0D],0xA4
    
    mov byte [gs:0x0E],'O' 
    mov byte [gs:0x0F],0xA4
    
    mov byte [gs:0x10],'A' 
    mov byte [gs:0x11],0xA4
    
    mov byte [gs:0x12],'D' 
    mov byte [gs:0x13],0xA4
    
    mov byte [gs:0x14],'E' 
    mov byte [gs:0x15],0xA4
    
    mov byte [gs:0x16],'R' 
    mov byte [gs:0x17],0xA4
    
    mov byte [gs:0x18],' ' 
    mov byte [gs:0x19],0xA4
    
    mov byte [gs:0x1A],'X' 
    mov byte [gs:0x1B],0xA4
    
    mov byte [gs:0x1C],'P' 
    mov byte [gs:0x1D],0xA4
    
    mov byte [gs:0x1E],'Y' 
    mov byte [gs:0x1F],0xA4
    
    jmp $

创建文件夹boot,将mbr.s  、mbr.bin、 ;loader.s放入boot文件夹:

在boot文件夹内进行编译生成二进制文件、将二进制文件写入虚拟硬盘:

nasm -I “你的include文件夹的路径” -o loader.bin loader.s
dd if=“你的loadee.bin文件所在的位置” of=“你的虚拟硬盘所在的位置” bs=512 count=1 seek=2 conv=notrunc

:因为在此之前,Loader程序我们设定好了从第2个扇区开始加载,所以相对于MBR所在的第0个扇区,我们要跳过2个扇区,因此 dd命令中,我们加入了参数“seek=2”

在bochs文件夹下运行仿真,选择选项6,输入c

bin/bochs -f bochsrc.disk ;启动bochs

成功运行结果如下所示:

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐