# drgn: 探索與除錯 Linux 核心的利器!
## 什麼是 drgn?
在分析 Linux 上的問題或者學習各個子系統時,相較於只看程式碼而臆測其執行行為,搭配觀察實際運行的狀況才是正確且有效的方式。而如果只是想觀察函式的執行,Linux 內建的 [ftrace](https://www.kernel.org/doc/html/latest/trace/ftrace.html) 已經提供了豐富的支援。但在此之上,如果我們能夠獲取 Linux 核心裡關鍵的變數或資料結構的內容,則可以對當前的系統狀態有更全面的了解。
這個目的可以藉由 [drgn](https://github.com/osandov/drgn) 做到。drgn 是一個強大的除錯工具,其目標是讓除錯感覺就像編寫程式碼一樣自然,可以輕鬆的用 Python 編寫腳本來分析運行的 Linux 系統、vmcore 或 userspace 的程式。針對 Linux 系統部分,drgn 可以分析 [`vmlinux`](https://en.wikipedia.org/wiki/Vmlinux) 裡的符號(symbol)和型別(type),並公開給除錯程式的撰寫者。搭配 [`KCORE`](https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/4/html/reference_guide/s2-proc-kcore#s2-proc-kcore) 揭示運行時核心的虛擬 ELF 檔案,就可以存取到核心中的變數與資料結構! 這意味著我們可以了解描述驅動程式的資訊結構、排程器的運行隊列(runqueue)、每一個 GPIO 或 interrupt 的 descriptor,在運行當下的資料結構中的內容為何! 在追蹤系統執行時,這絕對是相當有助益的資訊!
## 前置準備
### 安裝 drgn
以 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`](https://github.com/makedumpfile/makedumpfile/wiki) 的格式,可以額外從 [`libkdumpfile`](https://github.com/ptesarik/libkdumpfile) 安裝相關套件。
```
$ autoreconf -fi
$ ./configure
$ make
$ sudo make install
```
最後,就可以從 [drgn](https://github.com/osandov/drgn) 專案取得原始碼並建構與安裝。
```
$ git clone https://github.com/osandov/drgn.git
$ cd drgn
$ python3 setup.py build
$ sudo python3 setup.py install
```
如果不想從原始專案重新編譯,也可以選擇使用發行版上已經打包好的 package。詳細安裝步驟說明在 [drgn](https://github.com/osandov/drgn) 的 [README](https://github.com/osandov/drgn?tab=readme-ov-file#installation) 頁面上可以找到,請參考上面的內容。
### 安裝 debuginfo
除了 drgn 本身,我們還需要帶有除錯資訊的 `vmlinux` 檔案以及核心模組,讓 drgn 知道如何分析 `KCORE`。以 Ubuntu 為例,就是要由以下方式下載相關 package:
```
sudo apt-get install linux-image-`uname -r`-dbgsym
```
請參考 [Debug symbol packages](https://ubuntu.com/server/docs/debug-symbol-packages) 瞭解安裝除錯資訊的步驟細節。
## 使用說明
### 啟動 drgn
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
```
:::info
我的習慣是直接在腳本中加入以下 [Shebang](https://en.wikipedia.org/wiki/Shebang_(Unix)),然後直接執行。
```
#!/usr/bin/env drgn
```
:::
### Program
[**Program**](https://drgn.readthedocs.io/en/latest/api_reference.html#programs) 是所分析的程式實體,比如所運行的 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
```
### Object
所有變數、常數和函數在 drgn 中都視為是一種 [**Object**](https://drgn.readthedocs.io/en/latest/api_reference.html#objects)。Object 在 drgn 腳本中的使用就像在原始的 C 語言程式碼中使用一樣。比如說,想存取 `pci_bus_type` 的 `name` 成員時:
```
>>> prog["pci_bus_type"].name
(const char *).LC4+0x10a = 0xffffffff8286630a = "pci"
```
### Helper
雖然藉由 `Program` 和 `Object`,我們就足以存取 Linux 核心中的任何資料。不過,有時流程會過於複雜。我們可以善用 drgn 提供的 [Helper](https://drgn.readthedocs.io/en/latest/helpers.html) 以更容易得或許目標資料。舉例來說,當想存取 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`](https://github.com/osandov/drgn/tree/main/contrib) 中找到幾個針對不同目的的範例,會是不錯的起點。
除此之外,在工作上我也經常使用 drgn 來進行 Linux 系統的分析,在 [`drgn-utils`](https://github.com/RinHizakura/drgn-utils) 中蒐集了常用的幾個腳本。透過這些 Python 程式,可以有效率解析 Linux 核心中的 `task_struct`、`device`、`irq_desc` 和 `gpio_desc` 等描述系統中各組件的關鍵資料結構,在追蹤系統的狀態並除錯上十分有用! 試著使用並參考相關的原始程式碼,相信就可以理解 drgn 強大又容易上手的魅力 :)
## vmcore
### crash & drgn
在 [Linux 核心設計: Kernel Debugging(1): Kdump](https://hackmd.io/@RinHizakura/HkHacces6) 一文中,介紹了 Linux 核心如何在 Kernel Panic 發生時,產生具有除錯資訊的 `vmcore`,並說明了如何使用 [crash](https://github.com/crash-utility/crash) 對其進行分析。然而,crash 雖然可以分析 vmcore,但對於除錯的支援主要是藉由互動式的命令列(command line)。如果想要有更彈性的腳本支援,此時我們就可以改為使用 drgn。事實上,在 drgn 的 README 頁面就提到,drgn 的開發目的之一就是作為 Meta 在 crash 工具上的替代品!
### 取得 vmcore
下面說明使用 drgn 分析 vmcore 的方式。這裡以 [`virtme-ng`](https://github.com/arighi/virtme-ng) 為範例平台,參考 [Linux 核心設計: Kernel Debugging(1): Kdump](https://hackmd.io/@RinHizakura/HkHacces6) 的說明,在運行於 `virtme-ng` 上的 Linux 系統產生一份 Core Dump。
:::warning
步驟的詳細說明請參照 [Linux 核心設計: Kernel Debugging(1): Kdump](https://hackmd.io/@RinHizakura/HkHacces6) 原始文章,本文僅列出命令步驟。
:::
```
$ vng --append 'crashkernel=256M' -o '\-device vmcoreinfo'
_ _
__ _(_)_ __| |_ _ __ ___ ___ _ __ __ _
\ \ / / | __| __| _ _ \ / _ \_____| _ \ / _ |
\ V /| | | | |_| | | | | | __/_____| | | | (_| |
\_/ |_|_| \__|_| |_| |_|\___| |_| |_|\__ |
|___/
kernel version: 6.13.0-rc2-00036-g231825b2e1ff x86_64
(CTRL+d to exit)
```
使用 `kexec` 來將 dump-capture kernel 和 initrd 載入。
```
$ 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"
```
在 `sysrq-trigger` 觸發 panic 之後,待系統進入 dump-capture kernel。此時可以在 QEMU 中輸入 Ctrl+a c 進入 QEMU monitor 模式,然後由以下命令來產生 Core dump。
```
dump-guest-memory -z guest.img
```
### 使用 drgn 分析 vmcore
經過以上步驟,我們可以獲得 `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:27
LOAD AVERAGE: 0.20, 0.05, 0.02
TASKS: 225
NODENAME: virtme-ng
RELEASE: 6.14.0-rc2-00001-g444f2d09bdbd-dirty
VERSION: #24 SMP PREEMPT_DYNAMIC Thu Mar 20 21:08:10 CST 2025
MACHINE: x86_64 (2918 Mhz)
MEMORY: 1 GB
PANIC: "Kernel panic - not syncing: sysrq triggered crash"
PID: 308
COMMAND: "sh"
TASK: ffffa1ef03e63300 [THREAD_INFO: ffffa1ef03e63300]
CPU: 13
STATE: TASK_RUNNING (PANIC)
```
我們可以透過撰寫一個 drgn 腳本([drgn_crash.py](https://github.com/RinHizakura/drgn-utils/blob/main/drgn_crash.py))來獲得相同的內容! 但需要注意到的是,由於 [drgn issue#350](https://github.com/osandov/drgn/issues/350) 所描述的限制,目前 `drgn` 無法像 `crash` 直接利用 `guest.img` + `vmlinux` 來分析相關的資訊。如下所示:
```
$ drgn -c guest.img -s vmlinux drgn_crash.py
warning: the given file is in the makedumpfile flattened format; if open fails or is too slow, reassemble it with 'makedumpfile -R newfile <oldfile'
Traceback (most recent call last):
File "/usr/local/bin/drgn", line 33, in <module>
sys.exit(load_entry_point('drgn==0.0.26+77.g2857739f', 'console_scripts', 'drgn')())
File "/usr/local/lib/python3.10/dist-packages/drgn-0.0.26+77.g2857739f-py3.10-linux-x86_64.egg/drgn/cli.py", line 287, in _main
prog.set_core_dump(args.core)
Exception: kdump_vmcoreinfo_raw: linux.vmcoreinfo.raw is not set
```
做為解決方案,我們可以使用 [vmcoreinfo](https://github.com/brenns10/kernel_stuff/tree/master/vmcoreinfo) 工具來提取 `guest.img` 裡的 `vmcoreinfo`
```
$ dumpphys -i -c guest.img -o vmcoreinfo
```
接著,再透過下面的命令啟動 `drgn_crash.py`:
```
$ drgn -c guest.img -s vmlinux --vmcoreinfo vmcoreinfo --architecture x86_64 drgn_crash.py
```
就可以獲得相似的資訊了!
```
CPUS: 20
DATE: Sat Apr 5 20:44:51 2025
UPTIME: 0:00:27
LOAD AVERAGE: 0.20, 0.05, 0.02
TASKS: 225
NODENAME : virtme-ng
RELEASE : 6.14.0-rc2-00001-g444f2d09bdbd-dirty
VERSION : #24 SMP PREEMPT_DYNAMIC Thu Mar 20 21:08:10 CST 2025
MACHINE : x86_64
MEMORY : 1G
PANIC: "Kernel panic - not syncing: sysrq triggered crash"
PID: 308
COMM: "sh"
TASK: 0xffffa1ef03e63300
CPU: 13
STATE: R
```
## Reference
* [Enter the drgn](https://blogs.oracle.com/linux/post/enter-the-drgn)