要理解ret,retf,call指令,必须要先理清以下汇编基础知识:
一. [bx]和内存单元
[bx]是什么呢?
和[0]有些类似,[0]表示内存单元,它的偏移地址是0。
n 我们要完整地描述一个内存单元,需要两种信息:
n (1)内存单元的地址;
n (2)内存单元的长度(类型)。
n 我们用[0]表示一个内存单元时,0 表示单元的偏移地址,段地址默认在ds中,单元的长度(类型)可以由具体指令中的其他操作对象(比如说寄存器)指出
所以,按照这样的原理,我们对如下指令可以这样理解:
Mov ax,[0]:表示将一个内存单元的内容送入ax寄存器。其中内存单元的长度为2字节,偏移地址为0,段地址在ds中。
Mov al,[0]:表示将一个内存单元的内容送入al寄存器。其中内存单元的长度为1字节,偏移地址为0,段地址在ds中。
Mov ax,[bx]:表示将一个内存单元的内容送入ax寄存器。其中内存单元的长度为2字节,偏移地址在bx中,段地址在ds中。
例子:比如AX=1234,BX=1000, DS:1000内容为1111
mov [bx],ax把AX的值赋予BX所指向的内存单元
那么执行后AX=1234,BX=1000,DS:1000的内容为1234
这种做法就叫做:寄存器间接寻址
二. 描述性符号():
为了描述上的简洁,我们将使用一个描述性的符号 “() ”来表示一个寄存器或一个内存单元中的内容。
例如:
(ax):表示寄存器ax中的内容。
(20000h)表示内存单元20000h中的内容,(?)括号中的表示物理地址。
((ds)*16+(bx)),如果ds中的内容为1230,bx中的内容为4567,则(1230*16+4567)可以理解为:
Ds中的内容作为段地址,bx中的内容作为偏移地址,这样形成的物理地址的内容。
为了加强理解,我们看看下面的如何描述:
n 我们看一下(X)的应用,比如:
n (1)ax中的内容为0010H,我们可以这样来描述:(ax)=0010H;
n (2)2000:1000 处的内容为0010H,我们可以这样来描述:(21000H)=0010H;
n (3)对于mov ax,[2]的功能,我们可以这样来描述:(ax)=((ds)*16+2);
n (4)对于mov [2],ax 的功能,我们可以这样来描述:((ds)*16+2)=(ax);
n (5)对于 add ax,2 的功能,我们可以这样来描述:(ax)=(ax)+2;
n (6)对于add ax,bx的功能,我们可以这样来描述:(ax)=(ax)+(bx);
n (7)对于push ax的功能,我们可以这样来描述:
(sp) = (sp)-2
((ss)*16+(sp))=(ax)
三. mov ax,2 那其机器代码是 B8 02 00,这个设计计算机指令,汇编语言教材第一章一般就讲这个,B8表示把后面的一个WORD存放到AX里面,比如MOV AL,1的机器指令是B0 01,B0就表示把后面的字节保存到AL里面,而MOV AH,1的机器指令是B4 01。
四.总结:现在我们通过一个实例,将上述的指令综合一下:
题目:从offffh:0006内存单元中取一个值,并相加三次
分析:(1)运算后的结果是否会超出dx所能存储的范围?
Ffff:0006 单元中的数是一个字节型的数据,范围在0~255之间,则用它和3相乘结果不会大于65535,可以在dx 中存放下
(2)我们用循环累加来实现乘法,用哪个寄存器进行累加?
我们将ffff:0006单元中的数赋值给ax,用dx进行累加。先设(dx)=0,然后做3次(dx)=(dx)+(ax)。
(3) ffff:0006单元是一个字节单元,ax是一个 16 位寄存器,数据长度不一样,如何赋值?
我们说的是“赋值”,就是说,让 ax 中的数据的值(数据的大小)和ffff:0006 单元中的数据的值(数据的大小)相等。
8 位数据01H和16位数据0001H的数据长度不一样,但它们的值是相等的。
n 那么我们如何赋值?
n 设ffff:0006单元中的数据是XXH,若要ax中的值和ffff:0006单元中的相等,ax中的数据应为00XXH。
所以,若实现ffff:0006单元向ax 赋值,我们应该令(ah)=0,(al)=(ffff6H)。
n 实现计算ffff:0006单元中的数乘以3,结果存储在dx中的程序代码。
代码:
assume cs:codesg
codesg segment
mov ax,0ffffh
mov ds,ax
mov bx,6
mov al,[bx]
mov ah,0
mov dx,0
mov cx,3
s:add dx,ax
loop s
mov ax,
int 21h
codesg ends
end
现在我们用debug来跟踪这个程序:
我们来分析一下:
1. 图中ds=165h,所以程序cs=
2. 同样的道理,此时指针也是指向程序的第一条指令,所以指针的段地址也为
3. 破解软件时,我们要先学会用g命令,为了使大家看得更清楚一点,我们将循环次数改为10:
我们可以使用g 0012,则会自动执行到偏移地址为0012处。
当我们遇到loop时,因为后面的循环次数太多,我们可以用p命令。也可以用g 0016
4. 同一种写法,不同的处理方式:
对于这种写mov al,[0],debug表示:把ds:0内存单元处的内容放入al.
源程序中表示:就是把数据0放入al中。
解决方法:1.用过渡寄存器,比如:bx,如下:
Mov bx,0
Mov ax,[bx]
2.Mov ax,ds:[0]
五.活用:loop和[bx]的联合应用
例子:计算ffff:0~ffff:b单元中的数据的和,结果存储在dx中。
n 我们还是先分析一下
1.运算后的结果是否会超出 dx 所能存储的范围?
ffff:0~ffff:b内存单元中的数据是字节型数据,范围在0~255之间,12个这样的数据相加,结果不会大于 65535 ,可以在dx中存放下。
2. 我们是否将 ffff:0~ffff:b中的数据直接累加到dx中?
当然不行,因为ffff:0~ffff:b中的数据是8位的,不能直接加到16位寄存器dx中。
3.我们能否将ffff:0~ffff:b中的数据累加到dl中,并设置(dh=0,从而实现累加到dx中的目标?
这也不行,因为dl是8位寄存器,能容纳的数据的范围在小 255 之间,ffff : 0~ffff:b中的数据也都是 8 位,如果仅向dl中累加12个 8 位数据,很有可能造成进位丢失。
4. 我们到底怎样将用ffff:0~ffff:b中的8位数据,累加到16位寄存器dx中?
我们将内存单元中的 8 位数据赋值到一个16位寄存器ax中,再将ax中的数据加到dx上,从而使两个运算对象的类型匹配并且结果不会超界。
分析后,我们现在来写出代码:
Mov ax,0ffffh
Mov ds,ax
Mov bx,0
Mov dx,0
Mov cx,0
S:Mov al,[bx]
Mov ah,0
Add dx,ax
Inc bx
请大家自行加上伟指令运行。
通过前面的介绍,我们知道了[寄存器]的含义以及各个寄存器之间如何灵活计算的方式,这是学习破解的基础,那么破解时入口地址的确认以及如何查看反汇编的数据段,代码段,栈段呢?看下面的几个基础点。
六.在一个段中存放数据、代码、栈,我们先来体会一下不使用多个段时的情况;
1. 考虑这样一个问题,编程计算以下8个数据的和,结果存在ax 寄存器中:
0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H
看下面的程序:
assume cs:codesg
codesg segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
mov bx,0
mov ax,0
mov cx,8
s: add ax,cs:[bx]
add bx,2
loop s
mov ax,
int 21h
codesg ends
end
n 解释一下,程序第一行中的 “dw”的含义是定义字型数据。dw即define word。
n 在这里,我们使用dw定义了8个字型数据(数据之间以逗号分隔),它们所占的内存空间的大小为16个字节
n 这8个数据的偏移地址是多少呢?
因为用dw定义的数据处于代码段的最开始,所以偏移地址为0,这8 个数据就在代码段的偏移0、2、4、6、8、A、C、E处。
程序运行时,它们的地址就是CS:0、CS:2、CS:4、CS:6、CS:8、CS:A、CS:C、CS:E。
2.如何让这个程序在编译后可以存系统中直接运行呢?我们可以在源程序中指明界序的入口所在.
n 用 Debug加 载后,我们可以将 IP 设置为10h,从而使CS:IP指向程序中的第一条指令。然后再用T命令、P命令、或者是G 命令执行。
n 可是这样一来,我们就必须用Debug 来执行程序。
程序 6.1 编译成可执行文件后,在系统中直接运行可能会出现问题,因为程序的入口处不是我们所希望执行的指令
n 我们在程序的第一条指令的前面加上了一个标号start,而这个标号在伪指令end的后面出现。end 除了通知编译器程序结束外,还可以通知编译器程序的入口在什么地方。
2.通过上面的分析,我们明白了在代码段中使用数据的情况,现在在来看看在代码段中使用栈的情况。
n 完成下面的程序,利用栈,将程序中定义的数据逆序存放。
assume cs:codesg
codesg segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
?
code ends
end
分析:
(1).程序运行时,定义的数据存放在cs:0~cs:15单元中,共8个字单元。依次将这8个字单元中的数据入栈,然后再依次出栈到这 8 个字单元中,从而实现数据的逆序存放。
(2).问题是,我们首先要有一段可当作栈的内存空间。如前所述,这段空间应该由系统来分配。我们可以在程序中通过定义数据来取得一段空间,然后将这段空间当作栈空间来用。
程序如下所示:
assume cs:codesg
codesg segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
dw 0,0,0,0,0,0,0,0
start:mov ax,cs
mov ss,ax
mov sp,32
mov bx,0
mov cx,8
s: push cs:[bx]
add bx,2
loop s
mov bx,0
mov cx,8
s0:pop cs:[bx]
add bx,2
loop s0
mov ax,
int 21h
codesg ends
end start
解释:(1).其中mov ax,cs
mov ss,ax
mov sp,32
我们要讲 cs:16 ~ cs:31 的内存空间当作栈来用,初始状态下栈为空,所以 ss:sp要指向栈底,则设置ss:sp指向cs:32
七。看下面的代码,实现了两个字符串变大写和变小写的方法。
1. 数据段是放在程序开头的,程序指令执行段是在数据段之后,所以数据段的首地址为:ds+10h.
2. 注意data,and,or ,[bx]的用法。
3. [bx]与[bx+idata]的用法。后者只是取出bx中的数值(即偏移地址)后+一个数=新的偏移地址而已。例如:
2000:1000 00 be 00 06 00 05
Mov ax,2000h
Mov ds,ax
Mov bx,1000h
Mov cx,[bx] 那么此时cx=00
Mov cx,[bx+1],那么此时cx=be
4.si与di,与bx相似,只是不能分成两个8位寄存器。所以在传输时,一般按字传。(比如将一个字符串复制到另外的空间地址)。
5.mov ax,[bx+si],[bx+si]此种方式与数组类似。一般si计数,然后通过si找新偏移地址的数据。例如:
Mov ax,2000h
Mov ds,ax
Mov bx,1000h
Mov si,0
Mov ax,[bx+si]
Inc si
6.[bx+si+idata]与[bx+si]类似。
最后总结一下:
n 如果我们比较一下前而用到的几种定位内存地址的方法(可称为寻址方式),就可以发现有以下几种方式:
n (1)[iata] 用一个常量来表示地址,可用于直接定位一个内存单元;
n (2)[bx]用一个变量来表示内存地址,可用于间接定位一个内存单元;
n (3)[bx+idata] 用一个变量和常量表示地址,可在一个起始地址的基础上用变量间接定位一个内存单元;
n (4)[bx+si]用两个变量表示地址;
n (5)[bx+si+idata] 用两个变量和一个常量表示地址。