לדלג לתוכן

עקיבת קרנל וכלי בדיקה מתוכנתים

לבחור מקום שמייצג את השאלה

נניח שאנחנו רוצים לספור כמה פעמים תהליך ביקש מהקרנל לזהות את עצמו. אפשר לעקוב אחרי פונקציה פנימית בקרנל, אחרי נקודת עקיבה בגבול הקריאה או אחרי פונקציה בספריית המשתמש. שלוש האפשרויות אינן בהכרח סופרות את אותו דבר. ספרייה יכולה לשמור תוצאה, פונקציה פנימית יכולה להיקרא מכמה מסלולים, ונקודת גבול יכולה להבחין בין כניסה להשלמה.

נבחר נקודת תצפית לפי ההגדרה של האירוע. בשיעור הזה נספור כניסות לקריאת מערכת מסוימת בתהליך מעבדה. לאחר שנאמת את המדידה מול מספר פעולות שהיישום יודע שביצע, נוכל להרחיב את החקירה למשכים וליחסים בין אירועים. קודם נוכיח שהמונה שלנו מודד את הדבר ששמו מבטיח.

עומס עם אמת מידה ידועה

ניצור פרויקט עם התלות libc = "0.2" ונשמור בו את התוכנית הבאה. היא משתמשת ישירות בקריאת המערכת כדי שלא להסתמך על התנהגות מעטפת שיכולה להשתנות. מספר הקריאות מוגבל כדי שטעות בארגומנט לא תיצור ניסוי ארוך בלי כוונה.

use std::io;
fn main() -> io::Result<()> {
    let count: u64 = std::env::args().nth(1).unwrap_or_else(|| "1000".into())
        .parse().map_err(io::Error::other)?;
    if count > 1_000_000 { return Err(io::Error::other("lab count too large")); }
    let mut checksum = 0_i64;
    for _ in 0..count {
        let value = unsafe { libc::syscall(libc::SYS_getpid) };
        if value == -1 { return Err(io::Error::last_os_error()); }
        checksum ^= value as i64;
    }
    println!("issued={count} checksum={checksum}");
    Ok(())
}

נריץ פעם עם אפס ופעם עם אלף. ההפרש אמור לשקף את הקריאות שהלולאה מוסיפה. ייתכן שספריית הריצה עצמה מבצעת קריאות נוספות באתחול, ולכן בדיקת בסיס מועילה יותר מהנחה שמספר מוחלט מסוים תמיד יופיע בכל גרסה. כדי להצליב אפשר להשתמש גם בעקיבת הקריאות מההרצאה הקודמת.

מופע עקיבה פרטי

מסגרת ftrace חושפת ממשק שליטה דרך tracefs. נשתמש במופע עקיבה נפרד במקום לשנות את המאגר הראשי שאולי משמש כלי אחר. הסקריפט הבא מיועד למכונה הווירטואלית ולמערכת שבה מערכת הקבצים כבר מעוגנת בנתיב המקובל. הוא מקבל נתיב מוחלט לבינארי המעבדה ומספר קריאות.

#!/usr/bin/env bash
set -euo pipefail
binary=$(readlink -f -- "$1")
count=${2:-1000}
root=/sys/kernel/tracing
instance="$root/instances/amit-count-$$"
test -x "$binary"
test -d "$root/instances"
mkdir "$instance"
cleanup() {
    printf '0\n' > "$instance/tracing_on"
    printf '0\n' > "$instance/events/syscalls/sys_enter_getpid/enable"
    rmdir "$instance"
}
trap cleanup EXIT
test -r "$instance/events/syscalls/sys_enter_getpid/format"
cat "$instance/events/syscalls/sys_enter_getpid/format"
printf '0\n' > "$instance/tracing_on"
printf '1\n' > "$instance/events/syscalls/sys_enter_getpid/enable"
bash -c '
    instance=$1
    binary=$2
    count=$3
    printf "%s\n" "$$" > "$instance/set_event_pid"
    printf "1\n" > "$instance/tracing_on"
    exec "$binary" "$count"
' bash "$instance" "$binary" "$count"
printf '0\n' > "$instance/tracing_on"
cat "$instance/trace"
for stats in "$instance"/per_cpu/cpu*/stats; do cat "$stats"; done

הילד רושם את מזהה עצמו לפני שהוא מחליף את תמונת התהליך בבינארי שלנו. כך מסנן האירועים מתאים גם לאחר ההפעלה, בלי לנחש מזהה מראש. המסנן הזה אינו בהכרח כולל תהליכונים חדשים או ילדים חדשים בכל תצורה, ולכן המעבדה משתמשת בלולאה חד־תהליכית. הרחבה למערכת מקבילית דורשת מדיניות מעקב מפורשת.

המאגר הוא משאב מוגבל. אם נרשמו יותר אירועים מכפי שניתן לשמור, חלק מהראיות עלולות להיעלם או להידרס לפי המצב. נבדוק את נתוני המאגר ונשווה את מספר הרשומות למספר שהיישום דיווח. מספר נמוך בלי בדיקת אובדן אינו הוכחה שחלק מהקריאות לא קרו.

איסוף מחושב בתוך הקרנל

מנגנון eBPF מאפשר לטעון תוכנית קטנה שמופעלת בנקודת חיבור מוגדרת. במקום להעביר כל אירוע למרחב המשתמש אפשר לעדכן מונה או התפלגות במפה. כך מצמצמים נפח נתונים, אך מכניסים גם לוגיקה שצריך לבדוק. מונה שגוי בתוך הקרנל עדיין יכול להיראות מדויק מאוד כשהוא מודפס בסוף.

נשתמש בכלי bpftrace לכתיבת מדידה קצרה. נבדוק קודם את גרסתו ואת קיום נקודת העקיבה במערכת. הפקודה הבאה מפעילה את הבינארי כילד ומסננת לפי מזהה התהליך שהכלי מספק. יש להציב נתיב מעבדה מוחלט וקבוע בפקודת הילד.

sudo bpftrace -e '
tracepoint:syscalls:sys_enter_getpid /pid == cpid/ {
    @calls = count();
}
END {
    print(@calls);
    clear(@calls);
}' -c '/absolute/path/to/count_probe 1000'

הצירוף count() הוא פעולת צבירה שמתאימה למונה, ולא קריאה רגילה שמדפיסה אירוע. התוכנית מוגבלת לתהליך הילד ולכן אינה סופרת בכוונה את כל המחשב. אם הבינארי יוצר ילדים בעלי מזהים אחרים, הם אינם כלולים במסנן הזה. זאת החלטת תחום שצריך לתעד, ולא פרט שהכלי יחליט במקומנו.

תפקיד המאמת

הקרנל בודק את התוכנית לפני הרצתה בהתאם לסוג התוכנית ולממשקים שהיא מבקשת. המאמת עוקב אחר סוגי מצביעים, גבולות גישה, מצב אוגרים ומסלולי ביצוע. היכולות הנתמכות וההגבלות תלויות בגרסת הקרנל. לכן תוכנית שנדחתה אינה בהכרח תוכנית בעלת לוגיקה עסקית שגויה, ותוכנית שהתקבלה אינה בהכרח מדידה נכונה.

קבלה על ידי המאמת אינה מוכיחה שבחרנו את המזהה הנכון, שהיחידות נכונות או שאין אובדן אירועים בדרך למשתמש. היא גם אינה מחליפה הרשאות טעינה ותצורת אבטחה. נבנה בדיקה עצמאית של משמעות המדידה, כפי שעשינו עם מספר הקריאות הידוע.

מפות, תוכניות ונקודות חיבור הן אובייקטים בעלי חיי משאב. סגירת מתאר אחד אינה תמיד מסיימת אובייקט שיש אליו הפניות נוספות או עיגון במערכת הקבצים המתאימה. כלי שמסתיים צריך לשחרר את החיבורים שהוא יצר, ומפעיל צריך לדעת אילו אובייקטים הושארו בכוונה. מדידה שנשארת מחוברת אחרי הניסוי יכולה להשפיע על ניסוי הבא.

תאימות מבנית לעומת תאימות משמעות

מידע BTF מתאר טיפוסים ומסייע לכלים להבין מבני קרנל. מנגנוני התאמה מסוג CO-RE מאפשרים להתאים חלק מהגישות לשדות כאשר פריסתם שונה בין בניות. הם אינם מבטיחים שהשדה נשאר בעל אותה משמעות או שנקודת החיבור ממשיכה לייצג את אותו אירוע לוגי.

נקודת עקיבה מוגדרת בדרך כלל מתאימה יותר לשאלה תפעולית מפונקציה פנימית אקראית, אך גם נקודות עקיבה אינן חוזה קבוע ללא גבולות. חיבור לפונקציה פנימית באמצעות מנגנון בדיקה יכול להישבר עם שינוי שם, שילוב פונקציה או שינוי חתימה. נבדוק זמינות וסמנטיקה על כל גרסת יעד שנכללת בהבטחת הכלי.

הרחבה למדידת משך

כדי למדוד משך נחבר אירוע כניסה לאירוע יציאה באמצעות מפתח מתאים, למשל מזהה תהליכון יחד עם הקשר נוסף לפי הצורך. נצטרך לטפל ביציאה בלי כניסה שנצפתה, בתהליכון שנגמר ובאובדן אירוע. שימוש במזהה תהליך בלבד יכול לערבב פעולות של כמה תהליכונים. מונה פשוט הוא התחלה טובה בדיוק משום שהוא חושף פחות הנחות חיבור נסתרות.

המסגרות מתועדות במדריך עקיבת הקרנל, בתיעוד המאמת ובסקירת ספריית הטעינה. התחביר של כלי המדידה מופיע בתיעוד השפה. בכל תוצאה נציג גם ניסוי כיול שמראה מה המונה באמת סופר.