程序的处理思路:
1、调用int 16h读取键盘输入;
2、如果不是字符:
2.1 如果是退格键,从字符栈中弹出一个字符,显示字符栈中所有字符;继续执行"读取键盘输入"。
2.2 如果是Enter键,向字符栈中压入0,返回。
3、如果是字符键:字符入栈,显示字符栈中的所有字符,继续执行"读取键盘输入"。
assume cs:code ,ds:data
;定义栈空间
data segment
db 32 dup(?)
data ends
code segment
start:
mov ax,data
mov ds,ax
mov si,0
mov dh,12 ; 显示行:第12行
mov dl,20 ; 显示列:第20列
call getstr
return:
mov ax,4c00h
int 21h
getstr:
push ax
getstrs:
mov ah,0
int 16h ;阻塞,等等键盘输入
cmp al,20h ; 判读是否为可显示字符(ASCII≥20H)
jb nochar ; 非可显示字符,跳转到nochar处理
;字符al入栈
mov ah,0
call charstack
;显示栈中的字符
mov ah,2
call charstack
jmp getstrs
;处理非字符
nochar:
cmp ah,0eh ;退格键的扫描码
je backspace
cmp ah,1ch ;回车键的扫描码
je enter
jmp getstrs
;对退格键、回车键的处理
backspace:
;字符出栈
mov ah,1
call charstack
;显示栈中的字符
mov ah,2
call charstack
jmp getstrs
enter:
mov al,0
;0字符入栈
mov ah,0
call charstack
;显示栈中的字符
mov ah,2
call charstack
pop ax
ret ;getstr结束
;功能子程序实现
charstack:
jmp short charstart
table dw charpush ,charpop,charshow ; 功能跳转表(ah=0/1/2对应入栈/出栈/显示)
top dw 0 ; 栈顶指针(初始=0,记录栈中字符数) 记录栈中有多少个字符的计数器
charstart:
push bx
push dx
push di
push es
; 功能分发:ah=0(入栈)/1(出栈)/2(显示)
cmp ah,2
ja sret ; ah>2,直接退出
mov bl,ah
mov bh,0
add bx,bx ; bx=ah*2(dw占2字节,计算跳转表偏移)
jmp word ptr table[bx] ; 跳转到对应功能
charpush:
mov bx,top
mov [si][bx],al ;AL是键盘读入的字符的ASCII码
inc top
jmp sret
charpop:
cmp top,0
je sret
dec top
mov bx,top
mov al,[si][bx]
jmp sret
charshow:
;在dh行dl列显示
mov bx,0B800H
mov es,bx
mov al,160
mov ah,0
mul dh ;在代码第12行,dh=12 ;此处语法默认mul dh = ax = al*dh
mov di,ax ;示例:DH=12 → AX=160×12=1920(即 0780H),表示第 12 行的起始偏移是 1920 字节
;将行偏移存入 DI,作为显存地址的 "行基准"。本例为第12行的首地址
add dl,dl ;代码第13行,显示dl=20,dl+dl=40
mov dh,0 ;将 DX 的高 8 位清零,确保 DX 仅为列偏移的结果(避免 DH 原有值干扰);
add di,dx ;这是加法计算,相当于在di=12行的基础上,再加40,即1920+40=1960字节,也就是第 12 行第 20 列的起始位置
mov bx,0
charshows:
cmp bx,top
jne noempty ;当 bx ≠ top 时,跳转到 noempty 标签(继续显示栈中字符)。
mov byte ptr es:[di],' '
jmp sret
noempty:
mov al,[si][bx]
mov es:[di],al
mov byte ptr es:[di+2],' '
inc bx
add di,2
jmp charshows
sret:
pop es
pop di
pop dx
pop bx
ret
code ends
end start
关键知识点:
一、为什么是20H?
在汇编代码中 cmp al, 20h 是判断输入字符是否为可显示的 ASCII 字符 ,核心原因是:20H(十进制 32)是 ASCII 表中第一个 "可显示字符(空格)" 的编码,而小于20H的 ASCII 码均为不可显示的控制字符。
1、ASCII 码的分类(核心依据)
ASCII 码分为两类关键区间,这是判断的核心逻辑:
| ASCII 范围 | 十进制 | 类型 | 示例 |
|---|---|---|---|
| 0~31 | 0~20H | 控制字符(不可显示) | 回车 (0DH)、换行 (0AH)、退格 (08H)、ESC (1BH) 等 |
| 32~127 | 20H~7FH | 可显示字符 | 空格 (20H)、数字 (30H~39H)、字母 (41H~5AH/61H~7AH)、符号等 |
二、为什么使用al,20h中的AL呢?
明确 int 16h (ah=0) 的返回值分工
调用 BIOS 键盘中断 int 16h (ah=0) 后,CPU 会把键盘输入的两类关键信息存入 AX 寄存器,分工非常清晰:
| 寄存器 | 存储内容 | 核心作用 | 示例(退格键) |
|---|---|---|---|
AL |
字符的 ASCII 码 | 表征 "字符本身是什么" | 退格的 ASCII 是 08H(不可显示) |
AH |
键盘按键的扫描码 | 表征 "按的是哪个物理键" | 退格键的扫描码是 0EH |
简单说:
AL回答 "这个键对应的字符是什么"(ASCII 是字符的 "身份标识");AH回答 "按的是键盘上哪个键"(扫描码是按键的 "硬件标识")。
三、cmp与jb的配合使用
| 指令 | 功能说明 |
|---|---|
cmp al, 20h |
比较 AL 寄存器的值与 20H(ASCII 空格符的编码),本质是执行 AL - 20H,仅修改标志位(不保存结果)。 |
jb nochar |
jb = Jump if Below(无符号小于则跳转),检测 CF(进位标志)是否为 1:- 若 AL < 20H → CF=1 → 跳转到 nochar;- 若 AL ≥ 20H → CF=0 → 不跳转,执行下一条指令。 |
四、cmp与ja的配合使用
| 指令 | 功能说明 |
|---|---|
cmp ah, 2 |
比较指令:执行 AH - 2 的运算(不保存结果),仅修改标志寄存器(CF/OF/ZF 等) |
ja sret |
条件跳转指令:无符号数大于则跳转 (Jump if Above),跳转到sret标签 |
五、jmp与jne的配合使用
| 指令 | 全称 | 类型 | 功能说明 |
|---|---|---|---|
jmp |
Jump | 无条件跳转 | 直接跳转到指定标签 / 地址,必然执行跳转,无任何条件限制 |
jne |
Jump if Not Equal | 条件跳转 | 仅当标志寄存器中 ZF=0(即 "比较结果不相等")时,才跳转到指定标签 / 地址 |
常见搭配
cmp a, b+jne label→ 如果 a ≠ b,跳转
六、mul dh的说明解释
mul dh 是 x86 汇编语言中的一条指令,用于无符号乘法。下面是对这条指令的详细解释:
指令格式:
mul source
其中 source 是一个 8 位、16 位、32 位或 64 位的寄存器或内存操作数(取决于当前操作模式)。
针对 mul dh 的具体说明:
dh是 8 位寄存器,是dx寄存器的高字节。- 执行
mul dh表示将 AL (8 位累加器)中的值与 DH 中的值相乘。 - 结果是一个 16 位无符号整数 ,存储在 AX 寄存器中。
数学表达:
AX = AL × DH (无符号乘法)
七、去掉si应该也不影响程序运行吧?
结论先行 :去掉SI(直接删除mov si,0,并把所有[si][bx]改成[bx]),程序在逻辑上仍能运行 ,但会丧失SI带来的灵活性,且违背 8086 汇编的寻址设计规范 ------ 本质是SI=0时,[si][bx]等价于[bx],但SI的存在是为了 "适配通用场景",而非 "当前简单场景的必需"。
1、为什么去掉 SI 不影响当前程序运行?
核心原因是代码中SI被固定初始化为 0,且全程未修改:
asm
start:
mov ax,data
mov ds,ax
mov si,0 ; SI=0,全程不变
...
charpush:
mov bx,top
mov [si][bx],al ; SI=0 → 等价于[bx] = al
...
charpop:
mov bx,top
mov al,[si][bx] ; SI=0 → 等价于al = [bx]
...
charshow:
mov al,[si][bx] ; SI=0 → 等价于al = [bx]
8086 的[si][bx]是基址 + 变址寻址 ,公式为:物理地址 = DS×16 + SI + BX当SI=0时,公式简化为:物理地址 = DS×16 + BX这与直接使用[bx](仅变址寻址)的结果完全一致,因此去掉SI、将[si][bx]改为[bx],程序的栈读写逻辑不会变化,仍能正常入栈、出栈、显示。
2、但 SI 的存在绝非多余(为什么代码要保留 SI?)
虽然当前场景下SI=0看似无用,但它是为代码的可扩展性和规范性设计的,核心价值体现在:
2.1. 支持 "多栈 / 栈偏移存储"(扩展性)
如果后续需要在data段中定义多个栈(如 "输入栈"+"备份栈"),或让字符栈不从data:0开始存储,只需修改SI的值即可,无需重构核心寻址逻辑:
; 示例:字符栈从data:32开始(前32字节用于其他数据)
mov si,32 ; 仅修改SI,栈操作逻辑([si][bx])完全不变
若去掉SI,则需修改所有[bx]为[bx+32],代码耦合度高、易出错。