2

In a C program the local variables are stored on Stack. Let us say following there local variables are defined.

int x, y, z;

It means first 'x' will be pushed on stack, then 'y' and then 'z'. Now if I need to use 'x' variable before 'y' or 'z' then it would mean that I would have to pop 'y' and 'z' before I can get access of 'x' variable on the stack.

Is my understanding correct or is there anything missing in it?

1
  • 1
    @queueFunctions.h The order in which variables with the automatic storage duration will be placed in memory is unspecified. Commented Apr 28, 2021 at 12:02

4 Answers 4

6

You have a basic misunderstanding of how a typical system stack works.

Most important: It seems that you think that only the last element on the stack can be accessed but that's not how it works. All objects (aka variables) on the stack can be accessed at any time.

Further, there is typically not a push/pop of the individual objects. Typically it's a stack pointer which is changed and that single operation makes room for all the objects needed in the stack.

Finally, the order in which you declare variables doesn't dictate the order in which they are placed on the stack. It's even possible that one or more of your variables will be kept in registers and therefore won't be on the stack.

Also notice that nothing of this comes from the C standard. It all depends on the system being used. The C standard doesn't even mention/require that the implementation uses a stack.

Sign up to request clarification or add additional context in comments.

2 Comments

Re “Further, there is no push/pop of the individual objects”: This is up to each implementation.
@EricPostpischil True - it's implementation dependant. And so is it all. For instance could an implementation update the stack pointer once for every new object it needs to put on the stack instead of doing it as a single operation. Therefore I used the term "typical" several times and also explicit mentioned that all of this is systemt dependant stuff.
3

The order in which local variables are placed on the stack is an implementation detail of the compiler. They could appear in the order declared, in the reverse order, or mixed. The existence of these variables could even be optimized out.

The C standard does not dictate the ordering of local variables. In fact, it doesn't even dictate that a stack must be used.

With regard to using the variables, the compiler keeps track of their addresses and generates code to read/write from the address in question. Typically, nothing actually gets popped until either the function they are in returns or the enclosing scope they are in ends, but again that's an implementation detail.

1 Comment

Re “Nothing actually gets popped until the function they are in returns”: This is up to each implementation. In C’s abstract model, automatic objects associated with a nested block cease to exist when execution of the block they are in ends, and an implementation could pop them from the stack. In practice, it can pop any objects at the top of the stack that are no longer in use.
1

Your understanding is not correct. You do not have to do any pushing or popping to access different variables. As far as your source code is concerned, there's no awareness of a stack or any stack operations. You just reference the variables as you need to:

z = x * y;

First, C does not mandate the use of a stack to store local (auto) variables; that's an implementation detail. Almost every C implementation does use a stack for this purpose, but it's not required.

Secondly, for implementations that use a stack, individual items are not pushed or popped as they are accessed. Instead, when a function is called a whole region of the stack, usually called a stack frame, is set aside and the function's arguments and local variables are referred to via an offset from an address within that frame.

For example, assume the function

void foo( int x, int y )
{
  int a, b;

  // some code
}

On an x86-like system, when this function is called a region of space on the stack will be allocated to store the function arguments, the local variables, the return address (i.e., the address of the next instruction to execute after the function returns), and the address of the previous stack frame (which stores the state of whatever function called foo). It would look something similar to this:

             +-----------------+
 0x80000000: |        y        | Argument 2
             +-----------------+
 0x7FFFFFFC: |        x        | Argument 1
             +-----------------+
 0x7FFFFFF8: |    0x01234567   | Address of next instruction
             +-----------------+
 0x7FFFFFF4: |    0x80000010   | Address of previous stack frame <--+
             +-----------------+                                    |
 0x7FFFFFF0: |        a        | Local variable                     |
             +-----------------+                                    |
 0x7FFFFFDC: |        b        | Local variable <-+                 |
             +-----------------+                  |                 |                   
                                                  |                 |
             +-----------------+                  |                 |
        esp: |   0x7FFFFFC8    | Stack pointer ---+                 |
             +-----------------+                                    |
                                                                    |
             +-----------------+                                    |
        ebp: |   0x7FFFFFF4    | Frame pointer ---------------------+
             +-----------------+

(This assumes 32-bit integers and addresses, and the addresses shown are only for illustration)

There are two registers that are used to keep track of the runtime stack. The stack pointer (esp or rsp on x86 and x86-64 platforms) stores the address of the last item pushed on the stack. On x86 the stack grows "downward" from high to low address, so the "top" of the stack has a lower address than the "bottom" of the stack. The frame pointer (ebp or rbp on x86 and x86-64) stores the address of the current stack frame. Function arguments and local variables are referenced via offsets from the frame pointer. So if you write something like

a = 10;

the compiled machine code does something like

mov 0x0c,-0x04(%rpb) ;; write 10 to the location 4 bytes "below" the address 
                     ;; stored in rbp, or 0x7FFFFFF0

Function argument x would be 8 bytes "above" the frame pointer, or 0x08(%rbp) (0x7FFFFFFC).

There's no requirement that local variables be allocated in any specific order - the compiler is free to arrange the space for them however it sees fit. Similarly, there's no requirement that function arguments be pushed in any specific order (or that they be pushed on the stack at all), but almost all systems that use a stack for function arguments expect them to be pushed in reverse order.

Comments

0

Did you notice the details in the tag you used?

For questions about the call stack, use [callstack] or [stack-pointer] instead.


Only the call adresses are last-in-first-out. This is built-in in the CALL and RET instructions.

This return address stack can be extended to contain stack frames. The most direct way is to temporarily subtract the needed amount of bytes from the stack pointer. Here 32 bytes (0x20) for three long ints (x=5, y=7, z=11).

0000000000001143 <main>:
    1143:       48 83 ec 20             sub    rsp,0x20
    1147:       48 c7 44 24 08 05 00    mov    QWORD PTR [rsp+0x8],0x5
    1150:       48 c7 44 24 10 07 00    mov    QWORD PTR [rsp+0x10],0x7
    1159:       48 c7 44 24 18 0b 00    mov    QWORD PTR [rsp+0x18],0xb
    1162:       b8 00 00 00 00          mov    eax,0x0
    1167:       e8 ad ff ff ff          call   1119 <subsub>
    116c:       b8 00 00 00 00          mov    eax,0x0
    1171:       48 83 c4 20             add    rsp,0x20
    1175:       c3                      ret

The called subsub does no calls and can just access the unused part of the stack:

0000000000001119 <subsub>:
    1119:       c7 44 24 f4 2c 00 00    mov    DWORD PTR [rsp-0xc],0x2c
    1121:       c7 44 24 f8 bc 01 00    mov    DWORD PTR [rsp-0x8],0x1bc
    1129:       c7 44 24 fc 5c 11 00    mov    DWORD PTR [rsp-0x4],0x115c
    1132:       c3                      ret 

This is with -fomit-frame-pointer. Like that, rsp is used directly.


With frame pointer, there is one extra push and pop per call to save and restore rbp base pointer.

0000000000001119 <subsub>:
    1119:       55                      push   rbp
    111a:       48 89 e5                mov    rbp,rsp
    111d:       c7 45 f4 2c 00 00 00    mov    DWORD PTR [rbp-0xc],0x2c
    1124:       c7 45 f8 bc 01 00 00    mov    DWORD PTR [rbp-0x8],0x1bc
    112b:       c7 45 fc 5c 11 00 00    mov    DWORD PTR [rbp-0x4],0x115c
    1133:       5d                      pop    rbp
    1134:       c3                      ret  

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.