程序员自我修养阅读笔记——运行库

Posted 落樱弥城

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了程序员自我修养阅读笔记——运行库相关的知识,希望对你有一定的参考价值。

主要关注程序的启动过程。

1 入口函数和程序初始化

1.1 程序真正的入口

  通常写代码时,我们认为程序的入口是main函数,但是实际上有一些现象值得我们怀疑该结论是不是正确的。比如全局变量的初始化,C++全局变量构造函数的调用,C++静态对象的构造函数调用。
  程序开始运行的入口不是main,在进入main之前程序会先准备好环境运行一些必要的代码才进入main,运行这些代码的函数称为入口函数。入口函数根据平台的不同而不同,其实实际上程序初始化和结束的地方,一个典型的程序的运行步骤如下:

  • 操作系统创建进程后,将控制权移交给程序入口,这个入口一般为运行库的某个入口函数;
  • 入口函数对运行库和程序运行环境进行初始化,比如堆栈、IO、线程、全局变量构造等;
  • 入口函数在完成初始化之后调用main函数,开氏运行程序主体部分;
  • main函数执行结束后,返回到入口函数,入口函数进行清理工作,比如全局变量的析构,堆销毁,关闭IO等,然后结束进程。

1.2 入口函数实现

  主要关注glibc静态库用于可执行文件的情况。

1.2.1 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);

1.2.2 MSVC CRT入口函数

  作者书中提到的系统版本比较老,我现在使用的是windows10,下面是vs2017的实现。
  mainCRTStartupMicrosoft 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;
    

1.3 运行库和IO

  操作系统需要给用户提供读取相关上硬件上的文件或者设备的能力,但是又不能完全向用户开放对硬件的操作能力。在操作系统层面,对于文件的操作Linux提供了文件描述符,而windows则使用句柄来控制某个文件。
  在Linux中,一个文件会有一个fd,每个进程维护一份私有的文件打开表,这个表格是一个指针数组,每个元素指向一个打开的文件对象,而用户拿到的fd就是该表的索引。这个表格由内核维护,用户是无法直接访问的。
  在windows上类似,只不过windows上的句柄并不是索引号,而是通过特定的算法变换得到的一个数值。
  另外,C中使用到的FILE并不是文件的真实指针,而是关于fd或者句柄相关联的一个指针。

  文中提到了FILE的实现,但是新版的实现已经改变,因此不在赘述。

typedef struct _iobuf
    
        void* _Placeholder;
     FILE;

2 C/C++运行库

2.1 运行库

  CRT(C Runtime Library,C运行时库)包含了程序能够正常运行的代码,以及相关的标准库实现等基本的内容。
  Windows下CRT的源码目录为Windows Kits\\10\\Source\\10.0.17134.0\\ucrt
  一个C语言运行库的基本功能大致为:

  • 启动和退出:包括入口函数及入口函数所依赖的其它函数;
  • 标准函数:由C语言标准规定的C语言标准库所拥有的函数实现;
  • IO:IO功能的封装与实现;
  • 堆:堆的封装与实现;
  • 语言实现:语言相关的特性实现;
  • 调试:实现调试功能的代码。

2.2 标准库

  不赘述,请参考open-std-c99

2.3 glibc和MSVC CRT

  运行库是和操作系统紧密相关的,C语言仅仅是针对不同操作系统平台的一个抽象层。glibc和MSVCCRT分别是Linux和windows平台下的C实现。

2.3.1 glibc

  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_

2.3.2 MSVC CRT

  MSVC CRT库存储于Microsoft Visual Studio\\2017\\Community\\VC\\Tools\\MSVC\\14.16.27023\\lib\\x64,其中的14.16.27023可以替换为自己的版本,对应的库的名称的命名规则为libc[p][mt][d].lib

  • p表示C++标准库;
  • mt表示支持多线程;
  • d表示调试版本。

  在编译是可以通过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

3 运行库和多线程

3.1 线程的问题

  线程的访问能力非常自由,它可以访问进程内存里的所有数据,甚至包括其他线程的堆栈(如果它知道其他线程的堆栈地址,然而这是很少见的情况),但实际运用中线程也拥有自己的私有存储空间,包括:

  1. 栈(尽管并非完全无法被其他线程访问,但一般情况下仍然可以认为是私有的数据)
  2. 线程局部存储(Thread Local Storage,TLS)线程局部存储是某些操作系统为线程单独提供的私有空间,但通常只具有很有限的尺寸。
  3. ·寄存器(包括PC寄存器),寄存器是执行流的基本数据,因此为线程私有。

  虽然C++11提供了thread的实现,但是其使用非常有限,无法对线程进行更加精确的控制。

  C/C++标准库早期是不提供线程支持的,那么使用相关的库函数就无法做到线程安全。

3.2 CRT改进

  为了支持多线程CRT针对多线程环境进行了一些改进。
使用TLS
  多线程环境下有些变量的地址存放在线程的TLS中,比如errno。
加锁
  多线程环境中,一些库函数会在函数内部加锁保证线程安全。
改进函数调用方式
  改变一些库函数保证其线程安全比如strtokmsvc的改进版本为strtok_s,glibc版本为strtok_r。但是无法做到向后兼容。

3.3 线程局部存储的实现

  如果希望某个变量线程私有,就需要将变量存放到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。这部分了解就好。

4 C++全局构造和析构

4.1 glibc全局构造与析构

  glibc中存在两个段.init.finit组成_init()_finit()两个函数,分别执行初始化和善后的工作。本节就了解下他们如何完成对象的构造和析构工作。
  我们使用下面的代码反汇编查看初始化过程。

//gcc main.cpp && objdump -D a.out
#include <cstdio>

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 <main>
 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 程序员自我修养阅读笔记——Linux共享库管理

程序员自我修养阅读笔记——Linux共享库管理

程序员自我修养阅读笔记——Widnows下的动态链接

程序员自我修养阅读笔记——Widnows下的动态链接

程序员自我修养阅读笔记——静态编译

程序员自我修养阅读笔记——目标文件里有什么