Fasm---Win32汇编学习3

来源:互联网 发布:神田优 知乎 编辑:程序博客网 时间:2024/04/29 07:12

Fasm---Win32汇编学习3

                                         第三课-完整的界面

  

在今天这节课程中,我们来写一个Windows的界面。

 

理论:

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

 

 其实我们可以这样想,在windows中我们就处于另一个虚拟世界,那么这个世界里有很多预定义的对象,那么每个对象就是windows这个世界之前静态的定义的对象的实例。。      例如,每个进程就是windows静态定义的“进程对象”的实例。。那么每个窗口也就是windows预定义的窗口对象的实例,我们必须遵守这个规则,否则我们就会被踢出局。

 

那么我们要想创建一个windows界面就得遵守以下的规则:

   下面我就列出在桌面显示一个窗口的基本步骤。

    1.得到您应用程序的句柄。

    2.得到命令行参数。(可选)。

    3.注册窗口类。

    4.产生窗口。(必须)

    5.在桌面显示窗口。

    6.刷新窗口客户区。

    7.进入无限获取的窗口消息循环。

    8.如果有消息到达,则由负责该窗口的回调函数处理。

    9.如果用户关闭窗口,进行退出处理。

 

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

内容:下面是一段简单的窗口程序源代码。

 

 

说明:

  1.你最好把这个程序中所需要的所有的结构以及常量包含到一个头文件中,然后在我们的程序中包含这个头文件。当然这个我们的fasm已经帮我们做好了,我们只需要引用一个“win32ax.inc”头文件即可,这个是个扩展的头文件,里面包含了一些扩展的宏语句,例如addr 取局部变量,否则如果我们引用“win32a.inc”,我们必须通过lea指令来获取局部内存单元的偏移地址。

 

2.利用library宏语句来引用相应的我们需要调用函数所在的dll动态链接库名称,这样以便我们编译器构建输入表。因为windows是以动态链接库的形式提供给我们接口函数的,我们要调用这些接口,必须通过指定相应的动态链接库名称以及函数名称,并通过特定的格式够杂输入表,这样我们的程序在被载入到内存后相应的动态链接库才会被载入内存,以及重定向我们调用函数的地址。。最后通过include语句包含相应的动态库的头文件,这里面包含了相应函数的格式。。。

 

3.在其他地方运用头文件定义函数原型,常数和结构体的时,要严格保持和头文件中一致,包括大小写。在函数查询时,这将节约你很多时间。

    format PE GUI 4.0
    include 'win32ax.inc'
   

macro memmov [dst, src]
{
        common
        push [src]
        pop [dst]
}

    LPSTR equ dd
    ;************************数据********************************
    szClassName db 'first Windows',0
    szWndName   db '我的第一个程序',0
    lpCommand LPSTR ?
    hIcon      rd 1
    hInstanse  rd 1
    hCursor    rd 1
    hWnd       rd 1


    entry $
        invoke GetModuleHandle,NULL
;必须的,我们必须获得我们程序的模块句柄。如上面说的,如果不遵循我们将over。
        mov    [hInstanse], eax       
;在win32模式下, hMoudule == hInstance  mov  [hInstance], eax
        invoke GetCommandLine,NULL
;不是必须的,如果你的程序不处理命令行,则这句代码可以省去。
        mov [lpCommand], eax    
        stdcall _WinMain,hInstanse, NULL, [lpCommand], SW_SHOWDEFAULT
;调用主函数
        invoke ExitProcess,NULL
       
    proc _WinMain    hInstance:DWORD, hPrevInstance:DWORD, lpCmdLine:DWORD, nCmdShow:DWORD
        local @wc : WNDCLASSEX    
;创建局部变量
        local @msg : MSG
       
       
        invoke RtlZeroMemory,addr @wc,sizeof.WNDCLASSEX       
;填写wc(WNDCLASSEX结构)的成员
        invoke LoadIcon,NULL, IDI_WINLOGO
        mov [hIcon], eax
        invoke LoadCursor,NULL, IDC_ARROW
        mov [hCursor], eax
        mov [@wc.cbSize], sizeof.WNDCLASSEX
        mov [@wc.style], CS_HREDRAW or CS_VREDRAW
        mov [@wc.lpfnWndProc], _WndProc
        mov [@wc.cbClsExtra], NULL
        mov [@wc.cbWndExtra], NULL
        memmov @wc.hInstance, hInstance
        memmov @wc.hIcon, hIcon
    memmov @wc.hCursor, hCursor
        mov [@wc.hbrBackground], COLOR_WINDOW
        mov [@wc.lpszMenuName], NULL
        mov [@wc.lpszClassName], szClassName
      
;注册窗口类
        invoke RegisterClassEx,addr @wc 
       
;建立窗口
   
        invoke CreateWindowEx, NULL, szClassName, szWndName,/
WS_OVERLAPPEDWINDOW,/
    100, 100, 600, 400,/
    NULL, NULL, [hInstanse], NULL
        mov [hWnd], eax
        invoke ShowWindow,[hWnd],SW_SHOWNORMAL
        invoke UpdateWindow,[hWnd]
   
        GetMsg:
        invoke GetMessage,addr @msg, NULL, 0, 0
        or eax, eax
        jz EndMsg
        invoke TranslateMessage,addr @msg
        invoke DispatchMessage,addr @msg
        jmp GetMsg
       
EndMsg:
        mov eax, [@msg.wParam]
        ret
    endp
   
   
    proc _WndProc uses ebx esi edi,hWnd:DWORD, wMsg:DWORD, wParam:DWORD, lParam:DWORD
       
        cmp [wMsg], WM_DESTROY
        jz Quit
        invoke DefWindowProc,[hWnd],[wMsg],[wParam],[lParam]
        ret
   
   
    Quit:
        invoke PostQuitMessage,NULL
        jmp endWnd
       
   
    endWnd:
        xor eax,eax
        ret
    endp

   
;///////////////////////////输入表////////////////////////////////////////////////

   
section '.import' data import readable writeable
       
   
    library kernel32, 'kernel32.dll',/
                user32, 'user32.dll'
    include 'api/kernel32.inc'
    include 'api/user32.inc'

分析:

  看到上面一堆代码,是不是想撤,呵呵。我也有同样的感觉,不过上面的是模板而已,模板是说上面的代码对差不多所有的windows程序来说基本是相同的。在写windows的时候,你可以将其拷来拷去,当然把这些代码放到一个库中也挺好。其实真正要写的代码在_WinMain中。这和一些c的编译器一样,无需关心其他杂物。唯一不同的是c编译器要求你的源代码中必须要有一个WinMain。否则c不知道将那个函数和有关前后代码链接。相对c,汇编语言提供了较大的灵活性,它不强行叫WinMain函数。

下面开始分析,你可要做好准备,这可不是一件容易的事情。

 

    format PE GUI 4.0
    include 'win32ax.inc'

 

   entry $

  

  proc _WinMain        hInstance:DWORD, hPrevInstance:DWORD, lpCmdLine:DWORD, nCmdShow:DWORD

 

;///////////////////////////输入表////////////////////////////////////////////////

   
section '.import' data import readable writeable
       
   
    library kernel32, 'kernel32.dll',/
                user32, 'user32.dll'
    include 'api/kernel32.inc'
    include 'api/user32.inc'

 

  你可以把前两行以及最后的部分看成是必须的。

   format 指定我们的输出文件格式(例如PE格式(32位), MZ格式(16位)), 后面的GUI表示我们的子系统是windows窗口模式       4.0为子系统的版本号(一般编写win32程序我们设置为4.0)。

    接下来是我们的entry伪指令,这个伪指令是指定我们的入口点,$标示当前的地址。也就是指示我们当前的地址为入口点。

  然后是我们的WinMain函数的原型,因为稍后要用到,所以先声明。我们我们必须包含win32ax.inc头文件,因为其中包含了一些头文件中 是我们要用到的常量以及结构的定义。该文件是一个文本文件,你可以用任何编辑器打开。

  

   由于我们用到的相应的函数驻扎在user32.dll 和kernel32.dll中,所以我们必须通过library来引用相应的动态链接库 。(譬如:RegisterClassEx -- user32.dll ,       ExitProcess -- kernel32.dll)。

   接下来我问您需要把什么库引入到程序中?

   答案是:先查你要调用的函数在什么库中,然后包含进来。 譬如:例如你要调用gdi32.dll中的函数,则必须引入gdi32.dll,以及相应库的头文件。 gid32.inc 。

 

 

 接下来是

   LPSTR equ dd
    ;************************数据********************************
    szClassName db 'first Windows',0
    szWndName   db '我的第一个程序',0
    szCommand LPSTR ?
    hIcon      rd 1
    hInstanse  rd 1
    hCursor    rd 1
    hWnd       rd 1

  这里是定义的相应的数据,以及符号常量。。 

   符号常量是表示在编译程序的时候,所有的符号常量都被其后的真实值所替代,有点像C语言的中宏。 符号常量通过equ伪指令来定义。格式由符号常量名跟equ来定义 。。

   db伪指令可以定义以''括起来的字符串,这样编译的时候这段地址空间将是一段字节序列。因为我们windows中的字符串处理函数是以00来标示结尾的,所以我们在定义字符串的时候一定要在后面加上一个00。

   rd伪指令为保留数据,也就是它们是未初始化的,程序在启动时候它们是什么值无关紧要,只不过占了一段内存,以后在利用。

   hInstanse代表程序的实例句柄,lpCommand保存的是传入过来的命令行参数。我们之所以通过定义符号常量只是用来帮助我们记忆。例如LPSTR equ dd,我们之后直接就可以引用LPSTR,我们看到就知道是什么意思。。      这样实质程序在编译符号常量的时候是将其用真实值替换也就是用dd替换。

   entry $ 这里包含了我们的所有代码。

    entry $
        invoke GetModuleHandle,NULL
;必须的,我们必须获得我们程序的模块句柄。如上面说的,如果不遵循我们将over。
        mov    [hInstanse], eax       
;在win32模式下, hMoudule == hInstance  mov  [hInstance], eax
        invoke GetCommandLine,NULL
;不是必须的,如果你的程序不处理命令行,则这句代码可以省去。
        mov [lpCommand], eax    
        stdcall _WinMain,hInstanse, NULL, [lpCommand], SW_SHOWDEFAULT
;调用主函数
        invoke ExitProcess,NULL

  我们的第一条语句GetModuleHandle来获得我们程序的模块句柄,在win32环境下模块的句柄和程序的句柄是一样的。你可以把实例句柄看成您应用程序的ID号。我们在调用几个函数都是把它作为参数来进行传递,所以在一开始获得并保存它,省了很多事情。

 

特别注意:Win32环境下的实例句柄实际是您应用程序在内存的线性地址。

WIN32环境中如果函数由返回则,则是通过eax寄存器来传递的,其他的值可以用来传递参数地址来进行返回。一个win32函数被调用时,总是保存好段寄存器和ebx, esi edi 和ebp寄存器。而ecx 和edx值是不固定的,不能在返回时应用。特别注意:在windows api函数返回时, eax ecx edx的值和调用前不同。当函数返回时,返回值放在eax寄存器中。 如果你应用程序中的函数提供给windows调用,也必须遵守这一点,则必须在入口处保存段寄存器和 ebx esi edi ebp寄存器。如果不这样以来的话,你的程序很容易崩溃。从您的程序中提供给 Windows 调用的函数大体上有两种:Windows 窗口过程和 Callback 函数。

 

  如果你的程序不处理命令行参数,则无需调用GetCommandLine函数,这里只是告诉你如果要调用该怎么做。

  proc _WinMain        hInstance:DWORD, hPrevInstance:DWORD, lpCmdLine:DWORD, nCmdShow:DWORD

   上面是WinMain的定义,注意proc 后跟着函数名,这点和masm不同。。  函数名后面parameter:type 。它们是由调用者传递给WinMain的。我们直接引用参数名即可。至于退栈 压栈 fasm在编译的时候加入了前序和后序的处理指令。  local @wc : WNDCLASSEX    local @msg : MSG      伪指令用于在堆栈中分配内存。所有的local指令必须紧跟proc后。

  local后跟的局部变量的声明格式是 local 变量名: 变量类型 。例如

  local @wc : WNDCLASSEX

    是告诉编译器在栈中分配WNDCLASSEX结构体长度的内存空间,然后我们再使用的时候无需考虑堆栈的问题,考虑到dos下的汇编,这不能不说是一种恩赐。不过这就要求这样申明的局部变量再函数结束时释放栈空间,(也不能再函数体外被引用)。另一个缺点是你无法初始化你的局部变量,只能在稍后对其进行赋值。

 invoke RtlZeroMemory,addr @wc,sizeof.WNDCLASSEX       ;填写wc(WNDCLASSEX结构)的成员
        invoke LoadIcon,NULL, IDI_WINLOGO
        mov [hIcon], eax
        invoke LoadCursor,NULL, IDC_ARROW
        mov [hCursor], eax
        mov [@wc.cbSize], sizeof.WNDCLASSEX
        mov [@wc.style], CS_HREDRAW or CS_VREDRAW
        mov [@wc.lpfnWndProc], _WndProc
        mov [@wc.cbClsExtra], NULL
        mov [@wc.cbWndExtra], NULL
        memmov @wc.hInstance, hInstance
        memmov @wc.hIcon, hIcon
    memmov @wc.hCursor, hCursor
        mov [@wc.hbrBackground], COLOR_WINDOW
        mov [@wc.lpszMenuName], NULL
        mov [@wc.lpszClassName], szClassName
      
;注册窗口类
        invoke RegisterClassEx,addr @wc 
       
;建立窗口

上面的几行代码概念上说其实非常简单,只要几行代码就可以了。其中主要的概念就是窗口类,一个窗口类就有窗口的规范,这个规范定义了几个主要的窗口元素,如图标,光标,背景色和负责处理该窗口的函数。你需要产生一个窗口必须要有这样的窗口类,这是windows世界窗口对象的规范。如果你需要产生不止一个的窗口,最好的方法是将这个窗口存储起来,这种方法可以节约很多的内存空间。也许今天你体会不到,但是你想想之前pc机只有一M内存的时候。这么做是非常有必要的。如果您要定义自己创建窗口类就必须在一个WNDCLASS 或者WNDCLASSEX指明相关的成员。然后调用RegisterClass 或者 RegisterClassEx。在根据该窗口类,产生窗口。对不同特色的窗口定义不同的窗口类。windows有几个预定义的窗口类,例如按钮,编辑框等。要产生这种风格的窗口无需在定义了,只要包含相应的预定义的类名作为参数调用给CreateWindowEx就可以了。

  

上面调用invoke RtlZeroMemory,addr @wc,sizeof.WNDCLASSEX     ;将我们的@wc结构初始化为0。

  WNDCALSSEX结构最重要的成员是lpfnWndProc,它指向的是函数的一个长指针,在win32中由于内存模式是flat型,所以没有near和far之分,每一个窗口类必须由一个窗口过程,当windows把属于特定窗口的消息发送给窗口的时候,该窗口的窗口类负责处理所有的消息,如键盘消息,鼠标消息等。由于窗口过程智能的处理了所有的窗口消息循环,所以你只要在其加入消息处理过程即可。。下面我讲解WNDCALSSEX的每一个成员。

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

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

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

 

     invoke CreateWindowEx, NULL, szClassName, szWndName,/
WS_OVERLAPPEDWINDOW,/
    100, 100, 600, 400,/
    NULL, NULL, [hInstanse], NULL

我们来看下这个函数的参数

  dwExStyle    :附加的窗口风格,对于之前的createWindow这是一个新的参数。在9x/nt中你可以使用新的风格。你可以在dwExStyle中指定一般的窗口风格,但是一些特殊的窗口风格,如顶层窗口则必须在此参数中指定。如果你不想指定任何特别的风格,则把此参数设置NULL。

lpClassName :ASCIIZ形式的窗口类名称的地址,可以是你自定义的类,也可以是你定义的类名。像上面说的,每个窗口必须有一个窗口类。

lpWindowName : ASCIIZ形式的窗口名称的地址,该名称会显示到标题上。如果该参数空白,则标题栏什么都米。

dwStyle : 窗口的风格,在此你可以指定窗口的外观,可以指定该参数为零,但是那样该窗口就没有系统菜单。也没有最大化最小化按钮。那样你不得不按alt + F4 来进行关闭。最普通的窗口风格是WS_OVERLAPPEDWINDOW 。一种窗口风格是按位掩码,你可以通过"or"来将其连接起来。WS_OVERLAPPEDWINDOW 就是由几种不同的风格用or集成的。

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],SW_SHOWNORMAL
        invoke UpdateWindow,[hWnd]
    调用CreateWindowEx函数成功后,返回值在eax寄存器中。因为我们win32的api函数返回值都是在eax寄存器。我们必须保留已被后用,我们刚刚产生的窗口不会显示,我们必须通过ShowWindow函数来显示窗口。然后调用UpdateWindow函数来更新客户区。

 

     
        GetMsg:
        invoke GetMessage,addr @msg, NULL, 0, 0
        or eax, eax
        jz EndMsg
        invoke TranslateMessage,addr @msg
        invoke DispatchMessage,addr @msg
        jmp GetMsg

此时我们已经可以显示窗口了,但是此时的窗口不从外界接受消息。我们必须给他提供相关的消息。我们是通过一项消息循环来完成这样的工作的。每一个模块仅有一个消息循环,我们不断的通过GetMessage来获取windows维护给我们程序消息队列中的消息。GetMessage传递一个MSG结构体给Windows。然后windows在此MSG结构体填充相关的消息,一直到windows填充好后,GetMessage才返回。在这段时间内,相关的控制权可能转移给其他的程序,这样就构成win16位下的 多任务。如果GetMessage获取的是WM_QUIT消息就会返回false,使结束消息循环。TranslateMessage函数是一个实用函数,它从键盘接受原始按键消息,然后解释成WM_CHAR消息,在把WM_CHAR消息放入消息队列中,由于经过解释后的程序含有ASCII码,这比原始的按键消息好理解的多。如果你的应用程序不处理按键消息的话,则可以不调用该函数。DisPatchMessage会把消息发送给负责该窗口的过程函数。

 

   EndMsg:
        mov eax, [@msg.wParam]
        ret

   如果消息返回了,则退出吗在wParam参数中,你可以把它放在eax寄存器中返回给windows 操作系统。windows目前没有利用这个退出码,但是我们为了防止意外。

    proc _WndProc uses ebx esi edi,hWnd:DWORD, wMsg:DWORD, wParam:DWORD, lParam:DWORD
       
       这是我们的窗口过程函数,函数名可以任意设置,第一个参数hWnd是我们要接受消息的窗口句柄。wMsg是接受的消息,注意wMsg不是一个MSG结构体,它是一个DWORD类型。Windows定义了成千上百个消息,大多数你的程序用不到。当该窗口由消息发生时,windows会发送相关消息给该窗口。窗口过程函数会智能的处理这些函数,wParam 和 lParam 只是附加参数,以方便传递更多的和该消息有关的数据。

cmp [wMsg], WM_DESTROY
        jz Quit
        invoke DefWindowProc,[hWnd],[wMsg],[wParam],[lParam]
        ret
   
   
    Quit:
        invoke PostQuitMessage,NULL
        jmp endWnd
       
   
    endWnd:
        xor eax,eax
        ret
    endp

 

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