操作系统真相还原

操作系统真相还原

1、部署工作环境

1.2 Bochs下载安装

下载

下载地址:https://sourceforge.net/projects/bochs/files/bochs/2.7/

解压:

crescentp@crescentp:~/bochs$ tar zxvf bochs-2.7.tar.gz 

编译

./configure --prefix=/os/bochs --enable-debugger --enable-disasm --enable-iodebug --enable-x86-debugger --with-x --with-x11
make
make install

配置bochs

可以在安装目录下查看配置文件的样本文件

cat share/doc/bochs/bochsrc-sample.txt

image-20211014103441538

在 bin目录下编写bochsrc.disk代码

#############################################
# Configuration file for Bochs
# By CP
#############################################
# 第一步,首先设置Bochs在运行中过程中能够使用的内存,本列为32MB
# 关键字为:megs
megs: 32

# 第二步:设置对应真实机器的BIOS和VGA BIOS
# 对应的两个关键字为: romimage 和 vgaromimage
romimage: file=/os/bochs/share/bochs/BIOS-bochs-latest
vgaromimage: file=/os/bochs/share/bochs/VGABIOS-lgpl-latest

# 第三步,设置Bochs所使用的的磁盘,软盘的关键字为floppy
# 若只有一个软盘,则使用的floppya即可,若有多个,则为floppya,floppyb
# floppya: 1_44=a.img, status=inserted

# 第四步,选择启动盘符
#boot: floppy #默认从软盘启动
boot: disk   #改为从硬盘启动,我们的任何代码都将直接写在硬盘上,所以不会再有读写软盘的操作了

# 第五步,设置日志文件的输出
log: bochs.out

# 第六步,开启或关闭某些功能
# 下面是关闭鼠标,并且打开键盘
mouse: enabled=0
# keyboard_mapping: enabled=1,
keyboard:keymap=/os/bochs/share/bochs/keymaps/x11-pc-us.map

# 硬盘设置
ata0: enabled=1, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14
# ata0-master: type=disk,path="hd60M.img",mode=flat

# 下面的是增加的bochs对gdb的支持,这样gdb便可以远程连接到此机器的1234端口调试了
gdbstub: enabled=1, port=1234, text_base=0, data_base=0, bss_base=0

运行

bochs

image-20211014113017397

image-20211014113030925

image-20211014113114789

配置启动盘

没有启动盘,那我们就配置启动盘呗

/bin/bximage 是创建启动盘的工具

image-20211014113445746

bin/bximage -hd=60M  -q hd60M.img

image-20211014114553006

image-20211014114800713

image-20211014120007435

2、编写MBR

mbr.S

;主引导程序
;---------------------------------------------------------------------
SECTION MBR vstart=0x7c00	; 表示在本程序编译时,告诉编译器,把我的其实地址编译0x7c00
	mov ax,cs		; 这一块是用cs寄存器的值,去初始化其它的寄存器
	mov ds,ax		; 由于BIOS是通过 jmp 0:0x7c00 跳转到MBR的,故此时cs为0
	mov es,ax
	mov ss,ax
	mov fs,ax
	mov sp,0x7c00		;初始化栈指针,0x7c00以下是安全的区域,可以用来当栈使用

;清屏利用0x6号功能,上卷全部行,即可清屏
;---------------------------------------------------------------------
;INT 0x10 功能号:0x06 功能描述:上卷屏幕
;---------------------------------------------------------------------
;输入;
;AH 功能号 = 0x06
;AL = 上卷的行数(如果为0,表示全部)
;BH = 上卷行的属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值
	mov ax,0x600
	mov bx,0x700
	mov cx,0	;左上角:(0,0)
	mov dx,0x184f	;右下角:(80,25)
			;VGA文本模式中,一行只能容纳80个字符,共25行
			;下标从0开始,所以0x18=24,0x4f=79
	int 0x10	;int 0x10

;;;;;;;;	下面这三行代码获取光标位置	;;;;;;;;;;;;;
; .get_cursor 获取当前光标位置,在光标位置处打印字符
	mov ah,3	;输入:3号子功能是获取光标位置,需要存入ah寄存器
	mov bh,0	;bh寄存器存储的是待获取光标的页号
	
	int 0x10	;输出,ch=光标开始行,cl=光标结束行
			;dh=光标所在行号,dl=光标所在列号

;;;;;;;;	获取光标结束位置	;;;;;;;;;;;;

;;;;;;;		打印字符串		;;;;;;;;;;;;
	;还是用10h中断,不过这次调用13号子功能打印字符串
	mov ax,message
	mov bp,ax	;es:bp为串首地址,es此时和cs一致
			;开头已经为sreg初始化

	;光标位置要用到dx寄存器中的内容,cx中光标位置可忽略
	mov cx,5	;cx为串长度,不包括结束符0的字符个数
	mov ax,0x1301	;子功能号13显示字符及属性,要出入ah寄存器
			;al设置写字符方式 ah = 01;显示字符串,光标跟随移动
	mov bx,0x2	;bh存储的是要显示的页号,此处是第0页
			;bl是字符属性,属性为黑底绿字(bl = 02h)
	int 0x10	;执行BIOS 0x10中断
;;;;;;;;	打印字符串结束		;;;;;;;;;;;;;;

	jmp $				;是程序悬停在此处

	message db "1 MBR"
	times 510-($-$$) db 0	; $$ 是本section的地址,$是本行的地址,$-$$是本行到本section的偏移量,最后两个字节是确定的,这就把本扇区的剩余量补0
	db 0x55,0xaa

编译

nasm -o mbr.bin mbr.S 

image-20211014143307822

将mbr.bin转载进入磁盘的第一个扇区

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

运行

bin/bochs -f bin/bochsrc.disk

image-20211014144313543

3、完善MBR

我们不适用int10中断来显示MBR,而是通过操控显存的方式

;主引导程序
;
;LOADER_BASE_ADDR equ 0xA00
;LOADER_START_SECTOR equ 0x2
;---------------------------------------------------------------------
SECTION MBR vstart=0x7c00	; 表示在本程序编译时,告诉编译器,把我的其实地址编译0x7c00
	mov ax,cs		; 这一块是用cs寄存器的值,去初始化其它的寄存器
	mov ds,ax		; 由于BIOS是通过 jmp 0:0x7c00 跳转到MBR的,故此时cs为0
	mov es,ax
	mov ss,ax
	mov fs,ax
	mov sp,0x7c00		;初始化栈指针,0x7c00以下是安全的区域,可以用来当栈使用
	mov ax,0xb800		;显存文本模式中,其内存地址是0xb8000
	mov gs,ax	

;清屏
;利用0x6号功能,上卷全部行,即可清屏
;---------------------------------------------------------------------
;INT 0x10 功能号:0x06 功能描述:上卷屏幕
;---------------------------------------------------------------------
;输入;
;AH 功能号 = 0x06
;AL = 上卷的行数(如果为0,表示全部)
;BH = 上卷行的属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值
	mov ax,0x600
	mov bx,0x700
	mov cx,0	;左上角:(0,0)
	mov dx,0x184f	;右下角:(80,25)
			;VGA文本模式中,一行只能容纳80个字符,共25行
			;下标从0开始,所以0x18=24,0x4f=79
	int 0x10	;int 0x10

	;输出背景色绿色,前景色红色,并且跳动的字符串“1 MBR”
	mov byte [gs:0x00],'1'
	mov byte [gs:0x01],0xA4	;A表示绿色背景闪烁,4表示前景位红色

	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 $				;是程序悬停在此处

	message db "1 MBR"
	times 510-($-$$) db 0	; $$ 是本section的地址,$是本行的地址,$-$$是本行到本section的偏移量,最后两个字节是确定的,这就把本扇区的剩余量补0
	db 0x55,0xaa

编译

nasm -o mbr.bin mbr.S 

将mbr.bin装载进入磁盘的第一个扇区

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

运行

bin/bochs -f bin/bochsrc.disk

image-20211015093448087

image-20211015093457589

可见这是跳动的

接下来我们通过从硬盘中读取加载信息,将内核加载器读入到内存,

mbr.S

;主引导程序
;------------------------------------------------------------------------------
%include "boot.inc"		;让编译器在编译之前,把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

;清屏
;利用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

	;输出字符串MBR
	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		;A表示绿色背景闪烁,4表示前景颜色为红色

	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	;起始扇区lba地址,0x2
	mov bx,LOADER_BASE_ADDR		;写入的地址,0x900
	mov cx,1			;待写入的扇区数
	call rd_disk_m_16		;以下读取程序的起始部分(一个扇区)

	jmp LOADER_BASE_ADDR

;-------------------------------------------------------------------------------------
;功能:读取硬盘n个扇区
rd_disk_m_16:
;-------------------------------------------------------------------------------------
					;eax=LBA扇区号
					;bx=将数据写入的内存地址
					;cx=读入的扇区数
	mov esi,eax			;备份eax,因为al在out命令中会使用,会影响到eax的低8位
	mov di,cx			;备份cx,cx在读数据的时候会使用到
;读写硬盘
;第一步:设置要读取的扇区数
	mov dx,0x1f2			;虚拟硬盘属于ata0,是Primary通道,所以sector count 是由0x1f2访问
	mov al,cl			;cl是cx的低8位,就读一个扇区,这样就能传过去了
	out dx,al			;读取的扇区数,sector count 记录要读取的扇盘数量

	mov eax,esi			;恢复eax,现在eax存的是其实扇区lba的地址,0x2,第二个扇区

;第二步:将LBA地址存入 0x1f3 ~ 0x1f6

	;LBA地址 7~0 位写入端口 0x1f3
	mov dx,0x1f3			;LBA low
	out dx,al			;eax的第8位,就是al

	;LBA地址 15~8 位写入端口 0x1f4
	mov cl,8
	shr eax,cl			;eax右移8位,让al的数,变为eax中8位
	mov dx,0x1f4			;LBA mid
	out dx,al

	;LBA地址 23~16 位写入端口 0x1f5
	shr eax,cl			;再右移8位
	mov dx,0x1f5			;LBA high
	out dx,al

	shr eax,cl			;这样al为0000
	and al,0x0f			;lba第24~27位
	or al,0xe0			;设置7~4位为1110,表示lba模式
	mov dx,0x1f6			;就是拼凑出device寄存器的值
	out dx,al

;第3步:向0x1f7端口写入读命令,0x20
	mov dx,0x1f7
	mov al,0x20
	out dx,al			;command:0x1f7,写入命令,写入的命令是读命令

;第四步:检测硬盘状态
 .not_ready:
	;同一端口,写时表示写入命令字,读时表示写入硬盘的状态,所以不需要更换dx的值
	nop				;减少打扰硬盘的工作
	in al,dx			;将Status的寄存器的值读入到al中
	and al,0x88			;第四位为1表示硬盘控制器已准备好数据传输,第七位为1表示硬盘忙,保存第4位和第7位
	cmp al,0x08			;若第4位为1,表示数据已经准备好了,若第7位为1,表示硬盘处于忙
	jnz .not_ready			;若未准备好,继续等,判断结果是否为0

;第5步,从0x1f0端口读数据
	mov ax,di			;这个时候di存的是上面备份的cx,及时要读取的扇区的数量
	mov dx,256			;每次in操作只读取两个字节,根据读入的数据总量(扇区数*512字节)
	mul dx				;dx*ax就是总数量/2,然后将值送到cx中,cx就是要in的次数
	mov cx,ax			;di为要读取的扇区数,一个扇区有512个字节,每次读入一个字,共需要di*512/2次,所以di*256

	mov dx,0x1f0
 .go_on_read:
	in ax,dx			;读入到ax中
	mov [bx],ax			;读入到bx指向的内存
	add bx,2			;每次读入2个字节
	loop .go_on_read		;cx是循环的次数
	ret

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

boot.inc

;---------- loader 和 kernel-----------------
LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2

编译:将boot.inc一起编译

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

loader.S

%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR

;输出背景色绿色,前景色红色,并且跳动的字符串"1 MBR"
mov byte [gs:0x00],'2'
mov byte [gs:0x01],0xA4

mov byte [gs:0x02],' '
mov byte [gs:0x04],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=/os/bochs/mbr.bin of=/os/bochs/hd60M.img  bs=512 count=1 conv=notrunc

加载到第二个磁盘

dd if=/os/bochs/loader.bin of=/os/bochs/hd60M.img  bs=512 count=4 conv=notrunc seek=2

运行

bin/bochs -f bin/bochsrc.disk

image-20211015122608295

4、保护模式入门

mbr.S

;主引导程序
;------------------------------------------------------------------------------
%include "boot.inc"		;让编译器在编译之前,把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

;清屏
;利用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

	;输出字符串MBR
	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		;A表示绿色背景闪烁,4表示前景颜色为红色

	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	;起始扇区lba地址,0x2
	mov bx,LOADER_BASE_ADDR		;写入的地址,0x900
	mov cx,4			;待写入的扇区数,由于loader.bin超过了512个字节,可能是多个扇区
	call rd_disk_m_16		;以下读取程序的起始部分(一个扇区)

	jmp LOADER_BASE_ADDR

;-------------------------------------------------------------------------------------
;功能:读取硬盘n个扇区
rd_disk_m_16:
;-------------------------------------------------------------------------------------
					;eax=LBA扇区号
					;bx=将数据写入的内存地址
					;cx=读入的扇区数
	mov esi,eax			;备份eax,因为al在out命令中会使用,会影响到eax的低8位
	mov di,cx			;备份cx,cx在读数据的时候会使用到
;读写硬盘
;第一步:设置要读取的扇区数
	mov dx,0x1f2			;虚拟硬盘属于ata0,是Primary通道,所以sector count 是由0x1f2访问
	mov al,cl			;cl是cx的低8位,就读一个扇区,这样就能传过去了
	out dx,al			;读取的扇区数,sector count 记录要读取的扇盘数量

	mov eax,esi			;恢复eax,现在eax存的是其实扇区lba的地址,0x2,第二个扇区

;第二步:将LBA地址存入 0x1f3 ~ 0x1f6

	;LBA地址 7~0 位写入端口 0x1f3
	mov dx,0x1f3			;LBA low
	out dx,al			;eax的第8位,就是al

	;LBA地址 15~8 位写入端口 0x1f4
	mov cl,8
	shr eax,cl			;eax右移8位,让al的数,变为eax中8位
	mov dx,0x1f4			;LBA mid
	out dx,al

	;LBA地址 23~16 位写入端口 0x1f5
	shr eax,cl			;再右移8位
	mov dx,0x1f5			;LBA high
	out dx,al

	shr eax,cl			;这样al为0000
	and al,0x0f			;lba第24~27位
	or al,0xe0			;设置7~4位为1110,表示lba模式
	mov dx,0x1f6			;就是拼凑出device寄存器的值
	out dx,al

;第3步:向0x1f7端口写入读命令,0x20
	mov dx,0x1f7
	mov al,0x20
	out dx,al			;command:0x1f7,写入命令,写入的命令是读命令

;第四步:检测硬盘状态
 .not_ready:
	;同一端口,写时表示写入命令字,读时表示写入硬盘的状态,所以不需要更换dx的值
	nop				;减少打扰硬盘的工作
	in al,dx			;将Status的寄存器的值读入到al中
	and al,0x88			;第四位为1表示硬盘控制器已准备好数据传输,第七位为1表示硬盘忙,保存第4位和第7位
	cmp al,0x08			;若第4位为1,表示数据已经准备好了,若第7位为1,表示硬盘处于忙
	jnz .not_ready			;若未准备好,继续等,判断结果是否为0

;第5步,从0x1f0端口读数据
	mov ax,di			;这个时候di存的是上面备份的cx,及时要读取的扇区的数量
	mov dx,256			;每次in操作只读取两个字节,根据读入的数据总量(扇区数*512字节)
	mul dx				;dx*ax就是总数量/2,然后将值送到cx中,cx就是要in的次数
	mov cx,ax			;di为要读取的扇区数,一个扇区有512个字节,每次读入一个字,共需要di*512/2次,所以di*256

	mov dx,0x1f0
 .go_on_read:
	in ax,dx			;读入到ax中
	mov [bx],ax			;读入到bx指向的内存
	add bx,2			;每次读入2个字节
	loop .go_on_read		;cx是循环的次数
	ret

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

boot.inc

;---------------loader和kernel   ----------

LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2

;--------------   gdt描述符属性  -------------
DESC_G_4K   equ	  1_00000000000000000000000b   	;G在段描述符第23位,将这里设为1,就是使用4K的密度
DESC_D_32   equ	   1_0000000000000000000000b	;D在段描述符第22位,将这里设为1,表示操作数使用32位
DESC_L	    equ	    0_000000000000000000000b	;L:段是否64位模式  64位代码标记,此处标记为0便可。
DESC_AVL    equ	     0_00000000000000000000b	;  cpu不用此位,暂置为0  
DESC_LIMIT_CODE2  equ 1111_0000000000000000b	;段界限,段的长度
DESC_LIMIT_DATA2  equ DESC_LIMIT_CODE2		;
DESC_LIMIT_VIDEO2  equ 0000_000000000000000b	;段界限,设为0
DESC_P	    equ		  1_000000000000000b	;p:是否在内存中
DESC_DPL_0  equ		   00_0000000000000b	;DPL,为0的权限
DESC_DPL_1  equ		   01_0000000000000b
DESC_DPL_2  equ		   10_0000000000000b
DESC_DPL_3  equ		   11_0000000000000b
DESC_S_CODE equ		     1_000000000000b	;代码段
DESC_S_DATA equ	  DESC_S_CODE			;数据段,都是1,表示非系统段
DESC_S_sys  equ		     0_000000000000b
DESC_TYPE_CODE  equ	      1000_00000000b	;x=1,c=0,r=0,a=0 代码段是可执行的,非依从的,不可读的,已访问位a清0.  
DESC_TYPE_DATA  equ	      0010_00000000b	;x=0,e=0,w=1,a=0 数据段是不可执行的,向上扩展的,可写的,已访问位a清0.

DESC_CODE_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_CODE2 + DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE + 0x00				     ;代码段的平坦模型
DESC_DATA_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x00
DESC_VIDEO_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x0b

;--------------   选择子属性  ---------------
RPL0  equ   00b					;访问权限
RPL1  equ   01b
RPL2  equ   10b
RPL3  equ   11b
TI_GDT	 equ   000b				;访问GDT
TI_LDT	 equ   100b				;访问LDT

loader.S

	%include "boot.inc"
	section loader vstart=LOADER_BASE_ADDR
	LOADER_STACK_TOP equ LOADER_BASE_ADDR	;保护模式下的栈
	jmp loader_start

;构建 gdt 及其内部的描述符
	GDT_BASE: dd 0x00000000		;GDT的第0个段不可用
		  dd 0x00000000
	; 定义了三个有用的段描述符
	CODE_DESC: dd 0x0000FFFF	;段描述符的低4字节,其中的低2字节是段长度 FFFF,高2字节是段基址 0000,
		  dd DESC_CODE_HIGH4	;代码段的高四节,已经定义好了

	DATA_STACK_DESC: dd 0x0000FFFF	;数据段和栈段的段描述符
			 dd DESC_DATA_HIGH4
;显存段描述符,0xb8000~0xbffff是用于文本模式的显示内存,段基址:0x8000 长度 0007
VIDEO_DESC:	  dd 0x80000007		;limit=(0xbffff-0xb8000)/4k = 0x7	4k是段粒度
		  dd DESC_VIDEO_HIGH4	;此时dpl为0

	GDT_SIZE equ $ - GDT_BASE	;19~20 获取GDT的大小,为加载GDT做准备
	GDT_LIMIT equ GDT_SIZE - 1
	times 60 dq 0			;此处预留60个描述符的空位,为了将来往GDT中添加其它描述符,提前保留空间
	SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0	;相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0,构建代码段的段选择子,段描述符+TI+PRL
	SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0	;同上
	SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0	;同上

	;以下是gdt的指针,前2字节是gdt界限,后四字节是gdt起始地址

	gdt_ptr dw GDT_LIMIT		;定义GDT的指针.前16位,是GDT以字节为单位的长度,也就是GDT大小减1
		dd GDT_BASE		;GDT的起始位置
	loadermsg db '2 loader in real.'

	loader_start:

;--------------------------------------------------
;INT 0x10 功能号:0x13 功能描述:打印字符串
;--------------------------------------------------
;输入
;AH 子功能号=13H
;BH = 页码
;BL = 属性(若AL=00H或01H)
;CX = 字符串的长度
;(DH,DL) = 坐标(行,列)
;ES:BP=字符串的地址
;AL=显示输出方式
;	0--字符串中只含显示字符,其显示属性在BL中,显示后,光标位置不变
;	1--字符串中只含显示字符,其显示属性在BL中,显示后,光标位置改变
;	2--字符串中含显示字符和显示属性.显示后,光标位置不变
;	3--字符串中含显示字符和显示休息,显示后,光标位置改变
;无返回值
	mov sp,LOADER_BASE_ADDR
	mov bp,loadermsg		;ES:BP = 字符串地址
	mov cx,17			;字符串长度
	mov ax,0x1301			;AH = 13,AL=01h
	mov bx,0x001f			;页号为0(BH = 0) 蓝底粉红字(BL = 1fh)
	mov dx,0x1800			;dh=18h=24行,dl=0,最后一行的行首
	int 0x10			;10h 号中断

;------------------------   准备进入保护模式	--------------------------
;1 打开A20
;2 加载gdt
;3 将cr0的pe位置1


	;----------------    打开A20	----------------------------------
	in al,0x92
	or al,0000_0010B
	out 0x92,al

	;----------------     加载GDT	----------------------------------
	lgdt [gdt_ptr]


	;----------------     cr0 第0位置1 -------------------------------
	mov eax,cr0
	or eax,0x00000001
	mov cr0,eax

	jmp dword SELECTOR_CODE:p_mode_start	;刷新流水线


[bits 32]
p_mode_start:
	mov ax,SELECTOR_DATA	;83~89 用选择子初始化成各段寄存器
	mov ds,ax
	mov es,ax
	mov ss,ax
	mov esp,LOADER_STACK_TOP
	mov ax,SELECTOR_VIDEO
	mov gs,ax

	mov byte [gs:160], 'P'

	jmp $

编译:

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

装载至磁盘

dd if=/os/bochs/mbr.bin of=/os/bochs/hd60M.img  bs=512 count=1 conv=notrunc
dd if=/os/bochs/loader.bin of=/os/bochs/hd60M.img  bs=512 count=4 conv=notrunc seek=2

运行

bin/bochs -f bin/bochsrc.disk

5、保护模式进阶、向内核迈进

mbr.S

;主引导程序
;------------------------------------------------------------------------------
%include "boot.inc"		;让编译器在编译之前,把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

;清屏
;利用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

	;输出字符串MBR
	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		;A表示绿色背景闪烁,4表示前景颜色为红色

	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	;起始扇区lba地址,0x2
	mov bx,LOADER_BASE_ADDR		;写入的地址,0x900
	mov cx,4			;待写入的扇区数,由于loader.bin超过了512个字节,可能是多个扇区
	call rd_disk_m_16		;以下读取程序的起始部分(一个扇区)

	jmp LOADER_BASE_ADDR

;-------------------------------------------------------------------------------------
;功能:读取硬盘n个扇区
rd_disk_m_16:
;-------------------------------------------------------------------------------------
					;eax=LBA扇区号
					;bx=将数据写入的内存地址
					;cx=读入的扇区数
	mov esi,eax			;备份eax,因为al在out命令中会使用,会影响到eax的低8位
	mov di,cx			;备份cx,cx在读数据的时候会使用到
;读写硬盘
;第一步:设置要读取的扇区数
	mov dx,0x1f2			;虚拟硬盘属于ata0,是Primary通道,所以sector count 是由0x1f2访问
	mov al,cl			;cl是cx的低8位,就读一个扇区,这样就能传过去了
	out dx,al			;读取的扇区数,sector count 记录要读取的扇盘数量

	mov eax,esi			;恢复eax,现在eax存的是其实扇区lba的地址,0x2,第二个扇区

;第二步:将LBA地址存入 0x1f3 ~ 0x1f6

	;LBA地址 7~0 位写入端口 0x1f3
	mov dx,0x1f3			;LBA low
	out dx,al			;eax的第8位,就是al

	;LBA地址 15~8 位写入端口 0x1f4
	mov cl,8
	shr eax,cl			;eax右移8位,让al的数,变为eax中8位
	mov dx,0x1f4			;LBA mid
	out dx,al

	;LBA地址 23~16 位写入端口 0x1f5
	shr eax,cl			;再右移8位
	mov dx,0x1f5			;LBA high
	out dx,al

	shr eax,cl			;这样al为0000
	and al,0x0f			;lba第24~27位
	or al,0xe0			;设置7~4位为1110,表示lba模式
	mov dx,0x1f6			;就是拼凑出device寄存器的值
	out dx,al

;第3步:向0x1f7端口写入读命令,0x20
	mov dx,0x1f7
	mov al,0x20
	out dx,al			;command:0x1f7,写入命令,写入的命令是读命令

;第四步:检测硬盘状态
 .not_ready:
	;同一端口,写时表示写入命令字,读时表示写入硬盘的状态,所以不需要更换dx的值
	nop				;减少打扰硬盘的工作
	in al,dx			;将Status的寄存器的值读入到al中
	and al,0x88			;第四位为1表示硬盘控制器已准备好数据传输,第七位为1表示硬盘忙,保存第4位和第7位
	cmp al,0x08			;若第4位为1,表示数据已经准备好了,若第7位为1,表示硬盘处于忙
	jnz .not_ready			;若未准备好,继续等,判断结果是否为0

;第5步,从0x1f0端口读数据
	mov ax,di			;这个时候di存的是上面备份的cx,及时要读取的扇区的数量
	mov dx,256			;每次in操作只读取两个字节,根据读入的数据总量(扇区数*512字节)
	mul dx				;dx*ax就是总数量/2,然后将值送到cx中,cx就是要in的次数
	mov cx,ax			;di为要读取的扇区数,一个扇区有512个字节,每次读入一个字,共需要di*512/2次,所以di*256

	mov dx,0x1f0
 .go_on_read:
	in ax,dx			;读入到ax中
	mov [bx],ax			;读入到bx指向的内存
	add bx,2			;每次读入2个字节
	loop .go_on_read		;cx是循环的次数
	ret

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

loader.S

	%include "boot.inc"
	section loader vstart=LOADER_BASE_ADDR
	LOADER_STACK_TOP equ LOADER_BASE_ADDR	;保护模式下的栈

;构建 gdt 及其内部的描述符
	GDT_BASE: dd 0x00000000		;GDT的第0个段不可用
		  dd 0x00000000
	; 定义了三个有用的段描述符
	CODE_DESC: dd 0x0000FFFF	;段描述符的低4字节,其中的低2字节是段长度 FFFF,高2字节是段基址 0000,
		  dd DESC_CODE_HIGH4	;代码段的高四节,已经定义好了

	DATA_STACK_DESC: dd 0x0000FFFF	;数据段和栈段的段描述符
			 dd DESC_DATA_HIGH4
;显存段描述符,0xb8000~0xbffff是用于文本模式的显示内存,段基址:0x8000 长度 0007
VIDEO_DESC:	  dd 0x80000007		;limit=(0xbffff-0xb8000)/4k = 0x7	4k是段粒度
		  dd DESC_VIDEO_HIGH4	;此时dpl为0

	GDT_SIZE equ $ - GDT_BASE	;19~20 获取GDT的大小,为加载GDT做准备
	GDT_LIMIT equ GDT_SIZE - 1
	times 60 dq 0			;此处预留60个描述符的空位,为了将来往GDT中添加其它描述符,提前保留空间
	SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0	;相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0,构建代码段的段选择子,段描述符+TI+PRL
	SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0	;同上
	SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0	;同上
	;total_mem_bytes用于保存内存容量,以字节为单位,此位置比较好记
	;当前偏移 loader.bin 文件头0x200字节 loader.bin的加载地址是0x900	4个段描述符的定义 32 dq了60个 480 = 512 = 0x200
	;故total_mem_bytes内存中的地址是0xb00 将来在内核咱们会引用此地址
	total_mem_bytes dd 0		;存储获取到的内存容量
	;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

	;以下是gdt的指针,前2字节是gdt界限,后四字节是gdt起始地址
	gdt_ptr dw GDT_LIMIT		;定义GDT的指针.前16位,是GDT以字节为单位的长度,也就是GDT大小减1
		dd GDT_BASE		;GDT的起始位置

;人工对齐:total_mem_bytes 4+gdt_ptr 6+ards_buf 244+ards_nr2,共256字节
	ards_buf times 244 db 0
	ards_nr dw 0			;用于记录ARDS结构体数量,每执行一次中断就会获得一个ards结构的数据

	loader_start:

;int 15h eax = 0000E820h,edx = 534D4150h('SMAP')获取内存布局

	xor ebx,ebx			;第一次调用ebx要为0
	mov edx,0x543d4150		;edx只赋值一次,循环体中不改变
	mov di,ards_buf			;ards结构缓冲区,es:di,BIOS将获取到的内存信息写到此寄存器指向的内存,每次都以ARDS格式返回
.e820_mem_get_loop:			;循环获取每个ARDS内存范围描述结构
	mov eax,0x0000e820		;子功能号,eax用来指定子功能号,每次执行 int 0x15后,eax值会变为0x534d4150
	mov ecx,20			;ARDS结构的字节大小,ARDS地址范围描述符是20字节
	int 0x15
	jc .e820_failed_so_try_e801	;若cf位为1啧有错误发生,尝试0xe801子功能
	add di,cx			;使di增加20字节指向缓冲区中新ARDS结构位置
	inc word [ards_nr]		;记录ARDS数量
	cmp ebx,0			;ebx存放后续值,下一个ARDS的地址,在cf为0的情况下,若ebx为0,表示这是最后一个ARDS结构
	jnz .e820_mem_get_loop

;在只有ards结构中,找出(base_add_low + length_low)的最大值,即内存大小
	mov cx,[ards_nr]		;遍历每一个ARDS结构体,循环次数是ARDS的数量
	mov ebx,ards_buf
	xor edx,edx			;edx为最大的内存容量,在此先清0
.find_max_mem_area:			;无需判断type是否为1,最大内存块一定是可被使用的
	mov eax,[ebx]			;base_add_low
	add eax,[ebx+8]			;length_low
	add ebx,20			;指向缓冲区中下一个ARDS结构
	cmp edx,eax			;冒泡排序,找出最大,edx寄存器始终是最大的内存容量
	jge .next_ards
	mov edx,eax
.next_ards:
	loop .find_max_mem_area
	jmp .mem_get_ok
	
;-----------	int 15h ax = E801h 获取内存大小,最大支持4G -------------------
; 返回后,ax cx 值一样,以KB为单位,bx dx 值都一样,以64KB为单位
; 在ax和cx寄存器中为低16MB,在bx和dx寄存器中为16Mb到4GB
.e820_failed_so_try_e801:
	mov ax,0xe801			;功能号
	int 0x15
	jc .e801_failed_so_try88	;若当前e801方法失败,就尝试0x88方法

;1. 先算出低15MB的内存,ax和cx中是以KB为单位的内存数量,将其转化成以byte为单位
	mov cx,0x400			;cx和ax一样,cx用作乘数
	mul cx
	shl edx,16
	and eax,0x0000FFFF
	or edx,eax
	add edx,0x100000		;ax只是15MB,故要加1MB
	mov esi,edx			;先把低15MB的内存容量存入esi寄存器备份

;2. 再将16MB以上的内存转化为byte为单位,寄存器bx和dx中是以64KB为单位的内存数量
	xor eax,eax
	mov ax,bx
	mov ecx,0x10000			;0x10000 十进制为 64KB
	mul ecx				;32位乘法,默认的被乘数是eax,积为64位,高32位存入edx,低32位存入eax
	add esi,eax			;由于此方法只能测出4GB以内的内存,故32位eax足够了 edx肯定为0
	mov edx,esi			;edx为总内存大小
	jmp .mem_get_ok

;-----------	int 15h	ah = 0x88 获取内存大小,只能获取64MB之内	-----------------------
.e801_failed_so_try88:
	;int 15后,ax存入的是以KB为单位的内存容量
	mov ah,0x88
	int 0x15
	;jc .error_hlr
	and eax,0x0000FFFF

	;16位乘法,被乘数是ax,积为32位.积的高16位在dx中,积的低16位在ax中
	mov cx,0x400			;0x400等于1024,将ax中的内存容量换为byte为三维
	mul cx
	shl edx,16			;把dx移到高16位
	or edx,eax			;把积的低16位组合到edx,为32位的积
	add edx,0x100000		;0x88子功能只会返回1MB以上的内存,故实际内存要加上1MB

.mem_get_ok:
	mov [total_mem_bytes],edx	;将内存换成byte单位后存入total_mem_bytes处


;----------------------进入保护模式--------------------------------
;1 打开A20
;2 加载gdt
;3 将cr0的pe位置1


	;----------------    打开A20	----------------------------------
	in al,0x92
	or al,0000_0010B
	out 0x92,al

	;----------------     加载GDT	----------------------------------
	lgdt [gdt_ptr]


	;----------------     cr0 第0位置1 -------------------------------
	mov eax,cr0
	or eax,0x00000001
	mov cr0,eax

	jmp dword SELECTOR_CODE:p_mode_start	;刷新流水线


[bits 32]
p_mode_start:
	mov ax,SELECTOR_DATA	;83~89 用选择子初始化成各段寄存器
	mov ds,ax
	mov es,ax
	mov ss,ax
	mov esp,LOADER_STACK_TOP
	mov ax,SELECTOR_VIDEO
	mov gs,ax

	mov byte [gs:160], 'P'

	jmp $

boot.inc

;---------------loader和kernel   ----------

LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2

;--------------   gdt描述符属性  -------------
DESC_G_4K   equ	  1_00000000000000000000000b   	;G在段描述符第23位,将这里设为1,就是使用4K的密度
DESC_D_32   equ	   1_0000000000000000000000b	;D在段描述符第22位,将这里设为1,表示操作数使用32位
DESC_L	    equ	    0_000000000000000000000b	;L:段是否64位模式  64位代码标记,此处标记为0便可。
DESC_AVL    equ	     0_00000000000000000000b	;  cpu不用此位,暂置为0  
DESC_LIMIT_CODE2  equ 1111_0000000000000000b	;段界限,段的长度
DESC_LIMIT_DATA2  equ DESC_LIMIT_CODE2		;
DESC_LIMIT_VIDEO2  equ 0000_000000000000000b	;段界限,设为0
DESC_P	    equ		  1_000000000000000b	;p:是否在内存中
DESC_DPL_0  equ		   00_0000000000000b	;DPL,为0的权限
DESC_DPL_1  equ		   01_0000000000000b
DESC_DPL_2  equ		   10_0000000000000b
DESC_DPL_3  equ		   11_0000000000000b
DESC_S_CODE equ		     1_000000000000b	;代码段
DESC_S_DATA equ	  DESC_S_CODE			;数据段,都是1,表示非系统段
DESC_S_sys  equ		     0_000000000000b
DESC_TYPE_CODE  equ	      1000_00000000b	;x=1,c=0,r=0,a=0 代码段是可执行的,非依从的,不可读的,已访问位a清0.  
DESC_TYPE_DATA  equ	      0010_00000000b	;x=0,e=0,w=1,a=0 数据段是不可执行的,向上扩展的,可写的,已访问位a清0.

DESC_CODE_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_CODE2 + DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE + 0x00				     ;代码段的平坦模型
DESC_DATA_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x00
DESC_VIDEO_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x0b

;--------------   选择子属性  ---------------
RPL0  equ   00b					;访问权限
RPL1  equ   01b
RPL2  equ   10b
RPL3  equ   11b
TI_GDT	 equ   000b				;访问GDT
TI_LDT	 equ   100b				;访问LDT

编译:

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

装载至磁盘

dd if=/os/bochs/mbr.bin of=/os/bochs/hd60M.img  bs=512 count=1 conv=notrunc
dd if=/os/bochs/loader.bin of=/os/bochs/hd60M.img  bs=512 count=4 conv=notrunc seek=2

运行

bin/bochs -f bin/bochsrc.disk

loader.S

自己写的,注释很全,但是可能有点问题

	%include "boot.inc"
	section loader vstart=LOADER_BASE_ADDR
	LOADER_STACK_TOP equ LOADER_BASE_ADDR	;保护模式下的栈

;构建 gdt 及其内部的描述符
	GDT_BASE: dd 0x00000000		;GDT的第0个段不可用
		  dd 0x00000000
	; 定义了三个有用的段描述符
	CODE_DESC: dd 0x0000FFFF	;段描述符的低4字节,其中的低2字节是段长度 FFFF,高2字节是段基址 0000,
		  dd DESC_CODE_HIGH4	;代码段的高四节,已经定义好了

	DATA_STACK_DESC: dd 0x0000FFFF	;数据段和栈段的段描述符
			 dd DESC_DATA_HIGH4
;显存段描述符,0xb8000~0xbffff是用于文本模式的显示内存,段基址:0x8000 长度 0007
VIDEO_DESC:	  dd 0x80000007		;limit=(0xbffff-0xb8000)/4k = 0x7	4k是段粒度
		  dd DESC_VIDEO_HIGH4	;此时dpl为0

	GDT_SIZE equ $ - GDT_BASE	;19~20 获取GDT的大小,为加载GDT做准备
	GDT_LIMIT equ GDT_SIZE - 1
	times 60 dq 0			;此处预留60个描述符的空位,为了将来往GDT中添加其它描述符,提前保留空间
	SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0	;相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0,构建代码段的段选择子,段描述符+TI+PRL
	SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0	;同上
	SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0	;同上

	;total_mem_bytes用于保存内存容量,以字节为单位,此位置比较好记
	;当前偏移 loader.bin 文件头0x200字节 loader.bin的加载地址是0x900	4个段描述符的定义 32 dq了60个 480 = 512 = 0x200
	;故total_mem_bytes内存中的地址是0xb00 将来在内核咱们会引用此地址
	total_mem_bytes dd 0		;存储获取到的内存容量
	;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

	;以下是gdt的指针,前2字节是gdt界限,后四字节是gdt起始地址
	gdt_ptr dw GDT_LIMIT		;定义GDT的指针.前16位,是GDT以字节为单位的长度,也就是GDT大小减1
		dd GDT_BASE		;GDT的起始位置

;人工对齐:total_mem_bytes 4+gdt_ptr 6+ards_buf 244+ards_nr2,共256字节
	ards_buf times 244 db 0
	ards_nr dw 0			;用于记录ARDS结构体数量,每执行一次中断就会获得一个ards结构的数据

	loader_start:

;int 15h eax = 0000E820h,edx = 534D4150h('SMAP')获取内存布局

	xor ebx,ebx			;第一次调用ebx要为0
	mov edx,0x543d4150		;edx只赋值一次,循环体中不改变
	mov di,ards_buf			;ards结构缓冲区,es:di,BIOS将获取到的内存信息写到此寄存器指向的内存,每次都以ARDS格式返回
.e820_mem_get_loop:			;循环获取每个ARDS内存范围描述结构
	mov eax,0x0000e820		;子功能号,eax用来指定子功能号,每次执行 int 0x15后,eax值会变为0x534d4150
	mov ecx,20			;ARDS结构的字节大小,ARDS地址范围描述符是20字节
	int 0x15
	jc .e820_failed_so_try_e801	;若cf位为1啧有错误发生,尝试0xe801子功能
	add di,cx			;使di增加20字节指向缓冲区中新ARDS结构位置
	inc word [ards_nr]		;记录ARDS数量
	cmp ebx,0			;ebx存放后续值,下一个ARDS的地址,在cf为0的情况下,若ebx为0,表示这是最后一个ARDS结构
	jnz .e820_mem_get_loop

;在只有ards结构中,找出(base_add_low + length_low)的最大值,即内存大小
	mov cx,[ards_nr]		;遍历每一个ARDS结构体,循环次数是ARDS的数量
	mov ebx,ards_buf
	xor edx,edx			;edx为最大的内存容量,在此先清0
.find_max_mem_area:			;无需判断type是否为1,最大内存块一定是可被使用的
	mov eax,[ebx]			;base_add_low
	add eax,[ebx+8]			;length_low
	add ebx,20			;指向缓冲区中下一个ARDS结构
	cmp edx,eax			;冒泡排序,找出最大,edx寄存器始终是最大的内存容量
	jge .next_ards
	mov edx,eax
.next_ards:
	loop .find_max_mem_area
	jmp .mem_get_ok
	
;-----------	int 15h ax = E801h 获取内存大小,最大支持4G -------------------
; 返回后,ax cx 值一样,以KB为单位,bx dx 值都一样,以64KB为单位
; 在ax和cx寄存器中为低16MB,在bx和dx寄存器中为16Mb到4GB
.e820_failed_so_try_e801:
	mov ax,0xe801			;功能号
	int 0x15
	jc .e801_failed_so_try88	;若当前e801方法失败,就尝试0x88方法

;1. 先算出低15MB的内存,ax和cx中是以KB为单位的内存数量,将其转化成以byte为单位
	mov cx,0x400			;cx和ax一样,cx用作乘数
	mul cx
	shl edx,16
	and eax,0x0000FFFF
	or edx,eax
	add edx,0x100000		;ax只是15MB,故要加1MB
	mov esi,edx			;先把低15MB的内存容量存入esi寄存器备份

;2. 再将16MB以上的内存转化为byte为单位,寄存器bx和dx中是以64KB为单位的内存数量
	xor eax,eax
	mov ax,bx
	mov ecx,0x10000			;0x10000 十进制为 64KB
	mul ecx				;32位乘法,默认的被乘数是eax,积为64位,高32位存入edx,低32位存入eax
	add esi,eax			;由于此方法只能测出4GB以内的内存,故32位eax足够了 edx肯定为0
	mov edx,esi			;edx为总内存大小
	jmp .mem_get_ok

;-----------	int 15h	ah = 0x88 获取内存大小,只能获取64MB之内	-----------------------
.e801_failed_so_try88:
	;int 15后,ax存入的是以KB为单位的内存容量
	mov ah,0x88
	int 0x15
	jc .error_hlt
	and eax,0x0000FFFF

	;16位乘法,被乘数是ax,积为32位.积的高16位在dx中,积的低16位在ax中
	mov cx,0x400			;0x400等于1024,将ax中的内存容量换为byte为三维
	mul cx
	shl edx,16			;把dx移到高16位
	or edx,eax			;把积的低16位组合到edx,为32位的积
	add edx,0x100000		;0x88子功能只会返回1MB以上的内存,故实际内存要加上1MB

.mem_get_ok:
	mov [total_mem_bytes],edx	;将内存换成byte单位后存入total_mem_bytes处


;----------------------进入保护模式--------------------------------
;1 打开A20
;2 加载gdt
;3 将cr0的pe位置1

	;----------------    打开A20	----------------------------------
	in al,0x92
	or al,0000_0010B
	out 0x92,al

	;----------------     加载GDT	----------------------------------
	lgdt [gdt_ptr]

	;----------------     cr0 第0位置1 -------------------------------
	mov eax,cr0
	or eax,0x00000001
	mov cr0,eax

	jmp dword SELECTOR_CODE:p_mode_start	;刷新流水线,这将导致之前做的预测失效,从而起到了刷新的作用

.error_hlt:					;出错则挂起
	hlt

[bits 32]
p_mode_start:
	mov ax,SELECTOR_DATA	;83~89 用选择子初始化成各段寄存器
	mov ds,ax
	mov es,ax
	mov ss,ax
	mov esp,LOADER_STACK_TOP
	mov ax,SELECTOR_VIDEO
	mov gs,ax

;创建页目录表及初始化也内存位图
call setup_page

;要将描述符表位置及偏移量写入内存 gdt_ptr,一会用新位置重新加载
sgdt [gdt_ptr]			;存储到原来gdt所有的位置

;将gdt描述符中显存段描述符中的段基址+0xc0000000 使其成为内核所在的高地址,0xc0000000~0xfffffff是内核地址
mov ebx,[gdt_ptr+2]			;前两字节是偏移量,后面四个字节才是GDT基址
or dword [ebx + 0x18 + 4],0xc0000000	;显存段是第3个段描述符,每个段描述符是8字节,故是0x18
;段描述符的高四节字节的最高位是段基址的31~24位

;将gdt的基址加上0xc0000000使其成为内核所在的高地址
add dword [gdt_ptr + 2],0xc0000000

add esp,0xc0000000			;将栈地址同样映射到内核地址

;把页目录地址赋给cr3
mov eax,PAGE_DIR_TABLE_POS
mov cr3,eax

;打开cr0的pg位(第31位)
mov eax,cr0
or eax,0x80000000
mov cr0,eax

;在开启分页后,用gdt新的地址重新加载
lgdt [gdt_ptr]				;重新加载

mov byte [gs:160],'V'			;显存段段基址已经被更新,用字符V表示virtual addr

jmp $

;---------------------- 创建页目录及页表  ----------------------------------
setup_page:
;先把页目录占用的空间逐字节清0
	mov ecx,4096			;一个页目录项4Byte,一共1024个,所以是4096个
	mov esi,0
.clear_page_dir:
	mov byte [PAGE_DIR_TABLE_POS + esi],0
	inc esi
	loop .clear_page_dir

;开始创建页目录项(PDE)
.create_pde:				;创建Page Directory Entry
	mov eax,PAGE_DIR_TABLE_POS	;0x10000000
	add eax,0x1000			;此时eax为第一个页表的位置及属性,0x10001000
	mov ebx,eax			;此处为ebx赋值,是为.create_pte做准备,ebx为基址

; 下面将页目录0和0xc00都存为第一个页表的地址,每个页表表示4MB内存
; 这样0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的页表
; 这是为将地址映射为内核地址做准备
	or eax,PG_US_U | PG_RW_W | PG_P		;页目录项的属性RW和P为1,US为1,表示用户属性,所有特权级都可以访问
	mov [PAGE_DIR_TABLE_POS + 0x0],eax	;第一个目录项,页目录表中第1个目录写入第一个页表的位置0x101000及属性7
	mov [PAGE_DIR_TABLE_POS + 0xc00],eax	;一个页表项占4个字节,0xc00表示第768个页表占用的页表项,0xc00以上的目录用于内核空间
	;也就是页表的 0xc0000000~0xfffffff 共计1G属于内核,0x0~0xbfffffff共计3G属于用户进程
	sub eax,0x1000				;现在是0x100007
	mov [PAGE_DIR_TABLE_POS + 4092],eax	;使最后一个目录项指向页目录表自己

;下面创建页表项(PTE)
	mov ecx,256				;1M低端内存 / 每页大小 4K = 256
	mov esi,0
	mov edx,PG_US_U | PG_RW_W | PG_P	;属性为7
.create_pte:					;创建Page Table Entry
	mov [ebx+esi*4],edx			;此时ebx已经在上面通过eax赋值为0x101000,也就是第一个页表的地址
	add edx,4096
	inc esi
	loop .create_pte

;创建内核其它表的PDE
	mov eax,PAGE_DIR_TABLE_POS
	add eax,0x2000				;此时eax为第二个页表的位置
	or eax,PG_US_U | PG_RW_W | PG_P		;页目录项的属性US RW和P位都为1
	mov ebx,PAGE_DIR_TABLE_POS
	mov ecx,254				;范围为第769~1022所有的目录项数量
	mov esi,769
.create_kernel_pde:
	mov [ebx+esi*4],eax
	inc esi
	add eax,0x1000
	loop .create_kernel_pde
	ret

loader.S

   %include "boot.inc"
   section loader vstart=LOADER_BASE_ADDR
   LOADER_STACK_TOP equ LOADER_BASE_ADDR
   
;构建gdt及其内部的描述符
   GDT_BASE:   dd    0x00000000 
	       dd    0x00000000

   CODE_DESC:  dd    0x0000FFFF 
	       dd    DESC_CODE_HIGH4

   DATA_STACK_DESC:  dd    0x0000FFFF
		     dd    DESC_DATA_HIGH4

   VIDEO_DESC: dd    0x80000007	       ; limit=(0xbffff-0xb8000)/4k=0x7
	       dd    DESC_VIDEO_HIGH4  ; 此时dpl为0

   GDT_SIZE   equ   $ - GDT_BASE
   GDT_LIMIT   equ   GDT_SIZE -	1 
   times 60 dq 0					 ; 此处预留60个描述符的空位(slot)
   SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0         ; 相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0
   SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0	 ; 同上
   SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0	 ; 同上 

   ; total_mem_bytes用于保存内存容量,以字节为单位,此位置比较好记。
   ; 当前偏移loader.bin文件头0x200字节,loader.bin的加载地址是0x900,
   ; 故total_mem_bytes内存中的地址是0xb00.将来在内核中咱们会引用此地址
   total_mem_bytes dd 0					 
   ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

   ;以下是定义gdt的指针,前2字节是gdt界限,后4字节是gdt起始地址
   gdt_ptr  dw  GDT_LIMIT 
	    dd  GDT_BASE

   ;人工对齐:total_mem_bytes4字节+gdt_ptr6字节+ards_buf244字节+ards_nr2,共256字节
   ards_buf times 244 db 0
   ards_nr dw 0		      ;用于记录ards结构体数量

   loader_start:
   
;-------  int 15h eax = 0000E820h ,edx = 534D4150h ('SMAP') 获取内存布局  -------

   xor ebx, ebx		      ;第一次调用时,ebx值要为0
   mov edx, 0x534d4150	      ;edx只赋值一次,循环体中不会改变
   mov di, ards_buf	      ;ards结构缓冲区
.e820_mem_get_loop:	      ;循环获取每个ARDS内存范围描述结构
   mov eax, 0x0000e820	      ;执行int 0x15后,eax值变为0x534d4150,所以每次执行int前都要更新为子功能号。
   mov ecx, 20		      ;ARDS地址范围描述符结构大小是20字节
   int 0x15
   jc .e820_failed_so_try_e801   ;若cf位为1则有错误发生,尝试0xe801子功能
   add di, cx		      ;使di增加20字节指向缓冲区中新的ARDS结构位置
   inc word [ards_nr]	      ;记录ARDS数量
   cmp ebx, 0		      ;若ebx为0且cf不为1,这说明ards全部返回,当前已是最后一个
   jnz .e820_mem_get_loop

;在所有ards结构中,找出(base_add_low + length_low)的最大值,即内存的容量。
   mov cx, [ards_nr]	      ;遍历每一个ARDS结构体,循环次数是ARDS的数量
   mov ebx, ards_buf 
   xor edx, edx		      ;edx为最大的内存容量,在此先清0
.find_max_mem_area:	      ;无须判断type是否为1,最大的内存块一定是可被使用
   mov eax, [ebx]	      ;base_add_low
   add eax, [ebx+8]	      ;length_low
   add ebx, 20		      ;指向缓冲区中下一个ARDS结构
   cmp edx, eax		      ;冒泡排序,找出最大,edx寄存器始终是最大的内存容量
   jge .next_ards
   mov edx, eax		      ;edx为总内存大小
.next_ards:
   loop .find_max_mem_area
   jmp .mem_get_ok

;------  int 15h ax = E801h 获取内存大小,最大支持4G  ------
; 返回后, ax cx 值一样,以KB为单位,bx dx值一样,以64KB为单位
; 在ax和cx寄存器中为低16M,在bx和dx寄存器中为16MB到4G。
.e820_failed_so_try_e801:
   mov ax,0xe801
   int 0x15
   jc .e801_failed_so_try88   ;若当前e801方法失败,就尝试0x88方法

;1 先算出低15M的内存,ax和cx中是以KB为单位的内存数量,将其转换为以byte为单位
   mov cx,0x400	     ;cx和ax值一样,cx用做乘数
   mul cx 
   shl edx,16
   and eax,0x0000FFFF
   or edx,eax
   add edx, 0x100000 ;ax只是15MB,故要加1MB
   mov esi,edx	     ;先把低15MB的内存容量存入esi寄存器备份

;2 再将16MB以上的内存转换为byte为单位,寄存器bx和dx中是以64KB为单位的内存数量
   xor eax,eax
   mov ax,bx		
   mov ecx, 0x10000	;0x10000十进制为64KB
   mul ecx		;32位乘法,默认的被乘数是eax,积为64位,高32位存入edx,低32位存入eax.
   add esi,eax		;由于此方法只能测出4G以内的内存,故32位eax足够了,edx肯定为0,只加eax便可
   mov edx,esi		;edx为总内存大小
   jmp .mem_get_ok

;-----------------  int 15h ah = 0x88 获取内存大小,只能获取64M之内  ----------
.e801_failed_so_try88: 
   ;int 15后,ax存入的是以kb为单位的内存容量
   mov  ah, 0x88
   int  0x15
   jc .error_hlt
   and eax,0x0000FFFF
      
   ;16位乘法,被乘数是ax,积为32位.积的高16位在dx中,积的低16位在ax中
   mov cx, 0x400     ;0x400等于1024,将ax中的内存容量换为以byte为单位
   mul cx
   shl edx, 16	     ;把dx移到高16位
   or edx, eax	     ;把积的低16位组合到edx,为32位的积
   add edx,0x100000  ;0x88子功能只会返回1MB以上的内存,故实际内存大小要加上1MB

.mem_get_ok:
   mov [total_mem_bytes], edx	 ;将内存换为byte单位后存入total_mem_bytes处。


;-----------------   准备进入保护模式   -------------------
;1 打开A20
;2 加载gdt
;3 将cr0的pe位置1

   ;-----------------  打开A20  ----------------
   in al,0x92
   or al,0000_0010B
   out 0x92,al

   ;-----------------  加载GDT  ----------------
   lgdt [gdt_ptr]

   ;-----------------  cr0第0位置1  ----------------
   mov eax, cr0
   or eax, 0x00000001
   mov cr0, eax

   jmp dword SELECTOR_CODE:p_mode_start	     ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,
					     ; 这将导致之前做的预测失效,从而起到了刷新的作用。
.error_hlt:		      ;出错则挂起
   hlt

[bits 32]
p_mode_start:
   mov ax, SELECTOR_DATA
   mov ds, ax
   mov es, ax
   mov ss, ax
   mov esp,LOADER_STACK_TOP
   mov ax, SELECTOR_VIDEO
   mov gs, ax

   ; 创建页目录及页表并初始化页内存位图
   call setup_page

   ;要将描述符表地址及偏移量写入内存gdt_ptr,一会用新地址重新加载
   sgdt [gdt_ptr]	      ; 存储到原来gdt所有的位置

   ;将gdt描述符中视频段描述符中的段基址+0xc0000000
   mov ebx, [gdt_ptr + 2]  
   or dword [ebx + 0x18 + 4], 0xc0000000      ;视频段是第3个段描述符,每个描述符是8字节,故0x18。
					      ;段描述符的高4字节的最高位是段基址的31~24位

   ;将gdt的基址加上0xc0000000使其成为内核所在的高地址
   add dword [gdt_ptr + 2], 0xc0000000

   add esp, 0xc0000000        ; 将栈指针同样映射到内核地址

   ; 把页目录地址赋给cr3
   mov eax, PAGE_DIR_TABLE_POS
   mov cr3, eax

   ; 打开cr0的pg位(第31位)
   mov eax, cr0
   or eax, 0x80000000
   mov cr0, eax

   ;在开启分页后,用gdt新的地址重新加载
   lgdt [gdt_ptr]             ; 重新加载

   mov byte [gs:160], 'V'     ;视频段段基址已经被更新,用字符v表示virtual addr

   jmp $

;-------------   创建页目录及页表   ---------------
setup_page:
;先把页目录占用的空间逐字节清0
   mov ecx, 4096
   mov esi, 0
.clear_page_dir:
   mov byte [PAGE_DIR_TABLE_POS + esi], 0
   inc esi
   loop .clear_page_dir

;开始创建页目录项(PDE)
.create_pde:				     ; 创建Page Directory Entry
   mov eax, PAGE_DIR_TABLE_POS
   add eax, 0x1000 			     ; 此时eax为第一个页表的位置及属性
   mov ebx, eax				     ; 此处为ebx赋值,是为.create_pte做准备,ebx为基址。

;   下面将页目录项0和0xc00都存为第一个页表的地址,
;   一个页表可表示4MB内存,这样0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的页表,
;   这是为将地址映射为内核地址做准备
   or eax, PG_US_U | PG_RW_W | PG_P	     ; 页目录项的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问.
   mov [PAGE_DIR_TABLE_POS + 0x0], eax       ; 第1个目录项,在页目录表中的第1个目录项写入第一个页表的位置(0x101000)及属性(7)
   mov [PAGE_DIR_TABLE_POS + 0xc00], eax     ; 一个页表项占用4字节,0xc00表示第768个页表占用的目录项,0xc00以上的目录项用于内核空间,
					     ; 也就是页表的0xc0000000~0xffffffff共计1G属于内核,0x0~0xbfffffff共计3G属于用户进程.
   sub eax, 0x1000
   mov [PAGE_DIR_TABLE_POS + 4092], eax	     ; 使最后一个目录项指向页目录表自己的地址

;下面创建页表项(PTE)
   mov ecx, 256				     ; 1M低端内存 / 每页大小4k = 256
   mov esi, 0
   mov edx, PG_US_U | PG_RW_W | PG_P	     ; 属性为7,US=1,RW=1,P=1
.create_pte:				     ; 创建Page Table Entry
   mov [ebx+esi*4],edx			     ; 此时的ebx已经在上面通过eax赋值为0x101000,也就是第一个页表的地址 
   add edx,4096
   inc esi
   loop .create_pte

;创建内核其它页表的PDE
   mov eax, PAGE_DIR_TABLE_POS
   add eax, 0x2000 		     ; 此时eax为第二个页表的位置
   or eax, PG_US_U | PG_RW_W | PG_P  ; 页目录项的属性US,RW和P位都为1
   mov ebx, PAGE_DIR_TABLE_POS
   mov ecx, 254			     ; 范围为第769~1022的所有目录项数量
   mov esi, 769
.create_kernel_pde:
   mov [ebx+esi*4], eax
   inc esi
   add eax, 0x1000
   loop .create_kernel_pde
   ret

image-20211017154314786

image-20211017154255462

猜你喜欢

转载自blog.csdn.net/Destiny_159/article/details/120764425