为什么此代码与单个 printf 的行为不同? ucontext.h

问题描述


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

我正在跑步:)

永远(直到我向程序发送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,它完全优化不存在,因为它不再对代码产生任何影响。您将再次看到一个无限循环。

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

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