文章
服务器与存储开发
作者:Darryl Gove 和 Stephen Clamage,2011 年 5 月
第 1 部分 — 库和链接简介
第 2 部分 — 解析库中的符号同一符号可能出现在多个库中。这种情况下,链接程序通常绑定到它遇到的该符号的第一个定义(尽管稍后我们将讨论直接绑定如何使不同的库绑定到同一符号名称的不同实例)。这可以通过修改测试程序使其包含重复符号来演示,如清单 1 所示。
清单 1:几个包括重复符号的库
$ more main.c
#include <stdio.h>
void f();
void main()
{
f();
printf("In main\n");
}
$ more lib1.c
#include <stdio.h>
void f()
{
printf("In library 1\n");
}
$ more lib2.c
#include <stdio.h>
void f()
{
printf("In library 2\n");
} |
如果在链接行中将 lib1.so 列在前面进行编译,符号 f() 将解析成该库中的定义。如果将 lib2.so 列在前面进行编译,它将是所执行的代码。如清单 2 所示。
$ cc -G -Kpic lib1.c -o lib1.so $ cc -G -Kpic lib2.c -o lib2.so $ cc -o main main.c -L. -R'$ORIGIN' -l1 -l2 $ ./main In library 1 In main $ cc -o main main.c -L. -R'$ORIGIN' -l2 -l1 $ ./main In library 2 In main |
注意,在链接时和运行时都没有警告。可以使用调试工具 lari 识别这些重复的符号,如清单 3 所示。
$ lari ./main [2:0]: f(): codes/library/lib1.so [2:1E]: f(): codes/library/lib2.so |
输出显示有两个名为 f() 的符号,在输出中通过该符号旁边的文字 [2:*] 来指示。绑定的是 lib2.so 中的定义,这由文字 [2:1E](字母 E 指示绑定来自外部对象)指示,而未绑定 lib1.so 中的定义,这由 [2:0] 指示。
另一种检查重复符号的办法是检查库定义了哪些符号。这可以使用 nm 实用程序搜索已定义的全局符号来完成,如清单 4 所示。带标志 -s 的实用程序 elfdump 也可用于报告符号信息。
% nm ./lib1.so|grep GLOB |grep -v UNDEF [47] | 66300| 0|OBJT |GLOB |0 |13 |_DYNAMIC [45] | 66452| 0|OBJT |GLOB |0 |15 |_edata [41] | 66452| 0|OBJT |GLOB |0 |16 |_end [40] | 690| 0|OBJT |GLOB |0 |10 |_etext [39] | 660| 12|FUNC |GLOB |0 |8 |_fini [48] | 66228| 0|OBJT |GLOB |0 |11 |_GLOBAL_OFFSET_TABLE_ [42] | 648| 12|FUNC |GLOB |0 |7 |_init [44] | 672| 4|OBJT |GLOB |0 |9 |_lib_version [38] | 66236| 0|OBJT |GLOB |0 |12 |_PROCEDURE_LINKAGE_TABLE_ [43] | 592| 56|FUNC |GLOB |0 |6 |f |
多重定义符号的真正问题在于链接程序可以自由使用它首先找到的任意一个库来解析该符号。这意味着应用程序的运行时行为可能根据首先加载的库的不同而发生变化。这是一个严重问题,我们在后面的部分中将会发现这一点。
另一个潜在问题就是循环依赖关系。具体来说就是一个库依赖于另一个库提供的功能,而另一个库依赖于第一个库提供的功能。这种循环依赖有两种形式。
预设循环依赖的主要问题是,如果在链接时不保留几个未解析的依赖项,就不可能链接到库。清单 5 中的应用程序在 lib1.so 与 lib2.so 之间存在循环依赖关系。在该代码中,lib1.so 依赖 lib2.so 提供 g2() 的定义,lib2.so 依赖 lib1.so 提供 g1() 的定义。
$ more main.c
#include <stdio.h>
void f1();
void main()
{
f1();
printf("In main\n");
}
$ more lib1.c
#include <stdio.h>
void g2();
void f1()
{
g2();
printf("In library 1\n");
}
void g1()
{
printf("Back in library 1\n");
}
$ more lib2.c
#include <stdio.h>
void g1();
void g2()
{
g1();
printf("In library 2\n");
} |
如果尝试使用 -z defs 警告未解析的依赖关系来链接 lib1.so 或 lib2.so,链接将失败。为了使编译继续,我们需要链接这两个库中的一个来允许未解析的符号。链接的过程如清单 6 所示。
$ cc -G -Kpic -o lib1.so lib1.c -z defs -L. -R'$ORIGIN' -lc Undefined first referenced symbol in file g2 lib1.o ld: fatal: symbol referencing errors. No output written to lib1.so $ cc -G -Kpic -o lib1.so lib1.c -L. -R'$ORIGIN' -lc $ cc -G -Kpic -o lib2.so lib2.c -z defs -L. -R'$ORIGIN' -l1 -lc $ cc -o main main.c -L. -R'$ORIGIN' -l1 -l2 $ ./main Back in library 1 In library 2 In library 1 In main |
存在循环依赖关系意味着在链接应用程序时不能使用最佳实践,这样可能会掩盖潜在的问题。应用程序运行时,循环依赖可能导致不能保证库的正确初始化顺序。
当应用程序由多个库(除系统库外)组成时,最佳做法是确保任意两个库(如 libA.so 和 libB.so)之间是相互独立的或位于不同的层次。也就是说,如果 libA.so 使用 libB.so 中的某项内容,libB.so 就不使用 libA.so 中的任何内容。而且,此层次结构规则必须扩展到混合体中的其他库。
使用 libA.so->libB.so 这种表示法意味着 libA.so 使用 libB.so 中的某项内容,假设我们有这样一组依赖关系:
libA.so->libB.so->libC.so libC.so->libA.so |
必须打破这种依赖关系的循环,可以通过确保 libC.so 不使用 libA.so 中的任何内容来实现。必须将库组织成一个或多个层次结构。在本示例中,必须有以下层次结构:
Layer 1: libA.so Layer 2: libB.so Layer 3: libC.so |
如果无法通过简单重构代码来避免循环,可能必须将公共代码提出来,放到另一个层级较低的库中。在本示例中,libC.so 使用 libA.so 中的某项内容。应提取此代码并将其放入一个新库 libD.so 中。
Layer 1: libA.so Layer 2: libB.so Layer 3: libC.so Layer 4: libD.so |
要求是 libD.so 不使用 libA.so、libB.so 或 libC.so 中的任何内容。
理想情况下,应从应用程序中提取出循环依赖关系。然而,还需要考虑的是生成过程和编译器可能会实际创建重复符号,导致在应用程序中引入循环依赖关系。这将在后文中讨论。
lari 工具来检测。修订版 1,2011 年 5 月 10 日 |