x64dbg serun/sego 命令详解:吞掉异常并继续运行调试器
【免费下载链接】x64dbgAn open-source user mode debugger for Windows. Optimized for reverse engineering and malware analysis.项目地址: https://gitcode.com/gh_mirrors/x6/x64dbg
导读
serun(别名sego)是 x64dbg 调试器专用于异常场景的"运行"命令。当调试器因异常而中断(停锁)时,serun会在不通知被调试程序的情况下吞掉当前异常、跳过调试对象内的异常分发,并立即释放调试锁让程序继续运行。本文结合 关联文档 与 命令实现源码,完整讲解其用法、底层实现、与run/erun的区别,以及在反调试与异常分析中的典型实战场景。
一、命令速览
| 项目 | 说明 |
|---|---|
| 命令名 | serun |
| 别名 | sego |
| 所属类别 | debug-control(调试控制) |
| 注册位置 | src/dbg/x64dbg.cpp#L152:dbgcmdnew("serun,sego", cbDebugSerun, true); //run + swallow exception |
| 功能定位 | 吞掉当前异常 + 放行程序继续运行 |
| 是否设置结果变量 | 否 |
serun与同族的seStepInto(src/dbg/x64dbg.cpp#L157)、seStepOver(src/dbg/x64dbg.cpp#L160)共享相同的"se"(swallow exception,吞异常)前缀语义,是异常中断场景下"继续执行"一族命令的成员。
二、参数与结果变量
参数
serun [arg1] sego [arg1][arg1](可选):指定一个地址或表达式。当提供该参数时,x64dbg 会在该位置放置一个一次性(single-shot)断点,然后才开始运行。程序运行到该地址时自动停下,相当于"运行到此处"。- 若省略参数,则直接释放调试锁,程序无条件下继续运行,直到遇到下一个断点或异常。
结果变量
根据 serun.md 的说明,该命令不设置任何结果变量(如$result等),因此不能在脚本中依赖其返回值判断执行结果;判断执行是否成功应依靠后续的调试状态(如是否停到目标断点)或检查dbgisrunning()之类的状态。
参数内部处理
arg1参数并不是独立实现的"运行到地址"逻辑,而是复用断点命令:
- 在 cbDebugRunInternal 中,当
argc >= 2时执行:DbgCmdExecDirect(StringUtils::sprintf("bp \"%s\", ss", argv[1]).c_str()) - 即内部调用
bp <地址>, ss,设置一个一次性(single-shot,ss)软件断点后再放行程序。因此arg1支持 x64dbg 断点命令所接受的地址表达式语法。
三、底层实现:两步走
serun的命令处理函数 cbDebugSerun 实现非常简洁,仅两行:
bool cbDebugSerun(int argc, char* argv[]) { cbDebugContinue(argc, argv); return cbDebugRunInternal(argc, argv, history_clear); }可以看出serun是两步操作的组合:
第一步:设置异常处理状态(吞掉异常)
调用 cbDebugContinue(即continue命令的处理函数)。由于serun通常不带参数调用,此时走argc < 2分支:
dbgsetcontinuestatus(DBG_CONTINUE); dputs(QT_TRANSLATE_NOOP("DBG", "Exception will be swallowed"));dbgsetcontinuestatus(DBG_CONTINUE)将当前异常的继续状态设置为DBG_CONTINUE,表示"异常已被处理",调试器后续会把该异常当作已处理事件返回给系统,不再向被调试程序分发。这也正是文档中"swallowing the current exception, skipping exception dispatching in the debuggee"(吞掉当前异常、跳过调试对象内的异常分发)的实现位置。
第二步:释放锁并放行
调用 cbDebugRunInternal:
- 清空/记录历史(此处传
history_clear,即清空历史); - 若提供了
arg1,先执行bp "<arg1>", ss设置一次性断点; - 若程序已在运行则直接返回;
- 调用
GuiSetDebugStateAsync(running)将 GUI 状态切为"运行中"; - 调用
unlock(WAITID_RUN)释放调试等待锁,唤醒调试循环让程序继续执行; - 触发
CB_RESUMEDEBUG插件回调,通知各插件调试已恢复。
四、与 run、erun 的差异对比
在 cmd-debug-control.cpp 中,三个"运行"命令并排实现,语义差异非常清晰:
| 命令 | 处理函数 | 异常处理 | 适用场景 |
|---|---|---|---|
run/go/r/g | cbDebugRun | 不做特殊处理,仅跳过 INT3 单步(skipInt3Stepping) | 普通运行,交由异常过滤器决定是否中断 |
erun | cbDebugErun | 未运行时调用dbgsetskipexceptions(true)开启"跳过所有异常"模式 | 想连续跨过后续所有异常直到目标 |
serun/sego | cbDebugSerun | 将当前这一次异常标记为已处理(DBG_CONTINUE) | 只吞掉当前这次异常,立即继续 |
关键区别在于:
erun是"跳过未来所有异常":通过dbgsetskipexceptions(true)设置全局跳过标志bSkipExceptions(见 debugger.cpp#L61、debugger.cpp#L2311),后续异常在(bSkipExceptions || filter.breakOn != ExceptionBreakOn::FirstChance) && (!maxSkipExceptionCount || ++skipExceptionCount < maxSkipExceptionCount)条件下不再停表,直到再次遇到显式断点。serun是"只处理当前这一次":它只修改当前异常的继续状态,异常循环回到 debugger.cpp#L2310 的dbgsetcontinuestatus(...)时,已设置的DBG_CONTINUE会生效——注意 debugger.cpp#L2301-L2313 中首次异常会按过滤器重置继续状态,若过滤器配置为"中断所有首次异常",则下一次异常仍会照常中断。这正是serun与erun行为差异的本质。
提示:还有无前缀的
continue命令(cbDebugContinue),它只设置异常继续状态、不释放运行锁,必须配合run族命令使用。serun则是把"设状态"和"放行"合并为一条命令。
五、异常中断循环中的执行流程
当程序在异常处被 x64dbg 停住时,执行serun的完整流程为:
- 调试器当前处于异常中断态(调试循环停在异常处理处,
WAITID_RUN等待锁被占用); - 用户执行
serun(或不带参数的sego); cbDebugContinue(argc, argv)以argc < 2进入,dbgsetcontinuestatus(DBG_CONTINUE)把当前异常继续状态设为"已处理";cbDebugRunInternal检查是否提供了目标地址(有则下一次性断点)、检查程序是否已在运行;unlock(WAITID_RUN)释放等待锁,调试循环从 debugger.cpp 的等待点恢复;- 调试器把
DBG_CONTINUE返回给 Windows 调试 API,异常被标记为已处理,不会进入被调试程序的 SEH 异常处理链; - 程序继续运行,直到下一次断点或异常。
正是因为第 6 步"异常不再进入程序自身的处理链",serun成为反调试场景与异常陷阱绕过的常用手段。
六、典型实战场景
场景一:跳过反调试异常陷阱
许多保护壳/反调试代码会在执行路径上主动触发异常(如int3、int 2d),并用结构化异常处理(SEH)检测异常是否被"正常"处理,从而判断是否处于调试状态。此时若直接run,异常处理链会被触发、程序可能走反调试分支;而执行serun后异常被调试器吞掉、不进入程序处理链,可让程序按"未发生异常"的路径继续执行。注意:serun吞异常是"代为处理"而非"转发给程序",若程序依赖该异常被自身 SEH 捕获,行为会与真实环境不同,需结合具体样本判断。
场景二:快速跨过频繁的良性异常
某些程序在正常运行中会高频产生良性异常(如未处理的内存访问探测)。在异常中断后逐个按run效率低下,可:
- 执行
serun立即放行当前这一次; - 若此类异常会反复触发,改用
erun一次性跳过后续所有异常(可通过 Engine/MaxSkipExceptionCount 配置 限制跳过的异常数量上限,避免完全失控); - 更精细的做法是在 异常过滤器 中将该异常类型配置为"不中断",从源头减少停表。
场景三:运行到目标位置
serun 00401000先设置一次性断点于0x00401000再放行,等价于"运行到该地址",常用于从异常点快速跳到关键代码段。
场景四:脚本化控制
在 x64dbg 脚本(脚本命令参考)中:
serun ; 吞掉当前异常继续跑 serun 7FF700000000 ; 或一路跑到指定地址由于serun不设置结果变量,脚本中如需判断是否到达目标,可配合在目标地址预置断点并检查是否停在该地址来验证。
七、相关命令对照
| 命令 | 别名 | 行为 | 文档 |
|---|---|---|---|
run | go、r、g | 普通运行 | run.md |
erun | - | 跳过异常运行 | erun.md |
serun | sego | 吞异常运行 | serun.md |
StepInto | sti、si等 | 单步步入 | StepInto.md |
seStepInto | sesti等 | 吞异常单步 | seStepInto.md |
continue | - | 仅设置异常继续状态 | - |
八、源码索引
- 命令注册:src/dbg/x64dbg.cpp#L152
- 处理函数声明:src/dbg/commands/cmd-debug-control.h#L18
- 处理函数实现:src/dbg/commands/cmd-debug-control.cpp#L386-L390
- 运行内部逻辑(含一次性断点与解锁):src/dbg/commands/cmd-debug-control.cpp#L46-L65
- 异常继续状态设置:src/dbg/commands/cmd-debug-control.cpp#L483-L496
- 异常循环与继续状态生效点:src/dbg/debugger.cpp#L2301-L2313
- 异常过滤器配置说明:docs/gui/settings/Exceptions.md
结语
serun/sego的核心价值在于把"吞掉当前异常"与"继续运行"两个动作原子化:一条命令即可完成continue加run的组合。理解其底层对DBG_CONTINUE的设置逻辑,以及它相对于run(不干预异常)与erun(跳过全部异常)的差异,是准确使用异常控制命令、高效绕过反调试陷阱的关键。在调试会话中,根据"只处理这一次"还是"跳过后续所有"的意图在serun与erun之间做选择,能显著提升异常密集场景下的调试效率。
【免费下载链接】x64dbgAn open-source user mode debugger for Windows. Optimized for reverse engineering and malware analysis.项目地址: https://gitcode.com/gh_mirrors/x6/x64dbg
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考