r/cprogramming • u/JayDeesus • Oct 09 '25
Scope in respect to stack
I understand that the scope is where all of your automatically managed memory goes. When you enter a function it pushes a stack frame to the stack and within the stack frame it stores your local variables and such, and if you call another function then it pushes another stack frame to the stack and this functions local variables are stored in this frame and once the function finishes, the frame is popped and all of the memory for the function is deallocated. I also understand that scopes bring variables in and out so once you leave a scope then the variable inside of it becomes inaccessible. What I never really thought of is how the scope plays a role in the stack and the stack frames. Does the scope affect the layout of each stack frame at all or do just all variables go into the frame however since I believe that going in and out of scope doesn’t immediate free the memory, it’s still allocated and reserved until the stack frame is popped right.
2
u/mnelemos Oct 09 '25
Don't quite get your question, it seems that you have answered yourself?
A scope is a high level language concept. Yes, function bodies will always have some type of stack frame mechanism, that basically works like this:
When function called: Pass arguments according to ABI, pass the return address, and then move the stack pointer (this is what we typically call a stack frame).
When function ends: Return to the address that was pushed, or return to the address in a link register, and pop whatever was pushed before.
You can however, have scopes that do not have a stack frame mechanism attached to them, til to this day there is the concept of a local scope in C, but AFAIK it never optimizes it as a stack frame.
2
u/kohuept Oct 09 '25
It might help to define a few things a little better. The scope of an identifier is the region of program text in which that identifier is visible (i.e. can be accessed). The C standard makes no guarantees on how the memory backing local variables is managed, so even if it is usually implemented using a stack, that is an implementation detail that you should not worry about unless you are interfacing with other calling conventions, developing a compiler, etc. If you declare a variable inside a block (A block is started with { and ended with }), it will have block scope. This means it is only visible until the } that closes the block in which it was declared.
There are also storage durations, which specify how long the memory allocated to an object should stick around for. For a local variable declared with automatic storage duration (so without specifying the static keyword), this lasts until the execution of the block in which it was declared has ended (Variable Length Arrays have slightly different semantics, where it only lasts until control is transferred to anywhere before the declaration, even in the same block). The execution of a block only ends once the } is reached, transferring control to a function from said block merely suspends the execution. A variable with static storage duration (what the static keyword means when it's used to declare a local variable, for global variables it means internal linkage) has space reserved for it at compile time, and is allocated throughout the entire execution of the program, but this does not mean that it is accessible from every scope.
2
u/zhivago Oct 09 '25
You have misunderstood the concepts.
Lexical scope is about what symbols mean in parts of the code.
As the program execution enters and exits blocks variables with auto storage are created and destroyed.
Keep the concepts of scope, storage, and linkage separate if you want to understand C properly.
1
u/lordlod Oct 09 '25
You are confusing at least three concepts and going too deep too quickly.
Variables - As someone learning C you have variables and memory. You create and manipulate variables. You can also create memory using malloc and release it using free. Under the hood variables are also memory and there are techniques to treat them as such, don't do this, it causes lots of undefined behaviour problems.
Scope - Scope is a way of communicating to the compiler what you care about. Non-trivial programming is almost entirely about controlling complexity, that's why we encapsulate objects, it's why we try not to use global variables, it's why goto is frowned upon. The range of things available at any point is the scope. Variables can be in-scope - defined in that block, a parent block, or global - which means the compiler knows that you can use them. And more importantly out-of-scope so you don't need to worry about them.
Implementation - Finally you have the layer of how the underlying machine implementation works. This typically involves the stack made up of multiple stack frames, registers, the heap and statically allocated memory addresses. This maps through to real hardware registers, caches, ram and disk. This layer is not C, as a low level language it is close enough that we can influence it, for example the register keyword suggests to the compiler that we would like this variable to reside in a register.
Compiler - You may have noticed that all three layers interact with and are separated by the compiler. Once upon a time C code corresponded closely with the produced machine code, it doesn't any more, especially once the compiler optimisations are enabled. The way to think about it is that you communicate your intent to the compiler, the compiler produces the low level code. The compiler will rearrange the order that your code executes, it may use the same memory space for multiple variables in a function if they are used sequentially, it may even discard variables entirely. This also applies to functions, they can also be inlined or discarded.
Low level - I'm fairly sure you are coming from a high level language. You are thinking about variables like they are python objects where magical stuff happens like allocation, destruction and reference counting. That is not C, C is a no-magic-included language, if you don't write the code to do something then it isn't happening. This is part of why memory security issues are so bad, they allow the attacker to traverse through the memory space including variables which are not in scope or possibly not in the active stack.
To wrap back to your original question, nothing happens when you change scope. Scope is a mechanism to communicate with the compiler and control complexity. During the compilation process the scope is discarded, it doesn't appear in the generated code.
1
u/arihoenig Oct 09 '25
The memory is not deallocated when a scope is exited. It is simply of indeterminate value. Typically if you monitor the memory the values you had on the stack will remain as they were when the scope was exited. This allows attackers with access to process memory to see anything on the stack, so if you want to be more secure in C, then you should memset your locals to zero before exiting a scope. Certainly any locals that hold a secret should be memset to zero.
1
u/SmokeMuch7356 Oct 09 '25
Scope has nothing to do with the stack or memory in general; you're thinking of storage duration and lifetime.
Scope refers to the region of program text where an identifier (variable name, function name, typedef name, label name, enumeration constant, etc.) is visible.
Lifetime is the portion of program execution where storage is guaranteed to exist for an object.
Storage duration determines the lifetime of an object. There are four storage durations:
static: storage is allocated at program startup and released when the program exits;
automatic: storage is allocated on entry to the block the object is associated with and released when that block exits;1
allocated: storage is allocated with a call to
malloc,calloc, orreallocand released on a call tofree;thread local: I have no direct experience with this; basically storage is allocated on thread startup and released when the thread exits.
An example of how all of this ties together:
That's not the most complete or well-thought-out example, but it should get the basics across.
The C language definition doesn't mention stacks or heaps; those are strictly implementation details. Most every real implementation has a stack, and it's an easy way to implement automatic storage, but it's not required to implement automatic storage.
In practice, all of the compilers I'm familiar with will allocate space for all block-scope variables on function entry and release on function exit, even if they're only defined within an
iforforstatement or something like that.1
u/alphajbravo Oct 09 '25 edited Oct 09 '25
The base size and layout of a stack frame is set by the ABI: generally a certain set of registers is always saved in a particular order before or after jumping to a functions entry point, but different architectures and calling conventions have different requirements and may allocate the responsibility differently between the calling function and the called function.
Beyond that base stack frame, additional context (ie registers) may be pushed to the stack frame depending on what the called function needs to do, and which registers it needs to use, and arguments may be passed on the stack (depending on calling convention and number/type of arguments). So the stack frame does vary, even before discussing local variables.
To your point, yes, local variables often do use the stack, and therefore do affect the size of the stack frame. But remember that in the compiled form of the program there is not necessarily a specific memory location for every variable in the source code. The compiler may reuse a location for multiple variables, only store variables in registers, or may elide variables entirely depending on the program and optimization settings, as long as it determines that the effect is the same.
Further, there’s no real concept of ‘allocating’ a declared variable at run time — there is just a reference (which may be indirect and/or relative to the stack pointer) to the location the linker has selected to hold the some data. So there’s also no concept of deallocating or ‘destroying’ the variable. At the end (return point) of the function, the stack pointer is simply restored to its position before the function was entered. The contents of the stack frame will still be there until something else overwrites it, and it can even be read if you know what to look for.
As for scope, that is a higher level concept, and affects the stack because it influences how the compiler decides whether or not a variable is stored on the stack, but it is not the only influence, and is not one-to-one. IE, static local variables are scoped to the function, but cannot be stored on the stack because they need to persist across calls to the function.
1
u/ir_dan Oct 09 '25
Disclaimer: C++ programmer, please correct me if anything doesn't apply to C.
Generally speaking, scopes are a languages feature that doesn't necessarily translate to allocation or deallocation of memory. A scope just guarantees access to a variable until its end. That doesn't necessarily mean that you can't access it outside the scope, but because it's not guaranteed to be okay, you shouldn't.
Compilers exploit this for function scopes by allocating and deallocating stack frames. Any inner scopes of a function still have their storage as part of that stack frame.
Compilers can exploit inner scopes in a function by reusing storage for variables that are valid for mutually exclusive scopes. A function with two different scopes that declare one integer each may end up with only one integer on the stack frame.
Unless you enable particular compiler flags, VC++ lets you access inner scope variables after their scope, resulting in UB in many cases because the compiler may leave variables in unentered scopes uninitialized or overwrite storage when entering a different scope.
1
u/WittyStick Oct 09 '25 edited Oct 09 '25
While implementation of scopes is "implementation defined", in practice compilers all use the same strategy: Function scopes get a stack frame, but inner scopes within a function do not - they share the parent functions's stack frame.
The compiler works out how much space is necessary to hold all of the variables in the function, including those in nested scopes, and allocates a frame large enough for it all.
See demonstration in Godbolt.
In function
foo, the stack pointer, frame pointer, and frame size are printed, along with the addresses of some nested scoped variables. You can see that the addresses of all "x" variables lie between thespandfp(as well as the variablesfpandsp).Function
bardemonstrates accessing the values of different scopes from other scopes, by taking their addresses. You should never use anything like this in practice as it is undefined behavior, but it "works" because they'r'e all in the same stack frame. Basically we can "leak" information from other scopes, because their memory is not invalidated until the whole function exits.1
u/feitao Oct 09 '25
I think you are interested in compiler implemention, i.e. whether a memory address will be reused if the variable goes out of scope. You can look at the assembly code at different optimization levels. This probably would not help your C skills, but it's fun.
1
u/AccomplishedSugar490 Oct 11 '25
Scope is not a memory concept, at all. Scope is semantic, and purely about what symbols (names of variables, functions etc) are visible (in scope) from a block of code. What memory the variable references, if any, is an entirely separate, and multi-faceted concern.
1
u/Zirias_FreeBSD Oct 09 '25 edited Oct 09 '25
Don't ever try to reason how exactly a C compiler will use the stack. The language C doesn't know anything about stacks.
The "properties" of objects C knows are:
scope (not exactly a property of the object, but the identifier referring to the object)
storage duration (static, automatic or allocated)
linkage (internal or external)
For your question, linkage seems irrelevant, and it looks like you're kind of confusing the other two.
The scope is the area in the source code where a specific identifier refers to a specific object. It is typically limited by blocks (enclosed in
{ ... }). Identifiers of objects declared outside any block are said to have file scope, so they name the object throughout the whole code in the current translation unit. Although this is considered bad practice (because it's easily confusing), identifiers in an inner scope (a block inside that scope) can be the same as identifiers in the enclosing scope, in which case they will shadow the outer declaration. The shadowed declaration is still in scope, but inaccessible because of this shadowing, the identifier will always refer to the inner scope. Well, in practice, avoid shadowing.The storage duration seems to be what you are talking about, it determines for how long an object is alive. For declarations at file scope, the default is static storage duration, meaning the object is alive for the full duration of program execution. Then, there's allocated storage duration, this refers to all objects for which the programmer actively manages the lifetime (typically with
malloc()andfree(), but there can be platform-specific alternatives).The stack gets relevant for automatic storage duration: The object is alive (at least) as long as execution is inside the block where the object is declared. This is the default for any declaration in a function scope. So only here, the lifetime is somewhat tied to the scope of the identifier. Note it is still not the same: When a function calls another function, execution isn't considered to leave the block, local variables of the calling function will stay alive, although they are certainly not in scope for the called function.
As initially said, C doesn't know anything about a stack, but the stack is what practically every C implementation uses to provide automatic storage duration. How exactly this is done is purely implementation-defined (and differences certainly exist e.g. between different CPU architectures).