从实模式到保护模式

上节Boot成功加载loader到内存并且将控制权交给他,突破了512字节的限制,loader程序没有体积上的限制

这节就实战编写loader实现从实模式到保护模式再返回实模式

目录

1、从实模式到保护模式

1、从计算机的历史谈起

2、CPU历史的里程碑 - 8086

3、80286的登场

4、初识保护模式

5、80386的登场(计算机新时期的标志)

4、编程实验

2、深入保护模式

1、显存段、数据段和栈段

2、编程实验

2、从保护模式返回实模式

3、局部段描述符表的使用

4、初探保护模式的保护机制



1、从实模式到保护模式

1、从计算机的历史谈起

远古时期的程序开发:直接操作物理内存

CPU指令的操作数直接使用实地址(实际内存地址)

程序员拥有绝对的权利(利用CPU指哪打哪)

 

绝对的权利带来的问题

     - 难以重定位:程序每次都需要同样地址的内存执行

     - 给多道程序设计带来障碍:不管内存多大,但凡一个字节被其它程序占用都无法执行

 

2、CPU历史的里程碑 - 8086

地址线宽度为20位,可访问1M内存空间(0~0xFFFFF)

引入[段地址 : 偏移地址]的内存访问方式

     - 段地址左移4位,构成20位的基地址(起始地址):基地址+偏移地址=实地址(由地址加法器完成)

     - 8086的段寄存器和通用寄存器为16位

     - 单个寄存器寻址最多访问64K的内存空间

     - 需要两个寄存器配合,完成所有内存空间的访问

对于开发者的意义:

     - 更有效的划分内存的功能(数据段,代码段,等)

     - 当出现程序地址冲突时,通过修改段地址解决冲突(即书上所说的重定位寄存器)

mov ax, [0x1234] ;根据默认段地址ds和给出的偏移地址得出实地址: (ds<<4)+ 0x1234
mov ax, [es:0x1234] ;实地址:(es<<4)+ 0x1234

字面上[段地址:偏移地址]能访问的最大地址为0xFFFF:0XFFFF,即:10FFEF;超过了1MB的空间,CPU如何处理?

答案:不处理,而超出的部分我们称呼为高端地址区

8086中的高端地址区(High Memory Area

再谈8086历史

     - 8086在当时是非常成功的一款产品

     - 因此,拥有一大批的开发者和应用程序

     - 各种基于8086程序设计的技术得到了发展

     - 不幸的是,各种奇淫技巧也应运而生...

8086时期应用程序中的问题

     - 1MB内存完全不够用(内存在任何时期都不够用)

     - 开发者在程序中大量使用内存回卷技术(HMA地址被使用)

     - 应用程序之间没有界限,相互之间随意干扰

           ①A程序可以随意访问B程序中的数据

           ②C程序可以修改系统调度程序的指令

 

8086程序中问题的本质是什么?如果是你,准备如何解决?

安全性!!!没有考虑内存保护,所有内存想读就读,想写就写...

 

3、80286的登场

8086已经有那么多应用程序了,所以必须兼容再兼容

加大内存容量,增加地址线数量(24位),可访问16M内存(0~FFFFFF)

[段地址 : 偏移地址]的方式可以强化一下

     - 为每个段提供更多属性(如:范围,特权级,等)

     - 为每个段的定义提供固定方式

 

80286的兼容性

     - 默认情况下完全兼容8086的运行方式(实模式

          ★ 默认可直接访问1MB的内存空间(即便使用回卷技术的程序依旧可以正常运行)

          ★ 通过特殊的方式访问1MB+的内存空间

 

这个特殊的方式指的是80286之后的工作模式:保护模式

 

4、初识保护模式

每一段内存的拥有一个属性定义(描述符 Descriptor

所有段的属性定义构成一张表(描述符表 Descriptor Table

段寄存器保存的是属性定义在表中的索引(选择子 Selector

 

描述符(Descriptor)的内存结构

可以看到一个描述符在内存中占8字节。

段基址就是描述内存段的起始地址,分三部分存放,这是由于80386在80286基础上改进,硬件会自己拼装它们。

段界限就指出了段内偏移地址的最大值。

其它段属性后续用到时介绍

 

描述符表(Descriptor Table)

描述符表放在内存是数组的结构,每一个元素都是一个描述符,下标从0开始,索引号依次递增

描述符占8个字节所以在表中的偏移地址是索引号 * 8

最主要的描述符表是全局描述符表(GDT),处理器内部有一个48位的寄存器称为全局描述符表寄存器(GDTR)高32位存放描述符表地址,低16位存放表的界限。可以通过lgdt指令将GDT的入口地址装入此寄存器

 

选择子(Selector)的结构

索引号:3-15位保存着段描述符在段描述符表的位置

RPL:请求特权级标识,通过特权级判断是否可以访问对应段,有四个值0~3(详细后续介绍)

TI:描述符表指示器,TI=0,表示描述符在GDT(全局段描述符表);TI=1,表示描述符在LDT(局部段描述符表)。

       给定一个选择子通过TI位就能知道在哪个表中找描述符

 

选择子放在段寄存器中(在保护模式下叫段选择器),从80286开始每个段寄存器都配有段描述符高速缓冲寄存器。

 

进入保护模式的方式

   1. 定义描述符表

   2. 打开A20地址线(从0x92端口读数据,将其第2位置1,再写入该端口)

   3. 加载描述表(lgdt,将描述符表的地址和长度放入一个48位寄存器)

   4. 通知CPU进入保护模式(CR0寄存器的第1位置为1即可)

注解:CR0,CR1....是处理器内部的控制寄存器(80286叫MSW寄存器),CR0是32寄存器,包含一系列用于控制处理器操作模式和运行状态的标志位,它的第1位是保护模式允许位,为1则处理器进入保护模式,其他位后续若用到讲解

                                   

80286的光荣退场

     - 历史意义

              引入了保护模式,为现代操作系统和应用程序奠定了基础

     - 奇葩设计

              段寄存器为24位,通用寄存器为16位(不伦不类)

              ✦ 理论上,段寄存器中的数值可以直接作为段基址

(实模式下段寄存器只使用16位,保护模式下段寄存器保存16位的选择子,24位明显多余)

              ✦ 16位通用寄存器最多访问64K的内存,为了访问16M内存,必须不停切换段基址

小结

    - [段地址:偏移地址]的寻址方式解决了早期程序重定位难的问题

    - 8086实模式下的程序无法保证安全性

    - 80286中提出了保护模式,加强了内存段的安全性

    - 出于兼容性的考虑,80286之后的处理器都有2种工作模式

    - 处理器需要特定的设置步骤才能进入保护模式,默认为实模式

 

5、80386的登场(计算机新时期的标志)

32位地址总线(可支持4G的内存空间)

段寄存器和通用寄存器都为32位

     - 任何一个寄存器都能访问到内存的任意角落

              ✦ 开启了平坦内存模式的新时代

              ✦ 段基址为0,使用通用寄存器访问4G内存空间

新时期的内存使用方式

     - 实模式

              ✦ 兼容8086的内存使用方式(指哪打哪)

     - 分段模式

              ✦ 通过[段地址 : 偏移地址]的方式将内存从功能上分段(数据段,代码段)

     - 平坦模式

              ✦ 所有内存就是一个段[0:32位偏移地址]

 

段属性定义

选择子属性定义

 

保护模式中的段定义

汇编小贴士

     - section 关键字用于"逻辑的"定义一段代码集合

     - section 定义的代码段不同于[段地址 : 偏移地址]的代码段

           ✦ section定义的代码段仅限于源码中的代码段(代码节

           ✦ [段地址 : 偏移地址]的代码段指内存中的代码段

     - [bits 16]:用于指示编译器将代码按照16位方式进行编译

     - [bits 32]:用于指示编译器将代码按照32位方式进行编译

 

注意事项

     - 全局段描述表中的第0个描述符不使用(仅用于占位)

     - 代码中必须显示的指明16位代码段和32位代码段

     - 必须使用jmp指令从16位代码段跳转到32位代码段

 

4、编程实验

inc.asm


; Segment Attribute
DA_32    equ    0x4000
DA_DR    equ    0x90
DA_DRW   equ    0x92
DA_DRWA  equ    0x93
DA_C     equ    0x98
DA_CR    equ    0x9A
DA_CCO   equ    0x9C
DA_CCOR  equ    0x9E

; Selector Attribute
SA_RPL0    equ    0
SA_RPL1    equ    1
SA_RPL2    equ    2
SA_RPL3    equ    3

SA_TIG    equ    0
SA_TIL    equ    4

; 描述符
; usage: Descriptor Base, Limit, Attr
;        Base:  dd
;        Limit: dd (low 20 bits available)
;        Attr:  dw (lower 4 bits of higher byte are always 0)
%macro Descriptor 3	                          ; 段基址, 段界限, 段属性
    dw    %2 & 0xFFFF                         ; 段界限1
    dw    %1 & 0xFFFF                         ; 段基址1
    db    (%1 >> 16) & 0xFF                   ; 段基址2
    dw    ((%2 >> 8) & 0xF00) | (%3 & 0xF0FF) ; 属性1 + 段界限2 + 属性2
    db    (%1 >> 24) & 0xFF                   ; 段基址3
%endmacro                                     ; 共 8 字节

loader.asm

%include "inc.asm"

org 0x9000

jmp CODE16_SEGMENT

[section .gdt]
; GDT definition
;                                 段基址,       段界限,       段属性
GDT_ENTRY       :     Descriptor    0,            0,           0             ;第一个段描述符只用于占位
CODE32_DESC     :     Descriptor    0,    Code32SegLen  - 1,   DA_C + DA_32  ;32位保护模式代码段描述符
; GDT end

GdtLen    equ   $ - GDT_ENTRY

GdtPtr:
          dw   GdtLen - 1
          dd   0
          
          
; GDT Selector

Code32Selector    equ (0x0001 << 3) + SA_TIG + SA_RPL0

; end of [section .gdt]


;实模式代码段
[section .s16]
[bits 16]
CODE16_SEGMENT:
    mov ax, cs
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0x7c00
    
    ; initialize GDT for 32 bits code segment
    mov eax, 0
    mov ax, cs
    shl eax, 4
    add eax, CODE32_SEGMENT ;(段地址 << 4) + 偏移地址 ==》 32位代码段实地址
    mov word [CODE32_DESC + 2], ax ;段基址的0-15位放在描述符低4字节的16-31位
    shr eax, 16
    mov byte [CODE32_DESC + 4], al ;段基址的16-23位放在描述符的高4字节的0-7位
    mov byte [CODE32_DESC + 7], ah ;段基址的24-31位放在描述符的高4字节的24-31位
    
    ; initialize GDT pointer struct
    mov eax, 0
    mov ax, ds
    shl eax, 4
    add eax, GDT_ENTRY ;得到全局段描述符表的起始地址
    mov dword [GdtPtr + 2], eax

    ; 1. load GDT
    lgdt [GdtPtr]
    
    ; 2. close interrupt 
    cli    
    
    ; 3. open A20
    in al, 0x92
    or al, 00000010b
    out 0x92, al
    
    ; 4. enter protect mode
    mov eax, cr0
    or eax, 0x01 ;通知处理器进入保护模式:将某一位置1
    mov cr0, eax
    
    ; 5. jump to 32 bits code ;从16位实模式跳转到32保护模式
    jmp dword Code32Selector : 0 ;这一步会将Code32Selector 装入CS,通过选择子就能拿到段基址

	
;32位代码段
[section .s32]
[bits 32]
CODE32_SEGMENT:
    mov eax, 0
    jmp CODE32_SEGMENT

Code32SegLen    equ    $ - CODE32_SEGMENT

makefile


.PHONY : all clean rebuild

BOOT_SRC := boot.asm
BOOT_OUT := boot

LOADER_SRC  := loader.asm
INCLUDE_SRC := inc.asm
LOADER_OUT  := loader

IMG := data.img
IMG_PATH := /mnt/hgfs

RM := rm -fr

all : $(IMG) $(BOOT_OUT) $(LOADER_OUT)
	@echo "Build Success ==> D.T.OS!"

$(IMG) :
	bximage $@ -q -fd -size=1.44
	
$(BOOT_OUT) : $(BOOT_SRC)
	nasm $^ -o $@
	dd if=$@ of=$(IMG) bs=512 count=1 conv=notrunc
	
$(LOADER_OUT) : $(LOADER_SRC) $(INCLUDE_SRC)
	nasm $< -o $@
	sudo mount -o loop $(IMG) $(IMG_PATH)
	sudo cp $@ $(IMG_PATH)/$@
	sudo umount $(IMG_PATH)
	
clean :
	$(RM) $(IMG) $(BOOT_OUT) $(LOADER_OUT)
	
rebuild :
	@$(MAKE) clean
	@$(MAKE) all

很多东西都可以反汇编调试分析

例如:分析用lgdt指令将GDT基地址(32位)和边界(16位)装入GDTR(48位)

找到lgdt指令的内存地址,在bochs里打断点分析

上面的汇编代码只有两个描述符所以GDT大小为16字节(0x0f),GDT的起始地址是0x9004

再例如:实模式和保护模式对内存单元的访问不同

                   0008:00000000 -->  8是索引1<<3+0+0所得,通过选择子得到段基址(0x907c)

 

疑难问题:

为什么不直接使用标签定义描述符中的段基地址?

为什么16位代码段到32位代码段必须无条件跳转?

jmp dword Code32Selector : 0为什么使用dword?

 

需要掌握的重点

     - NASM将汇编文件当成一个独立的代码段编译,从0开始计算,每一条指令对应一个汇编地址

     - 汇编代码中的标签(Label)代表的是段内偏移地址

     - 实模式下需要配合段寄存器中的值计算标签的物理地址

流水线技术

     - 处理器为了提高效率将当前指令和后续指令预取到流水线

     - 因此,可能同时预取的指令中既有16位代码又有32位代码

     - 为了避免将32位代码用16位的方式运行,需要刷新流水线

     - 无条件跳转jmp 能强制刷新流水线...

不一般的jmp(s16→s32)

     - 在16位代码中,所有的立即数默认为16位

     - 从16位代码段跳转到32位代码段的时,必须做强制转换

     - 否则,段内偏移地址可能被截断

 

小结

    - 80386处理器是计算机发展史上的里程碑

    - 32位的寄存器和地址总线能够直接访问4G内存的任意角落

    - 需要在16位实模式中对GDT中的数据进行初始化

    - 代码中需要为GDT定义一个标识数据结构(GdtPtr)

    - 需要使用jmp指令从16位代码跳转到32位代码

 

2、深入保护模式

1、显存段、数据段和栈段

为了显示数据,必须存在两大硬件:显卡+显示器

     - 显卡:为显示器提供需要显示的数据,控制显示器的模式和状态

     - 显示器:将目标数据以可见的方式呈现在屏幕上

显存的概念和意义

     - 显卡拥有自己内部的数据存储器,简称显存

     - 显存在本质上和普通内存无差别,用于存储目标数据

     - 操作显存中的数据将导致显示器上内容的改变

显卡的工作模式:文本模式&图形模式

     - 在不同的模式下,显卡对显存内容的解释是不同的

     - 可以使用专属指令或int 0x10中断改变显卡工作模式

     - 在文本模式下:显存的地址范围映射 为:[0xB8000,0XBFFFF],一屏幕可以显示25行,每行80个字符

   

显卡的文本模式原理

          每行可以显示80个字符,一个字符由两个字节组成,低字节为要显示的字符,高字节为显示的属性

 

小目标:在保护模式下,打印指定内存中的字符串

     - 定义全局堆栈段(gs),用于保护模式下的函数调用

     - 定义全局数据段(.dat),用于定义只读数据(D.T.OS!)

     - 利用对显存段的操作定义字符串打印函数(PrintString)

 

保护模式下的栈段(Stack Segment)

    1. 指定一段空间,并为其定义段描述符

    2. 根据段描述符表中的位置定义选择子

    3. 初始化栈段寄存器(ss←StackSelector)

    4. 初始化栈顶指针(esp←TopOfStack)

 

打印函数(PrintString)的设计

汇编小贴士

32位保护模式下的乘法操作(mul)

     - 被乘数放到AX寄存器

     - 乘数放到通用寄存器或内存单元(16位)

     - 相乘的结果放到EAX寄存器中

再论$和$$

     - $表示当前行相对于代码起始位置处的偏移量

     - $$表示当前代码节(section)的起始位置

 

2、编程实验

深度体验保护模式 loader.asm

%include "inc.asm"

org 0x9000

jmp CODE16_SEGMENT

[section .gdt]
; GDT definition
;                                 段基址,       段界限,       段属性
GDT_ENTRY       :     Descriptor    0,            0,           0
CODE32_DESC     :     Descriptor    0,    Code32SegLen - 1,    DA_C + DA_32
VIDEO_DESC      :     Descriptor 0xB8000,     0x07FFF,         DA_DRWA + DA_32
DATA32_DESC     :     Descriptor    0,    Data32SegLen - 1,    DA_DR + DA_32
STACK32_DESC      :     Descriptor    0,     TopOfStack32,     DA_DRW + DA_32 ;实模式栈空间0~0x7c00,保护模式下的栈空间另外定义
; GDT end

GdtLen    equ   $ - GDT_ENTRY

GdtPtr:
          dw   GdtLen - 1
          dd   0
          
          
; GDT Selector

Code32Selector    equ (0x0001 << 3) + SA_TIG + SA_RPL0
VideoSelector     equ (0x0002 << 3) + SA_TIG + SA_RPL0
Data32Selector    equ (0x0003 << 3) + SA_TIG + SA_RPL0
Stack32Selector     equ (0x0004 << 3) + SA_TIG + SA_RPL0

; end of [section .gdt]

TopOfStack16   equ 0x7c00

[section .dat]
[bits 32]
DATA32_SEGMENT:
    DTOS               db  "D.T.OS!", 0
    DTOS_OFFSET        equ DTOS - $$        ;段内偏移地址
    HELLO_WORLD        db  "Hello World!", 0
    HELLO_WORLD_OFFSET equ HELLO_WORLD - $$ ;段内偏移地址

Data32SegLen equ $ - DATA32_SEGMENT

[section .s16]
[bits 16]
CODE16_SEGMENT:
    mov ax, cs
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, TopOfStack16 
    
    ; initialize GDT for 32 bits code segment
    mov esi, CODE32_SEGMENT
    mov edi, CODE32_DESC
    
    call InitDescItem ;实模式下的函数调用
    
    mov esi, DATA32_SEGMENT
    mov edi, DATA32_DESC
    
    call InitDescItem
	
	mov esi, STACK32_SEGMENT
    mov edi, STACK32_DESC
    
    call InitDescItem
    
    ; initialize GDT pointer struct
    mov eax, 0
    mov ax, ds
    shl eax, 4
    add eax, GDT_ENTRY
    mov dword [GdtPtr + 2], eax

    ; 1. load GDT
    lgdt [GdtPtr]
    
    ; 2. close interrupt
    cli 
    
    ; 3. open A20
    in al, 0x92
    or al, 00000010b
    out 0x92, al
    
    ; 4. enter protect mode
    mov eax, cr0
    or eax, 0x01
    mov cr0, eax
    
    ; 5. jump to 32 bits code
    jmp dword Code32Selector : 0


; esi    --> code segment label
; edi    --> descriptor label
InitDescItem:
    push eax

    mov eax, 0
    mov ax, cs
    shl eax, 4
    add eax, esi
    mov word [edi + 2], ax
    shr eax, 16
    mov byte [edi + 4], al
    mov byte [edi + 7], ah
    
    pop eax
    
    ret
    
    
[section .s32]
[bits 32]
CODE32_SEGMENT:
    mov ax, VideoSelector
    mov gs, ax
    
    mov ax, Stack32Selector  
    mov ss, ax
	
	mov eax, TopOfStack32
    mov esp, eax
    
    mov ax, Data32Selector
    mov ds, ax
    
    mov ebp, DTOS_OFFSET ;注意应该是数据段内偏移地址
    mov bx, 0x0C
    mov dh, 12
    mov dl, 33
    
    call PrintString ;32位保护模式下的函数调用
    
    mov ebp, HELLO_WORLD_OFFSET
    mov bx, 0x0C
    mov dh, 13
    mov dl, 31
    
    call PrintString
    
    jmp $

; ds:ebp    --> string address
; bx        --> attribute
; dx        --> dh : row, dl : col
PrintString:
    push ebp
    push eax
    push edi
    push cx
    push dx
    
print:
    mov cl, [ds:ebp]
    cmp cl, 0
    je end
    mov eax, 80
    mul dh
    add al, dl
    shl eax, 1 ;(80 * row + col) * 2
    mov edi, eax
    mov ah, bl
    mov al, cl
    mov [gs:edi], ax
    inc ebp
    inc dl
    jmp print

end:
    pop dx
    pop cx
    pop edi
    pop eax
    pop ebp
    
    ret
    
Code32SegLen    equ    $ - CODE32_SEGMENT

;32位保护模式的栈
[section .gs]
[bits 32]
STACK32_SEGMENT:
    times 1024 * 4 db 0
    
Stack32SegLen equ $ - STACK32_SEGMENT
TopOfStack32  equ Stack32SegLen - 1

 

小结

    - 实模式下可以使用32位寄存器和32位地址

    - 显存是显卡内部的存储单元,本质上与普通内存无差别

    - 显卡有两种工作模式:文本模式&图形模式

    - 文本模式下操作显存单元中的数据能够立即反映到显示器

    - 定义保护模式的栈段时,必须设置段选择子和栈顶指针

2、从保护模式返回实模式

80x86中的一个神秘限制

    - 无法直接从32位保护模式代码段回到实模式

    - 只能从16位保护模式代码段间接返回实模式(保护模式下也可以定义16位代码段)

      这是因为无法实现从32位代码段返回时cs高速缓冲寄存器中的属性符合实模式的要求(实模式不能改变段属性)。

      需要加载一个合适的描述符选择子到有关段寄存器,以使对应段描述符高速缓冲寄存器中含有合适的段界限和属性

    - 在返回前必须用合适的选择子对段寄存器赋值

 

80286之后的处理器都提供兼容8086的实模式, 然而,绝大多时候处理器都运行于保护模式,

因此,保护模式的运行效率至关重要。那么,处理器为了高效的访问内存中的段描述符增加高速缓冲存储器

当使用选择子设置段寄存器时,根据选择子 访问内存中的段描述符,将段描述符加载到段寄存器的高速缓冲存储器

需要段描述符信息时,直接从高速缓冲存储器中获得,这是前面讲过的。

 

那么当处理器运行于实模式时段寄存器的高速缓冲存储器是否会用到

 

注意事项!!!

    - 在实模式下,高速缓冲存储器仍然保存这三个属性,段基址是段寄存器左移4位的值

    - 段基址是32位,其值是相应段寄存器的值乘以16(不用每次做乘法,直接从高速缓冲寄存器取)

    - 实模式下段基址有效位为20位,段界限固定为0xFFFF(64K)

    - 段属性的值不可设置,只能继续沿用保护方式下所设置的值

因此,当从保护模式返回实模式时:在16位保护模式代码段中

通过加载一个合适的描述符选择子到有关段寄存器,以使得对应段描述符高速缓冲寄存器中含有合适的段界限和属性!!

 

返回实模式的流程

jmp指令

编程实验

从保护模式返回实模式

%include "inc.asm"

org 0x9000

jmp ENTRY_SEGMENT

[section .gdt]
; GDT definition
;                                 段基址,       段界限,       段属性
GDT_ENTRY       :     Descriptor    0,            0,           0
CODE32_DESC     :     Descriptor    0,    Code32SegLen - 1,    DA_C + DA_32
VIDEO_DESC      :     Descriptor 0xB8000,     0x07FFF,         DA_DRWA + DA_32
DATA32_DESC     :     Descriptor    0,    Data32SegLen - 1,    DA_DR + DA_32
STACK32_DESC    :     Descriptor    0,     TopOfStack32,       DA_DRW + DA_32
CODE16_DESC     :     Descriptor    0,        0xFFFF,          DA_C ;此处不可设置为Code16SegLen,否则在jmp时会检查地址越界CPU异常
UPDATE_DESC     :     Descriptor    0,        0xFFFF,          DA_DRW ;16位实模式下每一个段最大64k,段属性可读可写
; GDT end

GdtLen    equ   $ - GDT_ENTRY

GdtPtr:
          dw   GdtLen - 1
          dd   0
          
          
; GDT Selector

Code32Selector    equ (0x0001 << 3) + SA_TIG + SA_RPL0
VideoSelector     equ (0x0002 << 3) + SA_TIG + SA_RPL0
Data32Selector    equ (0x0003 << 3) + SA_TIG + SA_RPL0
Stack32Selector   equ (0x0004 << 3) + SA_TIG + SA_RPL0
Code16Selector    equ (0x0005 << 3) + SA_TIG + SA_RPL0
UpdateSelector    equ (0x0006 << 3) + SA_TIG + SA_RPL0
; end of [section .gdt]

TopOfStack16    equ 0x7c00

[section .dat]
[bits 32]
DATA32_SEGMENT:
    DTOS               db  "D.T.OS!", 0
    DTOS_OFFSET        equ DTOS - $$
    HELLO_WORLD        db  "Hello World!", 0
    HELLO_WORLD_OFFSET equ HELLO_WORLD - $$

Data32SegLen equ $ - DATA32_SEGMENT

[section .s16]
[bits 16]
ENTRY_SEGMENT:
    mov ax, cs
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, TopOfStack16
    
    mov [BACK_TO_REAL_MODE + 3], ax ;动态的填入cs寄存器运行后的值
    
    ; initialize GDT for 32 bits code segment
    mov esi, CODE32_SEGMENT
    mov edi, CODE32_DESC
    
    call InitDescItem
    
    mov esi, DATA32_SEGMENT
    mov edi, DATA32_DESC
    
    call InitDescItem
    
    mov esi, STACK32_SEGMENT
    mov edi, STACK32_DESC
    
    call InitDescItem
    
    mov esi, CODE16_SEGMENT
    mov edi, CODE16_DESC
    
    call InitDescItem
    
    ; initialize GDT pointer struct
    mov eax, 0
    mov ax, ds
    shl eax, 4
    add eax, GDT_ENTRY
    mov dword [GdtPtr + 2], eax

    ; 1. load GDT
    lgdt [GdtPtr]
    
    ; 2. close interrupt
    cli 
    
    ; 3. open A20
    in al, 0x92
    or al, 00000010b
    out 0x92, al
    
    ; 4. enter protect mode
    mov eax, cr0
    or eax, 0x01
    mov cr0, eax
    
    ; 5. jump to 32 bits code
    jmp dword Code32Selector : 0

;返回实模式
BACK_ENTRY_SEGMENT:
    mov ax, cs 
    mov ds, ax ;对段寄存器的赋值只会改变高速缓冲寄存器中的段基址,段界限和属性沿用UPDATE_DESC
    mov es, ax
    mov ss, ax
    mov sp, TopOfStack16 ;重新设置各个段寄存器的值,恢复sp的值
    
    in al, 0x92
    and al, 11111101b
    out 0x92, al
    
    sti
    
    mov bp, HELLO_WORLD
    mov cx, 12
    mov dx, 0
    mov ax, 0x1301
    mov bx, 0x0007
    int 0x10
    
    jmp $

; esi    --> code segment label
; edi    --> descriptor label
InitDescItem:
    push eax

    mov eax, 0
    mov ax, cs
    shl eax, 4
    add eax, esi
    mov word [edi + 2], ax
    shr eax, 16
    mov byte [edi + 4], al
    mov byte [edi + 7], ah
    
    pop eax
    
    ret
    

;保护模式下16位代码段
[section .s16]
[bits 16]
CODE16_SEGMENT:
    mov ax, UpdateSelector
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax  ;刷新对应段描述符高速缓冲寄存器(含有合适的段界限和属性)以合法表示实模式
	
    mov eax, cr0
    and al, 11111110b
    mov cr0, eax ;成功进入实模式

BACK_TO_REAL_MODE:    
    jmp 0 : BACK_ENTRY_SEGMENT ;此处按照16位实模式方式跳转,所以这个偏移地址(最大64k)不可超出段界限大小,执行时依旧受保护
    
Code16SegLen    equ    $ - CODE16_SEGMENT

;保护模式下32位代码段
[section .s32]
[bits 32]
CODE32_SEGMENT:
    mov ax, VideoSelector
    mov gs, ax
    
    mov ax, Stack32Selector
    mov ss, ax
    
    mov eax, TopOfStack32
    mov esp, eax
    
    mov ax, Data32Selector
    mov ds, ax
    
    mov ebp, DTOS_OFFSET
    mov bx, 0x0C
    mov dh, 12
    mov dl, 33
    
    call PrintString
    
    mov ebp, HELLO_WORLD_OFFSET
    mov bx, 0x0C
    mov dh, 13
    mov dl, 31
    
    call PrintString
    
    jmp Code16Selector : 0 ;跳转到16位保护模式代码段

; ds:ebp    --> string address
; bx        --> attribute
; dx        --> dh : row, dl : col
PrintString:
    push ebp
    push eax
    push edi
    push cx
    push dx
    
print:
    mov cl, [ds:ebp]
    cmp cl, 0
    je end
    mov eax, 80
    mul dh
    add al, dl
    shl eax, 1
    mov edi, eax
    mov ah, bl
    mov al, cl
    mov [gs:edi], ax
    inc ebp
    inc dl
    jmp print

end:
    pop dx
    pop cx
    pop edi
    pop eax
    pop ebp
    
    ret
    
Code32SegLen    equ    $ - CODE32_SEGMENT

[section .gs]
[bits 32]
STACK32_SEGMENT:
    times 1024 * 4 db 0
    
Stack32SegLen equ $ - STACK32_SEGMENT
TopOfStack32  equ Stack32SegLen - 1

 

小结

    - 从保护模式能够间接跳转返回实模式

    - 在实模式下,依然使用高速缓冲存储器中的数据做有效性判断

    - 通过运行时修改指令中的数据能够动态决定代码的行为

 

 

3、局部段描述符表的使用

什么是LDT(Local Descriptor Table)?

局部段描述符表

     - 本质是一个段描述符表,用于定义段描述符

     - 与GDT类似,可以看作“段描述符的数组"

     - 通过定义选择子访问局部段描述符表中的元素

LDTR局部描述符寄存器:16位寄存器,存放的是选择子。

①一个处理器只对应一个GDT,前面说过GDTR存放着GDT在内存的入口地址和界限,此后CPU通过GDTR找到GDT。

②局部描述符表可以有多个,局部段描述符表也是一段内存,即需要在GDT增加一个描述符描述它(③),该描述符的

    选择子是通过lldt指令装载到LDTR(16位)。通过这个选择子就能在GDT中找到LDT描述符,从而确定内存中的LDT。

④段选择器存放的是局部描述符表中描述符的选择子,于是就能确定描述符,从而确定描述符描述的一段内存(⑤)

 

                                 此图来源网络,由于源较多具体也不知是谁的

 

局部段描述符选择子

注意事项!!

     - 局部段描述符表需要在全局段描述符表中注册(增加描述项)

     - 通过对应的选择子加载局部段描述符(lldt指令)

     - 局部段描述符表从第0项开始使用(different from GDT)

 

LDT具体用来干什么?为什么还需要一个“额外的“段描述符表?

        要知道当需要很多段时全局描述符表肯定不够用

LDT的意义

     - 代码层面的意义:分级管理功能相同意义不同的段(如:多个代码段)

     - 系统层面的意义:实现多任务的基础要素(每个任务对应一系列不同的段)

LDT的定义与使用

     1. 定义独立功能相关的段(代码段,数据段,栈段)(有着对应的段描述符)

     2. 将目标段描述符组成局部段描述符表(LDT)

     3. 为各个段描述符定义选择子(SA_TIL)

     4. 在GDT中定义LDT的段描述符,并定义选择子

 

编程实验

使用LDT实现新功能

inc.asm


; Segment Attribute
DA_32    equ    0x4000
DA_DR    equ    0x90
DA_DRW   equ    0x92
DA_DRWA  equ    0x93
DA_C     equ    0x98
DA_CR    equ    0x9A
DA_CCO   equ    0x9C
DA_CCOR  equ    0x9E

; Special Attribute
DA_LDT   equ    0x82

; Selector Attribute
SA_RPL0    equ    0
SA_RPL1    equ    1
SA_RPL2    equ    2
SA_RPL3    equ    3

SA_TIG    equ    0
SA_TIL    equ    4

; 描述符
; usage: Descriptor Base, Limit, Attr
;        Base:  dd
;        Limit: dd (low 20 bits available)
;        Attr:  dw (lower 4 bits of higher byte are always 0)
%macro Descriptor 3	                          ; 段基址, 段界限, 段属性
    dw    %2 & 0xFFFF                         ; 段界限1
    dw    %1 & 0xFFFF                         ; 段基址1
    db    (%1 >> 16) & 0xFF                   ; 段基址2
    dw    ((%2 >> 8) & 0xF00) | (%3 & 0xF0FF) ; 属性1 + 段界限2 + 属性2
    db    (%1 >> 24) & 0xFF                   ; 段基址3
%endmacro                                     ; 共 8 字节

loader.asm

%include "inc.asm"

org 0x9000

jmp ENTRY_SEGMENT

[section .gdt]
; GDT definition
;                                 段基址,       段界限,       段属性
GDT_ENTRY       :     Descriptor    0,            0,           0
CODE32_DESC     :     Descriptor    0,    Code32SegLen - 1,    DA_C + DA_32
VIDEO_DESC      :     Descriptor 0xB8000,     0x07FFF,         DA_DRWA + DA_32
DATA32_DESC     :     Descriptor    0,    Data32SegLen - 1,    DA_DR + DA_32
STACK32_DESC    :     Descriptor    0,     TopOfStack32,       DA_DRW + DA_32
CODE16_DESC     :     Descriptor    0,        0xFFFF,          DA_C 
UPDATE_DESC     :     Descriptor    0,        0xFFFF,          DA_DRW
TASK_A_LDT_DESC :     Descriptor    0,     TaskALdtLen - 1,    DA_LDT ;LDT在GDT的描述符
; GDT end

GdtLen    equ   $ - GDT_ENTRY

GdtPtr:
          dw   GdtLen - 1
          dd   0
          
          
; GDT Selector

Code32Selector    equ (0x0001 << 3) + SA_TIG + SA_RPL0
VideoSelector     equ (0x0002 << 3) + SA_TIG + SA_RPL0
Data32Selector    equ (0x0003 << 3) + SA_TIG + SA_RPL0
Stack32Selector   equ (0x0004 << 3) + SA_TIG + SA_RPL0
Code16Selector    equ (0x0005 << 3) + SA_TIG + SA_RPL0
UpdateSelector    equ (0x0006 << 3) + SA_TIG + SA_RPL0
TaskALdtSelector  equ (0x0007 << 3) + SA_TIG + SA_RPL0 ;LDT在GDT的描述符的选择子
; end of [section .gdt]

TopOfStack16    equ 0x7c00

[section .dat]
[bits 32]
DATA32_SEGMENT:
    DTOS               db  "D.T.OS!", 0
    DTOS_OFFSET        equ DTOS - $$
    HELLO_WORLD        db  "Hello World!", 0
    HELLO_WORLD_OFFSET equ HELLO_WORLD - $$

Data32SegLen equ $ - DATA32_SEGMENT

[section .s16]
[bits 16]
ENTRY_SEGMENT:
    mov ax, cs
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, TopOfStack16
    
    mov [BACK_TO_REAL_MODE + 3], ax
    
    ; initialize GDT for 32 bits code segment
    mov esi, CODE32_SEGMENT
    mov edi, CODE32_DESC
    
    call InitDescItem
    
    mov esi, DATA32_SEGMENT
    mov edi, DATA32_DESC
    
    call InitDescItem
    
    mov esi, STACK32_SEGMENT
    mov edi, STACK32_DESC
    
    call InitDescItem
    
    mov esi, CODE16_SEGMENT
    mov edi, CODE16_DESC
    
    call InitDescItem
    
	;注意初始化段基址
    mov esi, TASK_A_LDT_ENTRY
    mov edi, TASK_A_LDT_DESC
    
    call InitDescItem
    
    mov esi, TASK_A_CODE32_SEGMENT
    mov edi, TASK_A_CODE32_DESC
    
    call InitDescItem
    
    mov esi, TASK_A_DATA32_SEGMENT
    mov edi, TASK_A_DATA32_DESC
    
    call InitDescItem
    
    mov esi, TASK_A_STACK32_SEGMENT
    mov edi, TASK_A_STACK32_DESC
    
    call InitDescItem
    
    ; initialize GDT pointer struct
    mov eax, 0
    mov ax, ds
    shl eax, 4
    add eax, GDT_ENTRY
    mov dword [GdtPtr + 2], eax

    ; 1. load GDT
    lgdt [GdtPtr]
    
    ; 2. close interrupt
    cli 
    
    ; 3. open A20
    in al, 0x92
    or al, 00000010b
    out 0x92, al
    
    ; 4. enter protect mode
    mov eax, cr0
    or eax, 0x01
    mov cr0, eax
    
    ; 5. jump to 32 bits code
    jmp dword Code32Selector : 0

BACK_ENTRY_SEGMENT:
    mov ax, cs
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, TopOfStack16
    
    in al, 0x92
    and al, 11111101b
    out 0x92, al
    
    sti
    
    mov bp, HELLO_WORLD
    mov cx, 12
    mov dx, 0
    mov ax, 0x1301
    mov bx, 0x0007
    int 0x10
    
    jmp $

; esi    --> code segment label
; edi    --> descriptor label
InitDescItem:
    push eax

    mov eax, 0
    mov ax, cs
    shl eax, 4
    add eax, esi
    mov word [edi + 2], ax
    shr eax, 16
    mov byte [edi + 4], al
    mov byte [edi + 7], ah
    
    pop eax
    
    ret
    

[section .s16]
[bits 16]
CODE16_SEGMENT:
    mov ax, UpdateSelector
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    
    mov eax, cr0
    and al, 11111110b
    mov cr0, eax

BACK_TO_REAL_MODE:    
    jmp 0 : BACK_ENTRY_SEGMENT
    
Code16SegLen    equ    $ - CODE16_SEGMENT

    
[section .s32]
[bits 32]
CODE32_SEGMENT:
    mov ax, VideoSelector
    mov gs, ax
    
    mov ax, Stack32Selector
    mov ss, ax
    
    mov eax, TopOfStack32
    mov esp, eax
    
    mov ax, Data32Selector
    mov ds, ax
    
    mov ebp, DTOS_OFFSET
    mov bx, 0x0C
    mov dh, 12
    mov dl, 33
    
    call PrintString
    
    mov ebp, HELLO_WORLD_OFFSET
    mov bx, 0x0C
    mov dh, 13
    mov dl, 31
    
    call PrintString
    
    mov ax, TaskALdtSelector
    
    lldt ax ;使用LDTR寄存器保存 LDT在GDT的描述符的选择子 
    
    jmp TaskACode32Selector : 0 ;通过选择子在LDT找到描述符获得段基址 (通过TI位得到哪个描述符表)
    
    ; jmp Code16Selector : 0

; ds:ebp    --> string address
; bx        --> attribute
; dx        --> dh : row, dl : col
PrintString:
    push ebp
    push eax
    push edi
    push cx
    push dx
    
print:
    mov cl, [ds:ebp]
    cmp cl, 0
    je end
    mov eax, 80
    mul dh
    add al, dl
    shl eax, 1
    mov edi, eax
    mov ah, bl
    mov al, cl
    mov [gs:edi], ax
    inc ebp
    inc dl
    jmp print

end:
    pop dx
    pop cx
    pop edi
    pop eax
    pop ebp
    
    ret
    
Code32SegLen    equ    $ - CODE32_SEGMENT

[section .gs]
[bits 32]
STACK32_SEGMENT:
    times 1024 * 4 db 0
    
Stack32SegLen equ $ - STACK32_SEGMENT
TopOfStack32  equ Stack32SegLen - 1


;启动一个新任务
; ==========================================
;
;            Task A Code Segment 
;
; ==========================================

[section .task-a-ldt]
; Task A LDT definition
;                                             段基址,                段界限,                段属性
TASK_A_LDT_ENTRY:
TASK_A_CODE32_DESC    :    Descriptor          0,           TaskACode32SegLen - 1,        DA_C + DA_32
TASK_A_DATA32_DESC    :    Descriptor          0,           TaskAData32SegLen - 1,        DA_DR + DA_32
TASK_A_STACK32_DESC   :    Descriptor          0,           TaskAStack32SegLen - 1,       DA_DRW + DA_32

TaskALdtLen  equ   $ - TASK_A_LDT_ENTRY

; Task A LDT Selector
TaskACode32Selector  equ   (0x0000 << 3) + SA_TIL + SA_RPL0 ;表明是局部段描述符表的选择子
TaskAData32Selector  equ   (0x0001 << 3) + SA_TIL + SA_RPL0
TaskAStack32Selector equ   (0x0002 << 3) + SA_TIL + SA_RPL0

[section .task-a-dat]
[bits 32]
TASK_A_DATA32_SEGMENT:
    TASK_A_STRING        db   "This is Task A!", 0
    TASK_A_STRING_OFFSET equ  TASK_A_STRING - $$
    
TaskAData32SegLen  equ  $ - TASK_A_DATA32_SEGMENT

[section .task-a-gs]
[bits 32]
TASK_A_STACK32_SEGMENT:
    times 1024 db 0 ;该栈大小1k
    
TaskAStack32SegLen  equ  $ - TASK_A_STACK32_SEGMENT
TaskATopOfStack32   equ  TaskAStack32SegLen - 1 ;初始栈顶指针位置

[section .task-a-s32]
[bits 32]
TASK_A_CODE32_SEGMENT:
    mov ax, VideoSelector 
    mov gs, ax
    
	;从新指定栈段,数据段
    mov ax, TaskAStack32Selector 
    mov ss, ax
    
    mov eax, TaskATopOfStack32
    mov esp, eax
    
    mov ax, TaskAData32Selector
    mov ds, ax
    
	;打印字符串
    mov ebp, TASK_A_STRING_OFFSET
    mov bx, 0x0C
    mov dh, 14
    mov dl, 29
    
    call TaskAPrintString ;为什么不直接调用PrintString而是拷贝一份在该段?
    
    jmp Code16Selector : 0
    

; ds:ebp    --> string address
; bx        --> attribute
; dx        --> dh : row, dl : col
TaskAPrintString:
    push ebp
    push eax
    push edi
    push cx
    push dx
    
task_print:
    mov cl, [ds:ebp]
    cmp cl, 0
    je task_end
    mov eax, 80
    mul dh
    add al, dl
    shl eax, 1
    mov edi, eax
    mov ah, bl
    mov al, cl
    mov [gs:edi], ax
    inc ebp
    inc dl
    jmp task_print

task_end:
    pop dx
    pop cx
    pop edi
    pop eax
    pop ebp
    
    ret
    
TaskACode32SegLen   equ  $ - TASK_A_CODE32_SEGMENT

发现关于ldtr寄存器网上很多说法不一致,这里断点分析一下

              ldtr保存的值是0x0038即56 = 7 * 8 索引值为7,说明ldtr保存的就是ldt在gdt中的选择子

 

多任务程序设计的实现思路

 

保护模式下的不同段之间如何进行代码复用(如:调用同一个函数)?

解决方案

       - 将不同代码段需要复用的函数定义到独立的段中(retf)

       - 计算每一个可复用函数的偏移量(FuncName-$$)

       - 通过段选择子 : 偏移地址的方式对目标函数进行远调用

 

小结

   - 局部段描述表用于组织功能相关的段(section)

   - 局部段描述符表需要加载后才能正常使用(lldt)

   - 局部段描述符表必须在全局段描述符表中注册(Descriptor)

   - 通过局部段描述符表的选择子对其进行访问

   - 局部段描述符表是实现多任务的基础

 


4、初探保护模式的保护机制

保护模式利用段界限对内存访问进行保护

    - 使用选择子访问段描述符表时,索引值会被处理器合法性检测

           ✦ 当索引值越界时,引发异常(cpu reset)

           ✦ 判断规则:索引值 * 8 + 7 <= 段描述表界限值

    - 使用选择子给段寄存器赋值时,内存段类型会被合法性检测

           ✦ 具备可执行属性的段(代码段)只能加载到CS寄存器

           ✦ 具备可写属性的段(数据段)才能加载到SS寄存器

           ✦ 具备可读属性的段才能加载到DS,ES,FS,GS寄存器

    - 代码段和数据段的保护

           ✦ 处理器每访问一个地址都要确认该地址不超过界限值

           ✦ 判断规则:

                          ☛ 代码段:IP+指令长度<=代码段界限

                          ☛ 数据段:访问起始地址+访问数据长度<=数据段界限

 

                                      IP+指令长度若大于界限值,后面的指令就不会再执行

举例说明:

1、CODE32_DESC     :     Descriptor    0,    Code32SegLen - 2,    DA_C + DA_32  

界限值正常应该是Code32SegLen - 1,而 - 2会有指令在界限值之外,cpu在执行这些指令的时候出现异常

2、若CODE32_DESC :     Descriptor    0,    Code32SegLen - 1,    DA_DR + DA_32 (没有可执行属性DA_C)

执行 jmp dword Code32Selector : 0时会触发异常 (cpu reset)。

执行这条指令时会将选择子Code32Selector加载到CS寄存器中时,要确保加载到CS寄存器的段要有可执行属性

注意:保护模式中代码中定义的界限值通常为:最大偏移地址值(相对于段基地址)。

 

保护模式除了利用段界限对内存访问进行保护,是否还提供其它的保护机制?

 

下一篇文章详细讲解其他保护机制


本节参考狄泰未来《操作系统专题课程》、《x86汇编 · 从实模式到保护模式》文中图片多直接引用其中

 

 

  • 5
    点赞
  • 52
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值