לדלג לתוכן

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
}

הוראות קימפול:

gcc -nostdlib -static -O2 -o step1 start.c

מה לבדוק:

./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);
}

הוראות קימפול:

gcc -nostdlib -static -O2 -o step2 puts.c
./step2

פלט צפוי:

Hello, World!
This program runs without libc!

שלב 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. הקוד ההתחלתי מפשט את זה, אבל ייתכן שתצטרכו להתאים אותו.

פלט צפוי:

Squares from our own allocator:
0 1 4 9 16 25 36 49 64 81

שלב 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:

gcc -nostdlib -static -O2 -T freestanding.ld -o step4 alloc.c
./step4

שלב 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 להשוואה:

// hello.c
#include <stdio.h>

int main() {
    printf("Hello, World!\n");
    return 0;
}

הריצו:

make compare

מה לבדוק ולדווח:

  1. גודל הקבצים - מהו היחס בין freestanding לבין hello_static?
  2. מספר הסקשנים בכל קובץ
  3. מספר סגמנטי LOAD
  4. הריצו readelf -a freestanding ובדקו:
  5. מהי נקודת הכניסה?
  6. אילו סקשנים יש?
  7. מה ההרשאות של כל סגמנט?
  8. הריצו objdump -d freestanding ובדקו את האסמבלי. כמה פונקציות יש? האם הכל הגיוני?
  9. הריצו nm freestanding ובדקו את הסמלים - אילו סמלים הגיעו מהlinker script?
  10. נסו לקמפל את freestanding גם עם -O0 ובדקו את ההבדל בגודל ובאסמבלי.

בונוס

אם סיימתם את כל חמשת השלבים, הנה כמה אתגרים נוספים:

  1. הוסיפו פונקציית my_memcpy - העתקת זיכרון בלי libc.
  2. הוסיפו טיפול בארגומנטים של שורת הפקודה - ב-_start, המחסנית מכילה את argc, argv, ו-envp. חלצו אותם.
  3. כתבו my_itoa שממיר מספר למחרוזת (בכל בסיס: 2, 8, 10, 16).
  4. הוסיפו my_free לallocator - שמור רשימה של חתיכות פנויות (free list).
  5. כתבו גרסת assembly טהורה של _start בקובץ .s נפרד, ושלבו עם קוד C.