Skip to content

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.

Đường kết nối từ gdb tới core CPU qua OpenOCD, debug probe và DAP

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_DEFAULT hoặc tương đương) và CONFIG_GDB_SCRIPTS, không bật CONFIG_DEBUG_INFO_REDUCED. Sau khi build, chạy make scripts_gdb để có các lệnh lx-*.
  • Tắt Kernel Address Space Layout Randomization (KASLR) bằng tham số boot nokaslr hoặc tắt CONFIG_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 nohlt hoặc cpuidle.off=1 khi 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:

openocd -f interface/jlink.cfg -f target/rk3399.cfg

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

rk3399.lcore0 configure -rtos hwthread

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

  1. 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.
  2. Boot board tới Linux như bình thường. Không cần dừng ở bootloader.
  3. Chạy gdb của toolchain tương ứng với kiến trúc (ví dụ aarch64-linux-gnu-gdb hoặc gdb-multiarch) từ thư mục build của kernel, để gdb tự nạp các helper script:

    $ gdb-multiarch vmlinux
    (gdb) target extended-remote :3333
    (gdb) monitor halt
    (gdb) info threads
    (gdb) bt
    (gdb) lx-dmesg
    (gdb) continue
    
  4. Khi kernel đang chạy, nhấn Ctrl-C trong gdb để dừng toàn bộ các core.

  5. Kết thúc bằng detach để kernel chạy tiếp, hoặc monitor 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 ý:

  1. Kết nối gdb và dừng mọi core bằng Ctrl-C hoặc monitor halt.
  2. Chạy info threads để xem PC của từng core, sau đó thread <n> và bt lầ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.
  3. 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.
  4. 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 trong vmlinux là đị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

Tài liệu tham khảo