PAYATHON 2026

x86 汇编(Intel syntax)计算器为什么无法正确执行减法?

数据小许

减法出错是因为 grd 对所有输入字符都执行了 sub al, 48,运算符也被转换了。保存到内存的 '-' 因此不再等于 45,减法比较失败。程序随后顺序执行紧接着的 sum,所以输入 2-1 会得到加法结果 3。

应当只转换数字字符,保留运算符的 ASCII 编码,并为未识别的运算符指定处理路径。原程序还需要修正输入指针复位、= 的处理和结果显示。

运算符为什么比较失败

int 16h 在这里通过 AL 返回按键的 ASCII 字符。对于数字,减去 '0' 是正确的:

'2' = 50,50 - 48 = 2
'1' = 49,49 - 48 = 1

但运算符不能做同样的转换:

'-' = 45,45 - 48 = -3,在 8 位 AL 中保存为 0xFD
'+' = 43,43 - 48 = -5,在 8 位 AL 中保存为 0xFB

因此,下面两次比较都不会匹配保存后的运算符:

cmp byte [bx], 43
je sum
cmp byte [bx], 45
je res

第二个 je 没有跳转时,CPU 会继续执行下一条指令。sum: 只是一个标签,不会阻止程序执行它下面的代码,因此程序仍然会做加法。

主循环中的 add al, 48 也无法修复这个问题:它发生在保存之后,只修改寄存器 AL,不会恢复已经写入内存的运算符。

原程序还有哪些相关问题

计算结束后没有正确复位 BX。cn 中的地址调整使 BX 最终停在 5002。直接 jmp cun 后,下一个字符会覆盖运算符位置,而不是开始保存新的第一个操作数。

程序没有等待 =。保存第二个操作数时,BX 已经等于 5004,程序立即进入计算。因此,输入示例中的 = 实际上会被后续输入流程处理。

pop bx 手动丢弃了返回地址。原程序通过 call grd 将返回地址入栈,再从 grd 跳转到 cn,因此 cn 开头的 pop bx 实际上移除了这个返回地址。这种做法依赖当前调用结构,调整代码时容易破坏栈。改成正常的 call 和 ret 更清楚。

BIOS 输出也使用 BX。int 10h 的 AH=0Eh 功能使用 BH 指定显示页,图形模式下还使用 BL 指定前景色。BX=5000 是十进制地址,即 0x1388,其中的 BH 并不是预期的显示页号。输出字符时应单独设置 BIOS 所需参数,并保护作为内存指针使用的 BX。

加上 '0' 只能显示 0 到 9。两个单数字操作数的运算结果可能是 -9 到 18,负数和两位数需要分别输出符号、十位与个位。

一个完整的修正示例

下面使用 NASM 的 Intel syntax,保留 5000、5002、5004 的存储布局。程序先回显输入,收到 = 后计算,随后复位输入位置;按大写 Q 退出。

示例按 数字 运算符 数字 = 的顺序处理输入,没有加入完整的输入校验。结果输出覆盖有效单数字操作数产生的 -9 到 18。

org 100h

; Use the program segment for data accesses.
push cs
pop ds

; Select display page 0.
mov ax, 0500h
int 10h

mov bx, 5000

read_key:
    xor ah, ah
    int 16h

    cmp al, 'Q'
    je exit

    call putc

    cmp al, '='
    je evaluate

    ; Three input characters have already been stored.
    cmp bx, 5004
    ja read_key

    ; Keep the operator as ASCII.
    cmp bx, 5002
    je store_input

    ; Convert only the operand characters.
    sub al, '0'

store_input:
    mov [bx], al
    add bx, 2
    jmp read_key

evaluate:
    ; Both operands and the operator must be present.
    cmp bx, 5006
    jne read_key

    mov al, [5000]
    mov dl, [5004]

    cmp byte [5002], '+'
    je do_add
    cmp byte [5002], '-'
    je do_sub

    ; Do not fall through into an arithmetic branch.
    mov al, '?'
    call putc
    jmp reset_input

do_add:
    add al, dl
    jmp show_result

do_sub:
    sub al, dl

show_result:
    call print_result

reset_input:
    mov al, 13
    call putc
    mov al, 10
    call putc
    mov bx, 5000
    jmp read_key

; Print signed AL in the range -9..18.
print_result:
    test al, al
    jns .nonnegative

    neg al
    push ax
    mov al, '-'
    call putc
    pop ax

.nonnegative:
    cmp al, 10
    jb .ones

    sub al, 10
    push ax
    mov al, '1'
    call putc
    pop ax

.ones:
    add al, '0'
    call putc
    ret

; Print AL and preserve AX/BX.
putc:
    push ax
    push bx
    mov ah, 0Eh
    mov bx, 0007h
    int 10h
    pop bx
    pop ax
    ret

exit:
    int 20h

这段代码对有效输入的预期显示为:

2-1=1
1-2=-1
9+9=18

print_result 是针对 -9..18 编写的简化输出例程。如果以后支持多位操作数,就需要换成通用的十进制整数输出例程。

固定地址与输入校验的注意事项

每次通过 mov [bx], al 保存的是一个字节,因此相邻字节地址也能保存这些值。每次将 BX 增加 2 会在数据之间留一个空字节。可以保留这种学习用布局,但这不是避免字节重叠的必要条件。

[5000] 等内存操作默认通过 DS 寻址。使用固定偏移时,需要确保 DS 指向预期的数据段,而且对应区域属于程序可用内存,没有覆盖代码或其他数据。示例通过 push cs、pop ds 明确设置了数据段;程序扩展后,使用标签定义存储空间会更容易维护。

增加输入校验时,应在数字转换前检查字符是否位于 '0' 到 '9',在运算符位置检查是否为 '+' 或 '-',并明确处理缺少操作数、额外字符及退格键。当前修正的核心是:数字按数值保存,运算符按字符保存,分支比较必须与保存形式一致。

备注:内容仅供参考。