先来看这样一个过程(非VISTA启动):CPU复位->传统BIOS->MBR->引导扇区->NTLDR->NTOS->SMSS->1:CSRSS 2:WinLogon->1:LSRSS 2:Services 3:LogonUI->2:Services->Svchost 3:LogonUI->USERINT->1:StartUp App 2:Shell
这是非 VISTA 的启动过程,是传统 Windows 的启动,这里有几处必要提到,首先 NTLDR 是一个非常重要的启动文件,在 C 盘下,相关的还有 boot.ini, pagefile.sys, NTDETECT.COM,我们按 F8 或者 F5 时进入安全模式的界面也是它产生的。然后是 NTOSKEL 这就是产生滚动条界面的,这是启动最漫长的过程!我曾经看到过有“高手”说过可以改掉滚动条滚动次数的注册表位置,其实这是不能改的,在这个界面下的过程非常漫长非常复杂,不提。然后是 SMSS 进程,这是第一个真正意义上由 .EXE 产生的进程。然后建出 CSRSS 子系统服务器进程和 WinLogon 进程。最后建出 Shell:Explorer.exe 进程。和本文最相关的就是这个 Shell 了,Shell “命令解释器”它的经典就是 Explorer.exe 。每一个窗口其实就是它创建的一个线程!这是一个非常厉害的多线程设计了!比如我们点击桌面上的图标启动一个程序,就是由它来加载创建的,好了,有了这些就开始来讲本文的其中一个内容:.EXE 的加载过程,然后再由此扩充开。
双击一个 .EXE 图标(操作系统怎么定位哪一个图标不是本文的内容),Shell 调用 CreateProcess 函数,系统找出在调用 CreateProcess 函数时的 .exe 文件,如果文件不在则返回 FALSE 进程创建失败。成功的话则为该进程创建一个新进程内核对象,这个内核对象是由系统内核管理的,关于内核对象也是一个很长的话题,详细叙述的话我写到明天恐怕都写不完这篇文章了,只要知道这个时候如果 CreateProcess 成功的话系统为这个进程创建了一个使用记数为1的进程内核对象,值得一提的是有一个进程内核对象句柄表,这里纪录了所有在该进程中所有创建出来的进程内核对象,但这个东西 Microsoft 没有提供相关文档,我的偶像 Jeffrey Richter 也只是简单的提了一提,接下来系统为这个新进程创建了一个私有地址空间,这就是我们很清楚的 4GB 地址空间! 写到这里先停住,我发现了一个比较大的问题,Shell 调用 CreateProcess 这个函数时其中的参数是哪里来的?我根据 Jeffrey Richter 的一句话也就是系统找出 EXE 文件,如果存在的话则创建成功,那么我进一步推论,那其中的参数很有可能是来自 PE 格式中的信息,可以继续推论下去,研究一下 CreateProcess 的参数再对照一下 PE 格式各个段的包含信息,那应该可以研究出某些东西来,这个细节我是还没有深入对比过,如果有哪位看官知道的话还请赐教!在这里先谢过了!接下来系统会保留一个足够大的区域来存放.exe 文件,默认的话是保存在从OXOO4OOOOO开始的位置,而这个数值是在 exe 这个 PE 结构中的信息,用 /BASE 是可以改的。这里就牵涉到内存映射文件了,不提。我们来研究一下 0X00400000 在内存中的什么区域,看一下 Win32 内存结构中4GB的划分(不再提 98 那破烂了)0x00000000 - 0x0000FFFF 这64KB用于 NULL 指针分配,举一个 Richter 的例子:int *pnSomeInteger = (int*)malloc(sizeof(int)); *pnSomeInteger = 5; 如果 malloc 不能找到足够的内存来分配的话那它就返回 NULL 就是保存在这里了,接下来 0x00010000 - 0x7FFEFFFF 这一段就是 Win32 进程私有的地址空间,刚才的.exe 加载就在这一段空间中,接下来继续加载程序执行所必须的 DLL 文件也是在这个段里,当然在这里还有一些数据需要加载,和为线程开辟的堆栈区,所以进程的这个用户区其实分成了3个部分:文本区包括指令和可读的数据,通常这个段是标记为可读,数据区,这个段对应 PE 中的 DATA_BASS 包含以初始化和未初始化的数据,静态变量也存放在这个区中,最后就是堆栈区,这是为线程分配的默认为1MB,当然是可以改的,在这里包含了函数参数和局部变量,当然也有现场保护时的信息,很关键的一点是 IP 指针也在这里,缓冲区溢出攻击就是在这个区域做文章,俗称:践踏堆栈。好!因为 Win32 中进程是一个惰性的,它什么也不执行,除了 EXE 和 DLL 还有它们所需的数据加载到进程地址空间外每个进程还拥有别的资源,这里只要记住:窗口和钩子是属于线程的外其他都是进程的,所以进程必须有一个线程来执行它的代码,这时候系统就为进程创建一个主线程,但它是调用 C 运行时库函数,这里又有区分 CUI 和 GUI,ANSI 和 UNIODE 的差别,如果以 GUI 和 UNICODE 来讲,它调用的是 wWinMainCTRStartup 这个可执行文件的启动函数。这个函数的功能不多讲,它会进行一系列初始化工作,完了后它将调用我们编写的主函数入口函数比如 wWinMain();最后 exit 调用调用操作系统的ExitProcess 函数,将 nMainRetVal 传递给它,这使得操作系统能够撤销进程并设置它的 exit 代码。到这里启动过程结束!
回顾上面的内容,就这一个启动过程实在复杂!复杂!牵涉到进程,线程,PE 结构,Win32 内存结构!而这些我看其实就是操作系统原理的核心部分了!上面的分析只是非常简单的提了个大概,里面的每一个内容我看都可以写成一本书了...再回顾一下 Windows 的启动过程,这两者之间的复杂程度何其的相似!有兴趣的可以去深入研究 Windows 的启动过程,单单就那个滚动条界面的时候, Windows 做了一大堆大堆的工作!想要用一篇短短的文章是不可能写完这些的。比如 PE 格式的有效性,Win32 内存管理,当然,很精彩的是我刚才提到的缓冲区溢出攻击,真佩服这个创始人!好了,写代码去了。
转自:http://www.zeroplace.cn/article.asp?id=156
一个microsoft的.exe程序的启动过程
学习windows 编程从mfc角度来说可分为两部分那就是WinMain函数以前的,和WinMain函
数以后的。前者涉及很多windows操作系统内部的知识,后者么看mfc源码就可以了。虽然大
多数程序不需要你了解太多关于os加载应用程序这方面的知识,但我认为能较深入了解wind
ows os的运行情况对程序员是很有帮助的。
最近我正在研究一个microsoft的.exe程序的启动过程,这也是以上所说的关于os如何加
载程序的知识,它包括进程创建,主线程 创建,PE文件加载,程序c运行时启动函数以及四种
main函数的调用等许多令我不得不静下心来好好思索的东东。从mfc编程角度来说,这些都是
不得见的,不过了解这些对程序员编制好的windows程序是有好处的。在平时的学习中中我有
很多疑点,到底在桌面双击一个exe程序,os调用的第一个函数是什么?甚至到现在我研究很
长时间后,还是得不到令我满意的答案。不过在学习的过程中我还是又说收获的,下面和大
家一起分享一下,我也把这段时间所学作一下总结。
要了解一个.exe程序的启动过程就不得不了解一下有关操作系统方面的知识,such as“
进程,线程,虚拟内存 "的基本的知识。当然这里我就不详细介绍了,有兴趣的同学可以自己
去查一查这方面的资料。在未真正开始之前,先统一一下本文出现的一些名词的含义:
App.exe----------假定为应用mfc的AppWizard做出的一个SDI程序,App是它的名字。你
可以把它看为一个标准的 "hello mfc! "程序。
PE------------不要以为它是“体育课”的缩写呦。它可是微软的标准win32可执行文件
.exe和动态链接库.dll的文件格式,它的english name是Portable Executable File Forma
t。
下面可要正式开始了。
一个microsoft的.exe程序的启动方法有很多,这里我们以双击App.exe图标启动为例(
其他方法,我想也是一样的)。在补充一下,我所用的os是Windows2000Server,所以这里也
主要讨论win2000下的应用程序,过要涉及较多关于NT内核,毕竟微软主推win2000/winxp和
Unicode么。
一个microsoft的.exe程序的启动过程如下:
(1)当我们双击App.exe图标启动程序时,系统首先做什么呢,让我们先听一听侯捷是如何
说的吧“执行起来的App进程其实是shell调用CreateProcess激活的”---- "深入浅出MFC se
cond edition " page39载。很多书上都是如是说的,shell又名“命令解释器”,是win32操
作系统基于浏览器的一个32位用户接口,它是一个多线程的好例子,屏幕上每一个文件夹浏
览窗口都是它的一个线程。它是操作系统引导时加载的系统进程,它具体表现为windows ex
plorer.exe。explorer.exe是所有用户应用程序的创造者。你完全可以将shell看成是所有应
用程序进程的父进程,就像桌面(desktop)可看成所有窗口的父窗口一样。shell的用途很多
,如启动应用程序,管理文件系统,将应用程序与相应文件相关联等等。我们常见的桌面上
的带有小箭头的快捷方式(shortcut)就是一个shell链接,shell负责管理一个叫 "名字空间 "
的类似文件系统似的“超文件系统”,它允许应用程序在任何地方在不知访问对象名字和位置
的前提下访问到这个对象,此类对象有:文件,目录,驱动器,打印机以及网络资源。而名
字空间就是shell把这些对象有层次组织起来的一个结构。名字空间为用户和应用程序提供了
一种可靠和高效的方法来访问和管理对象。好了不论它是什么,凡正它调用了CreateProcess,一切就从这里开始了。
(2)CreateProcess这个函数可作了不少工作。App进程由此诞生。当CreateProcess这个函
数被调用,系统就会创建一个“进程内核对象”。进程内核对象可以看作一个操作系统用来
管理进程的内核对象,它也是系统用来存放关于进程统计信息的地方(一个小的数据结构)
,其实它的真正创建者是一个叫NtCreateProcess的windows2000系统服务函数(也叫执行体服
务函数),他创建了进程内核对象供用户扩展。进程内核对象的初始使用计数为1。然后系统
为该进程创建4GB(=2^32)的虚拟地址空间(所谓虚拟就不是真的创建4GB的物理内存空间,这
些空间不是真在物理内存上).用于加载App.exe可执行文件和任何必要的dll文件的数据和代
码。
(3)下面概述一下系统的加载器(可称为loader)是如何加载这些东东的。首先了解一下系
统为该进程创建4GB的虚拟地址空间是如何分配的,对于win2000/winxp来说,默认情况下每
个用户进程可以占有2GB的私有地址空间;操作系统占有剩余的2GB空间。
在32位x86系统上,
从0x00000000到0x7fffffff的空间中存放着 应用程序代码,全局变量,每个线程堆栈,dll
代码。
从0x80000000到0xc0000000的空间中存放着 内核和执行体,HAL(硬件抽象层),引导驱动程序
。
从0xc0000000到0xc0800000的空间中存放着 进程页表和超空间。
从0xc0800000到0xffffffff的空间中存放着 系统高速缓存,分页缓冲池,非分页缓冲池。
首先,CreateProcess打开应用程序文件(.exe),它先扫描该文件的文件头,该文件头里含有文
件能运行在那个环境之下,如果是win32环境,系统就直接加载文件的代码和数据并输入(im
port)该文件执行所需的dll函数。如果不是win32环境比如时os/2的.exe则先加载相应的环境
子系统,载由该环境加载该文件的代码和数据以及该文件执行所需的dll函数。至于系统是如
何知道文件的代码和数据以及该文件执行所需的dll函数所在的位置就需要你了解一下PE文件
格式了,其实也很简单,PE文件拥有很多sections,数据和代码都放在不同的section里面,
文件执行所需的dll也放在单独的section(.idata)里,这里就不详述了。而且在加载过程中涉
及到有关虚拟内存,内存映射文件等很多较深的知识,我会在以后的系列文章中详细专题论
述的。
(4)进程加载代码和数据完毕后,就开始创建线程来执行进程空间内的代码。进程是静态的
,它只是线程的容器。一个进程至少因该有一个线程(main thread),其它线程都是主线程通
过调用CreateThread函数创建的。线程也是核心对象,他的实际创建者是一个叫NtCreateTh
read的windows2000系统服务函数。一个线程其实只是一个线程核心对象和两个堆栈(一个核
心堆栈,用于线程运行在核心态;一个用户堆栈,用于线程运行在用户态),线程与进程类似
,也拥有线程核心对象计数和线程句柄,这里不详述。线程用于描述进程中的运行路径。每
当进程被初始化时,系统就要创建一个主线程。该线程与c/c++运行时库的启动代码一道开始
运行,启动代码则调用进入点函数(就是我们的main函数,它也是主线程的进入点函数),并
且继续运行直到进入点函数返回并且c/c++运行时库的启动代码调用ExitProcess为止。每个
线程都有自己的入口点函数,主线程入口点函数名字必须是main,wmain,WinMain或wWinMain
.而其他的线程入口点函数名字可使用任何名字。每个线程函数必须有一个返回值,它将作为
线程的退出代码。对于主线程来说,这个返回值将传给c/c++运行时库的启动函数。
(5)c/c++运行时库的启动函数它其实是一个程序的真正调用的第一个函数,它是在程序链
接时由链接程序选择相应的启动函数并加到程序的开始处。c/c++运行时库有四个版本的启动
函数,他们分别对应不同类型的应用程序。比如,需要ANSI字符和字符串的GUI应用程序的启
动函数是WinMainCRTStartup,其对应的进入点函数是
WinMain,需要Unicode字符和字符串的GUI应用程序的启动函数是wWinMainCRTStartup,其对应
的进入点函数是
wWinMain,而需要ANSI字符和字符串的CUI应用程序(如控制台console程序)的应用程序的启动
函数是mainCRTStartup,对应的入口点函数为main;需要Unicode字符和字符串的CUI应用程序
(如控制台console程序)的应用程序的启动函数为wmainCRTStartup,对应的入口点函数为wma
in;c/c++运行时库的启动函数的功能如下:
以wWinMainCRTStartup(大多数运行在windows2000下的应用程序的启动函数都是它)为例。它
负责:
*检索指向新进程的完整命令行指针;
*检索指向新进程的环境变量的指针;
*对c/c++运行时的全局变量进行初始化;
*对c运行期的内存单元分配函数(比如malloc,calloc)和其他低层I/O例程使用的内存栈进
行初始化。
*为C++的全局和静态类调用构造函数。
当这些初始化工作完成后,该启动函数就调用wWinMain函数进入应用程序的执行。
当wWinMain函数执行完毕返回时,wWinMainCRTStartup启动函数就调用c运行期的exit()函
数,将返回值
(nMainRetVal)传递给它。之后exit()便开始收尾工作:
*调用由_onexit()函数调用和注册的任何函数。
*为C++的全局和静态类调用析构函数;
*调用操作系统的ExitProcess函数,将nMainRetVal传递给它,这使得操作系统能够撤销进
程并设置它的exit 代码。
(6)至此启动函数的任务完成,至于中间wWinMain函数的运行过程看看mfc源码即可。不过我还要提一下,wWinMain函数其实只是调用了mfc的AfxWinMain()函数,而一切的真正代码的运
行也是从AfxWinMain()开始的。
以上只是粗略将一下一个microsoft的.exe程序的启动过程,其中有很多深奥的知识我只是提了一下,有些知识在以后的文章中还会陆续提到的。
bigwhite
2002.5.18