Debugging the Loader
This page describes different methods for debugging the GLIBC dynamic linker/loader ld[64].so to achieve certain goals.
There are several loader environment scenarios:
- Loader is in the system toolchain, e.g. /lib[64]/ld[64].so.1
- Loader is in a self-contained toolchain, e.g. /opt/toolchain/lib[64]/ld[64].so.1
Loader is in an install_root installed GLIBC directory, e.g. /home/user/glibc/install_root/lib[64]/ld[64].so.1 (This is an intermediary install step of GLIBC performed using make install_root=path install and is used for packaging purposes).
- Loader is in a non-installed GLIBC build directory, e.g. /home/user/glibc/build/glibc[32|64]/elf/ld[64].so.1
The first two scenarios are relatively similar and debugging shouldn't provide any great challenges other than the following:
The loader has demonstrated fault in its symbol resolver code. - You can step into the loader resolver code from the application main procedure. Todo: Write this section.
The third scenario and fourth scenarios are challenging because you have to break the toolchain packages' assumptions about library paths. This is described by the following section:
An alternate loader is being used and an application or library needs to be debugged. - An application or library that uses an alternate loader must be debugged by debugging the loader and directing the loader to debug the application or library. This may require creating an application in order to step into a library.
Debugging With An Alternate Loader
Note: The examples here were done on the Power Architecture. The symbols that are used to break into the loader may vary by platform.
Note: For this example assume that GLIBC was configured and built into /home/user/glibc/build/glibc[32|64]. It doesn't need to be installed in order to test it. Our test application resides in /home/user/glibc/build/test.
Often times you may need to test an application against a GLIBC build that is not installed into the system root. You can use the following method to debug against a non-installed GLIBC build that was configured with any --prefix (but not installed).
In order to runtime test against the GLIBC build one must invoke the newly built loader, ld[64].so, from the GLIBC build directly passing ld[64].so the application name and using the --library-path directive to identify the location of the libraries built by the GLIBC build, e.g.
- 32-bit:
/home/user/glibc/build/glibc32/elf/ld.so.1 --library-path </home/user/glibc/build/glibc32/nptl:/home/user/glibc/build/glibc32/elf:etc> <application>
- 64-bit:
/home/user/glibc/build/glibc64/elf/ld64.so.1 --library-path </home/user/glibc/build/glibc64/nptl:/home/user/glibc/build/glibc64/elf:etc> <application>
We'll create a BASH script which can direct the loader to launch the application and load the appropriate libraries:
> cd /home/user/glibc/build/test > cat /home/user/glibc/build/test/run_test.sh
32-bit:
#!/bin/bash
ulimit -c unlimited
GLIBC="/home/user/glibc/build/glibc32"
${GLIBC}/elf/ld.so.1 --library-path \
${GLIBC}:\
${GLIBC}/math:\
${GLIBC}/elf:\
${GLIBC}/dlfcn:\
${GLIBC}/nss:\
${GLIBC}/nis:\
${GLIBC}/rt:\
${GLIBC}/resolv:\
${GLIBC}/crypt:\
${GLIBC}/nptl:\
${GLIBC}/nptl_db: \
/home/user/glibc/build/test/test
64-bit:
#!/bin/bash
ulimit -c unlimited
GLIBC="/home/user/glibc/build/glibc64"
${GLIBC}/elf/ld64.so.1 --library-path \
${GLIBC}:\
${GLIBC}/math:\
${GLIBC}/elf:\
${GLIBC}/dlfcn:\
${GLIBC}/nss:\
${GLIBC}/nis:\
${GLIBC}/rt:\
${GLIBC}/resolv:\
${GLIBC}/crypt:\
${GLIBC}/nptl:\
${GLIBC}/nptl_db: \
/home/user/glibc/build/test/test
If this GLIBC installation or the application that runs against it need to be debugged one must direct GDB to first debug the newly installed loader and the loader is directed to run the application to be debugged. We'll trap in the loader and then step into the application or libraries. This can be accomplished with a .gdb script.
In order to be able to step into the application or the libraries that are loaded we'll want to immediately trap in the loader. So we'll set a breakpoint on the following loader symbol:
- 32-bit:
_dl_main_dispatch
- 64-bit:
_dl_start_user
In order to direct the loader to execute the test application and load the dependent libraries we'll use the --library-path directive.
We can accomplished all of this easily using a GDB script: Note: if the application you're debugging is a GLIBC test-suite application you must append the --direct flag to the application invocation as you'll see in the following examples.
> cat test.gdb
- 32-bit:
set environment C -E -x c-header
break _dl_main_dispatch
run --library-path
/home/user/glibc/build/glibc32:\
/home/user/glibc/build/glibc32/nptl:\
/home/user/glibc/build/glibc32/math:\
/home/user/glibc/build/glibc32/elf:\
/home/user/glibc/build/glibc32/dlfcn:\
/home/user/glibc/build/glibc32/nss:\
/home/user/glibc/build/glibc32/nis:\
/home/user/glibc/build/glibc32/rt:\
/home/user/glibc/build/glibc32/resolv:\
/home/user/glibc/build/glibc32/crypt:\
/home/user/glibc/build/glibc32/nptl:\
/home/user/glibc/build/glibc32/nptl_db \
/home/user/glibc/build/test/test --direct
- 64-bit:
set environment C -E -x c-header
break _dl_start_user
run --library-path
/home/user/glibc/build/glibc64:\
/home/user/glibc/build/glibc64/nptl:\
/home/user/glibc/build/glibc64/math:\
/home/user/glibc/build/glibc64/elf:\
/home/user/glibc/build/glibc64/dlfcn:\
/home/user/glibc/build/glibc64/nss:\
/home/user/glibc/build/glibc64/nis:\
/home/user/glibc/build/glibc64/rt:\
/home/user/glibc/build/glibc64/resolv:\
/home/user/glibc/build/glibc64/crypt:\
/home/user/glibc/build/glibc64/nptl:\
/home/user/glibc/build/glibc64/nptl_db \
/home/user/glibc/build/test/test --direct
Now we'll create a script to execute GDB and direct it to use this gdb script. Note: the -d directive tells GDB where to find the library sources.
NOTE: In order for gdb to be able to fully debug the application and standalone GLIBC you need to invoke it through the new standalone loader.
Otherwise GDB will indicate that it's version of libthread_db is different from the libpthread you're attempting to debug by giving the following error:
versions of libpthread and libthread_db do not match
So we need GDB to be able to find all of the libraries created by the standalone GLIBC build.
32-bit:
#!/bin/bash
GLIBC="/home/user/glibc/build/glibc32"
# We need to make sure that gdb is linked against the standalone glibc so that
# it picks up the correct nptl_db/libthread_db.so. So that means invoking gdb
# using the standalone glibc's linker.
${GLIBC}/elf/ld.so.1 --library-path \
${GLIBC}:\
${GLIBC}/math:\
${GLIBC}/elf:\
${GLIBC}/dlfcn:\
${GLIBC}/nss:\
${GLIBC}/nis:\
${GLIBC}/rt:\
${GLIBC}/resolv:\
${GLIBC}/crypt:\
${GLIBC}/nptl:\
${GLIBC}/nptl_db:\
/usr/bin/gdb -x test.gdb -d /home/user/glibc/libc /home/user/glibc/build/glibc32/elf/ld.so.1
64-bit:
#!/bin/bash
GLIBC="/home/user/glibc/build/glibc64"
# We need to make sure that gdb is linked against the standalone glibc so that
# it picks up the correct nptl_db/libthread_db.so. So that means invoking gdb
# using the standalone glibc's linker.
${GLIBC}/elf/ld64.so.1 --library-path \
${GLIBC}:\
${GLIBC}/math:\
${GLIBC}/elf:\
${GLIBC}/dlfcn:\
${GLIBC}/nss:\
${GLIBC}/nis:\
${GLIBC}/rt:\
${GLIBC}/resolv:\
${GLIBC}/crypt:\
${GLIBC}/nptl:\
${GLIBC}/nptl_db:\
/usr/bin/gdb -x test.gdb -d /home/user/glibc/libc /home/user/glibc/build/glibc64/elf/ld64.so.1
You should see output like the following:
- 32-bit:
GNU gdb (GDB) 6.8.50.20080707-cvs ... Breakpoint 1 at 0x16bb8 Breakpoint 1, 0x08016bb8 in _dl_main_dispatch ()
- 64-bit:
GNU gdb (GDB) 6.8.50.20080707-cvs ... Breakpoint 1 at 0x65b4 Breakpoint 1, 0x0000000040d365b4 in ._dl_start_user ()
You'll notice that using the GDB script we've developed we're able to find the breakpoint for [_dl_main_dispatch\_dl_start_user] because GDB queried ld[64].so for debugging information. If we try to set a breakpoint on main GDB doesn't know where this is at (since libc the application hasn't been loaded yet) as indicated by the following output:
Function "main" not defined.
If we were to execute the GDB directive continue at this point the debugger would not trap on main because it has no knowledge of how the addresses it encounters map to the symbols associated with the executable and libraries.
The root of the problem is that when GDB is directed to debug the loader, ld[64].so it simply queries the loader for symbol information since the loader is the target executable. The loader is then directed to load and launch the actual application test and the libraries, e.g. libc.so, using the GDB run directive:
run --library-path <path to libraries> ./test
When the executable and libraries are loaded into the process address space GDB doesn't have any symbol or location information on these libraries and therefore it can't associate and symbols with addresses.
We can tell GDB how to find the symbol information about the executable and libraries by using the GDB add-symbol-file directive. But first we need to do a bit of homework to find out where the executable and libraries are mapped into the process's virtual address space.
We can use GDB's info proc mapping directive to find out where the libraries and application are mapped into the process address space:
(gdb) info proc mapping process 17755 cmdline = '/home/user/glibc/build/glibc32/elf/ld.so.1' cwd = '/home/user/glibc/build/test' exe = '/home/user/glibc/build/glibc32/elf/ld.so' Mapped address spaces: Start Addr End Addr Size Offset objfile 0x100000 0x103000 0x3000 0x100000 0x8000000 0x8020000 0x20000 0 /home/user/glibc/build/glibc32/elf/ld.so 0x802f000 0x8030000 0x1000 0x1f000 /home/user/glibc/build/glibc32/elf/ld.so 0x8030000 0x8031000 0x1000 0x20000 /home/user/glibc/build/glibc32/elf/ld.so 0xfc95000 0xfd3c000 0xa7000 0 /home/user/glibc/build/glibc32/math/libm.so 0xfd3c000 0xfd4c000 0x10000 0xa7000 /home/user/glibc/build/glibc32/math/libm.so 0xfd4c000 0xfd4f000 0x3000 0xa7000 /home/user/glibc/build/glibc32/math/libm.so 0xfd4f000 0xfd50000 0x1000 0xaa000 /home/user/glibc/build/glibc32/math/libm.so 0xfd93000 0xff32000 0x19f000 0 /home/user/glibc/build/glibc32/libc.so 0xff32000 0xff42000 0x10000 0x19f000 /home/user/glibc/build/glibc32/libc.so 0xff42000 0xff46000 0x4000 0x19f000 /home/user/glibc/build/glibc32/libc.so 0xff46000 0xff47000 0x1000 0x1a3000 /home/user/glibc/build/glibc32/libc.so 0xff47000 0xff4a000 0x3000 0xff47000 0x10000000 0x10001000 0x1000 0 /home/user/glibc/build/test/test 0x10010000 0x10011000 0x1000 0 /home/user/glibc/build/test/test 0xf7ffc000 0xf7fff000 0x3000 0xf7ffc000 0xff857000 0xff86d000 0x16000 0xff857000 [stack]
So which Start Addr do we use for the libraries we're interested in? GDB doesn't tell us which section is associated with each address range.
We can check /proc/<pid>/maps to find out information about the sections:
> ps aux | grep test | grep -v "grep" | awk -F' ' '{print $2}' 18171 > cat /proc/18171/maps 00100000-00103000 r-xp 00100000 00:00 0 08000000-08020000 r-xp 00000000 09:00 80040685 /home/user/glibc/build/glibc32/elf/ld.so 0802f000-08030000 r--p 0001f000 09:00 80040685 /home/user/glibc/build/glibc32/elf/ld.so 08030000-08031000 rw-p 00020000 09:00 80040685 /home/user/glibc/build/glibc32/elf/ld.so 0fc95000-0fd3c000 r-xp 00000000 09:00 78987266 /home/user/glibc/build/glibc32/math/libm.so 0fd3c000-0fd4c000 ---p 000a7000 09:00 78987266 /home/user/glibc/build/glibc32/math/libm.so 0fd4c000-0fd4f000 r--p 000a7000 09:00 78987266 /home/user/glibc/build/glibc32/math/libm.so 0fd4f000-0fd50000 rw-p 000aa000 09:00 78987266 /home/user/glibc/build/glibc32/math/libm.so 0fd93000-0ff32000 r-xp 00000000 09:00 78615158 /home/user/glibc/build/glibc32/libc.so 0ff32000-0ff42000 ---p 0019f000 09:00 78615158 /home/user/glibc/build/glibc32/libc.so 0ff42000-0ff46000 r--p 0019f000 09:00 78615158 /home/user/glibc/build/glibc32/libc.so 0ff46000-0ff47000 rw-p 001a3000 09:00 78615158 /home/user/glibc/build/glibc32/libc.so 0ff47000-0ff4a000 rw-p 0ff47000 00:00 0 10000000-10001000 r-xp 00000000 09:00 80561416 /home/user/glibc/build/test/test 10010000-10011000 rw-p 00000000 09:00 80561416 /home/user/glibc/build/test/test f7ffc000-f7fff000 rw-p f7ffc000 00:00 0 ff94e000-ff964000 rw-p ff94e000 00:00 0 [stack]
We're interested in the Start Addr of the .text section of the application and libraries, i.e. the section marked by r-xp.
> cat /proc/18171/maps | grep "r-xp" | grep test | awk -F'-' '{printf $1}' 10000000 > cat /proc/18171/maps | grep "r-xp" | grep "libc\.so" | awk -F'-' '{printf $1}' 0fd93000
These addresses indicate where in the process's virtual memory the application and C library are mapped, respectively. This information isn't enough but we'll use them later.
We need to look at the actual executable and library and determine at what offset the .text section actually begins. This is important because GDB needs to know where the .text information is at when it tries to gather symbol information about a library or executable. If we simply used the virtual address mapping of the executable and libraries then the symbol addresses would be wrong.
We can look at the actual executable and library ELF information for the .text information:
> objdump -s --section=".text" /home/user/glibc/build/glibc32/libc.so | grep Contents -A 1 | tail -n 1 | awk -F' ' '{printf $1}' 01f760
Executables are a bit different. If the process is built without PIE (position independent executable) then it will reference the absolute address.
> objdump -s --section=".text" /home/user/glibc/build/test/test | grep Contents -A 1 | tail -n 1 | awk -F' ' '{printf $1}' 100003a0
We're only interested in the offset portion.
0x3a0
At this point we want to tell GDB, via the command add-symbol-file, the address ranges of the application and libraries in virtual memory so that it can query the symbol information for those libraries.
In the following modification to the GDB script the variables $test and $libc hold the virtual address mapping of the test application and libc library, respectively. To these we add the start address offsets we found in the objdump output which indicates where the .text section starts:
> cat test.gdb
- 32-bit:
set environment C -E -x c-header
break _dl_main_dispatch
run --library-path
/home/user/glibc/build/glibc32:\
/home/user/glibc/build/glibc32/nptl:\
/home/user/glibc/build/glibc32/math:\
/home/user/glibc/build/glibc32/elf:\
/home/user/glibc/build/glibc32/dlfcn:\
/home/user/glibc/build/glibc32/nss:\
/home/user/glibc/build/glibc32/nis:\
/home/user/glibc/build/glibc32/rt:\
/home/user/glibc/build/glibc32/resolv:\
/home/user/glibc/build/glibc32/crypt:\
/home/user/glibc/build/glibc32/nptl\
/home/user/glibc/build/glibc32/nptl_db\
/home/user/glibc/build/test/test
set $test = 0x10000000
set $offset = 0x3A0
set $addr = $test + $offset
add-symbol-file ./test $addr
p/x &main
break main
set $libc = 0xfd93000
set $offset = 0x0001f760
set $addr = $libc + $offset
add-symbol-file ../glibc32/libc.so $addr
p/x ®ister_printf_function
break register_printf_function
- 64-bit:
- 32-bit:
set environment C -E -x c-header
break _dl_start_user
run --library-path
/home/user/glibc/build/glibc64:\
/home/user/glibc/build/glibc64/nptl:\
/home/user/glibc/build/glibc64/math:\
/home/user/glibc/build/glibc64/elf:\
/home/user/glibc/build/glibc64/dlfcn:\
/home/user/glibc/build/glibc64/nss:\
/home/user/glibc/build/glibc64/nis:\
/home/user/glibc/build/glibc64/rt:\
/home/user/glibc/build/glibc64/resolv:\
/home/user/glibc/build/glibc64/crypt:\
/home/user/glibc/build/glibc64/nptl\
/home/user/glibc/build/glibc64/nptl_db\
/home/user/glibc/build/test/test
set $test = 0x10000000
set $offset = 0x3A0
set $addr = $test + $offset
add-symbol-file ./test $addr
p/x &main
break main
set $libc = 0xfd93000
set $offset = 0x0001f760
set $addr = $libc + $offset
add-symbol-file ../glibc64/libc.so $addr
p/x ®ister_printf_function
break register_printf_function
Let's re-run GDB with this modified script:
> gdb -x test.gdb -d /home/user/glibc/libc /home/user/glibc/build/glibc32/elf/ld.so.1 GNU gdb (GDB) 6.8.50.20080707-cvs ... Breakpoint 1 at 0x16bb8 Breakpoint 1, 0x08016bb8 in _dl_main_dispatch () add symbol table from file "./printf_dfp_test" at .text_addr = 0x100003a0 $1 = 0x1000050c Breakpoint 2 at 0x10000520: file /home/ryanarn/glibc/stages/stage_printf_scanf/build/test/test.c, line 6. add symbol table from file "../glibc32_power5/libc.so" at .text_addr = 0xfdb2760 $2 = 0xfde9930 Breakpoint 3 at 0xfde996c: file printf-prs.c, line 117.
If we execute the GDB directive continue the debugger will then trap on the main and register_printf_function function addresses as we desired.