לדלג לתוכן

דיבוג קרנל - kernel debugging

מבוא

דיבוג קוד קרנל הוא סיפור שונה לגמרי מדיבוג תוכניות ב-user space. אי אפשר פשוט להריץ gdb ./kernel ולשים breakpoint (טוב, בעצם אפשר, אבל זה מורכב). הקרנל רץ על הברזל עצמו, מנהל את כל המשאבים, ואין שכבה מתחתיו שתספק שירותי דיבוג.

עם זאת, לינוקס מספק סט מרשים של כלי דיבוג - חלקם פשוטים כמו printk, וחלקם מתקדמים כמו eBPF. בואו נלמד את כולם.


printk - ה-printf של הקרנל

הכלי הבסיסי והנפוץ ביותר. כמו printf, אבל עם כמה הבדלים חשובים.

שימוש בסיסי

#include <linux/kernel.h>

printk(KERN_INFO "Module loaded, value = %d\n", value);
printk(KERN_ERR "Failed to allocate memory!\n");

רמות לוג

לכל הודעה יש רמת חשיבות:

רמה מאקרו מספר משמעות
חירום KERN_EMERG 0 המערכת לא שמישה
התראה KERN_ALERT 1 פעולה נדרשת מיד
קריטי KERN_CRIT 2 מצב קריטי
שגיאה KERN_ERR 3 שגיאה
אזהרה KERN_WARNING 4 אזהרה
הודעה KERN_NOTICE 5 מצב תקין אבל משמעותי
מידע KERN_INFO 6 מידע כללי
דיבוג KERN_DEBUG 7 הודעות דיבוג

מאקרואים נוחים

במקום לכתוב printk(KERN_INFO ...) בכל פעם, יש מאקרואים קצרים:

pr_emerg("Emergency: %s\n", msg);
pr_alert("Alert: %s\n", msg);
pr_crit("Critical: %s\n", msg);
pr_err("Error: %s\n", msg);
pr_warn("Warning: %s\n", msg);
pr_notice("Notice: %s\n", msg);
pr_info("Info: %s\n", msg);
pr_debug("Debug: %s\n", msg);

לדרייברים יש גם dev_info(), dev_err() וכו' שמוסיפים אוטומטית את שם ההתקן:

dev_err(&pdev->dev, "Failed to initialize device\n");
// [  123.456] my_device 0000:00:1f.0: Failed to initialize device

לאן הולכות ההודעות

הודעות printk הולכות ל-kernel ring buffer - מאגר מעגלי בזיכרון הקרנל.

# קריאת ה-ring buffer:
dmesg
dmesg | tail -20          # 20 ההודעות האחרונות
dmesg -w                  # מעקב בזמן אמת (כמו tail -f)
dmesg -T                  # עם timestamps קריאים

# קריאה ישירה:
cat /proc/kmsg            # חוסם וממתין להודעות חדשות

# ניקוי:
sudo dmesg -c             # קורא ומנקה

Dynamic Debug

מה אם יש אלפי הודעות pr_debug בקרנל, ואתם רוצים להפעיל רק חלק מהן? Dynamic Debug מאפשר הפעלה/כיבוי בזמן ריצה:

# הפעלת debug בקובץ ספציפי:
echo 'file my_driver.c +p' > /sys/kernel/debug/dynamic_debug/control

# הפעלת debug בפונקציה ספציפית:
echo 'func my_init_func +p' > /sys/kernel/debug/dynamic_debug/control

# הפעלת debug במודול שלם:
echo 'module my_module +p' > /sys/kernel/debug/dynamic_debug/control

# כיבוי:
echo 'file my_driver.c -p' > /sys/kernel/debug/dynamic_debug/control

# צפייה בכל נקודות ה-debug הזמינות:
cat /sys/kernel/debug/dynamic_debug/control

Kernel oops ו-panic

מה זה oops?

כשהקרנל מזהה שגיאה (למשל, גישה לכתובת NULL), הוא מייצר oops - הודעת שגיאה מפורטת. Oops לא בהכרח הורג את המערכת - הקרנל מנסה להמשיך (אבל המערכת עלולה להיות לא יציבה).

מה זה panic?

Panic = שגיאה בלתי הפיכה. המערכת נעצרת. זה קורה כאשר:
- Oops קורה ב-interrupt context (אין תהליך שאפשר להרוג)
- Oops קורה בתהליך init (PID 1)
- panic_on_oops מוגדר (ממיר כל oops ל-panic)
- קריאה ישירה ל-panic("message")

קריאת הודעת oops

BUG: unable to handle page fault for address: 0000000000001234
#PF: supervisor read access in kernel mode
#PF: error_code(0x0000) - not-present page
PGD 0 P4D 0
Oops: 0000 [#1] SMP NOPTI
CPU: 2 PID: 1842 Comm: my_program Not tainted 5.15.0 #1
Hardware name: QEMU Standard PC
RIP: 0010:my_buggy_function+0x42/0x100 [my_module]
Code: 48 8b 07 48 85 c0 74 12 ...
RSP: 0018:ffffc90000123abc EFLAGS: 00010246
RAX: 0000000000001234 RBX: ffff888100123000 RCX: 0000000000000000
RDX: 0000000000000001 RSI: ffff888100456000 RDI: 0000000000001234
...
Call Trace:
 <TASK>
 caller_function+0x28/0x50 [my_module]
 another_function+0x15/0x30 [my_module]
 do_syscall_64+0x3b/0x90
 entry_SYSCALL_64_after_hwframe+0x44/0xae
 </TASK>

איך לקרוא את זה:

  1. שורה ראשונה - סוג הבעיה: page fault בכתובת 0x1234 (נראה כמו NULL pointer + offset)
  2. RIP - ה-instruction pointer: my_buggy_function+0x42 - הפונקציה שבה קרתה השגיאה, 0x42 בייטים מתחילת הפונקציה
  3. רגיסטרים - RAX=0x1234, זו כנראה הכתובת שניסינו לגשת אליה
  4. RDI=0x1234 - הארגומנט הראשון של הפונקציה (אולי מצביע NULL שקיבלנו)
  5. Call Trace - שרשרת הקריאות שהובילה לשגיאה

פענוח כתובות

# המרת כתובות לשם קובץ ושורה:
scripts/decode_stacktrace.sh vmlinux < oops_message.txt

# או ידנית עם addr2line:
addr2line -e my_module.ko 0x42

# או עם gdb:
gdb my_module.ko
(gdb) list *(my_buggy_function+0x42)

חשוב: צריך שהקרנל/מודול קומפלו עם CONFIG_DEBUG_INFO (debug symbols) כדי שזה יעבוד.


KGDB - דיבוג עם GDB

KGDB מאפשר דיבוג אינטראקטיבי מלא של הקרנל, כולל breakpoints, single-step, ובדיקת זיכרון.

איך זה עובד

KGDB צריך ערוץ תקשורת בין המכונה המדובגת למכונה שמריצה GDB:
- חיבור סריאלי (RS-232)
- KDB (keyboard-based - על אותה מכונה)
- רשת (kgdboe - KGDB over Ethernet)

הפעלה

# בשורת הפרמטרים של הקרנל:
kgdboc=ttyS0,115200    # חיבור סריאלי
kgdbwait               # חכה לחיבור GDB לפני boot

# או בזמן ריצה:
echo ttyS0 > /sys/module/kgdboc/parameters/kgdboc
echo g > /proc/sysrq-trigger    # שולח את הקרנל ל-KGDB

שימוש

# במכונת הדיבוג:
gdb vmlinux
(gdb) target remote /dev/ttyS0
(gdb) break my_function
(gdb) continue
# ... כשמגיעים ל-breakpoint ...
(gdb) bt                    # backtrace
(gdb) print my_variable
(gdb) next                  # step over
(gdb) step                  # step into

QEMU + GDB - הדרך הפרקטית ביותר

בפועל, הדרך הנוחה ביותר לדבג קרנל היא להריץ אותו ב-QEMU ולחבר GDB.

הרצת הקרנל ב-QEMU

# קומפילציה עם debug info:
make menuconfig
# הפעילו: Kernel hacking -> Compile-time checks and compiler options -> Debug info

# הרצה עם דיבוג:
qemu-system-x86_64 \
    -kernel arch/x86/boot/bzImage \
    -initrd rootfs.cpio.gz \
    -append "console=ttyS0 nokaslr" \
    -nographic \
    -s \       # פתח GDB server על פורט 1234
    -S         # עצור בהתחלה (חכה ל-GDB)

הדגל nokaslr חשוב - בלעדיו KASLR ישנה את כתובות הקרנל וה-symbols לא יתאימו.

חיבור GDB

# בטרמינל נפרד:
gdb vmlinux
(gdb) target remote :1234
(gdb) break start_kernel        # breakpoint בתחילת הקרנל
(gdb) continue

# כשעוצר:
(gdb) list                       # הצגת קוד מקור
(gdb) info registers             # רגיסטרים
(gdb) print init_task.comm       # שם התהליך הראשון
(gdb) x/20i $rip                 # 20 הוראות מהמיקום הנוכחי

דיבוג מודולים

מודולים נטענים דינמית, אז GDB לא יודע את הכתובות שלהם מראש:

# בתוך ה-VM - מצאו את כתובות המודול:
cat /sys/module/my_module/sections/.text
# 0xffffffffa0001000
cat /sys/module/my_module/sections/.data
# 0xffffffffa0003000

# ב-GDB:
(gdb) add-symbol-file my_module.ko 0xffffffffa0001000 \
      -s .data 0xffffffffa0003000
(gdb) break my_module_function
(gdb) continue

Ftrace - מערכת מעקב מובנית

Ftrace (Function Tracer) היא מערכת מעקב חזקה שמובנית בקרנל. היא מאפשרת לעקוב אחרי קריאות פונקציות, אירועים, ועוד - ללא צורך בכלים חיצוניים.

ממשק בסיסי

הכל דרך /sys/kernel/debug/tracing/ (צריך debugfs מורכב):

# הפעלת function tracer:
echo function > /sys/kernel/debug/tracing/current_tracer

# הגבלה לפונקציות ספציפיות:
echo 'schedule*' > /sys/kernel/debug/tracing/set_ftrace_filter

# הפעלת המעקב:
echo 1 > /sys/kernel/debug/tracing/tracing_on

# קריאת התוצאות:
cat /sys/kernel/debug/tracing/trace

# כיבוי:
echo 0 > /sys/kernel/debug/tracing/tracing_on
echo nop > /sys/kernel/debug/tracing/current_tracer

function_graph tracer

מציג את עץ הקריאות עם זמני כניסה ויציאה:

echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
# ... עשו משהו ...
cat /sys/kernel/debug/tracing/trace

הפלט נראה כך:

 2)               |  do_sys_open() {
 2)               |    getname() {
 2)               |      kmem_cache_alloc() {
 2)   0.345 us    |        _cond_resched();
 2)   1.234 us    |      }
 2)   2.345 us    |    }
 2)               |    do_filp_open() {
 2)               |      path_openat() {
 ...
 2) + 15.678 us   |  }

אפשר לראות בדיוק כמה זמן כל פונקציה לקחה ואילו פונקציות היא קראה.

trace_printk

כמו printk, אבל הפלט הולך ל-trace buffer של ftrace (הרבה יותר מהיר מ-printk):

trace_printk("my_function called with arg=%d\n", arg);

חשוב: trace_printk מיועד לדיבוג בלבד, לא לקוד production. הקרנל ידפיס אזהרה אם מודול טעון עם trace_printk.


Perf - ניתוח ביצועים

perf הוא הכלי הראשי לניתוח ביצועים בלינוקס. הוא משתמש ב-hardware performance counters של ה-CPU וב-tracepoints של הקרנל.

פקודות בסיסיות

# פרופיילינג בזמן אמת - מראה את הפונקציות שאוכלות הכי הרבה CPU:
sudo perf top

# הקלטת פרופיל של פקודה:
sudo perf record -g ./my_program
sudo perf report

# הקלטת כל המערכת ל-10 שניות:
sudo perf record -a -g sleep 10
sudo perf report

# מעקב אחרי syscalls (כמו strace אבל מהיר יותר):
sudo perf trace ls

# מעקב אחרי אירוע ספציפי:
sudo perf stat -e cache-misses,cache-references,instructions,cycles ./my_program

Hardware Performance Counters

ה-CPU מספק מוני חומרה שסופרים אירועים ברמה נמוכה:

sudo perf stat -e cycles,instructions,cache-misses,branch-misses ./my_program
#  Performance counter stats for './my_program':
#
#      1,234,567,890      cycles
#        987,654,321      instructions     #    0.80  insn per cycle
#          2,345,678      cache-misses     #    5.23% of all cache refs
#            123,456      branch-misses    #    1.23% of all branches

BPF/eBPF - המהפכה בדיבוג קרנל

eBPF (Extended Berkeley Packet Filter) הוא מכונה וירטואלית בתוך הקרנל שמאפשרת להריץ תוכניות מוגדרות-משתמש בצורה בטוחה. זה שינה את הדרך שבה אנשים מדבגים ומנטרים את הקרנל.

למה eBPF מיוחד

  • בטוח - verifier בודק כל תוכנית לפני טעינה (אין לולאות אינסופיות, אין גישה לזיכרון לא חוקי)
  • מהיר - קוד BPF מקומפל ל-native code ע"י JIT
  • לא דורש שינוי בקרנל - אפשר להוסיף probes בלי לקמפל מודול

bpftrace - שורות-אחת לדיבוג

bpftrace הוא שפת סקריפטים ל-eBPF, בהשראת DTrace:

# עקוב אחרי כל הקבצים שנפתחים:
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat {
    printf("%s %s\n", comm, str(args->filename));
}'

# סטטיסטיקת זמני read לפי תהליך:
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_read {
    @start[tid] = nsecs;
}
tracepoint:syscalls:sys_exit_read /@start[tid]/ {
    @us[comm] = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}'

# ספירת syscalls לפי סוג:
sudo bpftrace -e 'tracepoint:raw_syscalls:sys_enter {
    @[comm, args->id] = count();
}'

# עקוב אחרי הקצאות זיכרון בקרנל:
sudo bpftrace -e 'kprobe:kmalloc { @bytes = hist(arg0); }'

bcc tools - כלים מוכנים

bcc (BPF Compiler Collection) מספק עשרות כלים מוכנים:

# מעקב אחרי קבצים שנפתחים:
sudo opensnoop

# מעקב אחרי תוכניות שמורצות:
sudo execsnoop

# מעקב אחרי חיבורי TCP:
sudo tcplife

# מעקב אחרי latency של דיסק:
sudo biolatency

# מעקב אחרי page faults:
sudo drsnoop

# פרופיילינג off-CPU (מה עושים threads כשהם לא רצים):
sudo offcputime

כלים נוספים

/proc/kallsyms

כתובות כל הסמלים (symbols) בקרנל:

sudo cat /proc/kallsyms | head
# ffffffff81000000 T _text
# ffffffff81000000 T startup_64
# ...

# חיפוש פונקציה ספציפית:
sudo cat /proc/kallsyms | grep ' T do_sys_open'

שימושי לדיבוג ידני - אם יש לכם כתובת מ-oops, אפשר לחפש אותה כאן.

הערה אבטחתית: ברירת המחדל (kptr_restrict=1) מחביאה את הכתובות ממשתמשים רגילים (מציגה 0). רק root רואה את הכתובות האמיתיות.

crash utility

כלי לניתוח kernel crash dumps (קבצי core של הקרנל):

# ניתוח crash dump:
sudo crash vmlinux /var/crash/vmcore

crash> bt                  # backtrace של התהליך שגרם לקריסה
crash> ps                  # רשימת תהליכים בזמן הקריסה
crash> files <pid>         # קבצים פתוחים
crash> kmem -i             # מידע על זיכרון
crash> log                 # kernel log
crash> struct task_struct <address>  # הצגת מבנה נתונים

כדי ליצור crash dumps, צריך להגדיר kdump (kernel crash dump):

# התקנה (Ubuntu):
sudo apt install linux-crashdump
sudo systemctl enable kdump-tools

# אחרי קריסה, ה-dump נשמר ב:
ls /var/crash/

סיכום - מתי להשתמש במה

כלי מתי יתרון חיסרון
printk דיבוג מהיר ופשוט תמיד זמין מאט, הרבה פלט
dynamic debug דיבוג ממוקד הפעלה/כיבוי בזמן ריצה רק הודעות pr_debug
QEMU + GDB פיתוח מודולים דיבוג אינטראקטיבי מלא רק ב-VM
ftrace מעקב אחרי זרימת קוד מובנה, מהיר ממשק מורכב
perf ניתוח ביצועים hardware counters דורש הבנה עמוקה
eBPF/bpftrace דיבוג production בטוח, מהיר, גמיש קרנל 4.x+ נדרש
crash ניתוח post-mortem ניתוח אחרי קריסה צריך crash dump

הכלל שלי: התחילו עם printk. אם זה לא מספיק, עברו ל-ftrace. אם צריכים דיבוג אינטראקטיבי, השתמשו ב-QEMU+GDB. ואם צריכים לדבג מערכת production, eBPF הוא הבחירה.