为什么会有 crashctl
做这个工具,起因是自己平时用 crash 实在太麻烦。
第一步就不是分析,而是把命令行拼对。 一个普通的 kdump vmcore 还好,crash vmlinux vmcore 就进去了;但一到厂商 ramdump 就变成一堆手工计算:高通要把 DDRCS*.BIN 按 load.cmm 里的地址拼成 file@addr,...,还要自己从 OCIMEM.BIN 推 KASLR 偏移、判断 vabits_actual 是 39 还是 48;SPRD 得去 dump_report.txt 里翻 memory map、phys_offset、kimage_voffset;MTK 要从 VMCOREINFO 或各种 log 里找 KASLR,再拼一条 --machdep kimage_voffset=...,phys_offset=...,max_physmem_bits=...。参数错一个,crash 要么起不来,要么起来了但符号全是错的,而这两种失败看起来都很像"dump 坏了"。这些经验每次都要重新翻一遍,写成脚本又会因为平台、内核版本、dump 布局不同而失效。
后来我写了 crash-mcp,把 crash 包成 MCP 工具接到智能体上,让它自己去执行 bt、log、struct 这些命令。方向是对的,智能体确实能顺着调用栈往下查;但很快就撞上成本问题:MCP 的工作方式是"命令 → 完整输出 → 塞回模型上下文"。一条 log 几千上万行,一次 bt -a 几十个任务,几十轮下来 token 消耗非常夸张,而这里面绝大部分内容对当前这一步推理是无关的,看过一次就再也不用了。上下文被撑爆之后,还得回头重新读,成本又翻一倍。
所以有了 crashctl。 它把这两件事一起解决:
平台参数交给
intake去识别和计算,人只需要给 dump 和 vmlinux;把"一个长驻 crash 进程 + 输出全部落盘"做进 CLI,命令只回一小段预览和一个命令 id,需要细节时再用
output/search精确取回。
对智能体来说,这等于把上下文从"整段输出"压缩成"结论 + 引用",token 消耗降下来一个量级,同时每条结论都能追溯到 session_id / command_id 的原始输出。它不依赖 MCP,也不依赖某个特定的模型——普通 shell 脚本能调,人和智能体都能用。
下面就是这套接口的完整说明。
它是什么
crashctl 是对 crash(8) 的会话化封装:把"一个 crash 进程 + 一堆命令 + 完整输出"变成一条可以反复调用的命令行接口,适合人手工排查,也适合脚本 / Agent 调用。
下文中的
crashctl指 skill 包里的bin/crashctl(自带的独立二进制,不依赖宿主 Python),源码树里也可以用scripts/crashctl包装脚本。crash 二进制默认找bin/crash,也可以用--crash-bin指定。
三个设计要点,理解它们就能理解所有命令:
一个 session 对应一个长期存活的 crash 进程。
open启动它,之后每条run只是通过 Unix socket 把命令送进去,不用重新加载 vmlinux / vmcore(大 dump 加载一次可能要几十秒到几分钟)。每条命令的完整输出都落盘,命令有稳定 id。
run只回你一小段预览 + 命令 id(c0004),要看全量用output翻页、用search检索。所以长输出不会灌满终端,也不会丢。所有命令支持
--json。 成功和失败都是一行 JSON,失败带稳定的error.code和hint。人用可以不加--json,输出是简短的文本。
通用约定
全局参数
--json 和 --state-dir 可以放在子命令前后任意位置。
crashctl --json list
crashctl list --json --state-dir /data/state状态目录
会话数据都放在状态目录下:
<state>/sessions/<sid>/
├── session.json # 元数据:argv、pid、状态、启动参数
├── ctl.sock # 0600 Unix socket
├── daemon.log # daemon 自己的日志
├── transcript.log # crash 进程的原始 PTY 转录
├── commands/cNNNN.txt # 每条命令的完整输出
└── ledger.jsonl # 只追加的证据账本解析顺序:--state-dir > $CRASHCTL_STATE_DIR > $XDG_STATE_HOME/crashctl > ~/.local/state/crashctl。
目录权限是 0700。已存在的目录如果是指向别处的符号链接、不属于当前用户、或对 group/other 开放,crashctl 不会偷偷改权限,而是直接报 STATE_DIR_INSECURE 让你处理。
会话 id
可以自己指定:
open ... --session-id mtk-0731不指定则自动生成:
<prefix>-<6 位十六进制>,例如crashctl-a1b2c3前缀解析顺序:
--session-prefix>$CRASHCTL_SESSION_PREFIX>crashctl合法字符:字母、数字、
-、_,首字符必须是字母或数字,最长 48 字符(作为前缀最多 41 字符)--session-id和--session-prefix同时给时,--session-id优先(前缀只在自动生成时使用)
crashctl open ... --session-prefix mtk-0731 # -> mtk-0731-9f3c21
CRASHCTL_SESSION_PREFIX=qcom crashctl open ... # -> qcom-4d8e02crash 二进制
查找顺序:--crash-bin > $CRASHCTL_CRASH_BIN > crashctl 同目录下的 bin/crash > PATH 里的 crash。
会话状态
starting → ready → closed
│
├─→ dead (crash 进程自己退了)
└─→ failed (启动就失败)
stale = 派生状态:session.json 记着 ready,但 daemon 进程已经没了ready:可以runclosed:不能run,但历史输出仍可output/search/export;可以reopen/rename/removedead/failed/stale:可以直接reopen(它会先把残留的 daemon / crash 进程清掉),也可以先close再reopen
退出码
被拒绝的 crash 命令
run 只接受单行 crash 命令,以下情况一律报 COMMAND_REJECTED,没有开关可以绕过:
多行命令(含
\r/ NUL)超过 4096 字符的命令
quit/exit/q(会结束 crash 进程)任何 shell 语法:
!开头的 shell 转义、|、>、<、反引号、$(
需要过滤输出时用 search,不要在 crash 里拼管道。
命令速查
逐条详解
doctor — 环境自检
crashctl doctor --json
crashctl doctor --crash-bin /opt/crash-arm64/crash --json检查项:python(3.10+)、platform(Linux)、procfs、pexpect、crash(路径 + 版本 + 内嵌 gdb 版本)、交叉编译的 crash-*、gdb、makedumpfile、state_dir、unix_socket。
每项是 ok / warn / fail,同时给出 message 和可选的 hint。顶层 ready 为 false 时退出码是 1。
{"ok": true, "schema": "crashctl/1", "command": "doctor",
"data": {"ready": true, "failed": [], "warnings": ["makedumpfile"],
"checks": [{"name": "crash", "status": "ok",
"message": "crash 9.0.2 at /opt/bin/crash",
"details": {"path": "/opt/bin/crash", "version": "9.0.2",
"embedded_gdb": "16.2", "first_line": "crash 9.0.2++"}}]}}先跑 doctor,crash 是 fail 就不要往下走了。
intake — 只做体检,不启动
crashctl intake /dumps/vmcore --vmlinux /dbg/vmlinux --json
crashctl intake --dump /dumps/vmcore --vmlinux /dbg/vmlinux --json
crashctl intake /dumps/qcom_dir --vmlinux /dbg/vmlinux --platform qcom --va-bits 39 --jsondump 路径既可以做位置参数,也可以用 --dump,两者同时给会报用法错误。
它会告诉你:dump 是什么格式(ELF vmcore / vendor ramdump 目录)、架构、kernel release、vmlinux 有没有调试信息、两边 release 是否匹配(确定的 mismatch 算错误,无法判断只算 warning)、厂商参数(KASLR、VA bits),以及最终打算怎么拼 crash 命令行。
关键字段:
ready:能不能直接opendump{format,arch,bits,endian,release,size}、vmlinux{format,arch,has_debug_info,has_symtab,release,size}version_check:{dump_release, vmlinux_release, match}plan{crash_args,dump_args,vmlinux,argv_tail}:将要执行的参数warnings[]/errors[]/hints[]
厂商 dump 目录还会多出 detected_platform、indicators(扫到的文件)、dump_mode(single / split)和 vendor.params(KASLR、VA bits 等)。
intake 不创建会话、不需要状态目录,是一个很便宜的预检,适合在批量处理前先筛一遍。
覆盖参数:--platform auto|generic|qcom|sprd|mtk、--kaslr <hex|auto>、--va-bits 39|48。
open — 启动会话
crashctl open /dumps/vmcore --vmlinux /dbg/vmlinux --json
crashctl open --dump /dumps/vmcore --vmlinux /dbg/vmlinux --session-id my-case --json常用参数:
启动时会自动执行 set scroll off 和 gdb set debuginfod enabled off(除非 --no-init),这些记录在账本里 kind 为 init。
返回里最重要的是 session_id。startup 字段是 c0000 的 run view —— 也就是 crash 启动时自己打印的那段头部信息(KERNEL / DUMPFILE / PANIC / PID 等),经常直接就能看出 panic 原因。
启动失败不丢现场:会话目录会保留,状态记成 failed,之后可以用 reopen 重试或用 remove 删除。
SESSION_START_FAILED/SESSION_START_TIMEOUT的details.startup_output里带 crash 自己的报错预览,details.startup_command_id指出对应的启动记录(首次open一般是c0000,reopen后编号会顺延)daemon 自己的日志在
<state>/sessions/<sid>/daemon.log
reopen — 重开会话,历史不丢
crashctl reopen s1111-33b6f9 --json
crashctl reopen s1111-33b6f9 --crash-bin /opt/crash-arm64/crash --startup-timeout 600 --json用于 closed / dead / failed / stale 的会话。ready 状态会报 SESSION_NOT_READY(已经在跑了,直接 run 就行)。
它是原地重启:复用 session.json 里记录的 crash argv,会话目录、已存的输出、账本全部保留,命令 id 从原来的最大号继续往后排。注意重开时也会新写一条 kind: "startup" 记录和两条 init 记录,所以例子是:原来最后一条是 c0003,重开后 c0004 是启动记录、c0005/c0006 是 init、你下一条 run 拿到 c0007。返回比 open 多一个 open_count 和 warnings[]。
crashctl close s1111-33b6f9
crashctl reopen s1111-33b6f9
crashctl run s1111-33b6f9 'bt -a'可覆盖的参数只有运行期的:--crash-bin、--startup-timeout、--idle-timeout、--max-clients、--no-init。dump / vmlinux / --crash-arg 沿用原记录。
重开前会检查记录下来的 vmlinux 是否还在,不在了直接报 PATH_NOT_FOUND 并指明是哪个文件;crash 二进制不在了则退回按默认顺序查找,并在 warnings[] 里说明。dump 文件本身不预检,路径失效会在启动阶段以 SESSION_START_FAILED 暴露,details.startup_output 里能看到 crash 自己的报错。
run — 执行一条 crash 命令
crashctl run s1111-33b6f9 'bt -a' --json
crashctl run s1111-33b6f9 log --timeout 300 --json
crashctl run --json s1111-33b6f9 -- 'foreach bt' # -- 之后的字面都算命令
--之后的部分原样变成 crash 命令,所以--json这类选项必须写在--之前,否则会被当成命令的一部分发给 crash。
返回的是一个 run view:
{"session_id": "s1111-33b6f9", "command_id": "c0003", "command": "bt", "kind": "user",
"status": "ok", "duration_s": 0.051, "lines": 7, "bytes": 421, "sha256": "…",
"artifact": "/…/sessions/s1111-33b6f9/commands/c0003.txt",
"preview": {"head": ["PID: 1234 TASK: ffff888003a1c000 CPU: 2 COMMAND: \"bash\"", "…"],
"tail": [], "omitted_lines": 0},
"parsed": {"command_type": "backtrace",
"frames": [{"function": "machine_kexec"},
{"function": "__crash_kexec"},
{"function": "panic"},
{"function": "write_sysrq_trigger", "module": "sysrq_mod"}],
"tasks": [{"pid": 1234, "comm": "bash", "cpu": 2, "task": "ffff888003a1c000"}],
"warnings": []}}输出很长时还会多一个 next[],直接给出下一步该敲的 output / search 命令;parsed 的 signatures(log/bt/sys)和 anchors(sys/启动头)也只在该类型命令上出现。
几个要点:
status: "error"时外层仍是ok: true、退出码0:这表示 crash 收到了命令但拒绝了(比如p参数写错),属于"执行成功但结果报错"。要判断请求本身失败,看ok/ 退出码;要判断 crash 是否接受了命令,看data.status和parsed.semantic_error,其中suggested_next通常就是help <cmd>。status: "timeout"⇒ 外层是ok: false、报COMMAND_TIMEOUT,但部分输出已经存好,在error.details.result里(同一份 run view),会话一般还能继续用(details.recovered)。status: "dead"⇒ crash 进程死了,报SESSION_DEAD,输出同样在error.details.result里保住。预览里的长行会被截到 400 字符;
output翻页时截到 4000 字符;磁盘上的 artifact 永远是完整的。同一个会话并发
run会互相排队,超出--wait报SESSION_BUSY;连接数超过--max-clients报DAEMON_OVERLOADED。
output — 翻页看完整输出
crashctl output s1111-33b6f9 c0004 --json
crashctl output s1111-33b6f9 c0004 --offset 200 --limit 500 --json返回 lines[]、offset、returned、total_lines、has_more、next_offset、truncated_long_lines。
next_offset 就是下一页该传的 --offset,has_more 为 false 时说明已经到底。读取是流式的:只读到窗口结束就停,不会把几万行的 log 整个读进内存。
search — 正则检索所有输出
crashctl search s1111-33b6f9 'BUG:|Oops|WARNING:' --json
crashctl search s1111-33b6f9 'page fault' --command c0004 --json
crashctl search s1111-33b6f9 'slab' --ignore-case --context 5 --max-matches 100 --json每条命中带 command_id、line_number(1 基)、line、before[]、after[],所以可以准确引用"哪条命令第几行":
{"pattern": "BUG", "searched_commands": ["c0000", "c0001", "c0002", "c0003", "c0004", "c0005"],
"total_matches": 1, "returned": 1, "truncated": false, "clipped_lines": 0,
"matches": [{"command_id": "c0005", "line_number": 1501, "offset": 1500,
"line": "[ 1500.000000] BUG: unable to handle page fault for address: 0000000000000008",
"before": ["[ 1498.000000] fake log line 1498", "[ 1499.000000] fake log line 1499"],
"after": ["[ 1501.000000] fake log line 1501", "[ 1502.000000] fake log line 1502"]}]}限制(都是有意的):
单行只扫前 64K 字符
单行正则限时 0.25 秒,超时直接报
REGEX_TIMEOUT(提示化简,避免(a+)+这类嵌套量词)单次调用总预算 20 秒,超了会返回已找到的结果并置
stopped_reason: "time_budget"
truncated: true 表示命中数超过 --max-matches 或扫描被预算截断。
list — 列出会话
crashctl list --json
crashctl list --prune --json每项包含 session_id、status、vmcore、vmlinux、platform、daemon_pid、crash_pid、command_count 等。
command_count是用户run的次数,不含启动记录c0000和初始化命令;账本里的全部记录数在status的command_total里。
--prune 会把 stale / dead / failed 的会话 close 掉(清理残留进程和 socket),被清理的 id 列在 pruned[]。
不加 --json 时输出三列对齐的简表:
s1111-33b6f9 closed /dumps/vmcore
s1111-2ebc53 ready /dumps/vmcorestatus — 单个会话详情
crashctl status s1111-33b6f9 --json
crashctl status s1111-33b6f9 --history 50 --json会话摘要(同
list的字段)+ 实际执行的argv若状态是
ready,多一个live:busy、current_command、uptime_s、idle_s(daemon 实时返回);连不上时会带live_error,并把status修正成stalecommands[]:最近 N 条命令的command_id/command/kind/status/duration_s/lines,以及总数command_total
想不起用过的命令 id,就查这里。
close — 停止会话
crashctl close s1111-33b6f9 --json
crashctl close s1111-33b6f9 --purge --json
crashctl close s1111-33b6f9 --grace 5 --json先尝试优雅关闭(通过 socket 通知 daemon),不行就发信号,再不行强杀;如果 daemon 已经被 SIGKILL 导致 crash 进程成了孤儿,会按启动时间校验 pid 后一并清理(防止误杀复用了 pid 的进程)。
--purge 连会话目录一起删。
返回里的 method 说明用了哪种方式:
close 之后会话是 closed,历史输出仍然可读。
export — 导出证据账本
crashctl export s1111-33b6f9 --format md --json
crashctl export s1111-33b6f9 --format json --out /tmp/evidence.json --json默认写到 <session_dir>/evidence.<fmt>。Markdown 版是一张表:命令 id、类型(startup/init/user)、状态、耗时、行数、命令、sha256 前 12 位,再加会话基本信息和 vmcore/vmlinux 路径。适合直接贴进分析报告或发给同事复核。
ledger.jsonl 本身是只追加的:每条记录都有 sha256,运行期间绝不会被覆盖或重写。
rename — 给已关闭的会话改名
crashctl close qcom-201426
crashctl rename qcom-201426 mtk-a06x-0731 --json返回:session_id、previous_session_id、status、session_dir、command_count(账本里的全部记录数)。
规则:
只允许
closed状态。ready会报SESSION_NOT_READY,并提示先close;如果还有残留进程则报SESSION_BUSY新 id 走和创建时同一套校验,非法报
INVALID_ARGUMENT;重名报SESSION_EXISTS;和原名相同也报INVALID_ARGUMENT实现是整个目录
os.rename,输出、账本、argv 一个字节都不动,所以改名后output/search/reopen全部照常工作,命令 id 继续往后排旧名字立即查不到(
SESSION_NOT_FOUND);session.json里留renamed_from/renamed_at便于追溯
适合把自动生成的名字改成有意义的名字,比如把工单号、机型、日期写进 id。
remove — 删除已关闭的会话
crashctl remove qcom-201426 --json只接受 closed 状态,否则报 SESSION_NOT_READY;有残留进程报 SESSION_BUSY。这是带 rmtree 的破坏性操作,没有回收站。
返回 removed、session_dir、command_count、error_count、vmcore。这里两个计数都取自账本:command_count 是全部记录数(含启动 c0000 和 init),error_count 是其中状态非 ok 的条数。
如果只是想释放进程、还想留着证据,用 close(不加 --purge)。
典型工作流
从零到定位一次 panic
# 1. 环境体检
crashctl doctor --json
# 2. 校验输入,确认 release 匹配、vmlinux 有调试信息
crashctl intake /dumps/vmcore --vmlinux /dbg/vmlinux --json
# 3. 开会话(大 vmlinux 把启动超时放宽)
crashctl open /dumps/vmcore --vmlinux /dbg/vmlinux --startup-timeout 600 --json
# -> session_id = s1111-33b6f9,顺手看返回里的 startup 预览
# 4. 看 panic 上下文
crashctl run s1111-33b6f9 sys --json
crashctl run s1111-33b6f9 'log' --timeout 300 --json # 预览会给 signatures
# 5. 沿调用栈下钻
crashctl run s1111-33b6f9 'bt -a' --json
crashctl run s1111-33b6f9 'bt -f <pid>' --json
# 6. 需要看具体某段
crashctl search s1111-33b6f9 'BUG:|Oops|WARNING:' --context 3 --json
crashctl output s1111-33b6f9 c0004 --offset 1200 --limit 200 --json
# 7. 导出证据
crashctl export s1111-33b6f9 --format md --json
# 8. 收尾
crashctl close s1111-33b6f9 --json今天关了,明天想接着看
crashctl list --json
crashctl reopen s1111-33b6f9 --json
crashctl status s1111-33b6f9 --json # 看历史命令 id
crashctl output s1111-33b6f9 c0004 --json # 昨天的输出照样能读清理机器
crashctl list --prune --json # 清 stale/dead/failed 的残留
crashctl remove old-case-001 --json # 删掉确实不要的
crashctl close s1111-33b6f9 --purge --json # 边关边删脚本里用
sid=$(crashctl open "$DUMP" --vmlinux "$VMLINUX" --json | jq -r .data.session_id)
crashctl run "$sid" 'bt -a' --json | jq -r '.data.preview.head[]'
crashctl close "$sid" --json >/dev/null--json 保证 stdout 上恰好一行 JSON,jq 可以直接吃。异常也走 stdout 的 JSON(ok: false),同时退出码非 0,两种判错方式都可用。
错误码速查
一句话总结
doctor 确认环境 → intake 验输入 → open 开会话 → run 跑命令拿 id → output / search 按需下钻 → export 出证据 → close 收尾;关掉的会话还能 reopen 接着看、rename 起个有意义的名字、remove 彻底删除。
完整 JSON schema 见 references/cli-contract.md。