סביבת ריצה מינימלית ואסמבלי משולב¶
אנחנו רגילים לכך ש־main מקבלת עולם מוכן: מחסנית תקינה, שירותי הקצאה, ספריית קלט ופלט ומדיניות פאניקה. כדי להבין מה הגיע מהשפה ומה הגיע מסביבת הריצה, נבנה תוכנית שמתחילה קרוב יותר לנקודת הכניסה של התהליך. הדוגמה מיועדת במפורש ללינוקס על x86-64, ואינה מעטפת ניידת לקריאות מערכת.
התכונה no_std מסירה את הקישור האוטומטי לספריית std, אבל משאירה את core. אפשר עדיין להשתמש במבנים, התאמת תבניות, איטרטורים ובטיפוסים שאינם דורשים שירותי מערכת. הספרייה alloc מוסיפה מבני נתונים שמקצים זיכרון, אבל דורשת מקצה שהסביבה מספקת. בחירת no_std אינה יוצרת בעצמה heap, scheduler או מנהל התקנים.
שמרו את התוכנית המלאה הבאה בקובץ minimal.rs. קוד הכניסה באסמבלי מיישר את המחסנית לפני קריאה לפונקציה בעלת ABI רגיל, משום שהקרנל נכנס לנקודת הכניסה בלי הוראת call רגילה.
#![no_std]
#![no_main]
use core::arch::{asm, global_asm};
use core::panic::PanicInfo;
global_asm!(
".global _start",
"_start:",
"and rsp, -16",
"call rust_start",
"ud2",
);
fn exit(code: usize) -> ! {
unsafe { asm!("syscall", in("rax") 60usize, in("rdi") code,
options(noreturn)); }
}
fn write_once(bytes: &[u8]) -> isize {
let result: isize;
unsafe {
asm!("syscall", inlateout("rax") 1usize => result,
in("rdi") 1usize, in("rsi") bytes.as_ptr(),
in("rdx") bytes.len(), lateout("rcx") _, lateout("r11") _,
options(nostack));
}
result
}
#[unsafe(no_mangle)]
extern "C" fn rust_start() -> ! {
let message = b"minimal runtime\n";
let count = write_once(message);
exit(if count == message.len() as isize { 0 } else { 1 });
}
#[panic_handler]
fn panic(_: &PanicInfo<'_>) -> ! { exit(101) }
בנו בסביבת לינוקס של x86-64 עם linker זמין. ההגדרה -nostartfiles מונעת הכנסת קוד כניסה אחר שמגדיר בעצמו _start.
rustc --edition=2024 -C panic=abort -C link-arg=-nostartfiles \
-C link-arg=-no-pie minimal.rs -o minimal
./minimal
readelf -h -l minimal
strace ./minimal
המספרים 1 ו־60 הם מספרי קריאות מערכת של הABI המסוים הזה. הם אינם מספרי פונקציות כלליים של ראסט ואינם מתאימים אוטומטית ל־ARM64. החזרה מ־syscall נמצאת ב־rax, ושגיאות גולמיות מוחזרות כערכים שליליים. העטיפה libc בדרך כלל ממירה את המוסכמה הזאת לתוצאת כשל ולעדכון errno, אבל כאן אין עטיפה כזאת.
ההכרזה על rcx ו־r11 חשובה משום שהוראת syscall משנה אותם. המהדר אינו מנתח את מחרוזת האסמבלי כדי להסיק את כל תופעות הלוואי. אנחנו צריכים לתאר אילו רגיסטרים הם קלט, פלט או נדרסים. השמטת פלט עשויה לגרום לכך שגרסת debug תיראה תקינה וגרסה ממוטבת תשתמש בערך שהוראת האסמבלי כבר הרסה.
שימו לב שלא השתמשנו ב־nomem. פעולת הכתיבה קוראת את הזיכרון שאליו מצביע הבאפר, ולכן ההבטחה שאינה נוגעת בזיכרון תהיה שקרית. גם preserves_flags או pure אינן קישוטים לביצועים, אלא הבטחות שהאופטימייזר רשאי לנצל. ההערה nostack מתייחסת לשימוש שהאסמבלי עצמו עושה במחסנית המשתמש, ואינה מבטיחה שהקרנל אינו משתמש במחסנית משלו.
הדוגמה מבצעת ניסיון כתיבה אחד ובודקת אם כל ההודעה נכתבה. היא אינה פתרון מלא לכתיבה חלקית או להפרעת אות, ואינה מריצה destructors לפני exit. בפרק הבא נבנה חוזי שגיאה ולולאות התקדמות שיכסו את המקרים האלה. כאן המטרה היא לחשוף את השירותים החסרים, ולא להחליף את ספריית התקן בכמה שורות אסמבלי.
הכניסה המינימלית גם אינה מאתחלת את כל מה שספריית זמן ריצה רגילה עשויה להכין, כמו TLS של ספריות חיצוניות. לכן לא נוסיף לפתע פונקציה של libc ונניח שהיא בטוחה רק מפני שהלינקר מצא אותה. כל הרחבה מחייבת בדיקה של תנאי האתחול שלה.
החוזים של operands ושל אפשרויות האסמבלי מופיעים בתיעוד האסמבלי המשולב של ראסט. קראו אותם כהתחייבויות כלפי המהדר, באותה רצינות שבה קראנו תנאי קדם של פונקציה unsafe.