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 逐步调试代码
- 首先在 shell 中输入以下命令,编译用于调试的
bitreverse.cuCUDA 代码:
nvcc -g -G bitreverse.cu -o bitreverse该命令假定源文件名为 bitreverse.cu,-g 为主机代码生成调试信息,-G 为设备代码生成调试信息(这会禁用某些优化,使程序运行变慢,但允许调试),编译时无需额外的编译器选项。
- 在 shell 中输入以下命令启动 CUDA-GDB:
cuda-gdb bitreverse- 设置断点。在此处同时设置 host (
main) 和 GPU (bitreverse) 断点。此外,在 device 函数的特定行 (bitreverse.cu:21) 处设置断点。
(cuda-gdb) break mainBreakpoint 1 at 0x18e1: file bitreverse.cu, line 26.(cuda-gdb) break bitreverseBreakpoint 2 at 0x18a1: file bitreverse.cu, line 8.(cuda-gdb) break 23Breakpoint 3 at 0x18ac: file bitreverse.cu, line 23.- 运行该 CUDA 程序,程序将持续执行,直至触发上一步中设置的首个断点 (
main)。
(cuda-gdb) runStarting 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:2624 int main(void) {- 此时可以输入指令以推进执行或打印程序状态。在本次演示中,我们继续执行直至设备内核启动。
(cuda-gdb) continueContinuing.[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:1212 array[threadIdx.x] = idata[threadIdx.x];CUDA-GDB 检测到已触发 CUDA 设备内核。调试器将打印当前聚焦的 CUDA 线程。Breakpoint 2.2 中的 .2 表示这是断点 2 的子断点位置编号。
- 使用
info cuda threads命令验证目标 CUDA 线程,并在主机线程与 CUDA 线程之间进行切换。
(cuda-gdb) info cuda threads BlockIdx ThreadIdx To BlockIdx To ThreadIdx Count PC Filename LineKernel 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 BlockIdx 和 To 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- 可以打印 block 和 thread 的索引信息:
(cuda-gdb) print blockIdx$1 = {x = 0, y = 0, z = 0}(cuda-gdb) print threadIdx$2 = {x = 0, y = 0, z = 0}- 也可以打印 grid 和 block 的维度:
(cuda-gdb) print gridDim$3 = {x = 1, y = 1, z = 1}(cuda-gdb) print blockDim$4 = {x = 256, y = 1, z = 1}- 推进内核执行并验证部分数据:
(cuda-gdb) next12 array[threadIdx.x] = idata[threadIdx.x];(cuda-gdb) next14 array[threadIdx.x] = ((0xf0f0f0f0 & array[threadIdx.x]) >> 4) |(cuda-gdb) next16 array[threadIdx.x] = ((0xcccccccc & array[threadIdx.x]) >> 2) |(cuda-gdb) next18 array[threadIdx.x] = ((0xaaaaaaaa & array[threadIdx.x]) >> 1) |(cuda-gdb) next
Breakpoint 3, bitreverse <<<(1,1),(256,1,1)>>> (data=0x100000) at bitreverse.cu:2321 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最终输出结果取决于当前该存储位置所存储的内容。
- 由于当前线程
(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]- 删除断点并继续运行程序直至结束:
(cuda-gdb) delete breakpointsDelete all breakpoints? (y or n) y(cuda-gdb) continueContinuing.
Program exited normally.(cuda-gdb)了解 CUDA-GDB 的基本使用后,我们进一步探讨其底层工作原理。这对理解 CUDA-GDB 的限制和排查调试问题很有帮助。
CUDA-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):

断点插入机制
软件断点原理:将目标地址的指令替换为陷阱指令(如 x86 的 INT 3 = 0xCC)。
流程 (mem-break.c:38-71):

执行恢复流程
完整执行流程:
用户输入 "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 thread、cuda device 等等。

CUDA-GDB 的常见问题
CUDA-GDB 是如何访问 GPU 内存的?
当你在 CUDA-GDB 中尝试打印 GPU 内存时,经常会遇到 Cannot access memory at address XXX 错误。本节将深入分析这个错误产生的原因,并揭示 CUDA-GDB 如何与 GPU 通信来访问显存。
错误追踪
首先追踪这个错误信息是怎么打印的。
在 gdb/corefile.c 中的 memory_error_message 函数中创建了这个错误信息字符串。

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

可以定位到是 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 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_statuscuda_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 intcuda_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)参考
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!