实验1 Windows汇编语言开发环境
Windows、Linux等现代操作系统都运行于CPU的保护模式下。学习保护模式的汇编语言编程,要选用合适的编译、调试工具,编译工具决定了汇编程序的语法、结构,而调试工具则能够帮助我们迅速查找程序中的错误,提高调试效率。
本实验指导书采用Microsoft公司的MASM 6.14作为编译工具,Microsoft Visual C/C++作为开发调试环境。
1.1 汇编程序结构
和其他语言一样,汇编语言的源程序也要符合一定的格式,才能被编译程序所识别和处理。学习和掌握这些格式,是进行汇编编程的第一步。
1.1.1
一个显示字符串的汇编程序
下面是一个简单的汇编程序。
;程序清单:test.asm(在控制台上显示一个字符串)
.386
.model flat, stdcall
option casemap:none
; 说明程序中用到的库、函数原型和常量
includelib msvcrt.lib
printf PROTO C :ptr sbyte, :vararg
; 数据区
.data
szMsg byte “Hello World!”, 0ah, 0
; 代码区
.code
start:
mov eax, OFFSET szMsg
invoke printf, eax
ret
end start
1.1.2
程序格式
在源程序test.asm中,以分号(;)开始的行是注释行。注释行对程序的编译和执行没有影响。
一、模式定义
程序的第一部分是有关模式定义的3条语句:
.386
.model flat, stdcall
option casemap:none
这些语句定义了程序使用的指令集、工作模式。
(1) 指令集
.386语句是汇编语言的伪指令,说明本程序使用的指令集是哪一种CPU的。还可以使用:.8086、.186、.286、.386、.386p、.486、.486p、.586、.586p等。
后面带p的伪指令则表示程序中可以使用特权指令。
(2) 工作模式
.model语句用来定义程序工作的模式,它的格式是:
.model 内存模式[, 调用规则][, 其他模式]
内存模式的定义影响最后生成的可执行文件,可执行文件的规模可以有很多种类型,在Windows环境下,内存模式为flat,可执行文件最大可以用4 GB内存。
在test.asm中,.model语句指明使用stdcall调用规则。调用规则就是子程序的调用方式,即调用子程序时参数传递的次序和堆栈平衡的方法。
(3). option语句
option语句有许多选项,这里介绍一种:
option casemap:none
这条语句说明程序中的变量和子程序名是否对大小写敏感。对大小写敏感表示区分大写、小写形式,例如变量XYZ和xyz是两个不同的变量。对大小写不敏感则不区分大写、小写形式,变量XYZ和xyz是同一个变量。
由于Windows API函数中的函数名称是区分大小写的,所以应该指定这个选项“casemap:none”,否则在调用函数的时候会出现问题。
二、includelib语句
和C程序一样,在汇编程序中也需要调用一些外部模块(子程序/函数)来完成部分功能。例如,在hello.asm中,就需要调用printf函数将字符串显示在屏幕上。
printf函数属于C语言的库函数。它的执行代码放在一个动态链接库DLL(dynamic-load library)中,这个动态库的名字叫msvcrt.dll。
在汇编源程序中,需要用includelib语句指出库文件的名称,链接时LINK就从库文件中找出了函数的位置,避免出现上面的错误提示。这种库文件也叫导入库(import library)。例如:
includelib msvcrt.lib
一个DLL文件对应一个导入库,如msvcrt.dll的导入库是msvcrt.lib;kernel32.dll的导入库是kernel32.lib;user32.dll的导入库是user32.lib等。导入库文件在Visual C/C++的库文件目录中,在链接生成可执行文件时使用。
可执行文件执行时,只需要DLL文件,不需要导入库。
三、函数声明语句
对于所有要用到的库函数(或Windows API函数),在程序的开始部分必须预先声明。包括函数的名称、参数的类型等,如:
在汇编语言程序中,函数声明为:
函数名称 PROTO [调用规则] :[第一个参数类型] [,:后续参数类型]
其中,PROTO后的调用规则是可选项。如果不写,则使用model语句中指定的调用规则。
如果函数使用C调用规则,则PROTO后跟一个C。接下来是参数的说明。如果参数个数、类型不定,则用VARARG说明(varible argument)。
先看在C语言头文件stdio.h中printf的函数声明:
_CRTIMP int __cdecl printf(const char *, ...);
可知printf函数的调用规则为C调用规则(__cdecl, 即c declare),第一个参数是字符串指针,后面的参数数量及类型不定。
这里,用ptr sbyte代表const char *。
printf PROTO C :ptr sbyte,:vararg
四、数据和代码部分
程序中的数据部分和代码部分是分开定义的,数据部分从这一行开始:
.data
代码部分从这一行开始:
.code
遇到end语句时,代码部分结束。
end语句一般是整个程序的最后一条语句。end语句后面跟的是起始标号。它指出了程序执行的第一条指令的位置。在例子中,使用start作为起始标号,程序从start处开始执行。注意,程序并不一定要从代码部分的第一行开始执行。例如,start前面可以写一些子程序等。
end 起始标号
如果要定义堆栈部分,可以使用堆栈定义语句:
.stack [堆栈大小]
1.2 Visual C/C++环境
Microsoft Visual C/C++(简称VC)是一个典型的集成开发环境(IDE,integrated development environment),在国内外十分流行。集成开发环境大大地提高了程序开发过程的效率,而且它还能够动态地调试程序。除了可以编写调试C/C++程序外,VC还可以用来编辑、修改、编译、调试汇编程序。本书使用的版本是Microsoft Visual C/C++ 6.0。
1.2.1
建立工程
首先,按照以下步骤建立一个能编译、调试汇编程序的工程:
(1) 启动VC后,从菜单中选择“File”→“New”。
(2) 如图1-1所示,在打开的“New”对话框顶部,单击“Projects”,再选中“Win32 Console Application”。在Location编辑框中输入“c:/asm”,再在“Project name”中输入“test”。输入“test”时,它自动地添加到Location编辑框中“c:/asm”的后面。
图1-1 建立汇编程序工程之一
(3) 单击“OK”键后,出现一个新的对话框,单击“Finish”。
(4) 接下来,VC的窗口的左边显示出“test classes”,下面有“ClassView”和“FileView”两种视图,如图1-2所示。
(5) 这时,可将hellow.asm(或其他的一个.asm源程序文件)复制到c:/asm/test中,并改名为test.asm;也可以将其他的汇编程序源文件复制到c:/asm/test/test.asm。
图1-2 建立汇编程序工程之二
(6) 接下来, 再从菜单中选择“Project”→“Add to Projects”→“Files”,在该对话框中的文件名处输入“c:/asm/test/test.asm”,如图1-3所示。
图1-3 建立汇编程序工程之三
(7) 在VC窗口左边的视图中,展开“FileView”中的“Source Files”,显示出“test.asm”。在“test.asm”上,单击鼠标右键,出现如图1-4所示的菜单。
图1-4 建立汇编程序工程之四
(8) 在菜单中选择“Setting”。弹出另一个对话框,如图1-5所示。在“Commands”编辑框中输入“ml /c /coff /Zi test.asm”,在“Outputs”编辑框中输入“test.obj”。再单击“OK”。
图1-5 建立汇编程序工程之五
(9) 最后,再将“ML.EXE”和“ML.ERR”两个文件复制到“c:/windows”。如果Windows安装到其他目录,则需要把这两个文件复制到相应的目录。可用“set windir”命令显示出Windows的安装目录。
(10)最后,验证是否能在VC中编译test.asm。在VC中按F7键,应该自动编译生成test.exe。如果源程序中有错误,编译后将错误信息显示在“Output”的“Build”视图中。点击该错误信息,光标自动定位到出现错误的程序行(也可以按F4键定位到错误的程序行)。
为了使VC适合于汇编语言的调试,可对它进行如下设置,如图1-6所示。
(11)从“Tools”菜单中选择“Options…”,再选择“Debug”页,选中“Disassembly window”中的“Code bytes”(前面打上对勾)。
(12)在“Memory window”中,选中“Fixed width”,在后面填入数字16。
(13)在“General”中,选中“Hexdecimal display”。
(14)不选“View floating point registers”。
图1-6 VC的调试设置选项
程序编译成功后,按Ctrl+F5可以运行已编译好的程序。
1.2.2
汇编程序的调试
一、设置断点
如果程序运行的结果不正确,可以在VC中调试。单击“FileView”视图中的test.asm,这个源程序就会自动地进入VC的编辑窗口。
将光标移动到程序入口所在的程序行上,按F9键。就在该行设置了一个断点。断点的程序行前有一个红色的小圆点,如图1-7所示。
按F5键在Debug状态下执行程序,或者从菜单中选择“Build”→“Start Debug”→“Go”。
这时,当前窗口显示出程序中的指令序列,有一个黄色箭头,它就是程序要执行的下一条指令,如图1-8所示。
二、内存窗口
从菜单中选择“View”→“Debug Windows”→“Memory”,打开内存窗口,在地址“Address:”后面的编辑框中可以输入内存变量的名称,这里输入szTitle。内存窗口中就显示出该变量所在内存单元的值。前面的部分是以十六进制的形式显示出来的,后面是以ASCII字符的形式显示出来的。
在内存窗口上单击鼠标右键,可以选择:按字节、字、双字显示内存单元的值。
三、寄存器窗口
从菜单中选择“View”→“Debug Windows”→“Registers”,打开寄存器窗口。在寄存器窗口中,显示了各个32位寄存器和段寄存器的值。
在调试程序时,如果某一个寄存器或内存单元的值被改变,则它的值用红色显示出来。
在寄存器窗口中的最后一行,显示的内存单元就是当前指令要读或写的操作数。
EFLAGS状态寄存器的值是按位显示的。但是,VC并没有使用我们所熟悉的OF、DF、IF、SF、ZF、AF、PF、CF名称,而是用它自己的一套名称,如表1-1所示。
表1-1 VC中的EFLAGS标志位
VC格式 |
OV |
UP |
EI |
PL |
ZR |
AC |
PE |
CY |
FLAGS位 |
OF |
DF |
IF |
SF |
ZF |
AF |
PF |
CF |
含义 |
溢出 |
方向 |
中断允许 |
符号 |
为零 |
辅助进位 |
奇偶 |
进位 |
例如UP=0表示DF=0。
四、监视窗口
从菜单中选择“View”→“Debug Windows”→“Watch”,打开监视窗口。在“Name”一栏下面,可以输入想要监视的变量或寄存器名称。监视窗口会随时将这些变量的值显示出来。
要在调试过程中改动寄存器或内存变量的值,可以在Watch窗口的该寄存器或变量的内容(在Value列)用鼠标左键单击,修改其值后,按回车键即可。
也可以在内存窗口中修改变量的值。在要修改的内存单元上点击,直接输入新的内容即可。
另外一种方法是按Shift+F9。弹出对话框后,在“Expression”处输入寄存器或内存变量的名称,再在下面的Value一列处修改其内容。最后,按“OK”。
图1-7 编辑、编译汇编源程序并设置断点
图1-8中为打开内存窗口、监视窗口和寄存器窗口后的屏幕显示。
调试过程中,编辑窗口中显示出汇编源程序。如果要查看程序的实际执行代码,从菜单中选择“View”→“Debug Windows”→“Disassmebly”。在运行过程中,实际上运行的是机器代码,而不是汇编源程序。机器代码及其反汇编的指令和源程序混合显示在编辑窗口中。反汇编中的程序地址和指令中的数据都是用十六进制显示的。在调试过程中,使用十六进制来表示地址和(变量或寄存器的)数值更方便。
按F10键可一步一步地执行程序。执行过程中,可以在内存窗口中观察变量的变化;在寄存器窗口中可以看到寄存器的变化;更加方便的是,可以把鼠标移动到编辑窗口中的寄存器或变量上,停留几秒钟后,VC会自动地显示它们的值。
按Shift+F5键,可结束调试。
图1-8 VC调试环境:编辑窗口、内存窗口、监视窗口和寄存器窗口
1.2.3
常用调试命令
常用的调试命令如表1-2所示。
表1-2 VC的常用调试命令
功能键 |
作 用 |
描 述 |
F11 |
单步执行 |
Step Into |
F10 |
执行 |
Step Over |
Ctrl+F10 |
执行到当前光标的位置的指令 |
Run to Cursor |
F9 |
在当前光标的位置的指令上设置/清除断点 |
Set/Clear Breakpoint at Cursor |
F5 |
执行程序 |
Go |
Shift+F5 |
终止程序,退出程序 |
Stop Debugging |
设置当前指令 |
将光标处的指令设为当前指令 |
Set Next Statement |
Shift+F11 |
当前子程序执行结束 |
Step Out |
l F11:单步执行当前指令。当前指令在反汇编窗口中用一个黄色箭头指示,CS:EIP指向当前指令。按F11键后,当前指令执行,黄色箭头和EIP随之变化,指向新的当前指令。
l F10:执行当前进程指令。F10和F11在执行一般指令时没有区别。在当前指令是一条CALL、INT指令的情况下有所区别。如果当前指令是CALL指令,按F11后,进入到子程序的第一条指令,子程序执行前就进入调试状态,可调试子程序的执行过程;按F10后,子程序执行完毕后才回到调试状态,不需要调试子程序的执行过程。
l Ctrl+F10:先把光标移动到一条指令上,可以用键盘上的上、下箭头移动光标,或者在某一行上点击。再按Ctrl+F10,程序就从当前指令处开始执行,一直到光标处的指令再停下来。
l F9:先把光标移动到一条指令上,按F9,就在该指令上设置了一个断点。再按F9,这个断点就清除了。设置断点后,指令的前面标有一个红色的圆点。程序运行到断点时,会停下来,这时就可以检查各个变量、寄存器的内容以及程序的执行流程是否正确,以查找程序中的错误。
l F5:从当前指令开始执行程序,直到遇到断点或程序结束时为止。
l Shift+F5:终止程序,不再执行后面的程序。终止后,可以再按F11键(或Ctrl+Shift+F5)重新开始调试过程。
l 设置当前指令:在调试时,可能希望跳过一部分程序不执行,也可能想将已执行过的一段程序再执行一遍。这可以通过改变当前指令来实现。在新的当前指令上按下鼠标右键,弹出一个菜单,在其中选择“Set Next Statement”。这时,黄色箭头就指到新设置的当前指令上。
l Shift+F11:先按住Shift键,再按下F11。当前指令在子程序中时,如果想使整个子程序执行完毕,返回到主程序,则使用Shift+F11。
某些功能也可以从“Debug”菜单中选择。如图1-9所示。
图1-9 VC的部分Debug菜单项
1.3 字符串输入、输出
在C语言中,常用printf、scanf、sprintf等函数来实现字符串的的输入输出,在汇编语言中,可以调用这些函数。
1. printf
在前面的程序例子中已经用到过printf。在程序中,要指明printf的调用规则,以及它的参数类型。
printf PROTO C :dword,:vararg
printf使用C调用规则(参数自右至左入栈,由主程序平衡堆栈)。第1个参数是一个双字(:dword),即字符串的地址,后面的其他参数个数可变,可以1个没有,也可以跟多个参数。
以下C语句输出3个整数A、B、R和一个字符Op:
printf ("%d %c %d = %d/n" , A, Op, B, R);
在汇编语言中,在数据区中要定义szOutputFmtStr:
szOutputFmtStr byte '%d %c %d = %d', 0ah, 0
使用invoke语句调用printf。Printf后面跟的第1个参数是格式字符串的地址;第2、3、4个参数分别是要输出的整数。
invoke printf, offset szOutputFmtStr, A, Op, B, R
在程序中,还需要包括以下语句,指示链接程序在msvcrt.lib库文件寻找链接信息。
includelib msvcrt.lib
2. sprintf
sprintf与printf相似,它将输出保存在第1个字符串szStr中。
invoke sprintf, offset szStr, offset szOutputFmtStr, A, Op, B, R
3. scanf
scanf是从控制台将用户的输入读入到程序的变量中,变量的类型可以是整数、字符、字符串等。
scanf的调用规则和参数类型说明为:
scanf PROTO C :dword,:vararg
scanf的链接信息也包括在msvcrt.lib库文件中。
程序中需要输入A、Op、B时,A、B是整数,OP是字符。它的第1个参数是格式字符串的地址;第2、3、4个参数分别是A、Op、B的地址。
szInputFmtStr byte '%d %c %d', 0
invoke scanf,offset szInputFmtStr,offset A,offset Op,offset B
其效果等价于:
scanf("%d %c %d", &A, &Op, &B);
下面的程序首先调用printf显示字符串,提示用户输入要计算的表达式,再调用scanf接收用户的输入。根据输入的运算符Op,通过条件跳转指令实现对加、减、乘、除的判断和处理。最后,调用printf输出计算结果。其执行结果为:
input (a Op b, Op=+-/*, ex: 28-2): 4*5
4 * 5 = 20
;程序清单:equation.asm(四则运算)
.386
.model flat,stdcall
Option casemap:none
includelib msvcrt.lib
scanf PROTO C :dword,:vararg
printf PROTO C :dword,:vararg
.data
szInPmt byte 'input (a Op b, Op=+-/*, ex: 28-2): ', 0 ;
szInputFmtStr byte '%d %c %d', 0
szOutputFmtStr byte '%d %c %d = %d', 0ah, 0
szErrMsg byte 'invalid Operation. ', 0ah, 0
A sdword ?
B sdword ?
Op dword ?
R sdword ?
.code
start:
invoke printf, offset szInPmt
invoke scanf, offset szInputFmtStr,
offset A,
offset Op,
offset B
mov eax, A
mov ebx, B
cmp Op, '+'
jz lab_add
cmp Op, '-'
jz lab_sub
cmp Op, '*'
jz lab_mul
cmp Op, '/'
jz lab_div
lab_err:
invoke printf, offset szErrMsg
jmp lab_exit
lab_add:
add eax, ebx
mov R, eax
jmp lab_output
lab_sub:
sub eax, ebx
mov R, eax
jmp lab_output
lab_mul:
imul eax, ebx
mov R, eax
jmp lab_output
lab_div:
cdq
idiv ebx
mov R, eax
jmp lab_output
lab_output:
invoke printf, offset szOutputFmtStr, A, Op, B, R
lab_exit:
ret
end start
equation.asm在C:/asm/sample/chap-01目录中,编译连接的步骤为:
(1) 进入C:/asm/sample/chap-01目录
cd C:/asm/sample/chap-01
(2) 设置编译环境
c:/asm/bin/asmvars.bat
(3) 编译连接
ml /coff equation.asm /link /subsystem:console
1.4 常用Windows API调用
API是application programming interface的缩写,代表应用程序编程接口。API一般使用stdcall调用规则。
1. MessageBox
printf和scanf适用于控制台程序(链接选项为/subsystem:console),而带窗口的Windows程序(链接选项为/subsystem:windows)不能使用printf和scanf。这里介绍一个输出信息用的API-MessageBox。
它的调用规则和参数类型说明为:
MessageBoxA PROTO :DWORD,:DWORD,:DWORD,:DWORD
MessageBoxA的链接信息包括在user32.lib库文件中。
MessageBoxA的C语言原型在VC附带的"winuser.h"中提供。
2. 确定函数的声明语句和库文件
在编程中,应尽可能地利用已有的C的库函数和Windows API函数,以减少编程的工作量。
通过查阅在MSDN(或VC等工具)资料和帮助文件、阅读示例程序等方法,弄清函数的功能以及入口、出口参数,以及每一个参数的用法。根据函数的名称、参数的个数、类型、调用规则等,写出这样的声明语句:
printf PROTO C :dword,:vararg
再确定它属于哪一个库文件。常用的库文件有:msvcrt.lib、kernel32.lib、user32.lib等。
函数可能返回的是一个整数、指针或其他类型。无论如何,返回值都在EAX中。要注意有些函数是通过传递地址指针的方式来改变参数的值,如scanf。
1.5 读取CPU标识
CPUID指令是获得CPU信息的汇编指令,它可以返回CPU类型、型号、制造商信息、商标信息、序列号、缓存等CPU相关信息。
CPUID指令使用eax作为输入参数,eax、ebx、ecx、edx作为输出参数。例如:
mov eax, 0
cpuid
执行结果为:EAX = 00000002 EBX = 756E6547 ECX =
使用eax=3作为输入参数时,在ECX、EDX中返回CPU序列号的第0~31位、第32~63位。
mov eax, 0
cpuid
;程序清单:cpuid.asm(读取CPU标识)
.586
.model flat,stdcall
Option casemap:none
includelib msvcrt.lib
printf PROTO C :dword,:vararg
.data
szVendorID byte 13 dup (0)
szFormatStr byte 'VendorID = %s; Processor SN = %08X%08X', 0ah
.code
start:
mov eax, 0
cpuid
mov dword ptr szVendorID, ebx
mov dword ptr szVendorID+4, edx
mov dword ptr szVendorID+8, ecx
mov eax, 3
cpuid
invoke printf, offset szFormatStr,
offset szVendorID, ecx, edx
ret
end start
程序运行结果如下所示:
VendorID = GenuineIntel; Processor SN = 00000000007B7040
1.6 WinDbg调试工具
WinDbg是微软公司发布的免费源码级调试工具。Windbg可以用于Kernel模式调试和用户模式调试,还可以调试Dump文件。
使用WinDbg调试汇编程序的主要步骤为:
1.首先,使用下面的命令编译、链接,产生.EXE文件。/coff选项要求MASM生成链接器所需要的COFF格式的.obj文件,/Zi则指定编译生成的目标文件中含有调试信息,/link表示要生成.EXE文件,而/subsystem:console则表示生成控制台程序。
ml /coff /Zi cpuid.asm /link /subsystem:console
2.启动WinDbg,从菜单File→Open Executable装入.EXE文件。
3.在WinDbg命令窗口的最下方,可输入命令,按回车键执行。如:
x cpuid!
在窗口内显示出cpuid.exe内的所有数据变量:
0:000> x cpuid!
*** WARNING: Unable to verify checksum for cpuid.exe
00404000 cpuid!szVendorID = 0x00 ''
0040400d cpuid!szFormatStr = 0x56 'V'
4.输入u命令,可以查看汇编指令:
0:000> u start l e
cpuid!start [cpuid.asm @ 11]:
00401010 b800000000 mov eax,0
00401015 0fa2 cpuid
00401017 891d00404000 mov dword ptr [cpuid!szVendorID (00404000)],ebx
0040101d 891504404000 mov dword ptr [cpuid!szVendorID+0x4 (00404004)],edx
00401023 890d08404000 mov dword ptr [cpuid!szVendorID+0x8 (00404008)],ecx
00401029 b803000000 mov eax,3
0040102e 0fa2 cpuid
00401030 52 push edx
00401031 51 push ecx
00401032 6800404000 push offset cpuid!szVendorID (00404000)
00401037 680d404000 push offset cpuid!szFormatStr (0040400d)
00401041
5.输入bp命令,可以设置断点,例如:
bp start
6.从菜单中选择Debug→Go,或者按F5键,可以执行到断点处。
7.从菜单中选择View→Disassembly,可以看到断点处的指令以高亮方式显示。
8.从菜单中选择View→Register,打开一个窗口,显示寄存器的当前值。
9.在WinDbg命令窗口输入r命令,也可以显示寄存器的当前值。还可以修改寄存器的值,如“r eax=
10.在WinDbg命令窗口输入输入d命令,可以查看内存内容,如:“d szVendorID L c”。将d换为db、dw、dd,则指定用字节、字、双字的格式查看。L后面跟的数字则表示查看的单元个数。
11.按F11、F10、Shift+F11、Ctrl+F10分别表示单步执行当前指令、执行完毕当前指令、执行完当前函数、执行到光标处,其用法与表1-2相同。
1.7 实验题:用MessageBox函数显示CPU信息
cpuid.asm采用的是控制台模式(链接选项为/subsystem:console),调用printf输出信息。
要求:
1. 使用VC 6.0建立工程文件;
2. 采用带窗口的Windows程序(链接选项为/subsystem:windows);
3. 使用sprintf 、MessageBoxA函数;
4. 输出制造商信息、序列号;
5. 输出CPU商标信息,执行CPUID的输入参数为80000002H、80000003H、80000004H,具体格式可查阅CPU指令手册。
运行结果如图1-10所示:
图1-10 在对话框中显示CPUID执行结果