时间过的太快了,为了不让自己太闲,写些IA-32架构方面的内容贴上来,主要是参考了IA-32的系统编程指南,还有自己的一些感悟与总结,行了,进入正题吧。
从386处理器开始,IA-32(英特32位CPU,486、586、奔腾系列都属于IA-32)开始提供一系列很优秀的特性,用于更好的支持操作系统,这些特性会呈现在多种不同的操作模式中,如:实模式、保护模式、虚拟8086模式、系统管理模式。
Intel 64体系架构几乎支持了所有在IA-32中提供的系统编程能力,并且还将这些能力进一步扩展,形成了一种新的操作模式,称为IA-32e模式,它主要用来支持64位编程环境,IA-32e模式允许软件工作在两种子模式下:
- 64位模式(支持64位操作系统与64位应用程序)
- 兼容模式(允许大多数旧程序运行,同时也可以在64位模式时运行64位应用程序)
IA-32提供的一系列特性如下:
- 内存管理
- 软件模块保护
- 多任务
- 异常与中断处理
- 多处理器控制
- Cache管理
- 硬件资源与电源管理
- 调试与性能监视
把这些特性整明白,对很多技术点会有潜移默化的作用,无论是系统编程还是应用编程。
2.1 系统级架构概述
系统级架构囊括了一组寄存器、一些数据结构、还有一堆指令,用于支持各种基本的系统级操作,如内存管理、中断与异常处理、任务管理以及多处理器控制。如下图所示,IA-32的寄存器和各种表基本上都囊括在图中:
其实分析这两张图里的内容确实有点让人头胀,可这是重中之重,这些东西是做什么用的必须整明白才能理解其它的东西,我们这次主要分析系统级的内容。
2.1.1 全局描述符表 - GDT
看第一张图,里面有个GDT,即全局描述符表,是重中之重,在保护模式中,所有的内存访问都必须经过它,在这个GDT里有很多描述符项,其中有一种叫做段描述符,这个段描述符里面包含了极其重要的内容,有段基址、访问权限、访问类型等等。
要找到GDT里的某个段描述符,是通过一个称为段选择子的东西,这个段选择子里面有一项内容叫index,就是想要访问的段描述符在GDT里的索引位置,当然段选择子里还有全局/本地标志位、访问权限信息。
要访问一个段(代码段、数据段、堆栈段)当中的某个字节,需要提供一个逻辑地址,这个逻辑地址通常放在段寄存器里,逻辑地址包含了两个重要的内容,即段选择子与段内偏移量,通过段选择子,可以在GDT中找到要访问段的段描述符,段描述符里面有要访问段的基地址,然后用这个基地址加上逻辑地址里的段内偏移量,就可以定位到要访问的内存位置了。
通常访问中断处理例程的代码段(或数据段),直接走GDT里的段描述符,直接定位,无需现场保护,这样处理起来快。然而,要做任务切换,就得走GDT里的任务状态段描述符,先找到任务状态段,然后在找相关的代码段、数据段、堆栈段,为什么要多走一层任务状态段(TSS)呢?因为需要现场保护,先把当前任务的现场(即执行环境)保存到一个TSS里,然后再把新任务TSS里的内容加载到当前执行环境中(执行环境就是执行任务时要用到的各种寄存器的统称),当然还有各种权限检查等工作要做,因此知道为什么任务切换要多走一道TSS了吧。
2.1.2 系统段、段描述符和门
作为一个程序的执行环境,除了代码段、数据段、堆栈段,IA-32还定义了两个系统段,即任务状态段(TSS)和LDT(局部描述符表)。这两种系统段都各自拥有描述符,这里还要解释一个重要的概念,即GDT和LDT有个明显的区别,LDT是一种段,而GDT不是,因为GDT没有相关的段描述符来描述并指向它,而LDT有其相关的段描述符,并且保存着关于LDT的各种信息,而LDT的段描述符就保存在GDT中。
说的再明白一些,LDT、TSS、代码段、数据段、堆栈段,它们虽然有很多不同之处,但是统统都是‘段’,而GDT是一个全局的表,保存着所有用于描述这些段信息的段描述符,所以之前提到了GDT是重中之重,所有对这些段资源的访问,统统要经过GDT。
IA-32还定义了一组特殊的段描述符,叫做‘门’,其实也是保存了一些段信息,用于访问段资源,只是它具有一些很严格的权限检查功能,可以更加安全的访问关键代码,这些‘门’有四类,即中断门、调用门、陷阱门、任务门,下面举个例子来说明‘门’是如何工作的。
例如,当前程序要调用中断服务例程,考虑到安全的问题,我们不能直接通过GDT里的普通段描述符,直接定位到中断服务例程的代码段,而是先要通过中断门,然后再通过GDT里的段描述符找到中断服务例程的代码段,处理器会对此次访问做安全性检查,在当前程序的特权级别(CPL)与中断门以及目标代码段(要访问的中断服务例程代码段)的特权级别间做个比较,看看当前程序是否有足够权限访问中断服务例程,如果权限没问题,此次访问就顺利通过了中断门的安检,然后可以继续通过GDT中的段描述符来访问目标代码段了。
再例如,当前程序要运行一个系统调用,系统调用是操作系统内核向用户态提供的一些受特权保护的代码,它为用户态程序提供一些操作系统特有的服务,会使当前用户进程陷入内核。这可不能随随便便通过GDT的段描述符直接定位到系统调用程序的代码段,当然需要很严格的‘安检’。因此,我们需要首先通过‘调用门’的安全检查,首先要有一个调用门的‘段选择子’,通过此选择子可以从LDT中找到调用门,大家看看上面的第一张图就会发现调用门确实放在LDT中,至于调用门描述符为什么放在LDT中,而不放在GDT中,我们以后再说,这里不必较真,另外请记住,调用门是一类描述符,而不是段。处理器会通过调用门进行特权级别的检查,看看当前程序是否有权利访问目标代码段,如果安检通过,处理器就可以从调用门获得目标代码段的段选择子及段内偏移量了(调用门里保存着这些重要信息),那么后续的访问工作就顺理成章了。
2.1.3 任务状态段TSS、任务门
任务状态段包含了一个任务运行环境中的各种状态,像通用寄存器的状态、段寄存器、标志寄存器、指令指针寄存器、段选择子还有栈指针,可以说一个任务的执行环境信息全都包含在TSS中了。
IA-32中,有个寄存器叫做TR(图2-1),即任务寄存器,里面保存了当前执行任务的TSS的段选择子,通过这个选择子就可以在GDT里找到TSS的描述符,继而找到TSS。
当发生任务切换的时候,处理器就执行下列动作:
1、将当前任务的状态保存到当前任务的TSS中(传说中的现场保护)。
2、将新任务状态段的段选择子加载到TR寄存器中。
3、通过TR中的TSS段选择子在GDT中找到TSS的描述符,继而找到新任务的TSS。
4、将新任务状态段中的内容,加载到当前执行环境中,比如通用寄存器、段寄存器、指令指针寄存器、LDTR、CR3(页目录基地址)、标志寄存器。
5、开始运行新任务。
一个新任务的段资源(代码段、数据段、堆栈段)还可以通过任务门访问,任务门和调用门很像,只不过任务门要通过TSS来找到新任务的段资源,而调用门则可以通过LDT中的调用门描述符,直接定位到新任务的代码段与堆栈段,上面提到过。
2.1.4 中断与异常处理
外部中断、软中断以及异常的处理都是通过IDT来完成,IDT提供了一组门描述符,用于访问中断与异常的处理例程。与GDT一样,IDT也不是段,而且它的线性地址也保存在一个寄存器里,即IDTR。
IDT中主要有三类门描述符,即中断门、任务门、陷阱门,为了访问一个中断或异常的处理例程,处理器需要接收一个中断向量,它可以来自内部硬件、外部中断控制器,甚至软件指令(INT等),这个中断向量其实是以个索引,用于在IDT中定位门描述符。
如果要访问的处理例程,需要通过中断门或陷阱门,那么就可以从GDT或LDT中的段描述符直接定位到相关处理例程的段资源。如果要访问的处理例程需要通过任务门(例如一次任务调度),那么就需要先从GDT中的TSS描述符找到相关的任务状态段,然后在定位到处理例程的段资源(代码段、数据段、堆栈段)。
2.1.5 内存管理
IA-32提出了两种内存寻址方式,即物理寻址与虚拟内存(通过分页机制实现)。当采用物理寻址时,内存的线性地址被当做物理地址直接使用。采用分页机制实现虚拟内存时,所有的代码段、数据段、堆栈段、GDT、IDT等系统段资源,都可以用虚拟内存页的方式存储,只有最近经常访问的页面才会被换入物理内存。(当然像GDT这类关键的资源,通常总是放在物理内存中的)
物理内存中的页面,通常被包含在两类系统数据结构中,即页目录(PGD)与页表(PT),这两种数据结构资源会常驻于物理内存中。一个页目录项包含了一张页表的基地址、访问权限及内存管理信息,而页目录自己的物理地址则存放在一个控制寄存器中,即CR3。
为了使用页机制,线性地址必须被分割成三部分,每一个部分都分别代表着页目录内便宜量、页表内偏移量,以及页面内偏移量。
操作系统中可以只拥有一张页目录,也可以有多个,例如每个任务都可以拥有自己的页目录。
2.1.6 系统寄存器
为了初始化CPU,也为了更好的控制系统级操作,IA-32提供了一组系统寄存器,以及一个用来提供各种系统标志的标志寄存器(EFLAGS):
待续······