前言

从0开始的汇编语言系列,选用的参考书籍是清华大学出版社,王爽老师的《汇编语言第四版》。该系列属于博主的笔记系列,文中会采用一些书中的例子,图片以及思考题供读者阅读,如需详细学习汇编语言可以购入一本,谢谢。

学习之前我们做如下约定(随着学习深入还会出现新的约定):

  1. 十六进制数均以H结尾
  2. 使用8086CPU作为案例
  3. 我们使用(地址或寄存器名称)表示一个寄存器或一个内存单元的内容,()内地址是且一定是物理地址
  4. 我们将idata视作常量
  5. 我们以reg表示一个寄存器包括ax、ah、sp、bp、si、di等,sreg表示一个段寄存器包括ds、ss、cs、es。

话不多说我们马上开始。

ret和retf

我们书接上回,上一篇我们学习了一些转移指令的原理,接下来我们继续学习一些新的转移指令:

ret指令用栈中的数据,修改IP的内容,从而实现近转移,当CPU执行ret指令时,进行下面两步操作:(IP)=((SS)×16+(SP));(SP)=(SP)+2。用汇编语法来解释ref指令相当于进行了pop IP。

retf指令用栈中的数据,修改CS和IP的内容,从而实现远转移,当CPU执行retf指令时,进行下面四步操作:(IP)=((SS)×16+(SP));(SP)=(SP)+2;(CS)=((SS)×16+(SP));(SP)=(SP)+2。用汇编语法来解释ref指令相当于进行了pop IP,pop CS。

call指令

call指令也是一种转移指令,当CPU执行call指令的时候,进行两步操作:将下一条指令的IP或CS和IP压入栈中;转移。call指令不能实现短转移,除此之外,call指令实现转移的方法和jmp指令的原理相同。接下来我们仔细介绍一下。

依据位移进行转移的call指令

首先,看一下call指令的格式:call 标号,将下一条指令IP压入栈中后,转到标号处执行指令。也就是这么一个过程:(SP)=(SP)-2;((SS)×16+(SP))=(IP);(IP)=(IP)+16位位移。其中16位位移的计算方法是:位移=标号处偏移地址-call指令下一条指令的偏移地址,结果用补码表示,在编译程序编译时算出。当然了16位位移是有取值范围的,取值范围是-32768~32767,用补码表示。用汇编语法来解释call指令相当于进行了push IP;jmp near ptr 标号。

转移的目的地址在指令中的call指令

首先,还是看一下call指令的格式:call far ptr 标号,将下一条指令CS和IP压入栈中后,转到标号处执行指令。也就是这么一个过程:(SP)=(SP)-2;((SS)×16+(SP))=(CS);(SP)=(SP)-2;((SS)×16+(SP))=(IP);(IP)=(IP)+16位位移;(CS)=标号所在段的段地址;(IP)=标号在段中的偏移地址。这样的call指令实现了段间转移,用汇编语法来解释call指令相当于进行了push CS;push IP;jmp far ptr 标号。

转移地址在寄存器中的call指令

这个call指令的格式是:call 16位reg。它的过程是:(SP)=(SP)-2;((SS)×16+(SP))=(IP);(IP)=(16位reg),用汇编语法来解释call指令相当于进行了push IP;jmp 16位reg。

转移地址在内存中的call指令

call指令中的转移地址也可以在内存之中,它也分为两种格式:

call word ptr 内存单元地址

用汇编语言解释这句指令的话,那么当CPU执行这句指令的时候,相当于进行了:push IP;jmp word ptr 内存单元地址。

call dword ptr 内存单元地址

用汇编语言解释这句指令的话,那么当CPU执行这句指令的时候,相当于进行了:push CS;push IP;jmp dword ptr 内存单元地址。

call和ret的配合使用

我们学会了ret指令也学会了call指令,学这两个指令的目的是为了实现子程序的机制,什么是子程序呢?直观的来说就是我们在高级语言中写的函数,怎么样是不是一下子就感受到这两个指令的强大了,事不宜迟,我们马上开始。 我们先来看一个程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
assume cs:code				内存中的情况(假设程序从内存1000:0处装入)

stack segment
db 8 dup (0) 1000:0000 00 00 00 00 00 00 00 00
db 8 dup (0) 1000:0008 00 00 00 00 00 00 00 00
stack ends

code segment
start: mov ax,stack 1001:0000 B8 00 10
mov ss,ax 1001:0003 8E D0
mov sp,16 1001:0005 BC 10 00
mov ax,1000 1001:0008 B8 E8 03
call s 1001:000B E8 05 00
mov ax,4c00H 1001:000E B8 00 4C
int 21H 1001:0011 CD 21
s: add ax,ax 1001:0013 03 C0
ret 1001:0015 C3
code ends
end start

程序中给出了内存中的情况,我们结合内存的情况一点点分析整个程序做的事情。从start开始前三行执行后,栈的情况如下:

1000:0000 00 00 00 00 00 00 00 00

此时SS:SP指向栈底,当call指令读入后,(IP)=000EH,CPU指令缓冲器中的代码为:E8 05 00(call s)

程序执行完call s后,栈的情况变为

1000:0000 00 00 00 00 00 00 0E 00

此时SS:SP指向0E也就是倒数第二个字节,然后(IP)=(IP)+0005=0013H。之后CPU从CS:0013H处(即标号s处)开始执行,ret指令读入后(IP)=0016H,CPU指令缓存器中的代码为:C3 (ret)

程序执行完C3后,栈的情况变为

1000:0000 00 00 00 00 00 00 0E 00

此时SS:SP指向栈底,之后CPU回到CS:000EH处(即call指令后面的指令处)继续执行。

怎么样,是不是有点像我们在高级程序中做的那个样子,写一个函数,当我们需要它的时候就调用它,不需要的时候我们就不调用它。我们给出一个子程序的源程序的框架:

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
assume cs:code
code segment
main:
·
·
·
call sub1 调用子程序sub1
·
·
·
mov ax,4C00H
int 21H
sub1:
·
·
·
call sub2 调用子程序sub2
·
·
·
ret 子程序返回
sub2:
·
·
·
ret 子程序返回
code ends
end main

怎么样现在的汇编语言是不是更有我们高级程序语言的模样啦!

mul指令

我们这里介绍一下mul指令,mul是乘法指令,在使用mul做乘法时要注意两点:

  1. 两个相乘的数:要不都是8位,要不都是16位。如果都是8位那么一个默认放在AL中,另一个默认放在8位reg或者内存字节单元中。如果都是16位那么一个默认放在AX中,另一个默认放在16位reg或者内存字单元中。
  2. 结果:如果8位乘法,结果默认在AX中;如果是16位乘法那么,结果高位默认在dx中,低位在ax中。

格式如下:

  • mul reg
  • mul 内存单元

内存单元可以通过不同的寻址方式给出,比如:

mul byte ptr ds:[0] 含义是(AX)=(AL)×((DS)×16+0);mul word ptr [BX+SI+8]含义是(AX)=(AX)×((DS)×16+(BX)+(SI)+8)结果的低16位;(DX)=(AX)×((DS)×16+(BX)+(SI)+8)结果的高16位。

参数与结果传递的问题

在高级语言中,我们都知道一个函数由返回值类型,函数名称、参数列表组成,我们将参数交给函数,函数在进行了指定的操作后,将结果交付给我们。在我们的汇编语言中也会使用这种模块化的程序设计。

我们现在思考这样的问题,现在我们有一个数N,设计一个子程序计算N的三次幂。这样一来我们就有了两个问题,参数N应该存放在哪里?得到的结果要存放在哪?很显然,可以用寄存器来存储,可以将参数放到BX中;因为子程序中要计算N的3次幂,所以可以使用多个mul指令,为了方便,可将结果存放到dx和ax中,我们来看程序,这个程序计算了data段第一组数据的3次方,结果保存到后一组的dword单元中:

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
assume cs:code
data segment
dw 1,2,3,4,5,6,7,8
dd 0,0,0,0,0,0,0,0
data ends

code segment
start: mov ax,data
mov ds,ax
mov si,0
mov di,16
mov cx,8
s: mov bx,[si]
call cube
mov [di],ax
mov [di].2,dx
add si,2
add di,4
loop s
mov ax,4c00H
int 21H
cube: mov ax,bx
mul bx
mul bx
ret
code ends
end start

批量数据的传递

上面的程序中子程序cube只有一个参数,放在bx中。如果有两个参数,那么可以使用两个寄存器来放,可是如果需要传递3个,4个,N个怎么办?显然一味地使用寄存器是不可靠的。这种时候,我们将批量的数据放到内存之中,然后将它们所在的内存空间的首地址放在寄存器中,传递给需要的子程序,同样需要返回多个数据,我们也这样做。我们写一个将字符串转为大写的程序,体验一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
assume cs:code
data segment
db 'conversation'
data ends

code segment
start: mov ax,data
mov ds,ax
mov si,0
mov cx,12
call capital
mov ax,4c00H
int 21H
capital:and byte ptr [si],11011111B
inc si
loop capital
ret
code ends
end start

除了使用寄存器传递参数,我们还有更通用的做法,使用栈来传递参数,我们接下来结合C语言的函数调用来看一下使用栈传递参数的思想,我们设定一个场景,我们要设计一个子程序计算(a-b)的3次幂,a和b是字型数据,我们来看用到的程序片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
	mov ax,1
push ax
mov ax,3
push ax
call difcube
difcube:push bp
mov bp,sp
mov ax,[bp+4]
sub ax,[bp+6]
mov bp,ax
mul bp
mul bp
pop bp
ret 4

其中ret idata的含义用汇编语法描述为:

pop IP;add sp,idata

我们来看看栈的变化,假设栈的初始情况如下:

1000:0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

此时SS:SP指向栈底。

当执行mov ax,1~push ax指令后,栈的情况为:

1000:0000 00 00 00 00 00 00 00 00 00 00 00 00 03 00 01 00

此时SS:SP指向03。

当执行call difcube指令后,栈的情况为:

1000:0000 00 00 00 00 00 00 00 00 00 00 XP XI 03 00 01 00

此时SS:SP指向XI(XIXPH即为call指令的下一条指令IP)。

执行push bp指令后,栈的情况为:

1000:0000 00 00 00 00 00 00 00 00 XB XA XI XP 03 00 01 00

此时SS:SP指向XB。

执行mov bp,sp后,SS:BP指向1000:8

之后执行mov ax,[bp+4]将栈中a的值送入ax中,sub ax,[bp+6]减去栈中b的值,mov bp,ax将ax中的值赋给bp,mul bp计算三次幂。

执行pop bp指令后,栈的情况为:

1000:0000 00 00 00 00 00 00 00 00 XB XA XI XP 03 00 01 00

此时SS:SP指向XI。

执行ret 4指令后,栈的情况为:

1000:0000 00 00 00 00 00 00 00 00 XB XA XI XP 03 00 01 00

此时SS:SP指向栈底。

我们接下来通过一个C语言程序编译后的汇编语言程序,看一下栈在参数传递中的应用,在高级语言中,局部变量也在栈中储存。

1
2
3
4
5
6
7
8
9
10
11
12
void add(int a,int b,int c)
{
c=a+b;
}
int main()
{
int a=1;
int b=2;
int c=0;
add(a,b,c);
c++;
}

编译后的汇编程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mov bp,sp
sub sp,6
mov word ptr [bp-6],0001
mov word ptr [bp-4],0002
mov word ptr [bp-2],0000
push [bp-2]
push [bp-4]
push [bp-6]
call ADDR
add sp,6
inc word ptr [bp-2]
ADDR:
push bp
mov bp,sp
mov ax,[bp+4]
add ax,[bp+6]
mov [bp+8],ax
mov sp,bp
pop bp
ret

尝试画出栈的情况变化,理解栈在参数传递中的作用。

寄存器冲突问题

现在你已经学会了子程序的编写,那么我们思考一个问题当我们循环调用一个子程序时,且子程序中使用了寄存器CX会发生什么?这个问题,我们之前遇到过类似的,当使用嵌套循环时也会出现这样的问题,内层循环更改了寄存器CX使得,外层循环出现了问题。同样,我们的子程序修改了CX使得循环出现了问题,我们要如何解决呢?没错还是使用栈来解决,我们在进入子程序后,立马将CX的值压入栈中,待程序完成后从栈中取出CX的值,不只是CX,只要是主程序用到,子程序重复使用的寄存器,我们都要这样处理一下,当然了要注意出栈入栈的顺序。