4.1 编写第一个Windows应用程序
(1)进程的组成:(两个部分)
①进程也是一个内核对象(即进程内核对象),操作系统用它来管理进程,也是系统保存进程统计信息的地方。
②进程是一个地址空间,包含可执行文件或DLL模块的代码和数据,还包含动态内存分配,如线程堆栈或堆的分配。
(2)应用程序类型和相应的入口点函数
应用程序类型 |
CC++入口点函数 |
嵌入可执行文件的 启动函数 |
链接器开关 |
处理ANSI字符(串)的GUI程序 |
_tWinMain(WinMain) |
WinMainCRTStartup |
/SUBSYSTEM:WINDOWS |
处理Unicode字符(串)的GUI程序 |
_tWinMain(wWinMain) |
wWinMainCRTStartup |
|
处理ANSI字符(串)的CUI程序 |
_tmain(Main) |
mainCRTStartup |
/SUBSYSTEM:CONSOLE |
处理Unicode字符(串)的CUI程序 |
_tmain(Wmain) |
wmainCRTStartup |
★两者界线是模糊的,即可以创建向控制台输出文字的GUI或能显示GUI的控制台应用程序。
(3)C/C++嵌入的启动函数
①C/C++应用程序中,进程启动过程wWinMainCRTStartup()→_tmainCRTStartup()→wWinMain()。由此可见操作系统并不调用我们写的入口点函数(如_tWinMain),而是调用C/C++运行期的启动函数(如wWinMainCRTStartup)。
②wWinMainCRTStartup的作用:——启动函数要进行一些额外的操作:
A、检索指向新进程的完整命令行的指针。
B、检索指向新进程的环境变量的指针。
C、对C/C + +运行期的全局变量进行初始化。如果包含了Stdlib.h 文件,代码就能访问这些变量。(如__environ、__argv等,见课本第69页,表4-2)
D、对C 运行期内存单元分配函数(malloc 和calloc)和其他低层输入/输出程序使用的堆进行初始化。这样就可以调用malloc和free之类的C库函数。
E、为所有全局和静态C++类对象调用构造函数。以确保已经声明的任何C++全局对象和静态对象能够在代码执行以前正确地创建。
F、如果是GUI程序,这里还将调用GetStartupinfo(&StartupInfo)函数,以获取一个进程的启动信息。
GetStartupInfo(&StartupInfo);
Int nMainRetVal= WinMain((HINSTANCE)&__ImageBase,NULL,pszCommandLine,
(StartupInfo.dwFlags &STARTF_USESHOWWINDOW)?StartupInfo.wShowWindow:SW_SHWODEFAULT);
其中__ImageBase是链接器定义的伪变量,表示映射到内存地位置。
G、调用我们写的入口函数(如WinMain)等函数,该入口函数返回后,嵌入的启动函数会调用C运行库exit函数,该函数将调用由_onexit添加的回调函数(调用顺序与添加顺序相反),然后清除全局对象和静态变量,最后调用系统ExitProcess函数,退出进程。
(4)如果链接器选项的“入口点”不设置的话(默认情况),C/C++程序入口点是被嵌入到可执行文件的WinMainCRTStartup启动函数(注意,不是WinMain)。如果设定入口点为我们指定的函数,这时不再嵌入那些启动函数。因此,所有的初始化和退出时清理全局变量的操作也得由我们自己来完成。(这对于纯API应用程序来说,有很好用的)
【OnExit程序】WinMainCRTStartup的执行流程
#include <tchar.h> #include <Windows.h> #include <strsafe.h> #define GRS_USEPRINTF() TCHAR pBuf[1024]={} //可变参数...与可变宏__VA_ARGS__ //宏定义中参数列表的最后一个参数为省略号(也就是三个点)。 //预定义宏__VA_ARGS__,当宏替换时,用来替换省略号所代表的字符串 #define GRS_PRINTF(...) StringCchPrintf(pBuf, 1024, __VA_ARGS__); WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), pBuf, lstrlen(pBuf), NULL, NULL); int fn1(void); int fn2(void); int fn3(void); int fn4(void); //注意以下创建的是GUI应用程序,但却使用控制台来输出! int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR pszCmdLine, int nShowCmd) { AllocConsole(); //为进程分配一个新的控制台(注意一个进程只可以拥用一个控制台的关联) GRS_USEPRINTF(); //因为WinMainCRTStartup函数会调用WinMain函数,但WinMain执行完毕后, //WinMainCRTStartup会继续调用由_onexit注册的回调函数且调用顺序与 //注册函数的顺序相反,所以当WinMain退出后,fn4、fn3、fn2、fn1会依次被调用。 //但注意,这4个函数只有在退出WinMain后才被调用。 _onexit(fn1); _onexit(fn2); _onexit(fn3); _onexit(fn4); GRS_PRINTF(_T("注意哦,我是第1个被输出的语句! ")); GRS_PRINTF(_T("回调函数的注册顺序:fn1->fn2->fn3->fn4 ")); GRS_PRINTF(_T("回调函数的执行顺序:")); return 0; //注意,该函数退出后,仍会执行退出回调函数fnX等函数 } int fn1() { GRS_USEPRINTF(); GRS_PRINTF(_T("->fn1 ")); _tsystem(_T("PAUSE")); FreeConsole(); return 0; } int fn2() { GRS_USEPRINTF(); GRS_PRINTF(_T("->fn2")); return 0; } int fn3() { GRS_USEPRINTF(); GRS_PRINTF(_T("->fn3")); return 0; } int fn4() { GRS_USEPRINTF(); GRS_PRINTF(_T("fn4")); return 0; }
4.1.1 进程实例句柄
(1)GetModuleHandle函数——获取可执行文件或DLL文件被加载到进程地址空间的位置
参数 |
描述 |
LPCTSTR lpModuleName |
为NULL,获得主调进程的可执行文件的基地址,注意在DLL内部用这参数获得到是仍是宿主进程的基地址,而不是DLL模块的基地址! 不为NULL,如GetModuleHandle(TEXT("kernel32.dll")),将获取Kernel32模块的基地址。(注意,这是在DLL外部调用的,与上面不同!) |
返回值 |
成功——返回模块的基地址 失败——返回NULL,可以GetLastError获取错误代码信息。 |
两大特征 |
①只检查主调进程的地址空间,如果主调进程没有使用任何通话对话框函数,则GetModuleHandle(TEXT("ComDlg32.dll"))将返回NULL,即使其他进程己经将该库加载到内存里。 ②如果传递NULL参数,将返回可执行文件的基地址,即使在一个DLL文件内部调用,返回值仍然是可执行文件的基地址,而非DLL文件的基地址! |
(2)GetModuleHandleEx函数
参数 |
描述 |
DWORD dwFlags |
①为0,则当调用该函数时,模块的引用计数自动增加,调用者在使用完模块句柄后,必须调用一次FreeLibrary。 ②如果是GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,则同GetModuleHandle相同,不增加引用计数 ③如果是GET_MODULE_HANDLE_EX_FLAG_PIN,则模块一直映射在调用该函数的进程中,直到该进程结束,不管调用多少次FreeLibrary ④如果是GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,则lpModuleName是模块中的一个地址。 |
LPCTSTR lpModuleName |
与GetModuleHandle参数一样 |
HMODULE *phModule |
存储要找的句柄 |
返回值 |
成功——返回非0 失败——返回0,可以GetLastError获取错误代码信息。 |
备注 |
_ImageBase、GetModuleHandleEx与GetModuleHandle不同,会根据代码放置的模块,来决定是获得可执行文件或DLL库的基地址 |
注意:进程的实例句柄,可以在“链接器”→“高级”→“基址”中指定(如0x00400000),同时 “随机基址”设为“否”
【DumpModule程序】获取进程与DLL库的基地址
//动态链接库
//DumpModule.h
#pragma once; #ifdef _cplusplus #ifdef API_EXPORT #define EXPORT extern "C" __declspec(dllexport) //当头文件供动态库本身使用时 #else #define EXPORT extern "C" __declspec(dllimport) //当头文件供调用库的程序使用时 #endif #else #ifdef API_EXPORT #define EXPORT __declspec(dllexport) //当头文件供动态库本身使用时 #else #define EXPORT __declspec(dllimport) //当头文件供调用库的程序使用时 #endif #endif EXPORT void DumpModule();
//DumpDll.c
#define API_EXPORT #include <windows.h> #include "DumpModule.h" #include <stdio.h> extern const IMAGE_DOS_HEADER __ImageBase; HINSTANCE hDll; //入口和退出点 int WINAPI DllMain(HINSTANCE hInstance, DWORD fdwReason, PVOID pvReserved) { hDll = hInstance; return TRUE; } //在DLL中用两种不同的方法来获取可执行程序及DLL库基地址 //注意两点: //1、无论GetModuleHandle放在DLL中,还是可执行文件中,获取的都是可执行文件的基地址 //2、__ImageBase、GetMoudleHandleEx,会根据代码放置位置,也决定是获得可执行文件 //或DLL库的基地址。 EXPORT void DumpModule() { //先获取可执行文件的基地址 printf("可执行文件的基地址:"); //利用GetModuleHandle来获取 HMODULE hModule = GetModuleHandle(NULL); printf("GetModuleHandle(NULL) = 0x%X ", hModule); printf("利用3种方法获取文件或(DLL库)的基地址, "); printf("因以下代码在DLL中调用,所以获取的是DLL的基地址: "); //先获取DLL或被加载的基地址 printf("方法1:通过DllMain参数获取的基地址 = 0x%X ",hDll); //方法2:利用链接器的伪变量__ImageBase来获取 printf("方法2:伪变量__ImageBase = 0x%X ", (HINSTANCE)&__ImageBase); //方法3:利用GetModuleEx函数来获取 hModule = NULL; GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (PCTSTR)DumpModule, &hModule); //将DumpModule自身的地址传进去 printf("方法3:GetModuleHandleEx =0x%X ", hModule); }
//测试程序
#include <windows.h> #include "..\4_DumpModuleDll\DumpModule.h" #include <tchar.h> #pragma comment(lib,"..\..\Debug\4_DumpModuleDll.lib") int _tmain(int arc, TCHAR* argv[]) { DumpModule(); return 0; }
4.1.4 进程环境变量
(1)环境变量的格式
=::=::... //不能作为环境变量使用,与进程当前目录有关
varName1=VarValue1 //注意等号前、后的空格也算是变量名或值的一部分。
varName2=varValue2
varName3=varValue3
varNameX=varValueX