【问题标题】:Linux shared library versioning for backwards compatibility using libtool使用 libtool 实现向后兼容性的 Linux 共享库版本控制
【发布时间】:2021-06-11 16:35:08
【问题描述】:

我维护一个使用 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,以便:

  • 我生成了一个库,该库仍可供针对旧版本库构建的二进制文件使用。因此,新库充当了替代品,无需重新构建旧的二进制文件。
  • 当找不到包含新 API 的库时,针对新库构建并使用新 API/ABI 的二进制文件在加载阶段失败。

如何使用 libtool 实现这一点?

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

使用旧版本的程序可以使用新版本作为替代,但使用新版本的程序可以使用旧版本中不存在的 API。换句话说,如果在运行时链接到旧版本,链接到新版本的程序可能会失败并出现“未解析的符号”:将修订设置为 0,增加当前和年龄。

这会产生以下文件:

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,但是我破坏了针对旧版本构建的二进制文件,而新版本仍然兼容。

【问题讨论】:

  • "如果在运行时遇到旧库,针对新库编译的二进制文件将在运行时失败并出现未定义符号错误。" -- 这不是正是你要求的吗?
  • 不,他们遇到符号时崩溃,我希望他们在加载时失败。
  • 它们不会崩溃,它们会失败(在加载阶段)。我认为您需要确切地说明您想要发生的事情。
  • 现在@EmployedRussian 更清楚了吗?
  • 哦,我想我明白了。您在运行时遇到故障(调用新符号时),而您希望在加载时发生故障?

标签: linux shared-libraries libtool


【解决方案1】:

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

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

正如我们所见,新版本的库仍然可以正常工作。

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

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,并尝试针对 foo.so.v2 版本的库运行 main_v3,您将在加载时失败:

// 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。

【讨论】:

  • 谢谢!仅供参考,我的一位同事也建议使用 -Wl,-z,relro,-z,now 链接器选项,这需要立即解析所有符号,而不是延迟解析。
  • @MathieuBorderé 哦,对了:如果您控制主可执行文件的链接命令行,-Wl,-z,now 也会导致加载时失败(但可能会导致启动时间的巨大成本)。
猜你喜欢
  • 2013-02-14
  • 2020-05-25
  • 1970-01-01
  • 1970-01-01
  • 2019-04-16
  • 1970-01-01
  • 2011-01-28
  • 1970-01-01
  • 2011-07-11
相关资源
最近更新 更多