Search code examples
cassemblystackinstruction-set

what's the purpose of pushing address of local variables on the stack(assembly)


Let's there is a function:

int caller()
{
   int arg1 = 1;
   int arg2 = 2
   int a = test(&arg1, &arg2)
}
test(int *a, int *b)
{
    ...
}

so I don't understand why &arg1 and &arg2 have to be pushed on the stack too like this

enter image description here

I can understand that we can get address of arg1 and arg2 in the callee by using

movl  8(%ebp), %edx
movl  12(%ebp), %ecx

but if we don't push these two on the stack, we can also can their address by using:

leal 8(%ebp), %edx
leal 12(%ebp), %ecx 

so why bother pushing &arg1 and &arg2 on the stack?


Solution

  • In the general case, test has to work when you pass it arbitrary pointers, including to extern int global_var or whatever. Then main has to call it according to the ABI / calling convention.

    So the asm definition of test can't assume anything about where int *a points, e.g. that it points into its caller's stack frame.

    (Or you could look at that as optimizing away the addresses in a call-by-reference on locals, so the caller must place the pointed-to objects in the arg-passing slots, and on return those 2 dwords of stack memory hold the potentially-updated values of *a and *b.)

    You compiled with optimization disabled. Especially for the special case where the caller is passing pointers to locals, the solution to this problem is to inline the whole function, which compilers will do when optimization is enabled.

    Compilers are allowed to make a private clone of test that takes its args by value, or in registers, or with whatever custom calling convention the compiler wants to use. Most compilers don't actually do this, though, and rely on inlining instead of custom calling conventions for private functions to get rid of arg-passing overhead.

    Or if it had been declared static test, then the compiler would already know it was private and could in theory use whatever custom calling convention it wanted without making a clone with a name like test.clone1234. gcc does sometimes actually do that for constant-propagation, e.g. if the caller passes a compile-time constant but gcc chooses not to inline. (Or can't because you used __attribute__((noinline)) static test() {})


    And BTW, with a good register-args calling convention like x86-64 System V, the caller would do lea 12(%rsp), %rdi / lea 8(%rsp), %rsi / call test or something. The i386 System V calling convention is old and inefficient, passing everything on the stack forcing a store/reload.

    You have basically identified one of the reasons that stack-args calling conventions have higher overhead and generally suck.