3

When I compile my code below it prints

I am running :)

forever(Until I send KeyboardInterrupt signal to the program),
but when I uncomment // printf("done:%d\n", done);, recompile and run it, it will print only two times, prints done: 1 and then returns.
I'm new to ucontext.h and I'm very confused about how this code is working and why a single printf is changing whole behavior of the code, if you replace printf with done++; it would do the same but if you replace it with done = 2; it does not affect anything and works as we had the printf commented at first place.
Can anyone explain:
Why is this code acting like this and what's the logic behind it?
Sorry for my bad English,
Thanks a lot.

#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;
}

2 Answers2

3

This is a compiler optimization "problem". When the "printf()" is commented, the compiler deduces that "done" will not be used after the "if (!done)", so it does not set it to 1 as it is not worth. But when the "printf()" is present, "done" is used after "if (!done)", so the compiler sets it.

Assembly code with the "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;

Assembly code without the "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)

To disable the optimization on "done", add the "volatile" keyword in its definition:

volatile register int done = 0;

This makes the program work in both cases.

Rachid K.
  • 4,490
  • 3
  • 11
  • 30
  • I don't think thats right, if you change printf with `done = 2;` , it doesn't break the loop and it goes forever, also we are changing the context with `swapcontext(&two, &one);` in the if block, and it's not supposed to get out of if block (by knowledge that i have from `swapcontext` manual page). – Ahmadreza Hadi Dec 30 '20 at 20:04
  • 2
    Compare the generated assembly code in each case to get the "truth" ;-) – Rachid K. Dec 30 '20 at 20:07
  • by this logic if we remove register in the definition from the first code, it should do the same job (Am I right?). but it doesn't! it prints for 2 times and returns. I think i get it from your comment on Nate's answer, THANKS A LOT – Ahmadreza Hadi Dec 30 '20 at 20:30
3

(There is some overlap with Rachid K's answer as it was posted while I was writing this.)

I am guessing you are declaring done as register in hopes that it will actually be put in a register, so that its value will be saved and restored by the context switch. But the compiler is never obliged to honor this; most modern compilers ignore register declarations completely and make their own decisions about register usage. And in particular, gcc without optimizations will nearly always put local variables in memory on the stack.

As such, in your test case, the value of done is not restored by the context switch. So when getcontext returns for the second time, done has the same value as when swapcontext was called.

When the printf is present, as Rachid also points out, the done = 1 is actually stored before the swapcontext, so on the second return of getcontext, done has the value 1, the if block is skipped, and the program prints done:1 and exits.

However, when the printf is absent, the compiler notices that the value of done is never used after its assignment (since it assumes swapcontext is a normal function and doesn't know that it will actually return somewhere else), so it optimizes out the dead store (yes, even though optimizations are off). Thus we have done == 0 when getcontext returns the second time, and you get an infinite loop. This is maybe what you were expecting if you thought done would be placed in a register, but if so, you got the "right" behavior for the wrong reason.

If you enable optimizations, you'll see something else again: the compiler notices that done can't be affected by the call to getcontext (again assuming it's a normal function call) and therefore it is guaranteed to be 0 at the if. So the test need not be done at all, because it will always be true. The swapcontext is then executed unconditionally, and as for done, it's optimized completely out of existence, because it no longer has any effect on the code. You'll again see an infinite loop.

Because of this issue, you really can't make any safe assumptions about local variables that have been modified in between the getcontext and swapcontext. When getcontext returns for the second time, you might or might not see the changes. There are further issues if the compiler chose to reorder some of your code around the function call (which it knows no reason not to do, since again it thinks these are ordinary function calls that can't see your local variables).

The only way to get any certainty is to declare a variable volatile. Then you can be sure that intermediate changes will be seen, and the compiler will not assume that getcontext can't change it. The value seen at the second return of getcontext will be the same as at the call to swapcontext. If you write volatile int done = 0; you ought to see just two "I am running" messages, regardless of other code or optimization settings.

Nate Eldredge
  • 48,811
  • 6
  • 54
  • 82
  • 1
    Moreover I did the same test adding volatile in the definition of "done": volatile register int done = 0; ==> This makes the program work with and without "printf()" as the compiler supposes that "done" can be changed at any time and so, there is no optimization for "done" – Rachid K. Dec 30 '20 at 20:19