转载: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. JVM基础和调优(二)

    主要讲述java虚拟机的内存体系结构 了解了JVM 的一些基础之后,我们来看看java虚拟机内存的体系结构,这个是理解JVM垃圾收集算法的前提,理解了内存结构我们才能够针对不同的部分根据我们的程序进行 ...

  2. maven compile时出现“非法字符: \65279”的解决

    我碰到的这个问题是因为Java文件编码为UTF-8 BOM格式导致:解决这个可以使用UltraEdit. 用UltraEdit打开出问题的Java文件,将文件另存为,在保存对话框的编码中选择UTF-8 ...

  3. [置顶] 【cocos2d-x入门实战】微信飞机大战之十三:游戏场景过渡

    原创作品,转载请标明:http://blog.csdn.net/jackystudio/article/details/12082043 游戏是实现了,但是如果有个欢迎界面和一个结束界面就更好了. 欢 ...

  4. cocos2d-x ios 设置横屏/竖屏(全)

    Cocos2d-x项目\iOS\RootViewController.mm文件中. 以下方法任选其一即可…      本人机子函数二ok! 函数一: (BOOL)shouldAutorotateToI ...

  5. phpcms:二、头部尾部包含

    1.index.html是默认的首页{template "content","header"}引入头部的模块{template "content&qu ...

  6. OpenXML - 如何导出List<DataModel>到Excel -- Part 1

    最近这几天研究OpenXML: 这是Open XML的一些介绍: Open XML 介绍:http://baike.baidu.com/view/1201978.htm 下载:http://www.m ...

  7. tool - 支持TestLink 1.93,将excel格式用例转化成可以导入的xml格式

     tool - 支持TestLink 1.93,将excel格式用例转化成可以导入的xml格式  https://github.com/zhangzheyuk/CaseConvert

  8. Android SDK及ADT更新访问问题的解决办法

    一.访问问题Eclipse使用SDK Manager更新时总是出现问题 Failed to fetch URL https://dl-ssl.google.com/android/repository ...

  9. 使用css3属性,大部分浏览器要识别前缀

    例如以下代码的解析 -ms-transform:rotate(7deg); -moz-transform:rotate(7deg); -webkit-transform:rotate(7deg); - ...

  10. (转)SQL Server2005 异常处理机制(Begin try Begin Catch)

    begin try --SQL  end trybegin catch --sql (处理出错动作) end catch我们将可能会出错的sql 写在begin try...end try 之间,若出 ...