分类:
2006-06-13 21:54:21
保护模式基础 by 罗伯特.科林斯
I remember when I was first learning protected mode. I had barely taught myself assembly language, and I got this crazy idea that I wanted to teach myself protected mode. I went out and purchased an 80286 assembly language book that included some protected mode examples, and I was off to learn. Within a few hours, I realized that the book I had purchased didn't have any usable examples, since the examples in the book were intended to be programmed in EPROM CHIPS. So I hit the bulletin boards in search of something I could use as a guiding example.
回想我一开始学习保护模式的情景,那时我刚刚自学完汇编语言,但是却有了想自学保护模式这个疯狂的念头。我去买了一本包含一些保护模式例子的80286汇编语言的书籍,然后就开始学习。几个小时过后,我意识到,我买的这本书没有任何可以使用的例子,因为这些例子只在EPROM上编程时才用得上。于是我便开始到bulletin boards上寻找一些可以作为入门指导的例子。
The only example I found was so poorly documented and convoluted with task switching that even now, many years later, I haven't figured it out. So with my IBM Technical Reference Manual and my 80286 book, I sat down and tried to figure out protected mode. After spending forty hours in three days of trying, I finally copied some source code out of the IBM Technical Reference Manual, and I was able to enter protected mode and then return to DOS.
我找到的唯一的一个例子不仅缺少说明文档,而且充满了令人费解的任务切换代码,以至于时至今日我还没有完全把它理解。因此,我只能依靠IBM技术参考手册和那本80286汇编语言来研究保护模式。在3天里,进行了40个小时的尝试之后,我终于可以在模仿IBM技术参考手册里的一些源代码的情况下,自由的进入保护模式并且返回DOS。
Since that time, I have learned much about protected mode and how the CPU handles it internally. I discovered that the CPU has a set of hidden registers that are inaccessible to applications. I also learned how these registers get loaded, their role in memory management, and most importantly, their exact contents. Even though these registers are inaccessible, understanding the role they play in memory management can be applied to application's programming. Applying this knowledge to programming can result in applications that use less data, less code, and execute faster.
从那时以后,我学了很多关于保护模式和CPU内部如何实现保护模式的知识。我发现CPU拥有一组应用程序无法进入的隐藏寄存器。同时,我明白了这些寄存器如何被加载,他们在内存管理中的作用,以及最重要的一点,他们的内容。即使这些寄存器是不可进入的,但是理解他们在内存管理中的作用对编写应用程序可以起到帮助作用。将这些知识应用到编程中去可以使应用程序节省更多的数据,代码空间,以及提高运行效率。
From an applications' point of view, protected mode and real mode aren't that different. Both use memory segmentation, interrupts, and device drivers to handle the hardware. But there are subtle differences that make porting DOS applications to protected mode non-trivial. In real mode, memory segmentation is handled automatically through the use of an internal mechanism, in conjunction with segment registers. The contents of these segment registers form part of the physical address that the CPU presents on the address bus (). The physical address is generated by multiplying the segment register by 16, then adding a 16-bit offset. Using 16-bit offsets implicitly limits the CPU to 64k segment sizes. Some programmers have programmed around this 64k segment size limitation by incrementing the contents of the segment registers. Their programs can point to 64k segments in 16-byte increments. Any program using this technique in protected mode would generate an exception (CPU-generated interrupt) -- since segment registers aren't used in the same manner. In protected mode, memory segmentation is defined by a set of tables (called descriptor tables) and the segment registers contain pointers into these tables. Each table entry is 8-bytes wide; therefore the values contained in the segment registers are defined in multiples of 8 (08h, 10h, 18h, etc.). The lower three bits of the segment register are defined, but for simplicity's sake, let's say that any program that loads a segment register with a value that isn't a multiple of 8 will generate a protection error. There are two types of tables used to define memory segmentation: the Global Descriptor Table (GDT), and the Local Descriptor Table (LDT). The GDT contains segmentation information that all applications can access. The LDT contains segmentation information specific to a task or program. As previously mentioned, segment registers don't form part of the physical address in protected mode, but instead are used as pointers to table entries in the GDT or LDT (). Each time a segment register is loaded, the base address is fetched from the table entry and stored in an internal, programer-invisible, register called the "." The physical address presented on the CPU address bus is formed by adding the 16 or 32-bit offset to the base address in the descriptor cache.
从一个应用程序的角度来看,保护模式和实模式倒是没有什么区别。两者都使用内存分段技术,中断技术,以及设备驱动程序来使用硬件资源。但是在将DOS应用程序移植到保护模式下的时侯,就会看到它们微妙的差别。在实模式中,内存分段是在段寄存器的作用下,通过CPU的内部机制而被自动处理的。这些段寄存器的内容参与了物理地址的形成(见图表1a)。物理地址由段寄存器中的段值乘以16再加上16位的偏移生成的。使用16位的偏移,这就隐式的限定了段的长度为64K。面对这个限制,一些程序员在编写程序的时侯会增加段寄存器中的段值,这样,他们的程序就可以以每次增加16字节的方式指向所有的64K个段。任何使用了如上技巧的程序,在保护模式下会触发一个异常(例外)-- 因为,在保护模式下段寄存器不能以这样的方式使用。在保护模式中,内存段是由一组表(称作 描述符表 Descriptor Tables)来定义的,而段寄存器中保存的则是指向这些表的指针。每个表项是8字节宽,因此段寄存器中的值也都是8的倍数(08h, 10h, 18h等等)。段寄存器的低3位是有特殊意义的,但是出于简化的缘故,任何一个程序向段寄存器中装入的值不是8的倍数的话,将导致CPU产生一个保护异常(#PG)。有两种用来定义内存段的表:全局表述符表(GDT, Global Descriptor Table)和局部描述符表(LDT, Local Descriptor Table)。GDT包含了可供所有程序访问的段的信息。LDT则包含了某个特定任务或程序自己使用的段的信息。正如先前所说,保护模式下段寄存器不再直接参与物理地址的形成,而是被用来作为指向GDT和LDT中表项的指针(见图表1b)。每当段寄存器被加载之后,段基地址从表项中被提取出来然后存储到一个内部的,程序员不可见的寄存器—“段描述符缓冲寄存器”(Segment Descriptor Cache)中。出现在CPU地址总线上的物理地址,则是在描述符缓冲寄存器中的段基址上加上16位或者32位的偏移而形成的。
Another major concern for porting real-mode applications to protected mode is the use of interrupts. In real mode, double-word pointers to interrupt routines lie at physical address 0 ('386 specific: unless the IDTR has been changed). illustrates interrupt service addressing in real mode. When an interrupt is called or generated, the CPU looks up the address of the Interrupt Service Routine (ISR) in this interrupt vector table. After the CPU pushes the flags on the stack, it performs a far call to the address in the table. The information pushed on the stack is the same for software, hardware, or CPU generated interrupts.
在将实模式的应用程序向保护模式移植时需要注意的另外一个主要问题是中断的使用。在实模式中,指向中断处理程序的那些双字指针被放置在物理内存的0地址处(对于386而言, 除非IDTR被改变)。图表4a 表示了实模式下的中断处理的地址情况。
当一个中断产生或者被调用的时侯,CPU在中断向量表里寻找中断服务程序(ISR)的地址。当CPU将当前环境保存后,便执行一个由表中地址确定的远调用。无论是软件中断,硬件中断或者CPU产生的异常,CPU都会保存相同的当前环境信息。
In protected mode, the information pushed on the stack can vary, as can the base address of the interrupt vector table and the size of the interrupt table. The interrupt vector look up mechanism is also quite different from its real-mode counterpart. shows how interrupts are called from protected mode. After an interrupt is generated, the CPU compares the interrupt number (x8) against the size of the IDT -- stored in the interrupt descriptor cache register. If the INT# x 8 doesn't exceed the IDT size, then the interrupt is considered invokable, and the IDT base address is fetched from the descriptor cache; then the ISR's protected mode address is fetched from the IDT. The ISR's address is not a physical address but a protected mode, segmented address. Using the segment selector specified in the IDT, the CPU must perform the same limit-checking process again on the GDT to calculate the physical address of the ISR. Once the physical address is calculated, the CPU pushes the FLAGS, SEGMENT (selector), OFFSET, and possibly an ERROR CODE on the stack before branching to the ISR. ISRs for software and hardware interrupts needn't be any different from their real-mode counterparts, but ISRs to service CPU generated interrupts and faults must be different.
在保护模式中,那些被压入堆栈的信息会发生很大变化,同样,中断向量表的基址和长度也会有所改变。中断向量寻找机制也和实模式下的有很大的区别。图表4b描述了保护模式下中断是如何被调用的。
在产生一个中断之后,CPU将会把中断号(乘以8)和IDT的长度进行比较(IDT的长度存储在中断描述符缓冲寄存器中),如果中断号乘以8之后的值没有超过IDT的长度,则该中断被认为是可调用的,然后将IDT的基址从中断描述符缓冲寄存器中取出;ISR的保护模式帝制从IDT中取出。ISR的地址不是一个物理地址,而是一个保护模式的,分了段的地址。因为使用了在IDT中声明的段选择子,CPU必须进行一个和在GDT中相同的限制检测来计算ISR的物理地址。一旦物理地址被计算了出来,CPU便在跳转至ISR之前将当前环境的标志位,段选择子,偏移,也须还有一个Error Code压入堆栈中。软件和硬件中断的ISR不需要和他们在实模式下时有任何的区别,但是那些为了处理CPU中断和异常的ISR会有很大不同。
The CPU generates three categories of interrupts: traps, faults, and aborts. The stack image varies from category to category, as an error code may, or may not, be pushed on the stack. Traps never push an error code; faults usually do; and aborts always do. Traps are similar to and include software interrupts. This type of interrupt is appropriately named, as the CPU is "trapping" the occurrence of an event. The CPU doesn't know the event occurred until after the fact; thus it must trap the event before signalling the interrupt. Therefore, the return address of these ISR's point to instruction following the occurrence of the event. Traps include division by 0, data breakpoints, and INT03. Faults occur because something went wrong -- something that should be fixed. The CPU knows instantly that something is wrong and signals the interrupt-generating mechanism. The primary purpose of this type of ISR, is to correct the problem and restart the program exactly where it left off. For this reason, the return address of the ISR points to the faulting instruction -- thus making the fault restartable. Aborts are the most severe type of interrupt and are considered non-restartable. An error code is pushed on the stack, but will always be 0. The CPU's stack segment, and state machines, may be in an
indeterminate state, and attempting to restart an abort may cause unpredictable behavior. categorizes the list of interrupts generated by the CPU for protected mode. In most cases, the CPU will also generate the same interrupt in real mode, but no error code is ever pushed on the stack.
CPU可以产生3个种类的中断:陷阱(Traps),错误(Faults)和终止(Aborts)。每种中断的堆栈镜像都不相同,这是由于不是每个中断都会将Error Code压入堆栈。Traps从来不压入Error Code,Faults一般情况下会压入Error Code,而Aborts永远都将Error Code入栈。Traps与软件中断相似并且包含软件中断。这种中断如此命名的原因是因为CPU好像在诱捕(Trapping)一个事件的发生。CPU只有在某个事件发生后才会知道该事件的发生,因此它必须在发信号给中断之前诱捕到这个事件。这样一来,Traps的ISR的返回地址是指向产生Trap的指令之后的那条指令。Traps包括除0错误,数据断点,还有int 03。Faults的产生是因为运行时出现了某种错误 — 一种能被修正的错误,CPU立即知道发生了错误,然后用促使中断产生机制产生中断。此类ISR的主要的目的在于修正产生的错误之后,返回程序中断处恢复执行。因为这样,ISR的返回地址是指向产生Fault的那条指令 – 这样可以重新执行该指令以达到解决问题的目的。Aborts是中断里最严重的,并且是不可恢复执行的。产生Abort的时侯,系统会把0作为Error Code 压入堆栈中。CPU的堆栈段和状态机也许会变成不确定状态,并且任何一个恢复执行产生Abort程序的尝试都将导致不可预见的行为。表1对保护模式下CPU的中断或异常进行了分类。
大部分情况下,在实模式中CPU也会产生同样的中断或异常,但是却从来不会设置任何的Error Code。
I used to wonder why the BIOS can't be used in protected mode. At that time, I thought it would be easy to write mode-independent code: just don't do any FAR JUMPs, or FAR CALLS. But it's not as simple as following these conventions. In addition to avoiding the use of far jumps and calls, the ISR must remove any error code pushed on the stack. This is where the impossibilities begin. Since the error code is placed on the stack only in protected mode, we need to detect whether or not we are in protected mode before the error code is removed. To determine this, we need access to the machine status work (MSW), or the system register CR0. Accessing the MSW can be done in any priviledge level, but accessing CR0 can only be done at the highest privilege level -- level 0. If the user program is executing at any level less than 0, then we might not be able to access these registers. It can be done through the use of a special call gate that allows us to switch privilege levels before calling the ISR. This isn't needed if we use the SMSW instruction. But even with that problem solved, let's suppose the program left a real-mode value in any one of the segment registers. If the ISR pushes and subsequently pops any of these registers, the pop will cause the CPU to look for a selector in the GDT, or LDT. More than likely, using a real-mode value will cause a protection error. Therefore, using the BIOS in protected mode is nearly impossible. If there were a defined set of rules (a standard) that all programmers and operating systems followed, it could be done.
我曾经怀疑在保护模式下为何BIOS无法使用。那时,我认为写不依赖于模式的代码是很简单的事情:仅仅不去做任何的远跳转或者远调用,但是想做到这些并不那样的简单。除了要避免长跳转/调用之外,ISR必须要删除掉在堆栈中的Error Code。这也是调用BIOS的不可能性的开始,我们在删除Error Code之前必须能分辨出我们是否是在保护模式下,因为只有在保护模式下Error Code才会被压入堆栈。为了分辨这个,我们必须读取机器状态字,也就是系统寄存器CR0。可以在任何特权级下读取机器状态字,但是要读取CR0的话只能在最高特权级下,也就是Level 0。如果用户的程序运行在任何一个比0特权级低的特权级上,那么将无法访问这些寄存器。这个问题可以通过使用特殊的调用门来解决,调用门在调用ISR之前可以改变特权级。如果我们使用了SMSW(????)指令,这些就不是必要的了。但是即使这些问题得到了解决,程序依然有可能将实模式的地址留在段寄存器中,如果ISR压入,随后又弹出这些段寄存器,那么CPU将会在GDT或LDT中寻找对应的选择子。在保护模式下使用实模式的地址值将导致保护异常的产生。因此,在保护模式下使用BIOS中断几乎是不可能的。