CUDA-GDB 入门与原理剖析

3426 字
17 分钟
CUDA-GDB 入门与原理剖析

引入: 使用 CUDA-GDB 进行调试#

这里使用官方文档中的 bitreverse 示例演示 CUDA-GDB 的使用方法。 该程序对数据集执行简单的 8 位数据反转操作。

源代码

#include <stdio.h>
#include <stdlib.h>
// Simple 8-bit bit reversal Compute test
#define N 256
__global__ void bitreverse(void *data) {
unsigned int *idata = (unsigned int *)data;
extern __shared__ int array[];
array[threadIdx.x] = idata[threadIdx.x];
// 位反转算法:通过分治策略,依次交换半字节、双位、相邻位
// 例如:0b10110100 → 0b00101101 (反转后)
array[threadIdx.x] = ((0xf0f0f0f0 & array[threadIdx.x]) >> 4) |
((0x0f0f0f0f & array[threadIdx.x]) << 4);
array[threadIdx.x] = ((0xcccccccc & array[threadIdx.x]) >> 2) |
((0x33333333 & array[threadIdx.x]) << 2);
array[threadIdx.x] = ((0xaaaaaaaa & array[threadIdx.x]) >> 1) |
((0x55555555 & array[threadIdx.x]) << 1);
idata[threadIdx.x] = array[threadIdx.x];
}
int main(void) {
void *d = NULL;
int i;
unsigned int idata[N], odata[N];
for (i = 0; i < N; i++)
idata[i] = (unsigned int)i;
cudaMalloc((void **)&d, sizeof(int) * N);
cudaMemcpy(d, idata, sizeof(int) * N, cudaMemcpyHostToDevice);
bitreverse<<<1, N, N * sizeof(int)>>>(d);
cudaMemcpy(odata, d, sizeof(int) * N, cudaMemcpyDeviceToHost);
for (i = 0; i < N; i++)
printf("%u -> %u\n", idata[i], odata[i]);
cudaFree((void *)d);
return 0;
}

使用 CUDA-GDB 逐步调试代码#

  1. 首先在 shell 中输入以下命令,编译用于调试的 bitreverse.cu CUDA 代码:
Terminal window
nvcc -g -G bitreverse.cu -o bitreverse

该命令假定源文件名为 bitreverse.cu-g 为主机代码生成调试信息,-G 为设备代码生成调试信息(这会禁用某些优化,使程序运行变慢,但允许调试),编译时无需额外的编译器选项。

  1. 在 shell 中输入以下命令启动 CUDA-GDB:
Terminal window
cuda-gdb bitreverse
  1. 设置断点。在此处同时设置 host (main) 和 GPU (bitreverse) 断点。此外,在 device 函数的特定行 (bitreverse.cu:21) 处设置断点。
(cuda-gdb) break main
Breakpoint 1 at 0x18e1: file bitreverse.cu, line 26.
(cuda-gdb) break bitreverse
Breakpoint 2 at 0x18a1: file bitreverse.cu, line 8.
(cuda-gdb) break 23
Breakpoint 3 at 0x18ac: file bitreverse.cu, line 23.
  1. 运行该 CUDA 程序,程序将持续执行,直至触发上一步中设置的首个断点 (main)。
(cuda-gdb) run
Starting program: /path/to/bitreverse
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, main () at /path/to/bitreverse.cu:26
24 int main(void) {
  1. 此时可以输入指令以推进执行或打印程序状态。在本次演示中,我们继续执行直至设备内核启动。
(cuda-gdb) continue
Continuing.
[New Thread 0x7ffff1dff000 (LWP 3111508)]
[New Thread 0x7ffff09ff000 (LWP 3111509)]
[Detaching after fork from child process 3111510]
[New Thread 0x7fffe992a000 (LWP 3111532)]
[New Thread 0x7fffe9129000 (LWP 3111533)]
[Switching focus to CUDA kernel 0, grid 1, block (0,0,0), thread (0,0,0), device 0, sm 0, warp 0, lane 0]
CUDA thread hit Breakpoint 2.2, bitreverse<<<(1,1,1),(256,1,1)>>> (data=0x7fffd1e00000) at bitreverse.cu:12
12 array[threadIdx.x] = idata[threadIdx.x];

CUDA-GDB 检测到已触发 CUDA 设备内核。调试器将打印当前聚焦的 CUDA 线程。Breakpoint 2.2 中的 .2 表示这是断点 2 的子断点位置编号。

  1. 使用 info cuda threads 命令验证目标 CUDA 线程,并在主机线程与 CUDA 线程之间进行切换。
(cuda-gdb) info cuda threads
BlockIdx ThreadIdx To BlockIdx To ThreadIdx Count PC Filename Line
Kernel 0
* (0,0,0) (0,0,0) (0,0,0) (255,0,0) 256 0x00007fffd72738b0 bitreverse.cu 12

输出表示当前有 256 个线程,block 索引从 (0,0,0)(0,0,0),thread 索引从 (0,0,0)(255,0,0)To BlockIdxTo ThreadIdx 列显示了线程索引范围的终点。

(cuda-gdb) thread
[Current focus set to CUDA kernel 0, grid 1, block (0,0,0), thread (0,0,0), device 0, sm 0, warp 0, lane 0]
(cuda-gdb) thread 1
[Switching to thread 1 (Thread 0x7ffff7fb0000 (LWP 3108354))]
#0 0x00007ffff226080b in ?? () from /lib/x86_64-linux-gnu/libcuda.so.1
(cuda-gdb) backtrace
#0 0x00007ffff226080b in ?? () from /lib/x86_64-linux-gnu/libcuda.so.1
#1 0x00007ffff201f673 in ?? () from /lib/x86_64-linux-gnu/libcuda.so.1
#2 0x00007ffff205e16b in ?? () from /lib/x86_64-linux-gnu/libcuda.so.1
#3 0x00007ffff2d9dc97 in ?? () from /lib/x86_64-linux-gnu/libcuda.so.1
#4 0x00007ffff2d9e0e5 in ?? () from /lib/x86_64-linux-gnu/libcuda.so.1
#5 0x00007ffff202bdd4 in ?? () from /lib/x86_64-linux-gnu/libcuda.so.1
#6 0x00007ffff2215a4d in ?? () from /lib/x86_64-linux-gnu/libcuda.so.1
#7 0x00007ffff221d8d7 in ?? () from /lib/x86_64-linux-gnu/libcuda.so.1
#8 0x00007ffff221d9c5 in ?? () from /lib/x86_64-linux-gnu/libcuda.so.1
#9 0x00007ffff2db0661 in ?? () from /lib/x86_64-linux-gnu/libcuda.so.1
#10 0x00007ffff1fce74c in ?? () from /lib/x86_64-linux-gnu/libcuda.so.1
#11 0x00007ffff2d5f36a in ?? () from /lib/x86_64-linux-gnu/libcuda.so.1
#12 0x00007ffff1fbea85 in ?? () from /lib/x86_64-linux-gnu/libcuda.so.1
#13 0x00007ffff21761da in ?? () from /lib/x86_64-linux-gnu/libcuda.so.1
#14 0x00007ffff217ecb0 in cuMemcpyDtoH_v2 () from /lib/x86_64-linux-gnu/libcuda.so.1
#15 0x0000555555597099 in libcudart_static_e760a7280bc1af45ef561886abc194823c5edadb ()
#16 0x0000555555565814 in libcudart_static_901c335edfe2e89188fa6b8fe3e2a2194ad05bff ()
#17 0x00005555555c31be in cudaMemcpy ()
#18 0x000055555555ccc4 in main () at /path/to/bitreverse.cu:39
(cuda-gdb) info cuda kernels
Kernel Parent Dev Grid Status SMs Mask GridDim BlockDim Invocation
* 0 - 0 1 Active 0x000000000000000000000000000001 (1,1,1) (256,1,1) bitreverse()
(cuda-gdb) cuda kernel 0
[Switching focus to CUDA kernel 0, grid 1, block (0,0,0), thread (0,0,0), device 0, sm 0, warp 0, lane 0]
12 array[threadIdx.x] = idata[threadIdx.x];
(cuda-gdb) backtrace
#0 bitreverse<<<(1,1,1),(256,1,1)>>> (data=0x7fffd1e00000) at bitreverse.cu:12
  1. 可以打印 block 和 thread 的索引信息:
(cuda-gdb) print blockIdx
$1 = {x = 0, y = 0, z = 0}
(cuda-gdb) print threadIdx
$2 = {x = 0, y = 0, z = 0}
  1. 也可以打印 grid 和 block 的维度:
(cuda-gdb) print gridDim
$3 = {x = 1, y = 1, z = 1}
(cuda-gdb) print blockDim
$4 = {x = 256, y = 1, z = 1}
  1. 推进内核执行并验证部分数据:
(cuda-gdb) next
12 array[threadIdx.x] = idata[threadIdx.x];
(cuda-gdb) next
14 array[threadIdx.x] = ((0xf0f0f0f0 & array[threadIdx.x]) >> 4) |
(cuda-gdb) next
16 array[threadIdx.x] = ((0xcccccccc & array[threadIdx.x]) >> 2) |
(cuda-gdb) next
18 array[threadIdx.x] = ((0xaaaaaaaa & array[threadIdx.x]) >> 1) |
(cuda-gdb) next
Breakpoint 3, bitreverse <<<(1,1),(256,1,1)>>> (data=0x100000) at bitreverse.cu:23
21 idata[threadIdx.x] = array[threadIdx.x];
(cuda-gdb) print array[0]@12
$7 = {0, 128, 64, 192, 32, 160, 96, 224, 16, 144, 80, 208}

@12 是 GDB 语法,表示从 array[0] 开始连续打印 12 个元素。print/x 以十六进制格式显示。

(cuda-gdb) print/x array[0]@12
$8 = {0x0, 0x80, 0x40, 0xc0, 0x20, 0xa0, 0x60, 0xe0, 0x10, 0x90, 0x50,
0xd0}
(cuda-gdb) print &data
$9 = (@global void * @parameter *) 0x10
(cuda-gdb) print *(@global void * @parameter *) 0x10
$10 = (@global void * @parameter) 0x100000

最终输出结果取决于当前该存储位置所存储的内容。

  1. 由于当前线程 (0,0,0) 处理的数据是 0,位反转结果仍然是 0,不够直观。切换到线程 170 可以看到更明显的反转效果(170 的二进制 10101010 反转后为 01010101 即 85)。
(cuda-gdb) cuda thread 170
[Switching focus to CUDA kernel 0, grid 1, block (0,0,0), thread
(170,0,0), device 0, sm 0, warp 5, lane 10]
  1. 删除断点并继续运行程序直至结束:
(cuda-gdb) delete breakpoints
Delete all breakpoints? (y or n) y
(cuda-gdb) continue
Continuing.
Program exited normally.
(cuda-gdb)

了解 CUDA-GDB 的基本使用后,我们进一步探讨其底层工作原理。这对理解 CUDA-GDB 的限制和排查调试问题很有帮助。

CUDA-GDB 底层原理#

首先介绍 GDB 的整体架构层次,如下图所示:

GDB 架构图
GDB 架构图

用户命令层位于最顶层,直接面对用户。当你在 GDB 命令行输入 c (continue) 或 s (step) 时,GDB 的前端逻辑会调用这些函数。这是调试器的高级逻辑,负责解析用户的意图(比如“让程序继续跑”)。

Target 抽象层是 GDB 架构的核心设计,这是 GDB 的目标向量 (Target Vector)机制。 GDB 需要支持多种环境(Linux, Windows, 远程嵌入式板卡等)。为了不让上层代码(用户命令层)关心底层是 Linux 还是 Windows,GDB 定义了一套统一的接口(API)。 无论底层是什么,上层只调用 target_resume(继续运行)、target_wait(等待事件)或 target_read_memory(读内存)。这一层起到了解耦的作用。

Native 目标层是平台特定实现层。因为我们在 Linux 上运行,所以 GDB 会加载 linux_nat_target 这一套具体的代码实现。 它实现了第 2 层定义的抽象接口。例如,当第 2 层调用 target_resume 时,这一层会将其转化为具体的 low_resume() 函数。low_resume() 的核心工作就是去调用操作系统的 ptrace 系统调用。

内核系统调用层包含操作系统内核接口。内容包含 ptrace / waitpid / /proc/PID/mem,这是 Linux 内核提供给用户态程序(GDB)用来调试其他进程的工具箱。

  • ptrace: 最核心的系统调用,允许一个进程(GDB)观察和控制另一个进程(被调试程序)的执行,并检查其内存和寄存器。
  • waitpid: 用于等待子进程(被调试进程)状态改变(比如遇到断点停下来)。
  • /proc/PID/mem: 另一种读写进程内存的高效方式(相比 ptrace 读取内存更快)。

被调试进程,这是你正在调试的那个程序。当 GDB 通过 ptrace(PTRACE_CONT, ...) 发送信号后,内核会解除对该进程的暂停,CPU 继续执行该进程的代码,直到遇到下一个断点或信号。

ptrace 系统调用#

GDB 依赖 ptrace 系统调用, 这是 Linux 提供的进程跟踪系统调用。

关键 ptrace 操作:

请求类型作用使用场景
PTRACE_TRACEME子进程请求被跟踪fork 后子进程调用
PTRACE_CONT继续执行continue 命令
PTRACE_SINGLESTEP单步执行step/next 命令
PTRACE_ATTACH附着到进程attach 命令
PTRACE_DETACH解除附着detach 命令
PTRACE_GETREGS读取寄存器信息收集
PTRACE_SETREGS设置寄存器修改执行状态
PTRACE_PEEKDATA读内存查看变量
PTRACE_POKEDATA写内存插入断点

实际调用示例 (i386-linux-nat.c):

i386_linux_nat_target::low_resume 源码截图
i386_linux_nat_target::low_resume 源码截图

断点插入机制#

软件断点原理:将目标地址的指令替换为陷阱指令(如 x86 的 INT 3 = 0xCC)。

流程 (mem-break.c:38-71):

mem-break.c 断点插入源码截图
mem-break.c 断点插入源码截图

执行恢复流程#

完整执行流程

用户输入 "continue"
proceed() @ infrun.c
├── 设置线程状态
├── 检查是否需要跨越断点
└── insert_breakpoints() - 插入所有断点
resume() @ infrun.c
├── 处理永久断点
├── displaced stepping 准备(可选)
do_target_resume() @ infrun.c
├── target_pass_signals() - 设置可传递的信号
└── target_resume()
linux_nat_target::resume() @ linux-nat.c
├── iterate LWPs
└── resume_lwp() → linux_resume_one_lwp()
low_resume() @ i386-linux-nat.c
└── ptrace(PTRACE_CONT/PTRACE_SINGLESTEP, pid, 0, signal)
进程开始执行...

等待事件机制#

waitpid 捕获进程事件 (linux-nat.c):

同步等待模式:

while (1) {
// 非阻塞 waitpid 检查事件
pid = waitpid(-1, &status, WNOHANG | __WALL);
if (pid > 0) {
// 有事件发生
handle_event(pid, status);
} else {
// 无事件, 等待 SIGCHLD
sigsuspend(&blocked_mask);
}
}

异步等待模式:

  • 注册 SIGCHLD 信号处理器
  • 当目标产生事件时,SIGCHLD 触发
  • 通过 event pipe 通知 GDB 主事件循环

事件处理机制#

进程停止时的事件处理 (handle_inferior_event):

// 当 ptrace 停止进程后,检查原因
switch (ecs->ws.kind ()) {
case TARGET_WAITKIND_STOPPED:
// 检查是否是断点触发
if (breakpoint_inserted_here_p(aspace, pc)) {
// 断点命中!
ecs->ws.set_stopped(GDB_SIGNAL_TRAP);
}
break;
case TARGET_WAITKIND_EXITED:
// 进程退出
break;
case TARGET_WAITKIND_FORK:
// fork 事件
follow_fork();
break;
}

单步执行原理#

硬件单步(使用 CPU 的 Trap Flag):

// PTRACE_SINGLESTEP 让内核设置 TF (Trap Flag)
ptrace(PTRACE_SINGLESTEP, pid, 0, 0);
// CPU 执行一条指令后,因 TF 触发 #DB 异常
// 内核将进程停止,GDB 通过 waitpid 检测到

软件单步(不支持硬件单步的架构):

// 在下一条指令地址插入临时断点
insert_single_step_breakpoint(gdbarch, aspace, next_pc);
// 然后用 PTRACE_CONT 执行
ptrace(PTRACE_CONT, pid, 0, 0);

多线程控制#

Linux Native 目标的 LWP 管理:

struct lwp_info {
ptid_t ptid; // 线程 ID
int stopped; // 是否停止
int resumed; // 是否已恢复
int step; // 是否单步
int last_resume_kind; // 最后恢复类型
target_waitstatus status; // 等待状态
CORE_ADDR stop_pc; // 停止时的 PC
};

停止所有线程:

// 发送 SIGSTOP 给所有线程
for (each LWP) {
if (!lp->stopped) {
ptrace(PTRACE_CONT, pid, 0, SIGSTOP);
lp->stop_requested = true;
}
}
// 等待所有线程停止
while (threads_not_stopped) {
waitpid(-1, &status, __WALL);
}

CUDA-GDB 的扩展#

CUDA-GDB 额外需要控制 GPU 执行。

cuda-linux-nat.c 中有 cuda_is_debugger_initialized 函数检测 CUDA driver 的初始化状态。

cuda-api.c 中提供了控制 GPU 的接口:

CUDA-GDB 在 GDB 的基础上添加了一些指令。

gdb/cuda/cuda-commands 中插入了 CUDA 的相关指令,比如 cuda threadcuda device 等等。

_initialize_cuda_commands 源码截图
_initialize_cuda_commands 源码截图

CUDA-GDB 的常见问题#

CUDA-GDB 是如何访问 GPU 内存的?#

当你在 CUDA-GDB 中尝试打印 GPU 内存时,经常会遇到 Cannot access memory at address XXX 错误。本节将深入分析这个错误产生的原因,并揭示 CUDA-GDB 如何与 GPU 通信来访问显存。

错误追踪#

首先追踪这个错误信息是怎么打印的。 在 gdb/corefile.c 中的 memory_error_message 函数中创建了这个错误信息字符串。

memory_error_message 源码截图
memory_error_message 源码截图

gdb/printcmd.cread_memory_backward 函数中当 target_read_memory 失败时会调用 gdb_printf (_("Cannot access memory at address %s\n"), paddress (gdbarch, memaddr)) 打印错误信息。

read_memory_backward 源码截图
read_memory_backward 源码截图

可以定位到是 target_read_memory 的问题。

GPU 内存访问架构#

GPU 内存访问流程概览

用户 print 命令
target_read_memory()
cuda_nat_linux::xfer_partial()
├── 提取地址类型 (address_class)
└── 调用对应读取函数
cuda_debugapi::read_xxx_memory()
CUDA Driver API
GPU 硬件

CUDA 使用地址的高位来编码地址类型,在 cuda-tdep.c 定义了一些编码地址的变量。

cuda-tdep.c 源码截图
cuda-tdep.c 源码截图

CUDA Debugger API 定义了多种内存存储类型(ptxStorageKind),不同类型有不同的访问要求:

内存类型说明是否需要设备焦点访问函数
ptxGlobalStorage全局内存read_global_memory
ptxConstStorage常量内存read_global_memory
ptxSharedStorage共享内存read_shared_memory
ptxLocalStorage本地内存read_local_memory
ptxParamStorage参数内存read_param_memory
ptxGenericStorage通用地址read_generic_memory
ptxCodeStorage代码内存read_code_memory

设备焦点:指当前调试器关注的 GPU 线程(device、SM、warp、lane)。共享内存、本地内存等线程私有内存必须在该线程的焦点下才能访问。

内存访问核心代码#

当调用 target_read_memory / target_write_memory 最终会调用 cuda_nat_linux::xfer_partial,在其中提取地址类型,然后调用对应的内存读取函数。

核心代码:

enum target_xfer_status
cuda_nat_linux<BaseTarget>::xfer_partial (...)
{
// 如果不是设备焦点,尝试 host 访问
if (!cuda_current_focus::isDevice ())
return BaseTarget::xfer_partial (...);
// 提取地址类型
address_class = gdbarch_address_class_from_core_address (gdbarch, offset);
// 尝试 CUDA 内存访问
if (readbuf)
cuda_read_memory_with_address_class (segment_address, address_class, ...);
else
cuda_write_memory_with_address_class (segment_address, address_class, ...);
// 如果失败,回退到 host 访问
return BaseTarget::xfer_partial (..., hostaddr, ...);
}

cuda_read_memory_with_address_class 核心逻辑(位于 cuda-tdep.c):

static int
cuda_read_memory_with_address_class_1 (...)
{
// Managed/Global 内存:直接通过 Driver API 读取,无需设备焦点
if (cuda_managed_address_p (address) ||
address_class == ptxGlobalStorage ||
address_class == ptxConstStorage)
return cuda_debugapi::read_global_memory (address, buf, len);
// 其他内存类型:需要当前线程焦点
const auto &c = cuda_current_focus::get ().physical ();
switch (address_class) {
case ptxSharedStorage:
// 共享内存:需要 SM 和 warp 信息
return cuda_debugapi::read_shared_memory (c.dev(), c.sm(), c.wp(), address, buf, len);
case ptxLocalStorage:
// 本地内存:需要完整的线程信息(包含 lane)
return cuda_debugapi::read_local_memory (c.dev(), c.sm(), c.wp(), c.ln(), address, buf, len);
case ptxParamStorage:
return cuda_debugapi::read_param_memory (c.dev(), c.sm(), c.wp(), address, buf, len);
case ptxGenericStorage:
// 通用地址:尝试设备内存,失败则回退到 host 映射
if (cuda_debugapi::read_generic_memory (c.dev(), c.sm(), c.wp(), c.ln(), address, buf, len))
return 0;
cuda_debugapi::get_host_addr_from_device_addr (c.dev(), address, &hostaddr);
return 1; // 回退到 host
case ptxCodeStorage:
return cuda_debugapi::read_code_memory (c.dev(), address, buf, len);
}
}

所有内存访问最终通过 CUDA Debugger API 与 GPU 驱动通信(代码位于 cuda-api.c):

// 全局内存读写 (cuda-api.c:1708-1750)
cuda_debugapi::read_global_memory (addr, buf, size)
cuda_debugapi::write_global_memory (addr, buf, size)
// 共享内存 (cuda-api.c:552-575)
cuda_debugapi::read_shared_memory (dev, sm, wp, addr, buf, sz)
// 本地内存 (cuda-api.c:578-608)
cuda_debugapi::read_local_memory (dev, sm, wp, ln, addr, buf, sz)
// 参数内存 (cuda-api.c:525-549)
cuda_debugapi::read_param_memory (dev, sm, wp, addr, buf, sz)
// 通用内存 (cuda-api.c:482-522)
cuda_debugapi::read_generic_memory (dev, sm, wp, ln, addr, buf, sz)

参考#

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

赞助
CUDA-GDB 入门与原理剖析
https://llm-tech.com.cn/posts/cuda-gdb/
作者
Ming
发布于
2026-04-13
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
Ming
你是来找 Ming 学习的吗
🎉 欢迎来到 Ming 的博客
这里是我的个人博客,分享 AI Infra、LLM 等技术内容。欢迎关注交流!
分类
标签
站点统计
文章
8
分类
6
标签
8
总字数
11,954
运行时长
0
最后活动
0 天前

目录