写在前面:
最近在研究一个VC界面库DuiLib,在细读它的源码时遇到些问题,比如
它的界面是如何绘制上去的,底层操作是如何实现的,就是CreateWindow和
ShowWindow又是如何实现的, 也不得而知, 因此我想有必要重新认识下Win32
应用程序的启动/运行原理才好。
如题所述, 本文讲的的Windows环境下exe可执行文件的运行原理, 这里
面涉及的知识很多,需要读者对Windows操作系统(如注册表、进程、线程、
内存管理、PE文件格式等) 、Windows编程(本文使用c++ 语言)等有所了解。
本文试图以通俗易懂的语言描述, 让更多的人看的懂, 从运行原理上对程序的运
行有个好的了解。
文章安排方面, 我这里是以大家都懂的main/WinMain函数执行前, 执行时,
执行后分为三个部分:exe程序的初始化;主函数的运行过程;程序收尾工作
PS:本人的技术也是有限的,文章中难免会有错误疏漏之处,还请各位高手批
评指正。转载请注明出处,谢谢
本文使用的例子444.exe程序下载地址:
下载地址1:
http://183.60.157.31/file/D4/53/tzydH05zcHTmkwTlAE44iue6lUk204.zip?key=n5f6
a61fc1a1fb190d2b250dc7cd74e2ce2082ccc1b4d4178e&uid=&token=l1az8k
lrmhi8bwxl49201cxedj&dir=%2F&name=xunlei.zip
下载地址2:
http://115.com/file/clg0o6il#
一、exe程序的初始化
打开一个软件是如此简单, 双击软件的图标就是了! 但你是否想过, 当你双
击那个图标时,系统都做了哪些工作?为什么双击图标软件就运行起来了?
没错, 就是Shell (以Explorer.exe进程实现) 。 当你启动电脑进入桌面时,
系统创建Explorer.exe进程, 而其它的进程, 可以说都是Explorer.exe的子进
程, 因为它们都是由Explorer.exe进程创建的。 也就是说, 当你双击图标时Shell
会侦测到这个动作, 注册表中有相关的项保存着双击操作的信息, 如exe文件关
联、启动exe的Shell是哪个。
注册表中保存的exe文件关联信息
打开一个exe时指定的参数信息
指定启动exe程序的Shell
我们看到, 启动exe文件指定的Shell就是Explorer.exe啦。 因此, 我们应该
知道了,双击exe文件图标时Explorer.exe进程的一个线程会侦测到这个操作,
它根据注册表中的信息取得文件名(根据%1这个参数) ,然后Explorer.exe以这
个文件名调用CreateProcess函数,这个函数做了很多工作。
CreateProcess函数的定义是这样的:
BOOLWINAPICreateProcess(
__in_optLPCTSTRlpApplicationName,
__inout_optLPTSTRlpCommandLine,
__in_optLPSECURITY_ATTRIBUTESlpProcessAttributes,
__in_optLPSECURITY_ATTRIBUTESlpThreadAttributes,
__inBOOLbInheritHandles,
__inDWORDdwCreationFlags,
__in_optLPVOIDlpEnvironment,
__in_optLPCTSTRlpCurrentDirectory,
__inLPSTARTUPINFOlpStartupInfo,
__outLPPROCESS_INFORMATIONlpProcessInformation
);
此函数的具体信息和用法在MSDN上已有详细的描述,在此就不作介绍了
http://msdn.microsoft.com/en-us/library/ms%28v=VS.85%29.aspx
那么CreateProcess函数内部都做了哪些工作呢?别急,马上为你一一道来!
创建进程内核对象
CreateProcess实际上是通过NtCreateProcess函数实现的, 此时, 系统会创建
一个被称为内核对象的对象, 这里是进程内核对象。 进程内核对象可以看作一个
操作系统用来管理进程的内核对象, 它也是系统用来存放关于进程统计信息的地
方(一个小的数据结构) ,进程内核对象维护了一个句柄表的结构。
当进程被初始化之后, 其句柄表是空的。 当进程内的一线程通过指定的函数
创建了一个内核对象时,内核会为对象分配一块内存区域并初始化这块区域。 然
后内核会在进程的句柄表中查找一个空的入口, 找到之后会初始化句柄表的以索
引定位的区域。 初始化的主要过程就是填充句柄表的一个单元, 包括指定内核对
象地址,指定访问码,指定标记等。
关于句柄表的描述,可以看看<< Windows进程内核对象句柄表>> 这篇文章
创建进程的虚拟地址空间
进程内核对象创建后, 它的引用计数被置为1。 然后系统为刚刚创建的进程
分配4GB的进程虚拟地址空间。 之所以是4GB, 是因为在32位的操作系统中,
一 个 指 针 长 度 是4字 节 , 而4字 节 指 针 的 寻 址 能 力 是 从
0x00000000~0xFFFFFFFF最大值0xFFFFFFFF表示的即为4GB大小的容量。
与虚拟地址空间相对的, 还有一个物理地址空间, 这个地址空间对应的是真
实的物理内存。如果你的计算机上安装了512M大小的内存,那么这个物理地
址空间表示的范围是0x00000000~0x1FFFFFFF。
当操作系统做虚拟地址到物理地址映射时, 只能映射到这一范围, 操作系统
也只会映射到这一范围。 当进程创建时, 每个进程都会有一个自己的4GB虚拟
地址空间。 要注意的是这个4GB的地址空间是 “虚拟” 的, 并不是真实存在的,
而且每个进程只能访问自己虚拟地址空间中的数据,无法访问别的进程中的数
据, 通过这种方法实现了进程间的地址隔离。 那是不是这4GB的虚拟地址空间
应用程序可以随意使用呢?很遗憾, 在Windows系统下, 这个虚拟地址空间被
分成了4部分:NULL指针区、用户区、64KB禁入区、内核区。应用程序
能使用的只是用户区而已,大约2GB左右(最大可以调整到3GB)。内核区
为2GB, 内核区保存的是系统线程调度、 内存管理、 设备驱动等数据, 这部分
数据供所有的进程共享,但应用程序是不能直接访问的。
虚拟地址空间如何划分
每个进程的虚拟地址空间都要划分成各个分区。地址空间的分区是根据操
作系统的基本实现方法来进行的。不同的Windows内核,其分区也略有不同。 下
图显 示 了 每 种 平 台 是 如 何 对 进 程 的 地 址 空 间 进 行 分 区 的 。
32位Windows2000的内核与64位Windows2000的内核拥有大体相同的分
区,差别在于分区的大小和位置有所不同。另一方面,可以看到Windows98下
的分区有着很大的不同。
初始化进程的虚拟地址空间
进程地址空间创建后, Windows的装载器 (loader, 也称为PE装载器) 开始
工作。Loader会读取exe文件的信息(PE文件) ,这里又涉及到PE文件格式的
知识,如不了解,可能会对下面的理解有些难度。赶快去百度充下电啦! !
此时loader会检查PE文件的有效性,如果PE文件有错误,可能会显示出
Thisprogramcannotberunindosmode,这样就启动不了啦。没有错误了,就把
PE文件的内容(二进制代码)映射到进程的地址空间中,原则是低地址的映射
到低地址的虚拟地址空间, 高地址的映射到高地址的虚拟地址空间。 实际上映射
时是增高的地址空间,因为PE文件中和地址空间的对齐方式大小不一样,你可
以通过/align开关调整这个数值。
然后是读取PE文件的导入地址表(ImportTable) ,这里存放有exe文件需
要导入的模块文件 (DLL) , 系统会一一加载这些dll到进程的地址空间中, 具体
做法是调用LoadLibrary函数加载程序代码到某个地址,然后系统会映射这些代
码到进程的地址空间中,要知道dll只需加载一次就可映射到所有进程的地址空
间中, 并为每个dll维护一个引用计数, 当引用计数为0时, dll就从内存中卸载
掉,释放占用的内存。Dll里面可能又引用了其它的dll,因此加载dll时是递归
形式的,直到加载完ImportTable里描述的所有dll模块,此时进程初始化部分
完成。
创建进程的主线程
当进程的初始化完成后, 开始创建进程的主线程, 一个进程至少要有一个主
线程才能运行, 可以说进程只是充当一个容器的作用, 而线程才是执行用户代码
的载体。
线程是用CreateThread这个函数创建的,它的定义如下:
HANDLEWINAPICreateThread(
__in_optLPSECURITY_ATTRIBUTESlpThreadAttributes,
__inSIZE_TdwStackSize,
__inLPTHREAD_START_ROUTINElpStartAddress,
__in_optLPVOIDlpParameter,
__inDWORDdwCreationFlags,
__out_optLPDWORDlpThreadId
);
该函数的具体信息请看MSDN的说明:
http://msdn.microsoft.com/en-us/library/ms%28v=VS.85%29.aspx
创建线程时,也和进程相似,系统会创建线程内核对象,初始化线程堆栈。
线程堆栈有两个, 一个是核心堆栈, 由核心态维护; 另一个是用户堆栈, 运行在
用户态下。同样的,线程的引用计数也置为1。
当进程、线程创建完成后(进程地址空间也有了,需要的dll也都加载完毕
了) ,CreateProcess函数返回,相关的信息会保存在PROCESS_INFORMATION
结构中,它的定义如下:
typedefstruct_PROCESS_INFORMATION{
HANDLEhProcess;
HANDLEhThread;
DWORDdwProcessId;
DWORDdwThreadId;
}PROCESS_INFORMATION,*LPPROCESS_INFORMATION;
可以看到是进程(线程)的句柄和进程(线程)的ID,有了句柄那样调用
CloseHandle函数并把句柄传入就使用引用计数减1,若引用计数为0了,就从
内存中卸载释放内存。还有一个是进程(线程)ID,这个参数可作为
OpenProcess(OpenThread)的传入参数打开一个已存在的进程(线程) 。
而其它的信息,如进程使用的环境变量(lpEnvironment指定) ,启动信息则
是保存在STARTUPINFO这个结构中,它被定义为:
typedefstruct_STARTUPINFO{
DWORDcb;
LPTSTRlpReserved;
LPTSTRlpDesktop;
LPTSTRlpTitle;
DWORDdwX;
DWORDdwY;
DWORDdwXSize;
DWORDdwYSize;
DWORDdwXCountChars;
DWORDdwYCountChars;
DWORDdwFillAttribute;
DWORDdwFlags;
WORDwShowWindow;
WORDcbReserved2;
LPBYTElpReserved2;
HANDLEhStdInput;
HANDLEhStdOutput;
HANDLEhStdError;
}STARTUPINFO,*LPSTARTUPINFO;
由于此结构的成员较多,就不详细介绍了,看MSDN上的解释吧
http://msdn.microsoft.com/en-us/library/ms%28v=VS.85%29.aspx
如果你想取得进程的启动信息, 可以调用GetStartupInfo这个函数。 事实上,
在下面要讲到的运行期库代码中,就调用了此函数以取得启动相关的信息。
C/C++ 运行期库的初始化
当进程的主线程初始化完成后, 并且线程得到了CPU时间片, CPU把CS:IP
指向程序入口点(OEP) ,这里以444.exe程序(一个普通的Windows程序,加
入了一些跟踪消息的输出)为例。
首先使用Stud_PE工具查看444.exe的PE文件信息,如下图所示:
可以看到444.exe的程序入口点是000111B3(下面那个000005B3的地址是
PE文件中的相对虚拟地址,而我们要是OEP是指映射到地址空间中的地址) 。
注意这个地址是相对虚拟地址,还要加上基址(这里是00)才是OEP的
地址, 所以OEP=00+000111B3=004111B3,这个地址相当重要, 因为
这是程序运行时CS:IP指向的地址,即:程序运行的第一条指令就在这个地址
处! !
下面我们来使用PEBrowse工具反汇编下.text节的内容,.text节就是存放代
码(指令)的节,关于节(section)的概念请查阅PE文件的相关资料。
从上面的图中可以看到0x4111B3地址处是一条JMP指令,它跳转到了
WinMainCRTStartup (地址是0x)这个函数的地址处执行, 好了, 终于引
出了这个神秘的“真正的入口函数” 。
事实上,入口函数有以下4种形式:
1、mainCRTStartup(用于ANSI版本的控制台应用程序)
2、wmainCRTStartup(用于Unicode版本的控制台应用程序)
3、WinMainCRTStartup(用于ANSI版本的窗口应用程序)
4、wWinMainCRTStartup(用于Unicode版本的窗口应用程序)
很显然, 我这个例子中使用的是第3种, ANSI版本的窗口应用程序, 值得
一提的是, 我这里是使用反汇编工具查看这些信息的, 目的是让程序运行过程一
目了然。 但你也可以看看C/C++ 运行期库的源代码文件, 相信你看了后会对程序
的运行原理更有体会。这些源码随你安装VC++6.0或VisualStudio时已经附
带有了,我电脑上装的是VisualStudio2010,由于我安装在H盘,所以在我电
脑上CRT库源代码文件的路径是:
H:\ProgramFiles\MicrosoftVisualStudio10.0\VC\crt\src
而WinMainCRTStartup这个函数的定义是在crtexe.c这个文件中,你可以打
开看看是不是和我这反汇编出来的代码是一样的呢。
再接着看WinMainCRTStartup源码,为了方便用XXX表示各种版本的调用
intXXXCRTStartup( void )
{
__security_init_cookie();
return__tmainCRTStartup();
}
可以看到,这个“真正的入口函数”首先调用__security_init_cookie()这个函
数完成一些安全方面的初始化, 然后是返回对__tmainCRTStartup()函数的调用,
这样一来,全部的操作都是在这个函数中一一去完成了! !
在__tmainCRTStartup中首先调用了GetStartupInfoW函数取得父进程创建
本进程时的启动信息, 然后又是一系列的初始化, 其中包括C++ 构造函数的调用,
还有静态变量,全局变量的初始化,这些操作是在_initterm这个函数中完成的。
然后往下看,你就会发现(w)WinMain/(w)main函数被调用了! ! !
我们对这个函数再熟悉不过了! !我们写程序时要写的第一个函数就是这个
所谓的主函数, 但你都看到了, 经过这么多复杂的一系列的初始化之后, 这个函
数才最终 “被” 调用, 其中的mainret就是主函数的返回值了, 习惯于写voidmain()
的朋友可看清楚了,从来就没有voidmain()形式的调用,不要被潭浩强的书给
“误导”了,呵呵。
#ifdefWPRFLAG
mainret=wWinMain(
#else/*WPRFLAG*/
mainret=WinMain(
#endif/*WPRFLAG*/
(HINSTANCE)&__ImageBase,
NULL,
lpszCommandLine,
StartupInfo.dwFlags&STARTF_USESHOWWINDOW
?StartupInfo.wShowWindow
:SW_SHOWDEFAULT);
#else/*_WINMAIN_*/
#ifdefWPRFLAG
__winitenv=envp;
mainret=wmain(argc,argv,envp);
#else/*WPRFLAG*/
__initenv=envp;
mainret=main(argc,argv,envp);
#endif/*WPRFLAG*/
本文使用的例子444.exe是一个ANSI版本的窗口程序,因此调用的是
WinMain函数了, 当然, 既然WinMain函数的调用已经出现了, 第一部分的
讲解也就完毕了,下面请看第二部分的内容。
二、主函数的运行过程
终于要讲主函数的运行原理了,是否很期待呢,是否觉得第一部分的东西
太复杂了呢,其实第一部分exe程序的初始化过程是非常非常复杂和繁琐的, 我
这里只是蜻蜓点水一样大概讲了下, 具体的许多细节不是查阅相关资料就能明白
的, 因为这是微软的东西, 不可能让你了解内部的核心的东西的, 不然他们就没
饭吃咯,呵呵。你要想了解的话也是可以去研究下的。
其实本文的重点也是放在这部分的, 就是我们熟悉的窗口程序的运行原理,
知道了这个原理, 对Windows程序设计是很有帮助的, 好了, 废话就不多说了,
请继续往下看。
Windows Windows窗口程序运行原理
不知有多少朋友一上来就是MFC,创建工程后,运行向导,下一步,下一
步, 一个窗口程序就出来了, 但是不知道为什么要这样做, 你可能连个WinMain
都找不到,也不知程序是怎么运行起来的,本文不是讲MFC的原理,而是讲原
生的Windows窗口程序的原理,说到MFC,本质上是C++ 对API函数的包装,
它的内部实现离不了API函数的调用, 只要你懂了这部分讲的原理, 那MFC的
原理也就好理解了。
在讲原理之前,我想有必要先讲下几个概念:
�窗口:是一个可视化的对象, 窗口上一般有标题栏,菜单栏, 最小化,最大
化,关闭按钮,这个大家都懂的
�句柄:是一个DWORD(32位)值,它用来标识各种不同的对象,如窗口, 图
标, 菜单, 文件, 字体等等, 相应的就有窗口句柄 ( HWND) , 图标句柄 (HICON )
菜单句柄(HMENU) ,文件句柄(HANDLE)等等
�消息:表示用户与程序交互时产生的各种操作的标识,也是一个DWORD
值,有窗口消息(以WM_XXX为标识) ,通知消息(WM_NOFITY) ,命
名消息(WM_COMMAND) ,各种控件消息等等
�窗口函数:各种消息的处理程序 (Handler) , 有消息到达时, 在窗口函数中
处理你想要的消息
还有些概念大家可以查找相关的资料,这里就不多说了。有了这些概念, 下
面就好办了。
首先要知道, Windows窗口程序是基于消息的, 消息的产生是用户与程序的
交互产生的,也可以是各种系统消息。消息是不断产生的,许多消息连在一直,
构成消息队列,系统会维护这些消息队列,主要有两种,一种是系统消息队列,
一种是用户消息队列。 由于消息不断的产生, 因此产生一个消息循环, 通常窗口
创建完成后会有这一句while(GetMessage(&msg,NULL,0,0,0))来获取消息。 这
样消息就源源不断的产生, 再把消息发送到窗口, 在窗口函数中处理, 程序就能
一直运行下去直到关闭退出。
消息是一个结构,它的定义如下:
typedefstructtagMSG{
HWNDhwnd;
UINTmessage;
WPARAMwParam;
LPARAMlParam;
DWORDtime;
POINTpt;
}MSG,*PMSG,*LPMSG;
hwnd是和消息相关的窗口, message就是消息的标识, wParam,lParam是附
加的两个参数, 在使用SendMessage或PostMessage时指定的, time表示消息被
发送的时间,pt表示消息发送时鼠标的位置(相对于屏幕坐标) 。
那么WinMain的执行流程是怎样的呢?别急,且往下看。 。 。
首先要把窗口创建出来, 但在创建窗口之前,还需要注册窗口类, 这里的类
并不是面向对象中的类, 而是指定一个窗口的风格, 样式等等, 如下面把创建窗
口类的操作写在一个函数中
ATOMMyRegisterClass(HINSTANCEhInstance)
{
WNDCLASSEXwcex;
wcex.cbSize=sizeof (WNDCLASSEX);
wcex.style=CS_HREDRAW|CS_VREDRAW;
wcex.lpfnWndProc=WndProc;
wcex.cbClsExtra=0;
wcex.cbWndExtra=0;
wcex.hInstance=hInstance;
wcex.hIcon=LoadIcon(hInstance,MAKEINTRESOURCE(IDI_MY444));
wcex.hCursor=LoadCursor(NULL,IDC_ARROW);
wcex.hbrBackground=(HBRUSH)(COLOR_WINDOW+1);
wcex.lpszMenuName=MAKEINTRESOURCE(IDC_MY444);
wcex.lpszClassName=szWindowClass;
wcex.hIconSm=LoadIcon(wcex.hInstance,MAKEINTRESOURCE(IDI_SALL));
returnRegisterClassEx(&wcex);
}
hInstace是指应用程序载入的模块, win32系统下通常是被载入模块的线性地
址, 还记得第一部分讲的吗, WinMain的第一个参数就是它, 它是这样被传入的
mainret=WinMain((HINSTANCE)&__ImageBase,。 。 。 )那这个__ImageBase又是
什么东西,看下面的图你就明白了
这个图是我用VS2010调试时查看内存时截下来的,从图中可以看出
__ImageBase应该就是值0x00905A4D,而这4个字节刚才是PE文件的头4个
字节(PE文件头两个字节是MZ标记)它是映射到地址空间中的,& 取地址后
便是hInstance , 很显然本例中hInstance应该是0x008B0000, 要注意的是每次运
行时hInstance的值都是不同的,这是被映射到地址空间中的地址。它和
HMODULE其实是一样的,
再看WNDCLASSEX这个结构,看到有个成员lpfnWndProc,这个是指定
窗口函数的地址, 这里千万不能写错了, 不然程序运行不起来, 而且没有任何错
误提示。lpszClassName这个成员指定了窗口的类名,在使用spy++工具查看窗
口时, 可以看到这个类名。 之后就是返回RegisterClassEx函数的调用, 这样窗口
类就注册好了。
接下来的工作是创建窗口了,创建窗口使用函数CreateWindow(Ex),这里也
把创建窗口的操作写函数中:
BOOLInitInstance(HINSTANCEhInstance,intnCmdShow)
{
HWNDhWnd;
hInst=hInstance;
hWnd=CreateWindow(szWindowClass,szTitle,WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,0,CW_USEDEFAULT,0,NULL,NULL,hInstance,NULL);
if(!hWnd)
{
returnFALSE;
}
ShowWindow(hWnd,nCmdShow);
UpdateWindow(hWnd);
returnTRUE;
}
创建窗口成功后会返回一个窗口句柄HWND, 这个创建窗口的过程其实也是
相当复杂的,创建时会发送几个消息到窗口函数中,比如WM_NCCREATE,
WM_CREATE,具体的细节下一节跟踪消息的执行时会有介绍。有了这个窗口
句柄后就可以做很多事了,因为许多函数的调用都需要传入一个窗口句柄值, 接
下来是显示窗口ShowWindow,这个函数也会发送几个消息出去,而后是更新
窗口UpdateWindow,主要是发送了WM_PAINT消息,让窗口重绘。
到了最后,就是一个消息循环了,消息循环典型的写法如下:
MSGmsg;
while(GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
只要程序在运行, 这个循环就不会退出,程序也不会终止, 当用户关闭窗口
时,会产生WM_CLOSE消息,默认的WM_CLOSE实现(在DefWindowProc
中)是发送了WM_DESTROY消息,我们需要在WM_DESTROY消息中调用
PostQuitMessage函数, 以便让它发送WM_QUIT消息, 当GetMessage函数取到
的消息是WM_QUIT消息时它会返回FALSE,这样循环就退出了,程序也会终
于。
还有一个重要的函数是窗口函数, 窗口函数是一个回调函数, 就是系统会自
己去调用这个函数, 你只要按要求写好, 当消息到来时, 系统会去处理, 不用你
去处理,就是Dot’tCallMe,IWillCallYou。这个函数是在注册窗口类时由
lpfnWndProc指定的,窗口函数的定义如下:
LRESULTCALLBACKWindowProc(
__inHWNDhwnd,
__inUINTuMsg,
__inWPARAMwParam,
__inLPARAMlParam
);
hwnd是和消息的发出相关的窗口句柄, uMsg就是发送的消息, 后两个是附
加参数。
窗口函数的处理比如可以这样写:
LRESULTCALLBACKWndProc(HWNDhWnd,UINTmessage,WPARAMwParam,LPARAMlParam)
{
PAINTSTRUCTps;
HDChdc;
switch(message)
{
caseWM_PAINT:
hdc=BeginPaint(hWnd,&ps);
::Rectangle(hdc,50,100,200,400);
EndPaint(hWnd,&ps);
break ;
caseWM_DESTROY:
PostQuitMessage(0);
break ;
default :
returnDefWindowProc(hWnd,message,wParam,lParam);
}
return0;
}
这里只处理了两条消息, 而往往我们要做的就是处理各种你想处理的消息,
因为Windows是基于消息的吗,所以如果要处理的消息很多,那么就会有很多
的caseWM_XXX的处理,如果用MFC的话,它把这些消息分流出去了,变成
了一些ON_XXX的处理函数,MFC里面会维护一个消息路由表。
总结下它的运行原理,是这样的一个过程:
注册窗口类–> 创建窗口–> 显示窗口–> 更新窗口–> 消息循环(取得消息, 分
发消息) ,窗口函数对消息的处理
跟踪运行过程
本节用一个实例来跟踪Windows程序的运行过程, 也就是探讨下程序启动时
产生了哪些消息,又是怎样处理的,使用的例子还是444.exe程序,此程序也没
有什么特别之处, 只是在调用一些函数前后输出一些信息, 还有在窗口函数的实
现代码中, 写上几句输出该消息是什么消息而已, 这样就可以对程序运行时产生
消息的顺序有一个认识。
从调试的信息可以看出,0x00F00000是hInstance (其实是一个地址)的值,
0x00905A4D是该地址处的值,就是PE文件的头几个字节。
往下看输出了EntryMainMethod表示主方法开始了,然后是调用
RegisterClassEx注册窗口类,写上EndXXX是为了看看函数被调用时会不会有
消息发出,这样就可以在窗口函数中捕获该消息。
注册完窗口类,开始调用CreateWindow函数创建窗口,前面说过,此函数
的内部操作是很复杂的,从图中也可以看出来,CreateWindow函数在返回前会
发出几个消息,首先是WM_GETMINMAXINFO (0x0024)消息,这样看来这
个消息是窗口创建后的第一条消息,而WM_QUIT则是最后一条消息
(GetMessage取得此消息后返回FALSE) 。
这里要注意的是以上这些消息的发出是在消息循环之前产生的,因为这是
CreateWindow函数内部发出的消息,而消息循环还没开始。然后是第二条消息
WM_NCCREATE(0x0081) ,消息产生在WM_CREATE之前,NC是notclient
非客户区的意思,是指标题栏,边框等几个区域。
后面有三条NULL的消息, 表示什么呢, 这个我本人没研究, 而且是仅有数
值并没有WM_XXX的定义,所以这里用NULL代替。CreateWindow在返回前
发出的最后一条消息是WM_CREATE消息,这个消息中的lParam参数就是
CreateWindow函数中的最后一个参数传入的,往往我们会在这里完成一些初始
化的设置,不建议拦截WM_NCCREATE消息,这个默认DefWindowProc中会
有处理。
接着是显示窗口和更新窗口了, ShowWindow的内部也是做了许多工作,请看
在ShowWindow函数返回前会发送许多消息出去,我们看到第一条消息是
WM_SHOWWINDOW,最后一条是WM_MOVE,然后函数返回,而在
UpdateWindow中只发送了一条WM_PAINT消息,让窗口重绘。
这些都完成之后,就进入消息循环
这些都是消息循环和窗口函数中的处理(通过DispatchMessage将消息送入
窗口函数)这样程序就一直在运行着,消息循环中通过GetMessage函数取得消
息以便交给窗口函数处理。 你也可以使用PeekMessage, 关于它们的区别你可以
百度下相关的资料。
当用户关闭窗口时,我们来看看又是怎样的一个过程呢
这 就 是 关 闭 窗 口 时 的 过 程 。 可 以 看 到 首 先 是 产 生了
WM_NCLBUTTONDOWN消息, 因为关闭按钮是在非客户区, 然后下面有一个
WM_SYSCOMMAND消息, 这个消息是系统菜单的消息, 你在标题栏上右键时
可以看到系统菜单,然后是WM_CLOSE消息,默认的实现在DefWindowProc
中,我想是调用了DestroyWindow这个函数,它是送出了一个WM_DESTROY
和WM_NCDESTROY消息,而我们的程序中在WM_DESTROY处理了,调用
了PostQuitMessage函数,发出了WM_QUIT消息,由于GetMessage函数取到
该消息时返回FALSE了,因此循环体内的语句没有执行,WM_QUIT消息也就
没有被打印出来, 这样WM_NCDESTROY就是最后一条消息 (不知还有没有其
它消息) ,while循环结束后,最后是主函数返回return(int)msg.wParam,至此,
主函数执行完毕。
总结下这部分的内容,消息产生的顺序应该是这样一个过程:
WM_GETMINMAXINFO(0x0024)
WM_NCCREATE(0x0081)
WM_NCCALCSIZE(0x0083)
NULL(0x0093)
NULL(0x0094)
NULL(0x0094)
WM_CREATE(0x0001)
WM_SHOWWINDOW(0x0018)
WM_WINDOWPOSCHANGING(0x0046)
WM_WINDOWPOSCHANGING(0x0046)
WM_ACTIVATEAPP(0x001C)
WM_NCACTIVATE(0x0086)
NULL(0x0093)
WM_GETICON(0x007F)
WM_GETICON(0x007F)
WM_GETICON(0x007F)
NULL(0x0093)
NULL(0x0091)
NULL(0x0092)
NULL(0x0092)
WM_ACTIVATE(0x0006)
WM_IME_SETCONTENT(0x0281)
WM_IME_NOTIFY(0x0282)
WM_SETFOCUS(0x0007)
WM_NCPAINT(0x0085)
NULL(0x0093)
NULL(0x0093)
NULL(0x0091)
NULL(0x0092)
NULL(0x0092)
WM_ERASEBKGND(0x0014)
WM_WINDOWPOSCHANGED(0x0047)
NULL(0x0093)
WM_SIZE(0x0005)
WM_MOVE(0x0003)
WM_PAINT(0x000F)
BeginMessageLoop…
WM_GETICON(0x007F)
WM_GETICON(0x007F)
WM_GETICON(0x007F)
WM_SYNCPAINT(0x0088)
###############################
#####消息循环中的其它消息######
###############################
#####下面是窗口关闭后消息######
###############################
WM_NCLBUTTONDOWN(0x00A1)
WM_CAPTURECHANGED(0x0215)
WM_SYSCOMMAND(0x0112)
WM_CLOSE(0x0010)
NULL(0x0090)
WM_WINDOWPOSCHANGING(0x0046)
WM_WINDOWPOSCHANGED(0x0047)
WM_NCACTIVATE(0x0086)
NULL(0x0093)
NULL(0x0093)
NULL(0x0091)
NULL(0x0092)
NULL(0x0092)
WM_ACTIVATE(0x0006)
WM_ACTIVATEAPP(0x001C)
WM_KILLFOCUS(0x0008)
WM_IME_SETCONTENT(0x0281)
WM_IME_NOTIFY(0x0282)
WM_DESTROY(0x0002)
WM_NCDESTROY(0x0082)
EndMessageLoop…
相信这一部分的内容会让你受益很多, 呵呵。这部分的内部也结束了, 请接
着看第三部分的内容。
三、程序收尾工作
WinMain函数结束之后,又发生了什么事呢。我们继续来分析。
经调试发现WinMain函数返回后回到了C/C++ 运行期库函数, 返回值赋给了
mainret。然后是一些内存回收清理工作,请看图
这里的工作主要是调用exit函数完成的,exit函数的实现其实是调用doexit
函数, 然后执行一些清理工作, 包括释放不用的指针, 被占用的内存等等。 而后
执行到了这里:
到这里就是退出进程了,如果__crtCorExitProcess函数没有成功,则后面的
ExitProcess保证成功调用。 在我调试时, 调试到ExitProcess这个函数就结束了,
调试程序也退出了,可见这样一个exe程序的执行的生命周期也就完成了。
总结
从这三部分讲的内容来看,总结出一个exe程序的执行过程是这样:
1、Shell(Explorer.exe )调用CreateProcess函数激活exe程序
2、系统创建一个进程内核对象,引用计数置为1
3、系统为进程创建一个4GB的进程虚拟地址空间
4、PE装载器把exe的代码映射到地址空间,并查找ImportTable引入相关
的动态链接库(DLLs )
5、系统为进程创建一个主线程,线程得到CPU后,把CS:IP指向.text节中
的程序进入点(OEP) ,此处是一条JMP指令,它跳到XXXCRTStartup
函数处执行
6、这里完成c/c++运行期库的一些初始化设置,包括c++ 构造函数的调用
全局变量,静态变量的初始化
7、调用WinMain/main函数,进入主函数
8、注册窗口类,创建窗口,显示窗口,更新窗口,进入消息循环
9、窗口关闭,循环退出,返回到C/C++ 运行期库
10、完成一些清理工作
11 、最后是ExitProcess退出进程
后话
本文的内容至此就讲完了, 希望这篇文章能给迷茫, 不知怎么学习Windows
程序设计的人一个指导,同时也对学习MFC有一个好的认识。
本人也不是什么高手, 只是肯去研究下这方面的技术, 如果你也喜欢去研究,
喜欢技术, 相信你也可以做, 也可能做的更好, 因此本文的一些错误之处还请多
多指教。完。 。 。
PS: 本人是一个喜欢编程, 喜欢技术的人, 就把技术当做吃饭一样, 那么你
也是吗?我的微博^_^http://weibo.com/zhuhuigong
作者:秋伤~~!
创建时间:2011/09/1400:01
完成时间:2011/09/1623:40
参考文章:
1、windows启动以及exe文件的加载简介
2、一个microsoft的.exe程序的启动过程今天的文章
windows可执行程序运行不起来_一个进程会伴随着其程序执行分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/89795.html