לדלג לתוכן

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) צריך לדעת בדיוק מה כל חלק בקובץ מכיל - איפה הקוד, איפה הנתונים, איפה טבלת הsymbols, איפה מידע הrelocation. לשם כך הוא משתמש בסקשנים (sections) שמוגדרים ב-section header table.

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

+------------------+
|    ELF Header    |
+------------------+
| Program Headers  |  <-- the kernel uses this (segments)
|   (segments)     |
+------------------+
|                  |
|  file content    |
|  (code, data,    |
|   symbols...)   |
|                  |
+------------------+
| Section Headers  |  <-- the linker uses this (sections)
|   (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;           // lives in .data
static int flag = 1;        // lives in .data

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

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

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

int buffer[1000];           // lives in .bss
static int count;           // lives in .bss

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

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

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

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

const int MAX = 100;                    // lives in .rodata
printf("Hello, World!\n");              // the string "Hello, World!\n" lives in .rodata

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

סקשן symtab. - טבלת symbols

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

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

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

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

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

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

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

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

ההבדל הקריטי: כשעושים 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 - זה אומר שהוא לא תופס מקום בקובץ.

צפייה בsymbols - 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).

טבלת symbols - objdump -t

objdump -t ./my_program

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

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

objdump -x ./my_program

מציג את כל הכותרות - ELF header, program headers, section headers, symbols ורלוקציות. כמו 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.


רזולוציית symbols - symbol resolution

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

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

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

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

static void internal_helper(void) {   // local symbol - not accessible from other files
    // ...
}

void public_function(void) {          // global symbol - accessible from anywhere
    internal_helper();
}

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

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

רלוקציות - relocations

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

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

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

נראה דוגמה:

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, תמלא הפניה לsymbol message. ב-offset 0x14, תמלא הפניה לsymbol printf."

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


הסרת symbols - strip

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

strip ./my_program

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

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

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

# before strip
ls -la my_program
-rwxr-xr-x 1 user user 16696 my_program

# after strip
strip my_program
ls -la my_program
-rwxr-xr-x 1 user user 14472 my_program

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


הפקודה nm - הצגת symbols

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

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

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

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

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

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

nm -D ./stripped_program

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


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

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

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

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

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

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


סיכום

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