您的位置:首页 > 其它

一步步学习汇编(11)之Call和ret指令(破解软件的必修课二)

2014-01-16 10:30 288 查看
/article/6136657.html

一.ret指令用栈中的数据,修改IP的内容,从而实现近转移;
CPU执行ret指令时,进行下面两步操作:
a) (1)(IP)=((ss)*16+(sp))
b) (2)(sp)=(sp)+2
二.retf指令用栈中的数据,修改CS和IP的内容,从而实现远转移;
CPU执行retf指令时,进行下面两步操作:
a) (1)(IP)=((ss)*16+(sp))
b) (2)(sp)=(sp)+2
c) (3)(CS)=((ss)*16+(sp))
d) (4)(sp)=(sp)+2
三.可以看出,如果我们用汇编语法来解释ret和retf指令,则:
a) CPU执行ret指令时,相当于进行:
pop IP
b).CPU执行retf指令时,相当于进行:
pop IP
pop CS
四.call 指令
CPU执行call指令,进行两步操作:
a) (1)将当前的 IP 或 CS和IP 压入栈中;
b) (2)转移。
call 指令不能实现短转移,除此之外,call指令实现转移的方法和 jmp 指令的原理相同。
解释:
n call 标号(将当前的 IP 压栈后,转到标号处执行指令)
n CPU执行此种格式的call指令时,进行如下的操作:
n (1) (sp) = (sp) – 2
((ss)*16+(sp)) = (IP)
n (2) (IP) = (IP) + 16位位移
call 标号
n 16位位移=“标号”处的地址-call指令后的第一个字节的地址;
n 16位位移的范围为 -32768~32767,用补码表示;
n 16位位移由编译程序在编译时算出。
五.综合实例演示:
看下面一段简单的代码:
Mov ax,0
Call s
Mov bx,0
S:add ax,1
Ret
解释如下:
Call s:实际上就是调用s处的子程序。并将Mov bx,0这条指令所对应的偏移地址入栈,此处为什么要入栈呢?实际上就是为了方便ret指令取出ip。
Ret指令实际上就是返回s,能使程序从mov bx,0处执行。

六.call 和 ret 的配合使用
1.例子1:
assume cs:code
code segment
start: mov ax,1
mov cx,3
call s
mov bx,ax ;(bx) = ?
mov ax,4c00h
int 21h
s: add ax,ax
loop s
ret
code ends
end start

我们来看一下 CPU 执行这个程序的主要过程:
n (1)CPU 将call s指令的机器码读入,IP指向了call s后的指令mov bx,ax,然后CPU执行call s指令,将当前的 IP值(指令mov bx,ax的偏移地址)压栈,并将 IP 的值改变为标号 s处的偏移地址;
n (2)CPU从标号 s 处开始执行指令,loop循环完毕,(ax)=8;
n (3)CPU将ret指令的机器码读入,IP指向了ret 指令后的内存单元,然后CPU 执行 ret 指令 ,从栈中弹出一个值(即 call 先前压入的mov bx,ax 指令的偏移地址)送入 IP 中。则CS:IP指向指令mov bx,ax;
n (4)CPU从 mov bx,ax 开始执行指令,直至完成。

2.例子2



n 我们看一下程序的主要执行过程:
n (1)前三条指令执行后,栈的情况如下:



n 2)call 指令读入后,(IP) =000EH,CPU指令缓冲器中的代码为 B8 05 00;
CPU执行B8 05 00,首先,栈中的情况变为:

然后,(IP)=(IP)+0005=0013H。
n (3)CPU从cs:0013H处(即标号s处)开始执行。
n (4)ret指令读入后:(IP)=0016H,CPU指令缓冲器中的代码为 C3;CPU执行C3,相当于进行pop IP,执行后,栈中的情况为:

(IP)=000EH;
n (5)CPU回到 cs:000EH处(即call指令后面的指令处)继续执行。
3. 从上面的讨论中我们发现,可以写一个具有一定功能的程序段,我们称其为子程序,在需要的时候,用call指令转去执行。
4. 可是执行完子程序后,如何让CPU接着call指令向下执行?
5. call指令转去执行子程序之前,call指令后面的指令的地址将存储在栈中,所以可以在子程序的后面使用 ret 指令,用栈中的数据设置IP的值,从而转到 call 指令后面的代码处继续执行。

七.mul 指令
n 因下面要用到,我们介绍一下mul指令,mul是乘法指令,使用 mul 做乘法的时候:
n (1)相乘的两个数:要么都是8位,要么都是16位。
8 位: AL中和 8位寄存器或内存字节单元中;
16 位: AX中和 16 位寄存器或内存字单元中。
n (2)结果
8位:AX中;
16位:DX(高位)和AX(低位)中。

n 格式如下:
mul reg
mul 内存单元

例子一:
n 例如:
n (1)计算100*10
100和10小于255,可以做8位乘法,程序如下:
mov al,100
mov bl,10
mul bl
结果: (ax)=1000(03E8H)
例子二:
n (1)计算100*10000
100小于255,可10000大于255,所以必须做16位乘法,程序如下:
mov ax,100
mov bx,10000
mul bx
结果: (ax)=4240H,(dx)=000FH
(F4240H=1000000)

8.参数和结果传递的问题
n 我们设计一个子程序,可以根据提供的N,来计算N的3次方。
n 这里有两个问题:
n (1)我们将参数N存储在什么地方?
n (2)计算得到的数值,我们存储在什么地方?

很显然,我们可以用寄存器来存储,可以将参数放到 bx 中 ;因为子程序中要计算 N×N×N ,可以使用多个 mul 指令,为了方便,可将结果放到 dx 和 ax中。
n 子程序:
n 说明:计算N的3次方
n 参数: (bx)=N
n 结果: (dx:ax)=N∧3
cube:mov ax,bx
mul bx
mul bx
ret
n 用寄存器来存储参数和结果是最常使用的方法。对于存放参数的寄存器和存放结果的寄存器,调用者和子程序的读写操作恰恰相反:
n 调用者将参数送入参数寄存器,从结果寄存器中取到返回值;
n 子程序从参数寄存器中取到参数,将返回值送入结果寄存器。
9.批量数据的传递
n 前面的例程中,子程序 cube 只有一个参数,放在bx中。如果有两个参数,那么可以用两个寄存器来放,可是如果需要传递的数据有3个、4个或更多直至 N个,我们怎样存放呢?
n 寄存器的数量终究有限,我们不可能简单地用寄存器来存放多个需要传递的数据。对于返回值,也有同样的问题。
n 在这种时候,我们将批量数据放到内存中,然后将它们所在内存空间的首地址放在寄存器中,传递给需要的子程序。
n 对于具有批量数据的返回结果,也可用同样的方法。
如下代码:



10.寄存器冲突的问题
n 设计一个子程序:
n 功能:将一个全是字母,以0结尾的字符串,转化为大写。
n 分析
应用这个子程序 ,字符串的内容后面定要有一个0,标记字符串的结束。子程序可以依次读取每个字符进行检测,如果不是0,就进行大写的转化,如果是0,就结束处理。
由于可通过检测0而知道是否己经处理完整个字符串 ,所以子程序可以不需要字符串的长度作为参数。我们可以用jcxz来检测0。



添加主框架
assume cs:code
data segment
db 'conversation',0
data ends
n 代码段中相关程序段如下:
mov ax,data
mov ds,ax
mov si,0
call capital
其中si运用了多次,怎么避免呢?
从上而的问题中,实际上引出了个一般化的问题:子程序中使用的寄存器,很可能在主程序中也要使用,造成了寄存器使用上的冲突。
那么我们如何来避免这种冲突呢 ?粗略地看,我们可以有两个方案:
n (1)在编写调用子程序的程序时 ,注意看看子程序中有没有用到会产生冲突的寄存器,如果有,调用者使用别的寄存器;
n (2)在编写子程序的时候,不要使用会产生冲突的寄存器。
我们编写子程序的标准框架如下:
子程序开始:子程序中使用的寄存器入栈
子程序内容
子程序使用的寄存器出栈
返回(ret、retf)
如下代码:
n capital: push cx
push si
change: mov cl,[si]
mov ch,0
jcxz ok
and byte ptr [si],11011111b
inc si
jmp short change
ok: pop si
pop cx
ret
n 要注意寄存器入栈和出栈的顺序。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: