加载到进程地址空间的每一个可执行文件或者DLL文件都被赋予了一个独一无二的实例句柄。可执行文件的实例被当作(w)WinMain函数的第一个参数hInstanceExe传入。在需要加载资源的函数调用中,一般都要提供此句柄的值。例如,为了从可执行文件的映像中加载一个图标资源,就需要调用下面这个函数:
HICON LoadIcon (
HINSTANCE hInstance,
PCTSTR pszIcon);
LoadIcon函数的第一个参数指出了哪个文件(可执行文件或DLL文件)包含了想要加载的资源。许多应用程序会将(w)WinMain函数的hInstanceExe参数保存在一个全局变量中,使其可以很容易被可以执行文件的所有代码访问到。
Platform SDK文档指出,有的函数需要一个HMODULE类型的参数。下面的GetModuleFileName函数就是一个例子:
DWORD GetModuleFileName (
HMODULE hInstMOdule,
PTSTR pszPath,
DWORD cchPath );
说明 事实上,HMODULE和HINSTANCE完全是一回事。如果某个函数的文档指出需要一个HMODULE参数,我们可以传入一个HInstance,反之亦然。之所以会有两种数据类型,是由于在16位windows中,HMODULE和HINSTANCE表示不同类型的数据。
(w)WinMain的hInstanceExe参数的实际值是一个内存基地址;系统将可执行文件的映像加载到进程空间中的这个位置。例如,假如系统打开可执行文件,并将它的内容加载到地址0x00400000,则(w)WinMain的hInstanceExe参数值就是0x00400000。
可执行文件的映像具体加载到哪一个基地址,是由链接器决定的。不同的链接器使用不同的默认基地址。由于历史原因,VS链接器使用的默认基地址是0x00400000,这是在运行windows 98时,可执行文件的映像能加载到的最低的一个地址。使用Microsoft链接器的/BASE:address链接器开关,可以更改应用程序加载到哪个基地址。
为了知道一个可执行文件或DLL文件加载到进程地址空间的什么位置,可以使用如下所示的GetModuleHandle函数来返回一个句柄/基地址:
HMODULE GetModuleHandle(PCTSTR pszModule);
调用这个函数时,要传递一个0-end字符串,它指定了已在主调进程的地址空间中加载的一个可执行文件或DLL文件的名称。如果系统找到了指定的文件,GetModuleHandle会返回可执行文件/DLL文件加载到的基地址。如果没有找到文件则返回NULL。GetModuleHandle的另一个用法是传入NULL,则它返回主调进程可执行文件的基地址。如果我们的代码在一个DLL中,那么可以利用两种方法来了解代码正在什么模块中运行。第一个办法是利用链接器提供的伪变量__ImageBase,它指向当前正在运行模块的基地址。如前所述,这是C运行库的启动代码在调用我们的(w)WinMain函数时所做的事情。
第二种方式是调用GetModuleHandleEx,将GET_MODULE_HANDLE_EX_ FLAG_FROME_ADDRESS作为它的第一个参数,将当前函数地址作为第二个参数。最后一个参数是指向HMODULE的指针,GetModuleHandleEx会用传入函数(第二个参数)所在DLL的基地址来填写该指针。下面代码演示了这几种用法:
extern "C" const IMAGE_DOS_HEADER __ImageBase;
void DumpModule(){
HMODULE hModule = GetModuleHandle(NULL);
_tprintf(TEXT("with GetModuleHandle(NULL) = 0x%x\r\n"),hModule);
_tprintf(TEXT("with __ImageBase = 0x%x\r\n"),(HINSTANCE)&__ImageBase);
hModule = NULL;
GetModuleHandleEx(
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
(PCTSTR)DumpModule,
&hModule);
_tprintf(TEXT("with GetModuleHandleEx(...) = 0x%x\r\n"),hModule);
}
int _tmain(int argc, _TCHAR* argv[])
{
DumpModule();
return 0;
}
记住GetModuleHandle函数的两个重要特征。首先,它只检查主调用进程的地址空间。如果主调进程地址空间没有用到任何通用对话框函数,那么一旦调用GetModuleHandle,并向其中传递ComDlg32,就会导致返回NULL——即使ComDlg32也许已经加载到其他进程的地址空间了。其次,调用GetModuleHandle并传NULL,会返回进程地址空间中的可执行文件的基地址。所以即使调用GetModuleHandle(NULL)的代码在一个DLL中,返回值仍是可执行文件的基地址,而不是DLL的基地址。
C/C++运行库总是向(w)WinMain的hPrevInstance参数传递NULL。该参数用于16位windows系统,因而仍然将其保留为(w)WinMain的一个参数,目的只是为了我们移植16位windows程序。绝对不要在自己的代码中引用这个参数。因此,最好像下面这样写自己的(w)WinMain函数:
int WINAPI _tWinMain (
HINSTANCE hInstanceExe,
HINSTANCE,
PSTR pszCmdLine,
Int nCmdShow);
由于没有为第二个参数指定参数名,所以编译器不会报告一个“参数没有被引用到”的警告。
系统的创建一个新进程的时候,会传一个命令行给它。这个命令行几乎总是非空的;至少,用于创建新进程的可执行文件的名称是命令行上的第一个标记(Token)。不过在调用CreateProcess函数的时候能接受只由一个字符构成的命令行,即用于终止字符的0。C运行库的启动代码开始执行一个GUI程序的时候,会调用windows函数GetCommandLIne来获取进程的完整命令行,忽略可执行文件的名称,然后将指向命令行剩余部分的一个指针传给WinMain的pszCmdLine参数。
应用程序可以通过自己选择的任何一种方式来分析和解释命令行字符串。甚至我们可以写数据到pszCmdLine指向的内存缓冲区,但在任何情况下,我们都不应该写入,最好把它当作一个只读缓冲区对待。如果要对命令行进行改动,最好首先把命令行缓冲区复制到应用程序的一个本地缓冲区,然后再对本地缓冲区进行修改。
我们也可以效仿C运行库的做法,调用GetCommandLIne函数来获取一个指向进程完整命令行的指针:
PTSTR GetCommandLine();
该函数返回一个缓冲区指针,缓冲区中包含完整的命令行(包括可执行文件的完整路径名)。注意GetCommandLine总是返回同一个缓冲区的地址。这是不应该向pszCmdLine写入的另一个理由:它指向同一个缓冲区,修改它之后,我们就没办法知道原来的命令行是什么了。
虽然Microsoft反对继续使用全局变量__argc和__argv(或__wargv),但是应用程序仍然可以利用它们来访问命令行的每个标记。利用ShellAPI.h文件中声明并由Shell32.dll导出的函数CommandLineToArgW即可将任何Unicode字符串分解成单独的标记:
PWSTR* CommandLineToArgW (
PWSTR pszCmdLine,
int* pNUmArgs);
这个函数只有Unicode版本。参数pszCmdLine指向一个命令行字符串。这通常是GetCommandLineW函数的返回值。pNUmArgs会被设为命令行中实参的数目。CommandLineToArgW返回的是一个Unicode字符串指针数组的地址。
CommandLineToArgW在内部分配内存。许多应用程序不会释放这部分内存——它们依靠操作系统在进程终止时释放这块内存。这是完全可以接受的,不过,如果自己想释放内存,正确的做法是调用HeapFree:
int pNumArgs;
PWSTR *ppArgv = CommandLineToArgvW(GetCommandLineW(), &pNumArgs);
if(*ppArgv[1] == L"x"){
//...
}
HeapFree(GetProcessHeap(),0,ppArgv);