העברת מערכת ליעד חדש וחקירתה¶
הצלחה במחשב אחד היא תחילת החקירה¶
תוכנית יכולה לעבוד במשך שנים על ארכיטקטורה אחת ולחשוף שגיאה מיד לאחר מעבר. לפעמים חסרה ספרייה, לפעמים פורמט בינארי מניח רוחב מצביע, ולפעמים מירוץ שהוסתר על ידי סדר זיכרון חזק יחסית נעשה גלוי. כדי להעביר מערכת היטב נפריד בין בעיית בנייה, בעיית אתחול, בעיית ממשק ובעיית הנחות במימוש.
נבחר יעד מעבדה נוסף של ארם בן שישים וארבע סיביות ונשתמש במכונה הווירטואלית הכללית שמספקת סביבת ההדמיה. אין מדובר בהדמיה של כל לוח מסחרי. בחירת מכונה קובעת התקנים, כתובות ומנגנון אתחול, ולכן גם קובץ קרנל נכון לארכיטקטורה אינו מספיק אם נבחרה מכונה אחרת מזו שהוא תומך בה.
תיאור חומרה והקוד שמפרש אותו¶
בעץ התקנים נשמר תיאור של רכיבי חומרה וקשרים כמו אזורי כתובות, פסיקות, שעונים ותלויות. הקרנל מתאים דרייברים לתיאורים הנתמכים. התיאור אינו דרייבר ואינו מוכיח שהחומרה באמת מתנהגת כפי שתוארה. טעות בכתובת או בקו פסיקה יכולה לגרום לתקלה שנראית כמו באג בקוד ההתקן.
במנגנון ACPI קיימות טבלאות וממשקי קושחה בעלי מודל אחר, כולל תיאור והפעלה של פעולות מסוימות. שתי הגישות קיימות גם מחוץ לחלוקה פשטנית של מחשב אישי מול לוח קטן. נזהה מה הקרנל במערכת שלנו קיבל בפועל, ונבדוק את מסלול ההתאמה של הדרייבר המתאים.
נוכל לבקש מסביבת ההדמיה לכתוב את עץ ההתקנים שהיא מייצרת עבור המכונה הכללית. לאחר מכן נפענח אותו לטקסט ונבחן זיכרון, מעבדים והתקני פלטפורמה.
qemu-system-aarch64 -machine virt,dumpdtb=virt.dtb -cpu cortex-a57 -m 512M
dtc -I dtb -O dts virt.dtb -o virt.dts
התיאור תלוי בגרסת סביבת ההדמיה ובאפשרויות המכונה. נשמור אותן יחד עם הקובץ. אם נדרש שחזור ארוך טווח, נבחר גם גרסת מכונה נתמכת מפורשת במקום להניח שהשם הכללי יישאר זהה בכל גרסה.
בנייה צולבת אינה הרצה צולבת¶
נוסיף יעד לראסט ונבנה תוכנית קטנה באמצעות כלי קישור של היעד. הדוגמה הבאה משתמשת בספרייה הדינמית של יעד ההפצה, ולכן להרצת הדמיית משתמש נדרש שורש הספריות המתאים. הנתיב בדוגמה מתאים להתקנה נפוצה של כלי בנייה צולבת ונבדק מול ההתקנה המקומית.
rustup target add aarch64-unknown-linux-gnu
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \
cargo build --target aarch64-unknown-linux-gnu
file target/aarch64-unknown-linux-gnu/debug/target_probe
readelf -h target/aarch64-unknown-linux-gnu/debug/target_probe
readelf -l target/aarch64-unknown-linux-gnu/debug/target_probe
qemu-aarch64 -L /usr/aarch64-linux-gnu \
target/aarch64-unknown-linux-gnu/debug/target_probe
הדמיית משתמש מריצה תוכנית יעד מעל קרנל המארח באמצעות תרגום מתאים. היא מועילה לבדיקת פריסה והוראות משתמש, אך אינה בודקת את דרייברי הקרנל של היעד, את תיאור החומרה או את מסלול האתחול שלו. לשם כך נאתחל מערכת שלמה.
נבנה את עץ הקרנל מהפרק הקודם בספריית פלט חדשה. נבדוק בתצורה תמיכה במכונה הכללית, במסוף הטורי, בתמונת אתחול ובמערכות הקבצים הדרושות. נקודת המוצא הבאה משתמשת בתצורת ברירת המחדל של הארכיטקטורה, שאותה נבחן ולא נציג כהבטחה לכל צורך.
make O=build-arm64 ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig
make O=build-arm64 ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- menuconfig
make O=build-arm64 ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j2 Image
נכין תמונת אתחול כמו בפרק הקודם, אך עם תוכנית כלי מערכת שנבנתה סטטית לארכיטקטורה החדשה. נבדוק באמצעות file ו־readelf גם את הארכיטקטורה וגם את היעדר המפרש הדינמי. העתקת הקובץ הקודם מהמחשב האישי לתמונה החדשה אינה בנייה צולבת.
qemu-system-aarch64 -machine virt -cpu cortex-a57 -m 512M -smp 2 \
-nographic -nic none -no-reboot \
-kernel build-arm64/arch/arm64/boot/Image \
-initrd initramfs-arm64.cpio.gz \
-append 'console=ttyAMA0 rdinit=/init panic=-1'
פורמט חיצוני לא מעתיקים ממבנה פנימי¶
נכתוב מפענח לכותרת שמגיעה ממקור חיצוני. הוא משתמש בבחירת סדר בתים מפורשת ובפרוסות, ולכן אינו דורש שהכתובת תהיה מיושרת למספר בן ארבעה בתים. בכך הוא נמנע גם מהנחת פריסה של מבנה מקומי.
#[derive(Debug, PartialEq)]
struct Header { version: u16, length: u32 }
fn parse(bytes: &[u8]) -> Result<Header, &'static str> {
let header = bytes.get(..6).ok_or("short header")?;
let version = u16::from_le_bytes(header[..2].try_into().unwrap());
let length = u32::from_le_bytes(header[2..6].try_into().unwrap());
if version != 1 { return Err("unsupported version"); }
if length > 4096 { return Err("too large"); }
Ok(Header { version, length })
}
fn main() {
let storage = [0xff, 1, 0, 16, 0, 0, 0];
assert_eq!(parse(&storage[1..]).unwrap(), Header { version: 1, length: 16 });
println!("pointer_bits={} header={:?}", usize::BITS, parse(&storage[1..]));
}
גם אם המעבד מסוגל לבצע גישה לא מיושרת, יצירת הפניה לא מיושרת בשפת ראסט יכולה להפר את כללי השפה. אפשרות חומרה אינה היתר לעקוף את חוזה ההפניה. כאשר נדרש לקרוא ערך לא מיושר נשתמש בפעולה מתאימה או בפענוח בתים כמו בדוגמה, ונבדוק גם את אורך המקור.
סדר זיכרון והנחות נסתרות¶
מעבר לארכיטקטורה בעלת סדר זיכרון חלש יותר יכול לחשוף פרסום שגוי בין תהליכונים או בין המעבד להתקן. אבל אם קיימת גישה מקבילית לא מסונכרנת שהשפה אוסרת, התוכנית כבר שגויה גם במחשב שבו היא נראתה עובדת. לא נתאר שינוי בארכיטקטורה כאילו הוא יצר את הבאג מן האוויר.
נבחן כל ערך משותף: מה מפרסם אותו, איזה מנגנון יוצר יחס סדר ומה מאפשר את שחרורו. נבדיל בין אטומים של שפת התכנות, מחסומי הקרנל ופעולות מול התקן. ניסוי עומס בשני יעדים הוא ראיה שימושית, אך אינו מחליף הוכחה לפי המודל הרלוונטי.
חקירה שיטתית של מערכת לא מוכרת¶
אם אין פלט, נתחיל במסוף ובשלב האתחול ולא נניח שהתהליך הראשי קרס. אם הקרנל מתחיל אך אינו מריץ את התוכנית הראשונה, נבדוק נתיב, הרשאות, ארכיטקטורה ומפרש. אם התוכנית מתחילה אך התקן חסר, נבדוק תיאור חומרה, התאמת דרייבר ותוצאת האתחול שלו.
לכל השערה נבחר ניסוי שמבדיל אותה מחלופה. למשל, קובץ הרצה שקיים אך דורש מפרש חסר יכול להפיק שגיאה שנראית כמו קובץ חסר. בדיקת כותרות הקובץ מבחינה בין המקרים בלי לשנות את כל מערכת האתחול. נרשום מה ראינו ומה עדיין הסקנו, כדי שהחקירה תישאר ניתנת לשחזור.
להרחבה ניעזר במודל השימוש בעץ התקנים, בתיעוד המכונה הווירטואלית ובחוזה אתחול הארכיטקטורה.