What is the purpose of the procedure linkage table?

Learn what is the purpose of the procedure linkage table? with practical examples, diagrams, and best practices. Covers c, linux, binary development techniques with visual explanations.

Understanding the Procedure Linkage Table (PLT) in Linux Binaries

Hero image for What is the purpose of the procedure linkage table?

Explore the crucial role of the Procedure Linkage Table (PLT) and Global Offset Table (GOT) in dynamic linking for Linux executables, enabling shared libraries and efficient code reuse.

When a program on a Linux system uses shared libraries, it doesn't directly embed the code for every function it calls. Instead, it relies on dynamic linking, where the actual function addresses are resolved at runtime. This mechanism allows for efficient memory usage, easier updates to libraries, and reduced binary sizes. At the heart of this dynamic linking process are two critical data structures: the Procedure Linkage Table (PLT) and the Global Offset Table (GOT).

The Challenge of Dynamic Linking

Consider a program that calls a function like printf() from the standard C library (libc.so). At compile time, the exact memory address of printf() within libc.so is unknown because libc.so might be loaded at a different base address each time the program runs. Hardcoding an address would break the program. The solution requires a flexible way to locate and jump to these external functions.

How PLT and GOT Work Together

The PLT and GOT work in tandem to resolve external function calls. The PLT is a section of executable code within your program, while the GOT is a section of data. When your program calls an external function for the first time, the PLT entry for that function acts as a trampoline, redirecting execution to the dynamic linker. The dynamic linker then finds the actual address of the function in the shared library, updates the corresponding GOT entry with this address, and transfers control to the function. Subsequent calls to the same function will directly jump to its resolved address via the updated GOT entry, bypassing the dynamic linker.

flowchart TD
    A[Program Calls External Function (e.g., printf)] --> B{PLT Entry for printf}
    B --> |First Call| C[Jump to Dynamic Linker Stub]
    C --> D[Dynamic Linker Resolves printf Address]
    D --> E[Update GOT Entry for printf]
    E --> F[Jump to Actual printf Function]
    F --> G[printf Executes]
    G --> H[Return to Program]
    B --> |Subsequent Calls| I[Jump to GOT Entry for printf]
    I --> F

Dynamic Linking Process via PLT and GOT

Anatomy of a PLT Entry

Each external function called by your program has a corresponding entry in the PLT. These entries are typically small code stubs. Let's look at a simplified x86 assembly representation of a PLT entry for a function foo:

; PLT entry for 'foo'
foo@plt:
    jmp    qword ptr [rip + GOT_OFFSET_FOR_FOO]  ; 1. Jump via GOT
    push   N                                     ; 2. Push relocation index
    jmp    qword ptr [rip + PLT_RESOLVER_OFFSET] ; 3. Jump to dynamic linker resolver

Simplified x86-64 PLT entry structure

Here's a breakdown of the PLT entry's behavior:

  1. Initial Call (Unresolved): When foo is called for the first time, the jmp instruction reads the address from the GOT entry. Initially, this GOT entry points back into the PLT itself, specifically to the push N instruction.
  2. Push Relocation Index: The push N instruction pushes an index onto the stack. This index tells the dynamic linker which relocation entry (and thus which function) needs to be resolved.
  3. Jump to Dynamic Linker: The second jmp instruction transfers control to the dynamic linker's resolver routine. This routine is responsible for finding the actual address of foo in the shared library.
  4. GOT Update and Execution: The dynamic linker updates the GOT entry for foo with the actual function address. It then jumps to foo.
  5. Subsequent Calls (Resolved): On all subsequent calls to foo, the first jmp instruction in the PLT entry will directly jump to the actual foo function's address, as the GOT entry has been updated.

Security Implications: GOT Overwrites

While essential for dynamic linking, the GOT can also be a target for security exploits. If an attacker can overwrite an entry in the GOT, they can redirect a legitimate function call to arbitrary malicious code. This technique, known as a GOT overwrite, is a common method in exploit development. Modern systems employ various mitigations, such as Read-Only Relocations (RELRO), to protect the GOT from being written to after initialization.

#include <stdio.h>

void external_function() {
    printf("Hello from external_function!\n");
}

int main() {
    printf("Calling external_function...\n");
    external_function();
    printf("external_function returned.\n");
    return 0;
}

A simple C program demonstrating an external function call that would use PLT/GOT

When compiling the above C code into an executable that links against a shared library containing external_function, the compiler and linker will set up PLT and GOT entries to handle the dynamic resolution of external_function at runtime.