zoukankan      html  css  js  c++  java
  • 8086汇编语言学习(八) 8086子程序

    1.8086过程跳转指令

      作为一门通用的编程语言,需要具有对代码逻辑进行抽象封装的能力。这一抽象元素,在有的语言中被称为函数方法或者过程,而在8086汇编中被称为子程序。子程序和子程序组合能够构造出更复杂的子程序,如此往复以至无穷。子程序的存在,使得开发人员可以使用不同层次的抽象,构建出越来越复杂的系统。

      8086汇编子程序的调用、返回本质上依然是程序指令的跳转。过程跳转和无条件跳转的不同之处在于,跳转的子程序执行完毕后,还需要能够正确的返回子程序执行完成后的第一条指令上,执行之后的程序。

      子程序可以调用子程序,互相之间理论上可以无限制的嵌套。程序跳转时,可以将当前的CS:IP值压入栈中,当子程序执行完毕后再将栈中的CS:IP弹出。栈的先进后出的特性使得栈这一结构可以很好的完成任务。

      虽然使用无条件跳转指令和显式的CS:IP压栈出栈也能实现子程序的调用和返回,但8086汇编为此提供了专门的跳转指令,这被成为过程跳转指令。过程跳转指令通过将CS:IP的压栈/出栈和之后的跳转合而为一,降低了使用子程序时的复杂度。

      8086汇编的子程序跳转指令可以分为两类,一是子程序调用指令,二是子程序返回指令

    子程序调用指令

      子程序调用指令call,执行时有两步操作,将IP或者CS/IP压入当前栈中,随后进行对应跳转。call指令主要有以下几种格式:

      call [标号]:其相当于push IP;jmp near ptr [标号]。是段内转移,位移的值由编译器在编译时根据标号位置动态指定,偏移的IP范围也如jmp near一致(-32678~32767)

      call far ptr [标号]:其相当于 push CS;push IP;jmp far ptr [标号]

      call [16位寄存器]:相当于push IP;jmp near [16位寄存器]

      call word ptr [内存单元地址]: 相当于 push IP; jmp word ptr [内存单元地址]

      call dword ptr [内存单元地址]: 相当于push IP; jmp dword ptr [内存单元地址]

    子程序返回指令

      有了子程序调用指令,在跳转前先将CS/IP的值压入栈中,并跳转。与之相对的子程序返回指令则是一个逆向的操作,先将栈中的CS/IP弹出,覆盖还原调用者在调用子程序跳转前的CS/IP值,再进行跳转,这样便能够正确的返回子程序执行完毕后调用者对应的指令处。

      ret指令: 其相当于pop IP;弹出栈中的一个数据,用于复原IP的值,从而实现近转移。

      ret n指令:类似ret,在ret的基础上进行了栈顶指针sp的偏移(例如 ret 4),相当于pop IP;add sp,n

      retf指令: 其相当于pop IP; pop CS;(和call far ptr的入栈顺序正好相反)弹出栈中的两个数据,分别用于复原CS、IP的值,从而实现远转移。 

      retf n指令:类似retf,在retf的基础上进行了栈顶指针sp的偏移(例如 retf 4),相当于pop IP;pop CS;add sp,n 。

    call和ret组合使用

      子程序的调用和返回跳转指令通常是配对使用的,call近转移和ret配对,而call远转移则和retf配对。

    下面是使用call/ret构造子程序的基础模版:

    assume cs:code
    code segment
    main: ..
          ..
          call sub1; 调用sub1子程序
          ..
          ..
          mov ax,4c00h
          int 21h
    sub1: ..
          ..
          call sub2; 调用sub2子程序
          ..
          ..
          ret; sub1子程序返回
    sub2: ..
          ..
          ..
          ret; sub2子程序返回
    code ends
    end main

    2.子程序与调用者之间参数/返回值传递的问题

      参数返回值传递的问题解决方法其实质是如何通过某一媒介,使得调用者和子程序都能访问到其中的数据。这一媒介主要有三种:寄存器、通用内存以及栈。

    通过寄存器传递参数返回值

      下面是一个计算N的三次方的子程序,其通过寄存器来进行参数和返回值传递。

    ;说明:计算N的三次方
    ;参数:(bx)=N
    ;返回值: (dx:ax)=N^3
    cube:mov ax,bx
         mul bx; mul bx可以简单理解为ax = ax * bx
         mul bx
         ret

      使用寄存器传递参数/返回值时,调用者需要将参数送入子程序指定的参数寄存器中,并在执行完毕后从指定的结果寄存器中获取返回值。相对的,子程序从参数寄存器中取出参数,将返回值送入结果寄存器中。

    通过通用内存传递参数返回值

      使用寄存器传递参数/返回值虽然简单,但存在一个致命缺陷:寄存器的数量是有限的,当子程序所需要传递的参数达到4、5个甚至十几个,几十个时(虽然不推荐传递过多参数,但理论上大多数编程语言是不限制参数个数的),使用寄存器传递参数/返回值就变得不可行了。可以考虑使用一片连续的内存来传递参数。

      下面是一个将ascll码字母转为大写的子程序。

    ;说明:将ascll字母转为大写
    ;参数: 将(ds:si)指向的内存单元中的字母转为大写
    capital:
      and byte ptr [si],11011111b; 利用字母大小写ascll码的规律进行大小写转换 inc si; si指向下一个内存单元 loop capital ret

    完整的示例程序:

    data segment
        db 'helloworld'
    data ends
    
    code segment
    start:
        mov ax,data
        mov ds,ax
        mov si,0
        mov cx,10; 'helloworld'的长度
        call capital
        mov ax,4c00h
        int 21h
    capital:
        and byte ptr [si],11011111b; 利用字母大小写ascll码的规律进行大小写转换
        inc si; si指向下一个内存单元
        loop capital
        ret 
    code ends
    end start

    通过栈传递参数返回值

      使用通用内存可以批量的传递参数,同理也可以使用栈来实现参数/返回值的传递。调用者将所需要传递的参数压入栈中,而子程序则从栈中弹出、取出参数。

    使用栈来传递参数比起使用通用内存来说具有几个优点:

      1.通用内存范围过于宽泛,不同的设计者会约定使用不同的内存空间进行参数传递,不利于理解。统一的使用栈进行参数传递能让代码易于理解。

      2.子程序与调用者之间存在着共享寄存器冲突的问题,通常使用栈来缓存子程序与调用者冲突的寄存器内容。

      3.一般高级程序语言的实现中存在着作用域的概念,子程序中的临时局部变量(也包括传入的参数)无法在调用者所处的外部作用域中被访问。出于空间效率的考量,子程序中的临时局部变量应该在当前子程序执行完毕后被销毁。栈这一后进先出的特性很适合这样的场景,在子程序执行时将临时局部变量压入栈中,并在子程序执行完毕后将栈中元素有序弹出复原。

      下面是一个子程序,用于计算两数之差的立方(a-b)^3 (demo中a=3,b=1)

    assume cs:code
    
    code segment
    start:
    ; 参数b先压入栈中,参数a后压入栈中
        mov ax,1
        push ax
        mov ax,3
        push ax
        call difcube
        mov ax,4c00h
        int 21h
    ; difcube 计算两数之差的立方 依赖子程序cube
    ; 参数a=[sp+4];b=[sp+6] (call指令会将当前IP压入栈中,因此IP=[sp+2],栈中元素占用两个内存单元)
    ; 返回值 ax = (a-b)^3
    difcube:
        push bp
        mov bp,sp
        mov ax,[bp+4]
        sub ax,[bp+6]
        push ax
        call cube
        pop bp
        ret 4; ret时需要将进行sp的偏移(参数个数为2,偏移量为4),将参数弹出栈中,使得程序得以正确的返回
    ; cube 计算N的立方
    ; 参数n=[sp+4]
    ; 返回值 ax = n^3
    cube:
        push bp
        mov bp,sp
        mov bx,[bp+4]
        mov ax,bx
        mul bx
        mul bx
        pop bp
        ret 2; ret时需要将进行sp的偏移(参数个数为1,偏移量为2),将参数弹出栈中,使得程序得以正确的返回
    code ends
    end start

    3.子程序与调用者之间寄存器冲突的问题

       子程序与调用者之间寄存器冲突通过一个示例程序来说明。

    assume cs:code
    data segment
        db 'word',0
        db 'unix',0
        db 'wind',0
        db 'good',0
    data ends
     
    code segment
    start: 
        mov ax,data
        mov ds,ax
        mov bx,0           
        mov cx,4   ; 共有4个字符串需要处理         
    s:
        mov si,bx
        call capital
        add bx,5  ; 每个字符串长度为5,bx增加指向下一字符串起始位置
        loop s
        
        mov ax,4c00h
        int 21h      
    capital: 
        mov cl,[si]
        mov ch,0
        jcxz ok  ; 当前字符串到达结尾,cl+ch=cx=0
        and byte ptr [si],11011111b ; 当前字母转换为大写
        inc si    ; 指向当前字符串下一个字母
        jmp short capital
    ok:
        ret
    code ends
    
    end start

      程序的思路大致是对每一字符串(和字符数组不同以0结尾,表示字符串的结束)循环调用capital子程序,并将字符串中的所有字母转为大写。乍看一下并没有什么问题,但由于外部调用者s以及capital都使用了条件跳转指令(loop、jcxz),导致了寄存器cx中的数据冲突。从高级语言作用域的角度来看,一个全局变量被调用者和子程序所共享,互相覆盖。

      要想解决这一问题有几种思路:调用者仔细检查以避免和子程序使用相同的寄存器;将子程序和调用者使用的寄存器解耦,不互相冲突,使得调用者和子程序互相之间都不必关心彼此使用的寄存器。

    避免调用者使用子程序依赖的寄存器

      由于寄存器数量是极其有限的,当程序足够复杂时(子程序调用子程序),很难做到完全不冲突。由于必须检查全局共享寄存器的存在,避免冲突导致bug,对开发人员也是一个极大的负担。

    调用者和子程序寄存器解耦

      将子程序和调用者之间的寄存器解耦,自然是最好不过的方案了。子程序只需要和调用者在参数/返回值处进行交互,而不必考虑例如cx计数寄存器之类的冲突。

      一个简单的寄存器解耦思路是使用栈。当程序指针进入子程序时,将子程序使用到的寄存器首先压入栈中,并在子程序执行完毕返回之前,按照相反的顺序将其弹出,还原进入子程序前的寄存器。这样,无论子程序使用的寄存器是否和调用者产生冲突,都不会产生冲突;如果子程序的设计者按照上述思路编写了代码,调用者也无需关心寄存器冲突的问题。

      因此,在设计子程序时应该将模版进一步优化,使之能够解决调用者和子程序之间寄存器冲突的问题。

    子程序开始:
        子程序所使用的寄存器入栈
        子程序内容
        子程序所使用的寄存器出栈
        子程序返回(ret retf)

      上文使用栈传递参数的例子中,子程序头部和尾部对寄存器BP的入栈/出栈便是使用了这一技巧,从而避免了上下文BP寄存器的冲突。

    改进后的程序如下:

    assume cs:code
    data segment
        db 'word',0
        db 'unix',0
        db 'wind',0
        db 'good',0
    data ends
     
    code segment
    start: 
        mov ax,data
        mov ds,ax
        mov bx,0           
        mov cx,4   ; 共有4个字符串需要处理         
    s:
        mov si,bx
        call capital
        add bx,5  ; 每个字符串长度为5,bx增加指向下一字符串起始位置
        loop s
        
        mov ax,4c00h
        int 21h      
    capital: 
        push cx
        push si
    change:
        mov cl,[si]
        mov ch,0
        jcxz ok  ; 当前字符串到达结尾,cl+ch=cx=0
        and byte ptr [si],11011111b ; 当前字母转换为大写
        inc si    ; 指向当前字符串下一个字母
        jmp short change
    ok:
        pop si
        pop cx
        ret
    code ends
    
    end start
  • 相关阅读:
    python实现满二叉树递归循环
    二叉树遍历规则,先顺遍历/中序遍历/后序遍历
    满二叉树的循环递归
    python 中的super()继承,搜索广度为先
    UITableview 中获取非选中的cell
    iOS——UIButton响应传参数
    iOS- iPad UIPopoverController
    IPAD之分割视图 SplitViewController
    IOS7 隐藏状态栏
    iOS 强制横屏
  • 原文地址:https://www.cnblogs.com/xiaoxiongcanguan/p/12508960.html
Copyright © 2011-2022 走看看