探寻main函数以外的大千世界

本文探讨了Linux用户程序的起点不仅仅局限于main函数,深入解析了_start的来源,揭示了glibc在其中的重要角色。通过链接脚本和源码分析,展示了__libc_csu_init和__libc_csu_fini在main函数前后执行的过程,同时介绍了通过.constructor和.destructor关键字实现类似功能的另一种写法。
摘要由CSDN通过智能技术生成

前言


本篇文章并不探寻程序的本源,那是个庞大的话题,当然也是一个很吸引人的话题,但是本文并不打算谈论那些;本篇文章也不旨在解决什么实质性问题,因为据我多年工作经历来说,只了解main函数已经足够,至于main函数以外有什么,似乎无关紧要。我写下本片文章仅仅是因为一个我一直知道但是一直没有去了解的一个问题,linux下一个用户程序的起点在哪里?
作为一个基于linux/uboot做驱动开发多年的老程序员来说,我当然知道main并不是唯一,不是起点,更不是终点,而仅仅是一个阶段,当然这是一个不可或缺的阶段。

寻找起点其实很简单


一个程序的主体结构是链接阶段决定的,你可能编译了很多代码,但最终哪些代码会被最终添加到可执行文件中去,是链接器决定的,那链接器基于什么来决定程序的主体结构呢?搞过底层开发的人大概都有所了解,那就是通过链接脚本。那用户程序是否也依赖于链接脚本呢?如果是的话,链接脚本里面肯定会标记程序的起始点。我稍微百度了下,发现用户程序确实也需要链接脚本,而查看链接脚本的命令也很简单:

ld -verbose

从输出结果中我看到了自己想要的:

ENTRY(_start)

很明显,这是一个程序的起点。

你从何处而来

然而另外一个问题接踵而来,_start来自于哪里?c文件里面并没有定义_start,那他来自哪里呢?既然我们定义_start,那它可定来自于某些库?glibc是最可疑的,于是我下载了glibc的代码,非常幸运,我又一次搜到了自己想要的东西:

  • sysdeps/i386/start.S
_start:
	/* Clear the frame pointer.  The ABI suggests this be done, to mark
	   the outermost frame obviously.  */
	xorl %ebp, %ebp

	/* Extract the arguments as encoded on the stack and set up
	   the arguments for `main': argc, argv.  envp will be determined
	   later in __libc_start_main.  */
	popl %esi		/* Pop the argument count.  */
	movl %esp, %ecx		/* argv starts just at the current stack top.*/

	/* Before pushing the arguments align the stack to a 16-byte
	(SSE needs 16-byte alignment) boundary to avoid penalties from
	misaligned accesses.  Thanks to Edward Seidl <seidl@janed.com>
	for pointing this out.  */
	andl $0xfffffff0, %esp
	pushl %eax		/* Push garbage because we allocate
				   28 more bytes.  */

	/* Provide the highest stack address to the user code (for stacks
	   which grow downwards).  */
	pushl %esp

	pushl %edx		/* Push address of the shared library
				   termination function.  */

#ifdef SHARED
	/* 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.  */

	pushl main@GOT(%ebx)

	/* Call the user's main function, and exit with its value.
	   But let the libc call main.    */
	call __libc_start_main@PLT
#else
	/* Push address of our own entry points to .fini and .init.  */
	pushl $__libc_csu_fini
	pushl $__libc_csu_init

	pushl %ecx		/* Push second argument: argv.  */
	pushl %esi		/* Push first argument: argc.  */

	pushl $main

	/* 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.  */

#ifdef SHARED
1:	movl	(%esp), %ebx
	ret
#endif

很明显,链接的时候,链接器偷偷把glibc里面的一些东西链接过来了,这就是为什么glibc对于用户程序来说是那么不可或缺,因为它是一切的源头,也是一切的结尾(这话不一定对)。而main函数,于glibc而言,只不过是一个简简单单的函数调用,普通的不能再普通,但是对于用户来说,main确实一切!
然而,main真的是用户唯一可见的吗?既然有了glibc源码,我们无需猜测,直接撸代码即可。

main是唯一面向用户的接口吗?

通过阅读代码很容易发现,main函数之前会调用__libc_csu_init,main函数之后会调用__libc_csu_fini。贴下这两个函数:

  • __libc_csu_init
void
__libc_csu_init (int argc, char **argv, char **envp)
{
  /* For dynamically linked executables the preinit array is executed by
     the dynamic linker (before initializing any shared object).  */

#ifndef LIBC_NONSHARED
  /* For static executables, preinit happens right before init.  */
  {
    const size_t size = __preinit_array_end - __preinit_array_start;
    size_t i;
    for (i = 0; i < size; i++)
      (*__preinit_array_start [i]) (argc, argv, envp);
  }
#endif

#ifndef NO_INITFINI
  _init ();
#endif

  const size_t size = __init_array_end - __init_array_start;
  for (size_t i = 0; i < size; i++)
      (*__init_array_start [i]) (argc, argv, envp);
}
  • __libc_csu_fini
void
__libc_csu_fini (void)
{
#ifndef LIBC_NONSHARED
 size_t i = __fini_array_end - __fini_array_start;
 while (i-- > 0)
   (*__fini_array_start [i]) ();
   # ifndef NO_INITFINI
 _fini ();
# endif
#endif
}

从链接脚本可以查到__init_array_start ~ __init_array_end是一个特殊.init_array.段,而__fini_array_start ~ __fini_array_end是一个特殊的段.fini_array.,很明显我只要把对应函数的地址放在.init_array.段,它就可以在main函数之前执行,把函数地址放在.fini_array.段,它就可以在main函数之前执行。
以下是验证代码:

#include <stdio.h>
#include <stdlib.h>

void before_main(int argc, char **argv, char **env)
{
   printf("before main\n");
}

void after_main(int argc, char **argv, char **env)
{
       printf("after main\n");
}

__attribute__ ((__section__(".init_array."))) void *a[1] = {(void *)before_main};
__attribute__ ((__section__(".fini_array."))) void *b[1] = {(void *)after_main};

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

   return 0;
}

执行结果如下:

before main
hello world
after main

符合预期。

另外一种写法换汤不换药

我在上网查到了另外一种写法,也能实现相同的效果,代码如下:

#include <stdio.h>
#include <stdlib.h>

__attribute__((constructor)) void before_main()
{
   printf("before main\n");
}

__attribute__((destructor)) void after_main()
{
       printf("after main\n");
}
int main(int argc, char **argv)
{
   printf("hello world\n");

   return 0;
}

我换衣编译器可以识别关键字constructor和destructor,将他们分别放在了.init_array.和.fini_array.段,这个很好证明,反汇编看一下就可以了:

Disassembly of section .init_array:

0000000000600e00 <__frame_dummy_init_array_entry>:
 600e00:       00 05 40 00 00 00       add    %al,0x40(%rip)        # 600e46 <_DYNAMIC+0x1e>
 600e06:       00 00                   add    %al,(%rax)
 600e08:       26 05 40 00 00 00       es add $0x40,%eax
       ...

Disassembly of section .fini_array:

0000000000600e10 <__do_global_dtors_aux_fini_array_entry>:
 600e10:       e0 04                   loopne 600e16 <__do_global_dtors_aux_fini_array_entry+0x6>
 600e12:       40 00 00                add    %al,(%rax)
 600e15:       00 00                   add    %al,(%rax)
 600e17:       00 37                   add    %dh,(%rdi)
 600e19:       05 40 00 00 00          add    $0x40,%eax

.init_array中有一个地址:0x00400526,它对应:

0000000000400526 <before_main>:
400526:       55                      push   %rbp
400527:       48 89 e5                mov    %rsp,%rbp
40052a:       bf f4 05 40 00          mov    $0x4005f4,%edi
40052f:       e8 cc fe ff ff          callq  400400 <puts@plt>
400534:       90                      nop
400535:       5d                      pop    %rbp
400536:       c3                      retq

.fini_array.也有个地址0x00400537,它对应:

0000000000400537 <after_main>:
  400537:       55                      push   %rbp
  400538:       48 89 e5                mov    %rsp,%rbp
  40053b:       bf 00 06 40 00          mov    $0x400600,%edi
  400540:       e8 bb fe ff ff          callq  400400 <puts@plt>
  400545:       90                      nop
  400546:       5d                      pop    %rbp
  400547:       c3                      retq

跟我的猜想完全一致。

结束

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值