9.5 פרויקט סיכום פרויקט
פרויקט סיכום - בניית תוכנית freestanding - building a freestanding program¶
הקדמה¶
בפרויקט הזה נבנה תוכנית מינימלית שרצה ישירות על לינוקס בלי libc - בלי printf, בלי malloc, בלי _start של ספריית C. הכל בידיים שלנו.
הפרויקט מחבר את כל מה שלמדנו בפרק 9:
- שלבי הקומפילציה (פרק 9.1) - נקמפל עם דגלים מיוחדים ונראה כל שלב
- הלינקר (פרק 9.2) - נחבר בלי ספריות סטנדרטיות
- סקריפטים של הלינקר (פרק 9.3) - נכתוב linker script משלנו
- אופטימיזציות (פרק 9.4) - נשווה בין רמות אופטימיזציה
וגם מפרקים קודמים:
- אסמבלי (פרק 1-2) - נשתמש ב-inline assembly לsyscalls
- שפת C (פרק 3) - הבסיס
- לינוקס (פרק 5) - syscalls ישירות
- פורמט ELF (פרק 5.11) - נבדוק את הבינארי שנוצר
הפרויקט בנוי בחמישה שלבים. כל שלב מוסיף יכולת חדשה. מומלץ לבנות שלב אחרי שלב.
שלב 1 - נקודת כניסה ויציאה¶
המטרה: כתבו פונקציית _start שמסיימת את התוכנית עם exit code 0, בלי libc.
קוד התחלתי:
// start.c
// exit syscall: syscall number 60, argument = exit code
static void my_exit(int code) __attribute__((noreturn));
static void my_exit(int code) {
__asm__ volatile (
"syscall"
: // no outputs
: "a" (60), // rax = 60 (exit syscall)
"D" (code) // rdi = exit code
: /* no clobbers needed - we never return */
);
__builtin_unreachable();
}
void _start(void) __attribute__((noreturn));
void _start(void) {
// TODO: קראו ל-my_exit עם exit code 0
}
הוראות קימפול:
מה לבדוק:
./step1
echo $? # צריך להדפיס 0
# בדקו את נקודת הכניסה
readelf -h step1 | grep Entry
nm step1 | grep _start
# בדקו את הגודל
ls -la step1
שלב 2 - הדפסה בלי printf¶
המטרה: כתבו פונקציית my_puts שמדפיסה מחרוזת ל-stdout באמצעות syscall write.
קוד התחלתי:
// puts.c
// ... כאן הדביקו את my_exit מהשלב הקודם ...
// write syscall: number 1, args = fd, buf, count
static long my_write(int fd, const void *buf, long count) {
long ret;
__asm__ volatile (
"syscall"
: "=a" (ret) // rax = return value
: "a" (1), // rax = 1 (write syscall)
"D" (fd), // rdi = file descriptor
"S" (buf), // rsi = buffer
"d" (count) // rdx = count
: "rcx", "r11", "memory"
);
return ret;
}
// TODO: כתבו פונקציית my_strlen שמחשבת אורך מחרוזת
static int my_strlen(const char *s) {
// ...
}
// TODO: כתבו פונקציית my_puts שמדפיסה מחרוזת עם newline
static void my_puts(const char *s) {
// רמז: השתמשו ב-my_write עם fd=1 (stdout)
// ...
}
void _start(void) __attribute__((noreturn));
void _start(void) {
my_puts("Hello, World!");
my_puts("This program runs without libc!");
my_exit(0);
}
הוראות קימפול:
פלט צפוי:
שלב 3 - הקצאת זכרון בלי malloc¶
המטרה: כתבו bump allocator פשוט שמשתמש ב-mmap syscall להקצאת זיכרון.
רקע: bump allocator הוא המקצה הפשוט ביותר - הוא מחזיק מצביע לסוף הזיכרון המוקצה, וכל הקצאה פשוט מקדמת את המצביע. אין free - הזיכרון לא משתחרר (זה מספיק לתוכנית הפשוטה שלנו).
קוד התחלתי:
// alloc.c
// ... כאן הדביקו את my_exit, my_write, my_strlen, my_puts מהשלבים הקודמים ...
// mmap syscall: number 9
// void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset)
static void *my_mmap(long size) {
void *ret;
__asm__ volatile (
"syscall"
: "=a" (ret)
: "a" (9), // rax = 9 (mmap)
"D" (0), // rdi = addr (0 = let kernel choose)
"S" (size), // rsi = length
"d" (3), // rdx = prot (PROT_READ | PROT_WRITE = 3)
"r" ((long)0x22) // r10 = flags (MAP_PRIVATE | MAP_ANONYMOUS = 0x22)
// r8 = fd (-1), r9 = offset (0) - default 0
: "rcx", "r11", "memory"
);
return ret;
}
// TODO: כתבו bump allocator
// שימו לב: צריך לטפל ב-r10 בצורה מיוחדת בsyscall (ראו הסבר למטה)
#define HEAP_SIZE (4096 * 16) // 64KB
static char *heap_base = 0;
static char *heap_current = 0;
static char *heap_end = 0;
static void heap_init(void) {
heap_base = (char *)my_mmap(HEAP_SIZE);
heap_current = heap_base;
heap_end = heap_base + HEAP_SIZE;
}
// TODO: כתבו פונקציית my_malloc
static void *my_malloc(long size) {
// אם הheap לא אותחל, אתחלו אותו
// בדקו שיש מספיק מקום
// קדמו את heap_current והחזירו מצביע
// ...
}
// TODO: כתבו פונקציה שממירה מספר למחרוזת (בבסיס 10)
static void print_number(long n) {
// רמז: המירו ספרה אחרי ספרה מהסוף להתחלה
// ...
}
void _start(void) __attribute__((noreturn));
void _start(void) {
// הדגמה של הallocator
int *arr = (int *)my_malloc(10 * sizeof(int));
// מילוי המערך
for (int i = 0; i < 10; i++) {
arr[i] = i * i;
}
// הדפסה
my_puts("Squares from our own allocator:");
for (int i = 0; i < 10; i++) {
print_number(arr[i]);
my_write(1, " ", 1);
}
my_write(1, "\n", 1);
my_exit(0);
}
הערה חשובה על mmap syscall: הsyscall של mmap דורש 6 ארגומנטים. ב-System V AMD64 ABI, ארגומנטים 1-6 עוברים ברגיסטרים rdi, rsi, rdx, r10, r8, r9. שימו לב שהארגומנט הרביעי עובר ב-r10 (ולא rcx כמו ב-calling convention רגילה של C). צריך לטפל ב-r10 עם constraint "r" ולהשתמש ב-mov ידני, או להשתמש ב-extended inline assembly. הקוד ההתחלתי מפשט את זה, אבל ייתכן שתצטרכו להתאים אותו.
פלט צפוי:
שלב 4 - linker script מותאם¶
המטרה: כתבו linker script שמגדיר את הפריסה של התוכנית, כולל סמלים שנגישים מהקוד.
צרו את הקובץ freestanding.ld:
/* freestanding.ld */
ENTRY(_start)
SECTIONS {
. = 0x400000;
.text : {
_text_start = .;
*(.text)
*(.text.*)
_text_end = .;
}
.rodata : {
_rodata_start = .;
*(.rodata)
*(.rodata.*)
_rodata_end = .;
}
. = ALIGN(4096);
.data : {
_data_start = .;
*(.data)
*(.data.*)
_data_end = .;
}
.bss : {
_bss_start = .;
*(.bss)
*(.bss.*)
*(COMMON)
_bss_end = .;
}
_program_end = .;
}
הוסיפו לקוד (לפני _start) פונקציה שמדפיסה את פריסת הזיכרון:
extern char _text_start, _text_end;
extern char _rodata_start, _rodata_end;
extern char _data_start, _data_end;
extern char _bss_start, _bss_end;
extern char _program_end;
// TODO: כתבו פונקציה print_hex שמדפיסה כתובת בהקסדצימלי
static void print_hex(unsigned long value) {
// ...
}
static void print_layout(void) {
my_puts("=== Memory Layout ===");
my_write(1, " .text: ", 11);
print_hex((unsigned long)&_text_start);
my_write(1, " - ", 3);
print_hex((unsigned long)&_text_end);
my_write(1, "\n", 1);
my_write(1, " .rodata: ", 11);
print_hex((unsigned long)&_rodata_start);
my_write(1, " - ", 3);
print_hex((unsigned long)&_rodata_end);
my_write(1, "\n", 1);
my_write(1, " .data: ", 11);
print_hex((unsigned long)&_data_start);
my_write(1, " - ", 3);
print_hex((unsigned long)&_data_end);
my_write(1, "\n", 1);
my_write(1, " .bss: ", 11);
print_hex((unsigned long)&_bss_start);
my_write(1, " - ", 3);
print_hex((unsigned long)&_bss_end);
my_write(1, "\n", 1);
}
קימפול עם הlinker script:
שלב 5 - בדיקת הבינארי¶
המטרה: בדקו את הבינארי שיצרתם עם כלי ניתוח, והשוו אותו ל-hello world רגיל.
צרו Makefile:
CC = gcc
CFLAGS = -nostdlib -static -O2 -Wall
LDSCRIPT = freestanding.ld
.PHONY: all clean compare
all: freestanding hello_static hello_dynamic
freestanding: alloc.c $(LDSCRIPT)
$(CC) $(CFLAGS) -T $(LDSCRIPT) -o $@ $<
hello_static: hello.c
gcc -static -O2 -o $@ $<
hello_dynamic: hello.c
gcc -O2 -o $@ $<
compare: all
@echo "=== File sizes ==="
@ls -la freestanding hello_static hello_dynamic
@echo ""
@echo "=== Section sizes ==="
@echo "Freestanding:"
@size freestanding
@echo "Static hello:"
@size hello_static
@echo "Dynamic hello:"
@size hello_dynamic
@echo ""
@echo "=== Number of sections ==="
@echo -n "Freestanding: "
@readelf -S freestanding | grep -c '\['
@echo -n "Static hello: "
@readelf -S hello_static | grep -c '\['
@echo -n "Dynamic hello: "
@readelf -S hello_dynamic | grep -c '\['
@echo ""
@echo "=== Number of LOAD segments ==="
@echo -n "Freestanding: "
@readelf -l freestanding | grep -c LOAD
@echo -n "Static hello: "
@readelf -l hello_static | grep -c LOAD
@echo -n "Dynamic hello: "
@readelf -l hello_dynamic | grep -c LOAD
clean:
rm -f freestanding hello_static hello_dynamic *.o
צרו hello.c להשוואה:
הריצו:
מה לבדוק ולדווח:
- גודל הקבצים - מהו היחס בין freestanding לבין hello_static?
- מספר הסקשנים בכל קובץ
- מספר סגמנטי LOAD
- הריצו
readelf -a freestandingובדקו: - מהי נקודת הכניסה?
- אילו סקשנים יש?
- מה ההרשאות של כל סגמנט?
- הריצו
objdump -d freestandingובדקו את האסמבלי. כמה פונקציות יש? האם הכל הגיוני? - הריצו
nm freestandingובדקו את הסמלים - אילו סמלים הגיעו מהlinker script? - נסו לקמפל את freestanding גם עם
-O0ובדקו את ההבדל בגודל ובאסמבלי.
בונוס¶
אם סיימתם את כל חמשת השלבים, הנה כמה אתגרים נוספים:
- הוסיפו פונקציית my_memcpy - העתקת זיכרון בלי libc.
- הוסיפו טיפול בארגומנטים של שורת הפקודה - ב-
_start, המחסנית מכילה את argc, argv, ו-envp. חלצו אותם. - כתבו my_itoa שממיר מספר למחרוזת (בכל בסיס: 2, 8, 10, 16).
- הוסיפו my_free לallocator - שמור רשימה של חתיכות פנויות (free list).
- כתבו גרסת assembly טהורה של
_startבקובץ.sנפרד, ושלבו עם קוד C.