How C structures get passed to function in assembly?
Asked Answered
B

3

4

1)How C structures get passed to function in assembly. I mean pass by value, not pass by reference. 2)By the way, how callees return structure to its callers? I'm so sorry for the poor expression since I'm not a native English speaker.

I wrote a simple program to testify how C structures get passed to function. But the result was quite surpirsed. Some value was passed by register, but some value was passed by pushing them into stack. Here is the code.

source code

#include <stdio.h>

typedef struct {
        int age;
        enum {Man, Woman} gen;
        double height;
        int class;
        char *name;
} student;

void print_student_info(student s) {
        printf("age: %d, gen: %s, height: %f, name: %s\n", 
                        s.age,
                        s.gen == Man? "Man":"Woman",
                        s.height, s.name);
}

int main() {
        student s;
        s.age = 10;
        s.gen = Man;
        s.height = 1.30;
        s.class = 3;
        s.name = "Tom";
        print_student_info(s);
        return 0;
}

asm

 6fa:   55                      push   %rbp
 6fb:   48 89 e5                mov    %rsp,%rbp
 6fe:   48 83 ec 20             sub    $0x20,%rsp
 702:   c7 45 e0 0a 00 00 00    movl   $0xa,-0x20(%rbp)
 709:   c7 45 e4 00 00 00 00    movl   $0x0,-0x1c(%rbp)
 710:   f2 0f 10 05 00 01 00    movsd  0x100(%rip),%xmm0        # 818 <_IO_stdin_used+0x48>
 717:   00 
 718:   f2 0f 11 45 e8          movsd  %xmm0,-0x18(%rbp)
 71d:   c7 45 f0 03 00 00 00    movl   $0x3,-0x10(%rbp)
 724:   48 8d 05 e5 00 00 00    lea    0xe5(%rip),%rax        # 810 <_IO_stdin_used+0x40>
 72b:   48 89 45 f8             mov    %rax,-0x8(%rbp)
 72f:   ff 75 f8                pushq  -0x8(%rbp)
 732:   ff 75 f0                pushq  -0x10(%rbp)
 735:   ff 75 e8                pushq  -0x18(%rbp)
 738:   ff 75 e0                pushq  -0x20(%rbp)
 73b:   e8 70 ff ff ff          callq  6b0 <print_student_info>
 740:   48 83 c4 20             add    $0x20,%rsp
 744:   b8 00 00 00 00          mov    $0x0,%eax
 749:   c9                      leaveq 
 74a:   c3                      retq   
 74b:   0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)   

I expected structure was passed to function using the stack, but the code above showed it wasn't.

Bankhead answered 3/9, 2019 at 7:13 Comment(7)
Generally, once compiled structures don't really exist anymore. What exists is an area of memory, and offsets into that memory for the members of the structure. And without packing (omitting padding between members) the only offset you can be sure of is for the first member which will always have offset 0. How the structures are passed to functions is an implementation detail that can differ between compilers.Juxtaposition
Forgive me if I'm being dumb (long time since I worked in assembly) - but what are the 4 pushq instructions doing, if not using the stack?Moonmoonbeam
@Adrian I was wondering the same thing as well. I've seen structures passed around in registers in ARM, but those were generally much smaller ones, up to 8 bytes max. Here it definitely looks to me like the struct is being passed using the stack, but I could be mistaken!Ette
The calling convention is specified by the ABI for the specific system, it should mention structs. Not very meaningful to discuss without a specific CPU and compiler in mind. In addition, passing structs by value is considered very bad practice in the first place.Settles
Is it safe to assume that this is GCC targeting something like Linux/MacOS/BSD (and not Windows)? If so you may wish to update the tags and your question with that information.Reluctance
@Settles : I agree the question could use some additional details. The code is clearly x86-64 CPU. The code can't be MS ABI without a shadow space so that pretty much leaves the x86-64 System V ABI. Since there really isn't any OS specific code being generated it is rather generic so the actual OS really doesn't matter (just the ABI needs to be specified). One can produce the identical (appears that way) code on godbolt: godbolt.org/z/mp8kKq using GCC.Reluctance
@MichaelPetch Yep. As per tag usage for the assembly tag, we should always add a tag for the target ISA.Settles
R
14

As has been pointed out by others - passing structures by value is generally frowned upon in most cases, but it is allowable by the C language nonetheless. I'll discuss the code you did use even though it isn't how I would have done it.


How structures are passed is dependent on the ABI / Calling convention. There are two primary 64-bit ABIs in use today (there may be others). The 64-bit Microsoft ABI and the x86-64 System V ABI. The 64-bit Microsoft ABI is simple as all structures passed by value are on the stack. In The x86-64 System V ABI (used by Linux/MacOS/BSD) is more complex as there is a recursive algorithm that is used to determine if a structure can be passed in a combination of general purpose registers / vector registers / X87 FPU stack registers. If it determines the structure can be passed in registers then the object isn't placed on the stack for the purpose of calling a function. If it doesn't fit in registers per the rules then it is passed in memory on the stack.

There is a telltale sign that your code isn't using the 64-bit Microsoft ABI as 32 bytes of shadow space weren't reserved by the compiler before making the function call so this is almost certainly a compiler targeting the x86-64 System V ABI. I can generate the same assembly code in your question using the online godbolt compiler with the GCC compiler with optimizations disabled.

Going through the algorithm for passing aggregate types (like structures and unions) is beyond the scope of this answer but you can refer to section 3.2.3 Parameter Passing, but I can say that this structure is passed on the stack because of a post cleanup rule that says:

If the size of the aggregate exceeds two eightbytes and the first eightbyte isn’t SSE or any other eightbyte isn’t SSEUP, the whole argument is passed in memory.

It happens to be that your structure would have attempted to have the first two 32-bit int values packed in a 64-bit register and the double placed in a vector register followed by the int being placed in a 64-bit register (because of alignment rules) and the pointer passed in another 64-bit register. Your structure would have exceeded two eightbyte (64-bit) registers and the first eightbyte (64-bit) register isn't an SSE register so the structure is passed on the stack by the compiler.

You have unoptimized code but we can break down the code into chunks. First is building the stack frame and allocating room for the local variable(s). Without optimizations enabled (which is the case here), the structure variable s will be built on the stack and then a copy of that structure will be pushed onto the stack to make the call to print_student_info.

This builds the stackframe and allocates 32 bytes (0x20) for local variables (and maintains 16-byte alignment). Your structure happens to be exactly 32 bytes in size in this case following natural alignment rules:

 6fa:   55                      push   %rbp
 6fb:   48 89 e5                mov    %rsp,%rbp
 6fe:   48 83 ec 20             sub    $0x20,%rsp

Your variable s will start at RBP-0x20 and ends at RBP-0x01 (inclusive). The code builds and initializes the s variable (student struct) on the stack. A 32-bit int 0xa (10) for the age field is placed at the beginning of the structure at RBP-0x20. The 32-bit enum for Man is placed in field gen at RBP-0x1c:

 702:   c7 45 e0 0a 00 00 00    movl   $0xa,-0x20(%rbp)
 709:   c7 45 e4 00 00 00 00    movl   $0x0,-0x1c(%rbp)

The constant value 1.30 (type double) is stored in memory by the compiler. You can't move from memory to memory with one instruction on Intel x86 processors so the compiler moved the double value 1.30 from memory location RIP+0x100 to vector register XMM0 then moved the lower 64-bits of XMM0 to the height field on the stack at RBP-0x18:

 710:   f2 0f 10 05 00 01 00    movsd  0x100(%rip),%xmm0        # 818 <_IO_stdin_used+0x48>
 717:   00 
 718:   f2 0f 11 45 e8          movsd  %xmm0,-0x18(%rbp)

The value 3 is placed on the stack for the class field at RBP-0x10:

 71d:   c7 45 f0 03 00 00 00    movl   $0x3,-0x10(%rbp)

Lastly the 64-bit address of the string Tom (in the read only data section of the program) is loaded into RAX and then finally moved into the name field on the stack at RBP-0x08. Although the type for class was only 32-bits (an int type) it was padded to 8 bytes because the following field name has to be naturally aligned on an 8 byte boundary since a pointer is 8 bytes in size.

 724:   48 8d 05 e5 00 00 00    lea    0xe5(%rip),%rax        # 810 <_IO_stdin_used+0x40>
 72b:   48 89 45 f8             mov    %rax,-0x8(%rbp)

At this point we have a structure entirely built on the stack. The compiler then copies it by pushing all 32 bytes (using 4 64-bit pushes) of the structure onto the stack to make the function call:

 72f:   ff 75 f8                pushq  -0x8(%rbp)
 732:   ff 75 f0                pushq  -0x10(%rbp)
 735:   ff 75 e8                pushq  -0x18(%rbp)
 738:   ff 75 e0                pushq  -0x20(%rbp)
 73b:   e8 70 ff ff ff          callq  6b0 <print_student_info>

Then typical stack cleanup and function epilogue:

 740:   48 83 c4 20             add    $0x20,%rsp
 744:   b8 00 00 00 00          mov    $0x0,%eax
 749:   c9                      leaveq 

Important Note: The registers used were not for the purpose of passing parameters in this case, but were part of the code that initialized the s variable (struct) on the stack.


Returning Structures

This is dependent on the ABI as well, but I'll focus on the x86-64 System V ABI in this case since that is what your code is using.

By Reference: A pointer to a structure is returned in RAX. Returning pointers to structures is preferred.

By value: A structure in C that is returned by value forces the compiler to allocate additional space for the return structure in the caller and then the address of that structure is passed as a hidden first parameter in RDI to the function. The called function will place the address that was passed in RDI as a parameter into RAX as the return value when it is finished. Upon return from the function the value in RAX is a pointer to the address where the return structure is stored which is always the same address passed in the hidden first parameter RDI. The ABI discusses this in section 3.2.3 Parameter Passing under the subheading Returning of Values which says:

  1. If the type has class MEMORY, then the caller provides space for the return value and passes the address of this storage in %rdi as if it were the first argument to the function. In effect, this address becomes a “hidden” first argument. This storage must not overlap any data visible to the callee through other names than this argument. On return %rax will contain the address that has been passed in by the caller in %rdi.
Reluctance answered 3/9, 2019 at 12:38 Comment(3)
Thanks a lot. Your answer really solve my confusionBankhead
@Bankhead No problem!Reluctance
On Windows, structs and unions of 8, 16, 32, and 64 bits can be passed in registersHands
J
6

It depends on the ABI for your system. On x86_64, most systems use SYSV ABI fo AMD64 -- the exception being Microsoft, who use their own non-standard ABI.

On either of those ABIs, this structure will be passed on the stack, which is what is happening in the code -- first s is constructed in main's stack frame, then a copy of it is pushed on the stack (the 4 pushq instructions).

Joaniejoann answered 3/9, 2019 at 7:39 Comment(1)
Your answer says “this structure will be passed on the stack,” but I suggest you emphasize this structure, since the question asks about structures generally. Others, notably small structures, may be passed in registers.The SYSV document you linked to discusses this in pages 18-20. Additionally, the question asks about returning structures, which this answer does not cover.Schulman
U
2

There's no general answer to your question - every compiler works differently and can do things differently according to what optimisations you select. What you've observed is a common optimisation - the first few parameters of suitable types are passed in registers, with extra and/or complex ones passed on the stack.

Unleash answered 3/9, 2019 at 7:18 Comment(1)
This code is unoptimized. The registers used are to build the s variable (student struct) on the stack. In the case of this code no registers are used to pass parameters at all. The entire structure created on the stack is then copied onto the stack to be passed as a parameter to the function.Reluctance

© 2022 - 2024 — McMap. All rights reserved.