当前位置:   article > 正文

IA-32汇编语言笔记(5)—— 控制转移 & 堆栈_push a指令

push a指令
  • 记录汇编语言课笔记,可能有不正确的地方,欢迎指出
  • 教材《新概念汇编语言》—— 杨季文
  • 这篇文章对应书第二章 IA32处理器基本功能 2.6~2.7部分

一、指令指针寄存器和简单控制转移

(1)指令指针寄存器

  1. 指令指针寄存器EIP:

    • IA-32系列CPU有一个32位的指令指针寄存器EIP,它始终指向当前处理的指令。
    • 它是早先8086CPU指令指针寄存器IP的扩展
    • 由CS和EIP确定所取指令的存储单元地址。段寄存器CS给出当前段代码段的段号,指令指 针寄存器EIP给出偏移。即CS:EIP
    • 如果代码段起始地址为0,则EIP给出的偏移直接决定所取指令的存储单元地址
    • 实方式下,段的最大范围是64k,EIP中高16位必须为0,只有低16位的IP起作用
  2. 顺序执行指令的过程
    CPU执行代码(程序)就是一条接一条地执行机器指令。可以把CPU执行指令的过程看做一条处理指令的流水线,通过以下两个步骤实现的指令的顺序执行

    1. 从存储器取指令
    2. 根据指令长度,自动调整指令指针寄存器EIP的值,使其指向下一条指令

    这些工作是CPU自动完成的,只需要把我们编写的汇编程序存入代码段,就可以自动顺序执行了。

  3. 控制转移指令
    顺序执行指令,相当于一个顺序结构,很多时候这并不能满足要求。就好像我们平时写的C语言程序只能从前往后一条条执行一样,如果我们想做一个分支或者循环,就必须要用关键词if、for、while。汇编程序中与其对应的就是控制转移指令,它通过直接改变EIP寄存器的内容,实现指令执行过程中的跳转

    1. 转移非自动顺序调整EIP内容
    2. 控制转移指令专门用于改变EIP内容的指令
    3. 各种控制转移指令用于根据不同的情形改变EIP内容,从而实现转移,包括:
      1. 条件转移指令
      2. 无条件转移指令
      3. 循环指令
      4. 函数调用及返回指令

(2)常用条件转移指令

  1. 条件转移某一条件满足时,发生转移,否则继续顺序执行。
  2. 标志寄存器中的状态标志用于表示条件。绝大部分条件转移指令根据某个或某几个标志作为判据
  3. 格式:Jcc LABEL
  4. 操作:jcc代表各种条件转移指令的缩写(助记符),当条件满足时,转到标号LABEL处执行;否则顺序执行
  5. 注意:就好像小于和不大于等于是一码事,同一条指令也可能有多个助记符,见下表

在这里插入图片描述

  1. 说明:条件转移指令本身不影响标志
  2. 条件转移是段内转移。在满足条件的情况下,只改变指令指针寄存器EIP。也就是说,条件转移的转移目的地仅限于同一个代码段内。这种不改变代码段寄存器,仅改变EIP的转移称为段内转移
  3. 条件转移指令可以向前/向后转移

(3)比较指令和数值大小比较

  • 让我们先来看一看什么叫比较,一段自然语言描述 “如果a比b大,那么…”,实际上包含2个工作:

    1. 比较a和b的大小
    2. 根据比较结果做某些处理

    对应到编程中也是如此,如果比较结果没有对程序运行造成任何影响,那么比较没有意义。上述2个工作在汇编实现中,对应到:

    1. 做减法a-b,这会影响标志寄存器中的标志位(cmp指令
    2. 用标志位判断比较结果,根据它改变eip寄存器的值,实现跳转(jcc指令
  1. 比较指令
名称CMP(比较指令)
格式CMP DEST,SRC
动作根据DEST-SRC的差影响标志寄存器中各状态标志,但不结果作为结果的差值送目的寄存器
合法值SRC:通用寄存器、存储单元、立即数
DEST:通用寄存器、存储单元
注意DEST 和 SRC 必须尺寸一致
除了不把差值结果送DEST外,其他和SUB指令完全一致
    CMP   EDX, -2             ;把EDX与-2比较
    CMP   ESI, EBX            ;把ESI与EBX比较
    CMP   AL, [ESI]           ;AL与由ESI所指的字节存储单元值作比较
    CMP   [EBX+EDI*4+5], DX   ;由EBX+EDI*4+5所指字存储单元值与DX作比较
  • 1
  • 2
  • 3
  • 4
  1. 比较数值大小
    1. 一般使用比较指令CMP。
    2. 根据零标志ZF判断是否相等(JN/JE
      • 如果都是无符号数,可根据进位CF判断大小(JNB/JAE/JC
      • 如果都是有符号数,同时根据符号标志SF和溢出标志OF判断(JL/JNGE
    3. IA-32同时提供两套以数值大小为条件的条件转移指令,分别使用无符号数之间比较和有符号数之间比较。二者判断标志不同
      • 有符号数间称:大于(G),等于(E),小于(L)
      • 无符号数间称:高于(A),等于(E),低于(B)
  • 示例1:
//假设ECX和EDX存储两个数,现在要把较大的存在ECX中,较小的存在EDX中
//如果这两个数是有符号数
	cmp ecx,edx	
	jge OK			//有符号数比较转移:ecx>=edx转到OK
	xchg ecx,edx	//ecx<edx,交换
OK:

//如果这两个数是无符号数
	cmp ecx,edx	
	jae OK			//无符号数比较转移:ecx>=edx转到OK
	xchg ecx,edx	//ecx<edx,交换
OK:
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 示例2:
//用汇编实现下面C函数转的功能:
int myCmpare(int x,int y)
{
	int z=1;
	if(x>=13 && y<=28)
		z=2;
	return z;
}

//转汇编,假设ecx传递x,edx传递y,eax作变量z:
	mov eax,1
	cmp ecx,13		//x和13比较			
	jl SHORT lab1	//SHORT参数代表转移目的地就在附近
	cmp edx,28		//y和28比较
	jg SHORT lab1	
	mov eax,2
lab1:
	ret				//函数结束,返回到调用者

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

(4)简单无条件转移指令

名称CMP(比较指令)
格式JMP LABEL
动作指令控制无条件转移到LABEL处
注意段内转移,没有任何前提一定发生转移,类似C中的goto
通常用在if-else分支用,if分支结束后跳过else分支
  • 示例:
//原始C代码(_fastcall表示用寄存器传参数)
int  _fastcall  cf215(int x, int y)
{
    int  z;
    if  ( x > 10 )            //语句A
        z = 3*x+4*y+7;
    else
        z = 2*x+7*y-12;
    if ( y <= 20 )           //语句B
        z = 4*z+3;
    return  z;               //语句C
}

//反编译
 	cmp   ecx, 10                          //x与10比较
    jle   SHORT  LN3cf215                  //当小于等于10时转
    lea   eax, DWORD PTR [ecx+ecx*2]       //计算表达式3*x+4*y+7
    lea   eax, DWORD PTR [eax+edx*4+7]
    jmp   SHORT  LN2cf215                  //无条件转(if-else语句结束)
LN3cf215:
    lea   eax, DWORD PTR [edx*8]           //计算表达式7*y+2*x-12
    sub   eax, edx
    lea   eax, DWORD PTR [eax+ecx*2-12]
LN2cf215:
    cmp   edx, 20                          //y与20比较
    jg    SHORT  LN1cf215                  //当大于20时转
    lea   eax, DWORD PTR [eax*4+3]         //计算4*z+3LN1cf215:
    ret           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

二、堆栈和堆栈操作

(1)堆栈

  1. 程序的运行和堆栈有密切关系

    • cpu运行期间需要堆栈保存某些关键信息
    • 程序自身用堆栈保存一些临时数据
  2. 堆栈

    • 堆栈一段内存区域,对他的访问限于一端进行。存储于堆栈段,段寄存器为SS
    • 栈底:堆栈中地址较大的一端
    • 栈顶:堆栈中地址较小的一端
  3. 堆栈的操作

    • 后进先出原则,所有存取在栈顶进行(存入数据的地址越来越小,堆栈生长方向为从高地址到低地址
    • 进栈/压栈操作:存入数据
    • 出栈/弹出操作:取出数据
  4. 堆栈相关寄存器

寄存器存储内容
SS(堆栈段寄存器)当前堆栈段号,指示堆栈所在内存区域的位置
ESP(堆栈指针寄存器)栈顶的偏移,SS:ESP永远指向栈顶,CPU自动控制
EBP(堆栈数据寄存器)栈内数据的偏移,SS:EBP指向栈中一个数据 (习惯指向函数帧栈底),手动控制
  1. 堆栈平衡

    1. 堆栈平衡在函数调用前后esp和ebp的值应当相同
    2. 为何要做堆栈平衡:esp和ebp寄存器在子函数调用时是非常重要的(见下方说明),其值在调用过程中会发生改变。一个程序中可能有很多函数,有时还会有嵌套调用的情况,但CPU只有esp和ebp两个寄存器,怎么处理大量的函数呢?intel的策略是同一时刻只处理该时刻执行的函数,也就是说esp和ebp的值在不断刷新。如果一个函数执行后没有恢复esp和ebp指针,就会影响它前后及嵌套的函数,使它们操作堆栈时地址错乱。
  2. 堆栈的用途

    1. 保护寄存器、保护现场
    2. 保存返回地址
    3. 传递参数
    4. 安排局部变量或临时变量
    5. 反转一组数据
  • 说明:
    1. 栈顶地址为SS:ESP; 堆栈数据地址为SS:EBP
    2. 随着数据入栈,ESP会自动移动并保持在栈顶EBP不会被CPU自动操作,它一般是手动控制的,用来暂时保存ESP的值,或者用来寻找在栈参数与变量。这里用一个堆栈传递函数参数的例子进一步说明:
      1. 假设某函参数列表数为(int x,int y),函数内有局部变量int z
      2. 在主函数先把参数列表压栈(一般从右往左压)
      3. call指令调用子函数,这条指令会自动保存函数返回地址
      4. 子函数开头先把ebp压栈,目的是在函数结束后可以恢复ebp的值(这里ebp可能是前一个函数的函数帧栈底),维持堆栈平衡
      5. mov ebp,esp使ebp存储函数帧低部。这是为了方便在栈中取数据,比如[ebp+4]就是函数返回地址,[ebp+8]就是参数y。当然我们也可以用esp来寻址,但esp是不断变化的,操作困难,编译器也不会这样做。
      6. 局部变量z压栈。现在可以用ebp加偏移的方法访问xyz各数据。(如果关闭编译优化并进行反汇编,可以看到编译器在每一个需要用xyz的地方都会从堆栈(内存)取数;如果开启编译优化,会看到程序只把数据取出一次放到寄存器中,然后用寄存器作为指令参数,寄存器取数比内存取数快得多,这是一个常见的优化方法
      7. 函数功能结束,mov esp,ebp,把栈顶放回函数帧底部,从而撤销所有局部变量。(这就是局部变量生命周期的本质
      8. pop ebp,恢复ebp调用前的值,平衡堆栈
      9. ret 自动返回到调用位置,函数返回地址出栈
      10. 现在回到主函数了,但是堆栈还没有平衡,压栈的参数y和x还在栈中,于是编译器采用了一种简单粗暴却有行之有效的办法,add esp,8 ,这样就ok了
        在这里插入图片描述

(2)堆栈操作指令

  1. 进栈指令
名称PUSH(进栈指令)
格式PUSH SRC
动作把源操作数SRC压入堆栈,并调整esp指向栈顶
合法值SRC:32/16位通用Reg或段Reg;双字/字存储单元;立即数
注意双字入栈:ESP-4,然后把双字送到ESP所指的数据单元
字入栈:ESP-2,然后把字送到ESP所指的数据单元
至少进栈一个字
  • 示例:
PUSH   EAX                //把EAX的内容压入堆栈
PUSH   DWORD PTR [ECX]    //把ECX指示的双字存储单元的内容压入堆栈
PUSH   BX                 //把BX的内容压入堆栈
PUSH   WORD PTR [EDX]     //把EDX指示的字存储单元的内容压入堆栈
  • 1
  • 2
  • 3
  • 4

在这里插入图片描述

  1. 出栈指令
名称POP(出栈指令)
格式POP DEST
动作从栈顶弹出一个双字/字到DEST,并调整esp指向栈顶
合法值DEST:32/16位通用Reg或段Reg;双字/字存储单元。但是不能是立即数或代码段寄存器CS
注意双字出栈:先从ESP所指存储单元弹出一个双字数据送DEST,然后ESP+=4
字出栈:先从ESP所指存储单元弹出一个字数据送DEST,然后ESP+=2
至少出栈一个字
DEST不能是立即数或代码段寄存器CS
  • 示例:
POP   ESI                  //从堆栈弹出一个双字到ESI
POP   DWORD PTR [EBX+4]    //从堆栈弹出一个双字到EBX+4所指示存储单元
POP   DI                   //从堆栈弹出一个字到DI
POP   WORD PTR [EDX+8]     //从堆栈弹出一个字到EDX+8所指示的存储单元
  • 1
  • 2
  • 3
  • 4
  • 示例2:
#include  <stdio.h>
int  main( )  
{
	int  varsp1, varsp2, varsp3, varsp4, varsp5;  //用于存放ESP值
	int  varr1, varr2;                            //用于存放EBX值
	_asm
	{
	    MOV   EAX, 12345678H     //初值
	    MOV   varsp1, ESP        //保存演示之初的ESP(假设为0013FA74H)
	    
	    PUSH  EAX                //把EAX压入堆栈
	    MOV  varsp2, ESP         //保存当前ESP(0013FA70H)
	    
	    PUSH  AX                 //把AX压入堆栈
	    MOV   varsp3, ESP        //保存当前ESP(0013FA6EH)
	    
	    POP   EBX                //从堆栈弹出双字到EBX
	    MOV   varsp4, ESP        //保存当前ESP(0013FA72H)
	    MOV   varr1, EBX
	    
	    POP   BX                 //从堆栈弹出字到BX
	    MOV   varsp5, ESP        //保存当前ESP(0013FA74H)
	    MOV   varr2, EBX
	}
    printf("ESP1=%08XH\n",varsp1);    //显示为ESP1=0013FA74H
    printf("ESP2=%08XH\n",varsp2);    //显示为ESP2=0013FA70H
    printf("ESP3=%08XH\n",varsp3);    //显示为ESP3=0013FA6EH
    printf("ESP4=%08XH\n",varsp4);    //显示为ESP4=0013FA72H
    printf("ESP5=%08XH\n",varsp5);    //显示为ESP5=0013FA74H
    printf("EBX1=%08XH\n",varr1);     //显示为EBX1=56785678H
    printf("EBX2=%08XH\n",varr2);     //显示为EBX2=56781234H
    return  0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  1. 通用寄存器全进出栈指令
  • 有时需要把多个通用Reg压入栈,以保护值。为了提高效率,从80186开始提供了通用寄存器全进出栈指令

(1)16位通用Reg

名称PUSHA(16位通用寄存器全进栈指令)
格式PUSHA
动作将8个16位通用寄存器的内容压入堆栈,压入顺序:AX、CX、DX、BX、SP、BP、SI、DI
名称POPA(16位通用寄存器全出栈指令)
格式POPA
动作从堆栈弹出内容,以PUSHA相反的顺序送通用寄存器

(2)32位通用Reg

名称PUSHAD(32位通用寄存器全进栈指令)
格式PUSHAD
动作将8个16位通用寄存器的内容压入堆栈,压入顺序:EAX、ECX、EDX、EBX、ESP、EBP、ESI、EDI
名称POPAD(32位通用寄存器全出栈指令)
格式POPAD
动作从堆栈弹出内容,以PUSHA相反的顺序送通用寄存器
  • 示例:
//演示PUSHAD指令的执行效果,还演示另一种访问堆栈区域存储单元的方法
#include  <stdio.h>
int   buff[8];       //全局数组,存放从堆栈中取出的各寄存器之值
int  main( )
{
    _asm 
    {    
    	 PUSH  EBP            //先保存EBP!!
	    ;
	    MOV   EAX, 0         //给各通用寄存器赋一个特定的值
	    MOV   EBX, 1
	    MOV   ECX, 2
	    MOV   EDX, 3
	    ;                    //决不能随意改变ESP!!
	    MOV   EBP, 5
	    MOV   ESI, 6
	    MOV   EDI, 7
	    ;
	    PUSHAD               //把8个通用寄存器之值全部推到堆栈
	    ;
		MOV   EBP, ESP       //使得EBP也指向堆栈顶
	    LEA   EBX, buff      //把数组buff首元素的有效地址送到EBX
	    MOV   ECX, 0         //设置计数器(下标)初值
	NEXT:
	    MOV   EAX, [EBP+ECX*4]    //依次从堆栈中取
	    MOV   [EBX+ECX*4], EAX    //依次保存到数组buff
	    INC   ECX                 //计数器加1
	    CMP   ECX, 8              //是否满8
	    JNZ   NEXT                //没有满8个,继续处理下一个
	    ;
	    POPAD                     //恢复8个通用寄存器
	    POP   EBP      
    }
    //依次显示数组buff各元素之值,从中观察PUAHAD指令压栈的效果
    int  i;
    for  (i=0; i<8; i++)
        printf("buff[%d]=%u\n", i, buff[i]);
    return  0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

从堆栈到数组图示
在这里插入图片描述

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号