目录
转移指令的原理
转移指令用作单独修改IP或是同时修改CS和IP,为了让CPU执行我们指定的指令。
若只修改IP,则转移指令可分为:
-
段内短转移。IP修改范围为(-128~127)
-
段内近转移。IP修改范围为(-32768~32767)
若同时修改CS和IP,则转移指令可称为:
- 段间转移。这种方式对CS和IP的范围没有限制。
操作符offset
offset用来得出某个标号的偏移地址,例如:
s: mov bx,0 mov ax,offset s
offset s可得出s处的偏移地址,并将其赋给ax。
jmp指令
段内短转移
1、jmp short s
通过指明标号,将IP修改为标号所在处的偏移地址。
Java
mov ax,0 jmp short s add ax,2 s:inc ax
段内近转移
1、jmp near ptr s
和段内短转移类似,只不过范围大一些。
2、jmp ax
从寄存器中获取数据,作为IP的值。
mov ax,0123h jmp ax
3、 jmp word ptr [0]
从内存中获取数据,作为IP的值。
mov ax,0123h mov [bx],ax jmp word ptr [bx]
段间转移
1、jmp far ptr s
执行后,CS=标号所在段的段地址,IP=标号所在段的偏移地址
c0 segment jmp far ptr s c0 ends c segment s: mov ax,0123h c ends
2、jmp dword ptr [0]
dword表示操作的数据长度有2个字(4个字节),执行后,CS=高地址字的数据,IP=低地址字的数据
mov word ptr [0],10h mov word ptr [2],76ah jmp dword ptr [0]
CS=076a,IP=10h
jmp指令原理
jmp指令数据无条件的转移指令,在运行jmp指令时,CPU及内存会经过如下过程:
1、计算 CS * 16+IP,确定指令所在内存地址
2、将内存中的指令加载到指令缓冲期
3、IP指向下一条命令的偏移地址
4、CPU执行指令
想要jmp达到跳转的效果,有两种方式:
1、指明目标地址,如 jmp 076a:0010 或 jmp 0123
2、指明偏移地址,如 jmp 3,表示跳转到(当前地址+3)处的指令
例如 jmp short s,其编译后的机器码中包含的是偏移地址;jmp far s 编译后,机器码存储的是目标地址。
jmp short s
当程序运行到jmp指令时,IP已经指向了下一条命令,因此IP只需要+3。EB03中,03代表的就是偏移长度(或称作位移)。
位移的范围从-128到127,也称作8位位移(8位bit)。8位位移 = 标号所在偏移地址 - jmp下一条指令所在偏移地址。
jmp near ptr s 的原理相同,只不过是16位位移。
jmp far ptr s
编译后的指令是明确的地址。jmp ddword ptr [0] 的原理相同。
jcxz、loop
语法:jcxz 标号
jcxz指令是条件转移指令,同时属于短转移指令,位移范围为-128~127。它的运行机制是:若cx=0,则IP=IP+8位位移;否则执行下一条指令。用 C语言描述为:if (cx == 0) jcxz 标号。
语法:loop 标号
loop指令是循环转移指令,同属于短转移指令,运行机制是:若cx!=0,则jmp 标号;否则执行下一条指令。用C语言描述为:if (cx != 0) jmp 标号。
根据位移进行转移的好处
当程序载入内存,机器码记录的是位移,那么整段程序可以存储器其他位置并成功执行。若机器码记录的是绝对地址,那么程序移动到其他地址后,jmp跳转的是固定地址,程序会出错。
但是如果只将jmp指令移动到其他位置,执行时仍会出错。
CALL和RET指令
ret 和 retf
ret指令运行过程:
pop IP
retf指令运行过程:
pop IP pop CS
call
call 指令无法实现短转移。
call指令运行过程:
将IP或CS和IP压栈 转移
1、call s
段内近转移。
push ip jmp near ptr s
原理也是IP = IP + 16位位移。不像jmp指令可通过 short 控制位移长度。
2、call ax
段内近转移。
push ip jmp ax
3、call word ptr [0]
段内近转移。
push ip jmp word ptr [0]
4、call far ptr s
段间转移。
push cs push ip jmp far ptr s
5、call dword ptr [0]
段间转移。
push cs push ip jmp dword ptr [0]
ret 和 call 搭配使用
call之后,将下一条指令的IP/CS和IP push到栈中,之后跳转到其他标号/位置运行指令,这个其他位置可以用来编写子程序(这个过程个人理解为跳转到其他函数),在子程序最后通过ret/retf,pop栈中的IP/CS和IP,达到函数返回的效果,这样可以继续执行之前call指令下方的指令。
Go
assume cs:code code segment main: ; call sub1 ; 调用子程序sub1 ; mov ax,4c00h int 21h sub1: ; call sub2 ; 调用子程序sub2 ; ret ; 子程序sub1返回 sub2: ; ret ; 子程序sub2返回 code ends end
mul指令
mul指令用作乘法运算,语法为 mul 寄存器
或 mul 内存单元
。mul运算的两个因子,要么都是8位,要么都是16位。
- 8位相乘:另一个因子默认存放在al中。运算的结果,存于ax中。
- 16位相乘:另一个因子默认存放在ax中。运算的结果存于ax和dx中,ax存放低8位,dx存放高8位。
模块化设计子程序
子程序类似高级语言的函数,需要有参数传递和结果返回的机制。在汇编语言中,是如何解决的?
问题一:参数传递和返回
- 通过寄存器传递参数,并将结果赋给寄存器。
这种做法相当于使用全局变量用作数据传递,编写多个子程序都能共享寄存器。若是参数过多,寄存器不够用,那么可以将参数分配到连续的内存上。
- 使用栈传递参数
先将参数push到栈段中,再运行call指令。这样做,参数会被拷贝到栈中,变得相对独立。子程序中,也能通过偏移量明确参数的位置。
问题二:寄存器冲突
外部调用程序和子程序共用一个寄存器,冲突不可避免。解决方案为:
- 备份现场 - 数据运算 - 恢复现场
在子程序运行到逻辑代码之前,先将当前用到的寄存器备份到内存(push到栈中),之后运算就不需要担心冲突。运算结束后,将备份重新赋值给寄存器。
fun: push cx push si ; 进行运算 pop si pop cx ret
标志寄存器
标志寄存器是16位的寄存器,它的每一位bit具有特殊的含义。在不同的CPU中,标志寄存器的位数和发挥作用的bit位可能不同。
ZF,零标志位
相关指令执行后,结果是否为0。若为0,则ZF=1,否则ZF=0。
PF,奇偶标志位
相关指令执行后,结果的所有bit位中1的个数是否为偶数。若为偶数,则PF=1,否则PF=0。
SF,符号标志位
SF将运算看作有符号的运算,物理意义上的运算结果(最高bit位是0还是1)为负数,SF=1,否则SF=0。
不能通过SF判断逻辑运算结果。 逻辑上运算结果的正负,不一定与物理bit位显示的正负一致。因为有可能发生溢出,导致逻辑结果虽然是负数,但最高bit位是0。
CF,进位标志位
CF将运算看作无符号的运算,最高位产生了进位,或是最高位需要借位,CF=1,否则CF=0。
inc不影响CF位。
OF,溢出标志位
OF将运算看作有符号的运算,逻辑运算的结果产生了溢出,则OF=1,否则OF=0。
adc 和 sbb
-
adc
语法:adc [对象1],[对象2]
运算:对象1 = 对象1 + 对象2 + CF -
sbb
语法:sbb [对象1],[对象2]
运算:对象1 = 对象1 - 对象2 - CF
当计算大位数相加或相减时,可以通过adc和sbb指令计算。例如48bit数相加,拆分成高16bit、次高16bit、低16bit,只需:
sub ax,ax ; 将CF置为0 adc [低16bit],[低16bit] adc [次高16bit],[次高16bit] adc [高16bit],[高16bit]
sbb计算大位数相减,原理同理。
cmp
语法:cmp [对象1],[对象2]
运算:对象1 - 对象2
cmp指令不会存储运算结果,但是会影响指令寄存器,通过标志位可以判断对象1和对象2的大小关系。
cmp将运算看作无符号运算:
- zf=1,则 ax = bx
- zf=0,则 ax != bx
- cf=1,则 ax < bx
- cf=0,则 ax >= bx
- cf=0且zf=0,则 ax > bx
- cf=1或zf=1,则 ax <= bx
cmp将运算看作有符号运算:
- sf=1且of=0,则 ax < bx
- sf=1且of=1,则 ax > bx
- sf=0且of=0,则 ax >= bx
- sf=0且of=1,则 ax < bx
检测比较结果的转移指令
cmp指令与上述转移指令搭配使用,可以通过判断大小进行指令转移。例如:求一段内存中大于32的数字个数
Go
mov ax,0 mov cx,8 s: cmp byte ptr [bx],32 jna next inc ax next: inc bx loop s
这种搭配使用类似于一种编程技巧,本质是通过改变标志位实现跳转,所以并不是只有cmp指令可以达到类似的效果,其他的指令也可以。
串送指令(movsb、movsw)
串传送指令的相关内容:
-
DF标志
每次串传送指令结束后,如果DF=0,则si、di递增;
每次串传送指令结束后,如果DF=1,则si、di递减。 -
movsb指令
语法:movsb
作用:将ds:si指向的字节内容,移动到es:di指向的字节。并根据DF的值,将si、di加1或减1。 -
movsw指令
语法:movsw
作用:将ds:si指向的字内容,移动到es:di指向的字。并根据DF的值,将si、di加2或减2。 -
rep指令
语法:rep movsb 或 rep movsw
作用:将某一连续内存的数据移动到另一处连续内存中。类似于:
Go
s: mov es:[di],byte ptr ds:[si] inc si ; DF=0 inc di loop s
-
cld指令
语法:clf
作用:将DF置为0 -
std指令
语法:stf
作用:将DF置为1
pushf、popf
-
pushf
语法:pushf
作用:将标志寄存器压栈 -
popf
语法:popf
作用:将栈顶内容弹出,并赋值给标志寄存器