2
$\begingroup$

Several years ago, I put together this little memory address exploration for my students to help them to understand pointers, arrays, the stack, and the heap.

I just compiled and ran it in a new environment (gcc on a AWS Linux server) and the order of the parameters for foo are different from what I would expect. The local function variables (d1 and e1) now have a higher address in comparison to the function parameters (a1, b1, and c1).

The addresses for the parameters / variables in function foo are listing as:

&a1: fa2740fc
&b1: fa2740f0
&c1: fa2740e8
&d1: fa27410c
&e1: fa274100

Any thoughts on why variables d1 and e1 have higher addresses than a1, b1, and c1?

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

int* foo (int a1, int b1[], int *c1)
{
  int d1 = *c1 + a1;
  int *e1;
  
  e1 = malloc (sizeof(int));

  printf ("5) Addresses of arguments/variables of foo:\n");
  printf ("   Update your stack sketch.\n");
  printf ("     &a1: %x\n", &a1);
  printf ("     &b1: %x\n", &b1);
  printf ("     &c1: %x\n", &c1);
  printf ("     &d1: %x\n", &d1);
  printf ("     &e1: %x\n\n", &e1);

  printf ("6) Values of arguments/variables in foo:\n");
  printf ("   Include these on your stack sketch as well.\n");
  printf ("     a1: %x\n", a1);
  printf ("     b1: %x\n", b1);
  printf ("     c1: %x\n", c1);
  printf ("     d1: %x\n", d1);
  printf ("     e1: %08x\n\n", e1);

  printf ("7) *c1 == %x, why?  Explain using your stack drawing.\n\n", *c1); 
  
  printf ("8) e1 is a reference to an integer, much like c1.  Why is e1 so\n ");
  printf ("   different in value?\n\n");

  return e1;
}


int main ()
{
  int a = 5;
  int b[] = {8, 14, -7, 128, 12};
  int c = 10;
  int d = 14;

  printf ("1) Locations...\n");
  printf ("   Use these locations to sketch the stack.\n");
  printf ("     &a: %x\n", &a);
  printf ("     &b: %x\n", &b);
  printf ("     &c: %x\n", &c);
  printf ("     &d: %x\n\n", &d);
  
  printf ("2a) Values:\n");
  printf ("   Why does b != 8?\n");
  printf ("     a: %x\n", a);
  printf ("     b: %x\n", b);
  printf ("     c: %x\n", c);
  printf ("     d: %x\n\n", d);

  printf ("2b) Values:\n");
  printf ("   What memory address is *b accessing?\n");
  printf ("     *b: %x\n\n", *b);
  
  printf ("3) Notice that the following increase by 4 each, why?\n");
  printf ("     &(b[0]): %x\n", &(b[0]));
  printf ("     &(b[1]): %x\n", &(b[1]));
  printf ("     &(b[2]): %x\n", &(b[2]));
  printf ("     &(b[3]): %x\n\n", &(b[3]));

  printf ("4) Pointers can be added, but the addition might have interesting results.\n");
  printf ("   Explain why b + 1 != b + 1 in the normal way of thinking about addition.\n");
  printf ("     b:   %x\n", b);
  printf ("     b+1: %x\n", b+1);
  printf ("     b+2: %x\n", b+2);
  printf ("     b+3: %x\n\n", b+3);
  
  foo (a, b, &c);
}
$\endgroup$
4
  • $\begingroup$ An interesting experiment. I have no answers, and merely a guess that it's a compiler optimization of some kind. Rather I have a suggestion for how to get a good answer. I'm not sure of the reception there, but you're more likely to get expert insight by asking the question on StackOverflow, where you already have an account anyway. $\endgroup$ – Gypsy Spellweaver Feb 4 at 16:06
  • $\begingroup$ local variables d1 and e? You said d and e, but d appears to be in main. $\endgroup$ – Ben I. Feb 4 at 16:45
  • $\begingroup$ Thank you Ben, I updated the variable names. $\endgroup$ – Bryan R Feb 4 at 17:08
  • $\begingroup$ FYI: In an abstract sense, the call stack is a stack of data objects known as activation records (a.k.a., "activation frames," "stack frames," etc.) Each one holds the state of one "activation" (a.k.a., "call to") a function. Activation records can take many forms. If I remember correctly, in the Self programming language, Activation records actually were allocated from the heap, and they were first-class objects that could be manipulated just like any other object. $\endgroup$ – Solomon Slow Feb 5 at 15:21
5
$\begingroup$

I haven't delved into the details, but if a language doesn't define its own implementation (few do) then the implementation will vary depending on the architecture for which it is compiled. It will also vary over time as research provides hints and clues about efficiency (both compile time and run time efficiency).

So, one stack protocol might be appropriate for one architecture, but wildly inefficient on another, depending on the registers available for the stack as well as, perhaps, paging systems, and all the rest. So, the implementer will take advantage of whatever hardware is available on the machine of interest.

The normally thought of "normal" architecture (register-transfer) disappeared in the 60's or so. It is fun to think about and to simulate, but not especially realistic. C was designed originally for the PDP-7 architecture, which is pretty much obsolete.

Also, if a compiler uses some top-down techniques derived from LL processing as well as more "normal for C" bottom up processing, the parameters, in particular might wind up in a different order. For example, a compiler might use an intermediate language that is itself compiled and who can say what the characteristics of that intermediate language might be.

It doesn't affect your question, I think, but heap implementations might differ wildly between implementations.

$\endgroup$
3
$\begingroup$

As Gypsy Spellweaver suggested, I posted the question to StackOverflow. It turns out the first 8 int / pointer parameters can get passed via registers rather than via the stack. Printf arguments not passed on the stack

$\endgroup$
1
  • $\begingroup$ That is the calling convention on x86-64. Others may differ. The "take the variable's address" forces the compiler to give it a place in memory so there is an address to take. A truly smart compiler may even figure out nothing useful is done with the variable through the pointer, and put each in turn at the very same spot. $\endgroup$ – vonbrand May 9 at 23:08
1
$\begingroup$

Why not. It is totally arbitrary. There may be no reason. It has to be one way, it is useful to be consistent. But there are many many orders even c1, b1, e1, e1, a1, are any other order. The C standard makes no guarantee. However it does say that it a lot of circumstances you can get a pointer to a variable. It says that you can use a pointer to traverse an array. However it says that it is undefined what is beyond the variable or array. You can not know the relative address of one variable from another, as the offset is undefined. I think relative position within structures is implementation defined.

Undefined means that it can change (change each time it is compiled).

One reason that it may change, is a system called "address space randomisation". It is a security measure, to make it harder to do a buffer overrun (and similar) attack, on buggy software.

$\endgroup$
2
  • 1
    $\begingroup$ A randomizer is less likely with a stack, though. There was once an implementation of (I think) BASIC that used a linked list for the stack. $\endgroup$ – Buffy Feb 4 at 20:28
  • 1
    $\begingroup$ Not that random. The stack from will still have to be in the same place, but the order of items in the frame can be randomised.Items in the stack of accessed with register offset addressing, so any change in order has to be matched with changes to the offsets in the assembler code. There is no run time cost in doing this, only compile time cost. All the variable mentioned (arguments and locals) are usually in the stack frame. $\endgroup$ – ctrl-alt-delor Feb 4 at 20:35
1
$\begingroup$

Depends on your machine (some ABIs decree the first few arguments -- of the right types -- are passed in registers, others say all go to the stack; but then again, some environments just diverge from what the machine's designer defined), on your compiler's whim (unless it has to match up with code written by others, it's up to it). A smart compiler may even decide that the function can be inlined, and thus make it dissappear completely.

For one example of complex calling conventions, take a peek at Jorgensen's "x86-64 assembly language programming with Ubuntu", section 12.8 (page 171 et seq). And when you mix in varargs is when the real fun starts.

Take a peek at e.g. the C standard: It is very careful to make clear that the order in which arguments are computed and stored is not defined by the language.

In your experiment the variables are required to be copied into memory, because you take their addresses. In which order they are placed in memory by the compiler is not fixed by the language. The compiler might even pick the same location for some of them if it can prove that there is no use through the pointer before placing the next one there.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.