GCC编译的背后( 预处理和编译 汇编和连接 )

by falcon<zhangjinw@gmail.com>
2008-02-22

    平时在Linux下写代码,直接用"gcc -o out in.c"就把代码编译好了,可是这后面到底作了什么事情呢?若是学习过编译原理则不难理解,通常高级语言程序编译的过程莫过于:预处理、编译、汇编、连接。gcc在后台实际上也经历了这几个过程,咱们能够经过-v参数查看它的编译细节,若是想看某个具体的编译过程,则能够分别使用-E,-S,-c和 -O,对应的后台工具则分别为cpp,cc1,as,ld。下面咱们将逐步分析这几个过程以及相关的内容,诸如语法检查、代码调试、汇编语言等。

一、预处理

    开篇简述:预处理是C语言程序从源代码变成可执行程序的第一步,主要是C语言编译器对各类预处理命令进行处理,包括头文件的包含、宏定义的扩展、条件编译的选择等。

    之前没怎么“深刻”预处理,脑子对这些东西老是很模糊,只记得在编译的基本过程(词法分析、语法分析)以前还须要对源代码中的宏定义、文件包含、条件编译等命令进行处理。这三类的指令很常见,主要有#define, #include和#ifdef ... #endif,要特别地注意它们的用法。(更多预处理的指令请查阅相关资料)

    #define除了能够独立使用以便灵活设置一些参数外,还经常和#ifdef ... #endif结合使用,以便灵活地控制代码块的编译与否,也能够用来避免同一个头文件的屡次包含。关于#include貌似比较简单,经过man找到某个函数的头文件,copy进去,加上<>就okay。这里虽然只关心一些技巧,不过预处理仍是蕴含着不少潜在的陷阱(可参考<C Traps & Pitfalls>),咱们也须要注意的。下面仅介绍和预处理相关的几个简单内容。

php

  • 打印出预处理以后的结果:gcc -E hello.c

        这样咱们就能够看到源代码中的各类预处理命令是如何被解释的,从而方便理解和查错。

        实际上gcc在这里是调用了cpp的(虽然咱们经过gcc的-v仅看到cc1),cpp即The C Preprocessor,主要用来预处理宏定义、文件包含、条件编译等。下面介绍它的一个比较重要的选项-D。

  • 在命令行定义宏:gcc -Dmacro hello.c

        等同于在文件的开头定义宏,即#define maco,可是在命令行定义更灵活。例如,在源代码中有这些语句。
    #ifdef DEBUG
    printf("this code is for debugging\n");
    #endif

        若是编译时加上-DDEBUG选项,那么编译器就会把printf所在的行编译进目标代码,从而方便地跟踪该位置的某些程序状态。这样-DDEBUG就能够看成一个调试开关,编译时加上它就能够用来打印调试信息,发布时则能够经过去掉该编译选项把调试信息去掉。

    本节参考资料:
    [1] C语言教程第九章:预处理
    http://www.bc-cn.net/Article/kfyy/cyy/jc/200409/9.html
    [2] 更多
    http://www.hemee.com/kfyy/c/6626.html
    http://www.91linux.com/html/article/program/cpp/20071203/8745.html
    http://www.janker.org/bbs/programmer/2006-10-13/327.html

    二、编译(翻译)

          开篇简要:编译以前,C语言编译器会进行词法分析、语法分析(-fsyntax-only),接着会把源代码翻译成中间语言,即汇编语言。若是想看到这个中间结果,能够用-S选项。须要提到的是,诸如shell等解释语言也会经历一个词法分析和语法分析的阶段,不过以后并不会进行“翻译”,而是“解释”,边解释边执行
    ************************

A、解释程序

所谓解释程序是高级语言翻译程序的一种,它将源语言(如BASIC)书写的源程序做为输入,解释一句后就提交计算机执行一句,并不造成目标程序。就像外语翻译中的“口译”同样,说一句翻一句,不产生全文的翻译文本。这种工做方式很是适合于人经过终端设备与计算机会话,如在终端上打一条命令或语句,解释程序就当即将此语句解释成一条或几条指令并提交硬件当即执行且将执行结果反映到终端,从终端把命令打入后,就能当即获得计算结果。这的确是很方便的,很适合于一些小型机的计算问题。但解释程序执行速度很慢,例如源程序中出现循环,则解释程序也重复地解释并提交执行这一组语句,这就形成很大浪费。

B、编译程序

这是一类很重要的语言处理程序,它把高级语言(如FORTRAN、COBOL、Pascal、C等)源程序做为输入,进行翻译转换,产生出机器语言的目标程序,而后再让计算机去执行这个目标程序,获得计算结果。

编译程序工做时,先分析,后综合,从而获得目标程序。所谓分析,是指词法分析和语法分析;所谓综合是指代码优化,存储分配和代码生成。为了完成这些分析综合任务,编译程序采用对源程序进行屡次扫描的办法,每次扫描集中完成一项或几项任务,也有一项任务分散到几回扫描去完成的。下面举一个四遍扫描的例子:第一遍扫描作词法分析;第二遍扫描作语法分析;第三遍扫描作代码优化和存储分配;第四遍扫描作代码生成。

值得一提的是,大多数的编译程序直接产生机器语言的目标代码,造成可执行的目标文件,但也有的编译程序则先产生汇编语言一级的符号代码文件,而后再调用汇编程序进行翻译加工处理,最后产生可执行的机器语言目标文件

在实际应用中,对于须要常用的有大量计算的大型题目,采用招待速度较快的编译型的高级语言较好,虽然编译过程自己较为复杂,但一旦造成目标文件,之后可屡次使用。相反,对于小型题目或计算简单不太费机时的题目,则多选用解释型的会话式高级语言,如BASIC,这样能够大大缩短编程及调试的时间
html

            ************************
        把源代码翻译成汇编语言,其实是编译的整个过程当中的第一个阶段,以后的阶段和汇编语言的开发过程没有什么区别。这个阶段涉及到对源代码的词法分析、语法检查(经过-std指定遵循哪一个标准),并根据优化(-O)要求进行翻译成汇编语言的动做

      若是仅仅但愿进行语法检查,能够用-fsyntax-only选项;而为了使代码有比较好的移植性,避免使用gcc的一些特性,能够结合-std和 -pedantic(或者-pedantic-erros)选项让源代码遵循某个C语言标准的语法。这里演示一个简单的例子。

java

$ cat hello.c
#include <stdio.h>
int main()
{
        printf("hello, world\n")
        return 0;
}
$ gcc -fsyntax-only hello.c
hello.c: In function ‘main’:
hello.c:5: error: expected ‘;’ before ‘return’
$ vim hello.c
$ cat hello.c
#include <stdio.h>
int main()
{
        printf("hello, world\n");
        int i;
        return 0;
}
$ gcc -std=c89 -pedantic-errors hello.c    #默认状况下,gcc是容许在程序中间声明变量的,可是turboc就不支持
hello.c: In function ‘main’:
hello.c:5: error: ISO C90 forbids mixed declarations and code
linux



    语法错误是程序开发过程当中难以免的错误(人的大脑在不少条件下都容易开小差),不过编译器每每可以经过语法检查快速发现这些错误,并准确地告诉你语法错误的大概位置。所以,做为开发人员,要作的事情不是“恐慌”(不知所措),而是认真阅读编译器的提示,根据平时积累的经验(最好在大脑中存一份常见语法错误索引,不少资料都提供了常见语法错误列表,如<C Traps&Pitfalls>和最后面的参考资料[12]也列出了不少常见问题)和编辑器提供的语法检查功能(语法加亮、括号匹配提示等)快速定位语法出错的位置并进行修改。

    语法检查以后就是翻译动做,gcc提供了一个优化选项-O,以便根据不一样的运行平台和用户要求产生通过优化的汇编代码。例如,

c++

$ gcc -o hello hello.c            #采用默认选项,不优化
$ gcc -O2 -o hello2 hello.c        #优化等次是2
$ gcc -Os -o hellos hello.c        #优化目标代码的大小
$ ls -S hello hello2 hellos        #能够看到,hellos比较小,hello2比较大
hello2  hello  hellos
$ time ./hello
hello, world

real    0m0.001s
user    0m0.000s
sys     0m0.000s
$ time ./hello2                #多是代码比较少的缘故,执行效率看上去不是很明显
hello, world

real    0m0.001s
user    0m0.000s
sys     0m0.000s

$ time ./hellos                #虽然目标代码小了,可是执行效率慢了些
hello, world

real    0m0.002s
user    0m0.000s
sys     0m0.000s
程序员



    根据上面的简单演示,能够看出gcc有不少不一样的优化选项,主要看用户的需求了,目标代码的大小和效率之间貌似存在一个“纠缠”,须要开发人员本身权衡。

    下面咱们经过-S选项来看看编译出来的中间结果,汇编语言,仍是以以前那个hello.c为例。shell

$ gcc -S hello.c        #默认输出是hello.s,可本身指定,输出到屏幕-o -,输出到其余文件-o file
$ cat hello.s
cat hello.s
        .file   "hello.c"
        .section        .rodata
.LC0:
        .string "hello, world"
        .text
.globl main
        .type   main, @function
main:
        leal    4(%esp), %ecx
        andl    $-16, %esp
        pushl   -4(%ecx)
        pushl   %ebp
        movl    %esp, %ebp
        pushl   %ecx
        subl    $4, %esp
        movl    $.LC0, (%esp)
        call    puts
        movl    $0, %eax
        addl    $4, %esp
        popl    %ecx
        popl    %ebp
        leal    -4(%ecx), %esp
        ret
        .size   main, .-main
        .ident  "GCC: (GNU) 4.1.3 20070929 (prerelease) (Ubuntu 4.1.2-16ubuntu2)"
        .section        .note.GNU-stack,"",@progbits
编程



    不知道看出来没?和咱们在课堂里学的intel的汇编语法不太同样,这里用的是AT&T语法格式。若是以前没接触过AT&T的,能够看看参考资料[2]。若是想学习Linux下的汇编语言开发,从下一节开始哦,下一节开始的全部章节基本上覆盖了Linux下汇编语言开发的通常过程,不过这里不介绍汇编语言语法。

    这里须要补充的是,在写C语言代码时,若是可以对编译器比较熟悉(工做原理和一些细节)的话,可能会颇有帮助。包括这里的优化选项(有些优化选项可能在汇编时采用)和可能的优化措施,例如字节对齐(能够看看这本书"Linux_Assembly_Language_Programming"的第六小节)、条件分支语句裁减(删除一些明显分支)等。

本节参考资料

[1] Guide to Assembly Language Programming in Linux(pdf教程,社区有下载)
http://oss.lzu.edu.cn/modules/wfdownloads/singlefile.php?cid=5&lid=94
[2] Linux汇编语言开发指南(在线):
http://www.ibm.com/developerworks/cn/linux/l-assembly/index.html
[3] PowerPC 汇编
http://www.ibm.com/developerworks/cn/linux/hardware/ppc/assembly/index.html
[4] 用于 Power 体系结构的汇编语言
http://www.ibm.com/developerworks/cn/linux/l-powasm1.html
[5] Linux Assembly HOWTO
http://mirror.lzu.edu.cn/tldp/HOWTO/Assembly-HOWTO/
[6] Linux 中 x86 的内联汇编
http://www.ibm.com/developerworks/cn/linux/sdk/assemble/inline/index.html
[7] Linux Assembly Language Programming
http://mirror.lzu.edu.cn/doc/incoming/ebooks/linux-unix/Linux_EN_Original_Booksubuntu

 

 

 

三、汇编

       开篇:这里实际上仍是翻译过程,只不过把做为中间结果的汇编代码翻译成了机器代码,即目标代码,不过它还不能够运行。若是要产生这一中间结果,可用gcc的-c选项,固然,也可经过as命令_汇编_汇编语言源文件来产生。

       汇编是把汇编语言翻译成目标代码的过程,在学习汇编语言开发时,你们应该比较熟悉nasm汇编工具(支持Intel格式的汇编语言)了,不过这里主要用 as汇编工具来汇编AT&T格式的汇编语言,由于gcc产生的中间代码就是AT&T格式的。下面来演示分别经过gcc的-c选项和as来产生目标代码。vim

Quote:

$ file hello.s
hello.s: ASCII assembler program text
$ gcc -c hello.s        #用gcc把汇编语言编译成目标代码
$ file hello.o            #file命令能够用来查看文件的类型,这个目标代码是可重定位的(relocatable),须要经过ld进行进一步的连接成可执行程序(executable)和共享库(shared)
hello.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
$ as -o hello.o hello.s        #用as把汇编语言编译成目标代码
$ file hello.o
hello.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped


    gcc和as默认产生的目标代码都是ELF格式[6]的,所以这里主要讨论ELF格式的目标代码(若是有时间再回顾一下a.out和coff格式,固然你也能够参考资料[15],本身先了解一下,并结合objcopy来转换它们,比较异同)。

       目标代码再也不是普通的文本格式,没法直接经过文本编辑器浏览,须要一些专门的工具。若是想了解更多目标代码的细节,区分relocatable(可重定位)、executable(可执行)、shared libarary(共享库)的不一样,咱们得设法了解目标代码的组织方式和相关的阅读和分析工具。下面咱们主要介绍这部份内容。
    "BFD is a package which allows applications to use the same routines to operate on object files whatever the object file format. A new object file format can be supported simply by creating a new BFD back end and adding it to the library."[24][25]。
    binutils(GNU Binary Utilities)的不少工具都采用这个库来操做目标文件,这类工具备objdump,objcopy,nm,strip等(固然,你也能够利用它。若是你深刻了解ELF格式,那么经过它来分析和编写Virus程序将会更加方便),不过另一款很是优秀的分析工具readelf并非基于这个库,因此你也应该能够直接用elf.h头文件中定义的相关结构来操做ELF文件。

    下面将经过这些辅助工具(主要是readelf和objdump,可参考本节最后列出的资料[4]),结合ELF手册[6](建议看第三篇中文版)来分析它们。

    下面大概介绍ELF文件的结构和三种不一样类型ELF文件的区别。

ELF文件的结构:

ELF Header(ELF文件头)
Porgram Headers Table(程序头表,实际上叫段表好一些,用于描述可执行文件和可共享库)
Section 1
Section 2   
Section 3
...
Section Headers Table(节区头部表,用于连接可重定位文件成可执行文件或共享库)

       对于可重定位文件,程序头是可选的,而对于可执行文件和共享库文件(动态链接库),节区表则是可选的。这里的可选是指没有也能够。能够分别经过 readelf文件的-h,-l和-S参数查看ELF文件头(ELF Header)、程序头部表(Program Headers Table,段表)和节区表(Section Headers Table)。

      文件头说明了文件的类型,大小,运行平台,节区数目等。先来经过文件头看看不一样ELF的类型。为了说明问题,先来几段代码吧。



Code:

[Ctrl+A Select All]





Code:

[Ctrl+A Select All]





Code:

[Ctrl+A Select All]



    下面经过这几段代码来演示经过readelf -h参数查看ELF的不一样类型。期间将演示如何建立动态链接库(便可共享文件)、静态链接库,并比较它们的异同。

Quote:

$ gcc -c myprintf.c test.c        #编译产生两个目标文件myprintf.o和test.o,它们都是可重定位文件(REL)
$ readelf -h test.o | grep Type   
  Type:                              REL (Relocatable file)
$ readelf -h myprintf.o | grep Type
  Type:                              REL (Relocatable file)
$ gcc -o test myprintf.o test.o    #根据目标代码链接产生可执行文件,这里的文件类型是可执行的(EXEC)
$ readelf -h test | grep Type
  Type:                              EXEC (Executable file)
$ ar rcsv libmyprintf.a myprintf.o    #用ar命令建立一个静态链接库,静态链接库也是可重定位文件(REL)
$ readelf -h libmyprintf.a | grep Type    #所以,使用静态链接库和可重定位文件同样,它们之间惟一不
                                        #同是前者能够是多个可重定位文件的“集合”。
  Type:                              REL (Relocatable file)
$ gcc -o test test.o -llib -L./        #能够直接链接进去,也可使用-l参数,-L指定库的搜索路径
$ gcc -Wall myprintf.o -shared -Wl,-soname,libmyprintf.so.0 -o libmyprintf.so.0.0
                                    #编译产生动态连接库,并支持major和minor版本号,动态连接库类型为DYN
$ ln -sf libmyprintf.so.0.0 libmyprintf.so.0
$ ln -sf libmyprintf.so.0 libmyprintf.so
$ readelf -h libmyprintf.so | grep Type
  Type:                              DYN (Shared object file)
$ gcc -o test test.o -llib -L./        #编译时和静态链接库相似,可是执行时须要指定动态链接库的搜索路径
$ LD_LIBRARY_PATH=./ ./test            #LD_LIBRARY_PATH为动态连接库的搜索路径
$ gcc -static -o test test.o -llib -L./    #在不指定static时会优先使用动态连接库,指定时则阻止使用动态链接库
                                        #这个时候会把全部静态链接库文件加入到可执行文件中,使得执行文件很大
                                        #并且加载到内存之后会浪费内存空间,所以不建议这么作


    通过上面的演示基本能够看出它们之间的不一样。可重定位文件自己不能够运行,仅仅是做为可执行文件、静态链接库(也是可重定位文件)、动态链接库的 “组件”。静态链接库和动态链接库自己也不能够执行,做为可执行文件的“组件”,它们二者也不一样,前者也是可重定位文件(只不过多是多个可重定位文件的集合),而且在链接时加入到可执行文件中去;而动态链接库在链接时,库文件自己并无添加到可执行文件中,只是在可执行文件中加入了该库的名字等信息,以便在可执行文件运行过程当中引用库中的函数时由动态链接器去查找相关函数的地址,并调用它们。从这个意义上说,动态链接库自己也具备可重定位的特征,含有可重定位的信息。对于什么是重定位?如何进行静态符号和动态符号的重定位,咱们将在连接部分和《动态符号连接的细节》一节介绍。

    下面来看看ELF文件的主体内容,节区(Section)。ELF文件具备很大的灵活性,它经过文件头组织整个文件的整体结构,经过节区表 (Section Headers Table)和程序头(Program Headers Table或者叫段表)来分别描述可重定位文件和可执行文件。但无论是哪一种类型,它们都须要它们的主体,即各类节区。在可重定位文件中,节区表描述的就是各类节区自己;而在可执行文件中,程序头描述的是由各个节区组成的段(Segment),以便程序运行时动态装载器知道如何对它们进行内存映像,从而方便程序加载和运行。
    下面先来看看一些常见的节区,而关于这些节区(section)如何经过重定位构成成不一样的段(Segments),以及有哪些常规的段,咱们将在连接部分进一步介绍。

    能够经过readelf的-S参数查看ELF的节区。(建议一边操做一边看文档,以便加深对ELF文件结构的理解)先来看看可重定位文件的节区信息,经过节区表来查看:

Quote:

$ gcc -c myprintf.c            #默认编译好myprintf.c,将产生一个可重定位的文件myprintf.o
$ readelf -S myprintf.o        #经过查看myprintf.o的节区表查看节区信息
There are 11 section headers, starting at offset 0xc0:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        00000000 000034 000018 00  AX  0   0  4
  [ 2] .rel.text         REL             00000000 000334 000010 08      9   1  4
  [ 3] .data             PROGBITS        00000000 00004c 000000 00  WA  0   0  4
  [ 4] .bss              NOBITS          00000000 00004c 000000 00  WA  0   0  4
  [ 5] .rodata           PROGBITS        00000000 00004c 00000e 00   A  0   0  1
  [ 6] .comment          PROGBITS        00000000 00005a 000012 00      0   0  1
  [ 7] .note.GNU-stack   PROGBITS        00000000 00006c 000000 00      0   0  1
  [ 8] .shstrtab         STRTAB          00000000 00006c 000051 00      0   0  1
  [ 9] .symtab           SYMTAB          00000000 000278 0000a0 10     10   8  4
  [10] .strtab           STRTAB          00000000 000318 00001a 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)
$ objdump -d -j .text   myprintf.o      #这里是程序指令部分,用objdump的-d选项能够看到反编译的结果,
                                                                 #-j指定须要查看的节区
myprintf.o:     file format elf32-i386

Disassembly of section .text:

00000000 <myprintf>:
   0:   55                      push   %ebp
   1:   89 e5                   mov    %esp,%ebp
   3:   83 ec 08                sub    $0x8,%esp
   6:   83 ec 0c                sub    $0xc,%esp
   9:   68 00 00 00 00          push   $0x0
   e:   e8 fc ff ff ff          call   f <myprintf+0xf>
  13:   83 c4 10                add    $0x10,%esp
  16:   c9                      leave
  17:   c3                      ret
$ readelf -r myprintf.o                         #用-r选项能够看到有关重定位的信息,这里有两部分须要重定位

Relocation section '.rel.text' at offset 0x334 contains 2 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
0000000a  00000501 R_386_32          00000000   .rodata
0000000f  00000902 R_386_PC32        00000000   puts
$ readelf -x .rodata myprintf.o         #.rodata节区包含只读数据,即咱们要打印的hello, world!.

Hex dump of section '.rodata':
  0x00000000 68656c6c 6f2c2077 6f726c64 2100     hello, world!.

$ readelf -x .data myprintf.o           #没有这个节区,.data应该包含一些初始化的数据

Section '.data' has no data to dump.
$ readelf -x .bss       mmyprintf.o             #也没有这个节区,.bss应该包含一些未初始化的数据,程序默认初始为0

Section '.bss' has no data to dump.
$ readelf -x .comment myprintf.o        #是一些注释,能够看到是是GCC的版本信息

Hex dump of section '.comment':
  0x00000000 00474343 3a202847 4e552920 342e312e .GCC: (GNU) 4.1.
  0x00000010 3200                                2.
$ readelf -x .note.GNU-stack myprintf.o #这个也没有内容

Section '.note.GNU-stack' has no data to dump.
$ readelf -x .shstrtab myprintf.o       #包括全部节区的名字

Hex dump of section '.shstrtab':
  0x00000000 002e7379 6d746162 002e7374 72746162 ..symtab..strtab
  0x00000010 002e7368 73747274 6162002e 72656c2e ..shstrtab..rel.
  0x00000020 74657874 002e6461 7461002e 62737300 text..data..bss.
  0x00000030 2e726f64 61746100 2e636f6d 6d656e74 .rodata..comment
  0x00000040 002e6e6f 74652e47 4e552d73 7461636b ..note.GNU-stack
  0x00000050 00                                  .

$ readelf -symtab myprintf.o    #符号表,包括全部用到的相关符号信息,如函数名、变量名

Symbol table '.symtab' contains 10 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 00000000     0 FILE    LOCAL  DEFAULT  ABS myprintf.c
     2: 00000000     0 SECTION LOCAL  DEFAULT    1
     3: 00000000     0 SECTION LOCAL  DEFAULT    3
     4: 00000000     0 SECTION LOCAL  DEFAULT    4
     5: 00000000     0 SECTION LOCAL  DEFAULT    5
     6: 00000000     0 SECTION LOCAL  DEFAULT    7
     7: 00000000     0 SECTION LOCAL  DEFAULT    6
     8: 00000000    24 FUNC    GLOBAL DEFAULT    1 myprintf
     9: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND puts
$ readelf -x .strtab myprintf.o #字符串表,用到的字符串,包括文件名、函数名、变量名等。

Hex dump of section '.strtab':
  0x00000000 006d7970 72696e74 662e6300 6d797072 .myprintf.c.mypr
  0x00000010 696e7466 00707574 7300              intf.puts.


    从上表能够看出,对于可重定位文件,会包含这些基本节区.text, .rel.text, .data, .bss, .rodata, .comment, .note.GNU-stack, .shstrtab, .symtab和.strtab。为了进一步理解这些节区和源代码的关系,这里来看一看myprintf.c产生的汇编代码。

Quote:

$ gcc -S myprintf.c
$ cat myprintf.s
        .file   "myprintf.c"
        .section        .rodata
.LC0:
        .string "hello, world!"
        .text
.globl myprintf
        .type   myprintf, @function
myprintf:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $8, %esp
        subl    $12, %esp
        pushl   $.LC0
        call    puts
        addl    $16, %esp
        leave
        ret
        .size   myprintf, .-myprintf
        .ident  "GCC: (GNU) 4.1.2"
        .section        .note.GNU-stack,"",@progbits


    是否是能够从中看出可重定位文件中的那些节区和汇编语言代码之间的关系?在上面的可重定位文件,能够看到有一个可重定位的节区,即. rel.text,它标记了两个须要重定位的项,.rodata和puts。这个节区将告诉编译器这两个信息在连接或者动态连接的过程当中须要重定位,具体如何重定位?将根据重定位项的类型,好比上面的R_386_32和R_386_PC32(关于这些类型的更多细节,请查看ELF手册[6])。

    到这里,对可重定位文件应该有了一个基本的了解,下面将介绍什么是可重定位,可重定位文件究竟是如何被连接生成可执行文件和动态链接库的,这个过程除了进行了一些符号的重定位外,还进行了哪些工做呢?

本节参考资料:

[1] 了解编译程序的过程
http://9iyou.com/Program_Data/linuxunix-3125.html
http://www.host01.com/article/server/00070002/0621409075078127.htm
[2] C track: compiling C programs.
http://www.cs.caltech.edu/courses/cs11/material/c/mike/misc/compiling_c.html
[3] Dissecting shared libraries
http://www.ibm.com/developerworks/linux/library/l-shlibs.html

四、连接

    开篇:重定位是将符号引用与符号定义进行连接的过程。所以连接是处理可重定位文件,把它们的各类符号引用和符号定义转换为可执行文件中的合适信息(通常是虚拟内存地址)的过程。连接又分为静态连接和动态连接,前者是程序开发阶段程序员用ld(gcc实际上在后台调用了ld)静态连接器手动连接的过程,而动态连接则是程序运行期间系统调用动态连接器(ld-linux.so)自动连接的过程。好比,若是连接到可执行文件中的是静态链接库libmyprintf.a,那么. rodata节区在连接后须要被重定位到一个绝对的虚拟内存地址,以便程序运行时可以正确访问该节区中的字符串信息。而对于puts,由于它是动态链接库libc.so中定义的函数,因此会在程序运行时经过动态符号连接找出puts函数在内存中的地址,以便程序调用该函数。在这里主要讨论静态连接过程,动态连接过程见《动态符号连接的细节》。

          静态连接过程主要是把可重定位文件依次读入,分析各个文件的文件头,进而依次读入各个文件的节区,并计算各个节区的虚拟内存位置对一些须要重定位的符号进行处理,设定它们的虚拟内存地址等,并最终产生一个可执行文件或者是动态连接库。这个连接过程是经过ld来完成的,ld在连接时使用了一个连接脚本(linker script),该连接脚本处理连接的具体细节。因为静态符号连接过程很是复杂,特别是计算符号地址的过程,考虑到时间关系,相关细节请参考ELF手册[6]。这里主要介绍可重定位文件中的节区(节区表描述的)和可执行文件中段(程序头描述的)的对应关系以及gcc编译时采用的一些默认连接选项。

    下面先来看看可执行文件的节区信息,经过程序头(段表)来查看:

Quote:

$ readelf -S test.o                        #为了比较,先把test.o的节区表也列出
There are 10 section headers, starting at offset 0xb4:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        00000000 000034 000024 00  AX  0   0  4
  [ 2] .rel.text         REL             00000000 0002ec 000008 08      8   1  4
  [ 3] .data             PROGBITS        00000000 000058 000000 00  WA  0   0  4
  [ 4] .bss              NOBITS          00000000 000058 000000 00  WA  0   0  4
  [ 5] .comment          PROGBITS        00000000 000058 000012 00      0   0  1
  [ 6] .note.GNU-stack   PROGBITS        00000000 00006a 000000 00      0   0  1
  [ 7] .shstrtab         STRTAB          00000000 00006a 000049 00      0   0  1
  [ 8] .symtab           SYMTAB          00000000 000244 000090 10      9   7  4
  [ 9] .strtab           STRTAB          00000000 0002d4 000016 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)
$ gcc -o test test.o libmyprintf.o
$ readelf -l test        #咱们发现,test和test.o,libmyprintf.o相比,多了不少节区,如.interp和.init等

Elf file type is EXEC (Executable file)
Entry point 0x80482b0
There are 7 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4
  INTERP         0x000114 0x08048114 0x08048114 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD           0x000000 0x08048000 0x08048000 0x0047c 0x0047c R E 0x1000
  LOAD           0x00047c 0x0804947c 0x0804947c 0x00104 0x00108 RW  0x1000
  DYNAMIC        0x000490 0x08049490 0x08049490 0x000c8 0x000c8 RW  0x4
  NOTE           0x000128 0x08048128 0x08048128 0x00020 0x00020 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4

 Section to Segment mapping:
  Segment Sections...
   00    
   01     .interp
   02     .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame
   03     .ctors .dtors .jcr .dynamic .got .got.plt .data .bss
   04     .dynamic
   05     .note.ABI-tag
   06    


    上表给出了可执行文件的以下几个段(segment),

PHDR: 给出了程序表自身的大小和位置,不能出现一次以上。
INTERP: 由于程序中调用了puts(在动态连接库中定义),使用了动态链接库,所以须要动态装载器/连接器(ld-linux.so)
LOAD: 包括程序的指令,.text等节区都映射在该段,只读(R)
LOAD: 包括程序的数据,.data, .bss等节区都映射在该段,可读写(RW)
DYNAMIC: 动态连接相关的信息,好比包含有引用的动态链接库名字等信息
NOTE: 给出一些附加信息的位置和大小
GNU_STACK: 这里为空,应该是和GNU相关的一些信息

    这里的段可能包括以前的一个或者多个节区,也就是说通过连接以后原来的节区被重排了,并映射到了不一样的段,这些段将告诉系统应该如何把它加载到内存中。

    从上表中,经过比较可执行文件(test)中拥有的节区和可重定位文件(test.o和myprintf.o)中拥有的节区后发现,连接以后多了一些以前没有的节区,这些新的节区来自哪里?它们的做用是什么呢?先来经过gcc的-v参数看看它的后台连接过程。

Quote:

$ gcc -v -o test test.o myprintf.o    #把可重定位文件连接成可执行文件
Reading specs from /usr/lib/gcc/i486-slackware-linux/4.1.2/specs
Target: i486-slackware-linux
Configured with: ../gcc-4.1.2/configure --prefix=/usr --enable-shared --enable-languages=ada,c,c++,fortran,java,objc --enable-threads=posix --enable-__cxa_atexit --disable-checking --with-gnu-ld --verbose --with-arch=i486 --target=i486-slackware-linux --host=i486-slackware-linux
Thread model: posix
gcc version 4.1.2
 /usr/libexec/gcc/i486-slackware-linux/4.1.2/collect2 --eh-frame-hdr -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test /usr/lib/gcc/i486-slackware-linux/4.1.2/../../../crt1.o /usr/lib/gcc/i486-slackware-linux/4.1.2/../../../crti.o /usr/lib/gcc/i486-slackware-linux/4.1.2/crtbegin.o -L/usr/lib/gcc/i486-slackware-linux/4.1.2 -L/usr/lib/gcc/i486-slackware-linux/4.1.2 -L/usr/lib/gcc/i486-slackware-linux/4.1.2/../../../../i486-slackware-linux/lib -L/usr/lib/gcc/i486-slackware-linux/4.1.2/../../.. test.o myprintf.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/i486-slackware-linux/4.1.2/crtend.o /usr/lib/gcc/i486-slackware-linux/4.1.2/../../../crtn.o


    从上边的演示看出,gcc在链接了咱们本身的目标文件test.o和myprintf.o以外,还链接了crt1.o,crtbegin.o等额外的目标文件,难道那些新的节区就来自这些文件?
    另外gcc在进行了相关配置(./configure)后,调用了collect2,却并无调用ld,经过查找gcc文档中和collect2相关的部分发现collect2在后台实际上仍是去寻找ld命令的。为了理解gcc默认链接的后台细节,这里直接把collect2替换成ld,并把一些路径换成绝对路径或者简化,获得以下的ld命令以及执行的效果。

Quote:

$ ld --eh-frame-hdr \
-m elf_i386 \
-dynamic-linker /lib/ld-linux.so.2 \
-o test \
/usr/lib/crt1.o /usr/lib/crti.o /usr/lib/gcc/i486-slackware-linux/4.1.2/crtbegin.o \
test.o myprintf.o \
-L/usr/lib/gcc/i486-slackware-linux/4.1.2 -L/usr/i486-slackware-linux/lib -L/usr/lib/ -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed \
/usr/lib/gcc/i486-slackware-linux/4.1.2/crtend.o /usr/lib/crtn.o
$ ./test
hello, world!


不出咱们所料,它完美的运行了。下面经过ld的手册(man ld)来分析一下这几个参数。

--eh-frame-hdr

要求建立一个.eh_frame_hdr节区(貌似目标文件test中并无这个节区,因此不关心它)。

 

 

Quote:

$ ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test /usr/lib/crt1.o /usr/lib/crti.o test.o myprintf.o -L/usr/lib -lc /usr/lib/crtn.o    #后面发现不用连接libgcc,也不用--eh-frame-hdr参数
$ readelf -l test

Elf file type is EXEC (Executable file)
Entry point 0x80482b0
There are 7 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4
  INTERP         0x000114 0x08048114 0x08048114 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD           0x000000 0x08048000 0x08048000 0x003ea 0x003ea R E 0x1000
  LOAD           0x0003ec 0x080493ec 0x080493ec 0x000e8 0x000e8 RW  0x1000
  DYNAMIC        0x0003ec 0x080493ec 0x080493ec 0x000c8 0x000c8 RW  0x4
  NOTE           0x000128 0x08048128 0x08048128 0x00020 0x00020 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4

 Section to Segment mapping:
  Segment Sections...
   00    
   01     .interp
   02     .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata
   03     .dynamic .got .got.plt .data
   04     .dynamic
   05     .note.ABI-tag
   06    
$ ./test
hello, world!

 

 

Quote:

$ ld  -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test /usr/lib/crt1.o test.o myprintf.o -L/usr/lib/ -lc
/usr/lib/libc_nonshared.a(elf-init.oS): In function `__libc_csu_init':
(.text+0x25): undefined reference to `_init'

 

 

Quote:

$ readelf -s /usr/lib/crt1.o | grep __libc_csu_init
    18: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND __libc_csu_init
$ readelf -s /usr/lib/crti.o | grep _init
    17: 00000000     0 FUNC    GLOBAL DEFAULT    5 _init

 

 

Quote:

$ ld  -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test test.o myprintf.o -L/usr/lib/ -lc
ld: warning: cannot find entry symbol _start; defaulting to 00000000080481a4

 

 

Quote:

$ ./test
hello, world!
Segmentation fault

 

 

Quote:
相关文章
相关标签/搜索