דיבוג קרנל - 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 - מאגר מעגלי בזיכרון הקרנל.
# reading the ring buffer:
dmesg
dmesg | tail -20 # the last 20 messages
dmesg -w # real-time follow (like tail -f)
dmesg -T # with readable timestamps
# direct read:
cat /proc/kmsg # blocks and waits for new messages
# clear:
sudo dmesg -c # reads and clears
Dynamic Debug¶
מה אם יש אלפי הודעות pr_debug בקרנל, ואתם רוצים להפעיל רק חלק מהן? Dynamic Debug מאפשר הפעלה/כיבוי בזמן ריצה:
# enable debug in a specific file:
echo 'file my_driver.c +p' > /sys/kernel/debug/dynamic_debug/control
# enable debug in a specific function:
echo 'func my_init_func +p' > /sys/kernel/debug/dynamic_debug/control
# enable debug in an entire module:
echo 'module my_module +p' > /sys/kernel/debug/dynamic_debug/control
# disable:
echo 'file my_driver.c -p' > /sys/kernel/debug/dynamic_debug/control
# view all available debug points:
cat /sys/kernel/debug/dynamic_debug/control
Kernel oops ו-panic¶
מה זה oops?¶
כשהקרנל מזהה שגיאה (למשל, גישה לכתובת NULL), הוא מייצר oops - הודעת שגיאה מפורטת. Oops לא בהכרח הורג את המערכת - הקרנל מנסה להמשיך (אבל המערכת עלולה להיות לא יציבה).
מה זה panic?¶
Panic = שגיאה בלתי הפיכה. המערכת נעצרת. זה קורה כאשר:
- Oops קורה ב-interrupt context (אין process שאפשר להרוג)
- Oops קורה בprocess 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>
איך לקרוא את זה:
- שורה ראשונה - סוג הבעיה: page fault בכתובת
0x1234(נראה כמו NULL pointer + offset) - RIP - ה-instruction pointer:
my_buggy_function+0x42- הפונקציה שבה קרתה השגיאה, 0x42 בייטים מתחילת הפונקציה - רגיסטרים - RAX=0x1234, זו כנראה הכתובת שניסינו לגשת אליה
- RDI=0x1234 - הארגומנט הראשון של הפונקציה (אולי מצביע NULL שקיבלנו)
- Call Trace - שרשרת הקריאות שהובילה לשגיאה
פענוח כתובות¶
# convert addresses to file name and line:
scripts/decode_stacktrace.sh vmlinux < oops_message.txt
# or manually with addr2line:
addr2line -e my_module.ko 0x42
# or with 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)
הפעלה¶
# in the kernel parameter line:
kgdboc=ttyS0,115200 # serial connection
kgdbwait # wait for a GDB connection before boot
# or at runtime:
echo ttyS0 > /sys/module/kgdboc/parameters/kgdboc
echo g > /proc/sysrq-trigger # sends the kernel to KGDB
שימוש¶
# on the debugging machine:
gdb vmlinux
(gdb) target remote /dev/ttyS0
(gdb) break my_function
(gdb) continue
# ... when the breakpoint is hit ...
(gdb) bt # backtrace
(gdb) print my_variable
(gdb) next # step over
(gdb) step # step into
QEMU + GDB - הדרך הפרקטית ביותר¶
בפועל, הדרך הנוחה ביותר לדבג קרנל היא להריץ אותו ב-QEMU ולחבר GDB.
הרצת הקרנל ב-QEMU¶
# compile with debug info:
make menuconfig
# enable: Kernel hacking -> Compile-time checks and compiler options -> Debug info
# run with debugging:
qemu-system-x86_64 \
-kernel arch/x86/boot/bzImage \
-initrd rootfs.cpio.gz \
-append "console=ttyS0 nokaslr" \
-nographic \
-s \ # open a GDB server on port 1234
-S # stop at the start (wait for GDB)
הדגל nokaslr חשוב - בלעדיו KASLR ישנה את כתובות הקרנל וה-symbols לא יתאימו.
חיבור GDB¶
# in a separate terminal:
gdb vmlinux
(gdb) target remote :1234
(gdb) break start_kernel # breakpoint at the start of the kernel
(gdb) continue
# when it stops:
(gdb) list # show source code
(gdb) info registers # registers
(gdb) print init_task.comm # name of the first process
(gdb) x/20i $rip # 20 instructions from the current location
דיבוג מודולים¶
מודולים נטענים דינמית, אז GDB לא יודע את הכתובות שלהם מראש:
# inside the VM - find the module's addresses:
cat /sys/module/my_module/sections/.text
# 0xffffffffa0001000
cat /sys/module/my_module/sections/.data
# 0xffffffffa0003000
# in GDB:
(gdb) add-symbol-file my_module.ko 0xffffffffa0001000 \
-s .data 0xffffffffa0003000
(gdb) break my_module_function
(gdb) continue
Ftrace - מערכת מעקב מובנית¶
Ftrace (Function Tracer) היא מערכת מעקב חזקה שמובנית בקרנל. היא מאפשרת לעקוב אחרי קריאות פונקציות, אירועים, ועוד - ללא צורך בכלים חיצוניים.
הinterface בסיסי¶
הכל דרך /sys/kernel/debug/tracing/ (צריך debugfs מורכב):
# enable the function tracer:
echo function > /sys/kernel/debug/tracing/current_tracer
# limit to specific functions:
echo 'schedule*' > /sys/kernel/debug/tracing/set_ftrace_filter
# start tracing:
echo 1 > /sys/kernel/debug/tracing/tracing_on
# read the results:
cat /sys/kernel/debug/tracing/trace
# disable:
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
# ... do something ...
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 מיועד לדיבוג בלבד, לא לקוד production. הקרנל ידפיס אזהרה אם מודול טעון עם trace_printk.
Perf - ניתוח ביצועים¶
perf הוא הכלי הראשי לניתוח ביצועים בלינוקס. הוא משתמש ב-hardware performance counters של ה-CPU וב-tracepoints של הקרנל.
פקודות בסיסיות¶
# real-time profiling - shows the functions consuming the most CPU:
sudo perf top
# record a profile of a command:
sudo perf record -g ./my_program
sudo perf report
# record the whole system for 10 seconds:
sudo perf record -a -g sleep 10
sudo perf report
# trace syscalls (like strace but faster):
sudo perf trace ls
# trace a specific event:
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:
# trace all files that are opened:
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat {
printf("%s %s\n", comm, str(args->filename));
}'
# read time statistics per process:
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]);
}'
# count syscalls by type:
sudo bpftrace -e 'tracepoint:raw_syscalls:sys_enter {
@[comm, args->id] = count();
}'
# trace memory allocations in the kernel:
sudo bpftrace -e 'kprobe:kmalloc { @bytes = hist(arg0); }'
bcc tools - כלים מוכנים¶
bcc (BPF Compiler Collection) מספק עשרות כלים מוכנים:
# trace files that are opened:
sudo opensnoop
# trace programs that are executed:
sudo execsnoop
# trace TCP connections:
sudo tcplife
# trace disk latency:
sudo biolatency
# trace page faults:
sudo drsnoop
# off-CPU profiling (what threads do when they're not running):
sudo offcputime
כלים נוספים¶
/proc/kallsyms¶
כתובות כל הסמלים (symbols) בקרנל:
sudo cat /proc/kallsyms | head
# ffffffff81000000 T _text
# ffffffff81000000 T startup_64
# ...
# search for a specific function:
sudo cat /proc/kallsyms | grep ' T do_sys_open'
שימושי לדיבוג ידני - אם יש לכם כתובת מ-oops, אפשר לחפש אותה כאן.
הערה אבטחתית: ברירת המחדל (kptr_restrict=1) מחביאה את הכתובות ממשתמשים רגילים (מציגה 0). רק root רואה את הכתובות האמיתיות.
crash utility¶
כלי לניתוח kernel crash dumps (קבצי core של הקרנל):
# analyze crash dump:
sudo crash vmlinux /var/crash/vmcore
crash> bt # backtrace of the process that caused the crash
crash> ps # list of processes at the time of the crash
crash> files <pid> # open files
crash> kmem -i # memory info
crash> log # kernel log
crash> struct task_struct <address> # display a data structure
כדי ליצור crash dumps, צריך להגדיר kdump (kernel crash dump):
# installation (Ubuntu):
sudo apt install linux-crashdump
sudo systemctl enable kdump-tools
# after a crash, the dump is saved in:
ls /var/crash/
סיכום - מתי להשתמש במה¶
| כלי | מתי | יתרון | חיסרון |
|---|---|---|---|
| printk | דיבוג מהיר ופשוט | תמיד זמין | מאט, הרבה פלט |
| dynamic debug | דיבוג ממוקד | הפעלה/כיבוי בזמן ריצה | רק הודעות pr_debug |
| QEMU + GDB | פיתוח מודולים | דיבוג אינטראקטיבי מלא | רק ב-VM |
| ftrace | מעקב אחרי זרימת קוד | מובנה, מהיר | הinterface מורכב |
| perf | ניתוח ביצועים | hardware counters | דורש הבנה עמוקה |
| eBPF/bpftrace | דיבוג production | בטוח, מהיר, גמיש | קרנל 4.x+ נדרש |
| crash | ניתוח post-mortem | ניתוח אחרי קריסה | צריך crash dump |
הכלל שלי: התחילו עם printk. אם זה לא מספיק, עברו ל-ftrace. אם צריכים דיבוג אינטראקטיבי, השתמשו ב-QEMU+GDB. ואם צריכים לדבג מערכת production, eBPF הוא הבחירה.