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中已经说过,spa
对于向上扩展的段(E=0),逻辑地址中的偏移值范围能够从0到(界限值*粒度);.net
对于向下扩展的段(E=1),逻辑地址中的偏移范围能够从(界限值*粒度)到0xFFFF(当B=0时)或者0xFFFF_FFFF(当B=1时)。指针
因此,对于描述符中的栈段,偏移范围是0x0000_7a00~0xffff_ffff. 仔细琢磨一下,这和咱们的想法不是那么一致,由于代码79行,令ESP的初值是0x7c00,也就是说,咱们本打算定义一个偏移范围是0x0000_7a00~0x0000_7c00的栈段。code
由于线性基地址是0x0000_0000,也就是说描述符定义的栈段,实际能够访问的物理空间是0x0000_7a00~0xffff_ffff,可是咱们却但愿这个栈能够访问的物理空间是0x0000_7a00~0x0000_7c00。示意图(根据原书的图11-14改编而成)以下图所示。虽然这个栈不完美,可是不用担忧,咱们会在后面的学习中用更好的方法来建立栈。blog
隐式的栈操做(如push、pop、call、ret、iret等)涉及两个段,一个是指令所在的代码段,一个是栈段。以前的博文咱们说过,对于可执行代码段,字符串
D=1:默认是32位地址和32位或8位的操做数get
D=0:默认是16位地址和16位或8位的操做数it
注意:指令前缀0x66能够用来选择非默认值的操做数大小;前缀0x67能够用来选择非默认值的地址大小class
对于栈段,
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.”
(完)