下面是用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语言也正是通过堆栈实现"自动变量"的。