MSVC下如何为单个函数禁用JMC、ESP校验及COMDAT链接生成
MSVC单函数粒度编译配置实现方案
适用于shellcode跳转类复用函数场景,所有配置均为单函数粒度生效,不需要强制修改引用项目的全局编译/链接设置,也不需要将函数单独编译为独立二进制文件。
各配置项的可行实现方式
1. 禁用JMC机制对应的_CheckForMyDebugger调用生成
JMC(Just My Code)调试机制开启时,编译器默认会在函数prolog位置插入_CheckForMyDebugger调试桩,可通过两种方式单函数关闭:
- 对MSVC 2022(编译器版本号>=19.30)及以上版本,直接给目标函数加
__declspec(no_jmc)修饰,这是原生支持的单函数JMC开关,不会影响其他函数的JMC逻辑。 - 兼容更早版本MSVC时,在函数定义前添加
__pragma(optimize("jmc-", on)),函数定义完成后再用__pragma(optimize("jmc", on))恢复全局设置,该pragma仅作用于紧邻的函数,不会修改全局编译状态。
2. 禁用_Check_ESP栈校验调用生成
- 最直接的方式是给函数添加
__declspec(safebuffer)修饰,该标记会告知编译器不要为当前函数插入栈指针校验、GS缓冲区检查相关的桩代码,全局GS安全检查不会受影响。 - 如果采用
__declspec(naked)裸函数实现,编译器不会为函数自动生成任何prolog/epilog代码,自然也不会插入_Check_ESP调用,非常适合shellcode跳转这类极简逻辑的函数。
3. 禁用函数COMDAT折叠、跳转thunk链接
COMDAT段折叠、增量链接跳转桩会导致函数实际执行时先经过一段链接器生成的中转代码,可通过以下方式单函数规避:
- 给函数添加
__declspec(no_icf)修饰,明确告知链接器不得对该函数执行相同内容折叠(ICF)操作。 - 不要给函数添加
__declspec(selectany)、inline关键字,这两个标记会强制编译器将函数放入COMDAT段,触发链接器的段逻辑。 - 若需要将函数放在头文件中复用避免重定义,给函数加
static修饰即可,配合__declspec(noinline)避免被编译器内联优化,此时函数会被分配到各编译单元的本地代码段,不会生成跨单元跳转thunk。 - 极端场景下可以用
__declspec(allocate(".text$mn"))将函数强制分配到主代码段,链接器不会对主代码段的非COMDAT函数执行折叠或插桩操作。
可直接复用的实现示例
以下代码可直接放入头文件使用,兼容MSVC2017及以上版本,不需要依赖任何全局编译配置:
// 适配不同版本编译器的函数修饰宏 #if _MSC_VER >= 1930 #define TRAMPOLINE_API __declspec(no_jmc, safebuffer, no_icf, noinline, naked) static #else __pragma(optimize("jmc-", on)) #define TRAMPOLINE_API __declspec(safebuffer, no_icf, noinline, naked) static #endif // 跳转到shellcode入口的示例函数 TRAMPOLINE_API void jump_to_shellcode(void* shellcode_entry) { // x64调用约定下第一个参数存在RCX寄存器,直接跳转即可 __asm { jmp rcx } } #if _MSC_VER < 1930 __pragma(optimize("jmc", on)) #endif
上述实现生成的函数不会包含任何编译器、链接器插入的额外桩代码,执行流会直接跳转到传入的shellcode地址。
内容的提问来源于stack exchange,提问作者Zachary Washburn
相关产品推荐
相关产品推荐

