《程序员的自我修养》第十一章读书笔记

本章正式开始介绍运行库,十分之难的一章,我能给大家分析多少就是多少吧。现在十分佩服这三位写书的大神,同样是研究生,水平差距太多了。这里免不了要提一句题外话,感觉周围人对操作系统原理感兴趣的不多。也许是本人闭门造车,对现在的国内外研究现状了解不深,乱说的几句,还希望大家不要喷我。

好了正式开始今天的主题,本章的一开始先从三个例子出发,我就直接给大家揭晓谜底吧,程序并不是从main函数开始的,其实在前面的章节中就已经提到过,没有main的程序一样能运行,就是得自己写个链接脚本。

接下来看看程序到底是怎么开始的,在glibc的程序入口为_start,这一函数已经被链接到了可执行程序中,通过objdump这一命令可以查看。书中提到_start函数位于start.S这个汇编文件中,不过在书中提到的位置下我并没有找到文件,应该说书中提到的文件夹我就没有找到。所以就采用了个笨办法,在glibc-2.21下搜索所有的start.S,结果发现“/glibc-2.21/sysdeps/x86_64”文件夹下的start.S 就是我要找的(别问我是怎么找到的,一个一个对出来的抓狂)。把这个汇编给大家贴出来一点吧:

/* This is the canonical entry point, usually the first thing in the text
   segment.  The SVR4/i386 ABI (pages 3-31, 3-32) says that when the entry
   point runs, most registers' values are unspecified, except for:

   %rdx        Contains a function pointer to be registered with `atexit'.
        This is how the dynamic linker arranges to have DT_FINI
        functions called for shared libraries that have been loaded
        before this code runs.

   %rsp        The stack contains the arguments and environment:
        0(%rsp)                argc
        LP_SIZE(%rsp)            argv[0]
        ...
        (LP_SIZE*argc)(%rsp)        NULL
        (LP_SIZE*(argc+1))(%rsp)    envp[0]
        ...
                        NULL
*/
以上注释比较关键,清晰的描述了start函数运行前,栈的内存分布。

ENTRY (_start)
	/* Clearing frame pointer is insufficient, use CFI.  */
	cfi_undefined (rip)
	/* Clear the frame pointer.  The ABI suggests this be done, to mark
	   the outermost frame obviously.  */
	xorl %ebp, %ebp //对ebp清零,

	/* Extract the arguments as encoded on the stack and set up
	   the arguments for __libc_start_main (int (*main) (int, char **, char **),
		   int argc, char *argv,
		   void (*init) (void), void (*fini) (void),
		   void (*rtld_fini) (void), void *stack_end).
	   The arguments are passed via registers and on the stack:
	main:		%rdi
	argc:		%rsi
	argv:		%rdx
	init:		%rcx
	fini:		%r8
	rtld_fini:	%r9
	stack_end:	stack.	*/ 通过这一句注释可以知道__libc_start_main函数参数的传递方式。

	mov %RDX_LP, %R9_LP	/* Address of the shared library termination 
				   function.  */ 有关于RDX_LP等的定义,请见同文件夹下的sysdep.h,rdx 寄存器中首先存储的是动态链接库的终止函数,将这一地址传给r9寄存器
#ifdef __ILP32__
	mov (%rsp), %esi	/* Simulate popping 4-byte argument count.  */ 这一句在反汇编没有找到,可见没有定义__ILP32__
	add $4, %esp
#else
	popq %rsi		/* Pop the argument count.  */ rsi指向argc
#endif
	/* argv starts just at the current stack top.  */
	mov %RSP_LP, %RDX_LP //弹出argc后当前栈顶指向argv,将这一地址赋给rdx
	/* Align the stack to a 16 byte boundary to follow the ABI.  */
	and  $~15, %RSP_LP

	/* Push garbage because we push 8 more bytes.  */
	pushq %rax //貌似rax中的数据是没有用的

	/* Provide the highest stack address to the user code (for stacks
	   which grow downwards).  */
	pushq %rsp //经过以上步骤后rsp可能已经指向实际的栈顶了(具体rsp实际指向的值我也不清楚),将rsp的值也压入栈中

#ifdef SHARED
	/* Pass address of our own entry points to .fini and .init.  */
	mov __libc_csu_fini@GOTPCREL(%rip), %R8_LP //__libc_csu_fini函数的地址赋给r8
	mov __libc_csu_init@GOTPCREL(%rip), %RCX_LP //在SHARED情况下,出现了got,估计动态链接器已经完成自举、装载、初始化等工作。因为如果动态链接器没有完成
                                                                                                                    以上工作,那么got中的符号如何重定位

	mov main@GOTPCREL(%rip), %RDI_LP //main函数的地址存放在rdi中,至此__libc_start_main函数的准备工作已经完成。

	/* Call the user's main function, and exit with its value.
	   But let the libc call main.	  */
	call __libc_start_main@PLT //调用函数
#else
	/* Pass address of our own entry points to .fini and .init.  */
	mov $__libc_csu_fini, %R8_LP
	mov $__libc_csu_init, %RCX_LP

	mov $main, %RDI_LP

	/* Call the user's main function, and exit with its value.
	   But let the libc call main.	  */
	call __libc_start_main
#endif

	hlt			/* Crash if somehow `exit' does return.	 */
END (_start)

/* Define a symbol for the first piece of initialized data.  */
	.data
	.globl __data_start
__data_start:
	.long 0
	.weak data_start
	data_start = __data_start
通过对上述程序的分析,可以发现_start函数其实就是为__libc_start_main做准备的函数,一个函数在调用前,需要传递参数,同时根据我们上一章的分析,最基本的两个操作就是将ebp的值压栈(此处,仅将ebp的值清零,并没有将ebp压栈,可能是由于用于不会返回到_start中,因此不需要保存ebp的值以进行清栈操作。若ebp为0并压栈,则根本无法进行清栈操作)。其中还存在一点问题就是程序中并没有将用户参数与环境参数压入堆栈,根据书中所写,这一部分工作是由装载器完成的。这里还要说一点的就是动态链接器的启动要早于用户程序的启动,解释请见以下这篇blog,有些内容还要参考linker & loaders。

http://blog.csdn.net/tigerscorpio/article/details/6227730

今早把rsp的变化过程使用gdb看了看:

rsp 的初始值为0x7fffffffde60,经过popq %rsi后,rsp的值变为0x7fffffffde68。

经过and  $~15, %RSP_LP后,15的二进制为0x1111,其反码为0xfffffffffffffff0,如此与之进行逻辑与运算,则最低位清零,因此rsp的值又变为0x7fffffffde60。

经过pushq %rax后,rsp的值变为0x7fffffffde58。这一步的作用不太明确。

经过pushq %rsp后,rsp的值再向前一部,变为0x7fffffffde50。具体作用还待分析。

好了,让我们看看__libc_start_main,其源文件实现位于:/glibc/csu/libc-start.c

__libc_start_main的主要作用就是完成初始化工作,给大家列一些我看懂了的函数:

__cxa_atexit ((void (*) (void *)) rtld_fini, NULL, NULL);//注册rtld_fini,rtld_fini是与动态加载有关的收尾工作
__libc_init_first (argc, argv, __environ); //根据它的注释,这是libc的准备工作
__cxa_atexit ((void (*) (void *)) fini, NULL, NULL); //注册fini函数,main结束后的收尾工作,就是objdump中的__libc_csu_fini
(*init) (argc, argv, __environ MAIN_AUXVEC_PARAM); //main调用前的初始化工作,就是objdump中的__libc_csu_init
/* Nothing fancy, just call the function.  */
  result = main (argc, argv, __environ MAIN_AUXVEC_PARAM); //特意将它的注释也搬上来了,此处开始调用main函数,等待其返回
exit (result); //执行函数清理工作
再来把exit函数给大家贴出来简单分析一下,它位于:/glibc-2.21/stdlib

void
exit (int status)
{
  __run_exit_handlers (status, &__exit_funcs, true);
}

void
attribute_hidden
__run_exit_handlers (int status, struct exit_function_list **listp,
             bool run_list_atexit)
{
  /* First, call the TLS destructors.  */
#ifndef SHARED
  if (&__call_tls_dtors != NULL)
#endif
    __call_tls_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 (*listp != NULL)
    {
      struct exit_function_list *cur = *listp; //通过其命名就可以知道退出函数列表

      while (cur->idx > 0)
    {
      const struct exit_function *const f =
        &cur->fns[--cur->idx];
      switch (f->flavor)
        {
          void (*atfct) (void);
          void (*onfct) (int status, void *arg);
          void (*cxafct) (void *arg, int status);

        case ef_free:
        case ef_us:
          break;
        case ef_on:
          onfct = f->func.on.fn;
#ifdef PTR_DEMANGLE
          PTR_DEMANGLE (onfct);
#endif
          onfct (status, f->func.on.arg);
          break;
        case ef_at:
          atfct = f->func.at;
#ifdef PTR_DEMANGLE
          PTR_DEMANGLE (atfct);
#endif
          atfct ();
          break;
        case ef_cxa:
          cxafct = f->func.cxa.fn;
#ifdef PTR_DEMANGLE
          PTR_DEMANGLE (cxafct);
#endif
          cxafct (f->func.cxa.arg, status);
          break;
        }
    }

      *listp = cur->next; //指向下一个元素
      if (*listp != NULL)
    /* Don't free the last element in the chain, this is the statically
       allocate element.  */
    free (cur); //执行一个,销毁一个
    }

  if (run_list_atexit) //true,总是执行
    RUN_HOOK (__libc_atexit, ());  根据RUN_HOOK宏,__libc_atexit()函数原型如下,不过我没有找到函数的实现

  _exit (status); //__exit好像是系统调用,不属于glibc
}
至此函数调用的过程就给大家分析到这里,再来给大家总结一下:

_start -> __libc_main_start                                                                           -> exit                            -> _exit

                -> __pthread_initialize_minimal ();                                                  ->__libc_atexit()

                -> __cxa_atexit ((void (*) (void *)) rtld_fini, NULL, NULL);

                -> __libc_init_first (argc, argv, __environ);

                -> __cxa_atexit ((void (*) (void *)) fini, NULL, NULL);

                -> (*init) (argc, argv, __environ MAIN_AUXVEC_PARAM); 


11.2 主要讲解C语言的运行库

所谓运行时库是指程序运行所需要的入口函数、及其所依赖的函数所构成的函数集合。C语言运行库又被称为C运行库(CRT)。

一个C语言运行库大致包含了如下功能:

  1. 启动与退出:包括入口函数及入口函数所依赖的其他函数等。
  2. 标准函数:由C语言标准规定的C语言标准库所拥有的函数实现(像绕口令一样)
  3. I/O:I/O功能的封装和实现,主要是标准输入、标准输出、标准错误的初始化工作
  4. 堆:堆的封装和实现。
  5. 语言实现:语言中一些特殊功能的实现。
  6. 调试:实现调试功能的代码。


你可能感兴趣的:(《程序员的自我修养》第十一章读书笔记)