本篇文章并不探寻程序的本源,那是个庞大的话题,当然也是一个很吸引人的话题,但是本文并不打算谈论那些;本篇文章也不旨在解决什么实质性问题,因为据我多年工作经历来说,只了解main函数已经足够,至于main函数以外有什么,似乎无关紧要。我写下本片文章仅仅是因为一个我一直知道但是一直没有去了解的一个问题,linux下一个用户程序的起点在哪里?
作为一个基于linux/uboot做驱动开发多年的老程序员来说,我当然知道main并不是唯一,不是起点,更不是终点,而仅仅是一个阶段,当然这是一个不可或缺的阶段。
一个程序的主体结构是链接阶段决定的,你可能编译了很多代码,但最终哪些代码会被最终添加到可执行文件中去,是链接器决定的,那链接器基于什么来决定程序的主体结构呢?搞过底层开发的人大概都有所了解,那就是通过链接脚本。那用户程序是否也依赖于链接脚本呢?如果是的话,链接脚本里面肯定会标记程序的起始点。我稍微百度了下,发现用户程序确实也需要链接脚本,而查看链接脚本的命令也很简单:
ld -verbose
从输出结果中我看到了自己想要的:
ENTRY(_start)
很明显,这是一个程序的起点。
然而另外一个问题接踵而来,_start来自于哪里?c文件里面并没有定义_start,那他来自哪里呢?既然我们定义_start,那它可定来自于某些库?glibc是最可疑的,于是我下载了glibc的代码,非常幸运,我又一次搜到了自己想要的东西:
_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
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函数之前会调用__libc_csu_init,main函数之后会调用__libc_csu_fini。贴下这两个函数:
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);
}
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
#include
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
#include
__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 :
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
400534: 90 nop
400535: 5d pop %rbp
400536: c3 retq
.fini_array.也有个地址0x00400537,它对应:
0000000000400537 :
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
400545: 90 nop
400546: 5d pop %rbp
400547: c3 retq
跟我的猜想完全一致。