我当年第一次上汇编实验课,老师把“转移指令和循环指令”这一节讲完之后,直接丢了个上机题目下来。我当时觉得JMP就是跳转,LOOP就是循环,概念一听就懂,结果坐在DOSBox面前写代码的时候,整个人的状态是:代码看着没问题,运行结果就是不对,不是死循环就是跳错位置,甚至编译都过不去。后来折磨了一下午,翻书、查资料、用DEBUG单步跟踪,才算是把这块真正弄明白。
这篇就围绕“转移指令和循环指令”这个主题,把我自己第二次上机的经验完整拆一遍。内容包括两类指令的原理、关键细节、完整可运行的代码示例、以及我踩过的一些坑。不管你是在校学生正在学汇编,还是自学8086汇编想补一补程序控制流的知识,这篇应该都能让你少走点弯路。
不过先说好,这篇文章的目标不是背指令表,而是建立一种“执行流”的直觉:CPU怎么决定下一条指令去哪,循环凭什么能停下来,标志位是怎么在背后起作用的。这些东西一旦想通,转移指令和循环指令就算真正学会了。
1. 先搞清楚:转移指令和循环指令到底在解决什么问题
1.1 没有跳跃的CPU只能“一条道走到黑”
先做个最简单的实验,假设有一段代码按顺序执行:
MOV AL, 1 MOV BL, 2 MOV CL, 3CPU执行完MOV AL, 1之后,自动执行下一条MOV BL, 2,再执行MOV CL, 3。这是CPU最自然的运行方式:指令寄存器IP(Instruction Pointer)在执行完一条指令后自动增加,指向内存中的下一条指令。换句话说,如果没有转移指令,程序就永远只能从上到下一条路走到底,无法根据条件做出选择,也无法回头重复做某件事。
但现实中的程序哪有这么简单?比如需求是“输入一个字符,如果是数字就提示digit,否则提示other”,这必须要在程序里做一个判断;又比如“把1加到100”,总不能写100行加法语句。这时候就必须让CPU能够“跳”到指定的指令地址继续执行,于是就有了转移指令;而“重复做某件事”的需求,就催生了循环指令。
有一个理解角度很关键:循环指令本质上就是转移指令的一种“包装”。LOOP循环无非是“先看看计数器减到0没有,没减到0就跳回去继续执行”。CPU本身并不知道什么叫循环,它只知道地址跳转。所以学习这两块内容时,不要把它们割裂开,而应该统一理解为“如何控制程序的执行流”。
1.2 课程上机背景:MASM、io.lib和DOSBox
如果你正在学校上汇编课,那么大概率会遇到这几个关键词:MASM、io.lib、DOSBox。标题的热搜词里也有“汇编语言第二次上机 io.lib masm”,这说明大家普遍在同一个环境里挣扎。
MASM是微软的宏汇编器,用来把汇编源码编译成OBJ文件,再用LINK工具链接成EXE。很多教材用的就是MASM 5.0或MASM 6.11。而io.lib则是部分教材或学校提供的I/O库,用来简化字符输入输出、十进制输入输出这类操作,比如ReadDec、WriteDec、ReadString、WriteString这些过程,避免学生一上来就跟INT 21H的中断细节死磕。
不过不同学校的io.lib版本和过程名可能不完全一样,我见过有的实验室用自定义的IO库,有的直接用INT 21H,所以如果你在课堂上发现函数名和网上对不上,别慌,以你课程提供的文档或老师PPT为准。
至于DOSBox,它是在现代操作系统上模拟DOS环境的软件,因为MASM编译出的程序是16位的,在Windows 10/11 64位系统上没法直接运行。DOSBox挂载汇编目录的命令一般是:
mount c: d:\asm c: masm test.asm; link test.obj; test.exe这里有个常见坑:有些人编译的时候漏了分号,导致MASM反复问你文件名;有些人忘记LINK,只生成OBJ就直接运行,结果提示找不到程序。这些小问题我后面会集中整理。
作为过来人,我建议你务必在电脑上装好DOSBox和MASM,因为这篇文章里的代码全部都需要实际跑一跑才能理解透彻。光看不练,汇编是绝对学不会的。
2. 转移指令:JMP与条件转移的核心细节
2.1 无条件转移JMP:段内、段间、直接、间接
无条件转移指令JMP,意思是“不管什么条件,直接跳”。它的分类方式有几种:按跳转范围分,有短转移SHORT、近转移NEAR、远转移FAR;按操作数形式分,有直接转移和间接转移。
初学者最需要理解的是SHORT和NEAR的区别。SHORT是8位偏移量,范围是当前IP之后的-128到+127字节;NEAR是16位偏移量,范围是当前IP之后的-32768到+32767字节。如果你写了一句:
JMP LABEL1而LABEL1离这条JMP指令很远,超过了8位偏移量的范围,编译时MASM通常会报错“Relative jump out of range”。解决办法是加NEAR或SHORT修饰,或者改写成:
JMP NEAR PTR LABEL1不过这种报错多出现在比较大的程序里,练习小程序一般不太会遇到。
另一个容易忽略的点是间接转移。JMP后面不一定跟标号,也可以跟寄存器或内存单元:
JMP AX ; 跳转到AX寄存器中存放的地址 JMP WORD PTR [SI] ; 从内存中取出一个16位地址作为跳转目标这种写法就是间接转移。它能实现类似C语言里函数指针的机制,但初学者也用得少,大致了解即可。真正希望大家记住的是:JMP本身不影响任何标志位,它只是把IP改成目标地址。
2.2 条件转移:看标志位下菜碟
条件转移是汇编里最出彩的部分。它的助记符很多:JZ、JNZ、JE、JNE、JC、JNC、JA、JB、JG、JL、JGE、JLE……光看名字容易晕,但核心其实只有一个:根据标志位的状态决定是否跳转。
标志位里最常见的就是ZF(零标志位)、CF(进位/借位标志位)、SF(符号标志位)、OF(溢出标志位)。条件转移指令本质上就是在做“标志位判断”:
- JZ / JE:ZF=1时跳转。JZ和JE机器码完全相同,只是语义不同,一个强调“结果为0”,一个强调“两数相等”。
- JNZ / JNE:ZF=0时跳转。
- JC:CF=1时跳转;JNC:CF=0时跳转。
- JS / JNS:根据SF判断,结果是否为负数。
- JO / JNO:根据OF判断,是否溢出。
后面还有一组用于无符号数和有符号数大小比较的条件转移,这是初学者最容易混乱的地方:
- 无符号数比较:JA(高于)、JAE(高于等于)、JB(低于)、JBE(低于等于)。
- 有符号数比较:JG(大于)、JGE(大于等于)、JL(小于)、JLE(小于等于)。
为什么会有两组?因为同一个二进制数,当作无符号数和有符号数时,大小关系是不同的。比如FFH,无符号数是255,有符号数是-1。CMP指令执行完,会影响CF、SF、OF、ZF等多个标志位,至于用哪个标志位来判断大小,取决于你把它当有符号还是无符号。JA/JB系列看CF和ZF,JG/JL系列看SF、OF和ZF,千万不能混用。
实操里最常见的写法是:
CMP AL, 30H JB NEXT ; 如果AL小于30H(十进制48,ASCII '0'),就跳到NEXTCMP AL, 30H做的事情,是计算AL - 30H,但不保存结果,只更新标志位。然后条件转移指令根据标志位来决定跳不跳。这种“比较指令+条件转移指令”的组合,就是汇编语言里最基础的分支结构。
2.3 实战:用一块字符分类程序把转移指令焊死在脑子里
看再多的指令表,不如亲手写一个小程序。我第二次上机时做的第一个练习就是“输入一个字符,判断它是数字、大写字母、小写字母,还是其他字符”。这个程序把无符号比较和条件转移全部用了一遍,非常经典。
完整代码如下,放到MASM下可以直接编译运行:
DATA SEGMENT msg1 DB 'Please input a character: $' msg2 DB 0DH, 0AH, 'It is a digit.', 0DH, 0AH, '$' msg3 DB 0DH, 0AH, 'It is an uppercase letter.', 0DH, 0AH, '$' msg4 DB 0DH, 0AH, 'It is a lowercase letter.', 0DH, 0AH, '$' msg5 DB 0DH, 0AH, 'Other character.', 0DH, 0AH, '$' DATA ENDS CODE SEGMENT ASSUME CS:CODE, DS:DATA START: MOV AX, DATA MOV DS, AX MOV AH, 09H LEA DX, msg1 INT 21H MOV AH, 01H INT 21H MOV BL, AL CMP BL, '0' JB OTHER CMP BL, '9' JA CHECK_UPPER JMP DIGIT CHECK_UPPER: CMP BL, 'A' JB OTHER CMP BL, 'Z' JA CHECK_LOWER JMP UPPER CHECK_LOWER: CMP BL, 'a' JB OTHER CMP BL, 'z' JA OTHER JMP LOWER DIGIT: MOV AH, 09H LEA DX, msg2 INT 21H JMP EXIT UPPER: MOV AH, 09H LEA DX, msg3 INT 21H JMP EXIT LOWER: MOV AH, 09H LEA DX, msg4 INT 21H JMP EXIT OTHER: MOV AH, 09H LEA DX, msg5 INT 21H EXIT: MOV AH, 4CH INT 21H CODE ENDS END START这段代码的逻辑线是这样的:读入一个字符存到BL,然后依次检查它是否在数字区间、大写字母区间、小写字母区间,最后落到对应的处理分支。重点看CMP BL, '0'和JB OTHER的组合:CMP BL, '0'实际是在算BL - '0',如果BL的ASCII码小于'0'(比如是空格),借位标志CF会置1,JB就跳转到OTHER,判断结束。如果BL大于等于'0',继续比较它是否小于等于'9',这里用的是JA CHECK_UPPER,也就是说如果BL大于'9',说明不是数字,有可能是字母,所以跳到下一个检查区。
有一个细节需要注意:ASCII字符编码中,数字、大写字母、小写字母的编码不是连续的。'0'到'9'对应30H到39H,'A'到'Z'对应41H到5AH,'a'到'z'对应61H到7AH。三个区间之间有不小的空隙,所以必须分区间判断,不能想着用一个范围覆盖。
我当初第一次写这段代码,犯的错是忘记在DIGIT、UPPER、LOWER分支末尾加JMP EXIT,结果程序判断完数字之后,没有跳到EXIT,而是直接掉入下一个分支的代码,把三条提示全部打印了一遍。这是汇编里非常经典的“掉入(fall-through)”问题——如果没有跳转指令,CPU就会接着执行下一条指令,哪怕这条指令是你认为已经结束的另一个分支。
3. 循环指令:LOOP系指令的原理与使用
3.1 LOOP的工作机制:先减一,再判断,别搞反
LOOP指令是8086汇编里最常用的循环指令,它的原理用一句话说就是:CX = CX - 1,如果CX != 0,就跳转到目标标号继续循环,否则顺序执行下一条指令。
这句话听起来简单,但放到具体例子里就有人绕晕。比如:
MOV CX, 5 L1: ; 循环体 LOOP L1这段代码循环体会被执行几次?答案是5次。因为第一次进入L1时CX=5,执行完循环体后执行LOOP,CX变成4,不等于0,跳回L1;第二次LOOP时CX从4变成3……直到第5次执行LOOP时,CX从1变成0,不跳转,循环结束。所以CX的初始值就是循环次数。
但这里有一个极其容易出错的坑:循环体里不能随便用CX。如果你在循环体内部改动了CX,比如用CX做其他计数,LOOP判断时就会出问题,循环次数彻底不可控。我见过一个同学在循环里用CX存中间结果,结果循环跑了上千次,程序直接卡死。
还有一个类似的坑:LOOP指令本身不影响标志位,但循环体里的MOV、ADD、CMP等指令会影响。这通常不会有问题,因为LOOP只看CX,不看ZF。但如果你用的是下面要讲的带条件的循环指令LOOPZ/LOOPNZ,标志位就会掺和进来。
最后补充一个细节:LOOP的跳转范围和条件转移一样,是8位短跳转,范围是当前IP之后-128到+127字节。所以如果循环体特别长,编译时可能会报错“Relative jump out of range”。解决方法通常是把循环体里大段的代码抽成子程序,用CALL调用。
3.2 循环嵌套:保存CX的两种方式
遇到二维数组遍历或者其他需要嵌套循环的场景时,问题就来了:内存循环和外层循环都需要用CX计数,但CPU只有一个CX寄存器,怎么办?
最经典的做法是用栈保存外层CX。进入内层循环之前,把CX压栈,内层循环结束后再弹出,恢复外层计数:
MOV CX, 4 ; 外层循环次数 OUTER: PUSH CX ; 保存外层CX MOV CX, 3 ; 内层循环次数 INNER: ; 内层循环体 LOOP INNER POP CX ; 恢复外层CX LOOP OUTER这段代码能正常工作,是因为内层LOOP结束之后,栈顶恰好保存着外层CX的值。只要你保证PUSH和POP配对,不要漏掉,嵌套循环就不会乱。
另一种做法是换寄存器,比如用SI或DI来辅助保存循环次数。比如外层循环用CX,内层循环改用SI计数:
MOV CX, 4 OUTER: MOV SI, 3 INNER: ; 内层循环体 DEC SI JNZ INNER LOOP OUTER这种方式在某些场景下比压栈更直观,因为避免了频繁入栈出栈的开销。但它要求你能熟练使用DEC和JNZ的组合,本质上就是用“减一+条件跳转”手动模拟LOOP。很多汇编老手其实更喜欢这种方式,因为灵活性更高,你可以在循环体里随意修改计数器的值,而LOOP指令做不到。
需要注意,跳转标号不要重名。嵌套循环里如果外层和内层用同一个标号,汇编器会弄混,编译器也容易报错。
3.3 LOOPZ/LOOPE与LOOPNZ/LOOPNE:循环还能带条件
LOOP指令只有一个判断维度:CX是否为0。但有些场景需要“一边循环一边检查某个条件是否满足”,比如在字符串中查找第一个等于某个字符的位置。这种情况用普通LOOP就不太方便,于是有了带条件的循环指令。
- LOOPZ(也写作LOOPE):CX先减1,如果CX不等于0,并且ZF=1,则跳转。也就是说,循环继续的条件是“计数没到0,且上一次运算结果为0(或相等)”。
- LOOPNZ(也写作LOOPNE):CX先减1,如果CX不等于0,并且ZF=0,则跳转。循环继续的条件是“计数没到0,且上一次运算结果不为0(或不相等)”。
举个例子,在字符串里跳过一个连续的字母'A':
MOV SI, 0 MOV CX, 100 ; 假设字符串最多100字节 L_SCAN: CMP BYTE PTR [BUF+SI], 'A' JNE L_EXIT ; 不是'A',提前退出 INC SI LOOP L_SCAN L_EXIT: ; SI 就是最后一个连续'A'的下一个位置这里如果在CMP之后用LOOPE,语义就变成了“如果相等且CX没减到0,继续循环”,等字符串当前字符不再是'A',或者CX减到0,循环都会终止。这种写法在查找类算法里非常实用。
不过凭心而论,LOOPZ/LOOPNZ在课内作业里用得并不算多,大部分循环用LOOP就够。但考试偶尔会考它们和ZF的关系,所以我建议至少把这个机制理清楚:执行顺序是先减CX,再判断CX是否为0,两个条件都满足才跳转。
4. 综合实战:求一组数据中的最大值和最小值
4.1 需求与思路拆解
前面把转移指令和循环指令分开讲了,现在把它们合起来做一个完整的练习。题目很简单:已知一组16位无符号数,存放在数据段中,求最大值和最小值并输出。
这个题目的算法思路和高级语言没区别:第一次取第一个数作为初始最大值和最小值,然后遍历剩下的每个数,如果比当前最大值大,就更新最大值;如果比当前最小值小,就更新最小值。
思路清晰之后,关键就是如何把“遍历”和“比较分支”翻译成汇编。遍历用LOOP,比较用CMP + JA/JB,更新用MOV。整个过程没有特别高深的东西,但它能把转移指令、循环指令、内存寻址、子程序调用全部串起来,非常适合练手。
4.2 完整代码与关键段讲解
DATA SEGMENT array DW 25, 72, 63, 98, 5, 41 count EQU 6 max_val DW 0 min_val DW 0 msg1 DB 'Max: $' msg2 DB 0DH, 0AH, 'Min: $' DATA ENDS STACK SEGMENT PARA STACK 'STACK' DW 100H DUP(?) STACK ENDS CODE SEGMENT ASSUME CS:CODE, DS:DATA, SS:STACK START: MOV AX, DATA MOV DS, AX LEA SI, array MOV AX, [SI] MOV max_val, AX MOV min_val, AX ADD SI, 2 MOV CX, count - 1 NEXT: MOV AX, [SI] CMP AX, max_val JBE CHECK_MIN MOV max_val, AX JMP CONTINUE CHECK_MIN: CMP AX, min_val JAE CONTINUE MOV min_val, AX CONTINUE: ADD SI, 2 LOOP NEXT MOV AH, 09H LEA DX, msg1 INT 21H MOV AX, max_val CALL PRINT_DEC MOV AH, 09H LEA DX, msg2 INT 21H MOV AX, min_val CALL PRINT_DEC MOV AH, 4CH INT 21H PRINT_DEC PROC NEAR ; 入口:AX为无符号16位数 PUSH AX PUSH BX PUSH CX PUSH DX MOV BX, 10 MOV CX, 0 L1: XOR DX, DX DIV BX PUSH DX INC CX TEST AX, AX JNZ L1 L2: POP DX ADD DL, '0' MOV AH, 02H INT 21H LOOP L2 POP DX POP CX POP BX POP AX RET PRINT_DEC ENDP CODE ENDS END START逐个说几个关键点。
首先,count EQU 6是宏定义,编译期就会把count替换成6,所以MOV CX, count - 1会被编译成MOV CX, 5。这样写的好处是,数组长度只需改一处,后面所有引用都会跟着变。
其次,取第一个数作为初始值后,循环从第二个元素开始,所以CX设为count - 1。如果你把CX设为count,然后从第一个元素开始遍历,也完全没问题,只是多了一次无意义的比较。但要注意如果数组只有一个元素,count - 1就等于0,LOOP指令会直接跳过,输出结果仍然是正确的——这就是“把第一个元素作为基准值”这个思路的优雅之处。
然后是分支结构的写法。这里我对比AX, max_val,用JBE CHECK_MIN。为什么是JBE而不是JB或JG?因为当AX小于等于max_val时,不需要更新最大值,可能要去检查最小值;只有当AX大于max_val时才更新最大值。CMP计算的是AX - max_val,无符号数比较时,如果AX小于或等于max_val,则CF或ZF会置位,JBE正好命中这个条件。更新完最大值后,一定要加一个JMP CONTINUE跳过CHECK_MIN分支,否则就会掉入更新最小值的逻辑,产生错误结果。
输出部分我用了一个十进制输出子程序PRINT_DEC。这个子程序的核心逻辑是:不断用DIV BX(BX里存10)去除AX,余数依次压栈,商继续除以10,直到商为0;最后一次从栈顶弹出的是最高位数字,加上字符'0'之后用INT 21H逐个输出。这种“除10取余,逆序输出”的做法是汇编里最常用的十进制输出方案,建议背下来。
这里的TEST AX, AX是一条很有用的指令,它做了一次按位与操作但不保存结果,只更新标志位。JNZ L1根据ZF来判断AX是否为0,等效于“如果AX不为0,继续循环”。
5. 上机常见问题与排查技巧实录
5.1 常见问题速查表
学汇编的人,没有谁是不踩坑就直接通关的。这一节我把上机过程里最容易遇到的问题整理成表,按经验概率排序:
| 现象 | 常见原因 | 解决办法 |
|---|---|---|
| MASM编译报错“Relative jump out of range” | 条件转移或LOOP跳转范围超过-128到+127字节 | 把跳转拆成两段,或改用JMP NEAR PTR,或把大段代码抽成子程序 |
| 程序运行后死循环,长时间不退出 | 循环体内修改CX;或LOOP目标标号位置错误 | 检查循环体里是否有对CX的改动,必要时用栈保存CX |
| 程序输出多了几行提示文本 | 分支末尾忘记写JMP,导致掉入下一个分支 | 检查每个分支结束后是否有JMP跳出到公共出口 |
| 比较结果完全相反,判断反了 | 条件转移指令选错,把无符号数比较和有符号数比较混用 | 明确数据是有符号还是无符号,再选JA/JB或JG/JL |
| 编译提示“Operand types do not match” | 变量类型不匹配,比如把一个字节量和一个字量做比较 | 使用BYTE PTR或WORD PTR显式指定类型 |
| 程序能编译能链接,但运行退出后异常 | 忘记定义栈段或栈空间不足 | 定义STACK SEGMENT PARA STACK 'STACK',并分配足够空间 |
| DEBUG里单步运行跳到奇怪地址 | 使用了间接转移,IP被内存或寄存器里的脏数据覆盖 | 检查JMP寄存器/JMP内存前的数据来源 |
这里我想特别强调第一条“Relative jump out of range”。初学者很少遇到,但一旦程序写得稍微长一点就会碰到。原因前面说过:条件转移和LOOP都是短跳转。如果编译器提示这个错误,千万不要想着用什么高深的技巧,最稳妥的解法是:把大块的逻辑拆成子程序,用CALL和RET组织代码。这不仅仅是为了满足编译器限制,更是写出可读性更好的汇编程序的基本功。
5.2 DEBUG单步跟踪:把每条跳转都看穿
我在学转移指令和循环指令时,最大的转折点是学会了用DEBUG单步跟踪。DOSBox里运行DEBUG,加载EXE程序后,输入T命令单步执行,U命令反汇编查看指令,R命令查看寄存器状态,D命令查看内存数据。这个工具堪称汇编学习神器。
用DEBUG跟踪循环的体验非常直观。比如运行上面求最大值最小值的程序,在DEBUG里可以看到每条指令执行后寄存器值的变化,IP指针如何跳转,CX如何递减,标志位如何被CMP影响。看几次之后,对“执行流”的理解会从“大概知道”变成“真的懂”。
跟踪时的操作流程大致如下:
- 在DOSBox中进入汇编程序所在目录,输入
debug test.exe加载程序。 - 输入
U反汇编,找到入口附近的指令地址,记下来。 - 输入
T单步执行几条指令,观察寄存器变化。 - 如果想直接跳到某条指令,先用
U查出该指令所在的段地址和偏移量,再用G 段地址:偏移量设置临时断点。 - 循环部分用
T一条条走,重点看CX、IP、ZF、CF的变化。
我第一次用DEBUG跟踪一个LOOP循环时,看到CX从5到4到3逐渐减到0,IP在循环体内来回跳,顿时就明白了“循环就是有条件跳转”这句话。后面写任何循环结构脑子里都有这种“IP来回弹跳”的画面,出错的概率大大下降。
5.3 一个容易被忽视的细节:指令对标志位的影响
学习转移指令时,很多人会忽略一个问题:每一条指令对标志位的影响是不同的。比如MOV不影响标志位,CMP会影响CF、ZF、SF、OF、AF、PF,ADD会影响CF、ZF、SF、OF、AF、PF,INC会影响OF、SF、ZF、AF、PF但不影响CF,TEST只影响ZF、SF、PF。
这个细节在条件转移里非常关键。举个例子:
MOV AX, 0 DEC AX JZ ZERODEC AX执行后,ZF=1,所以JZ会跳转,这很合理。但如果写成:
MOV AX, 0 MOV AX, 1 JZ ZEROMOV不影响标志位,所以ZF仍然是上一条指令留下的值。如果上一条指令是DEC AX,ZF=1,这里JZ就误跳了。这是新手很容易踩的隐形坑。
所以记住一个原则:条件转移指令前面紧跟着的那条影响标志位的指令,才是它真正的判断依据。中间如果夹了MOV、LEA这类不影响标志位的指令,那还好;但如果夹了ADD、SUB这类影响标志位的指令,判断结果就变了。写代码时要刻意控制“比较指令”和“条件转移指令”之间的距离。
写在最后
转移指令和循环指令这块,说难不难,说简单也不简单。难的地方在于它要求你用“执行流”的视角看程序,而不是像高级语言那样写一个if或for就觉得完事了。但一旦你建立起“CPU逐条执行指令,条件满足就跳转”的直觉,汇编里很大一部分内容都会豁然开朗。
我自己的体会是,学这两块内容最有效的方式不是背指令助记符,而是去DEBUG里单步跟踪一个完整的循环程序,亲眼看着IP指针在代码里跳来跳去,看着CX一点点减到0。这个过程比看十遍书都管用。
最后再分享一个小技巧:写汇编程序时,多写注释,并且每条跳转指令后面都注明“跳去干什么”。比如:
JZ EXIT ; 结果为0,跳出循环 JMP NEXT ; 继续处理下一个元素这种注释习惯在程序只有几十行时看不出来,一旦代码变长,转移指令一多,执行路线就会像蜘蛛网一样。没有注释的话,过两天你自己都看不懂自己写的跳转逻辑。这是在汇编上机里吃过亏之后,我最想提醒大家的一点。