使用库函数setjmp和longjmp可执行非局部跳转(local goto)。

术语"非局部(nonlocal)"是指跳转目标为当前执行函数之外的某个位置。

C语言里面有个"臭名昭著"的goto,每次介绍的时候都不忘了带一句,不要使用goto。

C语言的goto存在一个限制,即不能从当前函数跳转到另一函数。然而,偶尔还是需要这一功能的。考虑错误处理中经常出现的如下场景:在一个深度嵌套的函数调用中发生了错误,需要放弃当前任务,从多层函数调用中返回,并在较高层级的函数中继续执行(也许甚至是在main中)。

要做到这一点,可以让每个函数都返回一个状态值,由函数的调用者检查并做相应的处理。这一方法完全有效,而且,在许多情况下,是处理这类场景的理想方法。

然而,有时候如果能从嵌套函数调用中跳出,返回该函数的调用者之一(当前调用者或者调用者的调用者,等等),编码会更为简单。

setjmp和longjmp就提供了这一功能。

setjmp调用为后续由longjmp调用执行的跳转确立了跳转目标。该目标正是程序发起setjmp调用的位置。

从编程角度来看,调用longjmp函数之后,看起来就和从第二次调用setjmp返回时完全一样。

通过查看setjmp返回的整数值,可以区分setjmp调用是初始返回还是第二次返回。

初始调用的返回值是0,后续伪返回的返回值为longjmp调用中val参数所指定的任意值。通过对val参数使用不同值,能够区分出程序中跳转至同一目标的不同起跳位置。(妈的,还有起跳位置。。。)

如果指定longjmp函数的val参数值为0,而longjmp函数对此又不做检查,就会导致模拟setjmp时返回值为0,如同初次调用setjmp函数返回时一样。处于这一原因,如果指定val参数值为0,则longjmp调用实际会将其替换为1。

这两个函数的入参evn为成功实现跳转提供了黏合剂。

setjmp函数把当前进程环境中的各种信息保存到env参数中。

调用longjmp时必须指定相同的env变量,以此来执行伪返回。

由于setjmp和longjmp的调用分别位于不同函数(否则使用简单的goto即可),所以应该将env参数定义为全局变量,或者将env作为函数入参来传递,后一种做法较为少见。

调用setjmp时,env除了存储当前进程的其他信息外,还保存了程序计数寄存器(指向当前正在执行的机器语言指令)和栈指针寄存器(标记栈顶)的副本。这些信息能够使后续的longjmp调用完成两个关键步骤的操作。

将发起longjmp调用的函数与之前调用setjmp的函数之间的函数栈帧从栈上剥离。有时又将此过程称为"解开栈"(unwinding the stack),这是通过将栈指针寄存器重置为env参数内的保存值来实现的。

重置程序计数寄存器,使程序得以从初始的setjmp调用位置继续执行。同样,此功能是通过env参数中的保存值(程序计数寄存器)来实现的。

对setjmp使用的限制

SUSv3和C99规定,setjmp的调用只能在如下语境中使用。

构成选择或迭代语句中(if、switch、while等)的整个控制表达式。

作为一元操作符!(not)的操作对象,其最终表达式构成了选择或迭代语句的整个控制表达式。

作为比较操作(==、!=、<等)的一部分,另一操作对象必须是一个整数常量表达式,且其最终表达式构成选择或迭代语句的整个控制表达式。

作为独立的函数调用,且没有嵌入到更大的表达式之中。

注意:C语言赋值语句不在上述列表之列,一下语句是不符合标准的:

s = setjmp(env);

之所以规定这些限制,是因为作为常规函数的setjmp实现无法保证拥有足够的信息来保证所有寄存器值和封闭表达式中用到的临时栈位置,以便于在longjmp调用此类信息能够得以正确恢复。因此,仅允许在足够简单且无需临时存储的表达式调用setjmp。

滥用longjmp

如果env缓冲区定义为全局变量,对所有函数可见(这也是通常的用法),那么就可以执行如下操作序列。

1. 调用函数x,用setjmp调用在全局变量env中建立一个跳转目标。
2. 从函数x中返回
3. 调用函数y,使用env变量调用longjmp函数

这是一个严重的错误,因为longjmp调用不能跳转到一个已经返回的函数中。思考一下,在这种情况下,longjmp函数会对栈打什么主意——尝试将栈解开,恢复到一个不存在的栈帧位置,这无疑将引起混乱。如果幸运的话,程序将一死了之。然而,取决于栈的状态,也可能会引起调用与返回间的死循环,而程序好像真地从一个当前并未执行的函数中返回。(在多线程程序中有与之类似的滥用,在线程某甲中调用setjmp函数,却在线程某乙中调用longjmp。)

优化编译器的问题

优化编译器会重组程序的指令执行顺序,并在CPU寄存器中,而非RAM中存储某些变量。这种优化一般依赖于反映了程序词法结构的运行时(run-time)控制流。由于setjmp和longjmp的跳转需要在运行时才能得以确立和执行,并未在词法结构中有所反映,故而编译器在进行优化时也无法将其考虑在内。此外,某些应用程序二进制接口(ABI)实现的语义要求longjmp函数恢复先前setjmp调用所保存的CPU寄存器副本。这意味着longjmp操作会致使经过优化的变量被赋以错误值。一下就是一例。

尽可能避免使用setjmp和longjmp

如果说goto语句会使程序难以阅读,那么非局部跳转会让事情的糟糕程度增加一个数量级,因为它能在程序中任意两个函数间传递控制。因此,应当慎用setjmp函数和longjmp函数。在设计和编码时花点心思来避免使用这两个函数,这通常是值得的。程序更具有可读性,可能会更具有可执行性。话虽如此,但在编写信号处理器时,这些函数偶尔还会派上用场——讨论信号时将重新讨论这些函数的变体。

Unix系统编程()执行非局部跳转:setjmp和longjmp的更多相关文章

  1. 【转】浅析C语言的非局部跳转:setjmp和longjmp

    转自 http://www.cnblogs.com/lienhua34/archive/2012/04/22/2464859.html C语言中有一个goto语句,其可以结合标号实现函数内部的任意跳转 ...

  2. 《Linux/Unix系统编程手册》读书笔记3

    <Linux/Unix系统编程手册>读书笔记 目录 第6章 这章讲进程.虚拟内存和环境变量等. 进程是一个可执行程序的实例.一个程序可以创建很多进程. 进程是由内核定义的抽象实体,内核为此 ...

  3. 《Linux/UNIX系统编程手册》读书笔记

    2018-1-30 一.UNIX.C语言以及Linux的历史回顾 1. UNIX简史.C语言的诞生 1969年,贝尔实验室的Ken Thompson首次实现了UNIX系统. 1973年,C语言步入成熟 ...

  4. 《Linux/UNIX系统编程手册》第63章 IO多路复用、信号驱动IO以及epoll

    关键词:fasync_helper.kill_async.sigsuspend.sigaction.fcntl.F_SETOWN_EX.F_SETSIG.select().poll().poll_wa ...

  5. 《Linux/Unix系统编程手册》读书笔记8 (文件I/O缓冲)

    <Linux/Unix系统编程手册>读书笔记 目录 第13章 这章主要将了关于文件I/O的缓冲. 系统I/O调用(即内核)和C语言标准库I/O函数(即stdio函数)在对磁盘进行操作的时候 ...

  6. 《Linux/Unix系统编程手册》读书笔记7 (/proc文件的简介和运用)

    <Linux/Unix系统编程手册>读书笔记 目录 第11章 这章主要讲了关于Linux和UNIX的系统资源的限制. 关于限制都存在一个最小值,这些最小值为<limits.h> ...

  7. 《Linux/Unix系统编程手册》读书笔记6

    <Linux/Unix系统编程手册>读书笔记 目录 第9章 这章主要讲了一堆关于进程的ID.实际用户(组)ID.有效用户(组)ID.保存设置用户(组)ID.文件系统用户(组)ID.和辅助组 ...

  8. 《Linux/Unix系统编程手册》读书笔记1

    <Linux/Unix系统编程手册>读书笔记 目录 最近这一个月在看<Linux/Unix系统编程手册>,在学习关于Linux的系统编程.之前学习Linux的时候就打算写关于L ...

  9. 《Linux/Unix系统编程手册》读书笔记2

    <Linux/Unix系统编程手册>读书笔记 目录 第5章: 主要介绍了文件I/O更深入的一些内容. 原子操作,将一个系统调用所要完成的所有动作作为一个不可中断的操作,一次性执行:这样可以 ...

随机推荐

  1. java学习笔记9--内部类总结

    java学习笔记系列: java学习笔记8--接口总结 java学习笔记7--抽象类与抽象方法 java学习笔记6--类的继承.Object类 java学习笔记5--类的方法 java学习笔记4--对 ...

  2. HDU4626+博弈

    博弈... /* 博弈 对于当前人来说,如果完成自己的操作后,若mat[n][m]==0,则自己是胜者. 因为 如果mat其他位置不存在1了,肯定自己胜:如果存在1,则让下一位去反转那个1. */ # ...

  3. ThinkPad如何修改fn键默认操作

    ThinkPad如何修改fn键默认操作 ThinkPad笔记本如何修改fn键默认操作 Fn键F1-F12

  4. view xml 中的 button 调用web客户端事件

    最近写一个模块 需要 在客户端干点事. 按常规的方法, 应该是写个 客户端模块. 在 客户端 init, start, render 去渲染个按钮出来干事.暂时还不太理解WEB模块如何很好地同服务器端 ...

  5. Android 断点续传下载

    断点续传在面试中出现的概率还是比较大的,因为一般的应用都需要. 这个代码是从网上找来的,自己改了点东西,能跑通,但是这个代码并不是最优代码和设计.但是基本思路体现出来了,可以以这个为基础来进行修改.先 ...

  6. 移动通信安全——GSM安全体系

    一.GSM网络概述 1.安全机制 认证用户,防止未授权接入 对空中接口传输加密,防止无线信道上用户信息被窃听 SIM卡独立于终端,管理用户信息 在空中接口上以临时身份标识用户,防止用户被跟踪 但是GS ...

  7. PHP-WebService中Endpoint、Disco、WSDL都是做什么的?

    Endpoint: http://webservice.webxml.com.cn/WebServices/WeatherWS.asmx   web服务的URI地址,你访问之后,就会出现web服务的相 ...

  8. android的NDK和java进行本地socket通信

    关于Android应用与Framework的socket通信,相信关心这个问题的朋友们已经看过<android使用socket使底层和framework通信>这篇文章,美中不足的是作者只贴 ...

  9. 后台Post/Get 请求接口 方式

    Post请求 public string HttpPost(string Url, string postDataStr) { try { HttpWebRequest request = (Http ...

  10. javascript 打印错误信息 catch err

    使用 console.log(err); 是无法打印出来的.默认只能打印出错误信息.如图 http.interceptors.response.use(response => { return ...