12 - 调试技巧

🎯 学习目标

完成本章后,你将能够:

  • ✅ 使用 GDB 调试 IGT 测试
  • ✅ 分析测试失败的原因
  • ✅ 使用日志和调试输出
  • ✅ 查看内核日志和 GPU 状态
  • ✅ 排查常见问题

🔍 调试工具概览

可用的调试工具

工具 用途 优先级
GDB 源码级调试 ⭐⭐⭐⭐⭐
dmesg 内核日志 ⭐⭐⭐⭐⭐
strace 系统调用跟踪 ⭐⭐⭐⭐
valgrind 内存错误检测 ⭐⭐⭐
perf 性能分析 ⭐⭐⭐
IGT 日志 测试输出 ⭐⭐⭐⭐⭐

📊 IGT 日志系统

日志级别

IGT 提供多个日志级别:

c 复制代码
igt_debug("详细调试信息\n");      // 最详细
igt_info("一般信息\n");            // 正常输出
igt_warn("警告信息\n");            // 可能有问题
igt_critical("严重错误\n");        // 必须关注

设置日志级别

方法 1:环境变量

bash 复制代码
# 显示所有调试信息
IGT_LOG_LEVEL=debug sudo ./build/tests/my_test

# 只显示警告和错误
IGT_LOG_LEVEL=warn sudo ./build/tests/my_test

# 可选值:debug, info, warn, critical, none

方法 2:代码中设置

c 复制代码
igt_main
{
    igt_fixture {
        // 设置为调试级别
        igt_log_level_set(IGT_LOG_DEBUG);
    }
    
    igt_subtest("my-test") {
        igt_debug("这是调试信息\n");
        igt_info("这是普通信息\n");
    }
}

实用的日志宏

c 复制代码
// 打印变量
igt_info("fd = %d\n", fd);
igt_info("buffer size = %zu bytes\n", size);
igt_info("address = %p\n", ptr);

// 条件日志
if (igt_log_level >= IGT_LOG_DEBUG) {
    dump_debug_info();
}

// 断言失败时的信息
igt_assert_f(condition, "Expected %d, got %d\n", expected, actual);

🐛 使用 GDB 调试

基本 GDB 使用

启动调试

bash 复制代码
# 调试整个测试
sudo gdb --args ./build/tests/my_test

# 调试特定子测试
sudo gdb --args ./build/tests/my_test --run-subtest my-subtest

常用 GDB 命令

gdb 复制代码
# 运行程序
(gdb) run

# 在函数设置断点
(gdb) break main
(gdb) break igt_subtest_f

# 在文件的特定行设置断点
(gdb) break my_test.c:42

# 继续执行
(gdb) continue

# 单步执行(进入函数)
(gdb) step

# 单步执行(跳过函数)
(gdb) next

# 查看变量
(gdb) print fd
(gdb) print *version

# 查看调用栈
(gdb) backtrace
(gdb) bt

# 查看源码
(gdb) list

# 退出
(gdb) quit

调试示例

场景:测试在某个子测试中崩溃

bash 复制代码
# 1. 启动 GDB
sudo gdb --args ./build/tests/my_test --run-subtest crash-test

# 2. 设置断点
(gdb) break my_test.c:100

# 3. 运行
(gdb) run

# 4. 程序停在断点
(gdb) print fd
$1 = 3

# 5. 查看调用栈
(gdb) backtrace
#0  my_function () at my_test.c:100
#1  0x... in __igt_subtest_main () at igt_core.c:...
#2  0x... in main () at my_test.c:200

# 6. 继续执行到崩溃
(gdb) continue

# 7. 查看崩溃信息
(gdb) backtrace
(gdb) info registers

条件断点

gdb 复制代码
# 只在 i == 10 时断点
(gdb) break my_test.c:50 if i == 10

# 只在指针非空时断点
(gdb) break my_test.c:60 if ptr != NULL

观察点(Watchpoint)

gdb 复制代码
# 监视变量变化
(gdb) watch fd
(gdb) watch *buffer

# 当变量改变时停止
Hardware watchpoint 1: fd

📋 内核日志分析

查看 dmesg

实时查看

bash 复制代码
# 持续监控内核日志
sudo dmesg -w

# 带时间戳
sudo dmesg -T -w

# 只看错误
sudo dmesg -l err,crit,alert,emerg

过滤 DRM 相关日志

bash 复制代码
# 查看 DRM 日志
sudo dmesg | grep drm

# 查看 i915 驱动日志
sudo dmesg | grep i915

# 查看 amdgpu 驱动日志
sudo dmesg | grep amdgpu

# 实时过滤
sudo dmesg -w | grep -E "drm|i915|amdgpu"

启用内核调试

临时启用

bash 复制代码
# 启用 DRM 核心调试
echo 0x1e | sudo tee /sys/module/drm/parameters/debug

# 调试级别说明:
# 0x01 - CORE
# 0x02 - DRIVER
# 0x04 - KMS
# 0x08 - PRIME
# 0x10 - ATOMIC
# 0x20 - VBL
# 0x1e - 全部(除了 VBL)

启动时启用

bash 复制代码
# 编辑 GRUB 配置
sudo vim /etc/default/grub

# 添加内核参数
GRUB_CMDLINE_LINUX="drm.debug=0x1e"

# 更新 GRUB
sudo update-grub  # Debian/Ubuntu
sudo grub2-mkconfig -o /boot/grub2/grub.cfg  # Fedora

# 重启生效

查看 GPU 错误状态

Intel GPU

bash 复制代码
# GPU 错误状态
sudo cat /sys/class/drm/card0/error

# 解码错误
intel_error_decode < /sys/class/drm/card0/error > decoded_error.txt

# i915 调试信息
sudo cat /sys/kernel/debug/dri/0/i915_error_state
sudo cat /sys/kernel/debug/dri/0/i915_capabilities

AMD GPU

bash 复制代码
# GPU 信息
sudo cat /sys/kernel/debug/dri/0/amdgpu_pm_info
sudo cat /sys/kernel/debug/dri/0/amdgpu_gfx_info

# VRAM 使用
sudo cat /sys/class/drm/card0/device/mem_info_vram_used

🔬 系统调用跟踪

使用 strace

基本用法

bash 复制代码
# 跟踪所有系统调用
sudo strace ./build/tests/my_test

# 只跟踪 ioctl 调用
sudo strace -e ioctl ./build/tests/my_test

# 跟踪文件操作
sudo strace -e open,close,read,write ./build/tests/my_test

# 保存到文件
sudo strace -o trace.log ./build/tests/my_test

分析 DRM ioctl

bash 复制代码
# 跟踪 DRM ioctl
sudo strace -e ioctl ./build/tests/my_test 2>&1 | grep DRM

# 示例输出:
# ioctl(3, DRM_IOCTL_VERSION, ...)
# ioctl(3, DRM_IOCTL_GET_CAP, ...)
# ioctl(3, DRM_IOCTL_MODE_GETRESOURCES, ...)

统计系统调用

bash 复制代码
# 统计调用次数和时间
sudo strace -c ./build/tests/my_test

# 输出示例:
# % time     seconds  usecs/call     calls    errors syscall
# ------ ----------- ----------- --------- --------- ----------------
#  45.67    0.000823          27        30           ioctl
#  23.45    0.000422          14        30           read
#  ...

🧪 内存调试

使用 Valgrind

检测内存泄漏

bash 复制代码
# 基本内存检查
valgrind --leak-check=full ./build/tests/my_test

# 详细输出
valgrind --leak-check=full --show-leak-kinds=all \
         --track-origins=yes ./build/tests/my_test

# 保存到文件
valgrind --leak-check=full --log-file=valgrind.log \
         ./build/tests/my_test

示例输出

复制代码
==12345== HEAP SUMMARY:
==12345==     in use at exit: 4,096 bytes in 1 blocks
==12345==   total heap usage: 100 allocs, 99 frees, 40,960 bytes allocated
==12345==
==12345== LEAK SUMMARY:
==12345==    definitely lost: 4,096 bytes in 1 blocks

IGT 内存检查

IGT 有内置的内存泄漏检测:

c 复制代码
igt_main
{
    igt_fixture {
        // IGT 会自动跟踪 gem_create 等
        fd = drm_open_driver(DRIVER_INTEL);
    }
    
    igt_subtest("leak-test") {
        uint32_t handle;
        
        handle = gem_create(fd, 4096);
        // 忘记 gem_close(fd, handle);  // 会被 IGT 检测到
    }
    
    // IGT 会在这里检查泄漏
}

📈 性能分析

使用 perf

记录性能数据

bash 复制代码
# 记录测试执行
sudo perf record ./build/tests/my_test

# 查看报告
sudo perf report

# 记录特定事件
sudo perf record -e cycles,instructions ./build/tests/my_test

分析热点函数

bash 复制代码
# 生成火焰图需要的数据
sudo perf record -F 99 -g ./build/tests/my_test
sudo perf script > out.perf

# 查看最耗时的函数
sudo perf report --sort=dso,symbol

IGT 性能测量

c 复制代码
#include "igt_stats.h"

igt_subtest("performance") {
    igt_stats_t stats;
    struct timespec start, end;
    
    igt_stats_init(&stats);
    
    for (int i = 0; i < 100; i++) {
        clock_gettime(CLOCK_MONOTONIC, &start);
        
        // 执行操作
        do_operation();
        
        clock_gettime(CLOCK_MONOTONIC, &end);
        
        double elapsed = igt_nsec_elapsed(&start, &end);
        igt_stats_push(&stats, elapsed);
    }
    
    igt_info("平均: %.2f ns\n", igt_stats_get_mean(&stats));
    igt_info("中位数: %.2f ns\n", igt_stats_get_median(&stats));
    igt_info("标准差: %.2f ns\n", igt_stats_get_std_deviation(&stats));
    
    igt_stats_fini(&stats);
}

🔧 常见问题排查

问题 1:测试卡住不动

症状:测试运行后无响应。

调试步骤

bash 复制代码
# 1. 在另一个终端查看进程
ps aux | grep my_test

# 2. 查看进程调用栈
sudo cat /proc/$(pidof my_test)/stack

# 3. 使用 gdb 附加
sudo gdb -p $(pidof my_test)
(gdb) thread apply all bt
(gdb) continue

# 4. 检查是否等待锁
sudo cat /proc/$(pidof my_test)/status | grep State

常见原因

  • 等待 GPU 操作完成
  • 死锁
  • 等待用户输入

问题 2:断言失败

症状Test assertion failure

调试示例

bash 复制代码
# 运行测试并保存输出
sudo ./build/tests/my_test 2>&1 | tee test.log

# 查看断言失败的详细信息
grep "assertion" test.log

# 示例输出:
# (my_test:12345) CRITICAL: Test assertion failure function main
# (my_test:12345) CRITICAL: Failed assertion: ret == 0
# (my_test:12345) CRITICAL: Last errno: 22, Invalid argument

使用 GDB 调试

bash 复制代码
sudo gdb --args ./build/tests/my_test --run-subtest failing-test

(gdb) break igt_assert_failed
(gdb) run

# 当断点触发时
(gdb) backtrace
(gdb) print ret
(gdb) print errno

问题 3:间歇性失败

症状:测试有时通过,有时失败。

调试技巧

bash 复制代码
# 1. 循环运行测试
for i in {1..100}; do
    echo "=== 运行 $i ==="
    sudo ./build/tests/my_test --run-subtest flaky-test || break
done

# 2. 添加详细日志
IGT_LOG_LEVEL=debug sudo ./build/tests/my_test

# 3. 检查竞态条件
# 在代码中添加睡眠来改变时序
igt_subtest("race-test") {
    usleep(100000);  // 100ms
    // 测试代码
}

问题 4:权限被拒绝

症状Permission deniedEACCES

检查清单

bash 复制代码
# 1. 检查设备权限
ls -l /dev/dri/card*

# 2. 检查用户组
groups
id

# 3. 临时添加到 video 组
sudo usermod -aG video $USER

# 4. 或使用 sudo
sudo ./build/tests/my_test

# 5. 检查 SELinux(Fedora/RHEL)
getenforce
sudo setenforce 0  # 临时禁用测试

问题 5:找不到设备

症状No DRM device foundENOENT

检查步骤

bash 复制代码
# 1. 列出 DRM 设备
ls -la /dev/dri/

# 2. 检查驱动加载
lsmod | grep -E "drm|i915|amdgpu|nouveau"

# 3. 加载驱动
sudo modprobe i915
sudo modprobe amdgpu

# 4. 查看内核日志
sudo dmesg | tail -50

# 5. 使用 lspci 查看 GPU
lspci | grep -i vga
lspci -k -s 00:02.0  # 替换为你的 GPU PCI 地址

🛠️ 调试脚本工具

自动调试脚本

bash 复制代码
#!/bin/bash
# debug_igt.sh - IGT 测试调试助手

TEST_NAME=$1
SUBTEST=$2

if [ -z "$TEST_NAME" ]; then
    echo "用法: $0 <test_name> [subtest]"
    exit 1
fi

echo "=== IGT 调试助手 ==="
echo "测试: $TEST_NAME"
echo "子测试: ${SUBTEST:-all}"
echo ""

# 1. 检查测试是否存在
if [ ! -f "./build/tests/$TEST_NAME" ]; then
    echo "❌ 测试不存在: $TEST_NAME"
    exit 1
fi

# 2. 检查设备
echo "=== 检查 DRM 设备 ==="
ls -la /dev/dri/
echo ""

# 3. 检查驱动
echo "=== 检查驱动 ==="
lsmod | grep -E "drm|i915|amdgpu" || echo "未找到 DRM 驱动"
echo ""

# 4. 清空内核日志
sudo dmesg -C

# 5. 运行测试
echo "=== 运行测试 ==="
if [ -n "$SUBTEST" ]; then
    IGT_LOG_LEVEL=debug sudo ./build/tests/$TEST_NAME \
        --run-subtest $SUBTEST 2>&1 | tee test.log
else
    IGT_LOG_LEVEL=debug sudo ./build/tests/$TEST_NAME 2>&1 | tee test.log
fi

TEST_RESULT=$?

# 6. 保存内核日志
echo ""
echo "=== 内核日志 ==="
sudo dmesg > dmesg.log
tail -50 dmesg.log

# 7. 显示结果
echo ""
echo "=== 测试结果 ==="
if [ $TEST_RESULT -eq 0 ]; then
    echo "✅ 测试通过"
else
    echo "❌ 测试失败 (退出码: $TEST_RESULT)"
    echo ""
    echo "日志文件:"
    echo "  - test.log (测试输出)"
    echo "  - dmesg.log (内核日志)"
fi

使用方法:

bash 复制代码
chmod +x debug_igt.sh
./debug_igt.sh my_test
./debug_igt.sh my_test my-subtest

日志分析脚本

bash 复制代码
#!/bin/bash
# analyze_failure.sh - 分析测试失败

LOG_FILE=$1

if [ -z "$LOG_FILE" ]; then
    echo "用法: $0 <log_file>"
    exit 1
fi

echo "=== 分析测试日志: $LOG_FILE ==="
echo ""

# 1. 统计子测试结果
echo "=== 子测试统计 ==="
grep "Subtest.*:" $LOG_FILE | awk '{print $2}' | sort | uniq -c

echo ""

# 2. 查找失败的子测试
echo "=== 失败的子测试 ==="
grep "Subtest.*FAIL" $LOG_FILE

echo ""

# 3. 查找断言失败
echo "=== 断言失败 ==="
grep "Test assertion failure" $LOG_FILE -A 3

echo ""

# 4. 查找警告
echo "=== 警告信息 ==="
grep "WARN" $LOG_FILE | head -10

echo ""

# 5. 查找错误
echo "=== 错误信息 ==="
grep -E "ERROR|CRITICAL" $LOG_FILE | head -10

📚 调试最佳实践

1. 逐步缩小范围

bash 复制代码
# 第一步:确认问题
./build/tests/my_test

# 第二步:找到失败的子测试
./build/tests/my_test --run-subtest problem-test

# 第三步:添加调试输出
IGT_LOG_LEVEL=debug ./build/tests/my_test --run-subtest problem-test

# 第四步:使用 GDB
gdb --args ./build/tests/my_test --run-subtest problem-test

2. 添加调试代码

c 复制代码
igt_subtest("debug-test") {
    igt_info("开始测试\n");
    
    int fd = drm_open_driver(DRIVER_ANY);
    igt_info("fd = %d\n", fd);
    
    drmVersionPtr ver = drmGetVersion(fd);
    igt_info("版本: %s %d.%d.%d\n",
             ver->name,
             ver->version_major,
             ver->version_minor,
             ver->version_patchlevel);
    
    // 测试逻辑...
    
    igt_info("测试结束\n");
}

3. 保存现场

bash 复制代码
# 保存所有调试信息
mkdir -p debug_info
sudo ./build/tests/my_test 2>&1 | tee debug_info/test.log
sudo dmesg > debug_info/dmesg.log
sudo cat /sys/kernel/debug/dri/0/i915_error_state > debug_info/gpu_error.log
lspci -vv > debug_info/lspci.log

4. 比较正常和异常情况

bash 复制代码
# 运行正常的测试并保存日志
./build/tests/working_test 2>&1 > good.log

# 运行失败的测试并保存日志
./build/tests/failing_test 2>&1 > bad.log

# 比较差异
diff -u good.log bad.log

📚 下一步

现在你已经掌握了调试技巧,接下来:

👉 继续阅读13-高级主题 - 学习高级测试技术

或者

  • 实践调试:故意制造错误并调试
  • 阅读内核文档:了解 DRM 调试接口
  • 研究失败的测试:分析实际的测试失败

📎 参考资料

GDB 资源

内核调试

工具文档

IGT 调试

  • lib/igt_core.h - 日志系统
  • lib/igt_debugfs.h - debugfs 访问
  • docs/ - 官方文档