传递数据的第二种常用方法是通过堆栈:先将要传送的数据推入堆栈,再调用子过程,子过程从堆栈中取得所需的数据进行处理。请看下面的程序:
STRINGUP data segment assume ds:data mess db 'welcome to pc world',0dh,0ah,24h ;定义一个全部为小写的字符串 data ends code segment assume cs:code main proc far mov ax,data ;初始化DS寄存器指向数据段 mov ds,ax mov dx,offset mess ;DX寄存器指向字符串首地址 mov ah,9 ;选择DOS API的09功能 int 21h ;显示字符串MESS push dx ;通过堆栈传送字符串首地址 call change ;调用CHANGE子过程 mov ah,9 ;选择DOS API的09功能 int 21h ;输出大写的字符串 mov ah,4ch ;选择DOS API的4CH功能 int 21h ;结束进程 main endp change proc near ;CHANGE子过程 push bp ;保存BP寄存器至堆栈 mov bp,sp ;BP寄存器装入当前堆栈指针 mov bx,[bp+4] ;从堆栈中取得字符串的首地址 mov cx,19 ;转换19个字符 chloop: mov al,byte ptr [bx] ;从字符串中取一个字符 cmp al,20h ;是空格吗? jz continue ;是空格则转CONTINUE
CPU执行此指令时会把AL中的数作为偏移量从BX指向的表中取出一字节数据并以此取代AL中的数据。这条指令有些绕,应用起来也有些麻烦:
sub al,20h ;将小写字母转换成大写 mov [bx],al ;将大写字母重新存入字符串 continue: inc bx ;字符串指针加1 loop chloop ;处理下一个字符 pop bp ;恢复BP寄存器的原值 ret 2 ;返回主过程,并清空堆栈 change endp code ends end main
这个程序演示了如何将小写字母转换为大写字母。在主过程中使用了"PUSH DX"指令将DX寄存器中存储的字符串首地址压入堆栈,然后调用CHANGE子过程。那么CHANGE子过程又是如何得到字符串地址的呢?
首先可以肯定地说直接用"POP"指令不可能从堆栈中取得正确的字符串首地址,这是因为执行"CALL"指令时堆栈中存入了主过程的返回地址。习惯上我们常用"基指针寄存器" --BP来取得数据。
BP(BASS POINT)寄存器是一个十六位寄存器,它和BX寄存器一样可用于完成间接寻址,不过BP寄存器无法分成两个八位寄存器来使,而且它和BX寄存器也不完全一样。当我们使用BX做间接寻址时,如果不指定段寄存器,则CPU将默认DS寄存器的值为段地址,而使用BP做此工作时,CPU会默认SS的值为段地址,即BP寄存器常用于在堆栈中取得数据时应用。不难想象P寄存器为我们灵活处理堆栈中的数据提供了便利,这使我们可以不必顾及堆栈数据"后入先出"的规矩,也不必冒险修改SP寄存器。
需要说明的是BP也可以做其它的工作,比如暂存数据,或在其它段中完成间接寻址,只需注意明确给出段寄存器即可。一般来讲BP寄存器更多地用于取得参数,特别是当我们用汇编语言为一些高级语言编制子模块的时候,这几乎是唯一的方法。因为高级语言在调用子过程时几乎都用堆栈传递数据。
子过程中的"RET"指令有些不同寻常,它带了一个立即数。由于我们在调用子过程之前将一个数据压入了堆栈,显然子过程返回后这个数已经没有用了,所以我们要调整堆栈指针寄存器SP的值,以恢复堆栈的原始状态。
采用带常数的RET指令是个比较简单的方法,CPU在执行这样的RET指令时会在返回主过程后自动地将SP寄存器减掉这个常数值,堆栈中的参数也就算自动出栈了。不难想到这个常数在数值上应该等于"参数个数×2"。