从0开始写内核(三)完善MBR

参考书籍:操作系统真相还原

源码:https://github.com/wutiaojian000/AFKernel.git
本文地址:https://www.cnblogs.com/angel-fish/p/18800110

0. 基础知识

ndisasm工具可用于查看反汇编。

0.1 jmp

书里没有仔细介绍jmp指令,所以我这里补充一下。
jmp指令为无条件跳转,可以只修改IP,也可以同时修改CS和IP。
(1)jmp short 标号
实现的是段内短转移,对IP的修改范围为-128~127,向前(低地址)转移最多128,向后(高地址)转移最多127。需要注意的是cpu在对jmp指令取指后,cs:ip会跳转到下一条指令的地址,当jmp译码完毕后这时候才会发生跳转,因此跳转的偏移一定是基于下一条指令的地址,这也是0.2节作者说jmp -2的原因。
(2)jmp short 标号
实现的是段内近转移。短转移是8位,近转移是16位,-32768~32767,其他的和短转移一样。

0.2 堆栈架构

这里举一个例子,

int a = 0;
function(int b, int c)
{
  int d;
}
a++;

函数调用处实参入栈顺序是从右往左,先push c,再push d;然后push esp保存返回地址;push ebp备份ebp;mov ebp, esp,将esp复制给ebp做为新的栈基址;最后esp自减为局部变量分配空间。被调函数堆栈空间如下,

函数返回时跳过局部变量的空间,mov esp, ebp;恢复ebp的值。
这里有两个比较重要的东西,返回值和返回地址。在c++里,我们都知道函数的返回值是个右值,是个临时变量,从汇编的角度来看就能很好理解了,举个例子

//main.c
int func()
{
  int ret = 0;
  return ret;
}

int main()
{
  int x = func();
  return 0;
}

这里稍微介绍一下怎么生成nasm语法的汇编文件,我们gcc的-S选项只能生成MASM语法的汇编文件,gcc不支持nasm,生成的时候需要绕一下,先生成main.c的机器码但不链接(当然这里也不需要链接),然后再用objconv(https://www.agner.org/optimize/)进行反汇编。

gcc -m32 -fno-asynchronous-unwind-tables -s -O0 -c -o main.o main.c
./objconv -fnasm main.o main.asm

汇编代码如下

; Disassembly of file: main.o
; Sat Mar 29 20:04:37 2025
; Type: ELF32
; Syntax: NASM
; Instruction set: 80386


global func: function
global main: function
global __x86.get_pc_thunk.ax: function

extern _GLOBAL_OFFSET_TABLE_                            ; byte


SECTION .text   align=1 exec                            ; section number 1, code

func:   ; Function begin
        endbr32                                         ; 0000 _ F3: 0F 1E. FB
        push    ebp                                     ; 0004 _ 55
        mov     ebp, esp                                ; 0005 _ 89. E5
        sub     esp, 16                                 ; 0007 _ 83. EC, 10
        call    __x86.get_pc_thunk.ax                   ; 000A _ E8, FFFFFFFC(rel)
        add     eax, _GLOBAL_OFFSET_TABLE_-$            ; 000F _ 05, 00000001(GOT r)
        mov     dword [ebp-4H], 0                       ; 0014 _ C7. 45, FC, 00000000
        mov     eax, dword [ebp-4H]                     ; 001B _ 8B. 45, FC
        leave                                           ; 001E _ C9
        ret                                             ; 001F _ C3
; func End of function

main:   ; Function begin
        endbr32                                         ; 0020 _ F3: 0F 1E. FB
        push    ebp                                     ; 0024 _ 55
        mov     ebp, esp                                ; 0025 _ 89. E5
        sub     esp, 16                                 ; 0027 _ 83. EC, 10
        call    __x86.get_pc_thunk.ax                   ; 002A _ E8, FFFFFFFC(rel)
        add     eax, _GLOBAL_OFFSET_TABLE_-$            ; 002F _ 05, 00000001(GOT r)
        call    func                                    ; 0034 _ E8, FFFFFFFC(rel)
        mov     dword [ebp-4H], eax                     ; 0039 _ 89. 45, FC
        mov     eax, 0                                  ; 003C _ B8, 00000000
        leave                                           ; 0041 _ C9
        ret                                             ; 0042 _ C3
; main End of function


SECTION .data   align=1 noexec                          ; section number 2, data


SECTION .bss    align=1 noexec                          ; section number 3, bss


SECTION .text.__x86.get_pc_thunk.ax align=1 exec        ; section number 4, code

__x86.get_pc_thunk.ax:; Function begin
        mov     eax, dword [esp]                        ; 0000 _ 8B. 04 24
        ret                                             ; 0003 _ C3
; __x86.get_pc_thunk.ax End of function


SECTION .note.gnu.property align=4 noexec               ; section number 5, const

        db 04H, 00H, 00H, 00H, 0CH, 00H, 00H, 00H       ; 0000 _ ........
        db 05H, 00H, 00H, 00H, 47H, 4EH, 55H, 00H       ; 0008 _ ....GNU.
        db 02H, 00H, 00H, 0C0H, 04H, 00H, 00H, 00H      ; 0010 _ ........
        db 03H, 00H, 00H, 00H                           ; 0018 _ ....


func在返回时将ret也就是[ebp-4H]传给了eax,在main函数就是通过eax接收的返回值。这时候我们可能会想到如果eax存不下返回值呢,那就存地址呗。假如说return的是一个局部变量,只需要用eax记下这个局部变量的地址就行。不过需要注意的是返回之后,被调用函数的堆栈结构已经被释放,对应位置的内容很快会被覆盖,还是符合一个临时变量的特点的。
至于返回地址,在函数调用时call会压入返回地址。这里有一个短调用和远调用的概念,如果被调函数在同一个段里,则CS不需要改,只需要压入IP,返回时使用ret;如果不在同一个段,需要跨段访问,就得压入CS和IP,放回时使用retf。

0.3 vstart

vstart为section提供一个虚拟的起始地址,即每个section中数据的地址以vstart开始,看一下下面的例子:

这里vstart定义为0x7c00,$$被替换为vstart的值,故$$以该节的虚拟起始地址为主,若没有定义vstart则以该节section.code.start为主;$被替换为vstart+0x09(类似于重定位);jmp $编译成jmp -2按0.1节的解释。

0.4 CPU的实模式

0.4.1 寄存器

无论是实模式还是保护模式,通用寄存器都有8个,分别是AX,BX,CX,DX,SI,DI,BP,SP。他们都是16位的,但由于保护模式是32位的,因此他们都扩展了高16位的寄存器,比如AX扩展成了EAX,实模式下虽然是16位,但依旧可以使用整32位寄存器。
这些寄存器有一些约定俗成的用法

寄存器 助记名称 功能描述
ax 累加器 用法很多
bx 基址寄存器 存储一个地址做为基址
cx 计数器 用于计数
dx 数据寄存器 一般只用于保存外设寄存器的端口号地址
si 源变址寄存器 常用于字符串操作的数据源地址
di 目的变址寄存器 常用于字符串操作的数据目的地址
sp 栈指针寄存器 段基址是SS,用来指向栈顶
bp 基址指针 bp的默认段寄存器也是SS,可以通过SS:bp访问栈基址

0.4.2 寻址

8086寻址方式
 (1)寄存器寻址 涉及到寄存器都是寄存器寻址
 (2)立即数寻址
 (3)内存寻址 仍然还是段基址:段内偏移地址的方式,ds为数据段基址
  a)直接寻址
   mov ax, [0x1234] 读取ds:0x1234地址的数据;
   mov ax, [fs:0x5678] 读取fs:0x5678地址的数据。
  b)基址寻址
   实模式下只能用bx,bp做为基址寄存器,保护模式没有这个限制;其中bx的默认段寄存器为ds,bp的默认段寄存器为SS。举一个ebp的例子:

  假如这是一个函数的堆栈结构,要想访问该函数的形参和局部变量,只需要 ss:ebp+对应偏移 即可。
 c)变址寻址
  一般用于基址变址寻址。
 d)基址变址寻址

  就是基址寻址的基础上多了一次变址。

0.4.3调用方式

 (1)16位实模式相对近调用
  近就是指不跨段,不用修改CS;相对是指只需要提供相对于当前指令的偏移;由于操作数只是目标函数的相对增量,需要把这个增量还原为绝对地址,所以不能成为直接。一般形式为call near 立即数地址。立即数地址可以是立即数,函数,标号。

call near near_proc
jmp $
addr dd 4
near_proc:
  mov ax, 0x1234
  ret


其中e80600代表相对近调用,ebfe是jmp -2,04000000是变量,b83412是mov ax, 0x1234,c3是ret。相对近调用的偏移为目标函数地址-调用处的地址-调用指令的长度=6,故e8这里是0x0006。

 (2)16位实模式间接绝对近调用
  注意一下间接对应直接,指地址是否直接给出;绝对对应相对,指地址是不是偏移;近对应远,指需不需要跨段。
16位实模式间接绝对近调用的一般形式为call 寄存器寻址,call 内存寻址。

section call_test vstart=0x900
mov word [addr], near_proc
call [addr]
mov ax, near_proc
call ax
jmp $
addr dd 4
near_proc:
  mov ax, 0x1234
  ret


c70611091509是第一条mov指令;ff161109是call,可以看到是在内存0x911处取得目标函数地址;b83412是mov指令;c3 ret;b81509是mov指令;ffd0操作码是ff,操作数是d0,指ax寄存器;后面是函数体。

(3)16位实模式直接绝对远调用
一般形式为call far 段基址(立即数):段内偏移基址(立即数)。
也可以不用加far。

section call_test vstart=0x900
call 0:far_proc
jmp $
far_proc:
  mov ax, 0x1234
  retf

返回时需要用到retf。

call(0x9a)的参数为0x0000:0x0907。

(4)16位实模式间接绝对远调用
只支持内存寻址。读取内存的前两个字节是段内偏移,后两个字节是段基址。

section call_test vstart=0x900
call far [addr]
jmp $
addr dw far_proc, 0
far_proc:
  mov ax, 0x1234
  retf


call(0xff1e)的参数是ds:0x0906。

0.4.4 实模式下的jmp

一共有5中转移方式。
 (1)16位实模式相对短转移
jmp $,jmp short 标号,short可以省略,但省略后不一定还会将其编译为短转移的形式,相对短转移操作码为0xeb,操作数1字节,短转移的短就是这个意思,范围为-128`127。计算方法和上面将call时的一样。

section jmp_test vstart=0x900
jmp short start
times 127 db 0
start:
  mov ax, 0x1234
  jmp $


jmp指令为0xeb7f,jmp指令和start之间是127字节数据,所以偏移就是0x7f。
假如超出范围,比如中间有128字节数据,就会报错。

 (2)16位实模式相对近转移
相比与short,near的范围扩大到两个字节,-32768`32767,由于该指令长度为3字节,所以后面减去指令长度时减去3。

section jmp_test vstart=0x900
jmp near start
times 128 db 0
start:
  mov ax, 0x1234
  jmp $


 (3)16位实模式间接绝对近转移
指令格式为jmp near 寄存器或内存。

section jmp_in_abs_near vstart=0x900
mov ax, start
jmp near ax
times 128 db 0
start:
  mov ax, 0x1234
  jmp $

编译时会出现报错,jmp_near_abs_ind.S:3: warning: register size specification ignored,可以推断出来near可能是强制类型转换。

jmp指令操作数为0x0985,为jmp指令地址+128+5。

section jmp_in_abs_near vstart=0x900
mov word [addr], start
jmp near [addr]
times 128 db 0
addr dw 0
start:
  mov ax, 0x1234
  jmp $


jmp指令为ff26 8a09,操作数为0x098a,从ds:0x098a位置取绝对地址。

 (4)16位实模式直接绝对远转移

section jmp_in_abs_near vstart=0x900
jmp 0:start
times 128 db 0
start:
  mov ax, 0x1234
  jmp $


操作码是0xea,操作数为0x00000985。

 (5)16位实模式间接绝对远转移

section jmp_in_abs_near vstart=0x900
jmp far [addr]
times 128 db 0
addr dw start, 0
start:
  mov ax, 0x1234
  jmp $


jmp操作码是0xff2e,操作码是0x0984,在ds:0x0984出取得绝对地址。

0.4.5 标志寄存器flags

上面讲的都是无条件转移,有条件转移则需要借助标志寄存器flags。实模式下标志寄存器是16位,保护模式下变成32位eflags。

第0位是carry flag进位标志,用于检测运算时最高位有无进位或借位,不管是进位还是借位,CF位都会置1。可以用来判断运算是否有溢出。
第1、3、5、15没用。
第2位为PF位,parity flag,奇偶位,标记结果中低8位1的个数,偶数为1奇数为0。
第4位是AF位,Auxiliary carry flag,辅助进位标志,用于计算低4位进借位的情况,有进借位为1,否则为0.
第6位为ZF位,zero flag,零标志位,计算结果为0置1。
第7位为SF位,sign flag,符号标志位,计算结果为负置1。
第8位为TF位,trap flag,陷阱标志位,置1表示让CPU进入单步运行,为0是连续运行。
第9位是IF位,interrupt flag,中断标志位,置1表示中断开启,CPU可以响应外部可屏蔽中断;为0就不会再响应了。CPU内部的异常是管不了的。
第10位是DF位,direction flag,方向标志位,用于字符串操作指令,为1时指令中操作数地址会自动减少一个单位,为0时会增加一个单位。
第11位是OF位,overflow flag,标记运算结果是否超出该类型的范围,为1是有溢出,为0是没有。
以下标志位只在80286以上的CPU中有效。
12`13位为IOPL,input output privilege level,这个后面在说。
14位是NT,nest task任务嵌套标志位,一个任务嵌套调用另一个任务时置1。
以下标志位只在80386以上的CPU中有效。
16位是RF位,resume flag,恢复标志位。表示是否接受调试故障,要和调试寄存器一起使用,1表示忽略调试故障。
17位是VM位,virtual 8086 model,虚拟8086模式,用于兼容实模式下的用户程序,置1时保护模式下可以运行实模式下的程序。
以下标志位只在80486以上的CPU中有效。
第18位是AC位,alignment check,对齐检查。为1代表进行地址对齐检查。
以下标志位只在80586以上的CPU中有效。
第19位为VIF位,virtual interrupt flag,虚拟中断标志位,虚拟模式下的中断标志。
第20位为VIP位,virtual interrupt pending,虚拟中断挂起标志位,多任务情况下为操作系统提供虚拟中断挂起信息,需要和VIF配合。
第21位为ID位,identification,识别标志位。1表示当前CPU支持CPU id指令,可以获取CPU信号、厂商等。
剩下的22-31位没有实际用途。
(1)有条件转移

1. 开始搞显卡喽

1.1 io接口

IO接口是连接CPU与外部是被的逻辑控制部件,用来解决CPU与外设之间的种种不匹配,如速度、格式等。IO接口按其芯片是否可编程又分为可编程接口芯片和不可编程接口芯片。我们希望IO接口功能越多越好,可以设置多种工作模式,需要通过软件指令来配置它的功能和工作模式,这叫"IO接口控制编程"。
IO接口有以下功能:
(1)设置数据缓冲;
(2)设置信号电平转换电路;
(3)设置数据格式转换;
(4)设置时序控制电路来同步CPU和外部设备;
(5)提供地址译码。
南桥芯片:同一时刻CPU只能和一个IO接口通信,由ICH(I/O control hub)仲裁IO接口的竞争。用于连接pci,pci-express,AGP等低速设备。
北桥芯片:用于连接高速设备,如内存。

其中的pci接口是专门用于扩展的接口。
IO接口中用于与CPU进行数据交互的寄存器称为端口,可以通过内存映射来访问这些端口(统一编址):也可以把端口独立编址,把所有端口从0开始编号,位于一个IO接口上的端口号都是连续的。
CPU提供了in/out指令专门用来访问端口。
in指令用来从端口读取数据,一般形式:
 (1)in al,dx
 (2)in ax,dx
al和ax用来存储从端口中读到的数据,由端口的宽度决定是al还是ax。
out指令用来向端口写数据,一般形式:
 (1)out dx,al
 (2)out dx,ax
 (3)out 立即数,al
 (4)out 立即数,ax

1.2 显卡概述

显存地址分布(在第2章的表中也有)

先介绍文本模式,文本模式下有8025,4025,8043等模式,代表函数列数,一屏可以容纳的字符的个数。每个字符的低字节代表字符的ASCII码,高字节代表字符的属性,低4位是字符的前景色,高4位是字符的背景色,第4位用来控制亮度,第7位用来控制字符是否闪烁。

I是亮度位,第4位。

1.2.1 改进MBR

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

  mov ax, 0x0600
  mov bx, 0x0700
  mov cx, 0
  mov dx, 0x184f

  int 0x10

  mov byte [gs:0x00], '1'
  mov byte [gs:0x01], 0xa4

  mov byte [gs:0x02], ' '
  mov byte [gs:0x03], 0xa4

  mov byte [gs:0x04], 'M'
  mov byte [gs:0x05], 0xa4

  mov byte [gs:0x06], 'B'
  mov byte [gs:0x07], 0xa4

  mov byte [gs:0x08], 'R'
  mov byte [gs:0x09], 0xa4

  jmp $

  times 510-($-$$) db 0
  db 0x55, 0xaa


铺垫这么多,上手就是一会儿的事情。

2. bochs调试方法

bochs的硬件调试:
(1)调试时可以查看页表,gdt,idt等结构;
(2)可以查看栈中的数据;
(3)可以反汇编任意内存;
(4)实模式、保护模式相互变换时提醒;
(5)中断发生时提醒。

help+命令,如help x,help xp,x用于查看线性地址的内存,xp接physical物理地址。bochs的字为4字节。

xp/nuf nuf为一个序列,n指定显示单元数,u指定显示单元大小,f指定用那种进制显示。
bochs的输出中

说明了BIOS入口地址,cs:ip=0xf000:0xfff0,该地址上的指令为跳转指令,跳转到0xf000:0xe05b。

按小端序来,0xea 0xe05b,可以用u指令将内存数据反汇编成指令。

大概介绍一下常用的调试指令:

(1)debugger control

q|quit|exit:退出
set:指令族,常用的是设置寄存器的值
show:指令族,常用的有show mode-每次CPU切换模式就提示;show int-每次中断时就有提示,包括softint,extint,iret,可以单独显示某类中断如show softint;show call-每次有函数调用时就有提示
u|disasm [/num] [start] [end],不指定地址默认EIP指向的地址,num是反汇编的指令数
setsize = 16|32|64,使用反汇编时,告诉调试器段的大小
ctrl+c中断执行返回控制台

(2)execution control

c|cont|continue,继续执行
s|step [count],执行count条指令,默认为1
p|n|next,单步,函数调用不跳入

(3)breakpoint management

以地址打断点:
vb|vbreak [seg:off],以虚拟地址添加断点
lb|lbreak [addr],以线性地址添加断点
pb|pbreak|b|break [addr],以物理地址添加断点
以指令数打断点:
sb [delta],delta条指令后中断
sba [time],CPU从运行开始,第0条指令数,执行time条指令时中断
以读写IO打断点:
watch r|read [phy_addr],读断点
watch w|write [phy_addr],写断点
watch,显示所有读写断点
unwatch,清除所有断点
unwatch [phy_addr],清除该地址上所有断点
blist,显示所有断点信息
bpd|bpe [n],禁用断点/启用断点,n是断点号,可以用blist查出来
d|del|delete [n],删除断点n

(4)CPU and memory contents类

x/nuf [line_addr]显示线性地址内容
xp/nuf [phy_addr]显示线性地址内容
setpmem [phy_addr] [size] [val]设置以物理内存phy_addr为起始,size个字节为val,size<=4
r|reg|regs|register,显示8个通用寄存器+eflags寄存器+eip寄存器的值
ptime,显示总执行指令数
print-stack [num],显示堆栈,num默认16,输出栈栈顶在上,栈底在下
?|calc内置计算器
info,指令族
info pb|pbreak|b|break,查看断点信息,等同于blist
info CPU,显示CPU所有寄存器的值,包括不可见寄存器
info fpu,显示fpu的状态
info idt,显示中断向量表,保护模式下的
info gdt [num],显示gdt第num项描述符
info ldt,显示局部描述符表
info tss,显示任务状态段
info ivt [num],实模式下的
info flags|eflags,显示状态寄存器
sreg,显示所有段寄存器
dreg,显示所有调试寄存器的值
creg,显示所有控制寄存器的值
info tab,显示页表中线性地址到物理地址的映射
page line_addr,显示线性地址到物理地址的映射

后面给了一个保护模式下的调试实例,建议直接看书

3. 硬盘介绍

机械磁盘的原理网上有很多的介绍了,这里不展开。
对于PATA接口,主板上一般有primary通道和secondary通道两个接口,每个接口都能连接一个主盘一个从盘,一共连接4个设备。

3.1 硬盘寄存器端口

硬盘寄存器端口是位于硬盘控制器上的寄存器。
之后会用到的端口寄存器

control block register用于控制硬盘工作状态,command block register用于项硬盘驱动器写入命令字或获取硬盘状态。后面主要用command block register。需要注意的是端口是按通道的,一个通道上的主从硬盘共用一组端口。其中的device寄存器的第4位指定主从硬盘,0为主盘,1为从盘。
data寄存器是读写硬盘的数据缓冲区,16位宽度。
error寄存器是在读取硬盘失败时使用,未读取的扇区数存放在sector count寄存器中;feature寄存器是在写操作硬盘是使用的。它俩是一个寄存器,8位宽度。
sector count寄存器用于指定待读取或待写入的扇区数,0代表操作256个扇区。
CHS:柱面-磁头-扇区,用于定位一个扇区。还有以后一种方法是LBA。
LBA:逻辑块地址,将磁盘中的扇区从0开始编号。有LBA28,用28位表示扇区地址,最大支持128GB;LBA48,用48位表示扇区地址,最大支持128PB。我们这里用LBA28,用LBA low(0-7)、LBA mid(8-15)、LBA high(16-23)寄存器保存LBA地址的低24位,device寄存器的低4位保存LBA地址的24-27位。device的第4位指定通道上的主盘或从盘,第6位为1代表启用LBA,0代表启用CHS。另外两位固定为1。
读硬盘时,status寄存器用于给出硬盘的状态,8位。第0位是ERR,1代表命令出错,具体原因需要看error寄存器;第2位是data request,1代表硬盘已经准备好数据,主机可以把数据读出来;第6位是DRDY,代表硬盘就绪,用于检测硬盘正常;第7位是BSY,1代表硬盘正忙。写硬盘时,status寄存器变为command寄存器,用于存储让硬盘执行的指令:
identify:0xEC,硬盘识别;
read sector:0x20,读扇区;
write sector:0x30,写扇区。

操作顺序:
(1)选择通道,往该通道的sector count寄存器中写入待操作的扇区数;
(2)写入三个LBA寄存器;
(3)device寄存器低4位设置为LBA高4位地址,第6位设置为1LBA模式,第4位选择操作的硬盘;
(4)command寄存器写入操作指令;
(5)读取status寄存器,查看操作是否完成;
(6)如果以上操作时读硬盘,则进入下一个操作,否则结束;
(7)将硬盘数据读出。
硬盘数据准备好后获取数据的方式:
(1)无条件传送,该方法下数据源设备一定是随时准备好了数据,比如寄存器和内存;
(2)查询传送方式,也称程序I/O、PIO,传输数据之前需要检测设备状态,比如硬盘需要检测status寄存器;
(3)中断传送方式,中断驱动I/O,依靠中断不需要查询;
(4)DMA,不依靠CPU直接传到内存;
(5)I/O处理机传送,用一种专门处理I/O的处理机来传送。
我们用第2、3种。

3.2 在MBR中使用硬盘

注意我们创建的硬盘是primary通道,ata0-master说明是主盘

%include "boot.inc"
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
 
  mov ax, 0x0600
  mov bx, 0x0700
  mov cx, 0
  mov dx, 0x184f
 
  int 0x10
 
  mov byte [gs:0x00], '1'
  mov byte [gs:0x01], 0xa4
 
  mov byte [gs:0x02], ' '
  mov byte [gs:0x03], 0xa4
 
  mov byte [gs:0x04], 'M'
  mov byte [gs:0x05], 0xa4
 
  mov byte [gs:0x06], 'B'
  mov byte [gs:0x07], 0xa4
 
  mov byte [gs:0x08], 'R'
  mov byte [gs:0x09], 0xa4
 
  mov eax, LOADER_START_SECTOR  ;起始扇区1ba地址
  mov bx, LOADER_BASE_ADDR      ;写入的地址
  mov cx, 1                     ;待读入的扇区数
  call rd_disk__m_16            ;读取程序的起始部分
  
  jmp LOADER_BASE_ADDR

;功能读取硬盘的n个扇区
rd_disk__m_16:
  mov esi, eax    ;备份eax
  mov di, cx      ;备份cx
;读写硬盘
;设置读取的扇区数
  mov dx, 0x1f2
  mov al, cl
  out dx, al      ;读取的扇区数

  mov eax, esi    ;恢复eax

;存入LBA地址
  ;写入端口0x1f3
  mov dx, 0x1f3
  out dx, al

  ;写入端口0x1f4
  mov cl, 8
  shr eax, cl
  mov dx, 0x1f4
  out dx, al

  ;写入端口0x1f5
  shr eax, cl
  mov dx, 0x1f5
  out dx, al

  ;写入端口0x1f6
  shr eax, cl
  and al, 0x0f
  or al, 0xe0    ;7-4位为1110
  mov dx, 0x1f6
  out dx, al

  ;写入端口0x1f7读命令
  mov dx, 0x1f7
  mov al, 0x20
  out dx, al

  ;检测硬盘状态
.not_ready:
  nop
  in al, dx
  and al, 0x88    ;第4位为1代表准备好传输
  cmp al, 0x08
  jnz .not_ready

  ;从0x1f0读取数据
  mov ax, di      ;待读取的扇区数
  mov dx, 256
  mul dx          ;mul的被乘数放ax里,结果也放ax里,这里因为是按字读的,16位,所以是读256次
  mov cx, ax

  mov dx, 0x1f0
.go_on_read:
  in ax, dx
  mov [bx], ax
  add bx, 2
  loop .go_on_read  ;用cx来计数
  ret               ;返回到函数调用处
  
  times 510-($-$$) db 0
  db 0x55, 0xaa

还有boot.inc文件

LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2

编译

nasm -I include/ -o mbr.bin mbr.S

写入硬盘

dd if=mbr.bin of=/home/zcm/bochs/hd60M.img bs=512 count=1 conv=notrunc

现在还没有loader,所以先不执行。

4.实现内核加载器

一个简单的内核加载器

%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
  mov byte [gs:0x00], '2'
  mov byte [gs:0x01], 0xa4

  mov byte [gs:0x02], ' '
  mov byte [gs:0x03], 0xa4

  mov byte [gs:0x04], 'L'
  mov byte [gs:0x05], 0xa4

  mov byte [gs:0x06], 'O'
  mov byte [gs:0x07], 0xa4

  mov byte [gs:0x08], 'A'
  mov byte [gs:0x09], 0xa4

  mov byte [gs:0x0a], 'D'
  mov byte [gs:0x0b], 0xa4

  mov byte [gs:0x0c], 'E'
  mov byte [gs:0x0d], 0xa4

  mov byte [gs:0x0e], 'R'
  mov byte [gs:0x0f], 0xa4

  jmp $
nasm -I include/ -o loader.bin loader.S
dd if=loader.bin of=/home/zcm/bochs/hd60M.img bs=512 count=1 seek=2 conv=notrunc


成功!

posted @ 2025-04-26 17:45  横渡大海的神仙鱼  阅读(64)  评论(0)    收藏  举报