1-2 Windows下启动函数(真正的入口函数) 之 寻找入口函数与_security_init_cookie
2014-06-20 15:17
1456 查看
上一节详细介绍了/MT /MD之间的区别。在这一节中,我们首先要找到win32程序真正的启动函数。
win32程序分为两种:
1.控制台(/SUBSYSTEM:CONSOLE )
2.GUI(/SUBSYSTEM:WINDOWS)
首先看控制台版本的:
写一段最简单的,或者就直接使用编译器参数的默认main函数,如下:
1.使用/MD选项
F9下断到main函数的标签处,F11后,在Call Stack中回溯到_tmainCRTStartup(),当前文件为crtexe.c。如下:
2.使用/MT选项
F9下断到main函数的标签处,F11后,在Call Stack中回溯到_tmainCRTStartup(),当前文件为crt0.c。如下:
也就是说,不同的运行时链接方式,其实现代码也是不同的。但是其入口点函数的名称是相同的,都是_tmainCRTStartup。这便是Windows下的启动函数(真正的入口函数)。
下面我们分别讨论,这两者在入口函数中都干了些什么。
首先看_security_init_cookie,这个函数在两个实现文件中是一样的,代码如下:
_security_init_cookie(使用/GS选项后提供)的作用是通过初始化一个全部变量用来检测局部缓冲区溢出和对函数返回地址潜在的修改(这可能导致程序执行流程改变,以至于执行不安全的shellcode)。
下图是使用了/GS选项后的函数栈帧:
看到这个布局,就可以猜到,如果cookie被改变了(因为人为的缓冲区溢出导致),那么很有可能函数的返回地址被改变了,也可能ebp改变了。这样便能检测危险所在。
现在继续分析_security_init_cookie的代码,看看其到底做了些什么:
这个security cookie是一个已经预先定义好的全局变量(定义与gs_cookie.c中):
虽然这个值已经预先被定义好,但是在_security_init_cookie函数中会有一次机会继续对其重新赋值。
如果cookie已经初始化过,就不继续处理,但是如果在32位下,其高字节为0x0000,则需要对其进行重新赋值(获得一个高度随机的值),因为这个值在进程初始化后不会再发生变化。
现在看看security cookie是怎么被编译器安排并使用的。编译器会在可能发生栈缓冲区溢出的函数时,定义一个全局cookie,它位于局部变量和返回地址之间,测试代码如下:
其中,szBuffer会在strcpy后发生溢出,反汇编代码如下:
在退出函数时在ecx中保存[ebp-4] xor ebp
调用_security_check_cookie函数,代码如下:
通过_security_check_cookie函数便能知道ebp是否发生变化了。这是栈帧被破坏的一个标志。如果ebp发生变化了,则会调用__report_gsfailure显示错误。
现在分别看看在/MD和/MT下CRT入口函数的区别:
1./MT
__tmainCRTStartup代码如下:(crt0.c中)
大致的流程步骤如下:
1.设置应用程序类型,判断是否为托管程序
2.初始化堆
3.初始化多线程环境
4.初始化全局数据和浮点寄存器
5.获取命令行参数和环境变量
6.调用main/WinMain函数
分别深入源码;
1.设置应用程序类型,判断是否为托管程序
主要看判断是否为托管程序,跟进check_managed_app():
/***
*check_managed_app() - Check for a managed executable
*
*Purpose:
* Determine if the EXE the startup code is linked into is a managed app
* by looking for the COM Runtime Descriptor in the Image Data Directory
* of the PE or PE+ header.
*
*Entry:
* None
*
*Exit:
* 1 if managed app, 0 if not.
*
*Exceptions:
*
*******************************************************************************/
static int __cdecl check_managed_app (
void
)
{
PIMAGE_DOS_HEADER pDOSHeader;
PIMAGE_NT_HEADERS pPEHeader;
pDOSHeader = &__ImageBase;
if (pDOSHeader->e_magic != IMAGE_DOS_SIGNATURE)
{
return 0;
}
pPEHeader = (PIMAGE_NT_HEADERS) ((BYTE *) pDOSHeader + pDOSHeader->e_lfanew);
if (pPEHeader->Signature != IMAGE_NT_SIGNATURE)
{
return 0;
}
if (pPEHeader->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR_MAGIC)
{
return 0;
}
/* prefast assumes we are overflowing __ImageBase */
#pragma warning(push)
#pragma warning(disable:26000)
if (pPEHeader->OptionalHeader.NumberOfRvaAndSizes <= IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR)
{
return 0;
}
#pragma warning(pop)
return pPEHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR].VirtualAddress != 0
ab4f
;
}
原理很简单,通过判断PE节表中的最后一项IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR,它保存了.net的一些高级结构IMAGE_COR20_HEADER.
2.初始化堆
_heap_init这个函数在vs2012之前的版本和之后的版本中实现是不一样的,在讨论这个前可以先推荐一个关于堆的文章(http://msdn.microsoft.com/en-us/library/ms810466.aspx).
先看vs2008版本的代码:
/***
*_heap_init() - Initialize the heap
*
*Purpose:
* Setup the initial C library heap.
*
* NOTES:
* (1) This routine should only be called once!
* (2) This routine must be called before any other heap requests.
*
*Entry:
* <void>
*Exit:
* Returns 1 if successful, 0 otherwise.
*
*Exceptions:
* If heap cannot be initialized, the program will be terminated
* with a fatal runtime error.
*
*******************************************************************************/
int __cdecl _heap_init (
int mtflag
)
{
#if defined _M_AMD64 || defined _M_IA64
// HEAP_NO_SERIALIZE is incompatible with the LFH heap
mtflag = 1;
#endif /* defined _M_AMD64 || defined _M_IA64 */
// Initialize the "big-block" heap first.
if ( (_crtheap = HeapCreate( mtflag ? 0 : HEAP_NO_SERIALIZE,
BYTES_PER_PAGE, 0 )) == NULL )
return 0;
#ifndef _WIN64
// Pick a heap, any heap
__active_heap = __heap_select();
if ( __active_heap == __V6_HEAP )
{
// Initialize the small-block heap
if (__sbh_heap_init(MAX_ALLOC_DATA_SIZE) == 0)
{
HeapDestroy(_crtheap);
_crtheap=NULL;
return 0;
}
}
#ifdef CRTDLL
else if ( __active_heap == __V5_HEAP )
{
if ( __old_sbh_new_region() == NULL )
{
HeapDestroy( _crtheap );
_crtheap=NULL;
return 0;
}
}
#endif /* CRTDLL */
#elif defined _M_AMD64 || defined _M_IA64
{
// Enable the Low Fragmentation Heap for AMD64 and IA64 by default
// It's the 8 byte overhead heap, and has generally better
// performance charateristics than the 16 byte overhead heap,
// particularly for apps that perform lots of small allocations
ULONG HeapType = 2;
HeapSetInformation(_crtheap, HeapCompatibilityInformation,
&HeapType, sizeof(HeapType));
}
#endif /* defined _M_AMD64 || defined _M_IA64 */
return 1;
}
win32程序分为两种:
1.控制台(/SUBSYSTEM:CONSOLE )
2.GUI(/SUBSYSTEM:WINDOWS)
首先看控制台版本的:
写一段最简单的,或者就直接使用编译器参数的默认main函数,如下:
// EntryFunction.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { return 0; }
1.使用/MD选项
F9下断到main函数的标签处,F11后,在Call Stack中回溯到_tmainCRTStartup(),当前文件为crtexe.c。如下:
2.使用/MT选项
F9下断到main函数的标签处,F11后,在Call Stack中回溯到_tmainCRTStartup(),当前文件为crt0.c。如下:
也就是说,不同的运行时链接方式,其实现代码也是不同的。但是其入口点函数的名称是相同的,都是_tmainCRTStartup。这便是Windows下的启动函数(真正的入口函数)。
下面我们分别讨论,这两者在入口函数中都干了些什么。
首先看_security_init_cookie,这个函数在两个实现文件中是一样的,代码如下:
/*** *__security_init_cookie(cookie) - init buffer overrun security cookie. * *Purpose: * Initialize the global buffer overrun security cookie which is used by * the /GS compile switch to detect overwrites to local array variables * the potentially corrupt the return address. This routine is called * at EXE/DLL startup. * *Entry: * *Exit: * *Exceptions: * *******************************************************************************/ void __cdecl __security_init_cookie(void) { UINT_PTR cookie; FT systime={0}; LARGE_INTEGER perfctr; /* * Do nothing if the global cookie has already been initialized. On x86, * reinitialize the cookie if it has been previously initialized to a * value with the high word 0x0000. Some versions of Windows will init * the cookie in the loader, but using an older mechanism which forced the * high word to zero. */ if (__security_cookie != DEFAULT_SECURITY_COOKIE #if defined (_M_IX86) && (__security_cookie & 0xFFFF0000) != 0 #endif /* defined (_M_IX86) */ ) { __security_cookie_complement = ~__security_cookie; return; } /* * Initialize the global cookie with an unpredictable value which is * different for each module in a process. Combine a number of sources * of randomness. */ GetSystemTimeAsFileTime(&systime.ft_struct); #if defined (_WIN64) cookie = systime.ft_scalar; #else /* defined (_WIN64) */ cookie = systime.ft_struct.dwLowDateTime; cookie ^= systime.ft_struct.dwHighDateTime; #endif /* defined (_WIN64) */ cookie ^= GetCurrentThreadId(); cookie ^= GetCurrentProcessId(); #if _CRT_NTDDI_MIN >= NTDDI_VISTA #if defined (_WIN64) cookie ^= (((UINT_PTR)GetTickCount64()) << 56); #endif /* defined (_WIN64) */ cookie ^= (UINT_PTR)GetTickCount64(); #endif /* _CRT_NTDDI_MIN >= NTDDI_VISTA */ QueryPerformanceCounter(&perfctr); #if defined (_WIN64) cookie ^= (((UINT_PTR)perfctr.LowPart << 32) ^ perfctr.QuadPart); #else /* defined (_WIN64) */ cookie ^= perfctr.LowPart; cookie ^= perfctr.HighPart; #endif /* defined (_WIN64) */ /* * Increase entropy using ASLR relocation */ cookie ^= (UINT_PTR)&cookie; #if defined (_WIN64) /* * On Win64, generate a cookie with the most significant word set to zero, * as a defense against buffer overruns involving null-terminated strings. * Don't do so on Win32, as it's more important to keep 32 bits of cookie. */ cookie &= 0x0000FFFFffffFFFFi64; #endif /* defined (_WIN64) */ /* * Make sure the cookie is initialized to a value that will prevent us from * reinitializing it if this routine is ever called twice. */ if (cookie == DEFAULT_SECURITY_COOKIE) { cookie = DEFAULT_SECURITY_COOKIE + 1; } #if defined (_M_IX86) else if ((cookie & 0xFFFF0000) == 0) { cookie |= ( (cookie|0x4711) << 16); } #endif /* defined (_M_IX86) */ __security_cookie = cookie; __security_cookie_complement = ~cookie; }
_security_init_cookie(使用/GS选项后提供)的作用是通过初始化一个全部变量用来检测局部缓冲区溢出和对函数返回地址潜在的修改(这可能导致程序执行流程改变,以至于执行不安全的shellcode)。
下图是使用了/GS选项后的函数栈帧:
L | ||
ARG2 | 参数2 | |
ARG1 | 参数1 | |
COOKIE VAR | cookie变量 | |
EBP | EBP栈指针 | |
H | RET | 函数返回地址 |
现在继续分析_security_init_cookie的代码,看看其到底做了些什么:
这个security cookie是一个已经预先定义好的全局变量(定义与gs_cookie.c中):
* The global security cookie. This name is known to the compiler. * Initialize to a garbage non-zero value just in case we have a buffer overrun * in any code that gets run before __security_init_cookie() has a chance to * initialize the cookie to the final value. */ DECLSPEC_SELECTANY UINT_PTR __security_cookie = DEFAULT_SECURITY_COOKIE; DECLSPEC_SELECTANY UINT_PTR __security_cookie_complement = ~(DEFAULT_SECURITY_COOKIE);
虽然这个值已经预先被定义好,但是在_security_init_cookie函数中会有一次机会继续对其重新赋值。
如果cookie已经初始化过,就不继续处理,但是如果在32位下,其高字节为0x0000,则需要对其进行重新赋值(获得一个高度随机的值),因为这个值在进程初始化后不会再发生变化。
现在看看security cookie是怎么被编译器安排并使用的。编译器会在可能发生栈缓冲区溢出的函数时,定义一个全局cookie,它位于局部变量和返回地址之间,测试代码如下:
// EntryFunction.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include <string.h> int _tmain(int argc, _TCHAR* argv[]) { char szBuffer[5]; strcpy( szBuffer, "hello world" ); return 0; }
其中,szBuffer会在strcpy后发生溢出,反汇编代码如下:
int _tmain(int argc, _TCHAR* argv[]) { 00EE1990 push ebp 00EE1991 mov ebp,esp 00EE1993 sub esp,0D4h 00EE1999 push ebx 00EE199A push esi 00EE199B push edi 00EE199C lea edi,[ebp-0D4h] 00EE19A2 mov ecx,35h 00EE19A7 mov eax,0CCCCCCCCh 00EE19AC rep stos dword ptr es:[edi] 00EE19AE mov eax,dword ptr ds:[00F53084h] ;将全局变量cookie保存到eax 00EE19B3 xor eax,ebp ;将eax与ebp异或后保存到eax 00EE19B5 mov dword ptr [ebp-4],eax ;将异或的结果保存到ebp-4 char szBuffer[5]; strcpy( szBuffer, "hello world" ); 00EE19B8 push 0F3FD34h 00EE19BD lea eax,[szBuffer] 00EE19C0 push eax 00EE19C1 call _strcpy (0EDF758h) 00EE19C6 add esp,8 return 0; 00EE19C9 xor eax,eax } 00EE19CB push edx 00EE19CC mov ecx,ebp 00EE19CE push eax 00EE19CF lea edx,ds:[0EE19FCh] 00EE19D5 call @_RTC_CheckStackVars@8 (0EDF5E6h) 00EE19DA pop eax 00EE19DB pop edx 00EE19DC pop edi 00EE19DD pop esi 00EE19DE pop ebx 00EE19DF mov ecx,dword ptr [ebp-4] ;将ebp-4中的值(之前cookie与ebp异或后的值)保存到ecx 00EE19E2 xor ecx,ebp ;将ecx与ebp异或并保存到ecx中(如果ebp被破坏了,那么ecx中存放的应该与之前的cookie不同,否则相同) 00EE19E4 call @__security_check_cookie@4 (0EDF1DBh) ;fastcall 使用ecx传递参数(ecx存放了计算得出的cookie值) 00EE19E9 add esp,0D4h 00EE19EF cmp ebp,esp 00EE19F1 call __RTC_CheckEsp (0EDFEA1h) 00EE19F6 mov esp,ebp 00EE19F8 pop ebp 00EE19F9 ret }在进入函数时在ebp-4中保存cookie xor ebp
在退出函数时在ecx中保存[ebp-4] xor ebp
调用_security_check_cookie函数,代码如下:
/* x86 version written in asm to preserve all regs */ __asm { cmp ecx, __security_cookie 00B4AFD0 cmp ecx,dword ptr ds:[0BB3084h] ;比较ecx中的值是否与全局cookie相同 jne failure 00B4AFD6 jne failure (0B4AFDAh);不同则跳转到failure标签(_report_gsfailure函数)中 rep ret /* REP to avoid AMD branch prediction penalty */ 00B4AFD8 rep ret ;相同则直接返回,说明栈帧是没有被破坏的 failure: jmp __report_gsfailure 00B4AFDA jmp ___report_gsfailure (0B3F645h)
通过_security_check_cookie函数便能知道ebp是否发生变化了。这是栈帧被破坏的一个标志。如果ebp发生变化了,则会调用__report_gsfailure显示错误。
现在分别看看在/MD和/MT下CRT入口函数的区别:
1./MT
__tmainCRTStartup代码如下:(crt0.c中)
__declspec(noinline) int __tmainCRTStartup( void ) { int initret; int mainret=0; int managedapp; #ifdef _WINMAIN_ _TUCHAR *lpszCommandLine = NULL; WORD showWindowMode = 0; #ifndef _KERNELX showWindowMode = __crtGetShowWindowMode(); __set_app_type(_GUI_APP); #endif /* _KERNELX */ #else /* _WINMAIN_ */ #ifndef _KERNELX __set_app_type(_CONSOLE_APP); ;设置当前程序类型:1.console 2.gui #endif /* _CRT_APP */ #endif /* _WINMAIN_ */ /* * Determine if this is a managed application */ managedapp = check_managed_app(); ;检查是否为托管程序 if ( !_heap_init() ) /* initialize heap */ ;初始化堆(这个在vs2012之前和之后版本不一样) fast_error_exit(_RT_HEAPINIT); /* write message and die */ if( !_mtinit() ) /* initialize multi-thread */ ;初始化多线程环境 fast_error_exit(_RT_THREAD); /* write message and die */ /* Enable buffer count checking if linking against static lib */ _CrtSetCheckCount(TRUE); /* * Initialize the Runtime Checks stuff */ #if defined (_RTC) _RTC_Initialize(); #endif /* defined (_RTC) */ /* * Guard the remainder of the initialization code and the call * to user's main, or WinMain, function in a __try/__except * statement. */ __try { if (_ioinit() < 0) fast_error_exit(_RT_LOWIOINIT); /* write message and die */ #if !defined (_KERNELX) /* get wide cmd line info */ _tcmdln = (_TSCHAR *)GetCommandLineT(); ;获取命令行参数 /* get wide environ info */ _tenvptr = (_TSCHAR *)GetEnvironmentStringsT(); ;获取环境变量 if ( _tsetargv() < 0 ) _amsg_exit(_RT_SPACEARG); if ( _tsetenvp() < 0 ) _amsg_exit(_RT_SPACEENV); #endif /* !defined (_CRT_APP) */ initret = _cinit(TRUE); /* do C data initialize */ ;执行全局数据和浮点寄存器的初始化 if (initret != 0) _amsg_exit(initret); #ifdef _WINMAIN_ #if !defined (_KERNELX) lpszCommandLine = _twincmdln(); #endif /* _KERNELX */ mainret = _tWinMain( (HINSTANCE)&__ImageBase, ;调用gui的WinMain函数 NULL, lpszCommandLine, showWindowMode ); #else /* _WINMAIN_ */ #if !defined (_KERNELX) _tinitenv = _tenviron; mainret = _tmain(__argc, _targv, _tenviron); ;调用console的main函数 #else /* !defined (_KERNELX) */ mainret = _tmain(0, NULL, NULL); #endif /* !defined (_KERNELX) */ #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; }
大致的流程步骤如下:
1.设置应用程序类型,判断是否为托管程序
2.初始化堆
3.初始化多线程环境
4.初始化全局数据和浮点寄存器
5.获取命令行参数和环境变量
6.调用main/WinMain函数
分别深入源码;
1.设置应用程序类型,判断是否为托管程序
主要看判断是否为托管程序,跟进check_managed_app():
/***
*check_managed_app() - Check for a managed executable
*
*Purpose:
* Determine if the EXE the startup code is linked into is a managed app
* by looking for the COM Runtime Descriptor in the Image Data Directory
* of the PE or PE+ header.
*
*Entry:
* None
*
*Exit:
* 1 if managed app, 0 if not.
*
*Exceptions:
*
*******************************************************************************/
static int __cdecl check_managed_app (
void
)
{
PIMAGE_DOS_HEADER pDOSHeader;
PIMAGE_NT_HEADERS pPEHeader;
pDOSHeader = &__ImageBase;
if (pDOSHeader->e_magic != IMAGE_DOS_SIGNATURE)
{
return 0;
}
pPEHeader = (PIMAGE_NT_HEADERS) ((BYTE *) pDOSHeader + pDOSHeader->e_lfanew);
if (pPEHeader->Signature != IMAGE_NT_SIGNATURE)
{
return 0;
}
if (pPEHeader->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR_MAGIC)
{
return 0;
}
/* prefast assumes we are overflowing __ImageBase */
#pragma warning(push)
#pragma warning(disable:26000)
if (pPEHeader->OptionalHeader.NumberOfRvaAndSizes <= IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR)
{
return 0;
}
#pragma warning(pop)
return pPEHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR].VirtualAddress != 0
ab4f
;
}
原理很简单,通过判断PE节表中的最后一项IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR,它保存了.net的一些高级结构IMAGE_COR20_HEADER.
2.初始化堆
_heap_init这个函数在vs2012之前的版本和之后的版本中实现是不一样的,在讨论这个前可以先推荐一个关于堆的文章(http://msdn.microsoft.com/en-us/library/ms810466.aspx).
先看vs2008版本的代码:
/***
*_heap_init() - Initialize the heap
*
*Purpose:
* Setup the initial C library heap.
*
* NOTES:
* (1) This routine should only be called once!
* (2) This routine must be called before any other heap requests.
*
*Entry:
* <void>
*Exit:
* Returns 1 if successful, 0 otherwise.
*
*Exceptions:
* If heap cannot be initialized, the program will be terminated
* with a fatal runtime error.
*
*******************************************************************************/
int __cdecl _heap_init (
int mtflag
)
{
#if defined _M_AMD64 || defined _M_IA64
// HEAP_NO_SERIALIZE is incompatible with the LFH heap
mtflag = 1;
#endif /* defined _M_AMD64 || defined _M_IA64 */
// Initialize the "big-block" heap first.
if ( (_crtheap = HeapCreate( mtflag ? 0 : HEAP_NO_SERIALIZE,
BYTES_PER_PAGE, 0 )) == NULL )
return 0;
#ifndef _WIN64
// Pick a heap, any heap
__active_heap = __heap_select();
if ( __active_heap == __V6_HEAP )
{
// Initialize the small-block heap
if (__sbh_heap_init(MAX_ALLOC_DATA_SIZE) == 0)
{
HeapDestroy(_crtheap);
_crtheap=NULL;
return 0;
}
}
#ifdef CRTDLL
else if ( __active_heap == __V5_HEAP )
{
if ( __old_sbh_new_region() == NULL )
{
HeapDestroy( _crtheap );
_crtheap=NULL;
return 0;
}
}
#endif /* CRTDLL */
#elif defined _M_AMD64 || defined _M_IA64
{
// Enable the Low Fragmentation Heap for AMD64 and IA64 by default
// It's the 8 byte overhead heap, and has generally better
// performance charateristics than the 16 byte overhead heap,
// particularly for apps that perform lots of small allocations
ULONG HeapType = 2;
HeapSetInformation(_crtheap, HeapCompatibilityInformation,
&HeapType, sizeof(HeapType));
}
#endif /* defined _M_AMD64 || defined _M_IA64 */
return 1;
}
相关文章推荐
- Windows下程序入口函数与_security_init_cookie
- 1-1 Windows下启动函数(真正的入口函数) 之 运行时链接方式
- 第一个Windows应用程序--启动函数、入口点函数与退出函数
- 第一个Windows应用程序--启动函数、入口点函数与退出函数
- 第一个Windows应用程序--启动函数、入口点函数与退出函数
- Windows驱动(2)-Driver安装调试报错security_init_cookie&蓝屏
- main函数之前--真正的函数执行入口或开始
- main函数之前--真正的函数执行入口或开始
- Windows线程入口函数--静态或者全局!
- 寻找真正的入口(OEP)--广义ESP定律
- linux驱动的入口函数module_init的加载和释放
- [转载] linux启动分析(5)---C程序入口函数start_kernel
- Windows应用程序启动函数
- 寻找真正的入口(OEP)--广义ESP定律
- kernel 启动过程之三, start_kernel()函数 概叙!init/main.c
- kernel 启动过程之三, start_kernel()函数 概叙!init/main.c
- windows程序入口函数
- Windows内核分析之一 —— 内核入口函数
- 加壳脱壳]寻找真正的入口(OEP)--广义ESP定律
- kernel 启动过程之三, start_kernel()函数 概叙!init/main.c