Finding where relocations originate

Marc Mutz - mmutz picture Marc Mutz - mmutz · Sep 28, 2013 · Viewed 14.7k times · Source

Using Ulrich Drepper's relinfo.pl script, one can easily count the number of relocations of a DSO, but it doesn't work on .o files.

Say I have a large shared library and I'm not happy about the number of its relocations. is there a way to find out where they come from (symbol, or at least .o), to check whether they're of the easily fixable type (e.g.: const char * str = "Hello World";' -> const char str[] = "Hello World";)?

Answer

Nominal Animal picture Nominal Animal · Sep 29, 2013

Short answer: Use objdump or readelf instead.

Long answer: Let's look at an actual example case, example.c:

#include <stdio.h>

static const char global1[] = "static const char []";
static const char *global2 = "static const char *";
static const char *const global3 = "static const char *const";
const char global4[] = "const char []";
const char *global5 = "const char *";
const char *const global6 = "const char *const";
char global7[] = "char []";
char *global8 = "char *";
char *const global9 = "char *const";

int main(void)
{
    static const char local1[] = "static const char []";
    static const char *local2 = "static const char *";
    static const char *const local3 = "static const char *const";
    const char local4[] = "const char []";
    const char *local5 = "const char *";
    const char *const local6 = "const char *const";
    char local7[] = "char []";
    char *local8 = "char *";
    char *const local9 = "char *const";

    printf("Global:\n");
    printf("\t%s\n", global1);
    printf("\t%s\n", global2);
    printf("\t%s\n", global3);
    printf("\t%s\n", global4);
    printf("\t%s\n", global5);
    printf("\t%s\n", global6);
    printf("\t%s\n", global7);
    printf("\t%s\n", global8);
    printf("\t%s\n", global9);
    printf("\n");
    printf("Local:\n");
    printf("\t%s\n", local1);
    printf("\t%s\n", local2);
    printf("\t%s\n", local3);
    printf("\t%s\n", local4);
    printf("\t%s\n", local5);
    printf("\t%s\n", local6);
    printf("\t%s\n", local7);
    printf("\t%s\n", local8);
    printf("\t%s\n", local9);

    return 0;
}

You can compile it to an object file using e.g.

gcc -W -Wall -c example.c

and to an executable using

gcc -W -Wall example.c -o example

You can use objdump -tr example.o to dump the symbol and relocation information for the (non-dynamic) object file, or objdump -TtRr example to dump the same for the executable file (and dynamic object files). Using

objdump -t example.o

on x86-64 I get

example.o:     file format elf64-x86-64

SYMBOL TABLE:
0000000000000000 l    df *ABS*  0000000000000000 example.c
0000000000000000 l    d  .text  0000000000000000 .text
0000000000000000 l    d  .data  0000000000000000 .data
0000000000000000 l    d  .bss   0000000000000000 .bss
0000000000000000 l    d  .rodata    0000000000000000 .rodata
0000000000000000 l     O .rodata    0000000000000015 global1
0000000000000000 l     O .data  0000000000000008 global2
0000000000000048 l     O .rodata    0000000000000008 global3
00000000000000c0 l     O .rodata    0000000000000015 local1.2053
0000000000000020 l     O .data  0000000000000008 local2.2054
00000000000000d8 l     O .rodata    0000000000000008 local3.2055
0000000000000000 l    d  .note.GNU-stack    0000000000000000 .note.GNU-stack
0000000000000000 l    d  .eh_frame  0000000000000000 .eh_frame
0000000000000000 l    d  .comment   0000000000000000 .comment
0000000000000050 g     O .rodata    000000000000000e global4
0000000000000008 g     O .data  0000000000000008 global5
0000000000000080 g     O .rodata    0000000000000008 global6
0000000000000010 g     O .data  0000000000000008 global7
0000000000000018 g     O .data  0000000000000008 global8
00000000000000a0 g     O .rodata    0000000000000008 global9
0000000000000000 g     F .text  000000000000027a main
0000000000000000         *UND*  0000000000000000 puts
0000000000000000         *UND*  0000000000000000 printf
0000000000000000         *UND*  0000000000000000 putchar
0000000000000000         *UND*  0000000000000000 __stack_chk_fail

The output is described in man 1 objdump, under the -t heading. Note that the second "column" is actually fixed-width: seven characters wide, describing the type of the object. The third column is the section name, *UND* for undefined, .text for code, .rodata for read-only (immutable) data, .data for initialized mutable data, and .bss for uninitialized mutable data, and so on.

We can see from the above symbol table that local4, local5, local6, local7, local8, and local9 variables didn't actually get entries in the symbol table at all. This is because they are local to main(). The contents of the strings they refer to are stored in .data or .rodata (or constructed on the fly), depending on what the compiler sees best.

Let's look at the relocation records next. Using

objdump -r example.o

I get

example.o:     file format elf64-x86-64

RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE              VALUE 
0000000000000037 R_X86_64_32S      .rodata+0x000000000000005e
0000000000000040 R_X86_64_32S      .rodata+0x000000000000006b
0000000000000059 R_X86_64_32S      .rodata+0x0000000000000088
0000000000000062 R_X86_64_32S      .rodata+0x000000000000008f
0000000000000067 R_X86_64_32       .rodata+0x00000000000000a8
000000000000006c R_X86_64_PC32     puts-0x0000000000000004
0000000000000071 R_X86_64_32       .rodata+0x00000000000000b0
0000000000000076 R_X86_64_32       .rodata
0000000000000083 R_X86_64_PC32     printf-0x0000000000000004
000000000000008a R_X86_64_PC32     .data-0x0000000000000004
000000000000008f R_X86_64_32       .rodata+0x00000000000000b0
000000000000009f R_X86_64_PC32     printf-0x0000000000000004
00000000000000a6 R_X86_64_PC32     .rodata+0x0000000000000044
00000000000000ab R_X86_64_32       .rodata+0x00000000000000b0
00000000000000bb R_X86_64_PC32     printf-0x0000000000000004
00000000000000c0 R_X86_64_32       .rodata+0x00000000000000b0
00000000000000c5 R_X86_64_32       global4
00000000000000d2 R_X86_64_PC32     printf-0x0000000000000004
00000000000000d9 R_X86_64_PC32     global5-0x0000000000000004
00000000000000de R_X86_64_32       .rodata+0x00000000000000b0
00000000000000ee R_X86_64_PC32     printf-0x0000000000000004
00000000000000f5 R_X86_64_PC32     global6-0x0000000000000004
00000000000000fa R_X86_64_32       .rodata+0x00000000000000b0
000000000000010a R_X86_64_PC32     printf-0x0000000000000004
000000000000010f R_X86_64_32       .rodata+0x00000000000000b0
0000000000000114 R_X86_64_32       global7
0000000000000121 R_X86_64_PC32     printf-0x0000000000000004
0000000000000128 R_X86_64_PC32     global8-0x0000000000000004
000000000000012d R_X86_64_32       .rodata+0x00000000000000b0
000000000000013d R_X86_64_PC32     printf-0x0000000000000004
0000000000000144 R_X86_64_PC32     global9-0x0000000000000004
0000000000000149 R_X86_64_32       .rodata+0x00000000000000b0
0000000000000159 R_X86_64_PC32     printf-0x0000000000000004
0000000000000163 R_X86_64_PC32     putchar-0x0000000000000004
0000000000000168 R_X86_64_32       .rodata+0x00000000000000b5
000000000000016d R_X86_64_PC32     puts-0x0000000000000004
0000000000000172 R_X86_64_32       .rodata+0x00000000000000b0
0000000000000177 R_X86_64_32       .rodata+0x00000000000000c0
0000000000000184 R_X86_64_PC32     printf-0x0000000000000004
000000000000018b R_X86_64_PC32     .data+0x000000000000001c
0000000000000190 R_X86_64_32       .rodata+0x00000000000000b0
00000000000001a0 R_X86_64_PC32     printf-0x0000000000000004
00000000000001a7 R_X86_64_PC32     .rodata+0x00000000000000d4
00000000000001ac R_X86_64_32       .rodata+0x00000000000000b0
00000000000001bc R_X86_64_PC32     printf-0x0000000000000004
00000000000001c1 R_X86_64_32       .rodata+0x00000000000000b0
00000000000001d6 R_X86_64_PC32     printf-0x0000000000000004
00000000000001db R_X86_64_32       .rodata+0x00000000000000b0
00000000000001ef R_X86_64_PC32     printf-0x0000000000000004
00000000000001f4 R_X86_64_32       .rodata+0x00000000000000b0
0000000000000209 R_X86_64_PC32     printf-0x0000000000000004
000000000000020e R_X86_64_32       .rodata+0x00000000000000b0
0000000000000223 R_X86_64_PC32     printf-0x0000000000000004
0000000000000228 R_X86_64_32       .rodata+0x00000000000000b0
000000000000023d R_X86_64_PC32     printf-0x0000000000000004
0000000000000242 R_X86_64_32       .rodata+0x00000000000000b0
0000000000000257 R_X86_64_PC32     printf-0x0000000000000004
0000000000000271 R_X86_64_PC32     __stack_chk_fail-0x0000000000000004


RELOCATION RECORDS FOR [.data]:
OFFSET           TYPE              VALUE 
0000000000000000 R_X86_64_64       .rodata+0x0000000000000015
0000000000000008 R_X86_64_64       .rodata+0x000000000000005e
0000000000000018 R_X86_64_64       .rodata+0x0000000000000088
0000000000000020 R_X86_64_64       .rodata+0x0000000000000015


RELOCATION RECORDS FOR [.rodata]:
OFFSET           TYPE              VALUE 
0000000000000048 R_X86_64_64       .rodata+0x0000000000000029
0000000000000080 R_X86_64_64       .rodata+0x000000000000006b
00000000000000a0 R_X86_64_64       .rodata+0x000000000000008f
00000000000000d8 R_X86_64_64       .rodata+0x0000000000000029


RELOCATION RECORDS FOR [.eh_frame]:
OFFSET           TYPE              VALUE 
0000000000000020 R_X86_64_PC32     .text

The relocation records are grouped by the section they relocation resides in. Because string contents are in the .data or .rodata sections, we can restrict ourselves to look at the relocations where the VALUE starts with .data or .rodata. (Mutable strings, like char global7[] = "char []";, are stored in .data, and immutable strings and string literals in .rodata.)

If we were to compile the code with debugging symbols enabled, it would be easier to determine which variable was used to refer to which string, but I might just look at the actual contents at each relocation value (target), to see which references to the immutable strings need fixing.

The command combination

objdump -r example.o | awk '($3 ~ /^\..*\+/) { t = $3; sub(/\+/, " ", t); n[t]++ } END { for (r in n) printf "%d %s\n", n[r], r }' | sort -g

will output the number of relocations per target, followed by the target section, followed by the target offset in the section, sorted with the target that occurs most in relocations last. That is, the last lines output above are the ones you need to concentrate on. For me, I get

1 .rodata
1 .rodata 0x0000000000000044
1 .rodata 0x00000000000000a8
1 .rodata 0x00000000000000b5
1 .rodata 0x00000000000000c0
1 .rodata 0x00000000000000d4
2 .rodata 0x0000000000000015
2 .rodata 0x0000000000000029
2 .rodata 0x000000000000005e
2 .rodata 0x000000000000006b
2 .rodata 0x0000000000000088
2 .rodata 0x000000000000008f
18 .rodata 0x00000000000000b0

If I add optimization (gcc -W -Wall -O3 -fomit-frame-pointer -c example.c), the result is

1 .rodata 0x0000000000000020
1 .rodata 0x0000000000000040
1 .rodata.str1.1
1 .rodata.str1.1 0x0000000000000058
2 .rodata.str1.1 0x000000000000000d
2 .rodata.str1.1 0x0000000000000021
2 .rodata.str1.1 0x000000000000005f
2 .rodata.str1.1 0x000000000000006c
3 .rodata.str1.1 0x000000000000003a
3 .rodata.str1.1 0x000000000000004c
18 .rodata.str1.1 0x0000000000000008

which shows that compiler options do have a big effect, but that there is that one target that is anyways used 18 times: section .rodata offset 0xb0 (.rodata.str1.1 offset 0x8 if optimization is enabled at compile time).

That is the `"\t%s\n" string literal.

Modifying the original program into

    char *local8 = "char *";
    char *const local9 = "char *const";

    const char *const fmt = "\t%s\n";

    printf("Global:\n");
    printf(fmt, global1);
    printf(fmt, global2);

and so on, replacing the format string with an immutable string pointer fmt, eliminates those 18 relocations altogether. (You can also use the equivalent const char fmt[] = "\t%s\n";, of course.)

The above analysis indicates that at least with GCC-4.6.3, most of the avoidable relocations are caused by (repeated use of) string literals. Replacing them with an array of const chars (const char fmt[] = "\t%s\n";) or a const pointer to const chars (const char *const fmt = "\t%s\n";) -- both cases putting the contents to .rodata section, read-only, and the pointer/array reference itself is immutable too -- seems an effective and safe strategy to me.

Furthermore, conversion of string literals to immutable string pointers or char arrays is completely a source-level task. That is, if you convert all string literals using the above method, you can eliminate at least one relocation per string literal.

In fact, I don't see how object-level analysis will help you much, here. It will tell you if your modifications reduce the number of relocations needed, of course.

The above awk stanza can be extended to a function that outputs the string constants for dynamic references with positive offsets:

#!/bin/bash
if [ $# -ne 1 ] || [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
    exec >&2
    echo ""
    echo "Usage: %s [ -h | --help ]"
    echo "       %s object.o"
    echo ""
    exit 1
fi

export LANG=C LC_ALL=C

objdump -wr "$1" | awk '
    BEGIN {
        RS = "[\t\v\f ]*[\r\n][\t\n\v\f\r ]*"
        FS = "[\t\v\f ]+"
    }

    $1 ~ /^[0-9A-Fa-f]+/ {
        n[$3]++
    }

    END {
        for (s in n)
            printf "%d %s\n", n[s], s
    }
' | sort -g | gawk -v filename="$1" '
    BEGIN {
        RS = "[\t\v\f ]*[\r\n][\t\n\v\f\r ]*"
        FS = "[\t\v\f ]+"

        cmd = "objdump --file-offsets -ws " filename
        while ((cmd | getline) > 0)
            if ($3 == "section") {
                s = $4
                sub(/:$/, "", s)
                o = $NF
                sub(/\)$/, "", o)
                start[s] = strtonum(o)
            }
        close(cmd)
    }

    {
        if ($2 ~ /\..*\+/) {
            s = $2
            o = $2
            sub(/\+.*$/, "", s)
            sub(/^[^\+]*\+/, "", o)
            o = strtonum(o) + start[s]
            cmd = "dd if=\"" filename "\" of=/dev/stdout bs=1 skip=" o " count=256"
            OLDRS = RS
            RS = "\0"
            cmd | getline hex
            close(cmd)
            RS = OLDRS
            gsub(/\\/, "\\\\", hex)
            gsub(/\t/, "\\t", hex)
            gsub(/\n/, "\\n", hex)
            gsub(/\r/, "\\r", hex)
            gsub(/\"/, "\\\"", hex)
            if (hex ~ /[\x00-\x1F\x7F-\x9F\xFE\xFF]/ || length(hex) < 1)
                printf "%s\n", $0
            else
                printf "%s = \"%s\"\n", $0, hex
        } else
            print $0
    }
'

This is a bit crude, just slapped together, so I don't know how portable it is. On my machine, it does seem to find the string literals for the few test cases I tried it on; you should probably rewrite it to match your own needs. Or even use an actual programming language with ELF support to examine the object files directly.

For the example program shown above (prior to the modifications I suggest to reduce the number of relocations), compiled without optimization, the above script yields the output

1 .data+0x000000000000001c = ""
1 .data-0x0000000000000004
1 .rodata
1 .rodata+0x0000000000000044 = ""
1 .rodata+0x00000000000000a8 = "Global:"
1 .rodata+0x00000000000000b5 = "Local:"
1 .rodata+0x00000000000000c0 = "static const char []"
1 .rodata+0x00000000000000d4 = ""
1 .text
1 __stack_chk_fail-0x0000000000000004
1 format
1 global4
1 global5-0x0000000000000004
1 global6-0x0000000000000004
1 global7
1 global8-0x0000000000000004
1 global9-0x0000000000000004
1 putchar-0x0000000000000004
2 .rodata+0x0000000000000015 = "static const char *"
2 .rodata+0x0000000000000029 = "static const char *const"
2 .rodata+0x000000000000005e = "const char *"
2 .rodata+0x000000000000006b = "const char *const"
2 .rodata+0x0000000000000088 = "char *"
2 .rodata+0x000000000000008f = "char *const"
2 puts-0x0000000000000004
18 .rodata+0x00000000000000b0 = "\t%s\n"
18 printf-0x0000000000000004

Finally, you might notice that using a function pointer to printf() instead of calling printf() directly will reduce another 18 relocations from the example code, but I would consider that a mistake.

For code, you want relocations, as indirect function calls (calls via function pointers) are much slower than direct calls. Simply put, those relocations make function and subroutine calls much faster, so you most definitely want to keep those.

Apologies for the long answer; hope you find this useful. Questions?