# 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)