Programming 版 (精华区)
发信人: zhangyan (今朝有水今朝灌), 信区: Programming
标 题: Iczelion Win32汇编教程(2)
发信站: 哈工大紫丁香 (2001年02月14日16:06:06 星期三), 站内信件
第二课 消息框
----------------------------------------------------------------------------
----
在本课中,我们将用汇编语言写一个 Windows 程序,程序运行时将弹出一个消息框并显
示"Win32 assembly is great!"。
理论:
Windows 为编写应用程序提供了大量的资源。其中最重要的是Windows API (Applicati
on Programming Interface)。 Windows API是一大组功能强大的函数,它们本身驻扎在
Windows 中供人们随时调用。这些函数的大部分被包含在几个动态链接库(DLL)中,譬
如:kernel32.dll、 user32.dll 和 gdi32.dll。 Kernel32.dll中的函数主要处理内存
管理和进程调度;user32.dll中的函数主要控制用户界面;gdi32.dll中的函数则负责图
形方面的操作。除了上面主要的三个动态链接库,您还可以调用包含在其他动态链接库
中的函数,当然您必须要有关于这些函数的足够的资料。
动态链接库,顾名思义,这些 API 的代码本身并不包含在 Windows 可执行文件中,而
是当要使用时才被加载。为了让应用程序在运行时能找到这些函数,就必须事先把有关
的重定位信息嵌入到应用程序的可执行文件中。这些信息存在于引入库中,由链接器把
相关信息从引入库中找出插入到可执行文件中。您必须指定正确的引入库,因为只有正
确的引入库才会有正确的重定位信息。
当应用程序被加载时 Windows 会检查这些信息,这些信息包括动态链接库的名字和其中
被调用的函数的名字。若检查到这样的信息,Windows 就会加载相应的动态链接库,并
且重定位调用的函数语句的入口地址,以便在调用函数时控制权能转移到函数内部。
如果从和字符集的相关性来分,API 共有两类:一类是处理 ANSI 字符集的,另一类是
处理 UNICODE 字符集的。前一类函数名字的尾部带一个"A"字符,处理UNICODE的则带一
个"W"字符(我想"W"也许是代表宽字符的意思吧)。我们比较熟悉的ANSI字符串是以 NUL
L 结尾的一串字符数组,每一个ANSI字符是一个 BYTE 宽。对于欧洲语言体系,ANSI 字
符集已足够了,但对于有成千上万个唯一字符的几种东方语言体系来说就只有用 UNICO
DE 字符集了。每一个 UNICODE 字符占有两个 BYTE 宽,这样一来就可以在一个字符串
中使用 65336 个不同字符了。
这也是为什么引进 UNICODE 的原因。在大多数情况下我们都可以用一个包含头文件,在
其中定义一个宏,然后在实际调用函数时,函数名后不需要加后缀"A"或"W"。
<译者注:如在头文件中定义函数foo();
#ifdef UNICODE
#define foo() fooW()
#else
#define foo() fooA()
#endif
>
例子:
我先把框架程序放在下面,然后我们再向里面加东西。
.386
.model flat, stdcall
.data
.code
start:
end start
应用程序的执行是从 END 定义的标识符后的第一条语句开始的。在上面的框架程序中就
是从 START 开始。程序逐条语句执行一直到遇到 JMP,JNE,JE,RET 等跳转指令。这
些跳转指令将把执行权转移到其他语句上,若程序要退出 Windows,则必须调用函数 E
xitProcess。
ExitProcess proto uExitCode:DWORD
上面一行是函数原型。函数原型会告诉编译器和链接器该函数的属性,这样在编译和链
接时,编译器和链接器就会作相关的类型检查。 函数的原型定义如下:
FunctionName PROTO [ParameterName]:DataType,[ParameterName]:DataType,...
简言之,就是在函数名后加伪指令PROTO,再跟一串由逗号相隔的数据类型链表。在前面
的 ExitProcess 定义中,该函数有一个 DWORD 类型的参数。当您使用高层调用语句 I
NVOKE 时,使用函数原型定义特别有用,您可以简单地认为 INVOKE 是一个有参数类型
检查的调用语句。譬如,假设您这样写:
call ExitProcess
若您事先没把一个DWORD类型参数压入堆栈,编译器和链接器都不会报错,但毫无疑问,
在您的程序运行时将引起崩溃。但是,当您这样写:
invoke ExitProcess
连接器将报错提醒您忘记压入一个 DWORD 类型参数。所以我建议您用 INVOKE 指令而不
是CALL去调用一个函数。INVOKE 的语法如下:
INVOKE expression [,arguments]
expression 既可以是一个函数名也可以是一个函数指针。参数由逗号隔开。大多数API
函数的原型放在头文件中。 如果您用的是 hutch 的 MASM32,这些头文件在文件夹MAS
M32/include 下, 这些头文件的扩展名为 INC,函数名和 DLL 中的函数名相同,譬如
:KERNEL32.LIB 引出的函数 ExitProcess 的函数原形声明于kernel.inc中。您也可以
自己声明函数原型。 在我的教学课程中都使用 hutch 的windows。inc,这些头文件您
可以从http://win32asm.cjb.net下载。
好,我们现在回到ExitProcess 函数,参数uExitCode 是您希望当您的应用程序结束时
传递 Windows 的。 您可以这样写:
invoke ExitProcess,0
把这一行放到开始标识符下,这个应用程序就会立即退出 Windows,当然毫无疑问个应
用程序本身是一个完整的 Windows 程序。
.386
.model flat, stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
.data
.code
start:
invoke ExitProcess,0
end start
option casemap:none 一句的意思是告诉 MASM 要区分标号的大小写,譬如:start 和
START 是不同的。请注意新的伪指令 include,跟在其后的文件名所指定的文件在编译
时将“插”在该处。在我们上面的程序段中,当MASM处理到语句 include \masm\inclu
de\windows.inc 时,它就会打开文件夹\MASM32\include 中的文件windows.inc,这和
您把整个文件都粘贴到您的源程序中的效果是一样的。 hutch 的 windows.inc 包含了
WIN32 编程所需要的常量和结构体的定义。 但是它不包含函数原型的定义。尽管 hut
ch 和我尽力包含所有的常量和结构体的定义,但仍会有不少遗漏,为此我们将不断加入
新的内容。请随时注意我们主页,下载最新的头文件。
您的应用程序除了从 windows.inc 中得到相关变量结构体的定义外,还需要从其他的头
文件中得到函数原型的声明,这些头文件都放在 \masm32\include 文件夹中。 在我们
上面的例子中调用了驻扎在 kernel.dll 中的函数,所以需要包含有这个函数原型声明
的头文件 kernel.inc。如果用文本编辑器打开该文件您会发现里面全是从 kernel.dll
中引出的函数的声明。如果您不包含kernel.inc,您仍然可以调用(call)ExitProces
s,但不能够调用(invoke)ExitProcess(这会无法通过编译器和连接器的参数合法性
检查)。所以若用 invoke 去调用一个函数,您就必须事先声明,当然不一定要包含我
们的头文件,您完全可以在调用该函数前在源代码的适当位置进行声名。包含头文件主
要是为了节省时间(译者:当然还有正确性)
接下来我们来看看 includelib 伪指令,和 include 不同,它仅仅是告诉编译器您的程
序引用了哪个库。当编译器处理到该指令时会在生成的目标文件中插入链接命令告诉链
接器链入什么库。当然您还可以通过在链接器的命令行指定引入库名称的方法来达到和
用includelib指令相同的目的,但考虑到命令行仅能够传递128个字符而且要不厌其烦地
在命令行敲字符,所以这种方法是非常不可取的。
好了,现在保存例子,取名为msgbox.asm。把 ml.exe 的路径放到 PATH 环境变量中,
键入下面一行 进行编译:
ml /c /coff /Cp msgbox。asm (译者注:命令行参数大小写是有区别的)
/c 是告诉MASM只编译不链接。这主要是考虑到在链接前您可能还有其他工作要做。
/coff 告诉MASM产生的目标文件用 coff 格式。MASM 的 coff 格式是COFF(Common Ob
ject File Format:通用目标文件格式) 格式的一种变体。在 UNIX 下的 COFF 格式又
有不同。
/Cp 告诉 MASM 不要更改用户定义的标识符的大小写。若您用的是 hutch 的包含文件的
话,在.model 指令下加入 "option casemap:none" 语句,可达到同样的效果。
当您成功的编译了 msgbox.asm 后,编译器会产生 msgbox.obj 目标文件,目标文件和
可执行文件只一步之遥,目标文件中包含了以二进制形式存在的指令和数据,比可执行
文件相差的只是链接器加入的重定位信息。
好,我们来链接目标文件:
link /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm32\lib msgbox.obj
/SUBSYSTEM:WINDOWS 告诉链接器可执行文件的运行平台
/LIBPATH:〈path to import library〉 告诉链接器引入库的路径。
链接器做的工作就是根据引入库往目标文件中加入重定位信息,最后产生可执行文件。
既然得到了可执行文件,我们来运行一下。好,一、二、三,GO!屏幕上什么都没有。
哦,对了,我们除了调用了 ExitProcess 函数外,甚麽都还没做呢!但是别一点成就感
都没有哦,因为我们用汇编所写的是一个真正 Windows 程序,不信的话,查查您磁盘上
的 msgbox.exe文件,在我的机器上它的大小足有1,536字节呢。
下面我们来做一点可以看的见摸的着的,我们在程序中加入一个对话框。该函数的原型
如下:
MessageBox PROTO hwnd:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWO
RD
hWnd 是父窗口的句柄。句柄代表您引用的窗口的一个地址指针。它的值对您编 Window
s 程序并不重要(译者注:如果您想成为高手则是必须的),您只要知道它代表一个窗
口。当您要对窗口做任何操作时,必须要引用该窗口的指针。
lpText 是指向您要显示的文本的指针。指向文本串的指针事实上就是文本串的首地址。
lpCaption 是指向您要显示的对话框的标题文本串指针。
uType 是显示在对话框窗口上的小图标的类型。
下面是源程序
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
.data
MsgBoxCaption db "Iczelion Tutorial No.2",0
MsgBoxText db "Win32 Assembly is Great!",0
.code
start:
invoke MessageBox, NULL, addr MsgBoxText, addr MsgBoxCaption, MB_OK
invoke ExitProcess, NULL
end start
编译、链接上面的程序段,得到可执行文件。运行,哈哈,窗口上弹出了一个对话框,
上面有一行字:“Win32 Assembly is Great!”。想一想,我们是用汇编写出来的,所
以我们有理由为编写了一个最简单的 WIN32 程序感到高兴。(译者注:如果明天我们能
够像在 DOS 下那样每一行都用汇编写,那我们有理由为自己感到自豪。)
好,我们回过头来看看上面的源代码。我们在.DATA“分段”定义了两个NULL结尾的字符
串。我们用了两个常量:NULL 和 MB_OK。这些常量在windows.inc 文件中有定义,使用
常量使得您的程序有较好的可读性。 addr 操作符用来把标号的地址传递给被调用的函
数,它只能用在 invoke 语句中,譬如您不能用它来把标号的地址赋给寄存器或变量,
如果想这样做则要用 offset 操作符。在 offset 和 addr 之间有如下区别:
addr不可以处理向前引用,offset则能。所谓向前引用是指:标号的定义是在invoke 语
句之后,譬如在如下的例子:
invoke MessageBox,NULL, addr MsgBoxText,addr MsgBoxCaption,MB_OK
......
MsgBoxCaption db "Iczelion Tutorial No.2",0
MsgBoxText db "Win32 Assembly is Great!",0
如果您是用 addr 而不是 offset 的话,那 MASM 就会报错。
addr可以处理局部变量而 offset 则不能。局部变量只是在运行时在堆栈中分配内存空
间。而 offset 则是在编译时由编译器解释,这显然不能用offset 在运行时来分配内存
空间。编译器对 addr 的处理是先检查处理的是全局还是局部变量,若是全局变量则把
其地址放到目标文件中,这一点和 offset 相同,若是局部变量,就在执行 invoke 语
句前产生如下指令序列:
lea eax, LocalVar
push eax
因为lea指令能够在运行时决定标号的有效地址,所以有了上述指令序列,就可以保证
invoke 的正确执行了。
--
Take it slow, Set it couse, Make it happen.
※ 来源:·哈工大紫丁香 bbs.hit.edu.cn·[FROM: 202.97.235.21]
Powered by KBS BBS 2.0 (http://dev.kcn.cn)
页面执行时间:4.154毫秒