12

I've been experimenting a little bit with functions and I've found that the order of arguments is reversed in memory. Why is that?

stack-test.cpp:

#include <stdio.h>

void test( int a, int b, int c ) {
    printf("%p %p %p\n", &a, &b, &c);
    printf("%d %d\n", *(&b - 1), *(&b + 1) );
}

int main() {
    test(1,2,3);
    return 0;
}

CLang:

$ clang++ stack-test.cpp && ./a.out
0x7fffb9bb816c 0x7fffb9bb8168 0x7fffb9bb8164
3 1

GCC:

$ g++ stack-test.cpp && ./a.out
0x7ffe0b983b3c 0x7ffe0b983b38 0x7ffe0b983b34
3 1

EDIT: Not duplicate: Order of evaluation can be different from memory layout, so it's a different question.

Alex
  • 163
  • 7
kravemir
  • 9,380
  • 15
  • 54
  • 99
  • possible duplicate of [Compilers and argument order of evaluation in C++](http://stackoverflow.com/questions/621542/compilers-and-argument-order-of-evaluation-in-c) – Steephen Jun 28 '15 at 14:00
  • 1
    @Steephen Order of evaluation, really? – kravemir Jun 28 '15 at 14:01
  • @Miro it is implementation specific and the order of arguments processing by compiler – Steephen Jun 28 '15 at 14:04
  • In case you are unaware, `*(&b-1)` causes undefined behaviour – M.M Jun 28 '15 at 14:16
  • @MattMcNabb I'm aware, that it would be valid if b was reference to integer in middle of the array (where I can go to previous element). But, as I stated in question - I'm experimenting. – kravemir Jun 28 '15 at 14:19

6 Answers6

9

This behaviour is implementation specific.

In your case, it's because the arguments are pushed on the stack. Here an interesting article which shows the typical memory layout of a process, which shows how the stack grows down. The first argument that is pushed on the stack will hence have the highest address.

Christophe
  • 54,708
  • 5
  • 52
  • 107
  • Thx, but that doesn't answer *why* :) It's obvious, that the behavior is implementation specific. However, I'm asking why is it reversed, why ain't the first argument first in memory? – kravemir Jun 28 '15 at 14:03
  • @Miro I've edited with a link that explains that the stack grows down (at least on windows and linux) – Christophe Jun 28 '15 at 14:04
9

The calling convention depends on the implementation.

But in order to support C variadic functions (in C++ expressed with ... ellipse in the formal argument list) arguments are usually pushed, or stack space is reserved for them, in order from right to left. This is usually called (1)C calling convention. With this convention, and the common convention that the machine stack grows downward in memory, the first argument should end up at lowest address, opposite of your result.

And when I compile your program with MinGW g++ 5.1, which is 64-bit, I get

000000000023FE30 000000000023FE38 000000000023FE40

And when I compile your program with 32-bit Visual C++ 2015, I get

00BFFC5C 00BFFC60 00BFFC64

And both of those results are consistent with the C calling convention, contrary to your result.

So the conclusion appears to be that your compiler defaults to something else than C calling convention, at least for non-variadic functions.

You might test this by adding a ... at the end of the formal argument list.


1) The C calling convention also includes that it's the caller that adjusts the stack pointer when the function returns, but that's irrelevant here.

Cheers and hth. - Alf
  • 135,616
  • 15
  • 192
  • 304
  • Taking the address of the arguments probably has something to do with it, as that means they can't be (solely) stored in registers – M.M Jun 28 '15 at 14:15
  • I've tried to test my stack growth direction using recursion, and it grows downward in memory. So, it's different calling convention than that which you have stated. Now, I'm gonna investigate calling conventions and why is it using different one :) – kravemir Jun 28 '15 at 14:44
  • It's really the fact that calling convention in x86-64 is by definition register-based (I'm assuming the machine isn't Arm64 or Sparc64, or Itanium, Alpha64, etc, as they are fairly rare). There is no such thing as a `C` calling conventions - it is up to the compiler designer to make something that works - although a very common one is indeed push things from right to left and caller adjusts it back, thre is NOTHING saying it has to be that way (as shown by for example x86-64) – Mats Petersson Jun 28 '15 at 15:10
  • Note also that if you don't take the address of `a` and `c`, they won't be on the stack at all... (well, at least not on the stack inside the callframe of that function, they would be on the stack inside main, of course) – Mats Petersson Jun 28 '15 at 15:12
  • @MatsPetersson: For x86-64, see the example in this answer, which is incompatible with your view. For some info about it, see (https://en.wikipedia.org/wiki/X86_calling_conventions#Microsoft_x64_calling_convention). Also what you say about "no such thing as C calling convention" is just nonsense, misleading noise, sorry. To more easily google it try `cdecl`. However, what you say about registers can be relevant. With X86-64 stack space is reserved even though the registers are used to pass arguments, but this is not so for all 64-bit conventions. And then one might get this effect. – Cheers and hth. - Alf Jun 28 '15 at 18:26
  • I beg to differ The C standard does not require any particular ordering of arguments, and at least in the Linux implementation, space is not allocated on the stack when the argument can be passed in a register. I have not looked at what MS specifies, but seems crazy to allocate space when many times it won't be needed (particularly in small functions, which is where it would have the biggest impact). I can also tell from the stack location in the OP's post that the OP is (most likely) using Linux, not Windows. So explaining how it "should work on Windows) is not even helpful. – Mats Petersson Jun 28 '15 at 18:50
  • Re Mats's claims. (1) I tend to trust Wikipedia's [table of x86-64 calling conventions](https://en.wikipedia.org/wiki/X86_calling_conventions#List_of_x86_calling_conventions), which lists "(C)" order of argument pushing for all three (the C is in reference to the general C calling convention that Mats thinks doesn't exist); (2) Mats' claim that the C calling convention has anything to do with the C standard is misleading nonsense, just rubbish; (3) Mads' apparent quote, introduced by a quote character, is not a quote, it's a misrepresentation. – Cheers and hth. - Alf Jun 28 '15 at 19:59
2

The C (and C++) standard doesn't define the order of arguments being passed, or how they should be organised in memory. It is up to the compiler developer (usually in cooperation with the OS developers) to come up with something that works on a particular processor architecture.

In MOST architectures, the stack (and registers) is used to pass arguments to a function, and again, for MOST architectures, the stack grows from "high to low" addresses, and in most C implementations, the order of arguments being passed are "left last", so if we have a function

 void test( int a, int b, int c )

then arguments are passed in the order:

c, b, a

to the function.

However, what complicates this is when the value of the arguments are passed in registers, and the code using the arguments is taking the address of those arguments - registers don't have addresses, so you can't take the address of a register variable. So the compiler will generate some code to store the address on the stack [from where we can get the address of the value] locally to the function. This is entirely up to the compiler's decision which order it does this, and I'm fairly sure this is what you are seeing.

If you we take your code and pass it through clang, we see:

define void @test(i32 %a, i32 %b, i32 %c) #0 {
entry:
  %a.addr = alloca i32, align 4
  %b.addr = alloca i32, align 4
  %c.addr = alloca i32, align 4
  store i32 %a, i32* %a.addr, align 4
  store i32 %b, i32* %b.addr, align 4
  store i32 %c, i32* %c.addr, align 4
  %call = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([10 x i8], [10 x i8]* @.str, i32 0, i32 0), i32* %a.addr, i32* %b.addr, i32* %c.addr)
  %add.ptr = getelementptr inbounds i32, i32* %b.addr, i64 -1
  %0 = load i32, i32* %add.ptr, align 4
  %add.ptr1 = getelementptr inbounds i32, i32* %b.addr, i64 1
  %1 = load i32, i32* %add.ptr1, align 4
  %call2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([7 x i8], [7 x i8]* @.str.1, i32 0, i32 0), i32 %0, i32 %1)
  ret void
}

Although it may not be ENTIRELY trivial to read, you can see the first few lines of the test-function is:

  %a.addr = alloca i32, align 4
  %b.addr = alloca i32, align 4
  %c.addr = alloca i32, align 4
  store i32 %a, i32* %a.addr, align 4
  store i32 %b, i32* %b.addr, align 4
  store i32 %c, i32* %c.addr, align 4

This is essentially creating space on the stack (%alloca) and storing the variables a, b, and c into those locations.

Even less easy to read is the assembler code that gcc generates, but you can see a similar thing happening here:

subq    $16, %rsp           ; <-- "alloca" for 4 integers.
movl    %edi, -4(%rbp)      ; Store a, b and c. 
movl    %esi, -8(%rbp)
movl    %edx, -12(%rbp)
leaq    -12(%rbp), %rcx     ; Take address of ... 
leaq    -8(%rbp), %rdx
leaq    -4(%rbp), %rax
movq    %rax, %rsi
movl    $.LC0, %edi
movl    $0, %eax
call    printf              ; Call printf.

You may wonder why it allocates space for 4 integers - that's because the stack should always be aligned to 16 bytes in x86-64.

Mats Petersson
  • 119,687
  • 13
  • 121
  • 204
1

C (and C++) code uses the processor stack to pass arguments to functions.

How the stack operates, depends on the processor. The stack can (theoretically) grow down- or upwards. So your processor is defining, if the addresses grow or shrink. Finally, not the processor architecture alone is responsible for that, but there are calling conventions for code running on an architecture.

The calling conventions say, how the arguments should be put on the stack for one specific processor architecture. The conventions are necessary, that libraries from different compilers can be linked together.

Basically, for you as C user it normally makes no difference, if the addresses of variables on the stack grow or shrink.

Details:

Juergen
  • 11,315
  • 7
  • 34
  • 53
  • C does no such thing. Some implementations *may* do so, but it is not required by the language. For example, some processors have very large register sets to facilitate argument passing. – nobody Jun 28 '15 at 14:11
  • @AndrewMedico: What I meant, there are calling conventions for architectures, that regard also C code. The C standard thus not define such -- but it is necessary for an architecture to define such standards that different compiler output can be linked together. See my link. I clarified my sentence. – Juergen Jun 28 '15 at 14:14
  • This is indeed not defined by architecture. It's defined by ABI, and there can be multiple ABI for one architecture, and the processor has no idea on ABI, it's just a contract for programmers & compilers. – Non-maskable Interrupt Jun 28 '15 at 14:45
  • @Calvin: It is always possible, to find something wrong or unclear, when it comes to technical details. I separated between architecture and calling conventions, but it seems not clear enough. I also did not want to write a Wikipedia entry -- therefore exists the Wikipedia. I also linked to Wikipedia where all the details are described. – Juergen Jun 28 '15 at 14:57
1

The ABI defines how to pass parameters.

In your example, it's slightly complicated since the x86_64 ABI default by gcc and clang passes parameters on registers(*), there was no address for them.

Then you reference the parameters, so the compiler is forced to allocate local storage for those variables, and that ordering and memory layout is also implementation-specific.

  • Note: up to 6 trivial parameters, if there are more, it pass on stack.
  • Reference: x86_64 ABI
Non-maskable Interrupt
  • 3,641
  • 1
  • 17
  • 26
0

Speaking about 32bit x86 Windows

Short answer: pointer to the function arguments is not necessary pointer to the stack where were pushed on actual function call, but can be anywhere the compiler relocated the variable.

Long answer: Encountered the same issue while converting my code from bcc32 (Embarcadero classic compiler) to CLANG. RPC code generated by MIDL compiler was broken because RPC function arguments translator serialized arguments by taking the pointer to first function argument, assuming all following arguments will follow like e.g. Serialize(&a).

Debugged cdecl function calls generated by both BCC32 and CLANG:

  • BCC32: function arguments are passed in the correct order on the stack, then when the argument address is required, the stack address is given directly.

  • CLANG: function arguments are passed in the correct order on the stack, however in the actual function, a copy of all arguments is made in memory in reverse order of stack, and when function argument address is required, the memory address is given directly, resulting in reverted order.

Otherwise said, don't assume how function arguments are disposed in memory from within the function C/C++ code. Its compiler dependent.

In my case, a possible solution is to declare RPC functions with pascal calling convention (Win32), forcing MIDL compiler to parse arguments individually. Unfortunately MIDL generated code is heavy and bad code needing lots of tweaking to compile, still not done)

Niki
  • 412
  • 4
  • 8