5-控制转移与堆栈

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

(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. 控制转移指令
    控制转移指令,它通过直接改变EIP寄存器的内容,实现指令执行过程中的跳转

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

(2)常用条件转移指令

  1. 格式:Jcc LABEL
  2. 操作:jcc代表各种条件转移指令的缩写(助记符),当条件满足时,转到标号LABEL处执行;否则顺序执行
  3. 注意:就好像小于和不大于等于是一码事,同一条指令也可能有多个助记符,见下表
    image.png

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

  1. 比较指令
名称 CMP(比较指令)
格式 CMP DEST,SRC
动作 根据DEST-SRC的差影响标志寄存器中各状态标志,但不结果作为结果的差值送目的寄存器
合法值 SRC:通用寄存器、存储单元、立即数
DEST:通用寄存器、存储单元
注意 DEST 和 SRC 必须尺寸一致
除了不把差值结果送DEST外,其他和SUB指令完全一致
1
2
3
4
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. 比较数值的大小
    1. 一般使用比较指令CMP。
    2. 根据零标志ZF判断是否相等(JN/JE
      • 如果都是无符号数,可根据进位CF判断大小(JNB/JAE/JC
      • 如果都是有符号数,同时根据符号标志SF和溢出标志OF判断(JL/JNGE
    3. IA-32同时提供两套以数值大小为条件的条件转移指令,分别使用无符号数之间比较和有符号数之间比较。二者判断标志不同
      • 有符号数间称:大于(G),等于(E),小于(L)
      • 无符号数间称:高于(A),等于(E),低于(B)

示例:

1
2
3
4
5
6
7
8
9
10
11
12
//假设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
13
14
15
16
17
18
//用汇编实现下面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 //函数结束,返回到调用者

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

名称 CMP(比较指令)
格式 JMP LABEL
动作 指令控制无条件转移到LABEL处
注意 段内转移,没有任何前提一定发生转移,类似C中的goto
通常用在if-else分支用,if分支结束后跳过else分支

示例:

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
//原始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)堆栈

  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. 反转一组数据

一张很清晰的图片
image.png

(2)堆栈操作指令

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

image.png

  1. 出栈指令
名称 POP(出栈指令)
格式 POP DEST
动作 从栈顶弹出一个双字/字到DEST,并调整esp指向栈顶
合法值 DEST:32/16位通用Reg或段Reg;双字/字存储单元。但是不能是立即数或代码段寄存器CS
注意 双字出栈:先从ESP所指存储单元弹出一个双字数据送DEST,然后ESP+=4
字出栈:先从ESP所指存储单元弹出一个字数据送DEST,然后ESP+=2
至少出栈一个字
DEST不能是立即数或代码段寄存器CS
示例:
1
2
3
4
POP   ESI                  //从堆栈弹出一个双字到ESI
POP DWORD PTR [EBX+4] //从堆栈弹出一个双字到EBX+4所指示存储单元
POP DI //从堆栈弹出一个字到DI
POP WORD PTR [EDX+8] //从堆栈弹出一个字到EDX+8所指示的存储单元
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
#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. 通用寄存器全进出栈指令
  • 有时需要把多个通用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相反的顺序送通用寄存器

示例:

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
//演示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;
}

image.png