What exactly does `-rdynamic` do and when exactly is it needed?

42,170

Solution 1

Here is a simple example project to illustrate the use of -rdynamic.

bar.c

extern void foo(void);

void bar(void)
{
    foo();
}

main.c

#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>

void foo(void)
{
    puts("Hello world");
}

int main(void)
{
    void * dlh = dlopen("./libbar.so", RTLD_NOW);
    if (!dlh) {
        fprintf(stderr, "%s\n", dlerror());
        exit(EXIT_FAILURE); 
    }
    void (*bar)(void) = dlsym(dlh,"bar");
    if (!bar) {
        fprintf(stderr, "%s\n", dlerror());
        exit(EXIT_FAILURE); 
    }
    bar();
    return 0;
}

Makefile

.PHONY: all clean test

LDEXTRAFLAGS ?=

all: prog

bar.o: bar.c
    gcc -c -Wall -fpic -o $@ $<

libbar.so: bar.o
    gcc -shared -o $@ $<

main.o: main.c
    gcc -c -Wall -o $@ $<

prog: main.o | libbar.so
    gcc $(LDEXTRAFLAGS) -o $@ $< -L. -lbar -ldl

clean:
    rm -f *.o *.so prog

test: prog
    ./$<

Here, bar.c becomes a shared library libbar.so and main.c becomes a program that dlopens libbar and calls bar() from that library. bar() calls foo(), which is external in bar.c and defined in main.c.

So, without -rdynamic:

$ make test
gcc -c -Wall -o main.o main.c
gcc -c -Wall -fpic -o bar.o bar.c
gcc -shared -o libbar.so bar.o
gcc  -o prog main.o -L. -lbar -ldl
./prog
./libbar.so: undefined symbol: foo
Makefile:23: recipe for target 'test' failed

And with -rdynamic:

$ make clean
rm -f *.o *.so prog
$ make test LDEXTRAFLAGS=-rdynamic
gcc -c -Wall -o main.o main.c
gcc -c -Wall -fpic -o bar.o bar.c
gcc -shared -o libbar.so bar.o
gcc -rdynamic -o prog main.o -L. -lbar -ldl
./prog
Hello world

Solution 2

-rdynamic exports the symbols of an executable, this mainly addresses scenarios as described in Mike Kinghan's answer, but also it helps e.g. Glibc's backtrace_symbols() symbolizing the backtrace.

Here is a small experiment (test program copied from here)

#include <execinfo.h>                                                                                                                                                                                                                                                           
#include <stdio.h>
#include <stdlib.h>

/* Obtain a backtrace and print it to stdout. */
void
print_trace (void)
{
  void *array[10];
  size_t size;
  char **strings;
  size_t i;

  size = backtrace (array, 10);
  strings = backtrace_symbols (array, size);

  printf ("Obtained %zd stack frames.\n", size);

  for (i = 0; i < size; i++)
     printf ("%s\n", strings[i]);

  free (strings);
}

/* A dummy function to make the backtrace more interesting. */
void
dummy_function (void)
{
  print_trace (); 
}

int
main (void)
{
  dummy_function (); 
  return 0;
}

compile the program: gcc main.c and run it, the output:

Obtained 5 stack frames.
./a.out() [0x4006ca]
./a.out() [0x400761]
./a.out() [0x40076d]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f026597f830]
./a.out() [0x4005f9]

Now, compile with -rdynamic, i.e. gcc -rdynamic main.c, and run again:

Obtained 5 stack frames.
./a.out(print_trace+0x28) [0x40094a]
./a.out(dummy_function+0x9) [0x4009e1]
./a.out(main+0x9) [0x4009ed]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f85b23f2830]
./a.out(_start+0x29) [0x400879]

As you can see, we get a proper stack trace now!

Now, if we investigate ELF's symbol table entry (readelf --dyn-syms a.out):

without -rdynamic

Symbol table '.dynsym' contains 9 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND free@GLIBC_2.2.5 (2)
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.2.5 (2)
     3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND backtrace_symbols@GLIBC_2.2.5 (2)
     4: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND backtrace@GLIBC_2.2.5 (2)
     5: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __stack_chk_fail@GLIBC_2.4 (3)
     6: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5 (2)
     7: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
     8: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__

with -rdynamic, we have more symbols, including the executable's:

Symbol table '.dynsym' contains 25 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND free@GLIBC_2.2.5 (2)
     2: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
     3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.2.5 (2)
     4: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND backtrace_symbols@GLIBC_2.2.5 (2)
     5: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND backtrace@GLIBC_2.2.5 (2)
     6: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __stack_chk_fail@GLIBC_2.4 (3)
     7: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5 (2)
     8: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
     9: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
    10: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
    11: 0000000000601060     0 NOTYPE  GLOBAL DEFAULT   24 _edata
    12: 0000000000601050     0 NOTYPE  GLOBAL DEFAULT   24 __data_start
    13: 0000000000601068     0 NOTYPE  GLOBAL DEFAULT   25 _end
    14: 00000000004009d8    12 FUNC    GLOBAL DEFAULT   14 dummy_function
    15: 0000000000601050     0 NOTYPE  WEAK   DEFAULT   24 data_start
    16: 0000000000400a80     4 OBJECT  GLOBAL DEFAULT   16 _IO_stdin_used
    17: 0000000000400a00   101 FUNC    GLOBAL DEFAULT   14 __libc_csu_init
    18: 0000000000400850    42 FUNC    GLOBAL DEFAULT   14 _start
    19: 0000000000601060     0 NOTYPE  GLOBAL DEFAULT   25 __bss_start
    20: 00000000004009e4    16 FUNC    GLOBAL DEFAULT   14 main
    21: 00000000004007a0     0 FUNC    GLOBAL DEFAULT   11 _init
    22: 0000000000400a70     2 FUNC    GLOBAL DEFAULT   14 __libc_csu_fini
    23: 0000000000400a74     0 FUNC    GLOBAL DEFAULT   15 _fini
    24: 0000000000400922   182 FUNC    GLOBAL DEFAULT   14 print_trace

I hope that helps!

Solution 3

I use rdynamic to print out backtraces using the backtrace()/backtrace_symbols() of Glibc.

Without -rdynamic, you cannot get function names.

To know more about the backtrace() read it over here.

Solution 4

From The Linux Programming Interface:

42.1.6

Accessing Symbols in the Main Program

Suppose that we use dlopen() to dynamically load a shared library, use dlsym() to obtain the address of a function x() from that library, and then call x(). If x() in turn calls a function y(), then y() would normally be sought in one of the shared libraries loaded by the program.

Sometimes, it is desirable instead to have x() invoke an implementation of y() in the main program. (This is similar to a callback mechanism.) In order to do this, we must make the (global-scope) symbols in the main program available to the dynamic linker, by linking the program using the --export-dynamic linker option:

$ gcc -Wl,--export-dynamic main.c (plus further options and arguments)

Equivalently, we can write the following:

$ gcc -export-dynamic main.c

Using either of these options allows a dynamically loaded library to access global symbols in the main program.

The gcc -rdynamic option and the gcc -Wl,-E option are further

synonyms for -Wl,--export-dynamic.

I guess this only works for dynamically loaded shared library, opened with dlopen(). Correct me if I am wrong.

Share:
42,170
PSkocik
Author by

PSkocik

Updated on July 05, 2022

Comments

  • PSkocik
    PSkocik almost 2 years

    What exactly does -rdynamic (or --export-dynamic at the linker level) do and how does it relate to symbol visibility as defined by the -fvisibility* flags or visibility pragmas and __attribute__s?

    For --export-dynamic, ld(1) mentions:

    ... If you use "dlopen" to load a dynamic object which needs to refer back to the symbols defined by the program, rather than some other dynamic object, then you will probably need to use this option when linking the program itself. ...

    I'm not sure I completely understand this. Could you please provide an example that doesn't work without -rdynamic but does with it?

    Edit: I actually tried compiling a couple of dummy libraries (single file, multi-file, various -O levels, some inter-function calls, some hidden symbols, some visible), with and without -rdynamic, and so far I've been getting byte-identical outputs (when keeping all other flags constant of course), which is quite puzzling.

  • PSkocik
    PSkocik about 8 years
    You example makes it perfectly clear what the manpage means. Thanks a lot!
  • yugr
    yugr about 6 years
    A much better solution is to use normal unwinder which can access debuginfo.
  • thejinx0r
    thejinx0r over 5 years
    I was wondering why is rdynamic on the executable and not the shared object. As per this answer: stackoverflow.com/questions/50418941/…, a concise summary of this answer is: Symbols are only exported by default from shared libraries. -rdynamic tells linker to do the same for executables.
  • f3xy
    f3xy almost 5 years
    @yugr can you provide some reference to what you are referring to?
  • yugr
    yugr almost 5 years
    @f3xy See e.g. this Flameeyes post about disadvantages of adding extra symbols to dynamic symtab. Dedicated unwinders, like libbacktrace or libunwind can symbolize stack without overhead by using program's debug info.
  • Dima Litvinov
    Dima Litvinov about 4 years
    In addition to using -rdynamic, also check that your build system doesn't add -fvisibility=hidden option! (as it will completely discard the effect of -rdynamic)
  • Kaz
    Kaz almost 4 years
    @yugr Debug info adds a lot more bulk to the executable (think about embedded systems with small flash partitions) and may not be appropriate if you're shipping proprietary software. Already -rdynamic adds a lot of information helpful to someone reverse engineering the binary. -rdynamic is a nice trick: the binary can still be stripped, yet it will respect those symbols because they are dynamic.
  • yugr
    yugr almost 4 years
    @Kaz "Debug info adds a lot more bulk to the executable" - debuginfo-based unwinders need only -gline-tables-only which is significantly smaller than full -g debuginfo. "embedded systems with small flash partitions" - such systems usually print only addresses anyway (which are then symbolized on host). "may not be appropriate if you're shipping proprietary software" - I would not recommend any proprietary software to print symbolized backtraces in release builds, be it with debuginfo or -rdynamic.
  • Kaz
    Kaz almost 4 years
    @yugr such systems usually print only addresses anyway (which are then symbolized on host I happen to be in the process of merging a change which turns on the symbols on an embedded system, so we can get rid of the symbolizing on the host. "Line tables" stills sounds like more information than just symbols for addresses. Also, debug information won't survive strip, so you're looking at inspecting and fixing the whole build process to make sure stripping is disabled end-to-end, not just in Makefiles but in image preparation scripts.
  • yugr
    yugr almost 4 years
    @Kaz "I happen to be in the process of merging a change which turns on the symbols on an embedded system" - this (slightly) simplifies usage at the cost of wasted flash (which may or may not be acceptable depending on your situation).
  • yugr
    yugr almost 4 years
    @Kaz "Line tables stills sounds like more information than just symbols for addresses" - actually if symbolization is done on host debuginfo is not even needed - similar to backtrace(3) unwinders can simply use unwind info (-funwind-tables).
  • yugr
    yugr almost 4 years
    @Kaz "debug information won't survive strip, so you're looking at inspecting and fixing the whole build process" - I'm not sure why updating buildscripts is an issue, making stripping optional is not a complex change.
  • Maciej Załucki
    Maciej Załucki over 3 years
    It works with required dynamic libraries loaded automatically as well, no need to use dlopen. In my case I created dynamic library that declared extern symbol which was defined in executable that depends on this library. If I build executable with rdynamic, the symbol is visible to dynamic library I use. Note that there's huge drawback of using rdynamic - it will export everything else as well. Make sure to use version script so that you will only export the symbol you want. Otherwise performance will suffer (both from amount of symbols and worse optimizations).
  • Chan Kim
    Chan Kim almost 3 years
    Good example, but the -L. -lbar is not necessary during prog compile, is it? they are necessary only for static library linking. Dynamic library is found by LD_LIBRARY_PATH.
  • mchiasson
    mchiasson almost 3 years
    I agree with @ChanKim. -L. -lbar is not necessary since we are dlopening the lib by hand. It also should work fine without having to modify LD_LIBRARY_PATH since we are opening the lib with a path ("./libbar.so" instead of "libbar.so") so it's fine to leave LD_LIBRARY_PATH alone or as-is.
  • yyny
    yyny over 2 years
    -rdynamic is a terrible way to add debug information to stacktraces, it is meant for a completely different purpose, includes lots of information you will never use (like variable names and unused functions), and will often be incomplete or inaccurate, which is the last thing you want during debugging. If you want to resolve stacktrace information at runtime, you should just include actual debug information with -g, and if you don't want to include debug information (like embedded or proprietary software), then you should just return the offset into the executable segment.