5.11 פורמט ELF לעומק פתרון
פתרון - פורמט ELF לעומק¶
פתרון תרגיל 1 - זיהוי סקשנים¶
נכתוב את התוכנית:
#include <stdio.h>
int initialized_var = 100; // משתנה גלובלי מאותחל
int uninitialized_var; // משתנה גלובלי לא מאותחל
void greet(void) { // פונקציה נוספת
printf("Hello!\n"); // מחרוזת קבועה
}
int main(void) {
greet();
printf("value = %d\n", initialized_var);
return 0;
}
נקמפל:
נמצא את הסקשנים:
פלט (חלקי):
[Nr] Name Type Address Offset Size Flags
...
[14] .text PROGBITS 0000000000001060 00001060 0000000000000185 AX
[16] .rodata PROGBITS 0000000000002000 00002000 0000000000000020 A
[24] .data PROGBITS 0000000000004000 00003000 0000000000000014 WA
[25] .bss NOBITS 0000000000004014 00003014 0000000000000004 WA
...
עכשיו נבדוק את הסמלים:
פלט:
48: 0000000000004010 4 OBJECT GLOBAL DEFAULT 24 initialized_var
49: 0000000000004014 4 OBJECT GLOBAL DEFAULT 25 uninitialized_var
50: 0000000000001149 26 FUNC GLOBAL DEFAULT 14 greet
51: 0000000000001163 38 FUNC GLOBAL DEFAULT 14 main
סיכום:
| אלמנט | סקשן | הסבר |
|--------|-------|-------|
| initialized_var (ערך 100) | סקשן 24 - .data | משתנה גלובלי עם ערך התחלתי |
| uninitialized_var | סקשן 25 - .bss | משתנה גלובלי ללא ערך התחלתי |
| מחרוזת "Hello!\n" | .rodata (סקשן 16) | מחרוזות קבועות נשמרות ב-rodata |
| פונקציה greet | סקשן 14 - .text | קוד בר-הרצה |
| פונקציה main | סקשן 14 - .text | קוד בר-הרצה |
אפשר גם לוודא שהמחרוזת נמצאת ב-.rodata:
Contents of section .rodata:
2000 01000200 48656c6c 6f210a00 76616c75 ....Hello!..valu
2010 65203d20 25640a00 e = %d..
אכן, המחרוזות "Hello!\n" ו-"value = %d\n" נמצאות בסקשן .rodata.
פתרון תרגיל 2 - כתובת main ודיסאסמבלי¶
א. מציאת כתובת main:
הכתובת של main היא
0x1163.
ב. דיסאסמבלי:
0000000000001163 <main>:
1163: f3 0f 1e fa endbr64
1167: 55 push %rbp
1168: 48 89 e5 mov %rsp,%rbp
116b: b8 00 00 00 00 mov $0x0,%eax
1170: e8 d4 ff ff ff call 1149 <greet>
1175: 8b 05 95 2e 00 00 mov 0x2e95(%rip),%eax
117b: 89 c6 mov %eax,%esi
117d: 48 8d 05 8a 0e 00 00 lea 0xe8a(%rip),%rax
1184: 48 89 c7 mov %rax,%rdi
1187: b8 00 00 00 00 mov $0x0,%eax
118c: e8 9f fe ff ff call 1030 <printf@plt>
1191: b8 00 00 00 00 mov $0x0,%eax
1196: 5d pop %rbp
1197: c3 ret
ג. הכתובת תואמת - גם readelf וגם objdump מראים שmain מתחיל ב-0x1163.
הסבר השורות:
- endbr64 - הוראת אבטחה (CET/IBT) - מסמנת שזו נקודת כניסה חוקית לקפיצה
- push %rbp / mov %rsp,%rbp - פרולוג הפונקציה, שמירת הstack frame (כזכור מפרק 3 על שפת C)
- call 1149 <greet> - קריאה לפונקציה greet
- mov 0x2e95(%rip),%eax - קריאת הערך של initialized_var (הכתובת מחושבת יחסית ל-RIP)
- mov %eax,%esi - הכנת הארגומנט השני ל-printf (הערך של initialized_var)
- lea 0xe8a(%rip),%rax / mov %rax,%rdi - הכנת הארגומנט הראשון (כתובת המחרוזת "value = %d\n")
- call 1030 <printf@plt> - קריאה ל-printf דרך PLT
- mov $0x0,%eax - ערך החזרה 0 (return 0)
- pop %rbp / ret - אפילוג הפונקציה, חזרה לקורא
פתרון תרגיל 3 - השוואה בין קומפילציה סטטית לדינמית¶
נקמפל את אותה תוכנית בשתי דרכים:
א. מספר סקשנים:
לגרסה הדינמית יש 31 סקשנים, לגרסה הסטטית יש 33.
ב. סקשנים שמופיעים רק בגרסה הדינמית:
readelf -S dynamic_prog | grep -E "\.dynsym|\.dynstr|\.plt|\.got|\.rela\.plt|\.rela\.dyn|\.interp|\.dynamic"
[ 1] .interp PROGBITS ...
[ 6] .dynsym DYNSYM ...
[ 7] .dynstr STRTAB ...
[10] .rela.dyn RELA ...
[11] .rela.plt RELA ...
[13] .plt PROGBITS ...
[23] .dynamic DYNAMIC ...
[24] .got PROGBITS ...
הסקשנים .interp, .dynsym, .dynstr, .plt, .got, .rela.plt, .rela.dyn, .dynamic קיימים רק בגרסה הדינמית. הסיבה: הסקשנים האלה נחוצים לתהליך הלינקור הדינמי - הם מכילים את המידע שהdynamic linker צריך כדי לטעון ספריות משותפות ולפתור כתובות בזמן ריצה. בגרסה הסטטית כל הקוד כבר מקובע בתוך הקובץ ואין צורך בלינקור דינמי.
ג. השוואת גדלים:
הגרסה הסטטית גדולה בהרבה (כ-880KB מול 17KB) כי היא מכילה את כל קוד הlibc בתוך הקובץ עצמו, במקום להסתמך על ספריה חיצונית.
ד. בדיקה עם ldd:
linux-vdso.so.1 (0x00007ffff7fc3000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffff7c00000)
/lib64/ld-linux-x86-64.so.2 (0x00007ffff7fc5000)
הגרסה הדינמית תלויה ב-libc.so.6 ובdynamic linker. הגרסה הסטטית לא תלויה בשום ספריה חיצונית.
פתרון תרגיל 4 - רלוקציות ו-printf¶
נניח שיש לנו תוכנית שמשתמשת ב-printf ומקומפלת דינמית.
א+ב. מציאת הrelocation של printf:
ג. הסבר השדות:
| שדה | ערך | הסבר |
|---|---|---|
| Offset | 0x3fd0 |
הכתובת בGOT שבה צריך לכתוב את הכתובת האמיתית של printf |
| Info | 000200000007 |
מכיל את אינדקס הסמל (0x2) ואת סוג הrelocation (0x7) |
| Type | R_X86_64_JUMP_SLOT |
סוג הrelocation - "jump slot" - זה אומר שצריך למלא כתובת בGOT עבור PLT |
| Sym. Value | 0x0 |
הכתובת של הסמל עדיין לא ידועה (תימצא בזמן ריצה) |
| Sym. Name | printf@GLIBC_2.2.5 |
שם הסמל שצריך למצוא - printf מגרסת GLIBC 2.2.5 |
ד. הסבר במילים: רשומת הrelocation הזו אומרת לdynamic linker: "כשתטען את התוכנית, מצא את הכתובת של הפונקציה printf בספריית libc, וכתוב את הכתובת הזו לתוך הGOT בoffset 0x3fd0. ככה כשהתוכנית תקפוץ דרך הPLT לprintf, הPLT ייקח את הכתובת מהGOT ויקפוץ אליה."
ה. בדיקה מול הGOT:
הGOT מתחיל בכתובת 0x3fb8 ותופס 0x48 בתים, כלומר מסתיים ב-0x4000.
הoffset של הrelocation הוא 0x3fd0, שנופל בתוך הטווח 0x3fb8 עד 0x4000 - אכן מדובר בכניסה בGOT.
פתרון תרגיל 5 - פונקציות constructor ו-destructor¶
הקוד:
#include <stdio.h>
__attribute__((constructor))
void my_init(void) {
printf("constructor: before main\n");
}
__attribute__((destructor))
void my_cleanup(void) {
printf("destructor: after main\n");
}
int main(void) {
printf("main: running\n");
return 0;
}
א. קומפילציה והרצה:
סדר ההרצה: constructor קודם, אחריו main, ואחרון destructor - בדיוק כמו שציפינו.
ב. מציאת הסקשנים:
[19] .init_array INIT_ARRAY 0000000000003db8 00002db8 0000000000000010 08 WA 0 0 8
[20] .fini_array FINI_ARRAY 0000000000003dc8 00002dc8 0000000000000010 08 WA 0 0 8
הסקשן .init_array נמצא בכתובת 0x3db8 ותופס 0x10 בתים (16 בתים = 2 כתובות של 8 בתים כל אחת).
הסקשן .fini_array נמצא בכתובת 0x3dc8 ותופס 0x10 בתים.
ג. צפייה בתוכן הסקשן:
אנחנו רואים שתי כתובות (בlittle endian):
- 0x1100 - זו כתובת הפונקציה frame_dummy (פונקציה פנימית של GCC)
- 0x1149 - זו הכתובת של הconstructor שלנו
ד. אימות הכתובת:
הכתובת של my_init היא 0x1149, שזה בדיוק מה שראינו בתוכן של .init_array. הruntime של C עובר על הכתובות ב-.init_array וקורא לכל פונקציה לפני שהוא קורא ל-main. באופן דומה, אחרי שmain חוזר, הוא עובר על .fini_array וקורא לפונקציות שם - כולל my_cleanup.