Processing ELF relocations – understanding the relocs, symbols, section data, and how they work together

elfopcoderelocationreverse-engineeringsymbol-table

TL;DR

I tried to make this a short question but it's a complicated problem so it ended up being long. If you can answer any part of this or give any suggestions or tips or resources or anything at all, it would be extremely helpful (even if you don't directly solve all my issues). I'm banging my head against the wall right now. 🙂

Here are the specific issues I am having. Read on below for more information.

  • I'm looking for guidance on how to process relocation entries and update the unresolved symbols in the section data. I simply don't understand what to do with all the information I've pulled from the relocations and the sections, etc.
  • I'm also hoping to understand just what is going on when the linker encounters relocations. Trying to correctly implement the relocation equations and use all the correct values in the correct way is incredibly challenging.
  • When I encounter op codes and addresses and symbols, etc, I need to understand what to do with them. I feel like I am missing some steps.
  • I feel like I don't have a good grasp on how the symbol table entries interact with the relocations. How should I use the symbol's binding, visibility, value, and size information?
  • Lastly, when I output my file with the resolved data and new relocation entries used by the executable, the data is all incorrect. I'm not sure how to follow all the relocations and provide all the information necessary. What is the executable expecting from me?

My approach so far

I am trying to create a relocation file in a specific [undocumented] proprietary format that is heavily based on ELF. I have written a tool that takes an ELF file and a partially linked file (PLF) and processes them to output the fully resolved rel file. This rel file is used to load/unload data as needed in order to save memory. The platform is a 32bit PPC. One wrinkle is that tool is written for Windows in c#, but the data is intended for the PPC, so there are fun endian issues and the like to watch out for.

I've been trying to understand how relocations are handled when used to resolve unresolved symbols and so on. What I've done so far is to copy the relevant sections from the PLF and then for each corresponding .rela section, I parse the entries and attempt to fix up the section data and generate new relocation entries as needed. But this is where my difficulty is. I'm way out of my element here and this sort of thing seems to typically be done by linkers and loaders so there's not a lot of good examples to draw upon. But I have found a few that have been of help, including THIS ONE.

So what is happening is:

  1. Copy section data from PLF to be used for rel file. I'm only interested in the .init (no data), .text, .ctors, .dtors, .rodata, .data, .bss (no data), and another custom section we are using.
  2. Iterate over the .rela sections in the PLF and read in the Elf32_Rela entries.
  3. For each entry, I pull out the r_offset, r_info, and r_addend fields and extract the relevant info from r_info (the symbol and the reloc type).
  4. From the PLF's symbol table, I can get the symbolOffset, the symbolSection, and the symbolValue.
  5. From the ELF, I get the symbolSection's load address.
  6. I compute int localAddress = ( .relaSection.Offset + r_offset ).
  7. I get the uint relocValue from the symbolSection's contents at r_offset.
  8. Now I have all the info I need so I do a switch on the reloc type and process the data. These are the types I support:
    R_PPC_NONE
    R_PPC_ADDR32
    R_PPC_ADDR24
    R_PPC_ADDR16
    R_PPC_ADDR16_LO
    R_PPC_ADDR16_HI
    R_PPC_ADDR16_HA
    R_PPC_ADDR14
    R_PPC_ADDR14_BRTAKEN
    R_PPC_ADDR14_BRNTAKEN
    R_PPC_REL24
    R_PPC_REL14
    R_PPC_REL14_BRTAKEN
    R_PPC_REL14_BRNTAKEN
  9. Now what?? I need to update the section data and build companion relocation entries. But I don't understand what is necessary to do and how to do it.

The whole reason I'm doing this is because there is an old obsolete unsupported tool that does not support using custom sections, which is a key requirement for this project (for memory reasons). We have a custom section that contains a bunch of initialization code (totaling about a meg) that we want to unload after start up. The existing tool just ignores all the data in that section.

So while making our own tool that does support custom sections is ideal, if there are any bright ideas for another way to achieve this goal, I'm all ears! We've floated around an idea of using the .dtor section for our data since it's nearly empty anyway. But this is messy and might not work anyway if it prevents a clean shutdown.


Relocations plus example code

When I process the relocations, I'm working off of the equations and information found in the ABI docs HERE (around section 4.13, page 80ish) as well as a number of other code examples and blog posts I've dug up. But it's all so confusing and not really spelled out and all the code I've found does things a little differently.

For example,

  • R_PPC_ADDR16_LO –> half16: #lo(S + A)
  • R_PPC_ADDR14_BRTAKEN –> low14*: (S + A) >> 2
  • etc

So when I see this kind of code, how do I decipher it?

Here's one example (from this source)

case ELF::R_PPC64_ADDR14 : {
    assert(((Value + Addend) & 3) == 0);
    // Preserve the AA/LK bits in the branch instruction
    uint8_t aalk = *(LocalAddress+3);
    writeInt16BE(LocalAddress + 2, (aalk & 3) | ((Value + Addend) & 0xfffc));
} break;

case ELF::R_PPC64_REL24 : {
    uint64_t FinalAddress = (Section.LoadAddress + Offset);
    int32_t delta = static_cast<int32_t>(Value - FinalAddress + Addend);
    if (SignExtend32<24>(delta) != delta)
        llvm_unreachable("Relocation R_PPC64_REL24 overflow");
    // Generates a 'bl <address>' instruction
    writeInt32BE(LocalAddress, 0x48000001 | (delta & 0x03FFFFFC));
} break;

Here's some from another example (here)

case R_PPC_ADDR32: /* word32 S + A */
    addr = elf_lookup(lf, symidx, 1);
    if (addr == 0)
        return -1;
    addr += addend;
    *where = addr;
    break;

case R_PPC_ADDR16_LO: /* #lo(S) */
    if (addend != 0) {
        addr = relocbase + addend;
    } else {
        addr = elf_lookup(lf, symidx, 1);
        if (addr == 0)
            return -1;
    }
    *hwhere = addr & 0xffff;
    break;

case R_PPC_ADDR16_HA: /* #ha(S) */
    if (addend != 0) {
        addr = relocbase + addend;
    } else {
        addr = elf_lookup(lf, symidx, 1);
        if (addr == 0)
            return -1;
    }
    *hwhere = ((addr >> 16) + ((addr & 0x8000) ? 1 : 0)) & 0xffff;
    break;

And one other example (from here)

case R_PPC_ADDR16_HA:
    write_be16 (dso, rela->r_offset, (value + 0x8000) >> 16);
    break;
case R_PPC_ADDR24:
    write_be32 (dso, rela->r_offset, (value & 0x03fffffc) | (read_ube32 (dso, rela->r_offset) & 0xfc000003));
    break;
case R_PPC_ADDR14:
    write_be32 (dso, rela->r_offset, (value & 0xfffc) | (read_ube32 (dso, rela->r_offset) & 0xffff0003));
    break;
case R_PPC_ADDR14_BRTAKEN:
case R_PPC_ADDR14_BRNTAKEN:
    write_be32 (dso, rela->r_offset, (value & 0xfffc)
                                    | (read_ube32 (dso, rela->r_offset) & 0xffdf0003)
                                    | ((((GELF_R_TYPE (rela->r_info) == R_PPC_ADDR14_BRTAKEN) << 21)
                                    ^ (value >> 10)) & 0x00200000));
    break;
case R_PPC_REL24:
    write_be32 (dso, rela->r_offset, ((value - rela->r_offset) & 0x03fffffc) | (read_ube32 (dso, rela->r_offset) & 0xfc000003));
    break;
case R_PPC_REL32:
    write_be32 (dso, rela->r_offset, value - rela->r_offset);
    break;

I really want to understand the magic these guys are doing here and why their code doesn't always look the same. I think some of the code is making assumptions that the data was already properly masked (for branches, etc), and some of the code is not. But I don't understand any of this at all.


Following the symbols/data/relocations, etc

When I look at the data in a hexeditor, I see a bunch of "48 00 00 01" all over. I've figured out that this is an opcode and needs to be updated with relocation information (this specifically is for 'bl' branch and link), yet my tool doesn't operate on the vast majority of them and the ones that I do update have the wrong values in them (compared to an example made by an obsolete tool). Clearly I am missing some part of the process.

In addition to the section data, there are additional relocation entries that need to be added to the end of the rel file. These consist of internal and external relocations but I haven't figured out much at all about these yet. (what's the difference between the two and when do you use one or the other?)

If you look near the end of this file at the function RuntimeDyldELF::processRelocationRef, you'll see some Relocation Entries being created. They also make stub functions. I suspect this is the missing link for me, but it's as clear as mud and I'm not following it even a little bit.

When I output the symbols in each relocation entry, they each have a binding/visibility [Global/Weak/Local] [Function/Object] and a value, a size, and a section. I know the section is where the symbol is located, and the value is the offset to the symbol in that section (or is it the virtual address?). The size is the size of the symbol, but is this important? Maybe the global/weak/local is useful for determining if it's an internal or external relocation?

Maybe this relocation table I'm talking about creating is actually a symbol table for my rel file? Maybe this table updates the symbol value from being a virtual address to being a section offset (since that's what the value is in relocatable files and the symbol table in the PLF is basically in an executable)?


Some resources:

  1. blog on relocations: http://eli.thegreenplace.net/2011/08/25/load-time-relocation-of-shared-libraries/
  2. mentions opcodes at the end: http://wiki.netbsd.org/examples/elf_executables_for_powerpc/
  3. my related unanswered question: ELF Relocation reverse engineering

Whew! That's a beast of a question. Congrats if you made it this far. 🙂 Thanks in advance for any help you can give me.

Best Solution

I stumbled across this question and thought it deserved an answer.

Have elf.h handy. You can find it on the internet.

Each RELA section contains an array of Elf32_Rela entries as you know, but is also tied to a certain other section. r_offset is an offset into that other section (in this case - it works differently for shared libraries). You will find that section headers have a member called sh_info. This tells you which section that is. (It's an index into the section header table as you would expect.)

The 'symbol', which you got from r_info is in fact an index into a symbol table residing in another section. Look for the member sh_link in your RELA section's header.

The symbol table tells you the name of the symbol you are looking for, in the form of the st_name member of Elf32_Sym. st_name is an offset into a string section. Which section that is, you get from the sh_link member of your symbol table's section header. Sorry if this gets confusing.

Elf32_Shdr *sh_table = elf_image + ((Elf32_Ehdr *)elf_image)->e_shoff;
Elf32_Rela *relocs = elf_image + sh_table[relocation_section_index]->sh_offset;

unsigned section_to_modify_index = sh_table[relocation_section_index].sh_info;
char *to_modify = elf_image + sh_table[section_to_modify_index].sh_offset;

unsigned symbol_table_index = sh_table[relocation_section_index].sh_link;
Elf32_Sym *symbol_table = elf_image + sh_table[symbol_table_index].sh_offset;

unsigned string_table_index = sh_table[symbol_table].sh_link;
char *string_table = elf_image + sh_table[string_table_index].sh_offset;

Let's say we are working with relocation number i.

Elf32_Rela *rel = &relocs[i];
Elf32_Sym *sym = &symbol_table[ELF32_R_SYM(rel->r_info)];
char *symbol_name = string_table + sym->st_name;

Find the address of that symbol (let's say that symbol_name == "printf"). The final value will go in (to_modify + rel->r_offset).

As for the table on pages 79-83 of the pdf you linked, it tells us what to put at that address, and how many bytes to write. Obviously the address we just got (of printf in this case) is part of most of them. It corresponds to the S in the expressions.

r_addend is just A. Sometimes the compiler needs to add a static constant to a reloc i guess.

B is the base address of the shared object, or 0 for executable programs because they are not moved.

So if ELF32_R_TYPE(rel->r_info) == R_PPC_ADDR32 we have S + A, and the word size is word32 so we would get:

*(uint32_t *)(to_modify + rel->r_offset) = address_of_printf + rel->r_addend;

...and we have successfully performed a relocation.

I can't help you when it comes to the #lo, #hi, etc and the word sizes like low14. I know nothing about PPC but the linked pdf seems reasonable enough.

I also don't know about the stub functions. You don't normally need to know about those when linking (dynamically at least).

I'm not sure if I've answered all of your questions but you should be able to see what your example code does now at least.

Related Question