C语言的setjmp和longjmp

摘要

本文描述了C语言中setjmp和longjmp函数的功能和原理,目的是为学习SRS协程原理打下基础。

异常处理

我们知道,在C++语言中,我们可以通过try catch机制来捕获函数中的异常,然后从代码正常执行流程突然跳出到catch关键词描述的异常处理代码分支中。在C语言中,没有C++语言这种内置的异常捕获机制,该如何实现类似的功能呢?方法有两个,一是用操作系统提供的异常处理机制,但是这个破坏了C代码的可移植性,还有一个方法就是用setjmp和longjmp函数来模拟类似异常处理的机制。

示例代码

先看一段示例代码,对比C和C++如何处理一个除0的异常。
这是一个只包含单个main.cpp文件的C++示例项目。

#include <cstdio>
#include <cstdlib>
#include <csetjmp>

jmp_buf g_execution_point_context;

// 用C方式处理异常
int c_safe_div(int a, int b) {
    if (b == 0) {
        printf("[C] do not allow division by 0\n");
        // 将setjmp的返回值设置为1
        longjmp(g_execution_point_context, 1);  // 直接跳转到setjmp调用的下一行
        return 0; // 此行代码永远不会被执行
    } else {
        return a / b;
    }
}

void demo_c_setjmp_longjmp() {
    int ret = setjmp(g_execution_point_context);
    if (ret == 0) {
        printf("return from setjmp\n");
        // c_safe_div发现异常后,longjmp到setjmp保存的执行点,再次执行if判断。
        c_safe_div(10, 0);
    } else {
        printf("return from longjmp: %d\n", ret);
    }
}

// 用C++方式处理异常
int cpp_safe_div(int a, int b) {
    if (b == 0) {
        printf("[C++] do not allow division by 0\n");
        // 抛出值为1的异常
        throw 1; // 直接跳转到catch分支内。
        return 0; // 此行代码永远不会被执行。
    } else {
        return a / b;
    }
}

void demo_cpp_exception() {
    try {
        // 可抛出异常的代码块,抛出异常后,由catch语句列表处理。
        cpp_safe_div(10, 0);
    } catch (int exception_code) {
        printf("catched exception code: %d\n", exception_code);
    }
}

int main()
{
    demo_c_setjmp_longjmp();
    demo_cpp_exception();
    return 0;
}

 demo_c_setjmp_longjmp函数分析:
在一次函数调用中,一个if else语句的两个分支都被执行了,是不是有点不符合直觉?
这是因为setjmp保存了一个线程的执行点上下文,在遇到除数为0时,通过longjmp调用,又恢复了当时保存的执行点上下文,在那个执行点后继续执行if判断导致的。
对比C的setjmp/longjmp机制和C++的try catch机制,是否觉得还是C++的异常处理机制比较符合直觉。

执行点上下文

此文将某条尚未被执行但是即将被执行的机器指令的地址称为执行点(可在此地址处设置一个断点)。在上述代码用了一个jmp_buf结构类型的全局变量g_execution_point_context保存线程的执行点上下文。这全局变量名是作者独创的,中文意思是执行点上下文,也可以理解为执行点的环境,那什么是执行点上下文呢?
简单地说,就是CPU中的一些寄存器的值,这些值描述了线程执行到某条指令前的状态,x86架构的CPU包含如下寄存器:

  • esp:保存当前栈顶的地址
  • ebp:保存当前函数栈帧的地址,在函数的进入点处,把esp保存到ebp,这样在函数任何位置,都可以通过ebp加偏移拿到函数的参数。
  • eip:保存下一条指令的地址。
  • eflags:保存CPU执行指令时的各种标志位,比如溢出标志、进位标志、符号标志、零标志、单步标志等。.
  • eax:通常用于保存整数类型的返回值。
  • ecx:通常用于保存C++的this指针,或者用作循环指令的计数器。

实际上,x86 CPU功能寄存器有很多,但是setjmp只负责保存少数几个寄存器的值。
在同一个线程中,有了执行点上下文的保存与恢复机制,我们就可以通过longjmp在不同的执行点之间自由切换,不用担心丢失原来的执行点上下文,这就是协程的基本原理。

总结

setjmp和longjmp分别被用于保存和恢复同一个线程的执行点上下文,这个特性可用于模拟C++异常处理,还可以用于实现协程功能。

猜你喜欢

转载自blog.csdn.net/bigwave2000/article/details/132215805
今日推荐