Debug kernel bằng JTAG¶
JTAG cho phép một debugger bên ngoài dừng CPU, đọc thanh ghi và bộ nhớ thông qua khối debug phần cứng của SoC, mà không cần bất kỳ đoạn code nào chạy trong kernel. Trang này trình bày cách dùng JTAG với OpenOCD và gdb để debug Linux kernel trên board, theo thứ tự: các thành phần của hệ thống debug, so sánh với KGDB, chuẩn bị kernel, cấu hình OpenOCD cho SoC nhiều core, phiên debug với gdb, debug kernel treo cứng, các vấn đề về idle và nguồn, và lỗi thường gặp.
Trang này chỉ trình bày góc nhìn debug kernel. Phần nạp và debug firmware cho MCU thuộc tab Embedded (MCU), còn việc bố trí cổng JTAG trên board thuộc tab Hardware Design (Quy trình phát triển sản phẩm phần cứng). Nội dung dựa trên tài liệu OpenOCD (bản phát triển sau 0.12.0, tháng 11/2026) và tài liệu kernel bản 7.4-rc1. Ví dụ dùng SoC Arm 64 bit (ARMv8-A).
Thành phần của hệ thống debug¶
Hệ thống debug bằng JTAG gồm bốn phần, nối với nhau như hình dưới.
| Thành phần | Vai trò |
|---|---|
| Khối debug trên SoC | Mỗi core có thanh ghi debug riêng để dừng, chạy tiếp, step, đặt hardware breakpoint và watchpoint. Trên SoC Arm, các thanh ghi này được truy cập qua Debug Access Port (DAP); khối Cross Trigger Interface (CTI) cho phép dừng và chạy tiếp nhiều core cùng lúc |
| Debug probe | Thiết bị chuyển USB sang tín hiệu JTAG hoặc Serial Wire Debug (SWD, giao thức 2 dây thay cho JTAG trên chip Arm), ví dụ J-Link, CMSIS-DAP, ST-LINK, các mạch dùng chip FTDI |
| OpenOCD | Phần mềm trên host, điều khiển probe, hiểu cấu trúc debug của từng loại core, mở gdb server (mặc định cổng 3333) và cổng telnet cho lệnh của OpenOCD (mặc định cổng 4444) |
| gdb | Nạp vmlinux để có symbol, kết nối tới gdb server của OpenOCD bằng giao thức gdb remote |
OpenOCD không phải lựa chọn duy nhất: các bộ debug thương mại như Lauterbach TRACE32 hay phần mềm đi kèm J-Link cũng đóng vai trò tương tự. Trang này dùng OpenOCD vì là mã nguồn mở và hỗ trợ nhiều loại probe.
So sánh với KGDB¶
| Tiêu chí | JTAG | KGDB |
|---|---|---|
| Code trong kernel | Không cần | Cần CONFIG_KGDB và driver UART hỗ trợ polling |
| Phần cứng | Debug probe và cổng JTAG/SWD trên board | Chỉ cần cổng serial |
| Dừng được từ lúc nào | Từ lệnh đầu tiên sau reset, kể cả boot ROM và bootloader | Khi I/O driver của KGDB đã sẵn sàng |
| Khi kernel treo cứng (tắt ngắt trên mọi CPU) | Vẫn dừng được core và đọc trạng thái | Thường không vào được debugger |
Ngắt bằng Ctrl-C trong gdb |
Có | Không, phải gửi SysRq-G |
| Breakpoint | Hardware breakpoint của core, software breakpoint | Software breakpoint, hardware breakpoint tùy kiến trúc |
| Ảnh hưởng tới hệ thống | Core dừng thật; ngoại vi và timer của SoC vẫn có thể chạy | Kernel tự đưa mọi CPU vào vòng chờ |
| Rủi ro bảo mật | Cổng JTAG mở cho phép đọc toàn bộ bộ nhớ | Cổng serial có KGDB cho phép đọc toàn bộ bộ nhớ kernel |
Chi tiết về KGDB xem ở KGDB và kdb.
Chuẩn bị kernel¶
JTAG không yêu cầu option riêng trong kernel, nhưng gdb chỉ hữu ích khi vmlinux có thông tin debug và địa chỉ thực tế khớp với symbol:
- Bật thông tin debug (
CONFIG_DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULThoặc tương đương) vàCONFIG_GDB_SCRIPTS, không bậtCONFIG_DEBUG_INFO_REDUCED. Sau khi build, chạymake scripts_gdbđể có các lệnhlx-*. - Tắt Kernel Address Space Layout Randomization (KASLR) bằng tham số boot
nokaslrhoặc tắtCONFIG_RANDOMIZE_BASE. Tài liệu QEMU về gdbstub cũng nêu cùng vấn đề này: code bị dời địa chỉ sẽ làm gdb hiểu sai vị trí symbol. - Hạn chế trạng thái idle của CPU bằng
nohlthoặccpuidle.off=1khi cần dừng core thường xuyên. Lý do được trình bày ở mục Idle và nguồn của core. - Tắt hardware watchdog của SoC trong lúc debug, hoặc dùng tính năng dừng watchdog khi core ở chế độ debug nếu SoC có hỗ trợ; nếu không, watchdog có thể reset board khi core đang dừng.
Todo
Bổ sung cách tắt watchdog khi debug cho từng SoC cụ thể (i.MX8M, RK3399, Zynq UltraScale+). Tên và cơ chế phụ thuộc từng SoC, chưa kiểm chứng.
Cấu hình OpenOCD¶
OpenOCD được cấu hình bằng các file Tcl: một file cho probe (interface/*.cfg) và một file cho SoC (target/*.cfg). Thư mục tcl/target/ của OpenOCD đã có sẵn file cho nhiều SoC ARMv8-A, ví dụ bcm2711.cfg (Raspberry Pi 4), imx8m.cfg, imx8mp.cfg, rk3399.cfg, rk3588.cfg, ls1046a.cfg, renesas_rcar_gen3.cfg. Lệnh khởi động có dạng:
Cấu trúc file target cho SoC nhiều core¶
File target mô tả DAP và tạo một target OpenOCD cho mỗi core. Trích phần chính của tcl/target/rk3399.cfg, rút gọn còn một core:
transport select swd
swd newdap $_CHIPNAME cpu -expected-id $_DAP_TAPID -ignore-version
dap create $_CHIPNAME.dap -tap $_CHIPNAME.cpu
target create $_CHIPNAME.ahb mem_ap -dap $_CHIPNAME.dap -ap-num 0
cti create cti0 -dap $_CHIPNAME.dap -baseaddr 0x80038000 -ap-num 1
target create rk3399.lcore0 aarch64 \
-dap $_CHIPNAME.dap -coreid 0 -cti cti0 -dbgbase 0x80030000
target smp rk3399.lcore0 rk3399.lcore1 ...
| Lệnh | Ý nghĩa |
|---|---|
transport select swd |
Chọn SWD thay cho JTAG |
swd newdap, dap create |
Khai báo DAP và ID mong đợi của nó |
target create ... mem_ap |
Target đọc/ghi bộ nhớ hệ thống trực tiếp qua một access port, không cần dừng core |
cti create |
Khai báo CTI của core tại địa chỉ base trong không gian debug |
target create ... aarch64 -dbgbase |
Tạo target cho một core ARMv8-A, với địa chỉ base của thanh ghi debug lấy từ tài liệu của SoC |
target smp |
Gộp các core thành một nhóm SMP |
Với SoC chưa có file sẵn, các địa chỉ -dbgbase và CTI lấy từ Technical Reference Manual (TRM) của SoC, hoặc đọc từ ROM table của khối debug bằng lệnh <tên dap> info của OpenOCD (ví dụ rk3399.dap info).
Trong rk3399.cfg, các core từ thứ hai trở đi được tạo với -defer-examine: OpenOCD bỏ qua bước kiểm tra core đó khi quét chuỗi JTAG và sau reset, cho tới khi gọi arp_examine thủ công. Cách này tránh lỗi khi các core phụ chưa được bật nguồn lúc OpenOCD khởi động.
Chế độ SMP và hwthread¶
Khi các core nằm trong một nhóm target smp, OpenOCD chỉ mở một gdb server cho cả nhóm. Lệnh halt dừng mọi core, lệnh resume cho mọi core chạy tiếp, hardware breakpoint được đặt trên mọi core, và khi một core chạm breakpoint thì gdb hiển thị core đó. Lệnh aarch64 smp off tắt chế độ này để điều khiển từng core riêng, hữu ích trong giai đoạn boot khi mới có một core chạy.
Để gdb thấy từng core như một thread, thêm -rtos hwthread cho target đầu tiên (trong rk3399.cfg dòng này có sẵn nhưng đang bị comment):
Khi đó info threads trong gdb liệt kê trạng thái của từng core, thread <n> chuyển sang core khác, và bt cho backtrace của code đang chạy trên core đó.
OpenOCD cũng có chế độ -rtos linux, dựa vào symbol init_task để liệt kê các task của kernel như thread. Mã nguồn src/rtos/linux.c lưu địa chỉ bằng kiểu 32 bit, nên nhiều khả năng chế độ này chỉ dùng được với kernel 32 bit. Trên kernel 64 bit, dùng hwthread cho các core và các helper script lx-* của kernel cho danh sách task.
Todo
Kiểm chứng -rtos linux của OpenOCD trên kernel arm64; nhận định chỉ hỗ trợ kernel 32 bit ở trên mới dựa vào việc đọc kiểu dữ liệu trong mã nguồn.
Phiên debug với gdb¶
- Khởi động OpenOCD với file interface và target của board. OpenOCD in ra các core đã nhận diện và mở cổng 3333, 4444.
- Boot board tới Linux như bình thường. Không cần dừng ở bootloader.
-
Chạy gdb của toolchain tương ứng với kiến trúc (ví dụ
aarch64-linux-gnu-gdbhoặcgdb-multiarch) từ thư mục build của kernel, để gdb tự nạp các helper script: -
Khi kernel đang chạy, nhấn
Ctrl-Ctrong gdb để dừng toàn bộ các core. - Kết thúc bằng
detachđể kernel chạy tiếp, hoặcmonitor resume.
monitor <lệnh> gửi lệnh thẳng tới OpenOCD; monitor help liệt kê các lệnh. Các lệnh tương tự cũng chạy được qua telnet localhost 4444, ví dụ halt, resume, targets, reg, virt2phys <địa chỉ>.
Breakpoint¶
OpenOCD hỗ trợ cả software breakpoint (thay lệnh trong bộ nhớ bằng lệnh breakpoint) và hardware breakpoint (dùng thanh ghi so sánh địa chỉ của core, số lượng ít). Với kernel, hbreak thường an toàn hơn break:
- Hardware breakpoint không ghi vào bộ nhớ, nên dùng được cho code nằm trong vùng chỉ đọc hoặc code chưa được nạp tại thời điểm đặt breakpoint.
- Trong chế độ SMP, OpenOCD đặt cùng một hardware breakpoint trên mọi core của nhóm, nên breakpoint có hiệu lực bất kể task đang chạy trên core nào.
- Số hardware breakpoint bị giới hạn bởi phần cứng của core. Khi hết, gdb báo lỗi lúc tiếp tục chạy.
Lệnh gdb breakpoint_override hard của OpenOCD buộc mọi lệnh break của gdb thành hardware breakpoint, tiện khi dùng giao diện đồ họa không phân biệt hai loại. Watchpoint (watch, rwatch, awatch) dùng thanh ghi watchpoint của core, ví dụ để tìm code ghi đè lên một biến.
Lệnh aarch64 maskisr on (mặc định) giữ cho ngắt không được xử lý trong lúc single step, tránh trường hợp step nhảy vào handler ngắt thay vì đi tới dòng tiếp theo.
Debug module¶
Cách nạp symbol của module giống như với KGDB: chạy lx-symbols để nạp symbol của các module đang có, đặt breakpoint chờ (pending) vào hàm của module chưa nạp, rồi nạp module trên target. Với JTAG nên dùng hbreak cho breakpoint trong module: hardware breakpoint không phải ghi vào vùng code của module, vốn được đánh dấu chỉ đọc sau khi nạp nếu bật CONFIG_STRICT_MODULE_RWX. Ví dụ đầy đủ xem ở Debug một module.
Debug kernel treo cứng¶
Trường hợp JTAG có lợi thế rõ nhất là kernel treo mà không còn phản hồi trên console, kể cả SysRq. Quy trình gợi ý:
- Kết nối gdb và dừng mọi core bằng
Ctrl-Choặcmonitor halt. - Chạy
info threadsđể xem PC của từng core, sau đóthread <n>vàbtlần lượt trên từng core. Core đang quay vòng trên spinlock hoặc kẹt trong vòng lặp với ngắt bị tắt sẽ lộ ra ở bước này. - Chạy
lx-dmesgđể đọc kernel log buffer trực tiếp từ RAM, kể cả các dòng chưa kịp in ra console. - Dùng
$lx_current(),$lx_per_cpu()và$lx_task_by_pid()để xem task hiện tại và biến per-CPU của từng core.
Khi chỉ cần đọc bộ nhớ mà không muốn dừng core, có thể đọc qua target mem_ap, nhưng địa chỉ dùng ở đó là địa chỉ vật lý, không phải địa chỉ ảo của kernel. Lệnh virt2phys của OpenOCD (chạy khi core đang dừng) chuyển địa chỉ ảo sang địa chỉ vật lý theo bảng trang hiện tại.
Idle và nguồn của core¶
Hai cơ chế tiết kiệm năng lượng của CPU gây khó khăn cho JTAG:
- Lệnh WFI (wait for interrupt): idle loop của kernel thực thi WFI khi không có việc. Tài liệu OpenOCD cảnh báo rằng trên core Arm, WFI tắt clock của core, trong khi khối debug cần clock đó để nhận tín hiệu JTAG, nên lệnh halt có thể không thực hiện được. Cách khắc phục triệt để là không dùng WFI trong lúc debug.
- Tắt nguồn core hoặc cả cluster: tài liệu CoreSight CPU debug của kernel nêu rằng một số thiết kế tắt nguồn cả cluster khi mọi core trong cluster ở trạng thái idle sâu, kể cả phần thanh ghi debug lẽ ra phải luôn có nguồn. Khi đó truy cập thanh ghi debug có thể làm treo bus.
Kernel cung cấp các cách hạn chế idle:
| Cách | Tác dụng |
|---|---|
nohlt (tham số boot, ARM, ARM64, RISC-V và một số kiến trúc khác) |
Idle loop chỉ quay vòng (busy wait), không gọi arch_cpu_idle(), nên không thực thi WFI. Cần CONFIG_GENERIC_IDLE_POLL_SETUP |
cpuidle.off=1 (tham số boot) |
Tắt subsystem cpuidle, không vào các trạng thái idle sâu |
exec 3<> /dev/cpu_dma_latency; echo 0 >&3 |
Yêu cầu độ trễ 0 qua PM QoS, có hiệu lực tới khi đóng file descriptor |
echo 1 > /sys/devices/system/cpu/cpu<N>/cpuidle/state<M>/disable |
Tắt một trạng thái idle cụ thể trên một core |
Sau khi SoC reset hoặc core vừa được cấp nguồn lại, OS Lock và cờ power-down của khối debug có thể đang bật. Lệnh aarch64 dbginit của OpenOCD xóa các cờ này và cấu hình lại CTI; thông thường OpenOCD tự xử lý, chỉ cần gọi lệnh này khi không dừng được core sau reset.
Debug giai đoạn boot¶
JTAG dừng được core ngay sau reset bằng lệnh reset halt của OpenOCD, nên dùng được cho boot ROM, bootloader và đoạn đầu của kernel. Cần lưu ý:
- Trong lúc chạy, bootloader và kernel có thể đổi trạng thái clock, nguồn và chế độ bảo mật của khối debug. Nên dùng
hbreakđể dừng ở các mốc chuyển giai đoạn. - Đoạn code đầu của kernel arm64 (
arch/arm64/kernel/head.S) chạy khi MMU còn tắt, nên PC là địa chỉ vật lý, trong khi symbol trongvmlinuxlà địa chỉ ảo. Breakpoint theo tên symbol chỉ khớp sau khi MMU đã bật; điểm dừng thuận tiện đầu tiên làstart_kernel. - Với SoC nhiều core, giai đoạn này mới có một core chạy. Dùng
aarch64 smp offđể điều khiển riêng core đang chạy thay vì cả nhóm SMP.
Vai trò của JTAG khi debug bootloader được nhắc tới ở trang Bootloader (Embedded). Các giai đoạn boot của kernel xem ở Kernel Boot Process.
Todo
Bổ sung cách debug đoạn code chạy khi MMU còn tắt (nạp symbol với offset bằng add-symbol-file), sau khi thử trên board thật.
CoreSight CPU debug trong kernel¶
Trên SoC Arm có khối CoreSight CPU debug, kernel có driver coresight-cpu-debug dùng chính khối debug này ở chế độ self-hosted (không cần debugger ngoài). Khi kernel panic, driver in ra cho mỗi core giá trị PC được lấy mẫu (EDPCSR), trạng thái nguồn và exception level. Thông tin này giúp biết các core khác đang làm gì tại thời điểm panic, kể cả khi chúng không phản hồi IPI.
Bật lúc boot bằng coresight_cpu_debug.enable=1, hoặc lúc chạy bằng echo 1 > /sys/kernel/debug/coresight_cpu_debug/enable. Driver cần khai báo device tree theo binding arm,coresight-cpu-debug, và cũng chịu cùng ràng buộc về idle như ở mục trên.
Lỗi thường gặp¶
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| OpenOCD không đọc được ID của DAP | Sai dây, sai transport (JTAG hay SWD), tốc độ quá cao, cổng debug bị khóa | Kiểm tra sơ đồ chân, giảm adapter speed, kiểm tra transport select |
halt hết thời gian chờ |
Core đang thực thi WFI hoặc đã tắt nguồn | Boot với nohlt hoặc cpuidle.off=1 |
| Mất kết nối sau một lúc chạy | Cluster bị tắt nguồn khi idle | Hạn chế idle như ở mục Idle và nguồn của core |
| Breakpoint không dừng, symbol lệch địa chỉ | KASLR đang bật | Thêm nokaslr |
break không đặt được hoặc không có tác dụng |
Vùng code chỉ đọc, hoặc code chưa nạp | Dùng hbreak |
| Board tự reset khi đang dừng | Hardware watchdog | Tắt watchdog trong lúc debug |
gdb không có lệnh lx-* |
Chưa chạy make scripts_gdb hoặc gdb từ chối nạp script |
Chạy make scripts_gdb, thêm add-auto-load-safe-path vào ~/.gdbinit |
| Địa chỉ trả về có các bit cao lạ | Pointer authentication của ARMv8.3 | Dùng lệnh <target> pauth on của OpenOCD để gdb nhận mặt nạ bỏ chữ ký |
Liên quan¶
- KGDB và kdb: debugger chạy trong kernel, dùng khi không có JTAG.
- KGDB trên QEMU: lab KGDB trên máy ảo, có ví dụ debug module.
- Kernel Boot Process và Kernel Command Line: giai đoạn boot và các tham số
nokaslr,nohlt,cpuidle.off. - Bootloader (Embedded): dùng JTAG khi lỗi xảy ra trước khi có UART.
- MCU (Embedded): nạp và debug firmware MCU qua SWD/JTAG.
- Quy trình phát triển sản phẩm phần cứng (Hardware Design): boundary scan và bố trí cổng JTAG/SWD trên board.
- Kernel crash dump (kdump): thu vmcore khi panic, không cần probe
Tài liệu tham khảo¶
- OpenOCD User's Guide, đọc từ file
doc/openocd.texitrong kho mã nguồn OpenOCD (commit tháng 11/2026): các mục GDB Configuration, Define CPU targets working in SMP, Breakpoint and Watchpoint commands, ARMv8-A specific commands, GDB and OpenOCD, RTOS Support, Using OpenOCD SMP with GDB; cảnh báo về WFI ở lệnhhalt; cổng mặc định 3333 và 4444. - OpenOCD
tcl/target/rk3399.cfgvàtcl/target/bcm2711.cfg: ví dụ cấu hình SoC ARMv8-A nhiều core. - OpenOCD
src/rtos/linux.c: hiện thực-rtos linux. - Kernel docs: Coresight CPU Debug Module (
Documentation/trace/coresight/coresight-cpu-debug.rst): vấn đề clock và nguồn của khối debug, cách hạn chế idle, cách bật driver. - Kernel docs: kernel-parameters:
nohlt,cpuidle.off. - Kernel docs: Debugging kernel and modules via gdb: helper script
lx-*, dùng được với JTAG. - QEMU docs: GDB usage: mục Relocating code về ảnh hưởng của KASLR.