Linux 使用 libtool 实现向后兼容性的共享库版本控制

Linux shared library versioning for backwards compatibility using libtool

我维护一个使用 libtool 的共享库,(主要)在 Linux 上运行并输出以下文件。

lrwxrwxrwx  1 root root    18 jun 10 16:12 libxxx.so -> libxxx.so.0.0.1
lrwxrwxrwx  1 root root    18 jun 10 16:12 libxxx.so.0 -> libxxx.so.0.0.1
-rwxr-xr-x  1 root root  760K jun 10 16:12 libxxx.so.0.0.1

libtool version-info 目前是 0:1:0

我想向库的 API/ABI 添加功能,而不是删除或修改任何现有的 API/ABI,以便:

如何使用 libtool 实现此目的?

我尝试按照建议将版本信息设置为 1:0:1 here

Programs using the previous version may use the new version as drop-in replacement, but programs using the new version may use APIs not present in the previous one. In other words, a program linking against the new version may fail with “unresolved symbols” if linking against the old version at runtime: set revision to 0, bump current and age.

这会生成以下文件:

rwxrwxrwx  1 root root    18 jun 10 16:24 libxxx.so -> libxxx.so.0.1.0
lrwxrwxrwx  1 root root    18 jun 10 16:24 libxxx.so.0 -> libxxx.so.0.1.0
-rwxr-xr-x  1 root root  760K jun 10 16:24 libxxx.so.0.1.0

但是,如果在运行时输入包含旧库中不存在的新符号之一的代码路径,则针对新库构建的二进制文件将加载并在运行时失败并出现 undefined symbol 错误。

我可以将 SONAME 增加到 libxxx.so.1 但随后我破坏了针对旧版本构建的二进制文件,而新版本仍然兼容。

However, binaries built against the new library will load and will then fail during runtime with an undefined symbol error if at runtime they enter a codepath that includes one of the new symbols not present in the old library.

TL;DR:我认为现在 没有一个解决方案可以达到预期的结果(除非您已经在使用版本化符号),但您可以做到现在好多了,下次可以彻底修复


这是一个由 GNU symbol version extension 解决的问题。

最好有一个例子。初始设置:

// foo_v1.c
int foo() { return 42; }

// main_v1.c
int main() { return foo(); }

gcc -fPIC -shared -o foo.so foo_v1.c
gcc -w main_v1.c ./foo.so -o main_v1

./main_v1; echo $?
42

现在让我们修改 foo.c 使其成为一个直接替代品,但具有新功能:

mv foo.so foo.so.v1

// foo_v2.c
int foo() { return 42; }
int bar() { return 24; }

gcc -fPIC -shared -o foo.so foo_v2.c

./main_v1; echo $?
42

一切正常(如预期)。现在让我们构建需要新功能的 main_v2

// main_v2.c
int main() { return foo() - bar(); }

gcc -w main_v2.c ./foo.so -o main_v2

./main_v2; echo $?
18

一切正常。现在我们打破东西:

mv foo.so foo.so.v2
cp foo.so.v1 foo.so
./main_v1; echo $?
42
./main_v2
./main_v2: symbol lookup error: ./main_v2: undefined symbol: bar

瞧:我们在 运行 时出现故障,而不是在加载时出现(预期的)故障。 (这在上面的输出中实际上是不可见的,但可以通过在 main 中添加例如 printf 来简单地验证。)

解法: 让我们给foo.so一个版本脚本:

// foo.lds
FOO_v2 {
  global: bar;
};

gcc -fPIC -shared -o foo.so foo_v2.c -Wl,--version-script=foo.lds
gcc -w main_v2.c ./foo.so -o main_v2a

./main_v1; echo $?
42
./main_v2a; echo $?
18

正如我们所见,一切仍然适用于新版本的库。

但是当我们 运行 新的 main_v2a 对抗旧的 foo.so 时会发生什么?

mv foo.so foo.so.v2
cp foo.so.v1 foo.so
./main_v2

./main_v2a: ./foo.so: no version information available (required by ./main_v2a)
./main_v2a: symbol lookup error: ./main_v2a: undefined symbol: bar, version FOO_v2

这稍微好一点:失败 仍然 发生在 运行 时间,但加载器确实提到了 FOO_v2,提供了一个提示是由某种“版本太旧”问题引起的。

加载程序在加载时不会失败的原因是(旧的)foo.so 没有任何版本信息。

如果您现在重复此过程,使用新函数创建 FOO_v3,并尝试 运行 main_v3 针对库的 foo.so.v2 版本,您会加载时失败:

// foo_v3.c
int foo() { return 42; }
int bar() { return 24; }
int baz() { return 12; }

// foo_v3.lds
FOO_v2 {
  global: bar;
};
FOO_v3 {
  global: baz;
} FOO_v2;

// main_v3.c
int main() { return foo() - bar() - baz(); }

gcc -fPIC -shared -o foo.so foo_v3.c -Wl,--version-script=foo_v3.lds
gcc -w main_v3.c ./foo.so -o main_v3

./main_v3; echo $?
6

现在 运行 main_v3 对抗 foo.so.v2:

cp foo.so.v2 foo.so

./main_v3
./main_v3: ./foo.so: version `FOO_v3' not found (required by ./main_v3)

这一次,失败发生在加载时。 QED.