0%

OSX 下 makecontext 调用死循环的原因

在 OSX 下使用 makecontext 官网文档的例子的时候,发现会进入一个死循环:

下面是完整代码(附注释):

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#define _XOPEN_SOURCE
#include <ucontext.h>
#include <stdio.h>
#include <stdlib.h>

static ucontext_t uctx_main, uctx_func1, uctx_func2;

#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)

static void
func1(void)
{
printf("func1: started\n");
printf("func1: swapcontext(&uctx_func1, &uctx_func2)\n");
if (swapcontext(&uctx_func1, &uctx_func2) == -1)
handle_error("swapcontext");
printf("func1: returning\n");
}

static void
func2(void)
{
printf("func2: started\n");
printf("func2: swapcontext(&uctx_func2, &uctx_func1)\n");
if (swapcontext(&uctx_func2, &uctx_func1) == -1)
handle_error("swapcontext");
printf("func2: returning\n");
}

int
main(int argc, char *argv[])
{
// 16384 16K
char func1_stack[16384];
char func2_stack[16384];

if (getcontext(&uctx_func1) == -1)
handle_error("getcontext");
uctx_func1.uc_stack.ss_sp = func1_stack;
uctx_func1.uc_stack.ss_size = sizeof(func1_stack);
uctx_func1.uc_link = &uctx_main;
makecontext(&uctx_func1, func1, 0);

// 1. 初始化 uctx_func2 上下文环境,这个上下文恢复的时候,rip 指向 getcontext 汇编之后的下一条指令。
if (getcontext(&uctx_func2) == -1)
handle_error("getcontext");
// 2. uctx_func2 的 rip 如果没有被成功修改为 func2,将 uctx_func2 上下文恢复的时候,会从下面这一行开始执行。因为 getcontext 的调用是成功的。
uctx_func2.uc_stack.ss_sp = func2_stack;
uctx_func2.uc_stack.ss_size = sizeof(func2_stack);
/* Successor context is f1(), unless argc > 1 */
uctx_func2.uc_link = (argc > 1) ? NULL : &uctx_func1;

// 3.调用失败,但是不会有任何报错信息,也没有返回值。
// 如果调用成功,uctx_func2 的 rip 指向的是 func2,从而在恢复 uctx_func2 的时候,去执行 func2 函数。
makecontext(&uctx_func2, func2, 0);

printf("main: swapcontext(&uctx_main, &uctx_func2)\n");
// 4. 保存当前上下文到 uctx_main 中,还原到上下文 uctx_func2。
// 5. 由于 uctx_func2 指向 getcontext 的下一条指令,所以会从 `uctx_func2.uc_stack.ss_sp = func2_stack;` 这一行继续执行,进入一个死循环。
if (swapcontext(&uctx_main, &uctx_func2) == -1)
handle_error("swapcontext");

printf("main: exiting\n");
exit(EXIT_SUCCESS);
}

当在 OSX 下执行的时候,会无限输出 main: swapcontext(&uctx_main, &uctx_func2)

详细解答在: https://stackoverflow.com/questions/40299849/context-switching-is-makecontext-and-swapcontext-working-here-osx

OSX 的 makecontext 里面实现存在的问题:

源码:https://github.com/Apple-FOSS-Mirror/Libc/blob/2ca2ae74647714acfc18674c3114b1a5d3325d7d/x86_64/gen/makecontext.c

MINSIGSTKSZ 过大(32K),我们设置的栈大小只有 16K。

1
2
3
4
5
6
7
8
9
10
else if ((ucp->uc_stack.ss_sp == NULL) ||
(ucp->uc_stack.ss_size < MINSIGSTKSZ)) {
/*
* This should really return -1 with errno set to ENOMEM
* or something, but the spec says that makecontext is
* a void function. At least make sure that the context
* isn't valid so it can't be used without an error.
*/
ucp->uc_mcsize = 0;
}

总结来说就是:

  1. OSX 下的 MINSIGSTKSZ 为 32K,然后 OSX 里面的 makecontext 实现判断 ucontext_t 的栈大小小于 MINSIGSTKSZ 的时候,会直接返回,但是这个时候我们的调用是失败的, 因为成功的 makecontext 调用应该会将 ucontext_t 里面的 rip 修改为 func2 的地址。(rip 是保存下一条需要执行的指令的地址的寄存器)。

  2. getcontext 的作用是初始化一个 ucontext_t,初始化之后,这个 ucontext_t 指向 getcontext 调用的下一条指令(汇编层面的指令)。

结果就导致当我们调用 swapcontext 的时候,使用 uctx_func2 来恢复上下文环境的时候,实际上执行的是 getcontext 的下一条指令。然后后面调用 makecontext 仍然失败,从而无限循环。

解决方法

将栈的大小设置为 32K 或者更大,也就是把源码里面的 16384 修改为 32768。