לדלג לתוכן

פורמט ELF מוסבר לעומק - איך קובץ הרצה בלינוקס בנוי מבפנים

כל קובץ הרצה בלינוקס, כל ספרייה משותפת, וגם קבצי הליבה שנוצרים כשתוכנית קורסת, בנויים לפי אותו פורמט - ELF, ראשי תיבות של Executable and Linkable Format. זה לא רק "קובץ בינארי" סתמי - זה מבנה מדויק ומתועד, שהקרנל יודע לקרוא בזכות סדר קבוע של כותרות ומקטעים. מי שמבין את המבנה הזה, יכול לפתוח כל תוכנית עם עורך הקסדצימלי ולהבין בדיוק מה הוא רואה.

הכותרת שפותחת את הכל

כל קובץ ELF מתחיל בכותרת ELF - ELF header, שהיא הדבר הראשון שהקרנל בודק כשהוא מנסה להריץ קובץ. ארבעת הבתים הראשונים הם חתימה קבועה, 0x7F ואחריה האותיות ELF, שמאשרת מייד שזה בכלל קובץ מהפורמט הזה. אחרי החתימה, הכותרת מכילה מידע בסיסי כמו האם מדובר בקובץ 32 סיביות או 64, לאיזו ארכיטקטורה הוא מיועד, x86 מול ARM למשל, ומה כתובת הכניסה - entry point, שבה התוכנית מתחילה לרוץ בפועל.

שני מבטים שונים על אותו קובץ - segments מול sections

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

Program headers, שמתארים מקטעים - segments. אלה מיועדים לקרנל, בזמן טעינת התוכנית להרצה. כל segment מתאר בלוק זיכרון שצריך להיטען, לאיזו כתובת, ובאילו הרשאות - לקריאה, לכתיבה, או להרצה. הקרנל לא מתעניין בפרטים העדינים של הקוד, הוא רק צריך לדעת אילו בלוקים למפות ולאן, וזה בדיוק מה ש-program headers נותנים לו.

Section headers, שמתארים קטעים - sections. אלה מיועדים בעיקר לכלים כמו לינקר ודיבאגר, לא לקרנל בזמן ריצה. הם מפרקים את הקובץ לקטגוריות מפורטות יותר - .text לקוד עצמו, .data למשתנים גלובליים מאותחלים, .bss למשתנים גלובליים שלא אותחלו, .symtab לטבלת סמלים עם שמות פונקציות ומשתנים, ועוד רבים נוספים. כשמריצים דיבאגר ורוצים לראות שם של פונקציה ולא רק כתובת גולמית, המידע הזה מגיע בדיוק מ-section headers.

בפועל, כמה sections שונים בדרך כלל מתקבצים יחד לתוך אותו segment, כי הקרנל בזמן טעינה לא צריך להבדיל ביניהם ברמת הפירוט הזאת - הוא רק צריך לדעת שכל הבלוק הזה נטען לכתובת מסוימת עם הרשאות מסוימות.

סוגי קבצי ELF שונים

לא כל קובץ ELF הוא תוכנית הרצה עצמאית. יש כמה סוגים עיקריים - קובץ הרצה - executable, שרץ ישירות כתהליך משלו, קובץ אובייקט משותף - shared object, שהוא בעצם ספרייה משותפת שדיברנו עליה קודם, קובץ ניתן להעברה - relocatable, שהוא תוצר ביניים לפני שהלינקר מחבר הכל לקובץ סופי, וקובץ ליבה - core dump, שנוצר כשתוכנית קורסת ומכיל תמונת מצב מלאה של הזיכרון והרגיסטרים שלה ברגע הקריסה.

איך חוקרים קובץ ELF בעצמכם

הכלי הכי נפוץ לבדיקת קבצי ELF הוא readelf, שמאפשר להציג בדיוק את כל מה שדיברנו עליו - readelf -h מציג את הכותרת הראשית, readelf -l מציג את ה-program headers, ו-readelf -S מציג את ה-section headers. כלי נוסף, objdump, מאפשר להציג פירוק הוראות אסמבלי ישירות מהקטע .text. שני הכלים האלה, יחד עם הבנת המבנה, הופכים כל קובץ בינארי מקופסה שחורה למסמך קריא לגמרי.

למה כדאי להכיר את הפורמט לעומק

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

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

ואם ניסיתם לפרק ELF בעצמכם והתבלבלתם בדרך, יש קהילה שכבר עברה את זה.

הצטרפו לקהילה בדיסקורד

לסיכום

ELF הוא הפורמט שמאחורי כל קובץ הרצה בלינוקס, בנוי מכותרת קבועה, program headers שמכוונים את הקרנל בזמן טעינה, ו-section headers שמפרטים את התוכן עבור כלים כמו לינקר ודיבאגר. ברגע שמבינים את ההפרדה בין השניים, גם קריאה ישירה של קובץ בינארי עם readelf הופכת ממשימה מפחידה לתרגיל מסודר וברור.