注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

还东国的博客

行之苟有恒,久久自芬芳

 
 
 

日志

 
 

(转载)GCC 编译的背后 (预处理和编、汇编和链接) 续2  

2013-08-14 21:25:23|  分类: LINUX编程 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |

4、链接

 

    开篇:重定位是将符号引用与符号定义进行链接的过程。因此链接是处理可重定位文件,把它们的各种符号引用和符号定义转换为可执行文件中的合适信息(一般是虚拟内存地址)的过程。链接又分为静态链接和动态链接,前者是程序开发阶段程序员用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        #我们发现,testtest.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.omyprintf.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.omyprintf.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中并没有这个节区,所以不关心它)

?  -m elf_i386

 

这里指定不同平台上的链接脚本,可以通过--verbose命令查看脚本的具体内容,如ld -m elf_i386 --verbose,它实际上被存放在一个文件中(/usr/lib/ldscripts目录下),你可以去修改这个脚本,具体如何做?请参考ld的手册。在后面我们将简要提到链接脚本中是如何预定义变量的,以及这些预定义变量如何在我们的程序中使用。需要提到的是,如果不是交叉编译,那么无须指定该选项。

?  -dynamic-linker /lib/ld-linux.so.2

 

指定动态装载器/链接器,即程序中的INTERP段中的内容。动态装载器/连接器负责连接有可共享库的可执行文件的装载和动态符号连接。

?  -o test

 

指定输出文件,即可执行文件名的名字

?  /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/gcc/i486-slackware-linux/4.1.2/crtbegin.o

 

链接到test文件开头的一些内容,这里实际上就包含了.init等节区。.init节区包含一些可执行代码,在main函数之前被调用,以便进行一些初始化操作,在C++中完成构造函数功能,更多细节请参考资料[9]

?  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

 

链接libgcc库和libc库,后者定义有我们需要的puts函数

?  /usr/lib/gcc/i486-slackware-linux/4.1.2/crtend.o /usr/lib/crtn.o

 

链接到test文件末尾的一些内容,这里实际上包含了.fini等节区。.fini节区包含了一些可执行代码,在程序退出时被执行,作一些清理工作,在C++中完成析构造函数功能。我们往往可以通过atexit来注册那些需要在程序退出时才执行的函数。

 

    对于crtbegin.ocrtend.o这两个文件,貌似完全是用来支持C++的构造和析构工作的[9],所以可以不链接到我们的可执行文件中,链接时把它们去掉看看,

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!

 

 

    完全可以工作,而且发现.ctors(保存着程序中全局构造函数的指针数组), .dtors(保存着程序中全局析构函数的指针数组),.jcr(未知),.eh_frame节区都没有了,所以crtbegin.ocrtend.o应该包含了这些节区。

    而对于另外两个文件crti.ocrtn.o,通过readelf -S查看后发现它们都有.init.fini节区,如果我们不需要让程序进行一些初始化和清理工作呢?是不是就可以不链接这个两个文件?试试看。

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'

 

 

貌似不行,竟然有人调用了__libc_csu_init函数,而这个函数引用了_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

 

 

    竟然是crt1.o调用了__libc_csu_init函数,而该函数却引用了我们没有链接的crti.o文件中定义的_init符号。这样的话不链接crti.ocrtn.o文件就不成了罗?不对吧,要不干脆不用crt1.o算了,看看gcc额外连接进去的最后一个文件crt1.o到底干了个啥子?

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

 

 

    这样却说没有找到入口符号_start,难道crt1.o中定义了这个符号?不过它给默认设置了一个地址,只是个警告,说明test已经生成,不管怎样先运行看看再说。

Quote:

$ ./test

hello, world!

Segmentation fault

 

 

    貌似程序运行完了,不过结束时冒出个段错误?可能是程序结束时有问题,用gdb调试看看:

Quote:

$ gcc -g -c test.c myprintf.c    #产生目标代码非交叉编译,不指定-m也可以链接成功,所以下面可以去掉-m参数

$ ld -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 00000000080481d8   

$ ./test

hello, world!

Segmentation fault

$ gdb ./test

...

(gdb) l

1       #include "test.h"

2

3       int main()

4       {

5               myprintf();

6               return 0;

7       }

(gdb) break 7            #在程序的末尾设置一个断点

Breakpoint 1 at 0x80481bf: file test.c, line 7.

(gdb) r                    #程序都快结束了都没问题,怎么会到最后出个问题呢?

Starting program: /mnt/hda8/Temp/c/program/test

hello, world!

 

Breakpoint 1, main () at test.c:7

7       }

(gdb) n                    #单步执行看看,怎么下面一条指令是0x00000001,肯定是程序退出以后出了问题

0x00000001 in ?? ()

(gdb) n                    #诶,当然找不到边了,都跑到0x00000001

Cannot find bounds of current function

(gdb) c                    #原来是这么回事,估计是return 0返回之后出问题了,看看它的汇编去。

Continuing.

 

Program received signal SIGSEGV, Segmentation fault.

0x00000001 in ?? ()

$ gcc -S test.c #产生汇编代码

$ cat test.s    #后面就这么几条指令,难不成ret返回有问题,不让它ret返回,把return改成_exit直接进入内核退出

...

        call    myprintf

        movl    $0, %eax

        addl    $4, %esp

        popl    %ecx

        popl    %ebp

        leal    -4(%ecx), %esp

        ret

...

$ vim test.c

$ cat test.c    #就把return语句修改成_exit了。

#include "test.h"

#include <unistd.h> /* _exit */

 

int main()

{

        myprintf();

        _exit(0);

}

$ gcc -g -c test.c myprintf.c

$  ld -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 00000000080481d8

$ ./test    #竟然好了,再看看汇编有什么不同

hello, world!

$ gcc -S test.c

$ cat test.s    #貌似就把ret指令替换成了_exit函数调用,直接进入内核,然内核让处理了,那为什么ret有问题呢?

...

        call    myprintf

        subl    $12, %esp

        pushl   $0

        call    _exit

...

$ gdb ./test    #把代码改回去(改成return 0;),再调试看看调用main函数返回时的下一条指令地址eip

...

(gdb) l

warning: Source file is more recent than executable.

1       #include "test.h"

2

3       int main()

4       {

5               myprintf();

6               return 0;

7       }

(gdb) break 5

Breakpoint 1 at 0x80481b5: file test.c, line 5.

(gdb) break 7

Breakpoint 2 at 0x80481bc: file test.c, line 7.

(gdb) r

Starting program: /mnt/hda8/Temp/c/program/test

 

Breakpoint 1, main () at test.c:5

5               myprintf();

(gdb) x/8x $esp    #发现0x00000001刚好是之前我们调试时看到的程序返回后的位置,即eip,说明程序在初始化的时候

                #这个eip就是错误的。为什么呢?因为我们根本没有链接进来初始化的代码,而是在编译器自己给我们

                #初始化了一个程序入口即00000000080481d8,也就是说,没有任何人调用main,main不知道返回哪里去

                #所以,我们直接让main结束时进入内核调用_exit而退出则不会有问题

0xbf929510:     0xbf92953c      0x080481a4      0x00000000      0xb7eea84f

0xbf929520:     0xbf92953c      0xbf929534      0x00000000      0x00000001 

 

 

    通过上面的演示和解释发现只要把return语句修改为_exit语句,程序即使不链接任何额外的目标代码都可以正常运行(原因是不连接那些额外的文件时相当于没有进行初始化操作,如果在程序的最后执行ret汇编指令,程序将无法获得正确的eip,从而无法进行后续的动作)。但是为什么会有“找不到 _start符号”的警告呢?通过readelf -s查看crt1.o发现里头有这个符号,并且crt1.o引用了main这个符号,是不是意味着会从_start进入main呢?是不是程序入口是 _start,而并非main呢?

 

    先来看看刚才提到的链接器的默认链接脚本(ld -m elf_386 --verbose),它告诉我们程序的入口(entry)_start,而一个可执行文件必须有一个入口地址才能运行,所以这就是说明了为什么ld一定要提示我们“_start找不到”,找不到以后就给默认设置了一个地址。

Quote:

$ ld --verbose  | grep ^ENTRY    #非交叉编译,可不用-m参数;ld默认找_start入口,并不是main哦!

ENTRY(_start)

 原来是这样,程序的入口(entry)竟然不是main函数,而是_start。那干脆把汇编里头的main给改掉算了,看行不行?

Quote:

$ cat test.c

#include "test.h"

#include <unistd.h>     /* _exit */

 

int main()

{

        myprintf();

        _exit(0);

}

$ gcc -S test.c

$ sed -i -e "s#main#_start#g" test.s    #把汇编中的main全部修改为_start,即修改程序入口为_start

$ gcc -c test.s myprintf.c

$ ld -dynamic-linker /lib/ld-linux.so.2 -o test test.o myprintf.o -L/usr/lib/ -lc    #果然没问题了 :-)

$ ./test

hello, world!

 

 

    _start竟然是真正的程序入口,那在有main的情况下呢?为什么在_start之后能够找到main呢?这个看看alert7大叔的"Before main分析"[5]吧,这里不再深入介绍。总之呢,通过修改程序的return语句为_exit(0)和修改程序的入口为_start,我们的代码不链接gcc默认链接的那些额外的文件同样可以工作得很好。并且打破了一个学习C语言以来的常识:main函数作为程序的主函数,是程序的入口,实际上则不然。

 

    再补充一点内容,在ld的链接脚本中,有一个特别的关键字PROVIDE,由这个关键字定义的符号是ld的预定义字符,我们可以在C语言函数中扩展它们后直接使用。这些特别的符号可以通过下面的方法获取,

Quote:

$ ld --verbose | grep PROVIDE | grep -v HIDDEN

  PROVIDE (__executable_start = 0x08048000); . = 0x08048000 + SIZEOF_HEADERS;

  PROVIDE (__etext = .);

  PROVIDE (_etext = .);

  PROVIDE (etext = .);

  _edata = .; PROVIDE (edata = .);

  _end = .; PROVIDE (end = .);

 

    这里面有几个我们比较关心的,第一个是程序的入口地址__executable_start,另外三个是etextedataend,分别对应程序的代码段(text)、初始化数据(data)和未初始化的数据(bss)(可以参考资料[6]man etext),如何引用这些变量呢?看看这个例子。

、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

/* predefinevalue.c */

#include <stdio.h>

extern int __executable_start, etext, edata, end;

int

main ()

{

  printf ("program entry: 0x%x \n", &__executable_start);

  printf ("etext address(text segment): 0x%x \n", &etext);

  printf ("edata address(initilized data): 0x%x \n", &edata);

  printf ("end address(uninitilized data): 0x%x \n", &end);

  return 0;

}

、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

 

    到这里,程序链接过程的一些细节都介绍得差不多了。在《动态符号链接的细节》中将主要介绍ELF文件的动态符号链接过程。

 

本节参考资料

 

[1] An beginners guide to compiling programs under Linux.

http://www.luv.asn.au/overheads/compile.html

[2] gcc manual

http://gcc.gnu.org/onlinedocs/gcc-4.2.2/gcc/

[3] A Quick Tour of Compiling, Linking, Loading, and Handling Libraries on Unix

http://efrw01.frascati.enea.it/Software/Unix/IstrFTU/cern-cnl-2001-003-25-link.html

[4] Unix 目标文件初探

http://www.ibm.com/developerworks/cn/aix/library/au-unixtools.html

[5] Before main()分析

http://www.xfocus.net/articles/200109/269.html

[6] A Process Viewing Its Own /proc/<PID>/map Information

http://www.linuxforums.org/forum/linux-kernel/51790-process-viewing-its-own-proc-pid-map-information.html

  评论这张
 
阅读(1077)| 评论(0)
推荐 转载

历史上的今天

在LOFTER的更多文章

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017