KEEP K.I.S.S.

tk's blog

setjmp 和 longjmp

tk posted @ May 31, 2012 01:40:44 PM in C with tags 翻译 非局部跳转 , 1954 阅读

本文主要讲解 C 库中的函数 setjmp 和 longjmp,也就是所谓的 非局部跳转。

本文主要翻译和出自 Jim Plank 的讲座 CS360 Lecture notes -- Setjmp

翻译 by tisyang 自我感觉不直观的翻译都在括号中附加了原文


setjmp()/longjmp()

Setjmp() 和 longjmp() 是在 C/Unix 下用于执行复杂控制流的子程序。

理解 setjmp() 和 longjmp() 的关键之一就在于理解 机器布局(machine layout), 这个在几周前的 汇编和内存分配(assembler and malloc) 讲座中描述过。 一个程序的状态完全取决于它内存中的内容(contents of its memory) (比如 代码(code), 全局变量(globals), 堆(heap), 和栈(stack)), 以及寄存器中的内容。 寄存器中的内容包括 栈指针寄存器(stack pointer,缩写 sp)、帧指针寄存器(frame pointer,指向栈中一个函数的local 变量的首地址,缩写 fp)和 程序计数器寄存器(program counter,缩写 pc)。 setjmp() 所做的事情就是保存当前这些寄存器的内容以便在以后的某个时刻 longjmp() 可以恢复它们。 因此,longjmp() 可以“返回” 到 setjmp()被调用时刻的程序的状态。

具体来看:

#include < setjmp.h >
int setjmp(jmp_buf env);

这是将当前寄存器的状态保存到 env 中。 如果打开 /usr/include/setjmp.h, 你将看到 jmp_buf 的定义如下:

#define _JBLEN  9
typedef struct { int _jb[_JBLEN + 1]; } jmp_buf[1];

这说明 jmp_buf 是一个包含数量为 _JBLEN+1 的整数数组。

因此, 当调用 setjmp() 时,传递的参数是这样一个整数数组的地址,接着函数把所有寄存器的值保存到这个数组之中。在这种情况下调用, setjmp() 会返回 0。

longjmp(jmp_buf env, int val);

Longjmp() 会将寄存器重置为保存在 env 中的值 ,这包括 spfp 以及 pc这意味着 longjmp() 函数不会返回。 相反, 当调用 longjmp() 时,程序流会返回到好像刚刚调用完 setjmp() (保存了 env )一样。 这是因为 pc(程序计数器)和其他寄存器的内容都被一起恢复了。此时,setjmp() 会返回传递给 longjmp() 的参数 val 的值,注意 val 的值不允许为 0 (请参阅 man 帮助系统)。因此,当 longjmp() 被调用时,setjmp() 会返回一个非0值, 程序流也从 setjmp() 中返回。

用一个示例来说明,看如下的代码 (in sj1.c):

#include < setjmp.h >

main()
{
  jmp_buf env;
  int i;

  i = setjmp(env);
  printf("i = %d\n", i);

  if (i != 0) exit(0);

  longjmp(env, 2);
  printf("Does this line get printed?\n");

}

运行时,程序会输出:

UNIX> sj1
i = 0
i = 2
UNIX>

首先,程序调用 setjmp() , 会返回 0。 然后程序调用 longjmp() 并传递一个值 2,这造成程序流从 setjmp() 返回值 2。再接着,这个值被打印,然后程序退出。

Setjmp() 和 longjmp() 通常被用于在一长串的过程调用中检测是否发生了一个错误,这个错误也许会被更高层次的调用优雅地处理。 举个示例, 参见 sj2.c 的代码

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


int proc_4(jmp_buf env, int i)
{
  if (i == 0) longjmp(env, 1);
  return 14 % i;
}
  
int proc_3(jmp_buf env, int i, int j)
{
  return proc_4(env, i) + proc_4(env, j);
}

int proc_2(jmp_buf env, int i, int j, int k)
{
  return proc_3(env, i, k) + proc_3(env, j, k);
}

int proc_1(jmp_buf env, int i, int j, int k, int l)
{
  return proc_2(env, i, j, k+1);
}


main(int argc, char **argv)
{
  jmp_buf env;
  int sj;
  int i, j, k, l;

  if (argc != 5) {
    fprintf(stderr, "usage: sj2 i j k l\n");
    exit(1);
  }

  sj = setjmp(env);
  if (sj != 0) {
    printf("Error -- bad value of i (%d), j (%d), k (%d), l (%d)\n", 
           i, j, k, l);
    exit(0);
  }

  i = atoi(argv[1]);
  j = atoi(argv[2]);
  k = atoi(argv[3]);
  l = atoi(argv[4]);
  
  printf("proc_1(%d, %d, %d, %d) = %d\n", i, j, k, l, proc_1(env, i, j, k, l));
}
  

 这段代码看起来复杂但事实上并不复杂。这里有一个复杂的一系列过程调用—— proc_1 到 proc_4。如果 proc_4 的参数为 0, 那么它就会通过调用 longjmp() 来标明这个错误。反之,程序会正常运行。显然, 如果调用四个全都是正数的参数来调用 sj2 , 那么一切 OK。但是,如果四个参数都为 0,它就会调用 longjmp() call,标明一个错误:

UNIX> sj2 1 2 3 4
proc_1(1, 2, 3, 4) = 4
UNIX> sj2 0 0 0 0
Error -- bad value of i (0), j (0), k (0), l (0)
UNIX>

现在我们知道,setjmp() 会保存所有的寄存器值,包括 sp 和 fp这就意味着如果从一个调用 setjmp() 的过程中返回,那么之前通过 setjmp() 保存在 env 缓冲区中的内容就不再有效。 Why? 因为 env 缓冲区保存的是调用方过程(调用 setjmp() 的过程)的 sp 和 fp (栈指针和帧指针)。如果调用方过程返回, 那么当你恢复 sp 和 fp 寄存器的时候,栈就会处于一个与之前不同的状态,而程序会产生一个错误。举个示例,参见 sj3.c 的代码:

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


int a(char *s, jmp_buf env)
{
  int i;

  i = setjmp(env);
  printf("Setjmp returned -- i = %d, 0x%x\n", i, s);
  
  printf("s = %s\n", s);
  return i;
}

int b(int i, jmp_buf env)
{
  printf("In B: i=%d.  Calling longjmp(env, i)\n", i);

  longjmp(env, i);
}



main(int argc, char **argv)
{
  jmp_buf env;

  if (a("Jim", env) != 0) exit(0);
  b(3, env);
}

程序执行时,我们会得到如下输出:(在 win 下系统会报告程序出现问题,意外关闭,译者注)

UNIX> sj3
Setjmp() returned -- i = 0
s = Jim
In B: i=3.  Calling longjmp(env, i)
Setjmp() returned -- i = 3
Segmentation fault (core dumped)
UNIX>

啊,究竟发生了什么?当 main() 程序被调用时, 当前栈看起来会如下所示:

              Stack        
        |----------------|
        |                |
        |                |
        |                |
        |                |
        |                |
        |                | <-------- sp
        | env[0]         |
        | env[1]         |
        | env[2]         |               pc = main
        | env[3]         |
        | ....           |
        | env[8]         |
        | other stuff    | <------- fp
        |--------------- |

现在, main() 调用 a() 首先它会逆序地将所有参数压入栈中,接着 jsr 指令(汇编指令,用于跳转到子程序,译者注) 被调用,它会将返回(jsr 指令返回的)的 pc 寄存器的值(程序计数器)压入栈中。fp 和 sp 寄存器的值会被修改以提供一个空栈帧(empty stack frame)给 a() 

                                     Stack        
                               |----------------|
                               |                |
                               |                | <--------- sp, fp
                /------------- | old fp in main |
                |              | old pc in main |
                |   "Jim" <--- | s = "Jim"      |
                |         /--- | pointer to env | 
                |         \--> | env[0]         |
                |              | env[1]         |
                |              | env[2]         |               pc = a
                |              | env[3]         |
                |              | ....           |
                |              | env[8]         |
                \------------> | other stuff    | 
                               |--------------- |

a() 首先要做的就是给它的局部变量 i 分配空间(allocate room):

                                     Stack        
                               |----------------|
                               |                | <--------- sp
                               |      i         | <--------- fp
                /------------- | old fp in main |
                |              | old pc in main |
                |   "Jim" <--- | s = "Jim"      |
                |         /--- | pointer to env | 
                |         \--> | env[0]         |
                |              | env[1]         |
                |              | env[2]         |               pc = a
                |              | env[3]         |
                |              | ....           |
                |              | env[8]         |
                \------------> | other stuff    | 
                               |--------------- |

接着它调用 setjmp()这将保存当前寄存器的状态。换句话来说,它将保存当前 spfp, 和 pc 寄存器的值。然后a() 打印 "i = 0" 和 "s = Jim",并返回到 main()。现在,栈与之前相比是一样的,除了 env 被初始化成 a() 被调用时机器的状态:

                                     Stack        
                               |----------------|
                               |                |
                               |                | 
                               |                |
                               |                |
                               |                |
                               |                | <----------- sp
                               | env[0]         |
                               | env[1]         |
                               | env[2]         |               pc = main
                               | env[3]         |
                               | ....           |
                               | env[8]         |
                               | other stuff    | <------------ fp
                               |--------------- |

然后,main() 调用 b(), 此时,栈看起来如下所示:

                                     Stack        
                               |----------------|
                               |                |
                               |                | <--------- sp, fp
                /------------- | old fp in main |
                |              | old pc in main |
                |              | i = 3          |
                |         /--- | pointer to env | 
                |         \--> | env[0]         |
                |              | env[1]         |
                |              | env[2]         |               pc = b
                |              | env[3]         |
                |              | ....           |
                |              | env[8]         |
                \------------> | other stuff    | 
                               |--------------- |

 接着,longjmp() 被调用。寄存器会恢复到 a() 调用 setjmp() 时的状态,此时,pc 寄存器会将控制流从 a() 中的 setjmp() 返回。然而,此刻栈中的值会和它们在 b() 中是一样的:

                                     Stack        
                               |----------------|
                               |                | <--------- sp
                               | i = 2          | <--------- fp
                /------------- | old fp in main |
                |              | old pc in main |
                |              | s??    = 3     |
                |         /--- | pointer to env | 
                |         \--> | env[0]         |
                |              | env[1]         |
                |              | env[2]         |               pc = a
                |              | env[3]         |
                |              | ....           |
                |              | env[8]         |
                \------------> | other stuff    | 
                               |--------------- |

你应该发现问题所在了。栈处于一个错误的状态。特别地, a() 期许 s 指向一个 (char *), 但是那里其实是一个整数 3。因此,当它试图打印出 时,它会试图在内存地址为 3 的地方寻找一个字符串,于是就引发了核心转储(dumps core)。

这是一个很常见的使用 setjmp() 和 longjmp() 的 bug —— 想要正确的使用它们,你必须注意 不能从调用 setjmp() 的过程中返回(CANNOT RETURN FROM THE PROCEDURE THAT CALLS setjmp())如你所见,这个 bug 很微妙 —— b() 的栈帧(stack frame)与 a() 栈帧(stack frame)太过相似, 以致于这个 bug 一时不会被发现。

CBSE 4th Class Model 说:
Sep 17, 2022 08:19:20 PM

The CBSE 4th Class Model Paper 2023 along with Worksheets for both medium students for all regional languages are provided to subject experts of the Board and numerous top educational institutions to practise with the regular mock tests in order to improve their performance in Paper-1 & Paper-2 or Part-A & Part-B examination tests at Terms-1, Term-2, Term-3, and Term-4 in evaluation wise to every chapter in lesson-wide. CBSE 4th Class Model Paper The CBSE STD-4 Worksheet 2023 includes a sample exam and proposed answer options for the Hindi, English, Urdu, and other mediums.

I recently came acro 说:
Jan 10, 2023 03:08:44 AM

I recently came across your article and have been reading along. I want to express my admiration of your writing skill and ability to make readers read from the beginning to the end. I would like to read newer posts and to share my thoughts with you. SUPERSLOT1234


登录 *


loading captcha image...
(输入验证码)
or Ctrl+Enter