1. 程式人生 > >鏈接器之Map文件與符號表

鏈接器之Map文件與符號表

must 不可 group 程序 separate 可見 多個 -m ada

一、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 <[email protected]>

 * 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~。

鏈接器之Map文件與符號表