操作系统真相还原 第四章 保护模式入门

第四章 保护模式入门

实模式

什么是实模式

实模式,又叫实地址模式.从80386开始,CPU有三种工作模式:实模式、保护模式和虚拟8086模式。80286开始的CPU引入保护模式,实际上,实模式概念是在保护模式推出之后为了区别保护模式之前的8086CPU工作模式才有的,在8086时代CPU工作模式只有一种,自然没有实模式之说。

实模式的“实”体现在程序中用到的地址都是真实的物理地址,“段基址:段内偏移地址”产生的逻辑地址就是物理地址,即程序员可见的地址完全是真实的内存地址。

实模式的缺点

不安全

  1. 没有权限划分,操作系统程序和用户程序属于同一特权级.
  2. 程序中用到的地址都是真实的物理地址,逻辑地址等于物理地址.
  3. 程序可以自由修改段地址,也就是可以访问所有内存地址.
  4. 段大小受限,段内偏移最大为64K,如果程序超过64k,需要占用多个段,段寄存器需要切换段基址.

保护模式之寄存器扩展

原有寄存器宽度增加

除段寄存器外,通用寄存器、指令指针寄存器、标志寄存器都由原来的 16 位扩展到了 32 位。
经过 extend 后的寄存器,统一在名字前加了 E表示扩展.

image-20210207235602802

寄存器中低 16 位的部分是为了兼容实模式,可以单独使用。高 16 位没办法单独使用,只能在用 32位寄存器时才有机会用到它们。

添加新的寄存器

除了容量的扩展,还有新的寄存器添加;

添加新寄存器的目的:解决实模式的安全问题

安全问题之一:段没有级别划分,程序可以随意访问任意段,任意内存地址;

解决段随意访问问题->段添加描述信息,即段描述符->段描述符有多个,要描述多个段,全局描述符表包括多个段描述符->全局描述符表很大,不能直接放在寄存器,只能放在内存中->为了能找到全局描述符表,把全局描述符表的地址放在GDTR寄存器->段寄存器中不再保存段基址,保存"选择子"(selector),是段描述符在全局描述符表中的索引->获取段描述符的步骤:1.读取GDTR寄存器获取全局描述符表的地址;2.读取段寄存器获取选择子;3.全局描述符表地址+选择子,从内存中读取段描述符;->获取段描述符耗时长,而且段信息在执行同一个程序时,一般不会变动,所以可以在获取段描述符后,把段描述符放到寄存器中做缓存,此寄存器是段描述符缓存寄存器->在段寄存器的选择子更新时,就会重新获取段描述符,更新段描述符缓存寄存器;

保护模式之寻址扩展

实模式下寄存器的功能比较固定.实模式下对于内存寻址来说,其中的基址寻址、变址寻址、基址变址寻址,这三种形式中的基址寄存器只能是bx,bp,变址寄存器只能si、di,也就是说,只能用这个寄存器。其中bx默认的段寄存器是ds,它经常用于访问数据段,bp默认的段寄存器是ss,它经常用于访问栈。

mov ax, [si] 
mov ax, [di] 
mov ax, [bx] 
mov ax, [bx+si] 
mov ax, [bx+si+Oxl234] 
mov ax, [bx+di] 
mov ax, [bx+di+Oxl234]

在保护模式下,这一切都不同了,同样是内存寻址中,基址寄存器不再只是bx,bp,而是所有32位的通用寄存器,变址寄存器也是一样,不再只是si,di,而是除esp之外的所有32通用寄存器,偏移量由实模式的16位变成了32位。并且,还可以对变址寄存器乘以一个比例因子

mov eax, [eax+edx*8+0x12345678] 
mov eax, [eax+edx*2+0x8] 
mov eax, [ecx*4+0x1234]

保护模式之运行反转

兼容实模式和保护模式,cpu很难区分,需要生成不同的机器码来做处理;编译器提供伪指令bits,来区分16位和32位.

[bits 16] 
mov ax, Oxl234 
mov dx, Oxl234 
 
[bits 32] 
mov eax , Oxl234 
mov edx, Oxl234

image-20210215093827366

全局描述符表

全局描述符表( Global Descriptor Table, GDT )是保护模式下内存段的登记表,这是不同于实模式的显著特征之一。

一个段描述符描述一个段的信息,一个专门的数据结构保存着多个段描述符,称为“全局描述符表”,其实就是一个保存着段描述符的数组。

段描述符

img

  • 段描述符大小为8个字节

  • 段界限被分为两部分,段基址被分为三部分,原因是为了兼容80286,80286是第一款有保护模式的cpu,但是只有16位。

  • 段界限:表示段的大小,共20位。具体边界值要结合23位的G来看,G=1时,表示段界限的粒度为4KB,最大值为2的(20+12)次方,即4GB,G=0时,表示段界限的粒度为1字节,最大值为2的20次方,即1MB实际的段界限=(描述符里的段界限+1)*段界限粒度大小-1。

  • 段基址:共32位。

  • S:代表一个段是系统段还是数据段,在CPU眼里,凡是硬件使用到的东西称为系统,凡是软件使用到的东西称为数据。所以代码段、数据段、栈段等也属于S中所代表的的数据段。S为0时表示系统段,S为1时表示数据段。

  • Type:共四位,指定段的类型。只有S决定了,Type才有它的意义。下图是Type在系统段和数据段里不同的意义。

    • 非系统段:当段为代码段时,Type由X、R、C、A组成,分别代表是否可执行、是否可读、是否一致、是否被访问过。当段位数据段时,Type由X、W、E、A组成,分别代表是否可执行、是否可写、扩展方向、是否被访问过。

    824400-20191116211927143-64867343

img

  • DPL代表段的特权级。
  • P代表内存段是否存在,0代表段不存在,1代表段存在。
  • AVL代表可用的位,是对用户而言,但操作系统可以随意使用。
  • L代表代码段是64位还是32位。
  • D/B:有效地址和操作数的大小。对于代码段来说此位是D,为0时,有效地址和操作数的大小为16位,使用ip寄存器;为1时,有效地址和操作数的大小为32位,使用eip寄存器。对于栈段来说此位是B,为0时,操作数为16位,使用sp寄存器;为1时,操作数为32位,使用esp寄存器。

全局描述符表

全局描述符表示全局的,共用的,多个程序都可以在这个表定义自己的段描述符。我们进入保护模式的其中一个步骤之一就是加载全局描述符表,让CPU知道全局描述符表的位置,在操作内存的时候,CPU就会根据描述符的信息检查这操作是否有效。

全局描述符表位于内存中,由GDTR寄存器指向全局描述符表的内存地址。GDTR寄存器大小为48位,初始化GDTR的指令为lgdt 48位数值

这48位数值的后32位表示GDT的起始地址,前16位表示GDT的大小,单位为字节,即2的16次方,大小为65536字节;每个描述符大小为8字节,所以GDT最多容纳65536/8=8192个段。

选择子

段寄存器中存入的是选择子(selector),段寄存器是16位,所以选择子也是16位。

  • RPL:特权级,有0、1、2、3,四种。
  • TI:为0,描述符索引值为GDT中索引值;为1,描述符索引值为LDT中索引值。

保护模式下,使用选择子获取段描述符,从段描述符中取出段基址。

LDT:局部描述符表,很少使用。

进入保护模式

A20地址线

在实模式下,A20地址线是默认禁用的,原因是还未进入保护模式之前,地址总线还是要模拟20位的效果,即只保留20位以内的地址,如果地址超过20位,地址就会回绕到0,将地址20位(从0开始算)舍弃,所以要将A20地址线给禁用掉。但进入保护模式后,我们需要恢复地址总线的原貌,即使地址超过20位,地址也不应该回绕到0,所以此时将A20地址线打开,我们就能访问超过20位的地址了。因此,打开A20地址线,是进入保护模式的步骤之一。

CR0的PE位

  进入保护模式的最后一个步骤是,打开CR0的PE位,CR0是控制寄存器。控制寄存器是CPU的窗口,它既可以展示CPU的内部状态,也可以控制CPU的运行机制。CR0的第0位,PE位,就是保护模式的开关,我们打开PE位,就是告诉CPU接下来我们要进入保护模式。

进入保护模式

  由上面可以知道,进入保护模式的步骤如下:

  ① 打开A20地址线

  ② 加载GDT

  ③ 将CR0的PE位置为1

总共三个文件:include/boot.inc,mbr.S,loader.S

boot.inc

增加段描述符的常量

;-------------	 loader和kernel   ----------

LOADER_BASE_ADDR equ 0x900 
LOADER_START_SECTOR equ 0x2

;--------------   gdt描述符属性  -------------
DESC_G_4K   equ	  1_00000000000000000000000b   
DESC_D_32   equ	   1_0000000000000000000000b
DESC_L	    equ	    0_000000000000000000000b	;  64位代码标记,此处标记为0便可。
DESC_AVL    equ	     0_00000000000000000000b	;  cpu不用此位,暂置为0  
DESC_LIMIT_CODE2  equ 1111_0000000000000000b
DESC_LIMIT_DATA2  equ DESC_LIMIT_CODE2
DESC_LIMIT_VIDEO2  equ 0000_000000000000000b
DESC_P	    equ		  1_000000000000000b
DESC_DPL_0  equ		   00_0000000000000b
DESC_DPL_1  equ		   01_0000000000000b
DESC_DPL_2  equ		   10_0000000000000b
DESC_DPL_3  equ		   11_0000000000000b
DESC_S_CODE equ		     1_000000000000b
DESC_S_DATA equ	  DESC_S_CODE
DESC_S_sys  equ		     0_000000000000b
DESC_TYPE_CODE  equ	      1000_00000000b	;x=1,c=0,r=0,a=0 代码段是可执行的,非依从的,不可读的,已访问位a清0.  
DESC_TYPE_DATA  equ	      0010_00000000b	;x=0,e=0,w=1,a=0 数据段是不可执行的,向上扩展的,可写的,已访问位a清0.

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
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
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

;--------------   选择子属性  ---------------
RPL0  equ   00b
RPL1  equ   01b
RPL2  equ   10b
RPL3  equ   11b
TI_GDT	 equ   000b
TI_LDT	 equ   100b

mbr.S

此前只读入了一个扇区,由于 loader.bin 超过了 512 字节,52行增加读入的扇区为4

;主引导程序 
;------------------------------------------------------------
%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号功能,上卷全部行,则可清屏。
; -----------------------------------------------------------
;INT 0x10   功能号:0x06	   功能描述:上卷窗口
;------------------------------------------------------
;输入:
;AH 功能号= 0x06
;AL = 上卷的行数(如果为0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值:
   mov     ax, 0600h
   mov     bx, 0700h
   mov     cx, 0                   ; 左上角: (0, 0)
   mov     dx, 184fh		   ; 右下角: (80,25),
				   ; 因为VGA文本模式中,一行只能容纳80个字符,共25行。
				   ; 下标从0开始,所以0x18=24,0x4f=79
   int     10h                     ; int 10h

   ; 输出字符串:MBR
   mov byte [gs:0x00],'1'
   mov byte [gs:0x01],0xA4

   mov byte [gs:0x02],' '
   mov byte [gs:0x03],0xA4

   mov byte [gs:0x04],'M'
   mov byte [gs:0x05],0xA4	   ;A表示绿色背景闪烁,4表示前景色为红色

   mov byte [gs:0x06],'B'
   mov byte [gs:0x07],0xA4

   mov byte [gs:0x08],'R'
   mov byte [gs:0x09],0xA4
	 
   mov eax,LOADER_START_SECTOR	 ; 起始扇区lba地址
   mov bx,LOADER_BASE_ADDR       ; 写入的地址
   mov cx,4			 ; 待读入的扇区数
   call rd_disk_m_16		 ; 以下读取程序的起始部分(一个扇区)
  
   jmp LOADER_BASE_ADDR
       
;-------------------------------------------------------------------------------
;功能:读取硬盘n个扇区
rd_disk_m_16:	   
;-------------------------------------------------------------------------------
				       ; eax=LBA扇区号
				       ; ebx=将数据写入的内存地址
				       ; ecx=读入的扇区数
      mov esi,eax	  ;备份eax
      mov di,cx		  ;备份cx
;读写硬盘:
;第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

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

;第5步:从0x1f0端口读数据
      mov ax, di
      mov dx, 256
      mul dx
      mov cx, ax	   ; di为要读取的扇区数,一个扇区有512字节,每次读入一个字,
			   ; 共需di*512/2次,所以di*256
      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

loader.S

   %include "boot.inc"
   section loader vstart=LOADER_BASE_ADDR
   LOADER_STACK_TOP equ LOADER_BASE_ADDR
   jmp loader_start					; 此处的物理地址是:
   
;构建gdt及其内部的描述符
   GDT_BASE:   dd    0x00000000 
	       dd    0x00000000

   CODE_DESC:  dd    0x0000FFFF 
	       dd    DESC_CODE_HIGH4

   DATA_STACK_DESC:  dd    0x0000FFFF
		     dd    DESC_DATA_HIGH4

   VIDEO_DESC: dd    0x80000007	       ;limit=(0xbffff-0xb8000)/4k=0x7
	       dd    DESC_VIDEO_HIGH4  ; 此时dpl已改为0

   GDT_SIZE   equ   $ - GDT_BASE
   GDT_LIMIT   equ   GDT_SIZE -	1 
   times 60 dq 0					 ; 此处预留60个描述符的slot
   SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0         ; 相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0
   SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0	 ; 同上
   SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0	 ; 同上 

   ;以下是定义gdt的指针,前2字节是gdt界限,后4字节是gdt起始地址

   gdt_ptr  dw  GDT_LIMIT 
	    dd  GDT_BASE
   loadermsg db '2 loader in real.'

   loader_start:

;------------------------------------------------------------
;INT 0x10    功能号:0x13    功能描述:打印字符串
;------------------------------------------------------------
;输入:
;AH 子功能号=13H
;BH = 页码
;BL = 属性(若AL=00H或01H)
;CX=字符串长度
;(DH、DL)=坐标(行、列)
;ES:BP=字符串地址 
;AL=显示输出方式
;   0——字符串中只含显示字符,其显示属性在BL中。显示后,光标位置不变
;   1——字符串中只含显示字符,其显示属性在BL中。显示后,光标位置改变
;   2——字符串中含显示字符和显示属性。显示后,光标位置不变
;   3——字符串中含显示字符和显示属性。显示后,光标位置改变
;无返回值
   mov	 sp, LOADER_BASE_ADDR
   mov	 bp, loadermsg           ; ES:BP = 字符串地址
   mov	 cx, 17			 ; CX = 字符串长度
   mov	 ax, 0x1301		 ; AH = 13,  AL = 01h
   mov	 bx, 0x001f		 ; 页号为0(BH = 0) 蓝底粉红字(BL = 1fh)
   mov	 dx, 0x1800		 ;
   int	 0x10                    ; 10h 号中断

;----------------------------------------   准备进入保护模式   ------------------------------------------
									;1 打开A20
									;2 加载gdt
									;3 将cr0的pe位置1


   ;-----------------  打开A20  ----------------
   in al,0x92
   or al,0000_0010B
   out 0x92,al

   ;-----------------  加载GDT  ----------------
   lgdt [gdt_ptr]


   ;-----------------  cr0第0位置1  ----------------
   mov eax, cr0
   or eax, 0x00000001
   mov cr0, eax

   ;jmp dword SELECTOR_CODE:p_mode_start	     ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,
   jmp  SELECTOR_CODE:p_mode_start	     ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕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

   mov byte [gs:160], 'P'

   jmp $
posted @ 2021-10-21 10:47  dev_liufq  阅读(391)  评论(0编辑  收藏  举报