当我编译下面的代码时,它会打印
我正在运行:(
永远(直到我向程序发送KeyboardInterrupt信号(,
但当我取消注释// printf("done:%dn", done);
,重新编译并运行它时,它只打印两次,打印done: 1
,然后返回
我是ucontext.h的新手,我对这些代码的工作方式和为什么一个printf会改变代码的整个行为,如果你用done++;
替换printf
,它也会这样做,但如果用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:%dn", done);
return 0;
}
这是一个编译器优化"问题";。当";printf(("则编译器推断出";完成";将不会在";如果(!完成(";,因此它不会将其设置为1,因为它不值得。但是当";printf(("存在;完成";在";如果(!完成(";,所以编译器设置它。
带有";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:%dn", 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:%dn", 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)
为了禁用"0"上的优化;完成";,添加";挥发性";其定义中的关键字:
volatile register int done = 0;
这使得程序在这两种情况下都能工作。
(与Rachid K的答案有一些重叠,因为它是在我写这篇文章时发布的。(
我猜您是在将done
声明为register
,希望它能真正放入寄存器中,以便上下文开关保存和恢复它的值。但编译器从来没有义务遵守这一点;大多数现代编译器完全忽略register
声明,并自行决定寄存器的使用。特别是,没有优化的gcc
几乎总是将局部变量放在堆栈的内存中。
因此,在您的测试用例中,done
的值是,而不是由上下文开关恢复的。因此,当getcontext
第二次返回时,done
的值与调用swapcontext
时的值相同。
当存在printf
时,正如Rachid也指出的,done = 1
实际上存储在swapcontext
之前,因此在第二次返回getcontext
时,done
的值为1,跳过if
块,程序打印done:1
并退出。
然而,当printf
不存在时,编译器会注意到done
的值在赋值后从未使用过(因为它假设swapcontext
是一个正常函数,并且不知道它实际上会返回到其他地方(,所以它会优化死存储(是的,即使优化已关闭(。因此,当getcontext
第二次返回时,我们得到了done == 0
,并且得到了一个无限循环。如果你认为done
会被放在寄存器中,这可能是你所期望的,但如果是这样,你会得到";右";出于错误原因的行为。
如果启用优化,您将再次看到其他内容:编译器注意到done
不会受到对getcontext
的调用的影响(再次假设它是一个正常的函数调用(,因此它在if
处保证为0。所以这个测试根本不需要做,因为它永远都是真的。然后无条件地执行swapcontext
,而对于done
,它被优化得完全不存在,因为它不再对代码有任何影响。你将再次看到一个无限循环。
由于这个问题,您真的无法对在getcontext
和swapcontext
之间修改的局部变量做出任何安全的假设。当getcontext
第二次返回时,您可能看到也可能看不到更改。如果编译器选择围绕函数调用对某些代码进行重新排序,就会出现进一步的问题(它不知道为什么不这样做,因为它再次认为这些是普通的函数调用,看不到局部变量(。
获得任何确定性的唯一方法是声明一个变量volatile
。然后,您可以确保看到的中间更改,并且编译器不会认为getcontext
无法更改它。在getcontext
的第二次返回中看到的值将与调用swapcontext
时看到的值相同。如果你写CCD_ 40,你应该只看到两个";我在跑步;消息,而不管其他代码或优化设置如何。