汇编语言过程

汇编语言堆栈简介

如下图所示,如果把 10 个盘子垒起来,其结果就称为堆栈。虽然有可能从这个堆栈的中间移出一个盘子,但是,更普遍的是从顶端移除。新的盘子可以叠加到堆栈顶部,但不能加在底部或中部。

盘子构成的堆栈

堆栈数据结构(stack data structure)的原理与盘子堆栈相同:新值添加到栈顶,删除值也在栈顶移除。通常,对各种编程应用来说,堆栈都是有用的结构,并且它们也容易用面向对象的编程方法来实现。

堆栈也被称为 LIFO 结构(后进先出,Last-In First-Out),,其原因是,最后进入堆栈的值也是第一个出堆栈的值。

汇编语言运行时堆栈(内存数组)

运行时堆栈是内存数组,CPU 用 ESP(扩展堆栈指针,extended stack pointer)寄存器对其进行直接管理,该寄存器被称为堆栈指针寄存器(stack pointer register)。

32位模式下,ESP 寄存器存放的是堆栈中某个位置的 32 位偏移量。ESP 基本上不会直接被程序员控制,反之,它是用 CALL、RET、PUSH 和 POP 等指令间接进行修改。

ESP 总是指向添加,或压入(pushed)到栈顶的最后一个数值。为了便于说明,假设现有一个堆栈,内含一个数值。如下图所示,ESP 的内容是十六进制数 0000 1000,即刚压入堆栈数值(0000 0006)的偏移量。在图中,当堆栈指针数值减少时,栈顶也随之下移。

包含一个值得堆栈

上图中,每个堆栈位置都是32位长,这 是32位模式下运行程序的情形。

运行时堆栈工作于系统层,处理子程序调用。堆栈 ADT 是编程结构,通常用高级编程语言编写,如 C++ 或 Java。它用于实现基于后进先出操作的算法。

入栈操作

32 位入栈操作把栈顶指针减 4(4个byte)再将数值复制到栈顶指针指向的堆栈位置。下图展示了把 0000 00A5 压入堆栈的结果,堆栈中已经有一个数值(0000 0006)。注意,ESP 寄存器总是指向最后压入堆栈的数据项。

将整数压入堆栈

上图中显示的堆栈顺序与之前示例给出的盘堆栈顺序相反,这是因为运行时堆栈在内存中是向下生长的,即从高地址向低地址扩展。入栈之前, ESP=0000 1000h;入栈之后,ESP=0000 0FFCh。下图显示了同一个堆栈总共压入 4 个整数之后的情况。

压入数值00000001和00000002的堆栈

出栈操作

出栈操作从堆栈删除数据。数值弹岀堆栈后,栈顶指针增加(按堆栈元素大小),指向堆栈中下一个最高位置。下图展示了数值 0000 0002 弹出前后的堆栈情况。

从运行时堆栈弹出一个数值

ESP 之下的堆栈域在逻辑上是空白的,当前程序下一次执行任何数值入栈操作指令都可以覆盖这个区域。

堆栈应用

运行时堆栈在程序中有一些重要用途:

  • 当寄存器用于多个目的时,堆栈可以作为寄存器的一个方便的临时保存区。在寄存器被修改后,还可以恢复其初始值。
  • 执行 CALL 指令时,CPU 在堆栈中保存当前过程的返回地址
  • 调用过程时,输入数值也被称为参数,通过将其压入堆栈实现参数传递
  • 堆栈也为过程局部变量提供了临时存储区域

汇编语言PUSH和POP指令(压栈和出栈)

汇编里把一段内存空间定义为一个栈,栈总是先进后出,栈的最大空间为 64K(因为一个segment最大为64K)。由于 "栈" 是由高到低使用的,所以新压入的数据的位置更低,ESP 中的指针将一直指向这个新位置,所以 ESP 中的地址数据是动态的。

PUSH 指令

PUSH 指令首先减少 ESP 的值,再将源操作数复制到堆栈(先占位,再复制)。操作数是 16 位的,则 ESP 减 2,操作数是 32 位的,则 ESP 减 4。PUSH 指令有 3 种格式:

PUSH reg/mem16
PUSH reg/mem32
PUSH inm32

POP指令

POP 指令首先把 ESP 指向的堆栈元素内容复制到一个 16 位或 32 位目的操作数中,再增加 ESP 的值(先移走,再取消占位)。如果操作数是 16 位的,ESP 加 2,如果操作数是 32 位的,ESP 加 4:

POP reg/mem16
POP reg/mem32

PUSHFD 和 POPFD 指令

PUSHFD 指令把 32 位 EFLAGS 寄存器内容压入堆栈,而 POPFD 指令则把栈顶单元内容弹出到 EFLAGS 寄存器:

pushfd
popfd

不能用 MOV 指令把标识寄存器内容复制给一个变量,因此,PUSHFD 可能就是保存标志位的最佳途径。有些时候保存标志寄存器的副本是非常有用的,这样之后就可以恢复标志寄存器原来的值。通常会用 PUSHFD 和 POPFD 封闭一段代码:

pushfd           ;保存标志寄存器
;
;任意语句序列
;
popfd           ;恢复标志寄存器

当用这种方式使用入栈和出栈指令时,必须确保程序的执行路径不会跳过 POPFD 指令。当程序随着时间不断修改时,很难记住所有入栈和出栈指令的位置。因此,精确的文档就显得至关重要!

一种不容易出错的保存和恢复标识寄存器的方法是:将它们压入堆栈后,立即弹出给一个变量

.data
saveFlags DWORD ?
.code
pushfd ;标识寄存器内容入栈
pop saveFLags ;复制给一个变量

下述语句从同一个变量中恢复标识寄存器内容:

push saveFlags ;被保存的标识入栈
popfd ;复制给标识寄存器

PUSHAD,PUSHA,POPAD 和 POPA

PUSHAD 指令按照 EAX、ECX、EDX、EBX、ESP(执行 PUSHAD 之前的值)、EBP、ESI 和 EDI 的顺序,将所有 32 位通用寄存器压入堆栈。

POPAD 指令按照相反顺序将同样的寄存器弹出堆栈。与之相似,PUSHA 指令按序(AX、CX、DX、BX、SP、BP、SI 和 DI)将 16 位通用寄存器压入堆栈。

POPA 指令按照相反顺序将同样的寄存器弹出堆栈。在 16 位模式下,只能使用 PUSHA 和 POPA 指令。

如果编写的过程会修改 32 位寄存器的值,则在过程开始时使用 PUSHAD 指令,在结束时使用 POPAD 指令,以此保存和恢复寄存器的内容。示例如下列代码段所示:

MySub PROC
pushad ;保存通用寄存器的内容
.
.
mov eax,...
mov edx,...
mov ecx,...
.
.
popad ;恢复通用寄存器的内容
ret
MySub ENDP

必须要指岀,上述示例有一个重要的例外:过程用一个或多个寄存器来返回结果时,不应使用 PUSHA 和 PUSHAD。假设下述 ReadValue 过程用 EAX 返回一个整数;调用 POPAD 将会覆盖 EAX 中的返回值:

ReadValue PROC
pushad ;保存通用寄存器的内容
.
.
mov eax rreturn_value
.
.
popad ;覆盖 EAX !
ret
ReadValue ENDP

示例:字符串反转

现在查看名为 RevStr 的程序:在一个字符串上循环,将每个字符压入堆栈,再把这些字符从堆栈中弹出(相反顺序),并保存回同一个字符串变量。由于堆栈是 LIFO(后进先出)结构,字符串中的字母顺序就发生了翻转:

;字符串翻转(Revstr.asm)
.386
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD

.data
aName BYTE "Abraham Lincoln",0
nameSize = ($-aName)-1

.code
main PROC
;将名字压入堆栈
mov ecx,nameSize
mov esi,0

L1: movzx eax,aName[esi] ;获取字符
push eax ;压入堆栈
inc esi
loop L1
;将名字逆序弹出堆栈
;并存入aName数组
mov ecx,nameSize
mov esi,0

L2: pop eax ;获取字符
mov aName[esi],al ;存入字符串
inc esi
loop L2

INVOKE ExitProcess,0
main ENDP
END main

汇编语言PROC和ENDP伪指令:定义一个过程

如果大家已经学过了高级编程语言,那么就会知道将程序分割为子过程(subroutine)是多么有用。一个复杂的问题常常要分解为相互独立的任务,这样才易于被理解、实现以及有效地测试。

汇编语言中,通常用术语过程(procedure)来指代子程序。在其他语言中,子程序也被称为方法或函数。

就面向对象编程而言,单个类中的函数或方法大致相当于封装在一个汇编语言模块中的过程和数据集合。汇编语言出现的时间远早于面向对象编程,因此它不具备面向对象编程中的形式化结构。汇编程序员必须在程序中实现自己的形式化结构。

定义过程

过程可以非正式地定义为:以返回语句结束已命名的语句块。过程用 PROC 和 ENDP 伪指令来定义,并且必须为其分配一个名字(有效标识符)。到目前为止,所有编写的程序都包含了一个名为 main 的过程,例如:

main PROC
.
.
main ENDP

当在程序启动过程之外创建一个过程时,就用 RET 指令来结束它。RET 强制 CPU 返回到该过程被调用的位置

sample PROC
    .
    .
    ret
sample ENDP

过程中的标号

默认情况下,标号只在其被定义的过程中可见。这个规则常常影响到跳转和循环指令。在下面的例子中,名为 Destination 的标号必须与 JMP 指令位于同一个过程中:

jmp Destination

解决这个限制的方法是定义全局标号,即在名字后面加双冒号 (::)。

Destination::

就程序设计而言,跳转或循环到当前过程之外不是个好主意。过程用自动方式返回并调整运行时堆栈。如果直接跳出一个过程,则运行时堆栈很容易被损坏。

示例:三个整数求和

现在创建一个名为 SumOf 的过程计算三个 32 位整数之和。假设在过程调用之前,整数已经分配给 EAX、EBX 和 ECX。过程用 EAX 返回和数

SumOf PROC
add eax,ebx
add eax,ecx
ret
SumOf ENDP

过程说明

要培养的一个好习惯是为程序添加清晰可读的说明。下面是对放在每个过程开头的信息的一些建议:

  • 对过程实现的所有任务的描述。
  • 输入参数及其用法的列表,并将其命名为 Receives ( 接收 )。如果输入参数对其数值有特殊要求,也要在这里列岀来。
  • 对过程返回的所有数值的描述,并将其命名为 Returns ( 返回 )。
  • 所有特殊要求的列表,这些要求被称为先决条件 (preconditions),必须在过程被调用之前满足。列表命名为 Requires。例如,对一个画图形线条的过程来说,一个有用的先决条件是该视频显示适配器必须已经处于图形模式。

上述选择的描述性标号,如 ReceivesReturns 和 Requires,不是绝对的;其他有用的名字也常常被使用。

有了这些思想,现在对 SumOf 过程添加合适的说明:

;-------------------------------------------------------
; sumof
; 计算 3 个 32 位整数之和并返回和数。
; 接收:EAX、EBX和ECX为3个整数,可能是有符号数,也可能是无符号数。
; 返回:EAX=和数
;------------------------------------------------------
SumOf PROC
add eax,ebx
add eax,ecx
ret
SumOf ENDP

用高级语言,如 C 和 C++,编写的函数,通常用 AL 返回 8 位的值,用 AX 返回 16 位的值,用 EAX 返回 32 位的值。

汇编语言CALL和RET指令:调用一个过程

CALL 指令调用一个过程,指挥处理器从新的内存地址开始执行。过程使用 RET(从过程返回)指令将处理器转回到该过程被调用的程序点上。

从物理上来说,CALL 指令将其返回地址压入堆栈,再把被调用过程的地址复制到指令指针寄存器。当过程准备返回时,它的 RET 指令从堆栈把返回地址弹回到指令指针寄存器。32 位模式下,CPU 执行的指令由 EIP(扩展指令指针寄存器)在内存中指岀。16 位模式下,由 IP (指令指针寄存器)指出指令。

调用和返回示例

假设在 main 过程中,CALL 指令位于偏移量为 0000 0020 处。通常,这条指令需要 5 个字节的机器码,因此,下一条语句(本例中为一条 MOV 指令)就位于偏移量为 0000 0025 处:

     main PROC
00000020  call MySub
00000025  mov eax,ebx

然后,假设 MySub 过程中第一条可执行指令位于偏移量 0000 0040 处:

   MySub PROC
00000040 mov eaxz edx
      .
      .
      ret
   MySub ENDP

当 CALL 指令执行时如下图所示,调用之后的地址(0000 0025)被压入堆栈,MySub 的地址加载到 EIP。

执行一天CALL指令

执行 MySub 中的全部指令直到 RET 指令。当执行 RET 指令时,ESP 指向的堆栈数值被弹岀到 EIP(如下图所示,步骤 1)。在步骤 2 中,ESP 的数值增加,从而指向堆栈中的前一个值(步骤 2)。

执行RET指令

汇编语言过程调用嵌套

被调用过程在返回之前又调用了另一个过程时,就发生了过程调用嵌套。假设 main 调用了过程 Sub1。当 Sub1 执行时,它调用了过程 Sub2。当 Sub2 执行时,它调用了过程 Sub3。步骤如下图所示。

过程调用嵌套

当执行 Sub3 末尾的 RET 指令时,将 stack[ESP](堆栈段首地址 +ESP 给岀的偏移量)中的数值弹出到指令指针寄存器中,这使得执行转回到调用 Sub3 后面的指令。下图显示的是执行从 Sub3 返回操作之前的堆栈:

返回之后,ESP 指向栈顶下一个元素。当 Sub2 末尾的 RET 指令将要执行时,堆栈如下所示:

最后,执行 Sub1 的返回,stack[ESP] 的内容弹出到指令指针寄存器,继续在 main 中执行:

显然,堆栈证明了它很适合于保存信息,包括过程调用嵌套。一般说来,堆栈结构用于程序需要按照特定顺序返回的情况。

向过程传递寄存器参数

如果编写的过程要执行一些标准操作,如整数数组求和,那么,在过程中包含对特定变量名的引用就不是一个好主意。如果这样做了,该过程就只能作用于一个数组。更好的方法是向过程传递数组的偏移量以及指定数组元素个数的整数。这些内容被称为参数(或输入参数)。在汇编语言中,经常用通用寄存器来传递参数。

前面创建了一个简单的过程 SumOf,计算 EAX、EBX 和 ECX 中的整数之和。在 main 调用 SumOf 之前,将数值分配给 EAX、EBX 和 ECX:

.data
theSum DWORD ?
.code
main PROC
mov eax, 10000h ;参数
mov ebx, 20000h ;参数
mov ecx, 30000h ;参数
call Sumof ;EAX=(EAX+EEX+ECX)
mov theSum,eax ;保存和数

在 CALL 语句之后,选择了将 EAX 中的和数复制给一个变量。

汇编语言示例:整数数组求和

程序员在 C++ 或 Java 中编写过的非常常见的循环类型是计算整数数组之和。这在汇编语言中很容易实现,它可以被编码为按照尽可能快的方式来运行。比如,在循环内可以使用寄存器而非变量。

现在创建一个过程 ArraySum,从一个调用程序接收两个参数:一个指向 32 位整数数组的指针,以及一个数组元素个数的计数器。该过程计算和数,并用 EAX 返回数组之和:

;------------------------------------
;ArraySum
;计算32位整数数组元素之和
;接收:ESI = 数组偏移量
;      ECX = 数组元素的个数
;返回:EAX = 数组元素之和
;-------------------------------------
ArraySum PROC
    push esi                ;保存ESI和ECX
    push ecx
    mov eax,0               ;设置和数为0

L1: add eax,[esi]       ;将每个整数与和数相加
    add esi,TYPE DWORD      ;指向下一个整数

    loop L1                 ;按照数组大小重复
   
    pop ecx                 ;恢复ECX和ESI
    pop esi               
    ret                     ;和数在EAX中
ArraySum ENDP

这个过程没有特别指定数组名称和大小,它可以用于任何需要计算32位整数数组之和的程序。只要有可能,编程者也应该编写具有灵活性和适应性的程序。

测试 ArraySum 过程

下面的程序通过传递一个 32 位整数数组的偏移量和长度来测试 ArraySum 过程。调用 ArraySum 之后,程序将过程的返回值保存在变量 theSum 中。

;测试ArraySum过程
.386
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD

.data
array DWORD 10000h,20000h,30000h,40000h,50000h
theSum DWORD ?

.code
main PROC
    mov esi,OFFSET array          ;ESI指向数组
    mov ecx,LENGTHOF array        ;ECX = 数组计算器
    call ArraySum                 ;计算和数
    mov theSum,eax                ;用EAX返回和数

    INVOKE ExitProcess,0
main ENDP
;------------------------------------
;ArraySum
;计算32位整数数组元素之和
;接收:ESI = 数组偏移量
;       ECX = 数组元素的个数
;返回:EAX = 数组元素之和
;-------------------------------------
ArraySum PROC
    push esi                 ;保存ESI和ECX
    push ecx
    mov eax,0                ;设置和数为0

L1: add eax,[esi]        ;将每个整数与和数相加
    add esi,TYPE DWORD      ;指向下一个整数
    loop L1                 ;按照数组大小重复
    pop ecx                 ;恢复ECX和ESI
    pop esi               
    ret                     ;和数在EAX中
ArraySum ENDP
END  main

汇编语言USES运算符:保存和恢复寄存器

在上面的 ArraySum 示例中,ECX 和 ESI 在过程开始时被压入堆栈,在过程结束时被弹出堆栈。这是大多数过程修改寄存器的典型操作。总是保存和恢复被过程修改的寄存器,将使得调用程序确保自己的寄存器值不会被覆盖。但是对用于返回数值的寄存器应该例外,通常是指 EAX,不要将它们压入和弹出堆栈。

USES 运算符

USES 运算符与 PROC 伪指令一起使用,让程序员列出在该过程中修改的所有寄存器名。USES 告诉汇编器做两件事情:第一,在过程开始时生成 PUSH 指令,将寄存器保存到堆栈;第二,在过程结束时生成 POP 指令,从堆栈恢复寄存器的值。

USES 运算符紧跟在 PROC 之后,其后是位于同一行上的寄存器列表,表项之间用空格符或制表符(不是逗号)分隔。

在 ArraySum 过程使用 PUSH 和 POP 指令来保存和恢复 ESI 和 ECX。 USES 运算符能够更加容易地实现同样的功能:

ArraySum PROC USES esi ecx
  mov eax, 0                   ;置和数为0
L1:
  add eax,[esi]                ;将每个整数与和数相加
  add esi, TYPE DWORD          ;指向下个整数
  loop L1                      ;按照数组大小重复
  ret                          ;和数在 EAX 中
ArraySum ENDP
ArraySum PROC
  push esi
  push ecx
  mov eax, 0                      ;置和数为0
L1:
  add eax, [esi]                  ;将每个整数与和数相加
  add esi, TYPE DWORD             ;指向下一个整数
  loop L1                         ;按照数组大小重复
  pop ecx
  pop esi
  ret
ArraySum ENDP

调试提示:使用 Microsoft Visual Studio 调试器可以查看由 MASM 高级运算符和伪指令生成的隐藏机器指令。在调试窗口中右键点击,选择 Go To Disassembly。该窗口显示程序源代码,以及由汇编器生成的隐藏机器指令。

汇编语言链接库简介

如果编程者花时间的话,就可以用汇编语言编写岀详细的输入输岀代码。就好比自己从头开始搭建汽车,然后可以驾车出行一样。这个工作很有趣但也很耗时。接下来我们来了解一下什么是链接库。

背景知识

链接库是一种文件,包含了已经汇编为机器代码的过程(子程序)链接库开始时是一个或多个源文件,这些文件再被汇编为目标文件目标文件插入到一个特殊格式文件,该文件由链接器工具识别。

假设一个程序调用过程 WriteString 在控制台窗口显示一个字符串。该程序源代码必须包含 PROTO (原形)伪指令来标识 WriteString 过程:

WriteString proto

之后,CALL 指令执行 WriteString:

call WriteString

当程序进行汇编时,汇编器将不指定 CALL 指令的目标地址,它知道这个地址将由链接器指定链接器在链接库中寻找 WriteString,并把库中适当的机器指令复制到程序的可执行文件中。同时,它把 WriteString 的地址插入到 CALL 指令。

如果被调用过程不在链接库中,链接器就发出错误信息,且不会生成可执行文件。

链接命令选项

链接器工具把一个程序的目标文件与一个或多个目标文件以及链接库组合在一起。比如,下述命令就将 hello.obj 与 irvine32.lib 和 kernel32.lib 库链接起来:

link hello.obj irvine32.lib kernel32.lib

如果编程者花时间的话,就可以用汇编语言编写岀详细的输入输岀代码。就好比自己从头开始搭建汽车,然后可以驾车出行一样。这个工作很有趣但也很耗时。接下来我们来了解一下什么是链接库。

32位程序链接

kernel32.lib 文件是 Microsoft Windows 平台软件开发工具(Software Development Kit,SDK)的一部分,它包含了 kernel32.dll 文件中系统函数的链接信息。kernel32.dll 文件是 MS-Windows 的一个基本组成部分,被称为动态链接库(dynamic link library)。它含有的可执行函数实现基于字符的输入输出。

下图展示了为什么 kernel32.lib 是通向 kernel32.dll 的桥梁。

32位程序链接

汇编语言Irvine32链接库

汇编语言编程没有 Microsoft 认可的标准库。在 20 世纪 80 年代早期,程序员第一次开始为 x86 处理器编写汇编语言时,MS-DOS 是常用的操作系统。这些 16 位程序可以调用 MS-DOS 函数(即 INT 21h 服务)来实现简单的输入输出。

即使是在那个时代,如果想在控制台上显示一个整数,也需要编写一个相当复杂的程序,将整数的内部二进制表示转换为可以在屏幕上显示的 ASCII 字符序列。这个过程被称为 WriteInt,下面是其抽象为伪代码的逻辑:

初始化:

let n equal the binary value
let buffer be an array of char[size]

i = size -1                      ;缓冲区最后一个位置
repeat
  r = n mod 10                   ;余数
  n = n / 10                     ;整数除法
  digit = r OR 30h               ;将工转换为 ASCII 数字
  bufferf[i--] = digit           ;保存到缓冲区
until n = 0
if n is negative
  buffer[i] = "-"                ;插入负号
while i > 0
  print buffer[i]
  i++

注意,数字是按照逆序生成,插入缓冲区,从后往前移动。然后,数字按照正序写到控制台。虽然这段代码简单到足以用 C/C++ 实现,但是如果是在汇编语言中,它还需要一些高级技巧。

专业程序员通常更愿意自己建立库,这是一种很好的学习经验。在 Windows 的 32 位模式下,输入输出库必须能直接调用操作系统的内容。这个学习曲线相当陡峭,对编程初学者提出了一些挑战。因此,Irvine32 链接库被设计成给初学者提供简单的输入输岀接口。

随着学习的推进,我们将能获得自己创建库的知识和技术。只要成为库的创建者,就能自由地修改和重用库。

下表列出了 Irvine32 链接库的全部过程。

过程说明
CloseFile关闭之前已经打开的磁盘文件
Clrscr清除控制台窗口,并将光标置于左上角
CreateOutputFile为输出模式下的写操作创建一个新的磁盘文件
Crlf在控制台窗口中写一个行结束的序列
Delay程序执行暂停指定的 n 毫秒
DumpMem以十六进制形式,在控制台窗口写一个内存块
DumpRegs以十六进制形式显示 EAX、EEX、ECX、EDX、ESI、EDI、EBP、ESP、EFLAGS 和 EIP 寄存器。也显示最常见的 CPU 状态标志位
GetCommandTail复制程序命名行参数(称为命令尾)到一个字节数组
GetDateTime从系统获取当前日期和时间
GetMaxXY返回控制台窗口缓冲器的行数和列数
GetMseconds返回从午夜开始经过的毫秒数
GetTextColor返回当前控制台窗口的前景色和背景色
Gotoxy将光标定位到控制台窗口内指定的位置
IsDigit如果 AL 寄存器中包含了十进制数字(0-9)的 ASCII 码,则零标志位置 1
MsgBox显示一个弹出消息框
MsgBoxAsk在弹出消息框中显示 yes/no 问题
OpenlnputFile打开一个已有磁盘文件进行输入操作
ParseDecimal32将一个无符号十进制整数字符串转换为 32 位二进制数
Parselnteger32将一个有符号十进制整数字符串转换为 32 位二进制数
Random32在 0〜FFFFFFFFh 范围内,生成一个 32 位的伪随机整数
Randomize用一个值作为随机数生成器的种子
RandomRange在特定范围内生成一个伪随机整数
ReadChar等待从键盘输入一个字符,并返回该字符
ReadDec从键盘读取一个无符号 32 位十进制整数,用回车符结束
ReadFromFile将一个输入磁盘文件读入缓冲区
ReadHex从键盘读取一个 32 位十六进制整数,用回车符结束
Readlnt从键盘读取一个有符号 32 位十进制整数,用回车符结束
ReadKey无需等待输入即从键盘输入缓冲区读取一个字符
ReadString从键盘读取一个字符串,用回车符结束
SetTextColor设置控制台输出字符的前景色和背景色
Str_compare比较两个字符串
Str_copy将源字符串复制到目的字符串
Str_length用 EAX 返回字符串长度
Str_trim从字符串删除不需要的字符
Str_ucase将字符串转换为大写字母
WaitMsg显示信息并等待按键操作
WriteBin用 ASCII 二进制格式,向控制台窗口写一个无符号 32 位整数
WriteBinB用字节、字或双字格式向控制台窗口写一个二进制整数
WriteChar在控制台窗口写一个字符
WriteDec用十进制格式,向控制台窗口写一个无符号 32 位整数
WriteHex用十六进制格式,向控制台窗口写一个 32 位整数
WriteHexB用十六进制格式,向控制台窗口写一个字节、字或双字整数
Writelnt用十进制格式,向控制台窗口写一个有符号 32 位整数
WriteStackFrame向控制台窗口写当前过程的堆栈帧
WriteStackFrameName向控制台窗口写当前过程的名称和堆栈帧
WriteString向控制台窗口写一个以空字符结束的字符串
WriteToFile将缓冲区内容写入一个输出文件
WriteWindowsMsg显示一个字符串,包含 MS-Windows 最近一次产生的错误

下面将逐一介绍《Irvine32链接库》中 Irvine32 链接库中的过程是如何使用的,同时也会忽略一些更高级的过程,它们将在后续章节中进行解释。

CloseFile

CloseFile 过程关闭之前已经创建或打开的文件(参见 CreateOutputFile 和  OpenlnputFile)。该文件用一个 32 位整数的句柄来标识,句柄由 EAX 传递。如果文件成功关闭,EAX 中的返回值就是非零的。示例如下:

mov eax,fileHandle
call CloseFile

Clrscr

Clrscr 过程清除控制台窗口。该过程通常在程序开始和结束时被调用。如果在其他时间调用这个过程,就需要先调用 WaitMsg 来暂停程序,这样就可以让用户在屏幕被清除之前,阅读屏幕上的信息。调用示例如下:

call WaitMsg ; "Press any key..."
call Clrscr

CreateOutputFile

CreateOutputFile 过程创建并打开一个新的磁盘文件,进行写操作。调用该过程时,将文件名的偏移量送入 EDX。过程返回后,如果文件创建成功则 EAX 将包含一个有效文件句柄(32 位整数),否则,EAX 将等于 INVALID_HANDLE_VALUE(一个预定义的常数)。调用示例如下:

.data
filename BYTE "newfile.txt",0
.code
mov edx,OFFSET filename
call CreateOutputFile

下面的伪代码描述的是调用 CreateOutputFile 之后,可能会出现的结果:

if EAX = INVALID_HANDLE_VALUE
the file was not created successfully
else
EAX = handle for the open file
endif

Crlf

Crlf 过程将光标定位在控制台窗口下一行的开始位置。它写的字符串包含了 ASCII 字符代码 ODh 和 OAh。调用示例如下:

call Crlf

Delay

Delay 过程按照特定毫秒数暂停程序。在调用 Delay 之前,将预定时间间隔送入 EAXO 调用示例如下:

mov eax,1000 ;1 秒
call Delay

DumpMen

DumpMen 过程在控制台窗口中用十六进制的形式显示一段内存区域。ESI 中存放的是内存区域首地址;ECX 中存放的是单元个数;EBX 中存放的是单元大小(1 = 字节,2 = 字,4 = 双字)。下述调用示例用十六进制形式显示了包含 11 个双字的数组:

.data
array DWORD 1,2,3,4,5,6,7,8,9,0Ah,0Bh
.code
main PROC
mov esi,OFFSET array ;首地址偏移量
mov ecx, LENGTHOF array ;单元个数
mov ebx,TYPE array ;双字格式
call DumpMen

产生的输出如下所示:

00000001    00000002    00000003    00000004    00000005    00000006
00000007    00000008    00000009    0000000A    0000000B

DumpRegs

DumpRegs 过程用十六进制形式显示 EAX、EBX、ECX、EDX、ESI、EDI、EBP、ESP、EIP 和 EFL(EFLAGS)的内容,以及进位标志位、符号标志位、零标志位、溢出标志位、辅助进位标志位和奇偶标志位的值。调用示例如下:

call DumpRegs

示例输出如下所示:

EAX=00000613    EBX=00000000    ECX=000000FF    EDX=00000000
ESI=00000000    EDI=00000100    EBP=0000091E    ESP=000000F6
EIP=00401026    EFL=00000286    CF=0    SF=1    ZF=0    OF=0    AF=0    PF=1

EIP 显示的数值是调用 DumpRegs 的下一条指令的偏移量。DumpRegs 在调试程序时很有用,因为它显示了 CPU 快照。该过程没有输入参数和返回值。

GetCommandTail

GetCommandTail 过程将程序命令行复制到一个空字节结束的字符串。如果命令行是空,则进位标志位置 1 ;否则进位标志位清零。该过程的作用在于能让程序用户通过命令行传递参数。假设有一程序 Encrypt.exe 读取输入文件 filel.txt,并产生输出文件 file2.txt。程序运行时,用户可以通过命令行传递这两个文件名:

Encrypt filel.txt file2.txt

当 Encrypt 程序启动时,它可以调用 GetCommandTail,检索这两个文件名。调用 GetCommandTail 时,EDX 必须包含一个数组的偏移量,该数组至少要有 129 个字节。调用示例如下:

.data
cmdTail BYTE 129 DUP ( 0 ) ;空缓冲区
.code
mov edx,OFFSET cmdTail
call GetCommandTail ;填充缓冲区

在 Visual Studio 中运行应用程序时,有一种方法可以传递命令行参数。在 Project 菜单中,选择 <projectname>Properties。在 Property Pages 窗口,展开 Configuration Properties 选项,选择 Debugging。然后,在右边 Command Arguments 面板的编辑行中输入程序的命令参数。

GetMaxXY

GetMaxXY 过程获取控制台窗口缓冲区的大小。如果控制台窗口缓冲区大于可视窗口尺寸,则自动显示滚动条。GetMaxXY 没有输入参数。当过程返回时,DX 寄存器包含了缓冲区的列数,AX 寄存器包含了缓冲区的行数。每个数值的可能范围都不超过 255,这也许会小于实际窗口缓冲区的大小。调用示例如下:

.data
rows BYTE ?
cols BYTE ?
.code
call GetMaxXY
mov rows, al
mov cols,dl

GetMseconds

GetMseconds 过程获取主机从午夜开始经过的毫秒数,并用 EAX 返回该值。在计算事件间隔时间时,这个过程是非常有用的。过程不需要输入参数。

下面的例子调用了 GetMseconds,并保存了返回值。执行循环之后,代码第二次调用 GetMseconds,并将两次返回的时间值相减,结果就是执行循环的大致时间:

.data
startTime DWORD ?
.code
call GetMseconds
mov startTime,eax
LI :
;(loop body)
loop LI
call GetMseconds
sub eax, startTime ;EAX = 循环时间,按毫秒计

GetTextColor

GetTextColor 过程获取控制台窗口当前的前景色和背景色,它没有输入参数。返回时,AL 中的高四位是背景色,低四位是前景色。调用示例如下:

.data
color byte ?
.code
call GetTextColor
mov color,AL

Gotoxy

Gotoxy 过程将光标定位到控制台窗口的指定位置。默认情况下,控制台窗口的X轴范围为 0〜79,Y 轴范围为 0〜24。调用 Gotoxy 时,将 Y 轴(行数)传递到 DH 寄存器,X 轴(列数)传递到 DL 寄存器。调用示例如下:

mov dh, 10 ;第 10 行
mov dl, 20 ;第 20 列
call Gotoxy ;定位光标

用户可能会修改控制台窗口大小,因此可以调用 GetMaxXY 获取当前窗口的行列数。

IsDigit

IsDigit 过程确定 AL 中的数值是否是一个有效十进制数的 ASCII 码。过程被调用时,将一个 ASCII 字符传递到 AL。如果 AL 包含的是一个有效十进制数,则过程将零标志位置 1;否则,清除零标志位。调用示例如下:

mov AL,somechar
call IsDigit

MsgBox

MsgBox 过程显示一个带选择项的图形界面弹出消息框。(当程序运行于控制台窗口时有效。)过程用 EDX 传递一个字符串的偏移量,该字符串将显示在消息框中。还可以用 EBX 传递消息框标题字符串的偏移量,如果标题为空,则 EBX 为 0。调用示例如下:

.data
caption BYTE "Dialog Title", 0
HelloMsg BYTE "This is a pop-up message box.", 0dh, 0ah
BYTE "Click OK to continue...", 0
.code
mov ebx,OFFSET caption
mov edx,OFFSET HelloMsg
call MsgBox

MsgBoxAsk

MsgBoxAsk 过程显示带有 Yes 和 No 按钮的图形弹岀消息框。(当程序运行于控制台窗口时有效。)过程用 EDX 传递问题字符串的偏移量,该问题字符串将显示在消息框中。还可以用 EBX 传递消息框标题字符串的偏移量,如果标题为空,则 EBX 为 0。

MsgBoxAsk 用 EAX 中的返回值表示用户选择的是哪个按钮,返回值有两个选择,都是预先定义的 Windows 常数:IDYES (值为 6)或 IDNO(值为 7)。调用示例如下:

.data
caption BYTE "Survey Completed",0
question BYTE "Thank you for completing the survey."
BYTE 0dh,0ah
BYTE "Would you like to receive the results?",0
.code
mov ebx,OFFSET caption
mov edx,OFFSET question
call MsgBoxAsk ;查看 EAX 中的返回值

OpenlnputFile

OpenlnputFile 过程打开一个已存在的文件进行输入。过程用 EDX 传递文件名的偏移量。当从过程返回时,如果文件成功打开,则 EAX 就包含有效的文件句柄。 否则,EAX 等于 INVALID_HANDLE_VALUE(一个预定义的常数)。

调用示例如下:

.data
filename BYTE "myfile.txt",0
.code
mov edx,OFFSET filename
call OpenlnputFile

下述伪代码显示了调用 OpenlnputFile 后可能的结果:

if EAX = INVALID_HANDLE_VALUE
the file was not opened successfully
else
EAX = handle for the open file
endif

ParseDecimal32

ParseDecimal32 过程将一个无符号十进制整数字符串转换为 32 位二进制数。非数字符号之前所有的有效数字都要转,前导空格要忽略。过程用 EDX 传递字符 串的偏移量,用 ECX 传递字符串的长度,用 EAX 返回二进制数值。

调用示例如下:

.data
buffer BYTE "8193"
bufSize = ($ - buffer)
.code
mov edx,OFFSET buffer
mov ecx, bufSize
call Pars eDecimal32 ;返回 EAX
  • 如果整数为空,则 EAX=0 且 CF=1
  • 如果整数只有空格,则 EAX=0 且 CF=1
  • 如果整数大于(2³²-1),则 EAX=0 且 CF=1
  • 否则,EAX 为转换后的数,且 CF=0

参阅 ReadDec 过程的说明,详细了解进位标志位是如何受到影响的。

Parselnteger32

Parselnteger32 过程将一个有符号十进制整数字符串转换为32位二进 制数。字符串开始到第一个非数字符号之间所有的有效数字都要转,前导空格要忽略。过程用 EDX 传递字符串的偏移量,用 ECX 传递字符串的长度,用 EAX 返回二进制数值。调用示例如下:

.data
buffer byte ,'-8193"
bufSize = ($ - buffer)
.code
mov edx,OFFSET buffer
mov ecx,bufSize
call Parselnteger32 ;返回 EAX

字符串可能包含一个前导加号或减号,但其后只能跟十进制数字。如果数值不能表示为 32 位有符号整数(范围:-2 147 483 648 到 +2 147 483 647),则溢出标志位置 1,且在控制 台显示一个错误信息。

Random32

Random32 过程生成一个 32 位随机整数并用 EAX 返回该数。当被反复调用时,Random32 就会生成一个模拟的随机数序列,这些数由一个简单的函数产生,该函数有一个输入称为种子(seed)。

函数利用公式里的种子生成一个随机数值,并且每次都使用前次生成的随机数作为种子,来生成后续随机数。下述代码段展示了一个调用 Random32 的例子:

.data
randVal DWORD ?
.code
call Random32
mov randVal, eax

Randomize

Randomize 过程对 Random32 和 RandomRange 过程的第一个种子进行初始化。种子等于一天中的时间,精度为 1/100 秒。每当调用 Random32 和 RandomRaiige 的程序运行时,生成的随机数序列都不相同。而 Randomize 程只需要在程序开头调用一次。 下面的例子生成了 10 个随机整数:

call Randomize
mov ecx,10
L1: call Random32
;在此使用或显示 EAX 中的随机数
loop L1

RandomRange

RandomRange 过程在范围 0〜n-1 内生成一个随机整数,其中 n 是用 EAX 寄存器传递的输入参数。生成的随机数也用 EAX 返回。下面的例子在 0 到 4999 之间生成一个随机整数,并将其放在变量 randVal 中。

.data
randVal DWORD ?
.code
mov eax,5000
call RandomRange
mov randVal, eax

ReadChar

ReadChar 过程从键盘读取一个字符,并用 AL 寄存器返回,字符不在控制台窗口中回显。调用示例如下:

.data
char BYTE ?
.code
call ReadChar
mov char,al

如果用户按下的是扩展键,如功能键、方向键、Ins 键或 Del 键,则过程就把 AL 清零,而 AH 包含的是键盘扫描码。EAX 的高字节没有使用。下述伪代码描述了调用 ReadChar 之后可能产生的结果:

if an extended key was pressed
AL = 0
AH = keyboard scan code
else
AL = ASCII key value
endif

ReadDec

ReadDec 过程从键盘读取一个 32 位无符号十进制整数,并用 EAX 返回该值,前导空格要忽略。返回值为遇到第一个非数字字符之前的所有有效数字。比如,如果用户输入123AEC,则 EAX 中的返回值为 123。下面是一个调用示例:

.data
intVal DWORD ?
.code
call ReadDec
mov intVal,eax

ReadDec 会影响进位标志位:

  • 如果整数为空,则 EAX=0 且 CF=1
  • 如果整数只有空格,则 EAX=0 且 CF=1
  • 如果整数大于(2³²-1),则 EAX=0 且 CF=1
  • 否则,EAX 为转换后的数,且 CF=0

ReadFromFile

ReadFromFile 过程读取存储缓冲区中的一个输入磁盘文件。当调用 ReadFromFile 时,用 EAX 传递打开文件的句柄,用 EDX 传递缓冲区的偏移量,用 ECX 传递读取的最大字节数。

ReadFromFile 返回时要查看进位标志位的值:如果 CF 清零,则 EAX 包含了从文件中读取的字节数;如果 CF 置 1,则 EAX 包含了数字系统错误代码。调用 WriteWindowsMsg 程就可以获得该错误的文本。在下面的例子中,从文件读取的 5000 个字节复制到了缓冲区变量:

.data
BUFFER_SIZE = 5000
buffer BYTE BUFFER_SIZE DUP(?)
bytesRead DWORD ?
.code
mov edx,OFFSET buffer ;指向缓冲区
mov ecx,BUFFER_SIZE ;读取的最大字节数
call ReadFromFile ; 读文件 }

如果此时进位标志位清零,则可以执行如下指令:

mov bytesRead, eax ;实际读取的字节数

但是,如果此时进位标志位置 1,就可以调用 WriteWindowsMsg 过程,显示错误代码以及该应用最近产生错误的说明:

call WriteWindowsMsg

ReadHex

ReadHex 过程从键盘读取一个 32 位十六进制整数,并用 EAX 返回相应的二进制数。对无效字符不进行任何错误检查。字母 A 到 F 的大小写都可以使用。最多能够输入 8 个数字(超出的字符将被忽略),前导空格将被忽略。调用示例如下:

.data
hexVal DWORD ?
.code
call ReadHex
mov hexVal,eax

Readlnt

Readlnt 过程从键盘读取一个 32 位有符号整数,并用 EAX 返回该值。用户可以键入前置加号或减号,而其后跟的只能是数字。

Readlnt 设置溢出标志位,如果输入数值无法表示为 32 位有符号数(范围:-2 147 483 648 至 +2 147 483 647),则显示一个错误信息。返回值包括所有的有效数字,直到遇见第一个非数字字符。例如,如果用户输入 +123ABC,则返回值为 +123。调用示例如下:

.data
intVal SDWORD ?
.code
call Readlnt
mov intVal,eax

ReadKey

ReadKey 过程执行无等待键盘检查。换句话说,它检查键盘输入缓冲区以查看用户是否有按键操作。如果没有发现键盘数据,则零标志位置 1。如果 ReadKey 发现有按键,则清除零标志位,且向 AL 送入 0 或 ASCII 码。若 AL 为 0,表示用户可能按下了一个特殊键(功能键、方向键等)。

AH 寄存器为虚拟扫描码,DX 为虚拟键码,EBX 为键盘标志位。下述伪代码说明了调用 ReadKey 时的各种结果:

if no_keyboard_data then
ZF = 1
else
ZF = 0
if AL = 0 then
extended key was pressed, and AH = scan code, DX = virtual
key code, and EBX = keyboard flag bits
else
AL = the key's ASCII code
endif
endif

当调用 ReadKey 时,EAX 和 EDX 的高 16 位会被覆盖。

ReadString

ReadString 过程从键盘读取一个字符串,直到用户键入回车键。过程用 EDX 传递缓冲区的偏移量,用 ECX 传递用户能键入的最大字符数加 1(保留给终止空字节),用 EAX 返回用户键入的字符数。示例调用如下:

.data
buffer BYTE 21 DUP(0) ;输入缓冲区
byteCount DWORD ? ;定义计数器
.code
mov edx,OFFSET buffer ;指向缓冲区
mov ecxz SIZEOF buffer ;定义最大字符数
call ReadString ;输入字符串
mov byteCount, eax ;字符数

ReadString 在内存中字符串的末尾自动插入一个 null 终止符。用户输入“ABCDEFG”后,buffer 中前 8 个字节的十六进制形式和 ASCII 形式如下所示:

41 42 43 44 45 46 47 00 ABCDEFG

变量 byteCoun t等于 7。

SetTextColor

SetTextColor 过程(仅在 Irvine32 链接库中)设置输出文本的前景色和背景色。调用 SetTextColor 时,给 EAX 分配一个颜色属性。下列预定义的颜色常数都可以用于前景色和背景色:
 

black = 0red = 4gray = 8lightRed = 12
blue = 1magenta = 5lightBlue = 9light Magenta = 13
green = 2brown = 6light Green = 10yellow = 14
cyan = 3lightGray = 7lightCyan = 11white = 15


颜色常量在 Irvine32.inc 文件中进行定义。要获得完整的颜色字节数值,就将背景色乘以 16 再加上前景色。例如,下述常量表示在蓝色背景上输出黄色字符:

yellow + (blue * 16)

下列语句设置为蓝色背景上输出白色字符:

mov eax,white + (blue * 16)      ; 蓝底白字
call SetTextColor

另一种表示颜色常量的方法是使用 SHL 运算符,将背景色左移 4 位再加上前景色。

yellow + (blue SHL 4)

位移是在汇编时执行的,因此它只能用常数作操作数。

Str_length

Str_length 过程返回空字节结束的字符串的长度。过程用 EDX 传递字符串的偏移量,用 EAX 返回字符串的长度。调用示例如下:

.data
buffer BYTE "abcde",0
bufLength DWORD ?
.code
mov edx, OFFSET buffer ;指向字符串
call Str_length ;EAX=5
mov bufLength, eax ;保存长度

WaitMsg

WaitMsg 过程显示“Press any key to continue…”消息,并等待用户按键。当用户想在数据滚动和消失之前暂停屏幕显示时,这个过程就很有用。过程没有输入参数。 调用示例如下:

call WaitMsg

WriteBin

WriteBin 过程以 ASCII 二进制格式向控制台窗口输出一个整数。过程用 EAX 传递该整数。为了便于阅读,二进制位以四位一组的形式进行显示。调用示例如下:

mov eax,12346AF9h
call WriteBin

示例代码显示如下:

0001 0010 0011 0100 0110 1010 1111 1001

WriteBinB

WriteBinB 过程以 ASCII 二进制格式向控制台窗口输出一个 32 位整数。过程用 EAX 寄存器传递该整数,用 EDX 表示以字节为单位的显示大小(1、2,或 4)。为了便于阅读,二进制位以四位一组的形式进行显示。调用示例如下:

mov eax,00001234h
mov ebx,TYPE WORD ; 两个字节
call WriteBinB ; 显示 0001 0010 0011 0100

WriteChar

WriteChar 过程向控制台窗口写一个字符。过程用 AL 传递字符(或其 ASCII 码)。调用示例如下:

mov al, 'A'
call WriteChar ;显示:"A"

WriteDec

WriteDec 过程以十进制格式向控制台窗口输出一个 32 位无符号整数,且没有前置 0。过程用 EAX 寄存器传递该整数。调用示例如下:

mov eax,295
call WriteDec ;显示:"295"

WriteHex

WriteHex 过程以 8 位十六进制格式向控制台窗口输出一个 32 位无符号整数,如果需要,应插入前置 0。过程用 EAX 传递整数。调用示例如下:

mov eax,7FFFh
call WriteHex ;显示:"00007FFF"

WriteHexB

WriteHexB 过程以十六进制格式向控制台窗口输岀一个 32 位无符号整数,如果需要,应插入前置 0。过程用 EAX 传递整数,用 EBX 表示显示格式的字节数(1、2,或 4)。调用示例如下:

mov eax, 7FFFh
mov ebx, TYPE WORD ;两个字节
call WriteHexB ;显示:"7FFF"

Writelnt

Writelnt 过程以十进制向控制台窗口输岀一个 32 位有符号整数,有前置符号,但没有前置 0。过程用 EAX 传递整数。调用示例如下:

mov eax, 216543
call Writelnt ;显示:"+216543"

WriteString

WriteString 过程向操作台窗口输出一个空字节结束的字符串。过程用 EDX 传递字符串的偏移量。调用示例如下:

.data
prompt BYTE "Enter your name: ",0
.code
mov edx,OFFSET prompt
call WriteString

WriteToFile

WriteToFile 过程向一个输出文件写入缓冲区内容。过程用 EAX 传递有效的文件句柄,用 EDX 传递缓冲区偏移量,用 ECX 传递写入的字节数。当过程返回时,如果 EAX 大于 0,则其包含的是写入的字节数;否则,发生错误。下述代码调用了 WriteToFile:

BUFFER_SIZE = 5000
.data
fileHandle DWORD ?
buffer BYTE BUFFER_SIZE DUP(?)
.code
mov eax, fileHandle
mov edx, OFFSET buffer
mov ecx, BUFFER SIZE
call WriteToFile

下面的伪代码说明了调用 WriteToFile 之后对 EAX 返回值的处理:

if EAX = 0 then
error occurred when writing to file
call WriteWindowsMessage to see the error
else
EAX = number of bytes written to the file
endif

WriteWindowsMsg

WriteWindowsMsg 过程向控制台窗口输出应用程序在调用系统函数时最近产生的错误信息。调用示例如下:

call WriteWindowsMsg

下面的例子展示了一个消息字符串:

Error 2: The system cannot find the file specified.

参与评论 您还未登录,请先 登录 后发表或查看评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:书香水墨 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值