Skip to content

Latest commit

 

History

History
824 lines (539 loc) · 14.9 KB

File metadata and controls

824 lines (539 loc) · 14.9 KB

第20节 call和ret指令

❤️💕💕汇编语言目前仍在发挥着不可替代的作用,在效率上无可替代,在底层,学习linux内核,计算机外围设备和驱动,都离不开汇编。Myblog:http://nsddd.top


[TOC]

call指令

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

  1. 将当前的IPCS和IP压入栈中

  2. 转移到标号处执行指令

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

压栈一次存放2个字节,也就是用2个字节来存放IP或着CS,同时存放IP和CS一共占4个字节。一般都保存在低地址位。

比如:

push IP

此时栈内ss:[0EH]保存IP的数据。

push CS
push IP

此时栈内ss:[0EH]保存IP,ss:[0CH]的数据。

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

call 标号将当前的IP(下一条指令的IP)压栈后,转到标号处执行)

 1)(sp) = (sp) - 2

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

注意当call一开始执行时,IP就指向下一条指令了,所以IP的地址为下一条指令的地址。

 3)(IP) = (IP) + 16位位移 (不能实现短转移)

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

CPU执行“call 标号”时,相当于:

push IP
jmp near 标号

例:ax的数值?

内存地址              机器码               汇编指令
1000:0               B8 00 00            mov ax, 0
1000:3               E8 01 00            call s    //1000H
1000:6               40                  inc ax
1000:7               58                  s:pop ax
//注意当call一开始执行时IP就指向下一条指令了,所以IP的地址为下一条指令的地址。

ax 为 6

转移的目的地址在指令中的call指令(段间)

“call far pty 标号”实现的时段间转移。

call far pty 标号

第一步:

(sp) = (sp) - 2
((ss) * 16 + (sp)) = (CS)
(sp) = (sp) - 2
((ss) * 16 + (sp)) = (IP)

第二步:

(CS) = 标号所在的段地址
(IP) = 标号所在的偏移地址

高位存放的是段地址(CS),低位存放的是偏移地址(IP)。

CPU执行“call far ptr 标号”时,相当于进行:

push CS
push IP
jmp far ptr 标号

例:ax中的数值?

内存地址          机器码             汇编指令
1000:0           B8 00 00          mov ax, 0
1000:3           9A 09 00 00 10    call far ptr s   //cs = 1000  IP = 0008 压入栈
1000:8           40                inc ax           
1000:9           58                s: pop ax        //0008H			
							   add ax, ax       //0010H
							   pop bx          //1000
							   add ax, bx      //1010H
//注意当call一开始执行时IP就指向下一条指令了,所以IP的地址为下一条指令的地址。

ax = 1010H

转移地址在寄存器中的call指令(段内)

指令格式:

call 16位reg

功能:

(sp) = (sp) - 2
((ss) * 16 + sp) = (ip)
(ip) = 16位reg

相当于:

push ip
jmp 16位寄存器

例:ax中的数值?

内存地址             机器码                    汇编指令
1000:0              B8 06 00                 mov ax, 6
1000:2              FF d0                    call ax   ip = 0005
1000:5              40                       inc ax   //跳过
1000:6                                       mov bp, sp //此时sp指向栈顶
                                             add ax, [bp] //栈顶位置数据 + ax 

ax = 11

ret 和 retf

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

retf 指令用栈中的数据,修改CS和IP的内容,从而实现远转移。高位为CS(段地址 ),低位为IP(偏移地址)。入栈要先入CS的地址。

CPU执行ret指令时:

(IP) = ((ss) * 16 + (sp))

(sp) = (sp) + 2

相当于:

pop IP

CPU执行retf指令时:

(IP) = ((SS) * 16 + (SP))

(SP) = (SP) + 2

(CS) = ((SS) * 16 + (SP))

(SP) = (SP) + 2

相当于:

pop IP

pop CS

例:

assume cs:code, ss:stack

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	;ax赋值给0   入栈
			push ax
			mov bx, 0	
			ret		;这个地方没有用call,也是可以用ret
code ends

end start

程序中ret拿到的IP的值为0,所以从程序的第一行开始执行。

retf指令

  • 先pop IP
  • 再pop CS
assume cs:code, ss:stack
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, code
			push ax	;先压入cs
			
			mov ax, 0
			push ax	;再压入ip:0
			mov bx
			retf
code ends

end start

程序中IP也是0,回到开头

例: 从内存1000:0000处开始执行程序

assume cs:dode, ss:stack

stack segment

	db 16 dup (0)

stack ends

code segment

	stack:  mov ax, stack
			mov ss, ax
			mov sp, 16

			mov ax, 1000H    //设CS = 1000H
			push ax

			mov ax, 0      //设IP = 0000H
			push ax

			retf

			mov ax, 4c00H
			int 21H

code ends

end start

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

  1. call word ptr 内存单元地址

相当于:

push ip

jmp word ptr 内存单元地址

例:

mov sp, 10H
mov ax, 0123H
mov ds:[0], ax
call word ptr ds:[0]
(IP) = 0123H (sp) = 0EH
  1. call dword ptr 内存单元地址

相当于:

push cs

push ip

jmp dword ptr 内存单元

例:

mov sp, 10H

mov ax, 0123H

mov ds:[0], ax

mov word ptr ds:[2], 0

mov dword ptr ds:[0]

(cs) = 0000H (sp) = 0CH (ip) = 0123H

例1:ax的数值为多少?

assume cs:code

stack segment

	dw 8 dup (0)

stack ends

code segment

	start:  mov ax, stack
			mov ss, ax
			mov sp, 16
			
			mov ds, ax
			mov ax, 0

			call word ptr ds:[0EH] 

			inc ax
			inc ax
			inc ax
			
			mov ax, 4c00H
			int 21H

code ends

end start

ax = 3

执行call时,此时IP的值已经指向下一条地址了也就是,(IP)存储在栈中的位置等于下一个内存单元的地址。此时跳转到ds:[0EH]保存的偏移地址就是放在栈中的IP地址所以,call跳转到下一条指令,所以程序按顺序执行。

  1. CPU取指令 : (call word ptr ds:[0EH])
  2. ip自增上述指令的长度 , 指向了下一条指令 (inc ax)
  3. 开始执行该指令 3.1. push ip ; 将 ip 压入栈 , 也就是:ss:[0EH] 保存 ip 的低 8 位 , ss:[0FH] 保存高 8 位 3.2. jmp ds:[0EH] ( (ip) = ds:[0EH] , 也就是说 , 程序又从 ds:[0EH] 中取出数据赋值给 ip , 然后继续执行 )
  4. 现在其实就开始执行 ip 之前保存的地址的指令了 , 也就是三个 inc ax
  5. 因此最终 ax 值为 3

例2:ax 和 bx的数值为多少?

assume cs:code 

data segment
	dw 8 dup (0)
data ends

code segment
	start:  mov ax, data
			mov ss, ax
			mov sp, 16
	
			mov word ptr ss:[0], offset s
			mov ss:[2], cs

			call dword ptr ss:[0]   ;跳转位置 cs = 代码段开头, ip = s的位置 
			nop

		s:  mov ax, offset s
			sub ax, ss:[0Ch]  ;0
			mov bx, cs        ;0
			sub bx, ss:[0EH]
			
			mov ax, 4C00H
			int 21H

code ends

end start

ss:[0CH] 处保存的是 : call dword ptr ss:[0] 这条指令的下一条指令 nop 的相对于代码段偏移地址 ax = (mov ax, offset second 的偏移地址) - ( nop 的偏移地址) 也就是 nop 指令的长度 , 也就是 1。(0CH存放的就是call位置的下一条地址的偏移位置)

将代码段的基址赋值给 bx ,bx = (bx) - (ss:[0EH]),(ss:[0EH]) = (cs)因此 bx = 0。

call 和 ret的配合使用

例:bx的值?

assume cs:code

code segment

	start:  mov ax, 1
			mov cx, 3

			call s   ;到子程序s执行
			mov bx, ax
			mov ax, 4c00H	
			int 21H

		s:  add ax, ax
			loop s
			ret		

code ends

end start

分析:

 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开始执行指令,直至完成。

例:

源程序和内存中情况(1000:0000装入)

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   BE 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 strat

CPU读入call s指令后,程序跳转到s处,并把mov ax, 4C00H处的指令保存到栈中,然后执行从s处开始执行,当CPU读入ret指令后,ret跳转栈中存放的IP偏移地址所指向的位置,即mov ax, 4C00H 处。

根据call和ret可以写一个具有一定功能的程序段,我们称其为子程序,需要的时候,用call指令转去执行,可以在子程序的后面使用ret指令,从而跳转到call指令后面的代码处继续执行。

利用call和ret来实现子程序的机制。子程序框架:

标号:
	指令
	ret

程序框架:

assume cs:code

code segment

	main:  :
		   :
		   call sub1
		   :
		   :
		   mov ax, 4C00H
		   int 21H

     sub1: :
		   :
		   call sub2
           :
           :
           ret

     sub2: :
           :
		   ret

code ends

end main

参数和结果传递问题(call和ret连用)

例:计算data段中第一组数据的3次方,结果保存在后面一组数据中。

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
			mov di, 16
			mov cx, 8
			
		s1:	mov bx, ds:[si]
			mov dx, ax
			call s
			mov ds:[di], ax  //保存低位
			mov ds:[di].2, dx  //保存高位
			add si, 2     //后移
			add di, 4	  //后移
			loop s1

			mov ax, 4C00H
			int 21H

	    s:  mov ax, bx  //(不能用dx)
			mul bx
			mul bx
			ret

code ends

end start

批量数据的传递

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

例:将一个全是字母的字符串转化为大写。

assume cs:code

data segment

	db 'conversation'

data ends

code segment

	start:  mov ax, data
			mov ds, ax
			mov cx, 12
			mov si, 0
			call s

			mov ax, 4C00H
			int 21H


		s1: and byte ptr ds:[si]:11011111B  //转换,传递内存单元的首地址
			inc si
			loop s1
			ret

code ends

end start

除了用寄存器传递参数外,还有一种通用的方法是用栈来传递参数。

寄存器冲突问题

例:将一个全是字母,以0结尾的字符串,转换为大写。

assume cs:code

data segment

	db 'conversation', 0

data ends

code segment

	start:  mov ax, data
			mov ds, ax
			mov si, 0
			
			call s

			mov ax, 4C00H
			int 21H

		s:  mov cl, ds:[si]
			mov ch, 0
			jcxz ok
			and byte ptr ds:[si], 11011111B
			inc si
			jmp short s   //死循环
			
		ok: ret

code ends

end start

例:

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 cx, 4
			mov bx, 0

		s:  mov si, 0
			call s1
			add bx, 5
			pop cx
			loop s

			mov ax, 4C00H
			int 21H
	
		s1: push cx
			mov cl, ds:[bx + si]
			mov cH, 0
			jcxz s0
			and byte ptr ds:[bx + si], 11011111B
			inc si
			jmp short s1
			
		s0: ret

code ends

end atart

我们希望:

1)编写调用子程序的程序的时候,不必关心子程序到底使用了那些寄存器;

2)编写子程序的时候不必关系调用者使用了那些寄存器。

3)不会发生寄存器冲突。

解决这个问题的简捷方法是,在子程序的开始将子程序种所有用到的寄存器种的内容都保存起来,在子程序返回前再恢复,或不再子程序中改变寄存器的值。

编写子程序

例.显示字符串

在指定位置,用指定颜色,显示一个用0结束的字符串。

(dh)= 行号(0 ~ 24), dl = 列号(0 ~ 79), (cl) = 颜色。

在屏幕第8行,第3列,用绿色显示data段中的字符串。0 ~ 79 个字符,一个字符1字节加上1字节的颜色。

第8行相当于向下7个160个字节的位置,第3列相当于向右3个字符,即3 * 2个字节的位置。

显存存放,低位字符,高位颜色, 所以一共占2个字节。 字符一个字节,颜色一个字节。

assume cs:code

data segment

	db 'Welcome to masm!', 0

data ends

code segment
	
	start:  mov dh, 8
			mov dl, 3
			mov cl, 2
			mov ax, data
			mov ds, ax
			mov si, 0
			call show_str

			mov ax, 4C00H
			int 21H
		
  show_str: //保存用到的寄存器
			push cx
			push si
			push es
			push di
			push bx

			mov ax, 0B800H  //显存的起始地址(每台电脑不一定一样)
			mov es, ax
			mov di, 0       

			//计算行开始的位置
			mov al, 0A0H    //一行160个字节, 80 * 2 = 160
			dec dh          //第8行,0-7  也就是7的位置
			mul dh          //一共dh
			mov bx, ax

			//计算列开始的位置
			mov al, 2       //一列
			dec dl          //列也是 从零开始
			mul dl          //一个字符2个字节
			add bx, ax      //开始的偏移位置
			
	    	mov al, cl      //保存颜色
			
         s: mov cl, ds:[si]
			mov ch, 0
			mov es:[bx + di], ds:[si]  //显示字符串
			mov es:[bx + di], al   //显示颜色
			jcxz ok                //判断是否为零
			inc si
			add di, 2               //移动2个字节,即一个字符
			jmp short s

		ok: //取回保存的值,注意出栈顺序
			pop bx
			pop di
			pop es
			pop si
			pop cx
			ret	
	
code ends

end start

END 链接