引言

1.程序之间的加载和返回

2.call和ret指令都是转移指令,它们大都修改IP,或者同时修改CS和IP

3.它们经常被同用来实现自程序的设计

1.ret和retf 指令

1.ret指令是用栈中的数据,修改IP的内容,从而实现进转移

2.在CPU执行ret指令的时候

1
2
3
4
5
6
1.(IP)=((SS)*16+(SP))
2.(sp)=(sp)+2
IP中的值变成ss:[sp]中的
sp+2【】

pop ip

3.retf指令用栈中的数据,修改CS和IP的内容,从而实现远转移

4.在CPU执行retf指令的时候

1
2
3
4
5
6
7
8
9
10
1.
(IP)=((ss)*16+(sp))
(sp)=(sp)+2
2.
(cs)=((ss)*16+(sp))
(sp)=(sp)+2
低地址放的是ip,高地址放cs,都是字型单位

pop ip
pop cs

2.call指令

1.call指令经常和ret指令配合使用,因此CPU执行call指令

1
2
1.当前的IP或者CS和IP压入栈中
2.转移(jmp)

2.call指令不能实现段转移,除此之外,call指令实现转移的方法和jmp指令的原理相同

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

1.格式:call 标号

1
2
3
4
将当前的IP压入栈后,转移到标号处指令指令
相当于:
push IP
jmp near ptr 标号

2.CPU

1
2
3
4
5
6
7
8
1.  (SP)=(SP)-2
((ss)*16+(sp))=(IP);这一步感觉就像是:push 当前的IP[这么理解就错了]【它是push call 指令的下一条指令的IP】
2.(IP)=(IP)+16位位移
;这里和jmp的依据位移进行转移差不多也就是
“标号”处的地址-jmp指令后的第一个字节的地址;相互减
这里必须是16位的,不能是8位的哦
16位的范围
-32768~32767

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

1.call far ptr 标号

1
2
3
4
5
6
7
8
9
10
11
12
1.
(sp)=(sp)-2
((ss)*16+(sp))=(cs)
(sp)=(sp-2)
((ss)*16+(sp))=(ip)
2.
(cs)=标号处的段地址
(ip)=标号所在的偏移地址
3.
push CS
push IP
jmp far ptr 标号

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

1.格式:call 16位寄存器

2.功能

1
2
3
4
5
6
()=(sp)-2
((ss)*16+(sp))=(ip)
(ip)=(16位寄存器)

push IP
jmp 16位寄存器

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

1.call word ptr 内存单元地址

安装字型的数据来索引【短转移】

2.call dword ptr 内存单元地址

高cs 低 ip

7.call和ret的配合使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
assume cs:code

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

让我们来看看程序的主要执行过程

1,前三条指令执行后,栈的情况如下:

image-20230413204516225

2.call 指令读入后,(IP)=00EH,cpu指令缓冲器中的代码是 B8 05 00

cpu执行B8 05 00;首先栈中的变化为:

image-20230413204646240

然后,(IP)=(IP)+0005=0013H

3.CPU从cs:0013H处(即标号s处)开始执行

4.ret指令读入后:(IP)=0016H【IP永远都是指向的下一个指令,将要执行的IP】,cpu指令缓冲区中的代码为C3

​ 当CPU执行C3,相当于进行pop IP,执行后,栈中的情况为

image-20230413205239588

(IP)=000EH

5.CPU回到cs:000EH处(即call指令后面的指令处)继续执行

从上面的讨论中我们发现,可以写一个具有一定功能的程序段,我们称其为子程序,在需要的时候,用call指令转去执行

可是执行完子程序后,如何让CPU接着call指令向下执行?

没错就是ret

call指令后面的指令的地址存储在栈中,所以可以在子程序后面使用ret指令,用栈中的数据设置IP的值,从而转到call指令后面的代码继续执行

这样,我们可以利用call和ret来实现子程序的机制【函数】

8.mul 指令

1.因下面要用到,所以我们就来介绍一下mul指令,mul是乘法指令,使用mul做乘法的时候

1
2
3
1.相乘的2个数,要么都是8位,要么都是16位
8位:al中和8位寄存器或内存字节单元中
16位:ax中和16位寄存器或内存字单元中

2.结果

1
2
8位:AX中
16位:DX(高位)和AX(低位)中

3.格式

1
2
mul reg【寄存器】
mul 内存单元

1.mul byte ptr ds:[0]

1
2
3
8位的
含义:(ax)=(al)*((ds)*16+0)
乘数一个由mul给处一个是在al中

2.mul word ptr [bx+si+8]

1
2
3
4
5
16位:
含义位:
(ax)=(ax)*((ds)*16+(bx)+8)结果的低16位
(dx)=(ax)*((ds)*16+(bx)+8)结果的高16位
乘数一个由mul给出一个由ax中

比如:100*10

1
2
3
mov al,100
mov bl,10
mul bl

比如:100*10000

1
2
3
mov ax,100
mov bx,10000
mul bx

9.模块化程序设计

从上面我们看到,call和ret指令共同支持了汇编语言编程中的模块化设计,在实际编程中,程序的模块化是必不可少的

用call和ret指令==【面对过程的思想】

10.参数和结果传递的问题

子程序一般都要根据提供的参数处理一定的事务,处理后,将结果(返回值)提供给调用者

其实,我们讨论参数和返回值传递的问题,实际上是在探讨,应该如何存储子程序需要的参数和产生的返回值

我们设计一个子程序,可以根据提供的N,来计算N的3次方

1
2
3
1.我们将这个参数N存储在什么地方?
很显然,用寄存器来存储。可以将参数放到bx中
2.计算得到的数值,我们存储在什么地方?
1
2
3
4
cube:	mov ax,bx
mul bx
mul bx
ret

11.批量数据的传递

前面的例程中,子程序cube只有一个参数放在bx中。如果有两个参数,那么可以用两个寄存器来放,可是如果需要传递的数据有3个4个甚至多到N个呢,我们应该如何存放呢?

这个时候,我们将数据放在内存中,然后它们所在内存空间的首地址放在寄存器中,传递给需要的子程序。对于具有批量数据的返回结果,也可用同样的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
assume cs:code

data segment
db 'conversation'
data ends

code segment
start: mov ax,data
mov ds,ax
mov si,0 ;ds:[si]指向字符串(批量数据)所在空间的首地址
mov cx,12 ;cx 存放字符串的长度
call capital
mov ax,4c00H
int 21h

capital:and byte ptr [si],11011111B ;将ds:si所指单元中的字母转化为大写
inc si ;ds:si 指向下一个单元
loop capital
ret
code ends
end start

注意,除了用寄存器传递参数外,还有一种通用的方法是用栈来传递参数。

12.寄存器冲突的问题[10.12.play]

设计一个子程序,功能:将一个全是字母,以0结尾的字符串,转化为大写

程序要处理的字符串以0作为结尾符,这个字符串可以定义

1
db 'conversation',0

应用这个子程序,字符串的内容后面一定要有一个 0 ,标记字符串的结束。子程序可以依次读取每一个字符进行检测,如果不是0 ,就进行大写的转化;如果是0,就结束处理。由于可通过检测0而知道是否处理完这整个字符串,所以子程序不需要字符串的长度作为参数。可以用jcxz来检测0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
capital:	push cx
push si

mov cl,[si]
mov ch,0
jcxz ok
and byte ptr [si],11011111B
inc si
jmp short capital

ok: pop si
pop cx
ret
这个就是说cl是data中的数据,当访问到0的时候就去运行了jcxz

实验10

编写子程序

1
EXP 自己查看

自我总结一下最近学的把

1.乘法:

1
2
3
4
5
首先乘法:mul 乘数2
乘数1,是在寄存器或者内存单元中
乘数2如果是
8位:得到的是一个16位的数,放在ax中
16位:得到的是一个32位的数,放在dx【高】和ax【低】

2.除法:

1
2
3
4
5
6
7
8
9
10
11
然后是除法:div 除数
被除数可能是ax也可能是dx和ax

当除数是8位的时候,就调用的是ax
当除数是16位的时候,就调用dx和ax

8位的时候,al中放的是商 ah中放的是余数
16位的时候,ax中放的是商 dx中放的是余数

这个8位还是16位
看的是寄存器或者是内存空间的大小

3.循环的使用

1
2
3
4
1.已知或者能求出循环次数的循环
用 cx 和 loop
2.未知循环次数的循环
用cl和ch的结合 或者 cx 和jcxz

loop 如何使用呢?

1
2
3
4
这个很简单,设定循环的次数,cx
写入代码
loop 标号
就好了

jcxz 如何使用呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
这个比较复杂,我已知2种办法
1.对于 对字符串的处理【这里需要设置字符串的结束条件是0】
这样可以设置
s: mov ch,0
mov cl,ds:[si]
jcxz 标号【可以跳出去,这里是跳出循环】
代码
jmp short s【写了这个过后,它就会一直循环到标号s处,知道某个条件也就是cl=0的时候就会从jcxz挑走】

2.对于除法,需要取出某个数种的每一个数的时候
可以这样设置
s: div 某个16位 mov ch,0
mov cx,ax div 某个8位
mov cl,al

jcxz 标号

jmp short s

总的来说,在标号和 jmp short 标号 中间的都是循环语句,在中间的jcxz在满足条件的时候才会跳出去