64位操作系统——bootLoader

64位操作系统——bootLoader


作者:王赛宇

参考列表:

  • 《一个64位操作系统的设计与实现》
  • 《nasm用户手册》
  • 各种博客

第一部分: 写一个简单的引导程序并且显示一些字符

    org 0x7c00 ; 将程序加载到0x7c00位置,即:指定程序的起始地址

BaseOfStack equ 0x7c00


; 将CS寄存器的段基址设置到DS、ES、SS中
Label_Start:

    mov ax, cs
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, BaseOfStack

; ======  清空屏幕

    mov ax, 0600h
    mov bx, 0700h
    mov cx, 0
    mov dx, 184fh
    int 10h

; ===== set focus ??? 没看懂这是啥意思

    mov ax, 0200h
    mov bx, 0000h
    mov dx, 0000h
    int 10h

; ===== 在屏幕上显示:Start Booting......

    mov ax, 1301h
    mov bx, 000fh
    mov dx, 0000h
    mov cx, 10
    push ax
    mov ax, ds
    mov es, ax
    pop ax
    mov bp, StartBootMessage
    int 10h

; ===== 软盘驱动器复位

    xor ah, ah
    xor dl, dl
    int 13h

    jmp $


StartBootMessage:	db	"Start Boot"

;=======	fill zero until whole sector

	times	510 - ($ - $$)	db	0
	dw	0xaa55

这个程序可以被分为几个部分:

  • 初始化部分: 定义了程序被加载到的地方,以及程序中的常量,并且对所有寄存器进行了简单的初始化
  • 显示部分: 通过int 10h的指令,进行了显示方面的一系列处理
  • 软盘驱动器复位:这里我暂时也没搞明白是在干什么
  • 填充部分

简单的汇编知识

在搞明白这些之前,我们需要先学一点简单的汇编:

我们会发现,上面的程序主要是在对一些寄存器进行简单的操作,这些寄存器是:ax,bx,cx,dx他们是nasm语言的通用寄存器。同时,这些寄存器可以根据高位、低位被划分成:ah,al,bh,bl,ch,cl,dh,dl,可以理解为:ax = ah << 16 + al也就是说ax是由ah\al两个部分拼接而成的。

理解了这一点之后,你往下看,暂时就没有太大的难度了。

int 指令

我们首先来看一下这里的int指令,学过组成原理的同学都知道,这里的int就是软件中断,我们这里用到了两种int指令,他们实际上就是调用了BIOS里面的一些固有程序,进行了一些我们期望中的处理,我们来看一下这些int分别做了哪些操作:

第一次
; ======  清空屏幕
mov ax, 0600h
mov bx, 0700h
mov cx, 0
mov dx, 184fh
int 10h

这里设置了ax = 0600h,bx=0700h,cx=0,dx=184fh,然后调用了int 10h,INT 10h主要执行的是一些与显示相关的操作。我们来看一下这里的操作:

首先来看ax寄存器:我们经过划分可以知道:ah = 0x06al = 0x00,这里的ah指示得是执行INT 10h相关程序中的哪种操作:当ah = 0x06时,执行的是按指定范围滚动窗口的功能

具体的参数如下:

  • AL=滚动的列数,若为0则实现清空屏幕功能;
  • BH=滚动后空出位置放入的属性;
  • CH=滚动范围的左上角坐标列号;
  • CL=滚动范围的左上角坐标行号;
  • DH=滚动范围的右下角坐标列号;
  • DL=滚动范围的右下角坐标行号;
  • BH=颜色属性。
  • bit 0~2:字体颜色(0:黑,1:蓝,2:绿,3:青,4:红,5:紫,6:综,7:白)。
  • bit 3:字体亮度(0:字体正常,1:字体高亮度)。
  • bit 4~6:背景颜色(0:黑,1:蓝,2:绿,3:青,4:红,5:紫,6:综,7:白)。
  • bit 7:字体闪烁(0:不闪烁,1:字体闪烁)。

这里我们将AL设置为了0, 所以实现的是清空屏幕的功能。

第二次
; ===== set focus ??? 没看懂这是啥意思
    mov ax, 0200h
    mov bx, 0000h
    mov dx, 0000h
    int 10h

这里作者给出的代码中写的是set focus,我们还是来看一下ah的值:ah = 0x02ah = 0x02INT 10h执行屏幕光标位置的设置功能

  • DH=游标的列数;
  • DL=游标的行数;
  • BH=页码。
第三次
; ===== 在屏幕上显示:Start Booting......

    mov ax, 1301h
    mov bx, 000fh
    mov dx, 0000h
    mov cx, 10
    push ax
    mov ax, ds
    mov es, ax
    pop ax
    mov bp, StartBootMessage
    int 10h

相信大家已经掌握了这套研究的方法了,我们直入主题: 这里执行的是显示字符串的方法,字符串存储在StartBootMessage位置,参数如下:

  • AL=写入模式。
    • AL=00h:字符串的属性由BL寄存器提供,而CX寄存器提供字符串长度(以B为单位),显示后光标位置不变,即显示前的光标位置。
    • AL=01h:同AL=00h,但光标会移动至字符串尾端位置。
    • AL=02h:字符串属性由每个字符后面紧跟的字节提供,故CX寄存器提供的字符串长度改成以Word为单位,显示后光标位置不变。
    • AL=03h:同AL=02h,但光标会移动至字符串尾端位置。
  • CX=字符串的长度。
  • DH=游标的坐标行号。
  • DL=游标的坐标列号。
  • ES:BP=>要显示字符串的内存地址。
  • BH=页码。
  • BL=字符属性/颜色属性。
    • bit 0~2:字体颜色(0:黑,1:蓝,2:绿,3:青,4:红,5:紫,6:综,7:白)。
    • bit 3 :字体亮度(0:字体正常,1:字体高亮度)。
    • bit 4~6:背景颜色(0:黑,1:蓝,2:绿,3:青,4:红,5:紫,6:综,7:白)。
    • bit 7:字体闪烁(0:不闪烁,1:字体闪烁)

我们注意到这里执行的是光标移动到字符串尾端位置的写入模式,通过规定cx,规定了字符串的长度。我们尝试加长字符串的长度,但不更改cx会发生什么样的事情:我们将下面的StartBootMessage部分更改为:

StartBootMessage:	db	"01234567891011121314151617181920"

随后进行查看,结果如下:

我们对cx进行修改后即可显示完全:

    mov cx, 0x20
image-20200930162323357

其他值得一提的

$$$

在nasm语言中,$代表当前行的地址,$$代表本段程序起始地址。那么我们来看:

;=======	fill zero until whole sector

	times	510 - ($ - $$)	db	0
	dw	0xaa55

这里的($ - $$)就是当前行地址减掉程序开始的地址也就是当前程序占用的空间,我们的第一个扇区大小必须是512Byte,并且必须以0xaa55作为结尾,所以我们使用0来填充剩下的部分。

org 0x7c00

这里的意思,我们在注释里已经描述过了:

    org 0x7c00 ; 将程序加载到0x7c00位置,即:指定程序的起始地址

相信大家看到这里都会想,为什么要加载到0x7c00呢?实际上,不是我们不想加载到更前面,而是因为更前面已经被BIOS的标准例程给占上了,所以我们只能使用可以使用的最前面的空间了。

第二部分:加载Loader到内存

先导知识:FAT12文件系统

为了使操作简单一些,作者在这里使用的是FAT12文件系统,我们这里主要来了解一下FAT12文件系统。

引导扇区

首先来看引导扇区的结构:

名称偏移长度内容本系统引导程序数据
BS_jmpBoot03跳转指令jmp short Label_Start nop
BS_OEMName38生产厂商名‘MINEboot’
BPB_BytesPerSec112每扇区字节数512
BPB_SecPerClus131每簇扇区数1
BPB_RsvdSecCnt142保留扇区数1
BPB_NumFATs161FAT表的份数2
BPB_RootEntCnt172根目录可容纳的目录项数224
BPB_TotSec16192总扇区数2880
BPB_Media211介质描述符0xF0
BPB_FATSz16222每FAT扇区数9
BPB_SecPerTrk242每磁道扇区数18
BPB_NumHeads262磁头数2
BPB_HiddSec284隐藏扇区数0
BPB_TotSec32324如果BPB_TotSec16值为0,则由这个值记录扇区数0
BS_DrvNum361int 13h的驱动器号0
BS_Reserved1371未使用0
BS_BootSig381扩展引导标记(29h)0x29
BS_VolID394卷序列号0
BS_VolLab4311卷标‘boot loader’
BS_FileSysType548文件系统类型‘FAT12’
引导代码62448引导代码、数据及其他信息
结束标志5102结束标志0xAA550xAA55

下图所展示的就是FAT12格式组织的软盘的文件系统分配图:

FAT表

FAT12用簇作为基本单位来分配数据区的存储空间。我们来回顾一下上面所说到的几个变量:

  • BPB_SecPerClus: 每个扇区的字节数
  • BPB_SecPerClus:每个簇的扇区数

那么我们也可以知道:簇中字节数 = BPB_SecPerClus * BPB_SecPerClus,数据区的簇号与FAT表的表项是一一对应的关系,因此文件在FAT系统总的存储单位是簇。即使文件的大小不足一个簇, 也会为该文件分配一个簇的空间进行存储。这种存储方式与操作系统中的页表非常相似,它的意义在于将磁盘存储空间按照固定存储片有效管理,从而可以按照文件偏移,分片段的访问文件内的数据,不必一次将文件内的数据全部取出。

下面我们来看FAT表的存储结构:

FAT项实例值描述
0FF0h磁盘标示字,低字节与BPB_Media数值保持一致
1FFFh第一个簇已经被占用
2003h000h:可用簇
3004h002h~FEFh:已用簇,标识下一个簇的簇号
…………FF0h~FF6h:保留簇
NFFFhFF7h:坏簇
N+1000hFF8h~FFFh:文件的最后一个簇
…………

可以看到,FAT表的存储结构与单向链表的存储方式非常相似。

根目录区和数据区

根目录区和数据区是两个不同的分区,他们都保存着文件相关的数据,但是根目录区只能保存目录项信息,而数据区不仅保存目录项信息还保存文件内的数据。这里的目录项是一个32B的结构体,记录着名字、长度、数据起始簇号等信息。对于树状的目录结构而言,树的层级结构是由目录项结构建立,从根目录开始经过目录项的逐层嵌套渐渐形成了 树的结构(非常像TreeNode)。

名称偏移长度描述
DIR_Name0x0011文件名8 B,扩展名3 B
DIR_Attr0x0B1文件属性
保留0x0C10保留位
DIR_WrtTime0x162最后一次写入时间
DIR_WrtDate0x182最后一次写入日期
DIR_FstClus0x1A2起始簇号
DIR_FileSize0x1C4文件大小

开始书写引导区

变量约束

首先在头部添加代码

    org 0x7c00 ; 将程序加载到0x7c00位置,即:指定程序的起始地址

BaseOfStack             equ 0x7c00

BaseOfLoader            equ 0x1000
OffsetOfLoader          equ 0x00

RootDirSectors          equ 14
SectorNumOfRootDirStart equ 19
SectorNumOfFAT1Start    equ 1
SectorBalance           equ 17

    jmp short Label_Start ; 这里是BS_jmpBoot 
    ; 实现的是段内转移,如果转移范围超过128,那么就会出错
    nop
    BS_OEMName          db  'WSYuboot'  ; 表示的是生产商的名字,我不要脸一点直接写自己名字了
    BPB_BytesPerSec     dw  512         ; 每个扇区的字节数
    BPB_SecPerClus      db  1           ; 每个簇的扇区数
    BPB_RsvdSecCnt	    dw	1           ; 保留扇区数
	BPB_NumFATs	        db	2           ; FAT表的数量
	BPB_RootEntCnt	    dw	224         ; 根目录可容纳的目录项数
	BPB_TotSec16	    dw	2880        ; 总扇区数
	BPB_Media	        db	0xf0        ; 介质描述符
	BPB_FATSz16	        dw	9           ; 每FAT扇区数
	BPB_SecPerTrk	    dw	18          ; 每磁道扇区数
	BPB_NumHeads	    dw	2           ; 磁头数
	BPB_HiddSec	        dd	0           ; 隐藏扇区数
	BPB_TotSec32	    dd	0           
	BS_DrvNum	        db	0           ; int 13h 的驱动器号
	BS_Reserved1	    db	0           ; 未使用
	BS_BootSig	        db	0x29        ; 扩展引导标记
	BS_VolID	        dd	0           ; 卷序列号
	BS_VolLab	        db	'boot loader' ; 卷标
	BS_FileSysType	    db	'FAT12   '  ; 文件系统类型

这里各部分的信息,我们在注释里面已经写得非常详细了,其实这些就对应这上面的FAT12文件系统引导扇区的部分。同时,我们在上面进行了一些宏定义,宏定义的内容包括:BaseOfLoader + OffsetOfLoader这两个分别代表Loader的起始物理地址和偏移量,共同构成了Loader的起始物理地址,构成方法如下:
B a s e O f L o a d e r < < 4 + O f f s e t O f L o a d e r = 0 × 10000 BaseOfLoader << 4 + OffsetOfLoader = 0 \times 10000 BaseOfLoader<<4+OffsetOfLoader=0×10000
RootDirSectors定义的是根目录占用的扇区数,这个值是通过计算得到的:
KaTeX parse error: No such environment: align at position 8: \begin{̲a̲l̲i̲g̲n̲}̲ (BPB\_RootEntC…
实质上就是:根目录容纳的目录项数*32(每个项目的大小)除以每个扇区的字节数,这样就可以得到占用的扇区数,后面加上了扇区的字节数 - 1,是为了对结果做上取整的。

SectorNumOfRootDirStart记录的是根目录起始的扇区号,他也是通过计算而得的,他的计算方法如下:
保 留 扇 区 数 + F A T 表 扇 区 数 ∗ F A T 表 份 数 保留扇区数 +FAT表扇区数 * FAT表份数 +FATFAT
SectorNumOfFAT1Start记录的是起始扇区号,在FAT1表前面只有一个保留扇区,而且它的扇区编号是0,那么FAT1标的起始扇区号是1.

SectorBalance用于平衡文件的起始簇号与数据区起始簇号的差值。由于数据区对应的有效簇号为2,为了正确计算出FAT表项对应的数据起始扇区号,则必须将FAT表项值减2,或者将数据区的起始簇号减2。这里用的是一种比较取巧的方法,就是将根目录区的起始扇区号减2,从而间接地把数据区的起始扇区号减2.

软盘读取
;======  从软盘中读取一个扇区

Func_ReadOneSector:

    push    bp 
    mov bp, sp
    sub esp, 2
    mov byte    [bp - 2]. cl
    div	bl
	inc	ah
	mov	cl,	ah
	mov	dh,	al
	shr	al,	1
	mov	ch,	al
	and	dh,	1
	pop	bx
	mov	dl,	[BS_DrvNum]
Label_Go_On_Reading:
	mov	ah,	2
	mov	al,	byte	[bp - 2]
	int	13h
	jc	Label_Go_On_Reading
	add	esp,	2
	pop	bp
	ret
前导知识

首先我们需要了解的是:bp,sp,ss,esp四个寄存器的作用:

  • SS:存放栈的段地址;

  • SP:堆栈寄存器SP(stack pointer)存放栈的偏移地址;

    SS + SP就可以得到该堆栈栈顶元素的地址

  • BP: 基数指针寄存器BP(base pointer)是一个寄存器,它的用途有点特殊,是和堆栈指针SP联合使用的,作为SP校准使用的,只有在寻找堆栈里的数据和使用个别的寻址方式时候才能用到

  • SP:为栈指针,用于指向栈的栈顶(下一个压入栈的活动记录的顶部)

这里的BP、SP都是16位的,如果需要三十二位的操作,那么就使用EBP以及ESP即可,E即extend。

在上面的代码片段中,作者实际上是写了一个函数,作为一个函数,除了函数的主体部分外,他需要进行如下操作(下面内容来自nasm官方文档):

  • Step1:调用者按照相反的顺序(从右向左),一个接一个的将函数的参数压入栈
  • Step2:调用者执行CALL指令,将控制权传递给被调用者
  • Step3:被调用方获得控制权,并且通常从将ESP的值保存在EBP中开始,以便能够将EBP作为基本指针来查找其堆栈上的参数。但是,调用方也可能也在执行此操作,因此调用约定的一部分规定,任何C函数都必须保留EBP。 因此,如果被调用方要将EBP设置为帧指针,则必须先推送前一个值。(如果觉得太抽象了也没关系,一会结合作者的代码理解一些就可以了)
  • Step4:被呼叫者然后可以访问其相对于EBP的参数。 [EBP]中的双字保留了被推入时EBP的先前值; 下一个双字[EBP + 4]保留返回地址,
    由CALL隐式推送。 此后,参数从[EBP + 8]开始。 该函数的最左侧参数(自上次推送起)可以在距EBP的此偏移量处访问; 其他的则以相继更大的偏移量跟随。 因此,在诸如printf之类的函数中,该函数需要可变数量的参数,以相反顺序推入参数意味着该函数知道在哪里可以找到其第一个参数,从而告诉它其余参数的数量和类型。
  • Step5:被呼叫者如果希望将值返回给呼叫者,则应根据值的大小将其保留在AL,AX或EAX中。 浮点结果通常在ST0中返回。
  • Step6:被调用方完成处理后,如果已分配了本地堆栈空间,则从EBP中还原ESP,然后弹出先前的EBP值,并通过RET(等效于RETN)返回。
  • Step7:调用方从被调用方重新获得控制权时,功能参数仍在堆栈上,因此通常会向ESP添加立即数以将其删除(而不是执行许多慢速POP指令)。 因此,如果由于原型不匹配而意外地用错误数量的参数调用了函数,则堆栈将仍返回到明智的状态,这是因为知道调用了多少参数的调用者执行了删除操作。

除此之外,我们还需要熟悉一下除法指令,除法指令:div 寄存器,这个指令中,没有显式的表示除数和被除数。被除数,默认的将被除数放在了AX或DX和AX中。如果除数是8位的寄存器,那么被除数被认为是AX,如果除数是16位那么被除数是DX和AX,我们可以看下下面的表格:

就是在这里感觉,mips的设计实在是比x86人性化太多了。

Func_ReadOneSector

现在我们回到作者的程序,我们按照上面的流程来分析一下这个程序段:

  • 第一步:进入本程序段时,我们先将bp原来的值缓存到栈中,对应的语句是:push bp
  • 第二步:将sp的值赋给bp,sp是调用该函数前的堆栈寄存器的偏移值
  • 第三步:将esp的值减二,这里实际上是在更新栈的大小,因为前面压入栈的元素大小为2字节。
  • 第四步:后面就是函数的主体了,在主体中,会进行一系列的操作,在操作的时候,会以bp为基址进行寻址
  • 第五步:函数主题执行完毕后,会对栈进行还原,也就是:先将栈的大小进行还原:add esp, 2
  • 第六步:对bp进行还原,同时也对栈进行出栈操作:pop bp
  • 第七步:退出当前函数:ret

到此为止,我们把汇编中的函数相关的部分,基本都看完了,接下来我们来看bootloader相关的部分:

这里的div bl就是计算AX / BL,将商放到AL,余数放到AH

我们实质上是在调用一个中断方法来读取软盘相应扇区的,这个中断方法如下:

INT 13h,AH=02h 功能:读取磁盘扇区。

  • AL=读入的扇区数(必须非0);
  • CH=磁道号(柱面号)的低8位;
  • CL=扇区号1~63(bit 0~5),磁道号(柱面号)的高2位(bit 6~7, 只对硬盘有效);
  • DH=磁头号;
  • DL=驱动器号(如果操作的是硬盘驱动器,bit 7必须被置位);
  • ES:BX=>数据缓冲区。

到此为止,我们也就发现了,这里就是对这个中断函数的一个封装而已,只需要将下面的参数放置到对应的寄存器中,就可以实现读取扇区的功能:

  • AX=待读取的磁盘起始扇区号;
  • CL=读入的扇区数量;
  • ES:BX=>目标缓冲区起始地址。

同时,inc指令就是加1的意思,jc就是jump if carry,进位则跳转,和上一句操作有关。这里jc的含义就是,控制一直发生中断,直到正确读出为止。

搜索出引导加载程序

;=======   search loader.bin
    mov    word    [SectorNo],    SectorNumOfRootDirStart

Lable_Search_In_Root_Dir_Begin:

    cmp    word    [RootDirSizeForLoop],    0
    jz     Label_No_LoaderBin
    dec    word    [RootDirSizeForLoop]
    mov    ax,     00h
    mov    es,     ax
    mov    bx,     8000h
    mov    ax,     [SectorNo]
    mov    cl,     1
    call   Func_ReadOneSector
    mov    si,     LoaderFileName
    mov    di,     8000h
    cld
    mov    dx,     10h; 将dx赋值为10h,循环控制变量

Label_Search_For_LoaderBin:

    cmp    dx,     0
    jz     Label_Goto_Next_Sector_In_Root_Dir;循环十次
    dec    dx
    mov    cx,     11 ; 再循环十一次

Label_Cmp_FileName:

    cmp    cx,     0
    jz     Label_FileName_Found
    dec    cx
    lodsb  ; 
    cmp    al,     byte    [es:di]
    jz     Label_Go_On
    jmp    Label_Different

Label_Go_On:

    inc    di
    jmp    Label_Cmp_FileName

Label_Different:

    and    di,     0ffe0h
    add    di,     20h
    mov    si,     LoaderFileName
    jmp    Label_Search_For_LoaderBin

Label_Goto_Next_Sector_In_Root_Dir:

    add    word    [SectorNo],    1
    jmp    Lable_Search_In_Root_Dir_Begin

这段代码完成的是从根目录中搜索出引导加载程序的任务,我们跟随代码,对这段程序进行简单的解读。

  • 首先,将根目录的起始扇区号存储在[SectorNo]中,从这个地方开始搜索。

  • 在这段代码前,还有一小段:RootDirSizeForLoop dw RootDirSectors,这里执行cmp word [RootDirSizeForLoop], 0

    CMP结果ZFCF
    目的操作数 < 源操作数01
    目的操作数 > 源操作数00
    目的操作数 = 源操作数10

    比较RootDirSectors对应内存区域是否是0:

    • 是0:ZF=1,CF=0
    • 不是0,一定大于0:ZF=CF=0

    反观这里的jz,就是如果ZF寄存器为1,那么就进行跳转,也就是说,如果根目录存储的文件数量为0,那么就跳转到SectorNumOfRootDirStart进行处理。

  • [RootDirSizeForLoop]对应的值减一(其实这个就是控制变量啦)

    其实前面这些就相当于:for(int [RootDirSizeForLoop] = [RootDirSectors]; [RootDirSizeForLoop] > 0; [[RootDirSizeForLoop]] -- ),也就是循环体,接下来要看的是循环执行的具体内容

  • 调用读取扇区的功能:

    • AX=待读取的磁盘起始扇区号,在这里是[SectorNo];
    • CL=读入的扇区数量,在这里是1;
    • ES:BX=>目标缓冲区起始地址,此处是0000h:8000h。

    对这些变量初始化完成后使用Func_ReadOneSector对相应功能完成调用,调用结束后,扇区内的内容被放置到目标缓冲区。

  • si置为 LoaderFileName,这里的LoaderFileName就是字符串"LOADER BIN"

  • 使用CLD指令,将DF设置为0,即告诉程序,后面的si,di向前移动

  • 嵌套了两重循环,循环内执行了lodsb指令:

  • 以下是Intel官方白皮书对LODSB/LODSW/LODSD/LODSQ指令的概括描述。

    • 该命令可从DS:(R|E)SI寄存器指定的内存地址中读取数据到AL/AX/EAX/RAX寄存器。
    • 当数据载入到AL/AX/EAX/RAX寄存器后,(R|E)SI寄存器将会依据R|EFLAGS标志寄存器的DF标志位自动增加或减少载入的数据长度(1/2/4/8字节)。当DF=0时,(R|E)SI寄存器将会自动增加;反之,(R|E)SI寄存器将会自动减少。
  • 在这里执行的操作就是:将对应的字符放入AL之中,然后再使用cmp语句进行比对,如果相同,就进入Label_Go_On,否则就进入Label_Different

  • Label_Go_On,执行的就是:

    inc    di
    jmp    Label_Cmp_FileName
    

    也就是先–,然后再开始循环比对,也就是说,这里执行的实际上就是按位比对字符串的工作。

  • Label_Different

     and    di,     0ffe0h
     add    di,     20h
     mov    si,     LoaderFileName
     jmp    Label_Search_For_LoaderBin
    

    相当于break了本次读取到的字符,跳转到下一次。

  • 同理,如果在当前的Sector没找到,那么就找下一个Label_Goto_Next_Sector_In_Root_Dir

总的来说,上面代码的作用就是找到Loader.bin 这个文件。

错误提示

当loader没有被找到的时候,会调用这段代码,来进行错误提示:

;=======   display on screen : ERROR:No LOADER Found

Label_No_LoaderBin:

    mov    ax,    1301h
    mov    bx,    008ch
    mov    dx,    0100h
    mov    cx,    21
    push   ax
    mov    ax,    ds
    mov    es,    ax
    pop    ax
    mov    bp,    NoLoaderMessage
    int    10h
    jmp    $

这里就不多谈了,调用的是int 10h中断,负责输出一些信息。

FAT表项解析

;=======   get FAT Entry

Func_GetFATEntry:

    push   es
    push   bx
    push   ax
    mov    ax,    00
    mov    es,    ax
    pop    ax
    mov    byte   [Odd],    0
    mov    bx,    3
    mul    bx
    mov    bx,    2
    div    bx
    cmp    dx,    0
    jz     Label_Even
    mov    byte   [Odd],    1

Label_Even:

    xor    dx,    dx
    mov    bx,    [BPB_BytesPerSec]
    div    bx
    push   dx
    mov    bx,    8000h
    add    ax,    SectorNumOfFAT1Start
    mov    cl,    2
    call   Func_ReadOneSector

    pop    dx
    add    bx,    dx
    mov    ax,    [es:bx]
    cmp    byte   [Odd],    1
    jnz    Label_Even_2
    shr    ax,    4

Label_Even_2:
    and    ax,    0fffh
    pop    bx
    pop    es
    ret

Func_GetFATEntry是一个函数,可以通过当前表项索引出下一个表项,调用它需要给出一个参数:

  • AX=FAT表项号(输入参数/输出参数)。

这段程序首先会保存FAT表项号,并将奇偶标志变量(变量[odd])置0。因为每个FAT表项占1.5 B,所以将FAT表项乘以3除以2(扩大1.5倍),来判读余数的奇偶性并保存在[odd]中(奇数为1,偶数为0),再将计算结果除以每扇区字节数,商值为FAT表项的偏移扇区号,余数值为FAT表项在扇区中的偏移位置。接着,通过Func_ReadOneSector模块连续读入两个扇区的数据,此举的目的是为了解决FAT表项横跨两个扇区的问题。最后,根据奇偶标志变量进一步处理奇偶项错位问题,即奇数项向右移动4位。

;=======   found loader.bin name in root director struct

Label_FileName_Found:

    mov    ax,    RootDirSectors
    and    di,    0ffe0h
    add    di,    01ah
    mov    cx,    word    [es:di]
    push   cx
    add    cx,    ax
    add    cx,    SectorBalance
    mov    ax,    BaseOfLoader
    mov    es,    ax
    mov    bx,    OffsetOfLoader
    mov    ax,    cx

Label_Go_On_Loading_File:
    push   ax
    push   bx
    mov    ah,    0eh
    mov    al,    '.'
    mov    bl,    0fh
    int    10h
    pop    bx
    pop    ax

    mov    cl,    1
    call   Func_ReadOneSector
    pop    ax
    call   Func_GetFATEntry
    cmp    ax,    0fffh
    jz     Label_File_Loaded
    push   ax
    mov    dx,    RootDirSectors
    add    ax,    dx
    add    ax,    SectorBalance
    add    bx,    [BPB_BytesPerSec]
    jmp    Label_Go_On_Loading_File

Label_File_Loaded:

    jmp    $

Label_FileName_Found模块中,程序会先取得目录项DIR_FstClus字段的数值,并通过配置ES寄存器和BX寄存器来指定loader.bin程序在内存中的起始地址,再根据loader.bin程序的起始簇号计算出其对应的扇区号。为了增强人机交互效果,此处还使用BIOS中断服务程序INT 10h在屏幕上显示一个字符'.'。接着,每读入一个扇区的数据就通过Func_GetFATEntry模块取得下一个FAT表项,并跳转至Label_Go_On_Loading_File处继续读入下一个簇的数据,如此往复,直至Func_GetFATEntry模块返回的FAT表项值是0fffh为止。当loader.bin文件的数据全部读取到内存后,跳转至Label_File_Loaded处准备执行loader.bin程序。

从Boot跳转到Loader

我们在boot中植入跳转到loader的指令:

Label_File_Loaded:
	jmp BaseOfLoader:OffsetOfLoader

然后再写一个Loader的程序:

org	10000h

	mov	ax,	cs
	mov	ds,	ax
	mov	es,	ax
	mov	ax,	0x00
	mov	ss,	ax
	mov	sp,	0x7c00

;=======	display on screen : Start Loader......

	mov	ax,	1301h
	mov	bx,	000fh
	mov	dx,	0200h		;row 2
	mov	cx,	12
	push	ax
	mov	ax,	ds
	mov	es,	ax
	pop	ax
	mov	bp,	StartLoaderMessage
	int	10h

	jmp	$

;=======	display messages

StartLoaderMessage:	db	"Start Loader"


接下来就可以执行了,为了执行,我更改了一下makefile文件,同时添加了一些提示以防我忘记一些关键步骤:

BOOT_DIR=./src/boot
BUILD_DIR=./build
BUILD_BIN_DIR=./build/bin

all: boot.bin loader.bin
	echo 执行完成

boot.bin: 
# 生成boot的bin文件
	nasm $(BOOT_DIR)/boot.asm -o $(BUILD_BIN_DIR)/boot.bin 

loader.bin:
# 生成loader的bin文件
	nasm $(BOOT_DIR)/loader.asm -o $(BUILD_BIN_DIR)/loader.bin 


install: 
# 将boot的bin写入到引导扇区内 

	echo "特别声明:不要删除boot.img,如果删除了, 请到64位操作系统书中36页寻找复原方法"
	dd if=$(BUILD_BIN_DIR)/boot.bin of=$(BUILD_DIR)/boot.img bs=512 count=1 conv=notrunc 
	sudo mount $(BUILD_DIR)/boot.img /media/ -t vfat -o loop

	sudo cp $(BUILD_BIN_DIR)/loader.bin /media
	sync
	sudo umount /media/
	echo 挂载完成,请进入build文件夹后输入"bochs -f ./bochsrc"以启动虚拟机

clean: 
# 清空生成的文件的方法, 不清空本来就有的img文件
	rm -rf $(BUILD_BIN_DIR)/boot.bin $(BUILD_BIN_DIR)/loader.bin

执行结果:

image-20201014165851919

总体回顾

经过上面的学习,我们已经对boot有了一个比较全面的认知了,在这里我们再全面的梳理一下boot时发生的事件:

  • 首先,boot程序挂载在0x7c00的位置,在0x7c00前面的是bios例程,boot程序不需要我们手动操作,就会被自动执行,当然boot程序必须以0xaa55结尾,这样才能被系统识别,这是boot程序的一些基本属性。
  • 作为开机后执行的第一个程序,boot的大小被限制在512kb,显然,这是不够的,所以我们需要让boot加载磁盘中其他扇区的程序。
  • 在boot最开始的时候,我们定义了一些宏定义变量,同时也将临时文件系统定义为了FAT12文件系统,为此我们db、dw了一系列的变量。
  • 随后,我们将基地址存储到了相应寄存器中,又通过int 10h的方法清空了屏幕,并且在屏幕上输出了BootStarting的提示语
  • 其后,我们开始加载loader,为此我们需要在根目录区搜索名为LOADER BIN的文件,如果没有找到,那么就报错
  • 如果找到了,那就调用预定义好的Func_ReadOneSector函数读取对应扇区,同时使用Func_GetFATEntry检索FAT表项
  • 读取完毕后,跳转到对应区域,执行loader中的内容
  • 我们的loader非常简易,就是直接输出了一段话而已,在下一节中我们会继续完善loader部分

第三部分Loader

在上一节中,我们已经把Boot写好了,这一章的半壁江山也被我们打下来了。我们还记得在第二部分中,我们使用Boot将Loader读取到了内存中,并且跳转到了Loader中,也就是说,接下来就会执行Loader中的内容,或者用作者的话来讲就是控制权交到了Loader的手中,我们在上一节中,只是完成了一个极简的、能够验证loader是否被执行的程序。在这一节中,我们想要写出来一个正正八经的loader,那么在写loader之前,我们需要知道它是干什么的

Loader的作用

前导知识——实模式与保护模式

CPU复位或是上电的时候就是从实模式来启动的,在这个时候处理器以实模式来工作,不区分任何权限,也不能访问20位以上的地址线(1M内存),之后加载操作系统模块后,进入保护模式。

进入保护模式后,操作系统全权接管cpu,这个时候能够访问所有的内存,但是进入保护模式后,有些机器自身的信息是不能被读取的了。

Loader执行的操作

我们可以先简单的把Loader执行的操作划分为三个阶段:

  • 检测硬件信息:这个操作是只有在实模式下才能够进行的,Loader会检测硬件的一些参数,然后将他们传递到下一阶段
  • 处理器模式切换:这个就是我们前面说的从实模式转化到保护模式,进入到保护模式后,还要再进行一次切换,因为内核模式只能操作32位的操作系统,也就是智能操作4G内存,所以我们需要切换到IA-32e模式,也叫长模式,在该模式下才能够成为64位的操作系统。
  • 向内核传递数据:Loader实质上是一个预处理的阶段,如果你用的是双系统的话,进入grub2的时候,执行的应该就是Loader的操作,即:进行系统自检,并且让用户选择启动模式,然后再根据不同的启动模式来进入系统,综合来说,这些功能被参数控制,参数可以被概括为:
    • 控制信息:用户选择的启动模式等等(由软件决定)。
    • 硬件数据信息:获取一些硬件的信息,比如内存信息。

懂了他做的事情之后,我们来正式的写一下这个程序(看到作者给的例程接近800行,我感觉整个人都要归西了)

一些宏定义以及include

是的,汇编也可以include,我们首先建立一个fat12.inc

RootDirSectors	equ	14 ; 根目录区占用的扇区数
SectorNumOfRootDirStart	equ	19 ; 根目录区起始扇区号
SectorNumOfFAT1Start	equ	1 ; 起始扇区号
SectorBalance	equ	17	

    BS_OEMName          db  'MINEboot'  ; 表示的是生产商的名字,我不要脸一点直接写自己名字了
    BPB_BytesPerSec     dw  512         ; 每个扇区的字节数
    BPB_SecPerClus      db  1           ; 每个簇的扇区数
    BPB_RsvdSecCnt	    dw	1           ; 保留扇区数
	BPB_NumFATs	        db	2           ; FAT表的数量
	BPB_RootEntCnt	    dw	224         ; 根目录可容纳的目录项数
	BPB_TotSec16	    dw	2880        ; 总扇区数
	BPB_Media	        db	0xf0        ; 介质描述符
	BPB_FATSz16	        dw	9           ; 每FAT扇区数
	BPB_SecPerTrk	    dw	18          ; 每磁道扇区数
	BPB_NumHeads	    dw	2           ; 磁头数
	BPB_HiddSec	        dd	0           ; 隐藏扇区数
	BPB_TotSec32	    dd	0           
	BS_DrvNum	        db	0           ; int 13h 的驱动器号
	BS_Reserved1	    db	0           ; 未使用
	BS_BootSig	        db	0x29        ; 扩展引导标记
	BS_VolID	        dd	0           ; 卷序列号
	BS_VolLab	        db	'boot loader' ; 卷标
	BS_FileSysType	    db	'FAT12   '  ; 文件系统类型

这段内容就是我们将boot.asm中的内容去掉jump语句以及空语句完全复制过来的,上面的文件就是我们要%include的东西,我们来看下如何include

org    10000h
    jmp    Label_Start

%include    "fat12.inc" ; 引用了一个文件
;这个文件就在同目录下的fat12.inc中,其中储存着fat12的一些基本信息


BaseOfKernelFile          equ    0x00
OffsetOfKernelFile        equ    0x100000

BaseTmpOfKernelAddr       equ    0x00
OffsetTmpOfKernelFile     equ    0x7E00

MemoryStructBufferAddr    equ    0x7E00

可以看到,我们直接使用%include "fileName"的形式就将上面的东西都引入进来了。这里的Loader最终会被加载到:0x100000处,也就是内存的1MB处,因为1MB以下的地址并不全是可用空间,这段空间被划分成若干个子空间段,我们的内核程序跳过复杂的前1MB,从平坦的1MB开始,也是一个很好的选择。当然,由于我们的处理器在实模式下需要使用中断方法INT 13h实现读取内核程序,只能访问1MB的内存,所以我们需要将内核程序暂时存储在0x7E00内存处,然后再通过特殊方式搬运到1MB以上的内存空间中。当内核程序被转存到最终内存空间后,这个临时转存空间就可以另作他用,此处将其改为内存结构数据的存储空间,供内核程序初始化时使用。

输出提示信息

;=======	声明,在16位宽模式下实现
[SECTION .s16]
[BITS 16]

Label_Start:

	mov	ax,	cs
	mov	ds,	ax
	mov	es,	ax
	mov	ax,	0x00
	mov	ss,	ax
	mov	sp,	0x7c00

;=======	在屏幕上显示 : Start Loader......

	mov	ax,	1301h
	mov	bx,	000fh
	mov	dx,	0200h		;row 2
	mov	cx,	12
	push	ax
	mov	ax,	ds
	mov	es,	ax
	pop	ax
	mov	bp,	StartLoaderMessage
	int	10h

这里这些东西,我们都是非常的熟悉了,就不多做赘述了,主要要说的是:

[SECTION .s16]
[BITS 16]

这一段代码在做的是:声明这段代码处于16位宽状态下,如果我们想在这种状态下使用32位宽的数据指令或地址指令,那么我们需要在指令前加入前缀:0x660x67。同理在32位模式下,如果要使用16位宽的指令也需要加入前缀,但显然这不是我们需要研究的主要内容,所以我们不多赘述。

在实模式下4GB寻址

;=======	打开 A20, 进入Big Real Mode
	push	ax
	in	al,	92h ; 从92h端口读取一字节数据到al
	or	al,	00000010b ; 或一下
	out	92h,	al ; 将al输出到92h端口
	pop	ax 
	; 我猜测这里在做的就是不管92h是什么,都用92h这个端口将20A功能开启

	cli ; 禁止所有中断

	db	0x66 ; 声明在16位情况下使用32位宽数据指令
	lgdt	[GdtPtr]	; lgdt将段描述子读取到对应区域

	;  启动保护模式, 通过更改cr0的最低位
	mov	eax,	cr0
	or	eax,	1
	mov	cr0,	eax

	mov	ax,	SelectorData32 ; 将不知道啥玩意读取到ax
	mov	fs,	ax ; 再存到fs?
	mov	eax,	cr0 ; 重新更改cr0以进入实模式
	and	al,	11111110b
	mov	cr0,	eax

	sti ; 开始接收中断

我们首先需要搞懂的是,我们的代码是怎么在实模式下访问到1MB以外的内存空间的。这要从A20开始讲起:

A20

早期的处理器只有20根地址线,这就代表处理器只能寻找到1MB以内的物理地址空间,如果超过了那就只有低20位有效。但是后来的处理器变强了,已经突破了20根地址线了,这个时候就出现了兼容性问题,为解决该问题,就只好设置一个控制开启或禁止1MB范围的开关。对此,英特尔的8042键盘控制器上恰好有空闲的端口引脚,从而使用此引脚作为功能控制开关,这个被称为A20功能。这个8042控制器是一个芯片,位于我们的电脑主板上,负责与我们的键盘进行通信,可以把它理解为一个桥(interface)。

一句话总结A20对应的引脚:

  • 低电平(0):那么只有低20位有效,其他位都是0
  • 高电平(1):那么就全都有效
开启A20的方法

既然存在A20这个玩意,那就有很多开启他的方法:

  • 方法一:直接操作键盘控制器
  • 方法二:快速门,使用IO端口来处理A20信号线。
  • 方法三:使用BIOS中的中断服务程序INT 15h的主功能号AX=2401可以开启A20地址线,AX=2402可以关闭A20地址线,AX=2402可以查看A20的状态
  • 方法四:读0xee端口来开启A20信号线

如果让我选的话,我肯定用中断的方法来开启他(因为我只会这个),但可惜作者选择了使用快速门的方法,那么我们也跟着他学一手吧。

开启A20后的操作
  • 使用CLI指令关闭外部中断(这个指令是禁止一切中断发生的指令)
  • 通过指令LGDT加载保护模式结构数据信息
  • 为FS段寄存器加载新的数据段值
  • 一旦数据加载完成就从保护模式退出,并且开启外部中断(STI
  • 操作完成,进入Big Real Mode
调试观察

我们在sti语句下添加jmp $语句,以让程序停留在此处,然后让虚拟机运行起来,我们在这里调试一下,那段代码如下:

sti

jump $

开启后,按Ctrl + C并且执行sreg指令:

<bochs:2> sreg
es:0x1000, dh=0x00009301, dl=0x0000ffff, valid=1
        Data segment, base=0x00010000, limit=0x0000ffff, Read/Write, Accessed
cs:0x1000, dh=0x00009301, dl=0x0000ffff, valid=1
        Data segment, base=0x00010000, limit=0x0000ffff, Read/Write, Accessed
ss:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
        Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
ds:0x1000, dh=0x00009301, dl=0x0000ffff, valid=1
        Data segment, base=0x00010000, limit=0x0000ffff, Read/Write, Accessed
fs:0x0010, dh=0x00cf9300, dl=0x0000ffff, valid=1
        Data segment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
gs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
        Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
ldtr:0x0000, dh=0x00008200, dl=0x0000ffff, valid=1
tr:0x0000, dh=0x00008b00, dl=0x0000ffff, valid=1
gdtr:base=0x00010040, limit=0x17
idtr:base=0x00000000, limit=0x3ff

我们看fs寄存器还有段的Data:

fs:0x0010
Data segment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed

段基址:base=0x00000000,段限长:limit=0xffffffff寻址能力从20位到了32位。

需要注意的是:我们让段寄存器拥有这种特殊能力后,如果重新对其赋值的话,那么他就会失去特殊能力,变为之前的实模式寄存器。(但是bochs虚拟机放宽了这一需求)这一阶段就算完成了。

寻找kernel.bin

这段仍然是在根目录中寻找文件,只不过这里找的是kernel.bin,我们这里就把原来的代码搬过来用,需要改变的就是用于比较的字符串了:

;=======	搜索kernel.bin
	mov	word	[SectorNo],	SectorNumOfRootDirStart

Lable_Search_In_Root_Dir_Begin:

	cmp	word	[RootDirSizeForLoop],	0
	jz	Label_No_LoaderBin
	dec	word	[RootDirSizeForLoop]	
	mov	ax,	00h
	mov	es,	ax
	mov	bx,	8000h
	mov	ax,	[SectorNo]
	mov	cl,	1
	call	Func_ReadOneSector
	mov	si,	KernelFileName
	mov	di,	8000h
	cld
	mov	dx,	10h
	
Label_Search_For_LoaderBin:

	cmp	dx,	0
	jz	Label_Goto_Next_Sector_In_Root_Dir
	dec	dx
	mov	cx,	11

Label_Cmp_FileName:

	cmp	cx,	0
	jz	Label_FileName_Found
	dec	cx
	lodsb	
	cmp	al,	byte	[es:di]
	jz	Label_Go_On
	jmp	Label_Different

Label_Go_On:
	
	inc	di
	jmp	Label_Cmp_FileName

Label_Different:

	and	di,	0FFE0h
	add	di,	20h
	mov	si,	KernelFileName
	jmp	Label_Search_For_LoaderBin

Label_Goto_Next_Sector_In_Root_Dir:
	
	add	word	[SectorNo],	1
	jmp	Lable_Search_In_Root_Dir_Begin
	
;=======	没有找到时进入,输出 : ERROR:No KERNEL Found

Label_No_LoaderBin:

	mov	ax,	1301h
	mov	bx,	008Ch
	mov	dx,	0300h		;row 3
	mov	cx,	21
	push	ax
	mov	ax,	ds
	mov	es,	ax
	pop	ax
	mov	bp,	NoLoaderMessage
	int	10h
	jmp	$

转移内核程序

;=======	找到文件后进行,将内核程序从软盘转移到内存

Label_FileName_Found:
	mov	ax,	RootDirSectors
	and	di,	0FFE0h
	add	di,	01Ah
	mov	cx,	word	[es:di]
	push	cx
	add	cx,	ax
	add	cx,	SectorBalance
	mov	eax,	BaseTmpOfKernelAddr	;BaseOfKernelFile
	mov	es,	eax
	mov	bx,	OffsetTmpOfKernelFile	;OffsetOfKernelFile
	mov	ax,	cx

Label_Go_On_Loading_File:
	push	ax
	push	bx
	mov	ah,	0Eh
	mov	al,	'.'
	mov	bl,	0Fh
	int	10h
	pop	bx
	pop	ax

	mov	cl,	1
	call	Func_ReadOneSector
	pop	ax

;;;;;;;;;;;;;;;;;;;;;;;	
	push	cx
	push	eax
	push	fs
	push	edi
	push	ds
	push	esi

	mov	cx,	200h
	mov	ax,	BaseOfKernelFile
	mov	fs,	ax
	mov	edi,	dword	[OffsetOfKernelFileCount]

	mov	ax,	BaseTmpOfKernelAddr
	mov	ds,	ax
	mov	esi,	OffsetTmpOfKernelFile

Label_Mov_Kernel:	;------------------
	
	mov	al,	byte	[ds:esi]
	mov	byte	[fs:edi],	al

	inc	esi
	inc	edi

	loop	Label_Mov_Kernel

	mov	eax,	0x1000
	mov	ds,	eax

	mov	dword	[OffsetOfKernelFileCount],	edi

	pop	esi
	pop	ds
	pop	edi
	pop	fs
	pop	eax
	pop	cx
;;;;;;;;;;;;;;;;;;;;;;;	

	call	Func_GetFATEntry
	cmp	ax,	0FFFh
	jz	Label_File_Loaded
	push	ax
	mov	dx,	RootDirSectors
	add	ax,	dx
	add	ax,	SectorBalance

	jmp	Label_Go_On_Loading_File

这段主要在执行的就是:逐字节的将kernel文件移动到1MB以上的物理内存空间。还是老规矩,我们来解读一下代码,我们可以将他工作的流程大致分为以下几个阶段:

  • 调用Func_ReadOneSector,读取一个扇区

  • loop Label_Mov_Kernel循环转移Kernel,这段代码用于转移kernel的核心语句是:

    mov	al,	byte	[ds:esi]
    mov	byte	[fs:edi],	al
    
    inc	esi
    inc	edi
    

    他们的意思就是:将原来的一个字节存到al中,再将al存到目标地址,然后再将目标地址与当前存储的指针都向后移动。这个非常简单。

  • 通过Func_GetFATEntry进入下一个扇区继续使用Func_ReadOneSector进行读取,读取完毕后继续转移

可以说整体的流程还是非常简单的,我感觉读汇编最困难的地方,就像是你在读一个只会用abcd命名的人写的c语言一样,你看不懂每一个变量名都在干啥,就很难受,同时如果你像我一样没有nasm汇编基础,只学过mips的话,你会觉得intel设计的汇编语言非常不适合人类使用,我猜想,在早期,因特尔的汇编应当也是非常简单易用的,只不过随着时代的发展,汇编语言也在不断更新,但身为一个大厂,需要做到向下兼容,所以才导致了一条语句在不同情况下对应不同的操作数等等的情况,这种情况实属无奈。

快速显示文字

Label_File_Loaded:
		
	mov	ax, 0B800h
	mov	gs, ax
	mov	ah, 0Fh				; 0000: 黑底    1111: 白字
	mov	al, 'G'
	mov	[gs:((80 * 0 + 39) * 2)], ax	; 屏幕第 0 行, 第 39 列。

在这段代码中,我们实际上是在将’G’这个字符放到了内存中gs:((80 * 0 + 39) * 2)的这个地方,这个gs在这里是0B800h以这里为起点,向后偏移一段区域的内存专门用于存储在屏幕上显示的字,每个字占位两个字节,每行最多显示80个字符,所以就有上面的公式:((80 * 0 + 39) * 2)这表示第0行的39列显示该字符。

我们使用作者在下一章提供的kernel文件夹编译出的kernel.bin文件作为这里的内核文件,进行编译,结果如下:

image-20201015202939750

可以看到,先显示了一些点,然后又在前面显示了G,这个G没有通过系统中断进行显示,而是直接写入到了显示文字的内存区域。

关闭软驱马达

到上一个步骤,在屏幕上显示了那个G之后,我们已经成功的将kernel.bin文件从软盘加载到了内存,所以我们就可以把软盘的驱动关掉了。

KillMotor:
	
	push	dx
	mov	dx,	03F2h
	mov	al,	0	
	out	dx,	al
	pop	dx

这个操作是向IO端口03F2h写入控制命令实现的,这个端口控制着软驱的一些硬件功能。可以通过作者给出的表格了解具体的功能:

名称说明
7MOT_EN3控制软驱D马达,1:启动;0:关闭
6MOT_EN2控制软驱C马达,1:启动;0:关闭
5MOT_EN1控制软驱B马达,1:启动;0:关闭
4MOT_EN0控制软驱A马达,1:启动;0:关闭
3DMA_INT1:允许DMA和中断请求 0:禁止DMA和中断请求
2RESET1:允许软盘控制器发送控制信息 0:复位软盘驱动器
1DRV_SEL10011用于选择软盘驱动器AD
0DRV_SEL0

我们在前面已经调用过汇编语言中的OUT了,它是用于将一个寄存器中的值或是立即数等等,输出到I/O端口的。

获取不同类型内存块的范围

;=======	get memory address size type

	mov	ax,	1301h
	mov	bx,	000Fh
	mov	dx,	0400h		;row 4
	mov	cx,	24
	push	ax
	mov	ax,	ds
	mov	es,	ax
	pop	ax
	mov	bp,	StartGetMemStructMessage
	int	10h

	mov	ebx,	0
	mov	ax,	0x00
	mov	es,	ax
	mov	di,	MemoryStructBufferAddr	

Label_Get_Mem_Struct:

	mov	eax,	0x0E820
	mov	ecx,	20
	mov	edx,	0x534D4150
	int	15h
	jc	Label_Get_Mem_Fail
	add	di,	20

	cmp	ebx,	0
	jne	Label_Get_Mem_Struct
	jmp	Label_Get_Mem_OK

Label_Get_Mem_Fail:

	mov	ax,	1301h
	mov	bx,	008Ch
	mov	dx,	0500h		;row 5
	mov	cx,	23
	push	ax
	mov	ax,	ds
	mov	es,	ax
	pop	ax
	mov	bp,	GetMemStructErrMessage
	int	10h
	jmp	$

Label_Get_Mem_OK:
	
	mov	ax,	1301h
	mov	bx,	000Fh
	mov	dx,	0600h		;row 6
	mov	cx,	29
	push	ax
	mov	ax,	ds
	mov	es,	ax
	pop	ax
	mov	bp,	GetMemStructOKMessage
	int	10h	

这一节的名字并不严谨,它实际上执行的操作是:获取地址空间类型(寄存器地址空间、内存空洞等等的),并且将这些信息存储在0x7E00地址处的临时转存空间中。为了得到这些信息,我们使用了INT 15h中断(我们之前说过,可以用它开启A20模式)。我们先来看一下 INT 15h中断,再来讲解具体的执行过程。

INT 15 中断详解

这里的内容摘自一篇博客——《INT 15h AX=E820h的用法》

输入信息
寄存器作用
eax功能码,当输入e820h时能够探测内存
ebx主要用于指向内存区域,第一次调用时ebx=0,被称为continuation value
es:di用于指令执行后,在指向的内存写入描述内存区域的数据结构ARDS(Address Range Descriptor Structure)
ecx用于限制指令填充的ARDS的大小,实际上大多数情况这个是无效的,无论ecx设置为多少,BIOS始终会填充20字节的ARDS
edx0534D4150h(‘SMAP’),输入时在edx,输出时将会在eax中
输出信息
寄存器结果
CF当没有发生错误时,CF=0,否则CF=1
eax0534D4150h(‘SMAP’)
ebx指向下一个内存区域,而不是调用之前的内存区域,当ebx=0且CF=0时,表示当前是最后一个内存区域。
es:di和调用之前一样,如果要保存多个ARDS,需要手动修改es:di
ecx返回写入的ARDS的大小
ARDS的结构(共20字节,Type为4字节)
偏移名称意义
0BaseAddrLow基地址的低32位
4BaseAddrHigh基地址的高32位
8LengthLow长度(字节)的低32位
12LengthHigh长度(字节)的高32位
16Type这个内存区域的类型
ARDS的Type取值如下:
取值名称意义
1AddressRangeMemory可以被OS使用的内存
2AddressRangeReserved正在使用的区域或者不能系统保留不能使用的区域
其他未定义各个具体机器会有不同的意义,在这里我们暂时不用关心,将它视为AddressRangeReserved即可

结合我们的代码来理解一下,在这里我将执行INT 15h之前的一些参数列了出来:

mov	ebx,	0
mov	ax,	0x00
mov	es,	ax
mov	di,	MemoryStructBufferAddr	
mov	eax,	0x0E820
mov	ecx,	20
mov	edx,	0x534D4150
int	15h

这里:

  • ebx=0说明是第一次调用
  • es=0基地址为0,di偏移量是MemoryStructBufferAddr=0x7E00,共同构成了结果的返回地址0:0x7E00,这个和我们之前说的目标地址相同。
  • eax=0x0E820功能码,说明要探测内存
  • ecx据上面所说,没啥用
  • edx输入时在edx,输出时将会在eax中(这个没看懂是干啥的)

同理,返回时的结果就像上面表里说的一样,我们就不多提了,需要注意的是,进位那里如果是0则说明没有错误。

执行过程
  • 输出提示信息"Start Get Memory Struct."
  • 使用INT 15中断,获取内存信息,如果出错,进位为1跳转到Label_Get_Mem_Fail,如果没有错则继续执行
  • 判断ebx,如果ebx是0,那么就说明他指向的是最后一个内存区域,那么就跳转到Label_Get_Mem_OK,否则回到Label_Get_Mem_Struct
  • 如果回到Label_Get_Mem_Struct,那么就使用新的ebx继续读取,直到探测完毕为止
  • 探测完毕后进入Label_Get_Mem_OK,使用int 10h输出一行字进行提示

定义函数:输出十六进制数字到屏幕

;=======	输出al中的数字到屏幕

Label_DispAL:

	push	ecx
	push	edx
	push	edi
	
	mov	edi,	[DisplayPosition]
	mov	ah,	0Fh
	mov	dl,	al
	shr	al,	4
	mov	ecx,	2
.begin:

	and	al,	0Fh
	cmp	al,	9
	ja	.1
	add	al,	'0'
	jmp	.2
.1:

	sub	al,	0Ah
	add	al,	'A'
.2:

	mov	[gs:edi],	ax
	add	edi,	2
	
	mov	al,	dl
	loop	.begin

	mov	[DisplayPosition],	edi

	pop	edi
	pop	edx
	pop	ecx
	
	ret

这个函数就是将一个十六进制的数字输出到屏幕,我们也不去剖析它的原理了,和C语言处理字符串基本一致,别问我为什么是al,我也想把参数变成oct_num

调用方法:

mov al, 13
call Label_DispAL

获取显示模式信息进行展示,并且设置显示模式

获取显示模式的信息进行展示(非关键)
;=======	get SVGA information

	mov	ax,	1301h
	mov	bx,	000Fh
	mov	dx,	0800h		;row 8
	mov	cx,	23
	push	ax
	mov	ax,	ds
	mov	es,	ax
	pop	ax
	mov	bp,	StartGetSVGAVBEInfoMessage
	int	10h

	mov	ax,	0x00
	mov	es,	ax
	mov	di,	0x8000
	mov	ax,	4F00h

	int	10h

	cmp	ax,	004Fh

	jz	.KO
	
;=======	Fail

	mov	ax,	1301h
	mov	bx,	008Ch
	mov	dx,	0900h		;row 9
	mov	cx,	23
	push	ax
	mov	ax,	ds
	mov	es,	ax
	pop	ax
	mov	bp,	GetSVGAVBEInfoErrMessage
	int	10h

	jmp	$

.KO:

	mov	ax,	1301h
	mov	bx,	000Fh
	mov	dx,	0A00h		;row 10
	mov	cx,	29
	push	ax
	mov	ax,	ds
	mov	es,	ax
	pop	ax
	mov	bp,	GetSVGAVBEInfoOKMessage
	int	10h

;=======	Get SVGA Mode Info

	mov	ax,	1301h
	mov	bx,	000Fh
	mov	dx,	0C00h		;row 12
	mov	cx,	24
	push	ax
	mov	ax,	ds
	mov	es,	ax
	pop	ax
	mov	bp,	StartGetSVGAModeInfoMessage
	int	10h


	mov	ax,	0x00
	mov	es,	ax
	mov	si,	0x800e

	mov	esi,	dword	[es:si]
	mov	edi,	0x8200

Label_SVGA_Mode_Info_Get:

	mov	cx,	word	[es:esi]

;=======	display SVGA mode information

	push	ax
	
	mov	ax,	00h
	mov	al,	ch
	call	Label_DispAL

	mov	ax,	00h
	mov	al,	cl	
	call	Label_DispAL
	
	pop	ax

;=======
	
	cmp	cx,	0FFFFh
	jz	Label_SVGA_Mode_Info_Finish

	mov	ax,	4F01h
	int	10h

	cmp	ax,	004Fh

	jnz	Label_SVGA_Mode_Info_FAIL	

	add	esi,	2
	add	edi,	0x100

	jmp	Label_SVGA_Mode_Info_Get
		
Label_SVGA_Mode_Info_FAIL:

	mov	ax,	1301h
	mov	bx,	008Ch
	mov	dx,	0D00h		;row 13
	mov	cx,	24
	push	ax
	mov	ax,	ds
	mov	es,	ax
	pop	ax
	mov	bp,	GetSVGAModeInfoErrMessage
	int	10h

Label_SET_SVGA_Mode_VESA_VBE_FAIL:

	jmp	$

Label_SVGA_Mode_Info_Finish:

	mov	ax,	1301h
	mov	bx,	000Fh
	mov	dx,	0E00h		;row 14
	mov	cx,	30
	push	ax
	mov	ax,	ds
	mov	es,	ax
	pop	ax
	mov	bp,	GetSVGAModeInfoOKMessage
	int	10h

	jmp $

最后一行的jmp $是我自己加进来的,为了在这里停留一下。我们的程序输出了很多的字符,在第一排,这些字符就表示SVGA芯片支持显示的显示模式号。这个时候我们最开始输出的一些信息已经被覆盖了,这说明我们写入的这块内存就是用于存储屏幕上显示的字符的区域。

大家不要太过纠结这块代码,因为这块意义不大,最后显示的结果如下:

image-20201015212403205
设置显示模式

上面我们看到了一堆显示模式,这里我们就设置一下他们就可以了:

;=======	set the SVGA mode(VESA VBE)

	mov	ax,	4F02h
	mov	bx,	4180h	;========================mode : 0x180 or 0x143
	int 	10h

	cmp	ax,	004Fh
	jnz	Label_SET_SVGA_Mode_VESA_VBE_FAIL

	jmp $

设置完成后,程序在本地停留,我们现实的结果如下:可以看出来屏幕变大了哈哈哈,但是由于是图片的原因,所以可能感受不是特别直观,这张图片和上一张图片都缩放了67%,大家可以进行一个简单的对比。

image-20201015213224055

最后提醒一下大家,从这里继续向下之前,记得把jmp $删掉,要不然程序会一直停留在这里。

模式转化(知识性讲解)

在正式开始写这块代码之前,我们先来进行一些简单的讲解。

为什么需要转化模式

我们在实模式下开启了“Big Real Mode”,在该模式下可以访问4G内存了,那么我们为什么还需要转换模式呢?这是因为,我们虽然可以访问任何的内存空间,但是没有办法限制程序执行的权限,在这种情况下如果程序执行错误,那么就会让系统全部崩溃。

在保护模式中,处理器按照程序的级别分为0,1,2,3四个等级,最高级是0由操作系统的内核调用,可以执行所有的指令,最低的等级3由应用程序使用,中间的等级可以预留给系统调用等程序来使用。同时,保护模式引入了分页的功能。

如何从实模式进入保护模式

需要经过下面的过程:

  • 准备一个GDT表:GDT就是全局描述符表
  • 用lgdt加载gdtr
  • 关闭系统中断(在保护模式下,中断和异常都是由IDT来管理,需要使用LIDT指令将这个表加载到IDTR寄存器)
  • 打开A20
  • cr0的PE置为1(需要分页机制的话还需要将CR0寄存器的PG标志位开启【在此之前需要在内存中创建一个页目录和页表】)
  • 如果需要多任务机制,那么需要创建至少一个任务状态段TSS结构和附加的TSS段描述符并且使用LTR汇编指令将其加载至TR寄存器
  • 跳转,进入保护模式

我们这里就大概一讲,接下来我们看具体的代码是怎么做的:

创建GDT表

[SECTION gdt]

LABEL_GDT:		dd	0,0
LABEL_DESC_CODE32:	dd	0x0000FFFF,0x00CF9A00 ; 两个字拼接而成
LABEL_DESC_DATA32:	dd	0x0000FFFF,0x00CF9200 ; 双字

GdtLen	equ	$ - LABEL_GDT
GdtPtr	dw	GdtLen - 1
	dd	LABEL_GDT

SelectorCode32	equ	LABEL_DESC_CODE32 - LABEL_GDT
SelectorData32	equ	LABEL_DESC_DATA32 - LABEL_GDT

这段代码就是用于创建一个临时的GDT表的代码

这里将代码段和数据段的段基址都设置在了0x0000,同时将段限长设置为了0xffff,可以索引0~4GB的内存空间。

GdtPtr是此结构的起始地址,SelectorCode32SelectorData32是两个段选择子,他们是段描述符在GDT表中的索引号。

为IDT开辟内存空间

;=======	开辟 IDT 存储空间

IDT:
	times	0x50	dq	0
IDT_END:

IDT_POINTER:
		dw	IDT_END - IDT - 1
		dd	IDT

这里非常简单没啥好说的,就相当于是为IDT预留出了一部分的空间。

在处理器切换至保护模式前,引导加载程序已使用CLI指令禁止外部中断,所以在切换到保护模式的过程中不会产生中断和异常,进而不必完整地初始化IDT,只要有相应的结构体即可。如果能够保证处理器在模式切换的过程中不会产生异常,即使没有IDT也可以。

从实模式切换到保护模式

需要经过的步骤:

  • 使用CLI指令屏蔽硬件中断
  • 使用LGDT将GDT 的基地址和长度加载到GDTR寄存器中
  • 执行mov cr0更改cr0的PE标志位
  • mov cr0执行结束后,使用far jump切换到保护模式的代码去执行
  • 如果开启分页机制,那么MOV CR0指令和JMP/CALL(跳转/调用)指令必须位于同一性地址映射的页面内
  • 如需使用LDT,则必须借助LLDT汇编指令将GDT内的LDT段选择子加载到LDTR寄存器中。
  • 执行LTR汇编指令将一个TSS段描述符的段选择子加载到TR任务寄存器。处理器对TSS段结构无特殊要求,凡是可写的内存空间均可。
  • 进入保护模式后,数据段寄存器仍旧保留着实模式的段数据,需要重新加载数据段选择子,或重新使用JMP执行新任务
  • 执行LIDT,将LDT表加载
  • 执行STI指令使能可屏蔽硬件中断

当然,我们的代码没有必要完全遵守这一流程:

;=======	初始化 IDT GDT 切换到保护模式

	cli			;======关闭 中断

	db	0x66
	lgdt	[GdtPtr]

;	db	0x66
;	lidt	[IDT_POINTER]

	mov	eax,	cr0
	or	eax,	1
	mov	cr0,	eax	

	jmp	dword SelectorCode32:GO_TO_TMP_Protect

我们将jmp dword SelectorCode32:GO_TO_TMP_Protect替换为jmp $,使用sreg查看寄存器的值(这个操作前面搞过,我们这里就不谈了):

<bochs:4> sreg
es:0x1000, dh=0x00009301, dl=0x0000ffff, valid=1
        Data segment, base=0x00010000, limit=0x0000ffff, Read/Write, Accessed
cs:0x0008, dh=0x00cf9b00, dl=0x0000ffff, valid=1
        Code segment, base=0x00000000, limit=0xffffffff, Execute/Read, Non-Conforming, Accessed, 32-bit
ss:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
        Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
ds:0x1000, dh=0x00009301, dl=0x0000ffff, valid=3
        Data segment, base=0x00010000, limit=0x0000ffff, Read/Write, Accessed
fs:0x0010, dh=0x00cf9300, dl=0x0100ffff, valid=1
        Data segment, base=0x00000100, limit=0xffffffff, Read/Write, Accessed
gs:0xb800, dh=0x0000930b, dl=0x8000ffff, valid=7
        Data segment, base=0x000b8000, limit=0x0000ffff, Read/Write, Accessed
ldtr:0x0000, dh=0x00008200, dl=0x0000ffff, valid=1
tr:0x0000, dh=0x00008b00, dl=0x0000ffff, valid=1
gdtr:base=0x00010040, limit=0x17
idtr:base=0x00000000, limit=0x3ff

在上面的寄存器中,CS段寄存器中的段基地址base、段限长limit以及其他段属性,自汇编代码jmp dword SelectorCode32:GO_TO_TMP_Protect执行后皆发生了改变。与此同时,GDTR寄存器中的数据已更新为GdtPtr结构记录的GDT表基地址和长度。

退出后输出了信息:

00362101883i[      ] dbg: Quit
00362101883i[CPU0  ] CPU is in protected mode (active)
00362101883i[CPU0  ] CS.mode = 32 bit
00362101883i[CPU0  ] SS.mode = 16 bit
00362101883i[CPU0  ] EFER   = 0x00000000
00362101883i[CPU0  ] | EAX=60000011  EBX=00004180  ECX=0000001e  EDX=00000e00
00362101883i[CPU0  ] | ESP=00007c00  EBP=000008ef  ESI=00008098  EDI=0000bd00
00362101883i[CPU0  ] | IOPL=0 id vip vif ac vm rf nt of df if tf sf zf af PF cf
00362101883i[CPU0  ] | SEG sltr(index|ti|rpl)     base    limit G D
00362101883i[CPU0  ] |  CS:0008( 0001| 0|  0) 00000000 ffffffff 1 1
00362101883i[CPU0  ] |  DS:1000( 0005| 0|  0) 00010000 0000ffff 0 0
00362101883i[CPU0  ] |  SS:0000( 0005| 0|  0) 00000000 0000ffff 0 0
00362101883i[CPU0  ] |  ES:1000( 0005| 0|  0) 00010000 0000ffff 0 0
00362101883i[CPU0  ] |  FS:0010( 0002| 0|  0) 00000100 ffffffff 1 1
00362101883i[CPU0  ] |  GS:b800( 0005| 0|  0) 000b8000 0000ffff 0 0
00362101883i[CPU0  ] | EIP=00010364 (00010364)
00362101883i[CPU0  ] | CR0=0x60000011 CR2=0x00000000
00362101883i[CPU0  ] | CR3=0x00000000 CR4=0x00000000
(0).[362101883] [0x000000010364] 0008:00010364 (unk. ctxt): jmp .-2 (0x00010364)      ; ebfe
00362101883i[CMOS  ] Last time is 1602780155 (Fri Oct 16 00:42:35 2020)
00362101883i[XGUI  ] Exit
00362101883i[SIM   ] quit_sim called with exit code 0

CPU is in protected mode说明cpu已经在保护模式下了

从保护模式进入IA-32e模式(大体过程)

通过刚才的操作,我们从实模式进入到了保护模式,接下来我们想从保护模式进入IA-32e模式。

接下来我们来盘点一下大概的流程:

  • 首先,各种描述表符的寄存器GDTR\LDTR\IDTR\TR,需要重新加载
  • 对于中断与异常和上面也是同理,需要在进入IA-32e模式前关闭中断与异常,然后在进入后,重新打开。

详细来说,过程如下:

  • 在保护模式下,使用MOV CR0更改PG标志位,关闭分页机制
  • 更改CR4寄存器的PAE控制位,开启物理地址扩展功能
  • 将页目录的物理基地址加载到CR3寄存器中
  • 置位IA32_EFER寄存器的LME标志位,开启IA-32e模式
  • 重新开始分页机制

IA-32e模式的GDT表

[SECTION gdt64]

LABEL_GDT64:		dq	0x0000000000000000
LABEL_DESC_CODE64:	dq	0x0020980000000000
LABEL_DESC_DATA64:	dq	0x0000920000000000

GdtLen64	equ	$ - LABEL_GDT64
GdtPtr64	dw	GdtLen64 - 1
		dd	LABEL_GDT64

SelectorCode64	equ	LABEL_DESC_CODE64 - LABEL_GDT64
SelectorData64	equ	LABEL_DESC_DATA64 - LABEL_GDT64

和上面基本一样

调用函数,检测是否支持IA-32e模式

[SECTION .s32]
[BITS 32]

GO_TO_TMP_Protect:

;=======	go to tmp long mode

	mov	ax,	0x10
	mov	ds,	ax
	mov	es,	ax
	mov	fs,	ax
	mov	ss,	ax
	mov	esp,	7E00h

	call	support_long_mode
	test	eax,	eax

	jz	no_support

这里会调用函数support_long_mode,检测是否支持IA-32e模式,如果支持的话,就会继续运行,进行切换,如果不支持的话,那么就会跳转到no_support进行处理。

如果不支持,就直接挂起,如下:

;=======	no support

no_support:
	jmp	$

检测是否支持long_mode的函数:

;=======	test support long mode or not

support_long_mode:

	mov	eax,	0x80000000
	cpuid
	cmp	eax,	0x80000001
	setnb	al	
	jb	support_long_mode_done
	mov	eax,	0x80000001
	cpuid
	bt	edx,	29
	setc	al
support_long_mode_done:
	
	movzx	eax,	al
	ret

这个程序的大概意思就是检查cpuid,如果这个号码大于等于0x80000001就说明可以支持long_mode,那么我们就直接跳转到support_long_mode_done,如果不行的话,需要检查这个号码的第29位,如果29位符合预期,那么说明也是可以支持的,否则就不行。

这个具体的我也没去了解了,因为这个就更像是cpu在制作的时候的一些约定俗成的东西,去了解起来比较费劲,而且没有太大意义。

为IA-32e模式配置临时页目录和页表项

;=======	init temporary page table 0x90000

	mov	dword	[0x90000],	0x91007
	mov	dword	[0x90004],	0x00000
	mov	dword	[0x90800],	0x91007
	mov	dword	[0x90804],	0x00000

	mov	dword	[0x91000],	0x92007
	mov	dword	[0x91004],	0x00000

	mov	dword	[0x92000],	0x000083
	mov	dword	[0x92004],	0x000000

	mov	dword	[0x92008],	0x200083
	mov	dword	[0x9200c],	0x000000

	mov	dword	[0x92010],	0x400083
	mov	dword	[0x92014],	0x000000

	mov	dword	[0x92018],	0x600083
	mov	dword	[0x9201c],	0x000000

	mov	dword	[0x92020],	0x800083
	mov	dword	[0x92024],	0x000000

	mov	dword	[0x92028],	0xa00083
	mov	dword	[0x9202c],	0x000000

我们这里就不去探究这个玩意的细节了,后面我们会详细的看这块的。

重新加载全局描述表GDT

;=======	load GDTR

	db	0x66
	lgdt	[GdtPtr64]
	mov	ax,	0x10
	mov	ds,	ax
	mov	es,	ax
	mov	fs,	ax
	mov	gs,	ax
	mov	ss,	ax

	mov	esp,	7E00h

这个也是和进入保护模式一抹一样的

开启cr4寄存器的PAE标识位、设置CR3控制寄存器

;=======	open PAE

	mov	eax,	cr4
	bts	eax,	5
	mov	cr4,	eax

;=======	load	cr3

	mov	eax,	0x90000
	mov	cr3,	eax

别问我为什么讲的不仔细,问就是没学会(能看得懂汇编,但是不懂intel家的cpu)

开启长模式(IA-32e),操作CR0

;=======	enable long-mode

	mov	ecx,	0C0000080h		;IA32_EFER
	rdmsr

	bts	eax,	8
	wrmsr

;=======	open PE and paging

	mov	eax,	cr0
	bts	eax,	0
	bts	eax,	31
	mov	cr0,	eax

从loader跳转到内核程序

jmp	SelectorCode64:OffsetOfKernelFile

这里我会了,就像是刚才从实模式跳转到现在的保护模式一样,我们将处理器的状态进行切换后,运行的程序,仍然是上一状态的程序,这种状态就叫做兼容模式,比如:32位状态下运行16位程序,再比如现在的64位下运行32位程序。所以我们还是要用一条远跳转指令,来真正的切换到IA-32e模式下运行程序。

验证

0x100000打断点

老规矩,我们执行完后输入sreg查看寄存器状态:

<bochs:3> sreg
es:0x0010, dh=0x00009300, dl=0x00000000, valid=1
	Data segment, base=0x00000000, limit=0x00000000, Read/Write, Accessed
cs:0x0008, dh=0x00209900, dl=0x00000000, valid=1
	Code segment, base=0x00000000, limit=0x00000000, Execute-Only, Non-Conforming, Accessed, 64-bit
ss:0x0010, dh=0x00009300, dl=0x00000000, valid=1
	Data segment, base=0x00000000, limit=0x00000000, Read/Write, Accessed
ds:0x0010, dh=0x00009300, dl=0x00000000, valid=1
	Data segment, base=0x00000000, limit=0x00000000, Read/Write, Accessed
fs:0x0010, dh=0x00009300, dl=0x00000000, valid=1
	Data segment, base=0x00000000, limit=0x00000000, Read/Write, Accessed
gs:0x0010, dh=0x00009300, dl=0x00000000, valid=1
	Data segment, base=0x00000000, limit=0x00000000, Read/Write, Accessed
ldtr:0x0000, dh=0x00008200, dl=0x0000ffff, valid=1
tr:0x0000, dh=0x00008b00, dl=0x0000ffff, valid=1
gdtr:base=0x0000000000010060, limit=0x17
idtr:base=0x0000000000000000, limit=0x3ff

然后输入quit退出,查看模式

<bochs:4> q
00015798142i[      ] dbg: Quit
00015798142i[CPU0  ] CPU is in long mode (active)
00015798142i[CPU0  ] CS.mode = 64 bit
00015798142i[CPU0  ] SS.mode = 64 bit
00015798142i[CPU0  ] EFER   = 0x00000500
00015798142i[CPU0  ] | RAX=00000000e0000011  RBX=0000000000000000
00015798142i[CPU0  ] | RCX=00000000c0000080  RDX=0000000000000000
00015798142i[CPU0  ] | RSP=0000000000007e00  RBP=00000000000008ef
00015798142i[CPU0  ] | RSI=0000000000008098  RDI=000000000000bd00
00015798142i[CPU0  ] |  R8=0000000000000000   R9=0000000000000000
00015798142i[CPU0  ] | R10=0000000000000000  R11=0000000000000000
00015798142i[CPU0  ] | R12=0000000000000000  R13=0000000000000000
00015798142i[CPU0  ] | R14=0000000000000000  R15=0000000000000000
00015798142i[CPU0  ] | IOPL=0 id vip vif ac vm rf nt of df if tf sf zf af pf cf
00015798142i[CPU0  ] | SEG sltr(index|ti|rpl)     base    limit G D
00015798142i[CPU0  ] |  CS:0008( 0001| 0|  0) 00000000 00000000 0 0
00015798142i[CPU0  ] |  DS:0010( 0002| 0|  0) 00000000 00000000 0 0
00015798142i[CPU0  ] |  SS:0010( 0002| 0|  0) 00000000 00000000 0 0
00015798142i[CPU0  ] |  ES:0010( 0002| 0|  0) 00000000 00000000 0 0
00015798142i[CPU0  ] |  FS:0010( 0002| 0|  0) 00000000 00000000 0 0
00015798142i[CPU0  ] |  GS:0010( 0002| 0|  0) 00000000 00000000 0 0
00015798142i[CPU0  ] |  MSR_FS_BASE:0000000000000000
00015798142i[CPU0  ] |  MSR_GS_BASE:0000000000000000
00015798142i[CPU0  ] | RIP=0000000000100000 (0000000000100000)
00015798142i[CPU0  ] | CR0=0xe0000011 CR2=0x0000000000000000
00015798142i[CPU0  ] | CR3=0x00090000 CR4=0x00000020
(0).[15798142] [0x000000100000] 0008:0000000000100000 (unk. ctxt): jmp .+1229783165 (0x00000000495d0082) ; e97d004d49
00015798142i[CMOS  ] Last time is 1602867948 (Sat Oct 17 01:05:48 2020)
00015798142i[XGUI  ] Exit
00015798142i[SIM   ] quit_sim called with exit code 0

这里的0015798142i[CPU0 ] CPU is in long mode (active)显示我们退出时,是长模式,也就是IA-32e模式。

makefile文件

最后来分享下我的makefile,首先是boot文件夹下的makefile

all: boot.bin loader.bin kernel.bin

boot.bin: 
# 生成boot的bin文件
	nasm boot.asm -o ./build/boot.bin 

loader.bin: fat12.inc
# 生成loader的bin文件
	nasm loader.asm -o ./build/loader.bin

kernel.bin: loader.bin
	cp ./build/loader.bin ./build/kernel.bin

clean:
# 清除
	rm -rf ./build/*

总的makefile

BOOT_SRC_DIR=./src/boot
SUBDIRS=$(BOOT_SRC_DIR)

BOOT_BUILD_DIR=$(BOOT_SRC_DIR)/build
PROJECT_BUILD_DIR=./build

all: 
	cd $(BOOT_SRC_DIR) && $(MAKE)

install: 
# 将boot的bin写入到引导扇区内 

	echo "特别声明:不要删除boot.img,如果删除了, 请到64位操作系统书中36页寻找复原方法"
	dd if=$(BOOT_BUILD_DIR)/boot.bin of=$(PROJECT_BUILD_DIR)/boot.img bs=512 count=1 conv=notrunc 
	sudo mount $(PROJECT_BUILD_DIR)/boot.img /media/ -t vfat -o loop
	sudo cp $(BOOT_BUILD_DIR)/loader.bin /media
	sudo cp $(BOOT_BUILD_DIR)/kernel.bin /media
	sync
	sudo umount /media/
	echo 挂载完成,请进入build文件夹后输入"bochs"以启动虚拟机

clean:
	cd $(SUBDIRS) && $(MAKE) clean

使用方法:

# 在根目录进行:
make clean && make 
sudo make install
cd build
bochs

可能存在的问题

假的kernel从哪里来的

有两种方法:

  • 第一种是从作者的下一阶段代码中直接编译生成
  • 第二种是从将我们的loader.bin复制一份,改名叫做kernel.bin

我使用的方法是第二种。

无法进入长模式

我出现了一个巨大的、无法debug的问题,我找了非长久的问题(是因为我的代码根本没有问题),最后发现自己的虚拟机安装有误。

我们在安装bochs的时候,需要使用config进行配置,给大家分享一个可用的配制方法:

./configure --with-x11 --with-wx --enable-debugger --enable-disasm --enable-all-optimizations --enable-readline --enable-long-phy-address --enable-ltdl-install --enable-idle-hack --enable-plugins --enable-a20-pin --enable-x86-64 --enable-smp --enable-cpu-level=6 --enable-large-ramfile --enable-repeat-speedups --enable-fast-function-calls  --enable-handlers-chaining --enable-trace-linking --enable-configurable-msrs --enable-show-ips --enable-cpp --enable-debugger-gui --enable-iodebug --enable-logging --enable-assert-checks --enable-fpu --enable-vmx=2 --enable-svm --enable-3dnow --enable-alignment-check  --enable-monitor-mwait --enable-avx  --enable-evex --enable-x86-debugger --enable-pci --enable-voodoo

这里在作者给出的配制方法上,去除了enable-usb(加上这个我会报错,就暂时不加了)

总体来说,我们需要先进入bochs源代码目录,输入:sudo make uninstall来卸载原来装的东西,然后再从新配置安装。我猜测,之前无法进入长模式是因为我没有选择:--enable-x86-64,所以我的虚拟机只有32位功能。

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值