Search code examples
clinkershared-libraries

Why does this shared library not have an expected dependency?


I'm trying to create a shared library, libfunc.so, that has a dependency on another shared library (specifically libuv.so, but I think that the specific library is not relevant to the question). I.e. when I run ldd libfunc.so, I want to see the dependency from libfunc.so to libuv.so.

This is the code I want to compile to libfunc.so:

#include <uv.h>

int func() {
  uv_timespec64_t now;

  uv_clock_gettime(UV_CLOCK_REALTIME, &now);

  return 0;
}

...I compile it like this:

$ cc --version && cc -fpic -ggdb -Wall -c -o func.o func.c
cc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ cc -shared -o libfunc.so func.o -fpic -ggdb -Wall -luv
$

...when I run ldd libfunc.so I do not see the desired dependency on libuv.so:

$ ldd ./libfunc.so
        linux-vdso.so.1 (0x00007fff827ae000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f14b291e000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f14b2b54000)
$

...i.e. I was wanting to see something along the lines of:

        linux-vdso.so.1 (0x00007ffcbbdca000)
        libuv.so.1 => /lib/x86_64-linux-gnu/libuv.so.1 (0x00007f781b25c000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f781b034000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f781b2a2000)

My question is: why doesn't a dependency on libuv.so show up, and what do I need to do to create that dependency?

My understanding is probably rudimentary and incomplete, but I thought that creating a dependency relationship was along the lines of 1) write code that calls a function from a (shared) library, 2) compile object code, 3) create (shared) library from object code while linking with a library that defines the missing symbol.

Inspecting libfunc.so, I do see expected undefined symbols for libuv symbols:

$ nm libfunc.so | grep U
0000000000002000 r __GNU_EH_FRAME_HDR
                 U __stack_chk_fail@GLIBC_2.4
                 U uv_clock_gettime
$

For some context, I'm trying to create a MVCE from larger project. Specifically, that larger project creates a shared library that has a dependency on libuv. When I run ldd on the larger project's shared library, though, it does show a dependency on libuv (that's where I got the output for the "desired" ldd output, above).

The larger project is too large for me to post here on Stack Overflow, but from inspecting its make output, I believe that my MCVE is compiling/linking with the same flags. E.g. a few compile lines and the link line from the larger project are:

cc -fpic -ggdb -Wall   -c -o file1.o file1.c
cc -fpic -ggdb -Wall   -c -o file2.o file2.c
cc -shared -o libplugin.so file1.o file2.o -fpic -ggdb -Wall -luv

(there are more files compiled that comprise libplugin.so, but the above subset conveys the gist of it -- the compile flags are uniform for all the compiled files)


Update: If I add a call to uv_close() in my shared library's code, the desired dependency relationship shows up! I.e.:

#include <uv.h>

int func() {
  uv_timespec64_t now;

  uv_clock_gettime(UV_CLOCK_REALTIME, &now);
  uv_close(NULL, NULL); // <= Adding this line causes the desired dependency to show up in ldd

  return 0;
}
$ cc -fpic -ggdb -Wall -c -o func.o func.c
$ cc -shared -o libfunc.so func.o -fpic -ggdb -Wall -luv
$ ldd ./libfunc.so
        linux-vdso.so.1 (0x00007ffc1e323000)
        libuv.so.1 => /lib/x86_64-linux-gnu/libuv.so.1 (0x00007f412b761000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f412b539000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f412b7a1000)
$

Can someone help me understand this observation? Why does calling uv_clock_gettime() vs. uv_close() behave this way with regards to the dependency relationship created in the shared library?


Update: I wanted to explore @WeatherVane's comment RE: optimization a little more. Here again, my understanding is probably rudimentary and incomplete, but I thought that if I compiled with -O0, it would force the compiler to not optimize anything out, and therefore induce the dependency even when my shared library only calls uv_clock_gettime(). But reality didn't match this idea: returning func.c to only calling uv_clock_gettime() and compiling everything with -O0, I still see no dependency on libuv. I.e.:

$ cc -fpic -ggdb -O0 -Wall -c -o func.o func.c # Note the -O0
$ cc -shared -O0 -o libfunc.so func.o  -fpic -ggdb -luv # Note the -O0
$ ldd ./libfunc.so
        linux-vdso.so.1 (0x00007fff8e724000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fee6f03e000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fee6f274000)

I wanted to explore @Barmar's suggestion of excluding the possibility of optimizing-out by printing the value of now, but even that version of code resulted in there not being the desired dependency. I.e.:

#include <inttypes.h>
#include <stdio.h>
#include <uv.h>

int func() {
  uv_timespec64_t now;

  uv_clock_gettime(UV_CLOCK_REALTIME, &now);
  printf("%" PRId64 "\n", now.tv_sec);

  return 0;
}
$ cc -fpic -ggdb -Wall -c -o func.o func.c
$ cc -shared -o libfunc.so func.o  -fpic -ggdb -luv
$ ldd ./libfunc.so
        linux-vdso.so.1 (0x00007ffd72bdf000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0a89964000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f0a89b9a000)

Exploring @EmployedRussian's suggestion, the readelf output is:

$ readelf -Ws /usr/local/lib/libuv.so.1 | grep uv_close
   337: 0000000000013766   427 FUNC    GLOBAL DEFAULT   14 uv_close
   735: 0000000000013766   427 FUNC    GLOBAL DEFAULT   14 uv_close
$ readelf -Ws /usr/local/lib/libuv.so.1 | grep uv_clock_gettime
   341: 00000000000136a0   178 FUNC    GLOBAL DEFAULT   14 uv_clock_gettime
  1120: 00000000000136a0   178 FUNC    GLOBAL DEFAULT   14 uv_clock_gettime
$

I'm updating this post with some observations based on @EmployedRussian's answer because they're relevant, but cumbersome to add as a comment.

With the version of the shared library that calls only uv_clock_gettime, the use of the linker -y option reveals:

$ cc -shared -Wl,-y,uv_clock_gettime,-y,uv_close -o libfunc.so func.o  -fpic -ggdb -luv
/usr/bin/ld: func.o: reference to uv_clock_gettime
/usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/libuv.so: definition of uv_close
$

...and with the version of the shared lib that references both uv_clock_gettime and uv_close, the linker -y option reveals:

$ cc -shared -Wl,-y,uv_clock_gettime,-y,uv_close -o libfunc.so func.o  -fpic -ggdb -luv
/usr/bin/ld: func.o: reference to uv_close
/usr/bin/ld: func.o: reference to uv_clock_gettime
/usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/libuv.so: definition of uv_close

...which aligns with @EmployedRussian's explanation of symbols referenced vs. symbols found vs. DT_NEEDED.

Also, with the version of the shared lib that references only uv_clock_gettime, using the linker --no-as-needed flag did indeed "force" inclusion of the dependency:

$ cc -shared -Wl,--no-as-needed -o libfunc.so func.o  -fpic -ggdb -luv
$ ldd ./libfunc.so
        linux-vdso.so.1 (0x00007ffe312cf000)
        libuv.so.1 => /lib/x86_64-linux-gnu/libuv.so.1 (0x00007f36fe4f8000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f36fe2d0000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f36fe538000)
$

Solution

  • One other thing I noticed: I needed to grep /usr/local/lib/libuv.so.1 instead of /lib/x86_64-linux-gnu/libuv.so.1 because the latter did not have the uv_clock_gettime symbol.

    That is probably the answer. I am guessing that

    1. /lib/x86_64-linux-gnu/libuv.so.1 is being used at link time, and
    2. Your GCC is configured to pass -Wl,--as-needed by default.

    If both of these are true, then, when you link func.o that doesn't reference uv_close, the linker finds /lib/x86_64-linux-gnu/libuv.so.1 but discovers that it doesn't satisfy any symbols, and thus doesn't record it as DT_NEEDED for libfoo.so.

    When you change func.o to also require uv_close, the linker observes that /lib/x86_64-linux-gnu/libuv.so.1 is necessary to satisfy that symbol, and does record it in the DT_NEEDED tag for libfoo.so.


    To confirm these guesses, link libfoo.so with -Wl,-y,uv_clock_gettime,-y,uv_close flag. This should show you which binaries reference and which define the two symbols.

    You can also link with -Wl,--no-as-needed -- in that case libuv.so.1 will show up regardless of whether it satisfies any symbols.