操作系统真象还原学习日志——第5章

本文是笔者学习《操作系统真象还原》的读书笔记,若有侵权,请联系删除。笔者也是处于学习阶段,所陈述的内容如有错误,欢迎指正!

一、获取物理内存

Linux可以通过调用BIOS中断0x15的三个子功能获取物理内存。

子功能1:EAX=0xE820,获取所有内存地址,按类型返回

子功能2:AX=0xE801,获取低15MB和16MB~4GB的内存,最大支持4GB

子功能3:AH=0x88,最多检测出64MB,超过64MB也只能返回64MB

1.1、利用BIOS中断0x15子功能0xE820获取物理内存

BIOS按照类型属性来划分这片系统内存,所以该子功能的查询是迭代式的。每次BIOS只返回一种类型的内存信息。而该内存信息用地址范围描述符(ARDS)来描述。ARDS格式如下:

字节偏移量属性名称描述
0BaseAddrLow基地址低32位
4BaseAddrHigh基地址高32位
8LengthLow内存长度低32位,以字节位单位
12LengthHigh内存长度高32位,以字节位单位
16Type本段内存类型

BIOS中断是一段函数例程,调用它需要提供参数。

BIOS中断0x15子功能0xE820参数说明

输入参数:

  • EAX,子功能号
  • EBX,每次中断返回一个ARDS结构,EBX告诉BIOS该返回哪个ARDS,BIOS更新此值
  • ES:DI,ARDS缓冲区,将返回的ARDS写入此处
  • ECX,ARDS结构的字节大小
  • EDX,固定标记0x534d415,用此签名校验相关信息

输出参数:

  • CF,表示是否出错,CF=1表示出错
  • EAX,字符串SMAP的ASCII码0x534d415
  • ES:DI,ARDS缓冲区地址
  • ECX,BOS写入到ES:DI所指向的ARDS结构中的字节数
  • EBX,下一个ARDS的位置。若CF=0,EBX=0,表示这是最后一个ARDS结构

思考:该子功能如何得到最终物理内存?

1.2、利用BIOS中断0x15子功能0xE801获取物理内存

此方法,最多可以识别4GB内存,但是此部分获取的内存地址分为两部分。低于15MB的内存以1KB为单位来记录,记录在AX和CX中。16MB~4GB是以64KB为单位来记录,记录在BX和DX中

BIOS中断0x15子功能0xE801参数说明

输入参数:

  • AX,子功能号

输出参数:

  • CF,表示是否出错,CF=1表示出错
  • AX、CX,以1KB为单位,只显示15MB一下的内存,最大值为0x3c00
  • BX、DX,以64KB为单位,内存空间16MB~4GB中连续的单位数量

为什么此处是分两次来展示4GB内存呢?当然是兼容啦,80286拥有24位地址线,寻址空间为16MB,一些ISA设备需要用到15MB以上的内存作为缓冲区,所以硬件系统将这部分内存保留下来

1.3、利用BIOS中断0x15子功能0x88获取物理内存

此子功能,最大只能识别64MB的内存,即使容量大于64MB,也只会显示63MB,因为此中断只会显示1MB以上的内存,我们使用时,需要加上1MB

BIOS中断0x15子功能0x88参数说明

输入参数:

  • AH,子功能号

输出参数:

  • CF,表示是否出错,CF=1表示出错
  • AX,以1KB为单位,不包括低端1MB

1.4用代码获取物理内存大小

本实验,在lab3的基础上,实现获取物理内存的大小。

实验源代码分为三个文件:boot.inc、mbr.s、loader.s

boot.inc源码:

;-------------------loader&&kernel-------------------
Loader_Base_Addr equ 0x900
Loader_Start_Sector equ 0x2
Loader_Sector_Cnt equ 0x4

;----------------提前设置好gdt的相关属性-----------------
DESC_G_4K     equ 0x800000                  ;段界限粒度为4K
DESC_D_32     equ 0x400000                  ;代码段,指令中的有效地址及其操作数大小为32位
DESC_L        equ 0x000000                  ;32位代码段,置为0
DESC_AVL      equ 0x000000                  ;是否可用,置为0
DESC_CODE_LIMIT_16TO19   equ 0xf0000        ;代码段段界限的16~19位
DESC_DATA_LIMIT_16TO19   equ 0xf0000        ;数据段段界限的16~19位
DESC_VIDEO_LIMIT_16TO19  equ 0x00000        ;显存段段界限的16~19位
DESC_P        equ 0x8000                    ;存在位,置为1
DESC_DPL0     equ 0x0000                    ;优先级为0
DESC_DPL1     equ 0x2000                    ;优先级为1
DESC_DPL2     equ 0x4000                    ;优先级为2
DESC_DPL3     equ 0x6000                    ;优先级为3
DESC_S_NOTSYM equ 0x1000                    ;当前描述符不是系统段
DESC_S_SYM    equ 0x0000                    ;当前描述副是系统段
DESC_CODE_TYPE           equ 0x800          ;代码段TYPE字段描述
DESC_DATA_TYPE           equ 0x200          ;数据段TYPE字段描述

MID_WEIGHT    equ (0x00<<24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_P + DESC_DPL0 + DESC_S_NOTSYM

DESC_CODE_HIGH4          equ MID_WEIGHT + DESC_CODE_LIMIT_16TO19 + DESC_CODE_TYPE + 0x00
DESC_DATA_HIGH4          equ MID_WEIGHT + DESC_DATA_LIMIT_16TO19 + DESC_DATA_TYPE + 0x00
DESC_VIDEO_HIGH4          equ MID_WEIGHT + DESC_VIDEO_LIMIT_16TO19 + DESC_DATA_TYPE + 0x0b

;--------------------选择子属性----------------------
RPL0          equ 00b
RPL1          equ 01b
RPL2          equ 10b
RPL3          equ 11b
TI_GDT        equ 000b
TI_LDT        equ 100b

mbr.s源码:

%include "boot.inc"
section mbr vstart=0x7c00
;initialize sreg
mov ax, cs
mov ds, ax
mov es, ax
mov fs, ax
mov sp, 0x7c00
;let gs point to video memory
mov ax, 0xb800
mov gs, ax
 
;;;;;;;;;;clear screen;;;;;;;;;;
;ah = function number:0x06-->ah = 0x06
;al = number of rows to roll(0 means all)
;bh = scroll line properties
;(cl,ch) = the (x,y) position in the bottom left corner of the window
;(dl,dh) = the (x,y) position in the top right corner of the window
mov ax, 0x600;
mov bx, 0x700
mov cx, 0
mov dx, 0x184f   ;the default screen has 25row*80column
int 0x10
 
;output strings with red letters on green background through the graphics card
mov byte [gs:0x00], 'R'
mov byte [gs:0x01], 0xA4
mov byte [gs:0x02], 'e'
mov byte [gs:0x03], 0xA4
mov byte [gs:0x04], 'a'
mov byte [gs:0x05], 0xA4
mov byte [gs:0x06], 'l'
mov byte [gs:0x07], 0xA4
mov byte [gs:0x08], ' '
mov byte [gs:0x09], 0xA4
mov byte [gs:0x0a], 'M'
mov byte [gs:0x0b], 0xA4
mov byte [gs:0x0c], 'B'
mov byte [gs:0x0d], 0xA4
mov byte [gs:0x0e], 'R'
mov byte [gs:0x0f], 0xA4

mov eax, Loader_Start_Sector
mov bx, Loader_Base_Addr
mov cx, Loader_Sector_Cnt
call rd_disk

jmp Loader_Base_Addr+0x300                    ;let the program go to loader_start函数

rd_disk:

;first: set the number of read sectors
mov dx, 0x1f2
mov di, ax
mov al, cl
out dx, al
mov ax, di

;second: set LBA's address
;write the 0 to 7 bits of the LBA's address through port 0x1f3
mov dx, 0x1f3
out dx, al
;write the 8 to 15 bits of the LBA's address through port 0x1f4
mov cl, 0x08
shr eax, cl
mov dx, 0x1f4
out dx, al
;write the 16 to 23 bits of the LBA's address through port 0x1f5
shr eax, cl
mov dx, 0x1f5
out dx, al
;write device through port 0x1f6
shr eax, cl
mov dx, 0x1f6
and al, 0x0f
or al, 0xe0
out dx, al

;third: the read command, 0x20, is written to port 0x1f7
mov dx, 0x1f7
mov al, 0x20
out dx, al

;fourth: check the status of the hard disk
.not_ready:
nop
in al, dx
and al, 0x88   ;only the 3rd and 7th positions will be focused here
cmp al, 0x08
jnz .not_ready

;fifth: read data from port 0x1f0
;count reads times, cx get read times
mov ax, Loader_Sector_Cnt
mov dx, 256
mul dx
mov cx, ax
mov dx, 0x1f0
.read:
in ax, dx
mov [bx], ax
add bx, 2
loop .read

ret  ;return to "jmp Loader_Base_Addr"

times 510-($-$$) db 0  ;fill the program to 510 bytes size
dw 0xaa55                 ;fill in the magic number

loader.s源码:

%include"boot.inc"
section loader vstart=Loader_Base_Addr
Loader_Stack_Top equ Loader_Base_Addr

;构造DGT及其内部相关的段描述符表
;第0个段描述符(不可用)
GDT_BASE:    dd 0x00000000
             dd 0x00000000
;第1个段描述符
CODE_DESC:   dd 0x0000FFFF
             dd DESC_CODE_HIGH4
;第2个段描述符
DATA_EDSC:   dd 0x0000FFFF
             dd DESC_DATA_HIGH4
;第3个段描述符
VIDEO_EDSC:  dd 0x80000007          ;文本模式显示适配器的起始位置为0xb8000,空间大小为32KB,又因为段界限粒度为4K,所以,段界限为7
             dd DESC_VIDEO_HIGH4

GDT_SIZE     equ $ - GDT_BASE       ;在汇编语言中,equ伪指令定义的值并不占用程序的实际存储空间
GDT_LIMIT    equ GDT_SIZE - 1

times 60 dq 0                       ;dq表示8个字节,此处预留了60个段描述符的空间

;准备好三个段的选择子
SELECTOR_CODE  equ (0x0001<<3) + TI_GDT + RPL0
SELECTOR_DATA  equ (0x0002<<3) + TI_GDT + RPL0
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0

;此处,与loader文件开头的偏移量为0x200,(4 + 60)* 8 bytes = 0x200。loader的虚拟地址为0x900,所以total_mem_bytes在内存中的地址为0xb00
total_mem_bytes dd 0

gdt_ptr      dw GDT_LIMIT           ;先准备好lgdt寄存器的值
             dd GDT_BASE

loadermsg db "loader in real"

ards_buf times 230 db 0
ards_cnt dw 0

loader_start:
;;;;;;;;;;print string;;;;;;;;;;
;ah = function number;0x13-->ah = 0x13
;al = the way string are written
;cx = string's length
;bh = the page number of the page used to display string(0)
;bl = string's properties
;(dh,dl) = position(row,column)
mov sp, Loader_Base_Addr
mov bp, loadermsg         ;es:bp is the string's address
mov ax, 0x1301
mov cx, 14
mov bx, 0x001f
mov dx, 0x0100
int 0x10

;--------------获取物理内存方法-----------------
;----------------1、0xE820--------------------
mov edx, 0x534d4150      ;将固定标签写入edx
xor ebx, ebx             ;将ebx的数值提前置为0
mov di, ards_buf         ;将di指向ards的缓冲区
.e820_getmem_loop:
mov eax, 0xe820          ;设置子功能号
mov ecx, 0x14            ;设置ards的大小
int 0x15                 ;执行中断
jc .try_0xe801           ;cf为1表示出错,尝试子宫能e801
add di, cx               ;让di指向ards缓冲区中的下一个位置
inc word [ards_cnt]      ;让ards数量自增1
cmp ebx, 0               ;判断是否可以结束循环(ebx==0&&cf==0)
jnz .e820_getmem_loop    ;不满足条件,则继续循环

mov cx, [ards_cnt]       ;将ards数量复制给ecx
mov ebx, ards_buf        ;将ebx指向ards的存储区
xor edx, edx             ;先将edx清零,edx要存储最终内存大小
.get_maxmem:             ;获取最大内存
mov eax, [ebx]           ;BaseAddrLow
add eax, [ebx+8]         ;BaseAddrLow+LengthLow
add ebx, 20              ;指向下一个ards
cmp edx, eax             ;比较本次与前面的最大值的大小,保证每一次都是当前最大值
jge .next_ards
mov edx, eax
.next_ards:
loop .get_maxmem         ;循环,直到ards全部被遍历完
jmp .get_mem_ok          ;跳转到.get_mem_ok

;----------------2、0xE801--------------------
.try_0xe801:
mov ax, 0xe801           ;写入子功能号
int 0x15                 ;执行bochs中断
jc .try_0x88             ;cf=1,调用出错,调用子功能0x88
mov cx, 0x400            ;低15MB,以1KB为单位
mul cx                   ;将低15MB的将结果存在dx:ax中
shl edx, 16             
and eax, 0x0000ffff
or edx, eax              ;将低15MB的将结果存在edx中
add edx, 0x100000           ;15MB~16MB,预留出来给IAS设备
mov esi, edx             ;先将低15MB的结果存入esi执行的位置

mov ecx, 0x10000         ;16MB~4GB,以1MB为单位
or eax, eax
mov ax, bx
mul ecx
add esi, eax             ;16MB~4GB的结果置于eax中,将低15MB与16MB~4GB的结果相加,得到最后的内存大小
mov edx, esi             ;将内存大小写入edx
jmp .get_mem_ok

;----------------3、0x88----------------------
.try_0x88:
mov ah, 0x88             ;子功能号
int 0x15                 ;中断
jc .failed_get_mem
and eax, 0x0000ffff
mov cx, 0x400
mul cx
shl edx, 16
or edx, eax
add edx, 0x100000
jmp .get_mem_ok


.get_mem_ok:
mov [total_mem_bytes], edx


;--------------准备进入保护模式-----------------
;打开A20地址线
in al, 0x92
or al, 0x02
out 0x92, al
;加载GDT
lgdt [gdt_ptr]
;cr0第0位置1
mov eax, cr0
or eax, 0x00000001
mov cr0, eax


;刷新流水线
jmp dword SELECTOR_CODE:pro_mode_start

.failed_get_mem:            ;注意,如果此语句放在jmp的前面,将会出现“错误”的运行结果,不会显示”P“
hlt

[bits 32]
pro_mode_start:
;初始化数据段、附加数据段es、栈段寄存器
mov ax, SELECTOR_DATA
mov ds, ax
mov es, ax
mov ss, ax
;初始化栈顶指针
mov esp, Loader_Stack_Top
;初始化附加数据段gs
mov ax, SELECTOR_VIDEO
mov gs, ax
;在保护模式下,在显示屏第三行打下字符‘P’
mov byte [gs:320], 'P'
jmp $

编译:
nasm -I include/ -o mbr.bin mbr.s
nasm -I include/ -o loader.bin loader.s

写入磁盘:

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

运行:

/oskiller/bochs/bin/bochs -f bochsrc.disk

截图:

解释说明:运行界面的截图与lab3的截图没有区别,但是,本实验多加了获取物理内存的代码。在loader.s文件的0x200偏移处,定义了total_mem_bytes,用来记录物理内存的大小。而loader的虚拟起始地址为0x900,所以,查看0xb00处的内存,即可查看物理内存地址。

二、启用内存分页机制

2.1内存为什么需要分页

当系统中应用程序过多之后,或者内存碎片过多无法容纳新的进程(即使,碎片总容量大于新进程所需空间,也可能出现不能容纳的情况),或者曾经被换出磁盘的内存段需要再次装回内存,但是内存中找不到合适大小的内存区域怎么办?

一种方法就是,将内存中某个进程占用的某个段,换出磁盘,腾出空间。

内存段如何被换出?

段描述符中的A位是由CPU置1,操作系统清0的。操作系统每发现A为1时就将其清0,这样,可以统计出一个周期内该段被访问的频率了。当物理内存不足时,可以将使用频率最低的段换出磁盘,当被换出后,操作系统将该段描述符的P位置0

内存段如何被加载?

CPU允许在段描述符表中已经注册的段不在内存中,当P=0时,CPU抛出NP异常,转而去执行中断描述符表中的NP异常对应的中断处理程序,此程序是操作系统提供的,该程序将相应的段从外存中加载到内存,并将P置1。中断处理程序返回后,CPU重复检查P位,检查到P=1后,将A置1

然而,此种方法存在缺陷,当物理内存无法容纳任何一个进程的段时,就没法运行进程了。该如何处理呢?

出现上述问题,主要是在分段模式下,CPU认为线性地址等于物理地址。线性地址由编译器编译出来,本身是连续的,所以物理地址也是连续的。我们如何解除线性地址与物理地址一一对应的关系?

分页,实现了线性地址可以映射到任意的物理地址。编译出来连续的线性地址,可以被映射到不连续的物理地址上。

2.2一级页表

在不开启分页机制的情况下,保护模式中的段寄存器中的是选择子,依靠选择子在GDT或者LDT中找到段基址,然后结合段内偏移地址,就得到了线性地址,上述工作是由段部件完成的。CPU认为此地址是物理地址,可以直接被送上地址总线。

在开启分页机制之后,段部件输出的线性地址不再等同于物理地址,我们称之为虚拟地址,不应该被送上地址总线。此虚拟地址需要在页表中查找对应的物理地址,此项工作是由页部件完成的。

分页机制的作用

  • 将线性地址转化为物理地址
  • 用大小相等的页替代大小不等的段

如何进入分页机制?

①将页表地址加载到寄存器cr3;②将cr0的pg位置1

页号页内地址
12~310~11

一级页表机制下,线性地址被划分为2部分,页号是用来查找页表项,页内地址在确定页号之后,用来确定数据或者指令的具体物理地址。一般情况下,一页为4KB,一共有1M个页

怎么用虚拟地址找到真实的物理地址?

由cr3与页号,也可找到页表项,由页表项可以得到页表的起始物理地址,最后又页表的起始物理地址加上业内偏移,得到真实的物理地址。

2.3二级页表

二级页表机制,不需要一次性将页表项建好,需要时再动态建立页表项

二级页表,有一个页目录表,可以容纳1KB个页表项,一个页表可以容纳1KB个物理页,一个物理页4KB,刚好可以表示4GB内存。对于任意一个32位的物理地址,它一定再某个页表之内的某个物理页中。二级页表地址转换原理将32位线性地址,拆分成三部门,如下图所示:

页目录号页号页内地址
22~3112~210~11

怎么用虚拟地址找到真实的物理地址?

由cr3与页目录号找到页目录项,得到页表的物理地址;由页表的物理地址和页号找到页表项,得到物理页的物理地址;由物理页的物理地址和页内地址得到真实的物理地址

页目录项结构如下:

页表的物理地址AVLG0DAPCDPWTUSRWP
12~319~11876543210

页表项结构如下:

页表的物理地址AVLGPATDAPCDPWTUSRWP
12~319~11876543210

由于标准页大小为4KB,所以低12位是0,只需要记录高20位即可,剩下的12位存放其它属性

①P字段:存在位,P=1表示存在于内存中

②RW字段:读写位,RW=0表示可读不可写,RW=1表示可读写

③US字段:普通用户/超级用户位,US=1,表示用户

④PWT字段:页级通写位,本实验置0

⑤PCD字段:页级高速缓存禁止位,本实验置0

⑥A字段:访问位,CPU访问过,则置为1

⑦D字段:脏页位,为1表示被修改过

⑧PAT字段:页属性表位

⑨G字段:全局位

⑩AVL字段:可用位,软件可用位

2.4依据OS与用户进程的关系规划页表

在保护模式下,为了计算机安全,用户进程应该运行在低特权级下,需要访问硬件资源时,需要项OS申请,由OS做完之后,再将结果返回给用户进程。用户进程可以有很多个,但是操作系统只有一个,所以,操作系统必须“共享”给所有用户进程

如何在页表中实现共享呢?只要操作系统属于用户进程的虚拟地址空间就可以了。为了实现操作系统的共享,让所有用户进程3GB~4GB的虚拟地址空间都指向同一个操作系统,也就是指向同一片物理页地址。

2.5用代码开启分页

本实验,在获取物理内存的基础上,进行了页目录表与页表的构建,完成了开启分页机制,并在分页机制下,输出字符“V”

实验思路:

  • 页目录表的起始地址是真实物理地址0x100000,将页目录表所占的内存空间逐字节清零
  • 创建第0、768个页目录,指向第一个页表
  • 创建第1023个页目录,指向页目录表所在的物理页
  • 创建第0个页目录项指向的页表的256个页表项,对应真实物理地址的低1MB
  • 创建第769~1022个页目录
  • 将gdt加载到内核所在的高地址处
  • 进入分页模式

为什么第2步中,第0、768个页目录均指向第一个页表?

在加载内核之前,程序中运行的一直是loader,其本身代码在1MB以内,需要保证之前段机制下的线性地址与虚拟地址对应的物理地址一致,所以用了第0项来保证loader在分页机制下依然运行正确。

操作系统的虚拟地址是0xc0000000以上,我们需要将操作系统内核放在低端1MB物理空间中。所以,为了实现此映射,将第768个页目录也指向第一个页表了。(第一个页表中,前256个页表项中,对应真实物理内存的低1MB)

为什么第4步,只需要创建256个页表项?

我们的内核,不会超过1MB,所以,我们此处只需要建立256个页表项即可

为什么要创建第769~1022个页目录?

此处将内核空间的页目录填满,是为了将来用户进程做准备的,这样可以使得用户进程共享内核空间。为了实现所有用户进程共享内核,需要使得各用户进程的高1GB都指向内核所在的物理空间,也就是每个进程的页目录表中第768~1022项都是和其它进程一致的。在用户进程创建也表示,我们直接复制就好了。

本实验有三个文件,分别是:boot.inc、mbr.s、loader_2.s

boot.inc源代码:

;-------------------loader&&kernel-------------------
Loader_Base_Addr equ 0x900                  ;loader虚拟起始地址
Loader_Start_Sector equ 0x2                 ;开始扇区标号
Loader_Sector_Cnt equ 0x4                   ;操作扇区数

Page_Dir_Table_Pos equ 0x100000             ;页目录表起始地址

;----------------提前设置好gdt的相关属性-----------------
DESC_G_4K     equ 0x800000                  ;段界限粒度为4K
DESC_D_32     equ 0x400000                  ;代码段,指令中的有效地址及其操作数大小为32位
DESC_L        equ 0x000000                  ;32位代码段,置为0
DESC_AVL      equ 0x000000                  ;是否可用,置为0
DESC_CODE_LIMIT_16TO19   equ 0xf0000        ;代码段段界限的16~19位
DESC_DATA_LIMIT_16TO19   equ 0xf0000        ;数据段段界限的16~19位
DESC_VIDEO_LIMIT_16TO19  equ 0x00000        ;显存段段界限的16~19位
DESC_P        equ 0x8000                    ;存在位,置为1
DESC_DPL0     equ 0x0000                    ;优先级为0
DESC_DPL1     equ 0x2000                    ;优先级为1
DESC_DPL2     equ 0x4000                    ;优先级为2
DESC_DPL3     equ 0x6000                    ;优先级为3
DESC_S_NOTSYM equ 0x1000                    ;当前描述符不是系统段
DESC_S_SYM    equ 0x0000                    ;当前描述副是系统段
DESC_CODE_TYPE           equ 0x800          ;代码段TYPE字段描述
DESC_DATA_TYPE           equ 0x200          ;数据段TYPE字段描述

MID_WEIGHT    equ (0x00<<24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_P + DESC_DPL0 + DESC_S_NOTSYM

DESC_CODE_HIGH4          equ MID_WEIGHT + DESC_CODE_LIMIT_16TO19 + DESC_CODE_TYPE + 0x00
DESC_DATA_HIGH4          equ MID_WEIGHT + DESC_DATA_LIMIT_16TO19 + DESC_DATA_TYPE + 0x00
DESC_VIDEO_HIGH4          equ MID_WEIGHT + DESC_VIDEO_LIMIT_16TO19 + DESC_DATA_TYPE + 0x0b

;--------------------选择子属性----------------------
RPL0          equ 00b
RPL1          equ 01b
RPL2          equ 10b
RPL3          equ 11b
TI_GDT        equ 000b
TI_LDT        equ 100b

;--------------------页表相关属性----------------------
PG_P     equ 0x1
PG_RW_R  equ 0x0
PG_RW_W  equ 0x2
PG_US_S  equ 0x0
PG_US_U  equ 0x4

mbr.s源代码:

%include "boot.inc"
section mbr vstart=0x7c00
;initialize sreg
mov ax, cs
mov ds, ax
mov es, ax
mov fs, ax
mov sp, 0x7c00
;let gs point to video memory
mov ax, 0xb800
mov gs, ax
 
;;;;;;;;;;clear screen;;;;;;;;;;
;ah = function number:0x06-->ah = 0x06
;al = number of rows to roll(0 means all)
;bh = scroll line properties
;(cl,ch) = the (x,y) position in the bottom left corner of the window
;(dl,dh) = the (x,y) position in the top right corner of the window
mov ax, 0x600;
mov bx, 0x700
mov cx, 0
mov dx, 0x184f   ;the default screen has 25row*80column
int 0x10
 
;output strings with red letters on green background through the graphics card
mov byte [gs:0x00], 'R'
mov byte [gs:0x01], 0xA4
mov byte [gs:0x02], 'e'
mov byte [gs:0x03], 0xA4
mov byte [gs:0x04], 'a'
mov byte [gs:0x05], 0xA4
mov byte [gs:0x06], 'l'
mov byte [gs:0x07], 0xA4
mov byte [gs:0x08], ' '
mov byte [gs:0x09], 0xA4
mov byte [gs:0x0a], 'M'
mov byte [gs:0x0b], 0xA4
mov byte [gs:0x0c], 'B'
mov byte [gs:0x0d], 0xA4
mov byte [gs:0x0e], 'R'
mov byte [gs:0x0f], 0xA4

mov eax, Loader_Start_Sector
mov bx, Loader_Base_Addr
mov cx, Loader_Sector_Cnt
call rd_disk

jmp Loader_Base_Addr+0x300                    ;let the program go to Loader

rd_disk:

;first: set the number of read sectors
mov dx, 0x1f2
mov di, ax
mov al, cl
out dx, al
mov ax, di

;second: set LBA's address
;write the 0 to 7 bits of the LBA's address through port 0x1f3
mov dx, 0x1f3
out dx, al
;write the 8 to 15 bits of the LBA's address through port 0x1f4
mov cl, 0x08
shr eax, cl
mov dx, 0x1f4
out dx, al
;write the 16 to 23 bits of the LBA's address through port 0x1f5
shr eax, cl
mov dx, 0x1f5
out dx, al
;write device through port 0x1f6
shr eax, cl
mov dx, 0x1f6
and al, 0x0f
or al, 0xe0
out dx, al

;third: the read command, 0x20, is written to port 0x1f7
mov dx, 0x1f7
mov al, 0x20
out dx, al

;fourth: check the status of the hard disk
.not_ready:
nop
in al, dx
and al, 0x88   ;only the 3rd and 7th positions will be focused here
cmp al, 0x08
jnz .not_ready

;fifth: read data from port 0x1f0
;count reads times, cx get read times
mov ax, Loader_Sector_Cnt
mov dx, 256
mul dx
mov cx, ax
mov dx, 0x1f0
.read:
in ax, dx
mov [bx], ax
add bx, 2
loop .read

ret  ;return to "jmp Loader_Base_Addr"

times 510-($-$$) db 0  ;fill the program to 510 bytes size
dw 0xaa55                 ;fill in the magic number

loader_2.s源代码:

%include"boot.inc"
section loader vstart=Loader_Base_Addr
Loader_Stack_Top equ Loader_Base_Addr

;构造DGT及其内部相关的段描述符表
;第0个段描述符(不可用)
GDT_BASE:    dd 0x00000000
             dd 0x00000000
;第1个段描述符
CODE_DESC:   dd 0x0000FFFF
             dd DESC_CODE_HIGH4
;第2个段描述符
DATA_EDSC:   dd 0x0000FFFF
             dd DESC_DATA_HIGH4
;第3个段描述符
VIDEO_EDSC:  dd 0x80000007          ;文本模式显示适配器的起始位置为0xb8000,空间大小为32KB,又因为段界限粒度为4K,所以,段界限为7
             dd DESC_VIDEO_HIGH4

GDT_SIZE     equ $ - GDT_BASE       ;在汇编语言中,equ伪指令定义的值并不占用程序的实际存储空间
GDT_LIMIT    equ GDT_SIZE - 1

times 60 dq 0                       ;dq表示8个字节,此处预留了60个段描述符的空间

;准备好三个段的选择子
SELECTOR_CODE  equ (0x0001<<3) + TI_GDT + RPL0
SELECTOR_DATA  equ (0x0002<<3) + TI_GDT + RPL0
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0

;此处,与loader文件开头的偏移量为0x200,(4 + 60)* 8 bytes = 0x200。loader的虚拟地址为0x900,所以total_mem_bytes在内存中的地址为0xb00
total_mem_bytes dd 0

gdt_ptr      dw GDT_LIMIT           ;先准备好lgdt寄存器的值
             dd GDT_BASE

loadermsg db "loader in real"

ards_buf times 230 db 0
ards_cnt dw 0

loader_start:
;;;;;;;;;;print string;;;;;;;;;;
;ah = function number;0x13-->ah = 0x13
;al = the way string are written
;cx = string's length
;bh = the page number of the page used to display string(0)
;bl = string's properties
;(dh,dl) = position(row,column)
mov sp, Loader_Base_Addr
mov bp, loadermsg         ;es:bp is the string's address
mov ax, 0x1301
mov cx, 14
mov bx, 0x001f
mov dx, 0x0100
int 0x10

;--------------获取物理内存方法-----------------
;----------------1、0xE820--------------------
mov edx, 0x534d4150      ;将固定标签写入edx
xor ebx, ebx             ;将ebx的数值提前置为0
mov di, ards_buf         ;将di指向ards的缓冲区
.e820_getmem_loop:
mov eax, 0xe820          ;设置子功能号
mov ecx, 0x14            ;设置ards的大小
int 0x15                 ;执行中断
jc .try_0xe801           ;cf为1表示出错,尝试子宫能e801
add di, cx               ;让di指向ards缓冲区中的下一个位置
inc word [ards_cnt]      ;让ards数量自增1
cmp ebx, 0               ;判断是否可以结束循环(ebx==0&&cf==0)
jnz .e820_getmem_loop    ;不满足条件,则继续循环

mov cx, [ards_cnt]       ;将ards数量复制给ecx
mov ebx, ards_buf        ;将ebx指向ards的存储区
xor edx, edx             ;先将edx清零,edx要存储最终内存大小
.get_maxmem:             ;获取最大内存
mov eax, [ebx]           ;BaseAddrLow
add eax, [ebx+8]         ;BaseAddrLow+LengthLow
add ebx, 20              ;指向下一个ards
cmp edx, eax             ;比较本次与前面的最大值的大小,保证每一次都是当前最大值
jge .next_ards
mov edx, eax
.next_ards:
loop .get_maxmem         ;循环,直到ards全部被遍历完
jmp .get_mem_ok          ;跳转到.get_mem_ok

;----------------2、0xE801--------------------
.try_0xe801:
mov ax, 0xe801           ;写入子功能号
int 0x15                 ;执行bochs中断
jc .try_0x88             ;cf=1,调用出错,调用子功能0x88
mov cx, 0x400            ;低15MB,以1KB为单位
mul cx                   ;将低15MB的将结果存在dx:ax中
shl edx, 16             
and eax, 0x0000ffff
or edx, eax              ;将低15MB的将结果存在edx中
add edx, 0x100000           ;15MB~16MB,预留出来给IAS设备
mov esi, edx             ;先将低15MB的结果存入esi执行的位置

mov ecx, 0x10000         ;16MB~4GB,以1MB为单位
or eax, eax
mov ax, bx
mul ecx
add esi, eax             ;16MB~4GB的结果置于eax中,将低15MB与16MB~4GB的结果相加,得到最后的内存大小
mov edx, esi             ;将内存大小写入edx
jmp .get_mem_ok

;----------------3、0x88----------------------
.try_0x88:
mov ah, 0x88             ;子功能号
int 0x15                 ;中断
jc .failed_get_mem
and eax, 0x0000ffff
mov cx, 0x400
mul cx
shl edx, 16
or edx, eax
add edx, 0x100000
jmp .get_mem_ok


.get_mem_ok:
mov [total_mem_bytes], edx


;--------------准备进入保护模式-----------------
;打开A20地址线
in al, 0x92
or al, 0x02
out 0x92, al
;加载GDT
lgdt [gdt_ptr]
;cr0第0位置1
mov eax, cr0
or eax, 0x00000001
mov cr0, eax


;刷新流水线
jmp dword SELECTOR_CODE:pro_mode_start

.failed_get_mem:            ;注意,如果此语句放在jmp的前面,将会出现“错误”的运行结果,不会显示”P“
hlt

[bits 32]
pro_mode_start:
;初始化数据段、附加数据段es、栈段寄存器
mov ax, SELECTOR_DATA
mov ds, ax
mov es, ax
mov ss, ax
;初始化栈顶指针
mov esp, Loader_Stack_Top
;初始化附加数据段gs
mov ax, SELECTOR_VIDEO
mov gs, ax
;在保护模式下,在显示屏第三行打下字符‘P’
mov byte [gs:320], 'P'

;调研函数创建页目录与页表
call setup_page

sgdt [gdt_ptr]                    ;将GDT寄存器的信息写入内存[gdt_ptr]处

mov edx, [gdt_ptr + 2]            ;edx指向GDT的起始地址
or dword [edx + 0x18 + 4], 0xc0000000;修改视频段的段基址的高8位

add dword [gdt_ptr + 2], 0xc0000000  ;让gdt的起始地址处于内核空间中

add esp, 0xc0000000               ;将栈指针指向内核空间

;-----------------开启分页-------------------
;将页目录表的地址存入cr3
mov eax, Page_Dir_Table_Pos
mov cr3, eax
;将cr0的第31位(PG)置1
mov eax, cr0
or eax, 0x80000000
mov cr0, eax

;进入分页模式后,重新加载GDT寄存器
lgdt [gdt_ptr]

mov byte [gs:480], 'V'             ;开启分页后,打印字符“V”

jmp $                               ;使程序悬停于此

setup_page:
;将页目录表所在空间清零
;页目录表占4KB
mov ecx, 4096                              ;将循环次数提前写到cx中
mov esi, 0
.clear_page_dir:
;需要注意的是:现在处于平坦模式下,段基址为0,所以,需要访问相应的物理地址,直接令段内偏移等于该物理地址即可,此时仍未进入分页,所以段部件得出的地址直接是真实的物理地址
mov byte [Page_Dir_Table_Pos + esi], 0x0   ;逐字节清零
inc esi
loop .clear_page_dir

;开始创建第0、768个页目录项
.create_pde:
mov eax, Page_Dir_Table_Pos
add eax, 0x1000                           ;将第一个页表的物理地址写入eax
mov ebx, eax                              ;ebx也获取第一个页表的物理地址
or eax, PG_US_U | PG_RW_W | PG_P          ;初始化页目录项,将该页目录项写入第0、768个页目录中
mov [Page_Dir_Table_Pos + 0x0], eax       ;将初始化好的页目录项写入第0页目录中
mov [Page_Dir_Table_Pos + 0xc00], eax     ;将初始化好的页目录项写入第768页目录中

sub eax, 0x1000                           ;让eax的高20位指向页目录表的地址的高20位
mov [Page_Dir_Table_Pos + 4092], eax      ;使得最后一个页目录项指向页目录表的地址

;创建页表项
;此处的映射,是将第一个页表中的前256个页表项与物理内存中的低1MB建立映射关系
mov ecx, 256                              ;需要创建256个页表项,每页4KB,256页正好为1MB,因为我们编写的程序,都是在内存中的低1MB,所以只创建256个页表项即可
mov esi, 0
mov edx, PG_US_U | PG_RW_W | PG_P         ;准备第一个页表项,此处edx为页表项的值,高20位为0,所以,该页表项对应的物理页起始物理地址为0
.create_pte:
mov [ebx + esi*4], edx                    ;将准备好的页表项填入页表中
add edx, 4096                             ;准备下一个页表项
inc esi                                   ;增加esi的值,是的ebx + esi*4指向下一个页表项的地址
loop .create_pte                          ;循环

;创建内和其他页表的pde
mov eax, Page_Dir_Table_Pos
add eax, 0x2000                           ;eax获取第二个页表的位置
or eax, PG_US_U |  PG_RW_W | PG_P         ;准备页目录项
mov ebx, Page_Dir_Table_Pos
mov ecx, 254                              ;需要初始化第769~1022个页目录项
mov esi, 769                              ;esi与ebx配合,给出页目录项所在的地址
.create_kernel_pde:
mov [ebx + esi*4], eax                    ;写入提前准备好的页目录项
inc esi                                   ;更新esi的值,以便与ebx配合得到下一个页目录项的地址
add eax, 0x1000                           ;得到下一个页目录项
loop .create_kernel_pde
ret

实验结果:

结果分析:在代码中,我们重新加载了gdt,将其放到了高1GB的内核中间,可以看到,gdt的基址变成了0xc0000000。视频段,是用来实现输出的,打印功能需要在内核实现,所以也要将其置于内核中,栈段,也要修改成内核使用的地址。

在开启分页机制之前,讲cr0的PG位置1,将页目录表的起始地址加载到cr3中,执行命令“creg”,与预期相符合。

2.6用虚拟地址访问页表

二级页表,可以根据需要动态增减页目录项或者页表项。

页表位于内存中,修改页表的操作需要通过内存地址才行。页表是将虚拟地址转换位物理地址的映射,在分页机制下,如何使用虚拟地址访问页表本身?

在loader中,在第1023个页目录项存入了页目录表自身的地址,为用虚拟地址访问页表提供可能。

上述截图,是在bochs中执行“info tab”指令,得到的虚拟地址到物理地址的映射。

对于第一、二项,是我们在代码中刻意安排的,是第0、768个页表在发挥作用。使得虚拟地址的低1MB和内核中的低1MB都映射到了真实物理地址的低1MB

后面三项的映射,体现了我们在loader中的操作。以0xffc00000~0xffc00fff为例,对于0xffc00000,其高10位为全1,中间10位为全0,低12位亦为全0。高10位,为页目录项的索引,索引到第1023个页目录项。该页目录项中,记载着页表的起始地址,然而,在代码中,我们设定第1023个页目录项指向页目录表自身。所以,此时页目录表被当成页表,中间10位索引页表项,索引到第0项,我们指导,第0个页表的起始地址为0x101000。低12位为页内偏移地址,为0,所以,0xffc00000对应的真实物理地址是0x101000。其它两项类似,不再赘述。

虚拟地址获取页表中各类数据类型的方法:

  • 获取页目录表物理地址,需要使得虚拟地址高20位为全1,低12位为全0
  • 访问页目录中的页目录项,虚拟地址高20位位全1,低12位为***,***是页目录项的索引值乘4

2.7块表TLB

为什么需要快表?

①从虚拟地址到物理地址的转化,过程繁琐

②访问页表需要访问内存,cpu与内存的速度不是一个量级的,访问页表对于cpu来说十分耗时

快表干了什么?

存放虚拟地址页到物理地址页的映射,还存储部分属性。

缓存需要更新,快表如何更新?

TLB中存储的是程序赖以运行的指令和数据的内存地址,需要保证地址的有效性。快表可以通过重新加载cr3,也可以通过invlpg指令更新。第一种方式会使得整个快表失效,第二种针对某一条目更新。

三、加载内核

3.1用c语言编写内核

使用c语言编写脱离了操作系统的程序,必须自己指定程序入口

c语言程序经过编译之后,生成目标文件,此文件是待重定位文件。

链接阶段,可以指定最终生成的可执行文件的起始虚拟地址。由于程序的地址是在链接阶段编排的,所以链接阶段必须明确地址入口。

3.2elf格式的二进制文件

前面BIOS交权给MBR,MBR交权给Loader,都是通过jmp指令来实现的。这样实现的基础是,我们提前知道mbr在0x7c00处,loader在0x900处。这种方法是不灵活的,需要调用方和被调用方提前约定好调用地址。

elf,这种文件格式很好的解决了上述问题。elf的文件头中,有被调函数的程序入口信息,只需要读出入口信息,将被调程序加载到相应的入口地址,在跳转即可。elf中,有文件头和程序主体,所以,elf不再只包含程序本身的指令和数据,在文件头,包含了程序的一些其它信息。

elf目标文件类型有:待重定位文件、共享目标文件、可执行文件

elf header中包含的数据

  • e_ident[16]:包含魔术,elf文件类型,编码格式
  • e_type:目标文件类型
  • e_machine:描述elf目标文件的体系结构类型
  • e_version:版本信息
  • e_entry:虚拟起始地址
  • e_phoff:程序头表在文件内的偏移量
  • e_shoff:节头表在文件内的偏移量
  • e_flags:指明与处理器相关的标志
  • e_ehsize:指明elf header的字节大小
  • e_phentsize:程序头表中条目的字节大小
  • e_phnum:程序头表中条目数量
  • e_shentsize:节头表中条目的字节大小
  • e_shnum:节头表中条目数量
  • e_shstrndx:指明string name table在节头表中的索引

接下来介绍段的描述结构。注意,此段是指程序中的某个数据或代码的区域段落,并不是内存中的段,此处讨论的是磁盘上的程序文件。

phdr中包含的内容

  • p_type:段的类型
  • p_offset:本段在文件内的起始偏移地址
  • p_vaddr:本段在内存中的起始偏移地址
  • p_paddr:此处忽略
  • p_filesz:本段在文件中的大小
  • p_memsz:本段在内存中的大小
  • p_flags:与本段相关的标志(某些权限)
  • p_align:在文件、内存中的对齐方式

3.3用代码将内核载入内存

本实验有三个文件组成,分别是boot.inc、mbr.s、loader.s

实验思路:

①将内核文件kernel.bin从磁盘中读进内存

②此时,读进内存的是elf格式的可执行文件,需要将该文件内的各个segment拷贝到在链接阶段分配到的地址

Ⅰ、获取elf头文件中的程序头表的偏移量、程序头的数量、程序头的大小

Ⅱ、逐个将程序段拷贝到指定的地址(通过获取phdr得到程序段大小、目的地址、源地址)

实验前置知识点

  • 了解elf格式及其内容
  • 了解phdr的格式及其内容
  • 了解指令movsb、rep、lcd的用法

boot.inc源代码:

;-------------------loader&&kernel-------------------
Loader_Base_Addr equ 0x900                  ;loader虚拟起始地址
Loader_Start_Sector equ 0x2                 ;开始扇区标号
Loader_Sector_Cnt equ 0x4                   ;操作扇区数

Page_Dir_Table_Pos equ 0x100000             ;页目录表起始地址

Kernel_Start_Sector  equ 0x9
Kernel_Bin_Base_Addr equ 0x70000
Kernel_Entry_Addr    equ 0xc0001500

;----------------提前设置好gdt的相关属性-----------------
DESC_G_4K     equ 0x800000                  ;段界限粒度为4K
DESC_D_32     equ 0x400000                  ;代码段,指令中的有效地址及其操作数大小为32位
DESC_L        equ 0x000000                  ;32位代码段,置为0
DESC_AVL      equ 0x000000                  ;是否可用,置为0
DESC_CODE_LIMIT_16TO19   equ 0xf0000        ;代码段段界限的16~19位
DESC_DATA_LIMIT_16TO19   equ 0xf0000        ;数据段段界限的16~19位
DESC_VIDEO_LIMIT_16TO19  equ 0x00000        ;显存段段界限的16~19位
DESC_P        equ 0x8000                    ;存在位,置为1
DESC_DPL0     equ 0x0000                    ;优先级为0
DESC_DPL1     equ 0x2000                    ;优先级为1
DESC_DPL2     equ 0x4000                    ;优先级为2
DESC_DPL3     equ 0x6000                    ;优先级为3
DESC_S_NOTSYM equ 0x1000                    ;当前描述符不是系统段
DESC_S_SYM    equ 0x0000                    ;当前描述副是系统段
DESC_CODE_TYPE           equ 0x800          ;代码段TYPE字段描述
DESC_DATA_TYPE           equ 0x200          ;数据段TYPE字段描述

MID_WEIGHT    equ (0x00<<24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_P + DESC_DPL0 + DESC_S_NOTSYM

DESC_CODE_HIGH4          equ MID_WEIGHT + DESC_CODE_LIMIT_16TO19 + DESC_CODE_TYPE + 0x00
DESC_DATA_HIGH4          equ MID_WEIGHT + DESC_DATA_LIMIT_16TO19 + DESC_DATA_TYPE + 0x00
DESC_VIDEO_HIGH4          equ MID_WEIGHT + DESC_VIDEO_LIMIT_16TO19 + DESC_DATA_TYPE + 0x0b

;--------------------选择子属性----------------------
RPL0          equ 00b
RPL1          equ 01b
RPL2          equ 10b
RPL3          equ 11b
TI_GDT        equ 000b
TI_LDT        equ 100b

;--------------------页表相关属性----------------------
PG_P     equ 0x1
PG_RW_R  equ 0x0
PG_RW_W  equ 0x2
PG_US_S  equ 0x0
PG_US_U  equ 0x4

;--------------------program type定义-----------------
PT_NULL equ 0x0

mbr.s源代码:

%include "boot.inc"
section mbr vstart=0x7c00
;initialize sreg
mov ax, cs
mov ds, ax
mov es, ax
mov fs, ax
mov sp, 0x7c00
;let gs point to video memory
mov ax, 0xb800
mov gs, ax
 
;;;;;;;;;;clear screen;;;;;;;;;;
;ah = function number:0x06-->ah = 0x06
;al = number of rows to roll(0 means all)
;bh = scroll line properties
;(cl,ch) = the (x,y) position in the bottom left corner of the window
;(dl,dh) = the (x,y) position in the top right corner of the window
mov ax, 0x600;
mov bx, 0x700
mov cx, 0
mov dx, 0x184f   ;the default screen has 25row*80column
int 0x10
 
;output strings with red letters on green background through the graphics card
mov byte [gs:0x00], 'R'
mov byte [gs:0x01], 0xA4
mov byte [gs:0x02], 'e'
mov byte [gs:0x03], 0xA4
mov byte [gs:0x04], 'a'
mov byte [gs:0x05], 0xA4
mov byte [gs:0x06], 'l'
mov byte [gs:0x07], 0xA4
mov byte [gs:0x08], ' '
mov byte [gs:0x09], 0xA4
mov byte [gs:0x0a], 'M'
mov byte [gs:0x0b], 0xA4
mov byte [gs:0x0c], 'B'
mov byte [gs:0x0d], 0xA4
mov byte [gs:0x0e], 'R'
mov byte [gs:0x0f], 0xA4

mov eax, Loader_Start_Sector
mov bx, Loader_Base_Addr
mov cx, Loader_Sector_Cnt
call rd_disk

jmp Loader_Base_Addr+0x300                    ;let the program go to Loader

rd_disk:

;first: set the number of read sectors
mov dx, 0x1f2
mov di, ax
mov al, cl
out dx, al
mov ax, di

;second: set LBA's address
;write the 0 to 7 bits of the LBA's address through port 0x1f3
mov dx, 0x1f3
out dx, al

;write the 8 to 15 bits of the LBA's address through port 0x1f4
mov cl, 0x08
shr eax, cl
mov dx, 0x1f4
out dx, al

;write the 16 to 23 bits of the LBA's address through port 0x1f5
shr eax, cl
mov dx, 0x1f5
out dx, al

;write device through port 0x1f6
shr eax, cl
mov dx, 0x1f6
and al, 0x0f
or al, 0xe0
out dx, al

;third: the read command, 0x20, is written to port 0x1f7
mov dx, 0x1f7
mov al, 0x20
out dx, al

;fourth: check the status of the hard disk
.not_ready:
nop
in al, dx
and al, 0x88   ;only the 3rd and 7th positions will be focused here
cmp al, 0x08
jnz .not_ready

;fifth: read data from port 0x1f0
;count reads times, cx get read times
mov ax, Loader_Sector_Cnt
mov dx, 256
mul dx
mov cx, ax
mov dx, 0x1f0
.read:
in ax, dx
mov [bx], ax
add bx, 2
loop .read

ret  ;return to "jmp Loader_Base_Addr"

times 510-($-$$) db 0  ;fill the program to 510 bytes size
dw 0xaa55                 ;fill in the magic number

loader.s源代码:

%include"boot.inc"
section loader vstart=Loader_Base_Addr
Loader_Stack_Top equ Loader_Base_Addr

;--------------------构造DGT及其内部相关的段描述符表------------------------
;第0个段描述符(不可用)
GDT_BASE:    dd 0x00000000
             dd 0x00000000
;第1个段描述符
CODE_DESC:   dd 0x0000FFFF
             dd DESC_CODE_HIGH4
;第2个段描述符
DATA_EDSC:   dd 0x0000FFFF
             dd DESC_DATA_HIGH4
;第3个段描述符
VIDEO_EDSC:  dd 0x80000007          ;文本模式显示适配器的起始位置为0xb8000,空间大小为32KB,又因为段界限粒度为4K,所以,段界限为7
             dd DESC_VIDEO_HIGH4

GDT_SIZE     equ $ - GDT_BASE       ;在汇编语言中,equ伪指令定义的值并不占用程序的实际存储空间
GDT_LIMIT    equ GDT_SIZE - 1

times 60 dq 0                       ;dq表示8个字节,此处预留了60个段描述符的空间

;准备好三个段的选择子
SELECTOR_CODE  equ (0x0001<<3) + TI_GDT + RPL0
SELECTOR_DATA  equ (0x0002<<3) + TI_GDT + RPL0
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0

;此处,与loader文件开头的偏移量为0x200,(4 + 60)* 8 bytes = 0x200。loader的虚拟地址为0x900,所以total_mem_bytes在内存中的地址为0xb00
total_mem_bytes dd 0

gdt_ptr      dw GDT_LIMIT           ;先准备好lgdt寄存器的值
             dd GDT_BASE

loadermsg db "loader in real"

ards_buf times 230 db 0
ards_cnt dw 0

loader_start:
;;;;;;;;;;print string;;;;;;;;;;
;ah = function number;0x13-->ah = 0x13
;al = the way string are written
;cx = string's length
;bh = the page number of the page used to display string(0)
;bl = string's properties
;(dh,dl) = position(row,column)
mov sp, Loader_Base_Addr
mov bp, loadermsg         ;es:bp is the string's address
mov ax, 0x1301
mov cx, 14
mov bx, 0x001f
mov dx, 0x0100
int 0x10

;----------------------------获取物理内存方法--------------------------------
;----------------1、0xE820--------------------
mov edx, 0x534d4150      ;将固定标签写入edx
xor ebx, ebx             ;将ebx的数值提前置为0
mov di, ards_buf         ;将di指向ards的缓冲区
.e820_getmem_loop:
mov eax, 0xe820          ;设置子功能号
mov ecx, 0x14            ;设置ards的大小
int 0x15                 ;执行中断
jc .try_0xe801           ;cf为1表示出错,尝试子宫能e801
add di, cx               ;让di指向ards缓冲区中的下一个位置
inc word [ards_cnt]      ;让ards数量自增1
cmp ebx, 0               ;判断是否可以结束循环(ebx==0&&cf==0)
jnz .e820_getmem_loop    ;不满足条件,则继续循环

mov cx, [ards_cnt]       ;将ards数量复制给ecx
mov ebx, ards_buf        ;将ebx指向ards的存储区
xor edx, edx             ;先将edx清零,edx要存储最终内存大小
.get_maxmem:             ;获取最大内存
mov eax, [ebx]           ;BaseAddrLow
add eax, [ebx+8]         ;BaseAddrLow+LengthLow
add ebx, 20              ;指向下一个ards
cmp edx, eax             ;比较本次与前面的最大值的大小,保证每一次都是当前最大值
jge .next_ards
mov edx, eax
.next_ards:
loop .get_maxmem         ;循环,直到ards全部被遍历完
jmp .get_mem_ok          ;跳转到.get_mem_ok

;----------------2、0xE801--------------------
.try_0xe801:
mov ax, 0xe801           ;写入子功能号
int 0x15                 ;执行bochs中断
jc .try_0x88             ;cf=1,调用出错,调用子功能0x88
mov cx, 0x400            ;低15MB,以1KB为单位
mul cx                   ;将低15MB的将结果存在dx:ax中
shl edx, 16             
and eax, 0x0000ffff
or edx, eax              ;将低15MB的将结果存在edx中
add edx, 0x100000           ;15MB~16MB,预留出来给IAS设备
mov esi, edx             ;先将低15MB的结果存入esi执行的位置

mov ecx, 0x10000         ;16MB~4GB,以1MB为单位
or eax, eax
mov ax, bx
mul ecx
add esi, eax             ;16MB~4GB的结果置于eax中,将低15MB与16MB~4GB的结果相加,得到最后的内存大小
mov edx, esi             ;将内存大小写入edx
jmp .get_mem_ok

;----------------3、0x88----------------------
.try_0x88:
mov ah, 0x88             ;子功能号
int 0x15                 ;中断
jc .failed_get_mem
and eax, 0x0000ffff
mov cx, 0x400
mul cx
shl edx, 16
or edx, eax
add edx, 0x100000
jmp .get_mem_ok


.get_mem_ok:
mov [total_mem_bytes], edx


;-------------------------------准备进入保护模式-----------------------------
;打开A20地址线
in al, 0x92
or al, 0x02
out 0x92, al
;加载GDT
lgdt [gdt_ptr]
;cr0第0位置1
mov eax, cr0
or eax, 0x00000001
mov cr0, eax


;刷新流水线
jmp dword SELECTOR_CODE:pro_mode_start

.failed_get_mem:            ;注意,如果此语句放在jmp的前面,将会出现“错误”的运行结果,不会显示”P“
hlt

[bits 32]
pro_mode_start:
;初始化数据段、附加数据段es、栈段寄存器
mov ax, SELECTOR_DATA
mov ds, ax
mov es, ax
mov ss, ax
;初始化栈顶指针
mov esp, Loader_Stack_Top
;初始化附加数据段gs
mov ax, SELECTOR_VIDEO
mov gs, ax
;在保护模式下,在显示屏第三行打下字符‘P’
mov byte [gs:320], 'P'

;----------------------loader kernel------------------------------
mov eax, Kernel_Start_Sector
mov ebx, Kernel_Bin_Base_Addr
mov ecx, 200

call rd_disk_32

;调用函数创建页目录与页表
call setup_page

sgdt [gdt_ptr]                    ;将GDT寄存器的信息写入内存[gdt_ptr]处

mov edx, [gdt_ptr + 2]            ;edx指向GDT的起始地址
or dword [edx + 0x18 + 4], 0xc0000000;修改视频段的段基址的高8位

add dword [gdt_ptr + 2], 0xc0000000  ;让gdt的起始地址处于内核空间中

add esp, 0xc0000000               ;将栈指针指向内核空间

;---------------------------开启分页----------------------------------
;将页目录表的地址存入cr3
mov eax, Page_Dir_Table_Pos
mov cr3, eax
;将cr0的第31位(PG)置1
mov eax, cr0
or eax, 0x80000000
mov cr0, eax

;进入分页模式后,重新加载GDT寄存器
lgdt [gdt_ptr]

mov byte [gs:480], 'V'             ;开启分页后,打印字符“V”

jmp SELECTOR_CODE:enter_kernel

enter_kernel:
call kernel_init
mov esp, 0xc009f000                 ;更新栈指针
jmp Kernel_Entry_Addr               ;跳转到内核去执行了

;------------将kernel.bin中的segment拷贝到编译的地址-------------------
kernel_init:
xor eax, eax
xor ebx, ebx
xor ecx, ecx
xor edx, edx

mov dx, [Kernel_Bin_Base_Addr + 42]                ;dx获取program size
mov ebx, [Kernel_Bin_Base_Addr + 28]               
add ebx, Kernel_Bin_Base_Addr                      ;ebx获取程序头表的地址(此处是物理地址)
mov cx, [Kernel_Bin_Base_Addr + 44]                ;cx获取program number

;--------------------------生成内核映像-------------------------------
.each_segment:
cmp byte [ebx + 0x0], PT_NULL
je .PTNULL                                         ;当pro_type==PT_NULL时,表示当program header没有使用

;此处演示通过压栈传递函数参数
;memcpy(dst, src, size)
push dword [ebx + 16]                              ;先将size压栈

mov eax, [ebx + 4]
add eax, Kernel_Bin_Base_Addr
push eax                                           ;再将src压栈

push dword [ebx + 8]                               ;最后将dst压栈

call memcpy
add esp, 12                                        ;将压入的三个参数出栈

.PTNULL:
add ebx, edx                                       ;令ebx指向下一个program header
loop .each_segment                                 ;循环ecx次
ret                                                ;完成全部拷贝之后,返回调用处

;-------------------------copy by byte-------------------------------
memcpy:
cld                                                ;对于movsb指令,在搬运完数据之后,esi、edi并不会自己变化,增加此指令,使得movsb一次后,esi、edi自增
push ebp                                           ;先将ebp的值压栈
mov ebp, esp
push ecx                                           ;rep是的搬运指令重复执行,rep的依据就是ecx,由于外层也需要使用到ecx,所以此处先将ecx压栈
mov edi, [ebp + 8]                                 ;通过ebp访问dst
mov esi, [ebp + 12]                                ;通过ebp访问src
mov ecx, [ebp + 16]                                ;通过ebp访问size
rep movsb                                          ;重复执行ecx次

pop ecx
pop ebp
ret

;-----------------------创建页目录表和页表----------------------------
setup_page:
;将页目录表所在空间清零
;页目录表占4KB
mov ecx, 4096                              ;将循环次数提前写到cx中
mov esi, 0
.clear_page_dir:
;需要注意的是:现在处于平坦模式下,段基址为0,所以,需要访问相应的物理地址,直接令段内偏移等于该物理地址即可,此时仍未进入分页,所以段部件得出的地址直接是真实的物理地址
mov byte [Page_Dir_Table_Pos + esi], 0x0   ;逐字节清零
inc esi
loop .clear_page_dir

;开始创建第0、768个页目录项
.create_pde:
mov eax, Page_Dir_Table_Pos
add eax, 0x1000                           ;将第一个页表的物理地址写入eax
mov ebx, eax                              ;ebx也获取第一个页表的物理地址
or eax, PG_US_U | PG_RW_W | PG_P          ;初始化页目录项,将该页目录项写入第0、768个页目录中
mov [Page_Dir_Table_Pos + 0x0], eax       ;将初始化好的页目录项写入第0页目录中
mov [Page_Dir_Table_Pos + 0xc00], eax     ;将初始化好的页目录项写入第768页目录中

sub eax, 0x1000                           ;让eax的高20位指向页目录表的地址的高20位
mov [Page_Dir_Table_Pos + 4092], eax      ;使得最后一个页目录项指向页目录表的地址

;创建页表项
;此处的映射,是将第一个页表中的前256个页表项与物理内存中的低1MB建立映射关系
mov ecx, 256                              ;需要创建256个页表项,每页4KB,256页正好为1MB,因为我们编写的程序,都是在内存中的低1MB,所以只创建256个页表项即可
mov esi, 0
mov edx, PG_US_U | PG_RW_W | PG_P         ;准备第一个页表项,此处edx为页表项的值,高20位为0,所以,该页表项对应的物理页起始物理地址为0
.create_pte:
mov [ebx + esi*4], edx                    ;将准备好的页表项填入页表中
add edx, 4096                             ;准备下一个页表项
inc esi                                   ;增加esi的值,是的ebx + esi*4指向下一个页表项的地址
loop .create_pte                          ;循环

;创建内和其他页表的pde
mov eax, Page_Dir_Table_Pos
add eax, 0x2000                           ;eax获取第二个页表的位置
or eax, PG_US_U |  PG_RW_W | PG_P         ;准备页目录项
mov ebx, Page_Dir_Table_Pos
mov ecx, 254                              ;需要初始化第769~1022个页目录项
mov esi, 769                              ;esi与ebx配合,给出页目录项所在的地址
.create_kernel_pde:
mov [ebx + esi*4], eax                    ;写入提前准备好的页目录项
inc esi                                   ;更新esi的值,以便与ebx配合得到下一个页目录项的地址
add eax, 0x1000                           ;得到下一个页目录项
loop .create_kernel_pde
ret


rd_disk_32:

;first: set the number of read sectors
mov dx, 0x1f2
mov di, ax
mov al, cl
out dx, al
mov ax, di

;second: set LBA's address
;write the 0 to 7 bits of the LBA's address through port 0x1f3
mov dx, 0x1f3
out dx, al

;write the 8 to 15 bits of the LBA's address through port 0x1f4
mov cl, 0x08
shr eax, cl
mov dx, 0x1f4
out dx, al

;write the 16 to 23 bits of the LBA's address through port 0x1f5
shr eax, cl
mov dx, 0x1f5
out dx, al

;write device through port 0x1f6
shr eax, cl
mov dx, 0x1f6
and al, 0x0f
or al, 0xe0
out dx, al

;third: the read command, 0x20, is written to port 0x1f7
mov dx, 0x1f7
mov al, 0x20
out dx, al

;fourth: check the status of the hard disk
.not_ready:
nop
in al, dx
and al, 0x88   ;only the 3rd and 7th positions will be focused here
cmp al, 0x08
jnz .not_ready

;fifth: read data from port 0x1f0
;count reads times, cx get read times
mov ax, Loader_Sector_Cnt
mov dx, 256
mul dx
mov cx, ax
mov dx, 0x1f0
.read:
in ax, dx
mov [ebx], ax
add bx, 2
loop .read

ret

四、lab1~lab4总结

目前为止,程序经历了三次交权:

  • BIOS向MBR交权
  • MBR向LOADER交权
  • LOADER向KERNEL交权

BIOS是计算机一启动就在运行的程序,主要做一些检查硬件和初始化的工作。完成工作之后,将磁盘中0盘0道1扇区中的MBR加载到内存的0x7c00处,然后跳转到0x7c00,实现了向MBR的交权

在MBR中,我们可以通过BIOS中断或者直接操作显卡的方式,输出字符;可以直接操作磁盘,实现读盘和写盘。在实验中,我们通过读盘,把位于2、4、5、6三扇区的loader加载到以0x900为起始地址的内存中,然后跳转到0x900,实现了向loader的交权

目前为止,系统在实模式下运行,在loader中,我们实现了分段(全局描述符表、选择子,进入保护模式)、分页(实现线性地址可以被映射到任意的物理地址)、获取物理内存、加载内核(本质是读磁盘和程序段拷贝)。将内核中的段拷贝到各段自己被编译的虚拟地址处(编址是链接阶段的工作)。最后,跳转到内核去执行

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值