C++ 黑客编程揭秘与防范(第3版)
上QQ阅读APP看书,第一时间看更新

1.2.2 一个简单的Windows应用程序

相对一个简单的DOS程序来说一个简单的Windows应用程序要很长。下面的例子中只实现了一个特别简单的Windows程序,这个程序在桌面上显示一个简单的窗口,它没有菜单栏、工具栏、状态栏,只是在窗口中输出一段简单的字符串。虽然程序如此简单,但是也要编写100行左右的代码。考虑到初学的读者,这里将一部分一部分地逐步介绍代码中的细节,以减少代码的长度,从而方便初学者的学习。

1.Windows窗口应用程序的主函数—WinMain()

在DOS时代,或编写Windows下的命令行的程序,要使用C语言编写代码的时候都是从main()函数开始的。而在Windows下编写有窗口的程序时,要用C语言编写窗口程序就不再从main()函数开始了,取而代之的是WinMain()函数。

既然Windows应用程序的主函数是WinMain(),那么就从了解WinMain()函数的定义开始学习Windows应用程序的开发。WinMain()函数的定义如下:

        int WINAPI WinMain(
          HINSTANCE hInstance,
          HINSTANCE hPrevInstance,
          LPSTR lpCmdLine,
          int nCmdShow
        );

该函数的定义取自MSDN中,在看到WinMain()函数的定义后,很直观地会发现WinMain函数的参数比main()函数的参数变多了。从参数个数上来说,WinMain()函数接收的信息更多了。下面来看每个参数的含义。

hInstance是应用程序的实例句柄。保存在磁盘上的程序文件是静态的,当被加载到内存中时,被分配了CPU、内存等进程所需的资源后,一个静态的程序就被实例化为一个有各种执行资源的进程了。句柄的概念随上下文的不同而不同,句柄是操作某个资源的“把手”。当需要对某个实例化进程操作时,需要借助该实例句柄进行操作。这里的实例句柄是程序装入内存后的起始地址。实例句柄的值也可以通过GetModuleHandle()参数来获得(注意系统中没有GetInstanceHandle()函数,不要误以为是hInstance就会有GetInstance×××()类的函数)。

:句柄这个词在开发Windows程序时是非常常见的一个词。“句柄”一词的含义随上下文的不同而所有改变。比如,磁盘上的程序文件被加载到内存中后,就创建了一个实例句柄,这个实例句柄是程序装入内存后的“起始地址”,或者说是“模块的起始地址”。而在前面介绍的FindWindow()函数和SendMessage()函数中也提到了“句柄”这个词,而这时的“句柄”相当于某个资源的“把手”或“面板”。

拿SendMessage()函数举例来说,句柄相当于一个操作的面板,对句柄发送的消息相当于面板上的各个开关按键,消息的附加数据,相当于给开关按键送的各种参数,这些参数根据按键的不同而不同。

hPrevInstance是同一个文件创建的上一个实例的实例句柄。这个参数是Win16平台下的遗留物,在Win32下已经不再使用了。

lpCmdLine是主函数的参数,用于在程序启动时给进程传递参数。比如在“开始”菜单的“运行”中输入“notepad c:\boot.ini”,这样就通过记事本打开了C盘下的boot.ini文件。C:\Boot.ini文件是通过WinMain()函数的lpCmdLine参数传递给notepad.exe程序的。

nCmdShow是进程显示的方式,可以是最大化显示、最小化显示,或者是隐藏等显示方式(如果是启动木马程序的话,启动方式当然要由自己进行控制)。

主函数的参数都介绍完了。编写Windows的窗口程序,需要主函数中应该完成哪些操作是下面要讨论的内容。

2.WinMain()函数中的流程

编写Windows下的窗口程序,在WinMain()主函数中主要完成的任务是注册一个窗口类,创建一个窗口并显示创建的窗口,然后不停地获取属于自己的消息并分发给自己的窗口过程,直到收到WM_QUIT消息后退出消息循环结束进程。这是主函数中程序的执行脉络,程序中将注册窗口类、创建窗口的操作封装为自定义函数。

代码如下:

        int WINAPI WinMain(
                HINSTANCE hInstance,
                HINSTANCE hPrevInstance,
                LPSTR lpCmdLine,
                int nCmdShow)
        {
            MSG  Msg;
            BOOL bRet;

            // 注册窗口类
            MyRegisterClass(hInstance);

            // 创建窗口并显示窗口
            if ( !InitInstance(hInstance, SW_SHOWNORMAL) )
            {
                return FALSE;
            }

            // 消息循环
            // 获取属于自己的消息并进行分发
            while( (bRet = GetMessage(&Msg, NULL, 0, 0)) != 0 )
            {
                if ( bRet == -1 )
                {
                  // handle the error and possibly exit
                  break;
                }
                else
                {
                  TranslateMessage(&Msg);
                  DispatchMessage(&Msg);
                }
            }

            return Msg.wParam;
        }

在代码中,MyRegisterClass()和InitInstance()是两个自定义的函数,分别用来注册窗口类,创建窗口并显示更新创建的窗口。后面的消息循环部分用来获得消息并进行消息分发。它的流程如图1-7所示的“主程序”部分。

代码中主要是3个函数,分别是GetMessage()、TranslateMessage()和DispatchMessage()。这3个函数是Windows提供的API函数。GetMessage()的定义如下:

        BOOL GetMessage(
          LPMSG lpMsg,
          HWND hWnd,
          UINT wMsgFilterMin,
          UINT wMsgFilterMax
        );

该函数用来获取属于自己的消息,并填充MSG结构体。有一个类似于GetMessage()的函数是PeekMessage(),它可以判断消息队列中是否有消息,如果没有消息,可以主动让出CPU时间给其他进程。关于PeekMessage()函数的使用,请参考MSDN:

        BOOL TranslateMessage(CONST MSG *lpMsg);

该函数是用来处理键盘消息的。它将虚拟码消息转换为字符消息,也就是将WM_KEYDOWN消息和WM_KEYUP消息转换为WM_CHAR消息,将WM_SYSKEYDOWN消息和WM_SYSKEYUP消息转换为WM_SYSCHAR消息:

        LRESULT DispatchMessage(CONST MSG *lpmsg);

该函数是将消息分发到窗口过程中。

3.注册窗口类的自定义函数

在WinMain()函数中,首先调用了MyRegisterClass()这个自定义函数,需要传递进程的实例句柄hInstance作为参数。该函数完成窗口类的注册,分为两步:第一步是填充WNDCLASSEX结构体,第二步是调用RegisterClassEx()函数进行注册。该函数相对简单,但是,该函数中稍微复杂的是WNDCLASSEX结构体的成员较多。

代码如下:

        ATOM MyRegisterClass(HINSTANCE hInstance)
        {
            WNDCLASSEX WndCls;

            // 填充结构体为0
            ZeroMemory(&WndCls, sizeof(WNDCLASSEX));

            // cbSize是结构体大小
            WndCls.cbSize = sizeof(WNDCLASSEX);
            // lpfnWndProc是窗口过程地址
            WndCls.lpfnWndProc = WindowProc;
            // hInstance是实例句柄
            WndCls.hInstance = hInstance;
            // lpszClassName是窗口类类名
            WndCls.lpszClassName = CLASSNAME;
            // style是窗口类风格
            WndCls.style = CS_HREDRAW | CS_VREDRAW;
            // hbrBackground是窗口类背景色
            WndCls.hbrBackground = (HBRUSH)COLOR_WINDOWFRAME + 1;
            // hCursor是鼠标句柄
            WndCls.hCursor = LoadCursor(NULL, IDC_ARROW);
            // hIcon是图标句柄
            WndCls.hIcon = LoadIcon(NULL, IDI_QUESTION);
            // 其他
            WndCls.cbClsExtra = 0;
            WndCls.cbWndExtra = 0;

            return RegisterClassEx(&WndCls);
        }

在代码中,WNDCLASSEX结构体的成员都介绍了。WNDCLASSEX中最重要的字段是lpfnWndProc,它将保存的是窗口过程的地址。窗口过程是对各种消息进程处理的“汇集地”,也是编写Windows应用程序的重点部分。代码中的函数都比较简单,主要涉及LoadCursor()、LoadIcon()和RegisterClassEx()这3个函数。由于这3个函数使用简单,通过代码就可以进行理解,这里不做过多介绍。

注册窗口类(提到窗口类,你是否想到了FindWindow()函数的第一个参数呢?)的重点是在后面的代码中可以根据该窗口类创建该种类型的窗口。代码中,在定义窗口类时指定了背景色、鼠标指针、窗口图标等,那么使用该窗口类创建的窗口都具有相同的窗口类型。

4.创建主窗口并显示更新

注册窗口类后,根据该窗口类创建具体的主窗口并显示和更新窗口。

代码如下:

        BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
        {
            HWND hWnd = NULL;

            // 创建窗口
            hWnd = CreateWindowEx(WS_EX_CLIENTEDGE,
                        CLASSNAME,
                        "MyFirstWindow",
                        WS_OVERLAPPEDWINDOW,
                        CW_USEDEFAULT, CW_USEDEFAULT,
                        CW_USEDEFAULT, CW_USEDEFAULT,
                        NULL, NULL, hInstance, NULL);

            if ( NULL == hWnd )
            {
                return FALSE;
            }

            // 显示窗口
            ShowWindow(hWnd, nCmdShow);
            // 更新窗口
            UpdateWindow(hWnd);

            return TRUE;
        }

在调用该函数时,需要给该函数传递实例句柄和窗口显示方式两个参数。这两个参数的第1个参数通过WinMain()函数的参数hInstance指定,第2个参数可以通过WinMain()函数的第3个参数指定,也可以进行自定义指定。程序中的调用代码如下:

            InitInstance(hInstance, SW_SHOWNORMAL);

在创建主窗口时调用了CreateWindowEx()函数,先来看看它的函数原型:

        HWND CreateWindowEx(
          DWORD dwExStyle,
          LPCTSTR lpClassName,
          LPCTSTR lpWindowName,
          DWORD dwStyle,
          int x,
          int y,
          int nWidth,
          int nHeight,
          HWND hWndParent,
          HMENU hMenu,
          HINSTANCE hInstance,
          LPVOID lpParam
        );

CreateWindowEx()中的第2个参数是lpClassName,由注释可以知道是已经注册的类名。这个已经注册的类名就是WNDCLASSEX结构体的lpszClassName字段。

5.处理消息的窗口过程

按照如图1-7所示的流程,WinMain()主函数的部分已经都实现完成了。接下来看程序中关键的部分—窗口过程。从WinMain()主函数中看出,在WinMain()主函数中没有任何地方直接调用窗口过程,只是在注册窗口类时指定了窗口过程的地址。那么窗口类是由谁进行调用的呢?答案是由操作系统进行调用的。原因有二,首先窗口过程的地址是由系统维护的,注册窗口类时是将“窗口过程的地址”向操作系统进行注册。其次是除了应用程序本身会调用自己的窗口过程外,其他应用程序也会调用自己的窗口过程,比如前面的例子中调用SendMessage()函数发送消息后,需要系统调用目标程序的窗口过程来完成相应的动作。如果窗口过程由自己调用,那么窗口就要自己维护窗口类的信息,进程间消息的通信会非常繁琐,也会无形中增加系统的开销。

窗口过程的代码如下:

        LRESULT CALLBACK WindowProc(
                      HWND hwnd,
                      UINT uMsg,
                      WPARAM wParam,
                      LPARAM lParam)
        {
            PAINTSTRUCT ps;
            HDC hDC;
            RECT rt;
            char *pszDrawText = "Hello Windows Program.";
            switch (uMsg)
            {
            case WM_PAINT:
              {
                  hDC = BeginPaint(hwnd, &ps);
                  GetClientRect(hwnd, &rt);
                  DrawTextA(hDC,
                          pszDrawText, strlen(pszDrawText),&rt,
                          DT_CENTER | DT_VCENTER | DT_SINGLELINE);
                  EndPaint(hwnd, &ps);
                  break;
              }
            case WM_CLOSE:
              {
                  if ( IDYES == MessageBox(hwnd,
                        "是否退出程序", "MyFirstWin", MB_YESNO) )
                  {
                      DestroyWindow(hwnd);
                      PostQuitMessage(0);
                  }
                  break;
              }
            default:
              {
                  return DefWindowProc(hwnd, uMsg, wParam, lParam);
              }
            }
            return 0;
        }

在WinMain()函数中,通过调用RegisterClassEx()函数进行了窗口类的注册,通过调用CreateWindowEx()函数创建了窗口,并且GetMessage()函数不停地获取消息,但是在主函数中没有对被创建的窗口做任何处理。那是因为真正对窗口行为的处理全部放在了窗口过程中。当WinMain()函数中的消息循环得到消息以后,通过调用DispatchMessage()函数将消息派发(实际不是由DispatchMessage()函数直接派发)给了窗口过程,从而由窗口过程对消息进行处理。

窗口过程的定义是按照MSDN上给出的形式进行定义的,MSDN上的定义形式如下:

        LRESULT CALLBACK WindowProc(
          HWND hwnd,
          UINT uMsg,
          WPARAM wParam,
          LPARAM lParam
        );

WindowProc是窗口过程的函数名,这个函数名可以随意改变,但是该窗口过程的函数名必须与WNDCLASSEX结构体中lpfnWndProc的成员变量的值一致。函数的第1个参数hwnd是窗口的句柄,第2个参数uMsg是消息值,第3个和第4个参数是对于消息值的附加参数。这4个参数的类型与SendMessage()函数的参数相对应。

上面WindowProc()窗口过程中只对两个消息进行了处理,分别是WM_PAINT和WM_CLOSE。这里为了演示因此只简单处理了两个消息。Windows中有上千种消息,那么多的消息不可能全部都由程序员自己去处理,程序员只处理一些程序中需要的消息,其余的消息就交给了DefWindowProc()函数进行处理。DefWindowProc()函数实际上是将消息传递给了操作系统,由操作系统来处理程序中没有处理的消息。比如,在调用CreateWindow()函数时,系统会发送消息WM_CREATE给窗口过程,但是这个消息可能对程序的功能并不需要进行特殊的处理,因此直接交由DefWindowProc()函数让系统进行处理。

DefWindowProc()函数的定义如下:

        LRESULT DefWindowProc(
          HWND hWnd,
          UINT Msg,
          WPARAM wParam,
          LPARAM lParam
        );

该函数的4个参数跟窗口过程的参数相同,只要将窗口过程的参数依次传递给DefWindowProc()函数就可以完成该函数的调用。在switch分支结构中的default位置直接调用DefWindowProc()函数就可以了。

WM_CLOSE消息是关闭窗口时发出的消息,在这个消息中需要调用DestoryWindow()函数来销毁窗口,并且调用PostQuitMessage()来退出消息循环,使程序退出。对于WM_PAINT消息,这里不进行介绍,涉及的几个API函数可以参考MSDN进行了解。

有的资料在介绍消息循环时会给出一个建议,就是把需要经常处理的消息放到程序靠上的位置,而将不经常处理的消息放到程序靠下的位置,从而提高程序的效率。其实,在窗口过程中往往会使用switch结构对消息进行判断(如果使用if和else结构进行消息的判断,那么常用的消息是要放到前面),而switch结构在编译器进行编译后会进行优化处理,从而大大提高程序的运行效率。关于switch结构的优化,我们将在其他章节进行介绍。