Bootstrap

【汇编语言】call 和 ret 指令(三) —— 深度解析汇编语言中的批量数据传递与寄存器冲突

在这里插入图片描述

前言

📌

汇编语言是很多相关课程(如数据结构、操作系统、微机原理)的重要基础。但仅仅从课程的角度出发就太片面了,其实学习汇编语言可以深入理解计算机底层工作原理,提升代码效率,尤其在嵌入式系统和性能优化方面有重要作用。此外,它在逆向工程和安全领域不可或缺,帮助分析软件运行机制并增强漏洞修复能力。

本专栏的汇编语言学习章节主要是依据王爽老师的《汇编语言》来写的,和书中一样为了使学习的过程容易展开,我们采用以8086CPU为中央处理器的PC机来进行学习。

1. 批量数据的传递

1.1 存在的问题

前面学习的例程中,子程序 cube 只有一个参数,放在bx中。如果有两个参数,那么可以用两个寄存器来放,可是如果需要传递的数据有3个、4个或更多直至 N个,我们怎样存放呢?

寄存器的数量终究有限,我们不可能简单地用寄存器来存放多个需要传递的数据。对于返回值,也有同样的问题。

1.2 如何解决这个问题

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

接下来我们来看下具体的落实……

1.3 示例演示

1.3.1 问题说明

下面看一个例子,

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

这个子程序需要知道两件事,字符串的内容和字符串的长度。

因为字符串中的字母可能很多,所以不便将整个字符串中的所有字母都直接传递给子程序。但是,可以将字符串在内存中的首地址放在寄存器中传递给子程序。因为子程序中要用到循环,我们可以用loop指令,而循环的次数恰恰就是字符串的长度。

出于方便的考虑,可以将字符串的长度放到cx。

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

1.3.2 程序实现

编程:将data段中的字符串转化为大写。

assume cs:code

data segment
  db 'conversation'
data ends

code segment

start:	mov ax,data
      	mov ds,ax
      	mov si,0		;ds:si指向字符串(批量数据)所在空间的首地址
      
      	mov cx,12		;cx存放字符串的长度
		call capital

      	mov ax,4c00h
      	int 21h
		
capital:and byte ptr [si],11011111b
		inc si
		loop capital
      	ret
		
code ends

end start

❗注意:除了寄存器、内存传递参数外,还有一种通用的方法使用栈来传递参数

2. 寄存器冲突问题的引入

2.1 问题引入

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

2.2 分析与解决问题

2.2.1 字符串定义方式

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

db  ‘conversation’,0

2.2.2 分析子程序功能

应用这个子程序 ,字符串的内容后面定要有一个0,标记字符串的结束。

子程序可以依次读取每个字符进行检测,如果不是0,就进行大写的转化,如果是0,就结束处理。

由于可通过检测0而知道是否己经处理完整个字符串 ,所以子程序可以不需要字符串的长度作为参数。

我们可以直接用jcxz来检测0

2.2.3 得到子程序代码

;说明:将一个全是字母,以0结尾的字符串,转化为大写
;参数:ds:si指向字符串的首地址
;结果:没有返回值
capital:mov cl,[si]
		mov ch, 0
		jcxz ok							;如果(cx)=0,结束;如果不是0,处理
		and byte ptr [si], 11011111b	;将ds:si所指单元中的字母转化为大写
		inc si							;ds:si 指向下一个单元
		jmp short capital
     ok:ret

2.3 子程序的应用

来看一下这个子程序的应用。

2.3.1 示例1

要求:将data段中的字符串转化为大写。

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

代码段中的相关程序如下。

mov ax,data
mov ds,ax
mov si,0
call capital

2.3.2 示例2

要求:将data段中字符串全部转化为大写。

assume cs:code

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

可以看到,所有字符串的长度都是5(算上结尾符0),使用循环,重复调用子程序capital,完成对4个字符串的处理。

完整的程序如下。

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

capital:mov cl,[si]
		mov ch, 0
		jcxz ok
		and byte ptr [si], 11011111b
		inc si
		jmp short capital
     ok:ret

code ends

end start

3. 寄存器冲突问题的发现与解决

3.1 重看代码

上面的这个程序在思想上完全正确,但在细节上却有些错误,把错误找出来。

提示:问题在于cx的使用。

思考后看分析。

3.2 分析与解决问题

3.2.1 存在的冲突问题

问题在于cx的使用,主程序要使用cx记录循环次数,可是子程序中也使用了cx,在执行子程序的时候,cx中保存的循环计数值被改变,使得主程序的循环出错

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

3.2.2 解决方案的探讨

那么如何来避免这种冲突呢?

粗略地看,可以有以下两个方案。

(1)在编写调用子程序的程序时,注意看看子程序中有没有用到会产生冲突的寄存器,如果有,调用者使用别的寄存器;

(2)在编写子程序的时候,不要使用会产生冲突的寄存器。

我们来分析一下上面两个方案的可行性:

(1)这将给调用子程序的程序的编写造成很大的麻烦,因为必须要小心检查所调用的子程序中是否有将产生冲突的寄存器。

比如说,在上面的例子中,我们在编写主程序的循环的时候就得检查子程序中是否用到了bx和cx,因为如果子程序中用到了这两个寄存器就会出现问题。

如果采用这种方案来解决冲突的话,那么在主程序的循环中,就不能使用cx寄存器,因为子程序中已经用到。

(2)这个方案也是不可能实现的,因为编写子程序的时候无法知道将来的调用情况。

可见,我们上面所设想的两个方案都不可行。

我们希望:

  • (1)编写调用子程序的程序的时候不必关心子程序到底使用了哪些寄存器。

  • (2)编写子程序的时候不必关心调用者使用了哪些寄存器。

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

3.2.3 给出解决方案。

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

3.2.4 子程序的标准框架

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

在这里插入图片描述

3.2.5 改进之前的程序

我们改进一下子程序 capital的设计:

capital: push cx
		 push si

 change: mov cl,[si]
		 mov ch, 0
		 jcxz ok
		 and byte ptr [si], 11011111b
		 inc si
		 jmp short capital
     
     ok: pop si
     	 pop cx
     	 ret

要注意寄存器入栈和出栈的顺序。

结语

今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下。

也可以点点关注,避免以后找不到我哦!

Crossoads主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是作者前进的动力!

在这里插入图片描述

;