windows可执行程序运行不起来_一个进程会伴随着其程序执行

windows可执行程序运行不起来_一个进程会伴随着其程序执行写在前面:最近在研究一个VC界面库DuiLib,在细读它的源码时遇到些问题,比如它的界面是如何绘制上去的,底层操作是如何实现的,就是CreateWindow和ShowWindow又是如何实现的

写在前面:

最近在研究一个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

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注