2

I was putting together a C riddle for a couple of my friends when a friend drew my attention to the fact that the following snippet (which happens to be part of the riddle I'd been writing) ran differently when compiled and run on OSX

#include <stdio.h>
#include <string.h>

int main()
{
        int a = 10;
        volatile int b = 20;
        volatile int c = 30;

        int data[3];

        memcpy(&data, &a, sizeof(data));

        printf("%d %d %d\n", data[0], data[1], data[2]);
}

What you'd expect the output to be is 10 20 30, which happens to be the case under Linux, but when the code is built under OSX you'd get 10 followed by two random numbers. After some debugging and looking at the compiler-generated assembly I came to the conclusion that this is due to how the stack is built. I am by no means an assembly expert, but the assembly code generated on Linux seems pretty straightforward to understand while the one generated on OSX threw me off a little. Perhaps I could use some help from here.

This is the code that was generated on Linux:

        .file   "code.c"
        .section        .text.unlikely,"ax",@progbits
.LCOLDB0:
        .section        .text.startup,"ax",@progbits
.LHOTB0:
        .p2align 4,,15
        .globl  main
        .type   main, @function
main:
.LFB23:
        .cfi_startproc
        movl    $10, -12(%rsp)
        xorl    %eax, %eax
        movl    $20, -8(%rsp)
        movl    $30, -4(%rsp)
        ret
        .cfi_endproc
.LFE23:
        .size   main, .-main
        .section        .text.unlikely
.LCOLDE0:
        .section        .text.startup
.LHOTE0:
        .ident  "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609"
        .section        .note.GNU-stack,"",@progbits

And this is the code that was generated on OSX:

        .section        __TEXT,__text,regular,pure_instructions
        .macosx_version_min 10, 12
        .globl  _main
        .p2align        4, 0x90
_main:                                  ## @main
        .cfi_startproc
## BB#0:
        pushq   %rbp
Ltmp0:
        .cfi_def_cfa_offset 16
Ltmp1:
        .cfi_offset %rbp, -16
        movq    %rsp, %rbp
Ltmp2:
        .cfi_def_cfa_register %rbp
        subq    $16, %rsp
        movl    $20, -8(%rbp)
        movl    $30, -4(%rbp)
        leaq    L_.str(%rip), %rdi
        movl    $10, %esi
        xorl    %eax, %eax
        callq   _printf
        xorl    %eax, %eax
        addq    $16, %rsp
        popq    %rbp
        retq
        .cfi_endproc

        .section        __TEXT,__cstring,cstring_literals
L_.str:                                 ## @.str
        .asciz  "%d %d %d\n"


.subsections_via_symbols

I'm really only interested in two questions here.

  1. Why is this happening?

  2. Are there any get-arounds to this issue?

I know this is not a practical way to utilize the stack as I'm a professional C developer, which is really the only reason I found this problem interesting to invest some of my time into.

  • 3
    You're invoking undefined behavior. All bets are off. Personally, I wouldn't expect the output `10 20 30`. Rather, I'd expect `1͉̟̗̗͕͊́̅̋̇̽̊̔͠0̶̨̳̖̰̥͍̞͛̇̎̒̌̋̀̏̚͠ 2̶̨̼̰͖̬̒̓͟͞͡͞0̳͔̩̰̼̙͓͓̗̲͌̊̔͐̌̚͠ 3̗͔̰̥͉͎̦̦̀͋͊̓̂͛͢͜Ȟ̳̖͙͓̻͉͂̈́̀̀͊̑̓͢͞͝ͅĘ̛̯̣̞̱̰͉̗̂͐̑̍̓̔ͅͅ Ç̵͔̝͉͇̲͖̾͆̐̃̀̐̽̓̕͘͜Ỏ̵̰̣̳̤̪̅̄̌̈̐̅͌̚͞M͖̪̝͈͇͙͛̽͊̀̈́̀̔͝Ę̘̺̙͇̦̖̯̜̿̊̅̔́̑̃Ş̵̡̢̳̔̾̅̽̃̕̕͜͝` – Cornstalks Jul 27 '17 at 02:55
  • How's this undefined behavior? I'm giving `memcpy` a memory address to start at and a number of bytes to read – Fadi Hanna AL-Kass Jul 27 '17 at 03:04
  • 2
    @FadiHannaAL-Kass - There's only 1 element at the address for `a`, but you are reading 3 elements (`b` and `c` on some platforms). That's the undefined behavior. As you are witnessing, `a`, `b` and `c` may not be contiguous. Also see [Is it undefined behaviour to access an array beyond its end, if that area is allocated?](https://stackoverflow.com/q/12354413/608639) – jww Jul 27 '17 at 03:23
  • Is `volatile` your attempt at forcing the compiler to put b and c on the stack? – Ajay Brahmakshatriya Jul 27 '17 at 03:51
  • Ajay, no I declared them `volatile` to force the compiler to maintain them in optimization mode – Fadi Hanna AL-Kass Jul 27 '17 at 04:35

3 Answers3

4

Accessing memory past the end of a declared variable is undefined behaviour - there is no guarantee as to what will happen when you try to do that. Because of how the compiler generated the assembly under Linux, you happened to get the 3 variables directly in a row on the stack, however that behaviour is just a coincidence - the compiler could legally add extra data in between the variables on the stack or really do anything - the result is not defined by the language standard. So in answer to your first question, it's happening because what you're doing is not part of the language by design. In answer to your second, there's no way to reliably get the same result from multiple compilers because the compilers are not programmed to reliably reproduce undefined behaviour.

Logan Hunter
  • 397
  • 4
  • 8
  • Well, in this case gcc is forced to store the three values in memory as `b` and `c` are volatile and `a`'s passed by reference to `memcpy`. Though the rest of your answer makes sense – Fadi Hanna AL-Kass Jul 27 '17 at 03:14
  • @FadiHannaAL-Kass Just declaring it `volatile` is not going to be enough. Any smart compiler would discard the `volatile` because address is not taken. – Ajay Brahmakshatriya Jul 27 '17 at 03:52
  • But wouldn't `volatile` literally tell the compiler that the memory occupied may be accessed from outside the program itself? How would a compiler determine whether this is going to happen or not? I mean a smart compiler wouldn't try to outsmart the developer and introduce bugs to your code – Fadi Hanna AL-Kass Jul 27 '17 at 04:38
  • @FadiHannaAL-Kass: A C compiler is not obligated to store variables in *any* particular order in memory. Only if they're in a struct are there any rules. – Zan Lynx Jul 27 '17 at 05:45
2

undefined behavior. You don't expect to copy 10, 20 ,30. You hope not to seg-fault.

There is nothing to guarantee that a,b, and c are sequential memory addresses, which is your naive assumption. On Linux, the compiler happened to make them sequential. You can't even rely on gcc always doing that.

Jonathan Olson
  • 1,166
  • 9
  • 19
0

You already know that the behavior is undefined. A good reason for the behavior to be different on OS/X and Linux is these systems use a different compiler, that generates different code:

  • When you run gcc in Linux, you invoke the installed version the Gnu C compiler.

  • When you run gcc in your version of OS/X, you most likely invoke the installed version of clang.

Try gcc --version on both systems and amaze your friends.

chqrlie
  • 131,814
  • 10
  • 121
  • 189