Linux内核源码阅读系列(5)-链接2

可重定位目标文件实例解析

上回书说到ELF的文件格式,这里看一个真实的例子:用readelf工具窥看一下上篇提到的main.c编译而成的main.o文件。

$ gcc -O2 -g -c main.c -o main.o
$ file swap.o
swap.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped

file命令的结果显示,main.o文件是一个386结构上的“可重定位目标文件”,并且包含调试信息。这个我使用的编译命令是相应的。用readelf工具读出main.o的细节看一下,将是下面这个样子。重要的部分直接插入了注解。

ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
                                                      ^^^---小端
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
                                     ^^^---ObjectFile的类型,上篇提到的3种之一
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          900 (bytes into file)
                                     ^^^---这里指明了“节头表”的位置
  Flags:                             0x0
  Size of this header:               52 (bytes)
                                     ^^^---ELF头的大小
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           40 (bytes)
                                     ^^^---这里指明了“节头表”的大小
  Number of section headers:         23
  Section header string table index: 20

上面这个就是ELF的头部信息,头部信息主要提供了ELF文件适用的体系结构以及文件各个部分的定位信息,比如“节头表”的位置/大小等。当然还包括“节头表”中记录的数量。

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 000040 000021 00  AX  0   0 16
  [ 2] .rel.text         REL             00000000 000854 000008 08     21   1  4
  [ 3] .data             PROGBITS        00000000 000064 000008 00  WA  0   0  4
  [ 4] .bss              NOBITS          00000000 00006c 000000 00  WA  0   0  4
       ^^^---未初始化的全局变量将存放在此,但仔细看他的"Size"就知道,这个节是空的;
             事实上它通常就只是一个占位符。.bss这个名字来自于IBM 704汇编语言中的
             Block Storage Start指令的首字母缩写,鉴于它只是占位符号,可以记作
             Best Save Space。(来自于《深入理解计算机系统》)恩,随你怎么叫它。

  [ 5] .debug_abbrev     PROGBITS        00000000 00006c 000060 00      0   0  1
  [ 6] .debug_info       PROGBITS        00000000 0000cc 00006a 00      0   0  1
  [ 7] .rel.debug_info   REL             00000000 00085c 000060 08     21   6  4
  [ 8] .debug_line       PROGBITS        00000000 000136 000037 00      0   0  1
  [ 9] .rel.debug_line   REL             00000000 0008bc 000008 08     21   8  4
  [10] .debug_frame      PROGBITS        00000000 000170 000050 00      0   0  4
  [11] .rel.debug_frame  REL             00000000 0008c4 000010 08     21  10  4
  [12] .debug_loc        PROGBITS        00000000 0001c0 000043 00      0   0  1
  [13] .debug_pubnames   PROGBITS        00000000 000203 000023 00      0   0  1
  [14] .rel.debug_pubnam REL             00000000 0008d4 000008 08     21  13  4
  [15] .debug_aranges    PROGBITS        00000000 000226 000020 00      0   0  1
  [16] .rel.debug_arange REL             00000000 0008dc 000010 08     21  15  4
  [17] .debug_str        PROGBITS        00000000 000246 00004c 01  MS  0   0  1
  [18] .comment          PROGBITS        00000000 000292 00002a 00      0   0  1
  [19] .note.GNU-stack   PROGBITS        00000000 0002bc 000000 00      0   0  1
       ^^^---上面是一堆调试用的二进制内容。忽略之没有什么大碍。

  [20] .shstrtab         STRTAB          00000000 0002bc 0000c5 00      0   0  1
  [21] .symtab           SYMTAB          00000000 00071c 000120 10     22  15  4
       ^^^---符号表。这位神仙是链接过程处理的重点。

  [22] .strtab           STRTAB          00000000 00083c 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)

There are no section groups in this file.

There are no program headers in this file.
^^^----“可重定位目标文件”并不包含program header table,这个表用于将目标文件
       映射到虚拟存储器,后文详述。
Relocation section '.rel.text' at offset 0x854 contains 1 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000012  00001002 R_386_PC32        00000000   swap
                   ^^^---符号的重定位类型,一共有11种,现在遇到的这个是最重要的两种之一的
                         “与PC相关的地址引用”

Relocation section '.rel.debug_info' at offset 0x85c contains 12 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000006  00000501 R_386_32          00000000   .debug_abbrev
                   ^^^---符号的重定位类型,最重要之二,“32位地址引用”

0000000c  00000c01 R_386_32          00000000   .debug_str
00000011  00000c01 R_386_32          00000000   .debug_str
00000015  00000c01 R_386_32          00000000   .debug_str
00000019  00000201 R_386_32          00000000   .text
0000001d  00000201 R_386_32          00000000   .text
00000021  00000701 R_386_32          00000000   .debug_line
00000027  00000c01 R_386_32          00000000   .debug_str
00000031  00000201 R_386_32          00000000   .text
00000035  00000201 R_386_32          00000000   .text
00000039  00000901 R_386_32          00000000   .debug_loc
00000065  00001101 R_386_32          00000000   buf

Relocation section '.rel.debug_line' at offset 0x8bc contains 1 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
0000002a  00000201 R_386_32          00000000   .text

Relocation section '.rel.debug_frame' at offset 0x8c4 contains 2 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000018  00000801 R_386_32          00000000   .debug_frame
0000001c  00000201 R_386_32          00000000   .text

Relocation section '.rel.debug_pubnames' at offset 0x8d4 contains 1 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000006  00000601 R_386_32          00000000   .debug_info

Relocation section '.rel.debug_aranges' at offset 0x8dc contains 2 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000006  00000601 R_386_32          00000000   .debug_info
00000010  00000201 R_386_32          00000000   .text

There are no unwind sections in this file.

这一段描述了需要重定位的符号或者代码总览。其中每一个记录都描述了需要重定位的对象存在于哪一节,以及它相对于节开始位置的偏移量。

Symbol table '.symtab' contains 18 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 00000000     0 FILE    LOCAL  DEFAULT  ABS main.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    6
     7: 00000000     0 SECTION LOCAL  DEFAULT    8
     8: 00000000     0 SECTION LOCAL  DEFAULT   10
     9: 00000000     0 SECTION LOCAL  DEFAULT   12
    10: 00000000     0 SECTION LOCAL  DEFAULT   13
    11: 00000000     0 SECTION LOCAL  DEFAULT   15
    12: 00000000     0 SECTION LOCAL  DEFAULT   17
    13: 00000000     0 SECTION LOCAL  DEFAULT   19
    14: 00000000     0 SECTION LOCAL  DEFAULT   18
    ^^^---符号表的前14项无须特别关心,因为他们都是编译器自己加的默认值或者调试信息。

    15: 00000000    33 FUNC    GLOBAL DEFAULT    1 main
    16: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND swap
    17: 00000000     8 OBJECT  GLOBAL DEFAULT    3 buf
    ^^^---在main.o文件中真正存在的符号。其中,第一列描述了符号的地址相对于“节”开始的位置的偏移量。
          第二列说明的是这个对象的大小,第三列是对象类型,Ndx指明了对象存在在哪个节中,其中"1"是
          .text节,"3"是.data节。如果是"UND",它指的是没有在这个ObjectFile中定义的符号,是需
          要重定位的对象。

No version information found in this file.

谢谢各位看官的耐心,这玩意看一遍赶紧忘了吧,如果记住就是把大脑当硬盘用了。之所以细节到这个地步,只为了说明一个问题“万事万物都是有原因的”,只要仔细一点都能弄清楚事情到底怎么了,而这种仔细和耐心正是现在国内很多程序员缺少的。我参加过的与中国工程师打交道的软件项目大都让我感慨万千,中国人做事的态度和日本人做事的态度简直没有办法相提并论,不知道什么原因让很多程序员都浮躁的烧到不行……最近参加了一些中文的邮件列表,发现这个风气尤为盛行,难道真的是民族性格问题?话又说回来,同样的工作,日本工程师拿着不止2倍于中国工程师的工资,好歹做事情的态度也应该对得起这份钱。

符号和符号表

上面提到的“符号”,就是“链接”阶段需要解决的重点问题的作业对象。链接需要解决被链接在一起的各个模块之间的符号和代码之间的联系,并重定位这些信息,以便生成的机器代码能够正确的跳转,或者正确的引用到某个变量的值。从链接器的角度看,符号可以分为三类 1.在本模块中定义,被其他模块引用的符号,这可能包括非静态(static)函数和非静态变量; 2.在其他模块中定义,本模块引用的符号,这种符号被称为外部符号(external symbol); 3.在本模块中定义并只在本模块中引用的符号,这种符号成为本地符号(local symbol),可能主要包含static函数和static全局变量。值得一提的是,本地符号并不等于程序的本地变量,因为众所周知,本地程序变量是在运行时栈中管理的,他们的生存周期很短,通过pop和push就能瞬间产生和消灭,无需符号表管理。

链接器的符号解析规则

本地符号的定义在链接过程中不会有大问题,每个本地符号都有唯一的定义;包括Java或者C++中的重载函数等,编译器会运用规则位有同名不同参数的函数各自产生一个唯一的“内藏”函数名。

全局符号比较麻烦,首先是要按照强弱分类,1.函数,已经初始化的全局变量是“强符号”;2.未初始化的全局变量是“弱符号”。然后是取舍规则,1.同名两强必出错,链接器报错; 2.同名强弱,肯定是选择强者; 3.同名两弱就随便取一个。注意啦,如果这个时候你还没有意识到明明规则存在的重要性的话,真是后知后觉了。此外,还有一个问题,就是为什么链接大批动态链接库时会有莫名其妙的错误?原因就是这些“潜规则”导致的错误了。比如,下面的例子:

文中有注释,c&p注意。

/* foo2.c */

void bar(void);

int x = 12345;
        ^^^---已经初始化的全局变量,“强”符号

int main()
{
	bar();
	printf("x = %dn", x);
	return 0;
}
/* bar2.c */
int x;
    ^^^---未初始化的全局变量,“弱”符号

void bar()
{
	x = 54321;
}

这个程序被链接的并运行的话,其结果让程序员大跌眼睛的,x的输出居然还是12345。而实际现实中的问题比这个不知道要复杂多少倍,往往非常难于发现和排除,所以好的命名习惯真的是在体现一个程序员和一个软件开发团队的素养,而不是为了符合CodeStyle做的面子工程。如果你使用gcc作编译器的话,开启-warn-common选项,将帮助你查找重定义的错误。微软的编译器肯定也有这个选项,但是我有n多年都没给Windows写过程序了,实在是不知道。

未完待续。
—-
文中例子来自《深入理解计算机系统》

发表评论