(二十四)call 指令及应用--汇编笔记

汇编语言

Posted by YiMiTuMi on April 10, 2020

call指令

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

 1)将当前的IP或CS和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 标号

1)

   (sp) = (sp) - 2

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

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

2)

(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

转移地址在内存中的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

2) 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
			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)不会发生寄存器冲突。

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

编写子程序(实验10)

例.显示字符串

在指定位置,用指定颜色,显示一个用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

灯心草 – 温顺