最近跟CyberPunk:2077同世界观的动漫《边缘行者》上线了,看到那里面黑客流乱飞的芯片啥的,让我有了学微机原理的冲动。
在上一篇内容的最后,我们强调了MOV指令,其实强调它的原因是因为它是一个具有代表性的双操作数指令。不同CPU有不同的指令系统,但8086CPU和8088CPU是完全相同的。所以下面我们要学8086CPU的指令系统,在学的时候我们会发现之前的寻址方式和MOV指令的重要性。因为他们基本是起到奠基作用的前置知识。
数据传送类
MOV
1 | MOV DST, SRC; (DST)←(SRC) |
MOV指令在前一篇有详细的讨论,这里只占一下位置。实际上,MOV是一个经典的双操作数指令,要保证两个操作数的类型必须匹配,如果两者都指定了类型,则必须一致。如果两者之一指定了,那一般无错(未指定的会认为和指定的一样),如果都无类型,则指令出错。
LEA
1 | LEA BX, DATA1 |
LEA指令将DATA1的偏移地址转移到BX中,它传送的是操作数的有效地址(偏移地址),就像指针。这里的BX可以替换为BX,BP,SI,DI,总之是一个通用寄存器。实际上,这个指令还有一些别的意义:比如他可以快速做加法和简单的乘法,例如LEA BX, [A+4*B+10],同时,资料指出在新的CPU中,LEA是不占用CPU的ALU的,它有自己的ALU,这是后话了。总之它可以实现一些快速寻址的功能。
LDS,LES
1 | LDS/LES r, mem |
这里LDS是Load Data Segment,LES是Load Extra Segment,他俩是三操作数指令,包含两个目的操作数,我们举一个例子来顶真它:
1 | MOV BX, 2080H; |
这个操作,会将2080H处的值给SI,注意,一个字节是8位,在8080/8086中一个字是两个字节,这个指令要求的r是16位的,所以2080H和2081H依次存入SI的低位和高位。然后2080H+2的2082H会送入DS。
对于LES,即送入ES。
LAHF,SAHF
这两个是隐含寻址的指令,简单来说:
1 | LAHF ; (AH)←PSW的低8位 |
PSW在后面讲到运算指令时要拿出来记住捏,现在记一下这个指令的名字就好了,LAHF指的是Load AH with Flags, SAHF为Set Flags with AH。
XCHG
这是简洁的数据交换指令,只需:
1 | XCHG DST, SRC |
该指令可以完成寄存器与寄存器的交换,其实可以理解为一种抽象后的MOV,MOV可以的它就可以,MOV不可以它也不可以。比如不允许是段寄存器,立即数不能交换,阿巴阿巴。并且显然交换的要是等长的,而且至少有一个是寄存器(对应不能在两个存储单元上直接MOV)。
XLAT
这是一个称之为字节交换指令的指令,实际上它是隐含寻址,涉及AL与BX,它的功能即为(AL)←((BX)+(AL)),故称为查表。例如建立了一个0~9的平方表于偏移地址为2000H的内存中,求5的平方:
1 | MOV BX, 2000H |
这样AL就变成了2000H+5处的值,即5的平方。
PUSH/POP
1 | PUSH oprd |
显然,这是堆栈操作,它总是对16位的数据进行的,目的操作数隐含的其实就是堆栈,进栈操作把数据传输给以SS为段基址,SP为偏移地址的栈中。
整个过程分为两步,1) (SP)←(SP)-2, 2) (SP)←(oprd)
其中oprd可以是r,mem,seg,不能是立即数捏。这个缺陷是8086/8088的硬件限制,后来的CPU就支持了。
1 | POP oprd; |
自然,出栈也分为两步,(oprd)←(SP),(SP)←(SP)+2,同样不支持立即数。
特别地,有一对隐含寻址的指令PUSHF,POPF。它是用来把PSW压入堆栈和把栈顶所指数据赋给PSW的。用于调用子程序时保护和恢复标志位。
summary
实际上,数据传送类指令,如果在考试中要自己写,其实还是MOV和LEA用的多。这里的有些指令的出现与硬件设置强相关,所以……
算术运算类
算术运算就很重要了,从名字可以知道,算术运算需要ALU(算术逻辑单元),同时我们也注意到ALU往往会连一个PSW。还记得学计组的时候,学习到booth乘法和加法时的一堆机制,例如什么双符号位判断溢出等等。在玩具CPU里,有一个PSW,但它只有1位置,实际上由于玩具CPU只执行一段简短的冒泡排序,所以PSW并没有忠实的体现。现在我们要给出在8086中的标志寄存器的设置:
至于打//的地方是什么,有什么具体的意义,我也不是很清楚。这9个标志位要记录一下报个菜名:
标志 | 作用 |
---|---|
OF (Overflow Flag) | 溢出标志 |
SF (Sign Flag) | 符号标志 |
ZF (Zero Flag) | 零标志 |
AF (Auxiliary Flag) | 辅助进位标志 |
PF (Parity Flag) | 奇偶标志 |
CF (Carry Flag) | 进位标志 |
DF (Direction Flag) | 方向标志 |
IF (Interrupt Enable Flag) | 中断允许标志 |
TF (Trace Flag) | 追踪标志 |
重点关注前6个,后面在具体例子中给出。我们要树立一个意识,只要一个操作涉及到用ALU进行运算,这前6个标志位或多或少会被影响。(后三个标志位以后会用到)
算术运算指令有一些共同点,我先提前记录在这里:
1)运算指令影响状态标志位
2)参加运算的数可以是无符号整型,带符号整型,压缩BCD数和非压缩BCD数。
3)乘法除法中,乘数,被乘数,除数,被除数,商和余数存放的位置有规定。
4)乘法除法的书写形式有要求,
ADD,ADC
显然,它是相加,ADC是with carry,即带进位的意思。显然,它也是双操作数指令。它和MOV有许多共通的地方:
1 | ADD DST, SRC ; (DST)←(SRC)+(DST) |
SRC可以是立即数,通用寄存器,存储单元。DST就不能是立即数,可以是通用寄存器和存储单元。自然,SRC和DST不能同时是存储单元。计算完以后,结果存在DST里。
一个很好的例题是:设在DVAR开始的连续8字节中分别存放两个双字变量A和B,求C=A+B。并将结果C放到DVARC开始的内存中。设A=00127654H,B=00049821H。
1 | DVAR DD 00127654H |
s 首先,ADD和ADC也只能实现8位和16位的加法(这里的实现,依托的是硬件逻辑,这里略了。),那么32位的加法就需要一定操作了。
1 | LEA DI, DVAR ; |
要时刻记住地址由低位到高位,整个代码的流程其实很简单:
这个动图期望可以带来一些直觉上的帮助,因为制作比较花费时间。如果后面每一小串指令都绘制这样一个动图那太花时间了。在下一章简单的汇编语言编程里会有更多的这些内容。
SUB,SBB
它和加法几乎是一样的,这里SBB中的其中一个B是with borrow借位的意思。仍然用一个例题来解决:设DVAR1和DVAR2保存有双字数,并将结果存于DVARR中。
1 | DVAR1 DD 78127654H |
INC,DEC
INC即increase,DEC即decrease,是自加和自减。
这两个指令有个要求,其操作数不能是立即寻址,也就是你不能把一个立即数塞进去。这很容易理解,第二点是这两个指令不影响CF标志,第三点是,当操作数寻址到内存中的某一单元时,由于此时无法像ADD,SUB一样知道是8位还是16位,需用PTR指出一下数据类型。至于PTR,还需要在更后面我们再提到它。例如:
1 | INC AX。 |
NEG
它是negative,用于取负,它很简单,即把目标操作数作0-(DST),它与求补码等操作关系密切。注意,这个操作会影响PSW的状态位。这很好理解,因为它其实是特殊的SUB。
CMP
这是compare,它用于比较大小:
1 | CMP DST, SRC; |
它要做的就是(DST)-(SRC),并且设置PSW的标志位,后面我们会看到它在编程中的用处。
如果是两个无符号数,例如CMP AX, BX,出现AX>BX,则说明AX-BX不需要借位,CF=0。如果AX<BX,则需要借位,CF=1。
如果是两个有符号数,运算后,可以把之前CF的结论换成OF异或SF。原因是:
首先我们要知道,溢出是指正+正得负,负加负得正。如果AX-BX不发生溢出,则OF=0,且最后结果的符号位SF=1,说明AX
如果发生了溢出,OF=1,发生溢出时,一定是因为AX是一个正数,减去了一个负数(+正)或者AX是一个负数,减了一个正数(+负)最后结果发生变号了。而真正的结果,第一种情况仍然是正,第二种仍然是负。所以如果SF=1,这说明真实的结果是正的,也就是AX是正数,BX是负数,AX>BX,此时的情况是OF=1,SF=1、当SF=0时,自然AX<BX。
所以我们会发现,AX>BX时,OF=0,SF=0或OF=1,SF=1。此时OF异或SF是0。那么当AX<BX时,OF异或SF就是1。
MUL,IMUL
实际上,在刚才的加法和减法中,我们已经冥冥之中感受到了一些桎梏。这实际上是硬件实现时的一些规定。当到乘除法时,更是这样的。但这其实也带来了一些方便。
对于MUL,实际上只能call:
1 | MUL oprd |
因为它有着硬性的规定:①8位×8位→16位,16位×16位→32位;②乘数和被乘数都不能立即寻址;③乘数或被乘数必须放在AL或AX中,在指令中隐含;④16位运算结果在AX中,32位结果在DX和AX中。
IMUL用于计算有符号数的乘法,要求与MUL一样。实际上由于有符号数在计算机中以补码表示,如果用MUL来运算,也会得到一个结果。只不过此时由于符号位会参与进来,结果是错误的罢了。
MUL和IMUL对PSW的CF,OF位有影响,如果采用MUL或IMUL计算,DX=0,即两个字相乘还是个字,则CF=0,OF=0。如果字节运算(8位)运算后AH=0,说明字节相乘后还是字节,CF=0,OF=0.否则其余情况CF=1,OF=1。
DIV,IDIV
和乘法一样,DIV是无符号数除法,IDIV是有符号数的除法:
1 | DIV oprd |
它也有硬性的要求:①16位除8位,或32位除16位,被除数不够16位或32位,作符号扩展。②同样,被除数和除数都不能是立即寻址。③被除数必须放在AX或DX:AX中,在指令中则隐含;④16位运算的商放在AL中,余数放在 AH中;32位运算的商放在AX中,余数放在DX中。
除法并不会影响PSW。
实际上为什么乘法和除法要要求被除数/被乘数必须在AX/AX:DX中,以及为什么立即数不能用来当乘数或者除数,在计组中已经给出了答案。比如计算乘法时,本质上是部分积和乘数一位一位的位移,立即数显然不支持这样的操作。
CBW,CWD
CBW是convert byte to word,显然,它是将AL中8位数扩展到16位。这种操作多用于除法中,如果是带符号的除法,那么当符号位D7为1时,AH会补成FFH。D7=0时,AH补零。
CWD就是convert word to double word,道理是一样的,只不过补的是整个DX。
位操作指令
这个,位操作指令,包括逻辑运算,移位,指令。
逻辑运算指令:
助记符格式 | 功能说明 | |
---|---|---|
与 | AND DST, SRC | (DST) (DST)$\land$(SRC) |
或 | OR DST, SRC | (DST) (DST)$\lor$(SRC) |
异或 | XOR DST, SRC | (DST) (DST)$\oplus$(SRC) |
测试 | TEST DST, SRC | 根据 (DST)$\land$(SRC)置标志位 |
非 | NOT DST | (DST)各位取反 |
除了NOT,前四个指令均会给CF,OF置0。因为位操作不会导致进位和溢出。同时根据计算结果设置SF,ZF,PF的状态。
我们要知道,位操作允许许多的操作,可以实现不同的目的,相当的灵活。比如:
将AX寄存器中的$D_1,D_5,D_6,D_{11},D_{15}$位保留,其余位清零:
1 | AND AX, 1000100001100010B |
将DX寄存器中的低8位置1,其余位不变。
1 | OR DX, 00FFH |
将AX寄存器中的$D_1,D_5,D_6,D_{11},D_{15}$位求反,其余位保留。
1 | XOR AX, 1000100001100010 |
下面以TEST指令为例演示一些别的功用:例如判断AX寄存器的$D_1$位是否为1:
1 | TEST AX, 0002H |
这样执行后,只需检测PSW的ZF标志,即可知道是否为1。
逻辑移位指令:
名称 | 全称 | 特点 |
---|---|---|
逻辑左移SHL | Shift Left | 空位补零,移出的最后一位进CF |
逻辑右移SHR | Shift Right | 空位补零,移出的最后一位进CF |
算术左移SAL | Shift Arithmetic Left | 空位补零,移出的最后一位进CF |
算术右移SAR | Shift Arithmetic Right | 空位补最高位,移出的最后一位进CF |
循环左移ROL | Rotate Left | 空位由移空的位补充 |
循环右移ROR | Rotate Right | 空位由移空的位补充 |
带进位循环左移RCL | Rotate through Carry Left | 空位由CF补充 |
带进位循环右移RCR | Rotate through Carry Right | 空位由CF补充 |
这些指令的使用格式很统一,例如:
1 | SHL AX, 1 |
即SHL DST CNT。如果CNT由寄存器CL表示,那CL里是多少就移几次。这些指令都会正常影响PSW的SF,PF,ZF,CF和OF标志位,重点关注CF和OF,CF表示指令所移出的那一位,OF表示移位前后符号位发生了变化。CF位的灵活使用可以提供很多便利。
我们知道,移位后,一般来说数据会扩大2倍或者减小2倍,所以也有很多用途。
例如使用移位实现乘法:5*10
1 | MOV AL, 05H |
将一个字的内容除2,并进行四舍五入:
1 | SHR AX, 1 |
这个的精巧之处在于,SHR会把移动的那一位送入CF,而四舍五入即如果移出的是1,结果+1。所以此时只需要把标志位加进来即可,也就是用ADC+0。
将DX:AX中的32位二进制数乘2,结果再送入DX和AX:
1 | MOV AX, 8421H |
这样执行后即DX:AX=08430842H。用RCL正好可以续上AX的最高位。
程序控制转移指令
现在,我们有了数据传送类指令,数据运算类指令,和位操作指令。这已经足够构造一些相对复杂的程序。但是回忆最早学C语言时候的编程题目,找出一个数组里最大的数,或者冒泡排序,它们都需要选择,循环等操作,即转移,分支,重复。我们先说转移:在8086的汇编里,有两种转移,无条件的转移和有条件的。
本质上,如果程序转移后只有IP发生变化,那么就是段内转移,因为毕竟段址没变,即近程转移,称为NEAR型。如果CS,IP均发生改变,则称为段间转移,也叫FAR型。按照西电版教科书的分类,可以分为:
无条件转移指令 | 条件转移指令 |
---|---|
JMP,CALL,RET,IRET | JZ,JC,JCXZ,LOOP |
无条件的我们先说JMP,后面三个在最后提到中断时再说。
JMP
这个JMP,就是jump,其实可以很好用:
1 | JMP LABEL |
这里,LABEL确实只是一个“标号”,例如:
1 | JMP L1 |
宏汇编程序MASM.exe会根据标号的位置,自动生成相应的指令代码。
JMP REG16和JMP MEM也很自然,如果是16位寄存器,那只改变IP,这十分自然。如果MEM也是16位的字,那么也只改变IP,如果是双字,那就是高位CS,低位IP。实际上,更全面的使用是召唤PTR:
方式 | 指令 |
---|---|
段内直接转移 | JMP NEAR PTR address |
段内间接转移 | JMP CX / JMP WORD PTR [BX] |
段间直接转移 | JMP FAR PTR address |
段间间接转移 | JMP DWORD PTR [BX] [SI] |
而在书写时,还是JMP LABEL更方便。注意,这个段内直接转移,有一种情况下,位移量是8位的带符号数,比如PTR BYTE [BX],此时允许在±127字节中寻址,称为段内直接短转移。为什么要强调这一个呢,因为条件转移指令他们都只支持这种短转移。
原因是考虑到控制指令长度和提高运行速度,如果真的要进行一个有条件的远转移,可以先短转移到一个附近的单元,然后加一个无条件的转移。
有条件转移作为JMP的一个变种,下面给到底下,其实类似于某种咏唱的魔法咒语,看习惯英文就好了。首先,我们要意识到,这个条件从哪来。比如用任何一个高级语言,都可以写“if(a>b)”这样,那么在汇编里,没有这样的高级抽象,但大部分时候进行一个操作后,PSW都会发生变化。所以我们可以通过观测PSW各个标志位的情况来进行分支跳转。在上文介绍的PSW中,有6个状态标志位,AF是半进位标志位,不常用。其余5个标志可以反映10种状态:
指令 | 英文 | 指令 | 英文 |
---|---|---|---|
JC LABEL | Jump if carry | JNS LABEL | Jump if not sign |
JNC LABEL | Jump if not carry | JO LABEL | Jump if overflow |
JE/JZ LABEL | Jump if equal/zero | JNO LABEL | Jump if not overflow |
JNE/JNZ LABEL | Jump if not equal/zero | JP/JPE LABEL | Jump if parity/parity equal |
JS LABEL | Jump if sign | JNP/JPO LABEL | Jump if not parity/parity odd |
阅读英文名,可以知道是由哪个标志位来的。这里说明一个事情,这里的JE/JZ和JNE/JNZ中其实equal指的就是ZF的状态。后面也有很多指令会有E和Z,它们的操作码是一样的。究其原因是因为使用CMP指令,本质上是作减法,当比较出现相同的情况时,差为0,使得ZF亮起。
举一个例子捏:
1 | LEA BX, MAX |
首先,MAX和BUF的地址被存入BX和SI,并且给CL赋了一个20,很多情况下这其实都是循环次数。然后将SI所指的值赋给AL。按顺序进行NEXT处,首先SI自加,然后AL与SI所指的值进行比较。
当CMP a b,ab了,那么会不作交换,直接GOON。一直到CL变成0,才不会跳转,最后AL的值会送入[BX](即MAX的地址),然后停机。
所以这其实是在连续的20字的区域中找最大值。在刚才这个例子里实际就看出了使用CF来判断大于小于的好处(结合CMP指令)。现在引入更全面的指令来专门约束一下这一过程(大于/不低于,即取等情况。)
指令 | 英文 | |
---|---|---|
JA/JNBE | (CF=0)$\land $(ZF=0) | Jump if above / not below nor zero |
JAE/JNB | CF=0 | Jump if above or equal / not below |
JB/JNAE | CF=1 | Jump if below / not above nor equal |
JBE/JNA | (CF=1)$\lor$(ZF=1) | Jump if below or equal / not above |
JG/JNLE | (SF$\lor$OF)$\lor$ZF=0 | Jump if greater / not less nor equal |
JGE/JNL | (SF$\lor$OF)=0 | Jump if greater or equal / not less |
JL/JNGE | (SF$\lor$OF)=1 | Jump if less / not greater nor equal |
JLE/JNG | (SF$\lor$OF)$\lor$ZF=1 | Jump if less or equal / not greater |
上四行处理的是无符号数,所以用的是above/below,下面是有符号的,就greater和less了。
LOOP
为了便于循环控制,在8086CPU的指令集中专门设置了循环控制类指令。实际我们刚才已经看到,使用JMP即可实现某种意义的循环,这里要讲的LOOP对其进行了进一步的封装。总的来说,指令集中给出了4种指令:
指令 | 条件 |
---|---|
LOOP LABEL | (CX)←(CX)-1,(CX)$\ne$0时转LABEL |
LOOPZ/LOOPE LABEL | (CX)←(CX)-1,(CX)$\ne$0且ZF=1时转LABEL |
LOOPNZ/LOOPNE LABEL | (CX)←(CX)-1,(CX)$\ne$0且ZF=0时转LABEL |
JCXZ LABEL | CX=0时转LABEL |
下面依次举几个例子来说明这四条指令,首先是经典1加到100:
1 | SUM DW ? |
那么LOOPZ和LOOPNZ又有什么好处捏,实际上它们多见于一些字符串处理中,因为如果对于数字来使用,这种条件循环其实用处就很有限了。比如找出第一个非负的数等等。一般它们也是和一个会改变ZF的指令结合在一起使用,很多时候是CMP。
比如取出字母串string中的第一个大写字母并且放入DL:
1 | mov BX, -1 |
这里关于串的操作看起来非常自然,我们后面再进行严谨的补充。这么写,当AL是一个小写字母的ASCII码时, test后ZF就会是1,然后循环继续;如果是大写字母,ZF就会是0,然后跳出循环。这里为什么选取20H,可以看大小写字母的ASCII码分布。
实现循环输入字符,并将字符保存在string数组中,按回车结束:
1 | MOV BX, 0 |
当接受的字符是回车(0DH)时,由CMP指令ZF会等于1,那么loopnz直接跳出循环。至于这里的int 21H是什么,这可不是定义整型变量的意思,这是中断(interrupt),我们放后面提。
最后的JCXZ是一个用于处理特殊情况的指令,如果最开始的CX很不巧是0,那么对于16位的CX,需要循环65536次程序才会停止,这很不妙。确实大多数时候可以通过规范CX来处理这个问题,但是有些时候为了运行的健壮性,还是这样写会好一些,比如我们实现在BUFFER区域的字型数据的中值滤波,并保存进AVG缓冲区中。其中前2个字节是数据个数,但数据有可能是空的。那么:
1 | BUFFER DW n |
哒哒,这里的JCXZ完全是为了防止(CX)=0,来跳过循环的。
下面是关于子程序的一小部分,这也是程序控制的关键环节。简单来说,通过模块化的构建子程序,我们可以简化程序设计。当主程序需要调用这些子程序时,直接调用再返回即可。
这样直接的结果就是需要保护某些寄存器的内容,从而需要进行一些堆栈操作,对程序的执行速度产生一定影响。
子程序调用与返回
子程序调用指令CALL就像无条件跳转JMP一样,指令格式也是类似的。但CALL自动使此时的CS和IP寄存器都入栈了,来起到保护的作用。然后RET指令用于返回它。这里举一个例子:编写子程序实现统计AX记录的一个字中“1”的个数。
1 | VAR1 DW 1234H |
基本上意思就是这个意思。
同时,当程序运行期间遇到某些特殊情况,需要暂停现行程序,转而执行一组专门的程序来处理。这乘坐中断。8086的中断分为两种,内部中断如除法遇到除以0;外部中断主要用于处理I/O设备与CPU的通信,如之前的int 21H。
中断指令即INT n,这里n是不同中断例行程序的代号。与CALL不同的是,此时堆栈还要“保存现场信息”,需要把反应现场状态的PSW也送入堆栈。返回时,不仅恢复IP和CS,也恢复原有的PSW。这里的更详细的内容,可以参考一些相关方面的资料。
串操作指令
如在循环中举的字符串的例子一样,一个字符往往是一段ASCII码,而我们认知上的字符串如“shjdsa”就会是一串数组。同时,汇编语言本身,也是一组字符串。字符串存放在一个连续的区域中。在之前的例子中,我们也有用SI当作一个会移动的地址指针的用法。串操作实际就是把这个思想进行了进一步封装,提供了5类字符串操作指令:
指令 | 英文 | 作用 |
---|---|---|
MOVS(B/W) | Move byte or word string | (ES:DI)←(DS:SI) |
CMPS(B/W) | Compare byte or word string | (DS:SI)-(ES:DI) |
SCAS(B/W) | Scan byte or word string | AL/AX-(ES:DI) |
LODS(B/W) | Load byte or word string | AL/AX←(DS:SI) |
STOS(B/W) | Store byte or word string | (ES:DI)←AL/AX |
他们都是隐含寻址,下面慢慢解释它的机理。首先,在之前在介绍PSW时出现了一个DF标志位,它叫direction flag,在前面画图演示时,我们往往会画一个长条,然后从上到下是低地址到高地址。那么对于指针自加显然就是INC。那么在字符串这些打包好的指令里,就需要DF标志位来控制是低地址到高地址自加还是高地址到低地址自减。
总的来说,他们的共同点如下:
①源操作数的地址由DS:SI给出,目的操作数的地址由ES:DI给出。
②指令助记符里的B和W决定了是byte还是word。
③如上文所说,使用这些指令时,地址指针是自动移动的,DF=0地址单增,DF=1地址单减,步长是1还是2取决于是byte还是word
④串的长度由循环时用的计数器CX指定,这非常自然。
⑤实际上这里的MOVS和SCAS之类的,也只能一次处理一个字或字节,可以使用一个叫指令前缀的功能来使它循环。
MOVS
MOV已经是老朋友了,这里举个例子,结合指令前缀REP,实际上就是循环,像LOOPZ/E和LOOPNZ/E一样,它也有REPZ/REPE。是一样的。
1 | LEA SI, BUFF1 |
这样就会实现整个区域的MOV。
CMPS
与CMP类似,依然是不保留结果只改变标志位,但是CMP与目的操作数减源操作数,CMPS是反过来的,原因不详。
举个例子,BUFFER1和BUFFER2中有两个长度为100的字型缓冲区,对比这两个缓冲区的数据,找到第一个相同的字;如果找到相同的字,则在ADDR中保存该数据在BUFFER1中的地址,如果找不到,在ADDR中置0FFFFH。
1 | LEA SI, BUFFER1 |
下面自然引出了一个问题,如果我想只找有没有特定的字怎么办,那这就是SCAS扫描的作用了。
SCAS
这个指令允许你把你想搜索的字放进AL或者AX里,然后配合重复前缀进行搜索:
1 | LEA DI, BUFFER1 |
进一步,如果是要在一个区域里搜索存不存在某个字符串,那会更加复杂一点点,比如在字节型BUFFER中保存有20000个数据,要求在其中查找字符串“12345”,并将其在BUFFER中的偏移地址存放在ADDR中,如果没找到ADDR赋0FFFFH。
1 | BUFFER DB 20000 DUP(?) |
LDS/STOS
这两个指令忠实的完成着装入和存储,比如LDS是将存储单元的内容写入AL/AX,STOS是写出,这也导致这两个指令很少和指令前缀一起用。当然可以用STOS来加上指令前缀从而实现清零和置初值,有点像memset。
End
这一篇blog已经太长了,所以我略去了一些内容,它们在理解微机原理主体上没那么重要,比如BCD码校正的指令,和那些记忆向的处理器控制指令。以及,我并没有规范的介绍汇编语言编程的一些要求,很多例子虽然已经是不错的demo了但确实缺乏一定的严谨性,比如变量的定义等等,都很似然,在下一篇里会进一步介绍。
这一篇写的比较费时间,期间经历了一次国家奖学金答辩(在这里感谢准备答辩期间帮助我的朋友们),在那里我看见了很多半仙。有的门门课90好几,均分说出来都吓人,如果放在以前,我直接焦虑感拉满,然后转而思考自己在实验班里难看的rank夏令营不是得寄?后来格局打开了,第一,我未必会(能)保研;第二,当时考600来分然后交学费,大概不是为了多上四年高中的。我觉得大概是要找到自己的adventure然后write your own stroy。至少我现在很开心,置于均分不靠前,没有ICPC/CCPC经历的劣势会把我导向何处,这并不是我能决定的了。人生有梦,各自精彩。
“哈,你终于领悟了成为传奇的诀窍。”——CyberPunk2077