5

When allocating a int as well as a large array on the stack in C, the program executes without error. If I however, initialize the variable on the stack beforehand, it crashes with a segfault (probably because the stack size was exceeded by the large array). If initializing the variable after declaring the array this would make sense to me. What causes this behavior, memory wise?

I was under the impression, that by simply declaring a variable on the stack, the needed space would be allocated, leading to an immediate crash when allocating very large datatypes.

My suspicion is that it has something to do with the compiler optimizing it away, but it does not make sense, considering I am not changing foo in the second example either.

I am using gcc 7.2.0 to compile, without any flags set. Executed on Ubuntu 17.10.

This runs without errors:

int main(){
  int i;
  unsigned char foo [1024*1024*1024];
  return 0;
}

while this crashes immediately:

int main(){
  int i = 0;
  unsigned char foo [1024*1024*1024];
  return 0;
}

Can somebody give me some insight what is happening here?

Nisky
  • 53
  • 4
  • 2
    You could compare the difference in assembly output of the two programs – M.M Nov 09 '17 at 20:26
  • 1
    What compiler (and version) do you use? What optimization flags? BTW with [GCC](http://gcc.gnu.org/) 7, used as `gcc -O` on Linux/x86-64/Debian, neither program crashes. See [this](https://stackoverflow.com/a/7973588/841108) for the insight – Basile Starynkevitch Nov 09 '17 at 20:29
  • 1
    None of them crashes for me. – klutt Nov 09 '17 at 20:33
  • Included the information in the question. I will try to produce an assembly version and compare the results. Thanks for the help. – Nisky Nov 09 '17 at 20:34
  • Generated assembly code is exactly the same. – Nisky Nov 09 '17 at 20:40

1 Answers1

6

Note: What follows are implementation details. The C standard does not cover this.

The crash is not caused by allocating space. The crash is caused by writing to pages which are not writable, or reading from pages which are not readable.

You can see that a declaration doesn't actually need to read or write any memory, not necessarily:

int i;

But if it is initialized, you have to write the value:

int i = 0;

This triggers the crash. Note that the exact behavior will depend on the compiler you use and the optimization settings you have. Different compilers will allocate variables in different ways, and an optimizing compiler will normally remove both i and foo from the function entirely, since they aren't needed. Some compilers will also initialize variables to garbage values under certain configurations, to aid with debugging.

Allocating stack space just involves changing the stack pointer, which is a register. If you allocate too much stack space, the stack pointer will point to an invalid region of memory, and the program will segfault when it tries to read or write to those addresses. Most operating systems have “guard pages” so valid memory will not be placed next to the stack, ensuring that the program successfully crashes in most scenarios.

Here is some output from Godbolt:

main:
  push rbp
  mov rbp, rsp
  sub rsp, 1073741720        ; allocate space for locals
  mov DWORD PTR [rbp-4], 0   ; initialize i = 0
  mov eax, 0                 ; return value = 0
  leave
  ret

Note that this version does not crash, because i is placed at the top of the stack (which grows downwards). If i is placed at the bottom of the stack, this will likely crash. The compiler is free to put the variables on the stack in any order, so whether it actually crashes will depend heavily on the specific compiler you are using.

You can also see more clearly why the allocation won't crash:

; Just an integer subtraction. Why would it crash?
sub rsp 1073741720
Dietrich Epp
  • 205,541
  • 37
  • 345
  • 415
  • 1
    Good answer. You could add the info that even if you have no optimization flags, the compiler is allowed to change the order of the variables. This would not be the case within a struct. – klutt Nov 09 '17 at 20:40
  • Thank you very much for your answer. However, what I don't understand is: I declare and initialize int i before reaching the declaration of the array. So when executing the first line in my main function, no bad pages are accessed. So the read/write on int i can not be the reason for the seg-fault, or am I wrong? – Nisky Nov 09 '17 at 20:45
  • 1
    @Nisky: The compiler is free to allocate memory for `i` and `foo` however it wants. It can allocate `foo` on top and `i` on the bottom, or it can allocate `foo` on bottom and `i` on top. Or it is free to not allocate `foo` or `i` at all. No promises, except what are written in the standard! – Dietrich Epp Nov 09 '17 at 20:46
  • So am I right in understanding that this behaviour is caused by the compiler re-ordering the variables i and foo? – Nisky Nov 09 '17 at 20:48
  • 2
    @Nisky: The compiler doesn't “re-order” anything, since the variables don’t actually have any kind of order in the first place. If you put fields in a struct, the fields *do* have an order, but variables simply don’t have an order. – Dietrich Epp Nov 09 '17 at 20:49
  • By re-ordering I meant re-ordering from the way I wrote the C code. I am coming from a background where I was taught code in C was executed "in order" and I am just now reaching into the depths of compilers. So that is why this was causing me trouble. Thank you very much for your help, I understand this now @DietrichEpp. – Nisky Nov 09 '17 at 20:52
  • @Nisky The order of encountering the variable definitions doesn't imply that those variables will use storage in that same order in memory. – M.M Nov 09 '17 at 20:54
  • @M.M I understood that now, thank you very much for your help. – Nisky Nov 09 '17 at 20:59
  • 1
    @Nisky: It is also incorrect that C is executed “in order”—the compiler will emit a program that produces the same “observable behavior” as if the program executed in order, but the program might execute code in a completely different order, or even remove code entirely. So if you write `x = 3; y = x + 1;` the compiler can translate that into something like `y = 4; x = 3;` because the difference is not “observable” according to the standard. – Dietrich Epp Nov 09 '17 at 20:59
  • @DietrichEpp That's very interesting, although it makes an observable difference in error edge cases like mine (which is obviously not in the standard / defined behaviour). – Nisky Nov 09 '17 at 21:04
  • undefined behavior is undefined – klutt Nov 09 '17 at 21:08
  • @DietrichEpp Could you explain to me why the compiler decides to switch the order of declaration in this specific case in my example? – Nisky Nov 09 '17 at 21:08
  • @klutt I would have thought so. – Nisky Nov 09 '17 at 21:10
  • @Nisky: In this case, the “observed behavior” is that the program is incorrect, and therefore anything goes—maybe it crashes, maybe it works, maybe it gives you the wrong answer. As for switching the order of declaration—the order of declaration does not in any way correspond to the order of variables in memory, or the order in which that memory is initialized. – Dietrich Epp Nov 09 '17 at 21:15
  • @Nisky: So I would again say that nothing is “switched”. The compiler simply chose an order. Maybe the compiler puts the variables in alphabetical order, or maybe rolls some dice, or maybe the compiler consults astrological tables. – Dietrich Epp Nov 09 '17 at 21:16
  • @DietrichEpp Forgive me for using the wrong words, as you can surely see English isn't my native language. I just wanted to know if there is a specific rule, that applies in this case, that causes the compiler to choose this specific order (I see that no switching or re-ordering is going on here, just to clarify, it simply is "choosing" the order). So it just comes down to luck, dice rolling or spontaneous astrological suggestion, in your opinion? – Nisky Nov 09 '17 at 21:28
  • @Nisky: You can see this by compiling the same program with multiple compilers, or with the same compiler but different settings, you will sometimes get different results. – Dietrich Epp Nov 09 '17 at 23:07
  • If I was a compiler, I would want to a) easiest/fastest access to vars and b) not use any more space that as required. So, I might allocate all space that is a 256-bit multiple first, then 128, 64.... Your big array is a 256 multiple, (and more), so it gets located first. Then again, maybe the astrological tables provide better overall performance when planets are in alignment. – Martin James Nov 09 '17 at 23:15