לדלג לתוכן

5.11 פורמט ELF לעומק הרצאה

הקדמה

בפרק 5.3 למדנו כיצד הloader מזהה קבצי ELF וטוען אותם לזיכרון - ראינו את הELF header, את הprogram headers (סגמנטים), ואת התהליך שהקרנל עובר כדי להריץ תוכנית.
בפרק 5.10 למדנו על ספריות משותפות, ואיך מנגנוני PLT ו-GOT מאפשרים קריאה לפונקציות מספריות חיצוניות בזמן ריצה.

עכשיו הגיע הזמן להבין את פורמט הELF לעומק - לא רק מה הקרנל צריך כדי לטעון את הקובץ, אלא את כל המידע שנמצא בפנים. הידע הזה הוא בסיס קריטי להנדסה לאחור (reverse engineering), כתיבת exploit-ים, דיבוג, ובעצם להבנה אמיתית של מה שקורה כשאנחנו מקמפלים ומריצים תוכנית.


שתי נקודות מבט על קובץ ELF - linking view vs execution view

נקודה חשובה שכבר נגענו בה ב-5.3 אבל עכשיו נרחיב: לקובץ ELF יש שתי דרכים להסתכל על התוכן שלו:

נקודת המבט של הלינקר - linking view (סקשנים)
הלינקר (ld) צריך לדעת בדיוק מה כל חלק בקובץ מכיל - איפה הקוד, איפה הנתונים, איפה טבלת הסמלים, איפה מידע הrelocation. לשם כך הוא משתמש בסקשנים (sections) שמוגדרים ב-section header table.

נקודת המבט של הקרנל - execution view (סגמנטים)
הקרנל לא מתעניין בסקשנים. הוא צריך לדעת דבר אחד: מה לטעון לזיכרון ועם אילו הרשאות. לשם כך הוא משתמש בסגמנטים (segments) שמוגדרים ב-program header table.

+------------------+
|    ELF Header    |
+------------------+
| Program Headers  |  <-- הקרנל משתמש בזה (סגמנטים)
|   (segments)     |
+------------------+
|                  |
|  תוכן הקובץ     |
|  (קוד, נתונים,  |
|   סמלים...)     |
|                  |
+------------------+
| Section Headers  |  <-- הלינקר משתמש בזה (סקשנים)
|   (sections)     |
+------------------+

שימו לב - סגמנט אחד יכול להכיל מספר סקשנים. לדוגמה, סגמנט הקוד (PT_LOAD עם הרשאות R-X) מכיל בתוכו את הסקשנים .text, .rodata, .plt ועוד.


תזכורת קצרה - הELF Header

כבר ראינו את הELF header בפרק 5.3, אז רק נזכיר בקצרה. הכותרת נמצאת בתחילת הקובץ ומכילה את המידע הבסיסי ביותר:

readelf -h ./my_program
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
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Entry point address:               0x401040
  Start of program headers:          64 (bytes into file)
  Start of section headers:          14200 (bytes into file)
  Number of program headers:         13
  Number of section headers:         31

הדברים החשובים שנשים לב אליהם:
- שדה הType - אומר לנו אם זה executable, shared object (ספריה משותפת/PIE), או relocatable (קובץ אובייקט .o)
- שדה הEntry point - הכתובת שבה התוכנית מתחילה לרוץ
- מספר הprogram headers ומספר הsection headers - אומרים לנו כמה סגמנטים וסקשנים יש בקובץ


הסקשנים - sections (נקודת המבט של הלינקר)

עכשיו נכנסים לעומק. כל סקשן בקובץ ELF מכיל סוג מסוים של מידע. נעבור על הסקשנים החשובים ביותר:

סקשן text. - קוד בר-הרצה

הסקשן .text מכיל את הוראות המכונה - הקוד שהמעבד מריץ בפועל. כל הפונקציות שכתבנו (כולל main) מקומפלות להוראות אסמבלי ונשמרות כאן.

הסקשן הזה ממופה לזיכרון עם הרשאות קריאה והרצה בלבד (R-X) - אי אפשר לכתוב אליו בזמן ריצה. זה חלק ממנגנוני ההגנה שלמדנו עליהם בפרק 2 (paging והרשאות דפים).

סקשן data. - משתנים גלובליים מאותחלים

הסקשן .data מכיל משתנים גלובליים ו-static שקיבלו ערך התחלתי בקוד:

int counter = 42;           // נמצא ב-.data
static int flag = 1;        // נמצא ב-.data

הסקשן הזה ממופה עם הרשאות קריאה וכתיבה (RW-) כי המשתנים האלה יכולים להשתנות בזמן ריצה.

סקשן bss. - משתנים גלובליים לא מאותחלים

הסקשן .bss מכיל משתנים גלובליים ו-static שלא קיבלו ערך התחלתי (או שאותחלו ל-0):

int buffer[1000];           // נמצא ב-.bss
static int count;           // נמצא ב-.bss

הנקודה המעניינת: הסקשן .bss לא תופס מקום בקובץ עצמו. הקובץ רק רושם את הגודל של הסקשן, ובזמן טעינה הloader מקצה את הזיכרון ומאפס אותו. זו הסיבה שמשתנים גלובליים שלא אותחלו תמיד מתחילים עם ערך 0 ב-C.

חשבו על זה - אם יש לנו מערך של מיליון int-ים שלא מאותחל, אין סיבה לשמור מיליון אפסים בקובץ. מספיק לרשום "הסקשן הזה צריך 4MB של אפסים".

סקשן rodata. - נתונים לקריאה בלבד

הסקשן .rodata (read-only data) מכיל מחרוזות קבועות ונתונים שלא אמורים להשתנות:

const int MAX = 100;                    // נמצא ב-.rodata
printf("Hello, World!\n");              // המחרוזת "Hello, World!\n" נמצאת ב-.rodata

הסקשן הזה ממופה עם הרשאת קריאה בלבד (R--). ניסיון לכתוב אליו יגרום ל-segmentation fault.

סקשן symtab. - טבלת סמלים

הסקשן .symtab (symbol table) הוא אחד הסקשנים הכי חשובים להבנה. הוא מכיל רשימה של כל הסמלים בקובץ - שמות של פונקציות, משתנים גלובליים, וכו', יחד עם הכתובות שלהם.

כל רשומה בטבלת הסמלים מכילה:
- שם הסמל - למשל "main", "counter", "helper_func"
- כתובת - הכתובת שבה הסמל נמצא בזיכרון
- גודל - כמה בתים הסמל תופס
- סוג - האם זו פונקציה (FUNC), משתנה (OBJECT), או משהו אחר
- סקשן - באיזה סקשן הסמל נמצא (למשל .text עבור פונקציות)

הטבלה הזו היא מה שמאפשר לדיבאגרים כמו gdb להציג שמות פונקציות במקום כתובות גולמיות.

סקשן strtab. - טבלת מחרוזות

הסקשן .strtab (string table) פשוט מכיל את המחרוזות שטבלת הסמלים מצביעה עליהן. ב-.symtab כל רשומה מכילה אינדקס אל .strtab שבו נמצא שם הסמל עצמו.

למה לא לשמור את השם ישירות ב-.symtab? כי מחרוזות הן באורך משתנה, ושמירה שלהן בטבלה נפרדת מאפשרת מבנה יעיל יותר.

סקשנים dynsym. ו-dynstr. - סמלים דינמיים

הסקשנים .dynsym ו-.dynstr הם המקבילה הדינמית של .symtab ו-.strtab. הם מכילים רק את הסמלים שנדרשים בזמן ריצה - בעיקר פונקציות מספריות משותפות.

ההבדל הקריטי: כשעושים strip לקובץ (נדבר על זה בהמשך), הסקשנים .symtab ו-.strtab נמחקים, אבל .dynsym ו-.dynstr נשארים - כי הם הכרחיים להרצה.

סקשנים rel.plt. ו-rela.plt. - רלוקציות

הסקשנים .rel.plt או .rela.plt מכילים הוראות relocation עבור הPLT. כפי שלמדנו בפרק 5.10, כשהתוכנית קוראת לprintf, היא קופצת דרך הPLT שבתורו ניגש לGOT. הרשומות בסקשנים האלה אומרות לdynamic linker: "תכתוב את הכתובת האמיתית של printf בכניסה הזו בGOT".

סקשנים got. ו-got.plt. - טבלת הכתובות הגלובלית

הGlobal Offset Table - כבר הכרנו את הסקשנים האלה בפרק 5.10. הם מכילים את הכתובות בפועל של פונקציות ומשתנים מספריות משותפות. בפעם הראשונה שפונקציה נקראת, הdynamic linker כותב את הכתובת האמיתית לGOT, ומהפעם השניה הגישה ישירה.

סקשן plt. - סטאבים של PLT

הProcedure Linkage Table - גם על זה דיברנו בפרק 5.10. הסקשן .plt מכיל קטעי קוד קצרים (stubs) שמבצעים את הקפיצה דרך הGOT.

סקשנים init. ו-fini. - קוד אתחול וסיום

הסקשנים .init ו-.fini מכילים קוד שרץ לפני ואחרי main.

בנוסף, יש את .init_array ו-.fini_array - מערכים של מצביעים לפונקציות שצריכות לרוץ לפני/אחרי main. אפשר להוסיף פונקציות משלנו לשם:

#include <stdio.h>

__attribute__((constructor))
void before_main(void) {
    printf("this runs BEFORE main\n");
}

__attribute__((destructor))
void after_main(void) {
    printf("this runs AFTER main\n");
}

int main(void) {
    printf("this is main\n");
    return 0;
}

פלט:

this runs BEFORE main
this is main
this runs AFTER main

זה שימושי לאתחול ספריות, ניקוי משאבים, ובהקשר של אבטחה - גם לתוקפים שרוצים להריץ קוד לפני שmain מתחיל.

סקשני debug_*. - מידע דיבוג

אם מקמפלים עם הדגל -g, הקומפיילר מוסיף סקשנים כמו .debug_info, .debug_line, .debug_abbrev ועוד. הסקשנים האלה מכילים את המיפוי בין הוראות מכונה לשורות בקוד המקור, שמות משתנים מקומיים, טיפוסי נתונים, וכל המידע שדיבאגר צריך כדי לתת לנו חוויית דיבוג נוחה.

הסקשנים האלה לא נטענים לזיכרון בזמן ריצה - הם רק נקראים על ידי כלי דיבוג.


שימוש בreadelf - חקירת קבצי ELF

הכלי readelf הוא הכלי המרכזי לחקירת קבצי ELF. נעבור על הדגלים החשובים:

צפייה בסקשנים - readelf -S

readelf -S ./my_program

הפלט מציג טבלה של כל הסקשנים בקובץ:

There are 31 section headers, starting at offset 0x3978:

Section Headers:
  [Nr] Name              Type             Address           Offset    Size
  [ 0]                   NULL             0000000000000000  00000000  0000000000000000
  [ 1] .interp           PROGBITS         0000000000000318  00000318  000000000000001c
  [ 2] .note.gnu.build-i NOTE             0000000000000338  00000338  0000000000000024
  ...
  [14] .text             PROGBITS         0000000000001060  00001060  0000000000000185
  [15] .fini             PROGBITS         00000000000011e8  000011e8  000000000000000d
  [16] .rodata           PROGBITS         0000000000002000  00002000  0000000000000012
  ...
  [24] .data             PROGBITS         0000000000004000  00003000  0000000000000010
  [25] .bss              NOBITS           0000000000004010  00003010  0000000000000008
  ...

שימו לב ש-.bss מסומן כNOBITS - זה אומר שהוא לא תופס מקום בקובץ.

צפייה בסמלים - readelf -s

readelf -s ./my_program
Symbol table '.symtab' contains 67 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
   ...
    50: 0000000000001149    38 FUNC    GLOBAL DEFAULT   14 main
    51: 0000000000004000     4 OBJECT  GLOBAL DEFAULT   24 counter
    52: 0000000000004010     4 OBJECT  GLOBAL DEFAULT   25 uninitialized_var
   ...

כאן רואים שmain היא פונקציה (FUNC) בסקשן מספר 14 (שזה .text), ו-counter הוא אובייקט (OBJECT) בסקשן 24 (שזה .data).

צפייה בסגמנטים - readelf -l

readelf -l ./my_program
Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg
  PHDR           0x000040 0x0000000000000040 0x0000000000000040 0x0002d8 0x0002d8 R
  INTERP         0x000318 0x0000000000000318 0x0000000000000318 0x00001c 0x00001c R
  LOAD           0x000000 0x0000000000000000 0x0000000000000000 0x000628 0x000628 R
  LOAD           0x001000 0x0000000000001000 0x0000000000001000 0x0001f5 0x0001f5 R E
  LOAD           0x002000 0x0000000000002000 0x0000000000002000 0x000160 0x000160 R
  LOAD           0x002db8 0x0000000000003db8 0x0000000000003db8 0x000258 0x000260 RW
  DYNAMIC        0x002dc8 0x0000000000003dc8 0x0000000000003dc8 0x0001f0 0x0001f0 RW
  ...

 Section to Segment mapping:
  Segment Sections...
   00
   01     .interp
   02     .interp .note.gnu.build-id ...
   03     .init .plt .plt.got .text .fini
   04     .rodata .eh_frame_hdr .eh_frame
   05     .init_array .fini_array .dynamic .got .data .bss
   ...

שימו לב לחלק התחתון - section to segment mapping. זה מראה בדיוק אילו סקשנים נמצאים בתוך כל סגמנט. סגמנט 03 (עם הרשאות R E - קריאה והרצה) מכיל את .text, .plt ועוד קטעי קוד.

צפייה ברלוקציות - readelf -r

readelf -r ./my_program
Relocation section '.rela.plt' at offset 0x578 contains 2 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000003fd0  000200000007 R_X86_64_JUMP_SL 0000000000000000 printf@GLIBC_2.2.5 + 0
000000003fd8  000300000007 R_X86_64_JUMP_SL 0000000000000000 puts@GLIBC_2.2.5 + 0

זה מראה שיש רלוקציות עבור printf ו-puts - הdynamic linker צריך לכתוב את הכתובות האמיתיות שלהן בGOT בכתובות 0x3fd0 ו-0x3fd8.

צפייה בסקשן הדינמי - readelf -d

readelf -d ./my_program
Dynamic section at offset 0x2dc8 contains 27 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000000c (INIT)               0x1000
 0x000000000000000d (FINI)               0x11e8
 ...

זה מראה את הספריות שהתוכנית תלויה בהן (NEEDED), כתובות הinit ו-fini, ועוד מידע שהdynamic linker צריך.


שימוש בobjdump - דיסאסמבלי וניתוח

הכלי objdump משלים את readelf ומאפשר בעיקר דיסאסמבלי - הפיכת הוראות מכונה חזרה לאסמבלי.

דיסאסמבלי - objdump -d

objdump -d ./my_program
0000000000001149 <main>:
    1149:  f3 0f 1e fa          endbr64
    114d:  55                   push   %rbp
    114e:  48 89 e5             mov    %rsp,%rbp
    1151:  48 8d 05 ac 0e 00 00 lea    0xeac(%rip),%rax
    1158:  48 89 c7             mov    %rax,%rdi
    115b:  e8 d0 fe ff ff       call   1030 <puts@plt>
    1160:  b8 00 00 00 00       mov    $0x0,%eax
    1165:  5d                   pop    %rbp
    1166:  c3                   ret

זה מראה לנו את הקוד של main בשפת אסמבלי x86-64. שימו לב איך הקריאה ל-puts עוברת דרך הPLT (כפי שלמדנו בפרק 5.10).

טבלת סמלים - objdump -t

objdump -t ./my_program

דומה ל-readelf -s אבל בפורמט קצת שונה. שימושי כשרוצים לראות את כל הסמלים עם הכתובות שלהם.

כל הכותרות - objdump -x

objdump -x ./my_program

מציג את כל הכותרות - ELF header, program headers, section headers, סמלים ורלוקציות. כמו readelf -a אבל בפורמט אחר.


דוגמה מעשית - חקירת תוכנית פשוטה

נכתוב תוכנית פשוטה ונחקור אותה:

#include <stdio.h>

int initialized_global = 42;
int uninitialized_global;
const char *message = "Hello from .rodata";

void helper(void) {
    printf("I am helper\n");
}

int main(void) {
    printf("%s\n", message);
    printf("global = %d\n", initialized_global);
    helper();
    return 0;
}

נקמפל עם מידע דיבוג:

gcc -g -o example example.c

עכשיו נחקור:

איפה main נמצא?

readelf -s example | grep main

    52: 0000000000001169    50 FUNC    GLOBAL DEFAULT   14 main

main נמצא בסקשן 14 (שזה .text) בכתובת 0x1169, ותופס 50 בתים.

איפה המשתנה הגלובלי?

readelf -s example | grep initialized_global

    48: 0000000000004010     4 OBJECT  GLOBAL DEFAULT   24 initialized_global

נמצא בסקשן 24 (שזה .data) - הגיוני, כי נתנו לו ערך התחלתי.

איפה המשתנה הלא מאותחל?

readelf -s example | grep uninitialized_global

    49: 0000000000004020     4 OBJECT  GLOBAL DEFAULT   25 uninitialized_global

נמצא בסקשן 25 (שזה .bss) - בדיוק כמו שציפינו.

איפה המחרוזת?

objdump -s -j .rodata example

Contents of section .rodata:
 2000 01000200 48656c6c 6f206672 6f6d202e  ....Hello from .
 2010 726f6461 74610049 20616d20 68656c70  rodata.I am help
 2020 65720a00 25730a00 676c6f62 616c203d  er..%s..global =
 2030 2025640a 00                          %d..

הנה המחרוזות שלנו ב-.rodata - גם "Hello from .rodata" וגם "I am helper\n" וגם פורמטי הprintf.


רזולוציית סמלים - symbol resolution

כשהלינקר מחבר מספר קבצי אובייקט (.o) לקובץ הרצה אחד, הוא צריך לפתור הפניות - כשקובץ אחד קורא לפונקציה שמוגדרת בקובץ אחר, הלינקר צריך למצוא את הכתובת הנכונה.

ישנם שלושה סוגי סמלים:

סמלים גלובליים - global symbols
פונקציות ומשתנים שמוגדרים בקובץ אחד ונגישים מקבצים אחרים. כל פונקציה "רגילה" שאנחנו כותבים היא סמל גלובלי.

סמלים מקומיים - local symbols
פונקציות ומשתנים שמוגדרים עם static - הם נגישים רק מתוך הקובץ שבו הם מוגדרים. הלינקר לא חושף אותם לקבצים אחרים.

static void internal_helper(void) {   // סמל מקומי - לא נגיש מקבצים אחרים
    // ...
}

void public_function(void) {          // סמל גלובלי - נגיש מכל מקום
    internal_helper();
}

סמלים חלשים - weak symbols
סמלים שאפשר "לדרוס" אותם. אם קובץ מגדיר סמל חלש ומגדיר גם סמל גלובלי עם אותו שם, הלינקר ישתמש בגלובלי. זה שימושי לספריות שרוצות לספק מימוש ברירת מחדל שאפשר להחליף:

__attribute__((weak))
void custom_handler(void) {
    printf("default handler\n");
}

רלוקציות - relocations

כשהקומפיילר מקמפל קובץ מקור לקובץ אובייקט (.o), הוא עדיין לא יודע את הכתובות הסופיות. למשל, אם בקובץ main.c יש קריאה לפונקציה מקובץ utils.c, הקומפיילר לא יודע באיזו כתובת הפונקציה הזו תהיה בקובץ הסופי.

במקום זה, הקומפיילר יוצר רשומת relocation שאומרת: "בoffset הזה בסקשן .text, צריך למלא את הכתובת של הסמל X".

הלינקר עובר על כל הרלוקציות, מוצא את הכתובות הסופיות של כל הסמלים, ו"ממלא את החורים".

נראה דוגמה:

gcc -c -o main.o main.c
readelf -r main.o

Relocation section '.rela.text' at offset 0x2b0 contains 5 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000000000a  000500000002 R_X86_64_PC32    0000000000000000 message - 4
00000000000000014 000b00000004 R_X86_64_PLT32   0000000000000000 printf - 4
...

זה אומר: "בoffset 0xa בסקשן .text, תמלא הפניה לסמל message. ב-offset 0x14, תמלא הפניה לסמל printf."

ברלוקציות דינמיות (שנמצאות ב-.rela.plt), זה עובד קצת אחרת - הdynamic linker ממלא את הכתובות בGOT בזמן ריצה, כפי שלמדנו בפרק 5.10.


הסרת סמלים - strip

הפקודה strip מסירה את טבלת הסמלים (.symtab ו-.strtab) מקובץ ELF:

strip ./my_program

מה נמחק?
- הסקשן .symtab - שמות כל הפונקציות והמשתנים
- הסקשן .strtab - המחרוזות של שמות הסמלים
- סקשני דיבוג (.debug_*)

מה נשאר?
- הסקשנים .dynsym ו-.dynstr - כי הם הכרחיים לdynamic linker בזמן ריצה
- כל שאר הסקשנים שנחוצים להרצה

למה עושים strip?
- להקטין את הקובץ - טבלת סמלים ומידע דיבוג יכולים לתפוס הרבה מקום
- להקשות על הנדסה לאחור - בלי שמות פונקציות, הרבה יותר קשה להבין מה הקוד עושה

# לפני strip
ls -la my_program
-rwxr-xr-x 1 user user 16696 my_program

# אחרי strip
strip my_program
ls -la my_program
-rwxr-xr-x 1 user user 14472 my_program

שימו לב - התוכנית ממשיכה לעבוד בדיוק אותו דבר. הstrip לא משנה את ההתנהגות, רק מסיר מידע שלא נחוץ בזמן ריצה.


הפקודה nm - הצגת סמלים

הפקודה nm היא דרך נוחה לראות את הסמלים בקובץ:

nm ./my_program
0000000000004010 D initialized_global
0000000000004020 B uninitialized_global
0000000000001149 T main
0000000000001139 T helper
                 U printf@GLIBC_2.2.5

האותיות מציינות את סוג הסמל:
- T - סמל בסקשן .text (קוד) - גלובלי
- t - סמל בסקשן .text - מקומי (static)
- D - סמל בסקשן .data (נתונים מאותחלים) - גלובלי
- B - סמל בסקשן .bss (נתונים לא מאותחלים) - גלובלי
- R - סמל בסקשן .rodata (קריאה בלבד) - גלובלי
- U - סמל לא מוגדר (undefined) - מוגדר בספריה חיצונית
- W - סמל חלש (weak)

אם הקובץ עבר strip, הפקודה nm תחזיר שגיאה:

nm ./stripped_program
nm: ./stripped_program: no symbols

במקרה כזה אפשר להשתמש ב:

nm -D ./stripped_program

הדגל -D מציג רק סמלים דינמיים (.dynsym) שנשארים גם אחרי strip.


יישום מעשי - למה כל זה חשוב?

הידע שלמדנו בהרצאה הזו הוא הבסיס לתחומים רבים:

הנדסה לאחור - reverse engineering
כשמנתחים תוכנה שאין לנו את קוד המקור שלה, הדבר הראשון שעושים הוא לבדוק את מבנה הELF - אילו סקשנים יש, אילו סמלים יש (אם לא עשו strip), ואיפה הקוד המעניין נמצא.

פיתוח exploit-ים
הבנת הRLF חיונית לכתיבת exploit-ים - צריך להבין את מיפוי הזיכרון, לדעת איפה הGOT נמצא (כדי לדרוס כתובות), להבין relocation-ים, ועוד.

דיבוג
כלי דיבוג כמו gdb משתמשים במידע מתוך הELF - טבלת הסמלים, מידע הדיבוג, ומידע השורות. הבנת המבנה עוזרת להבין מה gdb מראה לנו ולמה.

בניית כלים
כלים כמו readelf, objdump, nm, strip - כולם פשוט קוראים את מבנה הELF. עכשיו שאנחנו מבינים את המבנה, אנחנו יכולים לבנות כלים משלנו.


סיכום

בהרצאה הזו למדנו:
- קובץ ELF ניתן להסתכל עליו משתי נקודות מבט - סגמנטים (עבור הloader) וסקשנים (עבור הלינקר)
- הסקשנים העיקריים: .text (קוד), .data (נתונים מאותחלים), .bss (נתונים לא מאותחלים), .rodata (קריאה בלבד), .symtab/.strtab (סמלים), .dynsym/.dynstr (סמלים דינמיים), .plt/.got (PLT/GOT), .init/.fini (אתחול וסיום)
- כלי readelf עם הדגלים -h, -S, -s, -l, -r, -d
- כלי objdump עם הדגלים -d, -t, -x
- רזולוציית סמלים - גלובליים, מקומיים, וחלשים
- רלוקציות - איך הלינקר ממלא כתובות
- הפקודה strip - הסרת סמלים
- הפקודה nm - הצגת סמלים מקובץ