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:
<span>argc</span>: Read from<span>[sp]</span><span>argv</span>: Start from<span>[sp+8]</span>, a total of<span>argc+1</span>elements (including NULL)<span>envp</span>: Start after the end of the<span>argv</span>, need to traverse to find NULL<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:
-
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