重学计算机(十三、程序从main开始的么?)

这一篇来一点比较硬核的东西,程序是main函数开始的么?

13.1 程序是从main函数开始的么?

13.1.1 gcc编译详细输出

在我们学习c语言的时候,老师是不是一直都在说c程序是从main函数开始的,然后我们写代码的时候,其实也都是从main函数开始写,编译执行之后打印也是从main函数打印,是不是c程序从main函数开始执行,就很根深蒂固一样,这次我们就来推翻一下。

我们来写一个代码:

#include <stdio.h>

int main(int argc, char **argv)
{
    printf("hello world\n");
    return 0;
}

又是熟悉的hello world,讲了十几篇了,好像又回到了原点。

root@ubuntu:~/c_test/13# gcc test.c -o test
root@ubuntu:~/c_test/13# ./test
hello world

又是编译,运行,好像还是熟悉的配方啊,没有其他变化。

我们用一个gcc的一个-v参数,(之前讲编译链接的时候忘记了,尴尬,不过现在补回来也不错)

/usr/lib/gcc/x86_64-linux-gnu/5/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/5/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/5/lto-wrapper -plugin-opt=-fresolution=/tmp/ccp83T0j.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --sysroot=/ --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -z relro -o test /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crt1.o /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/5/crtbegin.o -L/usr/lib/gcc/x86_64-linux-gnu/5 -L/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/5/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/5/../../.. /tmp/ccQmBcOv.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/x86_64-linux-gnu/5/crtend.o /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crtn.o

collect2就是我们之前说的链接器ld,截取的这一部分其实就是链接部分,通过查看确实发现有以下的.o文件,参与链接:crt1.o、crti.o、crtbegin.o。

但是这一点也不能证明这些.o会在main函数之气运行啊。

13.1.2 链接脚本

大家是不是忘记了,程序链接的时候,是通过链接器来控制的,如果我们没有指定链接器,那就是默认的连接器,忘记的可以回到这一篇文章学习学习:重学计算机(五、静态链接和链接控制)

现在我们就截取一段有用的过来就可以了:

root@ubuntu:/usr/lib/ldscripts# ld -verbose
==================================================
/* Script for -z combreloc: combine and sort reloc sections */
/* Copyright (C) 2014-2015 Free Software Foundation, Inc.
   Copying and distribution of this script, with or without modification,
   are permitted in any medium without royalty provided the copyright
   notice and this notice are preserved.  */
OUTPUT_FORMAT("elf64-x86-64", "elf64-x86-64",
	      "elf64-x86-64")		
OUTPUT_ARCH(i386:x86-64)    /* 输出格式 */
ENTRY(_start)				/* 这个重要,指定程序的入口函数 */
SEARCH_DIR("=/usr/local/lib/x86_64-linux-gnu"); SEARCH_DIR("=/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/local/lib64"); SEARCH_DIR("=/lib64"); SEARCH_DIR("=/usr/lib64"); SEARCH_DIR("=/usr/local/lib"); SEARCH_DIR("=/lib"); SEARCH_DIR("=/usr/lib"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib64"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib");
/* SEARCH_DIR就是ld链接器查找指定目录查找的库,相当于-Lpath */
SECTIONS	/* 这个就是各个段的定义,看到下面的段名字是不是很熟悉 */
{
  /* Read-only sections, merged into text segment: */
  /* 在链接脚本中定义某个符号,这个符号是在代码中可以使用的 */
  PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;  /* 这个就是定义程序的开始地址, SIZEOF_HEADERS 这个可以留到以后讲*/
  .interp         : { *(.interp) }		// *是通配符,表示所有文件的.interp段都符合条件
  .note.gnu.build-id : { *(.note.gnu.build-id) }
  
  .init           :
  {
    KEEP (*(SORT_NONE(.init)))
    //在连接命令行内使用了选项–gc-sections后,连接器可能将某些它认为没用的section过滤掉,此时就有必要强制连接器保留一些特定的 section,可用KEEP()关键字达此目的
  }
  .fini           :
  {
    KEEP (*(SORT_NONE(.fini)))
  }
  .init_array     :
  {
    PROVIDE_HIDDEN (__init_array_start = .);
    KEEP (*(SORT_BY_INIT_PRIORITY(.init_array.*) SORT_BY_INIT_PRIORITY(.ctors.*)))
    KEEP (*(.init_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .ctors))
    PROVIDE_HIDDEN (__init_array_end = .);
  }
  .fini_array     :
  {
    PROVIDE_HIDDEN (__fini_array_start = .);
    KEEP (*(SORT_BY_INIT_PRIORITY(.fini_array.*) SORT_BY_INIT_PRIORITY(.dtors.*)))
    KEEP (*(.fini_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .dtors))
    PROVIDE_HIDDEN (__fini_array_end = .);
  }
  .ctors          :
  {
    /* gcc uses crtbegin.o to find the start of
       the constructors, so we make sure it is
       first.  Because this is a wildcard, it
       doesn't matter if the user does not
       actually link against crtbegin.o; the
       linker won't look for a file to match a
       wildcard.  The wildcard also means that it
       doesn't matter which directory crtbegin.o
       is in.  */
    KEEP (*crtbegin.o(.ctors))
    KEEP (*crtbegin?.o(.ctors))
    /* We don't want to include the .ctor section from
       the crtend.o file until after the sorted ctors.
       The .ctor section from the crtend file contains the
       end of ctors marker and it must be last */
    KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .ctors))
    KEEP (*(SORT(.ctors.*)))
    KEEP (*(.ctors))
  }
}

通过这个文件我们看到函数入口是ENTRY(_start),这个,这次确定不是main函数开始了吧,我还留下了.init和.fini段,说明.text之前和之后都会有代码执行的。

我们稍微修改一下上面的代码:

#include <stdio.h>

// static void __attribute__((section(".init"))) init_main(void)
// {
//     printf("init main\n");
// }

static void __attribute__ ((constructor)) before_main(void)   // main函数之前
{
    printf("befor main\n");
}

static void __attribute__ ((destructor)) after_main(void)  // main函数之后
{
    printf("after main\n");
}

// static void __attribute__((section(".fini"))) fini_main(void)
// {
//     printf("fini main\n");
// }

int main(int argc, char **argv)
{
    printf("hello world\n");

    return 0;
}

用__attribute__来指定一下函数的属性,编译运行:

root@ubuntu:~/c_test/13# gcc test.c -o test
root@ubuntu:~/c_test/13# ./test
befor main
hello world
after main
root@ubuntu:~/c_test/13# 

发现确实是在main函数之前和之后,这里是不是有人就疑问了,为啥要把init和fini段屏蔽了,其实这两段代码打开的话,会出现段错误,虽然打印也可以,但是打印完了,就段错误了,这个问题记录一下,之后又时间再回来分析分析。还有关于__attribute__的可以看看这篇文章,写的还挺不错的:几个有用的gcc attribute介绍

13.1.3 _start函数

我这个系统是ubuntu64位的,所以_start.S是在: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		包含一个要用' atexit'注册的函数指针.
		这就是动态链接器为共享库调用DT_FINI类型的函数的方式,这些函数在代码运行之前已经加载.

   %rsp		堆栈包含参数和环境:
		0(%rsp)				argc
		LP_SIZE(%rsp)			argv[0]
		...
		(LP_SIZE*argc)(%rsp)		NULL
		(LP_SIZE*(argc+1))(%rsp)	envp[0]
		...
						NULL
*/
/* 上面注释的部分,是在_start开始之前就已经准备好,也就是这时候栈中已经有这么多变量了,到底是什么时候把这些变量存到寄存器中,这好像就真不知道了,难道是在exec函数中?这个要分析内核了,不过暂时先不管,以后有机会碰到了再说。 */
#include <sysdep.h>

ENTRY (_start)
	/* 清除帧指针不足,使用CFI.  */
	cfi_undefined (rip)
	/* 清除栈指针。  */
	/* EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部 */
	xorl %ebp, %ebp

	/* 提取堆栈上编码的参数,并设置__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).
	   参数通过寄存器和堆栈传递:
	main:		%rdi
	argc:		%rsi
	argv:		%rdx
	init:		%rcx
	fini:		%r8
	rtld_fini:	%r9
	stack_end:	stack.	*/

	mov %RDX_LP, %R9_LP	/* 有关于RDX_LP等的定义,请见同文件夹下的sysdep.h,rdx 寄存器中首先存储的是动态链接库的终止函数,将这一地址传给r9寄存器. 
    应该是动态链接器会把链接地址赋值在rdx寄存器中。*/
#ifdef __ILP32__
	mov (%rsp), %esi	/* Simulate popping 4-byte argument count.  */
	add $4, %esp
#else
	popq %rsi		/* rsi指向argc  */
#endif
	/* 弹出argc后,现在栈顶是argv,rdx指向argv  */
	mov %RSP_LP, %RDX_LP
	/* 将堆栈对齐到16字节的边界,以遵循ABI.  */
	and  $~15, %RSP_LP

	/* Push garbage because we push 8 more bytes.  */
	pushq %rax

	/* 经过以上步骤后rsp可能已经指向实际的栈顶了.省略的部分,尴尬  */
	pushq %rsp

#ifdef PIC    //动态链接走这一步
	/* Pass address of our own entry points to .fini and .init.  */
	mov __libc_csu_fini@GOTPCREL(%rip), %R8_LP		// 这两个继续赋值
	mov __libc_csu_init@GOTPCREL(%rip), %RCX_LP

	mov main@GOTPCREL(%rip), %RDI_LP			// main函数地址也存放在rdi中
#else
	/* 将我们自己的入口地址传递给.fini和.init。  */
	mov $__libc_csu_fini, %R8_LP
	mov $__libc_csu_init, %RCX_LP

	mov $main, %RDI_LP
#endif

	/* 调用用户的主函数, 然后带着它的值退出.
	   但是让libc调用main.  Since __libc_start_main in
	   libc.so is called very early, lazy binding isn't relevant
	   here.  Use indirect branch via GOT to avoid extra branch
	   to PLT slot.  In case of static executable, ld in binutils
	   2.26 or above can convert indirect branch into direct
	   branch.  */
	call *__libc_start_main@GOTPCREL(%rip)

	hlt			/* 崩溃,如果'退出'返回.	 */
END (_start)

x86的汇编确实让人头大,上面也简单分析了一波,看的不是很懂,感觉是给__libc_start_main填充了各种参数。可以看一下这一篇,写的还不错[《程序员的自我修养》第十一章读书笔记]

13.1.4 __libc_start_main函数

接下来,就跟着跳转函数一起走一遍了:

这个__libc_start_main 在csu/libc-start.c中,

STATIC int
LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
		 int argc, char **argv,
		 __typeof (main) init,
		 void (*fini) (void),
		 void (*rtld_fini) (void), void *stack_end)
    {
  /* Result of the 'main' function.  */
  int result;

  __libc_multiple_libcs = &_dl_starting_up && !_dl_starting_up;

#ifndef SHARED
  _dl_relocate_static_pie ();

  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;



  /* Initialize libpthread if linked in.  */    // 初始化多线程的吧
  if (__pthread_initialize_minimal != NULL)
    __pthread_initialize_minimal ();


#endif /* !SHARED  */

  /* 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
  /* 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);      // 初始化libc库

  /* Register the destructor of the program, if any.  */
    //注册程序的析构函数(如果有的话)
  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

  if (init)
    (*init) (argc, argv, __environ MAIN_AUXVEC_PARAM);    // init函数执行


  /* Nothing fancy, just call the function.  */
  result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);   // main函数在这里,看到这里松了好多口气

  exit (result);
}

这个函数本来是很多的,觉得被我删的差不多,功力不够之前,真的没有必要去深入研究各行代码,太难了,去掉一些不需要的,留下重点的就可以了,真的是太难了。

13.2 exit()函数

看到了linux应用到内核,都介绍了exit函数了,我也跟着看看吧,太难的话就立马撤退。

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

会调用__run_exit_handlers。

__run_exit_handlers函数只要是把那些注册在进程退出的时候,进行清理工作的函数,都执行了一遍,最后调用_exit()函数。

void
_exit (int status)
{
  while (1)
    {
#ifdef __NR_exit_group
      INLINE_SYSCALL (exit_group, 1, status);    //这好像是多线程退出
#endif
      INLINE_SYSCALL (exit, 1, status);		// 这是之前的退出

      // 看着这个调用就知道是内核中的系统函数了
      
#ifdef ABORT_INSTRUCTION
      ABORT_INSTRUCTION;
#endif
    }
}

内核的就先不看了,有缘再见了,太硬核分析好像也不行。

13.3 总结

这一篇虽然比较硬核,但是就作为了解的吧,知道这么回事就可以了,以后有机会再去分析内核的部分吧,现在就先刹住车。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值