3.2 Linux C程序解析

相信大家在最早学习c语言程序的时候都写过“helloworld”程序,下面就是一个linux c语言的“helloworld”程序:

3.2 Linux C程序解析

简单的main函数,简单调用printf打印一句话,简单的gcc编译 “gcc hello.c –o hello”以后,执行 “./hello”就可以看到打印结果。
做完这些以后,也许你会来上一句“so easy!”。但是,你真的了解了这个简单main函数的执行过程吗?你真的了解这个简单main函数的组成部分吗?下面让我们实际剖析hello程序,看看main函数背后隐藏的真实内幕是什么样的。

1、Hello程序的组成

上述的“helloworld”程序,我们在编译的时候加上“-v”选项,查看详细的编译过程:

3.2 Linux C程序解析

可以看到可执行程序hello,并不是由hello.c一个文件生成的,在链接的时候gcc编译器还给hello程序加上了另外5个.o文件crt1.o、crti.o、crtbegin.o、crtend.o、crtn.o,分别代表了启动、初始化、构造、析构、结束5个功能。
从文件路径上可以看出,其中crt1.o、crti.o、crtn.o由glibc提供:

  • /usr/lib/gcc/i586-suse-linux/4.1.2/../../../crt1.o
  • /usr/lib/gcc/i586-suse-linux/4.1.2/../../../crti.o
  • /usr/lib/gcc/i586-suse-linux/4.1.2/../../../crtn.o

crtbegin.o、crtend.o由gcc提供:

  • /usr/lib/gcc/i586-suse-linux/4.1.2/crtbegin.o
  • /usr/lib/gcc/i586-suse-linux/4.1.2/crtend.o

你也可以使用命令“objdump –dr hello”查看hello程序的反汇编代码,可以看到除了main函数以外还有很多根本没见过的代码,这都是从哪里来的?

1.1、crt1.o

从连接顺序可以看到到crt1.o是第一个被链接的程序,第一个链接的一般都是的入口程序。的确,hello程序的第一句被执行的程序并不是main函数,而是crt1.o程序中的_start语句。从gcc默认的链接脚本中也可以看到,c程序的默认入口是“_start”。crt1.o提供了hello程序的总入口。

3.2 Linux C程序解析

  • crt1.o是由由glibc提供,crt1.o在glibc源码csu目录下(C StartUp code的缩写)。
  • crt1.o是由以下命令生成的:“gcc -nostdlib -nostartfiles -r -o crt1.o start.o abi-note.o init.o”
  • start.o对应的源文件是sysdeps/i386/elf/start.S
  • abi-note.o对应的源文件是csu/abi-note.S
  • init.o对应的源文件是csu/init.c

下面来具体看看crt1.o的主要代码sysdeps/i386/elf/start.S内部的实现 :

3.2 Linux C程序解析

可以看到start.S的主要内容就是,构造__libc_start_main函数的调用参数堆栈,并调用__libc_start_main()函数。
其中的外部引用的符号有__libc_csu_init、__libc_csu_fini、__libc_start_main。

1.2、__libc_start_main()

上节可以看到,crt1.o中的程序总入口“_start”调用到函数__libc_start_main。那么__libc_start_main是在哪里定义的,实现了什么功能呢?
首先看看__libc_start_main的二进制代码存在于哪里。__libc_start_main函数并不存在于crt开头的.o文件中,它存在于glibc的动态链接库/lib/libc.so.6中。

3.2 Linux C程序解析

__libc_start_main函数的函数定义在glibc源码sysdeps/generic/lib-start.c中,我们看看它具体实现了那些功能:

3.2 Linux C程序解析

可以看到__libc_start_main函数才真正的调用到用户的main函数。此外,在调用用户main函数之前调用初始化函数__libc_csu_init,在调用用户main函数之后调用退出函数exit,exit函数中再调用__libc_csu_fini。
exit函数的功能,是依次调用使用__cxa_atexit注册的退出函数。Exit函数在stdlib/exit.c文件中定义,__cxa_atexit函数在stdlib/cxa_atexit.c文件中定义,感兴趣可以自己看看。

1.3、__libc_csu_init()和__libc_csu_fini()

从上述__libc_start_main函数的实现中,可以看到用户main程序之前的初始化函数是调用__libc_csu_init()实现的,用户main程序之后的退出函数是__libc_csu_fini()实现的。
看看__libc_csu_init()和__libc_csu_fini()的二进制代码存在于哪里。__libc_csu_init()和__libc_csu_fini()的函数定义在glibc源码csu/elf-init.c中,它的二进制代码存在于/usr/lib/libc_nonshared.a中。

3.2 Linux C程序解析

可以看到__libc_csu_init()和__libc_csu_fini()不同于__libc_start_main(),__libc_start_main()是在动态库/lib/libc.so.6中,而__libc_csu_init()和__libc_csu_fini()是在静态库/usr/lib/libc_nonshared.a中。由于是静态库,所以__libc_csu_init()和__libc_csu_fini()会被直接链接到hello程序当中。

3.2 Linux C程序解析

这里说一点glibc库的知识。我们在编译时候 使用选项 “-lc”链接的libc库是/usr/lib/libc.so,而在运行的时候动态链接的libc库是/lib/libc.so.6。为什么会有这种差异呢?我们看看/usr/lib/libc.so的实际内容:

3.2 Linux C程序解析

可以看到/usr/lib/libc.so并不是一个实际的.so文件,它包含了/lib/libc.so.6 和/usr/lib/libc_nonshared.a。为什么会出现这样的情况呢?因为libc库并不全部是动态链接的,一部分需要静态链接的.o几种放在了/usr/lib/libc_nonshared.a静态库中。

我们再来看看csu/elf-init.c中__libc_csu_init()和__libc_csu_fini()函数的实现:

3.2 Linux C程序解析

可以看到,__libc_csu_init()调用了_init()函数,和__libc_csu_fini()调用了_finit()函数。至于_init()、_fini()函数的定义见下节。

1.4、crti.o和crtn.o

crti.o和crtn.o也在glibc的csu目录下,crti.o来之crti.S,crtn.o来之crtn.S,而crti.S和crtn.S则都来之initfini.s,initfini.s则来之与sysdeps/generic/initfini.c。
具体的编译过程就是:将initfini.c编译成initfini.s,再将initfini.s分割成crti.S和crtn.S两个文件,crti.S编译生成crti.o,crtn.S编译生成crtn.o。
具体编译过程先不关注,先看看sysdeps/generic/initfini.c中的实现:

3.2 Linux C程序解析

可以看到initfini.c文件中,定义了两个函数_init()、_finit(),对应连接到section(.init)和section(.fini)中。函数_init()、_finit()就是上一节中__libc_csu_init()和__libc_csu_fini()要调用的函数。
在initfini.c编译生成的crti.o和crtn.o文件中,crti.o包含了_init()、_finit()两个函数的上半部分,crtn.o包含了_init()、_finit()两个函数的下半部分。可以通过objdump查看实际的crti.o、crtn.o的内容:

3.2 Linux C程序解析

crti.o包含_init()、_finit()两个函数的上半部分,crtn.o包含_init()、_finit()两个函数的下半部分。为什么要搞成这样?这是为了加入程序的初始化和结束函数。
从上几节的分析可以看到,__libc_csu_init()在main函数之前调用,__libc_csu_fini()在main函数之后调用,而__libc_csu_init()调用_init(),__libc_csu_fini()调用_finit()。所以_init()、_finit()实际上就是程序的初始化和结束函数。将_init()、_finit()分成两部分是为了用户加入自己的初始化和结束函数。例如:

  • 1、用户定义一个调用自己初始化的语句函数的语句,并将其属性设置为section(.init) :section(.init)、call usr_init ();
  • 2、在链接的时候使用先链接_init()的上半部分crti.o、再链接用户section(.init)程序、随后链接_init()的下半部分crtn.o的方法,成功将用户初始化调用加入到了_init()函数之中。那么用户的初始化函数会随着系统的初始化函数调用。

下一节的crtbegin.o和crtend.o就使用上述的方法,将c++语言中的构造和析构函数加入到了初始化和结束函数中。

initfini.c文件中,还包含了一个函数call_gmon_start,实际调用的是gmon_start()函数。gmon_start()函数定义在glibc的csu/gmon-start.c文件中,gmon_start()函数的功能是gcc性能监控的初始化函数。可以参考“man profil”关于这一功能的使用。

1.5、crtbegin.o和crtend.o

承接上一节,crtbegin.o和crtend.o使用向section(.init)、section(.fini)中定义“call func”的方法,实现了c++语言的构造和析构函数机制。__do_global_ctors_aux 是c语言构造函数的调用函数,__do_global_dtors_aux是c语言析构函数的调用函数,将语句“call __do_global_ctors_aux”定义为section(.init),将语句“call __do_global_dtors_aux”定义为section(.fini)。
值得注意的,crtbegin.o和crtend.o是gcc实现的,而不是glibc。为什么要将初始化和结束函数,一些放在glibc,一些放在gcc里,有如下的解释:
crtbeginT.o及crtend.o,这两个文件是真正用于实现C++全局构造和析构的目标文件。那么为什么已经有了crti.o和crtn.o之后,还需要这两个文件呢?我们知道,C++这样的语言的实现是跟编译器密切相关的,而glibc只是一个C语言运行库,它对C++的实现并不了解。而 GCC是C++的真正实现者,它对C++的全局构造和析构了如指掌。于是它提供了两个目标文件crtbeginT.o和crtend.o来配合glibc 实现C++的全局构造和析构。事实上是crti.o和crtn.o中的“.init”和“.finit”提供一个在main()之前和之后运行代码的机制,而真正全局构造和析构则由crtbeginT.o和crtend.o来实现。

crtbegin.o和crtend.o都来自文件gcc/crtstuff.c。下面来看其源码,看其功能是怎么实现的。

crtbegin.o提供两个函数:frame_dummy()属于section(.init),__do_global_dtors_aux()析构函数属于section(.fini)。frame_dummy是eh_frame(exception handling.)异常处理设计到的功能,这里先不做研究。

3.2 Linux C程序解析

crtend.o只提供了一个构造函数__do_global_ctors_aux属于section(.init)。

3.2 Linux C程序解析

1.6、attribute ((constructor))和attribute ((destructor))

上一节中已经知道,crtend.o中的do_global_ctors_aux()函数调用__CTOR_LIST数组中的构造函数,crtbegin.o中do_global_dtors_aux()函数调用__DTOR_LIST数组中的析构函数。
具体CTOR_LISTDTOR_LIST到底是怎样一个数组,看gcc/crtstuff.c中的实际定义:

3.2 Linux C程序解析

可以看到CTOR_LIST数组定义在section(.ctors)中,DTOR_LIST数组定义在section(.dtors)中。那么把我们的函数放在section(.ctors)和section(.dtors)中是不是就会被当成构造和析构函数调用呢?
没错,gcc的构造和析构函数就是这样实现的。Gcc的c语言中attribute ((constructor))和attribute ((destructor))就是拿来定义构造和析构函数的。在每个hpi插件的xxx_replace.c中就有应用。

3.2 Linux C程序解析

这里的构造函数libinit()会在用户的main函数之前被调用,析构函数libfinish()会在main函数结束之后被调用。
使用attribute((section(“.ctors”)))、attribute((section(“.dtors”))) 定义函数指针。和attribute ((constructor))和attribute ((destructor))是一样的效果。

2、Hello程序的执行过程

上一章中,对简单c程序hello的各个组成部分进行了分解,那么回到整体的角度来看,整个hello程序的执行过程是怎么样的呢?
可以看下面这个完整的流程图:

3.2 Linux C程序解析

可以使用objdump反汇编hello程序,对照上面的流程图学习:
[email protected]:~/ld_test> objdump -dr hello

3、其他情况下的crt文件

我们已经看过正常编译情况下链接的是crt1.o、crti.o、crtbegin.o、crtend.o、crtn.o,现在我们看看在特许的编译选项下crt文件的变种。

3.1、“-shared”选项

3.2 Linux C程序解析

涉及到的文件有4个:crti.o、crtbeginS.o、crtendS.o、crtn.o。

3.2、“-pg”选项

3.2 Linux C程序解析

涉及到的文件有5个:gcrt1.o、crti.o、crtbegin.o、crtend.o、crtn.o。

3.3、“-static”选项

3.2 Linux C程序解析

涉及到的文件有5个:crt1.o、crti.o、crtbeginT.o、crtend.o、crtn.o。

4、环境变量

一般我们main函数的定义形式只使用两个参数:int main(int argc, char *argv[])。但是我们在__libc_start_main()函数中可以看到,gcc实际上是使用3个参数来调用main函数 :

3.2 Linux C程序解析

多出的一个参数就是环境变量,__environ是一个弱符号,实际的环境变量指针是char **__environ = NULL。
所以可以通过以下函数参数3或者__environ引用到环境变量:

3.2 Linux C程序解析

可以看到实际环境变量就是一堆字符串,在c代码中可以使用getenv()、setenv()、unsetenv()、clearenv()一系列函数来操作环境变量。

5、参考资料