Skip to content
GOT and PLT
step 1/5

Reading — step 1 of 5

Read

~1 min readDynamic Linking

GOT and PLT

Position-independent code can't bake in absolute addresses (the loader picks the load address). Instead: indirection via GOT and PLT.

GOT (Global Offset Table):

  • Per-shared-object table of pointers.
  • For data accesses: mov rax, [GOT[symbol]].
  • Dynamic linker fills in addresses at load time.

PLT (Procedure Linkage Table):

  • Stub functions for external function calls.
  • call foo@plt → jumps to PLT[foo] → looks up GOT[foo] → calls actual foo.

Lazy resolution (default):

  • First call to PLT[foo]: jumps to dynamic linker stub.
  • Linker resolves foo, fills GOT entry.
  • PLT updates so subsequent calls go directly.
  • Saves startup time but slows first call.

-z now (or LD_BIND_NOW=1): resolve all symbols at startup. Slower start, faster later.

PLT entry (x86-64):

foo@plt:
    jmp [GOT[foo]]
    push <reloc_index>
    jmp _PLT0           # generic resolver stub

First time: GOT[foo] = address of "push + jmp resolver" stub. Resolver looks up foo, writes real address to GOT[foo]. Returns.

Second time: jmp [GOT[foo]] goes directly to foo.

GOT layout:

  • GOT[0]: address of .dynamic.
  • GOT[1]: link map (struct used by linker).
  • GOT[2]: address of resolver function.
  • GOT[3+]: per-symbol entries.

Security:

  • Original GOT was writable (needed for lazy fill).
  • Attacker who corrupts GOT can redirect calls.
  • RELRO (Relocation Read-Only): mark GOT read-only after init. Mitigation.
  • Full RELRO + -z now: GOT fully filled at start, then read-only. Strongest.

Modern compilers default to RELRO + lazy. Full RELRO is opt-in for security-critical apps.

Inspecting:

  • objdump -R foo: list dynamic relocations.
  • readelf --dyn-syms foo: dynamic symbol table.
  • objdump -d -j .plt foo: PLT disassembly.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…