参数与返回值传递
子程序一般都要根据提供的参数处理一定的运算,处理后将结果(返回值)提供给调用者。处理参数和返回值传递的问题就是在处理存储子程序需要的值和产生的结果值。
例如,以下是一个子程序,它根据提供的N,用来计算N3:
; calculate N^3
; param N | (BX)
; return | (DX:AX) = N^3
cube:
mov ax, bx
mul bx
mul bx
ret
该子程序需要的参数从 BX 寄存器中获得,结果存储在 DX 和 AX 寄存器中。
以下程序利用该子程序计算 datas
段中第一组数据的立方,并将结果保存在后面一组 dword
单元中:
assume cs: codes, ds: datas
datas segment
dw 1, 2, 3, 4, 5, 6, 7, 8
dd 8 dup (0)
datas ends
codes segment
start:
mov ax, datas
mov ds, ax
mov si, 0 ; DS:[SI] points to 1st group of word units
mov di, 16 ; DS:[DI] points to 2nd group of dword units
mov cx, 8
calcupow:
mov bx, [si]
call cube
mov [di], ax
mov [di+2], dx
add si, 2 ; DS:[SI] points to next word unit
add di, 4 ; DS:[DI] points to next dword unit
loop calcupow
mov ax, 4c00h
int 21h
cube:
mov ax, bx
mul bx
mul bx
ret
codes ends
end start
批量数据的传递
有时候需要传递的参数很多,不可能简单地用寄存器来存放多个需要传递的数据。对于返回值,也有同样的问题。
在这种情况下,需要将批量数据放到内存中,然后将这一段内存空间的首地址放在寄存器中,传递给需要的子程序。对于具有批量数据的返回结果,也可以用同样的方法。
以下程序将 datas
段中的字符串转化为大写:
assume cs: codes, ds: datas
datas segment
db 'uppercase'
datas ends
codes segment
start:
mov ax, datas
mov ds, ax
mov si, 0 ; DS:[SI] points to string address
mov cx, 9 ; (CX) is the length of string
call capitalize
mov ax, 4c00h
int 21h
; convert string to capital
; param string | DS:[SI]
; param length | CX
; return |
capitalize:
and byte ptr [si], 11011111B
inc si
loop capitalize
ret
codes ends
end start
寄存器冲突的问题
在子程序中使用的寄存器,很可能在主程序中也要使用,造成寄存器使用上的冲突。
解决寄存器冲突的便捷方法是,在子程序的开始将子程序中所有用到的寄存器中的内容保存起来,在子程序返回前再恢复。
可以用栈来保存寄存器中的内容。
这样,以后编写子程序的标准框架变为:
sub:
push needed_register
; ...code...
pop needed_register
ret
要注意寄存器入栈和出栈的顺序。
以下程序用于将以0结尾的字符串(假设全是字母)中的所有字符转换为大写:
assume cs: codes, ds: datas
datas segment
db 'uppercase', 0h
datas ends
stacks segment
db 128 dup (0)
stacks ends
codes segment
start:
mov ax, datas
mov ds, ax
mov si, 0 ; DS:[SI] points to string address
call capitalize
mov ax, 4c00h
int 21h
; convert string to capital
; param string | DS:[SI]
; return |
capitalize:
push cx
push si
_capitalize:
mov cl, [si]
mov ch, 0
jcxz return
and byte ptr [si], 11011111B
inc si
jmp short _capitalize
return:
pop si
pop cx
ret
codes ends
end start
该程序相比上一个程序,使用0结尾配合 JCXZ 指令判断字符串是否到达结尾,从而无需传入字符串的长度。
RET与用栈传递参数
RET 指令用栈中的信息修改寄存器 IP ,并将 SP 的值+2。CPU执行 RET 指令时,相当于进行“ POP IP ”操作。
RET 指令还有一种用法,就是 RET n 。该种格式的指令除了用栈中的信息修改寄存器 IP ,并将 SP 的值+2以外,还会继续将 SP 的值加上 n 代表的立即数。
CPU执行 RET n
指令时,相当于进行“ POP IP
”和“ ADD SP, n
”操作。
用栈传递参数的技术和高级语言编译器的工作原理密切相关。用栈传递参数的原理十分简单,就是由调用者将需要传递给子程序的参数压入栈中,子程序从栈中取得参数。
以下子程序说明了这种参数传递的技术:
; calculate (a-b)^3
; param a | stack stores IP/a/b in turn
; param b | stack top ^
; return | DX:AX = (a-b)^3
diffcube:
push bp
mov bp, sp
mov ax, [bp+4]
sub ax, [bp+6]
mov bp, ax
mul bp
mul bp
pop bp
ret 4
因为用栈传递参数,因此在调用程序时要向栈中压入参数,子程序在返回的时候可以使用 RET n
指令将栈顶指针修改为调用前的值。调用上面的子程序之前,需要压入两个参数,所以用 ret 4
返回。