从共享库调用时出现 ALSA 意外结果

ALSA unexpected results when called from shared library

ALSA 库包含两个 API 版本,通过定义 ALSA_PCM_OLD_HW_PARAMS_API 来访问旧版本。它使用一些高级技巧(使用 .symver 汇编指令)使单个 C 库包含不同的函数,具有相同的名称但不同的参数(对于旧的和新的 API)。这很好,但在某些情况下会造成麻烦。

例如,我们创建两个源文件。第一个是 main.cpp:

#include <alsa/asoundlib.h>

void lib_func();

void local_func()
{
    int err;
    unsigned int rate = 22050;
    snd_pcm_t *handle;
    snd_pcm_hw_params_t *params;
    snd_pcm_hw_params_alloca(&params);
    assert(snd_pcm_open(&handle, "default", snd_pcm_stream_t(0), 0) >= 0);
    assert(snd_pcm_hw_params_any(handle, params) >= 0);
    err = snd_pcm_hw_params_set_rate_near(handle, params, &rate, 0);
    printf("err out of lib: %d\n", err);
    snd_pcm_close(handle);
}

int main(int argc, char *argv[])
{
    local_func();
    lib_func();
}

第二个是mylib.cpp:

#include <alsa/asoundlib.h>

void lib_func()
{
    int err;
    unsigned int rate = 22050;
    snd_pcm_t *handle;
    snd_pcm_hw_params_t *params;
    snd_pcm_hw_params_alloca(&params);
    assert(snd_pcm_open(&handle, "default", snd_pcm_stream_t(0), 0) >= 0);
    assert(snd_pcm_hw_params_any(handle, params) >= 0);
    err = snd_pcm_hw_params_set_rate_near(handle, params, &rate, 0);
    printf("err in lib: %d\n", err);
    snd_pcm_close(handle);
}

请注意 local_func()lib_func() 的内容除了打印的消息外完全相同。

在 Linux 盒子上(我们测试了 Ubuntu 12/gcc 4.6.3 和 Ubuntu 14/gcc 4.8.4)构建和 运行 使用:

g++ -shared -fPIC -o libmylib.so mylib.cpp && g++ main.cpp -lasound -L . -lmylib
LD_LIBRARY_PATH=. ./a.out

我们在运行ning时得到的结果是:

err out of lib: 0
err in lib: 192000

这意味着 snd_pcm_hw_params_set_rate_near 在两个代码模块之间的行为不同。在共享库中,它错误地调用了旧版本的函数,它期望采样率 unsigned int val 而新版本期望 unsigned int *val 和 returns 采样率( 192000,因为它不接受我们的输入)而不是错误代码。

我们找到了解决此问题的方法:在创建共享库时将 -lasound 参数添加到链接器。然而,这仍然是一个错误,一些用户(比如这个,我们认为他有这个确切的问题:http://www.linuxquestions.org/questions/programming-9/snd_pcm_hw_params_set_rate_near-returns-huge-value-900199/)可以 运行 进入程序编译和链接没有错误或警告的情况然而不正确的行为发生了。

谁能解释一下这是怎么回事,也许这个问题可以确认为错误并修复?

这是设计使然。如果在链接 libmylib.so 时不添加 -lasound,链接器将看不到符号版本,因此它会向未定义的符号添加非版本化引用。当运行时链接程序绑定非版本化符号时,它会尝试使用最早的版本。


ALSA 对 snd_pcm_hw_params_set_rate_near 的定义如下:

$ readelf -Ws /usr/lib64/libasound.so.2 | grep set_rate_near
   255: 0000000000062640    72 FUNC    GLOBAL DEFAULT   12 snd_pcm_hw_params_set_rate_near@ALSA_0.9
   256: 000000000005d140    61 FUNC    GLOBAL DEFAULT   12 snd_pcm_hw_params_set_rate_near@@ALSA_0.9.0rc4
  1115: 000000000005d140    61 FUNC    GLOBAL DEFAULT   12 __snd_pcm_hw_params_set_rate_near@@ALSA_0.9

有一个较旧的ALSA_0.9版本,和一个较新的ALSA_0.9.0rc4版本,后者标记为默认(@@),当静态链接器(ld) 针对 -lasound.

的链接

ld 链接 libmylib.so 而没有 -lasound 时,libmylib.so 最终有一个未定义且 非版本化 的引用snd_pcm_hw_params_set_rate_near:

$ readelf -Ws libmylib.so | grep set_rate_near
     3: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND snd_pcm_hw_params_set_rate_near
    31: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND snd_pcm_hw_params_set_rate_near

虽然 a.out 已链接到 -lasound,但包含对默认版本的引用:

$ readelf -Ws a.out | grep set_rate_near
    15: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND snd_pcm_hw_params_set_rate_near@ALSA_0.9.0rc4 (5)
    56: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND snd_pcm_hw_params_set_rate_near@@ALSA_0.9.0rc4

然后,当运行时链接器 (ld.so) 使用此信息时,它最终会为 libmylib.soa.out 绑定不同版本的 snd_pcm_hw_params_set_rate_near:

$ LD_DEBUG=bindings LD_LIBRARY_PATH=. ./a.out 2>&1 | grep set_rate_near
     11364:     binding file ./libmylib.so [0] to /usr/lib64/libasound.so.2 [0]: normal symbol `snd_pcm_hw_params_set_rate_near'
     11364:     binding file /usr/lib64/libasound.so.2 [0] to /usr/lib64/libasound.so.2 [0]: normal symbol `__snd_pcm_hw_params_set_rate_near' [ALSA_0.9]
     11364:     binding file ./a.out [0] to /usr/lib64/libasound.so.2 [0]: normal symbol `snd_pcm_hw_params_set_rate_near' [ALSA_0.9.0rc4]

此行为已记录在案。来自 Ulrich Drepper 的 DSO howto §3.8:

All methods which depend on symbol versioning have one requirement in common: it is absolutely necessary for the users of the DSO to always link with it.

[...]

The problem is that unless the DSO containing the definitions is used at link time, the linker cannot add a version name to the undefined reference. Following the rules for symbol versioning [4] this means the earliest version available at runtime is used which usually is not the intended version.

"Symbol lookup" 中引用的 document 然后继续解释说,当尝试将非版本化引用绑定到版本化定义时,它会尝试以下操作:

  • 它尝试 BASE 版本,在 ELF 文件中 table 版本定义的索引 1。
  • 它尝试索引 2 处的基线版本,即文件启动时使用的第一个版本使用符号版本。
  • 否则,如果符号仅为某个版本定义,则使用该版本的符号。

版本定义的 table 如下所示:

$ readelf -a /usr/lib64/libasound.so.2 | awk '/Version.*.gnu.version_d/,/^$/'
Version definition section '.gnu.version_d' contains 8 entries:
  Addr: 0x000000000001b470  Offset: 0x01b470  Link: 4 (.dynstr)
  000000: Rev: 1  Flags: BASE   Index: 1  Cnt: 1  Name: libasound.so.2
  0x001c: Rev: 1  Flags: none  Index: 2  Cnt: 1  Name: ALSA_0.9
  0x0038: Rev: 1  Flags: none  Index: 3  Cnt: 2  Name: ALSA_0.9.0rc4
  0x0054: Parent 1: ALSA_0.9
  [...]

链接器标志 -z,defs|--no-undefined 可用于在链接时禁止未解析的符号:

$ g++ -Wl,-z,defs -shared -fPIC -o libmylib.so mylib.cpp 
/tmp/ccfJdVDG.o: In function `lib_func()':
mylib.cpp:(.text+0x20): undefined reference to `snd_pcm_hw_params_sizeof'
mylib.cpp:(.text+0x4b): undefined reference to `snd_pcm_hw_params_sizeof'
mylib.cpp:(.text+0x7c): undefined reference to `snd_pcm_open'
mylib.cpp:(.text+0xb2): undefined reference to `snd_pcm_hw_params_any'
mylib.cpp:(.text+0xf1): undefined reference to `snd_pcm_hw_params_set_rate_near'
mylib.cpp:(.text+0x116): undefined reference to `snd_pcm_close'
collect2: ld returned 1 exit status