这一篇来一点比较硬核的东西,程序是main函数开始的么?
13.1 程序是从main函数开始的么? 13.1.1 gcc编译详细输出在我们学习c语言的时候,老师是不是一直都在说c程序是从main函数开始的,然后我们写代码的时候,其实也都是从main函数开始写,编译执行之后打印也是从main函数打印,是不是c程序从main函数开始执行,就很根深蒂固一样,这次我们就来推翻一下。
我们来写一个代码:
#includeint main(int argc, char **argv) { printf("hello worldn"); 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
==================================================
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");
SECTIONS
{
PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + 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 :
{
KEEP (*crtbegin.o(.ctors))
KEEP (*crtbegin?.o(.ctors))
KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .ctors))
KEEP (*(SORT(.ctors.*)))
KEEP (*(.ctors))
}
}
通过这个文件我们看到函数入口是ENTRY(_start),这个,这次确定不是main函数开始了吧,我还留下了.init和.fini段,说明.text之前和之后都会有代码执行的。
我们稍微修改一下上面的代码:
#include// static void __attribute__((section(".init"))) init_main(void) // { // printf("init mainn"); // } static void __attribute__ ((constructor)) before_main(void) // main函数之前 { printf("befor mainn"); } static void __attribute__ ((destructor)) after_main(void) // main函数之后 { printf("after mainn"); } // static void __attribute__((section(".fini"))) fini_main(void) // { // printf("fini mainn"); // } int main(int argc, char **argv) { printf("hello worldn"); 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是在:sysdepsx86_64start.S中。
我们拷贝出来分析一下:
都是汇编+英语,一看就头大,还是用翻译软件来翻译翻译。
#includeENTRY (_start) cfi_undefined (rip) xorl %ebp, %ebp mov %RDX_LP, %R9_LP #ifdef __ILP32__ mov (%rsp), %esi add $4, %esp #else popq %rsi #endif mov %RSP_LP, %RDX_LP and $~15, %RSP_LP pushq %rax pushq %rsp #ifdef PIC //动态链接走这一步 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 mov $__libc_csu_fini, %R8_LP mov $__libc_csu_init, %RCX_LP mov $main, %RDI_LP #endif 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)
{
int result;
__libc_multiple_libcs = &_dl_starting_up && !_dl_starting_up;
#ifndef SHARED
_dl_relocate_static_pie ();
char **ev = &argv[argc + 1];
__environ = ev; // 取到环境变量
__libc_stack_end = stack_end;
// 初始化多线程的吧
if (__pthread_initialize_minimal != NULL)
__pthread_initialize_minimal ();
#endif
if (__glibc_likely (rtld_fini != NULL))
__cxa_atexit ((void (*) (void *)) rtld_fini, NULL, NULL);
//注册动态链接器的析构函数(如果有的话)
#ifndef SHARED
__libc_init_first (argc, argv, __environ); // 初始化libc库
//注册程序的析构函数(如果有的话)
if (fini)
__cxa_atexit ((void (*) (void *)) fini, NULL, NULL);
if (__builtin_expect (__libc_enable_secure, 0))
__libc_check_standard_fds ();
#endif
if (init)
(*init) (argc, argv, __environ MAIN_AUXVEC_PARAM); // init函数执行
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 总结这一篇虽然比较硬核,但是就作为了解的吧,知道这么回事就可以了,以后有机会再去分析内核的部分吧,现在就先刹住车。



