• BIOS由硬件(只读存储器ROM)加载。

  • BIOS 的主要工作是检测、初始化硬件(通过硬件自己提供的一些初始化的功能调用)。

  • BIOS 建立了中断向量表,可以通过“int中断号”来实现相关的硬件调用。当然BIOS 建立的这些功能就是对硬件的IO 操作,也就是输入输出,但由于就64KB的空间,不可能把所有硬件的IO 操作实现得面面俱到,而且也没必要实现那么多,毕竟是在实模式之下,对硬件支持得再丰富也自搭,精彩的世界是在进入保护模式以后才开始,所以挑一些重要的、保证计算机能运行的那些硬件的基本IO 操作,就行了。这就是BIOS 称为基本输入输出系统的原因。

  • BIOS 最后一项工作校验启动盘中位于0 盘0道1 扇区的内容。(MBR 所在的位置是磁盘上最开始的那个扇区,即0 盘0道1 扇区)。

    • 如果此扇区末尾的两个字节分别是魔数0x550xaa, BIOS 便认为此扇区中确实存在可执
      行的程序.接着执行jmp 0: 0x7c00跳转至0x7c00执行MBR程序
    • 如果不是,则认为没有可执行程序。

BIOS入口地址低端1M内存最顶端的16字节。这16字节必然容不下BIOS代码,所以存放的是跳转指令
使用bochs的xp命令查看0xffff0内存处的内容(小端序):

对该地址反汇编u,验证了该跳转指令:


MBR编写

  • MBR这个程序是独立于操作系统的,能够直接在裸机上运行.
  • MBR 的大小必须是512字节,这是为了保证0x550xaa这两个魔数恰好出现在该扇区的最后两个字节处,即第510 字节处和第511 字节处,这是按起始偏移为0 算起的.
  • 0x10中断是最为强大的BIOS 中断了,调用的方法是把功能号送入寄存器其他参数按照BIOS 中断手册的要求放在适当的寄存器中,随后执行int 0x10 即可。
; 在屏幕上打印字符串“ 1 MBR ”, 背景色为黑色,前景色为绿色。;BIOS由jmp 0: 0x7c00跳转到MBRSECTION MBR vstart=0x7c00   ;起始地址编译为0x7c00mov ax,cs   ;cs:ip -> 0: 0x7c00 -> cs为0mov ds,ax   ;段寄存器(sreg)没有从立即数到段寄存器的电路实现mov es,ax   ;只能通过其他寄存器中转(使用ax)mov ss,axmov fs,axmov sp,0x7c00   ;初始化栈指针; 输入:
; AH 功能号= Ox06
; AL =上卷的行数(如果为0 ,表示全部)
; BH =上卷行属性
; (CL, CH) =窗口左上角的( X , Y )位置
; (DL, DH) =窗口右下角的( X, Y )位置mov ax, 0x600   ;功能号0x06传入ah, 清屏操作mov bx, 0x700mov cx, 0mov dx, 0x184f  ;24 79 (0开始,25*80)int 0x10    ; BIOS 0x10中断是负责有关打印的例程; 获取光标位置 -> 将功能号(BIOS手册对应)送入ah寄存器,调用0x10中断mov ah, 3mov bh, 0int 0x10; 打印字符串mov ax, messagemov bp, axmov cx, 5mov ax, 0x1301  ;ah: 0x13 , al:1(对应是显示字符串,且光标跟随到新位置)mov bx, 0x2     ;bh: 存储显示的页号,bl是字符属性int 0x10jmp $   ; $是本行地址 -> 死循环message db "1 MBR"; MBR大小为512,且最后两字节是固定的魔数(0x55, 0xaa); 前510字节要填满: 用0填满($-$$为偏移量); times:重复汇编,在times后跟着的表达式会被重复汇编指定次数。times 510-($-$$) db 0   ;$$是本section地址db 0x55, 0xaa

编写程序后,需要做2件事:

  1. 编译
    nasm -o mbr.bin mbr.S
  2. 将编译后的文件存储到0盘0道1 扇区(创建的硬盘最开始的地方)中成为MBR,以供BIOS 加载.
    dd if=/home/dave/boot/mbr.bin of=/home/dave/bochs/hd60M.img bs=512 count=1 conv=notrunc

其中,hd60M.img为创建的硬盘。
结果如下:(MBR为512字节)

启动brochs测试:
brochs/bin目录下(即brochs安装目录)执行命令:
./bochs -f bochsrc.disk 启动brochs:

因为是debug模式,使用c以continue:

MBR运行,得到结果:

上述是利用bios的中断,下面直接操作显卡((相当于IO接口),通过向显存内写数据,显示在显示器)

实现文本模式输出,则需要将数据写入0xB8000地址起始的地方:

;在屏幕上打印字符串“ l MBR ”, 闪烁
;BIOS由jmp 0: 0x7c00跳转到MBR
;直接操作显卡,向显存内文本地址区域写入数据
;实模式下编程,故段基址(0xb8000)会被左移4位,因此初始设为0xb800(先右移4位)SECTION MBR vstart=0x7c00   ;起始地址编译为0x7c00mov ax,cs   ;cs:ip -> 0: 0x7c00 -> cs为0mov ds,ax   ;段寄存器(sreg)没有从立即数到段寄存器的电路实现mov es,ax   ;只能通过其他寄存器中转(使用ax)mov ss,axmov fs,axmov sp,0x7c00   ;初始化栈指针mov ax,0xb800   ;使用gs段寄存器作为段基址(称为段跨越前缀),不用默认的ds寄存器mov gs,ax; 输入:
; AH 功能号= Ox06
; AL =上卷的行数(如果为0 ,表示全部)
; BH =上卷行属性
; (CL, CH) =窗口左上角的( X , Y )位置
; (DL, DH) =窗口右下角的( X, Y )位置mov ax, 0x600   ;功能号0x06传入ah, 清屏操作mov bx, 0x700mov cx, 0mov dx, 0x184f  ;24 79 (0开始,25*80);int 0x10    ; BIOS 0x10中断是负责有关打印的例程int 10h;两个字节,高字节为属性,低字节为asciimov byte [gs:0x00], 'l'mov byte [gs:0x01], 0xA4    ;A 表示绿色背景闪烁, 4 表示前景色为红色mov byte [gs:0x02], ' 'mov byte [gs:0x03], 0xA4mov byte [gs:0x04], 'M'mov byte [gs:0x05], 0xA4mov byte [gs:0x06], 'B'mov byte [gs:0x07], 0xA4mov byte [gs:0x08], 'R'mov byte [gs:0x09], 0xA4jmp $   ; $是本行地址 -> 死循环; MBR大小为512,且最后两字节是固定的魔数(0x55, 0xaa); 前510字节要填满: 用0填满($-$$为偏移量); times:重复汇编,在times后跟着的表达式会被重复汇编指定次数。times 510-($-$$) db 0   ;$$是本section地址db 0x55, 0xaa

(有闪烁效果:

继续完善MBR

  • LBA(Logical Block Address):磁盘扇区从0 开始依次递增编号,不用考虑扇区所在的物理结构,0开始

    • 以下采用LBA28,即28比特位描述一个扇区地址。
  • CHS(Cylinder Head Sector):硬盘中的扇区在物理上是用“柱面.磁头.扇区”来定位,1开始
  • loader(加载器)从2号扇区(CHS地址是3,LBA地址是2。即第三块扇区)。
    • 第一块扇区512KB放置MBR;第二块空置(隔开),第三块放loader
    • MBR任务就是将loader从2号扇区读出,并放置到地址0x900处(实模式下内存布局的可用区域)

编写MBR程序内,加载2号扇区loader到指定地址的一般步骤如下:


1.0x1fx : 选择Primary通道

// mbr.S;将loader从第二个扇区读出,并放置到地址0x900处;BIOS由jmp 0: 0x7c00跳转到MBR
;直接操作显卡,向显存内文本地址区域写入数据
;实模式下编程,故段基址(0xb8000)会被左移4位,因此初始设为0xb800(先右移4位)%include "boot.inc"SECTION MBR vstart=0x7c00   ;起始地址编译为0x7c00mov ax,cs   ;cs:ip -> 0: 0x7c00 -> cs为0mov ds,ax   ;段寄存器(sreg)没有从立即数到段寄存器的电路实现mov es,ax   ;只能通过其他寄存器中转(使用ax)mov ss,axmov fs,axmov sp,0x7c00   ;初始化栈指针mov ax,0xb800   ;使用gs段寄存器作为段基址(称为段跨越前缀),不用默认的ds寄存器mov gs,ax; 输入:
; AH 功能号= Ox06
; AL =上卷的行数(如果为0 ,表示全部)
; BH =上卷行属性
; (CL, CH) =窗口左上角的( X , Y )位置
; (DL, DH) =窗口右下角的( X, Y )位置mov ax, 0x600   ;功能号0x06传入ah, 清屏操作mov bx, 0x700mov cx, 0mov dx, 0x184f  ;24 79 (0开始,25*80)int 0x10    ; BIOS 0x10中断是负责有关打印的例程;两个字节,高字节为属性,低字节为asciimov byte [gs:0x00], 'l'mov byte [gs:0x01], 0xA4    ;A 表示绿色背景闪烁, 4 表示前景色为红色mov byte [gs:0x02], ' 'mov byte [gs:0x03], 0xA4mov byte [gs:0x04], 'M'mov byte [gs:0x05], 0xA4mov byte [gs:0x06], 'B'mov byte [gs:0x07], 0xA4mov byte [gs:0x08], 'R'mov byte [gs:0x09], 0xA4mov eax, LOADER_START_SECTOR    ; 起始扇区LBA地址(设为第二块)mov bx, LOADER_BASE_ADDR        ; 写入的地址
; 1. 待操作的扇区数mov cx, 1                       ; 待读扇区数call rd_disk_m_16               ; jmp LOADER_BASE_ADDR            ; 跳转到loader地址,将控制权交给loader; 读取硬盘n个扇区
rd_disk_m_16:mov esi,eaxmov di,cx; 通过设置端口0x1f2,设置要读取的扇区数mov dx,0x1f2mov al,clout dx,almov eax,esi; 2.将LBA地址存入0x1f3-0x1f6(每个端口(寄存器)存8位)mov dx,0x1f3out dx,al   ; LBA地址0~7位mov cl,8    shr eax,clmov dx,0x1f4out dx,al   ; LBA地址8~15位shr eax,clmov dx,0x1f5out dx,al   ; LBA地址16~23位(8位); 3.往device寄存器的第4~7位设为1,表示LBA模式(6)、选择主从盘(4)shr eax,cland al,0x0f ; LBA地址24~27位(device寄存器(0x1f6)低4位)or al,0xe0  ; device寄存器的第4~7位设为1,表示LBA模式mov dx,0x1f6    ; LBA地址24~27位(4位)out dx,al; 4.向0x1f7端口(command寄存器)写入读命令, 0x20mov dx,0x1f7mov al,0x20out dx,al; 5.检测硬盘状态(0x1f7读时表示读入status寄存器)
.not_ready: nopin al,dxand al,0x88 ; 保留第4位(为1表示数据准备好了)和第7位(为1表示硬盘忙)cmp al,0x08 ; 判断status寄存器(0x1f7端口)第4位是否为1(为1表示数据已经准备好)jnz .not_ready; 7.从0x1f0端口(Data寄存器)读数据mov ax,di   ; di存储读取的扇区数mov dx,256mul dx      ; 读取次数=扇区数*扇区大小(512B)/2 -> 每次读取一个字mov cx,ax   ; 读取次数mov dx,0x1f0
.go_on_read:    ; 循环cx次in ax,dx    mov [bx],ax ; 每次读取一个字add bx,2loop .go_on_readret; MBR大小为512,且最后两字节是固定的魔数(0x55, 0xaa); 前510字节要填满: 用0填满($-$$为偏移量); times:重复汇编,在times后跟着的表达式会被重复汇编指定次数。times 510-($-$$) db 0   ;$$是本section地址db 0x55, 0xaa
// boot.incLOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2

MBR写入第一个扇区:(0号扇区)

dd if=/home/dave/boot/3/mbr.bin of=/home/dave/bochs/hd60M.img count=1 conv=notrunc bs=512
// loader.S
%include "boot.inc"SECTION LOADER vstart=LOADER_BASE_ADDRmov ax,0xb800   ; 向显存内文本地址区域(0xb8000)写入数据,实模式,先右移4位mov gs,axmov byte [gs:0x00],'2'mov byte [gs:0x01],0xA4mov byte [gs:0x02],' 'mov byte [gs:0x03],0xA4mov byte [gs:0x04],'L'mov byte [gs:0x05],0xA4mov byte [gs:0x06],'O'mov byte [gs:0x07],0xA4mov byte [gs:0x08],'A'mov byte [gs:0x09],0xA4mov byte [gs:0x0a],'D'mov byte [gs:0x0b],0xA4mov byte [gs:0x0c],'E'mov byte [gs:0x0d],0xA4mov byte [gs:0x0e],'R'mov byte [gs:0x0f],0xA4jmp $

loader写入3号扇区:(0开始)

dd if=/home/dave/boot/3/loader.bin of=/home/dave/bochs/hd60M.img count=1 seek=2 conv=notrunc bs=512

执行bochs -f bochsrc.disk结果如下,mbr将loader从扇区加载到指定地址(0x900)并运行loader


进入保护模式

  • 段描述符格式
  • D/B
  • TYPE

// boot.inc
// 定义一些宏,构造 段描述符和选择子 的各个字节
LOADER_START_SECTOR equ 0x2
LOADER_BASE_ADDR equ 0x900; gdt描述符属性
DESC_G_4K equ 1_00000000000000000000000b
DESC_D_32 equ  1_0000000000000000000000b
DESC_L equ      0_000000000000000000000b
DESC_AVL equ     0_00000000000000000000b
DESC_LIMIT_CODE2 equ 1111_0000000000000000b
DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2
DESC_LIMIT_VIDEO equ 0000_000000000000000b
DESC_P equ      1_000000000000000b
DESC_DPL_0 equ 00_0000000000000b
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
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,c=0,r=1,a=0 -> 代码段是不可执行、非一致性、可读、已访问位a清0; 代码段 段描述符的 高4字节
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; 数据段 段描述符的 高4字节
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; 显存段 段描述符 高4字节
DESC_VIDEO_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + \
DESC_L + DESC_AVL + DESC_LIMIT_VIDEO + 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
TI_LDT equ 100b
// mbr.S
// 只需修改mov cx, 4  -> 新的loader程序不止1个扇区,预先分配4个扇区
; 1. 待操作的扇区数mov cx, 4                       ; 待读扇区数call rd_disk_m_16               ; jmp LOADER_BASE_ADDR            ; 跳转到loader地址,将控制权交给loader
// loader.S 在实模式打印2 loader in read. 在保护模式打印字符P%include "boot.inc"SECTION LOADER vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
jmp loader_start; 构建gdt及其内部的描述符; 第0个段描述符没用GDT_BASE: dd 0x00000000 ; dd伪指令: 定义双字变量(编译地址从上到下越来越高)dd 0x00000000; 第1-3号描述符分别是 代码段描述符、数据段和栈段描述符、显存段描述符(间隔8个字节)CODE_DESC: dd 0x0000FFFF    ; 低32位 : 段基址(0x0000) 段界限(0xFFFF)dd DESC_CODE_HIGH4; 采取简单的数据段和栈段共同使用一个段描述符,都向上扩展(此时,栈段的边界检查也要向数据段一样的规则)DATA_STACK_DESC: dd 0x0000FFFFdd DESC_DATA_HIGH4; 实模式下用于文本模式显示适配器的内存地址: 0xb8000~0xbffff; 此处不使用平坦模式(基址0开始),直接把段基址设为文本模式起始地址0xb8000VIDEO_DESC: dd 0x80000007   ; limit = (0xbffff-0xb8000)/4k=0x7dd DESC_VIDEO_HIGH4GDT_SIZE equ $ - GDT_BASEGDT_LIMIT equ GDT_SIZE - 1; DB 定义的变量为字节型 Define Byte; DW 定义的变量为字类型(双字节)Define Word; DD 定义的变量为双字型(4字节)Define Double Word; DQ 定义的变量为4字型(8字节)Define Quadra Word; DT 定义的变量为10字节型 Define Ten Bytetimes 60 dq 0   ; 预留60个描述符的空位; 选择子; 相当于 (CODE_DESC - GDT_BASE) / 8 + TI_GDT + RPL0SELECTOR_CODE equ (0x0001 << 3) + TI_GDT + RPL0SELECTOR_DATA equ (0x0002 << 3) + TI_GDT + RPL0SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0gdt_ptr dw GDT_LIMITdd GDT_BASEloadermsg 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_ADDRmov bp,loadermsgmov cx, 17mov ax, 0x1301mov bx, 0x001fmov dx, 0x1800int 0x10; 进入保护模式; 1.打开A20; 2.加载gdt; 3.将cr0的pe位置为1; 打开A20 -> 将端口0x92的1位置设为1in al, 0x92or al, 0000_0010Bout 0x92,al; 加载GDT到gdtr寄存器; lgdt 48位内存数据(16位界限+32位GDT起始地址)lgdt [gdt_ptr]; cr0第0位置为1mov eax, cr0or eax, 0x00000001mov cr0, eax; 此处无条件跳转指令,主要目的是刷新流水线.; 否则流水线把32 位指令按照16 位译码就会出错jmp dword SELECTOR_CODE:p_mode_start[bits 32]
p_mode_start:mov ax, SELECTOR_DATAmov ds, axmov es, axmov ss, axmov esp, LOADER_STACK_TOPmov ax, SELECTOR_VIDEOmov gs, axmov byte [gs:160], 'P'  ; 保护模式下打印Pjmp $
dd if=/home/dave/boot/4/mbr.bin of=/home/dave/bochs/hd60M.img count=1 conv=notrunc bs=512dd if=/home/dave/boot/4/loader.bin of=/home/dave/bochs/hd60M.img count=4 seek=2 conv=notrunc bs=512

结果如下:
实模式下使用BIOS中断0x10打印左下角字符串,左上角第二行是保护模式下打印的。

查看全局描述符表
可见3号段描述符处使用非平坦模式,其基址为0xb8000.

运行前:

运行后:
cr0寄存器第0位置位1,PE置为1


利用BIOS int 15h中断 三种子功能获取物理内存大小

1.mbr.S修改:
需要将加载器loader基地址 + 0x300,直接跳到loader中的loader_start. 故loader中不需要使用跳转指令跳转。

 jmp LOADER_BASE_ADDR + 0x300            ; 跳转到loader地址,将控制权交给loader
  1. loader.S:
// loader.S
%include "boot.inc"
SECTION LOADER vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
; jmp loader_start; 构建gdt及其内部的描述符; 第0个段描述符没用GDT_BASE: dd 0x00000000 ; dd伪指令: 定义双字变量(编译地址从上到下越来越高)dd 0x00000000; 第1-3号描述符分别是 代码段描述符、数据段和栈段描述符、显存段描述符(间隔8个字节)CODE_DESC: dd 0x0000FFFF    ; 低32位 : 段基址(0x0000) 段界限(0xFFFF)dd DESC_CODE_HIGH4; 采取简单的数据段和栈段共同使用一个段描述符,都向上扩展(此时,栈段的边界检查也要向数据段一样的规则)DATA_STACK_DESC: dd 0x0000FFFFdd DESC_DATA_HIGH4; 实模式下用于文本模式显示适配器的内存地址: 0xb8000~0xbffff; 此处不使用平坦模式(基址0开始),直接把段基址设为文本模式起始地址0xb8000VIDEO_DESC: dd 0x80000007   ; limit = (0xbffff-0xb8000)/4k=0x7dd DESC_VIDEO_HIGH4GDT_SIZE equ $ - GDT_BASEGDT_LIMIT equ GDT_SIZE - 1; DB 定义的变量为字节型 Define Byte; DW 定义的变量为字类型(双字节)Define Word; DD 定义的变量为双字型(4字节)Define Double Word; DQ 定义的变量为4字型(8字节)Define Quadra Word; DT 定义的变量为10字节型 Define Ten Bytetimes 60 dq 0   ; 预留60个描述符的空位; 选择子; 相当于 (CODE_DESC - GDT_BASE) / 8 + TI_GDT + RPL0SELECTOR_CODE equ (0x0001 << 3) + TI_GDT + RPL0SELECTOR_DATA equ (0x0002 << 3) + TI_GDT + RPL0SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0; 用total_mem_bytes保存内存容量,字节为单位; 地址是0x900+0x200(偏移loader.bin文件头距离) = 0xb00; (4+60)*8=512=0x200字节 => 4个段描述符定义 + 60个段描述符槽位total_mem_bytes dd 0gdt_ptr dw GDT_LIMITdd GDT_BASE; 使得下面loader_start偏移是0x300 (0x200+0x100)ards_buf times 244 db 0ards_nr dw 0    ; ards结构体数量; 人工对齐(凑整地址而已): total_mem_bytes(4) + gdt_ptr(6) + ards_buf(244) + ards_nr(2) : 共256字节; loadermsg db '2 loader in real.'; 0x300
loader_start:; ----------------------------------------------------------------
; int 15h eax = 0000E820h, edx = 534D4150h ('SMAP') 获取内存布局
; ----------------------------------------------------------------xor ebx, ebxmov edx, 0x534d4150     ; edx不会改变; es:di 存放缓冲区地址,es在mbr中赋值完成了(0)mov di, ards_buf        ; ards结构缓冲区
.e820_mem_get_loop:mov eax, 0x0000e820mov ecx, 20             ; ards地址范围描述符结构时20字节int 0x15jc .e820_failed_so_try_e801    ; 当运算产生进位标志CF=1时(在这里是获取失败),跳转到目标程序处add di, cx               ; 成功,则di加20字节,指向下个ards位置inc word [ards_nr]cmp ebx, 0               ; cf=0且ebx=0 => 当前是最后一个,ards全部返回jnz .e820_mem_get_loop   ; 零标志ZF!=1,跳转. 即ebx != 0mov cx, [ards_nr]mov ebx, ards_bufxor edx, edx            ; 存放最大内存容量,清0; 寻找(base_add_low + length_low)的最大值,即内存容量
.find_max_mem_area:mov eax, [ebx]          ; base_add_low , 基地址低32位add eax, [ebx+8]        ; base_add_low + length_low    , 基地址长度的低32位add ebx, 20cmp edx, eax; JG 前>后 Jump if greater。; JGE 前>=后 Jump if greater or equal。jge .next_ardsmov edx, eax            ; edx 存储最大值(内存容量).next_ards:loop .find_max_mem_areajmp .mem_get_ok
; ----------------------------------------------------------------
; int 15h ax = 0xE801 获取内存大小, 最大支持4G
; 返回后,ax cx值一样,KB为单位; bx,dx值一样 ,以64KB为单位
; 在ax 和ex 寄存器中为低 16MB ,在bx 和dx 寄存器中为16MB 到4GB
; ----------------------------------------------------------------.e820_failed_so_try_e801:mov ax, 0xe801int 0x15jc .e801_failed_so_try88; 低15MB的内存mov cx, 0x400 ; KB -> byte 1024mul cx  ; 拼接计算结果shl edx, 16   and eax, 0x0000FFFFor edx, eaxadd edx, 0x100000   ; +1Mmov esi, edx        ; 低15MB内存容量存入esi寄存器备份; 16MB以上的内存xor eax, eaxmov ax, bxmov ecx, 0x10000    ; 64KBmul ecx             ; edx eax (结果的高低位置)add esi, eax        ; 4GB, 32位的eax足够mov edx, esijmp .mem_get_ok; -------------------------------------------------
; int 0x15 ah = 0x88 获取内存大小,只能获取64MB之内
; -------------------------------------------------
.e801_failed_so_try88:; int0x15后,ax 存储KB为单位的内存容量mov ah, 0x88int 0x15jc .error_hltand eax, 0x0000FFFFmov cx, 0x400   ; KB -> bytemul cxshl edx, 16or edx, eaxadd edx, 0x100000   ; +1MB.mem_get_ok:mov [total_mem_bytes], edx.error_hlt:            ;出错则挂起hlt

结果:
查看0xb00内存处值:0x02000000 ==> 32MB 等于 bochsrc.disk中的megs值(物理内存大小)

启用内存分页机制

程序虽然已经进了保护模式, 地址空间到达了的4GB ,但其依然是受限制
的——此进程要和其他进程包括操作系统共享这4GB 内存空间。我们把段基址+
段内偏移
地址称为线性地址,线性地址是唯一的,只能属于某一个进程。

只分段的情况下, CPU 认为线性地址等于物理地址。而线性地址是由编译器编译出来的,它本身是连续的,所以物理地址也必须要连续才行。

解除线性地址与物理地址一一对应的关系,然后将它们的关系重新建立。通过某种映射关系,可以将线性地址映射到任意物理地址。在CPU 实现中,这种映射关系是通过页表实现的,查找页表的工作也是由硬件完成的。

在保护模式中段寄存器中的内容是选择子,但选择子最终就是为了要找到段基址,其内存访问的核心机制依然是“段基址:段内偏移地址”,这两个地址在相加之后才是绝对地址,也就是我们所说
线性地址,此线性地址在分段机制下被CPU 认为是物理地址,此线性地址可以直接送上地址总线。

分页机制是建立在分段机制之上的。如果打开了分页机制段部件输出的线性地址就不再等同于物理地址了,称之为虚拟地址,它是逻辑上的,是假的,不应该被送上地址总线。此虚拟地址对应的物理地址需要在页表中查找,这项查找工作是由页部件自动完成的。


分页机制的思想是:通过映射,可以使连续的线性地址与任意物理内存地址相关联,逻辑
上连续的线性地址其对应的物理地址可以不连续
。(将大小不同的大内存段拆分成大小相等的小内存块

CPU 中采用的页大小是4KB

  • 若页大小太小,如1字节,指示4G内存要4G个页表项,每个页表项4字节(指示32位地址),单单页表要占用4Byte*4G = 16GB内存。



每个任务都有自己的页表,任务在切换时,页表也需要跟着切换。

启用分页机制步骤:

  • 1.准备好页目录表页表
  • 2.将页目录表地址写入控制寄存器cr3 (也称为页目录基址寄存器)。(分页还未开启,故存储的是物理地址
  • 3.寄存器cr0的PG 位置1 。
// boot.inc
// 增加部分
; 后续编写的内核体积大概在1M之内,故将内核加载到低端1M内存之内
; 页目录表放置地址(低端1M空间往上的第一个字节 -> 低端1M存放mbr、loader、os内核)
PAGE_DIR_TABLE_POS equ 0x100000 ; 页表相关属性
PG_P equ 1b
PG_RW_R equ 00b
PG_RW_W equ 10b
PG_US_S equ 000b
PG_US_U equ 100b
// loader.S
%include "boot.inc"
SECTION LOADER vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
; jmp loader_start; 构建gdt及其内部的描述符; 第0个段描述符没用GDT_BASE: dd 0x00000000 ; dd伪指令: 定义双字变量(编译地址从上到下越来越高)dd 0x00000000; 第1-3号描述符分别是 代码段描述符、数据段和栈段描述符、显存段描述符(间隔8个字节)CODE_DESC: dd 0x0000FFFF    ; 低32位 : 段基址(0x0000) 段界限(0xFFFF)dd DESC_CODE_HIGH4; 采取简单的数据段和栈段共同使用一个段描述符,都向上扩展(此时,栈段的边界检查也要向数据段一样的规则)DATA_STACK_DESC: dd 0x0000FFFFdd DESC_DATA_HIGH4; 实模式下用于文本模式显示适配器的内存地址: 0xb8000~0xbffff; 此处不使用平坦模式(基址0开始),直接把段基址设为文本模式起始地址0xb8000VIDEO_DESC: dd 0x80000007   ; limit = (0xbffff-0xb8000)/4k=0x7dd DESC_VIDEO_HIGH4GDT_SIZE equ $ - GDT_BASEGDT_LIMIT equ GDT_SIZE - 1; DB 定义的变量为字节型 Define Byte; DW 定义的变量为字类型(双字节)Define Word; DD 定义的变量为双字型(4字节)Define Double Word; DQ 定义的变量为4字型(8字节)Define Quadra Word; DT 定义的变量为10字节型 Define Ten Bytetimes 60 dq 0   ; 预留60个描述符的空位; 选择子; 相当于 (CODE_DESC - GDT_BASE) / 8 + TI_GDT + RPL0SELECTOR_CODE equ (0x0001 << 3) + TI_GDT + RPL0SELECTOR_DATA equ (0x0002 << 3) + TI_GDT + RPL0SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0; 用total_mem_bytes保存内存容量,字节为单位; 地址是0x900+0x200(偏移loader.bin文件头距离) = 0xb00; (4+60)*8=512=0x200字节 => 4个段描述符定义 + 60个段描述符槽位total_mem_bytes dd 0gdt_ptr dw GDT_LIMITdd GDT_BASE; 使得下面loader_start偏移是0x300 (0x200+0x100)ards_buf times 244 db 0ards_nr dw 0    ; ards结构体数量; 人工对齐(凑整地址而已): total_mem_bytes(4) + gdt_ptr(6) + ards_buf(244) + ards_nr(2) : 共256字节; loadermsg db '2 loader in real.'; 0x300
loader_start:; ----------------------------------------------------------------
; int 15h eax = 0000E820h, edx = 534D4150h ('SMAP') 获取内存布局
; ----------------------------------------------------------------xor ebx, ebxmov edx, 0x534d4150     ; edx不会改变; es:di 存放缓冲区地址,es在mbr中赋值完成了(0)mov di, ards_buf        ; ards结构缓冲区
.e820_mem_get_loop:mov eax, 0x0000e820mov ecx, 20             ; ards地址范围描述符结构时20字节int 0x15jc .e820_failed_so_try_e801    ; 当运算产生进位标志CF=1时(在这里是获取失败),跳转到目标程序处add di, cx               ; 成功,则di加20字节,指向下个ards位置inc word [ards_nr]cmp ebx, 0               ; cf=0且ebx=0 => 当前是最后一个,ards全部返回jnz .e820_mem_get_loop   ; 零标志ZF!=1,跳转. 即ebx != 0mov cx, [ards_nr]mov ebx, ards_bufxor edx, edx            ; 存放最大内存容量,清0; 寻找(base_add_low + length_low)的最大值,即内存容量
.find_max_mem_area:mov eax, [ebx]          ; base_add_low , 基地址低32位add eax, [ebx+8]        ; base_add_low + length_low    , 基地址长度的低32位add ebx, 20cmp edx, eax; JG 前>后 Jump if greater。; JGE 前>=后 Jump if greater or equal。jge .next_ardsmov edx, eax            ; edx 存储最大值(内存容量).next_ards:loop .find_max_mem_areajmp .mem_get_ok
; ----------------------------------------------------------------
; int 15h ax = 0xE801 获取内存大小, 最大支持4G
; 返回后,ax cx值一样,KB为单位; bx,dx值一样 ,以64KB为单位
; 在ax 和ex 寄存器中为低 16MB ,在bx 和dx 寄存器中为16MB 到4GB
; ----------------------------------------------------------------.e820_failed_so_try_e801:mov ax, 0xe801int 0x15jc .e801_failed_so_try88; 低15MB的内存mov cx, 0x400 ; KB -> byte 1024mul cx  ; 拼接计算结果shl edx, 16   and eax, 0x0000FFFFor edx, eaxadd edx, 0x100000   ; +1Mmov esi, edx        ; 低15MB内存容量存入esi寄存器备份; 16MB以上的内存xor eax, eaxmov ax, bxmov ecx, 0x10000    ; 64KBmul ecx             ; edx eax (结果的高低位置)add esi, eax        ; 4GB, 32位的eax足够mov edx, esijmp .mem_get_ok; -------------------------------------------------
; int 0x15 ah = 0x88 获取内存大小,只能获取64MB之内
; -------------------------------------------------
.e801_failed_so_try88:; int0x15后,ax 存储KB为单位的内存容量mov ah, 0x88int 0x15jc .error_hltand eax, 0x0000FFFFmov cx, 0x400   ; KB -> bytemul cxshl edx, 16or edx, eaxadd edx, 0x100000   ; +1MB.mem_get_ok:mov [total_mem_bytes], edx; 进入保护模式; 1.打开A20; 2.加载gdt; 3.将cr0的pe位置为1; 打开A20 -> 将端口0x92的1位置设为1in al, 0x92or al, 0000_0010Bout 0x92,al; 加载GDT到gdtr寄存器; lgdt 48位内存数据(16位界限+32位GDT起始地址)lgdt [gdt_ptr]; cr0第0位置为1mov eax, cr0or eax, 0x00000001mov cr0, eax; 此处无条件跳转指令,主要目的是刷新流水线.; 否则流水线把32 位指令按照16 位译码就会出错jmp dword SELECTOR_CODE:p_mode_start
.error_hlt:           ;出错则挂起hlt[bits 32]
p_mode_start:mov ax, SELECTOR_DATAmov ds, axmov es, axmov ss, axmov esp,LOADER_STACK_TOPmov ax, SELECTOR_VIDEOmov gs, ax   ; 1.创建页目录 及 页表 并初始化页内存位图call setup_page; 将描述符表地址及 偏移量 写入内存gdt_ptr, 一会儿用新地址重新加载; GDT要放在1G内核空间(0xc0000000 ~ 0xffffffff)sgdt [gdt_ptr]mov ebx, [gdt_ptr + 2]  ; GDT中的3号描述符是显存段描述符; 将显存段描述符段基址(段基址在段描述符高32位(+0x4)的最高8位) + 0xc0000000 ,移到内核空间; 防止用户直接操作显存or dword [ebx + 0x18 + 0x4], 0xc0000000   ; 修改gdt基址到内核空间add dword [gdt_ptr + 2], 0xc0000000; 栈指针映射到内核空间add esp, 0xc0000000 ; 2.将页目录地址赋给cr3mov eax, PAGE_DIR_TABLE_POSmov cr3, eax; 3.将cr0的pg位(31位)置1mov eax, cr0or eax, 0x80000000mov cr0, eax; 开启分页后,重新加载gdtlgdt [gdt_ptr]; 一个字符是2 字节,每行是80 个字符mov byte [gs:160], 'V'jmp $; 创建页目录及页表
setup_page:; 页目录占用空间 逐字节清0mov ecx, 4096mov esi, 0
.clear_page_dir:mov byte [PAGE_DIR_TABLE_POS + esi], 0inc esiloop .clear_page_dir; 创建页目录项(PDE, Page Directory Entry)
.create_pde:mov eax, PAGE_DIR_TABLE_POSadd eax, 0x1000                 ; +4KB为页目录表占用空间,预设页表地址紧邻页目录表(此时为第一个页表地址)mov ebx, eaxor eax, PG_US_U | PG_RW_W | PG_P; 第一个目录项写入第一个页表地址(0x101000)及属性 ; 这个页表映射的物理内存范围是1MB空间(0~0xfffff放置内核),另外3M未分配; 0项指向第一个页表是因为:loader也会被存放在物理内存的低1M空间; 这样保证了分页后该部分(内核)虚拟地址对应的物理地址与分段机制下对应的一致 (其部分执行时还未启用分页,保证前后一致性)mov [PAGE_DIR_TABLE_POS + 0x0], eax    ; 页目录表共1024项* 4B = 4096B地址空间; 0xc00 = 3072以上用于映射内核空间的页表地址; (4096 - 3072) * 4MB <每个页表映射的地址范围> = (1G -> 0xc0000000 ~ 0xffffffff)mov [PAGE_DIR_TABLE_POS + 0xc00], eax   ; 内核空间的第一个目录项也指向第一个页表地址(0x101000)sub eax, 0x1000mov [PAGE_DIR_TABLE_POS + 4092], eax    ; 最后一个页目录项指向自己的地址; 创建页表项(PTE); 后续编写的内核体积大概在1M之内,故将内核加载到低端1M内存之内 : mbr、loader、操作系统内核都放在这1M空间内(物理内存的0~0xfffff); 即将虚拟地址0xc0000000往上(内核空间)的1MB地址 映射到 物理内存的1MB之内mov ecx, 256    ; 1M低端内存 / 页大小4K = 256个页表项mov esi, 0mov edx, PG_US_U | PG_RW_W | PG_P
; 256个页表项初始化
.create_pte:mov [ebx+esi*4], edx    ; ebx初始存储第一个页表地址add edx, 4096inc esiloop .create_pte; 创建内核其他页表的PDEmov eax, PAGE_DIR_TABLE_POSadd eax, 0x2000          ; 第二个页表地址or eax, PG_US_U | PG_RW_W | PG_Pmov ebx, PAGE_DIR_TABLE_POSmov ecx, 254             ; 769 ~ 1022 所有目录项数量, 1023指向页目录表自己的起始地址,0xc01指向第一个页表地址mov esi, 769             ; 0xc0000000以上是内核空间,高10位是0x300=>768, 故之后的页目录项从769开始.create_kernal_pde:mov [ebx+esi*4], eaxinc esiadd eax, 0x1000loop .create_kernal_pderet


gdt的段基址已经移动到0xc0000000之上(内核空间), 显存段描述符也一样.



高10 全为1 ,即1111111111b=0x3ff=1023 ,则访问的是最后一个目录项.
指向页目录表首地址,由于该地址是经过虚拟地址的高10 位索引到的,所以被认为是个页表的地址,也就是说,页目录表此时被当作页表来用。
线性地址的中间10 位用来在页表中定位一个页表项,从该页表项中获取物理页地址。此时,中间10位是0,故索引到页目录表的第0项;其内容被认为是最终的物理地址,第0项内容是第一个页表物理地址Ox101000,故得到其映射。


TLB: 处理器在寻址之前会用虚拟地址的高20 位(虚拟页框号)作为索引来查找TLB 中的相关条目,如果命中
(匹配到相关条目)则返回虚拟地址所映射的物理页框地址,否则会查询内存中的页表,获得页框物理地
址后再更新TLB

TLB需要实时更新,这项工作交给了TLB 的维护工作交给操作系统开发人员,由开发人员手动控制。(否则从内存的页表实时去更新TLB,得不偿失)。更新方法:

  • 针对TLB 中所有条目的方法一一重新加载CR3
  • 针对TLB 中某个条目的更新。处理器提供了指令invlpg(invalidate page ),它用于在TLB 中刷新 某个虚拟地址对应的条目 .
    处理器是用虚拟地址来检索TLB 的,指令格式为invlpg m· 。注意,其中m 表示操作数为虚拟内存地址,并不是立即数,比如要更新虚拟地址0x1234 对应的条目,指令为invlpg [0xl234]

加载内核到内存

// 简易内核实现
int main(void) {while(1);return 0;
}
gcc -c main.c -o main.o
ld main.o -Ttext 0xc0001500 -e main -o kernel.bindd if=./kernel.bin of=/home/dave/bochs/hd60M.img bs=512 seek=9 count=200 conv=notrunc
// 放在第九个扇区

loader.S 需要修改两个地方。
加载内核:需要把内核文件加载到内存缓冲区。
初始化内核:需要在分页后,将加载进来的elf 内核文件安置到相应的虚拟内存地址,然后跳过去执行,从此loader 的工作结束。


内核被加载到内存后,还要经过分析将elf结构展开到新位置,解析后生成的内核映像(将程序的各种段segment复制到内存后的程序体)才是真正运行的内核。

  • kernel.bin放置在0x70000(高位置,随后会被覆盖)
  • 内核映像放在低地址处,这里选0x1500作为内核映像的入口地址.低端1MB的虚拟内存与物理内存是一一对应的,所以物理地址是0x1500 ,对应的虚拟地址是0xc0001500.

// loader.S
%include "boot.inc"
SECTION LOADER vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
; jmp loader_start; 构建gdt及其内部的描述符; 第0个段描述符没用GDT_BASE: dd 0x00000000 ; dd伪指令: 定义双字变量(编译地址从上到下越来越高)dd 0x00000000; 第1-3号描述符分别是 代码段描述符、数据段和栈段描述符、显存段描述符(间隔8个字节)CODE_DESC: dd 0x0000FFFF    ; 低32位 : 段基址(0x0000) 段界限(0xFFFF)dd DESC_CODE_HIGH4; 采取简单的数据段和栈段共同使用一个段描述符,都向上扩展(此时,栈段的边界检查也要向数据段一样的规则)DATA_STACK_DESC: dd 0x0000FFFFdd DESC_DATA_HIGH4; 实模式下用于文本模式显示适配器的内存地址: 0xb8000~0xbffff; 此处不使用平坦模式 (基址0开始),直接把段基址设为文本模式起始地址0xb8000VIDEO_DESC: dd 0x80000007   ; limit = (0xbffff-0xb8000)/4k=0x7dd DESC_VIDEO_HIGH4GDT_SIZE equ $ - GDT_BASEGDT_LIMIT equ GDT_SIZE - 1; DB 定义的变量为字节型 Define Byte; DW 定义的变量为字类型(双字节)Define Word; DD 定义的变量为双字型(4字节)Define Double Word; DQ 定义的变量为4字型(8字节)Define Quadra Word; DT 定义的变量为10字节型 Define Ten Bytetimes 120 dq 0   ; 预留120个描述符的空位; 选择子; 相当于 (CODE_DESC - GDT_BASE) / 8 + TI_GDT + RPL0SELECTOR_CODE equ (0x0001 << 3) + TI_GDT + RPL0SELECTOR_DATA equ (0x0002 << 3) + TI_GDT + RPL0SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0; 用total_mem_bytes保存内存容量,字节为单位; 地址是0x900+0x200(偏移loader.bin文件头距离) = 0xb00; (4+60)*8=512=0x200字节 => 4个段描述符定义 + 60个段描述符槽位total_mem_bytes dd 0gdt_ptr dw GDT_LIMITdd GDT_BASE; 使得下面loader_start偏移是0x300 (0x200+0x100)ards_buf times 244 db 0ards_nr dw 0    ; ards结构体数量; 人工对齐(凑整地址而已): total_mem_bytes(4) + gdt_ptr(6) + ards_buf(244) + ards_nr(2) : 共256字节; loadermsg db '2 loader in real.'; 0x300
loader_start:; ----------------------------------------------------------------
; int 15h eax = 0000E820h, edx = 534D4150h ('SMAP') 获取内存布局
; ----------------------------------------------------------------xor ebx, ebxmov edx, 0x534d4150     ; edx不会改变; es:di 存放缓冲区地址,es在mbr中赋值完成了(0)mov di, ards_buf        ; ards结构缓冲区
.e820_mem_get_loop:mov eax, 0x0000e820mov ecx, 20             ; ards地址范围描述符结构时20字节int 0x15jc .e820_failed_so_try_e801    ; 当运算产生进位标志CF=1时(在这里是获取失败),跳转到目标程序处add di, cx               ; 成功,则di加20字节,指向下个ards位置inc word [ards_nr]cmp ebx, 0               ; cf=0且ebx=0 => 当前是最后一个,ards全部返回jnz .e820_mem_get_loop   ; 零标志ZF!=1,跳转. 即ebx != 0mov cx, [ards_nr]mov ebx, ards_bufxor edx, edx            ; 存放最大内存容量,清0; 寻找(base_add_low + length_low)的最大值,即内存容量
.find_max_mem_area:mov eax, [ebx]          ; base_add_low , 基地址低32位add eax, [ebx+8]        ; base_add_low + length_low    , 基地址长度的低32位add ebx, 20cmp edx, eax; JG 前>后 Jump if greater。; JGE 前>=后 Jump if greater or equal。jge .next_ardsmov edx, eax            ; edx 存储最大值(内存容量).next_ards:loop .find_max_mem_areajmp .mem_get_ok
; ----------------------------------------------------------------
; int 15h ax = 0xE801 获取内存大小, 最大支持4G
; 返回后,ax cx值一样,KB为单位; bx,dx值一样 ,以64KB为单位
; 在ax 和ex 寄存器中为低 16MB ,在bx 和dx 寄存器中为16MB 到4GB
; ----------------------------------------------------------------.e820_failed_so_try_e801:mov ax, 0xe801int 0x15jc .e801_failed_so_try88; 低15MB的内存mov cx, 0x400 ; KB -> byte 1024mul cx  ; 拼接计算结果shl edx, 16   and eax, 0x0000FFFFor edx, eaxadd edx, 0x100000   ; +1Mmov esi, edx        ; 低15MB内存容量存入esi寄存器备份; 16MB以上的内存xor eax, eaxmov ax, bxmov ecx, 0x10000    ; 64KBmul ecx             ; edx eax (结果的高低位置)add esi, eax        ; 4GB, 32位的eax足够mov edx, esijmp .mem_get_ok; -------------------------------------------------
; int 0x15 ah = 0x88 获取内存大小,只能获取64MB之内
; -------------------------------------------------
.e801_failed_so_try88:; int0x15后,ax 存储KB为单位的内存容量mov ah, 0x88int 0x15jc .error_hltand eax, 0x0000FFFFmov cx, 0x400   ; KB -> bytemul cxshl edx, 16or edx, eaxadd edx, 0x100000   ; +1MB.mem_get_ok:mov [total_mem_bytes], edx; 进入保护模式; 1.打开A20; 2.加载gdt; 3.将cr0的pe位置为1; 打开A20 -> 将端口0x92的1位置设为1in al, 0x92or al, 0000_0010Bout 0x92,al; 加载GDT到gdtr寄存器; lgdt 48位内存数据(16位界限+32位GDT起始地址)lgdt [gdt_ptr]; cr0第0位置为1mov eax, cr0or eax, 0x00000001mov cr0, eax; 此处无条件跳转指令,主要目的是刷新流水线.; 否则流水线把32 位指令按照16 位译码就会出错jmp dword SELECTOR_CODE:p_mode_start
.error_hlt:           ;出错则挂起hlt[bits 32]
p_mode_start:mov ax, SELECTOR_DATAmov ds, axmov es, axmov ss, axmov esp,LOADER_STACK_TOPmov ax, SELECTOR_VIDEOmov gs, ax   ; 加载内核mov eax, KERNEL_START_SECTORmov ebx, KERNEL_BIN_BASE_ADDRmov ecx, 200call rd_disk_m_32; 1.创建页目录 及 页表 并初始化页内存位图call setup_page; 将描述符表地址及 偏移量 写入内存gdt_ptr, 一会儿用新地址重新加载; GDT要放在1G内核空间(0xc0000000 ~ 0xffffffff)sgdt [gdt_ptr]mov ebx, [gdt_ptr + 2]  ; GDT中的3号描述符是显存段描述符; 将显存段描述符段基址(段基址在段描述符高32位(+0x4)的最高8位) + 0xc0000000 ,移到内核空间; 防止用户直接操作显存or dword [ebx + 0x18 + 0x4], 0xc0000000   ; 修改gdt基址到内核空间add dword [gdt_ptr + 2], 0xc0000000; 栈指针映射到内核空间add esp, 0xc0000000 ; 2.将页目录地址赋给cr3mov eax, PAGE_DIR_TABLE_POSmov cr3, eax; 3.将cr0的pg位(31位)置1mov eax, cr0or eax, 0x80000000mov cr0, eax; 开启分页后,重新加载gdtlgdt [gdt_ptr]; 一个字符是2 字节,每行是80 个字符; jmp SELECTOR_CODE:enter_kernelenter_kernel:  call kernel_initmov esp, 0xc009f000jmp KERNEL_ENTRY_POINT;jmp $; 创建页目录及页表
setup_page:; 页目录占用空间 逐字节清0mov ecx, 4096mov esi, 0
.clear_page_dir:mov byte [PAGE_DIR_TABLE_POS + esi], 0inc esiloop .clear_page_dir; 创建页目录项(PDE, Page Directory Entry)
.create_pde:mov eax, PAGE_DIR_TABLE_POSadd eax, 0x1000                 ; +4KB为页目录表占用空间,预设页表地址紧邻页目录表(此时为第一个页表地址)mov ebx, eaxor eax, PG_US_U | PG_RW_W | PG_P; 第一个目录项写入第一个页表地址(0x101000)及属性 ; 这个页表映射的物理内存范围是1MB空间(0~0xfffff放置内核),另外3M未分配; 0项指向第一个页表是因为:loader也会被存放在物理内存的低1M空间; 这样保证了分页后该部分(内核)虚拟地址对应的物理地址与分段机制下对应的一致 (其部分执行时还未启用分页,保证前后一致性)mov [PAGE_DIR_TABLE_POS + 0x0], eax    ; 页目录表共1024项* 4B = 4096B地址空间; 0xc00 = 3072以上用于映射内核空间的页表地址; (4096 - 3072) * 4MB <每个页表映射的地址范围> = (1G -> 0xc0000000 ~ 0xffffffff)mov [PAGE_DIR_TABLE_POS + 0xc00], eax   ; 内核空间的第一个目录项也指向第一个页表地址(0x101000)sub eax, 0x1000mov [PAGE_DIR_TABLE_POS + 4092], eax    ; 最后一个页目录项指向自己的地址; 创建页表项(PTE); 后续编写的内核体积大概在1M之内,故将内核加载到低端1M内存之内 : mbr、loader、操作系统内核都放在这1M空间内(物理内存的0~0xfffff); 即将虚拟地址0xc0000000往上(内核空间)的1MB地址 映射到 物理内存的1MB之内mov ecx, 256    ; 1M低端内存 / 页大小4K = 256个页表项mov esi, 0mov edx, PG_US_U | PG_RW_W | PG_P
; 256个页表项初始化
.create_pte:mov [ebx+esi*4], edx    ; ebx初始存储第一个页表地址add edx, 4096inc esiloop .create_pte; 创建内核其他页表的PDEmov eax, PAGE_DIR_TABLE_POSadd eax, 0x2000          ; 第二个页表地址or eax, PG_US_U | PG_RW_W | PG_Pmov ebx, PAGE_DIR_TABLE_POSmov ecx, 254             ; 769 ~ 1022 所有目录项数量, 1023指向页目录表自己的起始地址,0xc01指向第一个页表地址mov esi, 769             ; 0xc0000000以上是内核空间,高10位是0x300=>768, 故之后的页目录项从769开始.create_kernal_pde:mov [ebx+esi*4], eaxinc esiadd eax, 0x1000loop .create_kernal_pderet; 保护模式的硬盘读取函数
rd_disk_m_32:mov esi, eaxmov di, cxmov dx, 0x1f2mov al, clout dx, almov eax, esimov dx, 0x1f3out dx, almov cl, 8shr eax, clmov dx, 0x1f4out dx, alshr eax, clmov dx, 0x1f5out dx, alshr eax, cland al, 0x0for al, 0xe0mov dx, 0x1f6out dx, almov dx, 0x1f7mov al, 0x20out dx, al.not_ready:nopin al, dxand al, 0x88cmp al, 0x08jnz .not_readymov ax, dimov dx, 256mul dxmov cx, axmov dx, 0x1f0.go_on_read:in ax, dxmov [bx], axadd bx, 2loop .go_on_readretkernel_init:xor eax, eax    xor ebx, ebx    ; ebx 记录程序头表地址xor ecx, ecx    ; cx 记录程序头表中program header数量xor edx, edx    ; program header大小 -> e_phentsizemov dx, [KERNEL_BIN_BASE_ADDR + 42]  ; e_phentsize -> program header大小mov ebx, [KERNEL_BIN_BASE_ADDR + 28] ; e_phoff, 程序头表地址 -> 第一个program header在文件中的偏移量add ebx, KERNEL_BIN_BASE_ADDRmov cx, [KERNEL_BIN_BASE_ADDR + 44]  ; e_phnum 程序头数目.each_segment:cmp byte [ebx + 0], PT_NULL         ; p_type == PT_NULL je .PTNULL; 为memcpy压入参数,从右往左以此; 函数原型类似于memcpy(dst, src, size)push dword [ebx + 16]   ; p_fileszmov eax, [ebx + 4]  ; p_offset; eax 为每一段的物理地址add eax, KERNEL_BIN_BASE_ADDRpush eaxpush dword [ebx + 8]    ; p_vaddr 目的地址call mem_cpyadd esp, 12.PTNULL:add ebx, edxloop .each_segmentretmem_cpy:cld ; clean direction -> 使eflags寄存器中的方向标志位DF=0,低地址向高地址 si, di自增; std 是set direction,该指令是将方向标志位; DF 置为1 ,每次rep 循环执行后面字符串指令时,[e]si 和[e]di 自动减去所搬运数据的字节大小。push ebpmov ebp, esppush ecx; 段寄存器DS 和ES 在进入保护模式之初就被赋成相同的选择子了,它们都指向同一个段描述符mov edi, [ebp+8]    ; dstmov esi, [ebp+12]   ; srcmov ecx, [ebp+16]   ; sizerep movsb   ; 逐字节拷贝pop ecxpop ebpret         ; ret 指令将栈顶中的值作为返回地址
// boot.inc
LOADER_START_SECTOR equ 0x2
LOADER_BASE_ADDR equ 0x900
KERNEL_START_SECTOR equ 0x9
KERNEL_BIN_BASE_ADDR equ 0x70000; 后续编写的内核体积大概在1M之内,故将内核加载到低端1M内存之内
; 页目录表放置地址(低端1M空间上的第一个字节 -> 低端1M存放mbr、loader、os内核)
PAGE_DIR_TABLE_POS equ 0x100000 ; gdt描述符属性
DESC_G_4K equ 1_00000000000000000000000b
DESC_D_32 equ  1_0000000000000000000000b
DESC_L equ      0_000000000000000000000b
DESC_AVL equ     0_00000000000000000000b
DESC_LIMIT_CODE2 equ 1111_0000000000000000b
DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2
DESC_LIMIT_VIDEO equ 0000_000000000000000b
DESC_P equ      1_000000000000000b
DESC_DPL_0 equ 00_0000000000000b
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
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,c=0,r=1,a=0 -> 代码段是不可执行、非一致性、可读、已访问位a清0; 代码段 段描述符的 高4字节
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; 数据段 段描述符的 高4字节
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; 显存段 段描述符 高4字节
DESC_VIDEO_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + \
DESC_L + DESC_AVL + DESC_LIMIT_VIDEO + 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
TI_LDT equ 100b; 页表相关属性
PG_P equ 1b
PG_RW_R equ 00b
PG_RW_W equ 10b
PG_US_S equ 000b
PG_US_U equ 100bPT_NULL equ 0
KERNEL_ENTRY_POINT equ 0xc0001500

实现一个操作系统 (1) - 过程记录blog相关推荐

  1. 又开发好一个系统,详细记录软著申请过程

    大家好,我是小悟 又开发好一个系统,详细记录软著申请过程 经过几个通宵达旦的撸代码之后,又开发好了一个系统,和以往一样,系统开发好了少不了申请软件著作权证书,兄弟们,软著申请搞起来啊. 在版权保护中心 ...

  2. VS调试dll详细过程记录

    VS调试dll详细过程记录 还可以参考博客: https://blog.csdn.net/u014738665/article/details/79779632 在我们写的程序中有时候调用dll,并且 ...

  3. 双系统的电脑中如何完美系统其中一个操作系统

    做软件开发的人员很多朋友为了开发方便和学习,都有安装双系统的习惯,可是卸载其中一个操作系统是个比较麻烦的事情,下面整理了常用的双系统搭配卸载方案,希望对大家有所帮助. 一.WIN7+XP,卸载XP 1 ...

  4. 解决Linux 负载过高问题过程记录

    解决问题的思路 1.top命令查看该机器的负载状况 2.cd  /proc/pid 查看对应高占用程序的位置 3.进入对应程序中查看日志,根据CPU和内存这两个因素分析 4.ps -ajxf 查看进程 ...

  5. C++春招实习和秋招面试过程记录

    2.22 1.数据为什么放文件里或者数据库里,放在内存中不行吗? 如果是定期更新的,一段时间内都在使用,放在内存中可以的,速度更快的. 2.说说C++的4种类型转换 C++的四种类型转换reinter ...

  6. 《操作系统原理》 记录 (41)

    /**** <操作系统原理> 记录 (41)* #系统组成* #硬件抽象层HAL (manager role)* 为操作系统的上层提供统一标准进而隐藏不同的硬件差异** #内核 kerne ...

  7. 源码编译安装git过程记录

    工作上需要使用一台装有银河麒麟的操作系统的计算机.这台计算机cpu是arm架构的并且没有预装git.下面介绍我如何在这样一台预装软件较少的计算机上用源码安装git 目录 1 源码安装autoconf ...

  8. 一次简单的爬虫过程记录:静态网页小说下载

    时间:2020年2月14日 环境:windows7 编程语言及版本:Python3.8 IDE:Sublime Text 3 工具:requests,BeautifulSoup,sys 浏览器:Fir ...

  9. 哈工大操作系统课程实验记录

    哈工大操作系统课程实验记录 0-课程准备 课程视频地址: https://www.bilibili.com/video/BV1d4411v7u7 实验楼地址: https://www.shiyanlo ...

最新文章

  1. 人工智能相关的几篇文章链接_20191008
  2. Hopfield神经网络和TSP问题
  3. 夏日炎炎 数据中心要降温更要注意湿度影响
  4. vs用c语言写贪吃蛇,熬书几个月,终于编出简易的贪吃蛇了,VS2013
  5. 每个tabpage中都有一个dategridview_每个女人,都有一个礼服梦
  6. 英语学习笔记2019-11-22
  7. 根据数据库名称glkf查看使用的用户
  8. tar命令打包并删除原文件
  9. UTrust4701F双界面NFC读写器读卡器测试软件读写NFC电子标签的文本|网址|电子名片|智能海报|蓝牙地址内容操作说明
  10. GCC 中的 aligned 和 packed 属性(关于地址对齐)
  11. noip2014 生活大爆炸版 石头剪刀布 (模拟)
  12. E-Competitive Seagulls
  13. Unity-模型导入-材质
  14. 虹科干货 | 仅需3步!教你如何基于Windows系统操作使用RELY-TSN-KIT评估套件
  15. delmatch oracle_oracle – 无法从结果集中读取列值
  16. oracle 11g rac 恢复,11G RAC 异机恢复至单实例测试
  17. VBA—压缩文件夹成一个rar压缩包
  18. (附源码)计算机毕业设计SSM羽毛球场地管理系统
  19. 【西行】Java 编程语言简单介绍
  20. AUTOCAD——构造线

热门文章

  1. Linux命令之useradd命令
  2. 有哪些半入耳蓝牙耳机推荐,盘点学生党平价蓝牙耳机
  3. 运动耳机防水怎么样?推荐六款运动党必备的防水耳机
  4. 蚁剑的下载与一句话木马
  5. 鸿蒙系统对国产软件的影响,鸿蒙2.0开启公测,国产软件集体上涨,华为成为国产带货一哥...
  6. html如何做旅游网页,HTML+CSS旅游网站
  7. 物联网系列⑤——基于ESP8266与点灯科技平台的氛围灯设计(接入小爱同学)
  8. Vue(07)——slot插槽和自定义事件
  9. excel翻译功能怎么用呢?有什么excel翻译英文的方法?
  10. 想不出深度学习创新点?让AI七剑客来帮你吧