在分析 Linux 上的問題或者學習各個子系統時,相較於只看程式碼而臆測其執行行為,搭配觀察實際運行的狀況才是正確且有效的方式。而如果只是想觀察函式的執行,Linux 內建的 ftrace 已經提供了豐富的支援。但在此之上,如果我們能夠獲取 Linux 核心裡關鍵的變數或資料結構的內容,則可以對當前的系統狀態有更全面的了解。
這個目的可以藉由 drgn 做到。drgn 是一個強大的除錯工具,其目標是讓除錯感覺就像編寫程式碼一樣自然,可以輕鬆的用 Python 編寫腳本來分析運行的 Linux 系統、vmcore 或 userspace 的程式。針對 Linux 系統部分,drgn 可以分析 vmlinux
裡的符號(symbol)和型別(type),並公開給除錯程式的撰寫者。搭配 KCORE
揭示運行時核心的虛擬 ELF 檔案,就可以存取到核心中的變數與資料結構! 這意味著我們可以了解描述驅動程式的資訊結構、排程器的運行隊列(runqueue)、每一個 GPIO 或 interrupt 的 descriptor,在運行當下的資料結構中的內容為何! 在追蹤系統執行時,這絕對是相當有助益的資訊!
以 Ubuntu 的系統為例,需要先安裝drgn 的依賴套件。
$ sudo apt install autoconf automake check \
gcc git liblzma-dev libelf-dev libdw-dev \
libtool make pkgconf python3 python3-dev \
python3-pip python3-setuptools zlib1g-dev
如果想讓 drgn 在 Core Dump 的支援上可以解析 makedumpfile
的格式,可以額外從 libkdumpfile
安裝相關套件。
$ autoreconf -fi
$ ./configure
$ make
$ sudo make install
最後,就可以從 drgn 專案取得原始碼並建構與安裝。
$ git clone https://github.com/osandov/drgn.git
$ cd drgn
$ python3 setup.py build
$ sudo python3 setup.py install
如果不想從原始專案重新編譯,也可以選擇使用發行版上已經打包好的 package。詳細安裝步驟說明在 drgn 的 README 頁面上可以找到,請參考上面的內容。
除了 drgn 本身,我們還需要帶有除錯資訊的 vmlinux
檔案以及核心模組,讓 drgn 知道如何分析 KCORE
。以 Ubuntu 為例,就是要由以下方式下載相關 package:
sudo apt-get install linux-image-`uname -r`-dbgsym
請參考 Debug symbol packages 瞭解安裝除錯資訊的步驟細節。
drgn 的使用體驗和 Python 其實並無太大差異。如果想對運行中的核心進行除錯,直接執行 drgn
就可以。這個模式下也會自動載入常用的 drgn 函式庫。
$ drgn
drgn 0.0.26+77.g2857739f (using Python 3.10.12, elfutils 0.186, with libkdumpfile)
For help, type help(drgn).
>>> import drgn
>>> from drgn import FaultError, NULL, Object, cast, container_of, execscript, offsetof, reinterpret, sizeof, stack_trace
>>> from drgn.helpers.common import *
>>> from drgn.helpers.linux import *
>>>
如果要除錯的步驟比較複雜,可以用 Python 編寫腳本並透過 drgn
執行之。實際上,drgn 可以被視為是 Python 中的一個分析運行時 Linux 、vmcore
或 userspace 程式的函式庫,因此很容易地可以搭配其他實用的 Python 套件,構建出符合目的的腳本。啟動 drgn 腳本的方式如下:
$ drgn my_drgn_script.py
我的習慣是直接在腳本中加入以下 Shebang,然後直接執行。
#!/usr/bin/env drgn
Program 是所分析的程式實體,比如所運行的 Linux 系統。drgn 使用名為 prog
的變數對此進行初始化。使用者可以藉由 prog
尋找符號、型別定義等。舉例來說,可藉其存取全域的 pci_bus_type
結構。
>>> prog["pci_bus_type"]
(struct bus_type){
.name = (const char *).LC4+0x10a = 0xffffffff8286630a = "pci",
.dev_name = (const char *)0x0,
.dev_root = (struct device *)0x0,
.bus_groups = (const struct attribute_group **)pci_bus_groups+0x0 = 0xffffffff831c0540,
.dev_groups = (const struct attribute_group **)pci_dev_groups+0x0 = 0xffffffff831c01e0,
.drv_groups = (const struct attribute_group **)pci_drv_groups+0x0 = 0xffffffff831c00c0,
.match = (int (*)(struct device *, struct device_driver *))pci_bus_match+0x0 = 0xffffffff818b0ec0,
.uevent = (int (*)(struct device *, struct kobj_uevent_env *))pci_uevent+0x0 = 0xffffffff818b0b50,
.probe = (int (*)(struct device *))pci_device_probe+0x0 = 0xffffffff818b1490,
.sync_state = (void (*)(struct device *))0x0,
.remove = (void (*)(struct device *))pci_device_remove+0x0 = 0xffffffff818b1420,
.shutdown = (void (*)(struct device *))pci_device_shutdown+0x0 = 0xffffffff818b0b00,
.online = (int (*)(struct device *))0x0,
.offline = (int (*)(struct device *))0x0,
.suspend = (int (*)(struct device *, pm_message_t))0x0,
.resume = (int (*)(struct device *))0x0,
.num_vf = (int (*)(struct device *))pci_bus_num_vf+0x0 = 0xffffffff818b0ae0,
.dma_configure = (int (*)(struct device *))pci_dma_configure+0x0 = 0xffffffff818b0a60,
.pm = (const struct dev_pm_ops *)0x0,
.iommu_ops = (const struct iommu_ops *)0x0,
.p = (struct subsys_private *)0xffff888100c6e800,
.lock_key = (struct lock_class_key){},
.need_parent_lock = (bool)0,
}
有些變數可能是靜態全域(static global),因此在不同的檔案有重複名稱。此時也可藉以下方式明確指定所欲存取的變數是存在哪一個檔案中。
>>> prog.variable("pci_bus_type", "drivers/pci/pci-driver.c")
藉由 prog
,也可以分析某個型別的成員(member)為何。
>>> prog.type('struct bus_type')
struct bus_type {
const char *name;
const char *dev_name;
struct device *dev_root;
const struct attribute_group **bus_groups;
const struct attribute_group **dev_groups;
const struct attribute_group **drv_groups;
int (*match)(struct device *, struct device_driver *);
int (*uevent)(struct device *, struct kobj_uevent_env *);
int (*probe)(struct device *);
void (*sync_state)(struct device *);
void (*remove)(struct device *);
void (*shutdown)(struct device *);
int (*online)(struct device *);
int (*offline)(struct device *);
int (*suspend)(struct device *, pm_message_t);
int (*resume)(struct device *);
int (*num_vf)(struct device *);
int (*dma_configure)(struct device *);
const struct dev_pm_ops *pm;
const struct iommu_ops *iommu_ops;
struct subsys_private *p;
struct lock_class_key lock_key;
bool need_parent_lock;
}
或者,明確指定符號是對應函式而非變數或型別時。
>>> prog.function('schedule')
(void (void))0xffffffff81f39b40
所有變數、常數和函數在 drgn 中都視為是一種 Object。Object 在 drgn 腳本中的使用就像在原始的 C 語言程式碼中使用一樣。比如說,想存取 pci_bus_type
的 name
成員時:
>>> prog["pci_bus_type"].name
(const char *).LC4+0x10a = 0xffffffff8286630a = "pci"
雖然藉由 Program
和 Object
,我們就足以存取 Linux 核心中的任何資料。不過,有時流程會過於複雜。我們可以善用 drgn 提供的 Helper 以更容易得或許目標資料。舉例來說,當想存取 pid 為 1 的任務(task) 之對應 struct task_struct
時,可以透過以下方式:
>>> find_task(1)
*(struct task_struct *)0xffff8881002d8000 = {
.thread_info = (struct thread_info){
.flags = (unsigned long)0,
.syscall_work = (unsigned long)0,
.status = (u32)0,
},
.__state = (unsigned int)1,
.stack = (void *)0xffffc9000000c000,
...
如果上述的說明仍不足以讓你了解 drgn 的完整使用方式,你可以在 drgn/contrib
中找到幾個針對不同目的的範例,會是不錯的起點。
除此之外,在工作上我也經常使用 drgn 來進行 Linux 系統的分析,在 drgn-utils
中蒐集了常用的幾個腳本。透過這些 Python 程式,可以有效率解析 Linux 核心中的 task_struct
、device
、irq_desc
和 gpio_desc
等描述系統中各組件的關鍵資料結構,在追蹤系統的狀態並除錯上十分有用! 試著使用並參考相關的原始程式碼,相信就可以理解 drgn 強大又容易上手的魅力 :)
在 Linux 核心設計: Kernel Debugging(1): Kdump 一文中,介紹了 Linux 核心如何在 Kernel Panic 發生時,產生具有除錯資訊的 vmcore
,並說明了如何使用 crash 對其進行分析。然而,crash 雖然可以分析 vmcore,但對於除錯的支援主要是藉由互動式的命令列(command line)。如果想要有更彈性的腳本支援,此時我們就可以改為使用 drgn。事實上,在 drgn 的 README 頁面就提到,drgn 的開發目的之一就是作為 Meta 在 crash 工具上的替代品!
下面說明使用 drgn 分析 vmcore 的方式。這裡以 virtme-ng
為範例平台,參考 Linux 核心設計: Kernel Debugging(1): Kdump 的說明,在運行於 virtme-ng
上的 Linux 系統產生一份 Core Dump。
步驟的詳細說明請參照 Linux 核心設計: Kernel Debugging(1): Kdump 原始文章,本文僅列出命令步驟。
$ vng --append 'crashkernel=256M' -o '\-device vmcoreinfo'
_ _
__ _(_)_ __| |_ _ __ ___ ___ _ __ __ _
\ \ / / | __| __| _ _ \ / _ \_____| _ \ / _ |
\ V /| | | | |_| | | | | | __/_____| | | | (_| |
\_/ |_|_| \__|_| |_| |_|\___| |_| |_|\__ |
|___/
kernel version: 6.13.0-rc2-00036-g231825b2e1ff x86_64
(CTRL+d to exit)
$ sudo /usr/local/sbin/kexec \
-p arch/x86_64/boot/bzImage \
--initrd=../busybox/root.img \
--append="root=/dev/sda console=ttyS0 1 irqpoll nr_cpus=1 reset_devices earlyprintk=serial"
$ sudo sh -c "echo c > /proc/sysrq-trigger"
(輸入 ctrl+a c 進入 QEMU debug 模式)
dump-guest-memory -z guest.img
mount -t proc none /proc
經過以上步驟,我們可以獲得 guest.img
檔案。回顧一下,如果使用 crash 進行分析,初步可以獲得以下資訊。
$ crash guest.img vmlinux
...
KERNEL: vmlinux
DUMPFILE: guest.img [PARTIAL DUMP]
CPUS: 20
DATE: Thu Jan 1 08:00:00 CST 1970
UPTIME: 00:00:31
LOAD AVERAGE: 0.55, 0.15, 0.05
TASKS: 225
NODENAME: virtme-ng
RELEASE: 6.13.0-rc2-00036-g231825b2e1ff
VERSION: #11 SMP PREEMPT_DYNAMIC Sat Dec 28 22:21:23 CST 2024
MACHINE: x86_64 (2918 Mhz)
MEMORY: 1 GB
PANIC: "Kernel panic - not syncing: sysrq triggered crash"
PID: 504
COMMAND: "sh"
TASK: ffff9b9b4228ac40 [THREAD_INFO: ffff9b9b4228ac40]
CPU: 11
STATE: TASK_RUNNING (PANIC)
讓我們嘗試撰寫一個 drgn 腳本(drgn_crash.py)來獲得相同的內容!
$ dumpphys -i -c guest.img -o vmcoreinfo
$ drgn -c guest.img -s vmlinux --vmcoreinfo vmcoreinfo --architecture x86_64 drgn_crash.py