前言
- 为什么要知道CPU的段页门
- 如何控制CPU由实模式进入保护模式
- CPU是如何找到数据并读写的?CPU是如何找到代码并执行的?
整体流程:
需要了解的知识点:
- 内存地址中逻辑地址、线性地址和物理地址之间的联系
- 逻辑地址:看到的所有的内存地址;
- 线性地址:线性地址 = 段基址 + 逻辑地址
没有启用分页机制的情况下,线性地址与物理地址相同
开启后线性地址 -> MMU -> 物理地址; - 物理地址:物理内存的地址 ;
- 运行模式
从开机到达64位长模式(Long Mode)
,需要经过实模式(Real Mode)
、保护模式(Protected Mode)
,以及在某些情况下的兼容模式(Compatibility Mode)
。
- 实时模式(16位):1MB的内存地址空间,并且没有硬件级别的内存保护,20根地址总线,但是寄存器是16位的;
- 保护模式(32位):内存的访问(高达4GB),硬件级别的内存保护,以及多任务功能。
- 兼容模式(32位):用于支持旧的32位操作系统和应用程序,它在64位处理器上允许运行未修改的32位代码。
- 64位长模式 (64位):允许访问远大于4GB的物理和虚拟内存。
- 虚拟内存
- 兼容模式和长模式一定要开虚拟内存;
- 保护模式可以开可以不开。
1、为什么要知道CPU的段页门
- 什么是段,就是你经常听到的:代码段、数据段
- 什么是页,就是你经常听到的:虚拟内存,又叫CPU分页机制
- 什么是门,与你经常听到的用户态切内核态有关
- 四种门:中断门、调用门、任务门、陷进门
- 快速调用:sysenter/sysexit、syscall/sysret
- 想让CPU由实模式进入包含模式,必须构建段
2、如何控制CPU由实模式进入保护模式
- 配置GDT表,至少包含一个代码段一个数据段
- 开A20总线
- 设置控制寄存器CR0
- 设置段寄存器
3、CPU是如何找到数据并读写的?CPU是如何找到代码并执行的?
以
mov eax, [0x100]
为例,其将0x100内存
中得数据复制到eax
寄存器当中。
- 需了解的知识:
0x100是逻辑地址,数据是存储在物理内存上的,得有物理地址,如何通过逻辑地址0x100找到它的物理地址?
A. 段寄存器:
- 常用的:
CS(code segment)
代码段 ,SS(stack segment)
栈段,DS (data segment)
数据段; - 不常用:ES、FS(用于kpcr)、GS
B. 段选择子:需要注意CPL
、DPL
和RPL
C. gdt
表、ldt
表以及对应的gdtr
寄存器ldtr
寄存器
D.段描述符
setup.asm基础框架:
会在下面基础框架基础上进行一系列操作进入保护模式。
[ORG 0x500] [SECTION .text] [BITS 16] global setup_start setup_start: mov ax, 0 mov ss, ax mov ds, ax mov es, ax mov fs, ax mov gs, ax mov si, ax mov si, prepare_enter_protected_mode_msg call print print: mov ah, 0x0e mov bh, 0 mov bl, 0x01 .loop: mov al, [si] cmp al, 0 jz .done int 0x10 inc si jmp .loop .done: ret prepare_enter_protected_mode_msg: db "Prepare to go into protected mode...", 10, 13, 0
- 拿到数据段寄存器中的值 ,代码中ds = 0x10
- 解析段寄存器中的值,0x10->段选择子
GDT定义:
分别定义了段描述符和段选择子
[SECTION .gdt] SEG_BASE equ 0 SEG_LIMIT equ 0xfffff CODE_SELECTOR equ (1 << 3) ;0000_0001 -> 0000_1000 即 0x08 DATA_SELECTOR equ (2 << 3) ;0000_0010 -> 0001_0000 即 0x10 gdt_base: dd 0, 0 gdt_code: dw SEG_LIMIT & 0xffff dw SEG_BASE & 0xffff db SEG_BASE >> 16 & 0xff ;P_DPL_S_TYPE db 0b1_00_1_1000 ;G_DB_0_AVL_LIMIT db 0b0_1_0_0_0000 | (SEG_LIMIT >> 16 & 0xf) db SEG_BASE >> 24 & 0xf gdt_data: dw SEG_LIMIT & 0xffff dw SEG_BASE & 0xffff db SEG_BASE >> 16 & 0xff ;P_DPL_S_TYPE db 0b1_00_1_1000 ;G_DB_0_AVL_LIMIT db 0b0_1_0_0_0000 | (SEG_LIMIT >> 16 & 0xf) db SEG_BASE >> 24 & 0xf gdt_ptr: dw $ - gdt_base dd gdt_base
分析: 0x10中段选择子RPL为0
,只能内核态访问;TI为0
,即GDT表;Index为2
,CPU支持的段数为2 需要注意本文章代码中:
gdt | 0 | -> 段描述符(unused 固定的,CPU硬性要求)<----------base | 1 | -> 代码段描述符 | 2 | -> 数据段描述符
- CPU读自己的gdtr寄存器
通过特权指令,r0可以执行
lgdt 写
sgdt 读
x86模式下,GDTR总共是48位(16位界限 + 32位基址)
x64模式下,GDTR总共是80位(16位界限 + 64位基址)
x86模式下 gdt_ptr: dw $ - gdt_base dd gdt_base 2 4 | limit | base |
- 取到数据段的描述符
base + 2 * 8
gdt | 0 | -> 段描述符(unused 固定的,CPU硬性要求)<----------base | 1 | -> 代码段描述符 | 2 | -> 数据段描述符
- 解析段描述符
下图为段描述符:
下图为S = 1 代码段或数据段的描述符:
A. 检查p位 有效位 1 0
B. 检查dpl位 0x10 rpl = 0
rpl 请求特权级 段选择子的低2位 cpl 当前请求特权级 CPU的内部 cpl = rpl = 0 dpl 访问段的最多要求特权级 dpl = 0 dpl <= cpl
C. s位、type域
一般s位和type域连在一起看
s = 1 代码段、数据段 s = 0 系统段
D. type域 数据段
E. 取base、limit
G位控制limit的单位 0: 字节 1M 1: 4K 1M个4K 4G
F. 判断base + offset <= limit
G. 返回线性地址
setup.asm完整代码:
[ORG 0x500] [SECTION .data] KERNEL_ADDR equ 0x1200 [SECTION .gdt] SEG_BASE equ 0 SEG_LIMIT equ 0xfffff CODE_SELECTOR equ (1 << 3) DATA_SELECTOR equ (2 << 3) gdt_base: dd 0, 0 gdt_code: dw SEG_LIMIT & 0xffff dw SEG_BASE & 0xffff db SEG_BASE >> 16 & 0xff ;P_DPL_S_TYPE db 0b1_00_1_1000 ;G_DB_0_AVL_LIMIT db 0b0_1_0_0_0000 | (SEG_LIMIT >> 16 & 0xf) db SEG_BASE >> 24 & 0xf gdt_data: dw SEG_LIMIT & 0xffff dw SEG_BASE & 0xffff db SEG_BASE >> 16 & 0xff ;P_DPL_S_TYPE db 0b1_00_1_0010 ;G_DB_0_AVL_LIMIT db 0b0_1_0_0_0010 | (SEG_LIMIT >> 16 & 0xf) db SEG_BASE >> 24 & 0xf gdt_ptr: dw $ - gdt_base dd gdt_base [SECTION .text] [BITS 16] global setup_start setup_start: xchg bx, bx mov ax, 0 mov ss, ax mov ds, ax mov es, ax mov fs, ax mov gs, ax mov si, ax mov si, prepare_enter_protected_mode_msg call print enter_protected_mode: ; 关中断 cli ; 加载gdt表 lgdt [gdt_ptr] ; 开A20 in al, 92h or al, 00000010b out 92h, al ; 设置保护模式 mov eax, cr0 or eax, 1 mov cr0, eax jmp CODE_SELECTOR:protected_mode ;长跳,cs:ip 刷新cs eip寄存器 print: mov ah, 0x0e mov bh, 0 mov bl, 0x01 .loop: mov al, [si] cmp al, 0 jz .done int 0x10 inc si jmp .loop .done: ret [BITS 32] protected_mode: xchg bx, bx mov ax, DATA_SELECTOR mov ss, ax mov ds, ax mov es, ax mov fs, ax mov gs, ax mov esp, 0x9fbff ; 将内核读入内存 mov edi, KERNEL_ADDR mov ecx, 3 ;从哪个山区开始读 mov bl, 60 ;指定从硬盘读取的扇区数 call read_hd jmp CODE_SELECTOR:KERNEL_ADDR ;修改esi寄存器 read_hd: ; 0x1f2 8bit 指定读取或写入的扇区数 mov dx, 0x1f2 mov al, bl out dx, al ; 0x1f3 8bit iba地址的第八位 0-7 inc dx mov al, cl out dx, al ; 0x1f4 8bit iba地址的中八位 8-15 inc dx mov al, ch out dx, al ; 0x1f5 8bit iba地址的高八位 16-23 inc dx shr ecx, 16 mov al, 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 add al, 0b1110_1111 out dx, al ; 0x1f7 8bit 命令或状态端口 inc dx mov al, 0x20 out dx, al ; 设置loop次数,读多少个扇区要loop多少次 mov cl, bl .start_read: push cx ; 保存loop次数,防止被下面的代码修改破坏 call .wait_hd_prepare call read_hd_data pop cx ; 恢复loop次数 loop .start_read .return: ret ; 一直等待,直到硬盘的状态是:不繁忙,数据已准备好 ; 即第7位为0,第3位为1,第0位为0 .wait_hd_prepare: mov dx, 0x1f7 .check: in al, dx and al, 0b1000_1000 cmp al, 0b0000_1000 jnz .check ret ; 读硬盘,一次读两个字节,读256次,刚好读一个扇区 read_hd_data: mov dx, 0x1f0 mov cx, 256 .read_word: in ax, dx mov [edi], ax add edi, 2 loop .read_word ret prepare_enter_protected_mode_msg: db "Prepare to go into protected mode...", 10, 13, 0
总结
- CPU是如何找到数据并读写的?
- 拿到数据段寄存器中的值 ds = 0x10
- 解析段寄存器中的值,0x10->段选择子
- CPU读自己的gdtr寄存器
- 取到数据段的描述符
- 解析段描述符
- 检查p位 有效位
- 检查dpl位
- s位、type域
- type域 数据段
- 取base、limit
- 判断base + offset <= limit
- 返回线性地址