10-ret/retf+call+mul

一、ret和retf

  • ret指令用栈中的数据,修改IP的内容,从而实现近转移。
  • retf指令用栈中的数据,修改CS和IP的内容,从而实现远转移。

CPU执行ret指令时,进行下面两步操作:

  1. (IP)=((ss)*16+(sp))
  2. (sp)=(sp)+2

CPU执行retf指令时,进行下面4步操作:

  1. (IP)=((ss)*16+(sp))
  2. (sp)=(sp)+2
  3. (cs)=((ss)*16+(sp))
  4. (sp)=(sp)+2

如果我们使用汇编语法来解释ret和retf指令,则:

CPU执行ret指令时,相当于进行:

pop IP

CPU执行retf指令时,相当于进行:

pop IP
pop CS

例子:

下面的程序中,ret指令执行后,(IP)=0,CS:IP指向代码段的第一条指令:

assume cs:code

stack segment
	db 16 dup (0)
stack ends

code segment
		mov ax,4c00h
		int 21h

start:	mov ax,stack
		mov ss,ax
		mov sp,16
		mov ax,0
		push ax
		mov bx,0
		ret
code ends

end start

下面的程序中,retf指令执行后,CS:IP指向代码段的第一条指令。

assume cs:code

stack segment
	db 16 dup (0)
stack ends

code segment
		mov ax,4c00h
		int 21h
start:	mov ax,stack
		mov ss,ax
		mov sp,16
		mov ax,0
		push cs
		push ax
		mov bx,0
		retf
code ends

end start

二、call指令

CPU执行call指令时,进行两步操作:

  1. 将当前的IP或CS+IP压入栈中
  2. 转移

call指令不能实现短转移,除此之外,call指令实现转移的方法和jmp指令的原理相同。

2.1 依据位移进行转移的call指令

指令语法为call 标号,执行的操作是将当前的IP压栈后,转到标号处执行指令。

CPU实际进行的操作如下:

  1. ( s p ) = ( s p ) − 2 (sp)=(sp)-2 (sp)=(sp)2
    ( ( s s ) × 16 + ( s p ) ) = ( I P ) ((ss)\times16+(sp))=(IP) ((ss)×16+(sp))=(IP)
  2. (IP)=(IP)+16位位移

这里:

  • 16位位移=标号处的地址-call指令后的第一个字节的地址
  • 16位位移的范围为-32768~32767,用补码表示、
  • 16位位移由编译程序在编译时算出

从上面的描述中,可以看出,如果我们用汇编语法来解释此种格式的call指令,则:

CPU指令"call 标号"时,相当于进行:

push IP
jmp near ptr 标号

练习:

下面的程序执行后,ax中的数值为多少?

内存地址				机器码				汇编指令
1000:0				b8 00 00 			mov ax,0
1000:3				e8 01 00			call s
1000:6				40					inc ax
1000:7				58				  s:pop ax

答案:6

2.2 转移的目的地址在指令中的call指令

上面的call指令,其对应的机器指令中并没有转移的目的地址,而是相对于当前IP的转移位移。

"call far ptr 标号"实现的是段间转移

CPU执行此种格式的call指令时,进行如下的操作:

  1. ( s p ) = ( s p ) − 2 ( ( s s ) × 16 + ( s p ) ) = ( c s ) ( s p ) = ( s p ) − 2 ( ( s s ) × 16 + ( s p ) ) = ( i p ) (sp)=(sp)-2\\((ss)\times16+(sp))=(cs)\\(sp)=(sp)-2\\((ss)\times16+(sp))=(ip) (sp)=(sp)2((ss)×16+(sp))=(cs)(sp)=(sp)2((ss)×16+(sp))=(ip)
  2. (cs)=标号所在的段地址
    (ip)=标号在段中的偏移地址

用汇编语法来解释此种格式的call指令,则:

push CS
push IP
jmp far ptr 标号

2.3 转移地址在寄存器中的call指令

指令格式:call 16位reg

功能:

( s p ) = ( s p ) − 2 ( ( s s ) × 16 + ( s p ) ) = ( i p ) ( i p ) = ( 16 位 r e g ) (sp)=(sp)-2\\((ss)\times16+(sp))=(ip)\\(ip)=(16位reg) (sp)=(sp)2((ss)×16+(sp))=(ip)(ip)=(16reg)

用汇编语法来解释此种格式的call指令:

push IP
jmp 16位reg

练习:

下面的程序执行后,ax中的数值为多少?

内存地址				机器码				汇编指令
1000:0				b8 06 00 			mov ax,6
1000:3				ff d0 				call ax
1000:5				40					inc ax
1000:6				58				  	mov bp,sp
										add ax,[bp]

2.4 转移地址在内存中的call指令

转移地址在内存中的call指令有两种格式:

call word ptr 内存单元地址

用汇编语法来解释此种格式的call指令,则:

push IP
jmp word ptr 内存单元地址

比如,下面的指令:

mov sp,10h
mov ax,0123h
mov ds:[0],ax
call word ptr ds:[0]

执行后,(IP)=0123H,(sp)=0EH

call dword ptr 内存单元地址

用汇编语法来解释此种格式的call指令,则:

push CS
push IP
jmp dword ptr 内存单元地址

比如,下面的指令:

mov sp,10h
mov ax,0123h
mov ds:[0],ax
mov word ptr ds:[2],0
call dword ptr ds:[0]

执行后,(CS)=0, (IP)=0123H, (SP)=0CH

三、call和ret的配合使用

首先看下这个问题:下面程序返回前,bx中的值是多少?

assume cs:code

code segment
	start:	mov ax,1
			mov cx,3
			call s				;将mov bx,ax的地址压栈	
			mov bx,ax			;(bx)=?
			mov ax,4c00h
			int 21h
		s:	add ax,ax
			loop s
			ret					;出栈,返回mov bx,ax所在地址
code ends
end start

首先看下CPU执行这个程序的主要过程:

  1. CPU将call s指令的机器码读入,IP指向了call s后的指令mov bx,ax,然后CPU执行call s指令,将当前的IP值(指令mov bx,ax的偏移地址)压栈,并将IP的值改变为标号s处的偏移地址
  2. CPU从标号s处开始执行指令,loop循环完毕后,(ax)=8
  3. CPU将ret指令的机器码读入,IP指向了ret指令后的内存单元,然后CPU执行ret指令,从栈中弹出一个值(即call s先前压入的mov bx,ax指令的偏移地址)送入IP中。则CS:IP指向指令mov bx,ax
  4. CPU从mov bx,ax开始执行指令,直至完成

程序返回前,(bx)=8。可以看出,从标号s到ret的程序段的作用是计算2的N次方,计算前,N的值由cx提供。

再来看如下的程序:

源程序								内存中的情况(假设程序从内存1000:0)处装入
assume cs:code					

stack segment
	db 8 dup (0)					1000:0000 00 00 00 00 00 00 00 00
	db 8 dup (0)					1000:0008 00 00 00 00 00 00 00 00
stack ends

code segment
	start:	mov ax,stack			1001:0000 B8 00 10
			mov ss,ax				1001:0003 8E D0
			mov sp,16				1001:0005 BC 10 00
			mov ax,1000				1001:0008 B8 E8 03
			call s					1001:000B E8 05 00
			mov ax,4c00h			1001:000E B8 00 4C
			int 21h					1001:0011 CD 21
		s:	add ax,ax				1001:0013 03 C0
			ret						1001:0015 C3
code ends

end start

看一下程序的主要执行过程:

  1. 前三条指令执行后,栈的情况如下:
    在这里插入图片描述
  2. call指令读入后,(IP)=000EH,CPU指令缓冲器中的代码为E8 05 00;
    CPU执行E8 05 00,首先,栈中的情况变为:
    在这里插入图片描述
    然后,(IP)=(IP)+0005=0013H。
  3. CPU从cs:0013H处(即标号s处)开始执行
  4. ret指令读入后:
    (IP)=0016H,CPU指令缓冲器中的代码为:C3
    CPU执行C3,相当于进行pop IP,执行后,栈中的情况为:
    在这里插入图片描述
  5. CPU回到cs:000EH处(即call指令后面的指令处)继续执行。

从上面的讨论中我们发现,我们可以写一个具有一定功能的程序段,我们称其为子程序,在需要的时候,用call指令转去执行,同时在子程序的后面使用ret指令,用栈中的数据设置IP的值,从而转到call指令后面的代码处继续执行。

这样,我们可以利用call和ret来实现子程序的机制,子程序的框架如下:

标号:
	指令
	ret

具有子程序的源程序的框架如下:

assume cs:code
code segment
	main:	...
			...
			call subl
			...
			...
			mov ax,4c00h
			int 21h
			
	sub1:	...
			...
			call sub2
			...
			... 
			ret
			
	sub2:	...
			...
			...
			ret
code ends
end main

四、mul指令

mul是乘法指令,使用mul做乘法时,注意下面两点:

  1. 两个相乘的数:两个相乘的数,要么都是8位,要么都是16位。如果是8位,一个默认放在AL中,另一个放在8位reg或内存字节单元中;如果都是16位,一个默认在AX中,另一个放在16位reg或内存字单元中。
  2. 结果:如果是8位乘法,结果默认放在AX中;如果是16位乘法,结果高位默认在DX中存放,低位在AX中放

格式如下:

mul reg
mul 内存单元

内存单元可以用不同的寻址方式给出,比如:

mul byte ptr ds:[0]

含义: ( a x ) = ( a l ) × ( ( d s ) × 16 + 0 ) (ax)=(al)\times((ds)\times16+0) (ax)=(al)×((ds)×16+0)

mul word ptr [bx+si+8]

含义:
( a x ) = ( a x ) × ( ( d s ) × 16 + ( b x ) + ( s i ) + 8 ) (ax)=(ax)\times((ds)\times16+(bx)+(si)+8) (ax)=(ax)×((ds)×16+(bx)+(si)+8)结果的低16位
( d x ) = ( a x ) × ( ( d s ) × 16 + ( b x ) + ( s i ) + 8 ) (dx)=(ax)\times((ds)\times16+(bx)+(si)+8) (dx)=(ax)×((ds)×16+(bx)+(si)+8)结果的高16位

五、模块化程序设计

从上面我们可以看出,call与ret指令共同支持了汇编语言编程中的模块化设计。在实际编程中,程序的模块化是必不可少的。因为现实的问题比较复杂,对现实问题进行分析时,把它转化成为相互联系、不同层次的子问题,是必须的解决方法。而call与ret指令对这种分析方法提供了程序实现上的支持。利用call和ret指令,我们可以用简捷的方法,实现多个相互联系、功能独立的子程序来解决一个复杂的问题。

5.1 参数和结果传递的问题

子程序一般都要根据提供的参数处理一定的事务,处理后,将结果(返回值)提供给调用者。本质上,我们需要了解,应该如何存储子程序需要的参数和产生的返回值。

比如,设计一个子程序,可以根据提供的N,来计算N的3次方

这里面就有两个问题:

  1. 将参数N存储在什么地方?
  2. 计算得到的数值,存储在什么地方?

很显然,可以使用寄存器来存储,可以将参数放到bx中;因为子程序中要计算N*N*N,可以使用多个mul指令,为了方便,可将结果放到dx和ax中,子程序如下:

cube:	mov ax,bx
		mul bx
		mul bx
		ret

用寄存器来存储参数和结果是最常使用的方法。对于存放参数的寄存器和存放结果的寄存器,调用者和子程序的读写操作恰好相反;调用者将参数送入参数寄存器,从结果寄存器中取到返回值;子程序从参数寄存器中取到参数,将返回值送入结果寄存器。

下面展示了一个完整的例子:计算data段中第一组数据的3次方,结果保存在后面一组dword单元中。

assume cs:code

data segment
	dw 1,2,3,4,5,6,7,8
	dd 0,0,0,0,0,0,0,0
data ends

code segment
	start:	mov ax,data
			mov ds,ax
			mov si,0			;ds:si指向第一组word单元
			mov di,16			;ds:di指向第二组dword单元
			
			mov cx,8
		s:	mov bx,[si]
			call cube
			mov [di],ax
			mov [di].2,dx
			add si,2			;ds:si指向下一个word单元
			add di,4			;ds:di指向下一个dword单元
			loop s

			mov ax,4c00h
			int 21h

	 cube:	mov ax,bx
	 		mul bx
	 		mul bx
	 		ret
code ends
end start

5.2 批量数据的传递

前面的例子中,子程序cube只有一个参数,放在bx中。如果有两个参数,那么可以用两个寄存器来放,可是如果需要传递的参数有3个、4个或更多直至N个,那么我们无法将它们存放在寄存器中,对于返回值,也有同样的问题。

在这种时候,我们将批量数据存放在内存中,然后将它们所在内存空间的首地址放在寄存器中,传递给需要的子程序。对于具有批量数据的返回结果,也可用同样的方法。

下面看一个例子,设计一个子程序,功能:将一个全是字母的字符串转化为大写。

这个子程序需要知道这个字符串的内容和长度。因为字符串中的字母可能很多,所以不便将整个字符串中的所有字母都传递给子程序。但是,可以将字符串在内存中的首地址放在寄存器中传递给子程序。因为子程序中要用到循环,我们可以用loop指令,而循环的次数恰恰就是字符串的长度。出于方便的考虑,可以将字符串的长度放到cx中。整个子程序的设计如下:

capital:	and byte ptr [si],11011111b		;这句的功能是将ds:si所指单元中的字母转化为大写
			inc si							;将ds:si指向下一个单元
			loop capital
			ret

一个完整的将data段中的字符串转化为大写的程序如下:

assume cs:code

data segment
	db 'conversation'
data ends

code segment
	start:	mov ax,data
			mov ds,ax
			mov si,0
			mov cx,12
			call capital
			mov ax,4c00h
			int 21h
  capital:	and byte ptr [si],11011111b
  			inc si
  			loop capital
  			ret
code ends
end start

5.3 寄存器冲突的问题

设计一个子程序,功能:将一个全是字母,以0结尾的字符串,转化为大写。

程序要处理的字符串以0作为结尾符,这个字符串可以如下定义:

db 'conversation',0

子程序的实现如下:依次读取每个字符进行检测,如果不是0,就进行大写的转化;如果是0,则结束处理。由于可通过检测0而知道是否已经处理完整个字符串,所以子程序可以不需要字符串的长度作为参数,而是使用jcxz来检测0

说明:将一个全是字母,以0结尾的字符串,转化为大写
参数:ds:si指向字符串的首地址
结果:没有返回值

capital:	mov cl,[si]
			mov ch,0
			jcxz ok								;如果(cx)=0,结束
			and byte ptr [si],11011111b			;如果不是0,将ds:si所指单元中的字母转化为大写
			inc si								;ds:si指向下一个单元
			jmp short capital
	 ok:	ret

接下来我们应用这个子程序:

首先,我们将一个字符串转化为大写:

assume cs:code
data segment
	db 'conversation',0
data ends

code segment
	mov ax,data
	mov ds,ax
	mov si,0
	call capital
	mov ax,4c00H
	int 21H
code ends

end

如果我们想要将多个字符串都转化为大写:

assume cs:code

data segment
	db 'word',0
	db 'unix',0
	db 'wind',0
	db 'good',0
data ends

code segment

start:	mov ax,data
		mov ds,ax
		mov bx,0
		mov cx,4
	s:  mov si,bx
	  	call capital
	  	add bx,5
	  	loop s
	  	mov ax,4c00H
	  	int 21H
code ends

end start

上面的程序在思想上完全正确,但在细节上却有些错误,问题在于cx的使用,主程序使用cx记录循环次数,可是子程序中也使用了cx,在执行子程序的时候,cx中保存的循环计数器值被改变,使得主程序的循环出错。

从上面的问题中,实际上引出了一个一般化的问题:子程序中使用的寄存器,很可能在主程序中也要使用,造成寄存器使用上的冲突。

实际上,在我们编写子程序时,我们希望:

  • 编写调用子程序的程序时候不必关心子程序到底使用了哪些寄存器
  • 编写子程序的时候不必关心调用者使用了哪些寄存器
  • 不会发生寄存器冲突

所以一个简便的解决方案是:在子程序的开始将子程序中所有用到的寄存器中的内容都保存起来,在子程序返回前再恢复。我们可以用栈来保存寄存器中的内容。

所以我们编写子程序的标准框架如下:

子程序开始:	子程序中使用的寄存器入栈
			子程序内容
			子程序中使用的寄存器出栈
			返回(ret,retf)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值