1.第一个汇编语言程序
汇编语言以隐晦难懂而著名,但是本教程从另一个角度来看它——它是一种几乎提供了全部信息的语言。程序员可以看到正在发生的所有事情,甚至包括 CPU 中的寄存器和标志!
但是,在拥有这种能力的同时,程序员必须负责处理数据表示的细节和指令的格式。程序员工作在一个具有大量详细信息的层次。现在以一个简单的汇编语言程序为例,来了解其工作过程。
程序执行两个数相加,并将结果保存在寄存器中。程序名称为 AddTwo:
main PROC
mov eax, 5 ;将数字 5 送入 eax 寄存器
add eax, 6 ;eax 寄存器加 6
INVOKE ExitProcess, 0 ;程序结束
main ENDP
现在按照一次一行代码的方法来仔细查看这段程序:
- 第 1 行开始 main 程序(主程序),即程序的入口;
- 第 2 行将数字 5 送入 eax 寄存器;
- 第 3 行把 6 加到 EAX 的值上,得到新值 11;
- 第 5 行调用 Windows 服务(也被称为函数)ExitProcess 停止程序,并将控制权交还给操作系统;
- 第 6 行是主程序结束的标记。
大家可能已经注意到了程序中包含的注释,它总是用分号开头。程序的顶部省略了一些声明,稍后会予以说明,不过从本质上说,这是一个可以用的程序。
它不会将全部信息显示在屏幕上,但是借助工具程序调试器的运行,程序员可以按一次一行代码的方式执行程序, 并查看寄存器的值。
添加一个变量
现在让这个程序变得有趣些,将加法运算的结果保存在变量 sum 中。要实现这一点,需要增加一些标记,或声明,用来标识程序的代码和数据区:
.data ;此为数据区
sum DWORD 0 ;定义名为sum的变量
.code ;此为代码区
main PROC
mov eax,5 ;将数字5送入而eax寄存器
add eax,6 ;eax寄存器加6
mox sum,eax
INVOKE ExitProcess,0 ;结束程序
main ENDP
变量 sum 在第 2 行进行了声明,其大小为 32 位,使用了关键字 DWORD。汇编语言中有很多这样的大小关键字,其作用或多或少与数据类型一样。
但是与程序员可能熟悉的类型相比它们没有那么具体,比如 int、double、float 等等。这些关键字只限制大小,并不检查变量中存放的内容。记住,程序员拥有完全控制权。
顺便说一下,那些被 .code 和 .data 伪指令标记的代码和数据区,被称为段。即,程序有代码段和数据段。
2.汇编语言常量
常量(constant)是程序中使用的一个确定数值,在汇编阶段就可以确定,直接编码于指令代码中,不是保存在存储器中可变的变量,因为是编码在指令中的量,和指令一起存储了,所以不用单独开辟主存空间,所以也就没法动态改变它了,这也正是高级语言常量无法修改的原因。
整数常量
整数常量(integer literal)(又称为整型常量(integer constant))由一个可选前置符号、一个或多个数字,以及一个指明其基数的可选基数字符构成:
[{+|-}] digits [radix]
提示:本教程使用 Microsoft 语法符号。方括号内的元素是可选的;大括号内的元素用 | 符号分隔,且必须要选择其中一个元素;斜体字标识的是有明确定义或说明的元素。
由此,比如 26 就是一个有效的整数常量。它没有基数,所以假设其是十进制形式。如果想要表示十六进制数 26,就将其写为 26h。同样,数字 1101 可以被看做是十进制值,除非在其末尾添加“b”,使其成为 1101b (二进制)。下表列出了可能的基数值:
h | 十六进制 | r | 编码实数 |
q/o | 八进制 | t | 十进制(备用) |
d | 十进制 | y | 二进制(备用) |
b | 二进制 |
下面这些整数常量声明了各种基数。每行都有注释:
26 ;十进制
26d ;十进制
11010011b ;二进制
42q ;八进制
42o ;八进制
1Ah ;十六进制
0A3h ;十六进制
以字母开头的十六进制数必须加个前置 0,以防汇编器将其解释为标识符。
整型常量表达式
整型常量表达式 (constant integer expression) 是一种算术表达式,它包含了整数常量和算术运算符。每个表达式的计算结果必须是一个整数,并可用 32 位 (从 0 到 FFFFFFFFh) 来存放。
下表列出了算术运算符,并按照从高 (1) 到低 (4) 的顺序给出了它们的优先级。对整型常量表达式而言很重要的是,要意识到它们只在汇编时计算。这里将它们简称为 整数表达式。
运算符 | 名称 | 优先级 |
---|---|---|
() | 圆括号 | 1 |
+,- | 一元加、减 | 2 |
*, / | 乘、除 | 3 |
MOD | 取模 | 3 |
+, – | 加、减 | 4 |
运算符优先级 (operator precedence) 是指,当一个表达式包含两个或多个运算符时,这些操作的执行顺序。下面是一些表达式和它们的执行顺序:
4 + 5 * 2 ;乘法,加法
12 - 1 MOD 5 ;取模,减法
-5 + 2 ;一元减法,加法
(4 + 2) * 6 ;加法,乘法
下面给出了一些有效表达式和它们的值:
表达式 | 值 |
---|---|
16/5 | 3 |
-(3+4) * (6-1 ) | -35 |
-3+4*6- 1 | 20 |
25 mod 3 | 1 |
提示:在表达式中使用圆括号来表明操作顺序,那么就不用去死记运算符优先级。
实数常量
实数常量(real number literal)(又称为浮点数常量(floating-point literal))用于表示十进制实数和编码(十六进制)实数。十进制实数包含一个可选符号,其后跟随一个整数,一个十进制小数点,一个可选的表示小数部分的整数,和一个可选的指数:
[sign]integer.[integer] [exponent]
符号和指数的格式如下:
sign {+,-}
exponent E[{+,-}]integer
下面是一些有效的十进制实数:
2.
+3.0
-44.2E+05
26.E5
至少需要一个数字和一个十进制小数点。
编码实数(encoded real)表示的是十六进制实数,用 IEEE 浮点数格式表示短实数。比如,十进制数 +1.0 用二进制表示为:
0011 1111 1000 0000 0000 0000 0000 0000
在汇编语言中,同样的值可以编码为短实数:
3F800000r
字符常量
字符常量 (character literal) 是指,用单引号或双引号包含的一个字符。汇编器在内存中保存的是该字符二进制 ASCII 码的数值。例如:
‘A’
“d”
表明字符常量在内部保存为整数,使用的是 ASCII 编码序列。因此,当编写字符常量“A”时,它在内存中存放的形式为数字 65 ( 或 41h)。
字符串常量
字符串常量 (string literal) 是用单引号或双引号包含的一个字符 ( 含空格符 ) 序列:
‘ABC’
‘X’
“Good night, Gracie”
‘40961’
嵌套引号也是被允许的,使用方法如下例所示:
“This isn’t a test”
‘Say “Good night,” Gracie’
和字符常量以整数形式存放一样,字符串常量在内存中的保存形式为整数字节数值序列。例如,字符串常量“ABCD”就包含四个字节 41h、42h、43h、44h。
3.汇编语言保留字
保留字(reserved words)有特殊意义并且只能在其正确的上下文中使用。默认情况下,保留字是没有大小写之分的。比如,MOV 与 mov、Mov 是相同的。
保留字有不同的类型:
- 指令助记符,如 MOV、ADD 和 MUL。
- 寄存器名称。
- 伪指令,告诉汇编器如何汇编程序。
- 属性,提供变量和操作数的大小与使用信息。例如 BYTE 和 WORD。
- 运算符,在常量表达式中使用。
- 预定义符号,比如 @data,它在汇编时返回常量的整数值。
下表是常用的保留字列表。
$ | PARITY? | DWORD | STDCALL |
? | PASCAL | FAR | SWORD |
@B | QWORD | FAR16 | SYSCALL |
@F | REAL4 | FORTRAN | TBYTE |
ADDR | REAL8 | FWORD | VARARG |
BASIC | REAL10 | NEAR | WORD |
BYTE | SBYTE | NEAR16 | ZERO? |
C | SDORD | OVERFLOW? | |
CARRY? | SIGN? |
4.汇编语言标识符及其命名规则
标识符(identifier)是由程序员选择的名称,它用于标识变量、常数、子程序和代码标签。
标识符的形成有一些规则:
- 可以包含 1 到 247 个字符。
- 不区分大小写。
- 第一个字符必须为字母 (A—Z, a—z) A 下划线 (_)、@、? 或 $。其后的字符也可以是数字。
- 标识符不能与汇编器保留字相同。
提示:可以在运行汇编器时,添加 -Cp 命令行切换项来使得所有关键字和标识符变成大小写敏感。
通常,在高级编程语言代码中,标识符使用描述性名称是一个好主意。尽管汇编语言指令短且隐晦,但没有理由使得标识符也要变得难以理解。
下面是一些命名良好的名称:
lineCount firstValue index line_count
myFile xCoord main x_Coord
下面的名称合法,但是不可取:
_lineCount $first @myFile
一般情况下,应避免用符号 @ 和下划线作为第一个字符,因为它们既用于汇编器,也用于高级语言编译器。
5.汇编语言伪指令
伪指令 (directive) 是嵌入源代码中的命令,由汇编器识别和执行。伪指令不在运行时执行,但是它们可以定义变量、宏和子程序;为内存段分配名称,执行许多其他与汇编器相关的日常任务。
默认情况下,伪指令不区分大小写。例如,.data,.DATA 和 .Data 是相同的。
下面的例子有助于说明伪指令和指令的区别。DWORD 伪指令告诉汇编器在程序中为一个双字变量保留空间。另一方面,MOV 指令在运行时执行,将 myVar 的内容复制到 EAX 寄存器中:
- myVar DWORD 26
- mov eax,myVar
尽管 Intel 处理器所有的汇编器使用相同的指令集,但是通常它们有着不同的伪指令。比如,Microsoft 汇编器的 REPT 伪指令对其他一些汇编器就是无法识别的。
定义段
汇编器伪指令的一个重要功能是定义程序区段,也称为段 (segment)。程序中的段具有不同的作用。如下面的例子,一个段可以用于定义变量,并用 .DATA 伪指令进行标识:
.data
.CODE 伪指令标识的程序区段包含了可执行的指令:
.code
.STACK 伪指令标识的程序区段定义了运行时堆栈,并设置了其大小:
.stack 100h
6.汇编语言指令详解
指令(instruction)是一种语句,它在程序汇编编译时变得可执行。汇编器将指令翻译为机器语言字节,并且在运行时由 CPU 加载和执行。
一条指令有四个组成部分:
- 标号(可选)
- 指令助记符(必需)
- 操作数(通常是必需的)
- 注释(可选)
不同部分的位置安排如下所示:
[label: ] mnemonic [operands] [;comment]
现在分别了解每个部分,先从标号字段开始。
1) 标号
标号(label)是一种标识符,是指令和数据的位置标记。标号位于指令的前端,表示指令的地址。同样,标号也位于变量的前端,表示变量的地址。标号有两种类型:数据标号和代码标号。
数据标号标识变量的位置,它提供了一种方便的手段在代码中引用该变量。比如,下面定义了一个名为 count 的变量:
count DWORD 100
汇编器为每个标号分配一个数字地址。可以在一个标号后面定义多个数据项。在下面的例子中,array 定义了第一个数字(1024)的位置,其他数字在内存中的位置紧随其后:
array DWORD 1024, 2048
DWORD 4096, 8192
程序代码区(指令所在区段)的标号必须用冒号(:)结束。代码标号用作跳转和循环指令的目标。例如,下面的 JMP 指令创建一个循环,将程序控制传递给标号 target 标识的位置:
target:
mov ax,bx
...
jmp target
代码标号可以与指令在同一行上,也可以自己独立一行:
L1: mov ax, bx
L2 :
标号命名规则要求,只要每个标号在其封闭子程序页中是唯一的,那么就可以多次使用相同的标号。
2) 指令助记符
指令助记符(instruction mnemonic)是标记一条指令的短单词。在英语中,助记符是帮助记忆的方法。相似地,汇编语言指令助记符,如 mov, add 和 sub,给出了指令执行操作类型的线索。下面是一些指令助记符的例子:
助记符 | 说明 | 助记符 | 说明 |
---|---|---|---|
MOV | 传送(分配)数值 | MUL | 两个数值相乘 |
ADD | 两个数值相加 | JMP | 跳转到一个新位置 |
SUB | 从一个数值中减去另一个数值 | CALL | 调用一个子程序 |
3) 操作数
操作数是指令输入输出的数值。汇编语言指令操作数的个数范围是 0〜3 个,每个操作数可以是寄存器、内存操作数、整数表达式和输入输岀端口。
生成内存操作数有不同的方法,比如,使用变量名、带方括号的寄存器等。变量名暗示了变量地址,并指示计算机使用给定地址的内存内容。下表列出了一些操作数示例:
示例 | 操作数类型 | 示例 | 操作数类型 |
---|---|---|---|
96 | 整数常量 | eax | 寄存器 |
2+4 | 整数表达式 | count | 内存 |
现在来考虑一些包含不同个数操作数的汇编语言指令示例。比如,STC 指令没有操作数:
stc ;进位标志位置 1
INC 指令有一个操作数:
inc eax ;EAX 加 1
MOV 指令有两个操作数:
mov count, ebx ;将 EBX 传送给变量 count
操作数有固有顺序。当指令有多个操作数时,通常第一个操作数被称为目的操作数,第二个操作数被称为源操作数(source operand)。
一般情况下,目的操作数的内容由指令修改。比如,在 mov 指令中,数据就是从源操作数复制到目的操作数。
IMUL 指令有三个操作数,第一个是目的操作数,第二个和第三个是进行乘法的源操作数:
imul eax,ebx,5
在上例中,EBX 与 5 相乘,结果存放在 EAX 寄存器中。
4) 注释
注释是程序编写者与阅读者交流程序设计信息的重要途径。程序清单的开始部分通常包含如下信息:
- 程序目标的说明
- 程序创建者或修改者的名单
- 程序创建和修改的日期
- 程序实现技术的说明
注释有两种指定方法:
- 单行注释,用分号(;)开始。汇编器将忽略在同一行上分号之后的所有字符。
- 块注释,用 COMMENT 伪指令和一个用户定义的符号开始。汇编器将忽略其后所有的文本行,直到相同的用户定义符号出现为止。
示例如下:
COMMENT !
This line is a comment.
This line is also a comment.
!
其他符号也可以使用,只要该符号不出现在注释行中:
COMMENT &
This line is a comment.
This line is also a comment.
&
当然,程序员应该在整个程序中提供注释,尤其是代码意图不太明显的地方。
5) NOP(空操作)指令
最安全(也是最无用)的指令是 NOP(空操作)。它在程序空间中占有一个字节,但是不做任何操作。它有时被编译器和汇编器用于将代码对齐到有效的地址边界。
在下面的例子中,第一条指令 MOV 生成了 3 字节的机器代码。NOP 指令就把第三条指令的地址对齐到双字边界(4 的偶数倍):
00000000 66 8B C3 mov ax,bx
00000003 90 nop ;对齐下条指令
00000004 8B D1 mov edx,ecx
x86 处理器被设计为从双字的偶数倍地址处加载代码和数据,这使得加载速度更快。
7.汇编语言整数加减法示例
在《第一个汇编语言程序》一节中给出的 AddTwo 程序,并添加必要的声明使其成为完全能运行的程序。
; AddTwo.asm -两个 32 位整数相加
.386
.model flat,stdcall
.stack 4096
ExitProcess PROTO, dwExitCode:DWORD
.code
main PROC
mov eax,5 ;将数字5送入eax寄存器
add eax,6 ;eax寄存器加6
INVOKE ExitProcess,0
main ENDP
END main
第 3 行是 .386 伪指令,它表示这是一个 32 位程序,能访问 32 位寄存器和地址。第 4 行选择了程序的内存模式(flat),并确定了子程序的调用规范(称为 stdcall)。其原因是 32 位 Windows 服务要求使用 stdcall 规范。第 5 行为运行时堆栈保留了 4096 字节的存储空间,每个程序都必须有。
第 6 行声明了 ExitProcess 函数的原型,它是一个标准的 Windows 服务。原型包含了函数名、PROTO 关键字、一个逗号,以及一个输入参数列表。ExitProcess 的输入参数名称为 dwExitCode。
可以将其看作为给 Windows 操作系统的返回值,返回值为零,则表示程序执行成功;而任何其他的整数值都表示了一个错误代码。因此,程序员可以将自己的汇编程序看作是被操作系统调用的子程序或过程。当程序准备结束时,它就调用 ExitProcess,并向操作系统返回一个整数以表示该程序运行良好。
大家可能会好奇,为什么操作系统想要知道程序是否成功完成。理由如下:与按序执行一些程序相比,系统管理员常常会创建脚本文件。在脚本文件中的每一个点上,系统管理员都需要知道刚执行的程序是否失败,这样就可以在必要时退出该脚本。
脚本通常如下例所示,其中,ErrorLevel1 表示前一步的过程返回码大于或等于 1 :
call program_1
if ErrorLevel 1 goto FailedLabel
call program_2
if ErrorLevel 1 goto FailedLabel
:SuccessLabel.
Echo Great, everything worked!
现在回到 AddTwo 程序清单。第 15 行用 end 伪指令来标记汇编的最后一行,同时它也标识了程序的入口(main)。标号 main 在第 9 行进行了声明,它标记了程序开始执行的地址。
汇编伪指令回顾
现在回顾一些在示例程序中使用过的最重要的汇编伪指令。
首先是 .MODEL 伪指令,它告诉汇编程序用的是哪一种存储模式:
.model flat,stdcall
32 位程序总是使用平面(flat)存储模式,它与处理器的保护模式相关联。关键字 stdcall 在调用程序时告诉汇编器,怎样管理运行时堆栈。然后是 .STACK 伪指令,它告诉汇编器应该为程序运行时堆栈保留多少内存字节:
.stack 4096
数值 4096 可能比将要用的字节数多,但是对处理器的内存管理而言,它正好对应了一个内存页的大小。所有的现代程序在调用子程序时都会用到堆栈。首先,用来保存传递的参数;其次,用来保存调用函数的代码的地址。
函数调用结束后,CPU 利用这个地址返回到函数被调用的程序点。此外,运行时堆栈还可以保存局部变量,也就是,在函数内定义的变量。
.CODE 伪指令标记一个程序代码区的起点,代码区包含了可执行指令。通常,.CODE 的下一行声明程序的入口,按照惯例,一般会是一个名为 main 的过程。程序的入口是指程序要执行的第一条指令的位置。用下面两行来传递这个信息:
.code
main PROC
ENDP 伪指令标记一个过程的结束。如果程序有名为 main 的过程,则 endp 就必须使用同样的名称:
main ENDP
最后,END 伪指令标记一个程序的结束,并要引用程序入口:
END main
如果在 END 伪指令后面还有更多代码行,它们都会被汇编程序忽略。程序员可以在这里放各种内容一一程序注释,代码副本等等,都无关紧要。
运行和调试 AddTwo 程序
使用 Visual Studio 可以很方便地编辑、构建和运行汇编语言程序。下面的步骤,按照 Visual Studio 2012,说明了怎样打开示例项目,并创建 AddTwo 程序:
1) 启动计算机上安装的最新版本的 Visual Studio。
2) 打开 Visual Studio 中 Solution Explorer 窗口。它应该已经是可见的,但是程序员也可以在 View 菜单中选择 Solution Explorer 使其可见。
3) 在 Solution Explorer 窗口右键点击项目名称,在文本菜单中选择 Add,再在弹出菜单中选择 New Item。
4) 在 Add New File 对话窗口中(如下图所示 ) ,将文件命名为 AddTwo.asm,填写 Location 项为该文件选择一个合适的磁盘文件夹。
5) 单击 Add 按钮保存文件。
6) 键入程序源代码,如下所示。这里大写关键字不是必需的:
; AddTwo.asm - adds two 32-bit integers.
.386
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD
.code
main PROC
mov eax, 5
add eax, 6
INVOKE ExitProcess,0
main ENDP
END main
7) 在 Project 菜单中选择 Build Project,查看 Visual Studio 工作区底部的错误消息。这被称为错误列表窗口。注意,当没有错误时,窗口底部的状态栏会显示 Build succeeded。
调试演示
下面将展示 AddTwo 程序的一个示例调试会话。演示使用的是 Visual Studio 2012,不过,自 2008 年起的任何版本的 Visual Studio 都可以使用。
运行调试程序的一个方法是在 Debug 菜单中选择 Step Over。按照 Visual Studio 的配置,F10 功能键或 Shift+F8 组合键将执行 Step Over 命令。
开始调试会话的另一种方法是在程序语句上设置断点,方法是在代码窗口左侧灰色垂直条中直接单击。断点处由一个红色大圆点标识出来。然后就可以从 Debug 菜单中选择 Start Debugging 开始运行程序。
如果试图在非执行代码行设置断点,那么在运行程序时,Visual Studio 会直接将断点前移到下一条可执行代码行。
当调试器被激活时,Visual Studio 窗口底部的状态栏变为橙色。当调试器停止并返回编辑模式时,状态栏变为蓝色。可视提示是有用的,因为在调试器运行时,程序员无法对程序进行编辑或保存。
自定义调试接口
在调试时可以自定义调试接口。例如,如果想要显示 CPU 寄存器,实现方法是,在 Debug 菜单中选择 Windows,然后再选择 Registerso, 其中 Registers 窗口可见,同时还关闭了一些不重要的窗口。EAX 数值显示为 0000000B,是十进制数 11 的十六进制表示。
Registers 窗口中,EFL 寄存器包含了所有的状态标志位(零标志、进位标志、溢出标志等)。如果在 Registers 窗口中 右键单击,并在弹出菜单中选择Flags,则窗口将显示单个的标志位值。
Registers 窗口的一个重要特点是,在单步执行程序时,任何寄存器,只要当前指令修改了它的数值,就会变为红色。尽管无法在打印页面(它只有黑白两色)上表示出来,这种红色高亮确实显示给程序员,使之了解其程序是怎样影响寄存器的。
在 Visual Studio 中运行一个汇编语言程序时,它是在控制台窗口中启动的。这个窗口与从 Windows 的 Start 菜单运行名为 cmd.exe 程序的窗口是相同的。或者,还可以打开项目 Debug\Bin 文件夹中的命令提示符,直接从命令行运行应用程序。如果采用的是这个方法,程序员就只能看见程序的输出,其中包括了写入控制台窗口的文本。查找具有相同名称的可执行文件作为 Visual Studio 项目。
8.汇编器以及汇编流程
- 步骤1:编程者用文本编辑器 (text editor) 创建一个 ASCII 文本文件,称之为源文件。
- 步骤2:汇编器读取源文件,并生成目标文件,即对程序的机器语言翻译。或者,它也会生成列表文件。只要出现任何错误,编程者就必须返回步骤 1,修改程序。
- 步骤3:链接器读取并检查目标文件,以便发现该程序是否包含了任何对链接库中过程的调用。链接器从链接库中复制任何被请求的过程,将它们与目标文件组合,以生成可执行文件。
- 步骤4:操作系统加载程序将可执行文件读入内存,并使 CPU 分支到该程序起始地址,然后程序开始执行。
列表文件
列表文件 (listing file) 包括了程序源文件的副本,再加上行号、每条指令的数字地址、每条指令的机器代码字节(十六进制)以及符号表。符号表中包含了程序中所有标识符的名称、段和相关信息。
高级程序员有时会利用列表文件来获得程序的详细信息。下面的代码展示了 AddTwo 程序的部分列表文件,现在进一步查看这个文件。1〜7 行没有可执行代码,因此它们原封不动地从源文件中直接复制过来。第 9 行表示代码段开始的地址为 0000 0000(在 32 位程序中,地址显示为 8 个十六进制数字)。这个地址是相对于程序内存占用起点而言 的,但是,当程序加载到内存中时,这个地址就会转换为绝对内存地址。此时,该程序就会从这个地址开始,比如 0004 0000h。
; AddTwo.asm - adds two 32-bit integers.
; Chapter 3 example
.386
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD
00000000 .code
00000000 main PROC
00000000 B8 00000005 mov eax, 5
00000005 83 C0 06 add eax,6
invoke ExitProcess,0
00000008 6A 00 push +000000000h
0000000A E8 00000000 E call ExitProcess
0000000F main ENDP
END main
第 10 行和第 11 行也显示了相同的开始地址 0000 0000,原因是:第一条可执行语句是 MOV 指令,它在第 11 行。请注意第 11 行中,在地址和源代码之间出现了几个十六进制字节,这些字节(B8 0000 0005)代表的是机器代码指令(B8 ),而该指令分配给 EAX 的就是 32 位常数值(0000 0005):
00000000 B8 00000005 mov eax, 5
数值 B8 也被称为操作代码(或简称为操作码),因为它表示了特定的机器指令,将一个 32 位整数送入 eax 寄存器。
第 12 行也是一条可执行指令,起始偏移量为 0000 0005。这个偏移量是指从程序起始地址开始 5 个字节的距离。
第 14 行有 invoke 伪指令。注意第 15 行和 16 行是如何插入到这段代码中的,插入代码的原因是,INVOKE 伪指令使得汇编器生成 PUSH 和 CALL 语句,它们就显示在第 15 行和 16 行。
代码中展示的示例列表文件说明了机器指令是怎样以整数值序列的形式加载到内存的,在这里用十六进制表示:B8、0000 0005、83、C0、06、6A、00、EB、0000 0000。每个数中包含的数字个数暗示了位的个数:2 个数字就是 8 位,4 个数字就是 16 位,8 个数字就是 32 位,以此类推。所以,本例机器指令长正好是 15 个字节(2 个 4 字节值和 7 个 1 字节值)。
当程序员想要确认汇编器是否按照自己的程序生成了正确的机器代码字节时,列表文件就是最好的资源。如果是刚开始学习机器代码指令是如何生成的,列表文件也是一个很好的教学工具。
若想告诉 Visual Studio 生成列表文件,则在打开项目时按下述步骤操作:在 Project 菜单中选择 Properties,在 Configuration Properties 下,选择 Microsoft Macro Assemblero 然后选择 Listing File。在对话框中,设置 Generate Preprocessed Source Listing 为 Yes,设置 List All Available Information 为 Yes。
9.汇编语言数据类型以及数据定义详解
汇编器识别一组基本的内部数据类型(intrinsic data type),按照数据大小(字节、字、双字等等)、是否有符号、是整数还是实数来描述其类型。这些类型有相当程度的重叠,例如,DWORD 类型(32 位,无符号整数)就可以和 SDWORD 类型(32 位,有符号整数)相互交换。
可能有人会说,程序员用 SDWORD 告诉读程序的人,这个值是有符号的,但是,对于汇编器来说这不是强制性的。汇编器只评估操作数的大小。因此,举例来说,程序员只能将 32 位整数指定为 DWORD、SDWORD 或者 REAL4 类型。
下表给出了全部内部数据类型的列表,有些表项中的 IEEE 符号指的是 IEEE 计算机学会出版的标准实数格式。
类型 | 用法 |
---|---|
BYTE | 8 位无符号整数,B 代表字节 |
SBYTE | 8 位有符号整数,S 代表有符号 |
WORD | 16 位无符号整数 |
SWORD | 16 位有符号整数 |
DWORD | 32 位无符号整数,D 代表双(字) |
SDWORD | 32 位有符号整数,SD 代表有符号双(字) |
FWORD | 48 位整数(保护模式中的远指针) |
QWORD | 64 位整数,Q 代表四(字) |
TBYTE | 80 位(10 字节)整数,T 代表 10 字节 |
REAL4 | 32 位(4 字节)IEEE 短实数 |
REAL8 | 64 位(8 字节)IEEE 长实数 |
REAL10 | 80 位(10 字节)IEEE 扩展实数 |
数据定义语句
数据定义语句(data definition statement)在内存中为变量留岀存储空间,并赋予一个可选的名字。数据定义语句根据内部数据类型(上表)定义变量。
数据定义语法如下所示:
[name] directive initializer [,initializer]…
下面是数据定义语句的一个例子:
count DWORD 12345
其中:
- 名字:分配给变量的可选名字必须遵守标识符规范。
- 伪指令:数据定义语句中的伪指令可以是 BYTE、WORD、DWORD、SBTYE、SWORD 或其他在上表中列出的类型。此外,它还可以是传统数据定义伪指令,如下表所示。
伪指令 | 用法 | 伪指令 | 用法 |
---|---|---|---|
DB | 8位整数 | DQ | 64 位整数或实数 |
DW | 16 位整数 | DT | 定义 80 位(10 字节)整数 |
DD | 32 位整数或实数 |
数据定义中至少要有一个初始值,即使该值为 0。其他初始值,如果有的话,用逗号分隔。对整数数据类型而言,初始值(initializer)是整数常量或是与变量类型,如 BYTE 或 WORD 相匹配的整数表达式。
如果程序员希望不对变量进行初始化(随机分配数值),可以用符号 ? 作为初始值。所有初始值,不论其格式,都由汇编器转换为二进制数据。 初始值 0011 0010b、32h 和 50d 都具有相同的二进制数值。
向 AddTwo 程序添加一个变量
前面《整数加减法》一节中介绍了 AddTwo 程序,现在创建它的一个新版本,并称为 AddTwoSum。这个版本引入了变量 sum,它出现在完整的程序清单中:
;AddTowSum.asm
.386
.model flat,stdcall
.stack 4096
ExitProcess PROTO, dwExitCode:DWORD
.data
sum DWORD 0
.code
main PROC
mov eax,5
add eax,6
mov sum,eax
INVOKE ExitProcess,0
main ENDP
END main
可以在第 13 行设置断点,每次执行一行,在调试器中单步执行该程序。执行完第 15 行后,将鼠标悬停在变量 sum 上,查看其值。或者打开一个 Watch 窗口,打开过程如下:在 Debug 菜单中选择 Windows(在调试会话中),选择 Watch,并在四个可用选项(Watch1,Watch2,Watch3 或 Watch4)中选择一个。然后,用鼠标高亮显示 sum 变量,将其拖拉到 Watch 窗口中。下图展示了一个例子,其中用大箭头指出了执行第 15 行后,sum 的当前值。
定义 BYTE 和 SBYTE 数据
BYTE(定义字节)和 SBYTE(定义有符号字节)为一个或多个无符号或有符号数值分配存储空间。每个初始值在存储时,都必须是 8 位的。例如:
value1 BYTE 'A' ;字符常量
value2 BYTE 0 ;最小无符号字节
value3 BYTE 255 ;最大无符号字节
value4 SBYTE -128 ;最小有符号字节
value5 SBYTE +127 ;最大有符号字节
问号(?)初始值使得变量未初始化,这意味着在运行时分配数值到该变量:
value6 BYTE ?
可选名字是一个标号,标识从变量包含段的开始到该变量的偏移量。比如,如果 value1 在数据段偏移量为 0000 处,并在内存中占一个字节,则 value2 就自动处于偏移量为 0001 处:
value1 BYTE 10h
value2 BYTE 20h
DB 伪指令也可以定义有符号或无符号的 8 位变量:
val1 DB 255 ;无符号字节
val2 DB -128 ;有符号字节
1) 多初始值
如果同一个数据定义中使用了多个初始值,那么它的标号只指出第一个初始值的偏移量。在下面的例子中,假设 list 的偏移量为 0000。那么,数值 10 的偏移量就为 0000, 20 的偏移量为 0001,30 的偏移量为 0002,40 的偏移量为 0003:
list BYTE 10,20,30,40
下图给出了字节序列 list,显示了每个字节及其偏移量。
并不是所有的数据定义都要用标号。比如,在 list 后面继续添加字节数组,就可以在下一行定义它们:
list BYTE 10,20,30,40
BYTE 50,60,70,80
BYTE 81,82,83,84
在单个数据定义中,其初始值可以使用不同的基数。字符和字符串常量也可以自由组合。在下面的例子中,list1 和 list2 有相同的内容:
list1 BYTE 10, 32, 41h, 00100010b
list2 BYTE 0Ah, 20h, 'A', 22h
2) 定义字符串
定义一个字符串,要用单引号或双引号将其括起来。最常见的字符串类型是用一个空字节(值为0)作为结束标记,称为以空字节结束的字符串,很多编程语言中都使用这种类型的字符串:
greeting1 BYTE "Good afternoon",0
greeting2 BYTE 'Good night',0
每个字符占一个字节的存储空间。对于字节数值必须用逗号分隔的规则而言,字符串是一个例外。如果没有这种例外,greeting1 就会被定义为:
greeting1 BYTE 'G', 'o', 'o', 'd'....etc.
这就显得很冗长。一个字符串可以分为多行,并且不用为每一行都添加标号:
greeting1 BYTE "Welcome to the Encryption Demo program "
BYTE "created by Kip Irvine.",0dh, 0ah
BYTE "If you wish to modify this program, please "
BYTE "send me a copy.",0dh,0ah,0
十六进制代码 0Dh 和 0Ah 也被称为 CR/LF (回车换行符)或行结束字符。在编写标准输出时,它们将光标移动到当前行的下一行的左侧。
行连续字符(\)把两个源代码行连接成一条语句,它必须是一行的最后一个字符。下面的语句是等价的:
greeting1 BYTE "Welcome to the Encryption Demo program "
和
greeting1 \
BYTE "Welcome to the Encryption Demo program "
3) DUP 操作符
DUP 操作符使用一个整数表达式作为计数器,为多个数据项分配存储空间。在为字符串或数组分配存储空间时,这个操作符非常有用,它可以使用初始化或非初始化数据:
BYTE 20 DUP ( 0 ) ;20 个字节,值都为 0
BYTE 20 DUP ( ? ) ;20 个字节,非初始化
BYTE 4 DUP ( "STACK" ) ; 20 个字节:
也可以使用传统的 DW 伪指令:
val1 DW 65535 ;无符号
val2 DW -32768 ;有符号
16 位字数组通过列举元素或使用 DUP 操作符来创建字数组。下面的数组包含了一组数值:
myList WORD 1,2,3,4,5
下图是一个数组在内存中的示意图,假设 myList 起始位置偏移量为0000。由于每个数值占两个字节,因此其地址递增量为 2。
DUP 操作符提供了一种方便的方法来声明数组:
array WORD 5 DUP (?) ; 5 个数值,未初始化
定义 DWORD 和 SDWORD 数据
DWORD(定义双字)和 SDWORD(定义有符号双字)伪指令为一个或多个 32 位整数分配存储空间:
val1 DWORD 12345678h ;无符号
val2 SDWORD -2147483648 ;有符号
val3 DWORD 20 DUP (?) ;无符号数组
传统的 DD 伪指令也可以用来定义双字数据:
val1 DD 12345678h ;无符号
val2 DD -2147483648 ;有符号
DWORD 还可以用于声明一种变量,这种变量包含的是另一个变量的 32 位偏移量。如下所示,pVal 包含的就是 val3 的偏移量:
pVal DWORD val3
32 位双字数组
现在定义一个双字数组,并显式初始化它的每 一个值:
myList DWORD 1,2,3,4,5
下图给岀了这个数组在内存中的示意图,假设 myList 起始位置偏移量为 0000,偏移量增量为 4。
定义 QWORD 数据
QWORD(定义四字)伪指令为 64 位(8 字节)数值分配存储空间:
quad1 QWORD 1234567812345678h
传统的 DQ 伪指令也可以用来定义四字数据:
quad1 DQ 1234567812345678h
定义压缩 BCD(TBYTE)数据
Intel 把一个压缩的二进制编码的十进制(BCD, Binary Coded Decimal)整数存放在一个 10 字节的包中。每个字节(除了最高字节之外)包含两个十进制数字。在低 9 个存储字节中,每半个字节都存放了一个十进制数字。最高字节中,最高位表示该数的符号位。如果最高字节为 80h,该数就是负数;如果最高字节为 00h,该数就是正数。整数的范围是 -999 999 999 999 999 999 到 +999 999 999 999 999 999。
示例下表列出了正、负十进制数 1234 的十六进制存储字节,排列顺序从最低有效字节到最高有效字节:
十进制数值 | 存储字节 |
---|---|
+1234 | 34 12 00 00 00 00 00 00 00 00 |
-1234 | 34 12 00 00 00 00 00 00 00 80 |
MASM 使用 TBYTE 伪指令来定义压缩 BCD 变量。常数初始值必须是十六进制的,因为,汇编器不会自动将十进制初始值转换为 BCD 码。下面的两个例子展示了十进制 数 -1234 有效和无效的表达方式:
intVal TBYTE 800000000000001234h ;有效
intVal TBYTE -1234 ;无效
第二个例子无效的原因是 MASM 将常数编码为二进制整数,而不是压缩 BCD 整数。
如果想要把一个实数编码为压缩 BCD 码,可以先用 FLD 指令将该实数加载到浮点寄存器堆栈,再用 FBSTP 指令将其转换为压缩 BCD 码,该指令会把数值舍入到最接近的整数:
.data
posVal REAL8 1.5
bcdVal TBYTE ?
.code
fid posVal ;加载到浮点堆栈
fbstp bcdVal ;向上舍入到 2,压缩 BCD 码值
如果 posVal 等于 1.5,结果 BCD 值就是 2。
定义浮点类型
REAL4 定义 4 字节单精度浮点变量。REAL8 定义 8 字节双精度数值,REAL10 定义 10 字节扩展精度数值。每个伪指令都需要一个或多个实常数初始值:
rVal1 REAL4 -1.2
rVal2 REAL8 3.2E-260
rVal3 REAL10 4.6E+4096
ShortArray REAL4 20 DUP(0.0)
下表描述了标准实类型的最少有效数字个数和近似范围:
数据类型 | 有效数字 | 近似范围 |
---|---|---|
短实数 | 6 | 1.18x 10-38 to 3.40 x 1038 |
长实数 | 15 | 2.23 x 10-308 to 1.79 x 10308 |
扩展精度实数 | 19 | 3.37 x 10-4932 to 1.18 x 104932 |
DD、DQ 和 DT 伪指令也可以定义实数:
rVal1 DD -1.2 ;短实数
rVal2 DQ 3.2E-260 ;长实数
rVal3 DT 4.6E+4096 ;扩展精度实数
MASM 汇编器包含了诸如 wal4 和 real8 的数据类型,这些类型表明数值是实数。更准确地说,这些数值是浮点数,其精度和范围都是有限的。从数学的角度来看,实数的精度和大小是无限的。
变量加法程序
到目前为止,本节的示例程序实现了存储在寄存器中的整数加法。现在已经对如何定义数据有了一些了解,那么可以对同样的程序进行修改,使之实现三个整数变量相加,并将和数存放到第四个变量中。
;AddTowSum.asm
.386
.model flat,stdcall
.stack 4096
ExitProcess PROTO, dwExitCode:DWORD
.data
firstval DWORD 20002000h
secondval DWORD 11111111h
thirdval DWORD 22222222h
sum DWORD 0
.code
main PROC
mov eax,firstval
add eax,secondval
add eax,thirdval
mov sum,eax
INVOKE ExitProcess,0
main ENDP
END main
注意,已经用非零数值对三个变量进行了初始化(9〜11 行)。16〜18 行进行变量相加。x86 指令集不允许将一个变量直接与另一个变量相加,但是允许一个变量与一个寄存器相加。这就是为什么 16〜17 行用 EAX 作累加器的原因:
mov eax,firstval
add eax,secondval
第 17 行之后,EAX 中包含了 firstval 和 secondval 之和。接着,第 18 行把 thirdval 加到 EAX 中的和数上:
add eax,thirdval
最后,在第 19 行,和数被复制到名称为 sum 的变量中:
mov sum,eax
作为练习,鼓励大家在调试会话中运行本程序,并在每条指令执行后检查每个寄存器。最终和数应为十六进制的 53335333。
在调试会话过程中,如果想要变量显示为十六进制,则按下述步骤操作:鼠标在变量或寄存器上悬停 1 秒,直到一个灰色矩形框出现在鼠标下。右键点击该矩形框,在弹出菜单中选择 Hexadecimal Display。
小端顺序
x86 处理器在内存中按小端(little-endian)顺序(低到高)存放和检索数据。最低有效字节存放在分配给该数据的第一个内存地址中,剩余字节存放在随后的连续内存位置中。考虑一个双字 12345678h。如果将其存放在偏移量为 0000 的位置,则 78h 存放在第一个字节,56h 存放在第二个字节,余下的字节存放地址偏移量为 0002 和 0003,如下图所示。
其他有些计算机系统采用的是大端顺序(高到低)。 下图展示了 12345678h 从偏移量 0000 开始的大端顺序存放。
声明未初始化数据
.DATA ? 伪指令声明未初始化数据。当定义大量未初始化数据时,.DATA ? 伪指令减少了编译程序的大小。例如,下述代码是有效声明:
.data
smallArray DWORD 10 DUP (0) ;40 个字节
.data?
bigArray DWORD 5000 DUP ( ? ) ;20 000 个字节,未初始化
而另一方面,下述代码生成的编译程序将会多岀 20 000 个字节:
.data
smallArray DWORD 10 DUP ( 0 ) ; 40 个字节
bigArray DWORD 5000 DUP ( ? ) ; 20 000 个字节
代码与数据混合汇编器允许在程序中进行代码和数据的来回切换。比如,想要声明一个变量,使其只能在程序的局部区域中使用。下述示例在两个代码语句之间插入了一个名为 temp 的变量:
.code
mov eax,ebx
.data
temp DWORD ?
.code
mov temp,eax
尽管 temp 声明的出现打断了可执行指令流,MASM 还是会把 temp 放在数据段中,并与保持编译的代码段分隔开。然而同时,混用 .code 和 .data 伪指令会使得程序变得难以阅读。
10.汇编语言等号=伪指令
等号伪指令(equal-sign directive)把一个符号名称与一个整数表达式连接起来,其语法如下:
name = expression
通常,表达式是一个 32 位的整数值。当程序进行汇编时,在汇编器预处理阶段,所有出现的 name 都会被替换为 expression。假设下面的语句出现在一个源代码文件开始的位置:
COUNT = 500
然后,假设在其后 10 行的位置有如下语句:
mov eax, COUNT
那么,当汇编文件时,MASM 将扫描这个源文件,并生成相应的代码行:
mov eax, 500
为什么使用符号?
程序员可以完全跳过 COUNT 符号,简化为直接用常量 500 来编写 MOV 指令,但是经验表明,如果使用符号将会让程序更加容易阅读和维护。
设想,如果 COUNT 在整个程序中出现多次,那么,在之后的时间里,程序员就能方便地重新定义它的值:
COUNT = 600
在该程序的后面,如果语句使用这个符号而不是整数常量,那么它会具有更强的自描述性。
使用
mov al,Esc_key ;好的编程风格
而非
mov al,27 ;不好的编程风格
使用DUP操作符
《数据定义》一节说明了怎样使用 DUP 操作符来存储数组和字符串。为了简化程序的维护,DUP 使用的计数器应该是符号计数器。
在下例中,如果已经定义了 COUNT,那么它就可以用于下面的数据定义中:
array dword COUNT DUP(0)
重定义
用“=”定义的符号,在同一程序内可以被重新定义。下例展示了当 COUNT 改变数值后,汇编器如何计算它的值:
COUNT = 5
mov al,COUNT ; AL = 5
COUNT = 10
mov al,COUNT ; AL = 10
COUNT = 100
mov al,COUNT ; AL = 100
符号值的改变,例如 COUNT,不会影响语句在运行时的执行顺序。相反,在汇编器预处理阶段,符号会根据汇编器对源代码处理的顺序来改变数值。
11.汇编语言计算数组和字符串长度
list BYTE 10,20,30,40
ListSize = 4
显式声明数组的大小会导致编程错误,尤其是如果后续还会插入或删除数组元素。声明数组大小更好的方法是,让汇编器来计算这个值。
$ 运算符(当前地址计数器)返回当前程序语句的偏移量。在下例中,从当前地址计数器($)中减去 list 的偏移量,计算得到 ListSize:
list BYTE 10,20,30,40
ListSize = ($ - list)
ListSize 必须紧跟在 list 的后面。下面的例子中,计算得到的 ListSize 值(24)就过大,原因是 var2 使用的存储空间,影响了当前地址计数器与 list 偏移量之间的距离:
list BYTE 10,20,30,40
var2 BYTE 20 DUP(?)
ListSize = ($ - list)
不要手动计算字符串的长度,让汇编器完成这个工作:
myString BYTE "This is a long string, containing"
BYTE "any number of characters"
myString_len = ($ - myString)
字数组和双字数组
当要计算元素数量的数组中包含的不是字节时,就应该用数组总的大小(按字节计)除以单个元素的大小。比如,在下例中,由于数组中的每个字要占 2 个字节(16 位),因此,地址范围应该除以 2:
list WORD 1000h,2000h,3000h,4000h
ListSize = ($ - list) / 2
同样,双字数组中每个元素长 4 个字节,因此,其总长度除以 4 才能产生数组元素的个数:
list DWORD l0000000h,20000000h,30000000h,40000000h
ListSize = ($ -list) / 4
12.汇编语言EQU伪指令
EQU 伪指令把一个符号名称与一个整数表达式或一个任意文本连接起来,它有 3 种格式:
name EQU expression
name EQU symbol
name EQU
第一种格式中,expression 必须是一个有效整数表达式。第二种格式中,symbol 是一个已存在的符号名称,已经用 = 或 EQU 定义过了。第三种格式中,任何文本都可以岀现在<…>内。当汇编器在程序后面遇到 name 时,它就用整数值或文本来代替符号。
在定义非整数值时,EQU 非常有用。比如,可以使用 EQU 定义实数常量:
PI EQU <3.1416>
【示例 1】下面的例子将一个符号与一个字符串连接起来,然后用该符号定义一个变量:
pressKey EQU <"Press any key to continue...", 0>
.data
prompt BYTE pressKey
【示例 2】假设想定义一个符号来计算一个 10 x 10 整数矩阵的元素个数。现在用两种不同的方法来进行符号定义,一种用整数表达式,一种用文本。然后把两个符号都用于数据定义:
matrix1 EQU 10 * 10
matrix2 EQU <10 * 10>
.data
M1 WORD matrix1
M2 WORD matrix2
汇编器将为 M1 和 M2 生成不同的数据定义。计算 matrix1 中的整数表达式,并将其赋给M1。而 matrix2 中的文本则直接复制到 M2 的数据定义中:
M1 WORD 100
M2 WORD 10 * 10
与 = 伪指令不同,在同一源代码文件中,用 EQU 定义的符号不能被重新定义。这个限制可以防止现有符号在无意中被赋予新值。
12.汇编语言TEXTEQU伪指令
TEXTEQU 伪指令,类似于 EQU,创建了文本宏(text macro)。它有 3 种格式:第一种为名称分配的是文本;第二种分配的是已有文本宏的内容;第三种分配的是整数常量表达式:
name TEXTEQU
name TEXTEQU textmacro
name TEXTEQU %constExpr
例如,变量 prompt1 使用了文本宏 continueMsg:
continueMsg TEXTEQU <"Do you wish to continue (Y/N)?">
.data
prompt1 BYTE continueMsg
文本宏可以相互构建。如下例所示,count 被赋值了一个整数表达式,其中包含 rowSize。然后,符号 move 被定义为 mov。最后,用 move 和 count 创建 setupAL:
rowSize = 5
count TEXTEQU %(rowSize * 2)
move TEXTEQU
setupAL TEXTEQU
因此,语句
setupAL
就会被汇编为
mov al,10
用 TEXTEQU 定义的符号随时可以被重新定义。
13.汇编语言64位编程
AMD 和 Intel 64 位处理器的出现增加了对 64 位编程的兴趣。MASM 支持 64 位代码,所有的 Visual Studio 2012 版本(最终版、高级版和专业版)以及桌面系统的 Visual Studio 2012 Express 都会同步安装 64 位版本的汇编器。
现在借助《数据定义》一节中给出的 AddTwoSum 程序,将其改为 64 位编程:
;AddTowSum_64.asm
ExitProcess PROTO
.data
sum DWORD 0
.code
main PROC
mov eax,5
add eax,6
mov sum,eax
mov eax,0
call ExitProcess
main ENDP
END
上述程序与之前给出的 32 位版本不同之处如下所示:
1) 32 位 AddTwoSum 程序中使用了下列三行代码,而 64 位版本中则没有:
.386
.model flat,stdcall
.stack 4096
2) 64 位程序中,使用 PROTO 关键字的语句不带参数,如第 3 行代码所示:
ExitProcess PROTO
32 位版本代码如下:
ExitProcess PROTO,dwExitCode:DWORD
3) 14〜15 行使用了两条指令(mov 和 call)来结束程序。32 位版本则只使用了一条 INVOKE 语句实现同样的功能。64 位 MASM 不支持 INVOKE 伪指令。
4) 在第 17 行,END 伪指令没有指定程序入口点,而 32 位程序则指定了。
使用 64 位寄存器
在某些应用中,可能需要实现超过 32 位的整数的算术运算。在这种情况下,可以使用 64 位寄存器和变量。例如,下述步骤让示例程序能使用 64 位数值:
- 在第 6 行,定义 sum 变量时,把 DWORD 修改为 QWORD。
- 在 10〜12 行,把 EAX 替换为其 64 位版本 RAX。
下面是修改后的 6〜12 行:
sum DWORD 0
.code
main PROC
mov rax,5
add rax,6
mov sum,rax
编写 32 位还是 64 位汇编程序,很大程度上是个人喜好的问题。但是,需要记住:64 位 MASM 11.0 (Visual Studio 2012 附带的)不支持 INVOKE 伪指令。同时,为了运行 64 位程序,必须使用 64 位Windows。
Comments NOTHING