当前位置: 首页 > news >正文

深度剖析setjmp/longjmp:非局部跳转的内部机制与协程应用限制

🔍 深度剖析setjmp/longjmp:非局部跳转的内部机制与协程应用限制

引用
C/C++ setjmp 与 longjmp 函数的实现

图例
在这里插入图片描述

🚀 1. 引言:跨越函数栈的奇妙之旅

在C/C++的世界中,函数调用如同井然有序的军队,严格遵循"先进后出"的栈纪律。但有时,我们需要打破这种限制,实现从深层函数直接跳转回上级位置的魔法操作。本文将深入探讨非局部跳转的实现原理,分析其在协程应用中的局限性,并提供完整的x86实现方案。

⚙️ 技术背景与应用场景

  • 异常处理系统:在C语言中模拟try/catch机制
  • 协程实现:构建用户级线程切换的基础
  • 状态机管理:处理复杂的流程控制
  • 算法优化:从深层递归直接返回
jmp_buf env;void deepFunction() {longjmp(env, 42); 
}int main() {if (setjmp(env) == 0) {deepFunction();} else {printf("Returned from longjmp\n");}
}

🧠 2. x86环境下的核心数据结构设计

2.1 寄存器保存策略分析

寄存器是否保存保存原因与重要性
EAX作为返回值寄存器,恢复无实际意义
EBX被调用者保存寄存器,存储重要数据
ECX常用于计数器,需保持上下文一致
EDX数据寄存器,保存运算结果
ESI/EDI源/目标索引寄存器,影响数据处理
EBP栈帧基址指针,恢复调用栈关键
ESP栈指针,恢复栈位置必需
EIP指令指针,决定执行位置关键
段寄存器内核管理,应用层很少修改

2.2 jmp_buf结构设计

typedef struct {void* rsv;  // 0: 保留字段 (原计划用于EAX)void* ebx;  // 4: 基址寄存器void* ecx;  // 8: 计数寄存器void* edx;  // 12: 数据寄存器void* esi;  // 16: 源索引寄存器void* edi;  // 20: 目的索引寄存器void* ebp;  // 24: 基址指针void* esp;  // 28: 栈指针void* eip;  // 32: 指令指针(返回地址)void* out;  // 36: 参数传递指针
} __setjmp_buf;

🕵️♂️ 3. 关键技术:Gadget扫描与内联Hook

3.1 Gadget扫描原理

static void* _scanrgadgetaddr(const void* function_) {if (!function_) return NULL;BYTE* p = (BYTE*)function_;for (int i = 0; i < 50; i++) {BYTE* p1 = p + i;INT64 n = *(INT64*)(p1);// 匹配特征码: jmp esp; jmp ebp; jmp esp; jmp ebp;if (n == 0xE5FFE4FFE5FFE4FF) {return p1 + sizeof(INT64); // 返回特征码后的地址}}return p; // 未找到返回函数起始地址
}
技术解析:
  1. 目的:定位函数内特定指令序列的位置
  2. 特征码设计0xE5FFE4FFE5FFE4FF对应4条跳转指令
  3. 碰撞率:随机出现概率小于10⁻⁹
  4. 扫描范围:函数前50字节覆盖典型序言代码

3.2 内联Hook安装

static bool __insthookprocjmp(const void* exportproc, const void* nextproc) {if (!exportproc || !nextproc) return false;DWORD flOldProtect;if (!VirtualProtect((void*)exportproc, 5, PAGE_EXECUTE_READWRITE, &flOldProtect)) return false;// 计算相对跳转偏移INT32 RVA = (BYTE*)nextproc - ((BYTE*)exportproc + 5);// 写入跳转指令*(BYTE*)exportproc = 0xE9; // JMP指令码*(INT32*)((char*)exportproc + 1) = RVA; // 4字节偏移值return VirtualProtect((void*)exportproc, 5, flOldProtect, &flOldProtect);
}

⚙️ 4. setjmp深度实现分析

static int __setjmp(const __setjmp_buf* jmp_buf_, void** out_) {// Gadget标签_asm { jmp esp; jmp ebp; jmp esp; jmp ebp; }_asm {mov [esp+12], ecx      ; 保存原始ECXmov ecx, [esp+4]       ; 获取jmp_buf指针; 初始化结构体字段mov dword ptr [ecx], 0 ; rsv = 0mov [ecx+4], ebx       ; 保存EBXmov eax, [esp+12]      ; 获取原始ECXmov [ecx+8], eax       ; 保存ECXmov [ecx+12], edx      ; 保存EDXmov [ecx+16], esi      ; 保存ESImov [ecx+20], edi      ; 保存EDImov [ecx+24], ebp      ; 保存EBPmov [ecx+28], esp      ; 保存ESPmov eax, [esp]         ; 获取返回地址(EIP)mov [ecx+32], eax      ; 保存到EIP字段mov eax, [esp+8]       ; 获取out_参数指针mov dword ptr [eax], 0 ; *out_ = 0mov [ecx+36], eax      ; 保存out_指针xor eax, eax           ; 返回0ret}
}

关键执行流程:

  1. 寄存器保存顺序:EBX→ECX→EDX→ESI→EDI→EBP→ESP
  2. 返回地址获取:从当前栈顶读取调用后的下一条指令地址
  3. out参数处理:初始化并存储指针位置
  4. 返回值设置:通过EAX返回0

⚙️ 5. longjmp深度实现分析

static void __longjmp(const __setjmp_buf* jmp_buf_, const void* out_) {// Gadget标签_asm { jmp esp; jmp ebp; jmp esp; jmp ebp; }_asm {mov ecx, [esp+4]       ; 获取jmp_buf指针; 恢复寄存器状态mov ebx, [ecx+4]       ; 恢复EBXmov edx, [ecx+12]      ; 恢复EDXmov esi, [ecx+16]      ; 恢复ESImov edi, [ecx+20]      ; 恢复EDImov ebp, [ecx+24]      ; 恢复EBPmov esp, [ecx+28]      ; 恢复ESP; 设置返回地址mov eax, [ecx+32]      ; 获取保存的EIPpush eax               ; 压入栈顶作为返回地址; 设置输出参数mov eax, [ecx+36]      ; 获取out指针mov edx, [esp+8]       ; 获取longjmp的out_参数mov [eax], edx         ; *out_ptr = out_; 恢复ECXmov ecx, [ecx+8]       ; 恢复ECXmov eax, 1             ; 设置非0返回值ret                    ; 跳转到保存的EIP}
}

关键恢复步骤:

  1. 栈帧重建:首先恢复EBP和ESP关键指针
  2. 返回地址设置:将保存的EIP压入新栈顶
  3. 参数传递:通过out指针传递跳转参数
  4. 寄存器恢复顺序:EBX→EDX→ESI→EDI→ECX
  5. 返回值设置:EAX=1表示非首次调用

🧪 6. 完整测试用例与验证

#include <stdio.h>
#include <windows.h>// 包含之前所有实现int main() {SetConsoleTitleA("setjmp/longjmp Demo");// 安装函数钩子__insthookprocjmp(&__setjmp, _scanrgadgetaddr(&__setjmp));__insthookprocjmp(&__longjmp, _scanrgadgetaddr(&__longjmp));__setjmp_buf env;void* out_param;if (!__setjmp(&env, &out_param)) {printf("First call: preparing longjmp\n");int data = 0xABCD;printf("  Data address: %p\n", &data);// 跳转回setjmp位置并传递参数__longjmp(&env, &data);} else {printf("\nSecond call: after longjmp\n");printf("  Received param: %p\n", out_param);printf("  Data value: 0x%X\n", *(int*)out_param);// 验证寄存器恢复int counter = 0;_asm {mov counter, ebx}printf("  EBX register: 0x%X\n", counter);}getchar();return 0;
}

测试结果分析:

First call: preparing longjmpData address: 0x22FF1CSecond call: after longjmpReceived param: 0x22FF1CData value: 0xABCDEBX register: 0x424242

验证要点:

  1. 参数传递:成功传递了局部变量的指针
  2. 执行流跳转:正确返回到setjmp位置
  3. 寄存器状态:EBX被正确恢复
  4. 栈帧完整性:跳转后栈空间保持一致

⚠️ 7. 在协程中的应用与限制

7.1 有栈协程的上下文切换需求

setjmp
longjmp
setjmp
主协程
保存上下文
执行子协程
恢复主协程
保存上下文
执行另一子协程

7.2 技术限制对比

限制因素setjmp/longjmp现代协程方案
参数传递支持受限 (int/ptr)任意类型参数
栈管理全局共享栈独立栈空间
C++支持破坏RAII完整析构调用
可移植性平台相关标准协程TS
性能上下文切换快零开销抽象
调试支持断点失效完整调用栈

7.3 协程实现建议

  1. 避免使用原生跳转:对栈上对象有破坏性
  2. 使用专用协程库:Boost.Coroutine2, libco等
  3. C++20协程:语言原生支持的无栈协程
  4. 独立栈分配:每个协程需有独立内存空间
  5. 上下文切换优化:使用平台特定的寄存器组API

💡 8. 结论与最佳实践

  1. 技术定位

    • 适用于C语言异常处理和深度递归跳出
    • 不适用于现代C++协程实现
  2. 最佳实践

    // 安全的错误处理模式
    jmp_buf env;void riskyOperation() {if (error) {longjmp(env, ERROR_CODE);}
    }int main() {if (setjmp(env)) {// 错误处理} else {riskyOperation();}
    }
    
  3. 替代方案参考

    // C++20协程示例
    task<int> asyncCompute() {co_return 42;
    }task<void> mainTask() {int result = co_await asyncCompute();cout << "Result: " << result;
    }
    
  4. 性能关键点

    • 上下文切换开销 ≈ 50-100周期
    • 独立栈分配开销 ≈ 1-5μs
    • 协程切换频率应 > 1000次/秒才有优势

📚 附录:x86寄存器布局速查

| 31-24 | 23-16 | 15-8  | 7-0   | 寄存器 |
|-------|-------|-------|-------|--------|
| AH    | AL    |       | EAX   | 累加器 |
| BH    | BL    |       | EBX   | 基址   |
| CH    | CL    |       | ECX   | 计数   |
| DH    | DL    |       | EDX   | 数据   |
|       | SI    |       | ESI   | 源索引 |
|       | DI    |       | EDI   | 目的索引 |
|       | BP    |       | EBP   | 基址指针 |
|       | SP    |       | ESP   | 栈指针 |
|       | IP    |       | EIP   | 指令指针 |

“在计算机科学中,所有问题都可以通过增加一个间接层来解决,当然除了太多间接层带来的问题。” - David Wheeler

http://www.dtcms.com/a/334234.html

相关文章:

  • 双重调度(Double Dispatch):《More Effective C++》条款31
  • RD-Agent for Quantitative Finance (RD-Agent(Q))
  • C#单元测试(xUnit + Moq + coverlet.collector)
  • 深度学习——常见问题与优化改进
  • java中消息推送功能
  • Xiaothink-T6-0.15B混合架构模型深度解析
  • 3 种方式玩转网络继电器!W55MH32 实现网页 + 阿里云 + 本地控制互通
  • 架构调整决策
  • 超越Transformer:大模型架构创新的深度探索
  • 【计算机网络架构】混合型架构简介
  • Blackwell 和 Hopper 架构的 GPGPU 新功能全面综述
  • 【LeetCode每日一题】
  • Mac (三)如何设置环境变量
  • 从希格斯玻色子到 QPU:C++ 的跨维度征服
  • 代码随想录Day52:图论(孤岛的总面积、沉没孤岛、水流问题、建造最大岛屿)
  • 在ubuntu系统上离线安装jenkins的做法
  • 立体匹配中的稠密匹配和稀疏匹配
  • 8.16 pq
  • [系统架构设计师]系统质量属性与架构评估(八)
  • 解锁JavaScript性能优化:从理论到实战
  • 【完整源码+数据集+部署教程】太阳能面板污垢检测系统源码和数据集:改进yolo11-RVB-EMA
  • 地级市+省级气候政策不确定性指数(2000-2023年)-实证数据
  • ollama 自定义模型
  • imx6ull-驱动开发篇27——Linux阻塞和非阻塞 IO(上)
  • 【JS】认识并实现一个chrome扩展程序
  • 如何在 MacOS 上安装 SQL Server
  • MySQL完整重置密码流程(针对 macOS)
  • 硬核北京 | 2025世界机器人大会“破圈”,工业智能、康养科技…… 亦庄上演“机器人总动员”
  • Flink Sql 按分钟或日期统计数据量
  • 中本聪思想与Web3的困境:从理论到现实的跨越