一个近(near)调用的程序
一个简单的包含子程序的汇编程序是:
; 要设置栈段,以便于call和ret指令使用
assume cs:code, ss:stack
stack segment
db 16 dup (0)
stack ends
code segment
start: mov ax,stack
mov ss,ax
mov sp,16
mov ax,1000
call s ;调用子程序
mov ax,4c00h
int 21h
s: add ax,ax ;子程序开始
ret ;子程序返回
code ends
end start
编译、连接后,用debug观察到:
从call
对应的机器指令中,可以看到这是一种近(near)调用,机器指令EB0500
中可以取出要调用的子程序,其偏移地址的位移是0005
。
进一步,用t命令,可以观察在调用子程序时,栈的变化过程,从而深刻理解子程序的机理。
一个远(far)调用的程序
assume cs:code, ss:stack
stack segment
db 16 dup (0)
stack ends
code segment
start: mov ax,stack
mov ss,ax
mov sp,16
mov ax,1000
call far ptr s ;在这儿变为远调用
mov ax,4c00h
int 21h
s: add ax,ax
ret
code ends
end start
编译、连接,用debug载入后,我们观察:
这就是远调用!在机器指令中,直接指定了子程序的CS和IP。
继续单步执行,观察在调用过程中栈的变化。这个观察,对我们了解程序设计中的子程序机制非常重要。
提高程序的可读性
下面,要将程序变个样。话从何说起呢?我怀念C语言中的{}
了。将一段逻辑上相关的代码,放在{}
中,看起来就有边有沿的,整齐,带来的好处,可读性提高,更关键的好处,程序的可读性提高。
于是有了下面的写法。
assume cs:code, ss:stack
stack segment
db 16 dup (0)
stack ends
code segment
main proc ;这是我们的主程序
start: mov ax,stack
mov ss,ax
mov sp,16
mov ax,1000
call far ptr s ;依然用s标识调用的入口
mov ax,4c00h
int 21h
main endp
subp proc ;这个作为子程序
s: add ax,ax
ret
subp endp
code ends
end start
从中看到,一个程序,分为若干个子程序,每个子程序长下面的样子:
名称 proc
…… ;实现逻辑功能的指令
(ret)
名称 endp
最为关键的就是,将一段程序,我们认为是逻辑功能独立的子程序,用两个关键字,proc
和endp
,包围成了一个整体。
子程序的名称,其实质也是代码的地址。如果子程序就是从第一条指令开始,按下面的写法也行:
assume cs:code, ss:stack
stack segment
db 16 dup (0)
stack ends
code segment
main proc
start: mov ax,stack
mov ss,ax
mov sp,16
mov ax,1000
call far ptr subp ;子程序的名称也就是子程序第一条指令的地址
mov ax,4c00h
int 21h
main endp
subp proc
s: add ax,ax
ret
subp endp
code ends
end start
冥冥中,我看到我C中的:
int main()
{
...
subp();
...
}
void subp()
{
...
}
模块化的味道出来了吧?
我们更进一步!
汇编程序的多文件组织
有人说,汇编只能编小程序。
我替我汇说:不服!
当编大程序时,分模块做就行了。更关键的,从工程组织的角度,高级语言能够将代码分别写在多个文件中,汇编语言照样能这么干!
怕有人郁闷,我悄悄地告诉大家,这一招,高级语言是从汇编语言处学的。其实,编程技术都是相通的,大家不要搞得不像一家人。
把上面的程序,分在两个文件中,一个文件中一个子程序:
step 1:建立“主程序”文件
;保存为p1.asm,这个文件中包括栈定义,以及“主程序”main
extrn subp:far ;声明在程序中要用到的subp是一个“外部”名称,要作为个far型的地址值
;这个声明必须有,可以上机试,看不加时有何提示
assume cs:code, ss:stack
stack segment stack
db 16 dup (0)
stack ends
code segment
main proc
start: mov ax,stack
mov ss,ax
mov sp,16
mov ax,1000
call far ptr subp
mov ax,4c00h
int 21h
main endp
code ends
end start
将p1.asm单独编译:
强烈建议:将extrn subp:far
省略掉,看看会出现什么?
step 2:建立“子程序”文件
;保存为p2.asm,这个文件中是“子程序”subp的定义
public subp ;声明subp将作为公共(public)符号,可以被外部访问
;试着将这个声明去掉,它不影响编译,但会影响连接(想想,为什么?)
assume cs:code
code segment
subp proc
s: add ax,ax
ret
subp endp
code ends
end
编译p2.asm:
step 3:连接
上述的两个.asm经过编译后,产生了两个.obj文件,分别是p1.obj和p2.obj。现在要做的工作,就是把这两个目标文件连接成一个可执行文件。
用的命令是:
连接的结果,产生了可执行文件p1.exe。
同学们,知道“连接”是什么意思了吧?再来多个文件,继续”+”好了。大工程,真的不惧。
提示:将step 2中的public subp
去掉,看看连接中会出现什么问题。进一步思考,在连接中有什么要求
step 4:运行程序
驾轻就熟的事情,debug就行。
“子程序”的代码哪去了?
可以发现,现在只是“主程序”的代码,主程序在076B段,而子程序,从子程序调用的指令看,在076D段。
继续看:
呵呵,这就找到了。
总结
本文用一个很简单的例子,介绍了汇编语言引入子程序后,程序的结构,以及多文件组织的形式。程序简单了些,但道理都在里面呢。
可以做一个练习,主程序调用子程序玩一玩。
【练习】
编制一个子程序,求y=x4,自变量 x 为字节,应变量y可以在一个字内存放而不溢出 参考解答
(1)版本1:子程序的参数由寄存器dl提供,返回结果在ax中;
(2)版本2:子程序不变,主程序中提供如下数据区,在主程序中,循环调用子程序,完成y=x4的求解,并将结果存入在相应的数据区:
data segment
x db 1,2,3,4,5,6,7,8
y dw 0,0,0,0,0,0,0,0
data ends
(3)版本3:数据区不变,子程序完成全部8个数据的求解任务,主程序只调用一次子程序即可。数据x的起始偏移地址由si提供,存放结果的y的偏移地址,由di提供,在调用前,由主程序为子程序提供si、di值。
(4)版本4:将上面的程序按多文件的方式存放。