下面是用DEBUG根踪这个程序的过程,请注意观察BP寄存器的应用和堆栈的变化:
-g=0 c welcome to pc world?程序显示出的字符串 AX=0924 BX=0000 CX=004E DX=0000 SP=0000 BP=0000 SI=0000 DI=0000 DS=1031 ES=1021 SS=1031 CS=1033 IP=000C NV UP EI PL NZ NA PO NC 1033:000C 52 PUSH DX
"PUSH DX"指令将字符串首地址压入堆栈,借助堆栈将参数传递给子过程。注意SP寄存器的原值为0000。
-t AX=0924 BX=0000 CX=004E DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=1031 ES=1021 SS=1031 CS=1033 IP=000D NV UP EI PL NZ NA PO NC 1033:000D E80800 CALL 0018
"CALL"指令执行时堆栈中存入返回地址,恰好在所传递的参数之上。
-t AX=0924 BX=0000 CX=004E DX=0000 SP=FFFC BP=0000 SI=0000 DI=0000 DS=1031 ES=1021 SS=1031 CS=1033 IP=0018 NV UP EI PL NZ NA PO NC 1033:0018 55 PUSH BP
为不破坏BP寄存器的原值,在应用BP寄存器之前先将其原状态压入堆栈,此时堆栈指针距传递参数的距离为4个BYTE。
-t AX=0924 BX=0000 CX=004E DX=0000 SP=FFFA BP=0000 SI=0000 DI=0000 DS=1031 ES=1021 SS=1031 CS=1033 IP=0019 NV UP EI PL NZ NA PO NC 1033:0019 8BEC MOV BP,SP
将堆栈指针置入BP寄存器,准备从堆栈之中取得参数。
-t AX=0924 BX=0000 CX=004E DX=0000 SP=FFFA BP=FFFA SI=0000 DI=0000 DS=1031 ES=1021 SS=1031 CS=1033 IP=001B NV UP EI PL NZ NA PO NC 1033:001B 8B5E04 MOV BX,[BP+04] SS:FFFE=0000
参数的实际位置应该是"WORD PTR SS:[BP+04]",应用BP寄存器做间接寻址时CPU会自动引用SS寄存器提取段地址,因此编程时可以不明确指定引用SS寄存器。
-t 注意此数据实际来自堆栈 AX=0924 BX=0000 CX=004E DX=0000 SP=FFFA BP=FFFA SI=0000 DI=0000 DS=1031 ES=1021 SS=1031 CS=1033 IP=001E NV UP EI PL NZ NA PO NC 1033:001E B91300 MOV CX,0013 SS:FFFE=0000
下面我们一次将子过程全部执行完,并在002BH处设置断点,以便于观察指令RET的执行情况。
-g=1e 2b AX=0944 BX=0013 CX=0000 DX=0000 SP=FFFC BP=0000 SI=0000 DI=0000 DS=1031 ES=1021 SS=1031 CS=1033 IP=002B NV UP EI PL NZ NA PO NC 1033:002B C20200 RET 0002
带参数的RET指令可以自动修改SP寄存器,如果在这里简单的使用不带参数的RET指令,则这个子程序返回后SP应该等于"0FFFEH",RET指令加上参数之后,SP寄存器就会被修改为0000,等于以前压入的参数自动出栈。
-t?注意SP寄存器的变化 AX=0944 BX=0013 CX=0000 DX=0000 SP=0000 BP=0000 SI=0000 DI=0000 DS=1031 ES=1021 SS=1031 CS=1033 IP=0010 NV UP EI PL NZ NA PO NC 1033:0010 B409 MOV AH,09 g=10 WELCOME TO PC WORLD 转换后的字符串 Program terminated normally
下面这个程序也是一个有关如何应用BP寄存器实例,这个程序示例揭示了BP寄存器的又一用途--动态分配内存。
CALC.ASM CR equ 0dh ;回车符的ASCII码 LF equ 0ah ;换行符的ASCII码 data segment assume ds:data mess db 'welco me to pc world',0dh,0ah,24h ;定义一个小写字符串 ascii db '01234 56789abcdef' ;十六进制的数字 data ends code segment assume cs:code main proc far mov ax,data ;设置数据段寄存器 mov ds,ax mov dx,offset mess ;DX寄存器指向字符串首 push dx ;字符串首地址压入堆栈 mov bx,offset ascii ;BX寄存器指向ASCII表 call calc ;调用CALC子过程计算字符数 call hexout ;调用HEXOUT子过程输出字符数 mov ah,4ch ;结束进程 int 21h main endp calc proc near ;CALC子过程 push bp ;保存BP寄存器 mov bp,sp ;堆栈指针置入BP寄存器 sub sp,2 ;堆栈指针上移2字节 push bx ;保存BX寄存器 mov bx,[bp+4] ;字符串首地址送入BX寄存器 mov word ptr [bp-2],0 ;堆栈中空出的2个字节清零 caloop: mov dl,byte ptr [bx] ;从字符串中取一个字符 cmp dl,24h ;到字符串结尾了吗? jz return ;到字符串结尾,返回主程序 mov ah,2 ;选择DOS API的02功能 int 21h ;调用中断显示字符 inc bx ;BX指向下一个字符 inc word ptr [bp-2] ;堆栈中的计数器加1 jmp caloop ;处理下一个字符 return: mov ax,[bp-2] ;字符个数送入AX寄存器 pop bx ;恢复BX寄存器 mov sp,bp ;恢复堆栈指针 pop bp ;恢复BP寄存器 ret 2 ;返回主程序 calc endp hexout proc near ;HEXOUT子程序 push ax ;保存AX寄存器 mov cx,2 ;显示2位十六进制数 hexloop: push cx ;保存CX中的计数值 mov cl,4 ;准备循环移位次数 rol al,cl ;循环移位 push ax ;保存AX寄存器 and al,0fh ;保留计数值的高4位 xlat ascii ;取得对应的ASCII码 mov dl,al ;准备显示数字对应的ASCII码 mov ah,2 int 21h ;显示数字 pop ax ;恢复AX寄存器 pop cx ;恢复CX寄存器 loop hexloop ;处理低4位 mov dl,CR ;DL寄存器置入回车符 mov ah,2 int 21h ;显示回车符 mov dl,LF ;DL寄存器置入换行符 int 21h ;显示换行符 pop ax ;恢复AX寄存器 ret ;返回主程序 endp hexout ends code end main
子过程CALC用于显示字符串并计算总字符数。程序首先将堆栈指针SP的值置入BP寄存器,而后将SP寄存器减去2,这样一来在堆栈中就出现了两字节存储空间,这两字节可用[BP-2]寻址,这一存储空间可用于暂存数据。
使用这种方式分配内存的好处是它比死板地使用DB、DW伪指令要灵活,当子过程返回后,所分到的内存会自动释放,而不占用固定的空间;如果我们把用DB、DW伪指令定义的变量称为"静态变量"(STATIC),那么这种在堆栈中分配到存储空间就是"自动变量"(AUTO)。这两种称谓都出现在C语言中,其实C语言也正是通过堆栈实现"自动变量"的。