16位模式/32位模式下PUSH指令探究——《x86汇编语言:从实模式到保护模式》读书笔记16

来源:互联网 发布:手机订单软件 编辑:程序博客网 时间:2024/06/05 05:46

一、Intel 32 位处理器的工作模式

New0004ia-32工作模式

如上图所示,Intel 32 位处理器有3种工作模式。

(1)实模式:工作方式相当于一个8086

(2)保护模式:提供支持多任务环境的工作方式,建立保护机制

(3)虚拟8086模式:这种方式可以使用户在保护模式下运行8086程序(比如cmd打开的console窗口,就是工作在虚拟8086模式)

有几点需要特别说明:

(1)保护模式可分为16位和32位的,由段描述符中的D标志指明。对于32位代码段和数据段,这个标志总是设为1;对于16位代码和数据段,这个标志被设置为0.

D=1:默认使用32位地址和32位或8位的操作数。

D=0:默认使用16位地址和16位或8位的操作数。(主要是为了能够在32位处理器上运行16位保护模式的程序)

指令前缀0x66用来选择非默认值得操作数大小,0x67用来选择非默认值的地址大小。

(2)在实模式下,也可以使用32位的寄存器,比如

mov eax,ecxmov ebx,0x12345678

(3)在书中,把实模式和16位的保护模式统称为“16位模式”;把32位保护模式称为“32位模式”。我的博文也沿用这种叫法。

(4)32位处理器可以执行16位的程序,包括实模式和16位保护模式。

(5)当处理器在16位模式下运行时,可以使用32位的寄存器,执行32位运算。

(6)在16位模式下,数据的大小是8位或者16位的;控制转移和内存访问时,偏移量也是16位的。

(7)32位保护模式兼容80286的16位保护模式。

(8)在16位模式下,处理器把所有指令都看成是16位的。

结合(5)和(8),我们发现一个问题:当处理器运行16位模式下,既然把所有指令都看成16位的,那么怎么使用32位的寄存器,执行32位的运算呢?答案是利用指令前缀0x66和0x67.前面已经说过,指令前缀0x66用来选择非默认值的操作数大小,0x67用来选择非默认值的地址大小。

比如说,指令码0x40在16位模式下对应的指令是

inc ax

如果加上前缀0x66,也就是指令码66 40,当处理器在16位模式下运行,66 40对应的指令是

inc eax

同理,如果处理器运行在32位模式下,处理器认为指令是32位的,如果加了0x66,那么就表示指令的操作数是16位的。

在编写程序的时候,我们应该考虑指令的运行环境。为了指令默认的运行环境,NASM提供了伪指令bits,用于指明其后的指令是被编译成16位的还是32位的。比如:

[bits 16] mov cx,dx      ;89 D1 mov eax,ebx    ;66 89 D8  [bits 32] mov cx,dx      ;66 89 D1 mov eax,ebx    ;89 D8

注意,[bits 16]和[bits 32]的方括号是可以省略的。

二、PUSH指令探究

由于32位的处理器都拥有32位的寄存器和算术逻辑部件,而且同内存芯片之间的数据通路至少是32位的,因此,所有以寄存器或者内存单元为操作数的指令都被扩充,以适应32位的算术逻辑操作。而且,这些扩展的操作即使是在16位模式下(实模式和16位保护模式)也是可用的。

我在博文 32位x86处理器编程导入——《x86汇编语言:从实模式到保护模式》读书笔记08 中已经总结了一般指令的扩展,在这里,我仅对PUSH指令进行实验和总结。

实验目的就是测试在3种模式下,PUSH指令的工作行为(比如SP或ESP到底怎么变化,压入的数到底是多少)。所以,我列了一个单子,把所有能想到的形式都列出来了,其中有的我也不确定(或许这样写编译都会报错)。不管那么多,先写出来,然后让编译器筛选吧眨眼

1    ;测试各种push23    ;操作数是立即数,分为一字节、两字节、四字节4        push 0x805        push byte 0x80 67        push 0x80008        push word 0x80009        10       push 0x8765432111       push dword 0x876543211213    ;操作数是寄存器,分为16位寄存器和32位寄存器    14       mov eax,0x8642135715       push ax16       push eax1718    ;操作数是内存单元,分为一字节、两字节、四字节         19       push [data]20       push byte [data]21       push word [data]22       push dword [data]

是不是有的写法明显就不对呢?

首先,第20行,肯定不对。因为如果是内存操作数的话,不能用byte修饰。剩下来的错误,我会在后文揭晓答案。

1.在实模式下的实验

(1)实验代码

1             ;PUSH 指令实验2             3             jmp near start4        5     data db 0x12,0x34,0x56,0x786     message db 'Hello,PUSH!'7            8     start:9             mov ax,0x7c0           ;设置数据段的段基地址 10             mov ds,ax11    12             mov ax,0xb800          ;设置附加段基址到显示缓冲区13             mov es,ax14    15             ;以下显示字符串 16             mov si,message          17             mov di,018             mov cx,start-message19         @g:20             mov al,[si]21             mov [es:di],al22             inc di23             mov byte [es:di],0x0224             inc di25             inc si26             loop @g27    28             ;测试各种push29             push 0x8030             push byte 0x8031             32             push 0x800033             push word 0x800034             35             push 0x8765432136             push dword 0x8765432137             38             mov eax,0x8642135739             push ax40             push eax41             42             ;push [data]43             push word [data]44             push dword [data]45             46             push ds47             push gs48           49             jmp near $ 50           51    52    times 510-($-$$) db 053                     db 0x55,0xaa

这段代码不是用的配书代码,是我自己写的。

第5行,定义了4字节的数据,这是为了后面验证“push + 内存操作数”这一情况。

第6行,定义了一个字符串,要把它显示在屏幕上。这样做是为了调试方便,让我们知道我们的程序已经RUN了。

第29行到47行,测试各种push,我会利用Bochs的调试功能,跟踪每条Push的执行情况,把结果总结出来。

好的,我们开始编译吧。

对于30行,有个警告:

push byte 0x80 ;warning: signed byte value exceeds bounds
既然是警告,那么30行不必去掉。相反我们更加好奇了,看看执行时会发生什么。

对于35行,还是一个警告:

push 0x87654321 ;warning: word data exceeds bounds

对于42行,呵呵,就是一个错误了。

push [data]  ; error: operation size not specified

好吧,看来这样不指定操作数的大小是不行的,所以我们把42行注释掉。

然后再编译,好的,可以了。

调试的过程就是不断用n命令,反复用print-stack命令,还有reg命令等,仔细观察栈的变化和SP的变化。(此处省略2000字)

(2)实验报告

小二,上实验报告!

New0005实模式_push

通过上面的实验,我们可以知道,如果CPU运行在实模式,如果用NASM编译,push指令可以这么用:

New0006PUSH_指令用法总结(实模式)

2.在16位保护模式下的实验

(1)关于16位保护模式

请参考我的博文 关于80286——《x86汇编语言:从实模式到保护模式》读书笔记15

(2)实验代码

实验代码由配书代码(代码清单11-1 (文件名:c11_mbr.asm))修改而成。目的就是我们要从实模式进入16位的保护模式,然后测试16位保护模式下PUSH指令的行为。

1         ;test push (16位保护模式下)23         ;设置堆栈段和栈指针 4         mov ax,cs      5         mov ss,ax6         mov sp,0x7c007      8         ;计算GDT所在的逻辑段地址 9          mov ax,[cs:gdt_base+0x7c00]        ;低16位 10         mov dx,[cs:gdt_base+0x7c00+0x02]   ;高16位 11         mov bx,16        12         div bx            13         mov ds,ax                          ;令DS指向该段以进行操作14         mov bx,dx                          ;段内起始偏移地址 15      16         ;创建0#描述符,它是空描述符,这是处理器的要求17         mov dword [bx+0x00],0x0018         mov dword [bx+0x04],0x00  1920         ;创建#1描述符,保护模式下的代码段描述符21         mov dword [bx+0x08],0x7c0001ff     22         mov dword [bx+0x0c],0x00009800     2324         ;创建#2描述符,保护模式下的数据段描述符(文本模式下的显示缓冲区) 25         mov dword [bx+0x10],0x8000ffff     26         mov dword [bx+0x14],0x0000920b     2728         ;创建#3描述符,保护模式下的堆栈段描述符29         mov dword [bx+0x18],0x00007a0030         mov dword [bx+0x1c],0x000096003132         ;初始化描述符表寄存器GDTR33         mov word [cs: gdt_size+0x7c00],31  ;描述符表的界限(总字节数减一)   34                                             35         lgdt [cs: gdt_size+0x7c00]36      37         in al,0x92                         ;南桥芯片内的端口 38         or al,0000_0010B39         out 0x92,al                        ;打开A204041         cli                                ;保护模式下中断机制尚未建立,应 42                                            ;禁止中断 43         mov eax,cr044         or eax,145         mov cr0,eax                        ;设置PE位46      47         ;以下进入保护模式... ...48         jmp  0x0008:flush                  ;描述符选择子:16位偏移49                                            ;清流水线并串行化处理器 50          5152    flush:53         mov cx,00000000000_10_000B         ;加载数据段选择子(0x10)54         mov ds,cx5556         ;以下在屏幕上显示"ABCDEFGHIJK" 57         mov byte [0x00],'A'  58         mov byte [0x02],'B'59         mov byte [0x04],'C'60         mov byte [0x06],'D'61         mov byte [0x08],'E'62         mov byte [0x0a],'F'63         mov byte [0x0c],'G'64         mov byte [0x0e],'H'65         mov byte [0x10],'I'66         mov byte [0x12],'J'67         mov byte [0x14],'K'68         6970         ;测试push71         mov cx,00000000000_11_000B         ;加载堆栈段选择子72         mov ss,cx73         mov sp,0x7c007475         76           push 0x8077           push byte 0x80  ; warning: signed byte value exceeds bounds78           79           push 0x800080           push word 0x800081           82           push 0x8765432183           ;warning: word data exceeds bounds84           push dword 0x8765432185         86           87           mov eax,0x8642135788           push ax89           push eax90           91           ;push [0x00]error: operation size not specified92           push byte [0x00]93           push word [0x00]94         95           push dword [0x00]96           97           push ds98           push gs99           push es100          push cs101      102  ghalt:     103         hlt                                ;已经禁止中断,将不会被唤醒 104105;-------------------------------------------------------------------------------106     107         gdt_size         dw 0108         gdt_base         dd 0x00007e00     ;GDT的物理地址 109                             110         times 510-($-$$) db 0111                          db 0x55,0xaa

对比32位保护模式的代码,就会发现16位保护模式的代码略有不同。

首先,比如说22行,段描述符的定义是

22         mov dword [bx+0x0c],0x00009800

因为80286中,段描述符的格式是

New000780286段寄存器

所以,高4字节的16~32位全部为0.

其次,

47         ;以下进入保护模式... ...48         jmp  0x0008:flush                  ;描述符选择子:16位偏移49                                            ;清流水线并串行化处理器

这里,没有加伪指令[bits 32],而且,偏移flush没有用dword修饰。因为操作数和偏移是16位的。

好了,代码就说到这里,我们看实验报告吧。

(3)实验报告

New000816位保护模式下push

通过和实模式的对比,可以发现,除了9、10两行中的指令码的偏移不一样(这和数据存放的位置有关系,和PUSH没有关系),PUSH指令的行为是惊人的相同。所以我们可以得出结论,16位保护模式下,PUSH的用法和实模式是一样的。我想,这也是在原书中,作者把实模式和16位的保护模式统称为“16位模式”,把32位保护模式称为“32位模式”的原因吧。

3.在32位保护模式下的实验

(1)实验代码

实验代码由配书代码(代码清单11-1 (文件名:c11_mbr.asm))修改而成。

1         ;test push (32位保护模式)23         ;设置堆栈段和栈指针 4         mov ax,cs      5         mov ss,ax6         mov sp,0x7c007      8         ;计算GDT所在的逻辑段地址 9          mov ax,[cs:gdt_base+0x7c00]        ;低16位 10         mov dx,[cs:gdt_base+0x7c00+0x02]   ;高16位 11         mov bx,16        12         div bx            13         mov ds,ax                          ;令DS指向该段以进行操作14         mov bx,dx                          ;段内起始偏移地址 15      16         ;创建0#描述符,它是空描述符,这是处理器的要求17         mov dword [bx+0x00],0x0018         mov dword [bx+0x04],0x00  1920         ;创建#1描述符,保护模式下的代码段描述符21         mov dword [bx+0x08],0x7c0001ff     22         mov dword [bx+0x0c],0x00409800     2324         ;创建#2描述符,保护模式下的数据段描述符(文本模式下的显示缓冲区) 25         mov dword [bx+0x10],0x8000ffff     26         mov dword [bx+0x14],0x0040920b     2728         ;创建#3描述符,保护模式下的堆栈段描述符29         mov dword [bx+0x18],0x00007a0030         mov dword [bx+0x1c],0x004096003132         ;初始化描述符表寄存器GDTR33         mov word [cs: gdt_size+0x7c00],31  ;描述符表的界限(总字节数减一)   34                                             35         lgdt [cs: gdt_size+0x7c00]36      37         in al,0x92                         ;南桥芯片内的端口 38         or al,0000_0010B39         out 0x92,al                        ;打开A204041         cli                                ;保护模式下中断机制尚未建立,应 42                                            ;禁止中断 43         mov eax,cr044         or eax,145         mov cr0,eax                        ;设置PE位46      47         ;以下进入保护模式... ...48         jmp dword 0x0008:flush             ;16位的描述符选择子:32位偏移49                                            ;清流水线并串行化处理器 50         [bits 32] 5152    flush:53         mov cx,00000000000_10_000B         ;加载数据段选择子(0x10)54         mov ds,cx5556         ;以下在屏幕上显示"ABCDEFGHIJK" 57         mov byte [0x00],'A'  58         mov byte [0x02],'B'59         mov byte [0x04],'C'60         mov byte [0x06],'D'61         mov byte [0x08],'E'62         mov byte [0x0a],'F'63         mov byte [0x0c],'G'64         mov byte [0x0e],'H'65         mov byte [0x10],'I'66         mov byte [0x12],'J'67         mov byte [0x14],'K'68         6970         ;测试push71         mov cx,00000000000_11_000B         ;加载堆栈段选择子72         mov ss,cx73         mov esp,0x7c007475         76           push 0x8077           push byte 0x80 ;warning: signed byte value exceeds bounds78           79           push 0x800080           push word 0x800081           82           push 0x8765432183           push dword 0x8765432184           85           mov eax,0x8642135786           push ax87           push eax88           89           90           push word [0x00]91           push dword [0x00]92           93           push ds94           push gs95           push es96           push cs97      98  ghalt:     99         hlt                                ;已经禁止中断,将不会被唤醒 100101;-------------------------------------------------------------------------------102     103         gdt_size         dw 0104         gdt_base         dd 0x00007e00     ;GDT的物理地址 105                             106         times 510-($-$$) db 0107                          db 0x55,0xaa

如果对上面的代码不熟悉的话,可以参考我的博文 进入保护模式(一)——《x86汇编语言:从实模式到保护模式》读书笔记12 等文章。

(2)实验报告

New001032_push

根据测试报告,我们可以归纳出32位保护模式下,针对NASM编译器的push指令用法:

push用法总结(32)

 

(完)

1 0
原创粉丝点击