符号表

链接过程的本质就是要把多个不同的目标文件之间相互粘到一起, 当然这些目标文件之间必须有固定的规则. 在链接中, 目标文件之间相互拼合实际上是目标文件之间对地址的引用, 即对函数和变量的地址的引用

比如目标文件 B 要用到目标文件 A 中的函数 foo, 那么我们就称目标文件 A 定义(define)了函数 foo, 目标文件 B 引用(reference)了目标文件 A 中的函数 foo, 这两个概念也同样适用于变量

每一个函数或变量都有自己独特的名字, 这样才能避免链接过程中不同变量和函数之间的混淆.

符号与符号值

在链接中, 我们将函数和变量统称为符号(Symbol), 函数名或变量名 称为 符号名(symbol name). 每一个目标文件都会有一个相应的符号表(symbol table), 每一个定义的符号都有一个对应的值叫做符号值(symbol value).

对于变量和函数来说, 符号值就是它们的地址

这里需要注意的是符号值是符号的地址, 并不是a=1这种实际的变量值. 变量值我们会单独保存在数据段中

依然以前文的代码为例

int printf(const char *format, ...);

int global_init_var = 84;
int global_uninit_var;

void func1(int i) {
    printf("%d\n",i);
}

int main(void) {
    static int static_var = 85;
    static int static_var2;
    int a = 1;
    int b;
    func1(static_var + static_var2 + a + b);
}

该文件编译得到的符号表中的符号一定是下面的类型之一:

  1. 定义在本目标文件的全局符号, 可以被其他目标文件引用, 例如 func1, main global_init_var
  1. 在本目标文件中引用的全局符号, 却没有定义在本目标文件中, 这种一般叫做外部符号(extern symbol), 比如 printf
  1. 段名, 这种符号由编译器产生, 符号值就是该段的起始地址, 比如 .text .data
  1. 局部符号, 这里的局部符号指局部静态变量, 也就是 static_var static_var2, 局部符号只在编译单元内部可见, 也就是说被 static 修饰的变量或函数只在当前文件可以被使用, 其他文件无法使用. 调试器可以使用这些符号来分析, 但链接器会忽略这些局部符号

    局部非静态变量比如 a, b 是不会进入符号表中的, 它们会在栈中分配内存空间.

  1. 行号信息, 即目标文件指令与源代码行的对应关系

对于我们来说最值得关注的就是全局符号, 也就是1和2. 因为链接的过程只关心全局符号的相互粘合, 局部符号,段名行号都是次要的, 它们对于其他目标文件来说都是不可见的, 在链接过程中也是无关紧要的

我们可以使用很多工具来查看一个 ELF 文件的符号表

objdump -t SimpleSection.o
readelf -s SimpleSection.o
nm SimpleSection.o
0000000000000000 T func1
0000000000000000 D global_init_var
0000000000000000 B global_uninit_var
000000000000002b T main
                 U printf
0000000000000004 d static_var.1
0000000000000004 b static_var2.0
符号字母含义
T 代码符号,表示该符号是一个函数或者可执行代码.
t 本地代码符号,表示该符号是本地的函数或代码,仅在当前目标文件中可见.
D 数据符号,表示该符号是一个初始化的全局变量或静态变量.
d 本地数据符号,表示该符号是本地的已初始化数据,仅在当前目标文件中可见.
B BSS符号,表示该符号是一个未初始化的全局变量或静态变量.
b 本地BSS符号,表示该符号是本地的未初始化数据,仅在当前目标文件中可见.
U 未定义符号,表示该符号在当前目标文件中未定义,需要在链接时从其它目标文件或库文件中解析.
R 重定位符号,表示该符号需要在链接时进行重定位.
r 本地重定位符号,表示该符号是本地的需要进行重定位的符号,仅在当前目标文件中可见.
C 弱符号,表示该符号是一个弱符号,其定义可以被覆盖.
W 弱符号,表示该符号是一个弱符号,其引用可以不被解析,或者可以被重复解析.
S 特殊符号,表示该符号是一个特殊符号(例如,汇编代码中的符号).
V 可变符号,表示该符号是一个可变符号(例如,C++虚函数).
I 间接符号,表示该符号是一个间接符号,需要进一步解析.

符号表字段

ELF 文件中可能会出现多个符号表, 需要遍历所有段并判断其类型属于 SHT_SYMTABSHT_DYNSYM

这里的 SHT_SYMTAB 对应静态符号表, SHT_DYNSYM 对应动态符号表

其段表字段值 sh_link 指向对应的字符串表(通常是 .strtab), st_info 的低4位用于符号类型, 高4位用于符号绑定信息

比如使用 readelf -S 查看一个可执行文件, 其中 .symtab 的 link 指向 .strtab, .dynsym 的 link 指向 .dynstr

  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 6] .dynsym           DYNSYM           00000000000003d8  000003d8
       00000000000002e8  0000000000000018   A       7     1     8
  [ 7] .dynstr           STRTAB           00000000000006c0  000006c0
       000000000000014a  0000000000000000   A       0     0     1
  ...
  [28] .symtab           SYMTAB           0000000000000000  00008040
       00000000000008d0  0000000000000018          29    25     8
  [29] .strtab           STRTAB           0000000000000000  00008910
       00000000000005bc  0000000000000000           0     0     1

在段表中搜索到字符表段之后就可以借助其 offset 定位到这个段了. 字符表段是一个长度为 n 的数组, 数组中的每一个元素都是 Elf64_Sym, 用于表示一个符号, 其结构体如下所示

typedef struct {
   uint32_t      st_name;   // 符号名
   unsigned char st_info;   // 符号类型和绑定信息
   unsigned char st_other;  // 暂未使用
   uint16_t      st_shndx;  // 符号所在的段
   Elf64_Addr    st_value;  // 符号值
   uint64_t      st_size;   // 符号大小
} Elf64_Sym;

其中符号名 st_name 为该符号在字符串表中的偏移量, 可以通过 .strtab + st_name 找到符号名

$ readelf -s SimpleSection.o
Symbol table '.symtab' contains 13 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS SimpleSection.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 .data
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 .bss
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 .rodata
     6: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    3 static_var.1
     7: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    4 static_var2.0
     8: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 global_init_var
     9: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    4 global_uninit_var
    10: 0000000000000000    43 FUNC    GLOBAL DEFAULT    1 func1
    11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf
    12: 000000000000002b    57 FUNC    GLOBAL DEFAULT    1 main

2/3/4/5 号符号的属性是 SECTION 代表这是一个段, 这里的对应的字符串并不是保存在 .strtab 中的, 需要使用 st_shndx 作为索引值在段表中查询对应段的名字, 也就是说在 .shstrtab 中才可以找到符号的名字, 对应的代码实现如下所> 示. 您可以在 readelf.c 中查看完整代码

char *symbol_name;
// 对于 st_name 的值不为0的符号或者 ABS, 去对应的 .strtab 中找
if (symtabs[j].st_name || symtabs[j].st_shndx == SHN_ABS) {
  symbol_name = (char *)(ELF_file_data->addr + strtab->sh_offset + symtabs[j].st_name);
} else {
  // 为 0 说明是一个特殊符号, 用 symbol_ndx 去段表字符串表中找
  symbol_name = (char *)(ELF_file_data->addr + ELF_file_data->shstrtab_offset +
                         ELF_file_data->shdr[symtabs[j].st_shndx].sh_name);
}

同时注意到有一列信息是 Vis, Vis 在 C/C++ 中未使用, 用于控制符号可见性, Rust/Swift 使用到了, 这里默认 DEFAULT 即可

但是在可执行文件中确实存在 HIDDEN ?

.dynsym 中显示函数名而不是 [...]@GLIBC_2.2.5 (2) ?

符号值

需要注意的是默认情况下,未初始化的全局变量会被放置在 BSS 段中,而不是 .common 块中

要将变量放置在 .common 块中,需要使用 __attribute__((common)) 属性或者编写链接器脚本来显式地指定.但是,这种方法并不常用,因为common 块是一种非标准的方式来管理未初始化的全局变量,不同的编译器和链接器可能会有不同的行为.

int printf(const char *format, ...);

__attribute__((common)) int global_init_var;
__attribute__((common)) int global_uninit_var;

void func1(int i) {
    printf("%d\n",i);
}

int main(void) {
    static int static_var = 85;
    static int static_var2;
    int a = 1;
    int b;
    func1(static_var + static_var2 + a + b);
}

这里我们还是以之前的 SimpleSection.o 为例

Symbol table '.symtab' contains 13 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS SimpleSection.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 .data
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 .bss
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 .rodata
     6: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    3 static_var.1
     7: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    4 static_var2.0
     8: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 global_init_var
     9: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    4 global_uninit_var
    10: 0000000000000000    43 FUNC    GLOBAL DEFAULT    1 func1
    11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf
    12: 000000000000002b    57 FUNC    GLOBAL DEFAULT    1 main

SYMBOL TABLE:
0000000000000000 l    df *ABS*  0000000000000000 SimpleSection.c
0000000000000000 l    d  .text  0000000000000000 .text
0000000000000000 l    d  .data  0000000000000000 .data
0000000000000000 l    d  .bss   0000000000000000 .bss
0000000000000000 l    d  .rodata        0000000000000000 .rodata
0000000000000004 l     O .data  0000000000000004 static_var.1
0000000000000004 l     O .bss   0000000000000004 static_var2.0
0000000000000000 g     O .data  0000000000000004 global_init_var
0000000000000000 g     O .bss   0000000000000004 global_uninit_var
0000000000000000 g     F .text  000000000000002b func1
0000000000000000         *UND*  0000000000000000 printf
000000000000002b g     F .text  0000000000000039 main

对于符号值来说有以下几种情况

  1. 符号不是 COMMON 类型, 符号值代表 该符号在段中的偏移量

    这种情况是最常见的情况, 例如对于 func1, 其位于1号段也就是 .text 段, 偏移量是 0. static_var 定义在 3 号 .data 偏移量是 0x4, 这个偏移量是因为在它前面还有一个 global_init_var

  1. 如果是 COMMON 类型, 符号值代表对齐属性
  1. 可执行文件中, 符号值代表符号的虚拟地址, 这个虚拟地址对于动态链接器十分有用

特殊符号

当我们使用 ld 作为链接器来链接生产可执行文件的时候, 它会为我们定义很多特殊的符号. 这些符号并没有在你的程序中定义, 但是可以直接声明并且使用它们

其实这些符号是被定义为 ld 链接器的脚本当中的, 在后文链接过程控制这一节会回顾这个问题

#include <stdio.h>

extern char __executable_start[]; // 程序的起始地址, 不是入口地址
extern char etext[], _etext[], __etext[]; // 代码段的结束地址
extern char edata[], _edata[]; // 数据段的结束地址
extern char end[], _end[]; // 程序结束地址

int main() {
    printf("Execuatable Start %p\n", __executable_start);
    printf("Text End %p %p %p\n", etext, _etext, __etext);
    printf("Data End %p %p\n", edata, _edata);
    printf("Execuatable End %p %p\n", end, _end);
    return 0;
}
Execuatable Start 0x5577aa444000
Text End 0x5577aa445205 0x5577aa445205 0x5577aa445205
Data End 0x5577aa448010 0x5577aa448010
Execuatable End 0x5577aa448018 0x5577aa448018

符号修饰和函数签名

20 世纪 70 年代以前, 编译器编译源代码产生目标文件的时候符号名和相应的变量名和函数名是一样的. 后来 UNIX 平台和 C 语言发明之后, 存在了相当多使用汇编写的库和目标文件, 这样就产生了一个问题, 如果一个 C 程序想要使用这些库就不可以使用冲突的符号名. 同样, 如果 C 的目标文件要用到一个使用 Fortran 语言编写的目标文件, 也必须避免命名冲突

为了防止类似的符号名冲突, UNIX 下的 C 语言就规定源代码文件中的所有全局变量和函数经过编译之后, 相对应的符号名前加下划线, Fortran 编译之后前后都加下划线, 也就是如果一个函数 foo, C 编译之后就变成了 _foo, Fortran 就变成了 _foo_

这种方式确实可以暂时减少符号冲突的概率, 但是并没有从根本上解决. 当程序很大的时候, 多个模块也有可能因为命名规范不严格造成命名冲突.

因此像 C++ 这样后来设计的语言也开始考虑到了这个问题, 增加了命名空间的方法来解决多模块的符号冲突的问题

现在的 Linux 下的 GCC 编译器默认情况下已经去掉了 C 语言符号前加 _, Windows 下的编译器还保持这样的传统. 例如 Visual C++ 编译器, cygwin, mingw. GCC 编译器也可以通过参数 -fleading-underscore -fno-leading-underscore 来打开/关闭是否在 C 符号前加下划线

C++符号修饰

众所周知C++拥有类/继承/虚机制/重载/命名空间等强大的特性, 这也使得符号的管理更为复杂. 为了支持这些复杂的特性, 人们发明了 符号修饰(Name Decoration)符号改编(Name Mangling)的机制

#include <iostream>

int func(int);
float func(float);

class C {
public:
    int func(int);
    class C2 {
    public:
        int func(int);
    };
};

namespace N {
    int func(int);
    class C {
    public:
        int func(int);
    };
};

int main() {
    func(10);
    float x = 10;
    func(x);
    C c;
    c.func(10);
    C::C2 c2;
    c2.func(10);
    (N::func(10));
    N::C nc;
    nc.func(10);
    return 0;
}

注意这里需要调用一次, 单纯的声明不调用 g++ 似乎不会将其计入符号表

上面的 C++ 代码定义了 6 个同名函数 func, 只不过它们的返回类型和参数以及所在的命名空间不相同. 这里引入一个术语叫做 函数签名(Function Signature), 对于上面的 6 个函数每个都有一个独一无二的函数签名, 编译器在生成这个签名的时候会考虑 返回类型/参数/命名空间/函数名 等多种情况

g++ -c func.cpp
readelf -s func.o

可以看到原有的函数名被

Symbol table '.symtab' contains 21 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS func.cpp
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 .bss
     4: 0000000000000000     1 OBJECT  LOCAL  DEFAULT    4 _ZStL8__ioinit
     5: 0000000000000096    86 FUNC    LOCAL  DEFAULT    1 _Z41__static_ini[...]
     6: 00000000000000ec    25 FUNC    LOCAL  DEFAULT    1 _GLOBAL__sub_I_main
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 .rodata
     8: 0000000000000000   150 FUNC    GLOBAL DEFAULT    1 main
     9: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _Z4funci
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _Z4funcf
    11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _ZN1C4funcEi
    12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _ZN1C2C24funcEi
    13: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _ZN1N4funcEi
    14: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _ZN1N1C4funcEi
    15: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND __stack_chk_fail
    16: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _ZNSt8ios_base4I[...]
    17: 0000000000000000     0 NOTYPE  GLOBAL HIDDEN   UND __dso_handle
    18: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_
    19: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _ZNSt8ios_base4I[...]
    20: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND __cxa_atexit

其中 9-14 行为对应的符号名

函数名C++ 符号名行号
int func(int) _Z4funci 9
float func(float) _Z4funcf 10
int C::func(int) _ZN1C4funcEi 11
int C::C2::func(int) _ZN1C2C24funcEi 12
int N::func(int) _ZN1N4funcEi 13
int N::C::func(int) _ZN1N1C4funcEi 14

上面只是一个比较简单的例子, 实际上 cpp 的名称修饰十分复杂, 您可参考 cpp-name-mangling 部分展开详细阅读

我们可以使用 binutils 中的 c++filt 工具来解析一个被修饰过的名称, 例如

$ c++filt _ZN1N1C4funcEi
N::C::func(int)
$ c++filt _ZN3foo3barE
foo::bar

值得注意的是, 函数和变量的类型并没有被加入到修饰后的名称中, 所以不论这个变量的类型是什么它的名称都是一样的. 这种机制可以很好的避免命名冲突

不同编译器厂商的名称修饰方法可能不同, 因此不同编译器编译产生的文件很有可能无法正常互相链接, 后面的 C++ABI 和 COM 节会详细讨论这个问题

$ nm -C func.o
                 U _GLOBAL_OFFSET_TABLE_
00000000000000ec t _GLOBAL__sub_I_main
0000000000000096 t __static_initialization_and_destruction_0(int, int)
                 U func(float)
                 U func(int)
                 U C::C2::func(int)
                 U C::func(int)
                 U N::C::func(int)
                 U N::func(int)
                 U std::ios_base::Init::Init()
                 U std::ios_base::Init::~Init()
0000000000000000 b std::__ioinit
                 U __cxa_atexit
                 U __dso_handle
                 U __stack_chk_fail
0000000000000000 T main

extern "C"

C++ 为了和 C 兼容, 在符号的管理上 C++ 有一个用来声明或者定义一个 C 的符号的 extern "C" 关键字用法

extern "C" {
    int func(int);
    int var;
}

在 extern "C" 内部的代码会被当作 C 语言代码处理, 所以这时候 C++ 的名称修饰机制不会起作用. 我们可以做一个很有意思的小实验, 尝试如下代码

#include <stdio.h>

namespace myname {
    int var = 42;
}

extern "C" int _ZN6myname3varE;

int main() {
    printf("%d\n", _ZN6myname3varE);
    return 0;
}

我们手动定义了以恶搞全局变量 var 并根据所掌握的 GCC 名称修饰规则手动声明一个外部符号

$ g++ manualnamemangling.cpp -o a
$ ./a
42

很多时候我们会碰到有些头文件声明了一些C语言的函数和全局变量,但是这个头文件可能会被C语言代码或C++代码包含.比如很常见的,我们的C语言库函数中的string.h中声明了memset这个函数,它的原型如下:

void *memset (void *, int, size_t);

如果不加任何处理,当我们的C语言程序包含 string.h 的时候,并且用到了memset 这个函数,编译器会将memset符号引用正确处理;

但是在C++语言中,编译器会认为这个memset 函数是一个C++函数,将memset 的符号修饰成 _Z6memsetPvii,这样链接器就无法与C语言库中的memset符号进行链接.所以对于C++来说,必须使用 extern "C" 来声明memset 这个函数.但是C语言又不支持extern "C" 语法,如果为了兼容C语言和C++语言定义两套头文件,未免过于麻烦.幸好我们有一种很好的方法可以解决上述问题,就是使用C++的宏 _cplusplus ,C++编译器会在编译C++的程序时默认定义这个宏,我们可以使用条件宏来判断当前编译单元是不是C++代码.具体代码如下:

#ifdef __cplusplus
extern "C" {
#endif

void *memset (void * , int, size_t ) ;

#ifdef __cplusplus
}
#endif

如果当前编译单元是 C++ 代码, 那么 memset 会在 extern "C" 中被声明, 如果是 C 代码则直接声明. 上面这段代码的技巧几乎在所有系统头文件中都被用到了

强符号和弱符号

在编程中经常会遇到的一种错误情况就是符号重复定义, 当多个目标文件中含有相同的名字全局符号定义, 那么当链接器在链接这些目标文件的时候就会出现符号重复定义的错误

对于 C/C++ 语言来说, 编译器默认函数和初始化的全局变量为强符号, 未初始化的全局变量为弱符号

这里有一点需要注意, 笔者使用的gcc版本是11.3.0, ld 的版本是 2.38, 对于下方的例子是无法编译通过的, 会存在两个强符号冲突! 但是如果切换到 gcc-9 版本则没有这个问题, 对于clang10.0.1版本及以下同样可以通过编译,clang11.0.0及以上存在链接报错, 笔者在知乎提问了这个问题并且得到了解答: C编译器版本导致强弱符号重定义的问题?, 需要使用 -fcommon 参数

// strong_weak_symbol.c
extern int ext;

int weak_symbol;
int strong_symbol = 1;

int __attribute__((weak)) weak_symbol2 = 2;

int main() {
    return 0;
}
// strong_symbol.c
int weak_symbol = 20;

对于 strong_weak_symbol.o 来说, 使用 readelf -s 结果如下所示

Symbol table '.symtab' contains 7 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS strong_weak_symbol.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
     3: 0000000000000004     4 OBJECT  GLOBAL DEFAULT  COM weak_symbol
     4: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    2 strong_symbol
     5: 0000000000000004     4 OBJECT  WEAK   DEFAULT    2 weak_symbol2
     6: 0000000000000000    15 FUNC    GLOBAL DEFAULT    1 main

strong_symbol.o 的结果如下

Symbol table '.symtab' contains 3 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS strong_symbol.c
     2: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    2 weak_symbol

可以注意到 strong_weak_symbol.c 中未初始化的全局变量 weak_symbol 定义在 COMMON 段, 作为一个弱符号. 而 strong_symbol.c 中的 weak_symbol 是一个已初始化的全局变量. 此时弱符号会被强符号所覆盖

针对强弱符号, 链接器会按照如下的规则处理被多次定义的全局符号:

  1. 不允许强符号被定义多次, 链接器报重复定义错误
  1. 如果一个符号某个目标文件中是弱符号, 那么选择其他文件中的强符号
  1. 如果一个符号在所有目标文件中都为弱符号, 那么选择占用空间最大的

上面的例子实际上对应的是第二种情况

需要注意的是, extern 修饰的符号既不是强符号也不是弱符号, 他甚至都不会进入符号表作为一个未定义符号.

强引用和弱引用

我们看到的对外部目标文件的符号引用在目标文件被最终链接成可执行文件的时候, 它们需要被正确决议.

对于未找到定义的弱引用, 链接器默认其值为0或一个特殊的值

在 GCC 中可以使用 __attribute__((weakref("tar"))) static void foo(); 来声明 foo 是一个到 tar 的弱引用

这里的相当混乱和奇怪, 实操下来和书上写的不太一样, 笔者也不是很清楚...

#include <stdio.h>
#include <pthread.h>

int pthread_create(pthread_t*, const pthread_attr_t*, void *(*)(void*), void *) __attribute__ ((weak));

int main() {
    if (pthread_create) {
        printf("This is multi-thread version\n");
    } else {
        printf("This is single-thread version\n");
    }
    return 0;
}

上面的代码理论上来说应该是根据弱引用的方法判断当前程序是链接到了单线程的Glibc还是多线程的Glibc, 根据是否使用了了 -lpthread 选项

gcc pthread.c -o pt
gcc pthread.c -lpthread -o pt

调试信息

目标文件中也可以保存调试信息, 几乎所有现代的编译器都成支持源代码级别的调试, 比如可以在函数里面设置断点, 监视变量变化, 单步执行等等

前提是编译器必须提前将源代码和目标代码之间的关系

有些高级的编译器和调试器甚至支持查看 STL 容器里面的内容, 即程序员在调试的过程中可以直接观察 STL 容器中成员的值

现在 ELF 采用的是一个叫做 DWARF(debug with arbitary record format) 的标准调试信息格式

有关调试信息的部分笔者将会在 "调试信息" 这一节展开, 作为扩展部分

zood