《汇编语言》- 读书笔记 - 第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:先将栈顶的值弹出,送进IPPOP 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 IP2. jmp near ptr 标号 | IP | call label |
call far ptr 标号 | 按目标地址,实现段间转移。相当于:1. push CS2. push IP3. jmp far ptr 标号 | CS:IP | call far ptr label |
call 16位寄存器 | 转移地址在寄存器中,相当于:1. push IP2. jmp 16位寄存器 | IP | call ax |
call word ptr [内存] | 转移地址在内存中,实现段内转移。相当于:1. push IP2. jmp word ptr 内存单元地址 | IP | call word ptr ds:[0] |
call dword ptr [内存] | 转移地址在内存中,实现段间转移。相当于:1. push CS2. push IP3. 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 编写子程序