Windows程序大体上分为程序代码和UI资源两个部分,其中C源程序通过C编译器生成.obj文件,各种RC资源文件通过RC编译器生成最终的.res文件,两个目标文件通过链接生成.exe可执行文件。
所谓UI资源是指功能菜单,对话框样式,程序图标,光标形状等,这些UI资源的实际内容(就是二进制代码)是借助于各种工具产生,并以各种扩展名存在,如.ico,.bmp,.cur等。程序员必须在一个所谓的资源描述文档(.rc)中描述这些UI资源。
.RC编译器(RC.EXE)读取RC文档的描述后将所有的UI资源集中制作出一个.RES文件。
任何高级编译型语言生成最终可执行文件的最后一步都是链接,Windows推荐的链接方式是动态链接,也就是说,应用程序所调用的API函数是在程序运行期间才链接上的。事实上,链接库并不仅是我们平常所看到的.dll文件,.fon,.mod,.drv,.ocx都是所谓的动态链接函数库。
Windows程序调用的函数可分为C Runtimes以及Windows API两大部分。早期的C Runtimes并不支持动态链接,但Visual C++4.0之后已支持。
C Runtimes函数所包含的库:
另一组函数则是Windows API,它是操作系统本身(主要是Windows三大模块GDI32.DLL,USER32.DLL和KERNEL32.DLL)提供的。虽说动态链接是在程序执行期间才调用的,但在链接时期,链接器仍需先为调用者(应用程序本身)准备一些适当的信息,才能够在执行时期顺序的跳转到DLL库上执行。32位Windows的三大模块所对应的函数库分别为GDI32.LIB,USER32.LIB和KERNEL.LIB。
所有的Windows程序都需要包含WINDOWS.H头文件,早期这是一个巨大的头文件,大约有5000行左右,Visual C++4.0时,其实已将它分割成各个较小的文件,但还以WINDOWS.H总括之。实际开发中,为了方便起见,我们都是直接引入WINDOWS.H头文件即可。
不过,WINDOWS.H只包含三大模块所提供的API函数,如果要用到其他系统链接库,例如COMMDLG.DLL或MAPI.DLL等,就得另外包含对应的头文件。
Windows程序的进行依靠外部发生的事件来驱动,也就是说,程序会不断等待(用一个while回路)任何可能的输入,然后做判断之后再做适当的处理。上述的输入由操作系统捕捉到后以消息(其实就是一种数据结构)的形式进入程序之中被相应的消息处理函数处理。那么,操作系统是如何捕捉外部设备(如键盘和鼠标)上所发生的事件呢?USER模块掌管着各个外部的驱动程序,它们之中各自设有侦测回路。
将应用程序获得的各种输入进行分类,可以分为由硬件所产生的消息(如鼠标移动或键盘按下),这会放在系统队列中(system queue),以及由Windows系统或其他Windows程序传送的消息,这类消息放在程序队列(application queue)中。
从应用程序的角度来看,无论来自哪里(从外部设备传进来的或从其他窗口传过来的),消息就是消息,没有太大区别。所有的GUI系统包括UNIX上的X Window都是以消息为基本的事件驱动系统。
windows消息传递机制:
可以想见,每一个Windows程序都应该有一个如下的while回路:
MSG msg;
while(GetMessage(&msg, NULL, NULL, NULL)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
而所谓消息,就是C语言里的一组数据结构:
typedef struct tagMSG {
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} MSG;
而接收并处理消息的主角就是窗口了,每一个窗口都应该有相应的消息处理函数(window procedure或window function)。当窗口获得一个消息时,这个函数要判别消息的类型以决定要采取怎样的处理方式。
下面推荐一个工具可以分析exe可执行文件的数据结构——dumpbin.exe,这个工具是VS安装时产生的,它在C:\Program Files(×86)\Microsoft Visual Studio 10.0\VC\bin目录下。
值得注意的是,dumpbin工具依赖于cl.exe编译器,而cl.exe又依赖于mspdb100.dll链接库(这个库在C:\Program Files(×86)\Microsoft Visual Studio 10.0\Common7\IDE目录下),可以把这个链接库复制到cl.exe所处的目录即C:\Program Files(×86)\Microsoft Visual Studio 10.0\VC\bin下,这样就不会出现找不到链接库的情况。
另外,要使用dumpbin和cl工具以及之后要介绍的编译makefile文件(.mak)的nmake工具,则要把这个目录(C:\Program Files(×86)\Microsoft Visual Studio 10.0\VC\bin)配置到环境变量中(这里不再赘述)。
用C#写一个test.cs文件:
using System; // 包含命令空间System
namespace HelloWorldApplication // 命名空间声明
{
class HelloWorld // 声明一个类
{
static void Main(string[] args) // Main()方法,C#程序的入口点,和java一样,C#中Main()函数定义在某个类中
{
Console.WriteLine("Hello World");
Console.ReadKey(); // 相当于C++中的System("pause")
}
}
}
使用csc命令编译成test.exe后,再通过dumpbin工具查看这个EXE文件的数据结构:
谈到了使用dumobin工具查看exe文件的二进制格式,这里可以补充下几个数据段的作用:
- .text: 就是放程序代码的,编译时确定,只读。
- .data段: 存放编译阶段(而非运行时)就能确定的数据,可读可写,就是通常所说的静态存储区,初始化后的全局变量和静态变量就存放在这个区域,常量也存放在这个区域。
- .bbs段: 定义了但没有初始化的全局变量和静态变量,存放在这个区域。
- .rdata: 资源数据段,程序用到的资源数据都在这里。
- .idata: Imports函数的代码段,这里集中存放了所有外部函数的地址,执行到这里后代码会先跳到该地址(指向某一dll链接库)后再执行,PE文件加载器在开始会获取真实的函数地址来修补idata段中的函数地址。与之对应的exports是edata.
如果要更详细的查看exe文件的二进制内容,可以使用反汇编工具IDA pro:
可见,dll链接库在汇编中是使用Imports关键字导入的。
cl.exe的用法:
cl [ 选项... ] 文件名... [ /link 链接选项... ]
下面再介绍一下makefile文件的写法。makefile中典型的写法是<左边>:<右边>
,如果右边所列出的任一文件的更新日期比左边文件的更新日期要晚,则makefile会继续往下执行。
# Generic.mak
all: generic.exe
generic.res : generic.rc generic.h
rc generic.rc
# 如果文件generic.rc或generic.h中的任一文件较generic.res更新,
# 则执行这一命令(使用命令rc generic.rc来将generic.rc文件生成为generic.res文件,
# 这样就做到了generic.res及时更新)
在开始创建窗口之前,我们必须做一些初始化工作。创建一个窗口没什么困难的,CreateWindow()已经包办了所有的内部实现细节。但是在窗口产生之前,我们还要将一些基本的属性(如窗口的消息处理函数)设置好,也就是使用RegisterClass()注册窗口类别。RegisterClass()需要一个大型的数据结构WNDCLASS作为参数,而CreateWindow()则另外需要11个参数指定窗口的名称和样式。
安装惯例,RegisterClass()被包装在InitApplication()函数中CreateWindow()则被包装在InitInstance()函数之中。
所以一个规范的Windows程序大致是这样:
int CALLBACK WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
InitApplication(hInstance);
if (!InitInstance(hInstance, nCmdShow))
return FALSE;
...
}
//--------------------------------------------------
BOOL InitApplication(HINSTANCE hInstance)
{
WNDCLASS wc;
...
return (RegisterClass(&wc));
}
//--------------------------------------------------
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
_hWnd = CreateWindow(...);
...
}
下面我们来看一个完整点的Windows应用程序:
首先是generic.h,
//---------------------------------------------------------------
// 文件名 : generic.h
//---------------------------------------------------------------
BOOL InitApplication(HANDLE);
BOOL InitInstance(HANDLE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
LRESULT CALLBACK About(HWND, UINT, WPARAM, LPARAM);
然后是generic.c:
#include // 每一个 Windows 程序都需要包含此文件
#include "resource.h" // resource.h文件中包含了每个UI资源的ID序列
#include "generic.h"
HINSTANCE _hInst; // 窗口句柄
HWND _hWnd; // 待创建的窗口
char _szAppName[] = "Generic";
char _szTitle[] = "Generic Sample Application";
//---------------------------------------------------------------
// WinMain - Windows应用程序入口函数,其中的四个参数是由操作系统传给它的
//---------------------------------------------------------------
int CALLBACK WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
MSG msg;
InitApplication(hInstance);
if (!InitInstance(hInstance, nCmdShow))
return FALSE;
while (GetMessage(&msg, NULL, 0, 0)) { // 消息循环
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return (msg.wParam); // 传回 PostQuitMessage 的参数
}
//---------------------------------------------------------------
// InitApplication - 窗口类注册
//---------------------------------------------------------------
BOOL InitApplication(HINSTANCE hInstance)
{
// 构造RegisterClass()函数所需要的结构体WNDCLASS
WNDCLASS wc;
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = (WNDPROC)WndProc; // 消息处理函数
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(hInstance, "jjhouricon");
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = GetStockObject(WHITE_BRUSH); // 窗口背景色
wc.lpszMenuName = "GenericMenu"; // Menu菜单资源的定义在.rc文件中
wc.lpszClassName = _szAppName;
return RegisterClass(&wc);
}
//---------------------------------------------------------------
// InitInstance - 创建窗口(一个应用程序可以有多个窗口)
//---------------------------------------------------------------
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
_hInst = hInstance; // 将窗口句柄存储为全局变量,方便之后的使用
_hWnd = CreateWindow(
_szAppName,
_szTitle,
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL
);
if (!_hWnd)
return FALSE;
ShowWindow(_hWnd, nCmdShow); // 显示窗口
UpdateWindow(_hWnd); // 刷新当前窗口,此时会向窗口送出一个 WM_PAINT 消息,以激活窗口的重绘消息处理函数
return (TRUE);
}
//---------------------------------------------------------------
// WndProc - 消息处理函数
//---------------------------------------------------------------
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
int wmId, wmEvent;
switch (message) {
case WM_COMMAND:
wmId = LOWORD(wParam);
wmEvent = HIWORD(wParam);
switch (wmId) {
case IDM_ABOUT:
DialogBox(_hInst, // Winmain第一个参数,应用程序的实例句柄
"AboutBox", // 对话框资源的名称
hWnd, // 父窗口
(DLGPROC)About //对话框函数名称
);
break;
case IDM_EXIT:
// 结束程序,处理方式与 WM_CLOSE 相同
DestroyWindow (hWnd);
break;
default: // 忽略消息
return (DefWindowProc(hWnd, message, wParam, lParam));
}
break;
case WM_DESTROY: // 窗口已经被摧毀 (程序即将结束)
PostQuitMessage(0);
break;
default:
return (DefWindowProc(hWnd, message, wParam, lParam));
}
return (0);
}
//---------------------------------------------------------------
// About -对话框函数
//---------------------------------------------------------------
LRESULT CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message) {
case WM_INITDIALOG:
return TRUE; // TRUE 表示我已处理过这个消息,之后该消息将不再向后传递
case WM_COMMAND:
if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) {
EndDialog(hDlg, TRUE);
return TRUE;
}
break;
}
return FALSE; // FALSE 表示该消息还尚未得到处理
}
再来看看一个.rc文件的写法:
//---------------------------------------------------------------
// 档名 : generic.rc - 定义各种UI资源
//---------------------------------------------------------------
#include "windows.h"
#include "resource.h"
jjhouricon ICON DISCARDABLE "Call.ico"
GenericMenu MENU DISCARDABLE
BEGIN
POPUP "&File"
BEGIN
MENUITEM "&New", IDM_NEW, GRAYED
MENUITEM "&Open...", IDM_OPEN, GRAYED
MENUITEM "&Save", IDM_SAVE, GRAYED
MENUITEM "Save &As...", IDM_SAVEAS, GRAYED
MENUITEM SEPARATOR
MENUITEM "&Print...", IDM_PRINT, GRAYED
MENUITEM "P&rint Setup...", IDM_PRINTSETUP, GRAYED
MENUITEM SEPARATOR
MENUITEM "E&xit", IDM_EXIT
END
POPUP "&Edit"
BEGIN
MENUITEM "&Undo\tCtrl+Z", IDM_UNDO, GRAYED
MENUITEM SEPARATOR
MENUITEM "Cu&t\tCtrl+X", IDM_CUT, GRAYED
MENUITEM "&Copy\tCtrl+C", IDM_COPY, GRAYED
MENUITEM "&Paste\tCtrl+V", IDM_PASTE, GRAYED
MENUITEM "Paste &Link", IDM_LINK, GRAYED
MENUITEM SEPARATOR
MENUITEM "Lin&ks...", IDM_LINKS, GRAYED
END
POPUP "&Help"
BEGIN
MENUITEM "&Contents", IDM_HELPCONTENTS, GRAYED
MENUITEM "&Search for Help On...", IDM_HELPSEARCH, GRAYED
MENUITEM "&How to Use Help", IDM_HELPHELP, GRAYED
MENUITEM SEPARATOR
MENUITEM "&About Generic...", IDM_ABOUT
END
END
AboutBox DIALOG DISCARDABLE 22, 17, 144, 75
STYLE DS_MODALFRAME | WS_CAPTION | WS_SYSMENU
CAPTION "About Generic"
BEGIN
CTEXT "Windows 95", -1,0, 5,144,8
CTEXT "Generic Application",-1,0,14,144,8
CTEXT "Version 1.0", -1,0,34,144,8
DEFPUSHBUTTON "OK", IDOK,53,59,32,14,WS_GROUP
END
事实上,这里谈到的消息映射是MFC中消息映射表格的雏形,下面所采用的结构名称和变量名称都与MFC相同。
首先,定义一个MSGMAP_ENTRY结构和一个dim宏:
struct MSGMAP_ENTRY {
UINT nMessage;
LONG (*pfn) (HWND, UINT, WPARAM, LPARAM);
// pfn是一个指向nMessage消息的处理函数的指针
};
#define dim(x) (sizeof(x) / sizeof(x[0])) // 用于计算数组的长度,之后要用到
接下来,再构建两个数组_messageEntries[]和_commandEntries[],把程序中欲处理的消息以及相应的消息处理函数关联起来,这样就形成了一张消息映射表格:
struct MSGMAP_ENTRY _messageEntries[] =
{
WM_CREATE, OnCreate, // 消息, 相应的消息处理函数
WM_PAINT, OnPaint,
WM_SIZE, OnSize,
WM_COMMAND, OnCommand,
WM_SETFOCUS, OnSetFocus,
WM_CLOSE, OnClose,
WM_DESTROY, OnDestory
};
下面是命令行命令映射表:
struct MSGMAP_ENTRY _commandEntries =
{
IDM_ABOUT, OnAbout, // WM_COMMAND, 命令处理程序
IDM_FILEOPEN, OnFileOpen,
IDM_SAVEAS, OnSaveAs
};
这样一来,窗口函数就可以这么设计:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
int i;
for(i = 0; i < dim(_messageEntries); i++) {
// 处理消息
if(message == _messageEntries[i].nMessage)
return (* _messageEntries[i].pfn) (hWnd, message, wParam, lParam);
}
return DefWindowProc(hWnd, message, wParam, lParam);
}
LONG OnCommand(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
int i;
for(i = 0; i < dim(_commandEntries); i++) {
// 处理命令
if(message == _commandEntries[i].nMessage)
return (* _commandEntries[i].pfn) (hWnd, message, wParam, lParam);
}
return DefWindowProc(hWnd, message, wParam, lParam);
}
// 消息处理函数定义
LONG OnCreate(HWND hWnd, UINT wMsg, UINT wParam, LONG lParam)
{
...
}
// 命令处理函数定义
LONG OnAbout(HWND hWnd, UINT wMsg, UINT wParam, LONG lParam)
{
...
}
可见,WndProc()和OnCommand()函数永远不需要改变,每当要新添加一组消息处理,只要在_messageEntries[]和_commandEntries[]两个数组中加上新元素,再额外为新消息定义相应的处理函数。
Windows内的对话框根据其与父窗口的关系,可分为两类:模态对话框(modal)和非模态对话框(modeless)。Modal对话框的激活与结束,靠的是DialogBox和EndDialog两个API函数。
为了做出一个对话框,程序员必须准备两样东西:
对话框处理过消息之后,应该传回TRUE;而如果未处理消息,则应该传回FALSE。这是因为你自定义的对话框函数之上还有一个系统提供的预设对话框函数,如果自定义的对话框函数没有处理好消息,则该消息就会传递给预设对话框函数来处理。
Windows程序需要一个模块定义文件,将模块名称,程序节区和数据节区的内存特性,模块堆积(heap)大小,堆栈(stack)大小,所有callback函数名称等记录下来。
下面是一个模块定义文件的示例:
NAME Generic
DESCRIPTION 'Generic Sample'
EXETYPE WINDOWS
STUB 'WINSTUB.EXE'
CODE PRELOAD DISCARDABLE
DATA PRELOAD MOVEABLE MULTIPLE
HEAPSIZE 4096
STACKSIZE 10240
EXPORTS
MainWndProc @1
AboutBox @2
在 Visual C++ 集成环境下开发程序,不再需要特别准备.DEF 文件,因为模块定义文件中的设定都有相应的默认值。
模块定义文件中的 STUB 指令用来指定所谓的stub 程序(埋在Windows
程序中的一个 DOS 程序,当你在非MS_DOS系统下打开EXE文件的二进制码时,你所看到的This Program Requires Microsoft Windows 或This
Program Can Not Run in DOS mode 就是此程序发出来的)。Win16 允许程序员自设一个stub 程序,但Win32 不允许,换句话说在Win32 之中Stub 指令已经失效。
资源描述文件.RC是一个以文本格式描述UI资源的文件,常用的资源大约有9项,分别是ICON,CURSOR,BITMAP,FONT,DIALOG,MENU,ACCELERATOR,STRING,VERSIONINFO。当然,还可能有新的资源不断加入,如Visual C++ 4.0就加入了一种名为TOOLBAR的资源。
这些文字描述经过RC编译器后,可产生可使用的二进制代码.res文件。
前面我们已经谈到了windows消息传递机制,这里我们以窗口的诞生与销毁来说明消息的发生与传递,以及应用程序的启动和结束。
所谓空闲时间(idle time),就是指[系统中没有任何消息在队列中等待处理]。举个例子,当没有任何程序使用定时器(timer,它会定时发送WM_TIMER消息)且使用者也没有触碰键盘和鼠标等任何外围设备时,此时系统就处于空闲时间。
空闲时间不是特例,事实上,它常常发生。不要以为移动鼠标时会产生一大堆的WM_MOUSEMOVE,实际上,夹杂在每一个WM_MOUSEMOVE之间的时间间隔就可能存在许多空闲时间。毕竟,计算机的速度不可想象。
传统的SDK程序如果要处理空闲时间,可以使用下列消息循环:
while (TRUE) {
if(PeekMeesage(&msg, NULL, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT)
break;
TranslateMessage(&msg);
DispatchMessage(&msg);
} else {
OnIdle(); // 空闲时间处理函数
}
}
PeekMessage()和GetMessage()在某些情况下表现不同。它们都是到消息队列中抓取消息,如果抓不到(没有消息等待处理),程序的主执行线程(primary thread,是一个UI执行线程)会被操作系统设为等待状态(线程等待)。
而当过一段时间后,操作系统再次打算分配时间给该线程时,发现消息队列仍然是空的,那么这时候两个API函数的行为就有点不同了:
很多人把DOS程序和Console程序混为一谈,认为它们是等价的,这显然是不对的。
windows环境下的DOSBox中,利用Windows编译器,链接器生成的可执行文件,实际上都是Win32程序。
如果程序是以main()函数作为入口点,调用的是C Runtime函数和(不包含GUI)的win32 API函数,那么它就是一个console程序,console窗口将成为这个程序的标准输入和输出端口。
Console程序可以调用部分的Win32 API(尤其是KERNEL32.DLL模块提供的那一部分),所以它可以使用Windows提供的各种高级功能。它可以产生进程(processes),产生执行线程(threads),取得虚拟内存的信息。但是它不能有华丽的外观,因为它不能调用与GUI有关的各种API函数。
核心对象可以看做是系统的一种资源,核心对象一旦产生,任何应用程序都可以开启并使用该对象。通常为我们所熟知的GDI对象就是一种核心对象,它可以是利用GDI函数所产生的一支画笔(Pen)或一把画刷(Brush)。
系统给予核心对象一个计数值(usage count)作为管理之用,核心对象主要包括以下数种:
前三者用于执行线程的同步化,file-mapping对象用于内存映射文件(memory mapping file)。
这些核心对象的产生方式(使用不同的API)都略有不同,但都会获得一个句柄handle作为标识。每被使用一次,其对应的计数值就加1。几个核心对象的结束方式都是一样的,调用CloseHandle即可。
执行一个程序,必然会产生一个进程(process),最直接的程序执行方式就是在shell中双击某一个可执行文件(这里假定其为App.exe)的图标。执行起来的App进程其实就是shell调用CreateProcess()激活的。
让我们来看看一个进程从激活到死亡的整个流程:
可以这样说,通过这种方式执行起来的所有Windows程序,都是shell的子进程。
产生子进程需要使用CreateProcess()函数。
CreateProcess (
LPCSTR lpApplicationName, // 可执行文件文件名
LPSTR lpCommandLine, // 欲传给新进程的命令行参数
LPSECURITY_ATTRIBUTES lpProcessAttributes, // 指定核心对象的安全属性
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles, // 设定这些安全属性是否要被继承
DWORD dwCreateFlags, // 一些影响进程建立的常数值
LPVOID lpEnvironment, // 指定进程所使用的环境变量区(如果要让子进程继承父进程的环境变量,则这里要设为NULL)
LPCSTR lpCurrentDirectory, // 指定子进程的工作目录与工作磁盘
LPSTARTUPINFO lpStartupInfo, // 一个指向LPSTARTUPINFO结构的指针,它可以用于设置窗口的标题,位置和大小等
LPPROCESS_INFORMATION LPPROCESS_INFORMATION // 一个指向LPPROCESS_INFORMATION结构的指针
);
LPPROCESS_INFORMATION结构体的定义如下:
typedef struct _PROCESS_INFORMATION
{
HANDLE hProcess;
HANDLE hThread;
DWORD dwProcessId;
DWORD dwThreadId;
} PEOCESS_INFORMATION;
当系统会我们产生[进程对象]和[执行线程对象]时,它会把两个对象的handle填入此结构的相关字段中,应用程序可以从这里获得这些handle。
当一个进程像结束自己的生命时,只要调用VOID ExitProcess(UINT fuExitCode);
即可。