问题描述
我维护一个共享库,它使用 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 实现此目的?
我尝试按照建议将版本信息设置为 1:0:1 here
使用旧版本的程序可能会使用新版本作为直接替换,但使用新版本的程序可能会使用前一个版本中不存在的 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
,但随后我破坏了针对旧版本构建的二进制文件,而新版本仍然兼容。
解决方法
但是,如果在运行时输入的代码路径包含旧库中不存在的新符号之一,则针对新库构建的二进制文件将加载并在运行时失败并显示未定义符号错误。
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
瞧:我们在运行时出现故障,而不是在加载时出现(预期的)故障。 (这在上面的输出中实际上并不可见,但可以通过在 printf
中添加例如 main
来验证。)
解决方案:
让我们给 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。