当我们在学C/C++语言编程的时候,老师总是跟我们说程序是从Main函数开始的,然而当我第一次用OD分析一个可以执行文件EXE的时候,找这个Main函数就花了我好长时间,后来才知道在Windows下不同的编译器都会在生产最后的可执行PE文件的时候,在我们写的启动函数之前加入一些编译器附带的启动代码,来做一些初始化工作,比如初始化全局变量,运行全局类对象的构造函数,初始化C运行时库等等。对于有(知其然知其所以然)强迫症的我,为此在网上爬了不少帖,也自己调试分析了好几天VS的启动代码,所以贴出来备忘。
在程序进入main/WinMain函数之前,通过在Visual Studio中调试,通过栈回溯可以找到位于crt0.c中的_tmainCRTStartup函数,这个函数负责进行一些初始化操作,因为C运行库代码又有两个版本,如果是静态编译的话代码位于crt0.c之中,如果是动态编译的话代码位于crtexe.c之中,这里可以通过项目属性的“配置属性”——“C/C++”——“代码生成”——“运行库”的MT和MD进行设置。
根据工程的类型的不同(Win32工程和Console工程),以及工程编码的不同(Unicode与多字节),实际的入口函数会有四种不同的可能,_tmainCRTStartup被设置为一个红,根据工程的设置,实际的名字选取其中的一种:
static
int
__tmainCRTStartup(
void
);
#ifdef _WINMAIN_
#ifdef WPRFLAG
int wWinMainCRTStartup(
#else /* WPRFLAG */
int WinMainCRTStartup(
#endif /* WPRFLAG */
#else /* _WINMAIN_ */
#ifdef WPRFLAG
int wmainCRTStartup(
#else /* WPRFLAG */
int mainCRTStartup(
#endif /* WPRFLAG */
#endif /* _WINMAIN_ */
void
)
{
/*
* The /GS security cookie must be initialized before any exception
* handling targetting the current image is registered. No function
* using exception handling can be called in the current image until
* after __security_init_cookie has been called.
*/
__security_init_cookie();
return __tmainCRTStartup();
}
_tmainCRTStartup实际上是__tmainCRTStartup的一个包装函数,在调用后者之前,对cookie进行了初始化操作,如果设置了/GS选项的话,在函数调用过程中,建立栈帧的时候会设置一个cookie,函数返回之前会校验cookie是否一致,简单的判断是否发出缓冲区溢出。 Windows下程序入口函数与_security_init_cookie这里有关系cookie的详细分析
先来看下使用静态C运行库(/MT /MTd)时的代码
位于crt0.c
int
__tmainCRTStartup(
void
)
{
int initret;
int mainret=0;
int managedapp;
#ifdef _WINMAIN_
_TUCHAR *lpszCommandLine;
STARTUPINFOW StartupInfo;
GetStartupInfoW( &StartupInfo );
#endif /* _WINMAIN_ */
#ifdef _M_IX86
// 对于32位程序,设置为如果检测到堆被破坏则自动结束进程
// 64位程序默认就设置了这个行为
if (!_NoHeapEnableTerminationOnCorruption)
{
HeapSetInformation(NULL, HeapEnableTerminationOnCorruption, NULL, 0);
}
#endif /* _M_IX86 */
// 检测PE头中的标志,判断是否为.net托管程序
managedapp = check_managed_app();
// ======================================================
// 堆初始化操作
// 对于32位程序而言,_heap_init通过CreateHeap创建一个堆
// ======================================================
if ( !_heap_init() ) /* initialize heap */
fast_error_exit(_RT_HEAPINIT); /* write message and die */
// 初始化多线程环境,暂时不做分析
if( !_mtinit() ) /* initialize multi-thread */
fast_error_exit(_RT_THREAD); /* write message and die */
_CrtSetCheckCount(TRUE);
#ifdef _RTC
_RTC_Initialize();
#endif /* _RTC */
__try {
// I/O初始化,暂时不做分析
if ( _ioinit() < 0 ) /* initialize lowio */
_amsg_exit(_RT_LOWIOINIT);
// 获取命令行参数
/* get wide cmd line info */
_tcmdln = (_TSCHAR *)GetCommandLineT();
// 获取环境变量参数
_tenvptr = (_TSCHAR *)GetEnvironmentStringsT();
// 解析并设置命令行参数
if ( _tsetargv() < 0 )
_amsg_exit(_RT_SPACEARG);
// 解析并设置环境变量参数
if ( _tsetenvp() < 0 )
_amsg_exit(_RT_SPACEENV);
// 初始化全局数据和浮点寄存器
initret = _cinit(TRUE); /* do C data initialize */
if (initret != 0)
_amsg_exit(initret);
// 进入(w)WinMain或者(w)main函数
#ifdef _WINMAIN_
lpszCommandLine = _twincmdln();
mainret = _tWinMain( (HINSTANCE)&__ImageBase,
NULL,
lpszCommandLine,
StartupInfo.dwFlags & STARTF_USESHOWWINDOW
? StartupInfo.wShowWindow
: SW_SHOWDEFAULT
);
#else /* _WINMAIN_ */
_tinitenv = _tenviron;
mainret = _tmain(__argc, _targv, _tenviron);
#endif /* _WINMAIN_ */
if ( !managedapp )
exit(mainret);
_cexit();
}
// 异常处理
__except ( _XcptFilter(GetExceptionCode(), GetExceptionInformation()) )
{
/*
* Should never reach here
*/
mainret = GetExceptionCode();
if ( !managedapp )
_exit(mainret);
_c_exit();
} /* end of try - except */
return mainret;
}
先来看下_Heap_Init
int __cdecl _heap_init (void)
{
ULONG HeapType = 2;
// Initialize the "big-block" heap first.
if ( (_crtheap = HeapCreate(0, BYTES_PER_PAGE, 0)) == NULL ) _crtheap 全局堆句柄 mallco 函数使用该句柄
return 0;
#ifdef _WIN64
// Enable the Low Fragmentation Heap by default on Windows XP and
// Windows Server 2003. It's the 8 byte overhead heap, and has
// generally better performance charateristics than standard heap,
// particularly for apps that perform lots of small allocations.
if (LOBYTE(GetVersion()) < 6)
{
HeapSetInformation(_crtheap, HeapCompatibilityInformation,
&HeapType, sizeof(HeapType));
}
#endif /* _WIN64 */
return 1;
}
这个函数用HeapCreate创建一个私有堆供标准C函数malloc使用来分配内存,它的第三个参数为0表示堆可以自动增长。因为是静态链接C运行库,所以在C运行库代码是嵌入到应用程序里的,因此_crtheap为应用程序私有,如果该应该程序调用一个同样使用静态运行库的DLL,那么该DLL同样会创建属于该DLL的_crtheap并调用HeapCreate为其获取句柄标识。
来看来malloc的实现位于 smallheap.c文件中
void * __cdecl malloc (
size_t size
)
{
return _nh_malloc( size, _newmode );
}
void __cdecl free (
void * pblock
)
{
if ( pblock == NULL )
return;
HeapFree(_crtheap, 0, pblock);
}
_nh_malloc同样位于这个文件中
void * __cdecl _nh_malloc (
size_t size,
int nhFlag
)
{
void * retp;
for (;;) {
retp = HeapAlloc( _crtheap, 0, size ); //使用全局句柄来申请空间
/*
* if successful allocation, return pointer to memory
* if new handling turned off altogether, return NULL
*/
if (retp || nhFlag == 0)
return retp;
/* call installed new handler */
if (!_callnewh(size))
return NULL;
/* new handler was successful -- try to allocate again */
}
}
在这个C文件中也有一个初始化堆的函数
int __cdecl _heap_init (
void
)
{
if ( (_crtheap = HeapCreate( 0, BYTES_PER_PAGE, 0 )) == NULL )
return 0;
return 1;
}
通过翻代码可以找到它在dllcrt0.c中和crtlib.c被调用
dllcrt0.c
BOOL WINAPI _CRT_INIT( DLL使用静态运行库时被调用
HANDLE hDllHandle,
DWORD dwReason,
LPVOID lpreserved
)
{
/*
* Start-up code only gets executed when the process is initialized
*/
if ( dwReason == DLL_PROCESS_ATTACH )
{
if ( !_heap_init() ) /* initialize heap */
return FALSE;
crtlib.c
__CRTDLL_INIT( //使用动态运行库时被调用
HANDLE hDllHandle,
DWORD dwReason,
LPVOID lpreserved
)
{
if ( dwReason == DLL_PROCESS_ATTACH ) {
if ( !_heap_init() ) /* initialize heap */
上面使用静态C运行库的DLL程序的启动代码的一部分,和使用动态运行库msvcrxx.dll时被调用的一部分,因此不难理解,当我们调用一个使用静态C运行库的DLL,或者一个使用动态C运行库的DLL, 他们分别创建和使用自己的私有堆,并用各自的_crtheap全局句柄来标识。如果这个DLL有个导出的函数,该函数使用malloc申请了一块内存并返回指针。
当我们编写一个使用静态运行库的EXE来调用这个DLL导出的该函数的时候,用Free来释放已申请的内存,会导致程序崩溃就不难理解了,相应的如果EXE用动态运行库来编写,那么DLL使用静态库时报错,使用动态运行库时候工作正常,因为它们调用的都是msvcrxx.DLL中的malloc 和 free,自然引用的是同一个_crtheap全局句柄,该句柄在mscrcxx.dll中。
由此我们不难看到在编写Windows程序的时候如果大量使用C标准库函数,应该使用动态运行库来避免一些不必要的错误。
使用C++的 new关键字来为对象分配空间的时候同样是使用_crtheap为标识 使用HeapAlloc来申请堆内存的。因此应该在同一个模块中使用NEW 和Delete来申请和释放堆对象