主要关注程序的启动过程。
通常写代码时,我们认为程序的入口是main
函数,但是实际上有一些现象值得我们怀疑该结论是不是正确的。比如全局变量的初始化,C++全局变量构造函数的调用,C++静态对象的构造函数调用。
程序开始运行的入口不是main
,在进入main
之前程序会先准备好环境运行一些必要的代码才进入main
,运行这些代码的函数称为入口函数。入口函数根据平台的不同而不同,其实实际上程序初始化和结束的地方,一个典型的程序的运行步骤如下:
主要关注glibc静态库用于可执行文件的情况。
程序的启动代码在glibc源代码的glibc-2.33\sysdeps\i386\start.S
中,下面是i386的实现的简化代码,从代码中能够看到最终调用了__libc_start_main
。
ENTRY (_start)
xorl %ebp, %ebp !寄存器清零
popl %esi !此时esi就是argc的值
movl %esp, %ecx !此时栈顶的一部分就是argv,ecx指向第一个参数的栈地址
andl $0xfffffff0, %esp
pushl %eax /* Push garbage because we allocate 28 more bytes. */
pushl %esp
pushl %edx /* Push address of the shared library termination function. */
#ifdef PIC
/* Load PIC register. */
call 1f
addl $_GLOBAL_OFFSET_TABLE_, %ebx
/* Push address of our own entry points to .fini and .init. */
leal __libc_csu_fini@GOTOFF(%ebx), %eax
pushl %eax
leal __libc_csu_init@GOTOFF(%ebx), %eax
pushl %eax
pushl %ecx /* Push second argument: argv. */
pushl %esi /* Push first argument: argc. */
# ifdef SHARED
pushl main@GOT(%ebx)
# else
/* Avoid relocation in static PIE since _start is called before
it is relocated. Don't use "leal main@GOTOFF(%ebx), %eax"
since main may be in a shared object. Linker will convert
"movl main@GOT(%ebx), %eax" to "leal main@GOTOFF(%ebx), %eax"
if main is defined locally. */
movl main@GOT(%ebx), %eax
pushl %eax
# endif
call __libc_start_main@PLT
#else
/* Push address of our own entry points to .fini and .init. */
!下面是传入__libc_start_main的参数
pushl $__libc_csu_fini
pushl $__libc_csu_init
pushl %ecx /* Push second argument: argv. */
pushl %esi /* Push first argument: argc. */
pushl $main
call __libc_start_main // /* Call the user's main function, and exit with its value. But let the libc call main. */
#endif
hlt /* Crash if somehow `exit' does return. */
__libc_start_main
实际的定义如下:
main
:就是main函数的指针;argc
:参数个数;argv
:参数数组;init
:调用前初始化工作;fini
:调用结束的收尾工作;rtld_fini
:和动态加载相关的收尾工作;stack_end
:表明栈底的地址。STATIC int
LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
int argc, char **argv,
#ifdef LIBC_START_MAIN_AUXVEC_ARG
ElfW(auxv_t) *auxvec,
#endif
__typeof (main) init,
void (*fini) (void),
void (*rtld_fini) (void), void *stack_end)
下面是省略了部分代码的__libc_start_main
的实现,其中省略了大部分初始化的代码,我们只关注上面传入的参数的执行情况。
__environ
,环境变量在栈中位于argv后面;init
函数进行初始化;main
,退出程序。/* Note: the fini parameter is ignored here for shared library. It
is registered with __cxa_atexit. This had the disadvantage that
finalizers were called in more than one place. */
STATIC int
LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
int argc, char **argv,
#ifdef LIBC_START_MAIN_AUXVEC_ARG
ElfW(auxv_t) *auxvec,
#endif
__typeof (main) init,
void (*fini) (void),
void (*rtld_fini) (void), void *stack_end)
{
/* Result of the 'main' function. */
int result;
char **ev = &argv[argc + 1];
__environ = ev;
/* Store the lowest stack address. This is done in ld.so if this is the code for the DSO. */
__libc_stack_end = stack_end;
//省略部分初始化代码
# ifdef DL_SYSDEP_OSCHECK
{
/* This needs to run to initiliaze _dl_osversion before TLS
setup might check it. */
DL_SYSDEP_OSCHECK (__libc_fatal);
}
# endif
/* Initialize libpthread if linked in. */
if (__pthread_initialize_minimal != NULL)
__pthread_initialize_minimal ();
/* Register the destructor of the dynamic linker if there is any. */
if (__glibc_likely (rtld_fini != NULL))
__cxa_atexit ((void (*) (void *)) rtld_fini, NULL, NULL);
#ifndef SHARED
/* Perform early initialization. In the shared case, this function
is called from the dynamic loader as early as possible. */
__libc_early_init (true);
/* Call the initializer of the libc. This is only needed here if we
are compiling for the static library in which case we haven't
run the constructors in `_dl_start_user'. */
__libc_init_first (argc, argv, __environ);
/* Register the destructor of the program, if any. */
if (fini)
__cxa_atexit ((void (*) (void *)) fini, NULL, NULL);
#endif
if (init)
(*init) (argc, argv, __environ MAIN_AUXVEC_PARAM);
/* Nothing fancy, just call the function. */
result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);
exit (result);
}
我们可以简单看下exit
的实现,exit实际的函数实体是__run_exit_handlers
,其中又一个参数是exit_funcs
我们可以怀疑是一个函数指针列表。从其结构体看__exit_funcs
是一个存储注册的函数的链表:
void exit (int status)
{
__run_exit_handlers (status, &__exit_funcs, true, true);
}
struct exit_function_list{
struct exit_function_list *next;
size_t idx;
struct exit_function fns[32];
};
struct exit_function_list *__exit_funcs = &initial;
能够从代码中exit
代码中会遍历注册的函数,一个一个执行,最终调用_exit
退出程序。
void
attribute_hidden
__run_exit_handlers (int status, struct exit_function_list **listp,
bool run_list_atexit, bool run_dtors)
{
/* We do it this way to handle recursive calls to exit () made by
the functions registered with `atexit' and `on_exit'. We call
everyone on the list and use the status value in the last
exit (). */
while (true)
{
struct exit_function_list *cur;
__libc_lock_lock (__exit_funcs_lock);
restart:
cur = *listp;
//执行函数调用相关的代码省略
*listp = cur->next;
if (*listp != NULL)
/* Don't free the last element in the chain, this is the statically
allocate element. */
free (cur);
__libc_lock_unlock (__exit_funcs_lock);
}
if (run_list_atexit)
RUN_HOOK (__libc_atexit, ());
_exit (status);
}
作者书中提到的系统版本比较老,我现在使用的是windows10,下面是vs2017的实现。
mainCRTStartup
在Microsoft Visual Studio\2017\Community\VC\Tools\MSVC\14.16.27023\crt\src\vcruntime\exe_main.cpp
中,其最终调用的是__scrt_common_main()
,之后在完成一些cookie
的初始化之后,调用__scrt_common_main_seh
。下面是微软官方文档对__security_init_cookie
的描述:
__security_init_cookie
全局安全 Cookie 用于在使用 /GS(缓冲区安全检查)编译的代码中和使用异常处理的代码中提供缓冲区溢出保护。 进入受到溢出保护的函数时,Cookie 被置于堆栈之上;退出时,会将堆栈上的值与全局 Cookie 进行比较。 它们之间存在任何差异则表示已经发生缓冲区溢出,并导致该程序的立即终止。
通常 ,__security_init_cookie 初始化时由 CRT 调用。 如果绕过 CRT 初始化(例如,如果使用/ENTRY指定入口点)则必须自己__security_init_cookie。 如果未 __security_init_cookie, 则全局安全 Cookie 将设置为默认值,缓冲区溢出保护会遭到入侵。 由于攻击者可以利用此默认 Cookie 值来阻止缓冲区溢出检查,因此,建议在定义自己的 入口点__security_init_cookie 始终调用命令。
extern "C" int mainCRTStartup(){
return __scrt_common_main();
}
// This is the common main implementation to which all of the CRT main functions
// delegate (for executables; DLLs are handled separately).
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:
__security_init_cookie();
return __scrt_common_main_seh();
}
下面是__scrt_common_main_seh
的完整实现,其基本流程和glibc类似,都是先初始化环境,注册相关的函数调用,然后遍历函数指针列表并调用,之后调用invoke_main
函数(invoke_main
实际上就是对main
的一个包装),最后调用eixt
退出。
_ACRTIMP int* __cdecl __p___argc (void);
_ACRTIMP char*** __cdecl __p___argv (void);
_ACRTIMP wchar_t*** __cdecl __p___wargv(void);
#ifdef _CRT_DECLARE_GLOBAL_VARIABLES_DIRECTLY
extern int __argc;
extern char** __argv;
extern wchar_t** __wargv;
#else
#define __argc (*__p___argc()) // Pointer to number of command line arguments
#define __argv (*__p___argv()) // Pointer to table of narrow command line arguments
#define __wargv (*__p___wargv()) // Pointer to table of wide command line arguments
#endif
static int __cdecl invoke_main(){
return main(__argc, __argv, _get_initial_narrow_environment());
}
static __declspec(noinline) int __cdecl __scrt_common_main_seh(){
if (!__scrt_initialize_crt(__scrt_module_type::exe))
__scrt_fastfail(FAST_FAIL_FATAL_APP_EXIT);
bool has_cctor = false;
__try
{
bool const is_nested = __scrt_acquire_startup_lock();
if (__scrt_current_native_startup_state == __scrt_native_startup_state::initializing)
{
__scrt_fastfail(FAST_FAIL_FATAL_APP_EXIT);
}
else if (__scrt_current_native_startup_state == __scrt_native_startup_state::uninitialized)
{
__scrt_current_native_startup_state = __scrt_native_startup_state::initializing;
//依次调用c的函数,__xi_a, __xi_z分别为c函数的首地址和尾地址
if (_initterm_e(__xi_a, __xi_z) != 0)
return 255;
//依次调用c++的函数,__xc_a, __xc_z分别为c++函数的首地址和尾地址
_initterm(__xc_a, __xc_z);
__scrt_current_native_startup_state = __scrt_native_startup_state::initialized;
}
else
{
has_cctor = true;
}
__scrt_release_startup_lock(is_nested);
// If this module has any dynamically initialized __declspec(thread)
// variables, then we invoke their initialization for the primary thread
// used to start the process:
_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);
}
// If this module has any thread-local destructors, register the
// callback function with the Unified CRT to run on exit.
_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();
//退出程序
if (!__scrt_is_managed_app())
exit(main_result);
if (!has_cctor)
_cexit();
// Finally, we terminate the CRT:
__scrt_uninitialize_crt(true, false);
return main_result;
}
__except (_seh_filter_exe(GetExceptionCode(), GetExceptionInformation()))
{
// Note: We should never reach this except clause.
int const main_result = GetExceptionCode();
if (!__scrt_is_managed_app())
_exit(main_result);
if (!has_cctor)
_c_exit();
return main_result;
}
}
操作系统需要给用户提供读取相关上硬件上的文件或者设备的能力,但是又不能完全向用户开放对硬件的操作能力。在操作系统层面,对于文件的操作Linux提供了文件描述符,而windows则使用句柄来控制某个文件。
在Linux中,一个文件会有一个fd,每个进程维护一份私有的文件打开表,这个表格是一个指针数组,每个元素指向一个打开的文件对象,而用户拿到的fd就是该表的索引。这个表格由内核维护,用户是无法直接访问的。
在windows上类似,只不过windows上的句柄并不是索引号,而是通过特定的算法变换得到的一个数值。
另外,C中使用到的FILE
并不是文件的真实指针,而是关于fd或者句柄相关联的一个指针。
文中提到了FILE的实现,但是新版的实现已经改变,因此不在赘述。
typedef struct _iobuf
{
void* _Placeholder;
} FILE;
CRT(C Runtime Library,C运行时库)包含了程序能够正常运行的代码,以及相关的标准库实现等基本的内容。
Windows下CRT的源码目录为Windows Kits\10\Source\10.0.17134.0\ucrt
。
一个C语言运行库的基本功能大致为:
不赘述,请参考open-std-c99。
运行库是和操作系统紧密相关的,C语言仅仅是针对不同操作系统平台的一个抽象层。glibc和MSVCCRT分别是Linux和windows平台下的C实现。
glibc是Linux平台的C标准库实现,其包含了标准库的头文件和相关的二进制文件。二进制文件提供了静态和动态库,静态库位于/usr/lib32/
下,动态库位于/lib32/
。除了标准库外,还提供了一个运行库,比如/usr/lib32/crt1.o /usr/lib32/crti.o /usr/lib32/crtn.o
。通过查看符号我们能够发现,crt1.o
包含了入口启动函数相关的实现,而crti.o
包含了初始化和结束的后处理相关的实现,crti.o,crtn.o
共同组成_init,_fini
的实现。编程语言本身编译器相关的,当然也需要包含一些gcc相关的库,具体目录在/usr/lib/gcc/x86_64-linux-gnu/7/
下,其中x86-64-linux-gnu/7
可以换成自己的系统版本,不再赘述。
➜ tmp nm /usr/lib32/crt1.o
00000000 D __data_start
00000000 W data_start
00000040 T _dl_relocate_static_pie
00000000 R _fp_hw
U _GLOBAL_OFFSET_TABLE_
00000000 R _IO_stdin_used
U __libc_csu_fini
U __libc_csu_init
U __libc_start_main
U main
00000000 T _start
➜ tmp nm /usr/lib32/crti.o
00000000 T _fini
U _GLOBAL_OFFSET_TABLE_
w __gmon_start__
00000000 T _init
00000000 T __x86.get_pc_thunk.bx
➜ tmp nm /usr/lib32/crtn.o
nm: /usr/lib32/crtn.o: no symbols
➜ tmp objdump -dr /usr/lib32/crti.o
/usr/lib32/crti.o: file format elf32-i386
Disassembly of section .init:
00000000 <_init>:
0: 53 push %ebx
1: 83 ec 08 sub $0x8,%esp
4: e8 fc ff ff ff call 5 <_init+0x5>
5: R_386_PC32 __x86.get_pc_thunk.bx
9: 81 c3 02 00 00 00 add $0x2,%ebx
b: R_386_GOTPC _GLOBAL_OFFSET_TABLE_
f: 8b 83 00 00 00 00 mov 0x0(%ebx),%eax
11: R_386_GOT32X __gmon_start__
15: 85 c0 test %eax,%eax
17: 74 05 je 1e <_init+0x1e>
19: e8 fc ff ff ff call 1a <_init+0x1a>
1a: R_386_PLT32 __gmon_start__
Disassembly of section .gnu.linkonce.t.__x86.get_pc_thunk.bx:
00000000 <__x86.get_pc_thunk.bx>:
0: 8b 1c 24 mov (%esp),%ebx
3: c3 ret
Disassembly of section .fini:
00000000 <_fini>:
0: 53 push %ebx
1: 83 ec 08 sub $0x8,%esp
4: e8 fc ff ff ff call 5 <_fini+0x5>
5: R_386_PC32 __x86.get_pc_thunk.bx
9: 81 c3 02 00 00 00 add $0x2,%ebx
b: R_386_GOTPC _GLOBAL_OFFSET_TABLE_
MSVC CRT库存储于Microsoft Visual Studio\2017\Community\VC\Tools\MSVC\14.16.27023\lib\x64
,其中的14.16.27023
可以替换为自己的版本,对应的库的名称的命名规则为libc[p][mt][d].lib
:
在编译是可以通过vs的选项选择编译的库版本,默认是libcmt.lib
。我们随便写一个C++文件使用,cl编译,使用dumpbin查看依赖的库。从下面的输出能够看到当前的main.obj
还依赖libcmt,oldnames,libcpmt
三个库。
E:\code\tmp>cl /c main.cpp
用于 x64 的 Microsoft (R) C/C++ 优化编译器 19.16.27045 版
版权所有(C) Microsoft Corporation。保留所有权利。
main.cpp
D:\Microsoft Visual Studio\2017\Community\VC\Tools\MSVC\14.16.27023\include\xlocale(319): warning C4530: 使用了 C++ 异常处理程序,但未启用展开语义。请指定 /EHsc
E:\code\tmp>dumpbin /DIRECTIVES main.obj
Microsoft (R) COFF/PE Dumper Version 14.16.27045.0
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file main.obj
File Type: COFF OBJECT
Linker Directives
-----------------
/FAILIFMISMATCH:_MSC_VER=1900
/FAILIFMISMATCH:_ITERATOR_DEBUG_LEVEL=0
/FAILIFMISMATCH:RuntimeLibrary=MT_StaticRelease
/DEFAULTLIB:libcpmt
/FAILIFMISMATCH:_CRT_STDIO_ISO_WIDE_SPECIFIERS=0
/DEFAULTLIB:LIBCMT
/DEFAULTLIB:OLDNAMES
线程的访问能力非常自由,它可以访问进程内存里的所有数据,甚至包括其他线程的堆栈(如果它知道其他线程的堆栈地址,然而这是很少见的情况),但实际运用中线程也拥有自己的私有存储空间,包括:
虽然C++11提供了thread的实现,但是其使用非常有限,无法对线程进行更加精确的控制。
C/C++标准库早期是不提供线程支持的,那么使用相关的库函数就无法做到线程安全。
为了支持多线程CRT针对多线程环境进行了一些改进。
使用TLS
多线程环境下有些变量的地址存放在线程的TLS中,比如errno。
加锁
多线程环境中,一些库函数会在函数内部加锁保证线程安全。
改进函数调用方式
改变一些库函数保证其线程安全比如strtok
msvc的改进版本为strtok_s
,glibc版本为strtok_r
。但是无法做到向后兼容。
如果希望某个变量线程私有,就需要将变量存放到TLS上。gcc可以使用__thread
修饰,msvc可以使用__declspec(thread)
修饰,这样每个变量在各自的线程上都有一个副本。
windows TLS实现
使用__declspec(thread)
的变量不会被放到数据段,而是放到.tls
段中,当系统启动一个新线程时,系统会从堆中分配一块内存,将tls的内容拷贝到这块空间供线程使用。对于存放在TLS的全局变量,PE文件中的数据目录结构有一项标记为IMAGE_DIRET_ENTRY_TLS
的项保存有TLS表,该表中存储了TLS所有TLS变量的构造函数和析构函数的地址,系统可以根据这些地址调用对应的函数完成构造和析构。TLS表本身存储在.rdata
中。
现在有了TLS空间和表格,线程如何访问?对于windows线程,系统会构建一个线程环境快(TEB),该结构中存储了线程的堆栈、线程ID等信息,其中一项就是TLS数组,课题通过该数组访问。
显式TLS
使用__thread,__declspec(thread)
修饰的变量程序员只需要直到他们是线程私有的变量即可,不需要管理,成为隐式TLS。相对的需要陈谷许愿管理的TLS叫做显式TLS。这部分了解就好。
glibc中存在两个段.init
和.finit
组成_init()_finit()
两个函数,分别执行初始化和善后的工作。本节就了解下他们如何完成对象的构造和析构工作。
我们使用下面的代码反汇编查看初始化过程。
//gcc main.cpp && objdump -D a.out
#include
class myclass{
public:
myclass(){ printf("constructor");}
~myclass(){ printf("destructor");}
};
myclass cls;
int main(){
return 0;
}
我们找到_start
能够看到初始化调用了__libc_csu_init
。
00000000000004f0 <_start>:
4f0: 31 ed xor %ebp,%ebp
4f2: 49 89 d1 mov %rdx,%r9
4f5: 5e pop %rsi
4f6: 48 89 e2 mov %rsp,%rdx
4f9: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
4fd: 50 push %rax
4fe: 54 push %rsp
4ff: 4c 8d 05 7a 01 00 00 lea 0x17a(%rip),%r8 # 680 <__libc_csu_fini>
506: 48 8d 0d 03 01 00 00 lea 0x103(%rip),%rcx # 610 <__libc_csu_init>
50d: 48 8d 3d e6 00 00 00 lea 0xe6(%rip),%rdi # 5fa
514: ff 15 c6 0a 20 00 callq *0x200ac6(%rip) # 200fe0 <__libc_start_main@GLIBC_2.2.5>
51a: f4 hlt
51b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
glibc在执行main之前会调用init相关函数,其调用的是__libc_csu_init
,在这个函数中调用了_init
即.init
中的代码。
void __libc_csu_init (int argc, char **argv, char **envp){
/* For dynamically linked executables the preinit array is executed by
the dynamic linker (before initializing any shared object). */
#ifndef LIBC_NONSHARED
/* For static executables, preinit happens right before init. */
{
const size_t size = __preinit_array_end - __preinit_array_start;
size_t i;
for (i = 0; i < size; i++)
(*__preinit_array_start [i]) (argc, argv, envp);
}
#endif
#if ELF_INITFINI
_init ();
#endif
const size_t size = __init_array_end - __init_array_start;
for (size_t i = 0; i < size; i++)
(*__init_array_start [i]) (argc, argv, envp);
}
//下面是__libc_csu_init的反汇编
0000000000000610 <__libc_csu_init>:
!省略部分代码
623: 48 8d 2d ce 07 20 00 lea 0x2007ce(%rip),%rbp # 200df8 <__init_array_end>
62a: 53 push %rbx
62b: 41 89 fd mov %edi,%r13d
62e: 49 89 f6 mov %rsi,%r14
631: 4c 29 e5 sub %r12,%rbp
634: 48 83 ec 08 sub $0x8,%rsp
638: 48 c1 fd 03 sar $0x3,%rbp
63c: e8 77 fe ff ff callq 4b8 <_init>
!省略部分代码
_init
的反汇编代码和书上不一样,这里调用了__gmon_start__
。但是__gmon_start
并不是初始化程序,实际上应该是call *%rax
这一句,但是我们不知道具体的地址。
00000000000004b8 <_init>:
4b8: 48 83 ec 08 sub $0x8,%rsp
4bc: 48 8b 05 25 0b 20 00 mov 0x200b25(%rip),%rax # 200fe8 <__gmon_start__>
4c3: 48 85 c0 test %rax,%rax
4c6: 74 02 je 4ca <_init+0x12>
4c8: ff d0 callq *%rax
4ca: 48 83 c4 08 add $0x8,%rsp
4ce: c3 retq
我们可以尝试在汇编代码中查找printf
寻找析构和构造数反向推导具体的调用位置。最后反推的路径为printf@plt->_ZN7myclassC1Ev->_Z41__static_initialization_and_destruction_0ii->_GLOBAL__sub_I_cls
。我们可以看下中间的那个函数的内容,在这个函数中调用了构造函数_ZN7myclassC1Ev
,并且使用__cxa_atexit
注册析构函数_ZN7myclassD1Ev
。
0000000000000735 <_Z41__static_initialization_and_destruction_0ii>:
735: 55 push %rbp
736: 48 89 e5 mov %rsp,%rbp
739: 48 83 ec 10 sub $0x10,%rsp
73d: 89 7d fc mov %edi,-0x4(%rbp)
740: 89 75 f8 mov %esi,-0x8(%rbp)
743: 83 7d fc 01 cmpl $0x1,-0x4(%rbp)
747: 75 2f jne 778 <_Z41__static_initialization_and_destruction_0ii+0x43>
749: 81 7d f8 ff ff 00 00 cmpl $0xffff,-0x8(%rbp)
750: 75 26 jne 778 <_Z41__static_initialization_and_destruction_0ii+0x43>
752: 48 8d 3d c0 08 20 00 lea 0x2008c0(%rip),%rdi # 201019
759: e8 32 00 00 00 callq 790 <_ZN7myclassC1Ev>
75e: 48 8d 15 a3 08 20 00 lea 0x2008a3(%rip),%rdx # 201008 <__dso_handle>
765: 48 8d 35 ad 08 20 00 lea 0x2008ad(%rip),%rsi # 201019
76c: 48 8d 3d 3d 00 00 00 lea 0x3d(%rip),%rdi # 7b0 <_ZN7myclassD1Ev>
773: e8 88 fe ff ff callq 600 <__cxa_atexit@plt>
778: 90 nop
779: c9 leaveq
77a: c3 retq
这个函数的大概的函数原型可能为:
__static_initialization_and_destruction_0(int, int){
myclass::myclass();
atexit(myclass::~myclass());
}
根据书上的描述这个函数_GLOBAL__sub_I_cls
是由编译器生成的,负责初始化全局静态对象并且注册析构函数,gcc会在编译单元的目标文件中生成.ctors
存放该函数的地址,而析构会生成.dtor
段存放析构函数的地址。
书上的实现已经和现在的一些实现不同,感觉后续需要单独了解下glibc的全局构造和析构的具体情况。可参考How to find global static initializations
前面mainCRTStartup
源码中有调用_initterm_e
来编译一个表格中所有的函数指针并执行相关内容,完成一些初始化。
typedef void (__cdecl* _PVFV)(void);
typedef int (__cdecl* _PIFV)(void);
typedef void (__cdecl* _PVFI)(int);
#ifndef _M_CEE
_ACRTIMP void __cdecl _initterm(
_In_reads_(_Last - _First) _In_ _PVFV* _First,
_In_ _PVFV* _Last
);
_ACRTIMP int __cdecl _initterm_e(
_In_reads_(_Last - _First) _PIFV* _First,
_In_ _PIFV* _Last
);
#endif
其中__xi_a
和__xc_z
等都是一个全局变量,且对应的变量是被分配在对应的段中的,比如__xi_a
就在.CRT$XIA
中。这些段是只读的,在链接时就会被合并到一起,形成了全局初始化函数数组。析构也是类似的都是使用atexit
注册析构函数。
#pragma section(".CRT$XCA", long, read) // First C++ Initializer
#pragma section(".CRT$XCAA", long, read) // Startup C++ Initializer
#pragma section(".CRT$XCZ", long, read) // Last C++ Initializer
#define _CRTALLOC(x) __declspec(allocate(x))
#ifndef _M_CEE
typedef void (__cdecl* _PVFV)(void);
typedef int (__cdecl* _PIFV)(void);
extern _CRTALLOC(".CRT$XIA") _PIFV __xi_a[]; // First C Initializer
extern _CRTALLOC(".CRT$XIZ") _PIFV __xi_z[]; // Last C Initializer
extern _CRTALLOC(".CRT$XCA") _PVFV __xc_a[]; // First C++ Initializer
extern _CRTALLOC(".CRT$XCZ") _PVFV __xc_z[]; // Last C++ Initializer
extern _CRTALLOC(".CRT$XPA") _PVFV __xp_a[]; // First Pre-Terminator
extern _CRTALLOC(".CRT$XPZ") _PVFV __xp_z[]; // Last Pre-Terminator
extern _CRTALLOC(".CRT$XTA") _PVFV __xt_a[]; // First Terminator
extern _CRTALLOC(".CRT$XTZ") _PVFV __xt_z[]; // Last Terminator
#endif
从上面大概能够看出linux和windows都是在运行前遍历全局表,逐个调用对应全局/静态对象的构造函数,并使用atexit
注册析构函数。