从计算机的示例谈起:
远古的程序开发:直接操作物理内存
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个字节
段界限来防止回卷,8086不推荐使用回卷。定义段内偏移地址的最大值来防止回卷。
段基址为什么要分三个部分? 历史原因。拼接是硬件来处理的。
在内存中,每个段描述符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位段
选择子属性定义:本质索引
15--3 2 1-0
描述符索引值 TI RPL
RPL--0123
TI--0 GDT 全局段描述符 ---1 LDT 局部段描述符
保护模式中的段定义:
保护模式中的段定义:
汇编小贴士:
-section关键字用于“逻辑的”定义一段代码集合
-section定义的代码段不同于[段地址:偏移地址]的代码段
--section定义的代码段仅限于源码中的代码段(代码节)。文本形式。编译之前
--[段地址:偏移地址]的代码段指向内存中的代码段。编译之后
汇编小贴士:
-[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才能成功访问数据。