# Linux Kernel Debug on Real Machine, KGDB
## 摘要
在有些時候,可能電腦啟動後沒有成功的彈出 Shell,在 Kernel 啟動過程中出現了某些錯誤,或是我們想要除錯的裝置在 QEMU 中沒有支援,因此我們沒有辦法通過模擬的方式進行除錯,這時候我們可以借助於 RS232 以及 KGDB,讓我們能夠輸出啟動訊息到其他電腦上,以及在實際機器上進行除錯。
## 硬體
在古老的 RS232 協定中,我們會看到 DTE 與 DCE 等等,DTE 全稱為 Data Terminal Equipment,而 DCE 全稱為 Data Communication Equipment。我們現在的任務是要將兩台電腦進行連接,電腦屬於 DTE 裝置,連接兩台 DTE,兩端都是 DTE <-> DTE,所以我們需要將一端的 TX 連接到 RX,RX 連接到 TX 才能夠實現互相溝通。
我們使用 RS232 將 Target Machine 與 Host 進行連結,Target Machine 為要 Debug 的目標桌機,而 Host 為一台跑 Fedora 的筆電,在 Target Machine 上需要 RS232 COM SERIAL PORT,把擴充卡插到主機板上,在主機板上會有一個 Serial Com 的腳位,如下圖所示
<img src="https://hackmd.io/_uploads/ryrguXkk-e.png" width=300>
[source](https://www.asrockrack.com/support/faq.tw.asp?id=31)
對於 Host 端,我們需要一條 USB to RS232(公)
<img src="https://hackmd.io/_uploads/S1I8tmJkZg.png" width=300>
[source](https://shop.cpu.com.tw/product/38016/info)
RS232 常見的接頭為 9-pin 的 D-sub 接頭,又被稱為 DB9,重要的 pin 為 pin2 和 pin3。pin2 為 RXD,pin3 為 TXD。而所謂 NULL Modem 的線或是稱為 2-3 反接線指的是 A port 的 pin3 接到 B port 的 pin2,A port 的 pin2 接到 B port 的 pin3,讓兩邊電腦可以互相溝通,也就是 DTE to DTE。
在 Host 與 Target Machine 之間,接上 Null modem 後 Target Mahcine 與 Host 之間就可以相互溝通了。以下為一個 NULL modem 接頭的外觀
<img src="https://hackmd.io/_uploads/HJmbGE1k-g.png" width=300>
[source](https://cablematic.com/en/products/null-modem-adapter-db9-mh-NM065/)
連接完畢後如下圖所示,Host 為筆電,Target Machine 為桌機
<img src="https://hackmd.io/_uploads/H1Tsyl7kZe.jpg" width=400>
Target Machine 連接 com to RS232 公,接著 RS232 公連接 NULL modem,RS232 母 <-> RS232 母,另一端使用 RS232 公 <-> USB 連接到筆電,實現兩台電腦互聯並且能互相通訊。
## 修改 Target 的 BIOS
接著我們需要啟用主機板上的 com,以 ASUS TUF B650M-PLUS 為例子,需要在 BIOS 中進行以下設定
在 Advanced -> Onboard Devices Configuration -> Serial Port Configuration

啟用 Serial Port,並修改設定,可以看到設定中有以下 4 個選項
- I/O base 0x3F8, IRQ=4
- I/O base 0x2F8, IRQ=3
- I/O base 0x3E8, IRQ=4
- I/O base 0x2E8, IRQ=3
I/O base 表示這一 UART (屬於 8250/16450/16550) 佔用哪一段 I/O port 實體記憶體區間。對於 x86 電腦有記憶體地址空間以及 I/O port 記憶體地址空間。I/O port 記憶體地址空間通過 `in/out` 指令進行存取,在傳統 PC 上 COM1 使用 I/O base 0x3F8, 而 COM2 使用 I/O base 0x2F8。
意思為 UART 的暫存器會被映射到從該記憶體地址開始的 8 bytes 範圍,例如 COM1 就是 0x3F8 到 0x3FF。之後驅動程式就直接對 0x3F8 使用 `in/out` 指令去做讀寫字元,設置 baudrate 等等操作。
至於 IRQ4, IRQ3,IRQ 為硬體中斷線。在老電腦上,裝置通過拉高 IRQ 線的電位,接著被 8259 等等晶片偵測到,PIC 對 CPU 發起中斷請求。CPU 收到之後跳轉到對應的 ISR。在 COM1 通常使用 IRQ4, COM2 使用 IRQ3。如果 UART 發生有新的資料可以讀取,或是 TX buffer 清空了可以再送資料進來,就會通過 IRQ4 等等讓 CPU 跳到給定的 ISR。
在現代已經沒有 8259,而是使用 I/O APIC/LAPIC 處理,不過在 BIOS 中還是使用傳統的名稱。
使用 `dmesg` 可以看到在系統早期啟動階段輸出以下訊息 (如果主機板有成功設定)
```shell
$ sudo dmesg | grep ttyS0
[ 0.794988] 00:04: ttyS0 at I/O 0x3f8 (irq = 4, base_baud = 115200) is a 16550A
```
表示 OS 將傳統的 COM1 對應到裝置節點 `/dev/ttyS0`。
## 接收來自於 Target Machine 的啟動訊息
我們可以在 grub 中,按下 `e` 編輯 kernel cmdline,我們嘗試將 Target Machine 的啟動訊息 (Boot Log) 通過 RS232 送到 host 上。
在 kernel cmdline 我們嘗試以下三種設定
- `console=ttyS0,115200n8`
- `earlycon=uart,io,0x3f8,115200 ignore_loglevel loglevel=8`
- `earlycon=uart,io,0x3f8,115200 keep_bootcon ignore_loglevel loglevel=8`
關於這一些參數的詳細資訊,可以參考 [kernel doc kernel-parameters.txt](https://www.kernel.org/doc/Documentation/admin-guide/kernel-parameters.txt)
### 第一種設定的輸出
以下為在 kernel cmdline 加上 `console=ttyS0,115200n8` 的輸出
```
[ OK ] Started plymouth-start.service - Show Plymouth Boot Screen.
[ OK ] Started systemd-ask-password-plymo…quests to Plymouth Directory Watch.
[ OK ] Reached target paths.target - Path Units.
[ OK ] Stopped systemd-vconsole-setup.service - Virtual Console Setup.
Stopping systemd-vconsole-setup.service - Virtual Console Setup...
Starting systemd-vconsole-setup.service - Virtual Console Setup...
...
[ OK ] Stopped target paths.target - Path Units.
[ OK ] Stopped target remote-fs.target - Remote File Systems.
[ OK ] Stopped target remote-fs-pre.targe…reparation for Remote File Systems.
[ OK ] Stopped target slices.target - Slice Units.
[ OK ] Stopped target sockets.target - Socket Units.
[ OK ] Stopped target sysinit.target - System Initialization.
[ OK ] Stopped target swap.target - Swaps.
Starting plymouth-switch-root.serv…e - Plymouth switch root service...
...
[ OK ] Listening on systemd-creds.socket - Credential Encryption/Decryption.
[ OK ] Listening on systemd-initctl.socke…- initctl Compatibility Named Pipe.
[ OK ] Listening on systemd-journald-audit.socket - Journal Audit Socket.
[ OK ] Listening on systemd-oomd.socket -… Out-Of-Memory (OOM) Killer Socket.
[ OK ] Listening on systemd-udevd-control.socket - udev Control Socket.
[ OK ] Listening on systemd-udevd-kernel.socket - udev Kernel Socket.
[ OK ] Listening on systemd-userdbd.socket - User Database Manager Socket.
...
[ OK ] Started crond.service - Command Scheduler.
Starting plymouth-quit-wait.servic…d until boot process finishes up...
Starting plymouth-quit.service - Terminate Plymouth Boot Screen...
p104p104
Fedora Linux 42 (KDE Plasma Desktop Edition)
Kernel 6.17.0-rc1+ on x86_64 (ttyS0)
apt login: fedora
Password:
Last login: Wed Oct 29 16:12:03 on ttyS0
% fedora@apt ~ ttttyttttytty
/dev/ttyS0
%
```
### 第二種設定的輸出
以下為在 kernel cmdline 加上 `earlycon=uart,io,0x3f8,115200 ignore_loglevel loglevel=8` 的輸出
```
[ 0.000000] printk: debug: ignoring loglevel setting.
[ 0.000000] NX (Execute Disable) protection: active
[ 0.000000] APIC: Static calls initialized
[ 0.000000] efi: EFI v2.9 by American Megatrends
[ 0.000000] efi: ACPI=0x74ce1000 ACPI 2.0=0x74ce1014 SMBIOS=0x794b9000 SMBIOS 3.0=0x794b8000 MEMATTR=0x67710698 ESRT=0x6aa07398 MOKvar=0x79507000 RNG=0x74cc6c18
[ 0.000000] random: crng init done
[ 0.000000] efi: Remove mem59: MMIO range=[0xe0000000-0xefffffff] (256MB) from e820 map
[ 0.000000] e820: remove [mem 0xe0000000-0xefffffff] reserved
[ 0.000000] efi: Remove mem60: MMIO range=[0xf7000000-0xfedfffff] (126MB) from e820 map
[ 0.000000] e820: remove [mem 0xf7000000-0xfedfffff] reserved
...
[ 0.114178] ACPI: DSDT 0x0000000074CB6000 00E321 (v02 ALASKA A M I 01072009 INTL 20230331)
[ 0.124277] ACPI: FACS 0x0000000076CC8000 000040
[ 0.129726] ACPI: SSDT 0x0000000074CD7000 00837C (v02 AMD Splinter 00000002 MSFT 04000000)
[ 0.139835] ACPI: SSDT 0x0000000074CD6000 000221 (v02 ALASKA CPUSSDT 01072009 AMI 01072009)
[ 0.149936] ACPI: FIDT 0x0000000074CCB000 00009C (v01 ALASKA A M I 01072009 AMI 00010013)
...
[ 1.462468] RCU Tasks Trace: Setting shift to 4 and lim to 1 rcu_task_cb_adjust=1 rcu_task_cpu_ids=16.
[ 1.476087] NR_IRQS: 524544, nr_irqs: 1096, preallocated irqs: 16
[ 1.483508] rcu: srcu_init: Setting srcu_struct sizes based on contention.
[ 1.491798] kfence: initialized - using 2097152 bytes for 255 objects at 0x(____ptrval____)-0x(____ptrval____)
[ 1.503802] Console: colour dummy device 80x25
[ 1.509100] printk: legacy console [tty0] enabled
[ 1.514706] printk: legacy bootconsole [uart0] disabled
```
注意到最後的輸出為 `bootconsole disable`。
### 第三種設定的輸出
以下為在 kernel cmdline 加上 `earlycon=uart,io,0x3f8,115200 keep_bootcon ignore_loglevel loglevel=8` 的輸出
```
[ 0.000000] printk: debug: ignoring loglevel setting.
[ 0.000000] NX (Execute Disable) protection: active
[ 0.000000] APIC: Static calls initialized
[ 0.000000] efi: EFI v2.9 by American Megatrends
[ 0.000000] efi: ACPI=0x74ce1000 ACPI 2.0=0x74ce1014 SMBIOS=0x794b9000 SMBIOS 3.0=0x794b8000 MEMATTR=0x67702018 ESRT=0x6aa07398 MOKvar=0x79507000 RNG=0x74cc6c18
[ 0.000000] random: crng init done
[ 0.000000] efi: Remove mem59: MMIO range=[0xe0000000-0xefffffff] (256MB) from e820 map
[ 0.000000] e820: remove [mem 0xe0000000-0xefffffff] reserved
[ 0.000000] efi: Remove mem60: MMIO range=[0xf7000000-0xfedfffff] (126MB) from e820 map
[ 0.000000] e820: remove [mem 0xf7000000-0xfedfffff] reserved
[ 0.000000] efi: Not removing mem61: MMIO range=[0xfee00000-0xfee00fff] (4KB) from e820 map
...
[ 0.344152] ACPI: SSDT 0x0000000074BBB000 00047C (v02 AMD AMDWOV 00000001 INTL 20230331)
[ 0.354350] ACPI: SSDT 0x0000000074BBA000 00044E (v02 AMD AmdTable 00000001 INTL 20230331)
[ 0.364529] ACPI: Reserving FACP table memory at [mem 0x74cd5000-0x74cd5113]
[ 0.372962] ACPI: Reserving DSDT table memory at [mem 0x74cb6000-0x74cc4320]
[ 0.381396] ACPI: Reserving FACS table memory at [mem 0x76cc8000-0x76cc803f]
[ 0.389811] ACPI: Reserving SSDT table memory at [mem 0x74cd7000-0x74cdf37b]
[ 0.398226] ACPI: Reserving SSDT table memory at [mem 0x74cd6000-0x74cd6220]
[ 0.406650] ACPI: Reserving FIDT table memory at [mem 0x74ccb000-0x74ccb09b]
...
[ 1.430906] rcu: RCU calculated value of scheduler-enlistment delay is 100 jiffies.
[ 1.440025] rcu: Adjusting geometry for rcu_fanout_leaf=16, nr_cpu_ids=16
[ 1.448154] RCU Tasks: Setting shift to 4 and lim to 1 rcu_task_cb_adjust=1 rcu_task_cpu_ids=16.
[ 1.458674] RCU Tasks Rude: Setting shift to 4 and lim to 1 rcu_task_cb_adjust=1 rcu_task_cpu_ids=16.
[ 1.469696] RCU Tasks Trace: Setting shift to 4 and lim to 1 rcu_task_cb_adjust=1 rcu_task_cpu_ids=16.
[ 1.483301] NR_IRQS: 524544, nr_irqs: 1096, preallocated irqs: 16
[ 1.490783] rcu: srcu_init: Setting srcu_struct sizes based on contention.
...
[ 3.201268] pci 0000:0e:00.3: [1022:15c0] type 00 class 0x0c0330 PCIe Endpoint
[ 3.210157] pci 0000:0e:00.3: BAR 0 [mem 0xf6c00000-0xf6cfffff 64bit]
...
[ 15.787008] NET: Registered PF_QIPCRTR protocol family
[ 16.090232] Realtek Internal NBASE-T PHY r8169-0-700:00: attached PHY driver (mii_bus:phy_addr=r8169-0-700:00, irq=MAC)
[ 16.260987] RPC: Registered named UNIX socket transport module.
[ 16.262647] r8169 0000:07:00.0 eno1: Link is Down
[ 16.268005] RPC: Registered udp transport module.
[ 16.279407] RPC: Registered tcp transport module.
[ 16.284955] RPC: Registered tcp-with-tls transport module.
[ 16.291485] RPC: Registered tcp NFSv4.1 backchannel transport module.
[ 18.909861] r8169 0000:07:00.0 eno1: Link is Up - 1Gbps/Full - flow control off
```
如果將以上三種情況結合,我們可以得到以下
```
console=ttyS0,115200n8 earlycon=uart8250,io,0x3f8,115200 keep_bootcon ignore_loglevel loglevel=8
```
分成兩個部份來看,第一部份為 `console`,第二部份為 `earlycon`。
### `earlycon`
以下為節錄自 `kernel parameters.txt` 的內容
```
earlycon= [KNL,EARLY] Output early console device and options.
When used with no options, the early console is
determined by stdout-path property in device tree's
chosen node or the ACPI SPCR table if supported by
the platform.
cdns,<addr>[,options]
Start an early, polled-mode console on a Cadence
(xuartps) serial port at the specified address. Only
supported option is baud rate. If baud rate is not
specified, the serial port must already be setup and
configured.
uart[8250],io,<addr>[,options[,uartclk]]
uart[8250],mmio,<addr>[,options[,uartclk]]
uart[8250],mmio32,<addr>[,options[,uartclk]]
uart[8250],mmio32be,<addr>[,options[,uartclk]]
uart[8250],0x<addr>[,options]
Start an early, polled-mode console on the 8250/16550
UART at the specified I/O port or MMIO address.
MMIO inter-register address stride is either 8-bit
(mmio) or 32-bit (mmio32 or mmio32be).
If none of [io|mmio|mmio32|mmio32be], <addr> is assumed
to be equivalent to 'mmio'. 'options' are specified
in the same format described for "console=ttyS<n>"; if
unspecified, the h/w is not initialized. 'uartclk' is
the uart clock frequency; if unspecified, it is set
to 'BASE_BAUD' * 16.
...
keep_bootcon [KNL,EARLY]
Do not unregister boot console at start. This is only
useful for debugging when something happens in the window
between unregistering the boot console and initializing
the real console.
```
先看 `earlycon`。所謂的 early,指的是在還沒有 tty 子系統,沒有 `/dev/ttyS0` 裝置節點的時候,我們想要看到一些訊息,如 boot log 時,就會使用到 `earlycon`。`earlycon` 為一個低階,最小依賴的 polled console。不需要完整的驅動程式,也沒有中斷等等,而是通過 x86 的 `in/out` 指令向 UART 暫存器進行操作。
`earlycon=uart,io,0x3f8,115200 keep_bootcon`
- `earlycon=uart` 表示使用 uart 的 handler,包含 uart 8250/16550A 等等
- `io,0x3f8` 表示這顆 UART 的 base I/O port 是 0x3f8
- `115200` 表示 baudrate
- `keep_bootcon` 阻止 disable bootconsole,保留並繼續輸出訊息
設定 `earlycon` 後,我們就能夠通過 host 看到 target machine 的 early boot log,但是如果和 Target Machine 的 `dmesg` 訊息比較,我們會發現少了非常多訊息。這和有沒有使用 `keep_bootcon` 選項有關,如果使用 `keep_bootcon` 選項,則可以看到與 `dmesg` 相同,完整的開機訊息。
### `console`
接著是 `console=ttyS0,115200n8`,等待後面 serial driver 以及 tty 系統啟動之後,我們就能夠使用更高階的抽象去進行存取了。在 kernel 開機之後,會有一個 console 綁定到 `/dev/ttyS0`,也就是 UART 對應到的裝置節點。設定使用 115200 baudrate, 8 data bits,我們可以通過這個 console 和 target machine 進行互動,如下圖所示

可以看到這個 console 對應到 `/dev/ttyS0`。
## 使用 kdb/kgdb 進行 Debug
### Debug core, kdb, kgdb, gdb
首先釐清一下 debug core, kdb, kgdb, gdb 這四個彼此之間的關係,debug core 為 debug 核心機制,可以當作是一個後端提供服務。而 kdb 與 kgdb 為不同的前端,kdb 為內建在 kernel 中,直接在目標機 console 或是可以把這個 console 輸入到 Host 進行操作,而 kgdb 為實作 GDB Remote Protocol 的 stub,所謂 stub 指的是跑在 Target Machine 上的一小段程式碼,負責處理 GDB Remote Serial Protocol (RSP) 的封包。而跑在 Host 上面的 gdb 通過 RSP 發送指令,發送到 Target Machine 之後在 Target Machine 裡面的 stub 進行解析,藉此實現 Target Machine 與 Host 之間的互動。
### KDB
有兩種方式可以使用 KDB 對 Kernel 進行除錯,第一種是在開機階段修改 kernel cmdline,加入 `kgdboc=ttyS0,115200 kgdbwait`,修改完成後按下 Ctrl + X 以此 kernel cmdline 開始執行,在 Host 便會收到來自於 Target Machine 的 kdb 界面,如下展示
```
Entering kdb (current=0xffff8881009d3080, pid 1) on processor 8 due to NonMaskable Interrupt @ 0xffffffff8152d7e4
[8]kdb> bt
Stack traceback for pid 1
0xffff8881009d3080 1 0 1 8 R 0xffff8881009d55e8 *swapper/0
CPU: 8 UID: 0 PID: 1 Comm: swapper/0 Not tainted 6.17.0-rc1+ #10 PREEMPT(lazy)
Hardware name: ASUS System Product Name/TUF GAMING B650M-PLUS, BIOS 2613 04/12/2024
Call Trace:
<TASK>
dump_stack_lvl+0x5d/0x80
kdb_show_stack+0x6b/0x80
kdb_bt1+0xbb/0x130
kdb_bt+0x377/0x3e0
kdb_parse+0x300/0x560
? srso_alias_return_thunk+0x5/0xfbef5
? kdb_read+0x43c/0x800
kdb_local.isra.0+0x453/0x840
...
kgdboc_probe+0x3a/0x50
platform_probe+0x39/0x70
really_probe+0xdb/0x340
? pm_runtime_barrier+0x55/0x90
? __pfx___device_attach_driver+0x10/0x10
__driver_probe_device+0x78/0x140
driver_probe_device+0x1f/0xa0
__device_attach_driver+0x89/0x110
? srso_alias_return_thunk+0x5/0xfbef5
bus_for_each_drv+0x8f/0xe0
__device_attach+0xb0/0x1c0
bus_probe_device+0x90/0xa0
device_add+0x4fb/0x6f0
platform_device_add+0xe4/0x240
? __pfx_init_kgdboc+0x10/0x10
init_kgdboc+0x41/0x70
do_one_initcall+0x58/0x300
do_initcalls+0x148/0x170
kernel_init_freeable+0xf6/0x140
? __pfx_kernel_init+0x10/0x10
kernel_init+0x1a/0x140
ret_from_fork+0x159/0x190
? __pfx_kernel_init+0x10/0x10
ret_from_fork_asm+0x1a/0x30
</TASK>
```
第二種是在開機之後,設定 kgdboc 通過 `ttyS0`,也就是 serial com。之後我們就能夠在 Host 上面 Debug Kernel,使用以下指令
```shell
$ echo ttyS0,115200 | sudo tee /sys/module/kgdboc/parameters/kgdboc
```
設定完成之前,如果我們輸入 `echo h | sudo tee /proc/sysrq-trigger` 會看到以下
```shell
[ 1726.913678] sysrq: HELP : loglevel(0-9) reboot(b) crash(c) terminate-all-tasks(e) memory-full-oom-kill(f) ...
```
設定完成之後,我們會發現 `dmesg` 中出現了 kgdb 相關訊息,以及再次輸入 `echo h | sudo tee /proc/sysrq-trigger` 會多出其他選項
```shell
[ 1908.082419] KGDB: Registered I/O driver kgdboc
[ 1960.191693] sysrq: HELP : loglevel(0-9) reboot(b) crash(c) terminate-all-tasks(e) memory-full-oom-kill(f) debug(g) ...
```
多出一個 `g` 的選項,使用 `echo g | sudo tee /proc/sysrq-trigger` 會陷入到 kdb 中,陷入到 kdb 中之後,我們可以看到在 host 上面多出一個 kdb 的互動界面,且 Target Machine 凍結,如下圖所示
```
[ 2296.966945] sysrq: DEBUG
Entering kdb (current=0xffff888178b43080, pid 70563) on processor 13 due to NonMaskable Interrupt @ 0xffffffff8152d7e4
[13]kdb>
```
輸入 `go` 之後可讓 Target Machine 恢復執行。
kdb 的指令比起 gdb 有許多不同,如恢復執行是使用 `go` 而非 `c`。中斷點在 gdb 中可以直接使用 `b` 下中斷點,而在 kdb 中有更多的選項,`bl`, `be`, `bd`, `bp` 等等。
### KGDB
Debug Kernel 除了上面直接使用 kdb 以外,也可以直接使用 gdb 完成,讓 gdb 與 kgdb 進行互動。先將帶有 Debug Symbol 的 image,如 vmlinux 複製到 host 上,接著 host 執行以下開始遠端 Debug
```shell
(gdb) gdb vmlinux
(gdb) source vmlinux-gdb.py
(gdb) set serial baud 115200
(gdb) target remote /dev/ttyUSB0
```
同樣在目標機器執行 `echo g | sudo tee /proc/sysrq-trigger` 之後就會陷入到 gdb 中,如下所示
```
(gdb) source vmlinux-gdb.py
(gdb) target remote /dev/ttyUSB0
Remote debugging using /dev/ttyUSB0
kgdb_breakpoint () at kernel/debug/debug_core.c:1214
1214 wmb(); /* Sync point after breakpoint */
(gdb) bt
#0 kgdb_breakpoint () at kernel/debug/debug_core.c:1214
#1 0xffffffff8128a7b0 in __handle_sysrq (key=<optimized out>, check_mask=check_mask@entry=false) at drivers/tty/sysrq.c:611
#2 0xffffffff81e74c19 in write_sysrq_trigger (file=<optimized out>, buf=<optimized out>, count=2, ppos=<optimized out>) at drivers/tty/sysrq.c:1222
#3 0xffffffff818aaf47 in pde_write (pde=0x0, file=<optimized out>, buf=<optimized out>, count=<optimized out>, ppos=<optimized out>) at fs/proc/inode.c:330
#4 proc_reg_write (file=0x67, buf=0x1 <error: Cannot access memory at address 0x1>, count=0, ppos=0x0) at fs/proc/inode.c:342
#5 0xffffffff817e207b in vfs_write (file=file@entry=0xffff88812207ac00, buf=buf@entry=0x7ffc4c5068a0 <error: Cannot access memory at address 0x7ffc4c5068a0>, count=count@entry=2,
pos=pos@entry=0xffffc900077bfe08) at fs/read_write.c:684
#6 0xffffffff817e2613 in ksys_write (fd=<optimized out>, buf=0x7ffc4c5068a0 <error: Cannot access memory at address 0x7ffc4c5068a0>, count=2) at fs/read_write.c:738
#7 0xffffffff8250a696 in do_syscall_x64 (regs=0xffffc900077bff58, nr=1) at arch/x86/entry/syscall_64.c:63
#8 do_syscall_64 (regs=0xffffc900077bff58, nr=1) at arch/x86/entry/syscall_64.c:94
#9 0xffffffff8100012f in entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:121
#10 0x0000557485a844c0 in ?? ()
#11 0x0000557485a844c0 in ?? ()
#12 0x00007ffc4c5068a0 in ?? ()
#13 0x0000000000000002 in ?? ()
#14 0x00007ffc4c506700 in ?? ()
#15 0x0000000000000002 in ?? ()
#16 0x0000000000000202 in ?? ()
#17 0x0000000000000000 in ?? ()
```
接著我們可以執行 `lx-symbols` 後再次執行 `bt` 觀察結果
```
(gdb) lx-symbols
loading vmlinux
warning: File "/home/fedora/XXX/scripts/gdb/vmlinux-gdb.py" auto-loading has been declined by your `auto-load safe-path' set to "$debugdir:$datadir/auto-load:/usr/lib/golang/src/runtime/runtime-gdb.py:/run/media/fedora/68D3-F08C/vmlinux-gdb.py".
scanning for modules in /home/fedora/XXX
loading @0xffffffffa1e5c000: /home/fedora/XXX/drivers/input/misc/uinput.ko
loading @0xffffffffa1e5b000: /home/fedora/XXX/sound/core/seq/snd-seq-dummy.ko
loading @0xffffffffa11ff000: /home/fedora/XXX/sound/core/snd-hrtimer.ko
loading @0xffffffffa1e13000: /home/fedora/XXX/net/sunrpc/sunrpc.ko
loading @0xffffffffa11fd000: /home/fedora/XXX/net/netfilter/nf_conntrack_netbios_ns.ko
loading @0xffffffffa05ff000: /home/fedora/XXX/net/netfilter/nf_conntrack_broadcast.ko
...
(gdb) bt
#0 kgdb_breakpoint () at kernel/debug/debug_core.c:1214
#1 0xffffffff8128a7b0 in __handle_sysrq (key=<optimized out>, check_mask=check_mask@entry=false) at drivers/tty/sysrq.c:611
#2 0xffffffff81e74c19 in write_sysrq_trigger (file=<optimized out>, buf=<optimized out>, count=2, ppos=<optimized out>) at drivers/tty/sysrq.c:1222
#3 0xffffffff818aaf47 in pde_write (pde=0x0 <__pfx_uinput_misc_init>, file=<optimized out>, buf=<optimized out>, count=<optimized out>, ppos=<optimized out>) at fs/proc/inode.c:330
#4 proc_reg_write (file=0x67, buf=0x1 <__pfx_uinput_misc_init+1> <error: Cannot access memory at address 0x1>, count=0, ppos=0x0 <__pfx_uinput_misc_init>) at fs/proc/inode.c:342
#5 0xffffffff817e207b in vfs_write (file=file@entry=0xffff88812207ac00, buf=buf@entry=0x7ffc4c5068a0 <error: Cannot access memory at address 0x7ffc4c5068a0>, count=count@entry=2,
pos=pos@entry=0xffffc900077bfe08) at fs/read_write.c:684
#6 0xffffffff817e2613 in ksys_write (fd=<optimized out>, buf=0x7ffc4c5068a0 <error: Cannot access memory at address 0x7ffc4c5068a0>, count=2) at fs/read_write.c:738
#7 0xffffffff8250a696 in do_syscall_x64 (regs=0xffffc900077bff58, nr=1) at arch/x86/entry/syscall_64.c:63
#8 do_syscall_64 (regs=0xffffc900077bff58, nr=1) at arch/x86/entry/syscall_64.c:94
#9 0xffffffff8100012f in entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:121
#10 0x0000557485a844c0 in ?? ()
#11 0x0000557485a844c0 in ?? ()
#12 0x00007ffc4c5068a0 in ?? ()
#13 0x0000000000000002 in __pfx_uinput_misc_init ()
#14 0x00007ffc4c506700 in ?? ()
#15 0x0000000000000002 in __pfx_uinput_misc_init ()
#16 0x0000000000000202 in ?? ()
#17 0x0000000000000000 in ?? ()
```
### 比較 KDB 與 KGDB 的輸出
在除錯時,常常會使用輸出 backtrace 的功能,而以下為 kdb 以及 kgdb 針對 `hw_access_profiling_start` 的輸出
以下為使用 gdb 連線到 kgdb 的輸出
執行 `lx-symbols` 之前
```
#0 hw_access_profiling_start () at arch/x86/mm/ibs.c:137
#1 0xffffffff8250d3de in set_debugreg (val=0, reg=7)
at ./arch/x86/include/asm/paravirt.h:138
#2 local_db_restore (dr7=0) at ./arch/x86/include/asm/debugreg.h:161
#3 exc_nmi (regs=0xfffffe000000def8) at arch/x86/kernel/nmi.c:596
#4 0xffffffff81001f2d in asm_exc_nmi () at arch/x86/entry/entry_64.S:1406
#5 0xffffffff83b1c460 in acpi_idle_driver ()
#6 0x0000000000000003 in ?? ()
#7 0xffffffff83b1c460 in acpi_idle_driver ()
#8 0xffffffff83b1c5b0 in acpi_idle_driver ()
#9 0xffff88810286d4cc in ?? ()
#10 0xffff88810286d4cc in ?? ()
#11 0x0000000000000000 in ?? ()
```
執行 `lx-symbols` 之後
```
(gdb) bt
#0 hw_access_profiling_start (period=period@entry=10000) at arch/x86/mm/ibs.c:136
#1 0xffffffff825106da in irqentry_nmi_exit (regs=regs@entry=0xfffffe00001aaef8, irq_state=..., irq_state@entry=...) at kernel/entry/common.c:246
#2 0xffffffff8250d3de in exc_nmi (regs=0xfffffe00001aaef8) at arch/x86/kernel/nmi.c:594
#3 0xffffffff81001f2d in asm_exc_nmi () at arch/x86/entry/entry_64.S:1406
#4 0x0000000000000000 in ?? ()
```
`lx-symbols` 可以讓我們重新載入 Linux Kernel 的 symbols 以及目前已經載入核心模組的 symbols。
以下為 kdb 的輸出
```
...
kdb_local.isra.0+0x453/0x840
kdb_main_loop+0xfb/0x240
kdb_stub+0x1a0/0x3e0
kgdb_cpu_enter+0x46a/0xae0
kgdb_handle_exception+0xb1/0xf0
__kgdb_notify+0x2f/0x80
? srso_alias_return_thunk+0x5/0xfbef5
? srso_alias_return_thunk+0x5/0xfbef5
kgdb_ll_trap+0x49/0x70
do_int3+0x2f/0x80
exc_int3+0x8f/0xd0
asm_exc_int3+0x39/0x40
RIP: 0010:hw_access_profiling_start+0x0/0x50
Code: 8b 4c 24 14 48 8b 54 24 08 48 8b 04 24 e9 07 ff ff ff 0f 1f 84 00 00 00 00 00 90 90 90 5
RSP: 0018:fffffe0000347ed0 EFLAGS: 00000083
RAX: ffff889ffb98d000 RBX: ffff889ffb98d000 RCX: 0000000000000004
RDX: 0000000000000001 RSI: 0000000000000000 RDI: 0000000000002710
RBP: fffffe0000347ef8 R08: 000000000006b2dd R09: ffff889f8012cbe0
R10: 0000001cc2d26849 R11: 0000000000000000 R12: 0000000000000000
R13: 0000000000000000 R14: 0000000000000000 R15: 0000000000000000
? hw_access_profiling_start+0x1/0x50
exc_nmi+0xce/0x1a0
end_repeat_nmi+0xf/0x53
RIP: 0010:io_idle+0x3/0x30
Code: 8b 00 a8 08 75 0c e8 3c d3 ff ff 90 fa 0f 1f 44 00 00 e9 7b 17 bf fe 90 90 90 90 90 90 5
RSP: 0018:ffffc900001ffe10 EFLAGS: 00000093
RAX: 0000000000000000 RBX: ffff8881027d4c98 RCX: 0000000000000040
RDX: 0000000000000414 RSI: 000000000000000e RDI: 0000000000000414
RBP: 0000000000000002 R08: ffff8881027d4c00 R09: ffffffff83b1c460
R10: 00000011e523a059 R11: 0000000000000000 R12: ffffffff83b1c548
R13: ffffffff83b1c460 R14: 0000000000000002 R15: 0000000000000000
? io_idle+0x3/0x30
? io_idle+0x3/0x30
</NMI>
<TASK>
acpi_idle_do_entry+0x22/0x50
acpi_idle_enter+0xab/0x180
? srso_alias_return_thunk+0x5/0xfbef5
? ct_kernel_exit.isra.0+0x78/0x100
cpuidle_enter_state+0x84/0x660
cpuidle_enter+0x31/0x50
cpuidle_idle_call+0xf5/0x160
do_idle+0x78/0xd0
cpu_startup_entry+0x29/0x30
start_secondary+0x126/0x170
common_startup_64+0x13e/0x141
</TASK>
```
kdb 的訊息可以看到會分成兩層 call stack 展開,首先是 Debugger 本身的路徑,像是 `do_int3` 之類的,接著後面是 NMI 的 context,接著後面的 TASK 為被 NMI 打斷的 TASK,kdb 看到的是進入到 debugger,以及 NMI context 和 TASK 的 context。而 gdb 看到的訊息是進入到 int 3 之後的 backtrace。
## 參考
- [Kernel parameter: kgdboc](https://www.kernel.org/pub/linux/kernel/people/jwessel/kgdb/ch03s03.html)
- [Using kgdb, kdb and the kernel debugger internals](https://www.kernel.org/doc/html/v5.0/dev-tools/kgdb.html)
- [The kernel’s command-line parameters](https://www.kernel.org/doc/html/v4.14/admin-guide/kernel-parameters.html)