143

From time to time, when disassembling x86 binaries, I stumble on reference to PLT and GOT, especially when calling procedures from a dynamic library.

For example, when running a program in gdb:

(gdb) info file
Symbols from "/home/user/hello".
Local exec file: `/home/user/hello', file type elf64-x86-64.
Entry point: 0x400400
    0x0000000000400200 - 0x000000000040021c is .interp
    0x000000000040021c - 0x000000000040023c is .note.ABI-tag
    0x000000000040023c - 0x0000000000400260 is .note.gnu.build-id
    0x0000000000400260 - 0x0000000000400284 is .hash
    0x0000000000400288 - 0x00000000004002a4 is .gnu.hash
    0x00000000004002a8 - 0x0000000000400308 is .dynsym
    0x0000000000400308 - 0x0000000000400345 is .dynstr
    0x0000000000400346 - 0x000000000040034e is .gnu.version
    0x0000000000400350 - 0x0000000000400370 is .gnu.version_r
    0x0000000000400370 - 0x0000000000400388 is .rela.dyn
    0x0000000000400388 - 0x00000000004003b8 is .rela.plt
    0x00000000004003b8 - 0x00000000004003c6 is .init
 => 0x00000000004003d0 - 0x0000000000400400 is .plt
    0x0000000000400400 - 0x00000000004005dc is .text
    0x00000000004005dc - 0x00000000004005e5 is .fini
    0x00000000004005e8 - 0x00000000004005fa is .rodata
    0x00000000004005fc - 0x0000000000400630 is .eh_frame_hdr
    0x0000000000400630 - 0x00000000004006f4 is .eh_frame
    0x00000000006006f8 - 0x0000000000600700 is .init_array
    0x0000000000600700 - 0x0000000000600708 is .fini_array
    0x0000000000600708 - 0x0000000000600710 is .jcr
    0x0000000000600710 - 0x00000000006008f0 is .dynamic
 => 0x00000000006008f0 - 0x00000000006008f8 is .got
 => 0x00000000006008f8 - 0x0000000000600920 is .got.plt
    0x0000000000600920 - 0x0000000000600930 is .data
    0x0000000000600930 - 0x0000000000600938 is .bss

And, then when disassembling (puts@plt):

(gdb) disas foo
Dump of assembler code for function foo:
   0x000000000040050c <+0>: push   %rbp
   0x000000000040050d <+1>: mov    %rsp,%rbp
   0x0000000000400510 <+4>: sub    $0x10,%rsp
   0x0000000000400514 <+8>: mov    %edi,-0x4(%rbp)
   0x0000000000400517 <+11>:    mov    $0x4005ec,%edi
=> 0x000000000040051c <+16>:    callq  0x4003e0 <puts@plt>
   0x0000000000400521 <+21>:    leaveq
   0x0000000000400522 <+22>:    retq
End of assembler dump.

So, what are these GOT/PLT ?

1
  • 10
    I suggest reading the book Linkers and Loaders, excellent book about the subject. the manuscripts are freely available here: iecc.com/linker Commented May 5, 2013 at 7:24

2 Answers 2

144

PLT stands for Procedure Linkage Table which is, put simply, used to call external procedures/functions whose address isn't known in the time of linking, and is left to be resolved by the dynamic linker at run time.

GOT stands for Global Offsets Table and is similarly used to resolve addresses. Both PLT and GOT and other relocation information is explained in greater length in this article.

Also, Ian Lance Taylor, the author of GOLD has put up an article series on his blog which is totally worth reading (twenty parts!):

0
39

Let me summarize the links given at https://reverseengineering.stackexchange.com/a/1993/12321 without going into serious disasembly analysis for now.

When the Linux kernel + dynamic linker is going to run a binary with exec, it traditionally just dumped the ELF section into a known memory location specified by the linker during link time.

So, whenever your coded:

  • referenced a global variable inside your code
  • called a function from inside your code

the compiler + linker could just hardcode the address into the assembly and everything would work.

However, how can we do it when dealing with shared libraries, which must necessarily get loaded at potentially different addresses every time to avoid conflicts between two shared libraries?

The naive solution would be to keep relocation metadata on the final executable, much like the actual linker does and whenever the program is loaded, have the dynamic linker go over every single access and patch it up with the right address.

However, this would be too time consuming, since there could be a lot of references to patch on a program, and then that program would take a long time to start running.

The solution, as usual, is to add another level of indirection: the GOT and PLT, which are two extra chunks of memory setup by the compilation system + dynamic linker.

After the program is launched, the dynamic linker checks the address of shared libraries, and hacks up the GOT and PLT so that it will point correctly to the required shared library symbols:

  • whenever a global variable of a shared library is accessed by your program, the compiler + linker emits instead two memory accesses:

    mov    0x200271(%rip),%rax        # 200828 <_DYNAMIC+0x1a0>
    mov    (%rax),%eax
    

    The first one load the true address of the variable from the GOT, which the dynamic linker previously set, into rax.

    The second indirect access actually accesses the variable indirectly through the address from rax.

  • for code, things are a bit more complicated.

    Whenever a function from a shared library is called, the linker makes us jump to an address in the PLT.

    The first time the function is called, the PLT code uses offsets stored in the GOT to decide the actual final location of the function, and then:

    • stores this pre-calculated value
    • jumps there

    The next times the function is called, the value has already been calculated, so it just jumps there directly.

    Due to this lazy resolution mechanism:

    • programs can start running quickly even if the shared libraries have a lot of symbols
    • we can replace functions on the fly by playing with the LD_PRELOAD variable

Nowadays, position independent executables (PIE) are the default on distros such as Ubuntu 18.04.

Much like shared libraries, these executables are compiled so that they can be placed at a random position in memory whenever they are executed, in order to make certain vulnerabilities harder to exploit.

Therefore, it is not possible to hardcode absolute function and variable addresses anymore in that case. Executables must either:

  • user instruction pointer relative addressing if those are available on the assembly language, e.g.:
    • ARMv8:
      • B does 26-bit jumps, B.cond 19-bit
      • "LDR (literal)" does 19-bit loads
      • ADR calculates 21-bit relative addresses that other instructions can use
  • use the GOT / PLT otherwise

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.