Search code examples
cunit-testingshared-librariesdynamic-linkingweak-linking

weak linking in shared object not working as expected


I'm trying to use the cmocka unit test framework which suggests to use weak linking to be able to select a user-defined implementation over the actual implementation of a function. In my environment I have a shared object which I want to unit test. I've implemented the unit tests in a separate file which I compile and link to the shared object. My problem is that calling a function bar in the shared object which in turn calls a function foo in that shared object always leads to the real implementation of foo and not the custom one. I've created a simplified implementation of the shared object and of the unit test.

The shared library, a.c:

#include <stdio.h>

void foo(void); __attribute__((weak))
void bar(void); __attribute__((weak))

void foo(void) {
    printf("called real foo\n");
}

void bar(void) {
    printf("called real bar calling\n");
    foo();
}

The unit test, b.c:

#include <stdio.h>
#include <stdbool.h>

bool orig_foo;
bool orig_bar;

void __wrap_foo(void) {
    printf("in foo wrapper\n");
    if (orig_foo)
            __real_foo();
    else
            printf("called wrapped foo\n");
}

void __wrap_bar() {
    printf("in bar wrapper\n");
    if (orig_bar)
            __real_bar();
    else
            printf("called wrapped bar\n");
}

int main(void) {

    orig_bar = true;
    orig_foo = false;

    printf("calling foo from main\n");
    foo();

    printf("\n");

    printf("calling bar from main\n");
    bar();

    return 0;
}

And finally, the Makefile:

all: a.out

a.out: b.c a.so
    gcc -Wall b.c a.so -Wl,--wrap=foo -Wl,--wrap=bar

a.so: a.c
    gcc -Wall -c a.c -shared -o a.so

clean:
    rm -f a.so a.out

Running a.out produces the following output:

# ./a.out
calling foo from main
in foo wrapper
called wrapped foo

calling bar from main
in bar wrapper
called real bar
called real foo

From main, the direct call to foo results in __wrap_foo being called, as expected.

Next, I call bar from main which correctly results in __wrap_bar being called, where I redirect the call to the real implementation of bar (__real_bar). bar then calls foo but the real implementation is used, not the wrapped one. Why isn't the wrapped implementation of foo called in this case? It looks like the issue is related to from where the function call originates.

In function bar, if I replaced the called to foo with __wrap_foo I do get the expected behaviour however I don't think that this is an elegant solution.

I've managed to bypass this problem using normal linking and dlopen(3) and friends however I'm curious as to why weak linking isn't working in my case.


Solution

  • In bar() the linker does not store a reference to foo() but to an offset in the text section of the translation unit. Therefore the name is lost.

    The "weak" attribute does not help here.

    Also it does not help to use -ffunction_sections because the reference will be to an offset of the section of foo().

    One straight forward way to get the desired result is to separate all functions in their own translation unit. You don't need separated source files for this, some conditional compilation will also help. But it makes the source ugly.

    You might like to look into my answer to the question "Rename a function without changing its references", too.