דיבוג מקור וניתוח קובץ קריסה¶
מצב התהליך נשאר אחרי שהתהליך נגמר¶
נניח שהתקלה מתרחשת אצל לקוח פעם בשבוע, והפעלת מנפה שגיאות משנה את התזמון כך שהיא נעלמת. קובץ תמונת תהליך מאפשר לשמור זיכרון, אוגרים ומידע נוסף ולחקור אותם לאחר האירוע. התמונה אינה תיעוד של כל ההיסטוריה, ולכן היא יכולה להראות ערך פגום בלי להראות מי שינה אותו קודם. נשתמש בה כדי לבנות השערה ונבחר ניסוי נוסף כדי לבדוק את ההיסטוריה החסרה.
הקובץ אינו עומד לבדו. מנפה השגיאות צריך את קובץ ההרצה המדויק, את הספריות ששימשו אותו ואת הסמלים המתאימים. קובץ שנבנה מאותו מקור עם דגלים אחרים אינו בהכרח תואם. אופטימיזציה יכולה לשנות פריסת קוד, להעלים משתנים ולמזג פעולות, ולכן שם הגרסה לבדו אינו מזהה בנייה מספיק.
עצירה לפני כשל מכוון¶
ניצור תוכנית מעבדה שמזהה אי־התאמה בין אורך מוצהר לאורך ממשי ומפסיקה את עצמה. ההפסקה מכוונת כדי שנוכל לחקור מצב מוגדר בלי להכניס שימוש בזיכרון משוחרר או התנהגות לא מוגדרת. נשמור את הקוד בקובץ core_probe.rs.
#[derive(Debug)]
struct Job {
id: u64,
expected: usize,
actual: usize,
payload: Vec<u8>,
}
#[inline(never)]
fn checkpoint(job: &Job) {
std::hint::black_box(job);
if job.expected != job.actual {
eprintln!("invariant failed id={} expected={} actual={}",
job.id, job.expected, job.actual);
std::process::abort();
}
}
fn main() {
let mut job = Job { id: 7, expected: 5, actual: 0,
payload: b"four".to_vec() };
job.actual = job.payload.len();
checkpoint(&job);
}
נבנה תחילה עם מידע ניפוי וללא אופטימיזציה, ואז נפתח את התוכנית במנפה. הפקודות הבאות משתמשות בכלי GDB. אם התקנת ראסט מספקת את המעטפת rust-gdb, אפשר להשתמש בה כדי לטעון גם עזרי תצוגה לטיפוסי השפה.
rustc --edition=2024 -C debuginfo=2 -C opt-level=0 \
-C force-frame-pointers=yes core_probe.rs -o core_probe
gdb -q ./core_probe
בתוך מנפה השגיאות נציב נקודת עצירה בפונקציה ונריץ. שמות סמלים ותצוגת טיפוסים עשויים להשתנות בין גרסאות, ולכן אם השם אינו נפתר נשתמש ב־info functions checkpoint כדי לראות את הסמל שנוצר בבנייה שלנו.
break core_probe::checkpoint
run
print *job
print (*job).expected
print (*job).actual
backtrace
info registers
generate-core-file core.snapshot
continue
התמונה שיצרנו היא תמונה בנקודת העצירה לפני ההפסקה, ולא תמונת הקריסה שנוצרה על ידי מערכת ההפעלה. ההבחנה חשובה: האוגר שמצביע על ההוראה והתהליכונים שנעצרו משקפים את נקודת העצירה. בתמונה אמיתית לאחר אות הפסקה נראה לרוב גם מסגרות של ספריית הריצה ושל מסלול ההפסקה.
שימוש בתמונה בלי להריץ שוב¶
נפתח את התמונה יחד עם קובץ ההרצה המקורי. כעת אין תהליך חי שאפשר להמשיך להריץ, אבל אפשר לקרוא את הזיכרון שנשמר ואת מחסניות התהליכונים.
נשתמש בפקודות info threads, בפקודה thread apply all backtrace ובבדיקת המשתנה בנקודת העצירה. בתוכנית הזאת נצפה לאורך מוצהר של חמש ולאורך ממשי של ארבע. התמונה מספקת ראיה לאי־ההתאמה, אך אינה מוכיחה לבדה מדוע הכותרת הכריזה על חמש. כדי לגלות את מקור הערך נצטרך קלט, יומן או ניסוי נוסף.
כאשר מערכת המעבדה משתמשת בשירות איסוף קריסות, נוכל לאתר אירוע בעזרת coredumpctl list, לבדוק פרטים בעזרת coredumpctl info ולייצא תמונה בעזרת coredumpctl dump. נבחר אירוע לפי מזהה תהליך, זמן וקובץ הרצה, ולא לפי הנחה שהאירוע האחרון שייך לניסוי שלנו. מזהי תהליך ממוחזרים, וייתכנו כמה קריסות של אותו שם.
מדוע לא נוצר קובץ¶
האפשרות ulimit -c היא רק חלק מהתנאים. מדיניות core_pattern, הרשאות, גבולות גודל, מצב ההרשאות של התהליך והגדרות שירות האיסוף משפיעים על התוצאה. מערכת יכולה להחזיק רשומת מטא־נתונים על קריסה בלי לשמור עוד את התמונה עצמה. לכן רשומה ביומן אינה הוכחה שאפשר לשחזר ממנה את כל הזיכרון.
נבדוק את מדיניות המכונה לפני שינוי כללי. לצורך המעבדה אפשר להשתמש ביצירת תמונה מתוך המנפה בלי להחליף את מנגנון איסוף הקריסות של המארח. במערכת אמיתית יש לתכנן מראש מקום אחסון, הרשאות ושמירה של סמלים. תמונת זיכרון יכולה להכיל קלט וסודות שהיו בתהליך, ולכן חומרי החקירה נשמרים באותה רמת הגנה שמתאימה לנתונים האלה.
הפרדת סמלים מקוד הרצה¶
ניצור עותק מופחת סמלים וקובץ סמלים נפרד מתוך אותה בנייה. הפקודות הבאות אינן בונות מחדש את התוכנית, אלא משנות את אופן אריזת המידע שלה.
objcopy --only-keep-debug core_probe core_probe.debug
cp -- core_probe core_probe.stripped
strip --strip-debug core_probe.stripped
objcopy --add-gnu-debuglink=core_probe.debug core_probe.stripped
readelf -n core_probe
readelf -n core_probe.stripped
gdb -q ./core_probe.stripped ./core.snapshot
מזהה הבנייה, כאשר המקשר יצר אותו, מאפשר לקשור בין הקוד לבין הסמלים. מנגנון הקישור לקובץ סמלים נפרד כולל גם בדיקת התאמה. נבדוק שהמנפה באמת טען את הקובץ המתאים ולא מצא קובץ ישן בעל שם דומה. בסביבת איסוף מרכזית נשמור את הקבצים לפי זהות הבנייה ולא נדרוס אותם בכל פריסה חדשה.
גם הספריות הדינמיות צריכות להתאים. תמונת התהליך מכילה מיפויים וכתובות, אבל חלק מהדפים עשויים לא להישמר בה לפי מדיניות הסינון. המנפה יכול להזדקק לקבצים המקוריים כדי להשלים מידע. כתובות מושפעות ממיקום הטעינה, ולכן אין לפרש כתובת כאילו הקוד תמיד מתחיל באותו מקום.
נקודת עצירה לעומת מעקב אחר שינוי¶
נקודת עצירה עונה לשאלה האם הגענו למקום מסוים בקוד ומה היה המצב אז. נקודת מעקב אחר כתיבה עונה לשאלה מתי ערך בזיכרון משתנה. כדי לחקור את השמת actual, נעצור אחרי יצירת המשתנה ולפני ההשמה, נציב מעקב על השדה ואז נמשיך. מספר המעקבים החומרתיים והגודל שניתן לעקוב אחריו מוגבלים, ולעיתים המנפה נאלץ לבחור מנגנון אחר.
נבנה גם גרסה עם אופטימיזציה ונחזור על הבדיקה. ייתכן שהמשתנה יוצג כחסר או שחלק מהשמות לא יתאימו לערך חומרתי נפרד. זו אינה הוכחה שהקוד לא רץ. נבחן קוד מכונה בעזרת disassemble /m, אוגרים ומיקומי זיכרון, ונחבר אותם למידע הניפוי הזמין. הגרסה הלא ממוטבת עוזרת ללמוד את המבנה, אבל אינה תמיד משחזרת תקלה שמופיעה רק בגרסה הממוטבת.
הכלי LLDB מספק פעולות מקבילות כגון breakpoint set, הצגת מסגרות ומידע אוגרים, אך התחביר ועזרי השפה שונים. המיומנות המשותפת היא ניסוח השאלה: האם מחפשים ערך, שינוי ערך, מסלול ביצוע או התאמה בין כתובת לסמל. לימוד שמות הפקודות בלי ההבחנה הזאת יוצר חקירה ארוכה שאינה מוסיפה ראיות.
החוזים מפורטים בתיעוד תמונות התהליך, בתיעוד הסמלים הנפרדים ובממשק איסוף הקריסות. בכל דוח נציין האם חקרנו תמונת קריסה, תמונה יזומה או תהליך חי, משום שלכל אחת מהן יש גבול ידע אחר.