计算机系统篇之链接:静态链接(中)——符号解析

Posted csstormq

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了计算机系统篇之链接:静态链接(中)——符号解析相关的知识,希望对你有一定的参考价值。

计算机系统篇之链接(4):符号解析

Author:stormQ

Wednesday, 15. April 2020 12:35PM


符号解析的整体过程

符号解析的目的是将每个符号引用与唯一的符号定义关联起来。符号解析的过程由编译器、汇编器和链接器协作完成。具体地处理行为如下:

符号编译器如何处理汇编器如何处理链接器如何处理
对于全局符号定义如果同一个全局符号在同一个目标模块内有多个定义,那么编译器会报错,不会进行后面的汇编和链接过程。只要编译器不报错,汇编器也不会报错。如果同一个全局符号在不同的目标模块内都有定义,那么链接器会报错。
对于全局符号引用只要编译器不报错,汇编器也不会报错。如果一个全局符号引用在其他目标模块内都没有定义,那么链接器会报错。
对于局部符号定义如果同一个局部符号在同一个目标模块内有多个定义,那么编译器会报错,不会进行后面的汇编和链接过程。只要编译器不报错,汇编器也不会报错。链接器不会涉及局部符号定义的处理过程。
对于局部符号引用只要编译器不报错,汇编器也不会报错。链接器不会涉及局部符号引用的处理过程。

1)验证“如果同一个全局符号在同一个目标模块内有多个定义,那么编译器会报错,不会进行后面的汇编和链接过程。”

# 全局变量 g_val(int 类型的)和 g_val(double 类型的)对应的全局符号的名称相同,属于“同一个全局符号在同一个目标模块内有多个定义”的情况
# 函数 int func(int val) 和 void func(int val) 对应的全局符号的名称相同,属于“同一个全局符号在同一个目标模块内有多个定义”的情况
$ cat compile_error1.cpp 
int g_val = 0;
double g_val = 0;

int func(int val)

  return val;


void func(int val)



# 在生成汇编文件时报错,即编译器会报错
$ g++ -S compile_error1.cpp -o compile_error1.s
compile_error1.cpp:2:8: error: conflicting declaration ‘double g_val’
 double g_val;
        ^~~~~
compile_error1.cpp:1:5: note: previous declaration as ‘int g_val’
 int g_val;
     ^~~~~
compile_error1.cpp: In function ‘void func(int)’:
compile_error1.cpp:9:6: error: ambiguating new declaration of ‘void func(int)’
 void func(int val)
      ^~~~
compile_error1.cpp:4:5: note: old declaration ‘int func(int)’
 int func(int val)
     ^~~~

2)验证“如果同一个全局符号在不同的目标模块内都有定义,那么链接器会报错。”

# 在不同的目标模块中定义相同名称的全局符号
$ cat link_error1.cpp 
int g_val = 1;

int func(int val)

  return val;

$ cat main.cpp
int g_val = 100;

void func(int val)



int main()

    return 0;

# 编译器和汇编器不会报错
$ g++ -c link_error1.cpp -o link_error1.o
$ g++ -c main.cpp -o main.o
# 在生成可执行目标文件时报错,即链接器会报错
$ g++ -o main main.o link_error1.o
link_error1.o:(.data+0x0): multiple definition of `g_val'
main.o:(.data+0x0): first defined here
link_error1.o: In function `func(int)':
link_error1.cpp:(.text+0x0): multiple definition of `func(int)'
main.o:main.cpp:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status

3)验证“如果一个全局符号引用在目标模块内既没有声明也没有定义,那么编译器会报错,不会进行后面的汇编和链接过程。”

# func2 在目标模块中既没有声明也没有定义
$ cat compile_error2.cpp 
int func(int val)

  return val + func2(val);

# 编译器会报错
$ g++ -S compile_error2.cpp -o compile_error2.s
compile_error2.cpp: In function ‘int func(int)’:
compile_error2.cpp:3:25: error: ‘func2’ was not declared in this scope
   return val + func2(val);
                         ^

4)验证“如果一个全局符号引用在目标模块内只有声明没有定义,那么编译器不会报错,继续进行后面的汇编和链接过程。”

# 一个全局符号引用在目标模块内只有声明没有定义
$ cat compile_correct1.cpp 
int func2(int);

int func(int val)

  return val + func2(val);

# 编译器和汇编器不会报错
$ g++ -c compile_correct1.cpp -o compile_correct

5)验证“如果一个全局符号引用在其他目标模块内都没有定义,那么链接器会报错。”

# 一个全局符号引用在其他目标模块内都没有定义
$ cat compile_correct1.cpp 
int func2(int);

int func(int val)

  return val + func2(val);

$ cat main.cpp
int main()

    return 0;

# 编译器和汇编器不会报错
$ g++ -o main main.o compile_correct
$ g++ -c main.cpp -o main.o
# 链接器会报错
$ g++ -o main main.o compile_correct.o
compile_correct.o: In function `func(int)':
compile_correct1.cpp:(.text+0x11): undefined reference to `func2(int)'
collect2: error: ld returned 1 exit status

6)验证“如果同一个局部符号在同一个目标模块内有多个定义,那么编译器会报错,不会进行后面的汇编和链接过程。”

# 局部变量 value(值为1的)与局部变量 value(值为2的)对应的局部符号的名称相同,属于“同一个局部符号在同一个目标模块内有多个定义”
# 局部函数 int func(int val) 与 void func(int val) 对应的局部符号的名称相同,属于“同一个局部符号在同一个目标模块内有多个定义”
# 局部变量 value(值为3的)与局部变量 value(值为4的)对应的局部符号的名称不相同
$ cat compile_error3.cpp 
static int value = 1;
static double value = 2;

static int func(int val)

  static int value = 3;
  return val + value;


static void func(int val)

  static int value = 4;

# 编译器会报错
$ g++ -S compile_error3.cpp -o compile_error3.s
compile_error3.cpp:2:15: error: conflicting declaration ‘double value’
 static double value = 2;
               ^~~~~
compile_error3.cpp:1:12: note: previous declaration as ‘int value’
 static int value = 1;
            ^~~~~
compile_error3.cpp: In function ‘void func(int)’:
compile_error3.cpp:10:13: error: ambiguating new declaration of ‘void func(int)’
 static void func(int val)
             ^~~~
compile_error3.cpp:4:12: note: old declaration ‘int func(int)’
 static int func(int val)
            ^~~~

7)验证“如果一个局部符号引用在同一个目标模块内既没有声明也没有定义,那么编译器会报错,不会进行后面的汇编和链接过程。”

# 一个局部符号引用在同一个目标模块内既没有声明也没有定义
# 局部函数 func 在引用变量 value 的前面没有 value 变量的声明或定义
$ cat compile_error4.cpp 
static int func(int val)

  return val + value;


static int value = 1;
# 编译器会报错
$ g++ -S compile_error4.cpp -o compile_error4.s
compile_error4.cpp: In function ‘int func(int)’:
compile_error4.cpp:3:16: error: ‘value’ was not declared in this scope
   return val + value;
                ^~~~~

8)验证“如果一个局部符号引用在同一个目标模块内只有声明但没有定义,那么编译器会报错,不会进行后面的汇编和链接过程。”

# 一个局部符号引用在同一个目标模块内只有声明但没有定义
$ cat compile_error5.cpp 
static int func2(int);

static int func(int val)

  return val + func2(val);

# 编译器会报错
$ g++ -S compile_error5.cpp -o compile_error5.s
compile_error5.cpp:1:12: warning: ‘int func2(int)’ used but never defined
 static int func2(int);
            ^~~~~

链接器如何解析重复的全局符号名称

编译器 cc1 将每个全局符号标记为强符号(Strong Symbol)或弱符号(Weak Symbol),汇编器将该标记信息隐式地编码在可重定位目标文件的符号表中(即用 READELF 查看符号表条目中的 Ndx 列的值为 COM)。而编译器 cc1plus 将每个全局符号标记为强符号。

编译器 cc1 的默认行为:全局函数定义和已初始化的全局变量(包括初始值为0的全局变量)属于强符号,未初始化的全局变量属于弱符号。另外,默认行为可以通过-fno-common选项改变,更改后的行为:全局函数定义和全局变量(无论是否已初始化)都属于强符号。

编译器 cc1plus 的默认行为:全局函数定义和全局变量(无论是否已初始化)都属于强符号。另外,默认行为不能通过-fcommon选项改变。

Linux 链接器处理重复的全局符号名称的规则为:

  • 规则1:不允许有多个同名的全局强符号。

  • 规则2:如果有一个全局强符号和多个全局弱符号同名,那么选择全局强符号。

  • 规则3:如果有多个全局弱符号同名,那么选择这些全局弱符号中的任意一个。

情形编译驱动器源文件后缀实际调用的编译器外部添加的编译选项行为
情形1gcc.ccc1无(即编译器 cc1 的默认行为)
  • 全局函数定义和已初始化的全局变量(包括初始值为0的全局变量)属于强符号
  • 未初始化的全局变量属于弱符号。
情形2gcc.ccc1-fno-common全局函数定义和全局变量(无论是否已初始化)都属于强符号。
情形3gcc.cppcc1plus无(即编译器 cc1plus 的默认行为)全局函数定义和全局变量(无论是否已初始化)都属于强符号。
情形4g++.ccc1plus无(即编译器 cc1plus 的默认行为)全局函数定义和全局变量(无论是否已初始化)都属于强符号。
情形5g++.cppcc1plus无(即编译器 cc1plus 的默认行为)全局函数定义和全局变量(无论是否已初始化)都属于强符号。

1)验证“编译器 cc1 的默认行为:全局函数定义和已初始化的全局变量(包括初始值为0的全局变量)属于强符号,未初始化的全局变量属于弱符号。”

$ cat test1.c
int g_val_1 = 0;
int g_val_2;

void func()

  g_val_1 = 1;
  g_val_2 = 2;


$ cat main.c 
#include <stdio.h>

int g_val_1;
int g_val_2 = 3;

void func();

int main()

  func();
  printf("g_val_1=%d, g_val_2=%d\\n", g_val_1, g_val_2);
  return 0;


$ gcc -c test1.c -o test1.o
$ gcc -c main.c -o main.o
$ readelf -s test1.o

Symbol table '.symtab' contains 11 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS test1.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     8: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    4 g_val_1
     9: 0000000000000004     4 OBJECT  GLOBAL DEFAULT  COM g_val_2
    10: 0000000000000000    27 FUNC    GLOBAL DEFAULT    1 func

$ readelf -s main.o

Symbol table '.symtab' contains 14 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS main.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
     9: 0000000000000004     4 OBJECT  GLOBAL DEFAULT  COM g_val_1
    10: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 g_val_2
    11: 0000000000000000    50 FUNC    GLOBAL DEFAULT    1 main
    12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND func
    13: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf

$ gcc -o main main.c test1.c -v
#省略
COLLECT_GCC_OPTIONS='-o' 'main' '-v' '-mtune=generic' '-march=x86-64'
 /usr/lib/gcc/x86_64-linux-gnu/6/cc1
#省略

$ ./main
g_val_1=1, g_val_2=2

2)验证“编译器 cc1 添加 -fno-common 编译选项后的行为:全局函数定义和全局变量(无论是否已初始化)都属于强符号。”

$ cat test1.c
int g_val_1 = 0;
int g_val_2;

void func()

  g_val_1 = 1;
  g_val_2 = 2;


$ cat main.c 
#include <stdio.h>

int g_val_1;
int g_val_2 = 3;

void func();

int main()

  func();
  printf("g_val_1=%d, g_val_2=%d\\n", g_val_1, g_val_2);
  return 0;


$ gcc -c test1.c -o test1.o -fno-common
$ gcc -c main.c -o main.o -fno-common
$ readelf -s test1.o

Symbol table '.symtab' contains 11 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS test1.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     8: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    4 g_val_1
     9: 0000000000000004     4 OBJECT  GLOBAL DEFAULT    4 g_val_2
    10: 0000000000000000    27 FUNC    GLOBAL DEFAULT    1 func

$ readelf -s main.o

Symbol table '.symtab' contains 14 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS main.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
     9: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    4 g_val_1
    10: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 g_val_2
    11: 0000000000000000    50 FUNC    GLOBAL DEFAULT    1 main
    12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND func
    13: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf

$ gcc -o main main.o test1.o
test1.o:(.bss+0x0): multiple definition of `g_val_1'
main.o:(.bss+0x0): first defined here
test1.o:(.bss+0x4): multiple definition of `g_val_2'
main.o:(.data+0x0): first defined here
collect2: error: ld returned 1 exit status

3)验证“编译器 cc1plus 的默认行为:全局函数定义和全局变量(无论是否已初始化)都属于强符号。”

$ cat test1.cpp 
int g_val_1 = 0;
int g_val_2;

void func()

  g_val_1 = 1;
  g_val_2 = 2;


$ cat main.cpp
#include <stdio.h>

int g_val_1;
int g_val_2 = 3;

void func();

int main()

  func();
  printf("g_val_1=%d, g_val_2=%d\\n", g_val_1, g_val_2);
  return 0;


$ gcc -c test1.cpp -o test_cpp.o
$ gcc -c main.cpp -o main_cpp.o
$ readelf -s test_cpp.o

Symbol table '.symtab' contains 11 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS test1.cpp
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     8: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    4 g_val_1
     9: 0000000000000004     4 OBJECT  GLOBAL DEFAULT    4 g_val_2
    10: 0000000000000000    27 FUNC    GLOBAL DEFAULT    1 _Z4funcv

$ readelf -s main_cpp.o

Symbol table '.symtab' contains 14 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS main.cpp
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
     9: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    4 g_val_1
    10: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 g_val_2
    11: 0000000000000000    45 FUNC    GLOBAL DEFAULT    1 main
    12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _Z4funcv
    13: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf

$ gcc -o main main_cpp.o test_cpp.o
test_cpp.o:(.bss+0x0): multiple definition of `g_val_1'
main_cpp.o:(.bss+0x0): first defined here
test_cpp.o:(.bss+0x4): multiple definition of `g_val_2'
main_cpp.o:(.data+0x0): first defined here
collect2: error: ld returned 1 exit status

$ gcc -o main main.cpp test1.cpp -v
#省略
COLLECT_GCC_OPTIONS='-o' 'main' '-v' '-mtune=generic' '-march=x86-64'
 /usr/lib/gcc/x86_64-linux-gnu/6/cc1plus
#省略
/tmp/ccIVPplI.o:(.bss+0x0): multiple definition of `g_val_1'
/tmp/cc715sBV.o:(.bss+0x0): first defined here
/tmp/ccIVPplI.o:(.bss+0x4): multiple definition of `g_val_2'
/tmp/cc715sBV.o:(.data+0x0): first defined here
collect2: error: ld returned 1 exit status

链接器如何使用静态库来解析外部符号引用

Linux 链接器ld使用静态库来解析可重定位目标文件中外部符号引用的规则为:链接器从左到右按照可重定位目标文件和静态库在编译驱动器的命令行上出现的顺序依次扫描,在扫描的过程中维护三个集合:a)一个可重定位目标文件的集合E(这个集合中的文件会被合并起来生成可执行目标文件);b)一个未解析的符号(即引用了但是尚未定义的符号)集合U;c)一个在前面输入文件中已定义的符号集合D。初始时,集合EUD均为空。

链接器对每个输入文件的处理规则为:

  • 首先,链接器判断输入文件的类型。如果是可重定位目标文件,那么链接器直接将其添加到集合E中,并将该目标文件中的符号定义和外部符号引用分别更新到集合DU中,然后链接器继续处理下一个输入文件。如果是静态库,那么链接器尝试匹配集合U中未解析的符号和由该静态库中的可重定位目标文件中定义的符号。如果匹配成功,那么将静态库中所匹配的可重定位目标文件添加到集合E中,并将该目标文件中的符号定义和外部符号引用分别更新到集合DU中。对静态库中的每个可重定位目标文件都一次进行这个过程,直到集合UD都不再发生变化。此时,静态库中任何不包含在集合E中的可重定位目标文件都被直接丢弃,然后链接器继续处理下一个输入文件。

  • 当链接器扫描完所有的输入文件后,判断集合U是否为空。如果为空,链接器会输出错误信息并终止;否则,它会合并和重定位集合E中的可重定位目标文件,最终生成可执行目标文件。

上述 Linux 链接器ld所使用的规则会带来这样一个问题:如果定义一个符号的库出现在引用这个符号的目标文件之前,那么引用就不能被解析,链接会失败。因此,可重定位目标文件和静态库在编译驱动器命令行中出现的先后顺序就非常重要了。

关于库的一般准则是将它们放在编译驱动器命令行的末尾。对于有循环依赖的两个库而言,比如:liba.a 依赖 libb.a,libb.a 也依赖 liba.a,那么这两个库的在编译驱动器命令行中的先后顺序必须为:liba.a libb.a liba.alibb.a liba.a libb.a

符号解析引发的问题

编译器 cc1 的默认行为容易导致一些诡异的错误。比如:全局变量的值被意外地修改,可以分为两种情形:全局变量的值被同名的全局变量修改全局变量的值被不同名的全局变量修改

1)全局变量的值被同名的全局变量修改

$ cat test1.c
int g_val_1 = 0;
int g_val_2;

void func()

  g_val_1 = 1;
  g_val_2 = 2;


$ cat main.c 
#include <stdio.h>

int g_val_1;
int g_val_2 = 3;

void func();

int main()

  func();
  printf("g_val_1=%d, g_val_2=%d\\n", g_val_1, g_val_2);
  return 0;


$ gcc -c test1.c -o test1.o
$ gcc -c main.c -o main.o
$ gcc -o main test1.o main.o
$ ./main
g_val_1=1, g_val_2=2

main.c 中定义的全局变量 g_val_2 的值被 test1.c 中定义的 func() 函数由 3 改为 2 了。

2)全局变量的值被不同名的全局变量修改

$ cat test2.c 
double g_val_1;

void func()

  g_val_1 = 1.1;


$ cat main2.c 
#include <stdio.h>

int g_val_0 = 1;
int g_val_1 = 2;
int g_val_2 = 3;

void func();

int main()

  func();
  printf("g_val_0=%d, g_val_1=%d, g_val_2=%d\\n", g_val_0, g_val_1, g_val_2);
  return 0;

$ gcc -o main2 test2.c main2.c
/usr/bin/x86_64-linux-gnu-ld: Warning: alignment 4 of symbol `g_val_1' in /tmp/ccIc3noO.o is smaller than 8 in /tmp/ccXjyx8c.o
/usr/bin/x86_64-linux-gnu-ld: Warning: size of symbol `g_val_1' changed from 8 in /tmp/ccXjyx8c.o to 4 in /tmp/ccIc3noO.o

$ ./main2
g_val_0=1, g_val_1=-1717986918, g_val_2=1072798105

main2.c 中定义的全局变量 g_val_2 的值被意外地修改了。

如果你觉得本文对你有所帮助,欢迎关注公众号,支持一下!

以上是关于计算机系统篇之链接:静态链接(中)——符号解析的主要内容,如果未能解决你的问题,请参考以下文章

计算机系统篇之链接:静态链接(下)——重定位

计算机系统篇之链接:静态链接(上)

计算机系统篇之链接:静态链接(上)

计算机系统篇之链接:动态链接

深入理解计算机系统链接

计算机系统篇之链接:目标文件