11.9 ~ 11.11
《汇编语言(第3版)》11.9 ~ 11.11、《零基础入门学习汇编语言》P58 ~ 59
虽然 je 的逻辑含义是“相等则转移”,但它进行的操作是,ZF=1 时则转移。
“相等则转移”这种逻辑含义,是通过和 cmp 指令配合使用来体现的,因为是 cmp 指令为“ZF=1”赋予了“两数相等”的含义。
至于究竟在 je 之前使不使用 cmp 指令,在于我们的安排。
je 检测的是 ZF 位置,不管 je 前面是什么指令,只要 CPU 执行 je 指令时,ZF=1,那么就会发生转移。
比如
mov ax,0
add ax,0
je s
inc ax
s:
inc ax
执行后,(ax)=1。add ax,0 使得 ZF=1,所以 je 指令将进行转移。
可在这个时候发生的转移确不带有“相等则转移”的含义。因为此处的 je 指令检测到的 ZF=1,不是由 cmp 等比较指令设置的,而是由 add 指令设置的,并不具有“两数相等”的含义。
但无论“ZF=1”的含义如何,是什么指令设置的,只要是 ZF=1,就可以使得 je 指令发生转移。
CPU 提供了 cmp 指令,也提供了 je 等条件转移指令,如果将它们配合使用,可以实现根据比较结果进行转移的功能。
但这只是“如果”,只是一种合理的建议,和事实上常用的方法。
但究竟是否配合使用它们,完全是你自己的事情。
这就好像,call 和 ret 指令的关系一样。
对于 jne、jb、jnb、ja、jna 等指令和 cmp 指令配合使用的思想和 je 相同,可以自己分析一下。
我们来看一组程序:data 段中的 8 个字节如下
data segment
db 8,11,8,1,8,5,63,38
data ends
编程:统计 data 段中数值为 8 的字节的个数,用 ax 保存统计结果。
编程思路:初始设置 (ax)=0,然后用循环依次比较每个字节的值,找到一个和 8 相等的数就将 ax 的值加 1。
assume cs:code
data segment
db 8,11,8,1,8,5,63,38
data ends
code segment
start:
mov ax,data
mov ds,ax
mov bx,0 # ds:bx 执行第一个字节
mov ax,0 # 初始化累加器
mov cx,0
s:
cmp byte ptr [bx],8 # 和 8 进行比较
jne next # 如果不相等转到 next,继续循环
inc ax # 如果相等就将计数值加 1
next:
inc bx
loop s
mov ax,4c00h
int 21h
code ends
end start
## 第二种方案
assume cs:code
data segment
db 8,11,8,1,8,5,63,38
data ends
code segment
start:
mov ax,data
mov ds,ax
mov bx,0 # ds:bx 指向第一个字节
mov ax,0 # 初始化累加器
mov cx,0
s:
cmp byte ptr [bx],8 # 和 8 进行比较
je ok # 如果相等就转到 ok,继续循环
jmp short next # 如果不相等就转到 next,继续循环
ok:
inc ax # 如果相等就将计数值加 1
next:
inc bx
loops
mov ax,4c00h
int 21h
code ends
end start
比起第一个程序,它直接的遵循了“等于 8 则计数值加 1”的原则,用 je 指令检测等于 8 的情况,但是没有第一个程序精简。
第一个程序用 jne 检测不等于 8 的情况,从而间接地检测等于 8 的情况。
注意:使用 cmp 和条件转移指令时的这种编程思想
编程:统计 data 段中数值大于 8 的字节的个数,用 ax 保存统计结果。
编程思路:初始设置 (ax)=0,然后用循环依次比较每个字节的值,找到一个大于 8 的数就将 ax 的值加 1。
assume cs:code
data segment
db 8,11,8,1,8,5,63,38
data ends
code segment
start:
mov ax,data
mov ds,ax
mov bx,0 # ds:bx 指向第一个字节
mov ax,0 # 初始化累加器
mov cx,0
s:
cmp byte ptr [bx],0 # 和 8 进行比较
jna next # 如果不大于 8 转到 next,继续循环
inc ax # 如果大于 8 就将计数值加 1
next:
inc bx
loop s # 程序执行后,(ax)=3
mov ax,4c00h
int 21h
code ends
end start
编程:统计 data 段中数值小于 8 的字节的个数,用 ax 保存统计结果。
编程思路:初始设置 (ax)=0,然后用循环依次比较每个字节的值,找到一个小于 8 的数就将 ax 的值加 1。
assume cs:code
data segment
db 8,11,8,1,8,5,63,38
data ends
code segment
start:
mov ax,data
mov ds,ax
mov bx,0 # ds:bx 指向第一个字节
mov ax,0 # 初始化累加器
mov cx,0
s:
cmp byte ptr [bx],8 # 和 8 进行比较
jnb next # 如果不小于 8 转到 next,继续循环
inc ax # 如果小于 8 就将计数值加 1
next:
inc bx
loop s # 程序执行后:(ax)=2
mov ax,4c00h
int 21h
code ends
end start
上面讲解了根据无符号数的比较结果进行转移的条件转移指令。
根据有符号数的比较结果进行转移的条件转移指令的工作原理和无符号数相同,只是检测了不同的标志位。
我们在这里主要探讨的是 cmp、标志寄存器的相关位、条件转移指令三者配合应用的原理,这个原理具有普遍性,而不是逐条讲解条件转移指令。
11.10 DF 标志和串传送指令
flag 的第 10 位是 DF,方向标志位。
在串处理指令中,控制每次操作后 si、di 的增减
- DF=0:每次操作后 si、di 递增
- DF=1:每次操作后 si、di 递减
格式 1:movsb
功能(以字节为单位传送):((es)×16+(di))=((ds)×16+(si))
- 如果 DF=0 则
- (si)=(si)+1
- (di)=(di)+1
- 如果 DF=1 则
- (si)=(si)-1
- (di)=(di)-1
movsb 的功能是将 ds:si 指向的内存单元中的字节送入 es:di 中,然后根据标志寄存器 DF 位的值,将 si 和 di 递增或递减。
当然,也可以传送一个字:movsw
格式 2:movsw
功能(以字为单位传送):将 ds:si 指向的内存字单元中 word 送入 es:di 中,然后根据标志寄存器 DF 位的值,将 si 和 di 递增 2 或递减 2。
movsb 和 movsw 进行的是串传送操作中的一个步骤,一般来说,movsb 和 movsw 都和 rep 配合使用,格式如下:rep movsb
rep 的作用是根据 cx 的值,重复执行后面的串传送指令。
由于每执行一次 movsb 指令 si 和 di 都会递增或递减指向后一个单元或前个单元,则 rep movsb 就可以循环实现 (cx) 个字符的传送。
由于 flag 的 DF 位决定着串传送指令执行后,si 和 di 改变的方向,所以 CPU 应该提供相应的指令来对 DF 位进行设置,从而使程序员能够决定传送的方向。
8086 CPU 提供下面两条指令对 DF 位进行设置
- cld 指令:将标志寄存器的 DF 位置 0
- std 指令:将标志寄存器的 DF 位置 1
我们来看两个程序
编程 1:用串传送指令,将 data 段中的第一个字符串复制到它后面的空间中。
data segment
db 'Welcome to masm!'
db 16 dup (0)
data ends
我们分析一下,使用串传送指令进行数据的传送,需要给它提供一些必要的信息,它们是
- 传送的原始位置:ds:si
- 传送的目的位置:es:di
- 传送的长度:cx
- 传送的方向:DF
在这个问题中,这些信息如下
传送的原始位置:data:0
传送的目的位置:data:16
传送的长度:16
传送的方向:因为正向传送(每次串传送指令执行后,si 和 di 递增)比较方便,所以设置 DF=0
assume cs:code data segment db 'Welcome to masm!' db 16 dup (0) data ends code segment start: mov ax,data mov ds,ax mov si,0 # ds:si 指向 data:0 mov es,ax mov di,16 # es:di 指向 data:16 mov cx,16 # (cx)=16,rep 循环 16 次 cld #(clear df)小甲鱼猜测的含义 rep movsb mov ax,4c00h int 21h code ends end start
编程 2:用串传送指令,将 F000H 段中的最后 16 个字符复制到 data 段中。
data segment
db 16 dup (0)
data ends
我们还是先来看一下应该为串传送指令提供什么样的信息:
要传送的字符串位于 F000H 段的最后 16 个单元中,那么它的最后一个字符的位置:F000:FFFF,是显而易见的。
我们可以将 ds:si 指向 F000H 段的最后一个单元,将 es:di 指向 data 段中的最后一个单元,然后逆向(即从高地址向低地址)传送 16 个字节即可。
相关信息如下
传送的原始位置:F000:FFFF
传送的目的位置:data:15
传送的长度:16
传送的方向:因为逆向传送(每次串传送指令执行后,si 和 di 递减)比较方便,所以设置 DF=1
assume cs:code data segment db 16 dup (0) data ends code segment start: mov ax,0f000h mov ds,ax mov si,0ffffh # ds:si 指向 f000:ffff mov ax,data mov es,ax mov di,15 # es:di 指向 data:15 mov cx,16 # (cx)=16,rep 循环 16 次 std # 设置 DF=1,逆向传送 rep movsb mov ax,4c00h int 21h code ends end start
11.11 pushf 和 popf
pushf:将标志寄存器的值压栈; popf:从栈中弹出数据,送入标志寄存器中。 pushf 和 popf,为直接访问标志寄存器提供了一种方法。
哦对了,顺便说一下:我们学习破解的同学,到这里应该可以算是功德圆满了。因为以后的内容是真正的 8086 汇编的内容,跟破解是无关的,大家可以到这里为止😃