链接器之Map文件与符号表
一、map、全局符号及静态符号
一般的大型工程都会在生成可执行文件的同时让链接器生成一个map文件,从而大致查看一下可执行文件中符号的内存布局以及从哪里引入可执行文件。这个通常对于小型工程是作用不大,因为代码就那么多,随便grep一下就知道符号定义位置了。但是对于一些大型工程或者涉及了比较多的第三方库、或者涉及了比较多的功能模块的时候,就需要知道这些符号是在哪里定义,或者说如果一个符号引用了但是没有知道函数定义,此时也需要找到这个符号是哪个模块引入的,为什么需要,所以需要一些通用的(形式化)的方法来搜索这些符号,而map文件就是一个比较好的切入点。但是map符号并不是万能的,它只能列出参与链接的全局变量的位置以及在哪个模块,对于一些静态变量,map文件中并不能体现它们,而在没有特殊声明的情况下,可执行文件中将会包含静态符号在符号表中,所以有时候我们只能依赖可执行文件本身里面的符号表来猜测一个符号的定义位置。说起静态符号,还有就是它是如何保证它只在一个编译模块中可见和被引用,而对其它模块不可见?
二、map文件相关
对于ld程序来说,生成map文件可以使用-Map=mapfile来指示链接器来生成一个可执行文件使用的map文件。在内核的构建过程中,也会生成一个System.map文件来表示内核中各个符号在内核中位置,但是这个文件并不是通过ld的-Map选项生成,而是使用了nm和grep工具来手动生成的,具体的文件文件及相关说明在linux-2.6.37.1\scripts\mksysmap文件中。我们这里只是结合ld的源代码来看一下这个Map文件是如何生成的。
1、map文件生成代码
对于map文件的生成,在ld的源代码中,名字也比较直观,就是通过lang_map函数来完成的,它的主要相关流程为
fprintf (config.map_file, _("\nLinker script and memory map\n\n"));
if (! link_info.reduce_memory_overheads)
{
obstack_begin (&map_obstack, 1000);
for (p = link_info.input_bfds; p != (bfd *) NULL; p = p->link_next)
bfd_map_over_sections (p, init_map_userdata, 0);
bfd_link_hash_traverse (link_info.hash, sort_def_symbol, 0);
}
lang_statement_iteration ++;
print_statements ();
其中的主要准备工作由bfd_link_hash_traverse (link_info.hash, sort_def_symbol, 0)语句完成,它遍历整个链接过程中所有的符号表,然后对其中的每个符号执行sort_def_symbol函数,这个函数的功能主要是将这个符号追加到符号定义节的userdata链表的最后,供之后执行的print_statements函数可以在遍历各个输入节的时候打印输入节的map信息。这里对于 bfd_link_hash_traverse (link_info.hash, sort_def_symbol, 0);语句实现要注意两个细节:
①、输出符号性质
在sort_def_symbol函数的定义中,它只会追加类型为bfd_link_hash_defined和bfd_link_hash_defweak属性的符号(代码不再粘贴,代码比较直观,贴出来影响阅读),其它的一概忽略,这也就意味着所有的局部变量符号没有机会在map文件中体现。
②、符号遍历规则
符号遍历是通过bfd_link_hash_traverse函数遍历,这个遍历的符号没有任何逻辑规律,它们只是依赖底层hash算法的选择而被放在不同的bucket中,这会导致对于每个输入节来说,它即将输出的定义符号列表并不一定是按照它们在内存中的逻辑地址位置排列的。
2、验证代码
[tsecer@Harry maporder]$ cat maporder.c
static int foo;
int main()
{
extern int bar(void);
return foo + bar();
}
int bar(void)
{
return 0x11111111;
}
[tsecer@Harry maporder]$ gcc maporder.c -Wl,-Map=map.txt
[tsecer@Harry maporder]$ grep -e 'main' -e 'bar' map.txt
0x00000000080482c4 __libc_start_main@@GLIBC_2.0
0x00000000080483ab bar
0x0000000008048394 main
[tsecer@Harry maporder]$ ld -V
GNU ld version 2.19.51.0.14-34.fc12 20090722
Supported emulations:
elf_i386
i386linux
elf_x86_64
[tsecer@Harry maporder]$
可以看到,其中bar的逻辑地址要比main的逻辑地址高,但是它在map文件中出现的顺序要比main早,所以说同一个节内符号在map文件中的出现顺序和它们的逻辑地址无关,大家不要依赖这个顺序。
3、ld2.20版本对map文件的优化
之前验证的代码可以看到,一个节内部符号出现顺序和逻辑地址无关,这个属性在链接器的2.20版本中进行了修改(优化),优化的结果就是map文件中同一个节中定义符号在map文件中出现顺序按照逻辑地址排序,这个代码合入时间比较晚,大致是在2009年9月的2.20版本总加入,我在网上也搜索到了这个补丁的讨论邮件,为了防止链接地址失效,这里还是把邮件内容拷贝一份过来,原始地址为(http://cygwin.com/ml/binutils/2009-07/msg00084.html):
Tristan.
2009-07-08 Tristan Gingold <gingold@adacore.com>
* ld.h (fat_user_section_struct): Add map_symbol_def_count field. * ldlang.c (hash_entry_addr_cmp): New function. (print_all_symbols): Sort the symbols by address before printing them.
RCS file: /cvs/src/src/ld/ld.h,v
retrieving revision 1.43
diff -u -p -r1.43 ld.h
--- ld.h 18 Mar 2009 11:27:18 -0000 1.43
+++ ld.h 8 Jul 2009 09:08:52 -0000
@@ -114,6 +114,7 @@ typedef struct fat_user_section_struct {
list of hash table entries for symbols defined in this section. */
struct map_symbol_def *map_symbol_def_head;
struct map_symbol_def **map_symbol_def_tail;
+ unsigned long map_symbol_def_count;
} fat_section_userdata_type;
#define get_userdata(x) ((x)->userdata) Index: ldlang.c =================================================================== RCS file: /cvs/src/src/ld/ldlang.c,v retrieving revision 1.311 diff -u -p -r1.311 ldlang.c --- ldlang.c 25 Jun 2009 13:18:46 -0000 1.311 +++ ldlang.c 8 Jul 2009 09:08:53 -0000 @@ -1988,6 +1988,7 @@ init_map_userdata (bfd *abfd ATTRIBUTE_U ASSERT (get_userdata (sec) == NULL); get_userdata (sec) = new_data; new_data->map_symbol_def_tail = &new_data->map_symbol_def_head; + new_data->map_symbol_def_count = 0; }
static bfd_boolean @@ -2015,6 +2016,7 @@ sort_def_symbol (struct bfd_link_hash_en def->entry = hash_entry; *(ud->map_symbol_def_tail) = def; ud->map_symbol_def_tail = &def->next; + ud->map_symbol_def_count++; } return TRUE; } @@ -3940,18 +3942,48 @@ print_one_symbol (struct bfd_link_hash_e return TRUE; }
+static int
+hash_entry_addr_cmp (const void *a, const void *b)
+{
+ const struct bfd_link_hash_entry *l = *(const struct bfd_link_hash_entry **)a;
+ const struct bfd_link_hash_entry *r = *(const struct bfd_link_hash_entry **)b;
+
+ if (l->u.def.value < r->u.def.value)
+ return -1;
+ else if (l->u.def.value > r->u.def.value)
+ return 1;
+ else
+ return 0;
+}
+
static void
print_all_symbols (asection *sec)
{
struct fat_user_section_struct *ud = get_userdata (sec);
struct map_symbol_def *def;
+ struct bfd_link_hash_entry **entries;
+ unsigned int i;
if (!ud) return;
*ud->map_symbol_def_tail = 0;
- for (def = ud->map_symbol_def_head; def; def = def->next)
- print_one_symbol (def->entry, sec);
+
+ /* Sort the symbols by address. */
+ entries = obstack_alloc (&map_obstack,
+ ud->map_symbol_def_count * sizeof (*entries));
+
+ for (i = 0, def = ud->map_symbol_def_head; def; def = def->next, i++)
+ entries[i] = def->entry;
+
+ qsort (entries, ud->map_symbol_def_count, sizeof (*entries),
+ hash_entry_addr_cmp);主要是加入了逻辑地址比较排序,这样map文件内部符号就会按照逻辑地址排序,所以新版本的链接器不用考虑这个问题。
+
+ /* Print the symbols. */
+ for (i = 0; i < ud->map_symbol_def_count; i++)
+ print_one_symbol (entries[i], sec);
+
+ obstack_free (&map_obstack, entries);
}
三、局部/本地(local)符号
正如之前所说,本地符号在map文件中是没有体现的,它们只可能在可执行文件的符号表中体现。这里说“可能”是因为如果使用了某些链接器选项,它们在可执行文件中也没有体现。
1、本地符号如何完成链接
为了简单,我们同样以之前的maporder.c为例子,看一下目标文件内容
[tsecer@Harry maporder]$ objdump -dr maporder.o
maporder.o: file format elf32-i386
Disassembly of section .text:
00000000 <main>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 e4 f0 and $0xfffffff0,%esp
6: e8 fc ff ff ff call 7 <main+0x7>
7: R_386_PC32 bar
b: 8b 15 00 00 00 00 mov 0x0,%edx
d: R_386_32 .bss
11: 01 d0 add %edx,%eax
13: 89 ec mov %ebp,%esp
15: 5d pop %ebp
16: c3 ret
上面对于局部变量foo的引用并没有体现foo这个符号的任何信息,相反,它是基于这个目标文件的.bss节来完成重定位的。由于这个文件只定义了一个局部变量,所以它在代码中体现的偏移为零。如果这里有多个变量,其中的mov 指令中的就会有一个局部变量在bss节中的偏移位置。
[tsecer@Harry maporder]$ cat maporder.c
static int foo[0x10];
int main()
{
extern int bar(void);
return foo[10] + bar();
}
int bar(void)
{
return 0x11111111;
}
[tsecer@Harry maporder]$ gcc maporder.c -c
[tsecer@Harry maporder]$ objdump -dr maporder.o
maporder.o: file format elf32-i386
Disassembly of section .text:
00000000 <main>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 e4 f0 and $0xfffffff0,%esp
6: 53 push %ebx
7: 83 ec 0c sub $0xc,%esp
a: 8b 1d 28 00 00 00 mov 0x28,%ebx这里的0x28即为foo[10]在bss中的偏移量,即0x28=0x40=10*4。
c: R_386_32 .bss 这里的重定位依然是相对于bss段,而没有使用符号foo本身。
10: e8 fc ff ff ff call 11 <main+0x11>
11: R_386_PC32 bar
15: 8d 04 03 lea (%ebx,%eax,1),%eax
18: 83 c4 0c add $0xc,%esp
1b: 5b pop %ebx
1c: 89 ec mov %ebp,%esp
1e: 5d pop %ebp
1f: c3 ret
这意味着当链接器最终完成链接的时候,它要从代码段的0xc位置读到这个偏移量0x28,然后将这个值和本目标文件的bss基地址相加,然后将符号绝对地址写回。
2、elf链接器生成符号表
(gdb) bt
#0 bfd_putl32 (data=134516896, p=0x815be9c)
at ../../binutils-2.21.1/bfd/libbfd.c:762
#1 0x08096d60 in bfd_elf32_swap_symbol_out (abfd=0x814f658, src=0xbfffef48,
cdst=0x815be98, shndx=0x0) at ../../binutils-2.21.1/bfd/elfcode.h:216
#2 0x080bc712 in elf_link_output_sym (finfo=0xbffff108,
name=0x8161d90 "statvar", elfsym=0xbfffef48, input_sec=0x815ced0, h=0x0)
at ../../binutils-2.21.1/bfd/elflink.c:8422
#3 0x080be22f in elf_link_input_bfd (finfo=0xbffff108, input_bfd=0x815abe8)
at ../../binutils-2.21.1/bfd/elflink.c:9289
#4 0x080c10e5 in bfd_elf_final_link (abfd=0x814f658, info=0x8144d40)
at ../../binutils-2.21.1/bfd/elflink.c:10707
#5 0x080614d9 in ldwrite () at ../../binutils-2.21.1/ld/ldwrite.c:581
#6 0x0805eb1a in main (argc=2, argv=0xbffff3a4)
at ../../binutils-2.21.1/ld/ldmain.c:471
(gdb)
3、对于局部符号链接及对应符号生成
这个过程主要在elf_link_input_bfd函数中完成,其中主要代码
/* Find local symbol sections and adjust values of symbols in
SEC_MERGE sections. Write out those local symbols we know are
going into the output file. */
isymend = isymbuf + locsymcount; 这里只处理一个节中的本地符号,全局符号不在这里处理。
for (isym = isymbuf, pindex = finfo->indices, ppsection = finfo->sections;
isym < isymend;
isym++, pindex++, ppsection++)
{
……
if (ELF_ST_TYPE (isym->st_info) == STT_SECTION)
{
/* We never output section symbols. Instead, we use the
section symbol of the corresponding section in the output
file. */
continue;
}
/* If we are stripping all symbols, we don't want to output this
one. */
if (finfo->info->strip == strip_all)
continue;
/* If we are discarding all local symbols, we don't want to
output this one. If we are generating a relocatable output
file, then some of the local symbols may be required by
relocs; we output them below as we discover that they are
needed. */
if (finfo->info->discard == discard_all) 这里
continue;
……
/* ELF symbols in relocatable files are section relative, but
in executable files they are virtual addresses. Note that
this code assumes that all ELF sections have an associated
BFD section with a reasonable value for output_offset; below
we assume that they also have a reasonable value for
output_section. Any special sections must be set up to meet
these requirements. */
osym.st_value += isec->output_offset;首先累加输入节在其对应输出节中的偏移。因为一个可执行文件的输出节可能由多个目标文件的输入节组成,这些输入节在同一个输出节中位置不同。例如,不同输入目标文件的data节在输出文件的data节中的起始位置不同。
if (! finfo->info->relocatable)
{
osym.st_value += isec->output_section->vma;这里再加上输出节在整个可执行文件中的起始地址,从而得到符号在可执行文件中的最终地址。
if (ELF_ST_TYPE (osym.st_info) == STT_TLS)
{
/* STT_TLS symbols are relative to PT_TLS segment base. */
BFD_ASSERT (elf_hash_table (finfo->info)->tls_sec != NULL);
osym.st_value -= elf_hash_table (finfo->info)->tls_sec->vma;
}
}
}
4、如何知道符号表中局部符号的个数
在一个符号节的info属性中,其中的个数指明了局部符号的个数,这也意味着一个符号节中所有的局部符号在所有的全局符号之前,否则这个变量没有意义。以下面输入为例
[tsecer@Harry maporder]$ readelf -a maporder.o
ELF Header:
……
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 000032 00 AX 0 0 4
[ 2] .rel.text REL 00000000 000358 000018 08 8 1 4
[ 3] .data PROGBITS 00000000 000068 000000 00 WA 0 0 4
[ 4] .bss NOBITS 00000000 000080 000080 00 WA 0 0 32
[ 5] .comment PROGBITS 00000000 000080 00002d 00 0 0 1
[ 6] .note.GNU-stack PROGBITS 00000000 0000ad 000000 00 0 0 1
[ 7] .shstrtab STRTAB 00000000 0000ad 000049 00 0 0 1
[ 8] .symtab SYMTAB 00000000 000288 0000b0 10 9 9 4这个数值表示符号表的前9项均为局部符号。
[ 9] .strtab STRTAB 00000000 000338 00001d 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.
Relocation section '.rel.text' at offset 0x358 contains 3 entries:
Offset Info Type Sym.Value Sym. Name
0000000c 00000401 R_386_32 00000000 .bss
00000011 00000401 R_386_32 00000000 .bss
00000019 00000a02 R_386_PC32 00000028 bar
There are no unwind sections in this file.
Symbol table '.symtab' contains 11 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FILE LOCAL DEFAULT ABS maporder.c
2: 00000000 0 SECTION LOCAL DEFAULT 1
3: 00000000 0 SECTION LOCAL DEFAULT 3
4: 00000000 0 SECTION LOCAL DEFAULT 4
5: 00000000 64 OBJECT LOCAL DEFAULT 4 foo
6: 00000040 64 OBJECT LOCAL DEFAULT 4 baz
7: 00000000 0 SECTION LOCAL DEFAULT 6
8: 00000000 0 SECTION LOCAL DEFAULT 5 前九项均为局部符号。
9: 00000000 40 FUNC GLOBAL DEFAULT 1 main
10: 00000028 10 FUNC GLOBAL DEFAULT 1 bar
5、符号表内容如何确定
这个在bfd_elf_final_link函数中完成
首先输出一个空符号表项。
/* Start writing out the symbol table. The first symbol is always a
dummy symbol. */
if (info->strip != strip_all
|| emit_relocs)
{
elfsym.st_value = 0;
elfsym.st_size = 0;
elfsym.st_info = 0;
elfsym.st_other = 0;
elfsym.st_shndx = SHN_UNDEF;
if (elf_link_output_sym (&finfo, NULL, &elfsym, bfd_und_section_ptr,
NULL) != 1)
goto error_return;
}
为可执行文件的每个节输出一个对应的符号项。
/* Output a symbol for each section. We output these even if we are
discarding local symbols, since they are used for relocs. These
symbols have no names. We store the index of each one in the
index field of the section, so that we can find it again when
outputting relocs. */
if (info->strip != strip_all
|| emit_relocs)
{
elfsym.st_size = 0;
elfsym.st_info = ELF_ST_INFO (STB_LOCAL, STT_SECTION);
elfsym.st_other = 0;
elfsym.st_value = 0;
for (i = 1; i < elf_numsections (abfd); i++)
{
o = bfd_section_from_elf_index (abfd, i);
if (o != NULL)
{
o->target_index = bfd_get_symcount (abfd);
elfsym.st_shndx = i;
if (!info->relocatable)
elfsym.st_value = o->vma;
if (elf_link_output_sym (&finfo, NULL, &elfsym, o, NULL) != 1)
goto error_return;
}
}
}
……输出所有输入节中的局部符号。
for (o = abfd->sections; o != NULL; o = o->next)
{
for (p = o->map_head.link_order; p != NULL; p = p->next)
{
if (p->type == bfd_indirect_link_order
&& (bfd_get_flavour ((sub = p->u.indirect.section->owner))
== bfd_target_elf_flavour)
&& elf_elfheader (sub)->e_ident[EI_CLASS] == bed->s->elfclass)
{
if (! sub->output_has_begun)
{
if (! elf_link_input_bfd (&finfo, sub))
goto error_return;
sub->output_has_begun = TRUE;
}
}
else if (p->type == bfd_section_reloc_link_order
|| p->type == bfd_symbol_reloc_link_order)
{
if (! elf_reloc_link_order (abfd, info, o, p))
goto error_return;
}
else
{
if (! _bfd_default_link_order (abfd, info, o, p))
goto error_return;
}
}
}
…… 输出所有全局符号。
/* Output any global symbols that got converted to local in a
version script or due to symbol visibility. We do this in a
separate step since ELF requires all local symbols to appear
prior to any global symbols. FIXME: We should only do this if
some global symbols were, in fact, converted to become local.
FIXME: Will this work correctly with the Irix 5 linker? */
eoinfo.failed = FALSE;
eoinfo.finfo = &finfo;
eoinfo.localsyms = TRUE;
elf_link_hash_traverse (elf_hash_table (info), elf_link_output_extsym,
&eoinfo);
这里可以看到,可执行文件的布局特征
①、空表项在最开始
②、接下来是可执行文件中的各个节
③、然后是所有输入文件的局部变量,所以局部变量在最终文件的符号中是按照在链接器输入顺序排列的。
④、接下来是全局符号。这些符号没有按照它们在链接器中输入顺序排列(因为是通过hash表遍历输出)。
⑤、可以给链接器传入-X选项来让链接器在链接时丢弃(discard)所有局部符号(静态变量和函数),它们在动态链接中没有用处,会增加可执行文件的大小。但是如果希望方便的通过调试器调试该版本的话,还是保留局部符号。
6、一个可执行文件的符号表
由于文件太大,这里就不贴了,有兴趣的同学可以自己看看,不感兴趣的同学你贴了他也不一定看,O(∩_∩)O~。