[Intel汇编-MASM]转移指令

1. 通过seg和offset操作符获取标号的段地址和偏移地址:

    1) 这两个都是伪指令,都是属于编译器的操作符,不能直接翻译成机器代码,前者是segment的缩写,而后者的意思就是“偏移量”;

    2) 这两者可以作用于任何标号,前者用于获取标号所在段的段基,后者用于获取标号所在段的偏移地址,这里的段都是由"XXX segment"定义的段,而这个XXX(也是标号)就是段基了;

    3) offset还可以作用于变量,而seg只能作用域标号,不能作用于其它东西(否则会报错的);

变量,是编译器的一种特殊功能,可以在某个地方用"VarName = Value"的形式定义一个以VarName作为代码中的代号的数据空间,可以直接在代码中把它当一个内存单元来使用,比如”mov ax, VarName“,而传送的值就是VarName所代表的那个内存中的数据Value,而offset VarName就相当于C语言的&(取地址运算符),可以获得该变量所在段的偏移地址;

!注意:offset只用于标号和变量,而seg只用于标号,不要用它们作用域其它任何东西,有时候不会报错,但是在程序中可能会出现无法预测的错误;

    4) 示例:

assume cs:code, ds:data, ss:stck

data segment
	db 3 dup(0)
s1:	mov		ax, bx
data ends

stck segment
	db 5 dup(0)
s2:	nop
	mov	ax, bx
stck ends

code segment

start:
	mov		ax, seg data
	mov		ds, ax
	mov		ax, seg stck
	mov		ss, ax

	mov		ax, offset s1
	mov		bx, offset s2
	mov		cx, seg s1
	mov		dx, seg s2

	mov		ax, 4C00H
	int		21H
code ends

end start
运行结果:

[Intel汇编-MASM]转移指令_第1张图片

可以看到s1和s2的offset都是相对各自段的段基的偏移量(s1的段基是data,偏移量是3,s2的段基是stck,偏移量是5),而ds和cx相等(都是data)、ss和dx相等(都是stck),虽然以前的那种写法"mov ax, code"也行,但不过这种更正式,建议以后都是用"mov ax, seg code",这样更符合逻辑;

!以后要常用seg和offset!养成良好的习惯!  

    5) 不要把标号当做立即数使用!

        i. 上面已经提到过了,在有些情况下是可以将标号当做立即数使用,比如上面讲过的"mov ax, codesg"等,但是这是有前提的;

        ii. 规则:如果标号定义在“把它当做立即数使用“之前则没问题,如果标号定义在”把它当做立即数使用“之后则会报错!这里的定义标号就是指"标号:"的出现,请看下例;

将代码段中的第一条指令mov ax, bx移动到nop处(注意,mov ax, bx指令占两个字节)

assume cs:code

code segment

s1:
	mov		ax, bx
	mov		ax, cs:[s1]
	mov		cs:[s2], ax

s2:
	nop
	nop

	mov		ax, 4C00H
	int		21H
code ends

end
!注意:

    i. nop是指空指令,占一个字节,但CPU不会执行它,但是会占用CPU时间,一般在用于给外设留一段响应时间时用到,但一般尽量不要使用它,因为浪费CPU时间,碰到这种情况一般用多线程处理;

    ii. 上面这段程序会报错,报错的语句时mov cs:[s2], ax,原因是s2在这条语句后面定义;

    iii. 但是编译器规定,不能在中括号[ ]中使用offset和seg对标号进行取值,因此正确的做法就是先将它们的地址暂存在寄存器中,然后进行中转;

!注意:以后要养成良好习惯,绝对不要把标号当做立即数使用,要用到标号的地址时就一定要用seg或offset对其进行取值(有时需要用寄存器或内存暂存该值),因为这两个操作符不受标号定义位置的限制!

请看正确的程序:

assume cs:code

code segment

s1:
	mov		ax, bx
	mov		si, offset s1
	mov		di, offset s2
	mov		ax, cs:[si]
	mov		cs:[di], ax

s2:
	nop
	nop

	mov		ax, 4C00H
	int		21H
code ends

end
运行结果:


可以看到最后一行的结果,成功的把mov ax, bx这条指令覆盖到了两个nop的位置; 


2. 转移指令jmp:

    1) 即jump的缩写,也叫做跳转指令,其中跳转的目的地地址可以是标号,该地址也可以保存在寄存器或内存中;

    2) 跳转的目的地为标号:

        i. 有两种形式,一种是段内的位移跳,另一种是段间的远跳;

!注意:源程序中的段是根据“XXX segment”分的,两个不同的段就是段间,同一个段的最大长度为64KB,即16位长度所能表示的最大字节数(65536个字节),如果在一个段中代码写得太多,超过64KB编译就会报错,告诉你代码长度已经超过一个段的最大长度了!

        ii. 段内的位移跳(之后标号都用tag表示):

            *1. 段内短转移:jmp short tag

            *2. 段内近转移:jmp near ptr tag

!注意:后者比前者多了一个ptr,这点一定要注意,千万不要写错了!两者的区别就是前者跳跃的范围是8bit大小的长度(即-128~127,即向前跳128不超过字节,向后跳不超过127字节),而后者跳跃的范围是16bit大小的长度(即-32768~32767,向前跳32768不超过字节向后跳32767不超过字节),后者比前者跳跃范围更远;

**由于这两者跳跃的范围都不超过16bit,并且是有符号的16bit,而编译器规定一个段的大小不能超过16bit无符号数(64KB),因此这两种跳跃都属于“段内”跳;

            *3. 位移性:通过查看它们的机器代码可以发现其并不是直接将ip寄存器的内容修改成offset tag,而是先将ip++(即加到jmp后面一条指令的代码处),然后再将ip加上offset tag和此时ip的差值(即一段位移量),段内转移是通过位移的方式实现跳转的!

        iii. 段间的远跳:jmp far ptr tag,这种方法可以直接修改cs:ip的值达到转移的目的,此时会把cs:ip的值改为tag的段地址和偏移地址,可以实现两个段之间的跳跃;

        iv. 编译器对跳出界的检查:

            *1. 对far ptr不作任何检查,可以全局随意跳,所以要慎用,出错率很大!

            *2. 对near ptr不检查跳跃的范围是否超出(-32768~32767),因为只要跳跃的标号是在段内,则必然不会超出这个范围的,因此只检查跳跃的标号是否在另一个段内(即另一个XXX segment内),只要是在另一个段内就会报错,不管跳跃的范围是否超出-32768~32767,因此它是一种语义上严格的“段内”跳;

            *3. 对short会检查跳跃范围是否在(-128~127)内,如果超出则会报错,同时也near ptr一样,不管范围有没超决不允许段间跳跃,否则会报错;

            *4. 小结:对于段内位移跳,必须只能在段内跳跃,短转移限于8bit长度大小跳跃,近转移不限,而段间远跳可以全局随意跳(也是段间跳跃的唯一办法),即使用远跳来实现段内跳跃,也是改变cs:ip的;

        v. 示例:

assume cs:code, ds:data

data segment
s3:	nop ; 标号不能定义在db/dw等伪指令之前,所以用一个nop顶一下
	db 65535 dup(0) ; 不能超过65536个字节(64KB),否则会超出段最大长度而报错
data ends

code segment

s1:
	mov		ax, bx
	mov		si, offset s1
	mov		di, offset s2
	mov		ax, cs:[si]
	mov		cs:[di], ax

	jmp		short s1

	jmp		near ptr s2
	db		128 dup(0) ; 超过8bit长度大小的区域
s2: nop
	nop

	jmp		far ptr s3

	mov		ax, 4C00H
	int		21H
code ends

end s1
运行结果:

[Intel汇编-MASM]转移指令_第2张图片 

可以看到对于远跳直接翻译成"jmp xx:xx"的形式,虽然段内位移跳汇编代码是被进一步翻译成"jmp 偏移地址"的形式,但是在机器代码中是表现成“标号地址和jmp后面一条指令地址的差值的补码”形式的,由此可见是一种位移跳!

    3) 转移目的地在寄存器中:

        i. 这种方式的转移是直接修改ip寄存器的值,即直接用寄存器给出转移目的地的偏移地址,因此也是一种段内转移方式;

        ii. 由于是直接修改ip值,又因为ip是16位的,因此保存偏移地址的寄存器只能是16位寄存器,除了段寄存器以外其余所有16位寄存器都行;

        iii. 使用形式:jmp 16-bit-register

        iv. 由于修改的是偏移地址,因此跳跃必定在段内,所以不做出界检查!

    4) 转移目的地在内存中:

        i. 同样也是通过修改cs和ip寄存器达到转移的目的的;

        ii. 一共有两种方式,一种是只修改ip的段内转移,还有一种是同时修改cs:ip的全局任意跳;

        iii. 段内跳:jmp word ptr 内存单元

        iv. 远跳:jmp dword ptr 内存单元,其中低16位是偏移地址,高16位是段地址;

        v. 对于这两种方式同样是不做出界检查的;

!注意:没有8字节的转移,只有16位和32位的转移!

    5) 寄存器转移和内存转移的示例:

assume cs:code

code segment

	jmp		ax ; ip -> ax
	
	mov		word ptr [bx], 2000H
	jmp		word ptr [bx] ; ip -> 2000H

	mov		word ptr [bx], 2000H
	mov		word ptr 2[bx], 735AH
	jmp		dword ptr [bx] ; cs:ip -> 0735AH:2000H

	mov		ax, 4C00H
	int		21H
code ends

end

3. 条件转移指令:

    1) 前面介绍的jmp指令都属于无条件转移指令,只要执行到jmp指令,则必定执行跳转;

    2) 有条件转移指令就是当执行到jmp指令时必须先判断某个条件是否成立,这要在该条件成立时才会执行跳转,否则就继续执行jmp指令后面的指令类似实现if语句的功能;

    3) MASM中所有的条件转移指令都是段内短转移类型的,等价于jmp short类型的转移,因此也属于位移转移,最常见的条件指令有jcxz以及我们所熟悉的loop循环指令;

    4) MASM规定所有的条件转移指令只能作用于标号,不能作用于其它任何东西(包括寄存器、内存单元),否则会报错;

    5) jcxz即为jump when cx is zero,即当cx为0的时候跳转,而loop的功能恰好相反,即当cx不为0的时候跳转,因此可以把loop理解为"jcxnz",即jump when cx is not zero;

    6) 示例:在2000H:0H为起始的内存中找出第一个等于0的字节,并将其偏移地址存入dx中

assume cs:code

code segment
	mov		ax, 2000H
	mov		ds, ax
	mov		bx, 0

	mov		cx, 0
lp:
	mov		cl, [bx]
	jcxz	quit
	inc		bx
	jmp		short lp

quit:
	mov		dx, bx

	mov		ax, 4C00H
	int		21H
code ends

end
运行结果:

[Intel汇编-MASM]转移指令_第3张图片


4. 位移转移的好处:

    1) 包括所有的条件转移,以及标号的段内转移;

    2) 根据位移转移的好处:因为每次程序装载在内存的位置可能是随机的,并且可能会出现这样的情况,就是一个程序同时开了好几个,那么这几个程序必然不可能位于内存的同一位置,这就造成了它们程序中标号的绝对地址都是不一样的,或者说是随着程序加载的位置不同而不同,而程序一经编译,这些标号就会被替换成固定的符号地址,如果是根据绝对地址跳转就会导致错误,但不过位移转移是相对地址,不管程序如何装载,跳转的方向和距离都是固定的,因此永远也不会出错,这就有利于程序在内存中的浮动;

    3) 只修改ip的段内转移和位移转移一样安全:因为偏移地址也是一种相对于段基的位移,偏移地址和段基之间也是一种相对距离,因此这种类型的转移也和程序在内存中的浮动无关,即程序的浮动对其无影响;

    4) 因此要尽量使用段内转移,而杜绝或尽量避免使用段间远转移,因为远转移是直接修改绝对地址,不利于程序在内存中的浮动,从而对程序造成危害!


你可能感兴趣的:(masm,intel汇编,转移指令)