omer levin
omer levin

Reputation: 23

ALSA unexpected results when called from shared library

The ALSA lib contains two API versions, enabled by defining ALSA_PCM_OLD_HW_PARAMS_API for accessing the older one. It employs some advanced trickery (using the .symver assembly directive) to enable a single C library to contain different functions, with the same name but different arguments (for the old and new API). This is all and well, but causes trouble in certain circumstances.

As an example, let's create two source files. The first is 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();
}

The second one is 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);
}

Note that the contents of local_func() and lib_func() are identical except for the printed message.

On a Linux box (we tested Ubuntu 12/gcc 4.6.3 and Ubuntu 14/gcc 4.8.4) build and run with:

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

The result we get when running is:

err out of lib: 0
err in lib: 192000

This means that snd_pcm_hw_params_set_rate_near is behaving differently between the two code modules. In the shared library it is mistakenly calling the old version of the function, which expects an unsigned int val for the sample rate as opposed to the new which expects unsigned int *val, and returns a sample rate (192000, since it didn't accept our input) instead of an error code.

We've found a workaround for this problem: add the -lasound argument to the linker when creating the shared library. However, this is still a bug, where some users (such as this one, whom we believe had this exact problem: http://www.linuxquestions.org/questions/programming-9/snd_pcm_hw_params_set_rate_near-returns-huge-value-900199/ ) can run into cases where a program compiles and links with no errors or warnings and yet incorrect behavior occurs.

Can someone explain what is going on here, and perhaps this problem could be acknowledged as a bug and fixed?

Upvotes: 2

Views: 3313

Answers (1)

ninjalj
ninjalj

Reputation: 43748

This is by design. If you don't add -lasound when linking libmylib.so, the linker cannot see symbol versions, so it adds non-versioned references to undefined symbols. When the runtime linker binds non-versioned symbols, it tries to use the earliest version.


ALSA has the following definitions for 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

There is an older ALSA_0.9 version, and a newer ALSA_0.9.0rc4, the latter marked as default (@@), which will be used when the static linker (ld) links against -lasound.

When ld links libmylib.so without -lasound, libmylib.so ends up having an undefined and non-versioned reference to 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

While a.out, which has been linked against -lasound, contains a reference against the default version:

$ 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

Then, when the runtime linker (ld.so) uses this information, it ends up binding different versions of snd_pcm_hw_params_set_rate_near for libmylib.so and a.out:

$ 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]

This behavior is documented. From Ulrich Drepper's 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.

The referenced document, in "Symbol lookup", then goes on to explain that when trying to bind a non-versioned reference to a versioned definition, it tries the following:

  • It tries the BASE version, at index 1 in the table of version definitions in the ELF file.
  • It tries the baseline version at index 2, i.e. the first version used when the file started used symbol versions.
  • Otherwise, if the symbol is only defined for a version, that version of the symbol is used.

The table of version definitions looks like this:

$ 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
  [...]

The linker flag -z,defs|--no-undefined can be used to disallow unresolved symbols while linking:

$ 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

Upvotes: 4

Related Questions