Developing a Dynamic Linker from Scratch: C Language Framework

Design and Implementation of the C Language Part of the Dynamic Linker

<span>dynamic_linker.c</span> contains the main logic of the dynamic linker.

1. Stack Parameter Parsing

<span>parse_stack</span> function is responsible for parsing all parameters from the stack pointer. A detailed introduction can be found in the previous article “Developing a Dynamic Linker from Scratch: Kernel Parameter Interaction,” so it will not be repeated here.

static void parse_stack(void *stack_ptr, int *argc, char ***argv, char ***envp, uint64_t **auxv)
{
    uint64_t *sp = (uint64_t *)stack_ptr;
    
    // Read argc
    *argc = (int)sp[0];
    
    // Calculate argv address (skipping argc)
    *argv = (char **)(sp + 1);
    
    // Calculate envp address (skipping argv array, argc+1 elements including NULL)
    *envp = (char **)(sp + 1 + (*argc) + 1);
    
    // Find the end of envp (NULL)
    char **env = *envp;
    while (*env != NULL)
    {
        env++;
    }
    
    // auxv is at the next position after envp ends
    *auxv = (uint64_t *)(env + 1);
}

This function parses according to the stack layout introduced in the previous article:

  1. <span>argc</span>: Read from <span>[sp]</span>
  2. <span>argv</span>: Start from <span>[sp+8]</span>, a total of <span>argc+1</span> elements (including NULL)
  3. <span>envp</span>: Start after the end of the <span>argv</span>, need to traverse to find NULL
  4. <span>auxv</span>: Start after the end of <span>envp</span>

2. Auxiliary Vector Parsing

The auxiliary vector (auxv) contains key information passed by the kernel. Before parsing the auxiliary vector, let’s take a look at the <span>get_auxv</span> helper function:

get_auxv Function

<span>get_auxv</span> function is used to find the value of a specified tag from the auxiliary vector array:

/**
 * Get auxiliary vector value
 * 
 * @param auxv: Auxiliary vector array
 * @param tag: Tag to search for
 * @return: Corresponding value, returns 0 if not found
 */
static uint64_t get_auxv(uint64_t *auxv, uint64_t tag)
{
    if (!auxv) 
    {
        return 0;
    }
    
    // Traverse the auxiliary vector array
    for (uint64_t *p = auxv; p[0] != AT_NULL; p += 2) 
    {
        if (p[0] == tag) 
        {
            return p[1];
        }
    }
    
    return 0;
}

Function Description:

  1. Parameters:

  • <span>auxv</span>: Pointer to the auxiliary vector array, each entry contains two <span>uint64_t</span> values (tag and value)
  • <span>tag</span>: Tag to search for (e.g., <span>AT_ENTRY</span>, <span>AT_PHDR</span>, etc.)
  • Return Value:

    • If the corresponding tag is found, return its <span>value</span>
    • If not found or <span>auxv</span> is NULL, return 0
  • Working Principle:

    • Traverse the auxiliary vector array, skipping two <span>uint64_t</span> each time (one tag, one value)
    • Compare if the current entry’s tag matches the target tag
    • If matched, return the value at the next position (i.e., the value of that entry)
    • If encountering <span>AT_NULL</span> (tag=0), it indicates the end of the array, stop traversing
  • Data Structure of Auxiliary Vector:

    auxv[0] = tag1
    auxv[1] = value1
    auxv[2] = tag2
    auxv[3] = value2
    ...
    auxv[n] = AT_NULL (0)
    auxv[n+1] = 0
    
  • Usage Example:

    uint64_t entry_point = get_auxv(auxv, AT_ENTRY);  // Get entry point address
    uint64_t phdr_addr = get_auxv(auxv, AT_PHDR);     // Get program header address
    
  • parse_auxv Function

    <span>parse_auxv</span> function uses <span>get_auxv</span> to parse and save all important auxiliary vector values:

    static void parse_auxv(uint64_t *auxv)
    {
        if (!auxv) 
        {
            printf_simple("auxv is NULL\n");
            return;
        }
        
        printf_simple("=== Parsing Auxiliary Vector ===\n");
        
        // Parse and save important auxv values
        g_auxv_entry = get_auxv(auxv, AT_ENTRY);
        g_auxv_phdr = get_auxv(auxv, AT_PHDR);
        g_auxv_phent = get_auxv(auxv, AT_PHENT);
        g_auxv_phnum = get_auxv(auxv, AT_PHNUM);
        g_auxv_pagesz = get_auxv(auxv, AT_PAGESZ);
        g_auxv_base = get_auxv(auxv, AT_BASE);
        
        uint64_t platform_ptr = get_auxv(auxv, AT_PLATFORM);
        if (platform_ptr) 
        {
            g_auxv_platform = (const char *)platform_ptr;
        }
        
        uint64_t execfn_ptr = get_auxv(auxv, AT_EXECFN);
        if (execfn_ptr) 
        {
            g_auxv_execfn = (const char *)execfn_ptr;
        }
        
        // Use printf_simple to print all important auxv values
        if (g_auxv_entry) 
        {
            printf_simple("AT_ENTRY: 0x%x\n", (unsigned int)g_auxv_entry);
        }
        if (g_auxv_phdr) 
        {
            printf_simple("AT_PHDR: 0x%x\n", (unsigned int)g_auxv_phdr);
        }
        if (g_auxv_phent) 
        {
            printf_simple("AT_PHENT: %d\n", (int)g_auxv_phent);
        }
        if (g_auxv_phnum) 
        {
            printf_simple("AT_PHNUM: %d\n", (int)g_auxv_phnum);
        }
        if (g_auxv_pagesz) 
        {
            printf_simple("AT_PAGESZ: %d\n", (int)g_auxv_pagesz);
        }
        if (g_auxv_base) 
        {
            printf_simple("AT_BASE: 0x%x\n", (unsigned int)g_auxv_base);
        }
        if (g_auxv_platform) 
        {
            printf_simple("AT_PLATFORM: %s\n", g_auxv_platform);
        }
        if (g_auxv_execfn) 
        {
            printf_simple("AT_EXECFN: %s\n", g_auxv_execfn);
        }
        
        printf_simple("==============================\n");
    }
    

    These global variables store key information, which will be used later for:

    • <span>g_auxv_phdr</span>: Locating the program header of the executable file
    • <span>g_auxv_entry</span>: Entry point of the executable file, which needs to be jumped to eventually
    • <span>g_auxv_base</span>: Base address of the dynamic linker itself
    • <span>g_auxv_pagesz</span>: Page size, used for memory mapping

    4. Environment Variable Handling

    The dynamic linker needs to handle some special environment variables:

        char *ld_library_path = get_env("LD_LIBRARY_PATH");
        if (ld_library_path) 
        {
            printf_simple("LD_LIBRARY_PATH=%s\n", ld_library_path);
        } else
        {
            printf_simple("LD_LIBRARY_PATH not set\n");
        }
        
        char *ld_preload = get_env("LD_PRELOAD");
        if (ld_preload) 
        {
            printf_simple("LD_PRELOAD=%s\n", ld_preload);
        }
        
        char *path = get_env("PATH");
        if (path) 
        {
            printf_simple("PATH=%s\n", path);
        }
    
    • <span>LD_LIBRARY_PATH</span>: Dynamic library search path
    • <span>LD_PRELOAD</span>: List of preloaded dynamic libraries
    • <span>PATH</span>: Executable file search path

    Special Notes (Pitfalls)

    It can be seen that the printed information uses the dynamic linker’s own implementation of <span>printf_simple</span> instead of the <span>printf</span> function from <span>libmini_libc.so</span>. The reason is:The dynamic linker exists as a dynamic library, and the compiler will organize the generated assembly code in a PLT manner. However, at the beginning, the dynamic linker has not completed its bootstrap and cannot run functions through the PLT method.

    Main Function

    <span>main</span> function is the core entry point of the dynamic linker:

    __attribute__((visibility("hidden")))
    int main(void *stack_pointer)
    {
        int argc;
        char **argv;
        char **envp;
        uint64_t *auxv;
        
        // Parse all parameters from the stack pointer
        parse_stack(stack_pointer, &argc, &argv, &envp, &auxv);
        
        // Save environment variable pointer
        g_environ = envp;
        
        // Use printf_simple for initialization phase printing
        printf_simple("=== Dynamic Linker Starting ===\n");
        printf_simple("argc: %d\n", argc);
        
        // Parse auxiliary vector (auxv)
        parse_auxv(auxv);
        
        // Parse environment variables
        printf_simple("\nParsing environment variables...\n");
        print_all_env();
        
        // Parse important environment variables
        char *ld_library_path = get_env("LD_LIBRARY_PATH");
        if (ld_library_path) 
        {
            printf_simple("LD_LIBRARY_PATH=%s\n", ld_library_path);
        } else
        {
            printf_simple("LD_LIBRARY_PATH not set\n");
        }
        
        char *ld_preload = get_env("LD_PRELOAD");
        if (ld_preload) 
        {
            printf_simple("LD_PRELOAD=%s\n", ld_preload);
        }
        
        char *path = get_env("PATH");
        if (path) 
        {
            printf_simple("PATH=%s\n", path);
        }
        
        printf_simple("\nDynamic Linker initialization complete.\n");
        
        return 0;
    }
    

    Note that the <span>main</span> function uses <span>__attribute__((visibility("hidden")))</span>, which marks the symbol as “hidden” so that it does not enter the dynamic symbol table (<span>.dynsym</span>), thus avoiding PLT calls. Here is a comparative example:

    # If main is visible (default)
    _start:
        bl main@plt    # ❌ Uses PLT, will fail at startup
    
    # If main is hidden
    _start:
        bl main        # ✅ Direct call, works normally at startup
    

    Testing

    As usual, after completing the code, we need to test it. First, we compile an executable program and specify its dynamic linker as our own <span>libdynamic_linker.so</span>

    # Add dynamic linking version of test_mini_libc
    # Use mini_libc_entry.S as the entry point for the user program
    add_executable(test_mini_libc_dynamic ${TEST_MINI_LIBC} src/mini_libc_entry.S)
    # Set dynamic linker path, using absolute path
    set(DYNAMIC_LINKER_PATH "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/libdynamic_linker.so")
    set_target_properties(test_mini_libc_dynamic PROPERTIES
        LINK_FLAGS "-nostdlib -Wl,--entry=_mini_libc_entry -Wl,-rpath,${CMAKE_RUNTIME_OUTPUT_DIRECTORY} -Wl,--dynamic-linker,${DYNAMIC_LINKER_PATH}")
    target_link_libraries(test_mini_libc_dynamic mini_libc_shared)
    

    Here, we customize the dynamic linker using <span>DYNAMIC_LINKER_PATH</span> in the <span>CMakeLists.txt</span> and finally compile to generate <span>test_mini_libc_dynamic</span>. The running result:

    qemu-aarch64 out/bin/test_mini_libc_dynamic
    === Dynamic Linker Starting ===
    argc: 1
    === Parsing Auxiliary Vector ===
    AT_ENTRY: 0x4019bc
    AT_PHDR: 0x400040
    AT_PHENT: 56
    AT_PHNUM: 8
    AT_PAGESZ: 4096
    AT_BASE: 0x801000
    AT_PLATFORM: aarch64
    AT_EXECFN: out/bin/test_mini_libc_dynamic
    ==============================
    
    Parsing environment variables...
    === Environment Variables ===
    OLDPWD=/root
    _=/usr/bin/qemu-aarch64
    PULSE_SERVER=unix:/mnt/wslg/PulseServer
    HOSTTYPE=x86_64
    DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/0/bus
    
    XDG_DATA_DIRS=/usr/local/share:/usr/share:/var/lib/snapd/desktop
    WSLENV=
    XDG_RUNTIME_DIR=/run/user/0/
    SHLVL=1
    DISPLAY=:0
    USER=root
    LESSOPEN=| /usr/bin/lesspipe %s
    TERM=xterm-256color
    LESSCLOSE=/usr/bin/lesspipe %s %s
    WAYLAND_DISPLAY=wayland-0
    
    WSL_INTEROP=/run/WSL/592_interop
    LANG=C.UTF-8
    HOME=/root
    MOTD_SHOWN=update-motd
    LOGNAME=root
    PWD=/work/github/multi_experiments/mini_libc
    NAME=WorkMachine
    WSL_DISTRO_NAME=Ubuntu-22.04
    WSL2_GUI_APPS_ENABLED=1
    SHELL=/bin/bash
    ============================
    LD_LIBRARY_PATH not set
    PATH=
    
    Dynamic Linker initialization complete.
    

    Conclusion

    This article mainly discusses the overall framework of the C language part of the dynamic linker, which occupies more than 90% of the code in the project, detailing auxiliary vector parsing, stack parsing, environment variable parsing, and the specific implementation of the main function. Subsequent articles will continue to improve other functions, ultimately achieving a basic usable dynamic linker.

    Series of articles:

    Developing a Dynamic Linker from Scratch: Basic Principles

    Developing a Dynamic Linker from Scratch: Kernel Parameter Interaction

    Developing a Dynamic Linker from Scratch: Implementing the Linker Entry

    Leave a Comment