0

In a C99 program, under the (theoretical) assumption that I'm not using variable-length arrays, and each of my automatic variables can only exist once at a time in the whole stack (by forbidding circular function calls and explicit recursion), if I sum up all the space they are consuming, could I declare that this is the maximal stack size that can ever happen?

A bit of context here: I told a friend that I wrote a program not using dynamic memory allocation ("malloc") and allocate all memory static (by modeling all my state variables in a struct, which I then declared global). He then told me that if I'm using automatic variables, I still make use of dynamic memory. I argued that my automatic variables are not state variables but control variables, so my program is still to be considered static. We then discussed that there has to be a way to make a statement about the absolute worst-case behaviour about my program, so I came up with the above question.

Bonus question: If the assumptions above hold, I could simply declare all automatic variables static and would end up with a "truly" static program?

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
Eric
  • 673
  • 1
  • 11
  • 22
  • Yes, you could, for both. This is how it worked before recursion was invented. – user253751 Nov 27 '20 at 13:03
  • What you describe can be a must on some microcontrollers. Architectures like 8-bit PIC (from Microchip) do often not have a stack nor functions like `malloc()` implemented. (The stack they have can only store return addresses, and only 8 or so, i do not consider this as a proper stack). – 12431234123412341234123 Nov 27 '20 at 13:16
  • 2
    This is not a C99 feature or a C 2018 feature. It relies on properties of the specific C implementation you are using. Also, the size of automatic objects in a function is not the size of its stack frame (or stack use). It may use more for temporary workspace while evaluating expressions. It uses more for the return address and other data required by the ABI. It may use less because some automatic objects are kept in registers or are optimized away. – Eric Postpischil Nov 27 '20 at 13:27

3 Answers3

2

Even if array sizes are constant a C implementation could allocate arrays and even structures dynamically. I'm not aware of any that do (anyone) and it would appear quite unhelpful. But the C Standard doesn't make such guarantees.

There is also (almost certainly) some further overhead in the stack frame (the data added to the stack on call and released on return). You would need to declare all your functions as taking no parameters and returning void to ensure no program variables in the stack. Finally the 'return address' of where execution of a function is to continue after return is pushed onto the stack (at least logically).

So having removed all parameters, automatic variables and return values to you 'state' struct there will still be something going on to the stack - probably.

I say probably because I'm aware of a (non-standard) embedded C compiler that forbids recursion that can determine the maximum size of the stack by examining the call tree of the whole program and identify the call chain that reaches the peek size of the stack.

You could achieve this a monstrous pile of goto statements (some conditional where a functon is logically called from two places or by duplicating code.

It's often important in embedded code on devices with tiny memory to avoid any dynamic memory allocation and know that any 'stack-space' will never overflow.

I'm happy this is a theoretical discussion. What you suggest is a mad way to write code and would throw away most of (ultimately limited) services C provides to infrastructure of procedural coding (pretty much the call stack)

Footnote: See the comment below about the 8-bit PIC architecture.

Persixty
  • 8,165
  • 2
  • 13
  • 35
  • 1
    There are architectures like 8-bit PIC that do not use a full stack but one that can only hold return addresses. This do not allow recursion. All needed memory is know at the end of compiling. – 12431234123412341234123 Nov 27 '20 at 13:19
  • Thanks for a citation. I'm only aware of them by having a friend who has worked with such embedded devices. It may well have been PIC. It's not far off `GOSUB`\ `RETURN` in some ancient BASIC dialects. – Persixty Nov 27 '20 at 13:32
  • The programm is actually written for an embedded device (esp32). We learned in school that dynamic allocation on embedded devices "is bad" and thus my friend and me started discussing how automaic varaibles are related to dynamic memory allocation. Isn't ultimatly an auto variable another pice of dynamic and we should try to avoide it? Can I say that my programm doesn't use dynamic memory even auto varaibles seem to be somehow dynamic? I'm not refering to dynamic heap memory, but to "dynamic memory in a more general way". – Eric Nov 27 '20 at 17:41
  • 1
    On some level automatic variables are dynamically allocated. But they're allocated on the stack. When we talk about dynamic memory allocation we're usually talking about heap allocation `malloc()` and `free()`. It's not preferred in embedded because it has an overhead and is often hard to prove that when everything is 'maxed' out may run out of memory. Most embedded applications are built with a fixed size for everything (how many sensor, cylinders, jet engines!) there are or how many 'previous' readings are needed. ... – Persixty Nov 27 '20 at 17:51
  • @Eric .. so if you fix the maximum size of all the structures/arrays at compile time you can analyse (or use a tool) to prove you'll never run out of memory and never exceed the available stack by (in essence) re-using memory in a defined way. – Persixty Nov 27 '20 at 17:53
  • @Eric Non-embedded applications want to be more flexible because they don't know what inputs they'll have. We don't want a browser to stop at 10 tabs because it's allocated space to 20 images when it hasn't got 20 images. If that makes sense. The input 'profile' (how many of what) isn't as controlled outside the typical embedded application so fixed maximums are a limitation that doesn't usually exist in an embedded application. – Persixty Nov 27 '20 at 17:57
  • @Persixty can you give me an example of such an tool? Or some keywords for further reading? It would be interessting to apply this to my programm. – Eric Nov 27 '20 at 18:08
  • I've never worked directly in that area. Only with people. @12431234123412341234123 may be able to help. – Persixty Nov 27 '20 at 18:11
  • @Eric I do not know about esp32. Maybe `-fstack-usage` when you use GCC helps? – 12431234123412341234123 Nov 27 '20 at 18:18
  • 2
    @Eric See this question https://stackoverflow.com/questions/6387614/how-to-determine-maximum-stack-usage-in-embedded-system-with-gcc – 12431234123412341234123 Nov 27 '20 at 18:19
  • @Eric If this does not work, you can always write a script that analyzes the assembly code, get the deepest path and then add the maximum main depth with all the deepest paths of interrupts of each interrupt level. Or ask a new question for how to calculate maximum stack depth on esp32. – 12431234123412341234123 Nov 27 '20 at 18:25
1

Bonus question: If the assumptions above hold, I could simply declare all automatic variables static and would end up with a "truly" static program?

No. This would change the function of the program. static variables are initialized only once. Compare this 2 functions:

int canReturn0Or1(void)
{
  static unsigned a=0;
  a++;
  if(a>1)
  {
    return 1;
  }
  return 0;
}

int willAlwaysReturn0(void)
{
  unsigned a=0;
  a++;
  if(a>1)
  {
    return 1;
  }
  return 0;
}
  • good point... but if i write "static unsigned a=0;a=0;"? So explicitly setting it to 0 on the second call? – Eric Nov 27 '20 at 22:58
  • @Eric Thin it would be the same, expect when you have a interrupt accessing the same function, you use more than 1 thread or you have recursion. – 12431234123412341234123 Nov 30 '20 at 18:24
0

In a C99 program, under the (theoretical) assumption that I'm not using variable-length arrays, and each of my automatic variables can only exist once at a time in the whole stack (by forbidding circular function calls and explicit recursion), if I sum up all the space they are consuming, could I declare that this is the maximal stack size that can ever happen?

No, because of function pointers..... Read n1570.

Consider the following code, where rand(3) is some pseudo random number generator (it could also be some input from a sensor) :

typedef int foosig(int);
int foo(int x) {
   foosig* fptr = (x>rand())?&foo:NULL;
   if (fptr) 
     return (*fptr)(x);
   else
     return x+rand();
}

An optimizing compiler (such as some recent GCC suitably invoked with enough optimizations) would make a tail-recursive call for (*fptr)(x). Some other compiler won't.

Depending on how you compile that code, it would use a bounded stack or could produce a stack overflow. With some ABI and calling conventions, both the argument and the result could go thru a processor register and won't consume any stack space.

Experiment with a recent GCC (e.g. on Linux/x86-64, some GCC 10 in 2020) invoked as gcc -O2 -fverbose-asm -S foo.c then look inside foo.s. Change the -O2 to a -O0.

Observe that the naive recursive factorial function could be compiled into some iterative machine code with a good enough C compiler and optimizer. In practice GCC 10 on Linux compiling the below code:

int fact(int n)
{
  if (n<1) return 1;
  else return n*fact(n-1);
}

as gcc -O3 -fverbose-asm tmp/fact.c -S -o tmp/fact.s produces the following assembler code:

    .type   fact, @function
   fact:
   .LFB0:
    .cfi_startproc
    endbr64 
   # tmp/fact.c:3:   if (n<1) return 1;
    movl    $1, %eax    #, <retval>
    testl   %edi, %edi  # n
    jle .L1 #,
    .p2align 4,,10
    .p2align 3
   .L2:
    imull   %edi, %eax  # n, <retval>
    subl    $1, %edi    #, n
    jne .L2 #,
   .L1:
   # tmp/fact.c:5: }
    ret 
    .cfi_endproc
   .LFE0:
    .size   fact, .-fact
    .ident  "GCC: (Ubuntu 10.2.0-5ubuntu1~20.04) 10.2.0"

And you can observe that the call stack is not increasing above.

If you have serious and documented arguments against GCC, please submit a bug report.

BTW, you could write your own GCC plugin which would choose to randomly apply or not such an optimization. I believe it stays conforming to the C standard.

The above optimization is essential for many compilers generating C code, such as Chicken/Scheme or Bigloo.

A related theorem is Rice's theorem. See also this draft report funded by the CHARIOT project.

See also the Compcert project.

Basile Starynkevitch
  • 223,805
  • 18
  • 296
  • 547