Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does assembly do parameter passing: by value, reference, pointer for different types/arrays?

In attempt to look at this, I wrote this simple code where I just created variables of different types and passed them into a function by value, by reference, and by pointer:

int i = 1;
char c = 'a';
int* p = &i;
float f = 1.1;
TestClass tc; // has 2 private data members: int i = 1 and int j = 2

the function bodies were left blank because i am just looking at how parameters are passed in.

passByValue(i, c, p, f, tc); 
passByReference(i, c, p, f, tc); 
passByPointer(&i, &c, &p, &f, &tc);

wanted to see how this is different for an array and also how the parameters are then accessed.

int numbers[] = {1, 2, 3};
passArray(numbers); 

assembly:

passByValue(i, c, p, f, tc)

mov EAX, DWORD PTR [EBP - 16]
    mov DL, BYTE PTR [EBP - 17]
    mov ECX, DWORD PTR [EBP - 24]
    movss   XMM0, DWORD PTR [EBP - 28]
    mov ESI, DWORD PTR [EBP - 40]
    mov DWORD PTR [EBP - 48], ESI
    mov ESI, DWORD PTR [EBP - 36]
    mov DWORD PTR [EBP - 44], ESI
    lea ESI, DWORD PTR [EBP - 48]
    mov DWORD PTR [ESP], EAX
    movsx   EAX, DL
    mov DWORD PTR [ESP + 4], EAX
    mov DWORD PTR [ESP + 8], ECX
    movss   DWORD PTR [ESP + 12], XMM0
    mov EAX, DWORD PTR [ESI]
    mov DWORD PTR [ESP + 16], EAX
    mov EAX, DWORD PTR [ESI + 4]
    mov DWORD PTR [ESP + 20], EAX
    call    _Z11passByValueicPif9TestClass


passByReference(i, c, p, f, tc)

    lea EAX, DWORD PTR [EBP - 16]
    lea ECX, DWORD PTR [EBP - 17]
    lea ESI, DWORD PTR [EBP - 24]
    lea EDI, DWORD PTR [EBP - 28]
    lea EBX, DWORD PTR [EBP - 40]
    mov DWORD PTR [ESP], EAX
    mov DWORD PTR [ESP + 4], ECX
    mov DWORD PTR [ESP + 8], ESI
    mov DWORD PTR [ESP + 12], EDI
    mov DWORD PTR [ESP + 16], EBX
    call    _Z15passByReferenceRiRcRPiRfR9TestClass

passByPointer(&i, &c, &p, &f, &tc)

    lea EAX, DWORD PTR [EBP - 16]
    lea ECX, DWORD PTR [EBP - 17]
    lea ESI, DWORD PTR [EBP - 24]
    lea EDI, DWORD PTR [EBP - 28]
    lea EBX, DWORD PTR [EBP - 40]
    mov DWORD PTR [ESP], EAX
    mov DWORD PTR [ESP + 4], ECX
    mov DWORD PTR [ESP + 8], ESI
    mov DWORD PTR [ESP + 12], EDI
    mov DWORD PTR [ESP + 16], EBX
    call    _Z13passByPointerPiPcPS_PfP9TestClass

passArray(numbers)

    mov EAX, .L_ZZ4mainE7numbers
    mov DWORD PTR [EBP - 60], EAX
    mov EAX, .L_ZZ4mainE7numbers+4
    mov DWORD PTR [EBP - 56], EAX
    mov EAX, .L_ZZ4mainE7numbers+8
    mov DWORD PTR [EBP - 52], EAX
    lea EAX, DWORD PTR [EBP - 60]
    mov DWORD PTR [ESP], EAX
    call    _Z9passArrayPi

    // parameter access
    push    EAX
    mov EAX, DWORD PTR [ESP + 8]
    mov DWORD PTR [ESP], EAX
    pop EAX

I'm assuming I'm looking at the right assembly pertaining to the parameter passing because there are calls at the end of each!

But due to my very limited knowledge of assembly, I can't tell what's going on here. I learned about ccall convention, so I'm assuming something is going on that has to do with preserving the caller-saved registers and then pushing the parameters onto the stack. Because of this, I'm expecting to see things loaded into registers and "push" everywhere, but have no idea what's going on with the movs and leas. Also, I don't know what DWORD PTR is.

I've only learned about registers: eax, ebx, ecx, edx, esi, edi, esp and ebp, so seeing something like XMM0 or DL just confuses me as well. I guess it makes sense to see lea when it comes to passing by reference/pointer because they use memory addresses, but I can't actually tell what is going on. When it comes to passing by value, it seems like there are many instructions, so this could have to do with copying the value into registers. No idea when it comes to how arrays are passed and accessed as parameters.

If someone could explain the general idea of what's going on with each block of assembly to me, I would highly appreciate it.

like image 669
atkayla Avatar asked Nov 08 '13 12:11

atkayla


People also ask

How are parameters passed in assembly?

To pass parameters to a subroutine, the calling program pushes them on the stack in the reverse order so that the last parameter to pass is the first one pushed, and the first parameter to pass is the last one pushed. This way the first parameter is on top of the stack and the last one is at the bottom of the stack.

How are arguments passed by value and reference?

When you pass an argument by reference, you pass a pointer to the value in memory. The function operates on the argument. When a function changes the value of an argument passed by reference, the original value changes. When you pass an argument by value, you pass a copy of the value in memory.

How is pass by reference different from pass by value when a pointer is passed?

The difference between pass-by-reference and pass-by-pointer is that pointers can be NULL or reassigned whereas references cannot. Use pass-by-pointer if NULL is a valid parameter value or if you want to reassign the pointer. Otherwise, use constant or non-constant references to pass arguments.

How many different ways are possible to pass a pointer to a function call?

There are three ways to pass variables to a function – pass by value, pass by pointer and pass by reference.


3 Answers

Using CPU registers for passing arguments is faster than using memory, i.e. stack. However there is limited number of registers in CPU (especially in x86-compatible CPUs) so when a function has many parameters then stack is used instead of CPU registers. In your case there are 5 function arguments so the compiler uses stack for the arguments instead of registers.

In principle compilers can use push instructions to push arguments to stack before actual call to function, but many compilers (incl. gnu c++) use mov to push arguments to stack. This way is convenient as it does not change ESP register (top of the stack) in the part of code which calls the function.

In case of passByValue(i, c, p, f, tc) values of arguments are placed on the stack. You can see many mov instruction from a memory location to a register and from the register to an appropriate location of the stack. The reason for this is that x86 assembly forbids direct moving from one memory location to another (exception is movs which moves values from one array (or string as you wish) to another).

In case of passByReference(i, c, p, f, tc) you can see many 5 lea instructions which copy addresses of arguments to CPU registers, and these values of the registers are moved into stack.

The case of passByPointer(&i, &c, &p, &f, &tc) is similar to passByValue(i, c, p, f, tc). Internally, on the assembly level, pass by reference uses pointers, while on the higher, C++, level a programmer does not need to use explicitely the & and * operators on references.

After the parameters are moved to the stack call is issued, which pushes instruction pointer EIP to stack before transferring the program execution to the subroutine. All moves of the parameters to the stack account for the coming EIP on stack after the call instruction.

like image 132
Igor Popov Avatar answered Oct 09 '22 22:10

Igor Popov


There's too much in your example above to dissect all of them. Instead I'll just go over passByValue since that seems to be the most interesting. Afterwards, you should be able to figure out the rest.

First some important points to keep in mind while studying the disassembly so you don't get completely lost in the sea of code:

  • There are no instructions to directly copy data from one mem location to another mem location. eg. mov [ebp - 44], [ebp - 36] is not a legal instruction. An intermediate register is needed to store the data first and then subsequently copied into the memory destination.
  • Bracket operator [] in conjunction with a mov means to access data from a computed memory address. This is analogous to derefing a pointer in C/C++.
  • When you see lea x, [y] that usually means compute address of y and save into x. This is analogous to taking the address of a variable in C/C++.
  • Data and objects that needs to be copied but are too big to fit into a register are copied onto the stack in a piece-meal fashion. IOW, it'll copy a native machine word at a time until all the bytes representing the object/data is copied. Usually that means either 4 or 8 bytes on modern processors.
  • The compiler will typically interleave instructions together to keep the processor pipeline busy and to minimize stalls. Good for code efficiency but bad if you're trying to understand the disassembly.

With the above in mind here's the call to passByValue function rearranged a bit to make it more understandable:

.define arg1  esp
.define arg2  esp + 4
.define arg3  esp + 8
.define arg4  esp + 12
.define arg5.1  esp + 16
.define arg5.2  esp + 20


; copy first parameter
mov EAX, [EBP - 16]
mov [arg1], EAX

; copy second parameter
mov DL, [EBP - 17]
movsx   EAX, DL
mov [arg2], EAX

; copy third
mov ECX, [EBP - 24]
mov [arg3], ECX

; copy fourth
movss   XMM0, DWORD PTR [EBP - 28]
movss   DWORD PTR [arg4], XMM0

; intermediate copy of TestClass?
mov ESI, [EBP - 40]
mov [EBP - 48], ESI
mov ESI, [EBP - 36]
mov [EBP - 44], ESI

;copy fifth
lea ESI, [EBP - 48]
mov EAX, [ESI]
mov [arg5.1], EAX
mov EAX, [ESI + 4]
mov [arg5.2], EAX
call    passByValue(int, char, int*, float, TestClass)

The code above is unmangled and instruction mixing undone to make it clear what is actually happening but some still needs explaining. First, the char is signed and it is a single byte in size. The instructions here:

; copy second parameter
mov DL, [EBP - 17]
movsx   EAX, DL
mov [arg2], EAX

reads a byte from [ebp - 17](somewhere on stack) and stores it into the lower first byte of edx. That byte is then copied into eax using sign-extended move. The full 32-bit value in eax is finally copied onto the stack that passByValue can access. See register layout if you need more detail.

The fourth argument:

movss   XMM0, DWORD PTR [EBP - 28]
movss   DWORD PTR [arg4], XMM0

Uses the SSE movss instruction to copy the floating point value from stack into a xmm0 register. In brief, SSE instructions let you perform the same operation on multiple pieces of data simultaneously but here the compiler is using it as an intermediate storage for copying floating-point values on the stack.

The last argument:

; copy intermediate copy of TestClass?
mov ESI, [EBP - 40]
mov [EBP - 48], ESI
mov ESI, [EBP - 36]
mov [EBP - 44], ESI

corresponds to the TestClass. Apparently this class is 8-bytes in size located on the stack from [ebp - 40] to [ebp - 33]. The class here is being copied 4-bytes at a time since the object cannot fit into a single register.

Here's what the stack approximately looks like prior to call passByValue:

lower addr    esp       =>  int:arg1            <--.
              esp + 4       char:arg2              |
              esp + 8       int*:arg3              |    copies passed
              esp + 12      float:arg4             |    to 'passByValue'
              esp + 16      TestClass:arg5.1       |
              esp + 20      TestClass:arg5.2    <--.
              ...
              ...
              ebp - 48      TestClass:arg5.1    <--   intermediate copy of 
              ebp - 44      TestClass:arg5.2    <--   TestClass?
              ebp - 40      original TestClass:arg5.1
              ebp - 36      original TestClass:arg5.2
              ...
              ebp - 28      original arg4     <--.
              ebp - 24      original arg3        |  original (local?) variables
              ebp - 20      original arg2        |  from calling function
              ebp - 16      original arg1     <--.
              ...
higher addr   ebp           prev frame
like image 37
greatwolf Avatar answered Oct 09 '22 23:10

greatwolf


What you're looking for are ABI calling conventions. Different platforms have different conventions. e.g. Windows on x86-64 has different conventions than Unix/Linux on x86-64.

http://www.agner.org/optimize/ has a calling-conventions doc detailing the various ones for x86 / amd64.

You can write code in ASM that does whatever you want, but if you want to call other functions, and be called by them, then pass parameters / return values according to the ABI.

It could be useful to make an internal-use-only helper function that doesn't use the standard ABI, but instead uses values in the registers that the calling function allocates them in. This is esp. likely if you're writing the main program in something other than ASM, with just a small part in ASM. Then the asm part only needs to care about being portable to systems with different ABIs for being called from the main program, not for its own internals.

like image 4
Peter Cordes Avatar answered Oct 10 '22 00:10

Peter Cordes