16位模式/32位模式下PUSH指令探究——《x86汇编语言:从实模式到保护模式》读书笔记16
一、Intel 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,ecx mov 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 ;测试各种push 2 3 ;操作数是立即数,分为一字节、两字节、四字节 4 push 0x80 5 push byte 0x80 6 7 push 0x8000 8 push word 0x8000 9 10 push 0x87654321 11 push dword 0x87654321 12 13 ;操作数是寄存器,分为16位寄存器和32位寄存器 14 mov eax,0x86421357 15 push ax 16 push eax 17 18 ;操作数是内存单元,分为一字节、两字节、四字节 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 start 4 5 data db 0x12,0x34,0x56,0x78 6 message db 'Hello,PUSH!' 7 8 start: 9 mov ax,0x7c0 ;设置数据段的段基地址 10 mov ds,ax 11 12 mov ax,0xb800 ;设置附加段基址到显示缓冲区 13 mov es,ax 14 15 ;以下显示字符串 16 mov si,message 17 mov di,0 18 mov cx,start-message 19 @g: 20 mov al,[si] 21 mov [es:di],al 22 inc di 23 mov byte [es:di],0x02 24 inc di 25 inc si 26 loop @g 27 28 ;测试各种push 29 push 0x80 30 push byte 0x80 31 32 push 0x8000 33 push word 0x8000 34 35 push 0x87654321 36 push dword 0x87654321 37 38 mov eax,0x86421357 39 push ax 40 push eax 41 42 ;push [data] 43 push word [data] 44 push dword [data] 45 46 push ds 47 push gs 48 49 jmp near $ 50 51 52 times 510-($-$$) db 0 53 db 0x55,0xaa
这段代码不是用的配书代码,是我自己写的。
第5行,定义了4字节的数据,这是为了后面验证“push + 内存操作数”这一情况。
第6行,定义了一个字符串,要把它显示在屏幕上。这样做是为了调试方便,让我们知道我们的程序已经RUN了。
第29行到47行,测试各种push,我会利用Bochs的调试功能,跟踪每条Push的执行情况,把结果总结出来。
好的,我们开始编译吧。
对于30行,有个警告:
push byte 0x80 ;warning: signed byte value exceeds bounds
对于35行,还是一个警告:
push 0x87654321 ;warning: word data exceeds bounds
对于42行,呵呵,就是一个错误了。
push [data] ; error: operation size not specified
好吧,看来这样不指定操作数的大小是不行的,所以我们把42行注释掉。
然后再编译,好的,可以了。
调试的过程就是不断用n命令,反复用print-stack命令,还有reg命令等,仔细观察栈的变化和SP的变化。(此处省略2000字)
(2)实验报告
小二,上实验报告!
通过上面的实验,我们可以知道,如果CPU运行在实模式,如果用NASM编译,push指令可以这么用:
2.在16位保护模式下的实验
(1)关于16位保护模式
请参考我的博文 关于80286——《x86汇编语言:从实模式到保护模式》读书笔记15
(2)实验代码
实验代码由配书代码(代码清单11-1 (文件名:c11_mbr.asm))修改而成。目的就是我们要从实模式进入16位的保护模式,然后测试16位保护模式下PUSH指令的行为。
1 ;test push (16位保护模式下) 2 3 ;设置堆栈段和栈指针 4 mov ax,cs 5 mov ss,ax 6 mov sp,0x7c00 7 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],0x00 18 mov dword [bx+0x04],0x00 19 20 ;创建#1描述符,保护模式下的代码段描述符 21 mov dword [bx+0x08],0x7c0001ff 22 mov dword [bx+0x0c],0x00009800 23 24 ;创建#2描述符,保护模式下的数据段描述符(文本模式下的显示缓冲区) 25 mov dword [bx+0x10],0x8000ffff 26 mov dword [bx+0x14],0x0000920b 27 28 ;创建#3描述符,保护模式下的堆栈段描述符 29 mov dword [bx+0x18],0x00007a00 30 mov dword [bx+0x1c],0x00009600 31 32 ;初始化描述符表寄存器GDTR 33 mov word [cs: gdt_size+0x7c00],31 ;描述符表的界限(总字节数减一) 34 35 lgdt [cs: gdt_size+0x7c00] 36 37 in al,0x92 ;南桥芯片内的端口 38 or al,0000_0010B 39 out 0x92,al ;打开A20 40 41 cli ;保护模式下中断机制尚未建立,应 42 ;禁止中断 43 mov eax,cr0 44 or eax,1 45 mov cr0,eax ;设置PE位 46 47 ;以下进入保护模式... ... 48 jmp 0x0008:flush ;描述符选择子:16位偏移 49 ;清流水线并串行化处理器 50 51 52 flush: 53 mov cx,00000000000_10_000B ;加载数据段选择子(0x10) 54 mov ds,cx 55 56 ;以下在屏幕上显示"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 69 70 ;测试push 71 mov cx,00000000000_11_000B ;加载堆栈段选择子 72 mov ss,cx 73 mov sp,0x7c00 74 75 76 push 0x80 77 push byte 0x80 ; warning: signed byte value exceeds bounds 78 79 push 0x8000 80 push word 0x8000 81 82 push 0x87654321 83 ;warning: word data exceeds bounds 84 push dword 0x87654321 85 86 87 mov eax,0x86421357 88 push ax 89 push eax 90 91 ;push [0x00]error: operation size not specified 92 push byte [0x00] 93 push word [0x00] 94 95 push dword [0x00] 96 97 push ds 98 push gs 99 push es 100 push cs 101 102 ghalt: 103 hlt ;已经禁止中断,将不会被唤醒 104 105;------------------------------------------------------------------------------- 106 107 gdt_size dw 0 108 gdt_base dd 0x00007e00 ;GDT的物理地址 109 110 times 510-($-$$) db 0 111 db 0x55,0xaa
对比32位保护模式的代码,就会发现16位保护模式的代码略有不同。
首先,比如说22行,段描述符的定义是
22 mov dword [bx+0x0c],0x00009800
因为80286中,段描述符的格式是
所以,高4字节的16~32位全部为0.
其次,
47 ;以下进入保护模式... ... 48 jmp 0x0008:flush ;描述符选择子:16位偏移 49 ;清流水线并串行化处理器
这里,没有加伪指令[bits 32],而且,偏移flush没有用dword修饰。因为操作数和偏移是16位的。
好了,代码就说到这里,我们看实验报告吧。
(3)实验报告
通过和实模式的对比,可以发现,除了9、10两行中的指令码的偏移不一样(这和数据存放的位置有关系,和PUSH没有关系),PUSH指令的行为是惊人的相同。所以我们可以得出结论,16位保护模式下,PUSH的用法和实模式是一样的。我想,这也是在原书中,作者把实模式和16位的保护模式统称为“16位模式”,把32位保护模式称为“32位模式”的原因吧。
3.在32位保护模式下的实验
(1)实验代码
实验代码由配书代码(代码清单11-1 (文件名:c11_mbr.asm))修改而成。
1 ;test push (32位保护模式) 2 3 ;设置堆栈段和栈指针 4 mov ax,cs 5 mov ss,ax 6 mov sp,0x7c00 7 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],0x00 18 mov dword [bx+0x04],0x00 19 20 ;创建#1描述符,保护模式下的代码段描述符 21 mov dword [bx+0x08],0x7c0001ff 22 mov dword [bx+0x0c],0x00409800 23 24 ;创建#2描述符,保护模式下的数据段描述符(文本模式下的显示缓冲区) 25 mov dword [bx+0x10],0x8000ffff 26 mov dword [bx+0x14],0x0040920b 27 28 ;创建#3描述符,保护模式下的堆栈段描述符 29 mov dword [bx+0x18],0x00007a00 30 mov dword [bx+0x1c],0x00409600 31 32 ;初始化描述符表寄存器GDTR 33 mov word [cs: gdt_size+0x7c00],31 ;描述符表的界限(总字节数减一) 34 35 lgdt [cs: gdt_size+0x7c00] 36 37 in al,0x92 ;南桥芯片内的端口 38 or al,0000_0010B 39 out 0x92,al ;打开A20 40 41 cli ;保护模式下中断机制尚未建立,应 42 ;禁止中断 43 mov eax,cr0 44 or eax,1 45 mov cr0,eax ;设置PE位 46 47 ;以下进入保护模式... ... 48 jmp dword 0x0008:flush ;16位的描述符选择子:32位偏移 49 ;清流水线并串行化处理器 50 [bits 32] 51 52 flush: 53 mov cx,00000000000_10_000B ;加载数据段选择子(0x10) 54 mov ds,cx 55 56 ;以下在屏幕上显示"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 69 70 ;测试push 71 mov cx,00000000000_11_000B ;加载堆栈段选择子 72 mov ss,cx 73 mov esp,0x7c00 74 75 76 push 0x80 77 push byte 0x80 ;warning: signed byte value exceeds bounds 78 79 push 0x8000 80 push word 0x8000 81 82 push 0x87654321 83 push dword 0x87654321 84 85 mov eax,0x86421357 86 push ax 87 push eax 88 89 90 push word [0x00] 91 push dword [0x00] 92 93 push ds 94 push gs 95 push es 96 push cs 97 98 ghalt: 99 hlt ;已经禁止中断,将不会被唤醒 100 101;------------------------------------------------------------------------------- 102 103 gdt_size dw 0 104 gdt_base dd 0x00007e00 ;GDT的物理地址 105 106 times 510-($-$$) db 0 107 db 0x55,0xaa
如果对上面的代码不熟悉的话,可以参考我的博文 进入保护模式(一)——《x86汇编语言:从实模式到保护模式》读书笔记12 等文章。
(2)实验报告
根据测试报告,我们可以归纳出32位保护模式下,针对NASM编译器的push指令用法:
(完)