读书频道 > 系统 > linux > 深度探索Linux操作系统系统构建和原理解析
2.2.12 启动代码
2013-09-28 15:08:44     我来说两句 
收藏    我要投稿   
全书一共8章:第1章介绍了如何准备工作环境。在第2章中构建了编译工具链,这是后面构建操作系统各个组件的基础。在这一章中,不仅详细讲解了工具链的构建过程,而且还通过对编译链接过程的探讨,深入讨论了工具链  立即去当当网订购

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

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

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

我们查看可执行程序hello的入口函数:
root@baisheng:~/demo# readelf -h hello | grep Entry
  Entry point address:               0x80482f0

根据ELF的头可见,可执行文件hello的入口地址为0x80482f0。但该地址对应的函数是main吗?
root@baisheng:~/demo# readelf -s hello | grep 80482f0
    61: 080482f0     0 FUNC    GLOBAL DEFAULT   13 _start

结果显然让我们很失望,可执行文件的入口不是我们熟悉的main函数,而是一个陌生的_start函数,而且凭我们的职业直觉,这个函数的定义很像汇编语言的函数名。我们再来看一下可执行文件hello的代码段的起始地址:
root@baisheng:~/demo# readelf -S hello           
There are 30 section headers, starting at offset 0x1198:
Section Headers:
  [Nr] Name              Type            Addr     Off    Size
   ...
  [13] .text             PROGBITS        080482f0 0002f0 0001b8
   ...

根据代码段的起始地址可见,hello的代码段的最开头的函数确实是函数_start,而不是我们熟悉的main函数。那么main函数在哪里呢?
root@baisheng:~/demo# readelf -s hello | grep main  
    64: 080483fc    38 FUNC    GLOBAL DEFAULT   13 main

我们做个减法运算:
0x080483fc – 0x080482f0 = 0x10c = 268

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

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

_start:
    ...
    popl %esi          /* Pop the argument count.  */
    movl %esp, %ecx    /* argv starts just at the current stack top.*/
    ...
    andl $0xfffffff0, %esp
    pushl %eax         /* Push garbage because we allocate
                       28 more bytes.  */
    ...
    pushl %esp

    pushl %edx         /* Push address of the shared library
                       termination function.  */
    ...
    /* 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 $BP_SYM (main)

    /* Call the user's main function, and exit with its value.
       But let the libc call main.    */
    call BP_SYM (__libc_start_main)

_start函数先作了一些初始化,接着就是调用__libc_start_main压栈参数,包括程序进入main函数之前的初始化函数__libc_csu_init、退出时可能执行的善后函数__libc_csu_fini以及main函数的参数,最后调用__libc_start_main。
glibc-2.15/csu/libc-start.c:

STATIC int LIBC_START_MAIN (...)
{
...
  if (init)
    (*init) (argc, argv, __environ MAIN_AUXVEC_PARAM);
  ...
  result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);
  ...
}

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

函数_start包含在启动文件crt1.o中。根据启动文件crt1.o的符号表也可看出这一点。
vita@baisheng:/vita$ readelf -s /vita/sysroot/usr/lib/crt1.o
    ...
    18: 00000000     0 FUNC    GLOBAL DEFAULT    2 _start
...

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

在链接时,gcc使用内置的spec文件来控制链接的启动文件。编译时,可以通过给gcc传递参数-specs=file来覆盖gcc内置的spec文件。我们可以传递参数-dumpspec来查看gcc内置的spec文件规定链接时链接哪些启动文件:
vita@baisheng:~$ i686-none-linux-gnu-gcc –dumpspec
...
*endfile:
%{Ofast|ffast-math|funsafe-math-optimizations:crtfastmath.o%s}
%{mpc32:crtprec32.o%s}
%{mpc64:crtprec64.o%s}
%{mpc80:crtprec80.o%s}
%{shared|pie:crtendS.o%s;:crtend.o%s}
crtn.o%s
...
*startfile:
%{!shared: %{pg|p|profile:gcrt1.o%s;pie:Scrt1.o%s;:crt1.o%s}}
crti.o%s
%{static:crtbeginT.o%s;shared|pie:crtbeginS.o%s;:crtbegin.o%s}
...

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

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

void __libc_csu_init (int argc, char **argv, char **envp)
{
  ...
#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

  _init ();

  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_init将先后调用段“.preinit_array”、“.init_array”中包含的函数指针指向的函数。因此,如果打算在程序执行main函数前或者在动态库被加载时做点什么,那么我们可以定义一个函数,并告诉链接器将函数指针存储到段“.preinit_array”或 “.init_array”中。示例代码如下:
foo.c

#include <stdio.h>

void myinit(int argc, char **argv, char **envp)
{
     printf("%s\n", __FUNCTION__);
}

__attribute__((section(".init_array"))) typeof(myinit) *_myinit =
myinit;

void test()
{
    printf("%s\n", __FUNCTION__);
}

bar.c
#include <stdio.h>

void main()
{
    printf ("Enter main./n");
    test();
}

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

使用如下命令编译并运行程序:
root@baisheng:~/demo# gcc -shared -fPIC foo.c -o libfoo.so
root@baisheng:~/demo# gcc bar.c -o bar -L./ -lfoo
root@baisheng:~/demo# LD_LIBRARY_PATH=./ ./bar
myinit
Enter main.
test

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

点击复制链接 与好友分享!回本站首页
分享到: 更多
您对本文章有什么意见或着疑问吗?请到论坛讨论您的关注和建议是我们前行的参考和动力  
上一篇:2.2.11 关于使用libtool链接库的讨论
下一篇:概述
相关文章
图文推荐
3.3.6 GNOME的软件管
3.3.5 GNOME的文件管
3.3.4 GNOME的窗口管
3.3.3 收藏夹和快捷
排行
热门
文章
下载
读书

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训
版权所有: 红黑联盟--致力于做最好的IT技术学习网站