RDTSC/RDTSCP 能暴露单步调试,因其测量的CPU实际时钟周期对调试器引入的微架构停顿高度敏感;正常执行周期差稳定,单步时暴涨10–100倍。

为什么 RDTSC 能暴露单步调试
当程序被调试器(如 GDB、x64dbg)单步执行时,CPU 会在每条指令后插入大量额外开销:保存上下文、检查断点、处理调试事件。而 RDTSC 返回的是 CPU 自启动以来的**实际时钟周期数**(TSC),它不随操作系统调度或中断延迟变化,但对调试器引入的微架构停顿极其敏感。正常连续执行一段固定代码,RDTSC 差值稳定;一旦被单步,差值可能暴涨 10–100 倍——这是检测的关键依据。
如何用内联汇编安全读取 TSC
现代 x86-64 下必须用 RDTSCP(带序列化)而非裸 RDTSC,否则编译器或 CPU 可能重排指令,导致测量包含无关代码。GCC/Clang 支持如下内联写法:
uint64_t rdtscp() {
uint32_t lo, hi;
__asm__ volatile ("rdtscp" : "=a"(lo), "=d"(hi) : : "rcx", "rdx", "rax");
return ((uint64_t)hi << 32) | lo;
}
注意三点:
-
volatile防止编译器优化掉该指令 - 输出约束
"=a"(lo), "=d"(hi)正确捕获低/高 32 位 - clobber 列表必须含
"rcx"——RDTSCP会修改 RCX,漏写会导致寄存器污染(尤其在函数调用前后)
测量代码块并判断是否被调试
不能只测一次:TSC 本身有微小波动,需多次采样取最小值(排除中断干扰)。典型模式是循环 5–10 次,记录最小差值:
立即学习“C++免费学习笔记(深入)”;
uint64_t min_diff = UINT64_MAX;
for (int i = 0; i < 5; ++i) {
uint64_t t0 = rdtscp();
asm volatile ("" ::: "rax", "rbx", "rcx", "rdx"); // 空操作占位
uint64_t t1 = rdtscp();
min_diff = std::min(min_diff, t1 - t0);
}
bool is_debugged = min_diff > 2000; // 阈值需实测调整,通常 1000–5000 周期
关键细节:
- 空
asm volatile ("" ::: ...)是被测“目标”,必须显式声明 clobber 寄存器,否则编译器可能把变量分配到这些寄存器并被覆盖 - 阈值不是固定值:Intel 和 AMD 的 TSC 基频不同,且 turbo boost 会影响绝对数值;建议在目标机器上运行未调试版本,取 95% 分位数作为基线
- 避免在函数开头/结尾直接测——编译器插入的栈帧操作(push rbp / mov rsp,rbp)会污染结果
绕过与反绕过的现实约束
这方法在实战中极易失效,因为:
- 现代调试器(如 GDB +
set debuginfod enabled off)可禁用硬件单步,改用软件断点,此时RDTSC差值几乎不变 - Windows 上启用
DisableExceptionChain或 Linux 的ptrace(PTRACE_SETOPTIONS, ..., PTRACE_O_TRACECLONE)后,调试器能在更底层拦截,绕过周期统计 - CPU 的
TSC_DEADLINE模式或禁用 TSC(如某些 VM)会让RDTSCP返回 0 或恒定值
真正可靠的检测必须组合多个信号:TSC 波动 + 异常的 ptrace 返回值 + /proc/self/status 中 TracerPid 非零 + 检查调试寄存器状态(DR0–DR7)。单靠周期差,只能拦住最基础的手动单步场景。


















