ELF Format — একটা বাইনারি ফাইলের ভেতরের নকশা
ELF Format
ELF একটাই container format — object file, executable, shared library সবই — magic bytes দিয়ে শুরু, headers দিয়ে linker/loader-কে গাইড করে; section গুলো build-time granularity, segment গুলো runtime mapping, আর অনেক section মিলে একটা segment হয়।
আগে এটা বুঝি
Assembler-linker-loader লেসনে আমরা পুরো pipeline দেখেছিলাম:
source.c → [compiler] → source.s → [assembler] → source.o
→ [linker] → a.out → [loader] → running processএকটা প্রশ্ন তখন এড়িয়ে গিয়েছিলাম — .o আর a.out ফাইলগুলো
আসলে কী? এগুলো কি টেক্সট? বাইনারি নিশ্চয়ই, কিন্তু কোন গঠনে?
Assembler .o-তে কী লেখে যাতে linker সেটা বুঝতে পারে? Linker
a.out-এ কী লেখে যাতে loader জানে কোথায় কোড, কোথায় ডেটা, কোথায়
শুরু করতে হবে?
$ file probe.o
probe.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
$ file libgreet.so
libgreet.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, ...
$ file a.out
a.out: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, ...তিনটাই — object file, shared library, executable — ELF। একই শব্দ, তিনবার। এটা কাকতাল না — Linux-এ (আর বেশিরভাগ Unix-এ) এই তিনটা জিনিসই আক্ষরিক অর্থে একই container format ব্যবহার করে। পার্থক্য শুধু ভেতরের একটা ফিল্ডের মানে, আর কোন অংশগুলো উপস্থিত তাতে।
গত লেসনে আমরা readelf -d, readelf -r, objdump -d দিয়ে GOT আর
PLT দেখেছি — সেই সব তথ্যই এই একই ফাইল-ফরম্যাটের ভেতরে সংরক্ষিত।
এই লেসনে আমরা পুরো ফরম্যাটটা খুলে দেখব — ELF: Executable and
Linkable Format।
মূল ধারণা
Magic bytes — প্রথম চার byte
প্রতিটা ELF ফাইল ঠিক এই চার byte দিয়ে শুরু হয়:
7f 45 4c 460x7f তারপর ASCII-তে E, L, F — "\x7fELF"। এটাই OS-কে (আর
file কমান্ডকে) বলে দেয় “এই ফাইলটা ELF, তোমার parser দিয়ে পড়ো”।
execve() সিস্টেম কল প্রথমেই এই চার byte চেক করে — না মিললে
ENOEXEC error।
$ xxd -l 16 /bin/ls
00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF............Magic-এর পরের byte গুলোও তথ্যবহুল — 02 মানে 64-bit (01 হলে
32-bit), 01 মানে little-endian (02 হলে big-endian)।
ELF header — পুরো ফাইলের সূচিপত্র
Magic bytes-এর পরই আসে ELF header — একটা fixed-size struct যা বাকি পুরো ফাইলটা কীভাবে পড়তে হবে তার নির্দেশিকা।
$ readelf -h /bin/ls
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Position-Independent Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x6b60
Start of program headers: 64 (bytes into file)
Start of section headers: 142208 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 13
Size of section headers: 64 (bytes)
Number of section headers: 31
Section header string table index: 30প্রতিটা লাইন গুরুত্বপূর্ণ:
| ফিল্ড | অর্থ |
|---|---|
Type | ET_REL / ET_EXEC / ET_DYN / ET_CORE — এই ফাইলটা কী (নিচে) |
Machine | কোন CPU architecture-এর জন্য (x86-64, AArch64, …) — ভুল আর্কিটেকচারে চালানোর চেষ্টা এখানেই ধরা পড়ে |
Entry point address | কোথা থেকে execution শুরু হবে (loader এই ঠিকানায় jump করে) |
Start of program headers | segment তালিকা ফাইলের কোথায় শুরু (loader পড়ে) |
Start of section headers | section তালিকা ফাইলের কোথায় শুরু (linker/tool পড়ে) |
Number of program/section headers | কতগুলো segment/section আছে |
e_type — একই format, তিন ভূমিকা
Type ফিল্ডটাই সেই এক জায়গা যেখানে “object file, executable, নাকি
shared library” সিদ্ধান্ত হয়:
| e_type | সংক্ষেপে | মানে |
|---|---|---|
ET_REL | Relocatable | .o ফাইল — লিংক করা হয়নি, ঠিকানা এখনো ঠিক হয়নি |
ET_EXEC | Executable | পুরনো-স্টাইল non-PIE executable, fixed load address |
ET_DYN | Shared object | shared library অথবা PIE executable (গত লেসনে PT_INTERP দিয়ে আলাদা করেছিলাম) |
ET_CORE | Core dump | ক্র্যাশ হওয়া প্রসেসের memory snapshot (নিচে realworld-এ) |
$ readelf -h probe.o | grep Type
Type: REL (Relocatable file)
$ readelf -h libgreet.so | grep Type
Type: DYN (Shared object file)
$ readelf -h ./a.out | grep Type
Type: DYN (Position-Independent Executable file)ভেতরে কী ঘটছে
Section বনাম Segment — সবচেয়ে সাধারণ বিভ্রান্তি
ELF-এ দুইটা সমান্তরাল “মানচিত্র” আছে একই ফাইলের content বর্ণনা করার জন্য — আর তারা ভিন্ন পাঠকের জন্য, ভিন্ন উদ্দেশ্যে বানানো।
Section — linker আর analysis টুলের জন্য। সূক্ষ্ম-দানার,
নাম-করা অংশ: .text, .data, .symtab, .rela.plt, ইত্যাদি।
Build-time-এ linker section-গুলো নিয়ে কাজ করে — একাধিক .o-র
.text section একসাথে জোড়া লাগিয়ে চূড়ান্ত .text বানায়, symbol
resolve করে, relocation প্রয়োগ করে। readelf -S, objdump -h,
nm — এই সব টুল section-ভিত্তিক তথ্য দেখায়।
Segment (program header-এ বর্ণিত) — loader/kernel-এর জন্য।
মোটা-দানার, নামহীন (শুধু type থাকে) memory region: “এই byte range-টা
পড়ো-চালাও (R-X) হিসেবে map করো”, “ঐটা পড়ো-লেখো (RW) হিসেবে”। Runtime-এ
execve() শুধু segment তালিকা পড়ে — section header পুরোপুরি
উপেক্ষা করে (আসলে একটা stripped executable-এ section header
থাকতেই পারে না, তবু চলে — নিচে দেখুন)।
Sections (readelf -S দেখায়):
┌────────┬────────┬────────┬────────┬─────────┬─────────┐
│ .text │.rodata │ .data │ .bss │ .symtab │ .strtab │
└────────┴────────┴────────┴────────┴─────────┴─────────┘
Segments (readelf -l দেখায়) — একই bytes-এর উপর superimposed:
┌─────────────────────────┬───────────────────┬──────────────────────┐
│ LOAD #1 (R-E) │ LOAD #2 (RW) │ (mapped না — build │
│ .text + .rodata │ .data + .bss │ metadata মাত্র) │
└─────────────────────────┴───────────────────┴──────────────────────┘
.symtab, .strtabসম্পর্কটা many-to-one — অনেক section একটা segment-এ গোষ্ঠীবদ্ধ
হয়। কেন গোষ্ঠীবদ্ধ করা দরকার: memory protection hardware page
(সাধারণত 4 KiB) granularity-তে কাজ করে, section granularity-তে না।
.text (কোড, execute-able হওয়া উচিত) আর .rodata (string literal,
read-only হওয়া উচিত কিন্তু execute-able না হওয়া উচিত) — দুটোরই
permission “read + maybe-execute, write না” — তাই একসাথে একটা
page-aligned RX (বা R) segment-এ গোষ্ঠীবদ্ধ করা efficient। প্রতিটা
আলাদা section-এর জন্য আলাদা page mapping করলে শত শত mapping লাগত,
memory আর TLB-এর অপচয়।
readelf -S — Section table
$ readelf -S probe.o
There are 13 section headers, starting at offset 0x2a8:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000018 0000000000000000 AX 0 0 1
[ 2] .data PROGBITS 0000000000000000 00000058
0000000000000004 0000000000000000 WA 0 0 4
[ 3] .bss NOBITS 0000000000000000 0000005c
0000000000000004 0000000000000000 WA 0 0 4
[ 4] .rodata PROGBITS 0000000000000000 0000005c
0000000000000004 0000000000000000 A 0 0 1
[ 5] .rela.text RELA 0000000000000000 000001a0
0000000000000018 0000000000000018 I 9 1 8
[ 6] .symtab SYMTAB 0000000000000000 00000110
00000000000000a8 0000000000000018 10 5 8
[ 7] .strtab STRTAB 0000000000000000 000001b8
0000000000000013 0000000000000000 0 0 1
[ 8] .shstrtab STRTAB 0000000000000000 000002dc
0000000000000053 0000000000000000 0 0 1লক্ষ্য করুন: .bss-এর Type NOBITS, বাকি সব PROGBITS।
এইটাই এই লেসনের একটা গুরুত্বপূর্ণ পয়েন্ট।
readelf -l — Program header table (Segments)
$ readelf -l ./a.out
Elf file type is DYN (Position-Independent Executable file)
Entry point 0x1139
There are 13 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000002d8 0x00000000000002d8 R 0x8
INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000640 0x0000000000000640 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
0x0000000000000185 0x0000000000000185 R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000
0x00000000000000f8 0x00000000000000f8 R 0x1000
LOAD 0x0000000000002e10 0x0000000000003e10 0x0000000000003e10
0x0000000000000228 0x0000000000000230 RW 0x1000
DYNAMIC 0x0000000000002e20 0x0000000000003e20 0x0000000000003e20
0x00000000000001d0 0x00000000000001d0 RW 0x8
GNU_RELRO 0x0000000000002e10 0x0000000000003e10 0x0000000000003e10
0x00000000000001f0 0x00000000000001f0 R 0x1
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.gnu.property .note.gnu.build-id .gnu.hash .dynsym .dynstr
03 .init .plt .plt.sec .text .fini
04 .rodata .eh_frame_hdr .eh_frame
05 .init_array .fini_array .dynamic .got .data .bss
06 .dynamic
07 .data.rel.roশেষের “Section to Segment mapping” টেবিলটাই এই লেসনের কেন্দ্রীয়
প্রমাণ — চোখে দেখা many-to-one সম্পর্ক। Segment 03-এ পাঁচটা section
(.init, .plt, .plt.sec, .text, .fini) — সবগুলোই কোড,
সবারই একই permission দরকার (R-E, read + execute, write না)। Segment
05-এ ছয়টা section — সবাই লেখার-যোগ্য ডেটা (RW), যার মধ্যে
.bss-ও আছে যদিও তার FileSiz অন্যদের চেয়ে ছোট।
লক্ষ্য করুন শেষ LOAD segment-এর FileSiz (0x228) MemSiz-এর
(0x230) চেয়ে ছোট — এই 0x8-byte পার্থক্যটাই .bss-এর অবদান:
ফাইলে যতটুকু আছে তার চেয়ে মেমরিতে একটু বেশি বরাদ্দ হবে, বাড়তি
অংশটা zero-filled।
উদাহরণ
সম্পূর্ণ উদাহরণ — একটা ছোট প্রোগ্রামের প্রতিটা স্তর
// demo.c
#include <stdio.h>
int initialized_global = 42; // .data
int uninitialized_global; // .bss
const char *msg = "hello elf"; // pointer .data, string .rodata
int add(int a, int b) { // .text
return a + b;
}
int main(void) {
printf("%s: %d\n", msg, add(initialized_global, uninitialized_global));
return 0;
}$ gcc -O2 -o demo demo.c
$ readelf -S demo | grep -E '\.text|\.data|\.bss|\.rodata|\.symtab|\.dynsym'
[15] .rodata PROGBITS 0000000000002000 00002000
[17] .text PROGBITS 0000000000001040 00001040
[24] .data PROGBITS 0000000000004010 00003010
[25] .bss NOBITS 0000000000004020 00003020
[ 6] .dynsym DYNSYM 0000000000000368 00000368.symtab এখানে নেই কারণ gcc -O2 -o demo demo.c-এর ডিফল্ট আউটপুট
strip করা না, কিন্তু dynamically-linked executable-এ .symtab
মাঝে মাঝে থাকে মাঝে মাঝে না — .dynsym (dynamic symbol table,
GOT/PLT resolve করতে dynamic linker-এর যা লাগে) সবসময় থাকে যদি
dynamically linked হয়।
$ objdump -d demo --disassemble=add
0000000000001149 <add>:
1149: 01 f7 add edi,esi
114b: 89 f8 mov eax,edi
114d: c3 retreadelf -x .data demo দিয়ে raw bytes দেখা:
$ readelf -x .data demo
Hex dump of section '.data':
0x00004010 2a000000 00000000 *.......0x2a = 42 (decimal) — ঠিক initialized_global-এর মান, লিটল-এন্ডিয়ানে।
পরের ৪ byte অন্য কোনো initialized global-এর অংশ বা padding।
$ readelf -x .bss demo
readelf: Warning: Section '.bss' has no data to dump.এইটাই .bss সম্পর্কে সবচেয়ে সরাসরি প্রমাণ — ফাইলে সত্যিই কোনো
byte নেই dump করার মতো, যদিও readelf -S-এ .bss-এর Size
নন-জিরো দেখায়।
- demo.c — সোর্স কোডvariable আর function-এর নাম আছে, ঠিকানা নেই
- demo.o — assembler-এর আউটপুট (ET_REL)section আছে, কিন্তু ঠিকানা এখনো relative-to-zero
- demo — linker-এর আউটপুট (ET_DYN, PIE)section চূড়ান্ত, program header (segment) যোগ হলো
- execve() — kernel loaderশুধু program header পড়ে, LOAD segment-গুলো mmap করে
- চলমান প্রসেস — /proc/pid/maps.text+.rodata একটা mapping (R-E), .data+.bss আরেকটা (RW)
নিজে চালিয়ে দেখুন
.bss-এর ফাইল-সাশ্রয় সরাসরি মাপা
// big_uninit.c
int big_array[10 * 1024 * 1024]; // 40 MB, uninitialized → .bss
int main(void) { return big_array[0]; }// big_init.c
int big_array[10 * 1024 * 1024] = {1}; // একটাও non-zero initializer থাকলেই .data
int main(void) { return big_array[0]; }gcc -O0 -o big_uninit big_uninit.c
gcc -O0 -o big_init big_init.c
ls -la big_uninit big_init
readelf -S big_uninit | grep -E '\.data|\.bss'
readelf -S big_init | grep -E '\.data|\.bss'সাধারণ ফলাফল:
-rwxr-xr-x 1 user user 16624 big_init
-rwxr-xr-x 1 user user 16624 big_uninit
[24] .data PROGBITS ... Size 0000000000000000 (big_uninit)
[25] .bss NOBITS ... Size 0000000000a00010 (big_uninit — ~40 MB!)
[24] .data PROGBITS ... Size 0000000000a00010 (big_init — ~40 MB)
[25] .bss NOBITS ... Size 0000000000000000 (big_init)ls -la প্রথমে সমান আকার দেখাচ্ছে বিভ্রান্তিকরভাবে — কারণ -O0-এ
compiler এখনো সরাসরি linking করেনি ফাইলের tail truncate করে; আসল
পার্থক্যটা readelf -S-এর .bss বনাম .data size-এ স্পষ্ট। একটা
আরও সরাসরি demonstration:
du -h big_uninit big_init # disk ব্যবহার
stat --format='%s bytes' big_uninit big_init40 MB-এর array .bss-এ ফাইলের আকারে প্রায় কিছুই যোগ করে না
(Size metadata হিসেবে থাকে, byte হিসেবে না), কিন্তু .data-এ
গেলে ফাইলে সত্যিই ৪০ মেগাবাইট বসে যায়। বড় স্কেলে (কল্পনা করুন কেউ
একটা বিশাল lookup table declare করেছে কিন্তু ভুলবশত একটা = {0}
initializer দিয়ে দিয়েছে) এই পার্থক্যটা একটা বাইনারির আকারকে
কয়েক মেগাবাইট থেকে কয়েক গিগাবাইটে নিয়ে যেতে পারে — বাস্তব bug,
বাস্তব build-time সমস্যা।
একটা বিশাল uninitialized array ফাইলের আকার প্রায় বাড়ায়ই না, কিন্তু একটা initialized array-এর সমান আকার সরাসরি ফাইলে যোগ হয় — .bss বনাম .data-এর পার্থক্য বাস্তবে পরিমাপযোগ্য।
নিজে বানান
Section Header Dumper — নিজের mini-readelf
- ELF header পার্স করে e_shoff, e_shentsize, e_shnum বের করুন
- সেই অফসেট থেকে প্রতিটা section header entry পড়ুন
- sh_name ইনডেক্স থেকে .shstrtab (section header string table) দিয়ে আসল নাম বের করুন
- প্রতিটা section-এর নাম, type, address, offset, size টেবিল আকারে প্রিন্ট করুন
- readelf -S-এর আসল আউটপুটের সাথে মিলিয়ে যাচাই করুন
মূল ধারণা: section header table নিজেও একটা array — e_shoff-এ
শুরু, প্রতিটা entry e_shentsize byte, মোট e_shnumটা। প্রতিটা
entry-র sh_name ফিল্ড একটা নাম নয়, বরং .shstrtab নামের
আরেকটা section-এর ভেতরে একটা byte-offset, যেখানে null-terminated
নামটা আছে — এই এক-ধাপ পরোক্ষতাই কেন সব section-এর নাম আলাদাভাবে
সংরক্ষণ না করে একটা কেন্দ্রীয় string table-এ রাখা হয় (পুনরাবৃত্তি
এড়াতে, ঠিক যেমন symbol table-এর নামও .strtab-এ রাখা হয়)।
import struct
import sys
SH_TYPES = {
0: "NULL", 1: "PROGBITS", 2: "SYMTAB", 3: "STRTAB",
4: "RELA", 8: "NOBITS", 9: "REL", 11: "DYNSYM",
}
def read_cstr(data, offset):
end = data.index(b"\x00", offset)
return data[offset:end].decode("utf-8", errors="replace")
def dump_sections(path):
with open(path, "rb") as f:
data = f.read()
assert data[:4] == b"\x7fELF", "ELF ফাইল না"
is_64 = data[4] == 2
if not is_64:
raise NotImplementedError("এই স্ক্রিপ্ট শুধু 64-bit ELF সমর্থন করে")
e_shoff = struct.unpack_from("<Q", data, 0x28)[0]
e_shentsize = struct.unpack_from("<H", data, 0x3a)[0]
e_shnum = struct.unpack_from("<H", data, 0x3c)[0]
e_shstrndx = struct.unpack_from("<H", data, 0x3e)[0]
# প্রথমে .shstrtab section-এর নিজের অফসেট বের করি
shstr_entry_off = e_shoff + e_shstrndx * e_shentsize
shstrtab_offset = struct.unpack_from("<Q", data, shstr_entry_off + 24)[0]
print(f"{'নাম':<18} {'Type':<10} {'Address':>16} {'Offset':>10} {'Size':>10}")
print("-" * 70)
for i in range(e_shnum):
off = e_shoff + i * e_shentsize
sh_name = struct.unpack_from("<I", data, off + 0)[0]
sh_type = struct.unpack_from("<I", data, off + 4)[0]
sh_addr = struct.unpack_from("<Q", data, off + 16)[0]
sh_offset = struct.unpack_from("<Q", data, off + 24)[0]
sh_size = struct.unpack_from("<Q", data, off + 32)[0]
name = read_cstr(data, shstrtab_offset + sh_name)
type_name = SH_TYPES.get(sh_type, f"?({sh_type})")
print(f"{name:<18} {type_name:<10} {sh_addr:>16x} {sh_offset:>10x} {sh_size:>10x}")
if __name__ == "__main__":
dump_sections(sys.argv[1])$ python3 section_dumper.py demo
নাম Type Address Offset Size
----------------------------------------------------------------
NULL 0 0 0
.interp PROGBITS 318 318 1c
.text PROGBITS 1040 1040 185
.rodata PROGBITS 2000 2000 18
.data PROGBITS 4010 3010 8
.bss NOBITS 4020 3020 4
.symtab SYMTAB 0 4210 480
.strtab STRTAB 0 4690 1a3
.shstrtab STRTAB 0 4833 c9readelf -S demo-এর সাথে পাশাপাশি রেখে প্রতিটা সারি মিলিয়ে দেখুন —
Type, Address, Offset, Size চারটাই মিলে যাওয়া উচিত।
নিজে বাড়ান:
- Program header table-ও একইভাবে পার্স করুন (
e_phoff,e_phnum), segment type আর permission flag প্রিন্ট করুন .bss-কে (typeNOBITS) বিশেষভাবে চিহ্নিত করুন আউটপুটে, আর পাশে দেখান “ফাইলে ০ byte, মেমরিতে sh_size byte”- Section-to-segment mapping নিজে গণনা করুন — কোন section-এর address range কোন LOAD segment-এর range-এর ভেতরে পড়ে তা মিলিয়ে
- Symbol table (
.symtab) পার্স করে function/variable নামের তালিকা বের করুন — এটাই মূলতnmকমান্ড যা করে
বাস্তব সিস্টেমে
ELF যেখানে সিদ্ধান্ত নেয়
Linux kernel-এর execve()। যখন আপনি ./program চালান, kernel-এর
binfmt_elf module প্রথমে magic bytes চেক করে, তারপর program
header পড়ে প্রতিটা PT_LOAD segment-কে সরাসরি mmap() করে —
section header এই পুরো প্রক্রিয়ায় স্পর্শও হয় না। এই কারণেই একটা
strip-করা বাইনারি (section header table নেই) তবু নিখুঁতভাবে চলে।
Core dump — ET_CORE। একটা প্রোগ্রাম crash করলে (segfault),
kernel চাইলে তার পুরো memory image একটা core ফাইলে লেখে — সেটাও
ELF, e_type = ET_CORE, আর segment-গুলো এবার “এই মুহূর্তে মেমরিতে
কী ছিল” ধারণ করে। gdb program core দিয়ে সেই মুহূর্তের অবস্থা
post-mortem analyze করা যায় — পরের লেসনে আমরা লাইভ debugging দেখব,
কিন্তু core dump একই ELF ভিত্তির উপর দাঁড়িয়ে “মৃত্যুর মুহূর্ত”
ধরে রাখে।
Debug info — .debug_* sections, DWARF। Source-level debugging
(লাইন নম্বর, variable নাম, type তথ্য) সংরক্ষিত হয় .debug_info,
.debug_line ইত্যাদি নামের section-এ, DWARF ফরম্যাটে এনকোড করা।
gcc -g এই section-গুলো যোগ করে; strip সেগুলো ফেলে দেয়। পরের
লেসনে gdb যখন “কোন লাইনে আছি” দেখায়, সেটা এই section থেকেই আসে।
Android-এর .so ফাইল। প্রতিটা Android app-এর native code
(libnative-lib.so) একটা সাধারণ ELF shared object, ARM64 বা x86-64
target করে — Android Runtime (ART) সেই একই ELF loader logic ব্যবহার
করে যা এই লেসনে দেখানো হলো, শুধু Machine ফিল্ড ভিন্ন CPU নির্দেশ
করে।
Firmware আর embedded ELF। Bootloader (U-Boot), Linux kernel
image (vmlinux), এমনকি কিছু microcontroller firmware — সবই ELF-এ
কম্পাইল হয়, পরে একটা raw binary-তে রূপান্তরিত হয় flash করার আগে
(objcopy -O binary), কারণ chip নিজে ELF parse করতে জানে না, শুধু
raw bytes flash-এ লেখা যায়।
Malware analysis আর static analysis টুল। radare2, Ghidra,
IDA Pro — সব ELF parsing দিয়ে শুরু করে: section/segment মানচিত্র
বানিয়ে, symbol table দেখে কোন function কোথায়, .dynsym/relocation
দেখে কোন বাইরের function call হচ্ছে (গত লেসনের PLT/GOT বিশ্লেষণ,
এখন malware-এ কী libc function ব্যবহার হচ্ছে বোঝার জন্য)।
strip আর বাইনারি আকার কমানো। Production বিল্ডে প্রায়ই
strip binary চালানো হয় — .symtab, .debug_* এর মতো section
মুছে ফেলে (যেগুলো শুধু debugging-এর জন্য, execution-এর জন্য না)।
.dynsym থাকে (dynamic linking-এর জন্য দরকার), কিন্তু .symtab
চলে যায় — বাইনারি ছোট হয়, আর reverse-engineering কিছুটা কঠিন হয়
(যদিও নিরাপত্তার প্রকৃত ব্যবস্থা না, শুধু বাধা)।
যে ভুলগুলো সবাই করে
“ELF মানে শুধু executable ফাইল।”
নামটাই ভুল ধারণা তৈরি করে — “Executable and Linkable Format”-এর
দ্বিতীয় শব্দটা প্রায়ই উপেক্ষিত হয়। এই লেসনের e_type টেবিলটাই
প্রমাণ: ET_REL (object file, এখনো linkable, executable না),
ET_DYN (shared library, নিজে থেকে চলে না), ET_EXEC/PIE
executable, ET_CORE (crash snapshot) — চারটাই ELF, মাত্র একটা
ET_EXEC-এর মতো ধরনের।
আসলে pipeline-এর প্রতিটা বাইনারি artifact — .o, .so, চূড়ান্ত
executable, এমনকি একটা crash-এর পরে core dump — সবই একই format-এর
variation।
“Section header না থাকলে ফাইলটা চলবে না।”
বিপরীত সত্য। execve() (আর আসলে dynamic linker ld.so-ও) শুধু
program header (segment) পড়ে, section header একদমই স্পর্শ করে
না। একটা strip-করা executable-এ section header table সম্পূর্ণ
অনুপস্থিত থাকতে পারে (e_shnum = 0), তবু নিখুঁতভাবে চলে।
Section header দরকার শুধু build-time টুল (linker) আর analysis
টুলের (readelf, gdb debug-info lookup, disassembler) জন্য —
runtime-এর জন্য না। এই পার্থক্যটাই “section (build/analysis) বনাম
segment (runtime)“-এর মূল কথা, শুধু একটা বিমূর্ত নিয়ম না — একটা
বাস্তব, পরীক্ষাযোগ্য পরিণতি (strip --strip-all করে চালিয়ে দেখুন)।
“.bss একটা section-এর মতোই normal, শুধু নাম আলাদা।”
readelf -S-এর টেবিলে .bss-কে অন্য যেকোনো section-এর মতোই একটা
সারি দেখায় — কিন্তু তার Type (NOBITS) বাকি সবার (PROGBITS)
থেকে গুণগতভাবে ভিন্ন। PROGBITS section-এর জন্য ফাইলে সত্যিই সেই
আকারের byte আছে — readelf -x দিয়ে dump করা যায়। .bss-এর জন্য
readelf -x করলে “no data to dump” — কারণ ফাইলে কোনো byte-ই নেই,
শুধু “লোড হওয়ার সময় এত byte শূন্য দিয়ে ভরে দাও” এই metadata।
এই পার্থক্যটা practically গুরুত্বপূর্ণ: একটা 40 MB uninitialized
array ফাইলের আকারে প্রায় কিছুই যোগ করে না (এই লেসনের experiment),
কিন্তু একই আকারের initialized array (এমনকি সব-শূন্য দিয়ে
initialize করলেও, compiler optimization না থাকলে) .data-এ যায়
আর সত্যিই ৪০ মেগাবাইট ফাইলে বসে যায়।
বুঝেছেন কি না দেখুন
1readelf -h-এ দুইটা ফাইল দেখলেন — একটার Type: REL, আরেকটার
Type: DYN। শুধু এই একটা তথ্য থেকে পাইপলাইনের কোন ধাপে ফাইল দুটো
আছে, আর Entry point address ফিল্ডে কী আশা করা উচিত তা বলুন।
যুক্তি
readelf -h-এ দুইটা ফাইল দেখলেন — একটার Type: REL, আরেকটার
Type: DYN। শুধু এই একটা তথ্য থেকে পাইপলাইনের কোন ধাপে ফাইল দুটো
আছে, আর Entry point address ফিল্ডে কী আশা করা উচিত তা বলুন।ET_REL — এটা একটা object file (.o), অর্থাৎ assembler-এর
আউটপুট, এখনো linked হয়নি। এই ফাইলটা pipeline-এর মাঝামাঝি —
compile/assemble হয়ে গেছে কিন্তু linker এখনো একাধিক .o মিলিয়ে
একটা চূড়ান্ত বাইনারি বানায়নি।
ET_REL-এ Entry point address অর্থহীন, সাধারণত 0x0 —
কারণ একটা relocatable object file নিজে থেকে চলে না, কোথা থেকে
execution শুরু হবে সেই প্রশ্নটাই এখনো প্রাসঙ্গিক না। এই ফাইলে
হয়তো main function-ই নেই (অন্য কোনো .o-তে আছে), বা থাকলেও এটা
এখনো একটা “খণ্ড”, পুরো প্রোগ্রাম না।
ET_DYN — এটা linking-পরবর্তী: শেয়ার্ড লাইব্রেরি অথবা PIE
executable (আগের লেসনের PT_INTERP টেস্ট দিয়ে আলাদা করতে হবে
কোনটা)। এখানে Entry point address একটা বাস্তব, অর্থবহ মান দেখাবে
— যদি এটা PIE executable হয়, সেই ঠিকানাটাই loader-এর jump করার
জায়গা (execution শুরুর বিন্দু, লোড-base-এর সাপেক্ষে আপেক্ষিক, যেহেতু
ET_DYN)। যদি এটা plain shared library হয়, Entry point address
থাকলেও সেটা সরাসরি ব্যবহার হয় না (library নিজে থেকে “শুরু” হয় না) —
তবে DT_INIT-এর মতো initialization hook থাকতে পারে।
2readelf -S-এ একটা section-এর Offset = 0x3010, Size = 0x8, আর
Type = PROGBITS। পরের section-এর Offset = 0x3020, Type = NOBITS, Size = 0x4। তার পরের section-এর Offset কত হবে বলে
আশা করেন, আর কেন?
প্রয়োগ
readelf -S-এ একটা section-এর Offset = 0x3010, Size = 0x8, আর
Type = PROGBITS। পরের section-এর Offset = 0x3020, Type = NOBITS, Size = 0x4। তার পরের section-এর Offset কত হবে বলে
আশা করেন, আর কেন?প্রথম section (PROGBITS, offset 0x3010, size 0x8) ফাইলে
বাস্তব byte দখল করে — শেষ হয় 0x3010 + 0x8 = 0x3018-এ। কিন্তু
তালিকায় পরের section-এর offset 0x3020 — মাঝে 0x3018 থেকে
0x3020 পর্যন্ত ৮ byte gap, সম্ভবত alignment padding (section-গুলো
প্রায়ই 8 বা 16-byte boundary-তে align করা থাকে)। এটা প্রথম clue
যে দ্বিতীয় section সম্ভবত .bss।
দ্বিতীয় section-এর Type = NOBITS নিশ্চিত করে এটা .bss (বা
.bss-এর মতো একটা section) — এর Size = 0x4 মানে মেমরিতে ৪ byte
লাগবে, কিন্তু ফাইলে এর জন্য কোনো byte বরাদ্দ নেই। যেহেতু NOBITS
section ফাইলে কোনো জায়গা দখল করে না, তৃতীয় section-এর Offset
দ্বিতীয় section-এর Offset-এর সমানই হবে — 0x3020।
এটাই NOBITS-এর সংজ্ঞাগত পরিণতি: NOBITS section-এর Size
পরবর্তী section-এর file offset নির্ধারণে কোনো ভূমিকা রাখে না,
কারণ ফাইলে তার কোনো “জায়গা” নেই — শুধু একটা সংখ্যা (কতটা মেমরি
দরকার) যা loader ব্যবহার করবে, ফাইল-লেআউট গণনায় না।
3একটা section, .text ধরুন, permission AX (Alloc + eXecutable)।
আরেকটা, .rodata, permission শুধু A (Alloc, লেখা যাবে না, চালানো
যাবে না)। readelf -l-এর “Section to Segment mapping”-এ দেখা যায়
দুটোই একই LOAD segment-এ (permission R E) গোষ্ঠীবদ্ধ। এটা
কি নিরাপত্তার দিক থেকে সমস্যা — .rodata-কে কি ভুলবশত executable
বানিয়ে দেওয়া হলো?
যুক্তি
.text ধরুন, permission AX (Alloc + eXecutable)।
আরেকটা, .rodata, permission শুধু A (Alloc, লেখা যাবে না, চালানো
যাবে না)। readelf -l-এর “Section to Segment mapping”-এ দেখা যায়
দুটোই একই LOAD segment-এ (permission R E) গোষ্ঠীবদ্ধ। এটা
কি নিরাপত্তার দিক থেকে সমস্যা — .rodata-কে কি ভুলবশত executable
বানিয়ে দেওয়া হলো?হ্যাঁ, প্রযুক্তিগতভাবে এটাই ঘটছে, আর এটা একটা পরিচিত trade-off, বাগ না।
Segment-এর permission R E-তে দুটোরই মধ্যে সবচেয়ে permissive
মিলিত মান নেওয়া হয় — .text চাই RX, .rodata চাই R, একসাথে
একই page-এ থাকলে গোটা page-টাকে RX হতে হবে (linker কোনো section-কে
তার প্রকৃত প্রয়োজনের চেয়ে কম access দিতে পারে না)। ফলে .rodata
“technically” execute-able হয়ে যায়, যদিও কোনো কোড সেখান থেকে চালানোর
কথা না।
এটা কেন গ্রহণযোগ্য: বাস্তব আক্রমণের ঝুঁকি কম, কারণ .rodata-এ
সাধারণত string literal আর constant থাকে — একজন আক্রমণকারীর সেখানে
“কার্যকর” শেল-কোড বসাতে হলে প্রথমে লেখার ক্ষমতা লাগবে, যা তার
নেই (.rodata কখনো W পায় না, এমনকি একই segment-এ .text-এর সাথে
মিশে থাকলেও)। আসল নিরাপত্তা boundary হলো W^X (write XOR
execute) — একটা page কখনো একই সাথে writable আর executable হবে না —
আর সেটা এখানে বজায় আছে।
কেন আলাদা segment না করে একসাথে রাখা হলো: যদি .rodata-কে
আলাদা R-only segment-এ রাখা হতো, একটা বাড়তি page-aligned mapping
লাগত — মেমরি আর syscall overhead বাড়ত সামান্য নিরাপত্তা লাভের বিনিময়ে
যা বাস্তবে তেমন কিছু আটকায় না। এটাই linker script-এর একটা
ইচ্ছাকৃত engineering trade-off: “একই read/execute-permission profile-এর
section-গুলো একসাথে গোষ্ঠীবদ্ধ করো, শুধু write-permission-এর
পার্থক্যেই আলাদা segment বানাও (W^X বজায় রাখতে)।”
4একটা টুল বানাচ্ছেন যেটা একটা ELF ফাইল “malicious” কিনা প্রাথমিক
স্ক্রিনিং করে — শুধু কতগুলো বাইরের function (libc-এর মতো) call হচ্ছে
তার সংখ্যা আর নাম গোনে, execute না করেই। কোন section(গুলো) পড়বেন,
আর কেন section-এর বদলে segment না?
ডিজাইন
পড়া দরকার .dynsym (dynamic symbol table) আর .rela.plt /
.rela.dyn (relocation entries) — গত লেসনের PIC/GOT/PLT লেসনের
সরাসরি প্রয়োগ।
.dynsym-এ প্রতিটা বাইরের symbol-এর নাম আছে যা এই বাইনারি import
করে (printf, system, strcpy, ইত্যাদি) — .rela.plt-এর
R_X86_64_JUMP_SLOT entry গুলো এই symbol-গুলোকেই GOT slot-এর
সাথে যুক্ত করে (গত লেসনের readelf -r আউটপুট ঠিক এটাই দেখিয়েছিল)।
এই দুটো টেবিল একসাথে পড়লে “এই বাইনারি কোন কোন বাইরের ফাংশন
ব্যবহার করে” সম্পূর্ণ তালিকা পাওয়া যায় — যেমন system() বা
exec*()-এর মতো সন্দেহজনক ফাংশন দেখলে একটা সতর্কতা flag তোলা যায়।
কেন section, segment না: এই তথ্য — “কোন symbol-এর নাম কী” —
আসলে build-time/analysis-time metadata, যা কোনো runtime loading
সিদ্ধান্তে সরাসরি প্রয়োজন হয় না, বরং কোন symbol নাম কী তা মানুষ
বা টুল বোঝার জন্য সংরক্ষিত (dynamic linker নিজেও আসলে .dynsym-ই
পড়ে symbol resolve করতে — কিন্তু সেটাও build-time-এ প্রস্তুত করা
একটা named table, program header-এ নেই)। Program header/segment
শুধু “কতটুকু মেমরি, কোন permission” বলে — কোনো নাম, কোনো symbol
তথ্য বহন করে না। আপনার টুলের প্রশ্ন (“কোন function”) নামভিত্তিক,
তাই section-ই সঠিক জায়গা।
সতর্কতা: যদি বাইনারিটা strip-করা হয় (.symtab নেই), .dynsym
তবু থাকবে যদি এটা dynamically linked হয় — কারণ dynamic linking
নিজেই runtime-এ symbol নাম দিয়ে resolve করে (গত লেসনের PLT
resolver), তাই .dynsym সাধারণ strip দিয়ে সরানো যায় না, শুধু
.symtab (যা শুধুই debugging/analysis-এর জন্য) সরানো যায়।
5“Section header আর program header — দুটোই ELF ফাইলের ভেতরে থাকা
টেবিল যা বলে দেয় ফাইলে কী কী আছে, তাই তারা কার্যত একই জিনিসের
দুইটা নাম” — এই দাবিটা কেন ভুল? দুই বাক্যে বলুন।
স্মরণ
ভুল, কারণ তাদের পাঠক আর উদ্দেশ্য সম্পূর্ণ আলাদা: section header
টেবিল linker আর analysis টুলের জন্য, সূক্ষ্ম-দানার নাম-করা
অংশ (.text, .symtab, …) বর্ণনা করে যা build-time-এ ও
offline-বিশ্লেষণে দরকার; program header টেবিল loader/kernel-এর
জন্য, মোটা-দানার নামহীন memory segment (শুধু permission ও range)
বর্ণনা করে যা শুধুমাত্র runtime-এ execve()/mmap()-এর সময় লাগে।
দ্বিতীয় প্রমাণ: এই লেসনে দেখানো হয়েছে একটা strip-করা বাইনারি
section header সম্পূর্ণ ছাড়াই চলতে পারে (কারণ loader সেটা কখনো
পড়েই না), কিন্তু program header বাদ দিলে বাইনারিটা চলবেই না
(execve() জানবে না কোথায় কী map করতে হবে) — যদি দুটো টেবিল সত্যিই
“একই জিনিসের দুই নাম” হতো, একটা বাদ দিলে অন্যটাও একই রকম গুরুত্বপূর্ণ
হতো।
এরপর কী
পরের ধাপ — এই স্থির নকশা একটা চলমান প্রসেসে দেখা
এই দুই লেসনে আমরা একটা বাইনারির স্থির (static) গঠন দেখেছি —
ফাইলে কী কী section আছে, কোন segment-এ কী map হবে, GOT/PLT কোথায়
বসে, .bss কীভাবে ফাইলে সংরক্ষিত না হয়েও মেমরিতে জায়গা পায়।
সবকিছু readelf, objdump দিয়ে — প্রোগ্রামটা না চালিয়েই।
কিন্তু এই পুরো module-এর driving question ছিল: “আমার C function compile হয়ে ঠিক কোন instruction-গুলো হলো, আর কেন?” — সেই প্রশ্নের পূর্ণাঙ্গ উত্তর পেতে এখন দরকার প্রোগ্রামটাকে লাইভ দেখা — register আসলে কোন মুহূর্তে কোন মান ধরে, stack frame আসলে কীভাবে গড়ে ওঠে আর ভেঙে যায়, GOT entry সত্যিই কখন patch হয়।
পরের, এই module-এর শেষ লেসনে আমরা gdb দিয়ে ঠিক এটাই করব —
এতক্ষণ যা কাগজে-কলমে (বা readelf-এর স্থির আউটপুটে) দেখেছি, তার
প্রতিটা এখন একটা চলমান প্রসেসে, breakpoint দিয়ে থামিয়ে, সরাসরি
চোখে দেখা।
আরও পড়ুন
- System V Application Binary Interface — Generic ABI, Chapter 4 (Object Files) · ELF-এর প্রামাণ্য স্পেসিফিকেশন — header layout, section types, program header types
- elf(5) — Linux manual page · man 5 elf — টার্মিনালেই এই লেসনের প্রতিটা struct-এর সংজ্ঞা
- Linkers and Loaders — John R. Levine · Chapter 2 — object file format-এর সাধারণ নকশা-দর্শন, ELF তার একটা বাস্তবায়ন