15

It seems state-of-art compilers treat arguments passed by stack as read-only. Note that in the x86 calling convention, the caller pushes arguments onto the stack and the callee uses the arguments in the stack. For example, the following C code:

extern int goo(int *x);
int foo(int x, int y) {
  goo(&x);
  return x;
}

is compiled by clang -O3 -c g.c -S -m32 in OS X 10.10 into:

    .section    __TEXT,__text,regular,pure_instructions
    .macosx_version_min 10, 10
    .globl  _foo
    .align  4, 0x90
_foo:                                   ## @foo
## BB#0:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $8, %esp
    movl    8(%ebp), %eax
    movl    %eax, -4(%ebp)
    leal    -4(%ebp), %eax
    movl    %eax, (%esp)
    calll   _goo
    movl    -4(%ebp), %eax
    addl    $8, %esp
    popl    %ebp
    retl


.subsections_via_symbols

Here, the parameter x(8(%ebp)) is first loaded into %eax; and then stored in -4(%ebp); and the address -4(%ebp) is stored in %eax; and %eax is passed to the function goo.

I wonder why Clang generates code that copy the value stored in 8(%ebp) to -4(%ebp), rather than just passing the address 8(%ebp) to the function goo. It would save memory operations and result in a better performance. I observed a similar behaviour in GCC too (under OS X). To be more specific, I wonder why compilers do not generate:

  .section  __TEXT,__text,regular,pure_instructions
    .macosx_version_min 10, 10
    .globl  _foo
    .align  4, 0x90
_foo:                                   ## @foo
## BB#0:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $8, %esp
    leal    8(%ebp), %eax
    movl    %eax, (%esp)
    calll   _goo
    movl    8(%ebp), %eax
    addl    $8, %esp
    popl    %ebp
    retl


.subsections_via_symbols

I searched for documents if the x86 calling convention demands the passed arguments to be read-only, but I couldn't find anything on the issue. Does anybody have any thought on this issue?

Jongware
  • 22,200
  • 8
  • 54
  • 100
Jeehoon Kang
  • 311
  • 1
  • 13
  • 1
    You have a good point here! `8(%ebp)` is in the caller's stack frame, but it is space which was allocated specifically for passing arguments to `foo`. Will the caller use that space for its own purposes *after* `foo` returns, rather than just destroying it by adjusting the stack pointer? If so, copying the value into `foo`'s stack frame is necessary. If not, it might be safe for `foo` to "borrow" the space in the caller's stack frame rather than copying. So, to know whether your idea is good or not, you need to see what the code for `foo`'s *caller* looks like. – Alex D May 17 '15 at 09:03
  • @AlexD Thank you for your comment! Since `foo` can be called by an arbitrary function, I think this is a question on calling conventions, rather than on specific context where `foo` is called. – Jeehoon Kang May 17 '15 at 09:11
  • 1
    This is an interesting question. I found [this other question](http://stackoverflow.com/questions/1684682/c-calling-conventions-and-passed-arguments?rq=1) which claims that gcc -O2 actually did modify the caller pushed stack argument. – JS1 May 17 '15 at 09:20
  • @JS1 Thank you for finding out the question I looked for! I believe the general consensus there is: the callee can change the arguments; or the calling convention does not specifically restrict that behaviour. Thank you! – Jeehoon Kang May 17 '15 at 09:28
  • 1
    At a guess, clang constructs an SSA value for the value of argument `x`, initialises it with a move from wherever the argument location is, and then neglects to track the information that `x` is already in memory. When it comes to `&x` the SSA value needs to be in memory, so it is assigned a new stack location (`-4(%ebp)`). Looks rather dumb here, but would be the right thing to do if `x` were a non-argument variable or an arg passed in a register. – gsg May 17 '15 at 13:55

3 Answers3

13

The rules for C are that parameters must be passed by value. A compiler converts from one language (with one set of rules) to a different language (potentially with a completely different set of rules). The only limitation is that the behaviour remains the same. The rules of the C language do not apply to the target language (e.g. assembly).

What this means is that if a compiler feels like generating assembly language where parameters are passed by reference and are not passed by value; then this is perfectly legal (as long as the behaviour remains the same).

The real limitation has nothing to do with C at all. The real limitation is linking. So that different object files can be linked together, standards are needed to ensure that whatever the caller in one object file expects matches whatever the callee in another object file provides. This is what's known as the ABI. In some cases (e.g. 64-bit 80x86) there are multiple different ABIs for the exact same architecture.

You can even invent your own ABI that's radically different (and implement your own tools that support your own radically different ABI) and that's perfectly legal as far as the C standards go; even if your ABI requires "pass by reference" for everything (as long as the behaviour remains the same).

Brendan
  • 35,656
  • 2
  • 39
  • 66
7

Actually, I just compiled this function using GCC:

int foo(int x)
{
    goo(&x);
    return x;
}

And it generated this code:

_foo:
        pushl       %ebp
        movl        %esp, %ebp
        subl        $24, %esp
        leal        8(%ebp), %eax
        movl        %eax, (%esp)
        call        _goo
        movl        8(%ebp), %eax
        leave
        ret

This is using GCC 4.9.2 (on 32-bit cygwin if it matters), no optimizations. So in fact, GCC did exactly what you thought it should do and used the argument directly from where the caller pushed it on the stack.

JS1
  • 4,745
  • 1
  • 14
  • 19
5

The C programming language mandates that arguments are passed by value. So any modification of an argument (like an x++; as the first statement of your foo) is local to the function and does not propagate to the caller.

Hence, a general calling convention should require copying of arguments at every call site. Calling conventions should be general enough for unknown calls, e.g. thru a function pointer!

Of course, if you pass an address to some memory zone, the called function is free to dereference that pointer, e.g. as in

int goo(int *x) {
    static int count;
    *x = count++;
    return count % 3;
}

BTW, you might use link-time optimizations (by compiling and linking with clang -flto -O2 or gcc -flto -O2) to perhaps enable the compiler to improve or inline some calls between translation units.

Notice that both Clang/LLVM and GCC are free software compilers. Feel free to propose an improvement patch to them if you want to (but since both are very complex pieces of software, you'll need to work some months to make that patch).

NB. When looking into produced assembly code, pass -fverbose-asm to your compiler!

Basile Starynkevitch
  • 223,805
  • 18
  • 296
  • 547
  • Since the space for the arguments are allocated by decrementing `%esp`, the space for the arguments are disjoint from caller's stackframe. So I think modifying the arguments in the stack does not affect the caller's stackframe. – Jeehoon Kang May 17 '15 at 08:44
  • I am not sure to follow your thinking (it looks like you are breaking the call-by-value requirement). But feel free to propose a patch to [Clang/LLVM](http://clang.llvm.org/) or to [GCC](http://gcc.gnu.org/) if you want to. That would take you months of work. – Basile Starynkevitch May 17 '15 at 08:46
  • Great, so dive into the compiler internals and start your patch. You'll need a few months of work. – Basile Starynkevitch May 17 '15 at 08:52
  • Sorry for disruptions by commenting on your answers. I edited my question to be more specific. I am rather interested in the reason Clang/GCC behave in this way. (In fact, I am improving CompCert, a verified C compiler, w.r.t. linking.) Thank you for your answers! – Jeehoon Kang May 17 '15 at 08:55
  • Ask Xavier Leroy about CompCert. I know him well enough, he'll be delighted to answer! But study the C11 or C99 standard (and what it says about call by value argument passing) before bothering him. – Basile Starynkevitch May 17 '15 at 08:55
  • 2
    @BasileStarynkevitch The value pushed on the stack by the calling function is already a copy of the value. The OP's question is why doesn't the callee use that copy directly? Instead, the callee makes **another** copy. – JS1 May 17 '15 at 08:57