45-超越 goto 的跳转 longjmp



不明所以的同学可能觉得本篇和信号这一专题关系不大,实际上,本篇是为 sigsetjmp 和 siglongjmp 函数作铺垫。但是在这讲这两函数前,先学习更简单的 setjmp 函数和 longjmp 函数。

1. 回忆 goto

回忆 C 语言中的 goto 语句,它所起到的作用就直接从一条语句跳转到另一条语句。这种程序往往破坏了程序的结构,所以专家们都不提倡使用 goto 语句,不过这个我们并不关心,讲 goto 是为了引出 longjmp 函数。

大家都知道 goto 语句只能在函数内跳转,并不能跨越函数进行跳转,像下面这样。

void func() {
hello:
  printf("hello world\n");
  goto hello;
}
  • 1
  • 2
  • 3
  • 4
  • 5

跨越函数?不能,下面这种用法不可行。

void func1() {
hello:
  printf("hello world\n");
  func2();
}

void func2() {
  goto hello;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

不过,脑洞大开的程序员们设计了一个称之为 longjmp 的函数,它可以帮我们搞定这种 nb 的跳转。

2. longjmp

goto 语句有与之配套的一个标号,longjmp 也不例外,只不过 longjmp 配套的标号仍然是一个函数——setjmp.

如果修改前面的程序,大概是这样的:

jmp_buf hello;

void func1() {
  setjmp(hello);
  printf("hello world\n");
  func2();
}

void func2() {
  longjmp(hello);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

不要试图编译上面的程序,这已经被简化了。

3. 牛刀小试

先把上面的代码稍微修改修改就可以编译运行了:

  • 代码
// longjmp.c
#include <unistd.h>
#include <setjmp.h>
#include <stdio.h>

jmp_buf hello; // 设置标号

void func2() {
  longjmp(hello, 1); 
}

void func1() {
  setjmp(hello);
  printf("hello world\n");
  sleep(2); // 防止刷屏了
  func2(); // 准备跳转
}


int main() {
  func1();
  return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 编译和运行
$ gcc longjmp.c -o longjmp
$ ./longjmp
  • 1
  • 2

接下来,会在屏幕每 2 秒打印一个 hello world.

hello world
hello world
hello world
hello world
...
  • 1
  • 2
  • 3
  • 4
  • 5

4. 为什么会这样?

我知道你心中有一万头草泥马奔腾而过,不过,搞懂原理后,写出这样的函数对你来说简直就是渣。原理有点复杂,请参考下一篇博文(勿抛砖)。

5. 函数原型

5.1 setjmp

int setjmp(jmp_buf env)
  • 1

当第一次程序显式调用 setjmp 时,它的返回值是 0. 此后通过 longjmp 跳转到 setjmp 这个位置时,setjmp 的返回值是 longjmp 函数的第二个参数的值。

setjmp 的参数 env 必须是一个全局变量,它用来保存当前程序运行环境(一系列寄存器及栈帧里的关键值)。此后 longjmp 需要依据此 env 来跳转到 setjmp 的位置。

实际上 jmp_buf 是一个固定大小的数组(比如大小为 16 ?)。

5.2 longjmp

void longjmp(jmp_buf env, int val);
  • 1

longjmp 第一个参数就是通过 setjmp 函数初始化后的值,第二个参数将通过 setjmp 返回值返回。

5.3 最后一个例子

下面这段程序,从终端读数据。如果你输入 100,longjmp 传递参数 1 并跳转到 setjmp 的位置,同时 setjmp 会返回 1. 如果你输入 200,longjmp 传递参数 2 并跳转到 setjmp 的位置,同时 setjmp 将返回 2.

  • 代码
// jmp.c
#include <setjmp.h>
#include <stdio.h>

jmp_buf jmpbuf;


void doSomething() {
  int n = 0;
  scanf("%d", &n);
  if (n == 100) {
    longjmp(jmpbuf, 1); 
  }

  if (n == 200) {
    longjmp(jmpbuf, 2); 
  }

}



int main() {
  int res = 0;
  if ((res = setjmp(jmpbuf)) != 0) {
    printf("hello! res = %d\n", res);
  }

  while(1) {
    doSomething();
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 编译和运行
$ gcc jmp.c -o jmp
$ ./jmp
  • 1
  • 2

输入 100 和 200 后的结果:

100 // 输入 100
hello! res = 1
200 // 输入 200
hello! res = 2
  • 1
  • 2
  • 3
  • 4

6. 总结

  • 学会 setjmp 和 longjmp 的用法
  • 思考这是如何做到的?(下一篇讲解原理)

练习:请尝试自己实现一个 setjmp 和 longjmp 函数。(提示:1. 需要使用汇编;2. 由于 gcc 不支持编写 naked 函数,请使用 Visual Studio 编程器)。

猜你喜欢

转载自blog.csdn.net/weixin_38054045/article/details/80898246