汇编——子程序应该怎么写、子程序传参、Fibonacci递归举例

在学习子程序的过程中,看了整个的ppt,也没搞明白一个带有子程序的汇编程序长什么样。
我们先看一个例子,然后分析。

datas segment
	w       dw 10
	h       dw 6
	turns   dw 30
	start_x dw 20
	start_y dw 20
datas ends

stack segment stack
	      dw 32 dup(?)
stack ends

code segment
	        assume cs:code,ds:datas,ss:stack
main proc near
	start:  
	        mov    ax,datas
	        mov    ds,ax
	        mov    ax,stack
	        mov    ss,ax

	        mov    ah,0
	        mov    al,4                     	;图像模式
	        int    10h
	        mov    cx,turns
	loops:  
	        call   hor_ver
	        mov    ah,0
	        int    16h
	        loop   loops
	        mov    ah,0                     	;文本模式
	        mov    al,3
	        int    10h

	        mov    ah,4ch
	        int    21h
main endp
	;打印一个楼梯
hor_ver proc near
	        push   ax
	        push   bx
	        push   cx
	        push   dx

	        mov    cx,w
	li:     
	        push   cx
	        mov    cx,start_x
	        mov    dx,start_y
	        mov    al,1
	        mov    ah,0ch
	        int    10h
	        inc    start_x
	        pop    cx
	        loop   li

	        mov    cx,h
	ld:     
	        push   cx                       	;写入像素点
	        mov    cx,start_x               	;x坐标
	        mov    dx,start_y               	;y坐标
	        mov    al,1                     	;像素值
	        mov    ah,0ch
	        int    10h
	        inc    start_y                  	;y++ 打印竖
	        pop    cx
	        loop   ld

	        pop    dx
	        pop    cx
	        pop    bx
	        pop    ax
	        ret
hor_ver endp
code ends
end start

基本结构

功能是像素点打印楼梯,随便找的例子没别的意思。
和一般的程序差不多,都是三段segment,code段有assume和数据段绑定。
(如果不是很清楚一个汇编程序的格式,看这篇博客

两个代码块

不过我们的code段中,出现了两个部分:
main proc near
……
main endp

hor_ver proc near
……
hor_ver endp

这两部分就有一点像C的主函数和子函数,我们按照命名就能看出来具体谁是谁了。
一个程序,需要有一个名字(废话),这个是要写在proc和endp前面的,所以一个简单的子程序/或者主程序是这样的:

**proc

**endp

至于这两个谁在上面都无所谓。

near?

但那个near是什么:

NEAR属性(段内近调用)的过程只能被相同代码段的其他程序调用。
FAR属性(段间远调用)的过程可以被相同或不同代码段的程序调用。

如果是有多个代码段,那么我们一定需要far,但如果是段内调用,还是选择near,因为near相对较快

有两个建议:

  1. 首先如果是段内调用,其实near还是far的都可以不写……,但是还是写了比较好
  2. 主程序部分建议定义为far类型,便于返回dos,也就是将我们的主程序

这时,我们的代码长这样:

**proc near	;缺省就是near

**endp
压栈出栈?

为什么调用子程序还需要压栈出栈啊?
因为我们只有这几个寄存器,如果主程序和子程序调用了同一个,如果不保存,那么我们主函数的内容不就没了?(专业一点,这叫保护现场)
这里我们保存的方式还是有几种类型的:

  1. 压栈出栈,这个是比较简单的。如果使用堆栈法,那么保护恢复一定要在同一个地方
    (子程序或者主程序,不能分成两部分)
  2. 存内存,也就是在data数据段中定义变量来存储。

当然了,如果不想管,直接就将四个寄存器都压栈,然后反过来都出栈了就行,

ret&call

ret就是return的缩写,也就是函数的返回部分。
看着似乎没什么,其实本质上人家可是干了很多事。
在调用子程序的部分,我们的cs段寄存器和ip寄存器记录了当前指令的位置,如果有在dos尝试单步调试会发现,在函数返回的过程中,IP寄存器的值是会发生改变的。
所以ret其实就是jmp+pop的操作。

那我们怎么调用?
可以看到,上面的程序中有一个call指令,后面跟着我们的子程序名称,这就是调用了。
call和ret一个将ip寄存器压栈,一个弹出,保证了我们的代码实现。

其实call规范来说是这样的:
call near/far ptr 函数名,如果是far调用需要将cs和ip都进行压栈。
但是我们还是如果缺省就默认为near。

ret的一个骚操作

ret [val],其中val一定是一个正偶数

啥?是返回值吗?

不是,我们在rer的过程中,我们有一个将ip出栈的操作,正常来说sp指针加2就行了,但是如果是有这个数值,那么我们还需要将sp再加上这个正偶数
在这里插入图片描述
因为是dw类型的堆栈,所以我们的val才需要是正偶数。
另外强调一点,栈顶在高位,也就是我们压栈其实是从高往低。

(可这nm有什么用吗?
比如在子程序中我们将一些数据进行了压栈处理的,但是后面就用不上了,这里我们就可以直接ret,不需要一个个出栈了。
虽然只是修改了指针,而不是出栈,但当我们继续压栈的过程中,是会将数据覆盖掉的,所以还是没有影响的。

start写在哪?

当时就这个头疼了半天。
我们想一下,start是开始程序的标志,汇编和C什么的差不多,都是串行结构,说白了就是一条条执行,所以C从主函数起步,在这里我们也是这样。
start写在主程序的开始,start end在代码段结束。

另外我们的assume是一切代码的前提,只有绑定了才能找到位置,所以assume在code 段的最开始

参数传递问题

子程序不可能都没有返回值吧,也不可能都没有参数传入吧。
这里我们给几个方法:

  • 寄存器法
  • 变量法(存内存)
  • 堆栈法
  • 参数地址指针法
寄存器法

就是将传入传出的参数放在寄存器里面。
主程序:传入不用管,传出视情况保护;
子程序:传入建议保护,传出不能保护和恢复。(保护的不是返回值,而是之前的值,如果恢复了会将返回值覆盖掉!)

堆栈法

就是将传入和传出的参数都压入堆栈中。
因为保存和恢复也在堆栈中,如果处理不好直接翻车,但是能省下内存和寄存器,优缺点都很明显。

比如我们是将传入和传出的参数都放在CX寄存器中,在子程序将ABCX三个寄存器压栈。
主程序调用:

PUSH	CX			;堆栈传递入口参数
CALL  	SQROOT	
POP 	CX			;堆栈返回运算结果 

我们一步步来分析:

  1. 刚进入子程序,参数在栈中,同时我们的返回地址也在(call指令干的活)
  2. 将三个寄存器压栈
  3. 我们需要将传入参数放入CX寄存器中,需要将sp指针加8,然后pop到cx中
  4. 计算返回值(在CX中),将CX压栈(就是那个平方根,白嫖的图)。
  5. 将sp-8,调整为压栈之前的样子
  6. 寄存器出栈,ret返回,这样在主程序中我们的栈顶元素就是返回的参数了。

在这里插入图片描述
在这里插入图片描述
这里我们修改了sp的值,即直接修改栈内指针位置,如果子程序中还需要使用栈,解决办法:

  1. 继续调整sp指针,不过很麻烦还容易出错;
  2. 将我们的cx寄存器赋值(传入参数)改为寻址方式: MOV CX, [BP+……],这样会好很多。
参数地址指针法

上面的方式传一个两个数据都行,但如果是多个数据(比如一个数组),如果还是采用之前的方式,尤其是堆栈法,算地址直接爆炸。
所以我们将数组的首地址在主程序中赋值到SI寄存器,将个数赋值到CX寄存器(习惯,不是强行要求的),在子程序直接使用就行,注意子程序中的SI和CX要保护一下。

和寄存器法其实差不多,
注意传数组地址要使用lea指令,非要offset也行,但是这个能叼一点,
另外数组中的元素直接使用[SI]就是了,如果是dw类型,si一次加2,db加1(指元素后移)。

子程序的嵌套问题

就是套娃,比如我们在一个子程序中再调用另一个子程序。

我们要注意,每一次调用都涉及到压栈出栈,即使没有寄存器压栈,也有ip压栈,所以堆栈空间一定要够用,然后就是call 和 ret 一定要玩明白,要保护好寄存器。

主要的部分我们想讲一讲递归的问题,这里以Fibonacci为例。
如果是递归的程序,那么参数传递就一定要是寄存器法或者堆栈法,除非一个递归函数只能延申出一个递归部分,如计算n!,我们调用的过程中函数个数线性增加,否则使用参数法就会爆炸。

和一般的一样,我们也需要有边界条件:
在Fibonacci中,F(1) = F(2) = 1。
剩下的F(n) = F(n-1)+F(n-2)。
(可以想象出,如果我们直接写到最后一层,一共需要传递很多个参数,所以还是堆栈法和寄存器法好一些;另外如果n足够大,堆栈也容易炸掉。)

功能:输入一个不限位数的十进制数,回车后打印fibo的结果。
(可以输入个位数、十位数,非要输入一个百位的也行吧,反正自己电脑受得了再加上bl寄存器大小受得了就行)

DATAS SEGMENT
    
DATAS ENDS

STACKS SEGMENT
	a      dw 20 dup(?)
STACKS ENDS

CODES SEGMENT
	        ASSUME CS:CODES,DS:DATAS,SS:STACKS

fibo proc
	;读入al的数据,计算,返回在ax中
	        push   bx
	        push   cx
	        push   dx

	        cmp    al, 1h
	        je     get_out
	        cmp    al, 2h
	        je     get_out                    	;if(n == 1 || n == 2) return 1;

	        mov    dl, al
	        sub    al, 1h
	        call   fibo
	        mov    bx,ax                      	;fibo(n-1)

	        mov    al,dl
	        sub    al, 2h
	        call   fibo                       	;fibo(n-2)
	        mov    cx,ax

	        mov    ax,bx
	        add    ax,cx

	        pop    dx
	        pop    cx
	        pop    bx
	        ret

	get_out:
	        mov    ax, 1h

	        pop    dx
	        pop    cx
	        pop    bx
	        ret
fibo endp
	;打印回车
dpcrlf proc                               		;过程开始
	        push   ax                         	;保护寄存器AX和DX
	        push   dx
	        mov    dl,0dh                     	;显示回车
	        mov    ah,2
	        int    21h
	        mov    dl,0ah                     	;显示换行
	        mov    ah,2
	        int    21h
	        pop    dx                         	;恢复寄存器DX和AX
	        pop    ax
	        ret                               	;子程序返回
dpcrlf endp
	;打印空格
printf PROC
	        push   ax
	        push   cx
	        push   dx
	        mov    cl,20h
	        mov    dl,cl
	        mov    ah,2h
	        int    21h
	        pop    dx
	        pop    cx
	        pop    ax
	        ret
printf ENDP
	;将ax中的16位内容按十进制打印
hex PROC
	        PUSH   AX
	        PUSH   BX
	        PUSH   CX
	        PUSH   DX
	        PUSHF
	        XOR    CX,CX
	        MOV    BX,10
@DSPAX1:
	        XOR    DX,DX
	        DIV    BX
	        INC    CX
	        OR     DX,30H
	        PUSH   DX
	        CMP    AX,0
	        JNE    @DSPAX1
	        MOV    AH,2
@DISPAX2:
	        POP    DX
	        INT    21H
	        LOOP   @DISPAX2
	        MOV    DL,32
	        INT    21H
	        POPF
	        POP    DX
	        POP    CX
	        POP    BX
	        POP    AX
	        RET
hex ENDP
main proc
	START:  
	        MOV    AX,DATAS
	        MOV    DS,AX
	        xor    bl,bl
	input:  
	        mov    ah,1h
	        int    21H
	        cmp    al,13                      	;换行
	        jz     next                       	;相等跳出
	        sub    al,30h
	        mov    dl,bl
	        mov    cl,3h
	        sal    bl,cl
	        sal    dl,1                       	;10
	        add    bl,dl                      	;存储在bl中
	        add    bl,al
	        jmp    input

	next:   
	        mov    al,bl
	        call   fibo
	        ;call   printf
	        call   hex
	        MOV    AH,4CH
	        INT    21H
main endp
CODES ENDS
    END START

第一个函数是计算Fibonacci的,按照递归的定义来就好了,别忘了两个分支上都要有出栈和ret
然后中间的间隔方式我给出了两种,一个是换行回车,一个是空格,这里调用了换行。(都是中断调用,没什么可讲的)
我们的十进制转换采用的是除法+压栈实现的。
实现不限位数的输入我们是采用判断回车来实现的,如果没有读到回车就将bl的内容乘十然后加上我们刚刚读入的内容。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值