Reputation: 23
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(¶ms);
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(¶ms);
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
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:
BASE
version, at index 1 in the table of version definitions in the ELF file.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