10、实模式到保护模式上

从计算机的示例谈起:

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

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

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

绝对的权利带来的问题:

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

有的程序用了开头和结尾,当在别的电脑上运行后,512k内存编写的程序用了开头32k,结尾8k,在256内存的设备上就不能执行了。

2给多道程序设计带来障碍:A程序和外设打交道,B程序也运行运算

A程序和B程序有内存重叠的话,就不能并行执行了。不管内存多大,但凡一个字节被其它程序占用都无法执行。

于是:出现了

cpu历史的里程碑-8086

当时地址线宽度为20位,可访问1M内存空间。

引入[段地址:偏移地址] 的内存访问方式:8086的段寄存器和和通用寄存器为16位,单个寄存器寻址最多访问64k的内存空间。需要两个寄存器配合,完成所有内存空间的访问。

发生冲突时改一下段地址就可以,不用改所有的地址了。

深入解析 [段地址:偏移地址]:

硬件所做的工作:

段地址左移4位,构成20位的基地址(起始地址)

基地址+偏移地址=实地址

对于开发者的意义:

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

当出现程序地址冲突时,通过修改段地址解决冲突。

示例:

mov ax, [0x1234]   ;实地址:(ds<<4)+0x1234

mov ax, [es:0x1234] ; 实地址:(es<<4)+0x1234

有趣的问题:

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

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

0xFFFF: 0xFFFF

-->0xFFFF0+0xFFFF

     -->0xFFFF0+(0xF+0xFFF0)

        -->(0xFFFF0+0xF)+0xFFF0  ==>HMA

HMA: [0x100000, 0x10FFEF]

8086的处理方式:

由于8086只有20位地址线,因此最高位被丢弃(溢出)!

0xFFFF : 0xFFFF==>10000111111111101111(21位)=>回卷0xFFEF

再谈8086历史:

8086在当时是非常成功的一款产品。因此,拥有一大批的开发者和应用程序。各种基于8086程序设计的技术得到了发展。不幸的是,各种不规范的开发方式也产生了。

8086时期应用程序中的问题:

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

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

应用程序之间没有界限,相互之间随意干扰:A程序可以随意访问B程序中的数据。c程序可以修改系统调度程序的指令。

思考:8086程序中问题的本质是什么?

80286的登场:

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

加大内存容量,增加地址线数量(24位:支持16M的内存)

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

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

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

80286的兼容性:

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

默认可直接访问1MB的内存空间。

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

这个特殊的方式指的是什么??

80286之后的工作模式:

实模式:任何内存随意访问

兼容8086的工作模式

实地址=(段寄存器<<4)+偏移地址

任意内存随意访问

保护模式:

新的工作模式

内存地址=段起始地址+偏移地址

每段增加各种属性描述,保证安全性

初始保护模式:

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

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

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

描述符(Descriptor)的内存结构:8个字节

10、实模式到保护模式上_第1张图片

段界限来防止回卷,8086不推荐使用回卷。定义段内偏移地址的最大值来防止回卷。

段基址为什么要分三个部分?  历史原因。拼接是硬件来处理的。

10、实模式到保护模式上_第2张图片

在内存中,每个段描述符8个字节。使用特殊寄存器来装段描述符在内存中的起始地址。每一个段描述符都是一段内存的定义。

选择子(Selector)的结构:

0-15 =>段描述符在段描述符表中的位置。0-1 RPL(特权级)。 2 TI(两个描述符表选择)。

说明:

RPL:请求者特权级标识,通过特权级判断是否可以访问对应段。

TI:   表示当前选择子所属的描述符表(0-GDT,1-LDT)。

进入保护模式的方式:

1、定义描述符表。

2、打开A20地址线。8086(0-19)

3、加载描述符表。

4、通知CPU进入保护模式。

小结:

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

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

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

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

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

 

11、实模式到保护模式(中)

80286的光荣退场:

历史意义:

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

奇葩设计:

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

理论上,段寄存器中的数值可以直接作为段基址。(不用左移4位,没用,上电时处于实模式,段寄存器只有16位能用)

16通用寄存器最多访问64k的内存。

为了访问16M内存,必须不停切换段基址。

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

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

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

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

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

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

新时期的内存使用方式:

实模式:

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

分段模式:

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

平坦模式:

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

有趣的问题?

x86指的究竟是什么处理器?

x指处理器的排名。

段属性定义:

DA_32  0x4000 保护模式下32位段

10、实模式到保护模式上_第3张图片

选择子属性定义:本质索引

15--3                       2      1-0

10、实模式到保护模式上_第4张图片

描述符索引值       TI      RPL

RPL--0123

TI--0  GDT 全局段描述符 ---1  LDT  局部段描述符

保护模式中的段定义:

10、实模式到保护模式上_第5张图片

保护模式中的段定义:

10、实模式到保护模式上_第6张图片

汇编小贴士:

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

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

--section定义的代码段仅限于源码中的代码段(代码节)。文本形式。编译之前

--[段地址:偏移地址]的代码段指向内存中的代码段。编译之后

10、实模式到保护模式上_第7张图片

汇编小贴士:

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

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

注意事项:

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

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

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

问题:

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

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

需要掌握的重点:问题1

-NASM将汇编文件当成一个独立的代码段编译。--标签代表的是段内偏移地址不是真正的物理地址

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

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

问题2:

小知识:

--流水线技术

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

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

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

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

-。。。

小结:

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

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

需要在16位实模式中对GDT中的数据进行初始化。定义段描述符和段描述符表。

代码中需要为GDT定义一个标识数据结构(GdtPtr)。(用来加载段描述符表)

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

强调:对于具体的段描述符里边的段基地址,需要在16位模式下面进行计算得到,不要使用标签定义段描述符表中段基地址,因为标签仅仅是个偏移地址。

12、实模式到保护模式下

一个值得注意的细节:

;5. jump to 32 bits code

jmp dword Code32Selector:0

为什么需要  dword?

不一般的jmp(s16->s32)

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

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

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

jmp Code32Selector:0x12345678   (段内偏移地址)   按照16位数据来解释0x12345678

结果为0x5678.

深入保护模式:定义显存段

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

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

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

显存的概念和意义:

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

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

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

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

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

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

-在文本模式下:

 显存的地址范围映射为:[0xB8000,0xBFFFF]

 一屏幕可以显示25行,每行80个字符。

显卡的文本模式原理:

0 字符       2 字符

1 属性      3 属性

文本模式下显示字符:

CODE_SEGMENT:

    mov ax, VideoSelector

    mov gs, ax                          ; 显存段选择子

    mov edi, (80*12+38)*2;    ; 屏幕第12行,第38列

    mov ah, 0x0c                     ; 0000 : 黑底。1100:红字

    mov al, 'p'                           ; 显示字符‘p’

    mov [gs:edi], ax

    jmp $

小目标:

-在保护模式下,打印指定内存中的字符串

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

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

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

打印函数(PrintString)的设计:

被打印的字符串必须以‘/0’结束。

; ds:ebp  -->string address

;bx          -->attribute

;dx         -->dh: row,  dl : col

PrintString

 ;...

 ;...

ret

 

mov cl, [ds:ebp] --> cl != 0? -->将cl代表的字符写入显存位置:(dh,dl) --> add ebp,1  add dl,1 (打印下一个字符)  返回去

当cl=0时,停止打印,打印完毕。

汇编小贴士:

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

-- 被乘数放到 AX 寄存器

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

-- 相乘的结果放到EAX寄存器中(结果放到32位寄存器中)

汇编小贴士:

--再论 & 和 &&

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

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

[ section .dat]

[ bits 32 ]

    DTOS    db   "D.T.OS", 0

    ; 计算DTOS在当前代码节中的偏移位置

   DTOS_OFFSET  equ  DTOS - $$

 

小结:

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

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

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

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

 

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

这里有“Bug”吗?

mov ax, StackSelector

mov ss, ax

call PrintString

指定栈段选择子之后,就可以直接进行函数调用吗?

没有esp指针?

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

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

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

3、初始化栈段寄存器(ss <- StackSelector)。

设置栈段的段基地址。在32位保护模式代码段中将选择子放到ss寄存器,将栈段界限值作为栈顶放入esp寄存器里边。

4、初始化栈顶指针(esp<-TopOfStack)。

栈段的一般性定义(保护模式)

问题:是否能够从保护模式返回实模式?如果可以,如何完成跳转?

80x86中的以神秘限制:

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

-只能从16位代码段间接返回实模式。

-在返回前必须用合适的选择子对段寄存器赋值。(中转站做的事)

先从32位转到16位保护模式代码段作为中转,在到16位实模式。

处理器中的设计简介:

-80286之后的处理器都提供兼容8086的实模式。

-然而,绝大多时候处理器都运行于保护模式。

-因此,保护模式的运行效率至关重要。

-那么,处理器如何高效的访问内存中的段描述符?

解决方案:高速缓存存储器

-当使用选择子设置段寄存器时:

--根据选择子访问内存中的段描述符。

--将段描述符加载到段寄存器的高速缓冲存储器。(每一个段寄存器都有一个高速缓存寄存器)

--需要段描述符信息时,直接从高速缓存存储器中获得。

思考:当处理器运行于实模式时,段寄存器的高速缓冲寄存器是否会用到?

no

注意事项:

-在实模式下,高速缓存寄存器仍然发挥着作用。

-段基址是32位,其值是相应段寄存器的值乘以16.。(实模式下的段基址是段寄存器左移4位)

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

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

段基址  段界限  段属性

因此,当从保护模式返回实模式时:

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

返回实模式的流程:

32位保护模式代码段-->16位保护模式代码段(刷新段寄存器,退出保护模式)-->16位实模式代码段(设置段寄存器的值,关闭A20地址线,启用硬件中断)

汇编小贴士:深入jmp指令:

段内跳转::低地址 (操作码 1Byte) E9   段内偏移地址(操作数 2Byte)      高地址

段间跳转::低地址(操作码 1Byte)EA 偏移地址  段基址(操作数 4 Bytes)  高地址 

实验

小结:

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

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

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

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

 

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

什么是LDT(Local Descriptor Table)?

-局部段描述符表:

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

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

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

局部段描述符选择子:

描述符索引值(15--3) 1(2)LDT选择子的第二位恒为1    RPL(1 -- 0)

TI--SA_TIG  equ  0; GDT

      SA_TIL  equ  4; LDT

局部段描述符表:

    DA_LDT  equ  0x82

  ldt_ptr --> LDT( 段描述符0, 段描述符1 ,段描述符2 )

没有第一项恒为0的限制。

注意事项:

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

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

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

问题:

LDT具体用来干什么?

为什么还需要一个"额外的"段描述符表?

LDT的意义:

-代码层面的意义:

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

-系统层面的意义:

-- 实现多任务的基础要素( 每个任务对一系列不同的段 )

局部段描述符表的使用:

LDT的定义与使用:

1、定义独立功能相关的段(代码段,数据段,栈段)

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

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

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

-LDT的定义与使用:

;step1

TASK_A_LDT_DESC: Descriptor  0,  TaskALdtLen-1, DA_LDT

; step2

TaskALdtSelector  equ(0x0007 << 3) + SA_TIG+SA_RPL0

; Step3

mov ax, TaskALdtSelector         ; specefy the LDT

lldt ax                                         ; load LDT to ldt_ptr 加载到专用寄存器

jmp TaskACode32Selector : 0  ; jump to objective segment to run

实验:

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

;GDT定义

TASK_A_LDT_DESC: Descriptor....

TASK_B_LDT_DESC:Descriptor...

;....

TaskASelector equ ...

TaskASelector equ ...

AB两个任务,以后的多任务执行。。。

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

小结:

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

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

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

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

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

 

15、保护模式中的特权级(上)

保护模式小结:

-使用选择子访问段描述符表时,索引值的合法性检测。

--当索引值越界时,引发异常。处理器充值reset

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

段描述符表:段描述符0,段描述符1,段描述符2,段描述符3

-内存段类型合法性检测:

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

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

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

-代码段和数据段的保护:

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

--判断规则:

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

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

段基地址 指令1  指令2  指令3 指令4(异常段极限,正常段界限)

注意:

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

问题:

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

保护模式中的特权级:

-x86架构中的保护模式提供了4个特权级 ( 0,1,2,3 )

-特权级从高到底分别是0,1,2,3( 数字越大特权级越低 )

内核(level 0)  系统程序(level 1,2)   应用程序( level 3 )

特权级的表现形式:

-CPL(Current Privilege Level)

--当前可执行代码段的特权级,由CS寄存器最低2位定义,保存的是选择子

-DPL(Descriptor Privilege Level)

--内存段的特权级,在段描述符表中定义。

-RPL(Request Privilege Level)

--选择子的特权级,由选择子最低2位定义。

初探特权级:

段基址 G D/B L AVL 段界限 P DPL S TYPE 基址   高32位

段基址 15 -- 0      段界限 15--0    低32位

 段描述符中的DPL用于标识内存段的特权级;可执行代码访问内存段时必须满足一定特权级(CPL),否则,处理器将产生异常。

CPL和DPL的关系:

-保护模式中,每一个代码段都定义了一个DPL

-当处理器从A代码段成功跳转到B代码段执行。

--跳转之前:CPL=DPLA

--跳转之后:CPL=DPLB

-保护模式中,每一个数据段都定义了一个DPL

-当处理器执行过程中需要访问数据段时:CPL <= DPLdata

段描述符中DPL常量定义:

标识符           常量值         意义

DA_DPL0      0x00           DPL=0

DA_DPL1      0x20           DPL=1

DA_DPL2      0x40           DPL=2

DA_DPL3      0x60           DPL=3

从cpl为0的代码段不能跳转到dpl为3的代码段。能跳转到dpl为0的代码段。

代码段访问数据段时,RPL没有参与。

栈段使用不同的规则。

实验结论:

--处理器进入保护模式后CPL=0(最高特权级)

--处理器不能直接从高特权级转换到低特权级执行。

--选择子RPL大于对应段描述符的DPL时,产生异常。

引出的问题:

-如何在不同特权级的代码段之间跳转执行?

-高特权级代码为什么不能使用低特权级栈段?

-选择子的RPL具体有什么用?

小结:

保护模式对内存的访问范围有严格定义。

保护模式定义了内存段的特权级(0,1,2,3)

-每个内存段都有固定的特权级(DPL)

-不同代码段之间成功跳转后CPL可能发生改变。

-CPL小于或等于数据段DPL才能成功访问数据。

你可能感兴趣的:(10、实模式到保护模式上)