《汇编语言》- 读书笔记 - 第10章-CALL 和 RET 指令
- 10.1 ret 和 retf
- 检测点 10.1
- 10.2 call 指令
- 10.3 依据位移进行转移的 call 指令
- 检测点 10.2
- 10.4 转移的目的地址在指令中的 call 指令
- 检测点 10.3
- 10.5 转移地址在寄存器中的 call 指令
- 10.6 转移地址在内存中的 call 指令
- 检测点 10.5
- 10.7 call 和 ret 的配合使用
- 问题 10.1
- 10.8 mul 指令
- 例1. 计算 100*10
- 例2. 计算 100*10000
- 10.9 模块化程序设计
- 10.10 参数和结果传递的问题
- 10.11 批量数据的传递
- 10.12 寄存器冲突的问题
- 问题举例:
- 解决思路:
- 子程序的标准框架
- 改进后的子程序 capital
- 实验 10 编写子程序
call
和
ret
指令都是转移指令,都有修改
IP
和
CS:IP
两个版本。
call
和
ret
指令共同支撑了汇编语言中的
模块化设计
实现。
call
指令用于
调用子程序
,它将
返回地址压入
堆栈并跳转至
子程序的
入口地址。
ret
指令在
子程序执行
完毕
后从
堆栈中弹出
返回地址
,并跳转回
主程序
的
调用点
继续执行。
10.1 ret 和 retf
指令 | 修改CS | 修改IP | 行为 | 用途 |
---|---|---|---|---|
ret (Return from Procedure) | ✅ | POP IP :将栈顶的值弹出,并送进IP | 从子程序返回。 | |
retf (Return from Procedure Far) | ✅ | ✅ | POP IP :先 将栈顶的值弹出,送进IP POP CS :再 将从栈中弹出一个值,送进CS | 从远过程(far subroutine)返回 |
检测点 10.1
《汇编语言》- 读书笔记 - 各章检测点归档 - 检测点 10.1
10.2 call 指令
CPU 执行 call
指令时,进行两步操作:
- 将当前的
IP
或CS
和IP
压入栈中; - 转移。(与
jmp
唯一的不同在于没有短转移 : jmp short 标号
)
命令 | 说明 | 修改的 寄存器 | 例子(假设有标号叫 label ) |
---|---|---|---|
call 标号 | 按位移 跳转,实现段内 转移。位移范围在:-32768 ~ 32767 。将当前 IP 压栈后,转到标号 处执行指令。相当于:1. push IP 2. jmp near ptr 标号 | IP | call label |
call far ptr 标号 | 按目标地址 ,实现段间 转移。相当于:1. push CS 2. push IP 3. jmp far ptr 标号 | CS:IP | call far ptr label |
call 16位寄存器 | 转移地址在寄存器 中,相当于:1. push IP 2. jmp 16位寄存器 | IP | call ax |
call word ptr [内存] | 转移地址在内存 中,实现段内 转移。相当于:1. push IP 2. jmp word ptr 内存单元地址 | IP | call word ptr ds:[0] |
call dword ptr [内存] | 转移地址在内存 中,实现段间 转移。相当于:1. push CS 2. push IP 3. jmp dword ptr 内存单元地址 | IP | call dword ptr ds:[0] |
10.3 依据位移进行转移的 call 指令
检测点 10.2
《汇编语言》- 读书笔记 - 各章检测点归档 - 检测点 10.2
10.4 转移的目的地址在指令中的 call 指令
检测点 10.3
《汇编语言》- 读书笔记 - 各章检测点归档 - 检测点 10.3
10.5 转移地址在寄存器中的 call 指令
## 检测点 10.4
《汇编语言》- 读书笔记 - 各章检测点归档 - 检测点 10.4
10.6 转移地址在内存中的 call 指令
检测点 10.5
《汇编语言》- 读书笔记 - 各章检测点归档 - 检测点 10.5
10.7 call 和 ret 的配合使用
问题 10.1
下面程序返回前,bx 中的值是多少?
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
call s
会将当前IP
存入栈中,跳到s
处执行。s
标号这里是一个loop
循环cx
初始为3
所以会循环3
次。
2.1. 第一次add ax, ax
, ax = 1+1 = 2;
2.2. 第二次add ax, ax
, ax = 2+2 = 4;
2.3. 第三次add ax, ax
, ax = 4+4 = 8;loop
循环结束后ret
返回call s
处,继续执行它下面的指令。此时ax = 8
。
s
到ret
这段实现的是计算2
的n
次方,n
由cx
提供。- 所以
mov bx, ax
的结果是bx = 8
。 - 下一句
mov ax,4c00h
退出程序,最终程序返回前bx
中的值是8
。
通过对问题 10.1
的探讨,引出:利用 call
和 ret
来实现子程序的机制。
子程序的框架如下:
标号:
指令
ret
具有子程序的源程序的框架如下:
10.8 mul 指令
mul
是乘法指令。两个相乘的数,要么都是8位,要么都是 16 位
。
-
乘数:可以是
8
或16
位。
1.1. 但两个乘数必须都是 8位,或都是16位。(不能一8位,一个16位)
1.2. 如果是8位乘法
:一个乘数默认在AL
中,另一个在8位寄存器
或内存【字节】单元
中。
1.3. 如果是16位乘法
:一个乘数默认在AX
中,另一个在16位寄存器
或内存【字】单元
中。 -
结果:
2.1.8位乘法
:结果在AX
。
2.2.16位乘法
:结果高16位
在DX
,低16位
在AX
。
乘法位数 | 乘数A | 乘数B | 结果 | 例子 |
---|---|---|---|---|
8位乘法 | AL | ah | bl | bh | cl | ch | dl | dh | [字节单元] | AX | mul bl mul byte ptr ds:[0] |
16位乘法 | AX | bx | cx | dx | [字单元] | DX AX | mul bx mul word ptr [bx+si+8] |
例1. 计算 100*10
assume cs:code
code segment
start:
mov al,100
mov bl,10
mul bl
code ends
end start
parseInt('03E8', 16); // 1000
例2. 计算 100*10000
assume cs:code
code segment
start:
mov ax,100
mov bx,10000
mul bx
code ends
end start
parseInt('F4240', 16); // 1000_000
10.9 模块化程序设计
模块化设计
在汇编语言中至关重要
,通过拆解复杂问题
为相互关联的子问题
。call
和ret
指令支持模块化编程
,分别用于调用
和返回
子程序。- 子程序利用这两指令实现
功能独立
与逻辑分离
,便于解决复杂问题。
总之,call
和 ret
提供了实现子程序的基础,以解决复杂的编程问题。什么提高代码可读性、可维护性和复用性,布啦布啦布啦。。。
10.10 参数和结果传递的问题
- 在设计
函数
经常要考虑的就是怎么传参
、怎么返回
值。
1.2. 优先使用寄存器,比较方便。寄存器不够用,就使用内存。 - 注释要写清楚。起码以后自己要能看懂(不要高估和曾经那个自己之间的默契)
编程,计算 data
段中第一组数据的3次方,结果保存在后面一组 dword
单元中。
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 ;ds:si 指向第一组 word 单元
mov di,16 ;ds:di 指向第二组 dword 单元
mov cx,8 ; 设置循环次数 8
s: mov bx,[si] ; 主程序,读取数据到 bx ("用 bx 传参")
call cube
mov [di],ax ; 先存低16位 "主程序拿到子程序放在 ax 中的返回值"
mov [di].2,dx ; 再存高16位 "主程序拿到子程序放在 dx 中的返回值"
add si,2 ;ds:si 指向下一个 word 单元
add di,4 ;ds:di 指向下一个 dword 单元
loop s
mov ax,4c00h
int 21h
; cube 子程序:计算 n 的 3 次方
cube: mov ax,bx ; 子程序从("bx 读取传参")
mul bx
mul bx
ret ; 子程序返回 "(返回值)"在 "ax,dx" 中
code ends
end start
计算 3 次方的数 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
内存中结果 低位在前,高位在后 (高16位都是0忽略) | 01 00 | 08 00 | 1B 00 | 40 00 | 7D 00 | D8 00 | 57 01 | 00 02 |
调整一下顺序 | 0001 | 0008 | 001B | 0040 | 007D | 00D8 | 0157 | 02 00 |
转成10进制 | 1 | 8 | 27 | 64 | 125 | 216 | 343 | 512 |
10.11 批量数据的传递
数据大了就需要用内存
来传参
和返回
了。
寄存器用来传递内存地址。
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 ; 转为大写字符
inc si
loop capital ; 循环处理字符
ret
code ends
end start
10.12 寄存器冲突的问题
本节用一个子程序
举例,在主程序
和子程序
使用了同样的寄存器
,那么将产生冲突。
问题举例:
- 首先:主程序在
CX
中保存了循环次数。 - 然后:子程序中的循环计数也用到了
CX
。 - 结果:当从
子程序
返回主程序
时,主程序
的循环计数
已经丢失。程序无法按预期执行。
解决思路:
- 在
子程序
具体业务代码开始前,把会用到的寄存器
保存到栈
中。 - 在
子程序
返回前出栈
还原寄存器
。 - 注意
入栈
与出栈
时的顺序
。(后入的先出)
子程序的标准框架
最终得出编写子程序的标准框架如下:
子程序开始: 子程序中使用的寄存器入栈
子程序内容
子程序中使用的寄存器出栈
返回(ret、retf)
改进后的子程序 capital
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
关于大小写的相关知识,详见:第7章 7.4 大小写转换的问题
实验 10 编写子程序
《汇编语言》- 读书笔记 - 实验 10 编写子程序