当前位置:首页 > longjmp
  • 干货 | 函数指针与软件设计

    作者:Li XianJing 记得刚开始工作时,一位高手告诉我,说,longjmp和setjmp玩得不熟,就不要自称为C语言高手。当时我半信半疑,为了让自己向高手方向迈进,还是花了一点时间去学习longjmp和setjmp的用法。 后来明白那不单是跳来跳去那样简单,而是一种高级的异常处理机制,在某些情况下确实很有用。 为了显示自己的技巧,也在自己的程序中用过几次。渐渐发现这样的技巧带来的好处是有代价的,破坏了程序的结构化设计,程序变得很难读,尤其对新手来说。 终于明白这种技巧不过是一种调味料,在少数情况使用几次,可以简化对问题的处理。如果把调味拿来当饭吃,一定会本末倒置,写出的程序会呈现营养不良之状。 事实上,longjmp和setjmp玩得熟不熟与是不是C语言高手,不是因果关系。但是,如果可以套用那位高手的话,我倒想说如果函数指针玩得不熟,就不要自称为C语言高手。为什么这么说呢,函数指针有那么复杂吗? 当然不是,任何一个稍有编程常识的人,不管他懂不懂C语言,在10分钟内,我想他一定可以明白C语言中的函数指针是怎么回事。 原因在于,难的不是函数指针的概念和语法本身,而是在什么时候,什么地方该使用它。函数指针不仅是语法上的问题,更重要的是它是一个设计范畴。 真正的高手当然不单应该懂得语法层面上的技巧,更应该懂得设计上的方法。不懂设计,能算高手吗?怀疑我在夸大其辞吗?那我们先看看函数指针与哪些设计方法有关:与分层设计有关。分层设计早就不是什么新的概念,分层的好处是众所周知的,比较明显好处就是简化复杂度、隔离变化。 采用分层设计,每层都只需关心自己的东西,这减小了系统的复杂度,层与层之间的交互仅限于一个很窄的接口,只要接口不变,某一层的变化不会影响其它层,这隔离了变化。 分层的一般原则是,上层可以直接调用下层的函数,下层则不能直接调用上层的函数。这句话说来简单,在现实中,下层常常要反过来调用上层的函数。 比如你在拷贝文件时,在界面层调用一个拷贝文件函数。界面层是上层,拷贝文件函数是下层,上层调用下层,理所当然。但是如果你想在拷贝文件时还要更新进度条,问题就来了。 一方面,只有拷贝文件函数才知道拷贝的进度,但它不能去更新界面的进度条。另外一方面,界面知道如何去更新进度条,但它又不知道拷贝的进度。怎么办? 常见的做法,就是界面设置一个回调函数给拷贝文件函数,拷贝文件函数在适当的时候调用这个回调函数来通知界面更新状态。 与抽象有关。抽象是面向对象中最重要的概念之一,也是面向对象威力强大之处。面向对象只是一种思想,大家都知道,用C语言一样可以实现面向对象的编程。 这可不是为了赶时髦,而是一种实用的方法。如果你对此表示怀疑,可以去看看GTK+、linux kernel等开源代码。 接口是最高级的抽象。在linux kernel里面,接口的概念无处不在,像虚拟文件系统(VFS),它定义一个文件系统的接口,只要按照这种接口的规范,你可以自己开发一个文件系统挂上去。 设备驱动程序更是如此,不同的设备驱动程序有自己一套不同的接口规范。在自己开发设备开发驱动程序时,只要遵循相应的接口规范就行了。接口在C语言中如何表示?很简单,就是一组函数指针。 与接口与实现分开有关。针对接口编程,而不是针对实现编程,此为《设计模式》的第一条设计准则。分开接口与实现的目标是要隔离变化。软件是变化的,如果不能把变化的东西隔离开来,导致牵一发而动全身,代价是巨大的。这是大家所不愿看到的。 C语言既然可以实现面向对象的编程,自然可以利用设计模式来分离接口与实现。像桥接模式、策略模式、状态模式、代理模式等等,在C语言中,无一不需要利用函数指针来实现。 与松耦合原则有关。面向过程与面向对象相比,之所以显得苍白无力,原因之一就是它不像面向对象一样,可以直观的把现实模型映射到计算机中。 面向过程讲的是层层控制,而面向对象更强调的对象间的分工合作。现实世界中的对象处于层次关系的较少,处于对等关系的居多。也就是说,对象间的交互往往是双向的。这会加强对象间的耦合性。 耦合本身没有错,实际上耦合是必不可少的,没有耦合就没有协作,对象之间无法形成一个整体,什么事也做不了。关键在于耦合要恰当,在实现预定功能的前提下,耦合要尽可能的松散。这样,系统的一部分变化对其它部分的影响会很少。 函数指针是解耦对象关系的最佳利器。Signal(如boost的signal和glib中的signal)机制是一个典型的例子,一个对象自身的状态可能是在变化的(或者会触发一些事件),而其它对象关心它的变化。一旦该对象有变化发生,其它对象要执行相应的操作。 如果该对象直接去调用其它对象的函数,功能是完成了,但对象之间的耦合太紧了。如何把这种耦合降到最低呢,signal机制是很好的办法。 它的原理大致如下:其它关注该对象变化的对象主动注册一个回调函数到该对象中。一旦该对象有变化发生,就调用这些回调函数通知其它对象。功能同样实现了,但它们之间的耦合度降低了。 在C语言中,要解决以上这些问题,不采用函数指针,将是非常困难的。在编程中,如果你从没有想到用函数指针,很难想像你是一个C语言高手。 免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

    时间:2021-05-17 关键词: 指针 C语言 longjmp

  • C语言的反人类函数:setjmp和longjmp的详细剖析

    我希望看这篇文章的你对C++的传统异常处理,即try...catch...throw有了解(不是Windows SEH),这样才能方便你最深入的理解这2个C语言的反人类函数。当然如果不了解就先看下面的“C++式的异常处理”,如果感觉自己了解了,可以直接skip看到“C语言中的模拟”。【C++式的异常处理】首先,我们写一个类,请不要想这个类有什么特别的地方,其只是为了打印出来构造和析构。class CFoo { public:     CFoo()     {         printf("Create CFoo.n");     }     ~CFoo()     {         printf("~Destroy CFoo.n");     } };然后我们写一个函数,这个函数foo是为了根据情况抛出异常:void foo(int exp) {     if (exp == 'a')         throw std::exception("a");     printf("foo ok %d.n",exp); }我们来写第一个main:int main() {     int val = getchar();     foo(val);     return 0; }此时我们输入b,其输出的肯定是:foo ok 98.这里98是b的ascii值。而我们输入a,则会出情况了:因为foo抛出了一个异常,但是没例程去处理他,所以程序崩溃。所以我们现在在main上加上处理foo异常的代码:int main() {     int val = getchar();     try{         foo(val);     }catch (std::exception& ex)     {         printf("skip ex:%s.n",ex.what());     }     return 0; }好了,我们再次输入a,则会出现:skip ex:a.foo在throw下正常的printf则不会执行,流程被改变。所以我们可以简单理解为throw是一个“带有异常信息的”return,当然实际情况比这个复杂的多,我这样说只是为了让你有一种C语言的感觉。还记得上面那个CFoo嘛,我一直没使用它,现在我们把foo函数改一下:void foo(int exp) {     CFoo cfoo;     if (exp == 'a')         throw std::exception("a");     printf("foo ok %d.n",exp); }可以看到我只加了一行代码,在堆栈上开了一个cfoo的实例,我们main不动,输入一个p试试:Create CFoo.foo ok 112.~Destroy CFoo.可以看到,其输出了CFoo的构造和析构,这个是正常的情况,因为我们看到printf执行了。那我们输入a呢,我们来尝试:Create CFoo.~Destroy CFoo.skip ex:a.我们可以看到,虽然throw下面的printf没有被执行,但是CFoo被构造和析构了,这就是C++异常会遵循C++的栈上展开的特点,也就是即便发生异常了,throw前的栈上对象,都需要被析构,如果他们有“真正的”析构代码的话。在执行析构的时候情况也是十分复杂,这里不扯那么多,因为这文章不是介绍C++异常处理的。。。不过为了让你看得更清除点,我们再来把CFoo函数改一下,也是一行代码:void foo(int exp) {     CFoo cfoo;     if (exp == 'a')         throw std::exception("a");     CFoo cfoo2;     printf("foo ok %d.n",exp); }我们再次输入p:Create CFoo.Create CFoo.foo ok 112.~Destroy CFoo.~Destroy CFoo.可以看到这是输出,好,我们输入a:Create CFoo.~Destroy CFoo.skip ex:a.可以很明显的看到,因为cfoo2构造在throw下面,所以它在异常导致foo进行return的时候,并不需要被析构,因为它并没有生成一个真正的实例。好了到这里你就算不懂C++异常处理可能也可以入门了(如果你有兴趣的话)。【C语言中的模拟】这里我们开始正式说一下setjmp和longjmp。如果上面那个foo函数:void foo(int exp) {     if (exp == 'a')         throw std::exception("a");     printf("foo ok %d.n",exp); }因为在C语言中没有C++异常,foo一般使用一个返回值来交出结果判断失败,然后调用者根据返回值进行流程控制,比如foo我们可以写成:bool foo(int exp) {     if (exp == 'a')         return false;     printf("foo ok %d.n",exp);     return true; }我们用bool来给出返回值,当然更多是使用int,char。如果我们有特殊的情怀,或者我们有一些批量的任务,希望用一个统一的例程处理他们的错误。。。我们想在C语言中,使用C++类似的东西,在foo中抛出一个异常,在main中catch呢?这里需要用到setjmp和longjmp,我先给你一些概念:setjmp=try;longjmp=throw。可以看到try和throw都有了,那catch在哪里?要知道C语言是流程式的语言,那catch在C语言中肯定得遵循某一个流程表达式,没错。。。就是if。。。所以你可以看到:setjmp=try,longjmp=throw,if=catch。好像所有条件都具备了,到底怎么玩?来我们继续。我们还是上面那个foo函数:(首先我们使用setjmp和longjmp需要include setjmp.h)void foo(int exp,jmp_buf& jb) {     if (exp == 'a')         longjmp(jb,'a'); //throw std::exception("a");     printf("foo ok %d.n",exp); }然后我们写main:int main() {     jmp_buf jb;     int jmp_ret = setjmp(jb);     if (jmp_ret == 0) //try     {         int val = getchar();         foo(val,jb);     }else{ //catch         printf("skip ex:%d.n",jmp_ret);     }     return 0; }按照上面的路子来,我们输入b:foo ok 98.其输入也是一样的,那我们输入a呢:skip ex:97.这里97是a的ascii码,也就是其是跟上面的异常流程处理是一样的,是不是感觉很奇葩。你肯定在想,为什么,按照理论上来说,setjmp后==0,foo才会执行,按照我们的传统流程,既然foo被执行了,那else应该永远得不到执行,那longjmp又是如何从foo里面跑回去了main?我们来设想一下,else要如何才能被执行?对了,肯定是jmp_ref != 0嘛,没错,longjmp做的就是这个工作。我们先不要在意jmp_buf,我们先看下longjmp的第二个参数,他是一个值类型,这个参数我指定的是'a',也就是97,你看到了,我在prntf里面打印了jmp_ret的值,也就是,我们在longjmp时指定某一个值后,longjmp会把当前函数的流程做一个大转弯,直接跳回到这里:if (jmp_ret == 0) //try而此时,jmp_ret已经是我们指定的值,就是97了,那if的==不会被成立,则去执行else了。此时可能你想,如果我这样:longjmp(jb,0);那不是jmp_ret还是==0,还又去执行foo,又被longjmp,不是死循环了么?这个情况在CRT已经考虑过了,如果你给longjmp使用0值,其会自动修改为1,也就是0值是永远不会被出现的。好,我们来总结:1、首先setjmp需要==0才执行foo。2、foo发现错误,把setjmp的==0给改了。3、if表达式的else被执行。可能你现在头还有点晕,不过我们先说这个到这里,我们来看setjmp的第一个参数:jmp_buf。这个jmp_buf是什么呢,首先我们来再写一个main:int main() {     char sz[128] = "hello.n";     jmp_buf jb;     int jmp_ret = setjmp(jb);     if (jmp_ret == 0) //try     {         int val = getchar();         foo(val,jb);     }else{ //catch         printf("skip ex:%d.n",jmp_ret);         printf(sz);     }     return 0; }输入a,则会输出:skip ex:97.hello.你肯定想这是当然的,因为sz变量在main范围内嘛。但是别忘了,我们访问sz可是在else里,也就是我们访问的时候,是被longjmp跳过去的。。。要知道,执行foo的时候,可能整个堆栈环境已经变得离谱了,如果你知晓汇编,肯定知道,执行foo的时候,main使用堆栈指针EBP(当然也可以直接ESP,不过这里做一个比方)会被保存起来,要等foo进行return的时候,才会恢复EBP,然后main的局部变量才能通过EBP访问到,但是我们的foo可是直接longjmp的,我们没有任何代码用于恢复EBP的值,那如何保证飞过去else的时候,访问sz变量的地址是正确的?对了,在setjmp的时候,CRT会把EBP等变量的值保存在jmp_buf里面,然后在longjmp里面,把EBP的值从jmp_buf里面取出来,进行恢复。这样在执行longjmp的时候,EBP会被恢复到setjmp时的情况,也就保证了sz变量的地址在执行else的时候也是正确的。如果你只会C语言,那看到这里,你应该大概理解了,如果你还了解过汇编,那可以继续看下去,我会为你揭示setjmp、longjmp背后的一些东西。【深入探索】我们把刚才那个exe进行动态反汇编,以便我们整体的了解setjmp和longjmp的所有情况。首先在调试器里面,main是这样的:可以看到,关键就是在TEST EAX,EAX这里有一个JNZ跳,如果不是0则跳到下面的catch。我们来看setjmp的汇编:可以看到其保存了几个windows关键的寄存器。注意,在win32下,eax、edx、ecx被定义为易失寄存器,比如我们调用foo的时候,如果foo需要用到ebx,esi,它也需要保存,退出时恢复,但是使用edx则不需要保存。setjmp也是遵循这个原则。可以看到setjmp的返回是XOR EAX,EAX,就是返回0。好我们来看longjmp的反汇编:可以看到其检测了一下jmp_buf的正确性,然后就进行寄存器的恢复,最终把call自身的堆栈平衡了后,就使用JMP指令直接JMP到setjmp后的那个指令地址,而此时其把EAX改成了longjmp的第二个参数:那接下来的TEST EAX,EAX肯定不会成功,就会跑去执行catch了。【与C++的结合】文章写到这里,应该快结束了,可还有一个点,可能你没注意到,我们还是回到我们第一个代码——CFoo这个类来。在上面的C++异常里面,我们看到了这样的代码:void foo(int exp) {     CFoo cfoo;     if (exp == 'a')         throw std::exception("a");     printf("foo ok %d.n",exp); }按照C++的规范,异常发生的时候,cfoo也会被析构,如果我们使用longjmp呢,就像下面:void foo(int exp,jmp_buf& jb) {     CFoo cfoo;     if (exp == 'a')         longjmp(jb,'a'); //throw std::exception("a");     printf("foo ok %d.n",exp); }你肯定会想,cfoo应该只会被构造,而不会被析构,因为longjmp可是CRT的函数。其实原来我也是这样想的,但是我不懂是不是VC spec,我在跟踪longjmp的时候发现了堆栈展开的代码。。。也就是,其实cfoo在longjmp的时候,也是会被析构的:Create CFoo.~Destroy CFoo.skip ex:97.hello.这个要注意一下。如果你想看汇编,在下面。这个是foo函数的汇编:SEH处理器在这里:然后会展开到析构函数:【完结】为这2个狗血的东西写了那么多,也说的差不多了。其实这2个东西,因为其反人类的特性,在项目开发中,不应该被使用上,在这里只是告诉大家,如果遇到有setjmp、longjmp的情况的时候,可以判断出来代码的执行流程。

    时间:2018-11-27 关键词: setjmp C语言 longjmp

发布文章

技术子站