简介
实验:构造页目录及页表,完成从虚拟地址3GB~3GB+1MB到实地址0 ~1MB的映射以及虚拟地址0 ~ 1MB向物理地址0 ~ 1MB的映射
注意:此时loader.bin大小超过1024字节 所以mbr要读取3个扇区,dd要复制3个扇区
代码
boot/mbr.s
; boot/mbr.s
; 功能:读取磁盘,加载loader到内存并跳转到loader的loader_start位置
%include "boot.inc"
SECTION MBR vstart=0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
mov ax, 0xb800
mov gs,ax
; 清屏利用0x06号功能,上卷全部行,则可清屏。
; 输入:
; AH 功能号=0x06
; AL = 上卷的行数(如果为0,表示全部)
; BH = 上卷行属性
; (CL,CH) = 窗口的左上角的(X,Y)位置
; (DL,DH) = 窗口的右上角的(X,Y)位置
; 无返回值:
mov ax,0x600
mov bx,0x700
mov cx,0
mov dx,0x184f
int 0x10
mov eax,LOADER_START_SECTOR
mov bx,LOADER_BASE_ADDR
mov cx,3
call rd_disk_m_16
jmp LOADER_BASE_ADDR + 0x300
; 读取硬盘n个扇区
; eax = LBA扇区号
; bx = 将数据写入的内存地址
; cx=读入的扇区数
rd_disk_m_16:
; 备份
mov esi,eax
mov di,cx
; 设置要读取的扇区数
mov cl,0x8
mov dx,0x1f2
mov al,cl
out dx,al
mov eax,esi
; 设置要读取的地址
mov dx,0x1f3 ; 写入0-7位
out dx,al
mov cl,0x8
shr eax,cl
mov dx,0x1f4 ; 写入8-15位
out dx,al
shr eax,cl
mov dx,0x1f5 ; 写入16-23位
out dx,al
shr eax,cl
and al,0x0f ; 24~27位
or al,0xe0 ; 设置7~4位为1110,表示lba模式
mov dx,0x1f6
out dx,al
; 写入读命令
mov dx,0x1f7
mov al,0x20
out dx,al
; 检测硬盘状态
.not_ready:
nop
in al,dx
and al,0x88
cmp al,0x08
jnz .not_ready
; 读数据
mov ax,di
mov dx,256
mul dx
mov cx,ax
mov dx,0x1f0
.go_on_read:
in ax,dx
mov [bx],ax
add bx,2
loop .go_on_read
ret
times 510-($-$$) db 0
db 0x55,0xaa
boot/loader.s
; boot/loader.s
; 功能:完成在内存里写好3个段描述符(代码段、数据段、显存段)、赋值好GDTR寄存器、创建好表示3个选择子的字段后。
; 读取物理内存。
; 进入保护模式
; 开启分页
%include "boot.inc"
SECTION loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
; ---构建gdt---
GDT_BASE: ; 一个8字节为段描述符结构体 注意:第一次4字节是及地址+段限长
dd 0x0000_0000 ; dd define dword(4字节)
dd 0x0000_0000
CODE_DESC:
dd 0x0000_ffff
dd DESC_CODE_HIGH4
DATA_STARK_DESC:
dd 0x0000_ffff
dd DESC_DATA_HIGH4
VEDIO_DESC:
dd 0x8000_0007 ; limit=(0xbffff-0xb8000)/4k=0x7
dd DESC_VIDEO_HIGH4
GDT_SIZE equ $ - GDT_BASE
GDT_LIMIT equ GDT_SIZE - 1
times 60 dq 0 ; 预留60个段描述的空间
; ---内存容量---
total_mem_bytes dd 0 ; 此处的地址为0x900 + (64 * 8 = 0x200) = 0xb00,偏移地址为0x200
; ---定义选择子---
SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0
; ---赋值GDTR寄存器---
gdt_ptr dw GDT_LIMIT
dd GDT_BASE
; ards_buf为缓冲区地址,缓冲区用来装0xe820子功能返回的ARDS结构体
; 一个ARDS结构体20字节,本次实验测试一共返回了6个
ards_buf times 244 db 0
ards_nr dw 0 ; 大小2个字节,用于记录 ARDS 结构体数量
; 人工对齐:total_mem_bytes+gdt_ptr+ards_buf+ards_nr=4+6+244+2=256,共256=0x100字节
loader_start: ; loader_start地址为0x900+0x300,偏移地址为0x300
;jmp .e820_failed_so_try_e801 ; 直接测试第2种
;jmp .e801_failed_so_try88 ; 直接测试第3种
; ---方法1 int 15h eax = 0000e820h ,edx = 534D4150h ('SMAP')获取内存布局 begin---
xor ebx,ebx ; 异或运算,第一次使用0xe820子功能ebx要清0
mov edx, 0x534d4150 ; edx 只赋值一次,循环体中不会改变
mov di,ards_buf ; ES:DI是ards结构缓冲区的指针,es已在mbr.s赋值完毕了,这里不用再赋值.
.e820_mem_get_loop: ; 循环获取每个ARDS内存范围描述结构
mov eax,0x0000e820 ; 执行int0x15后,eax值为0x534d4150, 所以每次执行int前都要更新为子功能号
mov ecx, 20 ; 1个ards结构体大小为20字节
int 0x15;
jc .e820_failed_so_try_e801 ;cf 位为 1则 有错误发生,尝试 Oxe801子功能
add di, cx ; 使di 增加 20 字节指向缓冲区中新的 ARDS 结构位置
inc word [ards_nr] ; 记录 ARDS 数量
cmp ebx, 0
jnz .e820_mem_get_loop ; 若ebx不等于0,继续调用e820子功能返回ards结构体
; 在所有的ARDS结构体中,找到(base_addr_low + length_low)的最大值,即内存的容量
mov cx, [ards_nr] ; 遍历每一个 ARDS 结构体,循环次数是 ARDS 的数量
mov ebx, ards_buf ; ards缓冲区首地址
xor edx, edx ; edx为最大容量,在此先清0
.find_max_mem_area:
; 无需判断 type 是否为1,最大的内存块一定是可被使用的
mov eax, [ebx] ; 拿到结构体的 base_addr_low
add eax, [ebx+8] ; base_addr_low + length_low
add ebx, 20 ; 指向缓冲区中下-个 ARDS 结构
cmp edx,eax
jge .next_ards ; edx大于eax就访问下一个ards结构体
mov edx, eax ; 小于就记录在dex中
.next_ards:
loop .find_max_mem_area ; cx记录了ards结构体数量,cx为0说明已经循环结束了
jmp .mem_get_ok ; 将最大内存容量放入total_mem_byte中
; ---方法1 int 15h eax = 0000E820h ,edx = 534D4150h ('SMAP')获取内存布局 end---
;---方法2 int 15h ax = e801h 获取内存大小,最大支持4G begin---
; 返回后, ax cx 值一样,以KB为单位,bx dx值一样,以64KB为单位
; 在ax和cx寄存器中为低16M,在bx和dx寄存器中为16MB到4G。
.e820_failed_so_try_e801:
mov ax,0xe801
int 0x15
jc .e801_failed_so_try88 ;若当前e801方法失败,就尝试0x88方法
; 1 先算出低15M的内存,ax和cx中是以KB为单位的内存数量,将其转换为以byte为单位
mov cx,0x400 ; cx和ax值一样,cx用做乘数 0x400 = 1024
mul cx
shl edx,16
and eax,0x0000FFFF
or edx,eax
add edx, 0x100000 ; ax只是15MB,故要加1MB
mov esi,edx
;2 再将16MB以上的内存转换为byte为单位,寄存器bx和dx中是以64KB为单位的内存数量
xor eax,eax
mov ax,bx
mov ecx, 0x10000 ;0x10000十进制为64KB
mul ecx ;32位乘法,默认的被乘数是eax,积为64位,高32位存入edx,低32位存入eax.
add esi,eax ;由于此方法只能测出4G以内的内存,故32位eax足够了,edx肯定为0,只加eax便可
mov edx,esi ;edx为总内存大小
jmp .mem_get_ok
;---方法2 int 15h ax = e801h 获取内存大小,最大支持4G end---
;---方法3 int 15h ah = 0x88 获取内存大小,只能获取64MB以内 begin---
.e801_failed_so_try88:
; int 15后,ax存入的是以kb为单位的内存容量
mov ah, 0x88
int 0x15
jc .error_hlt
and eax,0x0000FFFF
;16位乘法,被乘数是ax,积为32位.积的高16位在dx中,积的低16位在ax中
mov cx, 0x400 ;0x400等于1024,将ax中的内存容量换为以byte为单位
mul cx
shl edx, 16 ;把dx移到高16位
or edx, eax ;把积的低16位组合到edx
add edx,0x100000 ;0x88子功能只会返回1MB以上的内存,故实际内存大小要加上1MB
;---方法3 int 15h ah = 0x88 获取内存大小,只能获取64MB以内 end---
.mem_get_ok:
mov [total_mem_bytes ], edx
; ---进入保护模式---
; 打开A20
in al,0x92
or al,0000_0010B
out 0x92,al
; 加载gdt
lgdt [gdt_ptr]
; cr0第0位给1
mov eax,cr0
or eax,0x0000_0001
mov cr0,eax
; 刷新流水线
jmp dword SELECTOR_CODE:p_mode_start
.error_hlt: ; 出错则挂起
jmp $
[bits 32]
p_mode_start:
mov ax,SELECTOR_DATA
mov ds,ax
mov es,ax
mov ss,ax
mov esp,LOADER_STACK_TOP
mov ax,SELECTOR_VIDEO
mov gs,ax
call setup_page ; 创建页目录及页表
sgdt [gdt_ptr] ; 存储到原来gdt所有的位置
; 把视频段的基地址+3G
mov ebx, [gdt_ptr + 2] ; [gdt_ptr + 2]为gdt的起始地址
or dword [ebx + 0x18 + 4], 0xc000_0000 ; [ebx + 0x18 + 4]为视频段描述符的后4个字节的首地址 3*8=24=0x18
; 高8位是基地址的高8位,0xc000_0000(3G)的高8位给到基地址的高8位
; 实现的效果就是: 基地址 = 高8位的3G + 低24位的基地址值
add dword [gdt_ptr + 2],0xc000_0000 ; 映射之后 GDTR寄存器的全局描述符表地址也要加上3G
add esp,0xc000_0000 ; 将栈指针也要加上3G
;---开启分页机制 begin---
; 把页目录地址赋给cr3
mov eax, PAGE_DIR_TABLE_POS
mov cr3, eax
; 打开cr0的pg位(第31位)
mov eax, cr0
or eax, 0x80000000
mov cr0, eax
;---开启分页机制 end---
lgdt [gdt_ptr] ; 赋值GDTR寄存器新的段描述符表
mov byte [gs:160], 'V';
jmp $
;---创建页目录及页表 begin---
setup_page:
; 1.先把页目录的(1024个页目录项)4096字节清0
mov ecx, 4096
mov esi, 0
.clear_page_dir:
mov byte [PAGE_DIR_TABLE_POS + esi], 0
inc esi
loop .clear_page_dir
; 2;把页目录项0和页目录项0xc00(768)指向第0个页表,在把第0个页表的前256个页表项绑定物理地址0-1M
create_pde: ; Page Directory Entry
mov eax, PAGE_DIR_TABLE_POS
add eax, 0x1000 ; 0x1000=4096 eax+4096页目录后面挨着的第0个页表的地址
mov ebx, eax
; 把页第0个和第0xc00(768)个目录项的内容写入第0个页表的地址
or eax, PG_US_U | PG_RW_W | PG_P ; 页目录项的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问.
mov [PAGE_DIR_TABLE_POS + 0x0], eax
mov [PAGE_DIR_TABLE_POS + 0xc00], eax
sub eax, 0x1000 ; 此时eax是页目录的起始地址
mov [PAGE_DIR_TABLE_POS + 4092], eax ; 使最后一个目录项指向页目录表的地址
; 下面是把 第0个页表的前256个页表项的内容 绑定到 物理地址0-1M
mov ecx, 256 ; 1M低端内存 / 每页大小4k = 256
mov esi, 0
mov edx, PG_US_U | PG_RW_W | PG_P ; edx表示页表项结构体的内容
.create_pte: ; Page Table Entry
mov [ebx + esi * 4],edx ; 此时ebx是第0个页表的地址
add edx,0x1000 ; 每一个页表项的内容的第31-12位表示物理页地址的31-12位,所以递增0x1000(4096)的效果是把第31-12位递增1,相当于物理地址+4096
inc esi
loop .create_pte
; 3.把页目录项769-1022指向第1-255个页表,第1-255页表没有绑定物理地址
mov eax, PAGE_DIR_TABLE_POS
add eax, 0x2000 ; 0x2000=4096*2 eax+4096*2就是第1个页表的地址
or eax, PG_US_U | PG_RW_W | PG_P ; eax表示页目录项结构体的内容
mov ebx, PAGE_DIR_TABLE_POS ; ebx位页目录起始地址
mov ecx, 254 ; 循环次数254 范围为第769~1022的所有目录项数量
mov esi, 769 ; 起始索引
.create_kernel_pde:
mov [ebx + esi * 4], eax
inc esi
add eax, 0x1000 ; 页目录项结构体的第31-12位是页表物理地址的31-12位 所以递增0x1000(4096)的效果是把第31-12位递增1,相当于移动到下一个页表的位置
loop .create_kernel_pde
ret
;---创建页目录及页表 end---
编译
Makefile
BUILD_DIR = ./build
.PHONY : mk_dir bootloader clean all
mk_dir: #创建build目录
if [ ! -d $(BUILD_DIR) ]; then mkdir $(BUILD_DIR); fi
bootloader: #编译启动内核的文件 bootloader
nasm -I include/ -o $(BUILD_DIR)/mbr.bin boot/mbr.s
nasm -I include/ -o $(BUILD_DIR)/loader.bin boot/loader.s
dd if=/home/c/tityos/build/mbr.bin of=/home/c/tityos/hd60M.img bs=512 count=1 conv=notrunc
dd if=/home/c/tityos/build/loader.bin of=/home/c/tityos/hd60M.img bs=512 count=3 seek=2 conv=notrunc
clean: #删除build目录里的全部文件
cd $(BUILD_DIR) && rm -f ./*
# 创建build目录。编译启动内核的文件。
all: mk_dir bootloader
运行
start.sh
# !/bin/bash
# 功能:启动bochs
bin/bochs -f bochsrc.disk