2.2.12 启动代码

启动代码是工具链中C库和编译器都提供了的重要部分之一,但是由于应用程序员很少接触它们,因此非常容易引起程序员的困惑,所以我们特将其单独列出,使用一点篇幅加以讨论。

不知读者是否留意过这个问题:无论是在DOS下、Windows下,还是在Linux操作系统下,程序员使用C语言编程时,几乎所有程序的入口函数都是main,这是因为启动代码的存在。在"hosted environment"下,应用程序运行在操作系统之上,程序启动前和退出前需要进行一些初始化和善后工作,而这些工作与"hosted environment"密切相关,并且是公共的,不属于应用程序范畴的事情,这些应用程序员无需关心。更重要的一点是,有些初始化动作需要在main函数运行前完成,比如C++全局对象的构造。有些操作是不能使用C语言完成的,必须要使用汇编指令,比如栈的初始化。于是编译器和C库将它们抽取出来,放在了公共的代码中。

这些公共代码被称为启动代码,其实不只是程序启动时,也包括在程序退出时执行的一些代码,我们统称它们为启动代码,并将启动代码所在的文件称为启动文件。对于C语言来说,Glibc提供启动文件。显然,对于C++语言来说,因为启动代码是和语言密切相关的,所以其启动代码不在C库中,而由GCC提供。这些启动文件以"crt"(可以理解为C RunTime的缩写)开头、以".o"结尾。

我们查看可执行程序hello的入口函数:

2.2.12 启动代码 - 图1

根据ELF的头可见,可执行文件hello的入口地址为0x80482f0。但该地址对应的函数是main吗?

2.2.12 启动代码 - 图2

结果显然让我们很失望,可执行文件的入口不是我们熟悉的main函数,而是一个陌生的_start函数,而且凭我们的职业直觉,这个函数的定义很像汇编语言的函数名。我们再来看一下可执行文件hello的代码段的起始地址:

2.2.12 启动代码 - 图3

根据代码段的起始地址可见,hello的代码段的最开头的函数确实是函数_start,而不是我们熟悉的main函数。那么main函数在哪里呢?

2.2.12 启动代码 - 图4

我们做个减法运算:

2.2.12 启动代码 - 图5

也就是说,在代码段中,偏移268字节处才是main函数的代码,代码段的前268字节都是启动代码,当然,程序启动时的启动代码不仅限于这268字节,因为函数_start中可能还会调用C库中的一些函数。

如果用户的程序中,没有明确指明使用自己定义的启动代码,那么链接器将自动使用C库和C编译器中提供的启动代码。链接器将函数"_start"作为ELF文件的默认入口函数。函数_start的相关代码如下:

2.2.12 启动代码 - 图6

_start函数先作了一些初始化,接着就是调用libc_start_main压栈参数,包括程序进入main函数之前的初始化函数libc_csu_init、退出时可能执行的善后函数libc_csu_fini以及main函数的参数,最后调用libc_start_main。

2.2.12 启动代码 - 图7

2.2.12 启动代码 - 图8

进入函数libc_start_main后,将调用函数libc_csu_init等初始化函数进行各种初始化操作、准备程序运行环境,最后才进入我们熟知的main函数。

函数_start包含在启动文件crt1.o中。根据启动文件crt1.o的符号表也可看出这一点。

2.2.12 启动代码 - 图9

通过前面的简要分析,我们直观地感受到了所谓“启动代码”的意义。函数_start才是第一个从"hosted environment"进入到应用程序时运行的第一个函数,是名副其实的入口函数。从系统的角度看,main函数与普通函数无异,并不是什么真正的入口函数,main只是程序员的入口函数。因此,通过更改启动代码,这个程序员的入口函数也完全可以使用其他的函数名称而不是什么main,比如MFC中就不用main这个名字。

在链接时,gcc使用内置的spec文件来控制链接的启动文件。编译时,可以通过给gcc传递参数-specs=file来覆盖gcc内置的spec文件。我们可以传递参数-dumpspec来查看gcc内置的spec文件规定链接时链接哪些启动文件:

2.2.12 启动代码 - 图10

当然,编译时也可以根据实际情况传递参数如-nostartfiles、-nostdlib、-ffreestanding等给链接器,告诉链接器不要链接系统中提供的启动代码,而是使用自己程序中提供的。

最后,让我们以一个小例子,结束本章。回顾上面的函数libc_start_main,在其调用main函数前,启动代码中的函数libc_start_main将调用init函数,而_start传递给__libc_start_main的init函数指针指向的是_libc_csu_init:

2.2.12 启动代码 - 图11

根据函数可见,__libc_csu_init将先后调用段".preinit_array"、".init_array"中包含的函数指针指向的函数。因此,如果打算在程序执行main函数前或者在动态库被加载时做点什么,那么我们可以定义一个函数,并告诉链接器将函数指针存储到段".preinit_array"或".init_array"中。示例代码如下:

2.2.12 启动代码 - 图12

我们通过关键字"attribute((section(".init_array")))"指定链接器将函数myinit的地址放置到段".init_array"中,那么在库libfoo被加载时,函数myinit会被__libc_csu_init调用。

使用如下命令编译并运行程序:

2.2.12 启动代码 - 图13

根据程序bar的输出可见,函数myinit在进入函数main之前就被调用了。也就是说,库libfoo在加载时,函数myinit就被启动代码调用了。