GLIB入口函数
GLIB入口函数
main
函数并不是程序运行的第一个函数,在main
之前,还有初始化函数_init()
; main
函数之后还有_fini()
函数收尾。
入口函数
操作系统在装载程序之后,首先运行的代码并非 main 函数的第一行,而是某些别的代码。这些代码负责准备好 main 函数执行所需要的环境,并且负责调用 main 函数。在 main 返回之后,它会记录 main 函数的返回值,调用某些清理函数,然后结束进程。
这些特殊的代码称为入口函数或入口点(Entry Point),因不同的平台上而有不同的名字。程序的入口点实际上就是一个程序的初始化和结束的部分,它往往是 C/C++ 运行库的一部分。
一个典型的 C++ 程序其运行步骤大致如下:
- 操作系统在创建进程后,把控制权交到程序入口点,即运行库中某个入口函数;
- 入口函数对运行库和运行环境进行初始化,包括堆、I/O、线程、全局变量构造等;
- 入口函数在完成初始化之后,调用 main 函数,开始执行程序主体部分;
- main 函数执行完毕以后,返回到入口函数,入口函数进行清理工作,包括全局变量的析构、堆销毁、关闭 I/O 等,然后进行系统调用结束进程。
由此我们看到,运行库才是程序世界的创世者和终结者,是这个隐秘世界的真正主宰。
Windows 和 Linux 平台下主要的 C 运行库分别为 glibc (GNU C Library) 和 MSVC (Microsoft Visual Run-time),它们都是标准 C 语言运行库的超集,各自对 C 标准库进行了一些扩展。本文主要关注 Linux 平台下的 C/C++ 运行库。
_init() 和 _fini()
运行库在每个目标文件中引入了两个特殊的段:”.init” 段和 “.fini” 段,运行库会保证所有位于这两个段中代码先/后于 main 函数执行。对于全局、静态对象,又引入了两个特殊的段 “.ctors” 和 “.dtors”,分别处理这类对象的构造和析构。这两个段经过编译器的层层处理,最终会分别合入到 “.init” 段和 “.fini” 段当中。
void _init(void);
void _fini(void);
_init()
:负责初始化全局变量、加载动态库、分配内存等。位于 .init
段
- 在
dlopen()
返回前或库被装载时调用
_fini()
:负责释放内存、卸载动态库等。位于 .fini
段
- dlclose()返回前调用
- main()返回后调用
- exit()被调用时
定义自己的版本
_init()/_fini()
是共享库的初始化和析构函数。但这两个函数是给GCC编译器用的,我们不能直接使用它们,但可以用下述另外两种方法来实现。
(1)C库应该使用 __attribute__((constructor))
和 __attribute__((destructor))
属性来输出它的构造函数和析构函数。如下所示:
void __attribute__((constructor)) my_init(void);
void __attribute__((destructor)) my_fini(void);
gdb调试结果:
(gdb) bt 构造函数
#0 my_init () at test.cpp:4
#1 0x00007ffff7de8cb6 in call_init (env=<optimized out>, argv=0x7fffffffc7a8, argc=1) at ../csu/libc-start.c:145
#2 __libc_start_main_impl (main=0x555555555165 <main()>, argc=1, argv=0x7fffffffc7a8, init=<optimized out>, fini=<optimized out>,
rtld_fini=<optimized out>, stack_end=0x7fffffffc798) at ../csu/libc-start.c:347
#3 0x0000555555555071 in _start ()
(gdb) bt 析构函数
#0 my_fini () at test.cpp:8
#1 0x00007ffff7fcc0ca in _dl_call_fini (closure_map=closure_map@entry=0x7ffff7ffe2c0) at ./elf/dl-call_fini.c:43
#2 0x00007ffff7fcfc8e in _dl_fini () at ./elf/dl-fini.c:114
#3 0x00007ffff7e005f5 in __run_exit_handlers (status=0, listp=0x7ffff7f96680 <__exit_funcs>, run_list_atexit=run_list_atexit@entry=true,
run_dtors=run_dtors@entry=true) at ./stdlib/exit.c:111
#4 0x00007ffff7e0072a in __GI_exit (status=<optimized out>) at ./stdlib/exit.c:141
#5 0x00007ffff7de8b91 in __libc_start_call_main (main=main@entry=0x555555555165 <main()>, argc=argc@entry=1, argv=argv@entry=0x7fffffffc7a8)
at ../sysdeps/nptl/libc_start_call_main.h:74
#6 0x00007ffff7de8c45 in __libc_start_main_impl (main=0x555555555165 <main()>, argc=1, argv=0x7fffffffc7a8, init=<optimized out>,
fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffc798) at ../csu/libc-start.c:360
#7 0x0000555555555071 in _start ()
(2)C++库可以利用全局变量的构造和析构函数来实现类似的功能:
struct InitFini
{
InitFini() { ... }
~InitFini() { ... }
};
InitFini s_global; // 全局变量的构造和析构会发生在main函数之外,即.init段和.fini段
gdb调试结果:
(gdb) bt # 构造函数
#0 InitFini (this=0xa507bc) at test.cpp:7
#1 0x00a4f5e0 in __static_initialization_and_destruction_0 (__initialize_p=1, __priority=65535) at test.cpp:15
#2 0x00a4f611 in global constructors keyed to test () at test.cpp:21
#3 0x00a4f66a in __do_global_ctors_aux () from ./libtest.so
#4 0x00a4f4a9 in _init () from ./libtest.so
#5 0x002c8b4b in call_init () from /lib/ld-linux.so.2
#6 0x002c8c4a in _dl_init_internal () from /lib/ld-linux.so.2
#7 0x002bb83f in _dl_start_user () from /lib/ld-linux.so.2
(gdb) bt # 析构函数
#0 ~InitFini (this=0x0) at test.cpp:9
#1 0x00a4f5b3 in __tcf_0 () at test.cpp:15
#2 0x00303e6f in __cxa_finalize () from /lib/libc.so.6
#3 0x00a4f532 in __do_global_dtors_aux () from ./libtest.so
#4 0x00a4f692 in _fini () from ./libtest.so
#5 0x002c9058 in _dl_fini () from /lib/ld-linux.so.2
#6 0x00303c69 in exit () from /lib/libc.so.6
#7 0x002eddee in __libc_start_main () from /lib/libc.so.6
#8 0x080483b5 in _start ()
_exit()和exit()
_exit()
:直接进入内核,其会关闭进程所有的文件描述符,清理内存以及其他一些内核清理函数,但不会刷新流(stdin,stdout,stderr)
exit()
:则先执行一些刷新处理(在进程退出之前要检查文件状态,将文件缓冲区中的内容写回文件)再调用_exit()
进入内核。
简单记就是 _exit()
是即刻退出,exit()
是优雅退出。差别主要在于流是否刷新,且后者会调用前者。
exit()与_exit()函数最大的区别在于:exit()函数在调用exit系统之前要检查文件的打开情况,把文件缓冲区的内容写回文件。由于Linux标准函数中,“缓冲I/O”的操作,其特征即对应每一个打开的文件,在内存中都有一片缓冲区。每次读文件时,会连续读出若干条记录,在下次读文件时就可以直接从内存的缓冲区读取;同样每次写文件的时候也仅仅是写入内存的缓冲区,等满足了一定的条件(如达到了一定数量或遇到特定字符等),再将缓冲区中的内容一次性写入文件。这种技术大大增加了文件读写的速度。
这时如果用_exit()函数直接将进程关闭,缓冲区的数据将会丢失。
所以上面两个程序,第一个用
exit(0)
的,终端会输出“Hello Leap!”,而第二个用_exit(0)
的不会有输出。
因为_exit()
没有执行__exit_funcs
链表中的函数,如关闭fd、刷新IO缓冲区,导致printf使用的stdout缓存没有得到刷新。
入口函数总体流程分析
- 初始化和OS版本有关的全局变量。
- 初始化堆。
- 初始化I/O
- 获取命令行参数和环境变量。
- 初始化C库的一些数据。
- 调用main并记录返回值。
- 检查错误并将main的返回值返回。
代码讲解
glibc的程序入口为_start
(由ld链接器默认的链接脚本所指定)。_start
由汇编实现,实现如下:
;libc\sysdeps\i386\elf\Start.S:
_start:
xorl %ebp, %ebp
popl %esi
movl %esp, %ecx
pushl %esp
pushl %edx
pushl $__libc_csu_fini
pushl $__libc_csu_init
pushl %ecx
pushl %esi
pushl main
call __libc_start_main
hlt
改写后的伪代码如下:
void _start()
{
%ebp = 0;
int argc = pop from stack
char** argv = top of stack;
__libc_start_main( main, argc, argv, __libc_csu_init, __libc_csu_fini, edx, top of stack );
}
其中argv
除了指向参数表外,还隐含紧接着环境变量表。这个环境变量表要在__libc_start_main
里从argv
内提取出来。
所以实际执行代码的函数是__libc_start_main
,代码很长:
int __libc_start_main(
int (*main) (int, char**, char**),
int argc,
char* __unbounded* __unbounded ubp_av, // argc、参数表和环境变量表
__typeof (main) init, // _init()的函数指针
void (*fini) (void), // _fini()的函数指针
void (*rtld_fini) (void), // 和动态加载有关的收尾工作,rtld是RunTime LoaDer。
void* __unbounded stack_end)// 栈底地址(即最高的栈基址)
{
#if __BOUNDED_POINTERS__
char **argv;
#else
#define argv ubp_av
#endif
int result;
char** ubp_ev = &ebp_av[argc + 1]; // 计算环境变量的开始地址
INIT_ARGV_and_ENVIRON; // 宏展开后是 _environ = ubp_ev; 绕过环境变量数组__environ指针指向原来紧跟在argv数组之后的环境变量部分
__libc_stack_end = stack_end;
// ...很长一段,主要函数如下:
__pthread_initialize_minimal();
cxa_atexit(rtld_fini, NULL, NULL); // 把rtld_fini注册为exit回调
libc_init_first (argc, argv, __environ);
__cxa_atexit(fini, NULL, NULL); // 把fini注册为exit回调
(*init)(argc, argv, __environ);
// 这部分代码进行了一连串的系统调用,注意到,__cxa_atexit()函数是glibc的内部函数,等同于atexit,用于将参数指定的函数簇在main()结束之后调用。所以以参数传入的fini和rtld_fini均是用于main结束之后调用的
result = main (argc, argv, __environ); // 执行main()函数并收集返回值
exit (result);
}
__libc_start_main
参数中,init
和 fini
这两个函数指针实际分别指向函数 __libc_csu_init
和 __libc_csu_fini
,csu 为 “C start up” 缩写。
紧接着,__libc_csu_init
函数又调用了 _init
函数,内部调用了一个叫作 __do_global_ctorx_aux
的函数。这个函数并不属于 glibc,而是来自 gcc 提供的一个目标文件,简化后的代码如下:
// gcc/Crtstuff.c
void __do_global_ctors_aux(void){
/* call constructor functions */
unsigned long nptrs = (unsigned long) __CTOR_LIST__[0];
unsigned i;
for (i = nptrs; i >= 1; i--)
__CTOR_LIST__[i]();
}
这段代码中,__CTOR_LIST__
数组的第一个元素存放着这个数组元素的个数,然后将第一个元素之后的元素都当做是函数指针,并逐个调用。事实上,__CTOR_LIST__
里面存放的是所有全局对象的构造函数的指针,所有的全局对象会在这里被构造。
而 _libc_csu_fini
同理调用 __do_global_dtors_aux
做相反的事情。
可以看到,main()
函数 return
的结果是给CRT运行时(runtime)的。另外也能看出,即使main()
返回了,exit()
也会被调用。exit()
是进程正常退出的必经之路。
而exit()
函数的实现如下:
void exit(int status) {
while (__exit_funcs != NULL) {
...
__exit_funcs = __exit_funcs->next; //__exit_funcs其实是一个链表,里面挂着一个一个函数,这些函数是默认就有的,例如清理io缓存关闭文件等等,如果想要在这个链表中添加自己的函数,可以用atexit()函数来添加。
}
...
_exit(status);
}
最后执行的是_exit
,和_start
一样由汇编实现:
_exit:
movl 4(%esp), %ebp
movl $__NR_exit, %eax ; __NR_exit 是 exit() 系统调用,是操作系统用来回收资源,杀死进程的
int $0x80 ;中断指令陷入内核,执行%eax寄存器中设置的系统调用编号对应的系统调用(就是前一句设置的)
hlt
总结
本文作者:3to4
本文链接:https://www.cnblogs.com/3to4/p/18377869
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。