In a dynamically-linked ELF executable, all external function calls actually jump to the process linkage table. When such a function call is made in a process, the PLT indexes global offset table for the callee’s real address. Since all function symbols use lazy binding by default, if the function has not been called before, then its GOT entry will point back at the PLT, in which case PLT will just ask the runtime to find the function address and populate the entry. In the future, all calls that arrive at the PLT will look up the GOT and jump to the real function address; this act of jumping somewhere (PLT) then immediately jumping somewhere else (actual function) is called trampolining.

graph LR
    A[call fn] --- B[PLT] 
    C[GOT] --- D[actual fn]
    E[Runtime] -->|populates| C
    B --->|first-time| E
    B --->|future| C

On the other hand, if there’s any direct references to the function addresses (not just a call), the respective function’s entry will be placed in .plt.got, which contains actual function addresses, resolved at load time (i.e., not lazy-binded).

  • Why doesn’t ELF just use GOT and ditch PLT—why the layers of indirection? The -fno-plt option just disables PLT and uses GOT directly, so surely PLT is not absolute necessary?to-do (see source)