call 和 ret 都是转移指令,它们都修改 IP 或同时修改 CS 和 IP。共同用来实现子程序。
ret 和 retf
ret 指令用栈中的数据,修改 IP 的内容,实现近转移。
retf 指令用栈中的数据,修改 CS 和 IP 的内容,实现远转移。
CPU 指令 ret 指令时,相当于:
CPU 指令 retf 指令时,相当于:
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| assume cs:code
stack segment db 16 dup (0) stack ends
code segment mov ax,4c00h int 21h start: mov ax,stack mov ss,ax mov sp,16 mov ax,0 push cs push ax mov bx,0 retf code ends
end start
|
call 指令
将当前的 IP 压栈后,转到「标号」处执行指令。
- (SP) = (SP) - 2, ((SS)*16 + (SP)) = (IP)
- (IP) = (IP) + 16 位位移
相当于:
机器指令中没有转移的目的地址,是相当于当前 IP 的转移位移
转移的目的地址在指令中的 call 指令
实现的是段间转移。CPU 执行指令时进行如下操作:
- (SP) = (SP) - 2
((SS)*16 + (SP)) = (CS) CS 入栈
(SP) = (SP) - 2
((SS)*16 + (SP)) = (IP) IP 入栈
- (CS) = 标号所在的段的地址
(IP) = 标号所在的段的偏移地址
相当于:
1 2 3
| push CS push IP jmp far ptr 标号
|
转移地址在寄存器中的 call 指令
当前 IP 入栈
(SP) = (SP) - 2
((SS)*16 + (SP)) = (IP)
(IP) = (16 位 reg)
转移地址在内存中的 call 指令
只修改 IP
相当于:
1 2
| push IP jmp word ptr 内存单元地址
|
同时修改 CS 和 IP
相当于:
1 2 3
| push CS push IP jmp dword ptr 内存单元地址
|
检测点 10.5
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| assume cs:code data segment dw 8 dup (0) data ends code segment start: mov ax,data mov ss,ax mov sp,16 mov word ptr ss:[0],offset s mov ss:[2],cs call dword ptr ss:[0] nop s: mov ax,offset s sub ax,ss mov bx,cs sub bx,ss:[0eh] mov ax,4c00h int 21h code ends end start
|
求 ax,bx 中的值。
ax = 1,bx = 0.
提示:
- 栈操作每次操作的单位是一个字,CS 地址是一个字,IP 地址是一个字。
- nop 占一个字节。
- 执行 call dword ptr 命令,会把 call 命令下一条的 CS、IP 入栈保存
call 和 ret 的配合使用
示例程序:
1 2 3 4 5 6 7 8 9 10 11 12 13
| assume cs:code code segment start: mov ax,1 mov cx,3 call s mov bx,ax mov ax,4c00h int 21h s: add ax,ax loop s ret code ends end start
|
可以写一个具有一定功能的程序段,称之为子程序,在需要的时候用 call 指令转去执行,执行完之后,子程序后面使用 ret 指令,再转移回 call 指令后面的代码继续执行。
子程序的框架:
mul 指令
mul 是乘法指令,使用 mul 做乘法时,需要注意以下两点:
- 两个相乘的数:要么都是 8 位,要么都是 16 位。
- 如果是 8 位,一个默认放在 AL,另一个放在 8 位 reg 或内存字节单元中
- 如果是 16 位,一个默认放在 AX,另一个放在 16 位 reg 或内存字单元中
- 结果:
- 如果是 8 位乘法,结果默认放在 AX 中
- 如果是 16 位乘法,结果高位默认放在 DX,低位放在 AX
指令格式:
先把第一个数放到 AX 或 AL,然后指定另一个数的位置:
内存单元可以用不同的寻址方式给出:
含义:(ax) = (al) * ((ds)*16+0)
含义:
(ax) = (ax) * ((ds) * 16 + (bx) + (si) +8) 结果的低 16 位
(dx) = (ax) * ((ds) * 16 + (bx) + (si) +8) 结果的高 16 位
计算 100 * 10
1 2 3
| mov al,100 mov bl,10 mul bl
|
结果:(ax) = 1000
计算 100 * 10000
1 2 3
| mov ax, 100 mov bx,10000 mul bx
|
结果:(ax) = 4240h,(dx) = 000fh。f4340h = 1000000
参数和结果传递的问题
用寄存器来存储参数和结果是最常用的方法。
计算一组数据的三次方,将结果保存在一组 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 28 29 30
| 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
|
批量数据的传递
寄存器数目有限,不能简单地用寄存器来存放多个需要传递的数据,对于返回值也有同样的问题。
我们可以批量将数据放到内存,然后将它们所在的内存空间的首地址放在寄存器中,传递给需要的子程序。对于具有批量数据的返回结果,也可用同样的方法。
将一个全是字母的字符串转化为大写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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 capital: and byte ptr [si], 11011111b inc si loop capital ret code ends end start
|
还有一种通用的方法是用栈来传递参数。
原理是,由调用者将需要传递给子程序的参数压入栈中,子程序从栈中取得参数。
1 2 3 4 5 6 7 8
| difcube: push bp mov bp,sp mov ax,[bp+4] sub ax,[bp+6] mul bp mul bp pop bp ret 4
|
ret n 指令的含义:
将 IP 地址从栈中弹出,恢复到 IP 寄存器,删掉栈顶的 n 个字节,从而将栈顶指针恢复成调用前的值。
调用上面代码的方法:
1 2 3 4 5
| mov ax,1 push ax,1 mov ax,3 push ax push ax\call difcube
|
寄存器冲突的问题
编写子程序时,要先将子程序要使用的寄存器内容保存一份到栈中,在返回之前,将栈中的内容再恢复到寄存器,避免子程序污染寄存器,影响主程序。记录现场,恢复现场。
编写子程序的标准框架:
1 2 3 4
| 子程序开始:子程序中使用的寄存器入栈 子程序内容 子程序中使用的寄存器出栈 返回(ret、retf)
|
实验 10 编写子程序
10.1 显示字符串
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| assume cs:code,ds:data data segment db 'Welcome to masm!',0 data ends
code segment start: mov dh,2 mov dl,1 mov cl,2 mov ax,data mov ds,ax mov si,0 call show_str mov ax,4c00h int 21h
show_str: push es push dx push ax push bx mov ax,0b800h mov es,ax dec dl mov al,160 mul dh mov bx,ax mov al,2 mul dl add bx,ax mov al,cl
s: mov cl,ds:[si] jcxz ok mov es:[bx+di],cl mov es:[bx+di+1],al inc si add di,2 loop s ok: pop bx pop ax pop dx pop es ret code ends end start
|
这个程序有两个细节,外部传入的行列参数都是从 1 开始的,在计算偏移量的时候,行、列要减一。
但考虑到这点,编写代码后有一个疑问,为什么 dh 是 1 的时候终端显示不出来内容呢?只有 dh 是 2 的时候才能在终端的第一行打印出内容,整个差了一行,为什么?
这个问题花费了我数个小时去探索,为此我还安装了纯 DOS 虚拟机环境。是程序写的有问题吗?偏移地址计算错误吗?是行列参数差一错误吗?
实际上是 DOS 系统自身的问题,去掉 mov ax,4c00h 和 int 21h 两条指令,再运行,就能够显示出来了。
在终端执行命令 exp10.exe 结束的时候,终端窗口的内容会向上滚动,DOS 实现这种效果也是通过读写 b800h ~ b8f9fh 的显存来完成的。可以猜测过程大概如下:
- 在终端输入
exp10.exe,点击回车后,DOS 读写显存,让终端内容向下滚动一行。
- DOS 将 CPU 执行权交给 exp10.exe。
- exp10.exe 读写显存,在终端窗口第一行显示文字。
- exp10.exe 将 CPU 执行权交给 DOS。
- DOS 读写显存,让终端内容向下滚动一行。
这样一来,我们在第一行显示的内容就让 DOS 滚动终端信息的动作冲掉了。
所以实际上我们要从第二行开始写入,这样 DOS 修改显存让终端信息整体向上移动一行后,我们显示的内容正好是输入参数指定的位置。
这个程序没有办法使用 debug.exe 来调试看效果,debug.exe 输出调试信息的基本原理就是修改显存。所谓“观测会影响实验结果”,颇有一些量子力学的味道。
排除所有不可能的因素之后,无论剩下的多么难以置信,那就是真相。
10.2 解决除法溢出的问题
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 29 30 31 32 33 34 35 36 37 38 39
| assume cs:code,ds:data,ss,stack data segment dd 1000000 data ends
stack segment db 128 dup (0) stack ends
code segment start: mov ax,data mov ds,ax mov ax,stack mov ss,ax mov sp,100
mov ax,ds:[0] mov dx,ds:[2] mov cx,10 push ax mov bp,sp call divdw mov ax,4c00h int 21h
divdw: mov ax,dx mov dx,0 div cx push ax mov ax,ss:[bp+0] div cx mov cx,dx pop dx ret code ends end start
|
10.3 数值显示
编写一个整数转字符串的程序。
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
| assume cs:code,ds:data,ss:stack data segment db 10 dup (0) data ends
stack segment db 128 dup (0) stack ends
code segment start: mov ax,data mov ds,ax mov ax,stack mov ss,ax mov sp,100 mov ax,12666 mov si,0 call dtoc
mov dh,8 mov dl,3 mov cl,2 call show_str
mov ax,4c00h int 21h
dtoc: push dx push cx push ax push si push bx
mov bx,0
s1: mov cx,10d mov dx,0
div cx mov cx,ax jcxz s2
add dx,30h push dx
inc bx jmp short s1
s2: add dx,30h push dx inc bx
mov cx,bx mov si,0
s3: pop ax
mov [si],al inc si
loop s3
pop bx pop si pop ax pop cx pop dx
ret
show_str: push es push dx push ax push bx mov ax,0b800h mov es,ax dec dl mov al,160 mul dh mov bx,ax mov al,2 mul dl add bx,ax mov al,cl
s: mov cl,ds:[si] jcxz ok mov es:[bx+di],cl mov es:[bx+di+1],al inc si add di,2 loop s ok: pop bx pop ax pop dx pop es ret code ends end start
|