10

我在使用包含弱符号和--as-needed链接器标志的库时遇到问题。

例子

(这使用Jack库)

$ cat <<EOF >myjack.c
#include <jack/weakjack.h>
#include <jack/jack.h>
int main() {
  if (jack_client_opent)
     jack_client_open("foobar", JackNoStartServer, 0, 0);
  else return 1;
  return 0;
}
EOF

$ gcc -o myjack myjack.c  -Wl,--no-as-needed -ljack

$ ./myjack && echo "ok" || echo "K.O."
ok

$ ldd myjack | grep jack
    libjack.so.0 => /usr/lib/x86_64-linux-gnu/libjack.so.0 (0x00007f16f615f000)

$ gcc -o myjack myjack.c  -Wl,--as-needed -ljack

$ ./myjack && echo "ok" || echo "K.O."
K.O.

$ ldd myjack | grep jack

$

(示例代码被编辑为不再出现段错误,因为段错误不是我的实际问题)

问题

看来问题是:

  • Jack将所有符号声明为符号(如果我包含<jack/weakjack.h>)。这对我来说很好;我确实希望我的符号保持弱。尤其是 我的程序与 OSX ( -weak_framework Jackmp) 上的 jack 链接很弱,这需要包括<jack/weakjack.h>

  • 与 链接时--as-needed,链接器会排除任何不引用至少一个非弱符号的库。从手册页:

--as-needed 导致 DT_NEEDED 标记仅针对链接中的该点满足来自常规目标文件的非弱未定义符号引用的库发出

  • 某些操作系统(例如 Ubuntu-16.04LTS)已--as-needed默认启用。

现在我认为这--as-needed是一个很好的链接器功能,可以摆脱许多真正不需要的运行时依赖项。

但是,我不明白为什么依赖被认为根本没有依赖。对我来说,依赖是启用可选功能。如果可能,我确实希望启用这些功能,并且是否可能的决定应该是运行时决定。对于当前的行为,它变成了编译时的决定。(如果我想要,我只需通过一些预处理器魔法禁用相关代码)。

一种解决方案显然是添加--no-as-needed到链接器标志。我不希望这样:如果我的发行版(或编译我的二进制文件的人)认为这是要做的事情,我确实想摆脱过度链接。

所以我可能会在as-needed链接我已知的弱库后打开:

  gcc -o myjack myjack.c  -Wl,--no-as-needed -ljack -Wl,--as-needed ...

但这也感觉不对,因为在我强制需要的库之后的所有库都突然被迫这样做--as-needed(这可能不是我的发行版或编译我的二进制文件的人认为这是要做的事情)。它似乎也给构建链增加了很多麻烦,只是因为某些库恰好只导出弱符号。我不想手动跟踪所有这样做的库。

我当然也可以包括<jack/weakjack.h>. 之所以包含它,是因为该应用程序也可以在 OSX 上运行,我确实希望在其中选择依赖JACK 框架(所以我链接到-weak_framework Jackmp),并在没有该框架的情况下保持我的程序可运行。

我真的不想因为不同平台上的链接器之间的细微差别而弄乱我的应用程序代码。这可能是我遇到的主要问题:为什么我应该在的应用程序中添加特定于平台的代码来满足不同的链接器细节 - 我可能会添加特定于功能的代码,例如不包括weakjack.h如果编译器没有等价的-weak_libraryor -weak_framework; 但目前看来,我能得到的最接近的是#ifdef __APPLE__在这种情况下让我不寒而栗的东西)。

所以我真的很喜欢一些选项来强制只有弱符号的库仍然被动态链接。

有这样的事吗?

4

1 回答 1

17

我在使用包含弱符号和 --as-needed 链接器标志的库时遇到问题。

不你不是。

找出你libjack.so的位置,例如

$ locate libjack
/usr/lib/x86_64-linux-gnu/libjack.so
/usr/lib/x86_64-linux-gnu/libjack.so.0
/usr/lib/x86_64-linux-gnu/libjack.so.0.1.0
...

然后用于nm检查 JACK API 的符号类型libjack.so

$ nm -C -D /usr/lib/x86_64-linux-gnu/libjack.so | grep jack_
000000000000e7e0 T jack_acquire_real_time_scheduling
000000000000d530 T jack_activate
000000000002ccf0 T jack_client_close
000000000000e820 T jack_client_create_thread
....
....
000000000000f340 T jack_uuid_empty
000000000000f320 T jack_uuid_parse
000000000000f2e0 T jack_uuid_to_index
000000000000f330 T jack_uuid_unparse

您会发现它们都是类型T(= 文本部分中的普通全局符号:)man nm库中有 一些弱符号:

$ nm -C -D /usr/lib/x86_64-linux-gnu/libjack.so | egrep ' (w|W) '
                 w __cxa_finalize
                 w __gmon_start__
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
                 w _Jv_RegisterClasses
0000000000025410 W std::ctype<char>::do_widen(char) const
0000000000014c10 W void std::vector<unsigned short, std::allocator<unsigned short> >::_M_emplace_back_aux<unsigned short const&>(unsigned short const&)
0000000000014b10 W std::pair<std::_Rb_tree_iterator<unsigned short>, bool> std::_Rb_tree<unsigned short, unsigned short, std::_Identity<unsigned short>, std::less<unsigned short>, std::allocator<unsigned short> >::_M_insert_unique<unsigned short>(unsigned short&&)
0000000000014ad0 W std::_Rb_tree<unsigned short, unsigned short, std::_Identity<unsigned short>, std::less<unsigned short>, std::allocator<unsigned short> >::_M_erase(std::_Rb_tree_node<unsigned short>*)

但它们都不在 JACK API 中。除了重建你的东西​​之外,你无能为力libjack.so。描述问题的正确方法是:

我在将带有--as-needed链接器标志的库链接到我决定削弱对该库的所有引用的程序时遇到问题

JACK API 的定义符号引用libjack.so都很强大。您已经编写了一个程序,指示编译器在您的目标代码中发出对 JACK API 的弱未定义引用的符号,并且您发现,通过按需链接,这些弱引用无法强制链接libjack.so提供它们的缺少定义。

看来问题是:

jack 将所有符号声明为弱符号(如果我包含 )。

当使用 --as-needed 链接时,链接器会排除任何不引用至少一个非弱符号的库。

某些操作系统(例如 Ubuntu-16.04LTS)默认启用了 --as-needed。

最后两点是对的。默认情况下根据需要链接共享库的发行版与不回到 Debian Wheezy, 2013 的发行版之间的分裂 ,后者转到了as-needed。从那时起,源自 Debian 的发行版家族也纷纷效仿,而 RedHat/Fedora 家族则一直坚持现状

第一点很混乱。libjack.so,正如我们已经注意到的,导出一个强定义的 JACK API,你不能通过编写和编译新代码来改变它。如果您包含<jack/weakjack.h>在您的源文件之一中,那么将在您的代码中声明所有 JACK API 符号为弱,并且编译器将为您提供一个仅包含对 JACK API 的弱引用的目标文件。<jack/weakjack.h> 只定义具有这种效果的宏。

如果一个像旧的和主要的 linux 库这样的旧的和主要的 linux 库libjack 无法适应所需的分裂,那将是令人惊讶的。我怀疑您忽略了一些关于以下内容的小字jack/weakjack.h

详细说明

开发人员面临的一个挑战是利用 [JACK] 新版本中引入的新功能,同时仍支持旧版本的系统。通常,如果应用程序使用库/API 中的新功能,则无法在不支持该功能的早期版本的库/API 上运行。当尝试使用该功能时,此类应用程序将无法启动或崩溃。这个问题可以使用弱链接符号来解决。

...

一个具体的例子会有所帮助。假设有人使用我们称为“Jill”的 JACK 客户端版本。Jill 与包含 API 的更新部分(例如 jack_set_latency_callback())的 JACK 版本相关联,并且如果可用,希望使用它。

当 Jill 在具有适当“新”版本 JACK 的系统上运行时,此功能将完全正常可用。但是,如果 Jill 在具有旧版本 JACK 的系统上运行,则该功能不可用。

使用正常的符号链接,每当有人尝试使用“旧”版本的 JACK 运行 Jill 时,都会产生启动错误。但是,在 0.116.2 版本之后添加到 JACK 的函数都被声明为具有“弱”链接,这意味着它们的缺失不会在程序启动期间导致错误。相反,Jill 可以测试符号 jack_set_latency_callback 是否为空。如果为空,说明本机安装的JACK太旧,不支持该功能。如果它不为 null,那么 Jill 可以像使用 API 中的任何其他函数一样使用它。例如:

if (jack_set_latency_callback) {
    jack_set_latency_callback (jill_client, jill_latency_callback, arg);
}

但是,有些客户可能希望对 0.116.2 之前的 JACK API 部分使用这种方法。例如,他们可能想查看 API 的非常古老的基本部分(如 jack_client_open())是否在运行时存在。

此类客户端应在任何其他 JACK 标头之前包含 <jack/weakjack.h>这将使整个 JACK API 受到弱链接,因此 可以在运行时检查任何和所有函数是否存在。重要的是要 了解很少有客户需要这样做 - 如果您使用此功能,您 应该有明确的理由这样做。

[重点补充]

这清楚地表明,像您这样的程序,jack/weakjack.h为了削弱其对整个 JACK API 的引用而采取了包含特殊步骤的特殊步骤,只有在它在引用它之前测试每个 JACK API 符号的定义性时才能预期成功运行。并处理未定义的情况。你的程序不符合。这个可以:

myjack1.c

#include <jack/weakjack.h>
#include <jack/jack.h>
#include <stdio.h>

int main() {
    if (jack_client_open) { 
        jack_client_open("foobar", JackNoStartServer, 0, 0);
    } else {
        puts("`jack_client_open` is not available");
    }
    return 0;
}

做这个:

myjack2.c

#include <jack/weakjack.h>
#include <jack/jack.h>
#include <dlfcn.h>
#include <stdlib.h>
#include <stdio.h>

int main() {
    jack_client_t * (*jack_client_open_fp)
        (const char *, jack_options_t,jack_status_t *,...) = jack_client_open;

    if (!jack_client_open_fp) {
        void * dsoh = dlopen("libjack.so",RTLD_LAZY);
        if (!dsoh) {
            fputs("`libjack` is not available\n",stderr);
            exit(EXIT_FAILURE);
        }
        *(void**)(&jack_client_open_fp) = dlsym(dsoh,"jack_client_open");
        if (!jack_client_open_fp) {
            fputs("`jack_client_open` is not available\n",stderr);
            exit(EXIT_FAILURE);
        }
    }
    jack_client_open_fp("foobar", JackNoStartServer, 0, 0);
    exit(EXIT_SUCCESS);
}

它勾勒了可发现 API 的常用方法——适用于旨在在可能根本不提供的系统上安装和运行的程序libjack。因此,您无需参考以下内容即可构建它libjack

gcc -o myjack2 myjack2.c -ldl

在 Ubuntu 17.04 上——它确实提供libjack了——它可能会像这样运行:

$ ./myjack2 
Cannot connect to server socket err = No such file or directory
Cannot connect to server request channel
jack server is not running or cannot be started
JackShmReadWritePtr::~JackShmReadWritePtr - Init not done for 4294967295, skipping unlock
JackShmReadWritePtr::~JackShmReadWritePtr - Init not done for 4294967295, skipping unlock

因此,图书馆的 T&C 在按需链接方面处于良好状态。这似乎让您独立地不满意按需链接的工作方式,而不是以不同的方式让您削弱对 JACK API 的所有引用并且仍然需要libjack您的对其 API 符号的弱引用:-

我不明白为什么弱依赖被认为根本没有依赖。对我来说,弱依赖是启用可选功能。如果可能,我确实希望启用这些功能,并且是否可能的决定应该是运行时决定。对于当前的行为,它变成了编译时的决定。

您认为弱符号引用会导致对定义符号的库的链接依赖的观点对于 GNU 链接器没有立足点。如果程序的链接需要库提供的符号定义,则程序依赖于库;否则它不依赖于那个库:没有弱和强的依赖程度。(Darwin Mach-O 链接器确实支持同源区分)

弱符号,与默认和通常的类型相反,即{weak|strong} 符号{weakly|strongly} 引用符号的简写,因为同一个符号可能在多个链接器输入文件中被引用,有时或总是弱引用,有时或总是强引用。

强符号在链接中必须有一个定义引用。

弱符号是这样的:

  • 链接器没有义务为它找到定义:它可能在输出文件中保持未定义

  • 链接器没有义务对不同输入文件中同一符号的多个弱定义进行故障排除。如果链接中恰好有一个定义引用是强的,则选择该强定义并忽略所有弱定义。如果链接中的所有定义引用都很弱,则链接器将随机选择一个。

从第一部分可以看出,对符号的未定义弱引用根本不会产生链接依赖。不需要定义, 不需要定义的事实是程序员(例如#include <jack/weak_jack.h>)或编译器决定的结果。期望链接器(如果被指示仅链接需要的共享库)然后应该链接库以提供您或编译器已告诉它不需要定义的符号定义是不合理

如果链接器在您的情况表现得像这样,将构成一个链接时间决定冻结和启用一个 API,通过包含jack/weak_jack.h,您表示您希望完全保留用于运行时发现。

将您的问题程序与之链接-no-as-needed是成功消除程序中错误的一种方式。错误在于,通过将jack/weak_jack.h 您自己承诺到整个 API 的运行时发现,但不履行该承诺,而是将 API 的可用性视为理所当然。因此,具有按需链接的段错误。链接 with-no-as-needed只是取消了 include 的效果jack/weak_jack.h。包含它表示您的程序 不需要任何 API 定义:-no-as-needed表示,无论它们是什么,无论如何您都会得到它们。

鉴于所有 JACK API 发布版本 0.116.2 的定义很弱而没有诉诸jack/weak_jack.h,我认为除非您确实正在计划一个可以在libjack缺少的主机。如果您 计划这样做,那么无论链接约定如何,您都无法在运行时发现您使用的所有 JACK API,因为 libjack无论如何您都无法链接。

如果不是,那么只需链接libjack,如果您只是调用jack_client_open,您的程序在任何主机上都会动态链接所有 API 定义,无论它们在该主机上是什么,因为您对jack_client_open(在没有 的情况下<jack/weak_jack.h>)的引用将libjack 需要,无论这对链接或不链接的链接器很重要。如果您希望跨 API 版本兼容,那么您需要实现运行时检测 ,如使用该属性记录 的任何 API 的记录JACK_WEAK_EXPORT - 而不是JACK_OPTIONAL_WEAK_EXPORT, or JACK_OPTIONAL_WEAK_DEPRECATED_EXPORT:后者表示只能通过<jack/weak_jack.h>.

于 2017-07-09T09:36:48.253 回答