TASM汇编调试与编译运行时输入校验逻辑表现不一致
类UNO汇编卡牌游戏Valid_Press循环逻辑不一致问题修复
问题现象
- 项目为汇编实现的类UNO卡牌游戏,设计
Valid_Press循环作为输入校验入口:仅检测到用户合法操作时才退出循环,否则持续等待有效输入 - 直接编译后运行存在稳定复现的异常:非法操作(例如牌堆顶为红色3时打出蓝色1)会被误判为合法操作,
Valid_Press循环直接退出,流程错误推进到下一回合 - 完全相同的二进制在TASM调试器中运行时逻辑完全符合预期,可正确拦截非法输入,不会提前退出循环
- 异常触发于
Valid_Press循环执行阶段,对应核心汇编代码如下:
Valid_Press: cmp [First_Round], 0 je Skip_Plus_Two ; the first round variable is to determine if the first card on pile was discard card cmp [Last_Value], -4 ; check for +2 played last turn je Check_For_Two Skip_Plus_Two: mov [First_Round], 1 ; change it to one so it works only once call WaitForLeftClick ; first wait for left click Clicked: shr cx, 1 ; actual x ; check for draw cmp dx, 70 jb Maybe_Card cmp dx, 120 ja Maybe_Card ; now cmp x cmp cx, 30 jb Maybe_Card cmp cx, 60 ja Maybe_Card ; if all conditions not reached, press was on picture jmp Draw_One_Card Maybe_Card: push [Interval] ; interval push dx push cx call Is_On_Card ; check for press on card cmp ax, 0FFFFh ; the check je Press_Is_Card xor bx, bx xor cx, cx xor dx, dx ; card was not valid, jmping to Valid_Press jmp Valid_Press ; input not card Press_Is_Card: ; these calculaions are done to understand position of card pressed in hand based on his x cord and interval between cards ;-------------------------------------------------------- mov bx, cx ; x cordinate sub bx, 11 ; distance mov ax, 18 ; width of card add ax, [Interval] ; distance of each card plus interval mov cx, ax ; distance of each card + interval mov ax, bx ; the actual X div cl ; index in al xor ah, ah ;--------------------------------------------------------- add si, ax ; si points to the index of the card that was pressed mov [Current_Index], al ; save index of card ( it is legal) mov ax, [si] ; card that was pressed xor ah, ah mov bx, ax mov [Current_Value], bl ; value of card push si ; card offset call CanPlayCard cmp cx, 5 ; the check je Card_Is_Right jmp Valid_Press ; card cannot be played, waiting for legal input
根因分析
这是典型的未定义寄存器状态依赖导致的海森堡bug,调试环境和实机运行环境的初始状态差异触发了问题:
- 寄存器未初始化:代码在
Press_Is_Card分支直接使用si作为手牌数组基址,和计算出的索引相加得到卡牌地址,但整个Valid_Press逻辑中从未给si赋手牌数组的初始基址。TASM调试器启动程序时会默认将所有通用寄存器清零,si=0时如果手牌数组恰好定义在数据段偏移0的位置,计算出的卡牌地址就是正确的;直接在DOS环境运行时,si中残留的是系统加载程序时留下的随机值,add si,ax后得到的是完全随机的内存地址,读到的卡牌值是垃圾数据,有概率刚好匹配出牌规则,被误判为合法操作。 - 未遵守寄存器调用约定:调用
WaitForLeftClick/Is_On_Card/CanPlayCard等子函数时,没有按约定保护主逻辑使用的寄存器,子函数内部修改cx/si等值后,返回主逻辑时寄存器状态不可控。调试器中单步运行时子函数的执行路径和直接运行有细微差异,寄存器残留值恰好符合预期,不会触发判断错误。 - 返回值约定不规范:代码通过判断
cx是否等于5确认CanPlayCard的校验结果,但x86汇编惯例通过ax传递返回值,子函数如果没有显式给cx赋值,cx的值就是之前逻辑残留的随机数,有概率刚好等于5触发合法判定。
修复方案
按以下顺序修改代码即可解决逻辑不一致问题:
- 在
Valid_Press循环入口处增加寄存器初始化逻辑,每次进入循环都明确加载手牌数组基址到si,不要依赖寄存器的初始残留值:Valid_Press: mov si, offset Player_Hand ; 替换为代码中实际的手牌数组变量名 cmp [First_Round], 0 je Skip_Plus_Two ; 其余原有逻辑保持不变 - 统一子函数返回值约定:修改
CanPlayCard逻辑,校验通过时向ax写入5作为合法标记,主逻辑把cmp cx,5改为cmp ax,5做判断,避免依赖随机的cx值。 - 补充寄存器保护逻辑:调用所有子函数前,把主逻辑后续要用到的
si/bx/cx/dx寄存器压栈保存,子函数返回后立刻出栈恢复,避免子函数篡改寄存器值干扰主逻辑。 - 在所有跳回
Valid_Press的分支前,清空本次循环产生的临时寄存器值,避免上一次循环的计算结果污染下一轮输入判断。
内容的提问来源于stack exchange,提问作者ThErOmAnEmPiRe
相关产品推荐
相关产品推荐

