Skip to content

Debug ứng dụng user space trên board

Ứng dụng user space trên board nhúng thường khó debug hơn trên máy host: binary đã bị strip để tiết kiệm dung lượng, board không đủ tài nguyên để chạy gdb đầy đủ, rootfs có thể ở chế độ chỉ đọc nên không ghi được core file, và lỗi chỉ xuất hiện sau nhiều ngày chạy ngoài hiện trường. Trang này trình bày các công cụ debug user space mà không cần sửa code kernel, theo thứ tự: chuẩn bị symbol và sysroot, debug từ xa bằng gdbserver, thu thập core dump trên board, phân tích core trên host, và theo dõi system call bằng strace.

Cách dùng gdb cơ bản và ví dụ gdbserver tối thiểu nằm ở trang Debug và phân tích (Programming). Debug code kernel nằm ở KGDB và kdump (Linux Kernel).

Debug từ xa bằng gdbserver và phân tích core file

Hai đường debug: gdb điều khiển tiến trình đang chạy qua gdbserver, hoặc phân tích core file sau khi tiến trình crash.

Chuẩn bị symbol và sysroot

Phân công giữa hai máy dựa trên một tính chất của gdbserver: theo man page, binary trên board có thể bị strip vì gdbserver không xử lý symbol, mọi việc liên quan đến symbol do gdb trên host đảm nhận. Host cần giữ một bản binary chưa strip của đúng bản build đang chạy trên board.

Thành phần Nằm ở Yêu cầu
gdb cross Host Chạy trên host nhưng hiểu ABI của target, thường lấy từ SDK hoặc toolchain của dự án
Binary chưa strip Host Cùng bản build với binary trên board, có thông tin debug (-g)
Sysroot Host Bản sao thư viện của target (libc, ld.so), khớp với rootfs trên board
gdbserver, strace Board Có trong image dùng để debug

Binary và core file phải cùng bản build. Build ID của một binary xem bằng readelf -n <binary>, nên lưu cùng với binary chưa strip của mỗi bản phát hành.

Cách đưa các thành phần vào image:

  • Yocto: tài liệu Yocto Project mô tả IMAGE_FEATURE tools-debug (cài các công cụ như gdb, gdbserver, strace) và dbg-pkgs (cài các gói chứa thông tin debug). Gdb cross và sysroot lấy từ SDK của image.
  • Buildroot: các binary trong output/target/ đã bị strip, xem Buildroot. Cần giữ lại bản chưa strip, hoặc tắt strip cho image debug.

Todo

Chưa kiểm chứng trên dự án: tên chính xác các tùy chọn Buildroot cho gdbserver, gdb cross và việc tắt strip. Đối chiếu với manual của đúng phiên bản Buildroot đang dùng.

Debug từ xa bằng gdbserver

Theo man page, gdbserver có ba chế độ chạy:

Chế độ Lệnh trên board Dùng khi
Chạy một chương trình gdbserver <comm> <prog> [args] Debug từ lúc chương trình khởi động
Gắn vào tiến trình đang chạy gdbserver --attach <comm> <pid> Tiến trình đang chạy, cần xem trạng thái hiện tại. Không cần chỉ binary
Nhiều chương trình gdbserver --multi <comm> Host quyết định chạy hoặc gắn chương trình nào, kết nối bằng target extended-remote

<comm> là cổng TCP (host:port) hoặc thiết bị serial. Cổng TCP phải khớp với địa chỉ trong lệnh target remote trên host, và phải khởi động gdbserver trước khi chạy target remote, nếu không gdb báo Connection refused.

gdbserver không có cơ chế xác thực. Khi board nằm trong mạng dùng chung, nên cho gdbserver chỉ lắng nghe trên localhost và đi qua đường hầm SSH:

# trên host: mở đường hầm tới board
ssh -L 2345:localhost:2345 root@<địa-chỉ-board>

# trên board
gdbserver localhost:2345 /usr/bin/app

# trên host, trong gdb
(gdb) set sysroot /path/to/sysroot
(gdb) target remote localhost:2345

Lệnh set sysroot nên chạy trước target remote. Nếu thiếu, gdb phải tải các thư viện từ board qua kết nối và cảnh báo rằng việc này chậm.

Tham số <comm> còn nhận - hoặc stdio, khi đó gdbserver dùng stdin và stdout để giao tiếp. Cách này cho phép gdb chạy gdbserver qua SSH mà không cần mở cổng TCP:

(gdb) target remote | ssh -T root@<địa-chỉ-board> gdbserver - /usr/bin/app

Dừng tiến trình ở breakpoint làm thay đổi thời gian thực thi. Với lỗi phụ thuộc thời gian trên core real-time hoặc luồng poll-mode (ví dụ DPDK), nên dùng core dump hoặc trace (ftrace, perf) thay vì breakpoint.

Core dump trên board

Core dump là ảnh bộ nhớ của tiến trình tại lúc nhận một tín hiệu gây dừng, ví dụ SIGSEGV hoặc SIGABRT. Với thiết bị chạy ngoài hiện trường, đây là cách thu thập nguyên nhân crash mà không cần gdbserver. Theo man page core(5), kernel không tạo core dump trong các trường hợp sau, trong đó nhiều trường hợp gặp thường xuyên trên thiết bị nhúng:

  • Giới hạn RLIMIT_CORE hoặc RLIMIT_FSIZE của tiến trình bằng 0. Giá trị hiện tại xem bằng ulimit -c.
  • Thư mục đích không tồn tại hoặc không ghi được. Đây là trường hợp của rootfs chỉ đọc, xem Root filesystem cho thiết bị nhúng.
  • Filesystem đầy, hết inode, hoặc được mount chỉ đọc.
  • File thực thi không có quyền đọc, hoặc là chương trình set-UID.
  • Kernel được build không có CONFIG_COREDUMP.

Cấu hình

Cấu hình cần ba thành phần: bỏ giới hạn kích thước, chọn thư mục ghi được, và đặt tên file. Tên và vị trí file do /proc/sys/kernel/core_pattern quyết định:

# trên board
mkdir -p /data/cores
ulimit -c unlimited                 # chỉ áp dụng cho shell hiện tại và tiến trình con
echo '/data/cores/core.%e.%p.%t' > /proc/sys/kernel/core_pattern

Các ký hiệu thường dùng trong core_pattern (theo core(5)): %e tên chương trình (cắt còn 15 ký tự), %p PID, %t thời điểm dump (giây từ Epoch), %s số hiệu tín hiệu, %h tên máy. Tên file tối đa 128 byte. Với service chạy dưới systemd, giới hạn kích thước đặt bằng LimitCORE= trong unit thay cho ulimit.

Thư mục ghi core nên nằm trên phân vùng dữ liệu, không nằm trên rootfs chỉ đọc và không nằm trên tmpfs nếu cần giữ core qua lần khởi động lại.

Kiểm soát kích thước và ghi flash

Ứng dụng dùng nhiều bộ nhớ có thể tạo core rất lớn, vừa làm đầy phân vùng dữ liệu vừa tiêu hao số lần ghi của flash. Có hai cách kiểm soát:

  • coredump_filter: file /proc/<pid>/coredump_filter là mặt nạ bit chọn các vùng nhớ được ghi vào core. Giá trị mặc định là 0x33. Bit 0 và 1 là vùng anonymous private và shared, bit 2 và 3 là vùng ánh xạ file, bit 4 là ELF header, bit 5 và 6 là huge page private và shared. Ứng dụng dùng huge page, ví dụ DPDK, có thể loại các bit này để core nhỏ hơn. Giá trị được kế thừa qua fork và giữ nguyên qua execve.
  • Pipe tới chương trình xử lý: nếu ký tự đầu của core_pattern là |, phần còn lại là đường dẫn tuyệt đối của chương trình sẽ nhận core qua stdin. Chương trình chạy bằng quyền root, trong namespace gốc, thư mục hiện tại là /. RLIMIT_CORE không được áp dụng với core đi qua pipe, nên chương trình xử lý tự chịu trách nhiệm giới hạn kích thước.
echo '|/usr/sbin/core-handler %P %e' > /proc/sys/kernel/core_pattern
#!/bin/sh
# /usr/sbin/core-handler: nhận core qua stdin, nén trước khi ghi
# $1 = PID (%P), $2 = tên chương trình (%e)
exec gzip -c > "/data/cores/core.$2.$1.gz"

Ví dụ trên chưa giới hạn số file core. Trên thiết bị thật, handler cần xóa core cũ khi vượt ngưỡng dung lượng.

Để lấy core của tiến trình đang chạy mà không làm nó dừng lại ở lỗi, dùng lệnh gcore <pid> đi kèm gdb.

Phân tích core trên host

Sao chép core về host bằng scp, rồi mở bằng gdb cross cùng binary chưa strip và sysroot. Khai báo sysroot ngay khi khởi động gdb để thư viện dùng chung được tìm đúng khi gdb đọc core:

aarch64-linux-gnu-gdb -ex 'set sysroot /path/to/sysroot' \
    ./app.unstripped /tmp/core.app.1234.1760000000
(gdb) bt                       # backtrace của thread gây lỗi
(gdb) thread apply all bt      # backtrace của mọi thread
(gdb) info sharedlibrary       # kiểm tra thư viện nào gdb không tìm thấy

Nếu backtrace đi qua libc hiển thị ??, nguyên nhân thường là sysroot không khớp với rootfs trên board, hoặc binary không đúng bản build. Đối chiếu Build ID bằng readelf -n.

Theo dõi system call bằng strace

strace hiển thị các system call và tín hiệu của tiến trình, phù hợp khi cần biết ứng dụng đang mở file nào, kết nối tới đâu hoặc bị chặn ở lời gọi nào. Một số tùy chọn thường dùng:

Tùy chọn Tác dụng
-p <pid> Gắn vào tiến trình đang chạy
-f Theo dõi cả tiến trình con và thread
-tt Thêm dấu thời gian chính xác tới micro giây
-o <file> Ghi kết quả ra file thay vì stderr
-e trace=<nhóm hoặc tên> Chỉ theo dõi một số system call, ví dụ openat,connect
strace -f -tt -o /tmp/app.strace -p <pid>

strace làm chậm tiến trình được theo dõi đáng kể vì dừng tiến trình ở mỗi system call. Trên hệ thống có ràng buộc thời gian, nên giới hạn bằng -e trace= và thời gian chạy ngắn.

Liên quan

Tài liệu tham khảo