声明:本文大部分知识提炼自《操作系统真象还原》一书,读者若是对本文中的某些知识点有疑问,或者想亲手尝试如何编写操作系统那么非常推荐去看看这本书。其次,由于下面内容是本人的思考和总结难免会有理解上的一些问题,如有纰漏希望读者指正。
历史发展:
在介绍相关知识和概念之前我们先了解一点历史,毕竟无论是软件还是硬件的发展都会受到早期规则的影响。所以这里我们先讲一下芯片业的发展,又因为intel是芯片制造厂商的龙头企业,很多划时代的芯片是从他家开始所以接下的介绍无可避免的以intel公司作为背景板。其芯片的发展主要有三个阶段:
intel 4004
1968年,摩尔、诺伊斯、格鲁夫三人成立一家名叫英特尔的公司。
1971年,英特尔公司推出了世界上第一款微处理器4004,这是第一个用于微型计算机的4位微处理器。1972年,英特尔又推出了8008,8008是第一个8位的微处理器,但由于运算性能很差,其市场反应十分不理想。
1974年,8008发展成8080。8080作为代替电子逻辑电路的器件被用于各种应用电路和设备中,如果没有微处理器,这些应用就无法实现。
intel 8086
*1978年,*英特尔公司生产的8086是第一个16位的微处理器,因为8086以86结尾所以该芯片以及后续芯片被称为x86。没多久IBM就推出使用x86和磁盘操作系统(DOS)的个人电脑,从此便开启了x86时代。为什么x86如此重要?因为它是目前为止使用范围最广的微处理器架构。
1982年,英特尔公司在8086的基础上,研制出了80286微处理器,该微处理器的最大主频为20MHz,内、外部数据传输均为16位,使用24位内存储器的寻址,内存寻址能力为16MB。
intel 80386
1985年10月17日,英特尔的划时代的产品80386DX正式发布了。80386最经典的产品为80386DX-33MHz,一般我们说的80386就是指的它。
实模式初识:
好了,历史了解完毕,那么我们现在可以来说一下到底什么是实模式。实模式其实就是8086 cpu的工作环境,主要包括在使用8086下的内存划分、cpu寻址方式、寄存器大小、指令用法这四部分。
在实模式下,寄存器(英文简写:reg)默认16位,地址线20条故可以访问1MB的内存空间。
这1MB的内存空间划分情况如表所示:
编号 | 起始内存地址 | 结束内存地址 | 内存大小 | 说明 |
---|---|---|---|---|
1 | 0xFFFF0 | 0xFFFFF | 16 B | BIOS入口地址,jmp far F000:E05B,机器码 EA5BE000F0 |
2 | 0xF0000 | 0xFFFEF | 63KB+1008B | 系统BIOS范園是F000~FFFF共640KB,为说明入口地址,将最上面的字节从此处去掉了,所以此处终止地址是0XFFFEF |
3 | 0xC8000 | 0xEFFFF | 160 KB | 映射硬件适配器的ROM或内存映射式1/O |
4 | 0xC0000 | 0xC7FFF | 32 KB | 显示适配器BIOS |
5 | 0xB8000 | 0xBFFFF | 32 KB | 文本模式显示适配器 |
6 | 0xB0000 | 0xB7FFF | 32 KB | 黑白显示适配器 |
7 | 0xA0000 | 0xAFFFF | 64 KB | 彩色显示适配器 |
8 | 0x9FC00 | 0x9FFFF | 1 KB | Extended BIOS DataArea,EBDA ,扩展BIOS数据区 |
9 | 0x07E00 | 0x09FBF | 622080B | 可用区域(约608KB) |
10 | 0x07C00 | 0x07DFF | 512 B | MBR |
11 | 0x00500 | 0x07BFF | 30464 B | 可用区域(约30KB) |
12 | 0x00400 | 0x004FF | 256 B | BIOS Data Area,BIOS 数据区 |
13 | 0x00000 | 0x003FF | 1 KB | Interrupt Vector Table ,IDT,中断向量表 |
关于内存的划分我就留下这幅图,好有个大概的印象。上述内容不必要死记,在后续提到的时候再倒回来看即可。
实模式内存划分详解:
不知道在你拥有你的第一台电脑时除了心情激动外,有没有和小弟我一样好奇过这样的一台现代化的电子设备究竟是如何制造出了的?如果你有这样的疑问,那么这里我推荐一本书叫做《编码的奥秘》,这本书就比较详细的介绍了现代的计算机是如何从一个个开关变成能完成复杂问题处理的机器的。本文这里就先不介绍过多了,至于为什么提这一点,主要是因为好奇是驱动一个人学习的动力(并不是因为我想挖坑哈哈)。那么接下来就是对整个上述整个1MB内存分布的详细解读:
BIOS部分:
上述表中第1项,什么是BIOS?即硬件厂商提供一段自检程序,是整个电脑最基础的软件,读者感兴趣可以去某度看看具体介绍这里不过多展开。该表项中对应的地址空间分为两部分0xF00000xFFFEF和0xFFFF00xFFFFF;如果你有一台电脑,那么在你按下电源键的时候cpu就开始工作了,它将一刻不停地执行从内存中获取的命令直到你关闭电脑。(不知道读者你是否知道8086的寄存器以及它们的作用,如果不知道建议可以看看王爽老师的《汇编语言》第三版,书中内容比较简单且详尽的讲述了cpu各寄存器的功能和使用方法。但是不了解也没关系,我在第一次提到的时候会做一个简单的讲解。)首先,cpu只能运行在内存中程序,一个程序要先被加载进内存的某处,然后把cs:ip寄存器指向这个程序在内存中的地址,cpu才能根据cs:ip中的内容进行指令的读取与执行。cs和ip这俩合起来叫做指令寄存器,作用是保存下一条要执行的指定的地址值。一台电脑在我们按下启动电源键的时候最先执行的程序是位于主板上的BIOS程序,该程序保存在主板上的ROM中,而ROM也能够当做内存,所以20条地址线空间的0xF0000~0xFFFFF 这64KB直接对应这块ROM(ps:这里其实涉及到了内存映射的概念,但是为了方便理解我并没有展开,后续会提到)。
那么8086处理器是如何使用这俩寄存器去在这1MB大小的空间里进行寻址的?
这里我们需要花时间来说一下内存分段这个概念,这是为之后的理解做铺垫。简单来说地址线的条数用来表征电脑能够寻址的空间大小,比如20条地址线能够表示2的20次方个数,这些数可以想象成代表内存空间中的一个一个存储单元。8086使用的寄存器单个最大空间为16位,假如我想查找0xF0000这个数代表的存储单元,就需要先把该数放在寄存器,再由cpu根据该数寻找并取出对应单元数据。但是一个16位的寄存无法表示这样一个20位数,所以就需要两个寄存器来。但是又有新问题,两个寄存器加起来一共32位,能够表示2的32次方,多出来的数据就超过了能正常使用的1MB空间。8086是如何解决这个问题的呢?很简单,用两个寄存器,其中一个寄存器数值向左偏移4位再和另外一个寄存器数值相加就得到了一个完整的20位地址。注意这里的偏移不是对寄存器中的数值进行修改,而是在做相加计算的进行偏移相加。举个例子:地址0xF0000 cs中存放0xF000 ip中存放0x0000,那么cpu只要在相加时把cs中的F当做结果20位中的高四位,cs的低12位再和ip中的高12位相加即可。这种计算是放在硬件中实现的,不需要专门的寄存器存储这20位数,相当于给出两个16位数cpu就自动转成对应的20位数。由此便引出了内存分段,cs寄存器作为段基址它能表示的范围是从0到64k,(这里1k=1024),ip寄存器作为段内偏移,也是0到64k。假设现在段基址为0,ip作为段内偏移,那么这俩合起来能够表示从0x0~0x0FFFF这样一块64KB的内存空间。现在只要改变cs寄存器中的值也就是段基址,就能够灵活把1MB空间切成这样一块一块的64kB空间,这就是所谓的“分段”。且这种寻址方式十分灵活,不同的段基址加上段内偏移能够表示同一个内存空间。例如:cs=0x0200 ip=0x0345 和cs=0x0100 ip=0x1345都表示0x02345这一内存单元。
好了分段讲完了,读者需要明白,之后8086只要涉及到内寻址都是采用这种段基址偏移4位+段内偏移的方式进行的。说了这么多,现在我们得又绕回来,毕竟这一部分是为了讲解BIOS部分的却花了相当多的时间在说分段这件事。
现在我们知道了BIOS是处于内存何处—即1MB中的高64kB处,也了解了8086是如何进行内存寻址的—即分段寻址。那在开机的一瞬间,cpu的cs:ip 寄存器(reg)就被强制初始化为0xF000:0xFFF0,这两个数值按照上述分段计算方法即为0xFFFF0也就是BIOS的入口地址。CPU就从这里取出BIOS程序的相关指令进行执行就好了。
适配器部分:
即上述表中2到7项,其中因为小弟我经常接触到的是第5项,所以就拿它来举例子。(ps:主要是其他项没用过不敢乱说)
首先咱们的说一下啥是内存映射?对于cpu来说它认为只要是地址线能寻到的地方都是自己可使用的内存空间,并不关心物理上这些个内存空间位于何处。计算机上有很多硬件,有些硬件功能复杂在处理数据也是需要内存来存放处理的数据。还有些硬件需要使用内存保存外界的某些参数(比如显存),这些参数用来指出硬件要如何才能正确完成指定的任务。那么为了操作这些硬件自带的一些内存以正确的方式完成我们下达的任务,我们需要利用cpu去直接操作(读或者写操作)这样的一些内存。所以在地址线能表示的范围中划分出一部分空间去使用这些内存,这就是内存映射。
其次,如果说你认为访问某个ROM是先访问我们的内存条(DRAM),再由cpu进行某种工作将其映射到该ROM这就是错了。比如,从起始地址0xB8000~0xBFFFF,我们往0xB8000处的内存空间输出的字符会直接落到显存(内存的一种)中,显示器便会进行显示。究其原因我们的地址线只有20条,在我们的计算机中并不是只有内存条需要通过地址线上的地址访问,还有其他的外设比如显示适配器(显卡)中的内存部分同样要通过地址线进行访问,所以才会有上述的表格对地址线所能访问的空间进行划分。所以我们眼中的物理内存(内存条)无论有多大,实际上还是得看硬件的总线设计,是不是全部用于DRAM。对于8086来说内存条超过1MB多余的部分就没意义了,又因为空间的划分,即使是1MB大小的内存条,也只能使用其中640KB的空间。
好了,又解决一个概念,下面我们来说说如何使用这第5项内存空间来进行屏幕显示。哈哈,但是在具体说之前我没法直接讲,就只能先让读者你了解一下显示器具体的工作原理。
从IO接口说起:现在能够用计算机操作的硬件设备有很多,比如内存条、显示器、硬盘等等。这些硬件也被叫做外设,它们的实现原理和数据格式不尽相同,且无论它们的速度如何,在cpu看来都太慢了。所以为了统一cpu去跟这些硬件打交道的方式,在设计的逻辑结构上多加了一‘’层‘’—IO接口。IO接口的物理结构不限,可以是电路板可以是芯片或者插槽。它屏蔽了这些硬件的使用差异,协调cpu与硬件设备的沟通,其主要功能:1)设置数据缓冲,解决cpu与外设速度不匹配问题。2)设置电平信号转换电路,cpu使用TTL电平。3)设置数据转换格式 。4)时序控制,同步cpu和外设。5)提供地址译码,使cpu使用某个端口就能访问其地址总线(ps:总线这个词意义有些不明确计算机内多设备要实现向互通信)。同一时刻cpu与众多的IO接口中的一个进行数据交互,所以如何选择优先与哪个IO接口通信的工作交给了一个叫输入输出控制中心的东西来完成,它也叫做南桥芯片。虽然IO接口在物理实现上不限种类,但是它们在使用方式上是统一的。在IO接口的设计之初就被设计成使用寄存器与cpu通信,cpu与外设打交道具体上是通过IO接口提供的端口完成。这些端口虽然实际上就是寄存器,但是为了区别于cpu内部的寄存器,IO接口中的寄存器叫做端口(ps:这不是网络应用程序层面的端口,例如使用远程连接服务ssh会启动电脑的20端口,这与IO接口中的端口是两码事)。在8086所属的IA32指令结构下,操作这些端口有统一的指令格式:
in指令:用于从端口中读取数据
例子:
in al,dx
in ax,dx
ax/al:存储从端口获取的数据
dx:指端口
out指令:用于向端口中写入数据
例子:
out dx,al
out dx,ax
out 立即数,al
out 立即数,ax
ax/al:需要写入端口的数据
立即数/dx:指对应端口
IO接口说完了就来说说什么是适配器: 某些Io接口也叫适配器,是用于驱动某个外设的功能模块。例如显卡称为显示适配器,我们想要操作显示器没有其他办法只能使用显卡。其中,在实模式下,显卡提供的有显存—就是我上面提到的第5项,也是内存的一种,地址0xB800-0xBFFF是直接映射到显存的,向里面写入数据(数据遵循ASII规范)可以直接由显卡转移处理到显示屏上。其次,显卡给了我们的输入接口有两种:显存和端口,只是我一般使用的是显存,把显存作为接口,只需要把输入内容写在显存里,显卡就会帮我把它打印在显示器上。
可用区域:
一共有两部分,即0x7E000x9FBFF和0x5000x7BFF。这是实模式下预留给我们的可随意使用的安全空间。
MBR部分:
上述表中10项,什么是MBR?
之前说过电脑从开机进如BIOS进行开机自检,等BIOS检查硬盘和内存显卡等是否安装妥当之后,咱们cpu下一步又该执行啥命令呢?那你可能会说如果你电脑不是裸机安装的有windows或者linux操作系统的话那下一步应该就是去执行操作系统,把操作系统给跑起来然后就可打游戏刷视频了。如果读者你是这么想的那我能说你说对了一部分,因为这样的回答还不够详细,在进入操作系统之前我们需要使用一段加载程序来把硬盘中存储的操作系统内核程序加载到内存中正确的位置进行使用,这个加载器可以位于磁盘中的任意位置,MBR这段空间内包含着如何去找到并加载这个加载器的程序。因此,MBR也被称为引导程序。
一般来说,MBR按照约定应该位于磁盘的第一个扇区,它的任务就是加载loader—即加载器,最后再由loader加载操作系统到指定位置,然后执行加载过来的操作系统。MBR大小必须是512字节,这是为了占满硬盘0盘0道1扇区,且最后两个字节必须是0x55与0xaa。
在BIOS主要功能中,就有着寻找和加载MBR的任务。BIOS会校验启动盘(磁盘)中位于0盘0道1扇区(这是约定,其实就是0扇区,只不过按照CHS寻址方法用1开始编址)的内容,校验这里是不是放着主引导记录MBR,校验方法是检测这个扇区最后两个字节是不是0x55与0xaa。在此基础上,将该扇区内容加载至1MB内存空间中的0x7c00处,这个位置是由于历史遗留导致的兼容,由最初的操作系统本身所占内存大小与布局所决定。BIOS加载完毕后,然后跳转过去执行MBR中的程序。
所以电脑开机正常流程应该是:BIOS–>MBR—>loader—>OS(操作系统)
其他部分:
即我未解释的第8、12项。受限于本人现阶段的时间和精力尚未弄清楚其具体含义暂时不写,以后有机会在补充。将来吧反正。
实模式下寻址模式详解:
在上文中给读者讲述了“段基址+段内偏移”的寻址方式,在实模式下cpu访问数据将按照此规则进行。下面把8086的寻址模式给简单说一说(ps:这一段需要一点点的汇编基础,所以建议读者有空也去看看上面提到的《汇编语言》):
寻址模式从大方向上来看有三大类:
寄存器寻址:操作的操作数就在寄存器中,直接从寄存器中拿数据即可。
例如:
mov ax, 0x1234
mov dx,0x1
mul dx
解释一下:
mov ax,0x1234 是将ax中存储的数值修改为0x1234这个字面常量;
mov dx, 0x1同理;
mul dx 是默认将ax与dx中的值进行相乘。
其中,mul dx这一指令就是使用了寄存器寻址,它会从ax中去取出要操作的数值—0x1234。
立即数寻址:操作数以字面量的常数形式给出。
例如:mov ax,0x1234
内存寻址:内存寻址又分为:
直接寻址:在操作数中给出数字作为内存地址,汇编语言下通过中括号告诉cpu,取此地址中的值作为操作数。
例如:mov ax, [0x1234]
0x1234是段内偏移,默认的段基址保存在ds寄存器中,ds是除了cs外的另一个16位寄存器。这句指令在执行的时候,会从ds中取出段基址再加上中括号中偏移量0x1234,最后再根据得出的20位地址值去对应的内存单元中取出数据。
基址寻址:在操作数中用bx寄存器或bp寄存器作为地址起始。
例如:add word[bx], 0x1234
bx是一个16位的通用寄存器,它默认使用的段基址保存在ds中。ds寄存器和cs寄存器由于保存的是段基址所以也叫段寄存器,段寄存器除了这两个外还有ss和es。
这条命令会从ds中取出段基址再加上bx中保存的偏移量,最后再根据得出的20位地址值去把对应的内存单元的数据修改成0x1234。
变址寻址:与基址寻址类似,只是寄存器由bx、bp换成了si、di。这两个寄存器默认的段寄存器也是ds。
基址变址寻址:从名字上看是基址和变址的结合,即基址寄存器bx或bp再加上一个变址寄存器si或di。默认段寄存器是ds。
例如:mov [bx+di], ax
这条命令会从ds中取出段基址再把bx+di的和作为偏移量,最后相加,再根据得出的20位地址值去把对应的内存单元的数据修改成ax中的值。
总结
历史发展:
同样的在介绍保护模式前让我们先来了解点历史,保护模式这个概念是在Intel 80286首次出现的,因为早在8086中安全问题就已经凸显出来了。
在8086的工作模式—后面被称为实模式下,操作系统和用户程序处在同一级别下,这两者均可以随意的使用物理地址访问这1MB空间中任意的单元数据而不受限制。
其次,除了实模式下的安全问题外,随着cpu和内存的发展,资源的充分利用和管理也成为了一大难题:实模式下访问超过64KB就需要更换段基址,段基址切换容易犯迷糊;一次只能运行一个程序,无法充分利用cpu资源,比如cpu执行某一程序需要调用打印服务,cpu需要等打印结束后才能继续执行;实模式下仅有20条地址线最大可用内存只有1MB。
而80286出现解决了8086工作模式的一些弊端,比如,它的地址条数有24条,理论上可以访问16MB大小的内存空间。它可工作于两种方式,一种是实模式(8086工作模式),另外一种就是保护模式。但是由于其cpu和内部寄存器任然只有16位,在使用分段寻址中,单独的一个寄存器还是只能访问64kB的空间,想要访问完整的16MB空间仍需要不断变更段基址。所以80286虽然首次推出保护模式但是其并没有被广泛运用起来,所以这里只是提一嘴80286并不做详细展开,接下主要结合以目前成熟的32位cpu—即经典的80386来讲解什么是保护模式。
保护模式是相对于实模式来说的,它是为了解决在实模式下出现的问题而出现的。
保护模式下对于寄存器的拓展:
在80386中,无论是cpu、寄存器(但是注意,保护模式下段寄存器能使用的位数还是16位)还是地址总线都是32位的,其可用的内存空间达到了2的32次方—即4GB。
其次,保护模式之所以叫做保护模式,主要是提供了对于内存段的保护。
内存段概念介绍:
还记得我们之前说分段寻址吗?这里简单说一下,在实模式下对于内存的访问是利用段基址偏移4位+段偏移完成的,这样的做法相当于是将内存按照段基址作为起始地址,存放段内偏移量的寄存器或者内存单元所能表示的偏移大小划分成了一个又一个不同大小的空间。就比如说我们只使用段基址为0x0000,段内偏移规定只能使用0x0~0x0100这一部分,那么我们在内存中就划分出了一段256字节大小的空间,这个段空间就是广义上指的“内存段”。
但是对“内存段的保护”中说到“内存段”实际上指的是将可执行文件中由连接器整理好的代码段、数据段等二进制数据或指令组成的“编程意义上”的段加载到内存后所占用的那一部分内存空间。我们之后提到的内存段都是指这种根据可执行文件中的段被加载到内存中的所占用的空间。
接着说一下在可执行文件中的段(也就是上面说的“编程意义上”的段):实际上常见的编译型高级语言如c语言编写的程序,在生成可执行文件的过程中大体上分为预处理、编译、汇编和链接这4个阶段。预处理是是预处理器将高级语言中的宏展开,去掉代码注释,未调试器添加行号;编译是将预处理后的高级语言进行词法分析、语法分析、语义分析、优化,最后生成汇编代码;汇编是将汇编代码编译成二进制的目标文件;链接是将目标文件连接成最终的可执行文件。(单个目标文件如若不需要其他目标文件的内容,那么可以使用连接器手动指定程序加载的虚拟地址和程序执行的入口地址,这样生成的可执行文件能够借直接执行)。
在我们使用汇编语言编写的源码中通常用section或者segment伪指令来表示一段区域(主要是为了编写时代码的整洁),这个两个是编译器提供的汇编语言关键字,在语法中都表示为‘’节‘’,这里的segment只是关键字而非‘’段‘’。
汇编器根据语法,将源码中表示‘’节‘’语法的关键字section或者segment在目标文件中编译成‘‘节’。在后续的操作系统加载程序的时候并不关心编译后可执行文件中‘’节‘’的大小和数量,操作系统只关心‘’节‘’的属性,比如包含代码的‘’节‘’只能读和执行,包含数据的‘’节‘’可以进行读写。如果可执行文件需要多个目标文件交由链接器连接在一起后才能形成,那么链接器会会将这些不同目标文件中相同属性的‘’节‘’在可执行文件中合并成一个大的‘’节‘’,而最终形成的这个大的‘’节‘’随着可执行文件被加载进入内存后才是我们平常所说的内存段。
而这个内存段和上述的实模式下的内存段一样,也是可以用段基址+段内偏移进行访问,只不过保护模式下的内存段多了一些属性,段内偏移能表示的值只能在内存段大小的范围内进行增减。
既然80386已经有了32位寄存器,已经有能力直接寻址了,那为什么还要使用段基址+段内偏移这种看起来迂回的寻址方式?
那得先谈谈一件事:兼容。计算机无论如何发展,都有个向下兼容的老传统,这是最起码的要求。
所以80386只得在8086的分段寻址的基础上进行了拓展,这样做不仅兼容了以前的老代码—80386的32位的寄存器(不论是段寄存器还是通用寄存器)在实模式下其低16位也可以当做之前的16位寄存器来使用且使用规则与8086一致,而且也完成了保护模式下对于内存段的保护。其次,在开发上我们在代码中仍只操控着段寄存器和内存单元,这与实模式下的编程相差不大。
那80386是如何对内存段的保护呢?
想要弄明白保护模式下对于内存段的保护,我们可以从保护模式下的分段寻址又发生了那些变化了来一探究竟。
保护模式下的分段寻址中段内偏移地址还是和实模式下一样,只不过现在的偏移能够直接达到4GB—这意味着不需要再让段基址进行偏移直接和段内偏移地址相加即可。而段基址现在却不是一个简单的一个地址的事了,为了安全,必须的加一些约束条件。这些约束条件便是对内存段的描述信息。由于这些信息太大了所以使用了一个专门数据结构—全局描述符表(GDT)来进行保存。既然这是一张表,那么肯定会有表项,其中每一个表项称为段描述符—共64位,用来描述各个内存段的起始地址、内存段的大小(段界限)、内存段的权限等信息。
下面就来详细讲解一下段描述符和全局描述符表:
段描述符详解:
一个段描述符共64位,8个字节大小,用于描述内存段拥有的属性以便解决实模式下存在的问题。
首先实模式下用户程序可以破坏存储代码的内存区域,这是因为实模式下用户程序和操作系统是同级别的,所以可以添加个特权级别的属性来区分用户程序和操作系统的地位。其次,内存段是一片内存区域,访问内存需要段基址,所以内存段起始地址对应的段基址也需要有段基址属性。为了限制程序访问内存的范围,还要对内存段的大小进行约束,所以要有段界限属性。
最后,还有一些其他约束条件我之后会提到。我们先来看看段描述符的内存结构:
高32位,即高位4字节:
31~24 | 23 | 22 | 21 | 20 | 19~16 | 15 | 14~13 | 12 | 11~8 | 7~0 |
---|---|---|---|---|---|---|---|---|---|---|
段基址31~24 | G | D/B | L | AVL | 段界限19~16 | P | DPL | S | TYPE | 段基址23~16 |
低32位,即低位4字节:
31 16 | 15 0 |
---|---|
段基址15~0 | 段界限15~0 |
保护模式下地址总线宽度是32位,段基址也需要32位。段界限则表示段边界的扩展最值,即最大/小扩展到多少。对于数据段和代码段,段的扩展方向向上即地址越来越高,此时段界限表示以段基址为起始能够向上使用到的段内偏移的最大值。对于栈段,段的扩展方向向下即地址越来越低,此时段界限表示能使用到的段内偏移的最小值。这里不好理解直接上图:
栈的段界限是以栈段的基址为基准,并不是以栈底,因此栈的段界限一定是位于栈顶之下,又由于栈顶是从高地址扩展到地址值与段界限有个碰撞的的趋势,所以为了避免碰撞,将段界限+1视为栈可以访问的下限。段界限用20个二进制位来表示,只不过此段界限是个单位量,它的单位要么是字节—B,要么是4KB,这是由段描述符中的G位来决定的,最终的段的边界是此段界限数值*单位,故段的大小要么是1MB(2的20次方 x 1B)要么是4GB(2的20次方 x 4KB)。
下面我将从高4字节开始,从高到低逐位进行讲解段描述符:
段描述符第31~24位:段基址高8位
段描述符第23位G:1代表段界限单位为4kB;0代表段界限单位为1B。
段描述符22位D/B:如果是代码段则这位称为D位:1代表有效地址操作数是32位,使用32位寄存器eip;0代表16位使用eip低16位ip。如果是栈段则这位称为B位:1代表使用32位寄存器esp,操作数大小32位;0代表使用esp低16位sp,操作数大小16位。
段描述符第21位L:用来设置是否是64位代码段,1代表代码段是64位;0代表是32位。
段描述符第20位AVL:保留暂无意义。
段描述符第19~16位:段界限高四位。
段描述符第15位P:段是否存在,1代表段存在内存中;0代表不在,cpu进行检查,如果为0则抛出异常。
段描述符14~13位DPL:代表权级别,数字越小级别越高,os级别为0,应用程序为3。
段描述符第12位S:1代表该内存段是非系统段;0代表该内存段是系统段,配合type使用。
段描述符第11~8位:用于指定内存段的具体类型。
段描述符第7~0位:段基址中间8位。
段描述符低4字节:
全局描述符表详解:
全局描述符表是由上述的一个一个段描述符紧凑的排列在一起在内存中形成的一张表,想要使用这张表就需要知道该如何去找到这张表,这个步骤需要硬件的支持,在80386中有这样一个寄存器专门用来存放描述符表在内存中的位置,它名字叫做GDTR寄存器—即全局描述符表寄存器,是一个48位寄存器。
它的结构如下:
47 16 | 15 0 |
---|---|
GDT内存起始地址 | GDT界限 |
操作该寄存器的指令是:lgdt 48位内存数据 该命令用于修改gdt寄存器中的数值,指出当前全局描述符表所在的32位内存地址。
有了全局描述符表之后,段寄存器中的保存的值也不再是之前的段基址,而是叫做‘‘选择子’’(selector)。这个选择子实际上就一个索引,全局描述符表相当于一个数组,选择子是它的下标cpu会根据这个索引自动去寻找全局描述符表中对应的段描述符。选择子的位数结构长这样:
15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
index | … | … | index | TI | RPL_1 | RPL_0 |
RPL:PRL_0、RPL_1这两位表示请求特权级,0、1、2、3这四个特权级,数值越小特权级越高。
TI:表示当前索引的段描述符是在局部描述符表还是全局描述符表。
剩下的13位则是索引值,即2的13次方8192,这对应着全局描述符表的中的段描述符数量。全局描述符表总的大小为2的16次方,每个表项占用8个字节,所以共能使用8192个表项。另外提一嘴,全局描述符表是保存在内存中的,且一般是由loader在加载操作系统内核前进行构建的。又因为对于cpu来说访问内存的效率并不高,所以之后引入了寄存器用于缓存当前使用的内存段的段描述符,该寄存器叫做段描述符缓冲寄存器。其中80286是48位,386是64位。
在保护模式下,由于段寄存器不再保存段基址而是选择子,所以如果在代码中想要从cs=0x7 ip=0x1234代表的内存中取指令 ,那么就得先分析cs中0x7这个选择子,它的第0~1位是0,代表特权级为0级。第2位TI值是0,代表后面的索引是在全局描述符表中进行的。剩下的13位索引值是1,代表索引的是表中的第一项。cpu将利用gdt寄存器中的基址,从全局描述符表中的第一项段描述符处获取真正的段基址,得到段基址后需要注意现在已经是32位地址线和32位寄存器,所以不需要再将段基址进行偏移与段内偏移相加,直接用选择子对应的‘’段描述符中的段基址‘’加上‘’段内偏移‘’就是要访问的内存物理地址。
这样对内存的访问仍然使用了分段寻址,只不过从之前的直接访问物理地址对应的内存空间,变成了先访问全局描述符表再间接访问以段描述符中段基址为起始、段界限为大小的内存段中的内存。有了这样的一个中间操作,cpu就能完成对于内存段的保护。简单来说就是源代码经过编译、链接形成可执行文件后,如果说文件要被执行,那么该文件就会被操作系统加载到内存中去。操作系统负责即将可执行文件中划分好的‘‘段’‘’加载到合适的内存中,并填写对应的段描述符表。那么cpu在不断执行指令时,也会检查这条指令操作的内存空间的合法性。假如说有条指令是要修改代码内存段中的数据,那么这条指令是错误的,cpu会抛出异常。
下面来详细说说保护模式下如何利用段描述符实现内存段保护
总结