《汇编语言》- 读书笔记 - 第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,1mov cx,3call smov bx,ax ;(bx)=?mov ax,4c00hint 21h
s: add ax,axloop sret
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,100mov bl,10mul bl
code ends
end start
parseInt('03E8', 16); // 1000
例2. 计算 100*10000
assume cs:code
code segment
start: mov ax,100mov bx,10000mul 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 segmentdw 1,2,3,4,5,6,7,8dd 0,0,0,0,0,0,0,0
data endscode segment
start: mov ax,data ; 设置数据段mov ds,axmov si,0 ;ds:si 指向第一组 word 单元mov di,16 ;ds:di 指向第二组 dword 单元mov cx,8 ; 设置循环次数 8s: mov bx,[si] ; 主程序,读取数据到 bx ("用 bx 传参")call cubemov [di],ax ; 先存低16位 "主程序拿到子程序放在 ax 中的返回值"mov [di].2,dx ; 再存高16位 "主程序拿到子程序放在 dx 中的返回值"add si,2 ;ds:si 指向下一个 word 单元add di,4 ;ds:di 指向下一个 dword 单元loop smov ax,4c00hint 21h; cube 子程序:计算 n 的 3 次方cube: mov ax,bx ; 子程序从("bx 读取传参")mul bxmul bxret ; 子程序返回 "(返回值)"在 "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 segmentdb 'conversation'
data endscode segment
start: mov ax,data ; 设置数据段mov ds,axmov si,0 ; ds:si 指向字符串(批量数据)所在空间的首地址mov cx,12 ; cx存放字符串的长度call capital ; 调用子程序mov ax,4c00hint 21h; 子程序:转大写
capital:and byte ptr [si],11011111b ; 转为大写字符inc siloop capital ; 循环处理字符ret
code ends
end start
10.12 寄存器冲突的问题
本节用一个子程序
举例,在主程序
和子程序
使用了同样的寄存器
,那么将产生冲突。
问题举例:
- 首先:主程序在
CX
中保存了循环次数。 - 然后:子程序中的循环计数也用到了
CX
。 - 结果:当从
子程序
返回主程序
时,主程序
的循环计数
已经丢失。程序无法按预期执行。
解决思路:
- 在
子程序
具体业务代码开始前,把会用到的寄存器
保存到栈
中。 - 在
子程序
返回前出栈
还原寄存器
。 - 注意
入栈
与出栈
时的顺序
。(后入的先出)
子程序的标准框架
最终得出编写子程序的标准框架如下:
子程序开始: 子程序中使用的寄存器入栈子程序内容子程序中使用的寄存器出栈返回(ret、retf)
改进后的子程序 capital
capital:push cxpush sichange:mov cl,[si]mov ch,0jcxz okand byte ptr [si],11011111binc sijmp short changeok:pop sipop cxret
关于大小写的相关知识,详见:第7章 7.4 大小写转换的问题
实验 10 编写子程序
《汇编语言》- 读书笔记 - 实验 10 编写子程序