当前位置:   article > 正文

开启分页并进入内核_内核分页请求

内核分页请求

一、概要

本节主要是设置二级分页,配置线性内存和物理内存的映射关系,然后按照elf文件规则,将C语言写的内核加载到指定虚拟内存中。目前的内核是一个main函数,里面是一个while(1)循环。例外,获取内存大小用汇编的0x15中断会比较方便,所以在进入内核前先获取到内存大小,保持到内存中,以后会使用到。以下是设置的分页映射关系,左边是线性内存地址,右边是物理内存地址:在这里插入图片描述

二、获取内存信息

1、0x15子功能号
EAX=0xE820 : 遍历全部内存,dmesg 命令就与 0xE820 相关。
AX=0xE801 : 别检测低 15M和16M~4GB 的内存。
AH=0x88 : 最多检查64M。
2、0xe820介绍
(1)地址范围描述符(ARDS):存储内存信息内容。每次 int OxlS 之后, BIOS就返回这样一个结构的数据,20字节。
在这里插入图片描述
(2)Type字段说明内存的用途在这里插入图片描述
(3)CF位:若 CF 位为0表示调用未出错, CF为1,表示调用出错。
3、0xe801介绍
(1) AX和CX 存储15M及以下空间,AX和CX值一样,单位1KB。所以15M及以下容量=AX1024
BX和DX 存储16M到4G空间,BX和DX值一样,单位64KB,所以15M以上容量=BX
641024
4、0x88
内存大小:AX
1024 字节+1MB

三、分页机制

1、分页机制作用:
(1)将线性地址转换成物理地址
(2)用大小相等的页代替大小不等的段。操作系统先分段,再分页。
2、分页原理—— 一级页表
(1)在32位地址总线,默认高20位表内存块数量,即1M。低12位作为内存块尺寸,即4KB.
(2)线性地址的一页对应物理地址的一页。4GB物理内存也被分成4GB/4KB=1M(1048576)个页。
(3)32位线性地址中,高20位索引物理内存第几个页,低12位索引页块内具体物理地址(即偏移量)。虚拟地址的高20 位可用来定位一个物理页,低 12 位可用来在该物理页内寻址。
在这里插入图片描述

(4)页表物理地址加载到CR3寄存器。页表和页表项寻址时,都是物理地址。
(5)总结一级页表寻址:高20位*4B+CR3内页表物理地址+低12位地址。
表示高20位为页表项索引,每个页表项大小4B,CR3存储的地址相当基地址,这样就得到页表项的地址,该地址保存着对应物理页地址。然后+低12位,得到具体到1字节的物理地址。
举例:mov eax, [0x1234],平坦模式下段基址为0。寻址流程如下图:
在这里插入图片描述

四、二级页表

1、目的:一级页表是一次性建好,二级页表可以随进程创建动态生成页表项。
2、二级页表原理
(1)32位地址中,二级页表分为一级页号(高10位),二级页号(中10位),页内偏移(低12位)。
在这里插入图片描述
(2)一级页号也称页目录表,页目录表占4KB,里面页目录项占4B,共1024个页目录项。页目录项保存页表物理地址。
(3)二级页号也称页表,每个页表占4KB,里面页表项占4B,共1024个页表项。页表项保存页块物理地址。
(4)页块4KB,页块物理地址+页内偏移量=保存物理地址的页块内地址(映射的物理地址)。
3、地址转换
(1) 从CR3中读取页目录起始地址+高10位4,计算出页目录表内页目录项,保存要找的页表物理地址。
(2) 页表物理地址+中10位
4,计算出页表项地址,保存着物理页地址。
(3) 物理页地址+低12位,计算出物理地址。
mov eax,[0x1234567]为例,最后得到物理地址0xfa567,需要注意的是中位10位计算方式10 0011 0100很容易看成0x8d0(1000 1101 00)。
在这里插入图片描述
4、cr3寄存器
在这里插入图片描述
5、页目录项和页表项结构
在这里插入图片描述
一般只设置US,RW,P位,其他位为0。
(1)P:该页是否存在内存中,存在1,不存在0
(2)RW:意为读写位。若为 1 表示可读可写,若为 0 表示可读不可写 。
(3)US:普通用户/超级用户位。若为 1 时,表示处于 User 级,任意级别( 0 、 l 、 2、3 )特权的程序都可以访问该页。若为 0,表示处于 Supervisor 级,特权级别为 3的程序不允许访问该页,该页只允许特权级别为0、1、2的程序访问。
6、TLB中存储虚拟地地址高20位到物理地址高20位的映射关系。
7、TLB的维护由操作系统完成
(1)重新加载cr3寄存器,会重新加载整个TLB。
(2)使用invlpg指令对指定的缓存条目进行刷新。

五、启动分页方式

1、启用分页机制,按顺序做好三件事 :
(1)准备好页目录表及页表。
(2)将页表地址写入控制寄存器cr3。还是物理地址状态。
(3)寄存器cr0的PG位置1。开启后,使用虚拟地址。
2、规划页表:
(1)LINUX系统中,0~3GB 是用户进程, 3GB~4GB 是操作系统。
(2)实现操作系统被用户进程共享,则是所有用户进程的3GB~4GB都指向操作系统物理内存空间
3、二级页表物理内存分布
在这里插入图片描述
4、如果页目录项的物理地址放在0x100000,那么页表的内存地址分布如下:

六、代码

1、code tree
在这里插入图片描述
2、bochs

###############################################################
# Configuration file for Bochs
###############################################################

# how much memory the emulated machine will have
megs: 32

magic_break: enabled=1
display_library: x, options="gui_debug"

# filename of ROM images
romimage: file=/usr/local/share/bochs/BIOS-bochs-latest
vgaromimage: file=/usr/local/share/bochs/VGABIOS-lgpl-latest

# what disk images will be used
#floppya: 1_44=./hd30M.img, status=inserted
ata0-master: type=disk, path="./hd30M.img", mode=flat

# choose the boot disk.
boot: disk

# where do we send log messages?
# log: bochsout.txt

# disable the mouse
mouse: enabled=0

# enable key mapping, using US layout as default.
#keyboard_mapping: enabled=1, map=/usr/share/bochs/keymaps/x11-pc-us.map
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

3、makefile

.PHONY:build image clean

mbr_src=mbr.asm
boot_src=boot.asm
mbr=./out/boot/mbr.bin
boot=./out/boot/boot.bin
kernel=./out/kernel/kernel.bin
img=./hd30M.img

all:clean build image bochs

build:
	if [ ! -d "./out/boot" ]; then mkdir ./out/boot;fi;if [ ! -d "./out/kernel" ]; then mkdir ./out/kernel;fi
	nasm -I ./boot/include/ -o ./out/boot/mbr.bin ./boot/mbr.asm
	nasm -I ./boot/include/ -o ./out/boot/boot.bin ./boot/boot.asm
	gcc -m32 -c -o out/kernel/main.o kernel/main.c
	ld -m elf_i386 out/kernel/main.o -Ttext 0xc0001500 -e main -o out/kernel/kernel.bin

## boot.img,30MB大小,前512字节是MBR

image:
	@-rm -rf $(img)
	bximage -hd=30 -func=create -imgmode=flat -sectsize=512 -q $(img)
	dd if=$(mbr) of=$(img) bs=512 count=1  conv=notrunc
	dd if=$(boot) of=$(img) bs=512 count=4 seek=2 conv=notrunc
	dd if=$(kernel) of=$(img) bs=512 count=200 seek=9 conv=notrunc

bochs:
	bochs -qf bochsrc

clean:
	rm -rf *.img ./out/kernel/* ./out/boot/*

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

4、boot.inc

LOADER_BASE_ADDR equ 0x500  ;boot.asm的起始内存地址

PT_NULL equ 0               ;一个变量
  • 1
  • 2
  • 3

5、mbr.asm

;引入外部文件,这个文件定义常量
%include "boot.inc"
;代码以此地址作为偏移地址的基地址
[ORG 0x7c00]

;指定代码段和16bit模式
[SECTION .text]
[BITS 16]

_start:
	mov ax,cs           ;寄存器清0
	mov ds,ax
	mov es,ax
	mov ss,ax
	mov fs,ax
	mov sp,0x7c00       ;设置栈寄存器sp,栈地址从上往下存储,恰好0x500~0x7bff是可用区域
	mov ax,0xb800       ;设置gs段寄存器地址,gs段寄存器不能直接赋值,只能通过先传给通用寄存器,再传gs寄存器。
	mov gs,ax

	;call print          ;跳转打印

	mov eax,2           ;读取起始扇区地址
	mov bx,LOADER_BASE_ADDR       ;写入的地址
	mov cx,4			;待读入扇区数
	call rd_disk        ;将boot加载到内存

	call print          ;打印"MBR"
	jmp LOADER_BASE_ADDR

print:
	;int 0x10 相当函数,参数从ax,bx,cx,dx这些寄存器取。清掉BIOS的输出。
	mov	ax,	0x0600  ;ah=0x06,表示功能号,功能为上卷清屏.al=0x0,表示上卷行数,为0则表示全部
	mov	bx,	0x0700  ;上卷属性,黑底
	mov	cx,	0       ;左上角坐标(0, 0)
	mov	dx,	0x184f  ;右下角坐标(24, 79), 0x18=24,0x4f=79
	int	0x10        ;0x10中断

	;写入1MB内存中,文本显示区域0xb800-0xbffff,显卡会自动在该内存映射到显示器。显示字符需要两个字节,一个保存字符,一个保存字符属性
	mov byte [gs:0x00],'M'
	mov byte [gs:0x01],0x0F  ;表示黑色背景,4表示前景色为白色
	mov byte [gs:0x02],'B'
	mov byte [gs:0x03],0x0F
	mov byte [gs:0x04],'R'
	mov byte [gs:0x05],0x0F
	ret

rd_disk:
	mov esi,eax     ;备份esi
	mov di,cx       ;备份di

	;设置读取扇区数
	mov dx,0x1f2
	mov al,cl
	out dx,al
	mov eax,esi

	;0x1f3 8bit iba地址低八位 0-7
	inc dx
	out dx,al

	;0x1f4 8bit iba地址中八位 8-15
	inc dx
	mov cl,8        ;shr移多个位可以用cl存储移位数
	shr eax,cl
	out dx,al

	;0x1f5 8bit iba地址高八位 16-23
	inc dx
	shr eax,cl
	out dx,al

    ; 0x1f6 8bit
    ; 0-3 位iba地址的24-27
    ; 4 0表示主盘 1表示从盘
    ; 5、7位固定为1
    ; 6 0表示CHS模式,1表示LAB模式
    inc dx
    mov al, ch
    or al, 0xe0
    out dx, al

    ; 0x1f7 8bit  命令或状态端口
    inc dx
    mov al, 0x20
    out dx, al

    ;循环检查硬盘状态
.read_check:
    in al,dx
    and al,0x88  ; 取硬盘状态的第3、7位
    cmp al,0x08  ; 硬盘数据准备好了且不忙了
    jnz .read_check

    ; 读数据
    mov ax, di
    mov cx,256
    mul cx       ;一个扇区512字节,每次读2字节,故一个扇区需要读取256次。既是ax*cx。mul的隐式表达式为:dx*ax=ax*cx,溢出会存到dx,故这里dx会赋值0x00,所以mov dx,0x1f0需要写在mul后面,否则会被修改。
    mov cx,ax
    mov dx,0x1f0

    ;循环读取硬盘数据到内存
.read_data:
    in ax, dx
    mov [bx], ax
    add bx, 2
    loop .read_data

    ret

    times 510-($-$$) db 0    ;一个扇区512字节,减去0xaa55为510字节。($$-$)为上面代码所占用字节数,510-($-$$)为当前行到魔术符字节数。time 字节数 dd 0 : 填充当前字节开始,到字节数为0
    dw 0xaa55                ;BIOS程序识别MBR魔术符
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111

5、boot.asm

;引入外部文件,这个文件定义常量
%include "boot.inc"

[ORG  0x500]

[SECTION .data]
PAGE_DIR_TABLE_POS equ 0x100000            ;二级页目录表,页表放在内存中1M起始位置连续存放,尽可能简单
KERNEL_BIN_BASE_ADDR equ 0x70000           ;disk to memory address
KERNEL_START_SECTOR equ 0x9                ;load kernel in the disk start sector
KERNEL_ENTRY_POINT equ 0xc0001500          ;inter virtual kernel address
;----------------   页表相关属性    --------------
PG_P  equ   1b          ;
PG_RW_R	 equ  00b
PG_RW_W	 equ  10b
PG_US_S	 equ  000b
PG_US_U	 equ  100b

[SECTION .gdt]

SEG_BASE equ 0
SEG_LIMIT equ 0xfffff

CODE_SELECTOR equ (1 << 3)
DATA_SELECTOR equ (2 << 3)
;第一个段描述符,不可访问,设为0
gdt_base:
    dd 0, 0

;0x00_1_1_0_0_0xf_1_00_1_1000_0x00_0x0000_0xffff
gdt_code:
    dw SEG_LIMIT & 0xffff                           ;段界限低16位
    dw SEG_BASE & 0xffff                            ;段基址低16位
    db SEG_BASE >> 16 & 0xff                        ;段基址中8位
    db 0b1_00_1_1000                                ;P_DPL_S_TYPE
    db 0b1_1_0_0_0000 | (SEG_LIMIT >> 16 & 0xf)     ;G_DB_L_AVL_LIMIT:LIMIT段极限高4位
    db SEG_BASE >> 24 & 0xf                         ;段基址高8位

;0x00_1_1_0_0_0xf_1_00_1_0010_0x00_0x0000_0xffff
gdt_data:
    dw SEG_LIMIT & 0xffff                           ;段界限低16位
    dw SEG_BASE & 0xffff                            ;段基址低16位
    db SEG_BASE >> 16 & 0xff                        ;段基址中8位
    db 0b1_00_1_0010                                ;P_DPL_S_TYPE
    db 0b1_1_00_0000 | (SEG_LIMIT >> 16 & 0xf)      ;G_DB_AVL_LIMIT:LIMIT段极限高4位
    db SEG_BASE >> 24 & 0xf                         ;段基址高8位
gdt_display:
    dd 0x80000007                                   ;文本显示内存地址为0xb8000~0xbffff,所以文本显示段基址为0xb8000,段界限为(0xbffff-0xb8000)/4k=0x7
	db 0x0b
    db 0b1_00_1_0010                                ;P_DPL_S_TYPE
    db 0b1_1_00_0000 | (SEG_LIMIT >> 24 & 0xf)      ;G_DB_AVL_LIMIT
    db SEG_BASE >> 24 & 0xf                         ;段基址高8位

    gdt_limit   equ   $ - gdt_base - 1
    times 30 dq 0					                ;此处预留30个描述符的位置
    SELECTOR_CODE equ (0x0001<<3) + 000b + 00b      ;相当于(gdt_code - gdt_base)/8 + TI_GDT + RPL0
    SELECTOR_DATA equ (0x0002<<3) + 000b + 00b	    ;同上
    SELECTOR_DISPLAY equ (0x0003<<3) + 000b + 00b	;同上

    total_mem_bytes dd 0                        ;save memory size,this physical address is 0x700.(4+60)*8+0x500=0x700
;计算出全局描述符界限和内存起始地址
gdt_ptr:
    dw gdt_limit
    dd gdt_base


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

[SECTION .text]
[BITS 16]
global boot_start

boot_start:

    call get_memory_size

	;开启A20总线
    in al,0x92
    or al,0000_0010B
    out 0x92,al

	lgdt [gdt_ptr]

	;cr0寄存器PE位置1,开启保护模式
    mov eax, cr0
    or eax, 0x00000001
    mov cr0, eax

	;跳转到保护模式地址,刷新掉16位实模式下流水线和缓存。
	jmp  SELECTOR_CODE:p_mode_start

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

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

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

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

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

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

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

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

   ret
.error_hlt:		      ;出错则挂起
    hlt

[BITS 32]
p_mode_start:
   mov ax, SELECTOR_DATA       ;初始化寄存器
   mov ds, ax
   mov es, ax
   mov ss, ax
   mov esp,LOADER_BASE_ADDR
   mov ax, SELECTOR_DISPLAY
   mov gs, ax
; -------------------------   加载kernel  ----------------------
   mov eax, KERNEL_START_SECTOR        ; kernel.bin所在的扇区号
   mov ebx, KERNEL_BIN_BASE_ADDR       ; 从磁盘读出后,写入到ebx指定的地址
   mov ecx, 200			       ; 读入的扇区数

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

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

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

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

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

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

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

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

   mov byte [gs:160], 'V'     ;视频段段基址已经被更新,用字符v表示virtual addr
   mov byte [gs:162], 'i'     ;视频段段基址已经被更新,用字符v表示virtual addr
   mov byte [gs:164], 'r'     ;视频段段基址已经被更新,用字符v表示virtual addr
   mov byte [gs:166], 't'     ;视频段段基址已经被更新,用字符v表示virtual addr
   mov byte [gs:168], 'u'     ;视频段段基址已经被更新,用字符v表示virtual addr
   mov byte [gs:170], 'a'     ;视频段段基址已经被更新,用字符v表示virtual addr
   mov byte [gs:172], 'l'     ;视频段段基址已经被更新,用字符v表示virtual addr

;;;;;;;;;;;;;;;;;;;;;;;;;;;;  此时不刷新流水线也没问题  ;;;;;;;;;;;;;;;;;;;;;;;;
;由于一直处在32位下,原则上不需要强制刷新,经过实际测试没有以下这两句也没问题.
;但以防万一,还是加上啦,免得将来出来莫句奇妙的问题.
   jmp SELECTOR_CODE:enter_kernel	  ;强制刷新流水线,更新gdt
enter_kernel:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
   mov byte [gs:320], 'k'     ;视频段段基址已经被更新
   mov byte [gs:322], 'e'     ;视频段段基址已经被更新
   mov byte [gs:324], 'r'     ;视频段段基址已经被更新
   mov byte [gs:326], 'n'     ;视频段段基址已经被更新
   mov byte [gs:328], 'e'     ;视频段段基址已经被更新
   mov byte [gs:330], 'l'     ;视频段段基址已经被更新

   mov byte [gs:480], 'w'     ;视频段段基址已经被更新
   mov byte [gs:482], 'h'     ;视频段段基址已经被更新
   mov byte [gs:484], 'i'     ;视频段段基址已经被更新
   mov byte [gs:486], 'l'     ;视频段段基址已经被更新
   mov byte [gs:488], 'e'     ;视频段段基址已经被更新
   mov byte [gs:490], '('     ;视频段段基址已经被更新
   mov byte [gs:492], '1'     ;视频段段基址已经被更新
   mov byte [gs:494], ')'     ;视频段段基址已经被更新
   mov byte [gs:496], ';'     ;视频段段基址已经被更新
   xchg bx,bx
   call kernel_init
   mov esp, 0xc009f000
   jmp KERNEL_ENTRY_POINT                 ; 用地址0x1500访问测试,结果ok

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

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

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

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

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

;-------------------------------------------------------------------------------
			   ;功能:读取硬盘n个扇区
rd_disk_m_32:
;-------------------------------------------------------------------------------
							 ; eax=LBA扇区号
							 ; ebx=将数据写入的内存地址
							 ; ecx=读入的扇区数
      mov esi,eax	   ; 备份eax
      mov di,cx		   ; 备份扇区数到di
;读写硬盘:
;第1步:设置要读取的扇区数
      mov dx,0x1f2
      mov al,cl
      out dx,al            ;读取的扇区数

      mov eax,esi	   ;恢复ax

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

      ;LBA地址7~0位写入端口0x1f3
      mov dx,0x1f3
      out dx,al

      ;LBA地址15~8位写入端口0x1f4
      mov cl,8
      shr eax,cl
      mov dx,0x1f4
      out dx,al

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

      shr eax,cl
      and al,0x0f	   ;lba第24~27位
      or al,0xe0	   ; 设置7~4位为1110,表示lba模式
      mov dx,0x1f6
      out dx,al

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

;;;;;;; 至此,硬盘控制器便从指定的lba地址(eax)处,读出连续的cx个扇区,下面检查硬盘状态,不忙就能把这cx个扇区的数据读出来

;第4步:检测硬盘状态
  .not_ready:		   ;测试0x1f7端口(status寄存器)的的BSY位
      ;同一端口,写时表示写入命令字,读时表示读入硬盘状态
      nop
      in al,dx
      and al,0x88	   ;第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙
      cmp al,0x08
      jnz .not_ready	   ;若未准备好,继续等。

;第5步:从0x1f0端口读数据
      mov ax, di	   ;以下从硬盘端口读数据用insw指令更快捷,不过尽可能多的演示命令使用,
			   ;在此先用这种方法,在后面内容会用到insw和outsw等
      mov dx, 256	   ;di为要读取的扇区数,一个扇区有512字节,每次读入一个字,共需di*512/2次,所以di*256
      mul dx
      mov cx, ax
      mov dx, 0x1f0
  .go_on_read:
      in ax,dx
      mov [ebx], ax
      add ebx, 2
			  ; 由于在实模式下偏移地址为16位,所以用bx只会访问到0~FFFFh的偏移。
			  ; loader的栈指针为0x900,bx为指向的数据输出缓冲区,且为16位,
			  ; 超过0xffff后,bx部分会从0开始,所以当要读取的扇区数过大,待写入的地址超过bx的范围时,
			  ; 从硬盘上读出的数据会把0x0000~0xffff的覆盖,
			  ; 造成栈被破坏,所以ret返回时,返回地址被破坏了,已经不是之前正确的地址,
			  ; 故程序出会错,不知道会跑到哪里去。
			  ; 所以改为ebx代替bx指向缓冲区,这样生成的机器码前面会有0x66和0x67来反转。
			  ; 0X66用于反转默认的操作数大小! 0X67用于反转默认的寻址方式.
			  ; cpu处于16位模式时,会理所当然的认为操作数和寻址都是16位,处于32位模式时,
			  ; 也会认为要执行的指令是32位.
			  ; 当我们在其中任意模式下用了另外模式的寻址方式或操作数大小(姑且认为16位模式用16位字节操作数,
			  ; 32位模式下用32字节的操作数)时,编译器会在指令前帮我们加上0x66或0x67,
			  ; 临时改变当前cpu模式到另外的模式下.
			  ; 假设当前运行在16位模式,遇到0X66时,操作数大小变为32位.
			  ; 假设当前运行在32位模式,遇到0X66时,操作数大小变为16位.
			  ; 假设当前运行在16位模式,遇到0X67时,寻址方式变为32位寻址
			  ; 假设当前运行在32位模式,遇到0X67时,寻址方式变为16位寻址.

      loop .go_on_read
      ret
;-----------------   将kernel.bin中的segment拷贝到编译的地址   -----------
kernel_init:
   xor eax, eax
   xor ebx, ebx		;ebx记录程序头表地址
   xor ecx, ecx		;cx记录程序头表中的program header数量
   xor edx, edx		;dx 记录program header尺寸,即e_phentsize

   mov dx, [KERNEL_BIN_BASE_ADDR + 42]	  ; 偏移文件42字节处的属性是e_phentsize,表示program header大小
   mov ebx, [KERNEL_BIN_BASE_ADDR + 28]   ; 偏移文件开始部分28字节的地方是e_phoff,表示第1 个program header在文件中的偏移量
					  ; 其实该值是0x34,不过还是谨慎一点,这里来读取实际值
   add ebx, KERNEL_BIN_BASE_ADDR
   mov cx, [KERNEL_BIN_BASE_ADDR + 44]    ; 偏移文件开始部分44字节的地方是e_phnum,表示有几个program header
.each_segment:
   cmp byte [ebx + 0], PT_NULL		  ; 若p_type等于 PT_NULL,说明此program header未使用。
   je .PTNULL

   ;为函数memcpy压入参数,参数是从右往左依然压入.函数原型类似于 memcpy(dst,src,size)
   push dword [ebx + 16]		  ; program header中偏移16字节的地方是p_filesz,压入函数memcpy的第三个参数:size
   mov eax, [ebx + 4]			  ; 距程序头偏移量为4字节的位置是p_offset
   add eax, KERNEL_BIN_BASE_ADDR	  ; 加上kernel.bin被加载到的物理地址,eax为该段的物理地址
   push eax				  ; 压入函数memcpy的第二个参数:源地址
   push dword [ebx + 8]			  ; 压入函数memcpy的第一个参数:目的地址,偏移程序头8字节的位置是p_vaddr,这就是目的地址
   call mem_cpy				  ; 调用mem_cpy完成段复制
   add esp,12				  ; 清理栈中压入的三个参数
.PTNULL:
   add ebx, edx				  ; edx为program header大小,即e_phentsize,在此ebx指向下一个program header
   xchg bx,bx
   loop .each_segment
   ret

;----------  逐字节拷贝 mem_cpy(dst,src,size) ------------
;输入:栈中三个参数(dst,src,size)
;输出:无
;---------------------------------------------------------
mem_cpy:
   cld
   push ebp
   mov ebp, esp
   push ecx		   ; rep指令用到了ecx,但ecx对于外层段的循环还有用,故先入栈备份
   mov edi, [ebp + 8]	   ; dst
   mov esi, [ebp + 12]	   ; src
   mov ecx, [ebp + 16]	   ; size
   rep movsb		   ; 逐字节拷贝

   ;恢复环境
   pop ecx
   pop ebp
   ret
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316
  • 317
  • 318
  • 319
  • 320
  • 321
  • 322
  • 323
  • 324
  • 325
  • 326
  • 327
  • 328
  • 329
  • 330
  • 331
  • 332
  • 333
  • 334
  • 335
  • 336
  • 337
  • 338
  • 339
  • 340
  • 341
  • 342
  • 343
  • 344
  • 345
  • 346
  • 347
  • 348
  • 349
  • 350
  • 351
  • 352
  • 353
  • 354
  • 355
  • 356
  • 357
  • 358
  • 359
  • 360
  • 361
  • 362
  • 363
  • 364
  • 365
  • 366
  • 367
  • 368
  • 369
  • 370
  • 371
  • 372
  • 373
  • 374
  • 375
  • 376
  • 377
  • 378
  • 379
  • 380
  • 381
  • 382
  • 383
  • 384
  • 385
  • 386
  • 387
  • 388
  • 389
  • 390
  • 391
  • 392
  • 393
  • 394
  • 395
  • 396
  • 397
  • 398
  • 399
  • 400
  • 401
  • 402
  • 403
  • 404
  • 405
  • 406
  • 407
  • 408
  • 409
  • 410
  • 411
  • 412
  • 413
  • 414
  • 415
  • 416
  • 417
  • 418
  • 419
  • 420
  • 421
  • 422
  • 423
  • 424
  • 425
  • 426
  • 427
  • 428
  • 429
  • 430
  • 431
  • 432
  • 433

7、main.c

int main(void) {
   while(1);
   return 0;
}
  • 1
  • 2
  • 3
  • 4

8、运行结果
在这里插入图片描述

七、DEBUG

1、现象
jmp 0xc0001500会自动重启,jmp 0x00001500可以顺利进入main函数执行死循环。
2、解决思路
(1)加了代码段选择子 jmp SELECTOR_CODE:KERNEL_ENTRY_POINT,报以下警告
在这里插入图片描述
(2)查看设置的段描述符,果然代码段描述符段界限设置错误,正确的该是4G,像第02个选择子数据段那样。
在这里插入图片描述
(3)查看代码段描述符,果然G位设置错误了,所以之前博客的代码也是错误的,已经改正。代码段G位被我错误设为0在这里插入图片描述
这是我的笔记,若有错误,望请多指教。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/blog/article/detail/58750
推荐阅读
相关标签
  

闽ICP备14008679号