X86汇编语言-从实模式到保护模式—笔记(50)-第17章 中断和异常处理与抢占式多任(2)
【17.1.5 错误代码】有些异常产生时,处理器会在异常处理程序或中断任务的栈中压入一个错误代码。通常,这意味着异常和特定的段选择子或中断向量有关。
如图17-6所示,压入栈中的错误代码是32位的,但高16位不用。
EXT位的意思是,异常是由外部事件引发的(External Event)。此位置位时,表示异常是由NMI、硬件中断等引发的。
IDT位用于指示描述符的位置(Descriptor Location)。为“1”时,表示段选择子的索引部分(错误代码的位15—3)是指向中断描述符表(IDT)的;为“0”时,表示段选择子的索引部分指向GDT或者LDT。
TI位仅在IDT位是“0”的情况下才有意义。此位是“0”时,表示段选择子的索引部分指向GDT,否则,指向LDT。
段选择子的索引部分用于指示GDT/LDT内的段描述符,或者IDT内的门描述符,它就是我们平时所用的段选择子的高13位。
有时候,错误代码可能是全零(空),这表示异常的产生并非由于引用了一个特定的段。当然,也可能确实是在引用一个段的时候发生的,而且由于那个段的描述符是空描述符。所谓引用一个段,通常是执行了这样的指令:
mov ecx,0x0008
mov ds,ecx
注意,当通过iret/iretd指令从中断处理器程序返回时,处理器返回时,处理器并不会自动弹出错误代码。因此,对于那些有异常代码的异常处理过程来说,必须在执行iret/iretd指令前,先从栈中移去(或弹出)错误代码。否则,处理器执行iret/iretd指令时,加载(弹出)到CS和EIP中的返回地址就是错的。
对于外部异常(通过处理器引脚触发),以及用软中断int n引发的异常,处理器不会压入错误代码,即使它原本是一个有错误代码的异常。分配给外部中断的向量号在31—255之间,处于特殊的目的,外部的8259A或者I/OAPIC芯片可能会给出一个0-19的向量号,比如13(常规保护异常#GP),并希望进行异常处理。在这种情况下,处理器并不会像通常那样压入错误代码。同样地,用软中断指令int 0x0d有意引发的异常,也不会压入错误代码。
17.3内核的加载和初始化
【17.3.1 彻底终结多段模型】
平坦模型下也不是没有段,只是所有的段都很大,大到等于处理器所能寻址的全部空间。因此,在平坦模型下,至少要创建两个段描述符,一个是代码段,另一个是数据段,都是4GB。
主引导程序的加载位置是物理地址0x00007C00,进入保护模式之后,因为不再使用多段模型,所以,只能在平坦模式下使用基地址是0x00000000的代码段,为了继续执行程序,指令指针寄存器的初始内容必须是0x00007C00,并在此基础上随着指令的执行而增加。
程序第10行:SECTIONmbrvstart=0x00007c00
为了使程序在平坦模型下方便地引用内存地址,这里定义了段mbr,并要求段的虚拟地址从0x00007C00开始。
如果没有vstart子句,所有标号的地址都以程序开头为基准,从0开始计算;一旦加了该子句,当引用一个标号时,标号所代表的地址就以程序开头为基准,从给定的虚拟地址开始计算。
第199、200行声明了标号pgdt,并初始化了6字节,分别GDT的界限值和物理地址。为了在实模式下准备全局描述符表(GDT),第12-23行,从标号pgdt处取得GDT的物理地址,并计算它在实模式下的段地址和偏移地址。
在第13章里,GDT的物理地址是0x00007E00,现在是0x00008000,这正好是一个页的起始地址。这一变化和程序的运行无关,我只是觉得,将GDT安排在页的开头位置比较好,仅此而已。
第17行,从标号pgdt处取得GDT的物理地址时,使用了指令
mov eax, ;GDT的32位物理地址
当这条指令执行时,处理器工作在实模式下,段寄存器CS的内容为0x0000。如果没有那个vstart子句,pgdt的地址是0x7c00+pgdt。但是,因为有vstart子句,所以,标号pgdt的地址是从0x00007C00开始计算的,不用再加上0x7C00。
第27-32行,在GDT中安装最基本的两个段描述符,一个是4GB的代码段,另一个是4GB的数据段。段的基本地址都是0x00000000,段界限也都是0xFFFFF,粒度为4KB。当然,它们的属性不同。注意,没有为主引导程序创建单独的段描述符,稍后你就会看到,这实际上也不需要。
第35-47行,为进入保护模式做准备。首先是加载全局描述符表寄存器(GDTR);然后,打开第21根地址线A20;最后,设置控制寄存器CR0的PE位,进入保护模式。
进入保护模式后,按要求,要执行一条跳转指令,以清空流水线。第50行jmp dword 0x0008:flush,其中,标号flush是下一条指令在目标代码段内的32位偏移地址。在第13章里,段选择子是0x0010,所指示的段是“特制”的主引导程序,基地址为0x00007C00,段界限是0x1FF。在那时,flush所代表的偏移地址是相对于该段,从0开始计算,下一条指令的物理地址是0x00007C00+flush。
和第13章相比,在这里,目标代码段是4GB的段,基地址为0x00000000。由于使用了vstart子句,flush所代表的偏移地址是从0x00007C00开始计算的,下一条指令的物理地址是0x00000000+flush。
但是,实际上,在两种执行环境下,得到的结果是一样的。
第54-60行,令段寄存器DS、ES、FS、GS和SS都指向4GB数据段。只不过栈段是向下增长的,其他各段都向上增长。这样做,最直接的结果就是,从此再也看不到这样的指令了:
mov eax,0x0008
mov es,eax
原因是,因为所有段都是4GB的,用哪一个都无所谓。但是,这也带来了一个最大的好处,那就是不用再段之间换来换去,也不必记住所操作的数据位于哪个段。当它们都位于同一个4GB段时,很清爽。
第60行的指令定义了保护模式下的初始栈,给出了栈顶所在的位置,显然,该栈是从地址0x00007000开始向低地址方向扩展的。如图17-7所示,GDT位于物理地址0x00008000处;这里定义的初始栈从物理地址0x00007000开始向下推进,理论上,该地址以下的空间都可用于栈操作。另外还可以看出,由于当前正在执行的主引导程序并不在页的自然边界上,故,在它和GDT、栈之间出现了间隙(内存空洞)
第63-89行,从硬盘上加载系统内核,是从内存物理地址0x00040000开始加载的。这段代码和第14章一样,没有变化。
内核拥有自己独立的2GB内存空间,而且还要被映射到每个任务的高2GB,也就是从地址0x80000000开始的地方。为了完成这种映射,最简单的做法就是在内核代码中做点手脚,让所有对内存地址的引用(主要是对标号的引用)都从0x80000000开始。
内核代码(代码清单17-2)中只有一个段,数据和代码混合在一起,都在同一个段内,第25行是定义这个段的语句:、
SECTION core vstart=0x8004000
除此之外,不再有其他段定义的语句。你可能会问,内核的虚拟地址不是0x80000000吗?怎么会是一个奇怪的地址0x8004000呢?因为在前一章里,内核工作在多段模型下,内核代码和内核数据的重定位依赖于段的基地址。因此,在多段模型下,不管内核加载到内存中的任何位置都能正常运行。到了本章,内核工作在平坦模型下,不可能再一来段基地址实施重定位。为了使它能够正常工作,需要用vstart子句向编译器声明它的虚拟地址。如此一来,编译器就知道如何处理对标号地址的引用。不过,vstart子句中给出的虚拟地址必须和程序在运行时的虚拟地址相同。
如图17-8所示,内核占据着物理内存的低端1MB,其核心部分的加载地址是0x00040000.在开启页功能之后,内核需要将自己映射到从虚拟地址0x80000000开始的高端。这是一个完整的映射,很自然地,内核在高端的虚拟地址就是0x80040000了。
一旦知道了内核运行时的虚拟地址,那么,内核代码在编译时,vstart子句的虚拟地址也必须与之相同。只有这样,内核才能正常运行。
当然,我们知道,当内核运行时,不管是执行内核中的指令,还是访问内核中的数据,段部件所发出的线性地址都会高于0x8004000,正好位于每个任务虚拟地址空间的高端;而经页部件转换之后,得到的物理地址又变为原始的真实地址0x00040000。
回到正在执行的主引导程序。第95-105行,创建内核的页目录表和页表,并初始化必要的目录项和页表项。如图17-7所示,页目录表的物理地址是0x00020000,第98行,先令最后一个页目录项指向页目录表自己,即,该项所对应的页表就是当前页目录表,这是为修改页目录表而设的。接着,在页目录表内创建两个目录项,分别对应着两个不同的起始线性地址0x00000000和0x80000000。但是实际上,它们都指向同一个页表。其中,前一个目录项只在开启页功能的时候使用,作为临时过渡。
页表的物理地址是0x00021000,它的前256个页表项必须一一对应于物理内存最低端的256个页,这是内核能正常工作的基本要求。第108-118行,使用循环来建立这种一对一的映射关系。
第121-122行,将内核页目录表的物理地址传送到控制寄存器CR3,这是在开启页功能之前必须要做的事情。【这里为何没有将后面的256项清零呢?】
第125-128行,将全局描述符表(GDT)映射到虚拟内存的高端。这也是一一映射,GDT的新地址应当是线性地址0x80000000加上它原先的地址。
现在,内核已经从硬盘上加载完毕,页目录表和页表也已经创建。看样子一切都准备好了,第130-132行,开启分页功能。
现在已经工作在分页模式下,和从前不同的是,不需要重新加载段寄存器CS、SS、DS、ES、FS和GS以刷新它们的描述符高速缓存器,因为所有这些段都是4GB的。
关于在分页模式下,所有该做的工作都做了,但还是忽略了一个问题,那就是内核栈,应当将它映射到虚拟内存的高端。第137行,通过把栈指针寄存器ESP的内容在原来的基础上增加0x80000000,来做到这一点。
第139行,将控制转移到内核。注意,这是一个32位段内转移,而不是远转移(段间转移),在指令中没有使用关键字“far”。远转移在段间进行,需要16位的目标代码段选择子,以及32位段内偏移量。在平坦模型下,所有东西都在一个大的4GB段内,从一个地方转移到另一个位置去执行,自然是段内转移了。
事实上,这条指令有两个功能,一是转移到内核去执行,而是将处理器的执行流转移到虚拟内存的高端。内核已经从硬盘上加载,加载的位置是线性地址0x80040000.内核程序有一个头部,加载了内核的大小和入口点。参见代码清单17-2的第25-30行,内核程序内,偏移为0x00000004的地方,记载着内核要执行的第一条指令的偏移量,但没有段选择子。因此,当这条JMP指令执行时,处理器要先访问当前代码段,从线性地址0x80040004处取得一个32位的段内偏移量,传送到EIP寄存器。说时迟,那时快,内核就开始执行了。
页:
[1]