1

C Code:

void PtrArg1(int* a,int* b,int* c, int* d, int* e, int* f)
{
    return;
}

void PtrArg2(int* a,int* b,int* c, int* d, int* e, int* f, int* g, int* h)
{
    return;
}

Compiling with

gcc -c -m64 -o basics basics.c -O0

Running

objdump -d basics -M intel -r

then results in the following disassembly (Intel syntax):

000000000000000b <PtrArg1>:
   b:   f3 0f 1e fa             endbr64 
   f:   55                      push   rbp
  10:   48 89 e5                mov    rbp,rsp
  13:   48 89 7d f8             mov    QWORD PTR [rbp-0x8],rdi
  17:   48 89 75 f0             mov    QWORD PTR [rbp-0x10],rsi
  1b:   48 89 55 e8             mov    QWORD PTR [rbp-0x18],rdx
  1f:   48 89 4d e0             mov    QWORD PTR [rbp-0x20],rcx
  23:   4c 89 45 d8             mov    QWORD PTR [rbp-0x28],r8
  27:   4c 89 4d d0             mov    QWORD PTR [rbp-0x30],r9
  2b:   90                      nop
  2c:   5d                      pop    rbp
  2d:   c3                      ret    

000000000000002e <PtrArg2>:
  2e:   f3 0f 1e fa             endbr64 
  32:   55                      push   rbp
  33:   48 89 e5                mov    rbp,rsp
  36:   48 89 7d f8             mov    QWORD PTR [rbp-0x8],rdi
  3a:   48 89 75 f0             mov    QWORD PTR [rbp-0x10],rsi
  3e:   48 89 55 e8             mov    QWORD PTR [rbp-0x18],rdx
  42:   48 89 4d e0             mov    QWORD PTR [rbp-0x20],rcx
  46:   4c 89 45 d8             mov    QWORD PTR [rbp-0x28],r8
  4a:   4c 89 4d d0             mov    QWORD PTR [rbp-0x30],r9
  4e:   90                      nop
  4f:   5d                      pop    rbp
  50:   c3                      ret 

The number of arguments differs for PtrArg1 and PtrArg2, but the assembly instructions are the same for both. Why?

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
Sreeraj Chundayil
  • 5,548
  • 3
  • 29
  • 68

2 Answers2

3

This is due to the calling convention (System V AMD64 ABI, current version 1.0). The first six parameters are passed in integer registers, all others are pushed onto the stack.

After executing till location PtrArg2+0x4e, you get the following stack layout:

+----------+-----------------+
|  offset  |     content     |
+----------+-----------------+
| rbp-0x30 | f               |
| rbp-0x28 | e               |
| rbp-0x20 | d               |
| rbp-0x18 | c               |
| rbp-0x10 | b               |
| rbp-0x8  | a               |
| rbp+0x0  | saved rbp value |
| rbp+0x8  | return address  |
| rbp+0x10 | g               |
| rbp+0x18 | h               |
+----------+-----------------+

Since g and h are pushed by the caller, you get the same disassembly for both functions. For the caller

void Caller()
{
    PtrArg2(1, 2, 3, 4, 5, 6, 7, 8);
}

(I ommitted the necessary casts for clarity) we would get the following disassembly:

Caller():
    push    rbp
    mov     rbp, rsp
    push    8
    push    7
    mov     r9d, 6
    mov     r8d, 5
    mov     ecx, 4
    mov     edx, 3
    mov     esi, 2
    mov     edi, 1
    call    PtrArg2
    add     rsp, 16
    nop
    leave
    ret

(see compiler explorer)

The parameters h = 8 and g = 7 are pushed onto the stack, before calling PtrArg2.

janw
  • 8,758
  • 11
  • 40
  • 62
  • This is correct as far as it goes, but doesn't address some of the implicit assumptions the question seems to be making. e.g. that there should be any instructions at all for unused variables, or the fact that this is an un-optimized (debug mode) compile. I think your answer is helpful, but maybe only combined with my answer. (Or maybe I'm wrong and this would have been a sufficient answer for most people to clue them in the right direction.) – Peter Cordes Apr 25 '20 at 10:27
  • Why do we have add rsp, 16 in the instruction. Shouldn't pop rbp in PtrArg2 take care of reaching to the beginning of the Caller? – Sreeraj Chundayil Apr 25 '20 at 10:37
  • @PeterCordes I understood this question as merely related to the calling convention (i.e., why does the function only store a-f, but not g and h), since there was that explicit `-O0`. Instead of posting a long example which makes use of all parameters, OP chose the minimum example needed for their question. But this is only a guess, so I agree with your point, that this question needs some clarification. Expecting that all parameters are used in an empty function is indeed questionable. – janw Apr 25 '20 at 10:39
  • 1
    @InQusitive The `add rsp, 16` cleans up the stack after the call has completed, i.e., it removes `g` and `h` from the stack. The `pop rbp` only restores the previous value of `rbp` (as pushed at the beginning). – janw Apr 25 '20 at 10:40
  • 1
    @JanWichelmann: Ah, now I understand why you answered this way. I concluded that the OP was only looking at disassembly of the callees, not the callers, because that's all they showed in the question. And literally asked why the asm for those two functions were the same, without mentioning callers at all. – Peter Cordes Apr 25 '20 at 10:41
  • 1
    Maybe they got to this [MCVE] after looking at other stuff, but as asked it looks like gaps in understanding that your answer doesn't fill. (Not trying to crap on your answer, just provide feedback on answer-writing. I'm glad you posted it so I didn't feel the need to include any examples in mine :P) – Peter Cordes Apr 25 '20 at 10:44
  • @PeterCordes No worries! I always appreciate feedback :) Not the first time that I miss a possible interpretation of a question. I believe our answers complement each other well. – janw Apr 25 '20 at 10:53
1

Disappear? What did you expect the function to do with them that the compiler would emit asm instructions to implement?

You literally return; as the only statement in a void function so there's nothing the function needs to do other than ret. If you compile with a normal level of optimization like -O2, that's all you'll get. Debug-mode code is usually not interesting to look at, and is full of redundant / useless stuff. How to remove "noise" from GCC/clang assembly output?

The only reason you're seeing any instructions for some args is that you compiled in debug mode, i.e. the default optimization level of -O0, anti-optimized debug mode. Every C object (except register locals) has a memory address, and debug mode makes sure that the value is actually there in memory before/after every C statement. This means spilling register args to the stack on function entry. Why does clang produce inefficient asm with -O0 (for this simple floating point sum)?

The x86-64 System V ABI's calling convention passes the first 6 integer args in registers, the rest on the stack. The stack args already have memory addresses; the compiler doesn't emit code to copy them down next to other local vars below the return address; that would be pointless. The callee "owns" its own stack args, i.e. it can store new values to the stack space where the caller wrote the args, so that space can be the true address of args even if the function were to modify them.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847