为什么此代码与单个 printf 的行为不同? ucontext.h
Why is this code acting different with a single printf? ucontext.h
当我在下面编译我的代码时,它会打印
I am running :)
永远(直到我向程序发送 KeyboardInterrupt 信号),
但是当我取消注释 // printf("done:%d\n", done);
,重新编译并 运行 它时,它只会打印两次,打印 done: 1
然后 returns.
我是 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()”时,编译器推断“done”不会在“if (!done)”之后使用,因此不会将其设置为 1,因为它不值得。但是当“printf()”存在时,“done”在“if (!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 [=10=]x7b0,%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 [=10=]x0,-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 [=10=]x1,%edi
1235: e8 b6 fe ff ff callq 10f0 <sleep@plt>
if (!done)
123a: 83 bd 5c f8 ff ff 00 cmpl [=10=]x0,-0x7a4(%rbp)
1241: 75 27 jne 126a <main+0x81>
{
done = 1;
1243: c7 85 5c f8 ff ff 01 movl [=10=]x1,-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 [=10=]x0,%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 [=11=]x7b0,%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 [=11=]x0,-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 [=11=]x1,%edi
1215: e8 b6 fe ff ff callq 10d0 <sleep@plt>
if (!done)
121a: 83 bd 5c f8 ff ff 00 cmpl [=11=]x0,-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 [=11=]x0,%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)
要禁用对“完成”的优化,请在其定义中添加“volatile”关键字:
volatile register int done = 0;
这使得程序在这两种情况下都能正常工作。
(与我撰写本文时发布的 Rachid K 的回答有一些重叠。)
我猜你是在将 done
声明为 register
,希望它实际上会被放入一个寄存器中,这样它的值就会被上下文切换保存和恢复。但是编译器从来没有义务遵守这一点;大多数现代编译器完全忽略 register
声明并自行决定寄存器的使用。特别是,gcc
没有优化几乎总是将内存中的局部变量放在堆栈上。
因此,在您的测试用例中,done
的值是 而不是 由上下文切换恢复的。所以当 getcontext
return 第二次调用时, done
与调用 swapcontext
时的值相同。
当出现 printf
时,正如 Rachid 还指出的那样,done = 1
实际上存储在 swapcontext
之前,因此 [= 的第二个 return 15=],done
的值为 1,跳过 if
块,程序打印 done:1
并退出。
然而,当 printf
不存在时,编译器会注意到 done
的值在其赋值后从未被使用(因为它假定 swapcontext
是一个普通函数并且不我不知道它实际上会在其他地方 return),因此它优化了 dead store(是的,即使优化已关闭)。因此,当第二次 getcontext
return 时,我们有 done == 0
,你会得到一个无限循环。如果您认为 done
会被放置在寄存器中,这可能是您所期望的,但如果是这样,您会因为错误的原因得到“正确”的行为。
如果启用优化,您将再次看到其他内容:编译器注意到 done
不会受到对 getcontext
的调用的影响(再次假设这是一个正常的函数调用)因此它保证在 if
处为 0。所以这个测试根本不需要做,因为它永远是真的。 swapcontext
然后无条件执行,至于done
,它被优化得完全不存在了,因为它不再对代码有任何影响。您将再次看到无限循环。
由于这个问题,您真的无法对在 getcontext
和 swapcontext
之间修改的局部变量做出任何安全的假设。第二次 getcontext
returns 时,您可能会或可能不会看到更改。如果编译器选择围绕函数调用重新排序某些代码(它知道没有理由不这样做,因为它再次认为这些是看不到局部变量的普通函数调用),则会出现更多问题。
获得任何确定性的唯一方法是声明一个变量volatile
。那么你可以确定中间的变化会被看到,编译器不会假设getcontext
不能改变它。在 getcontext
的第二个 return 处看到的值将与调用 swapcontext
时看到的值相同。如果你写 volatile int done = 0;
你应该只看到两条“我是 运行”消息,不管其他代码或优化设置如何。
当我在下面编译我的代码时,它会打印
I am running :)
永远(直到我向程序发送 KeyboardInterrupt 信号),
但是当我取消注释 // printf("done:%d\n", done);
,重新编译并 运行 它时,它只会打印两次,打印 done: 1
然后 returns.
我是 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()”时,编译器推断“done”不会在“if (!done)”之后使用,因此不会将其设置为 1,因为它不值得。但是当“printf()”存在时,“done”在“if (!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 [=10=]x7b0,%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 [=10=]x0,-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 [=10=]x1,%edi
1235: e8 b6 fe ff ff callq 10f0 <sleep@plt>
if (!done)
123a: 83 bd 5c f8 ff ff 00 cmpl [=10=]x0,-0x7a4(%rbp)
1241: 75 27 jne 126a <main+0x81>
{
done = 1;
1243: c7 85 5c f8 ff ff 01 movl [=10=]x1,-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 [=10=]x0,%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 [=11=]x7b0,%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 [=11=]x0,-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 [=11=]x1,%edi
1215: e8 b6 fe ff ff callq 10d0 <sleep@plt>
if (!done)
121a: 83 bd 5c f8 ff ff 00 cmpl [=11=]x0,-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 [=11=]x0,%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)
要禁用对“完成”的优化,请在其定义中添加“volatile”关键字:
volatile register int done = 0;
这使得程序在这两种情况下都能正常工作。
(与我撰写本文时发布的 Rachid K 的回答有一些重叠。)
我猜你是在将 done
声明为 register
,希望它实际上会被放入一个寄存器中,这样它的值就会被上下文切换保存和恢复。但是编译器从来没有义务遵守这一点;大多数现代编译器完全忽略 register
声明并自行决定寄存器的使用。特别是,gcc
没有优化几乎总是将内存中的局部变量放在堆栈上。
因此,在您的测试用例中,done
的值是 而不是 由上下文切换恢复的。所以当 getcontext
return 第二次调用时, done
与调用 swapcontext
时的值相同。
当出现 printf
时,正如 Rachid 还指出的那样,done = 1
实际上存储在 swapcontext
之前,因此 [= 的第二个 return 15=],done
的值为 1,跳过 if
块,程序打印 done:1
并退出。
然而,当 printf
不存在时,编译器会注意到 done
的值在其赋值后从未被使用(因为它假定 swapcontext
是一个普通函数并且不我不知道它实际上会在其他地方 return),因此它优化了 dead store(是的,即使优化已关闭)。因此,当第二次 getcontext
return 时,我们有 done == 0
,你会得到一个无限循环。如果您认为 done
会被放置在寄存器中,这可能是您所期望的,但如果是这样,您会因为错误的原因得到“正确”的行为。
如果启用优化,您将再次看到其他内容:编译器注意到 done
不会受到对 getcontext
的调用的影响(再次假设这是一个正常的函数调用)因此它保证在 if
处为 0。所以这个测试根本不需要做,因为它永远是真的。 swapcontext
然后无条件执行,至于done
,它被优化得完全不存在了,因为它不再对代码有任何影响。您将再次看到无限循环。
由于这个问题,您真的无法对在 getcontext
和 swapcontext
之间修改的局部变量做出任何安全的假设。第二次 getcontext
returns 时,您可能会或可能不会看到更改。如果编译器选择围绕函数调用重新排序某些代码(它知道没有理由不这样做,因为它再次认为这些是看不到局部变量的普通函数调用),则会出现更多问题。
获得任何确定性的唯一方法是声明一个变量volatile
。那么你可以确定中间的变化会被看到,编译器不会假设getcontext
不能改变它。在 getcontext
的第二个 return 处看到的值将与调用 swapcontext
时看到的值相同。如果你写 volatile int done = 0;
你应该只看到两条“我是 运行”消息,不管其他代码或优化设置如何。