GDB 使用手册(详细版)

GDB 使用手册(详细版)

面向嵌入式固件调试(RISC-V / 裸机 / core dump 分析)的 GDB 实战文档。

所有标 ✅实测 的结论都在本机 riscv64-unknown-elf-gdb 上跑过,

附真实输出;标 📖手册 的是 GDB 通用行为,未在本机复现(多数需要 live target)。

项 值
文档版本 v1.0
编写日期 2026-09-28
基线 GDB GNU gdb (GDB) 16.2.90.20250210-git(Nuclei RISC-V 工具链自带)
路径 E:/software/nuclei_riscv_gcc/bin/riscv64-unknown-elf-gdb.exe
测试夹具 tools/mk_minicore.py → demo.core(本目录内,可复现)

0. 本机基线:先认清手里这把 GDB

调试工具的行为高度依赖编译配置 。同一句 set logging enabled on、

同一个 info unwinder,在不同 GDB 上命运完全不同。所以先看基线。

bash 复制代码
$ /e/software/nuclei_riscv_gcc/bin/riscv64-unknown-elf-gdb.exe --version
GNU gdb (GDB) 16.2.90.20250210-git
Copyright (C) 2024 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

show configuration 的关键几行(去掉无关项):

复制代码
configure --host=x86_64-w64-mingw32 --target=riscv64-unknown-elf
          --with-expat          <- 支持 XML target description(远程目标自带寄存器布局)
          --without-python      <- ★ 没有 Python!脚本化/pretty-printer/unwinder 全没了
          --without-python-libdir
          --without-debuginfod  <- 没有 debuginfod,符号必须自己喂
          --with-curses
          --enable-tui          <- TUI 可用
          --disable-source-highlight
          --disable-threading

0.1 能力矩阵(全部实跑验证)

能力 结果 说明
架构选择 ✅ 合法参数仅 4 个:riscv / riscv:rv64 / riscv:rv32 / auto
TUI(分屏源码/寄存器) ✅ --enable-tui
core 文件加载 ✅ 见第 12 章,需要 core 自身格式合法
target remote / extended-remote ✅ 走 GDB RSP 协议
target sim ✅ 编译内置模拟器(riscv64-unknown-elf-run 同一套)
record full(反向调试) ✅ 命令存在;裸机远程目标上实际可用性另说
maint packet / set debug remote ✅ 本机最有价值的两个命令,见 §11.4
compare-sections ✅ 校验已经烧进芯片的镜像和本地 ELF 是否一致
dprintf ✅ 动态 printf 断点
frame apply ✅ 批量对多帧执行命令
Python 脚本 ❌ Python scripting is not supported in this copy of GDB.
info unwinder ❌ Undefined info command: "unwinders"
disable unwinder ❌ 被解析成 disable <断点号>:Bad breakpoint number 'unwinder'
checkpoint ❌ Linux fork 快照机制,本工具链没有
info proc mappings ⚠️ 裸机 core 上静默无输出 ,改用 info files(§12.5)
bash 复制代码
# 实测原始输出(这三条务必记住)
$ gdb -q -batch -ex "python print('PY_OK')"
Python scripting is not supported in this copy of GDB.

$ gdb -q -batch -ex "info unwinders"
Undefined info command: "unwinders".  Try "help info".

$ gdb -q -batch -ex "disable unwinder global foo"
Bad breakpoint number 'unwinder'

⚠️ 这条影响你现有的 gdb_core_btfix 项目 :项目 README 里的

(gdb) info unwinder 和 (gdb) disable unwinder global trap-frame

两行在这把 GDB 上是无效命令 。BTW:disable 的子命令只有

breakpoints / display / mem / probes 四个,没有 unwinder。

也就是说------装了也没法列出来、也没法关掉 ,只能靠 bt 结果自己判断是否生效。

结论先行 :这台机器上调试 RISC-V,

能用的只有「命令脚本 + MI + maint packet」这条线,Python 那条线是断的。

所有依赖 Python 的方案(pretty-printer、Python unwinder、btfix.py)在本机不可执行,

只能拿去做「没 Python 时的备选路线」。


1. GDB 到底在调试什么:四个部件

搞清这台 GDB 的边界之后,先建立结构认知。GDB 不是一个程序,是四个部件的组合:

复制代码
┌──────────────────────────────────────────────────────────────┐
│  GDB 前端(你敲命令的地方)                                   │
│  - CLI 命令行  /  TUI 分屏  /  MI 机器接口(给 IDE 用)       │
├──────────────────────────────────────────────────────────────┤
│  符号层:把地址翻译成 函数名/变量名/行号/类型                 │
│  - 来源:ELF 的 .symtab/.debug_*(DWARF)                     │
│  - 没有它 → 全是 `??`,能看寄存器但看不懂意思                  │
├──────────────────────────────────────────────────────────────┤
│  target 层:抽象出的「被调试对象」                             │
│  - target exec    只加载 ELF,不执行                          │
│  - target core    离线快照,★只读★                            │
│  - target remote  通过 RSP 协议连真人(JTAG/OpenOCD/gdbserver)│
│  - target sim     内置模拟器                                  │
├──────────────────────────────────────────────────────────────┤
│  通信层:GDB ↔ 目标 的字节流                                  │
│  - 串口 / TCP / 管道;协议是 GDB RSP(一堆 $...#xx 包)        │
└──────────────────────────────────────────────────────────────┘

三条推论,直接决定你踩不踩坑:

  1. 符号和运行状态是两回事。 file fw.elf 只加载符号不加载内存;
    core-file fw.core 只加载内存快照不加载符号映射。两个都要喂,bt 才有意义。
  2. core 是只读 target。 所有 set/call/断点类操作都会失败,因为底层
    target_store_registers() 在 core 的 target 实现里是空的。这是后面第 12 章的根因。
  3. 远程调试的一切问题,本质上都是 RSP 包的问题。 所以 set debug remote 1
    和 maint packet 是嵌入式场景的终极武器。

2. 该用哪个 GDB

场景 用哪个 理由
调试 RISC-V 裸机 / ELF riscv64-unknown-elf-gdb(本机已有) target 编进了 riscv
同上,但没有交叉 GDB 任意 GDB + set architecture riscv:rv32 本机验证过:无 target 支持时该命令报 Invalid architecture
调试 Linux 用户态进程 发行版 gdb / gdb-multiarch 需要 ptrace、线程、共享库支持
调试嵌入式 Linux gdb-multiarch + gdbserver 双架构(host ARM/MIPS + target)
调试 x86 Windows 程序 MinGW-w64 的 gdb.exe 原生 host,能用 /c/... 之外的 Windows 路径

本机情况:只有一次 GDB(RISC-V 交叉版),宿主 GDB 不存在。

bash 复制代码
$ which gdb gdb.exe
# (无输出)
$ gdb --version
bash: gdb: command not found

所以:本机所有 GDB 操作都必须写全路径或先做别名。

bash 复制代码
# Git Bash 里建议这样(写进 ~/.bashrc)
alias gdb='/e/software/nuclei_riscv_gcc/bin/riscv64-unknown-elf-gdb.exe'

3. 启动方式(6 种)

bash 复制代码
# ① 只加载可执行文件(不运行)
gdb fw.elf

# ② ELF + core,最常用的离线分析组合
gdb fw.elf fw.core

# ③ 附加到正在跑的进程(Linux)
gdb -p 1234

# ④ 批量模式:执行完就退,适合写脚本/CI
gdb -q -batch -ex "bt" -ex "info registers" fw.elf fw.core

# ⑤ 用命令脚本(-x 可叠加多个)
gdb -q fw.elf fw.core -x btfix.gdb -x dump_all.gdb

# ⑥ 带程序参数启动
gdb --args ./a.out arg1 arg2

常用命令行开关:

开关 作用
-q / --quiet 不打印版权横幅(脚本里必加)
-batch 无人值守;出错则退出码非 0
-ex CMD 执行一条命令,可重复
-x FILE 执行脚本文件,可重复
-p PID attach 到进程
-c / --core FILE 指定 core
--args 之后全当被调程序的参数
--interpreter=mi2 切 MI 机器接口(给 IDE/自动化用)
-nx 不读 ~/.gdbinit(排障用:先排除配置污染)

排障第一招 :行为诡异时先加 -nx 再跑一遍。80% 的"GDB 抽风"是你的

.gdbinit 或某个 -x 脚本留下的后遗症。

3.1 路径的坑(✅实测,Windows 专属)

GDB 是原生 Windows 程序,它不认识 Git Bash 的 POSIX 路径:

bash 复制代码
# ❌ 实测失败:GDB 把 /c/... 当成不存在的文件
$ gdb -q -batch -ex "core-file /c/Users/gongyi/x.core"
... No such file or directory.

# ✅ 正确:用 Windows 路径(正斜杠即可,不必转义反斜杠)
$ gdb -q -batch -ex "core-file C:/Users/gongyi/x.core"

同样受影响的还有 :dump binary memory 的输出路径(写 /tmp/x.bin 会

静默写到你没想到的地方,ls /tmp/x.bin 报 No such file ------ 实测)。

规则:凡是喂给 GDB 的路径,一律写 C:/...;只有喂给 bash 的才用 /c/...。


4. 命令速查表

4.1 加载与切换被调试对象

命令 说明
file F / exec-file F 加载可执行文件(exec 只加载,不含符号)
symbol-file F 只加载符号表
add-symbol-file F ADDR 给动态加载的模块/固件补符号,ADDR 是它的加载地址
core-file F 加载 core(等价于 target core F)
target exec F / target core F 以 target 语法加载
target remote HOST:PORT 连接远程 stub(§11)
target extended-remote HOST:PORT 可反复 detach/attach、可跑多进程的远程
target sim 内置模拟器
info files 看当前加载了哪些、内存覆盖范围
info target 同上,格式更详细

4.2 运行控制

命令 缩写 说明
run r 从头开始跑(set args 可改参数)
start --- 在 main 设临时断点并停住
continue c 继续
next n 单步,跨过函数调用
step s 单步,进入函数
nexti / stepi ni / si 汇编级(不跨/跨)
finish fin 跑到当前函数返回
until u 跑到某行/某地址(用于跳出循环)
advance LOC adv 临时断点 + 继续,一步到位
jump LOC j ★直接改 PC 跳转,不改栈,危险
return [EXPR] --- 从当前函数返回,可指定返回值
call FN(args) --- 调用目标里的函数
signal SIG --- 继续并投递信号(如 signal 0 表示忽略)
kill k 结束被调试程序

4.3 断点

命令 说明
break LOC 普通断点;LOC 可为 file:line、func、*ADDR
tbreak LOC 一次性断点,命中即删
hbreak LOC 硬件断点(★Flash/ROM 代码必用)
rbreak REGEX 按正则给一批函数下断(含"未限定的全局函数")
thbreak LOC 一次性硬件断点
break LOC if COND 条件断点
watch EXPR 写监视点(值变化时停)
rwatch EXPR / awatch EXPR 读 / 读写监视点
watch -l EXPR 监视地址而非表达式值(防优化把变量优化掉)
catch syscall NAME 系统调用断点(Linux)
catch throw / catch catch C++ 异常
catch signal SIG 信号断点
catch exec / catch fork / catch vfork 进程事件
dprintf LOC, "fmt", args 动态 printf,不停机
ignore N COUNT 忽略断点 N 的前 COUNT 次命中
commands N ... end 给断点挂命令序列(§5.4)
condition N COND 事后给断点加条件
disable N / enable N 关/开
enable once N / enable delete N 开一次后自动关/自动删
delete N / clear LOC 删断点(clear 按位置删)
info breakpoints 列出所有断点

4.4 栈与帧

命令 说明
backtrace / bt 调用栈
bt 10 / bt -10 只看内侧 10 帧 / 最外侧 10 帧
bt full 附带每帧局部变量(输出极长,重定向到文件)
frame N / f N 切到第 N 帧
up / down 上/下一帧(up 3)
info frame 当前帧详情(含"是否保存了 PC",★关键)
info args / info locals 参数 / 局部变量
info frame 的补充:info registers 该帧的寄存器视图
frame apply all -s bt 对每帧执行命令(GDB 9+)
select-frame / up-silently 不打印的切换,用于脚本

4.5 数据与内存

命令 说明
print EXPR / p 求值打印
p/x p/d p/t p/c p/s p/a 十六进制/十进制/二进制/字符/字符串/地址
p *ptr@LEN 打印数组前 LEN 个元素(人造数组)
p $sp, p $pc, p $x8/s0 伪寄存器
ptype TYPE 看类型定义
ptype /o STRUCT 看结构体布局与洞(padding),GDB 8.2+
info registers 整数寄存器
info all-registers 全部(含浮点/CSR)
info registers $pc 单个
x/FMT ADDR 按地址看内存(§4.6)
set var X = V 改变量
set {int}0x80000000 = 0x1234 按类型改变量(也能写寄存器映射)
set $myvar = ... 自定义便捷变量($ 开头)
display EXPR 每次停机自动打印
undisplay N / disable display N 取消
whatis EXPR / info address SYM 类型 / 符号地址
info symbol ADDR 反向:地址 → 符号(★core 分析常用)
find START, END, VAL 搜内存
dump binary memory FILE START END 把内存导出成文件
restore FILE binary ADDR 反向:把文件写进目标内存
compare-sections 比对目标内存与 ELF 内容
set max-value-size unlimited 解除大对象打印限制

4.6 x 命令的完整语法(最容易被写错的一个)

格式:x/[数量][格式][单位] 地址

符号 格式 符号 单位
x 十六进制 b 字节 (8bit)
d 十进制 h 半字 (16bit)
u 无符号十进制 w 字 (32bit)
o 八进制 g 巨字 (64bit)
t 二进制
a 地址
c 字符
s 字符串
i 汇编(★反汇编用这个)
gdb 复制代码
(gdb) x/4xw 0x80000000     # 4 个 32 位字,十六进制
(gdb) x/10i $pc            # 从 PC 反汇编 10 条指令
(gdb) x/s 0x80001000       # 当字符串打印
(gdb) x/16bx &buf          # 16 个字节

本机实测(core 上的内存读取):

gdb 复制代码
(gdb) x/4xw 0x80000000
0x80000000:	0x564f4841	0x726b645d	0x8e878079	0xaaa39c95

4.7 源码 / 汇编 / TUI

命令 说明
list / l 看源码
list FUNC / list *ADDR 定位
directory DIR / dir 加源码搜索路径(编译机路径对不上时必用)
set substitute-path FROM TO 重写源码路径前缀(/home/build/... → E:/src/...)
info line LOC 行号 ↔ 地址
info source 当前源码文件信息
disassemble / disassemble FN 反汇编
disassemble /r FN 带机器码字节(对比 objdump 时有用)
disassemble /s FN 带源码交错(GDB 8.1+)
set disassemble-next-line on 每次停机自动反汇编下一条
set listsize 30 list 显示行数
tui enable / `layout src asm
`focus cmd src
winheight src +5 调窗口高度
update 刷新 TUI

TUI 快捷键 (tui enable 后):

按键 作用
Ctrl-x a 开/关 TUI
Ctrl-l 重绘(花屏时救命)
Ctrl-x o 切换焦点窗口
Ctrl-x 1 单窗口 / Ctrl-x 2 双窗口
Ctrl-x s 切 single-key 模式(之后 n/s/c 不用回车)

RISC-V 没有 set disassembly-flavor(那是 x86 的 AT&T/Intel 开关)。

本机 set architecture 只接受 riscv / riscv:rv64 / riscv:rv32 / auto。

4.8 多线程 / 多进程 / 多 inferior

命令 说明
info threads 线程列表(* 表示当前)
thread N 切线程
thread apply all bt ★所有线程打栈(死锁/挂死分析起手式)
thread apply all bt full 带上局部变量(输出巨大)
thread apply 1-4 bt 指定范围
`set scheduler-locking on step
set non-stop on 非全停模式(调试多核时有用)
`set follow-fork-mode parent child`
`set detach-on-fork on off`
`set follow-exec-mode same new`
add-inferior / clone-inferior 增/克隆 inferior
inferior N 切 inferior
set print thread-events off 关掉线程创建/退出噪音(嵌入式常关)

4.9 环境与信息

命令 说明
set args A B C / show args 程序参数
set environment VAR=VAL 环境变量
show version / show configuration 版本与编译配置(★排障第一步)
show architecture 当前架构
set architecture riscv:rv32 强制架构
info all-registers 全寄存器
maintenance info sections ★目标的内存段布局(core 上也有效)
info sharedlibrary 共享库加载情况
set sysroot PATH 交叉调试找库的根
set solib-search-path PATH 共享库搜索路径
set debug-file-directory PATH 分离调试符号的目录
apropos WORD ★按关键字搜命令(不记得命令名时用)
help CMD / help CMD SUB 帮助
set pagination off 关分页(脚本必加)
set confirm off 关确认(脚本必加)
set height 0 / set width 0 不限终端尺寸
set logging enabled on / set logging file F 把会话记进文件
set $var = ... 便捷变量

5. 断点体系(深入)

5.1 五种"断"

类型 机制 限制
软件断点 把指令替换成 trap 指令 不能在 Flash/ROM 上用;需要能写内存
硬件断点 hbreak 用 CPU 的断点寄存器 数量有限(RISC-V 通常 2~4 个)
监视点 watch 同样占用硬件资源 数量同样有限;-l 版盯地址更可靠
事件断点 catch GDB 在事件上停 依赖 target 支持(syscall 需 Linux)
动态打印 dprintf 不停机打日志 需要 live target

裸机/Flash 场景第一条纪律 :代码在 Flash 里时,

用 hbreak,而不是 break。软断点会写坏 Flash 内容(或直接写不进去)。

5.2 位置表达式的写法

gdb 复制代码
break main                          # 函数名
break func.c:42                     # 文件:行
break *0x80001234                   # 地址(前面必须带 *)
break *main+16                      # 函数+偏移
break "func.c":42                   # 文件名带特殊字符时加引号
break kernel_init if ctx->state==3  # 条件
break -qualified ns::Cls::fn        # 限定名

5.3 条件与命中计数

gdb 复制代码
(gdb) break process_cmd if lba == 0x1234      # 条件断点
(gdb) condition 2 lba > 0x1000                 # 事后加条件
(gdb) condition 2                              # 清掉条件
(gdb) ignore 3 100                             # 前 100 次命中不停

⚠️ 条件断点的性能陷阱 :条件是在每次命中时由 GDB 求值的 ------意味着

每次命中都要停下、读内存、求值、继续。循环里的条件断点会让程序慢几百倍。

替代方案:

  • 用 dprintf 打印(不停机);
  • 用 ignore 先跳过前 N 次;
  • 在目标代码里自己加判断(改代码比改调试条件快)。

5.4 断点命令序列(自动化的核心)

gdb 复制代码
(gdb) break ftl_write_page
(gdb) commands
> silent                       # 不打印 "Breakpoint 1, ..."
> printf "lba=%#x len=%u\n", lba, len
> if len > 64
>   bt 5
> end
> continue
> end

要点:

  • silent 放在第一条,否则每次命中都先刷一大段提示;
  • printf 里的 %#x 是十六进制带 0x 前缀(GDB 自己的 printf 方言);
  • 以 end 结尾(脚本文件里也一样)。

5.5 保存/复用断点

gdb 复制代码
(gdb) save breakpoints my.bp      # 存盘
(gdb) source my.bp                # 下次一键恢复

6. 反向调试(时间倒流)

gdb 复制代码
(gdb) record full                 # 开始记录(性能大降,内存涨)
(gdb) continue
(gdb) reverse-continue            # 倒着跑
(gdb) reverse-step                # 倒着单步
(gdb) reverse-stepi               # 倒着单步(汇编)
(gdb) record stop                 # 停止记录,释放
(gdb) info record                 # 看记录了多少条
要点 说明
只对 live target 有效 core 是快照,没有时间轴
record full 会显著变慢 别在长跑场景一直开着
内存有限 默认记录上限可调 set record full insn-number-max
record btrace 需要 Intel PT / ARM CoreSight,RISC-V 一般没有

真实价值场景:"这个变量什么时候被改坏的?"

→ watch var + record full + 命中后 reverse-continue

→ 直接落到上一次修改点的前一条指令。


7. 调用栈与帧(含"栈断了"的处置)

7.1 基本功

gdb 复制代码
(gdb) bt
#0  ftl_write_page (lba=291, len=8) at ftl_write.c:412
#1  0x80005678 in ufs_write_handler () at ufs_cmd.c:88
#2  0x80001234 in main () at main.c:31

(gdb) frame 1                     # 切到第 1 帧
(gdb) info args                   # 看参数
(gdb) info locals                 # 看局部变量
(gdb) info frame                  # 看这一帧的元信息

7.2 info frame 输出怎么读(★这是诊断栈的关键)

本机实测(core,无符号):

gdb 复制代码
(gdb) info frame
Stack level 0, frame at 0x0:
 pc = 0x80001234; saved pc = <not saved>
 Outermost frame: outermost
 Arglist at unknown address.
 Locals at unknown address, Previous frame's sp in sp

逐行解读:

行 含义 异常信号
frame at 0x0 该帧的栈基址 0x0 = GDB 找不到有效栈帧
pc = ...; saved pc = <not saved> 本帧 PC / 上一帧的返回地址 <not saved> = 没法往上回 ← 栈断的根因
Outermost frame: outermost GDB 认为这是最外层 明明在函数中间却说是最外层 = 明显错误
Arglist at unknown address 参数地址未知 缺 DWARF / 缺 CFI
Locals at unknown address 局部变量地址未知 同上

推论:bt 只能走到这一帧,是因为 GDB 不知道"上一帧的 PC 保存在哪"。

这就是 Backtrace stopped: frame did not save the PC 的含义。

7.3 栈断了的三种修法

方案 做法 优点 缺点 本机可用
A. 编译期根治 加 CFI / 保留帧指针 一次修好,永久有效 涨 FLASH ✅
B. GDB Python unwinder 注册规则告诉 GDB 帧信息在哪 不改固件、不改 core 要 GDB 有 Python ❌ 本机没 Python
C. 离线改 core 改 core 里 NT_PRSTATUS 的寄存器块 不改固件、不需要 Python 只挪了起点,治标 ✅

A 的具体做法(按模块开,注意体积代价):

makefile 复制代码
CFLAGS += -funwind-tables           # 生成 .eh_frame(GDB 才能做 CFI unwinding)
CFLAGS += -fno-omit-frame-pointer   # 保留 s0 帧指针链(最省事的兜底)

链接脚本必须保留:

复制代码
KEEP(*(.eh_frame))
KEEP(*(.eh_frame_hdr))

⚠️ 编译时把 ELF 和 core 都喂给 GDB :gdb fw.elf fw.core。

因为 CFI unwinder 要靠 prologue 扫描读 .text 指令,

不给 ELF,GDB 读不到指令,.eh_frame 也白给。

7.4 core 上为什么不能 set $pc(★根因 + 实测)

gdb 复制代码
(gdb) set $pc = 0x80009999
You can't do that without a process to debug.
(gdb) info registers pc
pc             0x80001234	0x80001234      <- 没改成功

根因 :set $reg 最终走 target_store_registers(),而 core 的 target 是只读的。

结论 :"改寄存器挪 bt 起点"只在 live 调试下可用

(JTAG / OpenOCD / qemu -s / 模拟器),core 场景必须换路走(上表 B 或 C)。

7.5 方案 C 的实现要点(离线改 core 寄存器)

RISC-V 的 core 里,寄存器排布遵循 GDB 约定:

复制代码
reg[0]=pc   reg[1]=ra   reg[2]=sp   reg[3]=gp   reg[4]=tp
reg[5..7]  = t0..t2
reg[8]     = s0/fp      reg[9] = s1
reg[10..17]= a0..a7
reg[18..27]= s2..s11
reg[28..31]= t3..t6
  • 共 32 个字 ,x0 不占位置 (所以 reg[1] 就是 ra,不是 x1 的错位);
  • 32 位 elf_prstatus 里 pr_reg 的偏移 = 72;
  • 结构体总长 = 72 + 32×4 + 4(pr_fpvalid) = 204。

这段结论已用本机 GDB 反向验证 :tools/mk_minicore.py 按上述偏移写入

pc/ra/sp/s0 后,GDB 读出完全一致(§12.6 有完整记录)。

7.6 RISC-V 特有的栈回溯难点

难点 说明
无帧指针约定 RISC-V ABI 不强制 s0 作 FP,编译器默认可能省掉
ra 会被覆盖 非叶函数压栈保存 ra,但没有 CFI 时 GDB 不知道压在哪
尾调用优化 -O2 会把 call 变成 j,栈上少一层,回溯结果"少帧"是对的
中断/trap trap entry 手工保存现场,编译器 CFI 完全对不上 → 栈必断
多处 -mcmodel / 链接脚本 .eh_frame 未 KEEP 时被 GC 掉,DWARF 在也救不了

8. 变量、表达式与内存

8.1 打印的讲究

gdb 复制代码
(gdb) p/x reg                         # 十六进制
(gdb) p *(struct ftl_ctx *)0x80002000 # 按结构体解引用
(gdb) p *lun_ptr@8                    # 当数组看 8 个元素
(gdb) p pa(ce, plane, block, page)    # 调用宏?不行,宏在编译期就没了
(gdb) p/d 0b1010                      # 二进制字面量 GDB 认
(gdb) set $i = 0                      # 便捷变量
(gdb) p $i++                          # 每次打印自增 1
(gdb) p $1                            # 值历史:上一次、上上次的 print 结果
(gdb) p $_                            # x 命令最后一次看的地址

宏不能打印 (p SOME_MACRO 会报 No symbol "SOME_MACRO")。

变通:编译时加 -g3,GCC 会把宏定义放进 DWARF,GDB 就能 info macro / macro expand。

8.2 打印相关设置(输出又长又乱时的救命设置)

gdb 复制代码
set print pretty on            # 结构体缩进多行(默认 off,实测可开)
set print elements 0           # 数组/字符串不限长度(0 = unlimited)
set print array on             # 数组每个元素一行
set print null-stop on         # 遇 \0 停(字符串)
set print object on            # C++ 按动态类型打印
set print demangle on          # C++ 名字反修饰
set print repeats 0            # 不折叠重复元素
set max-value-size unlimited   # 解除大对象 64KB 限制
set pagination off             # 关分页

8.3 内存读写

gdb 复制代码
(gdb) x/8xw 0x80000000                 # 读
(gdb) set {unsigned int}0x80000000 = 0xdeadbeef   # 写(live target)
(gdb) set {char}0x80000000 = 1
(gdb) find 0x80000000, 0x8000ffff, 0x1a2b3c4d      # 搜索
(gdb) dump binary memory ram.bin 0x80000000 0x80001000   # 导出
(gdb) restore ram.bin binary 0x80000000            # 写回

⚠️ 写内存的前提 :set mem inaccessible-by-default off。

默认情况下 GDB 拒绝访问"未知区域",裸机上几乎所有地址都是"未知区域",

所以这句在嵌入式调试里几乎是必配。

8.4 寄存器

gdb 复制代码
(gdb) info registers                   # 整数寄存器
(gdb) info all-registers               # 全部(含 CSR / 浮点)
(gdb) info registers pc ra sp s0       # 指定几个(可混用 ABI 名与 x 名)
(gdb) p $pc                            # 当值用
(gdb) set $sp = 0x8000ff00             # live 才行;core 上必失败
(gdb) info registers $mstatus          # CSR(需要 target description 支持)

本机实测(core 加载后):

gdb 复制代码
(gdb) info registers pc ra sp s0
pc             0x80001234	0x80001234
ra             0x80005678	0x80005678
sp             0x8000ff00	0x8000ff00
s0             0x8000ff00          0x8000ff00

9. 远程 / 嵌入式调试(JTAG、OpenOCD、gdbserver)

9.1 三条通路对比

通路 连接方式 典型场景
OpenOCD + JTAG target extended-remote localhost:3333 真实芯片,可下 Flash、可复位
gdbserver target remote TARGET_IP:2345 嵌入式 Linux 用户态
QEMU / 模拟器 target remote :1234 / target sim 无硬件时的功能验证

9.2 OpenOCD 标准起手式

bash 复制代码
# 终端 1:起 OpenOCD(以 RISC-V 为例,配置文件按你的调试器改)
openocd -f interface/ftdi/xxx.cfg -f target/riscv.cfg
gdb 复制代码
# 终端 2:
(gdb) file fw.elf
(gdb) set mem inaccessible-by-default off
(gdb) target extended-remote localhost:3333
(gdb) monitor reset halt               # ★monitor = 给 OpenOCD 发命令
(gdb) monitor flash write_image erase fw.bin 0x80000000
(gdb) load                             # 或用 GDB 自己的 load
(gdb) compare-sections                 # ★校验烧进去的和本地 ELF 是否一致
(gdb) hbreak main                      # Flash 里必须用硬件断点
(gdb) continue

9.3 monitor 与 maint packet 的区别

命令 发给谁 用途
monitor CMD gdbserver / OpenOCD(GDB 之外的远端代理) 复位、烧 Flash、查目标状态
maint packet HEX 直接构造 RSP 包 ★绕过 GDB 逻辑,直接问 stub"你会吗"
gdb 复制代码
(gdb) monitor help                     # 先看远端支持什么(实测命令存在)
(gdb) maint packet qSupported          # 问 stub 支持哪些包
(gdb) maint packet qXfer:features:read:target.xml:0,fff   # 取 target description

9.4 调试远程协议本身(嵌入式排障王牌)

gdb 复制代码
(gdb) set debug remote 1               # 打印每一次 RSP 包收发
(gdb) set debug remote 0               # 关掉

连接失败时它能直接告出"哪个包没被支持":

复制代码
Sending packet: $qSupported:multiprocess+;...:...#xx...Ack
Packet received: PacketSize=1000;qXfer:features:read+;...

典型症状 → 结论对照:

现象 看包得到的结论
连上了但 info registers 全是 0 stub 没实现 g 包,或不认识 arch
断点设不上 没实现 Z0(软件断点)/ Z1(硬件断点)
单步卡死 s 包被 stub 当成了 continue
读 Flash 失败 m 包返回 EXX,通常是 stub 不允许读该区域

9.5 远程相关的重要设置

gdb 复制代码
set remotetimeout 20                   # 远端慢/Flash 编程时调大(默认 2 秒易超时)
set remote hardware-breakpoint-limit 4 # 告诉 GDB 硬件断点上限
set remote memory-read-packet-size 0x400
set remote memory-write-packet-size 0x400
set sysroot /path/to/target-root       # 交叉环境找库
set solib-search-path /path/to/libs

9.6 裸机 / 无 OS 场景的必配清单

gdb 复制代码
set confirm off
set pagination off
set height 0
set mem inaccessible-by-default off    # ★最关键
set print thread-events off
set remotetimeout 20
set architecture riscv:rv32            # 若 GDB 认不出目标架构
set $sp = 0x8000ff00                   # 手工设栈(有时 stub 不给)

10. 脚本化:命令脚本、MI、以及"没有 Python 怎么办"

10.1 命令脚本(本机唯一可用的自动化通道)

dump.gdb:

gdb 复制代码
set pagination off
set confirm off
set print pretty on
set logging file gdb_session.log
set logging enabled on

echo \n===== REGISTERS =====\n
info registers

echo \n===== BACKTRACE =====\n
bt full

echo \n===== SECTIONS =====\n
maintenance info sections

echo \n===== STACK MEM (256B) =====\n
x/64xw $sp

set logging enabled off
quit
bash 复制代码
gdb -q -batch fw.elf fw.core -x dump.gdb

10.2 自定义命令(define)

gdb 复制代码
define ctxdump
  silent
  printf "--- ctx @ %p ---\n", $arg0
  set $c = (struct ftl_ctx *)$arg0
  printf "state=%d  lba=%#x  err=%d\n", $c->state, $c->lba, $c->err
  printf "pc=%#x sp=%#x\n", $pc, $sp
end

document ctxdump
  用法: ctxdump ADDR   打印 FTL 上下文
end

define walk_list
  set $p = $arg0
  set $n = 0
  while $p != 0 && $n < 32
    printf "[%2d] %p\n", $n, $p
    set $p = ((struct node *)$p)->next
    set $n = $n + 1
  end
  printf "total %d nodes\n", $n
end

要点:

语法 说明
$arg0 $arg1 ... 位置参数(最多 $arg9,再多用 $argc/$argv 不便,改用脚本文件)
$argc 参数个数
silent 不打印 define 调用回显
while ... end / if ... else ... end 控制流
document NAME ... end 写 help NAME 时显示的说明(强烈建议写)
define 放在 .gdbinit 里 每次启动自动可用
hook-stop ★每次停机自动执行的钩子

hook-stop 实战(每次停机自动打印关键状态):

gdb 复制代码
define hook-stop
  printf ">> pc=%#010x ra=%#010x sp=%#010x\n", $pc, $ra, $sp
  x/2i $pc
end

10.3 .gdbinit 自动加载与安全路径

GDB 只会在"安全路径"内自动加载 .gdbinit:

gdb 复制代码
(gdb) show auto-load safe-path
(gdb) set auto-load safe-path /            # 打开所有(本地开发方便,生产慎用)
set auto-load local-gdbinit on

10.4 用 -batch + 退出码做 CI/回归

bash 复制代码
gdb -q -batch fw.elf fw.core \
    -ex "bt" \
    -ex "info registers" \
    -ex "quit" > out.txt 2>&1
echo "退出码=$?"          # 命令失败时非 0,可直接卡 CI

10.5 MI 接口(给 IDE / 自动化用)

bash 复制代码
gdb -q --interpreter=mi2 fw.elf
复制代码
=thread-group-added,id="i1"
~"Reading symbols from fw.elf...\n"
(gdb)
-break-insert main
^done,bkpt={number="1",type="breakpoint",...}
-exec-run
*stopped,reason="breakpoint-hit",bkptno="1",frame={func="main",args=[],"file":"main.c",line":31}

特点:^done / *stopped / =notify / ~"文本" / &"日志"。

VS Code 的 cppdbg、CLion、Eclipse 走的都是这条线。

"GDB 没有 Python"时,用 MI + 自己写解析器,一样能做自动化。

10.6 没有 Python 时的替代方案(本机适用)

本该用 Python 做的事 没 Python 的替代
pretty-printer 手写 define + printf 拼输出
Python unwinder 修栈 离线改 core 寄存器块(§7.5)
遍历复杂数据结构 define 里的 while 循环 + $ 便捷变量
批量处理多个 core bash 循环 + -batch -x script.gdb
复杂格式化输出 printf + echo,或输出到 log 后用外部脚本处理

架构建议 :把「GDB 只负责取原始数据」+「外部脚本负责分析」分开。

用 dump binary memory 把关键内存导出来,用 Python 在外面解析------

这条路不依赖 GDB 的 Python,且可复现、可版本管理。


11. 推荐 .gdbinit

gdb 复制代码
# ---- 基础体验 ----
set pagination off
set confirm off
set print pretty on
set print elements 0
set print array on
set print demangle on
set print thread-events off

# ---- 裸机必备 ----
set mem inaccessible-by-default off
set remotetimeout 20

# ---- 源码路径重写(按自己环境改)----
# set substitute-path /home/build/fw E:/code/ufs2.2
# directory E:/code/ufs2.2/src
# directory E:/code/ufs2.2/include

# ---- 常用快捷命令 ----
define regs
  printf "pc=%#010x  ra=%#010x  sp=%#010x  s0=%#010x\n", $pc, $ra, $sp, $s0
  printf "a0=%#010x  a1=%#010x\n", $a0, $a1
end

define ihere
  x/12i $pc
end

define stack32
  x/32xw $sp
end

define xx
  x/$arg0
end

# ---- 每次停机自动显示 ----
# define hook-stop
#   regs
# end

12. core dump 分析(重点章节)

12.1 先明确:core 是"只读快照"

能做的事 不能做的事
看寄存器、看内存、看栈 set $reg / set var
bt / frame / info locals 下断点 / continue / next
反汇编(需喂 ELF) call 函数
导出内存、搜索 record 反向调试

12.2 生成 core 的三种途径

途径 做法
Linux 内核 ulimit -c unlimited;/proc/sys/kernel/core_pattern 决定路径
gdbserver (gdb) generate-core-file fw.core
自研(你的 panic_to_core_file.py) 从 panic 现场内存手工拼 ELF core

12.3 GDB 认不认你的 core,就看 ELF 头三项

这是最容易踩的坑 :core 文件本质是 ELF,如果头部几项不对,

GDB 直接拒绝,报 is not a core dump: file format not recognized。

字段 必须是 实测对比
e_type ET_CORE (4) 旧合成 core 写成 NONE(0) → GDB 拒绝加载
e_machine EM_RISCV (243) 写成 None 同样被拒
PT_NOTE 含 NT_PRSTATUS,name = "CORE\0" 没有它 → 读不到寄存器
bash 复制代码
$ riscv64-unknown-elf-readelf -h demo.core | head -6
  Class:                             ELF32
  Data:                              2's complement, little endian
  OS/ABI:                            UNIX - System V
  Type:                              CORE (Core file)      <- ★
  Machine:                           RISC-V                <- ★
bash 复制代码
$ riscv64-unknown-elf-readelf -n demo.core
Displaying notes found at file offset 0x00000074 with length 0x000000e0:
  Owner                Data size 	Description
  CORE                 0x000000cc	NT_PRSTATUS (prstatus structure)

12.4 加载与第一轮勘察

bash 复制代码
gdb -q -batch fw.elf fw.core \
    -ex "info files" \
    -ex "info registers" \
    -ex "bt" \
    -ex "x/16xw \$sp" \
    -ex "maintenance info sections"

⚠️ 本机 rv32 core 若架构识别不对,必须先 set architecture riscv:rv32 ,

否则 info registers 只回一句 The program has no registers now.(实测):

gdb 复制代码
(gdb) info registers
The program has no registers now.       <- 没设架构时的典型症状
(gdb) set architecture riscv:rv32
The target architecture is set to "riscv:rv32".
(gdb) info registers
pc             0x80001234	0x80001234          <- 正常了

12.5 怎么知道 core 覆盖了哪些内存?(★命令选择有讲究)

命令 裸机 core 上的实测结果
info proc mappings ❌ 静默无输出 (那是 Linux /proc 的概念)
info files ✅ 正确:0x80000000 - 0x80001000 is load1
maintenance info sections ✅ 最详细,能看到 note0 / .reg / load1 全貌
gdb 复制代码
(gdb) info files
Local core dump file:
	`C:\...\demo.core', file type elf32-littleriscv.
	0x80000000 - 0x80001000 is load1
gdb 复制代码
(gdb) maintenance info sections
Core file: `C:\...\demo.core', file type elf32-littleriscv.
 [0]     0x0000->0x00e0 at 0x00000074: note0 READONLY HAS_CONTENTS
 [1]     0x0000->0x0080 at 0x000000d0: .reg/1 HAS_CONTENTS
 [2]     0x0000->0x0080 at 0x000000d0: .reg HAS_CONTENTS
 [3]     0x80000000->0x80001000 at 0x00000154: load1 ALLOC LOAD READONLY HAS_CONTENTS

读法:

  • note0 = 寄存器/进程信息所在的 NOTE 段;
  • .reg / .reg/1 = GDB 从 NOTE 里抽出来的寄存器区;
  • load1 = 唯一真正被 dump 的内存范围 ;
    凡是 x 落到这个范围外,都会报 Cannot access memory。

12.6 一个完整可复现的验证(本机实跑记录)

下面这段是真实执行结果 ,可用本目录的 tools/mk_minicore.py 自己复现:

bash 复制代码
# 造 core:pc=0x80001234 ra=0x80005678 sp=0x8000ff00
python tools/mk_minicore.py demo.core
gdb 复制代码
$ riscv64-unknown-elf-gdb -q -batch \
    -ex "set architecture riscv:rv32" -ex "core-file demo.core" \
    -ex "info registers pc ra sp s0" -ex "x/4xw 0x80000000" \
    -ex "set \$pc = 0x80009999" -ex "info registers pc" -ex "bt"

The target architecture is set to "riscv:rv32".
[New process 1]
Program terminated with signal ?, Unknown signal.
#0  0x80001234 in ?? ()
pc             0x80001234	0x80001234
ra             0x80005678	0x80005678
sp             0x8000ff00	0x8000ff00
s0             0x8000ff00          0x8000ff00
0x80000000:	0x564f4841	0x726b645d	0x8e878079	0xaaa39c95
You can't do that without a process to debug.
pc             0x80001234	0x80001234
#0  0x80001234 in ?? ()

这段输出一次验证了 4 件事:

  1. pr_reg 偏移 = 72、RISC-V 寄存器顺序(reg[0]=pc, reg[1]=ra, reg[2]=sp, reg[8]=s0)正确 ------
    写进去的 4 个值 GDB 一字不差地读出来了;
  2. core 的内存映射生效(x/4xw 0x80000000 能读到内容);
  3. set $pc 在 core 上必然失败 :You can't do that without a process to debug.,
    且 pc 保持原值;
  4. 无符号时 bt 只能给出第 0 帧 #0 0x80001234 in ?? ()。
    signal ?, Unknown signal 是 newlib 版 GDB 的信号名映射问题,不影响寄存器与内存分析。

12.7 core 分析的推荐工作流

复制代码
1. 确认 core 合法性     readelf -h / -n        → Type=CORE? Machine=RISC-V? NT_PRSTATUS?
2. 确认架构             set architecture riscv:rv32
3. 加载符号             gdb fw.elf fw.core
4. 看覆盖范围           info files / maintenance info sections
5. 看现场               info registers / info frame
6. 打栈                 bt / bt full;断栈则走 §7.3
7. 定位崩溃点           info symbol $pc / x/10i $pc
8. 看内存               x/...  / dump binary memory 导出
9. 归档                 set logging file/enabled on

12.8 Backtrace stopped 的完整处置决策树

复制代码
bt 停在 "frame did not save the PC"
   │
   ├─ 有 GDB Python?
   │    ├─ 有 → 写 Python unwinder,读 trap frame 里保存的 pc/ra/sp
   │    │        (不改文件;但本机不可用)
   │    └─ 无 ─┐
   │           │
   └───────────┴─ 没 Python(★本机情况)
        ├─ 方案 C:离线改 core 的 NT_PRSTATUS 寄存器块(§7.5)
        │    只解决 frame0 → frame1 这一跳
        └─ 方案 A:重编固件,加 -funwind-tables / -fno-omit-frame-pointer
             链接脚本 KEEP(.eh_frame)  ← 根治,但涨 FLASH

13. 常见报错与坑(全部带真实输出)

13.1 路径类

报错 原因 解法
No such file or directory. 用 /c/... 这种 POSIX 路径 换 C:/...(✅实测)
dump 导出后 ls 找不到文件 同上,/tmp/x.bin 被当成相对路径 用 Windows 绝对路径
No symbol table is loaded 只加了 core,没加 ELF gdb fw.elf fw.core 两个都喂
源码找不到 / 路径不对 编译机路径 ≠ 本机路径 set substitute-path + directory

13.2 core / 格式类

报错 原因 解法
is not a core dump: file format not recognized e_type 不是 ET_CORE(✅实测:旧合成 core 是 NONE) 生成端把 ELF 头改对
The program has no registers now. 架构未指定(✅实测) set architecture riscv:rv32
You can't do that without a process to debug. core 是只读 target(✅实测) 换 §7.3 的 B/C 方案
Cannot access memory at address 0x... 地址在 core 覆盖范围外(看 info files) 只能换范围,或改 core 生成端多 dump 一点
info proc mappings 没输出 裸机 core 没有 /proc 概念(✅实测) 用 info files
Program terminated with signal ?, Unknown signal newlib GDB 信号名映射(✅实测) 无害,忽略
无符号时 bt 只有 #0 ... in ?? () 没加载 ELF 加载 ELF + -funwind-tables

13.3 功能缺失类(★本机专属)

报错 说明 对策
Python scripting is not supported in this copy of GDB. 编译时 --without-python(✅实测) 走命令脚本 / MI / 外部脚本
Undefined info command: "unwinders" 本 build 没有 unwinder 管理命令(✅实测) 只能靠 bt 结果判断
Bad breakpoint number 'unwinder' disable unwinder 不存在(✅实测) ---
Undefined command: "checkpoint" 非 Linux 平台没有该命令(✅实测) 用外部快照/多次 run

13.4 远程调试类

现象 原因 解法
连接超时 默认 2 秒太短 set remotetimeout 20
读内存报错 GDB 认为该区域不可访问 set mem inaccessible-by-default off
Flash 里断点不生效 软断点写不进 Flash hbreak
断点设多了失效 硬件断点数量有限 删掉不用的;set remote hardware-breakpoint-limit
单步行为怪 stub 实现有 bug set debug remote 1 看包
复位后错乱 目标被 reset,GDB 状态过期 monitor reset halt 后重新 load

13.5 断栈类

报错 原因 解法
Backtrace stopped: frame did not save the PC 缺 CFI / 缺帧指针 §12.8 决策树
Backtrace stopped: previous frame inner to this frame 栈已损坏 手工用 x 扫描栈找返回地址
回溯少帧/多帧 -O2 尾调用优化 属正常,需交叉验证 objdump

13.6 其它高频

报错 原因 解法
No symbol "XXX" in current context 打印的是宏 -g3 后用 macro expand
<optimized out> 变量被优化掉 -O0 重编;或算偏移手工读内存
行为空前古怪 .gdbinit 污染 加 -nx 复跑
info registers 看不到 CSR 目标没给 XML target description maint packet qXfer:features:read:target.xml:0,fff 查

14. 附录 A:一页速查卡

gdb 复制代码
### 加载
file fw.elf              加载符号
core-file fw.core        加载快照
target extended-remote localhost:3333
set architecture riscv:rv32
set mem inaccessible-by-default off

### 跑
r  start  c  n  s  ni  si  fin  u  adv  j  return  signal

### 断
b file:42     b *0x80001234   hbreak main   tbreak fn
watch var     watch -l var    rwatch var    awatch var
catch syscall read            catch throw
ignore N 100  condition N x>3
commands N ... end            dprintf fn, "v=%d\n", v
info breakpoints              delete N   clear fn

### 栈
bt   bt full   bt 10   bt -10   frame N   up   down
info frame    info args   info locals
thread apply all bt

### 看
p/x v    x/16xw 0x80000000    x/10i $pc    x/s 0x80001000
info registers   info all-registers   info symbol $pc
ptype /o struct ftl_ctx
find 0x80000000, 0x8000ffff, 0x1234
dump binary memory ram.bin 0x80000000 0x80001000

### 汇编 / 源码
disassemble /r fn      disassemble /s fn      info line fn
list fn    dir PATH    set substitute-path FROM TO
tui enable   layout asm   focus regs   update
Ctrl-x a 开/关TUI   Ctrl-l 重绘   Ctrl-x o 切窗口

### 信息 / 排障
info files                ★core 覆盖范围
maintenance info sections ★段布局
show configuration        ★编译配置(有没有 Python)
set debug remote 1        ★看 RSP 包
maint packet qSupported   ★直接问 stub
monitor help              远端代理命令
compare-sections          校验烧录镜像
apropos KEYWORD           搜命令
set logging enabled on

### 脚本
gdb -q -batch -x s.gdb fw.elf fw.core
define NAME ... end / document NAME ... end
while ... end  if ... end
-hook-stop 每次停机自动执行

### 反向
record full   reverse-continue   reverse-step   reverse-stepi

15. 附录 B:本目录文件说明

文件 作用
GDB_使用手册.md 本文档
tools/mk_minicore.py 生成 GDB 可加载的最小 RV32 core 测试夹具
tools/demo.core 由上面脚本生成的样例 core(pc=0x80001234 / ra=0x80005678 / sp=0x8000ff00)

复现验证:

bash 复制代码
cd tools
python mk_minicore.py demo.core
/e/software/nuclei_riscv_gcc/bin/riscv64-unknown-elf-gdb.exe -q -batch \
    -ex "set architecture riscv:rv32" -ex "core-file demo.core" \
    -ex "info registers pc ra sp s0" -ex "bt"

预期输出见 §12.6。


16. 待确认 / 未覆盖

项 状态
live target(JTAG/OpenOCD/QEMU)实机验证 未做,本机无硬件、无 qemu
record full 在 RISC-V 裸机上的实际可用性 命令存在,未实测
64 位 core(EI_CLASS==2)的 note 对齐 未验证;32 位实测 4 字节对齐
info proc mappings 在 Linux core(含 NT_FILE)上的行为 未测,裸机 core 上无输出
GDB Python 相关章节(pretty-printer / unwinder / MI+Python) 本机无 Python,仅作说明性收录
相关推荐
Chen—LSN2 小时前
C语言——文件操作(2)
c语言·开发语言·c++·经验分享·笔记·算法·c#
Escalating_xu2 小时前
【C 语言】深入理解指针(2):数组名、数组传参、二级指针与指针数组
java·c语言·开发语言
心中有你02144 小时前
C语言实现文件异或加密与RLE压缩算法
c语言·开发语言
阿钱真强道4 小时前
18 嵌入式操作系统 | uloop 定时器:周期任务与网络上报
c语言·定时器·周期任务·uloop
多多爱学习4 小时前
括号有几层?得看还有多少个左括号没闭合
c语言·c++·算法·面试
AI+程序员在路上4 小时前
AP6275S蓝牙双接口解析:HCI UART与PCM的分工与协同
linux·c语言·物联网
jingli95 小时前
本地向量库索引把 C 盘塞爆 48.6G:一次 LanceDB 膨胀排查与根治实录
c语言·开发语言
阿钱真强道5 小时前
21 嵌入式操作系统 | JSON 与 json-c:生成与解析
c语言·开发语言·json·json-c
心中有你02148 小时前
C语言埃氏筛法(素数筛选)
c语言·数据结构·算法