静态链接 Surager

链接器是个比编译器还古老的前辈。

静态链接

我们准备两个小程序

第一个:

/* a.c */
extern int shared;

int main()
{
	int a=100;
	swap( &a , shared );
}

第二个:

/* b.c */
int shared = 1;

void swap(int* a, int* b )
{
	*a ^= *b ^= *a ^= *b;
}

然后编译成目标文件:

gcc -c a.c b.c

这样我们就得到了两个文件a.o和b.o。模块a.c里面引用到了模块b.c里面的变量shared和函数swap。

相似段合并

早期的链接器会直接把目标文件的各个段按照次序叠加起来,这样就造成了空间浪费的问题。

于是一个更实际的方法——相似段合并,诞生了。它的思路是将所有输入文件的”.text”段合并到输出文件的”.text”段中,接着是”.data”段、”.bss”段等。

实际的空间分配策略

实际上我们在谈到空间分配时只关注于虚拟空间的分配。现在的链接器基本上采用相似段合并的策略。使用这种策略一般采用一种叫做两步链接(Two-pass Linking)的方法。

第一步是空间与地址分配。收集所有段的长度、属性和位置。收集符号定义和符号引用,统一放到一个全局变量表。

第二步是符号解析和重定位。链接的核心,尤其是重定位过程。

接下来我们把a.o和b.o连接起来。

ld a.o b.o -e main -o ab

-e main : 将main作为程序的入口,ld链接器默认程序入口为_start

-o ab : 链接的输出文件名为ab,默认为a.out

之后用objdump查看各个段的属性。

surager@LAPTOP-0H9RPTHA:~/linkers_loaders$ objdump -h a.o

a.o:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .group        00000008  00000000  00000000  00000034  2**2
                  CONTENTS, READONLY, GROUP, LINK_ONCE_DISCARD
  1 .text         0000004a  00000000  00000000  0000003c  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  2 .data         00000000  00000000  00000000  00000086  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  3 .bss          00000000  00000000  00000000  00000086  2**0
                  ALLOC
  . . .
surager@LAPTOP-0H9RPTHA:~/linkers_loaders$ objdump -h b.o

b.o:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .group        00000008  00000000  00000000  00000034  2**2
                  CONTENTS, READONLY, GROUP, LINK_ONCE_DISCARD
  1 .text         00000043  00000000  00000000  0000003c  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  2 .data         00000004  00000000  00000000  00000080  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  3 .bss          00000000  00000000  00000000  00000084  2**0
                  ALLOC
  . . .
surager@LAPTOP-0H9RPTHA:~/linkers_loaders$ objdump -h ab

ab:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00000091  08049000  08049000  00001000  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  . . .
  3 .data         00000004  0804c00c  0804c00c  0000300c  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  . . .

我们关心以上结果中的VMA(Virtual Memory Address,即虚拟地址)和Size。

在链接之前,目标文件所有段中的VMA都是0,因为虚拟地址没有分配,他们默认都是0。

目标文件、可执行文件与进程空间

(图片与内容对应有误)

当第一步完成之后,各个段在链接之后的虚拟地址就确定了。比如”.text”段起始地址为0x08049000,”.data”段起始地址为0x0804c00c。之后链接器开始计算各个符号的虚拟地址。即链接器要给每个符号加上一个偏移量,使他们能够调整到正确的虚拟地址。

符号解析与重定位

重定位

接下来来到了链接的核心部分。

我们先用objdump来查看编译器在编译a.c成指令时如何访问外部变量的。

surager@LAPTOP-0H9RPTHA:~/linkers_loaders$ objdump -d a.o

a.o:     file format elf32-i386


Disassembly of section .text:

00000000 <main>:
   0:   8d 4c 24 04             lea    0x4(%esp),%ecx
   4:   83 e4 f0                and    $0xfffffff0,%esp
   7:   ff 71 fc                pushl  -0x4(%ecx)
   a:   55                      push   %ebp
   b:   89 e5                   mov    %esp,%ebp
   d:   53                      push   %ebx
   e:   51                      push   %ecx
   f:   83 ec 10                sub    $0x10,%esp
  12:   e8 fc ff ff ff          call   13 <main+0x13>
  17:   05 01 00 00 00          add    $0x1,%eax
  1c:   c7 45 f4 64 00 00 00    movl   $0x64,-0xc(%ebp)
  23:   83 ec 08                sub    $0x8,%esp
  26:   8b 90 00 00 00 00       mov    0x0(%eax),%edx    ;8b 90 00 00 00 00 四个字节为0
  2c:   52                      push   %edx
  2d:   8d 55 f4                lea    -0xc(%ebp),%edx
  30:   52                      push   %edx
  31:   89 c3                   mov    %eax,%ebx
  33:   e8 fc ff ff ff          call   34 <main+0x34>    ;四个字节为-4
  38:   83 c4 10                add    $0x10,%esp
  3b:   b8 00 00 00 00          mov    $0x0,%eax
  40:   8d 65 f8                lea    -0x8(%ebp),%esp
  43:   59                      pop    %ecx
  44:   5b                      pop    %ebx
  45:   5d                      pop    %ebp
  46:   8d 61 fc                lea    -0x4(%ecx),%esp
  49:   c3                      ret

. . .


对shared的引用用到了一个”mov”指令,这个指令有6个字节,后面的 00 00 00 00 是shared的地址。因为编译器编译的时候shared定义在其他目标文件中,所以编译器暂时把地址0看成shared的地址。

另一个偏移为0x33的指令其实是对swap函数的调用。add指令的的地址为0x38,-4即 0x38 - 4 = 0x34 。

编译器暂时把地址部分用0x00000000和0xFFFFFFFC替代,剩下的工作留给了链接器。链接器已经收集了所有符号的信息了,所以可以根据符号对地址进行修正。

我们用objdump来查看修正之后的结果。

surager@LAPTOP-0H9RPTHA:~/linkers_loaders$ objdump -d ab

ab:     file format elf32-i386


Disassembly of section .text:

08049000 <main>:
 8049000:       8d 4c 24 04             lea    0x4(%esp),%ecx
 8049004:       83 e4 f0                and    $0xfffffff0,%esp
 8049007:       ff 71 fc                pushl  -0x4(%ecx)
 804900a:       55                      push   %ebp
 804900b:       89 e5                   mov    %esp,%ebp
 804900d:       53                      push   %ebx
 804900e:       51                      push   %ecx
 804900f:       83 ec 10                sub    $0x10,%esp
 8049012:       e8 33 00 00 00          call   804904a <__x86.get_pc_thunk.ax>
 8049017:       05 e9 2f 00 00          add    $0x2fe9,%eax
 804901c:       c7 45 f4 64 00 00 00    movl   $0x64,-0xc(%ebp)
 8049023:       83 ec 08                sub    $0x8,%esp
 8049026:       c7 c2 0c c0 04 08       mov    $0x804c00c,%edx
 804902c:       52                      push   %edx
 804902d:       8d 55 f4                lea    -0xc(%ebp),%edx
 8049030:       52                      push   %edx
 8049031:       89 c3                   mov    %eax,%ebx
 8049033:       e8 16 00 00 00          call   804904e <swap>
 8049038:       83 c4 10                add    $0x10,%esp
 804903b:       b8 00 00 00 00          mov    $0x0,%eax
 8049040:       8d 65 f8                lea    -0x8(%ebp),%esp
 8049043:       59                      pop    %ecx
 8049044:       5b                      pop    %ebx
 8049045:       5d                      pop    %ebp
 8049046:       8d 61 fc                lea    -0x4(%ecx),%esp
 8049049:       c3                      ret

0804904e <swap>:
 804904e:       55                      push   %ebp

相对于add指令来说0x8049038 + 0x16 = 0x804904e,所以swap指令填充地址为 16 00 00 00 。

重定位表

ld链接器通过重定位表保存与重定位相关的信息。重定位表往往是ELF中的一个段,所以重定位表也可以叫做重定位段。比如有”.text”有要被重定位的地方,那么必定有一个相对应的叫做”.rel.text”的段保存代码段的重定位信息。我们用objdump来查看目标文件的重定位表。

surager@LAPTOP-0H9RPTHA:~/linkers_loaders$ objdump -r a.o

a.o:     file format elf32-i386

RELOCATION RECORDS FOR [.text]:
OFFSET   TYPE              VALUE
00000013 R_386_PC32        __x86.get_pc_thunk.ax
00000018 R_386_GOTPC       _GLOBAL_OFFSET_TABLE_
00000028 R_386_GOT32X      shared
00000034 R_386_PLT32       swap

这个0x28和0x34就是”mov”指令和”call”指令的地址部分。

符号解析

我们用readelf查看a.o的符号表。

surager@LAPTOP-0H9RPTHA:~/linkers_loaders$ readelf -s a.o

Symbol table '.symtab' contains 15 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 00000000     0 FILE    LOCAL  DEFAULT  ABS a.c
     2: 00000000     0 SECTION LOCAL  DEFAULT    2
     3: 00000000     0 SECTION LOCAL  DEFAULT    4
     4: 00000000     0 SECTION LOCAL  DEFAULT    5
     5: 00000000     0 SECTION LOCAL  DEFAULT    6
     6: 00000000     0 SECTION LOCAL  DEFAULT    8
     7: 00000000     0 SECTION LOCAL  DEFAULT    9
     8: 00000000     0 SECTION LOCAL  DEFAULT    7
     9: 00000000     0 SECTION LOCAL  DEFAULT    1
    10: 00000000    74 FUNC    GLOBAL DEFAULT    2 main
    11: 00000000     0 FUNC    GLOBAL HIDDEN     6 __x86.get_pc_thunk.ax
    12: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_
    13: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND shared
    14: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND swap

UND即”undefined”未定义类型。未定义的原因即该目标文件中有关于他们的重定位项。所以链接器会屁颠屁颠来找这些UND。

指令修正方式

原因:寻址模式太™多了。

x86规定了两种基本的修正方式

修正方式

对照前面的重定位表,我们的系统是x64,emmmm。

那翻译一下其实就解决了吧。shared的修正类型是R_386_32,swap修正类型是R_386_PC32。

我们可以假设一下。

假设ab生成之后,main的虚拟地址为0x1000,swap的虚拟地址为0x2000,shared的虚拟地址为0x3000。

绝对地址修正

​ S+A: S : shared的实际地址0x3000 A : 被修正位置的值,即0x00000000

所以修正以后指令变成:

1018:c7 c2 00 30 00 00 mov $0x3000,%edx

相对地址修正

​ S+A+-P: S:swap的实际地址0x2000 A:被修正位置的值0xFFFFFFFC P:被修正的位置即0x1000+0x34

​ 所以修正之后的地址应该是0x2000 + (-4) - (0x1000 + 0x27) = 0xFD5

所以修正后指令变成:

1026:e8 d5 0f 00 00 call 0xfd5 <swap>

可以看出,绝对地址修正之后的地址是该符号的实际地址,而相对地址修正之后的地址为地址差。

COMMON块

由于弱符号和强符号的机制,编译器和链接器都支持COMMON块的机制来处理空间问题。

静态库链接

一种语言的开发环境往往会附带有语言库,这些库就是对操作系统的API的包装。比如在Linux下,printf是一个”write”的系统调用,在windows下,它是”WriteConsole”系统API。

由于目标文件的零散程度,人们使用”ar”压缩程序将目标文件压缩到了一起,使用”ar”工具可以查看文件中包含了哪些目标文件。

ar -t libc.a

下面假设我们用如下方法编译hello.c

gcc -c -fno-builtin hello.c

-fno-builtin:禁止函数优化选项,因为gcc会把仅有字符串输出的printf替换成puts。

-c:生成.o文件

然后用”ar”解压出printf:

ar -x libc.a

接着用ld将hello.o和printf.o链接再一起,结果是失败了。

printf.o:In function '_IO_printf':undefined reference to 'stdout'

printf.o:In function '_IO_printf':undefined reference to 'vfprintf'

很显然,printf依赖于其他文件。

实际上,人工操作一次hello.o的链接过程非常麻烦,我们用-verbose命令将整个编译链接过程打印出来:

surager@LAPTOP-0H9RPTHA:~/linkers_loaders$ gcc -static --verbose -fno-builtin hello.c
Using built-in specs.
COLLECT_GCC=/usr/bin/gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/i686-linux-gnu/9/lto-wrapper
Target: i686-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Debian 9.2.1-21' --with-bugurl=file:///usr/share/doc/gcc-9/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++,gm2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-9 --program-prefix=i686-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib=auto --enable-objc-gc=auto --enable-targets=all --enable-multiarch --disable-werror --with-arch-32=i686 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-checking=release --build=i686-linux-gnu --host=i686-linux-gnu --target=i686-linux-gnu --with-build-config=bootstrap-lto-lean --enable-link-mutex
Thread model: posix
gcc version 9.2.1 20191130 (Debian 9.2.1-21)
COLLECT_GCC_OPTIONS='-static' '-v' '-fno-builtin' '-mtune=generic' '-march=i686'

-----------------------------------------------
 /usr/lib/gcc/i686-linux-gnu/9/cc1 -quiet -v -imultiarch i386-linux-gnu hello.c -quiet -dumpbase hello.c -mtune=generic -march=i686 -auxbase hello -version -fno-builtin -fasynchronous-unwind-tables -o /tmp/ccWvkCsF.s
----------------------------------------------- 
 
GNU C17 (Debian 9.2.1-21) version 9.2.1 20191130 (i686-linux-gnu)
        compiled by GNU C version 9.2.1 20191130, GMP version 6.1.2, MPFR version 4.0.2, MPC version 1.1.0, isl version isl-0.22-GMP

GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
ignoring nonexistent directory "/usr/local/include/i386-linux-gnu"
ignoring nonexistent directory "/usr/lib/gcc/i686-linux-gnu/9/../../../../i686-linux-gnu/include"
#include "..." search starts here:
#include <...> search starts here:
 /usr/lib/gcc/i686-linux-gnu/9/include
 /usr/local/include
 /usr/lib/gcc/i686-linux-gnu/9/include-fixed
 /usr/include/i386-linux-gnu
 /usr/include
End of search list.
GNU C17 (Debian 9.2.1-21) version 9.2.1 20191130 (i686-linux-gnu)
        compiled by GNU C version 9.2.1 20191130, GMP version 6.1.2, MPFR version 4.0.2, MPC version 1.1.0, isl version isl-0.22-GMP

GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
Compiler executable checksum: 65914783ff6ceeacd00570e1b7ef81be
COLLECT_GCC_OPTIONS='-static' '-v' '-fno-builtin' '-mtune=generic' '-march=i686'

----------------------------------------------
 as -v --32 -o /tmp/cc7lBA5Q.o /tmp/ccWvkCsF.s
----------------------------------------------
 
GNU assembler version 2.33.1 (i686-linux-gnu) using BFD version (GNU Binutils for Debian) 2.33.1
COMPILER_PATH=/usr/lib/gcc/i686-linux-gnu/9/:/usr/lib/gcc/i686-linux-gnu/9/:/usr/lib/gcc/i686-linux-gnu/:/usr/lib/gcc/i686-linux-gnu/9/:/usr/lib/gcc/i686-linux-gnu/
LIBRARY_PATH=/usr/lib/gcc/i686-linux-gnu/9/:/usr/lib/gcc/i686-linux-gnu/9/../../../i386-linux-gnu/:/usr/lib/gcc/i686-linux-gnu/9/../../../../lib/:/lib/i386-linux-gnu/:/lib/../lib/:/usr/lib/i386-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/i686-linux-gnu/9/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-static' '-v' '-fno-builtin' '-mtune=generic' '-march=i686'

----------------------------------------
 /usr/lib/gcc/i686-linux-gnu/9/collect2 -plugin /usr/lib/gcc/i686-linux-gnu/9/liblto_plugin.so -plugin-opt=/usr/lib/gcc/i686-linux-gnu/9/lto-wrapper -plugin-opt=-fresolution=/tmp/cc4jYbv3.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_eh -plugin-opt=-pass-through=-lc --build-id -m elf_i386 --hash-style=gnu --as-needed -static /usr/lib/gcc/i686-linux-gnu/9/../../../i386-linux-gnu/crt1.o /usr/lib/gcc/i686-linux-gnu/9/../../../i386-linux-gnu/crti.o /usr/lib/gcc/i686-linux-gnu/9/crtbeginT.o -L/usr/lib/gcc/i686-linux-gnu/9 -L/usr/lib/gcc/i686-linux-gnu/9/../../../i386-linux-gnu -L/usr/lib/gcc/i686-linux-gnu/9/../../../../lib -L/lib/i386-linux-gnu -L/lib/../lib -L/usr/lib/i386-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/i686-linux-gnu/9/../../.. /tmp/cc7lBA5Q.o --start-group -lgcc -lgcc_eh -lc --end-group /usr/lib/gcc/i686-linux-gnu/9/crtend.o /usr/lib/gcc/i686-linux-gnu/9/../../../i386-linux-gnu/crtn.o
COLLECT_GCC_OPTIONS='-static' '-v' '-fno-builtin' '-mtune=generic' '-march=i686'
----------------------------------------

第一步:调用cc1程序,把hello.c编译成/tmp/ccWvkCsF.s

第二步:调用as程序,把ccWvkCsF.s汇编成cc7lBA5Q.o,这个cc7lBA5Q.o实际上就是前面的hello.o

第三步:调用collect2程序完成链接。

collect2可以看成是ld的一个包装,在这里可以把他简单地看成ld链接器。最后一步中至少有这些目标文件和库被链接入最终的可执行文件:

  • crt1.o

  • crti.o

  • crtbeginT.o

  • crtend.o

  • crtn.o

  • libgcc.a

  • libgcc_eh.a

  • libc.a

链接控制过程

由于整个链接过程有很多内容需要确定,链接器提供了三种控制链接过程的方法。

  1. 使用命令行指定参数。-o、-e就属于这类。
  2. 存放在目标文件中。
  3. 使用链接控制脚本。使用参数-T可以指定使用脚本。

最”小”的程序

hello.c盲打(蒙上眼睛)也能正常运行(指鼠标精确地定位到运行的位置

但是经典的helloworld

  1. 用到了printf,c语言库与hello.o链接才能生成可执行文件。
  2. helloworld有main函数,程序的入口在_start处。
  3. main的指令会生成.text段,字符串”Hello world!\n”会被存放在.data段或者.rodata段,还有各种c语言库生成的段,为了方便,我们把很多段放在同一个段中,即tinytext段。

TinyHelloWorld.c的源代码如下:

char* str = "Hello world!\n";

void print()
{
        asm("movl $13,%%edx \n\t"
            "movl %0,%%ecx \n\t"
            "movl $0,%%ebx \n\t"
            "movl $4,%%eax \n\t"
            "int $0x80     \n\t"
            ::"r"(str):"edx","ecx","ebx");
}

void exit()
{
        asm("movl $42,%ebx \n\t"
            "movl $1,%eax \n\t"
            "int $0x80 \n\t");
}

void nomain()
{
        print();
        exit();
}


此程序使用了gcc内嵌汇编,系统调用通过0x80中断实现,其中eax为调用号,ebx、ecx、edx等通用寄存器用来传递参数。例如WRITE调用的原型是

int write(int filedesc, char* buffer, int size)

  • WRITE调用号为4,则执行movl $4,%%eax
  • filedesc使用ebx传参,在默认终端(stdout)输出,所以movl $0,%%ebx
  • buffer表示写入的缓冲区地址,
  • size是字节数,”Hello world!\n”有13字节。

我们用普通命令行编译和链接

gcc -c -fno-builtin TinyHelloWorld.c

ld -static -e nomain -o TinyHelloWorld TinyHelloWorld.o

-e nomain 是指定以nomain为程序的入口函数。

程序可以正常运行。

surager@LAPTOP-0H9RPTHA:~/linkers_loaders$ ./TinyHelloWorld 
Hello world!

我们用objdump查看这个ELF文件

surager@LAPTOP-0H9RPTHA:~/linkers_loaders$ objdump -h TinyHelloWorld

TinyHelloWorld:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00000065  08049000  08049000  00001000  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .rodata       0000000e  0804a000  0804a000  00002000  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .eh_frame     00000090  0804a010  0804a010  00002010  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .got.plt      0000000c  0804c000  0804c000  00003000  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  4 .data         00000004  0804c00c  0804c00c  0000300c  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  5 .comment      00000026  00000000  00000000  00003010  2**0
                  CONTENTS, READONLY

接下来分析各个段

  • .text保存指令,只读
  • .rodata保存”Hello world!\n”,只读
  • .data保存str全局变量,但是我们并没有改写此变量,所以实际也是只读的
  • .comment保存编译器和系统版本信息,可以丢弃

据上分析,我们可以将三个段合并,将.comment段丢弃。

使用ld链接脚本

TinyHelloWorld.lds

ENTRY(nomain)

SECTIONS
{
    . = 0x08048000 + SIZEOF_HEADERS;
    tinytext  : {*(.text) *(.data) *(.rodata)}
    /DISCARD/  : { *(.comment)}
}
  • ENTRY(nomain)规定了程序的入口函数为nomain()。
  • SECTIONS是链接脚本的主体。
    • 第一条是赋值语句,将当前虚拟地址设置成0x08048000 + SIZEOF_HEADERS
    • 第二条语句是段转换规则,将”.text”、”.data”、”.rodata”合并到”tinytext”
    • 第三条语句把”.comment”段丢掉。

gcc -c -fno-builtin TinyHelloWorld.c

ld -static -T TinyHelloWorld.lds -e nomain -o TinyHelloWorld TinyHelloWorld.o

用objdump查看一波

surager@LAPTOP-0H9RPTHA:~/linkers_loaders$ objdump -h TinyHelloWorld

TinyHelloWorld:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 tinytext      0000006f  08048094  08048094  00000094  2**0
                  CONTENTS, ALLOC, LOAD, CODE
  1 .text.__x86.get_pc_thunk.ax 00000004  08048103  08048103  00000103  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  2 .eh_frame     00000090  08048108  08048108  00000108  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .data.rel.local 00000004  08048198  08048198  00000198  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  4 .got.plt      0000000c  0804819c  0804819c  0000019c  2**2
                  CONTENTS, ALLOC, LOAD, DATA

合并成功。