正如3.2节提到到的那样,保护模式中,你可以使用32位的线性地址,这意味着直接访问4GB的内存。由于这个原因,选择器不用像实模式中段寄存器那样频繁地修改。顺便提一句,这份教程中所说的保护模式指的是386以上的保护模式,或者,Microsoft通常称为“增强模式”的那种。
在为选择器装入数值的时候一定要非常小心。错误的数值往往会导致无效页面错误(在Windows中经常出现:)。同时,也不要忘记你的地址是32位的,这也是保护模式的主要优势之一。
现在假设存在一个描述符描述从物理的0:0开始的全部内存,并已经加载进DS(数据选择器),则我们可以通过下面的程序来操作VGA的VRAM:
mov edi,0a0000h mov byte ptr [edi],0fh | ; VGA显存的偏移量 ; 将第一字节改为0fh |
很明显,这比实模式下的程序
mov ax,0a000h mov ds,ax mov di,0 mov [di],0fh | ; AX -> VGA段地址 ; 将AX值载入DS ; DI清零 ; 修改第一字节 |
看上去要舒服一些。
到目前为止,您已经了解了基本的寄存器以及内存的操作知识。事实上,您现在已经可以写出很多的底层数据处理程序了。
下面我来说说堆栈。堆栈实在不是一个让人陌生的数据结构,它是一个先进后出(FILO)的线性表,能够帮助你完成很多很好的工作。
先进后出(FILO)是这样一个概念:最后放进表中
的数据在取出时最先出来。先进后出(FILO)和先
进先出(FIFO, 和先进后出的规则相反),以及随
机存取是最主要的三种存储器访问方式。
对于堆栈而言,最后放入的数据在取出时最先出
现。对于子程序调用,特别是递归调用来说,这
是一个非常有用的特性。
一个铁杆的汇编语言程序员有时会发现系统提供的寄存器不够。很显然,你可以使用普通的内存操作来完成这个工作,就像C/C++中所做的那样。
没错,没错,可是,如果数据段(数据选择器)以及偏移量发生变化怎么办?更进一步,如果希望保存某些在这种操作中可能受到影响的寄存器的时候怎么办?确实,你可以把他们也存到自己的那片内存中,自己实现堆栈。
太麻烦了……
既然系统提供了堆栈,并且性能比自己写一份更好,那么为什么不直接加以利用呢?
系统堆栈不仅仅是一段内存。由于CPU对它实施管理,因此你不需要考虑堆栈指针的修正问题。可以把寄存器内容,甚至一个立即数直接放到堆栈里,并在需要的时候将其取出。同时,系统并不要求取出的数据仍然回到原来的位置。
除了显式地操作堆栈(使用PUSH和POP指令)之外,很多指令也需要使用堆栈,如INT、CALL、LEAVE、RET、RETF、IRET等等。配对使用上述指令并不会造成什么问题,然而,如果你打算使用LEAVE、RET、RETF、IRET这样的指令实现跳转(比JMP更为麻烦,然而有时,例如在加密软件中,或者需要修改调用者状态时,这是必要的)的话,那么我的建议是,先搞清楚它们做的到底是什么,并且,精确地了解自己要做什么。
正如前面所说的,有两个显式地操作堆栈的指令:
助记符 | 功能 |
PUSH | 将操作数存入堆栈,同时修正堆栈指针 |
POP | 将栈顶内容取出并存到目的操作数中,同时修正堆栈指针 |
我们现在来看看