本文是笔者学习《操作系统真象还原》的读书笔记,若有侵权,请联系删除。笔者也是处于学习阶段,所陈述的内容如有错误,欢迎指正!
一、获取物理内存
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格式如下:
字节偏移量 | 属性名称 | 描述 |
0 | BaseAddrLow | 基地址低32位 |
4 | BaseAddrHigh | 基地址高32位 |
8 | LengthLow | 内存长度低32位,以字节位单位 |
12 | LengthHigh | 内存长度高32位,以字节位单位 |
16 | Type | 本段内存类型 |
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~31 | 0~11 |
一级页表机制下,线性地址被划分为2部分,页号是用来查找页表项,页内地址在确定页号之后,用来确定数据或者指令的具体物理地址。一般情况下,一页为4KB,一共有1M个页
怎么用虚拟地址找到真实的物理地址?
由cr3与页号,也可找到页表项,由页表项可以得到页表的起始物理地址,最后又页表的起始物理地址加上业内偏移,得到真实的物理地址。
2.3二级页表
二级页表机制,不需要一次性将页表项建好,需要时再动态建立页表项。
二级页表,有一个页目录表,可以容纳1KB个页表项,一个页表可以容纳1KB个物理页,一个物理页4KB,刚好可以表示4GB内存。对于任意一个32位的物理地址,它一定再某个页表之内的某个物理页中。二级页表地址转换原理将32位线性地址,拆分成三部门,如下图所示:
页目录号 | 页号 | 页内地址 |
22~31 | 12~21 | 0~11 |
怎么用虚拟地址找到真实的物理地址?
由cr3与页目录号找到页目录项,得到页表的物理地址;由页表的物理地址和页号找到页表项,得到物理页的物理地址;由物理页的物理地址和页内地址得到真实的物理地址。
页目录项结构如下:
页表的物理地址 | AVL | G | 0 | D | A | PCD | PWT | US | RW | P |
12~31 | 9~11 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
页表项结构如下:
页表的物理地址 | AVL | G | PAT | D | A | PCD | PWT | US | RW | P |
12~31 | 9~11 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
由于标准页大小为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中,我们实现了分段(全局描述符表、选择子,进入保护模式)、分页(实现线性地址可以被映射到任意的物理地址)、获取物理内存、加载内核(本质是读磁盘和程序段拷贝)。将内核中的段拷贝到各段自己被编译的虚拟地址处(编址是链接阶段的工作)。最后,跳转到内核去执行