概述
Visual Studio 调试场景下对 memcpy 的修改(问题原因参考AI分析结果)
问题描述
环境说明:
- 调试器为 Visual Studio 2022
- 平台工具集和Windows SDK 版本可以任意选,这个不影响该问题的复现
- 目标机器:Windows Server 2012(目测也和该机器无关)
- 被调试程序为 x86
Visual Studio 远程调试一个注入程序,在调用 memcpy 将一段函数拷贝到目标内存时,会将部分内存的值修改后拷贝到目标内存。大致如下图所示:
可以看到在 memcpy 的反汇编代码中,正在执行内存拷贝的汇编代码为:
assembly
016020BE rep movs byte ptr es:[edi],byte ptr [esi]

图示内容补充:
- 当前 esi 的值为 0x010c88ac
- 当前 edi 的值为 0x00b987d8
说明刚刚拷贝的目标内存为 0x00b987d7,也就是图中已经标记的位置。
但是,也可以看到源地址的 c7 被拷贝成为了 cc。与实际不符。问题大致就是如此。且只有该函数的该位置会出现问题,以及该函数往后偏移若许处的一位也会出现同样的问题(8b->cc)。
换用其他调试器,如 x86dbg 等均无法复现,所以才能定位到是 Visual Studio 的问题,但具体原因却是不知道的。
AI 分析的结果
几种可能的情况
这里补充一下就相关问题 ChatGPT 的回答:
Visual Studio 调试器本身在修改你的代码段 / 内存页,触发了内联补丁、断点、代码重写或代码分析,导致 memcpy 出错。
为什么只有 VS 会导致 memcpy 出错?
因为 VS 调试器会:
| VS行为 | 导致的副作用 |
|---|---|
| 注入软件断点(0xCC) | 复制代码段时复制到修改后的指令 |
| 对代码段做 Edit-and-Continue 热补丁 | 某些字节被重写 |
| 对 prologue 做 stack-frame 分析 | 修改 C7 45 xx 类指令 |
| 设置 PAGE_GUARD / WRITECOPY | memcpy 的 read 被 VEH 拦截并修改数据 |
| Hidden breakpoint & Symbol mapping | 对单字节自动 patch |
这些都是 VS 特有的,WinDbg/OD 都不会这么做。
因此 VS 调试才导致 memcpy(代码段) 出现错误字节。
看实际情况确实也是在 c7 45 xx 处修改了内存,猜测大概率是因为 JMC 的问题,但是 JMC 无法解释后文中的另一处修改,如下所示:
assembly
45 dc 50 8d 4d e8 51 6a
被修改为了
45 dc 50 8d 4d cc 51 6a
关于这一问题,chatgpt 的回答算是解惑了,原因就在于 VS 的,相关回答如下所示:
🔥 总结一句话(核心结论)
Visual Studio 调试器在本地变量访问指令(含栈帧偏移的 ModR/M displacement)里插入 0xCC,当作"隐藏断点/调试标记 stub"。偏移字节被替换成 0xCC,使指令成为调试器可识别的 patch point,但实际执行时不会触发,因为调试器会动态恢复原字节。
换句话说:
VS 调试器在 patch 指令,而你的 memcpy 把"调试器补丁"也复制过去,所以看到了错误的内存内容。
🔍 逐条解释三处修改
1. c7 45 f8 00 00 → cc 45 f8 00 00
原始:
C7 45 F8 00 00 00 00
mov dword ptr [ebp-8],0
修改后:
CC 45 F8 00 00 00 00
Visual Studio 改了 第 1 个字节(原本是 C7)。
🔧 这是 VS 的"单字节 patch point"标记
C7 是 mov r/m32, imm32 的 opcode。
调试器将 opcode 改成 0xCC,以便:
- 单步调试时识别到这条指令
- 用于本地变量调试 / Edit & Continue 更新
- 用于 IL → Native mapping (JMC) 的断点映射点
调试器在实际执行前会恢复原始字节,因此程序不会崩。
你的 memcpy 是只看"内存当前值",因此复制了 VS 的临时补丁。
➡ 本质:对 mov [ebp-8], imm32 的 opcode 插了 int3 标记。
2. 8b 45 f8 → cc 45 f8
原始:
8B 45 F8
mov eax,[ebp-8]
修改后:
CC 45 F8
调试器又修改了 opcode。
8B 是 "mov r32, r/m32"。
VS 将整个 opcode 变成 0xCC,使其成为调试标记。
这种 patch 最常见于:
- 单步调试
- 本地变量监视(Watch)
- Edit&Continue
🔥 为什么偏移字节(F8)保留?
因为调试器会用"恢复 + 执行 + 再 patch"的方式处理执行,不需要修改 displacement。
3. 8D 4D E8 → 8D 4D CC
原始:
8D 4D E8
lea ecx,[ebp-18]
修改后:
8D 4D CC
lea ecx,[ebp-34]
这里调试器修改了 displacement 字节 (E8 → CC)。
🔧 这是 VS 调试器最常见的"隐藏断点"标记
VS 经常使用:
- 修改 ModR/M 的 位移字节
- 或 opcode
来插入"不可见调试点(hidden breakpoint)"。
为什么不是 opcode?
因为:
-
lea ecx,[ebp-18] 这种指令不能简单替换成 int3,否则 EIP 变化会破坏栈跟踪
-
所以只能改 displacement,这样:
- 调试器知道这里是一个 patch
- 真正执行前会恢复字节
- 对用户不可见
➡ 本质:在栈访问偏移中埋入调试标记字节 CC。
🔥 结论:三处修改不是随机,而是 Visual Studio 的三种 patch 策略
| 示例 | 补丁方式 | 原因 |
|---|---|---|
| 1 | 修改 opcode → CC | 在 mov 指令上插桩 |
| 2 | 修改 opcode → CC | 在读取本地变量时插桩 |
| 3 | 修改 displacement → CC | 在不能改 opcode 的情况下"隐藏断点" |
你看到的就是 VS 调试引擎(vsdebugeng + mscordbi + enc_k)运行时的:
✔ 调试插桩(Instrumentation)
✔ 非文档化内存 patch
✔ 本地变量访问 patch point
✔ 单步调试补丁
✔ JMC / ENC 在 native 函数上的辅助 patch
🛠 为什么只有 Visual Studio 会这样?
因为:
-
WinDbg / x64dbg 是"原生调试",不会修改指令
-
VS 的"混合调试器"要支持:
- 托管 + Native stack mapping
- 单步进入 IL
- Edit & Continue
- 本地变量显示
- 调试优化代码
- Async stepping
- JMC 的 IL ↔ Native 行号映射
为此,它必须:
- 在某些指令前打补丁点
- 改 opcode / displacement
- 回写 int3
- 每次执行前恢复原始字节
🛠 如何验证 VS 在 patch?
WinDbg 下 attach VS 的调试器进程,用写断点:
ba w1 <address_of_modified_byte>
命中后,你会看到调用栈:
- vsdebugeng.dll
- mscordbi.dll
- vcruntime???.dll
立刻证明这是调试器插桩。
✔ 如何彻底解决?
方法 1:禁用所有 VS 调试增强
- 关闭 JMC
- 关闭 Edit and Continue
- 关闭 .NET Native/托管混合调试
- 关闭 Diagnostic Tools
- 关闭 Symbol loading
- 进入 Debug → Options → General,关掉所有 "Enable ..." 相关自动调试功能
方法 2:使用 WinDbg / x64dbg 调试
你已经验证过它们不会修改字节。
方法 3:设置代码区为只读(强制禁止调试器写入)
VirtualProtect(func_addr, size, PAGE_EXECUTE_READ, &old);
调试器 patch 会失败。