How Linux Dynamic Linking Works: From PIC to Lazy Binding Explained
This article explores the mechanisms of dynamic linking on Linux, covering global symbol interposition, lazy binding, position‑independent code, relocation tables, hidden symbols, and initialization order, with detailed code examples and assembly analysis to help developers understand and troubleshoot symbol conflicts and address‑independent execution.
In software development, dynamic library linking issues often cause symbol conflicts that lead to runtime errors or crashes. To understand the dynamic linking mechanism and its operation, this article revisits the concepts from "The Programmer's Self‑Care" and demonstrates them through practical examples and disassembly analysis.
Introduction
The focus is on the dynamic linking mechanisms in Linux, including Global Symbol Interposition, Lazy Binding, and Position‑Independent Code (PIC). By discussing these concepts and technical details, the article aims to provide a clear framework for recognizing the root causes of symbol conflicts and to offer practical measures for prevention and resolution.
Example C Program
The following simple C program is used to illustrate dynamic linking within a module and across modules. The -fPIC option ensures the generation of position‑independent code.
#include <stdio.h>
// static variable a is visible only within this module
static int a;
// external global variable b
extern int b;
// module‑internal global variable c
int c = 3;
// external function declaration
extern void ext();
// static function inner() is limited to this module
static void inner() {}
// bar() modifies static variable a and external global variable b
void bar() {
a = 1; // modify a
b = 2; // modify b
c = 4; // modify c
}
// foo() calls inner, bar, and ext, then prints variable values
void foo() {
inner(); // call static function
bar(); // call function bar
ext(); // call external function
printf("a = %d, b = %d, c = %d
", a, b, c);
}
int b = 1;
void ext() { b = 3; }
int main() {
foo();
return 0;
}Compilation commands:
gcc -shared -fPIC -o libpic.so pic.c -g
gcc -o main main.c -L. -lpicModule‑Internal Function Calls
Disassembly of the .plt section shows how bar and foo are linked.
Disassembly of section .plt:
0000000000000670 <bar@plt-0x10>:
670: ff 35 92 09 20 00 push QWORD PTR [rip+0x200992] # 201008 <_GLOBAL_OFFSET_TABLE_+0x8>
676: ff 25 94 09 20 00 jmp QWORD PTR [rip+0x200994] # 201010 <_GLOBAL_OFFSET_TABLE_+0x10>
67c: 0f 1f 40 00 nop DWORD PTR [rax+0x0]
0000000000000680 <bar@plt>:
680: ff 25 92 09 20 00 jmp QWORD PTR [rip+0x200992] # 201018 <_GLOBAL_OFFSET_TABLE_+0x18>
686: 68 00 00 00 00 push 0x0
68b: e9 e0 ff ff ff jmp 670 <_init+0x20>
00000000000007e8 <foo>:
...Static Function Call: inner()
The call uses a relative offset instruction ( e8 …) to jump directly to the function address, which is simple and fast because the address is known at compile time.
Global Function Call: bar()
The first call goes through the PLT entry, which jumps to an address in the .got.plt table. The dynamic linker resolves the real address of bar at runtime and patches the GOT entry, enabling lazy binding. Subsequent calls jump directly to the resolved address.
Module‑Inter‑Module Function Calls
In the example, foo() calls ext(), which resides in another shared object. The disassembly shows the same PLT‑based mechanism as for bar():
800: b8 00 00 00 00 mov eax,0x0
805: e8 a6 fe ff ff call 6b0 <ext@plt>Because both calls use the PLT/GOT mechanism, there is no difference between intra‑module and inter‑module calls after symbol resolution.
Variable Access
Static variable a resides in .bss and is accessed via a fixed offset; no relocation is needed.
External variable b is accessed through the GOT ( .got), requiring runtime relocation.
Module‑internal global variable c is also accessed via the GOT, but its address is resolved at load time, not lazily.
Global variables are typically resolved during loading, not lazily, because the overhead of delayed resolution outweighs the benefit.
Hidden Symbols
When a symbol is marked with __attribute__((visibility("hidden"))), it is omitted from the dynamic symbol table ( .dynsym) and does not participate in global symbol interposition. The following code demonstrates hidden symbols:
#include <stdio.h>
static int a;
extern int b;
__attribute__((visibility("hidden"))) int c = 3;
extern void ext();
void bar() __attribute__((visibility("hidden")));
void bar() {
a = 1;
b = 2;
c = 4;
}
static void inner() {}
void foo() {
inner();
bar();
ext();
printf("a = %d, b = %d, c = %d
", a, b, c);
}Disassembly shows that bar is called directly via a relative jump, and c resides in the .data section, confirming that hidden symbols do not require PLT/GOT relocation.
PIC‑Related Questions
How to tell if a shared object (DSO) is PIC? Run readelf -d xxx.so | grep TEXTREL. No output means the library was built with PIC.
How to tell if a static library is PIC? List its objects with ar -t xxx.a and inspect each with readelf -r xxx.o. Presence of absolute relocations such as R_X86_64_GOTPCREL indicates non‑PIC code.
Can a non‑PIC static library be linked with a PIC shared library? No. The linker will emit errors like
relocation R_X86_64_PC32 ... cannot be used when making a shared object; recompile with -fPIC.
Why isn’t PIC the default for shared libraries? Historical reasons, build‑system flexibility, and a small performance penalty for PIC code.
Initialization Order
When multiple shared objects are loaded, the order of global variable initialization follows the dependency graph: a library that other libraries depend on is initialized first. Constructors can be ordered explicitly with GCC attributes:
TestClass obj __attribute__((init_priority(102)));Functions can be given constructor or destructor priorities:
void __attribute__((constructor(102))) init_func();
void __attribute__((destructor(102))) fini_func();These attributes affect the order in which initialization and cleanup code runs before and after main().
Summary
The article has detailed the dynamic linking process in Linux, covering compilation, linking, loading, and execution stages. It explained how ELF sections such as .plt, .got, .rela.plt, and .rela.dyn cooperate to enable position‑independent code, lazy binding, and symbol interposition. It also discussed hidden symbols, PIC detection, and how to control initialization order with GCC attributes.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Alibaba Cloud Developer
Alibaba's official tech channel, featuring all of its technology innovations.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
