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

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

好了正式开始今天的主题,本章的一开始先从三个例子出发,我就直接给大家揭晓谜底吧,程序并不是从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 [email protected](%rip), %R8_LP //__libc_csu_fini函数的地址赋给r8
	mov [email protected](%rip), %RCX_LP //在SHARED情况下,出现了got,估计动态链接器已经完成自举、装载、初始化等工作。因为如果动态链接器没有完成
                                                                                                                    以上工作,那么got中的符号如何重定位

	mov [email protected](%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 [email protected] //调用函数
#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. 调试:实现调试功能的代码。
时间: 2024-10-10 09:34:42

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

第十一章 读书笔记

第十一章  Linux驱动程序中的 并发控制 并发(concurrency)指的是多个执行单元同时.并行被执行.而并发的执行单元对共享资 源〈如硬件资摞.程序中的全局变量.静态变量等〉的访问很容易导致竞态条件( race conditions). 自旋锁并不关心锁定的|临界区究竟是怎样的操作,不管是读还是写,都只允许同时只有一个执 行单元可以极取自旋锁, 即使有多个单元同时读取临界区资源也会被锁住.实际上,对于并发访问 共享资源时,多个执行单元同时读取它是不会有任何问题的.为了解决这个问题,自旋

《移山之石》第十一章 读书笔记

看了本书中有关结对编程的一章,结合本次遇到的一些问题,笔记如下 1. 作者把时间放在了项目管理三角形的底边上,这是偶然为之还是时间有其特殊性?换一种问法,在“对工作的估计”这一平衡中,这三者有什么样的关系? 2.作者谈到了时间驱动和功能驱动,那么这两种不同的驱动模式的项目在开发上有没有不同的地方? 3.关于单元测试,想起了上学期java的经验:对于某些功能复杂.参数是数组等复杂变量的单元,如何编写完善的测试用例?难道是通过不断地new新的数组去做测试吗? 4.作者讲到了开始阶段的技术研讨,结合本

第十一章读书笔记

并发(concurrency)指的是多个执行单元同时.并行被执行.而并发的执行单元对共享资 源〈如硬件资摞.程序中的全局变量.静态变量等〉的访问很容易导致竞态条件( race conditions).例如,有一个设备文件.进程 A 向该个设备文件写入 1000 个“a飞而进程 B 向 设备文件写入了 2000 个“b”,每次写数据之前会清空上一次写入的数据.现在有一个进程 C, 需要先读出进程 A 写入设备文件的全部字符( 1000 个“a”〉,然后再读出进程 B 写入设备文 件的全部字符(20

《内核设计与实现》第一章读书笔记

<内核设计与实现>第一章读书笔记 第一章:Linux内核简介 1.1 Unix的历史 Unix强大的特点 A.简洁,几百个系统调用,明确的设计目的 B.文件对待所有东西 C.移植性强(C语言) D.进程创建快,使用fork()系统调用. E.进程间通信元语,进程间通信机制 1.2 linux的简介 Linux系统的基础是内核.C库.工具库和系统的基本工具. 1.3 操作系统和内核简介 内核:管理者,操作系统的核心 通常一个内核由负责响应中断的中断服务程序和网络.进程间通信等系统服务共同组成.

Android深度探索——第九章读书笔记及心得

HAL模块 ——第9章读书笔记及心得 通过本章学习学会了Android中特有的与linux驱动交互的方法,即通过HAL模块.HAL模块本质上就是通过linux共享库(.so)与linux交互驱动,然后利用应用程序再访问linux共享库.每一个HAL共享库指定一个ID,在利用这个ID配合一定的规则找到linux共享库.知道了HAL对于Android的意义.知道了Android HAL的架构. HAL是建立在linux驱动之上的一套不属于linux内核的程序库.它属于linux内核层之上的应用层.H

Android深度探索——第二章读书笔记及心得

Android开发环境搭建 ——第二章读书笔记及心得 通过本章的学习了解了如何对Ubuntu Linux下的Android进行搭建,包括搭建Android应用程序开发环境.Android NDK开发环境和交叉编译环境的搭建.了解了搭建Android环境所需要的各种东西,了解了配置ADT的必要性—以便ADT能够找到Android SDK.知道了底层开发所需要的各种工具.学会了如何在Ubuntu下安装JDK.以及编译交叉编译环境,当初在学习Linux交叉环境编译的时候就不是特别成功,中间出了很多的错

Android深度探索——第四章读书笔记及心得

源代码的下载及编译 ——第四章读书笔记及心得 通过阅读书籍知道了Android源代码和Linux源代码的区别,了解了什么是Android移植.知道了Android移植就是Linux内核移植,而Linux内核移植主要就是Linux的驱动移植.为了开发和测试内核环境需要对环境进行搭建,进而知道了如何搭建环境.知道了如何下载Android/Linux源代码及配置Android源代码下载环境(并且知道Android源代码的下载并非一定要将4MB多的东西全部下载,也可以挑选着自己需要的东西下载,,可以利用

Android深度探索——第三章读书笔记及心得

了解Git ——第三章读书笔记及心得 对于Android的理解是从这学期才开始,所以不可谓说是了解的太少太少.对于Linux虽然经过了一学期的学习.经过一次紧张的实训,但是了解的也不是很多.不过我终究是知道Android和Linux是开源的,这是很多老师都曾经告诉我们的.虽然Git并不是学习Android和Linux开发必须掌握的技术,但是对于想要认真学习好这门技术的我们来说应该要努力掌握好这门技术.就像书上说的学习新技术的方式不是一味的读书,只会纸上谈兵.更应该深入的理解自己感兴趣的源代码,通

MySQL cookbook第10章读书笔记

1,使用load data和mysqlimport导入数据 1个ok,2个warning!!! 为什么会有warning,而且数据也没有load进去 2,指定数据文件位置 了解MySQL查找文件位置的规则. 如果load data语句没有local选项,MySQL读取数据文件将按下述规则在服务器所在机器上定位文件的位置: 文件的绝对全路径名(从文件系统的根开始),MySQL直接读取该文件. 给定的是文件的相对路径名,按照路径名的两种不同形式来解释和处理. 3,指定数据文件的结构 有一个数据文件但