这道题我在当学生的时候写过后来带新人时又看着他们写了一遍又一遍。说它是“入门题”确实没错但真正能一次跑通的人并不多。问题通常不在统计逻辑上而在输入缓冲区的结构、数字到ASCII的转换以及最后怎么用EXE格式把程序跑起来。今天就把这题完整拆一遍从代码到编译把那些教材上“默认你会”的细节都摊开讲清楚。1. 先拆题这30个字符在考察哪些隐藏知识点1.1 三层需求拆解题目原文是“从键盘输入一个字符串长度不超过30统计字符串中非数字的个数并将统计的结果显示在屏幕上用EXE格式实现。”看起来是三个动作但背后其实藏了四件事键盘输入、字符串存储、逐字节判断、十进制数值输出。键盘输入这件事在很多高级语言里也就是一条input()的事但在汇编里得调用DOS中断而且不是简单地“读一个字符串”而是要用INT 21H的0AH号功能往一个预先定义好的缓冲区里填数据。缓冲区结构长什么样、实际输入的长度放在哪个字节、字符串内容从哪个偏移开始这些细节都写死在教材里但正因为它绕才有考察价值。第二层是“统计非数字的个数”。这里的关键不是怎么写循环而是怎么界定“数字”。ASCII编码里0到9是连续的从30H到39H所以判断一个字节是不是数字只需要看它落不落在这个区间里。写两个CMP加两个条件跳转就能搞定是不是“非数字”反而更简单不在区间内就是。第三层是显示结果。计数结果在寄存器里是个二进制数值屏幕却只能显示ASCII字符。你得把这个数值拆成一位一位的十进制数字再每个加上30H变成字符才能用INT 21H的09H或02H号功能输出。这就是经典的“除10取余”转换法后面我会给出完整的例程。还有一层大家经常忽略题目要求“用EXE格式实现”。这意味着程序不是简单地写完就完了你还得走完整的编译、链接流程让链接器生成带文件头的.EXE可执行文件而不是那种没有头部的.COM文件。这层考察的是对工具链的理解很多人恰恰是卡在这里。1.2 “非数字”的边界条件什么叫非数字理论上只要ASCII码不在30H~39H之间都算非数字。空格算非数字字母算非数字标点算非数字回车算不算算但一个容易被坑的地方是你统计的数据范围里会不会混入回车符。我用的是INT 21H的0AH号缓冲输入功能它会把用户按下的回车键0DH当作输入结束标志但你按回车产生的那个0DH字符并不会存进字符串数据区也不会算进实际输入长度里。也就是说正常流程下统计范围内不会出现回车符。但如果你用01H号功能逐字符读取就会有一堆额外处理要做相对麻烦。这也是为什么这个题目更适合用0AH缓冲输入来解。还有一类边界情况是“输入为空字符串”也就是用户什么字符都没输入直接按回车。此时实际长度字节为0循环一次都不执行统计结果应该是0。我的代码里没有对这种特殊情况做任何分支因为CX为0时循环自动跳过非常干净。2. 环境准备从DOSBox到MASM/TASM的完整配置2.1 为什么必须用模拟环境现在几乎所有电脑都是64位系统加图形界面的操作系统原生环境下已经没有DOS的容身之地。要用汇编写一个跑在DOS下的EXE程序最稳妥的方案是安装一个模拟器在模拟出来的8086环境里运行。最常见的搭配是DOSBox加MASM或者DOSBox加TASM。我自己用的是DOSBox 0.74加MASM 6.11原因有三点它模拟了完整的INT 21H中断调用DOS交互方式跟真实环境一致不会出现“高级语言里能跑、汇编里跑不了”的玄学。MASM的语法是老教材的默认标准网上能找到的参考代码几乎都是这种写法。DOSBox能在现代高分辨率屏幕上开一个DOS窗口编译、运行、调试都直观。2.2 挂载、编辑、编译三步准备先假设你的MASM整个文件夹放在C:\masm下面。打开DOSBox之后第一步是挂载。mount c c:\masm c: cd work这里我把C:\masm挂载成DOS下的C盘然后在C:\masm里建一个work子目录专门放代码。DOSBox启动后默认的工作目录是Z:如果不挂载就直接写文件很可能写到虚拟盘里重启就没了。这是很多人第一次练汇编必踩的坑。编辑源码可以用DOSBox内置的edit命令也可以直接在Windows下用记事本写好存成test.asm再挂载进来。我更推荐后者因为记事本可以随便复制粘贴写错了改起来也快只是要注意保存时编码最好选ANSI或纯ASCII。如果你用带特殊符号的中文注释编码不一致很容易在DOS下变成乱码。然后是编译和链接。以MASM为例masm test.asm; link test.obj;命令后面的分号是告诉汇编器“剩下的提问全部用默认值”。如果你不写分号它会问你“是否生成列表文件”、“是否生成交叉引用文件”、“列表文件叫什么名字”之类的问题新手容易懵。加了分号直接回车一条龙结束。链接完之后work目录里会多出一个test.exe直接输入test.exe就能运行。3. 核心代码拆解输入缓冲、遍历判断、十进制输出3.1 数据段布局先把完整源码放出来再一段一段解释。STACKSEG SEGMENT STACK DW 64 DUP(0) STACKSEG ENDS DATA SEGMENT MAXLEN DB 30 ACTLEN DB ? BUFFER DB 30 DUP(0) COUNT DW 0 MSG_IN DB Input: $ MSG_OUT DB 0DH, 0AH, Non-digit count: $ DATA ENDS CODE SEGMENT ASSUME CS:CODE, DS:DATA, SS:STACKSEG START: MOV AX, DATA MOV DS, AX LEA DX, MSG_IN MOV AH, 09H INT 21H LEA DX, MAXLEN MOV AH, 0AH INT 21H XOR CH, CH MOV CL, ACTLEN LEA SI, BUFFER XOR AX, AX COUNT_LOOP: MOV BL, [SI] CMP BL, 0 JB NOT_DIGIT CMP BL, 9 JA NOT_DIGIT JMP IS_DIGIT NOT_DIGIT: INC AX IS_DIGIT: INC SI LOOP COUNT_LOOP MOV COUNT, AX LEA DX, MSG_OUT MOV AH, 09H INT 21H MOV AX, COUNT LEA SI, NUMBUF MOV CX, 0 MOV BX, 10 CONVERT_LOOP: XOR DX, DX DIV BX PUSH DX INC CX TEST AX, AX JNZ CONVERT_LOOP OUT_LOOP: POP DX ADD DL, 0 MOV AH, 02H INT 21H LOOP OUT_LOOP MOV AH, 4CH INT 21H CODE ENDS COMMENT ! 数据段里需要加 NUMBUF 定义 DATA SEGMENT ... NUMBUF DB 6 DUP(0) ... DATA ENDS ! END START我在数据段注释里提到还需要NUMBUF DB 6 DUP(0)因为磁盘缓冲区要放着转换后的字符。虽然我用的是边弹栈边输出的方式用一个临时变量也行但保留一个缓冲区会更清晰。下面把正确完整的数据段合并一下DATA SEGMENT MAXLEN DB 30 ACTLEN DB ? BUFFER DB 30 DUP(0) COUNT DW 0 NUMBUF DB 6 DUP(0) MSG_IN DB Input: $ MSG_OUT DB 0DH, 0AH, Non-digit count: $ DATA ENDS数据段里最容易被忽视的是STACKSEG。如果你不定义堆栈段经典版LINK链接时会给你一个LINK : warning L4021 : no stack segment的警告。程序还能运行但作为作业或测试多一个红字警告总归不够体面。加一个64字的堆栈段不亏。MAXLEN DB 30这个值是有讲究的。它是DOS的0AH功能缓冲区定义里的第一个字节表示缓冲区能容纳的最大字符数。这个数不是“字符串长度”而是“包括回车符在内的总容量”。如果用户输入30个字符第31个回车符还要占一个位置但MAXLEN只有30输入就会被截断。换句话说想支持“长度不超过30”最稳妥的做法是把MAXLEN设成31或32。这个话题我在第5节还会详细谈。3.2 输入中断的两个关键细节INT 21H的0AH功能调用前要求DS:DX指向一个三部分组成的缓冲区偏移长度含义01字节缓冲区最大容量含回车11字节实际输入字符数不含回车2N字节用户输入的字符数据区调用时用户输入的字符会被依次写进缓冲区每按一个字符屏幕上有回显直到按下回车。回车符本身也占一个缓冲区字符位置但它的0DH不会进数据区而实际长度字节只记录数据区里的字符数量。所以在我的数据段里MAXLEN放在第一个字节ACTLEN是第二个字节BUFFER是真正的数据区。输入完成后ACTLEN里存的就是用户输入了几个字符。这里有个很容易搞混的点INT 21H的0AH功能名虽然叫“键盘输入字符串”但它不负责自动在字符串末尾加$结束符也不会把回车转换成00H。整个缓冲区就是一堆裸数据你必须用ACTLEN来控制循环次数。另外要注意0AH功能从DS:DX指向的缓冲区的第一个字节开始读取所以代码里写的是LEA DX, MAXLEN而不是LEA DX, BUFFER。很多人第一次写这里直接指向BUFFER结果整个缓冲区结构错位统计出来全是乱数据。3.3 非数字判断的两种策略判断一个字符是不是数字常规做法是CMP BL, 0 JB NOT_DIGIT CMP BL, 9 JA NOT_DIGIT JMP IS_DIGIT这条链路的含义是先看字符是不是小于0小于则跳去非数字分支再看是不是大于9大于也跳去非数字分支两个条件都不成立说明它夹在0和9之间是数字。这里用的是无符号比较JB和JA分别对应低于和高于正好匹配ASCII码连续排列的特点。另一种写法是反过来先判断是否在数字区间再统计CMP BL, 0 JB IS_DIGIT_START ; 如果小于0就是非数字 CMP BL, 9 JBE IS_DIGIT_END ; 如果在范围内就是数字这种写法分支更多思路不如前一种直观。我在代码里选的是前一种因为它的逻辑边界清晰非数字覆盖“小于0”和“大于9”两端数字只占中间一小段。循环本身用的是LOOP COUNT_LOOP。LOOP指令的执行原理是先把CX减1再判断CX是不是0不是0就跳转。所以循环次数必须提前赋给CX。这里我用XOR CH, CH MOV CL, ACTLEN因为实际长度最多不超过30高字节CH清零即可。如果你偷懒直接MOV CX, ACTLEN编译器不会报错但CH里残留的垃圾数据会决定循环次数结果会很诡异。3.4 除法转十进制的经典套路统计结果在AX里是个纯二进制数值比如统计出19个非数字AX的值是00000013H。要让屏幕显示“19”必须把它转换成字符1和9。转换思路很简单反复把数除以10余数就是当前最低位的十进制数字商继续作为下一次除法的被除数。比如19除以10商1余9先得到个位9再把1除以10商0余1得到十位1。因为个位先出来所以是逆序我选择把每一步的余数压栈全部除完之后再依次弹栈输出。MOV AX, COUNT MOV CX, 0 MOV BX, 10 CONVERT_LOOP: XOR DX, DX ; 清空高16位余数寄存器 DIV BX PUSH DX INC CX TEST AX, AX JNZ CONVERT_LOOP这里有个极其关键的细节DIV BX是32位除以16位被除数默认是DX:AX所以除法前必须把DX清零。新手最容易犯的错就是忘了XOR DX, DX直接DIV BX结果高位残留数据会把商撑爆轻则结果离谱重则触发除法溢出直接崩溃。弹栈输出时每一项都是0到9之间的余数加上0就成了ASCII字符OUT_LOOP: POP DX ADD DL, 0 MOV AH, 02H INT 21H LOOP OUT_LOOPMOV AH, 02H是单字符显示功能DL里放要显示的字符。LOOP指令自动控制循环次数因为弹栈前CX里放的是压栈的次数。这个算法之所以值得反复练是因为它是“把内部数值交给外部世界”最基本的翻译手段。以后学高级语言底层原理也不过如此。4. 从ASM到EXE编译链接运行的完整演示4.1 MASM与TASM两条工具链写好的源码保存成test.asm后有两种经典工具链可以选择。MASM方案。masm test.asm; link test.obj;第一行把汇编源码翻译成目标文件test.obj分号让汇编器跳过所有交互提问。第二行把目标文件链接成可执行文件test.exe。如果源码里定义了堆栈段链接器不会有额外警告。TASM方案。tasm test.asm; tlink test.obj;TASM跟MASM语法基本兼容都是Intel风格但TASM对语法的宽容度略高。有些人习惯用TASM是因为它的错误提示信息相对直白一点。如果你在DOSBox里用的是TASM流程跟上面没有本质差别。两种工具链对比见下表对比项MASMTASM汇编命令masm test.asm;tasm test.asm;链接命令link test.obj;tlink test.obj;伪指令兼容教材标准基本兼容错误提示较简略较直白适合人群做练习、考试习惯Borland风格的人我个人的建议是教材用哪个就用哪个没必要在环境上纠结。真正需要记住的是汇编后不一定直接生成EXE中间那个.obj文件就是链接器的原料。4.2 链接时常见的两个提示第一次链接时经常会出现两个提示。一个是上面说过的“no stack segment”警告。虽然程序能跑但严格来说这不是干净的程序。解决方案已经写在源码里了在代码段之前加上STACKSEG SEGMENT STACK并且把ASSUME SS:STACKSEG写好。这样链接器才会为程序设置正确的堆栈段寄存器。另一个是LINK让你输入“Run File [test.EXE]”之类的提问。这是因为你没在命令末尾加分号。处理办法就是在link test.obj后面加分号让它自动接受默认值。还有一件事要提醒不要指望在Windows图形界面下双击test.exe就能正常运行。它是16位DOS程序现代64位系统根本不认这种格式。必须在DOSBox里或者任何能跑16位程序的DOS模拟环境中运行。4.3 完整的运行验证过程我在DOSBox里的完整流程是这样的mount c c:\masm c: cd work edit test.asm masm test.asm; link test.obj; test.exe程序启动后屏幕先显示Input:我输入abc123xy4然后回车。第二行显示Non-digit count: 6。因为abc三个字母、x和y两个字母加上没有参与统计的回车一共6个非数字字符。数字1、2、3、4不计数结果正确。再测一组纯数字12345结果应该是0。再测一组纯字母hello结果应该是5。边界测试通过之后这道题就算设计实现完成。5. 踩坑与扩展30字节上限、中文输入、堆栈警告5.1 “MAXLEN 30”到底坑在哪回到数据段里的MAXLEN DB 30。前面说了0AH功能的第一个字节表示整个缓冲区的容量包括结尾的回车符。这意味着如果用户输入30个字符回车符放不下了程序只会读取到前29个字符剩余输入会被忽略系统还会响一声提示。题目的要求是“字符串长度不超过30”。严格来说想让用户能够完整输入30个字符缓冲区容量至少要是31。也就是MAXLEN DB 31 BUFFER DB 31 DUP(0)不少教材例程直接写MAXLEN DB 30配合BUFFER DB 30 DUP(0)其实是把“缓冲区容量”和“实际字符串长度”混在一起了。你要是上交作业测试人员输入30个英文字母统计结果就会莫名其妙少一个字符。这个问题我问过不少人十个里面至少有四个人会中招。5.2 中文输入与多字节字符的问题DOS环境下用ASCII编码处理中文是个大麻烦。比如你输入一个汉字“中”在GBK编码里它占两个字节分别是D6H和D0H。这两个字节都落在0~9区间之外所以我的程序会把一个汉字统计成两个“非数字”。这其实不算程序bug而是“字符编码”和“字节处理”的天然语义差异。作业题如果没明确说支持中文输入默认就是纯ASCII字符环境。但作为经验我得提醒你如果你在DOSBox里粘贴中文千万不要用统计结果的数字去硬凑预期否则会怀疑人生。5.3 再聊两个值得标注的编码细节第一MSG_OUT DB 0DH, 0AH, Non-digit count: $里的0DH, 0AH是回车加换行作用是让结果输出在下一行。很多人只写换行不写回车在DOS下输出可能正常但一旦换成某些终端环境就会出问题。两个一起写最稳。第二程序结尾的MOV AH, 4CH和INT 21H是标准返回DOS的姿势。如果你直接用INT 20H或者RET在EXE程序里都有可能出问题。4CH号功能可以附带一个退出码通常我们让AL0表示正常退出。我的代码里没有显式设置AL但前面除法结束时AL恰好是0。为了可读性更稳妥的写法是MOV AX, 4C00H INT 21H5.4 进一步的扩展方向这道题做完之后可以衍生出一堆同类型的小练习把这个程序改成统计数字个数判断逻辑正好反过来。加上大小写字母分组统计。把非数字字符单独打印出来不仅显示个数。修改循环判断支持回车符和空格的特殊处理做一个简易的单词计数器。如果你继续沿着这个方向走下一站可以考虑把程序改成COM格式。COM文件没有文件头要求所有内容都在同一个64KB段内链接命令也会不同。体验过EXE和COM的区别之后你对“段”这个概念的理解会超出教材一大截。就我的个人经验而言这道题最大的价值不在于“会写”而在于“知道每一行为什么这么写”。当你能解释清楚MAXLEN为什么不能写30、DIV之前为什么必须清DX、链接器为什么警告没有堆栈段的时候说明你已经摸到了汇编程序从源码到EXE这条路的完整脉络。下次再遇到更复杂的中断调用或者内存操作至少心里有底。