📌 汇编语言是很多相关课程(如数据结构、操作系统、微机原理)的重要基础。但仅仅从课程的角度出发就太片面了,其实学习汇编语言可以深入理解计算机底层工作原理,提升代码效率,尤其在嵌入式系统和性能优化方面有重要作用。此外,它在逆向工程和安全领域不可或缺,帮助分析软件运行机制并增强漏洞修复能力。 本专栏的汇编语言学习章节主要是依据王爽老师的《汇编语言》来写的,和书中一样为了使学习的过程容易展开,我们采用以8086CPU为中央处理器的PC机来进行学习。
前面学习的例程中,子程序 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 ;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
❗注意:除了寄存器、内存传递参数外,还有一种通用的方法使用栈来传递参数。
设计一个子程序:功能:将一个全是字母,以0结尾的字符串,转化为大写。
程序要处理的字符串以0作为结尾符,这个字符串可以如下定义:
db ‘conversation’,0
应用这个子程序 ,字符串的内容后面定要有一个0,标记字符串的结束。
子程序可以依次读取每个字符进行检测,如果不是0,就进行大写的转化,如果是0,就结束处理。
由于可通过检测0而知道是否己经处理完整个字符串 ,所以子程序可以不需要字符串的长度作为参数。
我们可以直接用jcxz来检测0。
;说明:将一个全是字母,以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
来看一下这个子程序的应用。
要求:将data段中的字符串转化为大写。
assume cs:code
data segment
db 'conversation',0
data ends
代码段中的相关程序如下。
mov ax,data
mov ds,ax
mov si,0
call capital
要求:将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
上面的这个程序在思想上完全正确,但在细节上却有些错误,把错误找出来。
提示:问题在于cx的使用。
思考后看分析。
问题在于cx的使用,主程序要使用cx记录循环次数,可是子程序中也使用了cx,在执行子程序的时候,cx中保存的循环计数值被改变,使得主程序的循环出错。
从上面的问题中,实际上引出了一个一般化的问题:子程序中使用的寄存器,很可能在主程序中也要使用,造成了寄存器使用上的冲突。
那么如何来避免这种冲突呢?
粗略地看,可以有以下两个方案。
(1)在编写调用子程序的程序时,注意看看子程序中有没有用到会产生冲突的寄存器,如果有,调用者使用别的寄存器;
(2)在编写子程序的时候,不要使用会产生冲突的寄存器。
我们来分析一下上面两个方案的可行性:
(1)这将给调用子程序的程序的编写造成很大的麻烦,因为必须要小心检查所调用的子程序中是否有将产生冲突的寄存器。
比如说,在上面的例子中,我们在编写主程序的循环的时候就得检查子程序中是否用到了bx和cx,因为如果子程序中用到了这两个寄存器就会出现问题。
如果采用这种方案来解决冲突的话,那么在主程序的循环中,就不能使用cx寄存器,因为子程序中已经用到。
(2)这个方案也是不可能实现的,因为编写子程序的时候无法知道将来的调用情况。
可见,我们上面所设想的两个方案都不可行。
我们希望:
解决这个问题的简捷方法是,在子程序的开始将子程序中所有用到的寄存器中的内容都保存起来,在子程序返回前再恢复。可以用栈来保存寄存器中的内容。
以后,我们编写子程序的标准框架如下:
我们改进一下子程序 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
要注意寄存器入栈和出栈的顺序。