C++ 反汇编
配套资源下载
环境及工具
反汇编引擎工作原理(可略过)
基本数据类型的表现形式
程序的真正入口
各种表达式的求值过程
本文档使用 MrDoc 发布
-
+
首页
程序的真正入口
**<center> 慢慢看,看不懂就只看最后的如何识别 main </center>** -------------- ## 程序的真正入口 - 应用程序被操作系统加载时,操作系统会**分析执行文件内的数据,并分配相关资源,读取执行文件中的代码和数据到合适的内存单元,然后才是执行入口代码**,**入口代码其实并不是 main 或 WinMain 函数**,通常是 **mainCRTStartup、wmainCRTStartup、WinMainCRTStartup 或 wWinMainCRTStartup**,具体视编译选项而定。其中 **`mainCRTStartup` 和 `wmainCRTStartup` 是控制台环境下多字节编码和 Unicode 编码的启动函数**,而 **`WinMainCRTStartup` 和 `wWinMainCRTStartup` 则是 Windows 环境下多字节编码和 Unicode 编码的启动函数**。在开发过程中,**C++也允许程序员自己指定入口**。 ## 了解启动函数 - VS C++在控制台和多字节编码环境下的启动函数为 mainCRTStartup,由系统库 KERNEL32.dll 负责调用,在 mainCRTStartup 中再调用 main 函数。我使用 VS2022 查看 main 函数之前的代码,操作如下:**在调试环境下,依次选择菜单“调试”→“窗口”→“调用堆栈”,打开出栈窗口(快捷键:Ctrl+Alt+C)**。如果显示没有的话,**右键调用堆栈窗口→“显示外部代码”**即可。  **<center>64 位控制台</center>**  **<center>32 位控制台</center>**  **<center>书上案例</center>** - 下面的是书上的图,可以**看到 32 和 64 位的环境下的函数是不一样的**。图中我的函数名没有像书中一样加载出来。解决方案如下:**调试环境下,依次选择菜单“调试”“窗口”“模块”**,可以看到有些模块显示“无法查找或打开 PDB 文件。”,**右键模块“加载符号”**。问题如果还没有解决,**右键模块->符号设置->修改设置(加载首选项、缓存位置、符号文件位置)->加载所有符号。**如果还是不行,则**调试 ->选项->调试->常规->勾选“启用源服务器支持”(包含的 3 个子选项不用勾选)。然后"调试->符号->修改缓存位置,然后重启VS调试下载符号"**。这个时间比较长,可以在“输出”窗口中观察,需要等一会。 <br>  参考 [微软符号服务器下载符号总结](https://www.cnblogs.com/maifengqiang/archive/2011/10/11/2206925.html) - 关注 32 位的调用函数,程序运行时调用的8个函数,依次是 1. ntdll.dll!__RtlUserThreadStart@8 2. ntdll.dll!__RtlUserThreadStart 3. kernel32.dll!@BaseThreadInitThunk@12 4. 当前.exe!mainCRTStartup 5. 当前.exe!__scrt_common_main 6. 当前.exe!__scrt_common_main_seh 7. 当前.exe!invoke_main 8. 当前.exe!main - 64 位的调用函数,依次为: 1. ntdll.dll!RtlUserThreadStart() 2. kernel32.dll!BaseThreadInitThunk() 3. 当前.exe!mainCRTStartup(void *) 4. 当前.exe!__scrt_common_main() 5. 当前.exe!__scrt_common_main_seh() 6. 当前.exe!invoke_main() 7. 当前.exe!main() - 在Windows下一个进程是使用 **CreateProcess** 函数创建的,这个函数调用的成功,标志着有一个程序被加载至内存并准备开始运行了。系统在建立进程时,会完成给进程分配资源,初始化进程的内存空间,初始化进程内核对象,初始化进程环境快(PEB),加载PE映像文件,初始化全局堆,载入DLL等等工作(以后再研究),但是这些工作完成后却并不代表我们的程序已经开始执行了。这是因为进程本身是有“惰性”的,它不会主动执行代码,所以操作系统这时候会**建立程序的第一个线程**,而我们的代码将从这个线程来开始执行。 - 且不谈系统提供使用的 **CreateThread** 等函数,首先需要知道的是实际上一个线程的建立最终是由 **NtCreateThreadEx** 函数实现的,而这个函数则是将 58 作为调用号传入 EAX 然后直接使用 sysenter 指令陷入内核,从而使得线程能够最终建立。 - 内核在接收到新建线程的请求后就会替我们着手新建一个线程,并且为该线程初始化一些参数(包括 TEB 等,以后再研究),最终将线程的入口设置为 **RtlUserThreadStart** 函数,该函数是由 ntdll.dll 导出的。RtlUserThreadStart 的主要工作就是建立 SHE 的异常处理函数链,它有两个参数,其中一个是用户之前指定的线程入口函数,另一个是传给该入口的参数。注意虽然 RtlUserThreadStart 有两个参数,但这并不意味着曾经有人调用过它并且给它传参了,这两个参数实际上这是操作系统硬性写入的两个值。我们必须知道,实际上 RtlUserThreadStart 的执行只是因为操作系统把该线程 EIP 硬性设置到了 RtlUserThreadStart 的入口处而已。 - 在此之后,它会调用由线程启动参数指定的真正的线程函数(对于主线程而言,这个函数就是 **BaseThreadInitThunk**)。 - RtlUserThreadStart 会调用 **BaseThreadInitThunk** 函数,这个函数是由 kernel.dll 导出的,它主要将用户指定的参数压栈。接收两个关键参数: 1. lpStartAddr: 线程实际要执行的函数地址(对于主线程,就是CRT的入口点mainCRTStartup)。 2. lpParameter: 传递给lpStartAddr函数的参数。 - BaseThreadInitThunk的工作非常简单:只是简单地使用提供的参数调用lpStartAddr函数(即 **mainCRTStartup**)。在 lpStartAddr 函数执行完毕返回后,它会调用 ExitThread 来正确地结束线程。 - **除了 dll 文件中被调用的函数**,从 mainCRTStartup 函数开始,我们就可以看到源代码了。下面依次分析**VS环境下的**这些代码(GCC 和 Clang 环境下有区别): ```c // mainCRTStartup 函数 extern "C" DWORD mainCRTStartup(LPVOID) { return __scrt_common_main(); } ``` - 序由mainCRTStartup开始执行。这里的启动函数可能为下述四种之一。 ```c // 无控制台窗口,入口为WinMain #pragma comment( linker, "/subsystem:windows /entry:WinMainCRTStartup" ) // 无控制台窗口,但入口为main #pragma comment( linker, "/subsystem:windows /entry:mainCRTStartup" ) // 有控制台窗口,入口为main #pragma comment( linker, "/subsystem:console /entry:mainCRTStartup" ) // 有控制台窗口,但入口为WinMain #pragma comment( linker, "/subsystem:console /entry:WinMainCRTStartup" ) ``` - 在代码中直接调用了 **__scrt_common_main** 函数。 ```c // __scrt_common_main 函数 // This is the common main implementation to which all of the CRT main functions // 翻译:这是所有 CRT 主要功能的通用主要实现 // delegate (for executables; DLLs are handled separately). // 翻译:委托(对于可执行文件;DLL 单独处理)。 static __forceinline int __cdecl __scrt_common_main() { // The /GS security cookie must be initialized before any exception handling // targeting the current image is registered. No function using exception // handling can be called in the current image until after this call: // 翻译:在处理任何异常之前,必须初始化/GS安全cookie // 翻译:以当前图像为目标进行注册。没有使用异常的函数 // 翻译:可以在当前图像中调用handling,直到该调用之后: __security_init_cookie(); // 初始化缓冲区溢出全局变量 return __scrt_common_main_seh(); } ``` - **__scrt_common_main** 函数也比较简单,先是调用__**security_init_cookie**初始化缓冲区溢出全局变量,用于在函数中检查缓冲区是否溢出。 然后调用了 **__scrt_common_main_seh** 函数。 ```c // __scrt_common_main_seh 函数 // static:内部链接;__declspec(noinline):告诉编译器绝对不要将这个函数内联优化到任何调用它的地方。 static __declspec(noinline) int __cdecl __scrt_common_main_seh() { // _initialize_crt: 初始化 C 运行时库。这个函数接受一个参数,指定当前模块的类型(exe 或者 dll)。 // 失败则调用 __scrt_fastfail 立即终止进程。 if (!__scrt_initialize_crt(__scrt_module_type::exe)) __scrt_fastfail(FAST_FAIL_FATAL_APP_EXIT); bool has_cctor = false; __try { // 调用 __scrt_acquire_startup_lock() 函数获取启动锁,并将返回值赋给布尔变量 is_nested。启动锁用于确保在多线程环境下只有一个线程能够执行初始化逻辑。后续根据 __scrt_current_native_startup_state 的值进行不同的操作。 bool const is_nested = __scrt_acquire_startup_lock(); // __scrt_current_native_startup_state:全局变量,用于跟踪当前CRT初始化过程所处的阶段。它的值是在初始化的不同步骤中动态设定和更新的,而不是在某个单一时刻设定的。 // initializing:程序正在初始化中,此时调用 __scrt_fastfail() 函数,导致应用程序退出。 if (__scrt_current_native_startup_state == __scrt_native_startup_state::initializing) { __scrt_fastfail(FAST_FAIL_FATAL_APP_EXIT); } // uninitialized:表示程序尚未初始化,调用 _initterm_e() 函数执行静态构造函数(从 __xi_a 到 __xi_z 范围内的函数),如果返回值不为 0,表示初始化失败,返回255。 // 接着,调用 _initterm() 函数执行全局对象构造函数(从 __xc_a 到 __xc_z 范围内的函数)。最后,将 __scrt_current_native_startup_state 设置为 __scrt_native_startup_state::initialized,表示初始化完成。 else if (__scrt_current_native_startup_state == __scrt_native_startup_state::uninitialized) { __scrt_current_native_startup_state = __scrt_native_startup_state::initializing; // 遍历一个由函数指针组成的表,并按顺序调用其中的每一个函数,如果任何函数调用失败(返回非零值),则停止执行并返回错误码。 // 用于初始化 C 语法中的全局数据 if (_initterm_e(__xi_a, __xi_z) != 0) return 255; // 用于初始化 C++语法中的全局数据,上面是 C 语法。 _initterm(__xc_a, __xc_z); __scrt_current_native_startup_state = __scrt_native_startup_state::initialized; } // 如果以上条件都不满足,则将 has_cctor 设置为 true else { has_cctor = true; } // 释放启动锁。在多线程环境下,当一个线程完成了初始化逻辑时,可以调用该函数释放启动锁,以便其他线程能够继续执行初始化过程。 __scrt_release_startup_lock(is_nested); // 如果此模块包含任何动态初始化的 __declspec(thread) 变量, // 我们需要为启动进程的主线程调用这些变量的初始化程序: // 初始化线程局部存储变量__declspec(thread)用于声明线程局部存储(Thread-Local Storage, TLS)变量。 // 这意味着:被它修饰的全局或静态变量,不是整个进程共享一份,而是每个线程都拥有该变量的一个独立副本。一个线程对自己副本的修改,完全不会影响其他线程中的同名变量。 // __scrt_get_dyn_tls_init_callback:获取线程局部存储(TLS)变量的回调函数,用于初始化使用__declspec(thread)定义的变量。 // 总结:检查是否存在动态初始化的 __declspec(thread) 变量,并获取其初始化回调函数的指针 tls_init_callback。如果回调函数不为空且可被写入(即地址位于当前模块中),则调用此回调函数进行初始化。 _tls_callback_type const* const tls_init_callback = __scrt_get_dyn_tls_init_callback(); if (*tls_init_callback != nullptr && __scrt_is_nonwritable_in_current_image(tls_init_callback)) { (*tls_init_callback)(nullptr, DLL_THREAD_ATTACH, nullptr); } // 如果此模块包含任何线程局部 destructors(析构函数), // 需向统一 CRT(Unified CRT)注册回调函数以便在退出时执行。 // 注册线程局部存储析构函数 // __scrt_get_dyn_tls_dtor_callback:获取线程局部存储变量的析构回调函数,用于注册析构回调函数。 // 总结:检查是否存在线程局部析构函数,并获取其销毁回调函数的指针 tls_dtor_callback。如果回调函数不为空且可被写入,则将此回调函数注册为在程序退出时执行。 _tls_callback_type const * const tls_dtor_callback = __scrt_get_dyn_tls_dtor_callback(); if (*tls_dtor_callback != nullptr && __scrt_is_nonwritable_in_current_image(tls_dtor_callback)) { _register_thread_local_exe_atexit_callback(*tls_dtor_callback); } // 初始化完成调用 main()函数 int const main_result = invoke_main(); // main函数已返回;需执行退出处理。 // __scrt: Static C/C++ RunTime。_is_managed_app: 判断是否为托管应用程序。这里的“托管”(Managed)特指 .NET Framework 的托管运行环境(Common Language Runtime, CLR)。 // main()函数返回执行析构函数或 atexit 注册的函数指针,并结束程序 if (!__scrt_is_managed_app()) exit(main_result); // 如果没有静态构造函数(has_cctor 为假),则调用 _cexit() 函数来清理静态对象。 if (!has_cctor) _cexit(); // 最后,我们终止CRT(C运行时库)。 __scrt_uninitialize_crt(true, false); return main_result; } __except (_seh_filter_exe(GetExceptionCode(), GetExceptionInformation())) { // 注意:我们绝不应执行到此处 except 子句。 int const main_result = GetExceptionCode(); if (!__scrt_is_managed_app()) _exit(main_result); if (!has_cctor) _c_exit(); return main_result; } } ``` - **这里和书里面不一样,重新探究一下** ```c // invoke_main 函数 // 该函数获取main函数所需的3个参数信息之后,当调用main函数时,便可以将_argc、_argv、env这3个全局变量作为参数,传递到main函数中 static int __cdecl invoke_main() { return main(__argc, __argv, _get_initial_narrow_environment()); } ``` - 下面对部分函数进行介绍: - _initterm_e 函数:用于全局数据和浮点寄存器的初始化,该函数由两个参数组成,类型为“_PIFV *”,这是一个函数指针数组,其中保留了每个初始化函数的地址。初始化函数的类型为_PIFV,其定义原型如下所示。 ```c typedef int (__cdecl* _PIFV)(void); ``` - 如果初始化失败,返回非 0 值,程序终止运行。一般而言,_initterm_e 初始化的都是 C 语言支持库中所需的数据。参数 _xi_a 为函数指针数组的起始地址,_xi_z 为结束地址,具体如代码所示: ```c extern "C" int __cdecl _initterm_e(_PIFV* const first, _PIFV* const last) { for (_PIFV* it = first; it != last; ++it) { if (*it == nullptr) continue; int const result = (**it)(); if (result != 0) return result; } return 0; } ``` - _initterm 函数:C++全局对象和 IO 流等的初始化都是通过这个函数实现的,可以利用_initterm 函数进行数据链初始化。这个函数由两个参数组成,类型为“_PVFV *”,这也是一个函数指针数组,其中保留了每个初始化函数的地址。初始化函数的类型为_PVFV,其定义原型如下所示。 ```c typedef void (_cdecl *_PVFV)(void); ``` - 也就是说,这个初始化函数是无参数也无返回值的。大家知道,C++规定全局对象和静态对象必须在 main 函数前构造,在 main 函数返回后析构。所以,这里的_PVFV 函数指针数组就是用来代理调用构造函数的,具体如代码所示。 ```c extern "C" void __cdecl _initterm(_PVFV* const first, _PVFV* const last) { for (_PVFV* it = first; it != last; ++it) { if (*it == nullptr) continue; (**it)(); } } ``` - C++所需数据的初始化操作会在如代码所示的_initterm 函数调用时执行,一般都是全局对象或静态对象初始化函数。 - __scrt_get_dyn_tls_init_callback 函数:获取线程局部存储(TLS)变量的回调函数,用于初始化使用__declspec(thread)定义的变量。 - __scrt_get_dyn_tls_dtor_callback 函数:获取线程局部存储变量的析构回调函数,用于注册析构回调函数。 - invoke_main 函数:该函数获取 main 函数所需的 3 个参数信息之后,当调用 main 函数时,便可以将_argc、_argv、env 这3个全局变量作为参数,传递到main函数中。 - exit 函数:执行析构函数或 atexit 注册的函数指针,并结束程序。 ## main 函数的识别 - 控制台程序 main 函数有 3 个参数,分别是命令行参数个数、命令行参数信息和环境变量信息,而且 main 函数是启动函数中唯一具有 3 个参数的函数。同理,WinMain 也是启动函数中唯一具有 4 个参数的函数。main 函数返回后需要调用 exit 函数,结束程序根据 main 函数调用的特征,**找到入口代码第一次调用 exit 函数处,离 exit 最近的且有 3 个参数的函数通常就是 main 函数(有 4 个参数的函数通常就是 WinMain 函数)。** - 利用 exit 函数定位,**如果 IDA 无法识别出 exit 函数,就需要加载 sig 文件重新识别**。
别卷了
2025年8月26日 14:10
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
PDF文档(打印)
分享
链接
类型
密码
更新密码