目录
1. 引言 2. C/C++运行库 3. 静态Glibc && 可执行文件 入口/终止函数 4. 动态Glibc && 可执行文件 入口/终止函数 5. 静态Glibc && 共享库 入口/终止函数 6. 动态Glibc && 共享库 入口/终止函数 7. 静态库/共享库->编译/使用、动态加载 8. 和静态库/动态库相关的辅助工具
1. 引言
0x1: glibc
Any Unix-like operating system needs a C library: the library which defines the 'system calls' and other basic facilities such as open, malloc, printf, exit... The GNU C Library is used as the C library in the GNU systems and most systems with the Linux kernel. The GNU C Library is primarily designed to be a portable and high performance C library. It follows all relevant standards including ISO C11 and POSIX.1-2008. It is also internationalized and has one of the most complete internationalization interfaces known.
glibc是GNU发布的libc库,即c运行库。glibc是linux系统中最底层的api,几乎其它任何运行库都会依赖于glibc。glibc除了封装linux操作系统所提供的系统服务外,它本身也提供了许多其它一些必要功能服务的实现。由于 glibc 囊括了几乎所有的 UNIX 通行的标准,其内容包罗万象。而就像其他的 UNIX 系统一样,其内含的档案群分散于系统的树状目录结构中,像一个支架一般撑起整个操作系统
Glibc主要实现的功能如下
1. string: 字符串处理 2. signal: 信号处理 3. dlfcn: 管理共享库的动态加载 4. direct: 文件目录操作 5. elf: 共享库的动态加载器,也即interpreter 6. iconv: 不同字符集的编码转换 7. inet: socket接口的实现 8. intl: 国际化,也即gettext的实现 9. io 10. linuxthreads 11. locale: 本地化 12. login: 虚拟终端设备的管理,及系统的安全访问 13. malloc: 动态内存的分配与管理 14. nis 15. stdlib: 其它基本功能
0x2: 初始化代码和终止代码(Initialization and Termination code)
要明白的是,对于可执行文件、和共享对象来说,它们本质上都是可执行代码的集合体,区别仅仅在于
1. 可执行ELF文件可以独立运行可执行代码,并且可以载入外部的动态共享库ELF中的代码进行执行 2. 共享对象自身仅仅是可执行代码的一个"集合体",需要被载入到可执行程序中得以执行
不管是可执行文件的执行、还是共享对象被载入到可执行文件中,它们都有对应的"初始化代码"和"终止代码"
1. 可执行文件 1) 初始化代码: 初始化代码在用户程序开始执行前执行,即在main()函数之前执行 2) 终止代码: 终止代码则在进程退出时执行,即程序return、exit返回的时候 //可执行文件的初始化代码和终止代码由"Glibc初始化入口函数"负责执行 2. 共享对象 1) 共享对象的初始化代码(_init()函数)在可共享对象文件获得控制权之前执行 2) 共享对象的终止代码(_fint()函数)在共享对象被卸载之后执行 //共享对象的初始化代码和终止代码由"动态连接器(ld-linux.so.2)"负责执行
ELF的初始化工作和终止工作都统一由Glibc运行库负责
0x3: Linux程序运行的基本流程
1. 操作系统运行用户程序时,将ELF文件映射到内存中 2. 当它看到可执行文件中的"PT_INERP"段时,操作系统将"PT_INTERP"段指定的"动态共享库加载器(ld.linux.so.2)"映射进内存,并通过栈向其传递它所需要的参数,并跳到"动态共享库加载器(ls-linux.so.2)"的入口处开始执行,将控制权交给"动态共享库加载器(ld.linux.so.2)" 3. "动态共享库加载器(ld.linux.so.2)"开始自举(Bootstrap) 1) 动态链接器入口地址就是自举代码的入口,当操作系统将进程控制权交给动态链接器时,动态链接器的自举代码即开始执行 2) 自举代码会找到自己的GOT。而GOT的第一个入口保存的即是".dynamic"段的偏移地址,由此获得了动态链接器本身的".dynamic"段 3) 通过".dynamic"段中的信息,自举代码便可以获得动态链接器本身的重定位表和符号表等,从而得到动态链接器本身的重定位入口,先将它们全部重定位 4) 从这一步开始动态链接器代码中才可以开始使用自己的全局变量和静态变量 4. 自举完成后,动态链接器根据可执行文件".dynamic"段中的"DT_NEEDED"元素开始依次加载(广度优先)依赖的共享对象,并加入它的符号表。如果这个共享对象依赖其它的共享对象,动态链接器也会加载它们。当这个过程结束时,所有需要的共享对象都已加载进内存,动态链接器也具有了程序和所有共享库的符号表 5. 当完成动态链接器的装载、普通共享对象的装载之后,链接器开始重新遍历可执行文件和每个共享对象的重定位表,将它们的GOT/PLT中的每个需要重定位的位置进行修正 要注意的是,链接器对共享库的重定位顺序是遵循"图的后序遍历顺序"依赖进行的,即如果A对象依赖B对象,则先处理B对象再处理A对象,加载时重定位包括: 1) 对数据的引用(在.rel.dyn段中),需要初始化一个 GOT(在.got中)项为一个全局符号的地址 2) 对代码的引用(在.rel.plt段中),需要初始化一个 GOT(在.got.plt)项为PLT表中第二条指令的地址(Procedure Linkage Table) 6. 重定位完成后就,如果某个共享对象有".init"段,那么动态链接器会执行".init"段中的代码,用以实现共享对象特有的初始化过程,例如 1) 共享对象中的C++全局/静态对象的构造就是通过"init"段来初始化 当完成了重定位和初始化后,所有的准备工作就宣告完成了,所需要的共享对象也都已经装载并且链接完成了,程序执行逻辑准备开始运行可执行程序本身了,值得注意的是,为了能在程序退出时让动态链接器有机会调用共享对象的终止代码,动态链接器会传递 一个"终止函数"(用以调用共享对象的终止代码)给用户程序 7. Glibc运行库负责在可执行程序main()函数执行前对运行库和程序的运行环境进行必要的初始化工作,包括 1) 堆、栈 2) I/O 3) 线程 4) 全局变量构造 8. 用户程序开始执行 1) 注册动态链接器的终止函数 2) 注册已加载的共享对象的终止函数 3) 注册可执行程序自身的终止函数 然后调用用户程序的初始化代码(和共享对象一样,可执行程序也有_init()函数) 9. 调用用户定义的main()函数,正式开始执行程序主体部分,这已部分是程序员可以控制的部分 10. main()函数返回后,返回到Glibc运行库入口函数,Glibc运行库入口函数进行清理工作,,包括 1) 全局变量析构 2) 堆的销毁 3) 关闭I/O Glibc的入口函数进行销毁操作,以注册的相反顺序调用终止函数 1) 调用用户程序的终止函数 2) 调用已加载的共享对象的终止函数 3) 调用动态链接器的终止函数 最后调用_exit()系统调用退出进程
Relevant Link:
https://www.gnu.org/software/libc/index.html http://baike.baidu.com/view/1323132.htm http://blog.csdn.net/tigerscorpio/article/details/6227730 http://dbp-consulting.com/tutorials/debugging/linuxProgramStartup.htm
Glibc的启动过程在不同的情况下差别很大,静态/动态的glibc、用于可执行文件/共享库文件的差别。可以组合出4种情况
2. C/C++运行库
运行时库(Runtime Library 运行期库),在计算机程序设计领域中,是指一种被编译器用来实现编程语言内置函数以提供该语言程序运行时(执行)支持的一种特殊的计算机程序库。这种库一般包括基本的输入输出或是内存管理等支持。它是一群支持正在运行程序的函数,与操作系统合作提供诸如数学运算、输入输出等功能,让程序员不需要"重新发明轮子",并善用操作系统提供的功能
运行时库由编译器决定,以面向编程语言,提供其最基本的执行时需要
1. Windows Visual Basic Visual Basic Virtual Machine (5.1) Visual Basic Virtual Machine (6.0) 2. Windwos C/C++ Microsoft C Runtime Library (7.0) Microsoft C Runtime Library (7.10) Microsoft Visual C++ 2005 SP1 (8.0.59193) Microsoft Visual C++ 2008 SP1 (9.0.30729) Microsoft Visual C++ 2008 ATL Update kb973924 (9.0.30729.4148) Microsoft Visual C++ 2010 (10.0.40219) 3. Windows .NET/C# .NET Framework 4. Java JVM(Java Virtual Machine) 5. Linux C/C++ Glibc Runtime Library
0x1: C语言运行库
任何一个C程序,它的背后都有一套庞大的代码库来支撑,以使得程序能够正常运行,这样的代码集合库称之为"运行时库(Runtime Library)",而其中C语言的运行库,即被称为C运行库(CRT),这套代码库包括
1. 启动与退出: 1) 入口函数 2) 入口函数所依赖的其他函数 3) 终止函数 2. 标准函数: 由C语言标准规定的C语言标准库所拥有的函数实现 3. I/O: I/O功能的封装和实现 4. 堆: 堆的封装和实现 5. 语言实现: 语言中一些特殊功能的实现 6. 调试: 实现调试功能的代码
这些运行库的组成成分中,C语言标准库占据了主要地位。C语言标准库是C语言标准化的基础函数库,例如printf、exit等都是标准库中的一部分。标准库定义了C语言中普遍存在的函数集合,程序员可以直接使用标准库中规定的函数而不用担心将代码移植到别的平台时对应的平台上不提供这个函数
ANSI C的标准库由24个C头文件组成,与许多其他语言(如java)的标准库不同,C语言的标准库非常轻量,它仅仅包含了数学函数、字符/字符串处理、I/O等基本方面,例如
1. 标准输入输出: stdio.h 2. 文件操作: stdio.h 3. 字符操作: ctype.h 4. 字符串操作: string.h 5. 数学函数: math.h 6. 资源管理: stdlib.h 7. 格式转换: stdlib.h 8. 时间/日期: time.h 9. 断言: assert.h 10. 各种类型上的常数: limits.h & float.h 11. 变长参数: stdarg.h 12. 非局部跳转: setjmp.h
0x2: glibc && MSVC CRT
运行库是和平台相关的,它和操作系统结合得非常紧密。C语言的运行库从某种程序上讲是C语言和不同操作系统平台之间的抽象层,它将不同的操作API抽象成相同的库函数,例如程序员可以在不同平台下使用fread来读取文件,而不用考虑在操作系统层面的实现是不同的。虽然各个平台下的C语言运行库提供了很多功能,但很多时候它们毕竟有限,例如用户的权限控制、操作系统线程创建等操作都不属于标准的C语言运行库,在这种情况下,我们不得不绕过C语言运行库直接调用操作系统的系统调用API或使用其他的库。也造成了在程序中可能会出现C运行库API和系统调用API混用的情况
Glibc(GNU C Library)是Linux平台下的主要运行库,MSVCRT(Microsoft Visual C Run-time)
Relevant Link:
http://zh.wikipedia.org/wiki/%E8%BF%90%E8%A1%8C%E6%97%B6%E5%BA%93
3. 静态Glibc && 可执行文件 入口/终止函数
在静态Glibc编译进可执行文件这种情况下,glibc的程序入口为_start(),这个入口由ld链接器默认的链接脚本所指定的。_start()由汇编实现,并且和平台相关
glibc-2.18sysdepsi386start.S
_start: /* 让ebp寄存器清零,这样做的目的是表明当前是程序的最外层函数 */ xorl %ebp, %ebp /* Extract the arguments as encoded on the stack and set up the arguments for main(argc, argv). envp will be determined later in __libc_start_main. 在调用_start前,装载器会把用户的参数和环境变量压入栈中,按照压栈的方法,栈顶的元素是argc、紧接的就是argv、env环境变量的数组(栈是向高地址生长的,所以最后入栈的参数位于栈的最高位置) */ // Pop the argument count. popl %esi // argv starts just at the current stack top. %ecx指向argv和env环境变量数组 movl %esp, %ecx /* Before pushing the arguments align the stack to a 16-byte (SSE needs 16-byte alignment) boundary to avoid penalties from misaligned accesses. T */ andl $0xfffffff0, %esp pushl %eax /* Push garbage because we allocate 28 more bytes. */ /* Provide the highest stack address to the user code (for stacks which grow downwards). */ pushl %esp /* Push address of the shared library termination function. */ pushl %edx #ifdef SHARED /* 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 /* Push second argument: argv. */ pushl %ecx /* Push first argument: argc. */ pushl %esi pushl main@GOT(%ebx) /* Call the user's main function, and exit with its value. But let the libc call main. */ call __libc_start_main@PLT #else /* Push address of our own entry points to .fini and .init. */ pushl $__libc_csu_fini pushl $__libc_csu_init /* Push second argument: argv. */ pushl %ecx /* Push first argument: argc. */ pushl %esi pushl $main /* Call the user's main function, and exit with its value. But let the libc call main. */ call __libc_start_main #endif /* Crash if somehow `exit' does return. */ hlt
综上分析,我们可以把_start改写为一段更具有可读性的伪代码
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_finit, edx, top of stack ); } //argv除了指向参数表外,还隐含紧接着环境变量表,这个环境变量表要在__libc_start_main里从argv内提取出来
实际执行代码的函数是__libc_start_main
glibc-2.18csulibc-start.c
/* 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. 这是__libc_start_main函数的头部,和_start函数里的调用一致 */ 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 //init: main调用前的初始化工作 __typeof (main) init, //fini: main结束后的收尾工作 void (*fini) (void), //rtld_fini: 和动态加载有关的收尾工作,rtld_fini即runtime loader void (*rtld_fini) (void), //stack_end: 标明了栈底的地址,即最高的栈地址 void *stack_end) { /* Result of the 'main' function. */ int result; __libc_multiple_libcs = &_dl_starting_up && !_dl_starting_up; #ifndef SHARED //根据当前栈的布局获取紧跟在argv数组之后的环境变量的地址 char **ev = &argv[argc + 1]; __environ = ev; /* Store the lowest stack address. This is done in ld.so if this is he code for the DSO. */ __libc_stack_end = stack_end; # ifdef HAVE_AUX_VECTOR /* First process the auxiliary vector since we need to find the program header to locate an eventually present PT_TLS entry. */ # ifndef LIBC_START_MAIN_AUXVEC_ARG ElfW(auxv_t) *auxvec; { char **evp = ev; while (*evp++ != NULL) ; auxvec = (ElfW(auxv_t) *) evp; } # endif _dl_aux_init (auxvec); if (GL(dl_phdr) == NULL) # endif { /* Starting from binutils-2.23, the linker will define the magic symbol __ehdr_start to point to our own ELF header if it is visible in a segment that also includes the phdrs. So we can set up _dl_phdr and _dl_phnum even without any information from auxv. 获取程序入口点地址 */ extern const ElfW(Ehdr) __ehdr_start __attribute__ ((weak, visibility ("hidden"))); if (&__ehdr_start != NULL) { assert (__ehdr_start.e_phentsize == sizeof *GL(dl_phdr)); GL(dl_phdr) = (const void *) &__ehdr_start + __ehdr_start.e_phoff; GL(dl_phnum) = __ehdr_start.e_phnum; } } # ifdef DL_SYSDEP_OSCHECK if (!__libc_multiple_libcs) { /* This needs to run to initiliaze _dl_osversion before TLS setup might check it. */ DL_SYSDEP_OSCHECK (__libc_fatal); } # endif /* Perform IREL{,A} relocations. */ apply_irel (); /* Initialize the thread library at least a bit since the libgcc functions are using thread functions if these are available and we need to setup errno. */ __pthread_initialize_minimal (); /* Set up the stack checker's canary. */ uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random); # ifdef THREAD_SET_STACK_GUARD THREAD_SET_STACK_GUARD (stack_chk_guard); # else __stack_chk_guard = stack_chk_guard; # endif #endif /* Register the destructor of the dynamic linker if there is any. __cxa_atexit函数是glibc的内部函数,等同于atexit,用于将参数指定的函数在main结束之后调用 */ if (__builtin_expect (rtld_fini != NULL, 1)) __cxa_atexit ((void (*) (void *)) rtld_fini, NULL, NULL); #ifndef SHARED /* 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'. 调用共享库的_init()函数 */ __libc_init_first (argc, argv, __environ); /* Register the destructor of the program, if any. __cxa_atexit函数是glibc的内部函数,等同于atexit,用于将参数指定的函数在main结束之后调用 */ if (fini) __cxa_atexit ((void (*) (void *)) fini, NULL, NULL); /* Some security at this point. Prevent starting a SUID binary where the standard file descriptors are not opened. We have to do this only for statically linked applications since otherwise the dynamic loader did the work already. */ if (__builtin_expect (__libc_enable_secure, 0)) __libc_check_standard_fds (); #endif /* Call the initializer of the program, if any. */ #ifdef SHARED if (__builtin_expect (GLRO(dl_debug_mask) & DL_DEBUG_IMPCALLS, 0)) GLRO(dl_debug_printf) (" initialize program: %s ", argv[0]); #endif if (init) (*init) (argc, argv, __environ MAIN_AUXVEC_PARAM); #ifdef SHARED /* Auditing checkpoint: we have a new object. */ if (__builtin_expect (GLRO(dl_naudit) > 0, 0)) { struct audit_ifaces *afct = GLRO(dl_audit); struct link_map *head = GL(dl_ns)[LM_ID_BASE]._ns_loaded; for (unsigned int cnt = 0; cnt < GLRO(dl_naudit); ++cnt) { if (afct->preinit != NULL) afct->preinit (&head->l_audit[cnt].cookie); afct = afct->next; } } #endif #ifdef SHARED if (__builtin_expect (GLRO(dl_debug_mask) & DL_DEBUG_IMPCALLS, 0)) GLRO(dl_debug_printf) (" transferring control: %s ", argv[0]); #endif #ifdef HAVE_CLEANUP_JMP_BUF /* Memory for the cancellation buffer. */ struct pthread_unwind_buf unwind_buf; int not_first_call; not_first_call = setjmp ((struct __jmp_buf_tag *) unwind_buf.cancel_jmp_buf); if (__builtin_expect (! not_first_call, 1)) { struct pthread *self = THREAD_SELF; /* Store old info. */ unwind_buf.priv.data.prev = THREAD_GETMEM (self, cleanup_jmp_buf); unwind_buf.priv.data.cleanup = THREAD_GETMEM (self, cleanup); /* Store the new cleanup handler info. */ THREAD_SETMEM (self, cleanup_jmp_buf, &unwind_buf); /* Run the program. */ result = main (argc, argv, __environ MAIN_AUXVEC_PARAM); } else { /* Remove the thread-local data. */ # ifdef SHARED PTHFCT_CALL (ptr__nptl_deallocate_tsd, ()); # else extern void __nptl_deallocate_tsd (void) __attribute ((weak)); __nptl_deallocate_tsd (); # endif /* One less thread. Decrement the counter. If it is zero we terminate the entire process. */ result = 0; # ifdef SHARED unsigned int *ptr = __libc_pthread_functions.ptr_nthreads; # ifdef PTR_DEMANGLE PTR_DEMANGLE (ptr); # endif # else extern unsigned int __nptl_nthreads __attribute ((weak)); unsigned int *const ptr = &__nptl_nthreads; # endif if (! atomic_decrement_and_test (ptr)) /* Not much left to do but to exit the thread, not the process. */ __exit_thread (0); } #else /* Nothing fancy, just call the function. 调用主程序main函数 */ result = main (argc, argv, __environ MAIN_AUXVEC_PARAM); #endif exit (result); }
最后的_exit函数由汇编实现,且与平台相关,可能是80中断陷入,也可能是sysenter调用,可见,_exit()的作用仅仅是调用了exit这个系统调用,_exit调用后,进程就会直接结束,程序正常结束有2种情况
1. main函数正常返回 2. 程序中用exit退出 //exit是进程正常退出的必经之路
值得注意的是,_start和_exit的末尾都有一个hlt指令。这是因为在linux中,进程必须使用eixt系统调用结束,一旦exit被调用,程序的运行就会终止。
_exit里的hlt指令是为了检测exit系统调用是否成功,如果失败,程序就不会终止,hlt指令的的作用就是强行把程序停止下来。而_start里的hlt的作用也是如此。为了预防某种没用exit就回到了_start的情况(例如被误删了__libc_main_start末尾的exit)
Relevant Link:
http://dbp-consulting.com/tutorials/debugging/linuxProgramStartup.html http://blog.csdn.net/langchibi_zhou/article/details/5744922
4. 动态Glibc && 可执行文件 入口/终止函数
5. 静态Glibc && 共享库 入口/终止函数
6. 动态Glibc && 共享库 入口/终止函数
我们知道,可执行ELF程序的初始化工作是由Glibc来完成,而动态共享库的加载和初始化工作由"动态加载器(ld-linux-so.2)"完成
动态连接器的入口是_start, 在glibc/sysdeps/i386/dl-machine.h中的RTLD_START宏中定义。它首先调 用_dl_start()
#define RTLD_START asm (" .text .align 16 0: movl (%esp), %ebx ret .align 16 .globl _start .globl _dl_start_user _start: # Note that _dl_start gets the parameter in %eax. movl %esp, %eax call _dl_start ...
glibc-2.18elf tld.c
static ElfW(Addr) __attribute_used__ internal_function _dl_start (void *arg) { #ifdef DONT_USE_BOOTSTRAP_MAP # define bootstrap_map GL(dl_rtld_map) #else struct dl_start_final_info info; # define bootstrap_map info.l #endif /* This #define produces dynamic linking inline functions for bootstrap relocation instead of general-purpose relocation. Since ld.so must not have any undefined symbols the result is trivial: always the map of ld.so itself. */ #define RTLD_BOOTSTRAP #define RESOLVE_MAP(sym, version, flags) (&bootstrap_map) #include "dynamic-link.h" if (HP_TIMING_INLINE && HP_TIMING_AVAIL) #ifdef DONT_USE_BOOTSTRAP_MAP HP_TIMING_NOW (start_time); #else HP_TIMING_NOW (info.start_time); #endif /* Partly clean the `bootstrap_map' structure up. Don't use `memset' since it might not be built in or inlined and we cannot make function calls at this point. Use '__builtin_memset' if we know it is available. We do not have to clear the memory if we do not have to use the temporary bootstrap_map. Global variables are initialized to zero by default. */ #ifndef DONT_USE_BOOTSTRAP_MAP # ifdef HAVE_BUILTIN_MEMSET __builtin_memset (bootstrap_map.l_info, '