PUSHA/PUSHAD POPA/POPAD 指令详解

PUSHA/PUSHAD POPA/POPAD 指令详解

官方文档的解释

《Intel Architecture Software Developer’s Manual Volume 2:Instruction Set Reference》中说明了PUSHA/PUSHAD,POPA/POPAD指令的用法。

PUSHA/PUSHAD

他们的指令码是一样的。
opcode
当操作数的大小是32位时:
这两个指令的作用是把通用寄存器压栈。寄存器的入栈顺序依次是:EAX,ECX,EDX,EBX,ESP(初始值),EBP,ESI,EDI.
当操作数的大小是16位时:
这两个指令的作用是把通用寄存器压栈。寄存器的入栈顺序依次是:AX,CX,DX,BX,SP(初始值),BP,SI,DI.

注意:指令执行后,对标志寄存器无影响。
如果你还不理解上面的文字,那么官方的伪代码更能说明问题:

Operation

IF OperandSize = 32 ( PUSHAD instruction )
THEN
Temp ← (ESP);
Push(EAX);
Push(ECX);
Push(EDX);
Push(EBX);
Push(Temp);
Push(EBP);
Push(ESI);
Push(EDI);
ELSE ( OperandSize = 16, PUSHA instruction )
Temp ← (SP);
Push(AX);
Push(CX);
Push(DX);
Push(BX);
Push(Temp);
Push(BP);
Push(SI);
Push(DI);
FI;

但是,但是,有段话要注意:

The PUSHA (push all) and PUSHAD (push all double) mnemonics reference the same opcode. The PUSHA instruction is intended for use when the operand-size attribute is 16 and the PUSHAD instruction for when the operand-size attribute is 32. Some assemblers may force the operand size to 16 when PUSHA is used and to 32 when PUSHAD is used. Others may treat these mnemonics as synonyms (PUSHA/PUSHAD) and use the current setting of the operand-size attribute to determine the size of values to be pushed from the stack, regardless of the
mnemonic used.

我把这段话翻译一下:
PUSHA (push all) and PUSHAD (push all double)这两个指令助记符对应的是同一个指令码。当操作数大小是16位的时候用PUSHA,当操作数大小是32位的时候用PUSHAD。对于某些编译器,当使用PUSHA的时候会强制操作数为16位,当使用PUSHAD的时候,会强制操作数为32位(在后面的实验中,我发现用NASM编译,在16位模式下就是这样);对于另外一些编译器,会把PUSHA/PUSHAD当成是相同的指令助记符,根据当前操作数的大小来决定压入一个字还是一个双字(用NASM编译器,在32位保护模式下,PUSHA/PUSHAD都是压入双字)。
由此可见,NASM真是一个优秀且奇葩的编译器 :smiley:

POPA/POPAD

这两个指令的指令码也是一样的。

这两个指令用于把栈中的值弹出到通用寄存器。其实是执行和PUSHA/PUSHAD相反的操作。
当操作数的大小是32位时:
出栈顺序依次是:EDI,ESI,EBP,EBX,EDX,ECX,EAX;
当操作数的大小是16位时:
出栈顺序依次是:DI,SI,BP,BX,DX,CX,AX;

The value on the stack for the ESP or SP register is ignored. Instead, the ESP or SP register is incremented after each register is loaded.

需要注意的是,栈中的ESP或者SP的值是被忽略的,ESP或SP的值在每出栈一个寄存器后会增加4或者增加2;
同样,指令执行后,对标志寄存器无影响。
官方的伪代码是:

Operation
IF OperandSize = 32 (instruction = POPAD)
THEN
EDI ← Pop();
ESI ← Pop();
EBP ← Pop();
increment ESP by 4 (skip next 4 bytes of stack)
EBX ← Pop();
EDX ← Pop();
ECX ← Pop();
EAX ← Pop();
ELSE (OperandSize = 16, instruction = POPA)
DI ← Pop();
SI ← Pop();
BP ← Pop();
increment ESP by 2 (skip next 2 bytes of stack)
BX ← Pop();
DX ← Pop();
CX ← Pop();
AX ← Pop();
FI;

同样,也有句话需要注意:

The POPA (pop all) and POPAD (pop all double) mnemonics reference the same opcode. The POPA instruction is intended for use when the operand-size attribute is 16 and the POPAD instruction for when the operand-size attribute is 32. Some assemblers may force the operand size to 16 when POPA is used and to 32 when POPAD is used (using the operand-size override prefix [66H] if necessary). Others may treat these mnemonics as synonyms (POPA/POPAD) and use the current setting of the operand-size attribute to determine the size of values to be popped from the stack, regardless of the mnemonic used. (The D flag in the current code segment’s segment descriptor determines the operand-size attribute.)

这段话和上面那段话类似,我翻译一下:
The POPA (pop all) and POPAD (pop all double)这两个指令助记符对应的是同一个指令码。当操作数大小是16位的时候用POPA,当操作数大小是32位的时候用POPAD。对于某些编译器,当使用POPA的时候会强制操作数为16位,当使用POPAD的时候,会强制操作数为32位(如果有必要的话,会使用指令前缀0x66,以反转默认操作数的大小);对于另外一些编译器,会把POPA/POPAD当成是相同的指令助记符,根据当前操作数的大小来决定压入一个字还是一个双字(当前代码段描述符的D标志位决定了操作数的大小)。
好了,官方文档就读到这里。下面我们做实验。

用NASM来测试

我决定在三种模式下分别验证pusha/pushad和popa/popad的行为。
这三种模式分别是:
1. 实模式
2. 16位保护模式
3. 32位保护模式

如果你对这三种模式有疑惑的话,可以参考我的另外一篇博文:
http://blog.csdn.net/longintchar/article/details/50602851

1.实模式

1.1实验过程
        ;文件说明:实模式下测试 pusha/pushad, popa/popad
        ;创建日期:2016-3-10

        ;设置堆栈段和栈指针 
        mov ax,cs      
        mov ss,ax
        mov sp,0x7c00

        mov ax,0x01
        mov bx,0x02
        mov cx,0x03
        mov dx,0x04
        mov bp,0x05
        mov si,0x06
        mov di,0x07

        pusha
        popa

        pushad
        popad

a:      jmp a       

times 510-($-$$) db 0
                 db 0x55,0xaa

在Bochs模拟器环境下,我们看看调试截图。
pusha-real_mode

1.1.1.pusha

可以看到,寄存器的入栈顺序依次是:AX,CX,DX,BX,SP(初始值0x7c00),BP,SI,DI.与官方文档相符。

1.1.2.popa

popa-real_mode
图片说明了一切,我就不多说了。:smiley:

1.1.3.pushad

pushad-real_mode
可以看到,寄存器的入栈顺序依次是:EAX, ECX,EDX,EBX,ESP(初始值0x7c00),EBP,ESI,EDI.也就是说,强制压入32位寄存器的值。

1.1.4.popad

popad-real_mode
可以看到,依次弹出双字的值到32位通用寄存器。

1.2.实验结论

上面的实验表明,如果采用NASM编译器,那么在实模式下,pusha和pushad的效果是不一样的,前者是入栈16位的通用寄存器,后者是入栈32位的通用寄存器。popa和popad也是这个道理,此处略去若干字。
如果观察编译后的指令码,你会发现pushad和popad是添加了指令前缀0x66的,表示强制操作数大小为32位。

2.16位保护模式

2.1.实验过程
 ;test pusha/pushad,popa/popad (16位保护模式下)

       ;设置堆栈段和栈指针 
       mov ax,cs      
       mov ss,ax
       mov sp,0x7c00

       ;计算GDT所在的逻辑段地址 
        mov ax,[cs:gdt_base+0x7c00]        ;低16位 
        mov dx,[cs:gdt_base+0x7c00+0x02]   ;高16位 
        mov bx,16        
        div bx            
        mov ds,ax                          ;令DS指向该段以进行操作
        mov bx,dx                          ;段内起始偏移地址 

        ;创建0#描述符,它是空描述符,这是处理器的要求
        mov dword [bx+0x00],0x00
        mov dword [bx+0x04],0x00  

        ;创建#1描述符,保护模式下的代码段描述符
        mov dword [bx+0x08],0x7c0001ff     
        mov dword [bx+0x0c],0x00009800     

        ;创建#2描述符,保护模式下的数据段描述符(文本模式下的显示缓冲区) 
        mov dword [bx+0x10],0x8000ffff     
        mov dword [bx+0x14],0x0000920b     

        ;创建#3描述符,保护模式下的堆栈段描述符
        mov dword [bx+0x18],0x00007a00
        mov dword [bx+0x1c],0x00009600

        ;初始化描述符表寄存器GDTR
        mov word [cs: gdt_size+0x7c00],31  ;描述符表的界限(总字节数减一)   

        lgdt [cs: gdt_size+0x7c00]

        in al,0x92                         ;南桥芯片内的端口 
        or al,0000_0010B
        out 0x92,al                        ;打开A20

        cli                                ;保护模式下中断机制尚未建立,应 
                                           ;禁止中断 
        mov eax,cr0
        or eax,1
        mov cr0,eax                        ;设置PE位

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


   flush:
        mov cx,0x10         ;加载数据段选择子(0x10)
        mov ds,cx




        mov cx,0x18         ;加载堆栈段选择子
        mov ss,cx
        mov sp,0x7c00

        mov eax,0x01
        mov ebx,0x02
        mov ecx,0x03
        mov edx,0x04
        mov ebp,0x05
        mov esi,0x06
        mov edi,0x07

        pusha
        popa

        pushad
        popad

        hlt                                ;已经禁止中断,将不会被唤醒 

;-------------------------------------------------------------------------------

         gdt_size         dw 0
         gdt_base         dd 0x00007e00     ;GDT的物理地址 

         times 510-($-$$) db 0
                          db 0x55,0xaa

以上的代码,是我偷懒从上面那个链接博文的代码改过来的。其实没有必要定义那么多的段描述符,不管了,我们开始试验吧。

2.1.1.pusha

pusha-pm16
可以看到,压入的是16位的寄存器值。

2.1.2.popa

依次按照字出栈,我就不截图了。

2.1.3.pushad

pushad-pm16
可以看到,压入的是32位的寄存器值。

2.1.4.popad

依次按照双字出栈到32位通用寄存器。截图略。

2.2.实验结论

我们再看看编译后的指令码。

上面的实验表明,如果采用NASM编译器,那么在16位保护模式下,pusha和pushad的效果是不一样的,前者是入栈16位的通用寄存器,后者是入栈32位的通用寄存器。这与实模式下的表现相同。

3.32位保护模式

3.1.实验过程
        ;文件说明:32位保护模式下,测试pusha/pushad,popa/popad
        ;创建日期:2016-3-7



        ;设置堆栈段和栈指针 
        mov eax,cs      
        mov ss,eax
        mov sp,0x7c00


        ;计算GDT所在的逻辑段地址
        mov eax,[cs:pgdt+0x7c00+0x02]      ;GDT的32位线性基地址 
        xor edx,edx
        mov ebx,16
        div ebx                            ;分解成16位逻辑地址 

        mov ds,eax                         ;令DS指向该段以进行操作
        mov ebx,edx                        ;段内起始偏移地址 



        ;跳过0#号描述符的槽位 

        ;创建1#描述符,这是一个数据段,对应0~4GB的线性地址空间
        mov dword [ebx+0x08],0x0000ffff    ;基地址为0,段界限为0xFFFFF
        mov dword [ebx+0x0c],0x00cf9200    ;粒度为4KB,存储器段描述符 

        ;2#创建保护模式下初始代码段描述符
        mov dword [ebx+0x10],0x7c0001ff    ;基地址为0x00007c00,界限0x1FF 
        mov dword [ebx+0x14],0x00409800    ;粒度为1个字节,代码段描述符 

        ;3#建立保护模式下的堆栈段描述符      ;基地址为0x00007C00,界限0xFFFFE 
        mov dword [ebx+0x18],0x7c00fffe    ;粒度为4KB 
        mov dword [ebx+0x1c],0x00cf9600


        ;初始化描述符表寄存器GDTR
        mov word [cs: pgdt+0x7c00],31      ;描述符表的界限   

        lgdt [cs: pgdt+0x7c00]

        in al,0x92                         ;南桥芯片内的端口 
        or al,0000_0010B
        out 0x92,al                        ;打开A20

        cli                                ;中断机制尚未工作

        mov eax,cr0
        or eax,1
        mov cr0,eax                        ;设置PE位

        ;以下进入保护模式... ...
        jmp dword 0x0010:flush             ;16位的描述符选择子:32位偏移

        [bits 32]                          
flush:                                     

        mov eax,0x0008                     ;加载数据段(0..4GB)选择子; ds,es,fs,gs指向了(0..4G)
        mov ds,eax   
        mov es,eax
        mov fs,eax
        mov gs,eax

        mov eax,0x0018                   ;加载栈段选择子
        mov ss,eax
        xor esp,esp                        ;ESP <- 0    

        mov eax,0x01
        mov ebx,0x02
        mov ecx,0x03
        mov edx,0x04
        mov ebp,0x05
        mov esi,0x06
        mov edi,0x07

        pusha
        popa

        pushad
        popad

        hlt 
;-------------------------------------------------------------------------------
        pgdt     dw 0
                 dd 0x00007e00      ;GDT的物理地址

        times 510-($-$$) db 0
        db 0x55,0xaa
3.1.1.pusha


虽然用的是pusha,但是压入的还是32位寄存器。这个是可以理解的,因为代码段描述符的D位=1;

3.1.2.popa


依次出栈双字。

3.1.3.pushad

同pusha,图略。

3.1.4.popad

同popa,图略。

3.2.实验结论

如果采用NASM编译器,在32位保护模式下,pusha就是pushad,他们是一样的,编译后指令码也一样。同理,popa就是popad,他们的行为也一样。

总结

实验环境:Bochs模拟器(版本号2.6.8)
编译工具:NASM(版本号2.11.08)

在16位模式下(包括实模式和16位保护模式),pusha依次入栈AX,CX,DX,BX,SP(初始值),BP,SI,DI;pushad依次入栈EAX,ECX,EDX,EBX,ESP(初始值),EBP,ESI,EDI.

在32位模式下(就是32位保护模式),pusha和pushad的行为一样,依次入栈EAX,ECX,EDX,EBX,ESP(初始值),EBP,ESI,EDI.

(完)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值