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

时间:2021-06-21 13:17:50

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

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

如上图所示,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到底怎么变化,压入的数到底是多少)。所以,我列了一个单子,把所有能想到的形式都列出来了,其中有的我也不确定(或许这样写编译都会报错)。不管那么多,先写出来,然后让编译器筛选吧16位模式/32位模式下PUSH指令探究——《x86汇编语言:从实模式到保护模式》读书笔记16

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

既然是警告,那么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)实验报告

小二,上实验报告!

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

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

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

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中,段描述符的格式是

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

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

其次,

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

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

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

(3)实验报告

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

通过和实模式的对比,可以发现,除了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)实验报告

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

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

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

(完)