8

This simple C program rarely terminates at the same call depth:

#include <stdio.h>
#include <stdlib.h>

void recursive(unsigned int rec);

int main(void)
{
  recursive(1);
  return 0;
}

void recursive(unsigned int rec) {
    printf("%u\n", rec);
    recursive(rec + 1);
}

What could be the reasons behind this chaotic behavior?

I am using fedora (16GiB ram, stack size of 8192), and compiled using cc without any options.

EDIT

  • I am aware that this program will throw a stackoverflow
  • I know that enabling some compiler optimizations will change the behavior and that the program will reach integer overflow.
  • I am aware that this is undefined behavior, the purpose of this question is to understand/get an overview of the implementation specific internal behaviors that might explain what we observe there.

The question is more, given that on Linux the thread stack size is fixed and given by ulimit -s, what would influence the available stack size so that the stackoverflow does not always occur at the same call depth?

EDIT 2 @BlueMoon always sees the same output on his CentOS, while on my Fedora, with a stack of 8M, I see different outputs (last printed integer 261892 or 261845, or 261826, or ...)

  • @moffeltje 1 2 3 4 5 ... infinity – Binkan Salaryman Jul 02 '15 at 09:25
  • 1
    There is no stop condition: eventually rec+1 will overflow – n0p Jul 02 '15 at 09:25
  • 5
    @moffeltje Read the title: **Why are stackoverflow errors chaotic?** – Binkan Salaryman Jul 02 '15 at 09:26
  • @moffeltje Why do you think it terminates? Step through the code...what would make it stop? –  Jul 02 '15 at 10:02
  • @moffeltje It's good to be curious, but the thing to take away from this is that whenever there is undefined behaviour anything can happen. –  Jul 02 '15 at 10:15
  • 1
    you should know what [undefined behavior](https://en.wikipedia.org/wiki/Undefined_behavior) is. [What Every C Programmer Should Know About Undefined Behavior #1/3](http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html) https://randomascii.wordpress.com/2014/05/19/undefined-behavior-can-format-your-drive/ – phuclv Jul 02 '15 at 10:24
  • 1
    You could put a high enough stop condition on the recursion, the behavior wouldn't be undefined according to the C standard and the program would still crash unpredictably. At least I can't find any mention in any C standard about any limits on stack depth or recursion depth. "undefined behavior" as an answer to this question is just a hand wave:y way of saying "I don't know, go away". – Art Jul 02 '15 at 12:23
  • @this, Alex: the question originally used signed integers, which if overflown WOULD cause Undefined Behavior. But until the overflow happens, the behavior is defined, and clearly the stack overflow was crashing the program before the integer overflow happened. So, what Undefined Behavior are you seeing here? – hmijail Mar 20 '16 at 11:55
  • NASTINESS WARNING: my reasoning was that, even if there was a possibility of Undefined Behavior (as in the original, uncorrected source), it would not be a problem if the program stopped before reaching that point. I WAS WRONG. Once there is UB in the code, the WHOLE program is compromised. http://shape-of-code.coding-guidelines.com/2012/07/12/undefined-behavior-can-travel-back-in-time/ – hmijail Mar 21 '16 at 08:08

7 Answers7

10

Change the printf call to:

printf("%u %p\n", rec, &rec);

This forces gcc to put rec on the stack and gives you its address which is a good indication of what's going on with the stack pointer.

Run your program a few times and note what's going on with the address that's being printed at the end. A few runs on my machine shows this:

261958 0x7fff82d2878c
261778 0x7fffc85f379c
261816 0x7fff4139c78c
261926 0x7fff192bb79c

First thing to note is that the stack address always ends in 78c or 79c. Why is that? We should crash when crossing a page boundary, pages are 0x1000 bytes long and each function eats 0x20 bytes of stack so the address should end with 00X or 01X. But looking at this closer, we crash in libc. So the stack overflow happens somewhere inside libc, from this we can conclude that calling printf and everything else it calls needs at least 0x78c = 1932 (possibly plus X*4096) bytes of stack to work.

The second question is why does it take a different number of iterations to reach the end of the stack? A hint is in the fact that the addresses we get are different on every run of the program.

1 0x7fff8c4c13ac
1 0x7fff0a88f33c
1 0x7fff8d02fc2c
1 0x7fffbc74fd9c

The position of the stack in memory is randomized. This is done to prevent a whole family of buffer overflow exploits. But since memory allocations, especially at this level, can only be done in multiple of pages (4096 bytes) all initial stack pointers would be aligned at 0x1000. This would reduce the number of random bits in the randomized stack address, so additional randomness is added by just wasting a random amount of bytes at the top of the stack.

The operating system can only account the amount of memory you use, including the limit on the stack, in whole pages. So even though the stack starts at a random address, the last accessible address on the stack will always be an address ending in 0xfff.

The short answer is: to increase the amount of randomness in the randomized memory layout a bunch of bytes on the top of the stack are deliberately wasted, but the end of the stack has to end on a page boundary.

Art
  • 19,807
  • 1
  • 34
  • 60
4

You won't have the same behaviour between executions because it depends on the current memory available. The more memory you have available, the further you'll go in this recursive function.

Dimitri Mockelyn
  • 1,535
  • 2
  • 11
  • 17
2

Your program runs infinitely as there is no base condition in your recursive function. Stack will grow continuously by each function call and will result in stack overflow.
If it would be the case of tail-recursion optimization (with option -O2), then stack overflow will occur for sure. Its invoke undefined behavior.

what would influence the available stack size so that the stackoverflow does not always occur at the same call depth?

When stack overflow occurs it invokes undefined behavior. Nothing can be said about the result in this case.

haccks
  • 104,019
  • 25
  • 176
  • 264
  • Yup but what causes the stackoverflow to happen chaoticaly? – Alexandre de Champeaux Jul 02 '15 at 09:58
  • 1
    @AlexandredeChampeaux; Undefined behavior. – haccks Jul 02 '15 at 10:03
  • 1
    @AlexandredeChampeaux undefined behavior's result in undefined. It may delete all files on your system, trigger some atomic explosion or [make demons fly out of your nose](http://catb.org/jargon/html/N/nasal-demons.html) – phuclv Jul 02 '15 at 10:21
  • There is no Undefined Behavior in the code posted in the question (either the original using signed integers nor the modified one using unsigned integers). If you think there is UB, please point to it. – hmijail Mar 20 '16 at 12:43
  • @haccks, stack overflow is not Undefined Behavior, at least not in the sense used in the C Standards (or at the very least I can not find any reference. Can you?). Might be better to specify that, or use other wording. – hmijail Mar 20 '16 at 14:44
1

There is a gap between the stack segment and the heap segment. Now because the size of heap is variable( keeps on changing during execution), therefore the extent to which your stack will grow before stackoverflow occurs is also variable and this is the reason why your program rarely terminates at the same call depth.

enter image description here

Sumeet
  • 8,086
  • 3
  • 25
  • 45
  • I thought the maximum stack size was constant. Is it not the case? – Alexandre de Champeaux Jul 02 '15 at 10:03
  • Look, it is due to the heap size that is not fixed, and as a consequence the maximum stack size is not constant. Take a look at google images for process stack and heap. – Sumeet Jul 02 '15 at 10:10
  • 1
    The standard doesn't mandate that the heap and stack are laid out in memory in that manner. The size of the stack could change for other reasons. –  Jul 02 '15 at 10:19
  • @Dante It's implementation dependant. –  Jul 02 '15 at 12:10
1

The above code can cause two issue:

  • Stack Overflow.
  • Integer overflow.

Stack Overflow: When a recursive function is called, all its variable is pushed onto the call stack including its return address. As there is no base condition which will terminate the recursion and the stack memory is limited, the stack will exhausted resulting Stack Overflow exception. The call stack may consist of a limited amount of address space, often determined at the start of the program. The size of the call stack depends on many factors, including the programming language, machine architecture, multi-threading, and amount of available memory. When a program attempts to use more space than is available on the call stack (that is, when it attempts to access memory beyond the call stack's bounds, which is essentially a buffer overflow), the stack is said to overflow, typically resulting in a program crash.

Note that, every time a function exits/return, all of the variables pushed onto the stack by that function, are freed (that is to say, they are deleted). Once a stack variable is freed, that region of memory becomes available for other stack variables. But for recursive function, the return address are still on the stack until the recursion terminates. Moreover, automatic local variables are allocated as a single block and stack pointer advanced far enough to account for the sum of their sizes. You maybe interested at Recursive Stack in C.

Integer overflow: As every recursive call of recursive() increments rec by 1, there is a chance that Integer Overflow can occur. For that, you machine must have a huge stack memory as the range of unsigned integer is: 0 to 4,294,967,295. See reference here.

Community
  • 1
  • 1
mazhar islam
  • 5,561
  • 3
  • 20
  • 41
  • 2
    Yep but why is it chaotic? – Alexandre de Champeaux Jul 02 '15 at 09:54
  • @AlexandredeChampeaux, can you elaborate what exactly you mean by _chaotic_? The stack memory will be exhausted, your code will crush, if not, then integer overflow will occur, which immediately cause _Undefined Behaviour_. – mazhar islam Jul 02 '15 at 09:57
  • As described in my question, if you run this program without any compiler optimizations, the last printed integer before an error is rarely the same. – Alexandre de Champeaux Jul 02 '15 at 09:59
  • @AlexandredeChampeaux, I edited my answer, the call stack may consist of a limited amount of address space, often determined at the start of the program where the size of the call stack depends on many factors. – mazhar islam Jul 02 '15 at 10:05
  • @raked.void Thanks for your edits. Any pointers on what are the factors that determine the size of the stack? I thought this was a constant determined by the os – Alexandre de Champeaux Jul 02 '15 at 10:11
1

Your recursive call is not necessarily going to cause undefined behaviour due to stackoverflow (but will due to integer overflow) in practice. An optimizing compiler could simply turn your compiler into an infinite "loop" with a jump instruction:

void recursive(int rec) {
   loop:
    printf("%i\n", rec);
    rec++;
   goto loop;
}

Note that this is going to cause undefined behaviour since it's going to overflow rec (signed int overflow is UB). For example, if rec is of an unsigned int, for example, then the code is valid and in theory, should run forever.

P.P
  • 117,907
  • 20
  • 175
  • 238
  • 1
    My question is more general: I know that optimization could happen and so on, but in the case they don't, why is the behavior chaotic? – Alexandre de Champeaux Jul 02 '15 at 09:56
  • 2
    @AlexandredeChampeaux There's no reason to believe it would act the same; it's not defined in any standard. –  Jul 02 '15 at 10:04
  • Your original question didn't state that you want to know callstack influence. A better testcase would be to use unsigned integer to get integer out of the equation. – P.P Jul 02 '15 at 10:06
  • @AlexandredeChampeaux On my centOS with a 10MB stacksize, it always segfaults at a certain frame and always prints 392704 (using unsigned int). You need to state exactly what "chaotic" behaviour you observe on your system. – P.P Jul 02 '15 at 10:43
  • @BlueMoon This is interesting. I do observe a different integer each time (using unsigned int as well). I edited the question accordingly. – Alexandre de Champeaux Jul 02 '15 at 11:12
  • @AlexandredeChampeaux Are you also interested in why you can sometimes use freed memory but other times the program crashes? Have you noticed whether it crashes more often if the method name begins with a capital letter? –  Jul 02 '15 at 12:10
  • @Alex The first part does make sense to me, it is quite easy to grasp. However I am curious about your capital letter theory. – Alexandre de Champeaux Jul 02 '15 at 13:10
1

When a process loads a program from an executable, typically it allocates areas of memory for the code, the stack, the heap, initialised and uninitialised data.

The stack space allocated is typically not that large, (10s of megabytes probably) and so you would imagine that physical RAM exhaustion would not be an issue on a modern system and the stack overflow would always happen at the same depth of recursion.

However, for security reasons, the stack isn't always in the same place. Address Space Layout Randomisation ensures that the base of the stack's location varies between invocations of the program. This means that the program may be able to do more (or fewer) recursions before the top of the stack hits something inaccessible like the program code.

That's my guess as to what is happening, anyway.

JeremyP
  • 84,577
  • 15
  • 123
  • 161
  • Hmm this is interesting, I wasn't aware of this behavior. However the stacks location should not have an influence on its size, right? I thought the stack size was fixed by ulimit -s? – Alexandre de Champeaux Jul 02 '15 at 11:19
  • `ulimit -s` sets the **maximum** stack size according to the man page. I don't know what controls the stack size for an individual process, nor do I know what protection, if any, there is for exceeding the limit. I think it probably just runs into the read only program code. Clearly the stack of your program is not of a set size, or it would always crash after the same number of recursions. – JeremyP Jul 06 '15 at 13:15