《软件工程师的自小弟我修养》第十一章读书笔记

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

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

好了正式开始今天的主题,本章的一开始先从三个例子出发,我就直接给大家揭晓谜底吧,程序并不是从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.****.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。具体作用还待分析。