3

当我在下面编译我的代码时,它会打印

我在跑步 :)

永远(直到我向程序发送 KeyboardInterrupt 信号),
但是当我取消注释// printf("done:%d\n", done);,重新编译并运行它时,它只会打印两次,打印done: 1然后返回。
我是 ucontext.h 的新手,我对这段代码的工作方式以及为什么单个 printf 会改变代码的整个行为感到非常困惑,如果你用它替换printfdone++;会做同样的事情,但如果你用它替换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;
}
4

2 回答 2

3

这是一个编译器优化“问题”。当注释“printf()”时,编译器推断在“if (!done)”之后不会使用“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    $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;

这使得程序在这两种情况下都有效。

于 2020-12-30T19:57:29.773 回答
3

(我在写这篇文章时发布的 Rachid K 的答案有一些重叠。)

我猜你正在声明doneregister希望它实际上会被放入一个寄存器中,以便它的值将被上下文切换保存和恢复。但是编译器从来没有义务尊重这一点;大多数现代编译器register完全忽略声明并自行决定寄存器的使用。特别是,gcc如果没有优化,几乎总是将局部变量放在堆栈上的内存中。

因此,在您的测试用例中,上下文切换不会恢复的done值。所以当第二次返回时,与调用时的值相同。getcontextdoneswapcontext

printf存在时,正如 Rachid 指出的那样,done = 1实际存储在 之前swapcontext,所以在 的第二次返回时getcontextdone值为 1,if跳过该块,程序打印done:1并退出。

但是,当printf不存在时,编译器会注意到在done赋值后从未使用过的值(因为它假定swapcontext是一个普通函数并且不知道它实际上会返回其他地方),因此它优化了死存储(是的,即使优化已关闭)。因此,done == 0getcontext第二次返回时,您会得到一个无限循环。如果您认为将被放置在寄存器中,这可能是您所期望done的,但如果是这样,那么您出于错误的原因得到了“正确”的行为。

如果启用优化,您将再次看到其他内容:编译器注意到done不会受到调用的影响getcontext(再次假设它是一个正常的函数调用),因此它保证在if. 所以测试根本不需要做,因为它总是正确的。然后swapcontext无条件执行,至于done,它被优化完全不存在,因为它不再对代码有任何影响。您将再次看到一个无限循环。

getcontext由于这个问题,你真的不能对在和之间修改的局部变量做出任何安全的假设swapcontext。第二次返回时getcontext,您可能会或可能不会看到更改。如果编译器选择围绕函数调用重新排序您的一些代码(它知道没有理由不这样做,因为它再次认为这些是看不到您的局部变量的普通函数调用),则会出现进一步的问题。

获得任何确定性的唯一方法是声明一个变量volatile。然后您可以确定看到中间更改,并且编译器不会假设getcontext无法更改它。在第二次返回时看到的值getcontext将与调用时相同swapcontext。如果您编写volatile int done = 0;,您应该只看到两条“我正在运行”消息,而不管其他代码或优化设置如何。

于 2020-12-30T20:11:00.303 回答