76 ;以下用简单的示例来帮助阐述32位保护模式下的堆栈操作 77 mov cx,00000000000_11_000B ;加载堆栈段选择子 78 mov ss,cx 79 mov esp,0x7c00
第77~79行用来初始保护模式下的栈。栈段描述符是GDT中第3个(从0开始数)描述符,这个描述符的线性基地址是0x0000_0000,段界限是0x0000_7a00,粒度是字节,B=1,属于可读可写、向下扩展的数据段。
我在博文数据段描述符和代码段描述符(一)——《x86汇编语言:从实模式到保护模式》读书笔记10中已经说过,
对于向上扩展的段(E=0),逻辑地址中的偏移值范围可以从0到(界限值*粒度);
对于向下扩展的段(E=1),逻辑地址中的偏移范围可以从(界限值*粒度)到0xFFFF(当B=0时)或者0xFFFF_FFFF(当B=1时)。
所以,对于描述符中的栈段,偏移范围是0x0000_7a00~0xffff_ffff. 仔细琢磨一下,这和我们的想法不是那么一致,因为代码79行,令ESP的初值是0x7c00,也就是说,我们本打算定义一个偏移范围是0x0000_7a00~0x0000_7c00的栈段。
因为线性基地址是0x0000_0000,也就是说描述符定义的栈段,实际可以访问的物理空间是0x0000_7a00~0xffff_ffff,但是我们却希望这个栈可以访问的物理空间是0x0000_7a00~0x0000_7c00。示意图(根据原书的图11-14改编而成)如下图所示。虽然这个栈不完美,但是不用担心,我们会在后面的学习中用更好的方法来创建栈。
隐式的栈操作(如push、pop、call、ret、iret等)涉及两个段,一个是指令所在的代码段,一个是栈段。之前的博文我们说过,对于可执行代码段,
D=1:默认是32位地址和32位或8位的操作数
D=0:默认是16位地址和16位或8位的操作数
注意:指令前缀0x66可以用来选择非默认值的操作数大小;前缀0x67可以用来选择非默认值的地址大小
对于栈段,
B=1:栈指针使用ESP
B=0:栈指针使用SP
就本文的实验代码,其代码段描述符的D位是1,其栈段描述符的B位也是1. 所以,当进行隐式的栈操作时,默认是32位操作数(比如压栈的时候,压入的是双字),且用ESP进行操作。
所以,下面的代码就用来验证这个事实。
81 mov ebp,esp ;保存堆栈指针 82 push byte '.' ;压入立即数(字节) 83 84 sub ebp,4 85 cmp ebp,esp ;判断压入立即数时,ESP是否减4 86 jnz ghalt 87 pop eax 88 mov [0x1e],al ;显示句点 89 90 ghalt: 91 hlt ;已经禁止中断,将不会被唤醒
在阅读这段代码的时候,我多少有点怀疑:书上说的到底是不是真的?我想通过实践来检验:探究一下PUSH指令在16位模式和32位模式下的执行规律。经过一番折腾,终于有了结果。请参考我的博文 16位模式/32位模式下PUSH指令探究——《x86汇编语言:从实模式到保护模式》读书笔记16
第81行,复制esp的值给ebp;第82行,压入一个字节(byte关键字不能省略);理论上,把ebp的值减去4后(第84行),应该和此时esp的值相等。为了证明这一点,第85行比较ebp和esp的值,如果不相等,就跳转到91行执行停机指令;如果相等,就把字符“.”显示在之前的字符串后面。
OK,第11章的内容已经学习完了。最后我们看一下代码的运行结果吧,结果就是在屏幕的左上角显示“Protect mode OK.”
(完)