3.2 了解VS2019的启动函数

VS C++在控制台和多字节编码环境下的启动函数为mainCRTStartup,由系统库KERNEL32.dll负责调用,在mainCRTStartup中再调用main函数。使用VS2019进行调试时,入口断点总是停留在main函数的首地址处。如何挖掘main函数之前的代码呢?我们可以利用VS2019的栈回溯功能。在调试环境下,依次选择菜单“调试”→“窗口”→“调用堆栈”,打开出栈窗口(快捷键:Ctrl+Alt+C)。如图3-1所示,此窗口显示了程序启动后,函数的调用流程。

图3-1 栈回溯窗口

图3-1中显示了程序运行时调用的8个函数,依次是__RtlUserThreadStart@8、__RtlUserThreadStart、@BaseThreadInitThunk@12、mainCRTStartup、__scrt_common_main、__scrt_common_main_seh、invoke_main和main。其中@BaseThreadInitThunk@12调用mainCRTStartup,我们无法查看mainCRTStartup函数之前的高级源码,而VS 2019则提供了mainCRTStartup函数的源码,安装完整版的VS2019并下载符号文件就可以查看。双击调用栈窗口中的mainCRTStartup函数,查看函数的内部实现,如代码清单3-1所示。

代码清单3-1 mainCRTStartup函数在VS2019中的代码片段

extern "C" int mainCRTStartup()
{
  return __scrt_common_main();
}

static __forceinline int __cdecl __scrt_common_main()
{
  //初始化缓冲区溢出全局变量
  __security_init_cookie();
  return __scrt_common_main_seh();
}

static __declspec(noinline) int __cdecl __scrt_common_main_seh()
{
  //用于初始化C语法中的全局数据
    if (_initterm_e(__xi_a, __xi_z) != 0)
                return 255;

  //用于初始化C++语法中的全局数据
  _initterm(__xc_a, __xc_z);

  //初始化线程局部存储变量
  _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);
  }

  //注册线程局部存储析构函数
  _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()函数返回执行析构函数或atexit注册的函数指针,并结束程序
  if (!__scrt_is_managed_app())
    exit(main_result);
}

static int __cdecl invoke_main()
{
  //调用main函数,传递命令行参数信息
    return main(__argc, __argv, _get_initial_narrow_environment());
}

代码清单3-1展示了在VS2019控制台程序的默认启动函数中做了一系列初始化工作。下面详细解读启动函数的工作流程。

  • __security_init_cookie函数:初始化缓冲区溢出全局变量,用于在函数中检查缓冲区是否溢出。
  • _initterm_e函数:用于全局数据和浮点寄存器的初始化,该函数由两个参数组成,类型为“_PIFV *”,这是一个函数指针数组,其中保留了每个初始化函数的地址。初始化函数的类型为_PIFV,其定义原型如下所示。
typedef int  (__cdecl* _PIFV)(void);

如果初始化失败,返回非0值,程序终止运行。一般而言,_initterm_e初始化的都是C语言支持库中所需的数据。参数_xi_a为函数指针数组的起始地址,_xi_z为结束地址,具体如代码清单3-2所示。

代码清单3-2 _initterm_e函数的代码片段

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,其定义原型如下所示。
typedef void (_cdecl *_PVFV)(void);

也就是说,这个初始化函数是无参数也无返回值的。大家知道,C++规定全局对象和静态对象必须在main函数前构造,在main函数返回后析构。所以,这里的_PVFV函数指针数组就是用来代理调用构造函数的,具体如代码清单3-3所示。

代码清单3-3 _initterm函数的代码片段

extern "C" void __cdecl _initterm(_PVFV* const first, _PVFV* const last)
{
  for (_PVFV* it = first; it != last; ++it)
  {
    if (*it == nullptr)
       continue;

    (**it)();
  }
}

C++所需数据的初始化操作会在如代码清单3-3所示的_initterm函数调用时执行,一般都是全局对象或静态对象初始化函数。关于全局对象初始化流程的更多内容请见第10章。

  • __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注册的函数指针,并结束程序。

VS编译器的版本不同,mainCRTStartup函数也可能会有所不同,GCC和Clang编译器的入口函数与所选择的库相关。本书只针对VS2019版本进行讲解,其他VS版本或编译器入口函数也需要做一些同样的初始化工作,读者可自行分析。