系统软件开发系列文章之二:IA-32体系结构CPU保护模式和32位操作系统常见误区
(20100604随笔版,不保证完全的学术严谨)
1、工作在80386保护模式上的32位操作系统使用80386保护模式的硬件任务切换功能支持多任务。
错误!现代32位操作系统,例如Windows(这里指Windows NT,包括Windows 2000/XP及其后续版本)或者Linux,都是具有可移植性的操作系统,也就是说,操作系统的源程序,只需要修改与硬件相关的最底层部分(例如Windows中的硬件抽象层——HAL),即可重新编译以适应在不同CPU上运行。因此现代32位操作系统只能尽量使用各种不同32位CPU都能支持的通用功能,对于某些CPU特有的硬件支持功能尽量不用,以保证可移植性。
例如,大多数32位CPU只支持2个特权级,尽管80386支持Ring 0、Ring 1、Ring 2和Ring 3合计4个特权级,但工作在80386上的Windows和Linux都只使用2个特权级——Ring 0和Ring 3。
再例如,除了80386支持分段内存寻址和段保护之外,其它32位CPU很少支持分段内存寻址,因此工作在80386上的Windows,无论是在Ring 0还是在Ring 3上运行,其代码段、数据段和堆栈段的基址都是0,限长都是4GB——即所谓的“平坦(Flat)内存模式”,32位偏移量就等于线性地址,基本没有使用分段内存寻址,段保护也很少使用。Windows使用在32位CPU中支持较为广泛的分页寻址和页保护。
因此,只有80386支持的保护模式硬件任务切换功能(通过TR、TSS和任务门实现)通常是不会被现代32位操作系统使用的,Windows和Linux都通过软件实现任务切换。
尽管Windows不会使用80386保护模式的硬件任务切换功能,但是TSS(任务状态段)保存了当前任务(这里仅指80386硬件任务,与Windows进程/线程无关,实际上所有正常运行的Windows进程/线程,都对应同一个80386硬件任务)的某些重要信息,例如特权级改变时通常是一定要用到TSS的,所以Windows仍然至少要维护1个TSS。
这里给出一道思考题:
Windows中,工作于用户模式(Ring 3)的应用程序通常不能直接访问I/O端口,也就是说,IN/OUT指令以及C/C++中的_inp/_outp函数对于一般应用程序是无效的,但在使用一种名为“WinIO”的第三方开发库(工具包)的情况下,则可以使用IN/OUT指令以及_inp/_outp函数直接访问I/O端口。
已知WinIO的核心是一个文件名为winio.sys的设备驱动程序,调用了3个未公开的Windows内核功能调用:
Ke386SetIoAccessMap
Ke386QueryIoAccessMap
Ke386IoSetAccessProcess
试查阅相关资料,思考一下这3个内核功能调用与TSS中的哪一部分重要信息相关?
2、80386保护模式分页内存寻址中,页目录、页表和页是3种完全不同的4KB块。
不准确!实际上,无论页目录、页表还是页,它们的本质都是4KB的页,页目录项和页表项的数据格式实际上是完全相同的。这就意味着,一个4KB的页目录同时也可以作为一个页表使用,一个4KB的页目录或者页表也可以当作普通的页来访问。
启用80386保护模式分页内存寻址之后,CR3寄存器保存页目录的物理地址,因此页目录必须常驻物理内存,而页表和页则可以在需要时予以分配。
3、CR3寄存器、页目录项和页表项中存储的地址都是物理地址,但是启用80386保护模式分页内存寻址之后,程序员只能访问线性地址,如何知道页目录/页表自身对应的线性地址并维护页目录项/页表项呢?绕糊涂了……
不必糊涂,使用线性地址维护页目录项和页表项的方法非常简单。
如上所述,页目录也可以作为页表使用,页目录和页表又都可以作为普通页使用,那么可以在创建页目录时,将某一页目录项对应的物理地址设置为页目录自身的物理地址,也就是某一页目录项指向页目录自身。
例如,假设页目录的第2页目录项(页目录项索引为1)指向页目录自身,则以下线性地址(二进制,下同):
0000000001 0000000000 000000000000b
该线性地址对应第2页目录项指向的页表,但是第2页目录项指向的是页目录自身,因此页目录同时就作为页表使用,该线性地址又对应第1页表项指向的页,但是现在的页表就是页目录,因此该线性地址对应的是第1页目录项指向的“页”,即第1页表。最终,该线性地址指向第1页表(起始处),使用该线性地址即可直接访问第1页表。
以下线性地址:
0000000001 0000000001 000000000000b
该线性地址对应第2页目录项指向的页表,但是第2页目录项指向的是页目录自身,因此页目录同时就作为页表使用,该线性地址又对应第2页表项指向的页,但是现在的页表就是页目录,因此该线性地址对应的是第2页目录项指向的“页”——即页目录自身!最终,该线性地址指向页目录(起始处),使用该线性地址即可直接访问页目录。
Windows中,任何进程的页目录线性地址都是C0300000h,为什么呢?将十六进制转换成二进制,C0300000h地址对应的二进制地址是:
1100000000 1100000000 000000000000b
显然,只要将进程页目录的第769页目录项(页目录项索引为0300h,即1100000000b)指向进程页目录自身,则任何进程的页目录线性地址一定都是C0300000h,不管不同进程页目录的物理地址是否相同。
对于任何进程,还可以得出以下结论:
线性地址1100000000 0000000000 000000000000b=C0000000h一定指向进程第1页表;
线性地址1100000000 0000000001 000000000000b=C0001000h一定指向进程第2页表;
线性地址1100000000 0000000010 000000000000b=C0002000h一定指向进程第3页表;
……
这样一来,使用线性地址维护页目录项/页表项就变得非常简单了,如果需要添加新的页表或者页,只要通过线性地址修改对应页目录项/页表项,使其指向一块4KB空闲物理内存即可,然后可以立即通过对应这段物理内存的新线性地址访问之。操作系统通常需要维护一个数据结构,以标识4KB物理内存块的分配情况。
最后给出一道练习题:
在Windows下,使用内核调试器(SoftICE、WinDbg等)检查CR3寄存器的值,然后与索引为0300h的页目录项对应的物理地址,即线性地址C0300000h+0300h*04h处的DWORD(将低12位清空)相比较,看二者是否相同,为什么?