拓展:汇编语言的子程序

一个近(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

  最为关键的就是,将一段程序,我们认为是逻辑功能独立的子程序,用两个关键字,procendp,包围成了一个整体。
  子程序的名称,其实质也是代码的地址。如果子程序就是从第一条指令开始,按下面的写法也行:

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:将上面的程序按多文件的方式存放。

上一篇:juqery和dom对象互换


下一篇:【汇编语言/底层开发】9、转移指令的原理