转载:http://tieba.baidu.com/p/1393753521

灌水的时候从goto一路拐到了setjmp, 顺便也试了试貌似这东西确实是没有析构效果的。之前并没有看过setjmp的实现,也有一些错误的理解,于是实现了一个简单的版本澄清一下。其实setjmp让人感兴趣的地方大概也就是它和fork在使用方式上的相似之处(对一条c语言看来的调用语句,检查了两次返回值),不过如果稍加考据的话,会发现其实我们可以使用setjmp和longjmp来实现简单的协程——不过比起setjmp和longjmp这种东西,现有的协程库是更好的选择。

很容易写出一个使用setjmp-longjmp的例子:

#include    <iostream>

#include    <setjmp.h>

static jmp_buf buf;

class test_class {
    private:
        int val;
    public:
        test_class(int v = 0) : val(v) {
            std::cout << "constructor " << val << std::endl;
        }
        ~test_class() {
            std::cout << "destructor" << val << std::endl;
        }    
};

void rtn(void) {
    test_class t(5);
    longjmp(buf, 1);
}

int main(void) {
    if (!setjmp(buf)) {
        rtn();
    } else {
        std::cout << "main" << std::endl;
        while (1);
    }

return 0;
}

编译之后运行的情况是:

varna@iflit:~/backup/essay$ ~/workspace/langtest/cc/testjmp 
constructor 5
main
^C
varna@iflit:~/backup/essay$

这里我们注意到if语句的两个分支都得到了执行,看起来像是初次调用setjmp的时候返回了0,而rtn调用longjmp时setjmp又返回了longjmp指定的1。读者可能会感到奇怪,毕竟程序是逐条语句执行的,longjmp是否真的调用了setjmp也没法确定。总之,setjmp像我们熟悉的fork一样返回了两个值,当然与fork的差别在于fork在同一进程之内只有一种返回值,无论是0,一个pid或是错误。

那么下面我们来实现一个简单的setjmp-longjmp. 这里的实现是体系相关的,下面的例子都假设是IA-32;出于方便起见也使用了GCC内联汇编而不是独立的汇编单元,并没有考虑到线程安全的因素因而不要真的拿来做协程。同时我也不保证c标准库的实现是否是这样,这只是一个〔来自钟表外侧的猜测〕。

 
首先我们假设我们在使用cdecl调用约定。实际上在涉及栈桢废弃和构造的废止性返回当中,最好明确调用约定以确保在被调函数的栈桢上(这个说法并不严格,实际上我们会向调用者的栈桢顶方向稍微走几步)能取得调用者的栈桢范围以及被调函数的返回地址。cdecl保证了由调用者完成栈桢的清理(因而很容易实现不定长参数列表),所以我们在返回的时候只需要恢复调用者的栈桢并跳转到返回地址就可以。

为了获得调用者的栈桢范围,考虑函数对应的汇编代码:

push ebp
mov ebp, esp

...调整esp给出参数、局部变量和临时变量的空间

...函数体

mov esp, ebp
pop ebp
ret

这里我们能看出,调用者的栈桢底被压到栈上,如果读者对call指令有了解的话,会意识到次栈顶此时是call压入的返回地址。之后把ebp置为运行时栈的栈顶esp, 同时也是被调函数的栈桢底。当函数决定返回的时候,从ebp中恢复出曾经的esp, 弹出栈顶以恢复调用者的栈桢底,执行ret指令将弹出目前的栈顶,也就是被调函数的返回地址。这时,esp已经恢复成了调用者真实的栈顶。

如果要简单地图解一下,在被调函数完成esp调整之后的时刻,栈上的状况是

(高址)

(调用者的栈桢)
返回地址(四字节)
被调函数的栈桢底(四字节),此处的地址也是当前ebp的地址
(被调函数的栈桢,此处位于最低地址的有效数据被esp所指向)

这样,当我们拿到ebp之后,0x4(ebp)的位置就是返回地址,而ebp的值加0x8就是调用者的栈桢顶。同时我们知道,cdecl调用使用eax来传递返回值,因此我们可以给出下面的结构用于辅助完成废止性跳转:

struct c_jmpbuf {
    unsigned long esp;
    unsigned long ebp;
    unsigned long eip;
    unsigned long eax;
};

这是完成跳转所需要的最少数量的硬件上下文。接着我们可以根据前面的描述实现setjmp, 完成在某一点上对上下文结构的填写:

 
int c_setjmp(struct c_jmpbuf *buf) {
    buf->eax = 0;
    asm volatile ("mov %%ebp, %0" : "=r" (buf->ebp));
    asm volatile ("mov %%esp, %0" : "=r" (buf->esp));
    asm volatile ("mov 0x4(%%ebp), %0" : "=r" (buf->eip));

buf->esp = buf->ebp + 0x8;
    buf->ebp = *((unsigned long *) buf->ebp);

return 0;
}

首先获得ebp和esp(这里的实现并没有真的用到esp),之后获得0x4(ebp)处的返回地址填入断点eip. 之后调整出调用者栈桢需要的esp和ebp填入上下文,注意如果懒得用局部变量的话就必须先调整esp, 因为它的值直接依赖于当前栈桢使用的ebp值。

最麻烦的工作已经完成了,下面我们可以轻易实现跳转本身:

int c_longjmp(struct c_jmpbuf *buf, int retval) {
    asm volatile ("mov %0, %%eax" : : "r" (retval));
    asm volatile ("mov %0, %%ecx" : : "d" (buf->eip));
    asm volatile ("mov %0, %%esp" : : "d" (buf->esp));
    asm volatile ("mov %0, %%ebp" : : "d" (buf->ebp));
    asm volatile ("jmp *%ecx");
}

我们只需要根据上下文恢复四个寄存器:作为返回值的eax, 作为返回地址的eip, 标识栈桢范围的ebp和esp. 之后强行跳转到eip给出的返回地址就可以了。但是这段代码本身只是个简单例子,读者会注意到eip/esp/ebp的恢复过程中都指定了edx作为中间寄存器以避开ebx, 但esi和edi并没有恢复。我并不确定这样的实现在绝大部分情况下都能正常工作,所以如果真的要实现最好使用独立的汇编单元来做,当然可以考虑约定fastcall或者扩展regparm, 不然在上下文突变区里清理栈桢绝对不是什么容易测试的工作。

 
两小段程序,完成了一套基本的废止性跳转操作。下面我们编写一个样例来测试看:

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

struct c_jmpbuf {
    unsigned long esp;
    unsigned long ebp;
    unsigned long eip;
    unsigned long eax;
};

int c_setjmp(struct c_jmpbuf *buf) {
    buf->eax = 0;
    asm volatile ("mov %%ebp, %0" : "=r" (buf->ebp));
    asm volatile ("mov %%esp, %0" : "=r" (buf->esp));
    asm volatile ("mov 0x4(%%ebp), %0" : "=r" (buf->eip));

buf->esp = buf->ebp + 0x8;
    buf->ebp = *((unsigned long *) buf->ebp);

return 0;
}

int c_longjmp(struct c_jmpbuf *buf, int retval) {
    asm volatile ("mov %0, %%eax" : : "r" (retval));
    asm volatile ("mov %0, %%ecx" : : "d" (buf->eip));
    asm volatile ("mov %0, %%esp" : : "d" (buf->esp));
    asm volatile ("mov %0, %%ebp" : : "d" (buf->ebp));
    asm volatile ("jmp *%ecx");
}

struct c_jmpbuf buf, buf1, buf2;

void rtn() {
    printf("world\n");
    c_longjmp(&buf2, 1);
}

void rtn2() {
    if (c_setjmp(&buf2) == 0) {
        printf("hello rtn2\n");
        rtn();
    } else {
        printf("again rtn2\n");
        c_longjmp(&buf, 1);
    }
}

void rtn1() {
    if (c_setjmp(&buf1) == 0) {
        printf("hello rtn1\n");
        rtn2();
    } else {
        printf("again rtn1\n");
        c_longjmp(&buf, 1);
    }
}

int main(void) {
    unsigned long ret;
    asm volatile ("mov %%ebp, %0" : "=r" (ret));
    printf("main ebp = %x\n", ret);

if (c_setjmp(&buf) == 0) {
        printf("hello main\n");
        rtn1();
    } else {
        printf("again main\n");
    }

return 0;
}

下面是运行结果。注意到这里rtn2的废止性返回越过了rtn1返回main. 读者可以修改样例进行测试,如果有失败的情况欢迎提出。

varna@iflit:~/backup/essay$ ~/workspace/langtest/spec/sjmp 
main ebp = bfe7c408
hello main
hello rtn1
hello rtn2
world
again rtn2
again main
varna@iflit:~/backup/essay$

最后是一点编写和调试方面的建议。如果读者并不习惯操作程序的运行时结构,可以练习使用一种反汇编工具和一种调试工具,逐步改正自己的程序。反汇编工具可以用来确定实际的返回地址,而调试器则可以显示出各寄存器和各变量的值,从而可以逐个发现并修正错误的地址。

[jumping to the gate] 娱乐向setjmp的更多相关文章

  1. Android Jni 调用

    Chap1:JNI完全手册... 3 Chap2:JNI-百度百科... 11 Chap 3:javah命令帮助信息... 16 Chap 4:用javah产生一个.h文件... 17 Chap5:j ...

  2. 使用Red Gate Sql Data Compare 数据库同步工具进行SQL Server的两个数据库的数据比较、同步

    Sql Data Compare 是比较两个数据库的数据是否相同.生成同步sql的工具. 这一款工具由Red Gate公司出品,我们熟悉的.NET Reflector就是这个公司推出的,它的SQLTo ...

  3. 使用Red Gate Sql Compare 数据库同步工具进行SQL Server的两个数据库的结构比较、同步

    将测试版的项目同步(部署)到正式版的时候,两个数据库的结构比较与同步时,如果修改数据库的时候没有记录好修改了那些表,很难将两个数据库进行同步 RedGate Sql Compare使用简介说明: 1. ...

  4. 【.net深呼吸】动态类型(娱乐篇)

    有朋友跟老周说,动态类型是干吗的,他不太熟悉,希望老周可以讲一讲.没事,这事情老周也比较TMD乐意做的,因为老周写的这些烂文本来就是为了普及基础知识的,坚定不移地为社会基础教育而服务. 首先,咱们要知 ...

  5. Gate Of Babylon bzoj 1272

    Gate Of Babylon (1s 128MB) babylon [问题描述] [输入格式] [输出格式] [样例输入] 2 1 10 13 3 [样例输出] 12 [样例说明] [数据范围] 题 ...

  6. C和指针 第十六章 标准函数库 本地跳转setjmp.h

    setjmp和longjmp提供一种类似goto语句的机制,但它的作用域不局限于同一个函数的作用域之内.这些函数可以用于深层次的嵌套函数调用链. int setjmp(jmp_buf state); ...

  7. 复利计算APP版-----娱乐一下

    先不说那么多,下载地址来一个:http://pan.baidu.com/s/1eSz2GBg 目前版本号为:0.3 lastest 软件上线了!三平台首发! 下载地址: http://shouji.b ...

  8. Red Gate(SQLToolbelt)SQL Server的安装与注册(破解)

    Red Gate(SQLToolbelt)是SQL Server辅佐工具 1.SQL Compare 比较和同步SQL Server数据库结构 2.SQL Data Compare 比较和同步SQL ...

  9. C 语言中 setjmp 和 longjmp

    在 C 语言中,我们不能使用 goto 语句来跳转到另一个函数中的某个 label 处:但提供了两个函数——setjmp 和 longjmp来完成这种类型的分支跳转.后面我们会看到这两个函数在处理异常 ...

随机推荐

  1. 【Xamarin For IOS 开发需要的安装文件】

    官网安装文件下载: http://download.xamarin.com/XamarinforMac/Mac/xamarin.mac-2.0.1.64.pkghttp://download.xama ...

  2. 再论dynamic 关键字

    有关动态数据类型 ,大家估计在实际中用的比较多了,不是很陌生.有关自己在项目中 的实际钉子总结: 1  匿名对象中的字段,是只读的,不能赋值 2 动态类型 指向强类型实例,注意观察内部的属性可访问性 ...

  3. Delphi 把字符串读到流中的操作。

    var FReQuestM := TMemoryStream FReQuestM.Write(PChar(FcVoucherXML)^, Length(FcVoucherXML)); 这样就读到流中了 ...

  4. poj1305:概念水题

    了解一下毕达哥拉斯三元组概念= = 暴力求出所有的本源三元组即可 代码: #include <iostream> #include <stdio.h> #include< ...

  5. Ubuntu 无线连接能上网,但是有线连接不能上

    这两天装Ubuntu,遇到小问题.最头疼的还是上网,过去我装了Ubuntu时,都是插上网线就能直接上网,这次就不行了. 我刚点开一个网页,接下来点就不能上了,但是无线连接就可以正常上网. 我在一个论坛 ...

  6. input文本框获取焦点和失去焦点判断

    onBlur:当输入框失去焦点后 onFocus:当输入框获得焦点后 这两个JavaScript事件是写在html标签中的例如: <input type="text" onB ...

  7. HDU--1584--蜘蛛牌--深搜版本号

    蜘蛛牌 Time Limit: 10000/5000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total Submi ...

  8. QT5 r 加入qwtplot3d 三维库

          qwtplot3d是基于QtOpenGL开发的,也是qwt库的三维库,我使用的是qwtplot3d-0.2.7.zip版本.   步骤跟编译qwt库一样(不明白可以看回前面写的一篇文章“Q ...

  9. 事件监听:诀别Android繁琐的事件注册机制——view.setOnXXXXListener

    本版本为1.0,支持较少,使用不够方便.相关封装逻辑结构已升级至2.0,详情可参见:更完善的安卓事件监听实现 先简单扯两句这几天学习下来对java事件监听机制的一点感触.客观地讲,java的事件监听机 ...

  10. Geodatabase - 打开要素类

    string dbPath = @"G:\doc\gis\1.400\data\pdb.mdb"; ESRI.ArcGIS.Geodatabase.IWorkspaceFactor ...