C语言中利用setjmp和longjmp做异常处理

错误处理是任何语言都需要解决的问题,只有不能保证100%的正确运行,就需要有处理错误的机制。异常处理就是其中的一种错误处理方式。

1 过程活动记录(Active Record)

C语言中每当有一个函数调用时,就会在堆栈(Stack)上准备一个被称为AR的结构,抛开具体编译器实现细节的不同,这个AR基本结构如下所示。

每当遇到一次函数调用的语句,C编译器都会产生出汇编代码来在堆栈上分配这个AR。例如下面的C代码:

void a(int i)
{
    if(i==0){
        i = 1;
    }
    else
    {
        printf("i = %d \n", i);
    }
}

int main(int argc, char** argv)
{
    a(1);
}

当程序运行后执行到printf()语句时,堆栈上的AR布局如下:

2 通过setjmp和longjmp操纵AR,完成任意跳转

那么如何来操纵AR呢,一个可能的方法是,根据局部变量的地址进行推算,例如对于上面的a函数,执行a函数时的当前AR地址就是参数i的地址偏移8个字节,也就是 ((char*)&i) - 8。然而,不同的C编译器,以及不同的硬件平台都会产生不同的AR结构布局,甚至在一些平台上,AR根本不会存放到Stack中。所以这种方式操纵AR是不通用的。

为此,C语言通过库函数的方式提供了操纵AR的统一方法,那就是setjmp和longjmp函数。

int setjmp(jmp_buf jb);
void longjmp(jmp_buf jb, int r);

setjmp用于保存当前AR到jb变量中;

而longjmp用于设置当前AR为jb,并跳转到调用setjmp();之后的第一个语句处。其结果就相当于回到了setjmp()刚执行完毕,只是偷偷的修改了setjmp的返回值。

setjmp()第一次调用时总是返回0,而通过longjmp(jb,r)跳转后其返回值总是被修改为r,并且r不能为0。这样程序中就很容易根据setjmp()的返回值来判断是否是longjmp()导致了跳转才执行到此。

setjmp/longjmp主要从嵌套的函数调用中跳出来。

#include <stdio.h>
#include <setjmp.h>

jmp_buf jb;
void a();
void b();
void c();

int main()
{
    if(setjmp(jb)==0){
        a();
    }
    printf("after a(); \n");
    return 0;
}
void a()
{
    b();
    printf("a() is called\n");
}
void b()
{
    c();
    printf("b() is called\n");
}
void c()
{
    printf("c() is called\n");
    longjmp(jb, 1);
}

在c()中可以直接跳转到main()中,实际上longjmp不限制跳转的目的地,可以跳转到任意位置并恢复当时的堆栈环境(堆栈平衡)。

3 C语言中实现异常处理

异常处理是错误处理的一种方式,C语言中更常用的错误处理方式是检测函数返回值。

#include <stdio.h>

int f1()
{
    if(1/*正确执行*/) { return 1; }
    else { return -1; }
}
int f2()
{
    if(0/*正确执行*/) { return 1; }
    else { return -1; }
}

int main()
{
    if(f1()<0){
        printf("错误处理1\n");
        exit(1);
    }

    if(f2()<0){
        printf("错误处理2\n");
        exit(2);
    }
    return 0;
}

上面代码显示了常见的C语言错误处理方式。严谨的软件开发中,必须检测每一次函数调用可能出现的错误,并做相应的处理。造成的后果就是冗长繁琐的代码。为了统一处理错误,C++,C#,Java等现代语言引入了异常处理机制。同样功能的C++代码大概如下:

#include <stdio.h>

class Ex1{
};
class Ex2{
};
void f1()
{
    printf("进入f1()\n");
    if(0/*正确执行*/){ }
    else {
        throw Ex1();
    }
    printf("退出f1()\n");
}
void f2()
{
    printf("进入f2()\n");
    if(1/*正确执行*/) {  }
    else {
        throw Ex2();
    }
    printf("退出f2()\n");
}

int main()
{
    try{
        f1();
        f2();
    }catch(Ex1 &ex){
        printf("处理错误1\n");
        exit(1);
    }
    catch(Ex2 &ex){
        printf("处理错误2\n");
        exit(2);
    }
    return 0;
}

程序输出:

进入f1()
处理错误1

可见,异常处理让代码看起来更加整洁,逻辑代码在一起,错误处理代码在一起。throw后面的语句不再执行,执行流直接跳转到最近的try对应的catch块。

可以推测,

  • throw要负责两件事情:(1)完成跳转;(2)恢复堆栈AR;
  • try则负责保存当前AR

可见这与setjmp/longjmp基本相当。于是可以在C中近似写成。

#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>

jmp_buf jb;

void f1()
{
    printf("进入f1()\n");
    if(0/*正确执行*/){ }
    else {
        longjmp(jb,1);
    }
    printf("退出f1()\n");
}
void f2()
{
    printf("进入f2()\n");
    if(1/*正确执行*/) {  }
    else {
        longjmp(jb, 2);
    }
    printf("退出f2()\n");
}

int main()
{
    int r = setjmp(jb);
    if(r==0){
        f1();
        f2();
    }else if(r==1){
        printf("处理错误1\n");
        exit(1);
    }else if(r==2){
        printf("处理错误2\n");
        exit(2);
    }
    return 0;
}

当然完整的异常处理远比这里的代码要复杂,需要考虑异常的嵌套等,这里仅仅给出最简单的思路。

4 不要在C++中使用setjmp和longjmp

C++为异常处理提供了直接支持。除非极特殊需要,不要再重新实现自己的异常机制,尤其需要说明的是,简单的调用setjmp/longjmp有可能带来问题。如

#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>

class MyClass
{
public:
    MyClass(){ printf("MyClass::MyClass()\n");}
    ~MyClass(){ printf("MyClass::~MyClass()\n");}
};
jmp_buf jb;

void f1()
{
    MyClass obj;
    printf("进入f1()\n");
    if(0/*正确执行*/){ }
    else {
        longjmp(jb,1);
    }
    printf("退出f1()\n");
}
void f2()
{
    printf("进入f2()\n");
    if(1/*正确执行*/) {  }
    else {
        longjmp(jb, 2);
    }
    printf("退出f2()\n");
}

int main()
{
    int r = setjmp(jb);
    if(r==0){
        f1();
        f2();
    }else if(r==1){
        printf("处理错误1\n");
        exit(1);
    }else if(r==2){
        printf("处理错误2\n");
        exit(2);
    }
    return 0;
}

g++编译,程序输出:

MyClass::MyClass()
进入f1()
处理错误1

vc++编译,程序输出:

MyClass::MyClass()
进入f1()
MyClass::~MyClass()
处理错误1

longjmp()跳转前局部对象可能并不会析构(g++),也可能析构(VC++),C++标准对此并无明确要求。这种依赖于具体编译器版本的代码是应该避免的。

而C++本身的throw关键字,却能严格保证局部对象构造和析构的成对调用。

5 辩证看待异常处理

为实现异常处理,C++编译器为此必须做更多的工作,也必然导致在AR中直接或间接地存放更多的信息,并产生操作这些信息的汇编代码,最终必然导致运行效率的降低。

另一方面,已经存在大量没有严格使用异常处理C++函数库和类库,兼容的C库更是没有异常的概念,历史的包袱让C++很难完全采用异常处理。在这个方面,Java和C#从头开始,重要的库都实现了标准的异常处理规范,完全采用异常机制切实可行。

有趣的是C++11在标准中删除了异常规范,而且添加了 noexcept关键字来声明一个函数不会抛出异常,可见异常并不是那么受欢迎。

C++编译器也会提供一个禁用异常的选项,下面是VC++中禁用异常的方法。

然而,C++的STL广泛使用异常,所以实际上使用了STL的C++程序是不可能禁用异常的,要是没有了STL,C++又有什么优势了呢?C++在不断的矛盾冲突中向前发展者。

时间: 2024-12-23 10:33:13

C语言中利用setjmp和longjmp做异常处理的相关文章

【示例】C语言中利用数组存放函数指针

C语言中利用数组存放函数指针,增加函数使用的灵活性.使用时只需提供数组索引,即可调用不同函数. 预备知识: 1.指向函数的指针 一个函数在编译时被分配一个入口地址,这个地址就被称为函数的指针. 例如: int max(int,int); // 声明函数,比较两数大小 int (*p)(); //声明指向函数的指针变量 p=max; //将函数max的入口地址赋给指针变量p int c=(*p)(a,b); //调用函数 2.函数指针作为函数参数 该例子中每次给process函数不同实参(函数名)

C中的setjmp与longjmp

setjmp与longjmp是属于C语言中的,当然,C++也会有这两个函数了.他们的原型如下: int setjmp( jmp_buf env ); 作用:第一次调佣时,将寄存器的当前状态信息全部存入到env中,并返回0.如果在某处调用了longjmp(env,x),且x!=0,则setjmp的返回值将设为x.而若x==0,则setjmp返回1. void longjmp( jmp_buf env,int value ); 作用:重新存储当前寄存器的状态信息,并将setjmp的返回值设为valu

C语言中值得深入知识点----数组做函数参数、数组名a与&amp;a区别、数组名a的&quot;数据类型&quot;

1.数组作为函数参数 C语言中,数组做为函数的参数,退化为指针.数组作为参数传给函数时,传的是指针而不是数组,传递的是数组的首元素的地址.这里我们以将以整形变量排序来讲解. void sortArray(int a[] ,int num )以及void sortArray(int a[100] ,int num )都可以用void sortArray(int *a ,int num )表示.一般来说函数参数如果为数组,可以有两个参数,一个是数组名,一个是数组长度.对于排序而已,一般是要知道给定数

Python语言中的关键字(自己做的读书笔记)

电脑配置:联想笔记本电脑 windows8系统 Python版本:2.7.8 本文章撰写时间:2015.1.1 作者:陈东陈 阅读说明: 1.本文都是先解释,后放图片: 2.文中斜体部分要么为需要输入的内容,要么为电脑本来的一些功能名称 python语言中的关键词: and del from not while as elif global or with assert if else pass yield break except import  print class exec in rai

Go语言中利用append巧妙的删除slice切片中的元素

package main import ( "fmt" ) //删除函数 func remove(s []string, i int) []string { return append(s[:i], s[i+1:]...) } func main() { s := []string{"a", "b", "c"} fmt.Println(s) s = remove(s, 1) fmt.Println(s) } 原文地址:http

setjmp()、longjmp() Linux Exception Handling/Error Handling、no-local goto

目录 1. 应用场景 2. Use Case Code Analysis 3. 和setjmp.longjmp有关的glibc and eglibc 2.5, 2.7, 2.13 - Buffer Overflow Vulnerability 1. 应用场景 非局部跳转通常被用于实现将程序控制流转移到错误处理模块中:或者是通过这种非正常的函数返回机制,返回到之前调用的函数中 1. setjmp.longjmp的典型用途是异常处理机制的实现:利用longjmp恢复程序或线程的状态,甚至可以跳过栈中

setjmp和longjmp简介

setjmp和longjmp简介 1setjmp和longjmp简介 与刺激的abort函数和exit函数相比,goto语句看起来是处理异常的更可行方案.但是goto是本地的,它只能跳到所在函数内部的标号上,而不能将控制权转移到所在程序的任意地点(当然,除非你的所有代码都在main体中).为了解决这个限制,C函数库提供了setjmp函数和longjmp函数,它们分别承担非局部标号和goto作用.头文件<setjmp.h>申明了这些函数及同时所需的jmp_buf数据类型. 原理非常简单: 1.

C语言之setjmp和longjmp详细剖析

我希望看这篇文章的你对C++的传统异常处理,即try...catch...throw有了解(不是Windows SEH),这样才能方便你最深入的理解这2个C语言的反人类函数. 当然如果不了解就先看下面的"C++式的异常处理",如果感觉自己了解了,可以直接skip看到"C语言中的模拟". [C++式的异常处理] 首先,我们写一个类,请不要想这个类有什么特别的地方,其只是为了打印出来构造和析构. class CFoo { public: CFoo() { printf(

【C语言天天练(五)】setjmp和longjmp

setjmp和longjmp组合可以实现跳转,与goto语句有相似的地方.但有以下不同: 1.用longjmp只能跳回到曾经到过的地方.在执行setjmp的地方仍留有一个过程活动记录.从这个角度看,longjmp更像是"从何处来"而不是"往何处去".longjmp接收一个额外的整型参数并返回它的值,这可以知道是由longjmp转移到这里的还是从上一条语句执行后自然而然来到这里的. 2.goto语句不能跳出C语言当前的函数,而longjmp可以跳的更远,可以跳出函数,