I’m likely not the only one, who, after updating a third party dependency has ran into some linker error. This could be in the form of:
Sometimes when you get a weird and wonderful linker error, it can be useful to peek under the under the covers a little to get a better understanding of how different libraries depend on each other.
Luckily there are plenty of utilities provided by Binutils that can help with this.
Binutils contains a variety of tools which can be used to better understand
linking problems. The key utilities are nm
, objdump
and readelf
.
All of these can be used to display symbol information from a binary.
The descriptions from the manpages of the tools are:
nm
- list symbols from object filesobjdump
- display information from object filesreadelf
- display information about ELF filesFor the basic symbol information which I’d normally be interested in, they all do a similar job, just with different output formats. So choosing one can be down to personal taste. I’ve included a variety of example commands below.
It is worth noting that some commands are a little bit more choosy about the
symbols they output. nm
and objdump
appear to only output non-dynamic
symbols as part of their “--syms
” output, whereas readelf
seems to always give
both normal and dynamic symbols when “--syms
” is specified.
Two common symbol tables to see in the readelf
output are “.symtab
” and
“.dynsym
”.
The “.symtab
” table typically contains the symbols required for
static linking (aka link editing). For a production executable or shared
library, you typically wouldn’t see much in this section as the binary would
likely have been stripped. The “.dynsym
” table contains the symbols relevant
to dynamic linking. We expect to see the dynamic symbols being both imported and
exported listed there.
The following commands will list (at least) the normal symbols of interest to a binary:
nm --demangle --print-file-name $FILE
objdump --demangle --syms $FILE
readelf --demangle --wide --syms $FILE
If the version of the tool being used is missing the --demangle
option you can
run its output through c++filt
to decipher any mangled symbols:
readelf --wide --syms $FILE | c++filt
The following will list the symbols available for / required by dynamic linking:
nm --demangle --dynamic --print-file-name $FILE
objdump --demangle --dynamic-syms $FILE
readelf --demangle --wide --dyn-syms $FILE
The following command will list the shared libraries that need to be dynamically
linked in at runtime (but not any explicitly loaded via dlopen()
):
readelf --dynamic $FILE | grep NEEDED
I’ve constructed a very basic example project in order to demonstrate some of the above Binutils functions. It consists of three c++ source files, which are used on Linux to generate a:
libstatic.a
libshared.so
executable
The source code can be found on github.
The following sketch shows how the functions contained within the different source files interact with each other:
While the sketch below shows the much less interesting interaction of the compiled code:
When ran, executable
produces the following output on stdout
:
In main
In static_function_one
In static_function_two
In shared_function_one
In shared_function_two
We’re going to use some of the Binutils commands to investigate the linkage of these binaries.
Firstly, lets see what’s in libstatic.a
. As this is just a static library, its
expected to be linked into something else - in this case executable
.
Ignoring the less interesting parts of the output, we see the following when
inspecting the library with readelf
:
$ readelf --demangle --wide --syms libstatic.a
File: libstatic.a(static_library.cpp.o)
Symbol table '.symtab' contains 19 entries:
Num: Value Size Type Bind Vis Ndx Name
6: 0000000000000000 75 FUNC LOCAL DEFAULT 1 static_library::(anonymous namespace)::static_function_two()
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND shared_library::shared_function_one()
13: 000000000000004b 75 FUNC GLOBAL DEFAULT 1 static_library::static_function_one()
This tells us that the library needs shared_function_one()
but it is undefined
("UND
") in this library. At this point, the tooling doesn’t know how its going
to be resolved - it could be dynamically or statically linked in.
The functions static_function_one()
and static_function_two()
by not being marked as
“UND
” are present in this library. As static_function_two()
is
marked “LOCAL
” it cannot be linked to from other sources.
static_function_one()
can be linked to from elsewhere as its binding is
“GLOBAL
”
Running objdump
on the same binary we get similar information, with “l
” and
“g
” indicating “local” and “global” binding respectively.
$ objdump --demangle --syms libstatic.a
SYMBOL TABLE:
0000000000000000 l F .text 000000000000004b static_library::(anonymous namespace)::static_function_two()
0000000000000000 *UND* 0000000000000000 shared_library::shared_function_one()
000000000000004b g F .text 000000000000004b static_library::static_function_one()
When running readelf
over libshared.so
, the interesting part of the output
is:
$ readelf --demangle --wide --symbols libshared.so
Symbol table '.dynsym' contains 12 entries:
Num: Value Size Type Bind Vis Ndx Name
11: 00000000000011bf 75 FUNC GLOBAL DEFAULT 14 shared_library::shared_function_one()
Symbol table '.symtab' contains 35 entries:
Num: Value Size Type Bind Vis Ndx Name
11: 0000000000001179 70 FUNC LOCAL DEFAULT 14 shared_library::(anonymous namespace)::shared_function_two()
29: 00000000000011bf 75 FUNC GLOBAL DEFAULT 14 shared_library::shared_function_one()
In this case, we’re mostly interested in the fact that
shared_function_one()
is present and can be dynamically linked i.e. it:
The dynamic symbols of this library could have been explicitly targeted with the
following readelf
command:
$ readelf --demangle --wide --dyn-syms libshared.so
The following condensed output is the result of inspecting libshared.so
’s
dynamic symbols with objdump
:
$ objdump --demangle --dynamic-syms libshared.so
libshared.so: file format elf64-x86-64
DYNAMIC SYMBOL TABLE:
00000000000011bf g DF .text 000000000000004b Base shared_library::shared_function_one()
In this case the symbol is marked:
Inspecting the executable with the same command as above:
$ readelf --demangle --wide --symbols executable
We see the following condensed output:
Symbol table '.dynsym' contains 13 entries:
Num: Value Size Type Bind Vis Ndx Name
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND shared_library::shared_function_one()
Symbol table '.symtab' contains 45 entries:
Num: Value Size Type Bind Vis Ndx Name
13: 0000000000001287 75 FUNC LOCAL DEFAULT 16 static_library::(anonymous namespace)::static_function_two()
24: 00000000000011c9 79 FUNC GLOBAL DEFAULT 16 main
35: 0000000000000000 0 FUNC GLOBAL DEFAULT UND shared_library::shared_function_one()
42: 00000000000012d2 75 FUNC GLOBAL DEFAULT 16 static_library::static_function_one()
The functions from the static library have been linked into our executable.
The function shared_function_one()
has been included in the .dynsym
table as “UND
” - the linker has correctly determined its to be dynamically
linked in at runtime from a shared library.
When inspecting the symbols contained within executable
, we seen that
shared_function_one()
was a function that was undefined, and required to be
dynamically linked.
The library which contains this symbol will have been
embedded into the executable at link time, when symbols were being resolved.
We can check the dynamic linking information stored in the executable with the following command:
$ readelf --dynamic executable | grep NEEDED
Which produces the output shown below. The entries marked NEEDED list the shared
objects the dynamic linker needs to find at runtime. As expected, the library
libshared.so
is present.
0x0000000000000001 (NEEDED) Shared library: [libshared.so]
0x0000000000000001 (NEEDED) Shared library: [libstdc++.so.6]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
Shared libraries can have their own list of shared library dependencies that need to be dynamically linked at runtime.
If we issue the same command on the shared library we get:
$ readelf --dynamic libshared.so | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libstdc++.so.6]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
The --print-file-name
option of nm
was included in the examples above
as it is quite useful when filtering output from multiple libraries - such as
when looking for references to a particular symbol.
The following command can be used to find which binaries are interested in a
symbol, in this case “shared_function_one
”.
The --print-file-name
option helpfully ensures each line contains the file
the symbol was found in:
$ nm --demangle --print-file-name executable libshared.so libstatic.a \
| grep "shared_function_one" \
| tr -s ' '
executable: U shared_library::shared_function_one()
libshared.so:00000000000011bf T shared_library::shared_function_one()
libstatic.a:static_library.cpp.o: U shared_library::shared_function_one()