问题描述
当我编译下面的代码时,它会打印
我正在跑步:)
永远(直到我向程序发送KeyboardInterrupt信号),
但是当我取消注释 // printf("done:%d\n",done);
时,重新编译并运行它,它只会打印两次,打印 done: 1
然后返回。
我是 ucontext.h 的新手,我对这段代码的工作方式感到非常困惑
为什么单个 printf 会改变代码的整个行为,如果你用 printf
替换 done++;
它会做同样的但如果你用 done = 2;
替换它它不会影响任何东西并且像我们首先评论了 printf
。
谁能解释一下:
为什么这段代码会这样,背后的逻辑是什么?
对不起,我的英语不好,
非常感谢。
#include <ucontext.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
int main()
{
register int done = 0;
ucontext_t one;
ucontext_t two;
getcontext(&one);
printf("I am running :)\n");
sleep(1);
if (!done)
{
done = 1;
swapcontext(&two,&one);
}
// printf("done:%d\n",done);
return 0;
}
解决方法
这是一个编译器优化“问题”。当注释“printf()”时,编译器推断在“if(!done)”之后不会使用“done”,因此它不会将其设置为1,因为它不值得。但是当存在“printf()”时,“if (!done)”之后会使用“done”,因此编译器会设置它。
带有“printf()”的汇编代码:
$ gcc ctx.c -o ctx -g
$ objdump -S ctx
[...]
int main(void)
{
11e9: f3 0f 1e fa endbr64
11ed: 55 push %rbp
11ee: 48 89 e5 mov %rsp,%rbp
11f1: 48 81 ec b0 07 00 00 sub $0x7b0,%rsp
11f8: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
11ff: 00 00
1201: 48 89 45 f8 mov %rax,-0x8(%rbp)
1205: 31 c0 xor %eax,%eax
register int done = 0;
1207: c7 85 5c f8 ff ff 00 movl $0x0,-0x7a4(%rbp) <------- done set to 0
120e: 00 00 00
ucontext_t one;
ucontext_t two;
getcontext(&one);
1211: 48 8d 85 60 f8 ff ff lea -0x7a0(%rbp),%rax
1218: 48 89 c7 mov %rax,%rdi
121b: e8 c0 fe ff ff callq 10e0 <getcontext@plt>
1220: f3 0f 1e fa endbr64
printf("I am running :)\n");
1224: 48 8d 3d d9 0d 00 00 lea 0xdd9(%rip),%rdi # 2004 <_IO_stdin_used+0x4>
122b: e8 70 fe ff ff callq 10a0 <puts@plt>
sleep(1);
1230: bf 01 00 00 00 mov $0x1,%edi
1235: e8 b6 fe ff ff callq 10f0 <sleep@plt>
if (!done)
123a: 83 bd 5c f8 ff ff 00 cmpl $0x0,-0x7a4(%rbp)
1241: 75 27 jne 126a <main+0x81>
{
done = 1;
1243: c7 85 5c f8 ff ff 01 movl $0x1,-0x7a4(%rbp) <----- done set to 1
124a: 00 00 00
swapcontext(&two,&one);
124d: 48 8d 95 60 f8 ff ff lea -0x7a0(%rbp),%rdx
1254: 48 8d 85 30 fc ff ff lea -0x3d0(%rbp),%rax
125b: 48 89 d6 mov %rdx,%rsi
125e: 48 89 c7 mov %rax,%rdi
1261: e8 6a fe ff ff callq 10d0 <swapcontext@plt>
1266: f3 0f 1e fa endbr64
}
printf("done:%d\n",done);
126a: 8b b5 5c f8 ff ff mov -0x7a4(%rbp),%esi
1270: 48 8d 3d 9d 0d 00 00 lea 0xd9d(%rip),%rdi # 2014 <_IO_stdin_used+0x14>
1277: b8 00 00 00 00 mov $0x0,%eax
127c: e8 3f fe ff ff callq 10c0 <printf@plt>
return 0;
没有“printf()”的汇编代码:
$ gcc ctx.c -o ctx -g
$ objdump -S ctx
[...]
int main(void)
{
11c9: f3 0f 1e fa endbr64
11cd: 55 push %rbp
11ce: 48 89 e5 mov %rsp,%rbp
11d1: 48 81 ec b0 07 00 00 sub $0x7b0,%rsp
11d8: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
11df: 00 00
11e1: 48 89 45 f8 mov %rax,-0x8(%rbp)
11e5: 31 c0 xor %eax,%eax
register int done = 0;
11e7: c7 85 5c f8 ff ff 00 movl $0x0,-0x7a4(%rbp) <------ done set to 0
11ee: 00 00 00
ucontext_t one;
ucontext_t two;
getcontext(&one);
11f1: 48 8d 85 60 f8 ff ff lea -0x7a0(%rbp),%rax
11f8: 48 89 c7 mov %rax,%rdi
11fb: e8 c0 fe ff ff callq 10c0 <getcontext@plt>
1200: f3 0f 1e fa endbr64
printf("I am running :)\n");
1204: 48 8d 3d f9 0d 00 00 lea 0xdf9(%rip),%rdi # 2004 <_IO_stdin_used+0x4>
120b: e8 80 fe ff ff callq 1090 <puts@plt>
sleep(1);
1210: bf 01 00 00 00 mov $0x1,%edi
1215: e8 b6 fe ff ff callq 10d0 <sleep@plt>
if (!done)
121a: 83 bd 5c f8 ff ff 00 cmpl $0x0,-0x7a4(%rbp)
1221: 75 1d jne 1240 <main+0x77>
{
done = 1; <------------- done is no set here (it is optimized by the compiler)
swapcontext(&two,&one);
1223: 48 8d 95 60 f8 ff ff lea -0x7a0(%rbp),%rdx
122a: 48 8d 85 30 fc ff ff lea -0x3d0(%rbp),%rax
1231: 48 89 d6 mov %rdx,%rsi
1234: 48 89 c7 mov %rax,%rdi
1237: e8 74 fe ff ff callq 10b0 <swapcontext@plt>
123c: f3 0f 1e fa endbr64
}
//printf("done:%d\n",done);
return 0;
1240: b8 00 00 00 00 mov $0x0,%eax
}
1245: 48 8b 4d f8 mov -0x8(%rbp),%rcx
1249: 64 48 33 0c 25 28 00 xor %fs:0x28,%rcx
1250: 00 00
1252: 74 05 je 1259 <main+0x90>
1254: e8 47 fe ff ff callq 10a0 <__stack_chk_fail@plt>
1259: c9 leaveq
125a: c3 retq
125b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
要禁用对“done”的优化,请在其定义中添加“volatile”关键字:
volatile register int done = 0;
这使得程序在两种情况下都能工作。
,(与 Rachid K 的回答有一些重叠,因为它是在我写这篇文章时发布的。)
我猜您将 done
声明为 register
是希望它实际上被放入寄存器中,以便它的值可以通过上下文切换来保存和恢复。但是编译器从来没有义务遵守这一点;大多数现代编译器完全忽略 register
声明并自行决定寄存器的使用。特别是,没有优化的 gcc
几乎总是将局部变量放在内存中的堆栈中。
因此,在您的测试用例中,done
的值不会被上下文切换恢复。因此,当 getcontext
第二次返回时,done
的值与调用 swapcontext
时的值相同。
当存在 printf
时,正如 Rachid 也指出的,done = 1
实际上存储在 swapcontext
之前,因此在 getcontext
的第二次返回时,{{ 1}} 的值为 1,跳过 done
块,程序打印 if
并退出。
然而,当 done:1
不存在时,编译器会注意到 printf
的值在赋值后从未使用过(因为它假定 done
是一个正常的函数并且不会知道它实际上会返回其他地方),所以它优化了 dead store (是的,即使优化关闭)。因此,当 swapcontext
第二次返回时,我们有 done == 0
,你会得到一个无限循环。如果您认为 getcontext
会被放置在寄存器中,这可能就是您所期望的,但如果是这样,您就会出于错误的原因得到“正确”的行为。
如果您启用优化,您将再次看到其他内容:编译器注意到 done
不会受到对 done
的调用的影响(再次假设它是一个正常的函数调用),因此在 getcontext
处保证为 0。所以根本不需要进行测试,因为它永远是真的。然后无条件执行 if
,至于 swapcontext
,它完全优化不存在,因为它不再对代码产生任何影响。您将再次看到一个无限循环。
由于这个问题,您真的无法对在 done
和 getcontext
之间修改的局部变量做出任何安全的假设。当 swapcontext
第二次返回时,您可能会也可能不会看到更改。如果编译器选择围绕函数调用对您的某些代码重新排序(它知道没有理由不这样做,因为它再次认为这些是无法看到您的局部变量的普通函数调用),则会出现更多问题。
获得任何确定性的唯一方法是声明一个变量 getcontext
。然后您可以确定中间更改会被看到,并且编译器不会假设 volatile
不能更改它。在第二次返回 getcontext
时看到的值将与调用 getcontext
时相同。如果您编写 swapcontext
,您应该只会看到两条“我正在运行”消息,而不管其他代码或优化设置如何。