自制操作系统Antz(5)——深入理解保护模式与进入方法
Antz系统更新地址: https://www.cnblogs.com/LexMoon/category/1262287.html
Linux内核源码分析地址:https://www.cnblogs.com/LexMoon/category/1267413.html
在前几天的任务中,我们已经简单实现了MBR,直接操作显示器和硬盘操作来加载其他扇区的程序,我们这些任务都是为了进入保护模式做准备,虽然我们已经给出了jmp到保护模式的方法,但是我们还是需要理解保护模式下的一些特性,才能更好的实现我们操作系统的功能。
0 .为什么要有保护模式
以下是实模式的不足。
1)操作系统和用户程序属于同一特权级。
2)用户程序所引用的地址都是指向真实的物理地址,也就是说逻辑地址等于物理地址。
3)用户程序可以自由修改段基址,可以访问任意内存。
4)访问超过64KB的内存区域时要切换段基址。
5)一次只能运行一个程序,无法充分利用计算机资源。
6)共20跟地址线,最大可用内存为1MB。
1~3是安全缺陷,4~5是使用方面的缺陷,第6条简直就是不能忍受的硬伤,1MB内存真的太束缚手脚了。
后来为了解决这些问题,厂商开发处保护模式。这时,物理内存地址不能被程序直接访问,程序内部的地址(虚拟地址)需要被转换为物理地址后再去访问,程序对此一无所知。而且地址的转换时由处理器和操作系统共同协作完成的,处理器在硬件上提供地址转换部件,操作系统提供转换过程需要的页表。
1 .保护模式的寄存器扩展
计算机的发展必须遵守兼容的特点,CPU发展到32位之后,地址总线和数据总线也发展到了32位,寻址空间更是达到了4GB。寻址空间大了,但寻址方式还是得兼容老方法,就是“段基址:偏移地址”,如果还是16位的话,不能承受4GB寻址的重任,所以寄存器也得跟上。为了让一个寄存器就可以寻址4GB空间,寄存器扩展到了32位。除了段寄存器,其他寄存器均扩展到了32位,因为段寄存器16位就够用了。
寄存器的低16位都是为了兼容模式,高16位无法单独使用,只能在用32位寄存器时才可以用到。
偏移地址还是和实模式下一样,但段基址为了安全,在其内添加了约束信息,这些约束信息就是内存段的描述信息,由于这些信息在寄存器中放不下,所以用了一个专门的数据结构——全局描述符表。其中有表项,用来描述各个内存段的起始地址,大小,权限等信息,每个表项大小是64字节,因为全局描述符表很大,只能放在内存中,由寄存器指向它。
至此,段寄存器中再也不是段基址了,里面保存的叫做选择子(selector) ,它是一个数,用来索引全局描述符表中的段描述符,把全局描述符表当作数组,选择子就像是下标。
段描述符是放在内存中的,访问内存对于CPU而言效率不高,而且段描述符的格式很奇怪,一个数据要分三个地方存,所以CPU把这些数组合并成一个完整数据也是需要花时间的。既然如此花费时间,在保护模式中,CPU为了提高效率,采取了对段寄存器的缓存技术,将段信息用一个寄存器来存储,这就是段描述符缓冲寄存器(对程序员不可见)。在获得一个段描述符之后,以后访问相同段时,会直接访问该寄存器。
下面是三种段描述符寄存器的结构:
2 .保护模式的运行模式反转
保护模式如何分辨16位和32位指令和操作数呢?
汇编产生的机器码机器并不能识别是运行在16位还是32位系统下,在编译时可以通过[bits 16]和[bits 32]来确定编译器将代码编译为多少位的机器码。
[bis]是伪指令,编译并无具体机器码,那么在编译之后机器如何识别呢? 这里引入前缀,在指令前加入前缀指令重复前缀rep,段跨越前缀"段寄存器",还有操作数反转前缀0x66,寻址方式反转前缀0x67。
行号 | 指令 | 机器码 |
1 | [bits 16] | 伪指令 |
2 | mov ax,0x1234 | B83412 |
3 | mov eax,0x1234 | 66B834120000 |
4 | [bits 32] | 伪指令 |
5 | mov ax,0x1234 | 66B83412 |
6 | mov eax,0x1234 | B834120000 |
如果32位的代码被编译为16位的代码就会在机器码前加入前缀。即一种模式下要用另一中模式的操作数大小,需要在指令前加入指令前缀0x66。
以上是操作数大小的改变时的前缀,如果是寻址方式改变,则添加前缀0x67。
3 .全局描述符表概述
上面我们已经提到过全局描述符表了,它可以当作一个数组,而段描述符就是这个数组的下标。其结构如下:
段描述符是8字节的,专门用来描述一个内存段,8字节也就是64位,而且是连续的8个字节。
保护模式下地址总线是32位,段基址需要32位地址表示,段界限用20位表示,不过这个段界限只是个单位量,它的单位要么是字节,要么是4KB,这是感觉描述符的G位来确定的。最终段的边界是此段界限值*单位,故段的大小要么是2的20次方1MB要么是2的32次方(4KB==2的12次方)4GB。
这里说的1MB和4GB只是个范围,并不是具体的边界值。由于段界限只是个偏移量,是从0开始算,所以实际的段界限边界值等于(描述符中段界限+1)*(段界限的粒度大小:4KB或1) -1 。
这个公式的意思就是表示有多少个4KB或1 。由于描述符中的段界限是从0起的,所以左边第1个括号中要加个1,表示实际数量,由于地址是从0开始的,所以最后减1 。
内存访问需要用到“段基址:偏移地址”,段界限其实就是用来限制段内偏移地址的,段内偏移地址必须位于段内,否则CPU会抛异常,“段界限*单位”就是限定偏移地址的最值的。
仔细观察上面段描述符,你会发现段界限属性被分为了两部分,32位的段基址属性居然被分为了三份,这是为了兼容性考虑的。
段描述符的低32位分为了两部分,前16位来存储段的段界限的前0~15位,后16位存储段基址的0~15位。
主要属性都是在段描述符的高32位,0~7位是段基址的16~23位,24~31位是段基址的24~31位,加上段描述符的低32位的0~15位,这下32位的基地址才算完整。
8~11位的type属性,四位,用来指定本描述符的类型。一个段描述符在CPU眼中分为两类,要么是系统段,要么是数据段。这是感觉段描述符的S位决定的,它用来指示是否是系统段,CPU眼中,硬件运行需要的都是系统,软件需要的都是数据,S是0表示系统段,S是1是数据段。type字段是和S字段配合才能确定段描述符的确切类型,至有S确定了,type才有具体意义。
再来看type字段,它用于表示内存段或门的子类型。
这是type在S确定之后的意义,我们需要注意的是非系统段。
段描述符的第13~14位就是DPL字段,即描述符特权符,这是保护模式提供的安全解决方案,将计算机世界分为不同等级。这两位可以代表四种特权级,分别是0~4,数字越小特权越大。
段描述符的第15位的P字段,即段是否存在,如果段存在于内存中,P为1,否则P为0。P是由CPU检查的,如果为0,CPU将抛出异常。
段描述符的第16~19位是段界限的第16~19位。这样段界限就齐全了。
段描述符的第20位是AVL,这位是相对用户的,暂不用理会。
段描述符的第21位是L,是用来检查是否是64位代码段,在我们32位CPU时,将其置0即可。
段描述符的第22位是D/B字段,用来表示有效地址及操作数的大小。
段描述符的第23位是G,用来指定段界限的大小,粒度。
段描述符的第24~31位是段基址的24~31位,是段基址的最后8位。
4 .全局描述符表GDT及选择子
一个段描述符只能用来定义一个内存段,代码段要占用一个段描述符,数据段和栈段等,多个内存段也要各自占用一个段描述符,这些描述符会放在全局描述符表中,也就是GDT,GDT是公用的,它位于内存中,需要专门的寄存器指向。这个寄存器就是GDTR,一个48位寄存器。
gdtr不能直接用mov gdtr,xxxx的方式初始化,而是有专门的指令,就是lgdt。
lgdt指令格式是: lgdt 48位内存数据
这48位内存数据分为两部分,前16位是GDT以字节为单位的界限值,后32位是GDT的其实地址,由于GDT的大小是16位,所以范围是65536字节,每个描述符大小是8字节,所以一个GDT中有8192个段或门。
在保护模式下,原本存在段寄存器的段基址,现在放在了段描述符中,而段寄存器中放入的是选择子,就是一个索引,在描述符表中索引描述符的索引。
段寄存器是16位的,所以选择子也是16位的,在其低2位即0~1位,用来存储RPL,即请求特权级,可以表示四种特权。高13位,即3~15是索引部分,2的13次方是8192,故可以索引8192个段,正好吻合GDT的8192个段。
下图是描述符表和内存段的关系,还有选择子的结构:
5 .进入保护模式
1. 打开A20地址线
打开A20Gate的方式极其简单,只需要将0x92端口的第一个位置置1就好了。
1 in al,0x92 2 or al,0000_0010B 3 out 0x92,al
2.保护模式的开关,CR0寄存器的PE位
这是进入保护模式的最后一步,CR0寄存器的PE位置1。
PE为0表示在实模式下运行,PE为1表示在保护模式运行。
1 mov eax,cr0 2 or eax,0x00000001 3 mov cr0,eax
写入完毕,接下来可以让我们进入保护模式了!
boot.inc :
1 ;------------- loader和kernel ---------- 2 3 LOADER_BASE_ADDR equ 0x900 4 LOADER_START_SECTOR equ 0x2 5 6 ;-------------- gdt描述符属性 ------------- 7 DESC_G_4K equ 1_00000000000000000000000b 8 DESC_D_32 equ 1_0000000000000000000000b 9 DESC_L equ 0_000000000000000000000b ; 64位代码标记,此处标记为0便可。 10 DESC_AVL equ 0_00000000000000000000b ; cpu不用此位,暂置为0 11 DESC_LIMIT_CODE2 equ 1111_0000000000000000b 12 DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2 13 DESC_LIMIT_VIDEO2 equ 0000_000000000000000b 14 DESC_P equ 1_000000000000000b 15 DESC_DPL_0 equ 00_0000000000000b 16 DESC_DPL_1 equ 01_0000000000000b 17 DESC_DPL_2 equ 10_0000000000000b 18 DESC_DPL_3 equ 11_0000000000000b 19 DESC_S_CODE equ 1_000000000000b 20 DESC_S_DATA equ DESC_S_CODE 21 DESC_S_sys equ 0_000000000000b 22 DESC_TYPE_CODE equ 1000_00000000b ;x=1,c=0,r=0,a=0 代码段是可执行的,非依从的,不可读的,已访问位a清0. 23 DESC_TYPE_DATA equ 0010_00000000b ;x=0,e=0,w=1,a=0 数据段是不可执行的,向上扩展的,可写的,已访问位a清0. 24 25 DESC_CODE_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_CODE2 + DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE + 0x00 26 DESC_DATA_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x00 27 DESC_VIDEO_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x0b 28 29 ;-------------- 选择子属性 --------------- 30 RPL0 equ 00b 31 RPL1 equ 01b 32 RPL2 equ 10b 33 RPL3 equ 11b 34 TI_GDT equ 000b 35 TI_LDT equ 100b
loader.asm:
1 %include "boot.inc" 2 section loader vstart=LOADER_BASE_ADDR 3 LOADER_STACK_TOP equ LOADER_BASE_ADDR 4 jmp loader_start 5 GDT_BASE: dd 0x00000000 6 dd 0x00000000 7 8 CODE_DESC: dd 0x0000FFFF 9 dd DESC_CODE_HIGH4 10 11 DATA_STACK_DESC: dd 0x0000FFFF 12 dd DESC_DATA_HIGH4 13 14 VIDEO_DESC: dd 0x80000007 ;limit=(0xbffff-0xb8000)/4k=0x7 15 dd DESC_VIDEO_HIGH4 16 17 GDT_SIZE equ $ - GDT_BASE 18 GDT_LIMIT equ GDT_SIZE - 1 19 times 60 dq 0 20 SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0 21 SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0 22 SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0 23 24 gdt_ptr dw GDT_LIMIT 25 dd GDT_BASE 26 loadermsg db '2 loader in real.' 27 28 loader_start: 29 30 mov sp, LOADER_BASE_ADDR 31 mov bp, loadermsg 32 mov cx, 17 33 mov ax, 0x1301 34 mov bx, 0x001f 35 mov dx, 0x1800 36 int 0x10 37 38 in al,0x92 39 or al,0000_0010B 40 out 0x92,al 41 lgdt [gdt_ptr] 42 mov eax, cr0 43 or eax, 0x00000001 44 mov cr0, eax 45 46 ;jmp dword SELECTOR_CODE:p_mode_start 47 jmp SELECTOR_CODE:p_mode_start 48 49 [bits 32] 50 p_mode_start: 51 mov ax, SELECTOR_DATA 52 mov ds, ax 53 mov es, ax 54 mov ss, ax 55 mov esp,LOADER_STACK_TOP 56 mov ax, SELECTOR_VIDEO 57 mov gs, ax 58 59 mov byte [gs:160], 'P' 60 61 jmp $
Antz_mbr.asm:
1 %include "boot.inc" 2 SECTION MBR vstart=0x7c00 3 mov ax,cs 4 mov ds,ax 5 mov es,ax 6 mov ss,ax 7 mov fs,ax 8 mov sp,0x7c00 9 mov ax,0xb800 10 mov gs,ax 11 12 mov ax, 0600h 13 mov bx, 0700h 14 mov cx, 0 15 mov dx, 184fh 16 int 10h 17 18 mov byte [gs:0x00],'1' 19 mov byte [gs:0x01],0xA4 20 21 mov byte [gs:0x02],' ' 22 mov byte [gs:0x03],0xA4 23 24 mov byte [gs:0x04],'M' 25 mov byte [gs:0x05],0xA4 26 27 mov byte [gs:0x06],'B' 28 mov byte [gs:0x07],0xA4 29 30 mov byte [gs:0x08],'R' 31 mov byte [gs:0x09],0xA4 32 33 mov eax,LOADER_START_SECTOR 34 mov bx,LOADER_BASE_ADDR 35 mov cx,4 36 call rd_disk_m_16 37 38 jmp LOADER_BASE_ADDR 39 40 rd_disk_m_16: 41 42 mov esi,eax ;备份eax 43 mov di,cx 44 45 46 mov dx,0x1f2 47 mov al,cl 48 out dx,al 49 50 mov eax,esi 51 52 mov dx,0x1f3 53 out dx,al 54 55 56 mov cl,8 57 shr eax,cl 58 mov dx,0x1f4 59 out dx,al 60 61 shr eax,cl 62 mov dx,0x1f5 63 out dx,al 64 65 shr eax,cl 66 and al,0x0f 67 or al,0xe0 68 mov dx,0x1f6 69 out dx,al 70 71 mov dx,0x1f7 72 mov al,0x20 73 out dx,al 74 75 .not_ready: 76 nop 77 in al,dx 78 and al,0x88 79 cmp al,0x08 80 jnz .not_ready 81 82 mov ax, di 83 mov dx, 256 84 mul dx 85 mov cx, ax 86 87 mov dx, 0x1f0 88 .go_on_read: 89 in ax,dx 90 mov [bx],ax 91 add bx,2 92 loop .go_on_read 93 ret 94 95 times 510-($-$$) db 0 96 db 0x55,0xaa
作者: AntzUhl
首发地址博客园:http://www.cnblogs.com/LexMoon/
代码均可在Github上找到(求Star) : Github
个人博客 : http://antzuhl.cn/
公众号 |
赞助
支付宝 |
微信 |