Loader ও Dynamic Linker — ./a.out টাইপ করলে kernel আর ld.so কী করে
Loader and Dynamic Linker
./a.out টাইপ করা আর main()-এর প্রথম লাইনের মধ্যে দুইজন অভিনেতা কাজ করে — kernel execve-তে PT_LOAD segment গুলো মেমরিতে বসায়, তারপর PT_INTERP নির্দেশিত dynamic linker লাইব্রেরি জোগাড় করে বাকি relocation সারায়।
আগে এটা বুঝি
আপনি terminal-এ টাইপ করলেন ./a.out, Enter চাপলেন। shell একটা
fork করে, সন্তান process একটা system call
ডাকল — execve(”./a.out”, …)। এই মুহূর্ত থেকে main()-এর
প্রথম লাইন চলা পর্যন্ত ঠিক কী ঘটে?
আগের দুই লেসনে আমরা জোড়ার পুরো খেলাটা দেখেছি: compiler code emit করল, linker ভাঙা address জোড়া লাগাল। কিন্তু PIE executable-এর জগতে লিংক-টাইমের কাজ ইচ্ছা করেই অসম্পূর্ণ রাখা হয় — শেষ base address কেউ জানে না, ASLR প্রতিবার নতুন random ঠিকানা বেছে নেয়। কেউ একজনকে চালু হওয়ার মুহূর্তে বাকি জোড়াগুলো লাগাতে হবে। আর যদি প্রোগ্রাম libc-এর printf ডাকে, সেই libc.so.6 ফাইলটাও তো কেউ খুঁজে, মেমরিতে বসায়, আর তার symbol-গুলোর ঠিকানা হাতে ধরিয়ে দিতে হবে।
সেই দুইজন অভিনেতা: kernel-এর loader (execve-এর ভেতরে,
process image তৈরি করে) আর dynamic linker —
loader-এর সেই বিশেষ রূপ যার নাম /lib64/ld-linux-x86-64.so.2,
ডাকনাম ld.so। একজন মঞ্চ সাজায়, আরেকজন অভিনয়ের আগে পোশাক
জোড়া লাগায়। আজ আমরা পর্দার আড়ালে ঢুকে দুজনকেই কাজ করতে দেখব —
আর শেষে নিজের হাতে তাদের সবচেয়ে বিখ্যাত মঞ্চ, GOT/PLT-র
indirect jump path, instruction-by-instruction হাঁটব।
মূল ধারণা
ধাপ এক — kernel: execve-এর ভেতরে
execve ফিরে আসে না — সফল হলে address space পুরো প্রতিস্থাপিত। Kernel-এর sequence:
১. ফাইল খুলে প্রথম ৪ byte পড়ো: 7f 45 4c 46 ("ELF magic")?
২. ELF header parse: e_type (ET_EXEC/ET_DYN), e_machine,
e_entry, e_phoff (program header কোথায়)
৩. Program header table scan — প্রতিটা entry বলে দেয়
ফাইলের কোন অংশ মেমরির কোন ঠিকানায়, কী permission-এ বসবে
৪. প্রতিটা PT_LOAD segment-এর জন্য mmap:
ফাইল-backed mapping (text: R-X, data: RW)
p_filesz-র বাইরে p_memsz বড় হলে (.bss!) anonymous zero page
৫. Stack বসাও, তাতে argv, envp, আর auxiliary vector (auxv):
AT_ENTRY, AT_PHDR, AT_BASE, AT_RANDOM (ASLR seed)...
৬. PT_INTERP থাকলে সেই ফাইলও মেমরিতে তুলে entry point
সেটার দিকে মোড়ে দাও; না থাকলে e_entry-তেই jumpProgram header-ই kernel-এর নির্দেশিকা — “section linker-এর জন্য,
segment loader-ের জন্য” (elf-format lesson) বাক্যের প্রয়োগ এখানে.
readelf -l a.out-এ kernel-ের পড়া তালিকা:
$ readelf -l a.out | head -14
Elf file type is DYN (Position-Independent Executable file)
Entry point 0x1040
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align
PHDR 0x000040 0x0000000000000040 0x0000000000000040 0x0002d8 0x0002d8 R 0x8
INTERP 0x000318 0x0000000000000318 0x0000000000000318 0x00001c 0x00001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x000000 0x0000000000000000 0x0000000000000000 0x000628 0x000628 R 0x1000
LOAD 0x001000 0x0000000000001000 0x0000000000001000 0x0002a5 0x0002a5 R E 0x1000
LOAD 0x002db0 0x0000000000003db0 0x0000000000003db0 0x000148 0x000148 RW 0x1000
DYNAMIC 0x002dc0 0x0000000000003dc0 0x0000000000003dc0 0x0001d0 0x0001d0 RW 0x8চার নাম মুখস্থ: PT_LOAD (মেমরিতে বসাও), PT_INTERP (dynamic linker ডাকো), PT_DYNAMIC (ld.so-র info-pack: DT_NEEDED, relocation, symbol table), PHDR (header-এর মেমরি-অবস্থান)।
ধাপ দুই — ld.so: ছোট্ট একটা OS-এর মতো bootstrap
Dynamic executable-এর ক্ষেত্রে kernel যে ঠিকানায় jump করে, সেটা আপনার program-এর entry না — ld.so-র entry। ld.so নিজেই একটা PIE-style shared object, এবং তার সামনে মুরগি-ডিম সমস্যা: যে কোড সবার relocation সারাবে, তার নিজের relocation-ও তো আছে। সমাধান: ld.so-র প্রথম কয়েক dozen instruction সূক্ষ্মভাবে লেখা যাতে self-relocation করে নিজের GOT ঠিক করে ফেলে। এরপরের ধাপগুলো:
১. নিজের relocation (R_X86_64_RELATIVE — সস্ততম)
২. PT_DYNAMIC → DT_NEEDED তালিকা
৩. NEEDED library খোঁজো + mmap (order নিচে)
৪. Global scope: exe → lib১ → lib২... (breadth-first)
৫. Eager relocation (GLOB_DAT, RELATIVE)
৬. JUMP_SLOT lazy রাখো — প্রথম call-এর ফাঁদ
৭. Library init/constructor চালাও
৮. আপনার _start-এ jump — main() আর দূরে নাLibrary-খোঁজার ক্রম (ব্যর্থ হলে পরের):
| ক্রম | উৎস | নিয়ন্ত্রণ |
|---|---|---|
| ১ | DT_RPATH (পুরনো) | binary-র ভেতরে |
| ২ | LD_LIBRARY_PATH | ব্যবহারকারীর env |
| ৩ | DT_RUNPATH (আধুনিক) | binary-র ভেতরে |
| ৪ | /etc/ld.so.cache | ldconfig-এর বানানো index |
| ৫ | /lib, /usr/lib ইত্যাদি | ডিফল্ট |
Symbol resolution order-টাই global scope: একটা নাম খুঁজতে
ld.so scope-এর প্রথম member থেকে শেষ পর্যন্ত linearly চলে। তাই
executable-এ define করা printf হলে libc-র printf ছায়া হয়ে
যায় — এই interposition-ই LD_PRELOAD-এর ভিত্তি।
Lazy binding — প্রথম call-এর ফাঁদ
PIC লেসনে GOT/PLT-র কাঠামো দেখেছি; এবার সেই গল্পের loader-side:
ld.so ইচ্ছা করে প্রতিটা external function-এর GOT slot-কে resolver-এ
ফেরত পাঠানোর trampoline-এ point করিয়ে রাখে। প্রথম call puts@plt
ঘটলে ঘটনাক্রমে:
call puts@plt
→ PLT stub: jmp *GOT[puts] # GOT এখনো trampoline-এ point করছে
→ push relocations-index # "আমি কাকে খুঁজছি" — শনাক্তকারী
→ jmp _dl_runtime_resolve
→ symbol lookup: global scope ধরে 'puts' খোঁজা
→ GOT[puts] ← আসল puts-এর ঠিকানা (patch — একবারই)
→ puts() চলে
দ্বিতীয় call: jmp *GOT[puts] → সরাসরি puts — resolver ভূতুড়ে মৃতLD_LIBRARY_PATH ও LD_PRELOAD — ক্ষমতা আর বিপদ
দুটো env var-ই ld.so-কে override করতে দেয়:
- LD_LIBRARY_PATH=/home/me/libs:$LD_LIBRARY_PATH — নিজের
build করা
.soদিয়ে system version-কে ছায়া ফেলানো। Developer-দের দৈনন্দিন হাতিয়ার, এবং production-এ “আমার machine-এ চলত” বাগের প্রধান উৎস। - LD_PRELOAD=./myshim.so — পুরো scope-এর একদম শুরুতে একটা library ঢোকান, যার symbol সবার আগে জেতে। malloc counter, open() logging, mock testing — সব সম্ভব। আবার password sniffer-ও সম্ভব।
এই ক্ষমতার দাম নিরাপত্তা: setuid-root program চালুর সময় dynamic loader এই দুই variable নীরবে বাদ দেয় — নইলে যে কেউ root-এর malloc হাইজ্যাক করে বসে। এটা মনে রাখা জরুরি: env-var-নিয়ন্ত্রিত loader privilege boundary-র উপরে বসে না।
ভেতরে কী ঘটছে
পুরো জীবনী — shell prompt থেকে main পর্যন্ত
./hello একটা dynamic C program। মুহূর্ত-by-মুহূর্ত টাইমলাইন:
T+0.0ms shell: fork() → সন্তান process-এ execve("./hello")
T+0.1ms kernel: ELF magic যাচাই, program header পড়া
T+0.2ms kernel: ৪-৫টা PT_LOAD mmap (text R-X, rodata R, data RW)
.bss-এর জন্য anonymous zero pages; stack setup;
auxv-তে AT_PHDR/AT_ENTRY/AT_BASE/AT_RANDOM বসানো
T+0.3ms kernel: PT_INTERP পড়ে /lib64/ld-linux-x86-64.so.2 ম্যাপ করে
PC সেট করল ld.so-র entry-তে — আপনার main তখনো স্বপ্নে
T+0.5ms ld.so: নিজের relocation সারায়, PT_DYNAMIC খোলে
T+0.7ms ld.so: DT_NEEDED = [libc.so.6] → ld.so.cache ঘেঁটে পাওয়া,
libc mmap (~২ MB ম্যাপিং, বেশিরভাগ পাতাই আগে থেকে page cache-এ)
T+1.0ms ld.so: global scope সাজানো, eager relocation, lazy ফাঁদ
T+1.2ms ld.so: libc-র initializer চালানো
T+1.3ms আপনার _start → __libc_start_main → main()মোট ~১ ms-এর জাদুশো — প্রতিটা process চালুর সময় প্রতিবার। Static binary-তে T+0.3ms-এর পরেই সরাসরি _start: kernel-ই একমাত্র loader।
- shell: fork + execvesystem call boundary — user থেকে kernel
- kernel loader: mmap segments, auxv, stackELF-কে মেমরির ভূগোলে রূপান্তর
- ld.so: self-reloc, DT_NEEDED, scopeuser-space-এ দ্বিতীয় ধাপের linking
- libc init + constructorsenvironment প্রস্তুত
- _start → main()অবশেষে আপনার কোড
ASLR — কেন এই পুরো নাচটাই দরকার
Kernel প্রতি ম্যাপিংয়ে random offset ছোড়ে — exe base, libs, heap,
stack আলাদাভাবে; /proc/self/maps প্রতিবার ভিন্ন। Randomization কাজ
করে শুধু PIC-এর কারণে: RIP-relative + GOT indirection লোড-ঠিকানা
নির্বিশেষে সত্য। Absolute pointer-ঘরগুলোর জন্য ld.so load-time-এ
R_X86_64_RELATIVE চালায় (“base+addend”) — lookup ছাড়া, তাই সস্তা。
Startup-এর দাম, আর prelink-এর মৃত্যু
Launch-প্রতি relocation-খরচ ছোট না — হাজার-entry app-এ ms-scale। ২০০০-দশকের প্রতিকার prelink: library-গুলো fixed ঠিকানায় বসিয়ে relocation আগেই সেরে ফাইলে লেখা। ASLR উল্টে দিল — fixed ঠিকানা = randomization শূন্য। Prelink মৃত; উত্তরাধিকারী দ্রুততর ld.so আর static linking।
উদাহরণ
একই প্রোগ্রাম, দুই জীবন — static বনাম dynamic
// hello.c
#include <stdio.h>
int main(void) {
printf("hello\n");
return 0;
}gcc hello.c -o hello_dyn
gcc hello.c -static -o hello_static
file hello_static hello_dyn
# hello_static: ELF 64-bit LSB executable, x86-64, statically linked
# hello_dyn: ELF 64-bit LSB pie executable, ..., dynamically linked
readelf -l hello_dyn | grep -A1 INTERP
# INTERP ... [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
readelf -l hello_static | grep INTERP || echo "no INTERP — kernel সরাসরি _start-এ যাবে"পার্থক্য টেবিলে:
| hello_dyn | hello_static | |
|---|---|---|
| ফাইল সাইজ | ~১৬ KB | ~৮০০ KB |
| চালুর সময় ম্যাপিং | exe + ld.so + libc | শুধু exe |
| Relocation কে সারায় | ld.so (প্রতি launch-এ) | linker-ই (একবার) |
| libc bug fix | libc upgrade = সবাই ঠিক | সবাইকে rebuild |
| ASLR | পূর্ণ (PIE) | non-PIE হলে exe-র base fixed |
কোনটা ভালো — dependency freshness বনাম cold-start; universal উত্তর নেই, workload-ভিত্তিক সিদ্ধান্ত।
ছবিতে
চালু হওয়ার পরে প্রসেসের মেমরি-ভূগোল
উচ্চ ঠিকানা
┌──────────────────────────────┐ 0x7ffd...
│ stack (নিচের দিকে বাড়ে) │ argv · envp · auxv · __libc_start_main frame
├──────────────────────────────┤
│ ld.so-র ম্যাপিং │ ◄── kernel PT_INTERP দিয়ে বসিয়েছে
├──────────────────────────────┤
│ libc.so.6-এর ম্যাপিং │ ◄── ld.so DT_NEEDED দিয়ে বসিয়েছে
│ r--p rodata │ (page cache share — সব প্রসেসে এক)
│ r-xp .text │
│ rw-p .data/.got │
├──────────────────────────────┤
│ heap (brk/mmap, উপরে বাড়ে) │
├──────────────────────────────┤
│ hello-র নিজের image (PIE) │ ◄── kernel PT_LOAD গুলো
│ r--p headers + rodata │
│ r-xp .text │
│ rw-p .data + .bss │
└──────────────────────────────┘ নিম্ন ঠিকানালক্ষণ: ঠিকানা-জগৎ তিনজনের স্বাক্ষর — kernel, ld.so, link-time layout; libc-র r-xp পাতা এক physical কপি শত প্রসেসে share (dynamic linking- এর অর্থনীতি); stack-এর উপরে auxv, AT_RANDOM-ই ASLR seed।
নিজে চালিয়ে দেখুন
Loader-কে ধরা দিন — LD_DEBUG, strace, perf
১. ld.so-র নিজের বয়ান শোনা:
LD_DEBUG=libs ./hello_dyn 2>&1 | head -12 1234: find library=libc.so.6 [0]; searching
1234: search cache=/etc/ld.so.cache
1234: trying file=/lib/x86_64-linux-gnu/libc.so.6
1234:
1234:
1234: calling init: /lib/x86_64-linux-gnu/libc.so.6
1234:
1234:
1234: initialize program: ./hello_dyn
1234:
1234:
1234: transferring control: ./hello_dyntransferring control — ld.so-র নিজের স্বীকারোক্তি: সে ছিল
মধ্যস্থতাকারী। LD_DEBUG=bindings-এ প্রতিটা lookup দেখা যায়।
২. syscall স্তরে একই ঘটনা:
strace -e trace=openat,mmap,execve ./hello_dyn 2>&1 | grep -E "libc|ld-linux|execve" | head -6execve("./hello_dyn", ...) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
mmap(NULL, 195700, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f...Concept-টাইমলাইনের প্রতিটা লাইন, syscall-স্তরে নিজের চোখে।
৩. Interpreter কে সে?
patchelf --print-interpreter ./hello_dyn # apt/brew install patchelf
# /lib64/ld-linux-x86-64.so.2৪. Startup-এর দাম মাপা:
perf stat ./hello_dyn > /dev/null 2>stat_dyn.txt
perf stat ./hello_static > /dev/null 2>stat_st.txt
grep "elapsed\|task-clock" stat_dyn.txt stat_st.txtপ্যাটার্ন: static কয়েকশ μs, dynamic ২-৫ গুণ — সেটাই ld.so bootstrap। এক process-এ negligible; হাজারবার spawn-এ যোগ হয়।
./a.out আর main()-এর মাঝের পুরো ld.so-নাচটা বাইরে থেকে দেখা, ট্রেস করা আর সময় মাপা যায় — loader কোনো বিমূর্ত ধারণা না, প্রতিটা launch-এ চলা মাপযোগ্য কোড।
নিজে বানান
GOT/PLT walkthrough lab — তিন-ধাপ indirect jump নিজের চোখে
- একটা printf/puts call করা ছোট program compile করুন দুইভাবে — ডিফল্ট (-fplt) আর -fno-plt
- objdump -d দিয়ে দুই version-এর call site পাশাপাশি বের করুন
- readelf -r দিয়ে JUMP_SLOT আর GLOB_DAT relocation-এর পার্থক্য দেখুন
- প্রথম call-এর resolver path-টা হাতে annotate করুন
- LD_BIND_NOW=1 দিয়ে eager binding-এ কী বদলায় যাচাই করুন
Script-টা সব বানিয়ে পাশাপাশি dump করে দেয়:
#!/usr/bin/env bash
# plt_lab.sh — GOT/PLT mechanics lab (Linux)
set -e
cat > probe.c <<EOF
#include <stdio.h>
int main(void) {
printf("one\n");
printf("two\n");
return 0;
}
EOF
echo "=== ১. ডিফল্ট (PLT সহ) ==="
gcc probe.c -o probe_plt
objdump -d probe_plt | sed -n '/<puts@plt>/,/^$/p;/<main>:/,+8p'
echo "=== ২. -fno-plt (সরাসরি GOT call) ==="
gcc probe.c -fno-plt -o probe_noplt
objdump -d probe_noplt | sed -n '/<main>:/,+8p'
echo "=== ৩. Relocation পার্থক্য ==="
readelf -r probe_plt | grep -E "JUMP_SLOT|GLOB_DAT"
readelf -r probe_noplt | grep -E "JUMP_SLOT|GLOB_DAT" || echo "(JUMP_SLOT নেই!)"
echo "=== ৪. GOT-এর প্রকৃত বিষয়বস্তু (launch-এর আগে vs পরে) ==="
readelf -x .got.plt probe_plt | tail -4
LD_BIND_NOW=1 ./probe_plt >/dev/null
readelf -x .got.plt probe_plt | tail -4চালানোর পরে ডিফল্ট version-এর disassembly আনুমানিক এই — প্রতিটা লাইনের পাশে ব্যাখ্যা:
probe_plt: file format elf64-x86-64
Disassembly of section .plt:
0000000000001020 <.plt>: ; ফাঁদ-স্থাপন কেন্দ্র
1020: ff 35 0a 30 00 00 push 0x3030(%rip) # GOT[1] = link_map ঠেলে দিল
1026: ff 25 0b 30 00 00 jmp *0x3038(%rip) # GOT[2] = _dl_runtime_resolve-এ jump!
↑ এই ঠিকানাটাই ld.so নিজের
relocation-এ বসিয়েছিল
Disassembly of section .plt.sec:
0000000000001040 <puts@plt>: ; প্রতি external ফাংশনে এক stub
1040: f3 0f 1e fa endbr64
1044: f2 ff 25 f5 2e 00 00 bnd jmp *0x2ef5(%rip) # ধাপ ২: GOT[puts]-এর মানে jump
# প্রথমবার: মানটা = 0x1030 (নিচের pusher!)
104b: 0f 1f 44 00 00 nop
0000000000001159 <main>:
...
1169: e8 d2 fe ff ff call 1040 <puts@plt> # ধাপ ১: সাধারণ direct-looking call
# rel32 = 0xfffffed2 = −302 → 0x1040
116e: 48 8d 05 9b 0e 00 00 lea 0xe9b(%rip),%rax # "two\n" literal
1175: 48 89 c7 mov %rax,%rdi
1178: e8 c3 fe ff ff call 1040 <puts@plt> # দ্বিতীয় call — একই stub
...
0000000000001030: ; (disassembly-তে নাম নেই)
1030: push $0x0 # ধাপ ৩a: "আমি relocation #0" শনাক্তকারী
1035: jmp 1020 # ধাপ ৩b: .plt[0]-তে → resolverআর -fno-plt version-এ main-এর ভেতরটা হয়ে যায়:
1178: ff 15 82 2e 00 00 call *0x2e82(%rip) # এক ধাপ: সরাসরি GOT-এর মান দিয়ে call
# (stub, pusher, resolver-trampoline সব বাদ)তিন-ধাপ path-টা এবার এক নিঃশ্বাসে: (১) call puts@plt — একটা
সাধারণ rel32 call, শুধু গন্তব্য local stub; (২) stub-এর
jmp *GOT[n] — indirect, কিন্তু GOT entry-র প্রাথমিক মানটা আসল
puts না, পরের লাইনের pusher; (৩) pusher নিজের relocation
নম্বর push করে resolver-এ পৌঁছায় — resolver lookup করে, GOT[n]
patch করে, argument stack ঠিক করে আসল puts-এ পৌঁছে দেয়। দ্বিতীয়
call থেকে ধাপ ৩ আর কখনো ঘটে না — ধাপ ২-ই সরাসরি puts।
নিজে বাড়ান:
LD_BIND_NOW=1 ./probe_pltচালিয়ে আবারstraceকরুন — প্রথম call-এর behavior কী বদলাল?- Script-এ
readelf -d probe_plt | grep BIND_NOWযোগ করে-Wl,-z,nowbuild-এর DT_FLAGS দেখুন perf statদিয়ে plt বনাম no-plt version-এর মোট runtime তুলনা করুন — পার্থক্য measurable কি, নাকি noise?
বাস্তব সিস্টেমে
Loader-এর গল্প যেখানে বাস্তবে ওঠে
Container cold-start। Docker/Kubernetes-এ pod চালুর সময় প্রতিটা process-এর ld.so bootstrap যোগ হয়ে যায়; Go/Rust-এর statically-linked microservice এজন্যই serverless-এ প্রিয় — kernel-ই একমাত্র loader, launch latency কমে যায়।
Hardening-ত্রয়ী। PIE + Full RELRO + immediate binding — distro ডিফল্ট, এই লেসনের মেকানিজমগুলোরই ফল। Attack side-ও এখানে: GOT overwrite classic exploitation-পাঠ; LD_PRELOAD rootkit-গল্পও।
ldd সাবধানে। ldd টার্গেটকে tracing-সহ চালায় — untrusted binary- তে execution মানেই বিপদ; নিরাপদ: readelf -d / objdump -p.
Android/iOS/console। Bonic linker64, dyld, console static-only SDK — নাম-format ভিন্ন (ELF/Mach-O), ধারা এক: interpreter bootstrap, dependency graph, relocation, lazy/eager trade-off। Console static কেন — predictable memory map, exploit surface ছোট。
যে ভুলগুলো সবাই করে
“Loader মানে linker — দুইটা একই সফটওয়্যারের দুই নাম।”
দুই অভিনেতা, দুই সময়: linker (ld) build-time-এ ফাইল→ফাইল; ld.so run-time-এ প্রসেসের ভেতরে। কাজের মিল (resolution, relocation) কারণ একই সমস্যার দুই সময়ের সমাধান — codebase আর constraint আলাদা।
“'ldd' untrusted binary-তে চালানো safe, এটা তো শুধু inspection টুল।”
ldd-র implementation টার্গেটকে execute করে — constructor এমনকি main-ও
চলতে পারে; man page নিজেই সতর্ক করে। নিরাপদ: readelf -d | grep NEEDED
— pure parsing।
“Static binary-তে loader জড়িতই না।”
Kernel-এর loader তো সবার জন্যই কাজ করে — ELF parse, mmap, stack,
auxv সব static binary-তেও ঘটে; শুধু PT_INTERP ধাপটা skip হয়। আর
static-PIE হলে (আধুনিক -static-pie) নিজের relocation-ও load-time-এই
সারাতে হয় — kernel বা self-relocating startup-এর হাতে। “Static =
loader-free” ভুল; সঠিক বাক্য: “static = dynamic-loader-free।”
“Dynamic linking সবসময় মেমরি বাঁচায়।”
Code page sharing-এর অর্থনীতি সত্য — কিন্তু শর্তসাপেক্ষে। একই library-র ভিন্ন version ঘুরলে sharing শূন্য; .data পাতা প্রসেসভেদে private copy (CoW); relocation-এ লেখা গেলে পাতা dirty হয়ে private হয়ে যায়; আর per-process GOT/PLT overhead সবার জন্যই আলাদা। শত-প্রসেস, এক-version সিস্টেমে dynamic জেতে; বিচ্ছিন্ন, ভিন্ন-version workload-এ static অনেক সময় ছোট footprint দেয়।
বুঝেছেন কি না দেখুন
1Program header-এর PT_LOAD, PT_INTERP, PT_DYNAMIC — তিনটার কাজ এক
বাক্যে করে বলুন। কোনটা না থাকলে প্রোগ্রাম চলবে কি?
স্মরণ
PT_LOAD: মেমরি-ম্যাপিং ছাড়া কোড-ডেটাই নেই। PT_INTERP: dynamic linker- এর নাম — না থাকলে static হতে হবে। PT_DYNAMIC: ld.so-র info-pack, dynamic binary-তে এটা ছাড়া ld.so অসহায়। Static binary-তে দুটোই অনুপস্থিত — আর সে চলে।
2LD_LIBRARY_PATH=/home/me/lib ./app চালালে app-এর চেয়ে ছোট একটা
/home/me/lib/libc.so.6 (হ্যাঁ, নামটাই) থাকলে কী হবে? কেন setuid
program-এ একই পরিস্থিতি নিরাপদ?
প্রয়োগ
LD_LIBRARY_PATH=/home/me/lib ./app চালালে app-এর চেয়ে ছোট একটা
/home/me/lib/libc.so.6 (হ্যাঁ, নামটাই) থাকলে কী হবে? কেন setuid
program-এ একই পরিস্থিতি নিরাপদ?Search order-এ LD_LIBRARY_PATH ld.so.cache-এর আগে — তাই আপনার libcurl-এর মতোই এই libc.so.6-ও আসল libc-কে ছায়া ফেলবে; app আপনার ফাইলটাকেই libc ভেবে চলবে (version mismatch হলে crash বা আরো খারাপ)। এটাই hijacking। Setuid-এর ক্ষেত্রে ld.so elevated privilege দেখে LD_LIBRARY_PATH/LD_PRELOAD উপেক্ষা করে — attacker-এর ফাইল search path-এই ঢোকার সুযোগই নেই, তাই নিরাপদ। Loader নিজে privilege-boundary enforce করে — এটাই মূল শিক্ষা।
3প্রথম call puts@plt আর দ্বিতীয়টার মধ্যে execution path-এর
পার্থক্য কোন লাইনে? সেই পার্থক্য সম্ভব হয় কোন মেমরি-লেখার জন্য?
যুক্তি
call puts@plt আর দ্বিতীয়টার মধ্যে execution path-এর
পার্থক্য কোন লাইনে? সেই পার্থক্য সম্ভব হয় কোন মেমরি-লেখার জন্য?দুই call-ই jmp *GOT[n] পার হয়; পার্থক্য GOT[n]-এর মানে — প্রথমবার
pusher/resolver, পরে সরাসরি puts। Resolver-ই মান patch করে; GOT
writable segment-এ বলেই সম্ভব (Full RELRO সেই writability বন্ধ করে)।
4ASLR চালু থাকলেও কোনো এক program-এর সব library প্রতিবার একই ঠিকানায়
লোড হচ্ছে — কী সম্ভাব্য কারণ?
যুক্তি
সবচেয়ে সাধারণ: non-PIC/non-PIE — absolute relocation fixed address
চায়। অথবা ASLR disabled (setarch -R, randomize_va_space=0), বা
prelink-হওয়া সিস্টেম। যাচাই: file prog-এ “pie”, /proc/self/maps
দুইবার তুলনা।
5আপনার দায়িত্বে একটা latency-critical microservice — প্রতি সেকেন্ডে
হাজারবার নতুন short-lived process spawn হয়। Dynamic না static
linking — সিদ্ধান্তের পক্ষে তিনটা যুক্তি দিন, একটা প্রতি-যুক্তিসহ।
ডিজাইন
Static-এর পক্ষে: (১) প্রতি launch-এ ld.so bootstrap + relocation খরচ বাদ — ছোট process-এর জীবনে এটা শতকরা বড় ভগ্নাংশ; (২) কোনো library search/GOT/PLT indirection নেই — steady-state call path ছোট ও predictable; (৩) deployment unit self-contained — host-এর library zoo নিয়ে মাথা নেই। প্রতি-যুক্তি: libc CVE এলে dynamic world-এ একটা package update-ই সব service-কে ঢেকে দেয়, static হলে প্রতিটা image rebuild+redeploy — নিরাপত্তা-প্রস্তুতির দাম। বাস্তব সমঝোতা: static + musl/distroless, বা dynamic + minimal base image, workload-এর spawn-rate আর CVE-response SLA দিয়ে ঠিক করা।
এরপর কী
এরপর কী
Module-এর শেষ লেসনে পা রাখছেন — আর নিয়ম ভাঙব: যদি কোড চলতে চলতে তৈরি হয়? Browser JS-এ interpreter চলতে চলতে একসময় native চলে; JVM hot method C1→C2 tier-এ ওঠে। JIT-এ compiler runtime-এর অঙ্গ — bytecode, profile, speculation, emission, mprotect-নach। Codegen, relocation, GOT/PLT-র সব জ্ঞান এক জায়গায় মিলবে — compile/run-এর দেয়াল ভাঙবে।
আরও পড়ুন
- Linkers and Loaders — John R. Levine · Chapter 8-9 — loading আর shared library-র ধারণাগত ভিত্তি, kernel loader বনাম program loader-এর পার্থক্যসহ
- How To Write Shared Libraries — Ulrich Drepper · ld.so-র ভেতরের সিদ্ধান্ত — startup cost, relocation model, symbol lookup scope — লেখক নিজে glibc-র maintainer
- ld.so(8) ও elf(5) Linux manual pages · LD_* environment variable, search order আর auxiliary vector-এর সরকারি তথ্যসূত্র — man 8 ld.so