The Unterminated String

Embedded Things and Software Stuff

Bin Hoking - Using Binutils to Inspect Symbols

Posted at — Mar 13, 2024

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 Commands

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:

For 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.

Viewing Symbols

The following commands will list (at least) the normal symbols of interest to a binary:

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:

View Dynamic Symbols

The following will list the symbols available for / required by dynamic linking:

View Required Dynamic Libraries

The following command will list the shared libraries that need to be dynamically linked in at runtime (but not any explicitly loaded via dlopen()):

Example

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:

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:

Source File Interaction

While the sketch below shows the much less interesting interaction of the compiled code:

Binary File Interaction

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.

libstatic.a

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()

libshared.so

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:

executable

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.

Dynamic Linking

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]

Single Command for Overview of Symbol Interaction

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()

Additional Information