美文网首页汇编(MOV,SUB,PUSH,POP,...)
汇编开发(六):程序进阶

汇编开发(六):程序进阶

作者: _凌浩雨 | 来源:发表于2019-02-20 19:53 被阅读3次

    1. 堆栈框架

    1). 栈参数

    之前使用寄存器传递参数,现在我们将使用运行栈在子程序中传递参数。堆栈框架(或激活记录)是为传递的参数,子程序返回地址,局部变量和已保存的寄存器预留的堆栈区域。栈框架的创建步骤:
    I. 如果有要传递的参数,则压入栈中。
    II. 调用子程序,使子程序返回地址被压入堆栈。
    III. 子程序开始执行时,EBP被压入栈中。
    IV. EBP等于ESP。 从这一点开始,EBP充当所有子例程参数的基础参考。
    V. 如果存在局部变量,则ESP递减以为堆栈上的变量保留空间。
    VI. 如果需要保存任何寄存器,则将它们压入堆栈。

    2). 寄存器参数的缺点

    缺点:相同的寄存器用于保存数据值,例如计算中的循环计数器和操作数。 因此,在程序之前必须首先将用作参数的任何寄存器压入堆栈
    调用,分配过程参数的值,然后在过程返回后恢复为原始值。例如:

    push ebx                ; save register values
    push ecx
    push esi
    mov esi,OFFSET array    ; starting OFFSET
    mov ecx,LENGTHOF array  ; size, in units
    mov ebx,TYPE array      ; doubleword format
    call DumpMem            ; display memory
    pop esi                 ; restore register values
    pop ecx
    pop ebx
    

    不仅所有额外的推送和弹出都会造成代码混乱,它们往往会通过避免寄存器参数来消除我们希望获得的性能优势! 此外,程序员必须非常小心每个寄存器的PUSH与其适当的POP匹配,即使代码中存在多个执行路径。

    两种参数类型被压入栈中产生的子程序调用方式有:值传递引用传递

    • 值传递
      向栈中存入变量的值
    .data
        val1 DWORD 5
        val2 DWORD 6
    .code
        push val2
        push val1
        call AddTwo
    
    值传递.png
    • 引用传递
      向栈中压入变量的地址
    push OFFSET val2
    push OFFSET val1
    call Swap  
    
    引用传递.png
    • 传递数组
      高级语言通过引用传递向子程序传递数组。
    .data
        array DWORD 50 DUP(?)
    .code
        push OFFSET array
        call ArrayFill
    
    3). 接收栈参数
    • 基址偏移寻址
      我们使用基址偏移地址接收栈参数,EBP是一个基础的寄存器并且偏移量是一个常量。
    AddTwo PROC
        push ebp
        mov ebp,esp         ; base of stack frame
        mov eax,[ebp + 12]  ; second parameter
        add eax,[ebp + 8]   ; first parameter
        pop ebp
        ret
    AddTwo ENDP
    
    Accessing Stack Parameters.png
    • 显示堆栈参数
      显示堆栈参数:栈参数使用表达式引用。
    y_param EQU [ebp + 12]
    x_param EQU [ebp + 8]
    AddTwo PROC
        push ebp
        mov ebp,esp
        mov eax,y_param
        add eax,x_param
        pop ebp
        ret
    AddTwo ENDP
    
    • 清理堆栈

    当子程序返回后,我们必须从堆栈中移除参数。否则,会早晨内存泄漏和栈损坏。

    4). 32位调用约定
    • C调用约定
      C调用约定通常在C和C++程序中使用,子程序参数以相反的顺序被压入堆栈,因此进行函数调用的C程序将首先将B推入堆栈,然后按A。C调用约定解决清理堆栈的方法是:当程序调用子程序时,它在CALL指令后面跟一个语句,该语句向堆栈指针(ESP)添加一个等于子程序参数组合大小的值。
    Example1 PROC
        push 6
        push 5
        call AddTwo
        add esp,8   ; remove arguments from the stack
        ret
    Example1 ENDP
    
    • STDCALL调用约定
      当程序使用RET时,在RET后跟一个整型参数,整型参数的值为该子程序参数的大小。
    AddTwo PROC
        push ebp
        mov ebp,esp         ; base of stack frame
        mov eax,[ebp + 12]  ; second parameter
        add eax,[ebp + 8]   ; first parameter
        pop ebp
        ret 8               ; clean up the stack
    AddTwo ENDP
    
    • 保存和恢复寄存器
      子程序经常在修改寄存器前保存他们当前的内容。这是一个好方法,因为原数据在子程序返回之后可以被恢复。
    MySub PROC
        push ebp        ; save base pointer
        mov ebp,esp     ; base of stack frame
        push ecx
        push edx        ; save EDX
        mov eax,[ebp+8] ; get the stack parameter
        .
        .
        pop edx         ; restore saved registers
        pop ecx
        pop ebp         ; restore base pointer
        ret             ; clean up the stack
    MySub ENDP
    
    Stack frame for the MySub procedure.png
    5). 局部变量

    局部变量在运行栈上创建,通常在基址(EBP)之下。

    Local Variables.png
    MySub PROC
        push ebp
        mov ebp,esp
        sub esp,8               ; create locals
        mov DWORD PTR [ebp�4],10 ; X
        mov DWORD PTR [ebp�8],20 ; Y
        mov esp,ebp             ; remove locals from stack
        pop ebp
        ret
    MySub ENDP
    
    Stack frame after creating local variables.png
    • 局部变量符号
      为了阅读更简便,可以定义符号为每个局部变量偏移地址。
    X_local EQU DWORD PTR [ebp�4]
    Y_local EQU DWORD PTR [ebp�8]
    MySub PROC
        push ebp
        mov ebp,esp
        sub esp,8       ; reserve space for locals
        mov X_local,10  ; X
        mov Y_local,20  ; Y
        mov esp,ebp     ; remove locals from stack
        pop ebp
        ret
    MySub ENDP
    
    6). 引用参数

    引用参数通常使用基址偏移地址(EBP)被程序所访问。因为每个引用参数是一个指针,它通常被加载到寄存器中以用作间接操作数。

    • 数组填充实例
    ; 数组填充
    INCLUDE Irvine32.inc
    
    .data
        count = 100
        array WORD count DUP(?)
    
    .code
    main PROC
        push OFFSET array
        push count
        call ArrayFill
    
        call Crlf
        call WaitMsg
        exit
    main ENDP
    
    ArrayFill PROC
        push ebp
        mov ebp, esp
        pushad                  ; 保存寄存器
        mov esi, [ebp + 12]     ; 数组地址
        mov ecx, [ebp + 8]      ; 数组长度
        cmp ecx, 0              ; ECX == 0?
        je L2                   ; 如果是,则结束循环
    L1:
        mov eax, 10000          ; 从0——10000获取随机数
        call RandomRange        ; 从Irvine32库中
        mov [esi], ax           ; 插入数组
        add esi, TYPE WORD      ; 移动到下一个结点
        LOOP L1
    L2:
        popad
        pop ebp
        RET 8                   ; 清除堆栈
    ArrayFill ENDP
    
    END
    
    7). LEA 指令

    LEA指令返回间接操作数的地址。因为间接操作数包含一个或多个寄存器,它们的偏移量在运行时计算。

    makeArray PROC
        push ebp
        mov ebp,esp
        sub esp,32          ; myString is at EBP�30
        lea esi,[ebp–30]    ; load address of myString
        mov ecx,30          ; loop counter
        L1: mov BYTE PTR [esi],'*' ; fill one position
        inc esi             ; move to next
        loop L1             ; continue until ECX = 0
        add esp,32          ; remove the array (restore ESP)
        pop ebp
        ret
    makeArray ENDP
    
    8). ENTER 和 LEAVE 指令
    • ENTER 指令

    ENTER 指令调用程序时自动创建一个堆栈框架。它为局部变量保留堆栈空间并将EBP保存在堆栈中。具体来说它执行三个操作:
    I. 将EBP压入栈中(push ebp)
    II. 设置EBP为堆栈框架的基地址(mov ebp, esp)
    III. 为局部变量预留空间(sub esp, numbytes)

    格式

    ENTER numbytes, nestinglevel
    

    第一个参数是一个长廊,为局部变量预留的字节空间。第二个参数指定过程的词法嵌套级别。这两个操作数都是立即数。numbytes是4的倍数。在我们的程序中,nestinglevel总是0.

    示例:

    MySub PROC
        enter 8,0
    
    ; 类似于
    MySub PROC
        push ebp
        mov ebp,esp
        sub esp,8
    
    Stack frame before and after ENTER has executed.png
    • LEAVE 指令
      LEAVE指令终止进程的堆栈帧。 它通过将ESP和EBP恢复为调用过程时分配的值来反转前一个ENTER指令的操作。

    格式

    MySub PROC
     enter 8,0
     .
     .
     leave
     ret
    MySub ENDP
    
    9). LOCAL 指令

    LOCAL 定义一个或多个局部变量名并分配它们的类型。

    格式

    LOCAL varlist
    ; 其中每个变量类似于label:type
    

    示例:

    MySub PROC
        LOCAL TempArray[10]:DWORD
    
    BubbleSort PROC
        LOCAL temp:DWORD, SwapFlag:BYTE
    

    2. 递归

    递归子程序是指直接或间接的调用它自身。在处理具有重复模式的数据结构时,递归(调用递归子例程的实践)可以是一个强大的工具。

    • 无穷的递归
    ; 无穷的递归
    INCLUDE Irvine32.inc
    
    .data
        endlessStr BYTE "This recursion never stops",0
    
    .code
    main PROC
        call Endless
    
        call Crlf
        call WaitMsg
        exit
    main ENDP
    
    Endless PROC
        mov edx,OFFSET endlessStr
        call WriteString
        call Crlf
        call Endless
        ret                     ; never executes
    Endless ENDP
    
    END 
    
    1). 递归计算和

    有用的递归子例程始终包含终止条件。 当终止条件变为真时,当程序执行所有挂起的RET指令时,堆栈将展开。

    ; 递归计算和
    INCLUDE Irvine32.inc
    
    .code
    main PROC
        mov ecx, 5          ; 设置count = 5
        mov eax, 0          ; 设置sum = 0
        call CalcSum        ; 计算和
    
    L1:
        call WriteDec       ; 显示EAX
    
        call Crlf
        call WaitMsg
        exit
    main ENDP
    
    ;----------------------------------------------
    ; 计算一串数字之和
    ; Receives: ECX = count
    ; Returns: EAX = sum
    ;----------------------------------------------
    CalcSum PROC
        cmp ecx, 0          ; 检查counter
        jz L2               ; 如果为0, 则退出程序
        add eax, ecx        ; 否则求和
        dec ecx             ; counter递减
        call CalcSum        ; 递归
    L2: 
        ret                     
    CalcSum ENDP
    
    END 
    
    Stack Frame and Registers (CalcSum).png
    2). 计算因子
    ; 递归相乘
    INCLUDE Irvine32.inc
    
    .code
    main PROC
        push 5              ; 计算5!
        call Factorial      ; 计算因子
        call WriteDec       ; 显示EAX
    
        call Crlf
        call WaitMsg
        exit
    main ENDP
    
    ;----------------------------------------------
    ; 计算因子乘积
    ; Receives: [ebp + 8] = n, 计算
    ; Returns: EAX = 乘积
    ;----------------------------------------------
    Factorial PROC
        push ebp
        mov ebp, esp
        mov eax, [ebp + 8]      ; 获取n
        cmp eax, 0              ; 判断 n > 0?
        ja L1                   ; 继续L1
        mov eax, 1              ; 如果不是,则返回1
        jmp L2                  ; 返回结果
    L1:
        dec eax
        push eax
        call Factorial
    ReturnFact:
        mov ebx, [ebp + 8]          ; 获取n
        mul ebx                     ; EDX:EAX = EAX * EBX
    L2:
        pop ebp
    
        ret 4               
    Factorial ENDP
    
    END
    
    Recursive calls to the factorial function.png

    3. INVOKE, ADDR, PROC, and PROTO

    在32位模式下,INVOKE,PROC和PROTO指令为定义和调用过程提供了强大的工具。除了这些指令外,ADDR运算符也是必不可少的工具
    定义程序参数。

    1). INVOKE 指令

    INVOKE指令,仅在32位模式下可用,将参数压入栈中和调用程序。INVOKE是CALL指令的一个方便替代品,因为它允许您使用一行代码传递多个参数。

    • 格式
    INVOKE procedureName [, argumentList]
    
    Argument types used with INVOKE.png

    EAX,EDX重写: 如果将小于32位的参数传递给程序,INVOKE会经常重写EAX和EDX,因此在使用INVOKE调用程序时,最好使用保存和恢复EAX、DEX的方式。

    • 替换代码
    push TYPE array
    push LENGTHOF array
    push OFFSET array
    call DumpArray
    
    ; call 四行代码等价于 INVOKE 一行代码
    INVOKE DumpArray, OFFSET array, LENGTHOF array, TYPE array
    
    2). ADDR 操作

    ADDR 操作,32位模式下可用,使用INVOKE指令时可以传递指针参数。

    • 格式
    INVOKE FillArray, ADDR myArray
    
    3). PROC 指令
    • PROC 指令语法
    label PROC [attributes] [USES reglist], parameter_list
    

    其中label是用户自定义标签,attributes可选值为[distance] [langtype] [visibility] [prologuearg]

    Attributes field in the PROC directive.png
    • 参数列表
    label PROC [attributes] [USES reglist],
                parameter_1,
                parameter_2,
                .
                .
                parameter_n
    ; 其中每个parameter_n的格式为paramName:type
    
    • 由PROC修改的RET指令
    push ebp
    mov ebp,esp
    .
    .
    leave
    ret (n*4)
    
    • 指定参数传递协议
    Example1 PROC C,
        parm1:DWORD, parm2:DWORD
    
    Example1 PROC STDCALL,
        parm1:DWORD, parm2:DWORD
    
    4). PROTO 指令

    在64位中,我们可以使用PROTO指令标识外部的程序。

    ExitProcess PROTO
    .code
        mov ecx,0
        call ExitProcess
    

    在32位中,PROTO是一个更强大的功能,因为它可以包含一个列表
    程序参数。

    ; 类型于C语言中的函数声明
    MySub PROTO     ; procedure prototype
    .
    INVOKE MySub    ; procedure call
    .
    MySub PROC      ; procedure implementation
    .
    .
    MySub ENDP
    
    • 程序声明的方法
      I> 将PROC改为PROTO
      II> 移除USES操作和寄存器列表
    ; 程序 PROTO 声明:
    ArraySum PROTO,
        ptrArray:PTR DWORD,     ; points to the array
        szArray:DWORD           ; array size
    
    ; 程序实现
    ArraySum PROC USES esi ecx,
        ptrArray:PTR DWORD,     ; points to the array
        szArray:DWORD           ; array size
    
        ; (remaining lines omitted...)
    ArraySum ENDP
    
    • 编译时参数检查
      PROTO 指令编译时比较程序声明和程序实现的参数。

    • MASM 检测错误
      如果参数超出声明参数的大小,MASM将生成一个错误。

    • MASM 不检测的错误
      如果参数小于声明参数的大小,MASM将不生成错误。

    • 数组求和示例

    ; 数组求和
    INCLUDE Irvine32.inc
    
    .data
        array DWORD 10000h, 20000h, 30000h, 40000h, 50000h
        theSum DWORD ?
    
    .code
    ArraySum PROTO,
        ptrArray: PTR DWORD,        ; points to the array
        szArray:DWORD               ; array size
    
    main PROC
        INVOKE ArraySum,
            ADDR array,         ; 数组首地址
            LENGTHOF array      ; 数组元素个数
    
        call WriteHex
    
        call Crlf
        call WaitMsg
        exit
    main ENDP
    
    ArraySum PROC USES esi ecx,
        ptrArray: PTR DWORD,        ; points to the array
        szArray:DWORD               ; array size
        
        mov esi, ptrArray           ; 数组地址
        mov ecx, szArray            ; 数组长度
        mov eax, 0                  ; 设置和为0
        cmp ecx, 0                  ; 数组长度是否为0
        je L2                       ; 如果等于则quit
    L1:
        add eax, [esi]              ; 求和
        add esi, 4                  ; 移动到下一个数组元素
        LOOP L1                     ; 循环
    L2:
        ret                         ; sum 在EAX寄存器中  
    ArraySum ENDP
    
    END 
    
    ArraySum.png
    5). 参数分类

    过程参数通常根据调用程序和被调用过程之间的数据传输方向进行分类:
    Input -> 输入参数是由调用程序传递给过程的数据.
    Output -> 当调用程序将变量的地址传递给过程时,将创建输出参数.
    Input-Output -> 输入输出参数与输出参数相同,但有一个例外:被调用过程期望参数引用的变量包含一些数据。

    6). 示例:交换两个整数
    ; 交换两个数
    INCLUDE Irvine32.inc
    
    Swap PROTO, pValX: PTR DWORD, pValY: PTR DWORD
    
    .data
        Array DWORD 10000h, 20000h  
    
    .code
    main PROC
        ; 交换之前显示数组
        mov esi, OFFSET Array
        mov ecx, 2              ; 数组长度
        mov ebx, TYPE Array
        call DumpMem            ; 显示数组数据
    
        INVOKE Swap, ADDR Array, ADDR[Array + 4]
    
        ; 显示交换后的数据
        call DumpMem
    
        call Crlf
        call WaitMsg
        exit
    main ENDP
    
    ;--------------------------------------------
    ; 交换两个32位整型
    ; Return: 无
    ;--------------------------------------------
    Swap PROC USES eax esi edi,
        pValX: PTR DWORD,
        pValY: PTR DWORD
    
        mov esi, pValX          ; 获取指针
        mov edi, pValY
        mov eax, [esi]          ; 获取第一个整数
        xchg eax, [edi]         ; 交换两个数
        mov [esi], eax          ; 替换第一个整数
        RET
    Swap ENDP
    END 
    
    效果.png
    7). 调试提醒

    常见错误:

    • 参数大小不匹配
    • 传递指针类型错误
    • 传递即时值
    8). WriteStackFrame 程序
    ; 交换两个数
    INCLUDE Irvine32.inc
    
    WriteStackFrame PROTO,
        numParam:DWORD,     ; 传递参数个数
        numLocalVal: DWORD, ; 本地变量 
        numSavedReg: DWORD  ; 保存寄存器的个数
    
    .code
    
    myProc PROC USES eax ebx,
        x: DWORD, y: DWORD
        LOCAL a: DWORD, b: DWORD
    
        PARAMS = 2
        LOCALS = 2
        SAVED_REGS = 2
        mov a, 0AAAAh
        mov b, 0BBBBh
        INVOKE WriteStackFrame, PARAMS, LOCALS, SAVED_REGS
        RET
    myProc ENDP
    
    main PROC
        mov eax, 0EAEAEAEAh
        mov ebx, 0EBEBEBEBh
    
        INVOKE myProc, 1111h, 2222h;
    
        call Crlf
        call WaitMsg
        exit
    main ENDP
    
    END 
    
    效果.png

    4. 创建多模块程序

    一个很大的源文件管理起来困难并且编译起来比较慢,可以将单个文件拆分成多个文件,但是对任何源文件的修改仍然需要完整组装所有文件。更好的方法是将程序划分为模块(组合单元)。每个模块都是独立组装的,因此更改一个模块的源代码只需要重新组装单个模块。 链接器将所有已组装的模块(OBJ文件)组合成一个可执行文件很快。 链接大量对象模块所需的时间远远少于组装相同数量的源代码文件所需的时间.
    有两种方法去创建多模块程序:第一个是传统的,使用EXTERN指令,这或多或少可以在不同的x86汇编程序中移植。第二种是使用微软的高级指令INVOKEPROTO,隐藏更多的低级细节。

    1). 隐藏和导出程序名称

    默认情况下,MASM让所有的程序时public,它们可以被任意模块被调用。我们可以覆盖这个行为,使用PRIVATE修饰符,mySub PROC PRIVATE

    • OPTION PROC:PRIVATE指令:源程序模块之外的程序总是隐藏。所有的程序默认的访问修饰符变为私有的。然后我们必须使用PUBLIC指令标识它们。
    OPTION PROC:PRIVATE
    PUBLIC mySub
    
    2). 调用外部程序

    EXTERN 指令,在调用当前模块外部的过程时使用,标识过程的名称和堆栈帧大小。

    INCLUDE Irvine32.inc
    EXTERN sub1@0:PROC
    .code
    main PROC
    call sub1@0
    exit
    main ENDP
    END main
    
    3). 跨模块边界使用变量和符号
    • 导出变量和符号
      在一个闭合的模块中,变量和符号默认是私有的。我们可以使用PUBLIC指令导出特殊的名字。
    PUBLIC count, SYM1
    SYM1 = 10
    .data
        count DWORD 0
    
    • 接收外部变量 和符号
      我们可以使用EXTERN指令接收定义在其他模块的变量和符号
    EXTERN name : type
    
    • 使用INCLUDEEXTERNDEF

    vars.inc

    ; vars.inc
    EXTERNDEF count:DWORD, SYM1:ABS
    

    sub1.asm

    ; sub1.asm
    .386
    .model flat,STDCALL
    INCLUDE vars.inc
    SYM1 = 10
    .data
        count DWORD 0
    END
    

    main.asm

    .386
    .model flat,stdcall
    .stack 4096
    ExitProcess proto, dwExitCode:dword
    INCLUDE vars.inc
    .code
    main PROC
        mov count,2000h
        mov eax,SYM1
        INVOKE ExitProcess,0
    main ENDP
    END main
    
    4). 示例:数组求和

    模块图结构:

    Structure chart, ArraySum program.png
    5). 使用EXTERN创建模块

    _prompt.asm

    ; 提示用户输入整数
    
    INCLUDE Irvine32.inc
    
    .code
    ;------------------------------------------
    PromptForIntegers PROC
    ; 提示用户输入整数数组
    ; Receives: 
    ;   ptrPrompt: PTR BYTE         ; 提示信息
    ;   ptrArray: PTR DWORD         ; 数组首地址
    ;   arraySize: DWORD            ; 数组大小
    ; Returns: 无
    ;------------------------------------------
    arraySize EQU [ebp + 16]
    ptrArray EQU [ebp + 12]
    ptrPrompt EQU [ebp + 8]
    
        enter 0,0
        pushad              ; 保存所有寄存器
    
        mov ecx, arraySize
        cmp ecx, 0          ; 判断数组长度是否小于等于0
        jle L2              ; 如果是则退出
        mov edx, ptrPrompt  ; 提示信息的地址
        mov esi, ptrArray
    L1:
        call WriteString    ; 显示字符串
        call ReadInt        ; 从EAX读取一个整数
        call Crlf           ; 换行
        mov [esi], eax      ; 存储到数组
        add esi, 4          ; 下一个整数
        LOOP L1
    L2:
        popad               ; 恢复所有寄存器
        leave               ; 恢复栈
        ret 12
    PromptForIntegers ENDP
    END
    

    _arraysum.asm

    ; 数组求和
    
    INCLUDE Irvine32.inc
    
    .code
    ;------------------------------------------
    ArraySum PROC
    ; 计算数组元素之和
    ; Receives: 
    ;   ptrArray            ; 数组首地址
    ;   arraySize           ; 数组大小
    ; Returns: EAX = sum
    ;------------------------------------------
    ptrArray EQU [ebp + 8]
    arraySize EQU [ebp + 12]
        enter 0,0
        push ecx            ; 不要push EAX
        push esi
    
        mov eax, 0          ; 设置和为0
        mov esi, ptrArray   
        mov ecx, arraySize
        cmp ecx, 0          ; 数组大小是否小于0
        jle L2              ; 如果是则退出
    L1: 
        add eax, [esi]      ; 添加一个整数到sum
        add esi, 4          ; 指向数组下一个元素
        LOOP L1             ; 循环
    L2:
        pop esi
        pop ecx             ; 返回sum到EAX中
        leave
        ret 8               ; 恢复栈
    ArraySum ENDP
    END
    

    _display.asm

    ; 显示程序
    
    INCLUDE Irvine32.inc
    
    .code
    ;------------------------------------------
    DisplaySum PROC
    ; 在控制台显示数组和
    ; Receives: 
    ;   ptrPrompt           ; 提示信息首地址
    ;   theSum              ; 数组和
    ; Returns: 无
    ;------------------------------------------
    theSum EQU [ebp + 12]
    ptrPrompt EQU [ebp + 8]
        enter 0, 0
        push eax
        push edx
    
        mov edx, ptrPrompt      ; 指向提示信息
        call WriteString            
        mov eax, theSum
        call WriteInt           ; 显示EAX
        call Crlf
    
        pop edx
        pop eax
        leave
        ret 8   
    DisplaySum ENDP
    END
    

    Sum_main.asm

    ; 数组求和 总模块
    ; 使用到的模块:_arraysum.asm, _display.asm, _prompt.asm
    
    INCLUDE Irvine32.inc
    
    EXTERN PromptForIntegers@0:PROC
    EXTERN ArraySum@0:PROC, DisplaySum@0:PROC
    
    ; 重定义方法名,为了使用方便
    ArraySum EQU ArraySum@0
    PromptForIntegers EQU PromptForIntegers@0
    DisplaySum EQU DisplaySum@0
    
    ; 数组长度
    Count = 3
    
    .data
        prompt1 BYTE "Enter a signed integer: ",0
        prompt2 BYTE "The sum of the integers is: ",0
        array DWORD Count DUP(?)
        sum DWORD ?
    
    .code
    main PROC
        call Clrscr
    
        ; 提示用户输入数据
        push Count
        push OFFSET array
        push OFFSET prompt1
    
        call PromptForIntegers
        
        ; 计算数组之和
        push Count
        push OFFSET array
        call ArraySum
        mov sum, eax
    
        ; 显示和
        push sum
        push OFFSET prompt2
        call DisplaySum
    
        call Crlf
        call WaitMsg
        exit
    main ENDP
    END
    

    运行效果:

    效果.png
    6). 使用INVOKE and PROTO 创建模块

    sum.inc

    ; (sum.inc)
    INCLUDE Irvine32.inc
        PromptForIntegers PROTO,
        ptrPrompt:PTR BYTE,     ; prompt string
        ptrArray:PTR DWORD,     ; points to the array
        arraySize:DWORD         ; size of the array
    ArraySum PROTO,
        ptrArray:PTR DWORD,     ; points to the array
        arraySize:DWORD         ; size of the array
    DisplaySum PROTO,
        ptrPrompt:PTR BYTE,     ; prompt string
        theSum:DWORD            ; sum of the array
    

    __prompt.asm

    ; 提示用户输入整数
    
    INCLUDE sum.inc                 ; 获取方法原型
    
    .code
    ;------------------------------------------
    PromptForIntegers PROC,
        ptrPrompt: PTR BYTE,        ; 提示信息
        ptrArray: PTR DWORD,        ; 数组首地址
        arraySize: DWORD            ; 数组大小
    ; 提示用户输入整数数组
    ; Returns: 无
    ;------------------------------------------
        pushad              ; 保存所有寄存器
    
        mov ecx, arraySize
        cmp ecx, 0          ; 判断数组长度是否小于等于0
        jle L2              ; 如果是则退出
        mov edx, ptrPrompt  ; 提示信息的地址
        mov esi, ptrArray
    L1:
        call WriteString    ; 显示字符串
        call ReadInt        ; 从EAX读取一个整数
        call Crlf           ; 换行
        mov [esi], eax      ; 存储到数组
        add esi, 4          ; 下一个整数
        LOOP L1
    L2:
        popad               ; 恢复所有寄存器
        ret 
    PromptForIntegers ENDP
    END
    

    __arraysum.asm

    ; 数组求和
    
    INCLUDE sum.inc
    
    .code
    ;------------------------------------------
    ArraySum PROC,
        ptrArray:PTR DWORD,         ; 数组首地址
        arraySize:DWORD             ; 数组大小
    ; 计算数组元素之和
    ; Returns: EAX = sum
    ;------------------------------------------
        push ecx            ; 不要push EAX
        push esi
    
        mov eax, 0          ; 设置和为0
        mov esi, ptrArray   
        mov ecx, arraySize
        cmp ecx, 0          ; 数组大小是否小于0
        jle L2              ; 如果是则退出
    L1: 
        add eax, [esi]      ; 添加一个整数到sum
        add esi, 4          ; 指向数组下一个元素
        LOOP L1             ; 循环
    L2:
        pop esi
        pop ecx             ; 返回sum到EAX中
        ret                 ; 恢复栈
    ArraySum ENDP
    END
    

    __display.asm

    ; 显示程序
    
    INCLUDE Irvine32.inc
    
    .code
    ;------------------------------------------
    DisplaySum PROC,
        ptrPrompt: PTR BYTE,    ; 提示信息首地址
        theSum:DWORD            ; 数组和
    ; 在控制台显示数组和
    ; Returns: 无
    ;------------------------------------------
        push eax
        push edx
    
        mov edx, ptrPrompt      ; 指向提示信息
        call WriteString            
        mov eax, theSum
        call WriteInt           ; 显示EAX
        call Crlf
    
        pop edx
        pop eax
        ret 
    DisplaySum ENDP
    END
    

    Sum__main.asm

    ; 数组求和 总模块
    ; 使用到的模块:_arraysum.asm, _display.asm, _prompt.asm
    
    INCLUDE sum.inc
    
    ; 数组长度
    Count = 3
    
    .data
        prompt1 BYTE "Enter a signed integer: ",0
        prompt2 BYTE "The sum of the integers is: ",0
        array DWORD Count DUP(?)
        sum DWORD ?
    
    .code
    main PROC
        call Clrscr
    
        INVOKE PromptForIntegers, ADDR prompt1, ADDR array, Count
        INVOKE ArraySum, ADDR array, Count
        mov sum, eax
        INVOKE DisplaySum, ADDR prompt2, sum
        
        call Crlf
        call WaitMsg
        exit
    main ENDP
    END
    

    注:这两种方式,使用较多的为EXTERN创建多模块方式,第二种在32位中使用较多。

    相关文章

      网友评论

        本文标题:汇编开发(六):程序进阶

        本文链接:https://www.haomeiwen.com/subject/dgdfyqtx.html