Passing argument to functions is simple when you have four or fewer non-floating-point arguments. You use rcx, rdx, r8, and r9 and provide shadow space on the stack before calling the function. After the call, you re-adjust the stack for the shadow space, and everything is fine. If you have more than four arguments, things are more complicated.
Using More Than Four Arguments
Let’s first see why things get complicated with more than four non-floating-point arguments, as shown in Listing 41-1.
; arguments1.asm
extern printf
section .data
first db "A",0
second db "B",0
third db "C",0
fourth db "D",0
fifth db "E",0
sixth db "F",0
seventh db "G",0
eighth db "H",0
ninth db "I",0
tenth db "J",0
fmt db "The string is: %s%s%s%s%s%s%s%s%s%s",10,0
section .bss
section .text
global main
main:
push rbp
mov rbp,rsp
sub rsp,8
mov rcx, fmt
mov rdx, first
mov r8, second
mov r9, third
push tenth ; now start pushing in
push ninth ; reverse order
push eighth
push seventh
push sixth
push fifth
push fourth
sub rsp,32 ; shadow space
call printf
add rsp,32+8 ; restore stack
leave
ret
Listing 41-1
arguments1.asm
Look at the instruction sub rsp,8; it is there because when we call printf, the stack needs to be 16-byte aligned. Why not just use one instruction, such as sub rsp,40 just before the call? Well, the stack would be 16-byte aligned, but printf is likely to fail. If we decrease the stack by 40 instead of 32 just before the call, the arguments on the stack are not where printf expects them to be, just above the shadow space. So, we need to align the stack before we start pushing the arguments. Note that we need to push the arguments in reverse order. After the call, we restore the stack for the alignment and for the shadow space.
You can also build the stack in another way. Listing 41-2 shows how it works.
;arguments2.asm
extern printf
section .data
first db "A",0
second db "B",0
third db "C",0
fourth db "D",0
fifth db "E",0
sixth db "F",0
seventh db "G",0
eighth db "H",0
ninth db "I",0
tenth db "J",0
fmt db "The string is: %s%s%s%s%s%s%s%s%s%s",10,0
section .bss
section .text
global main
main:
push rbp
mov rbp,rsp
sub rsp,32+56+8 ;shadow space + 7 arguments on stack + alignment
mov rcx, fmt
mov rdx, first
mov r8, second
mov r9, third
mov qword[rsp+32],fourth
mov qword[rsp+40],fifth
mov qword[rsp+48],sixth
mov qword[rsp+56],seventh
mov qword[rsp+64],eighth
mov qword[rsp+72],ninth
mov qword[rsp+80],tenth
call printf
add rsp, 32+56+8 ;not needed before leave
leave
ret
Listing 41-2
arguments2.asm
First use sub rsp,32+56+8 to adjust the stack.
32 bytes for shadow space
7 arguments to be pushed times 8 bytes, for a total of 56 bytes
Then you start building the stack, and when you see that you have to align the stack, another 8 bytes have to be subtracted from the stack pointer.
Now at the bottom of the stack, you have 32 bytes for the shadow space, and just above that you have the fourth argument, above the fifth, and so on. The stack that you build here looks the same as the one in the previous program. It is up to you to decide what you prefer.
How does this work in the called function? Listing 41-3 shows some example code that uses the function lfunc to build a string buffer to be printed by printf.
The main function is the same as in arguments1.asm; however, the function called is lfunc instead of printf, which is called later in the code.
In lfunc, look at the instruction mov rax, qword [rbp+8+8+32], which loads the fourth argument from the stack into rax. The register rbp contains a copy of the stack pointer. The first 8-byte value on the stack is the rbp we pushed in the prologue of lfunc. The 8-byte value higher up is the return address to main, which was automatically pushed on the stack when lfunc was called. Then we have shadow space with 32 bytes. Finally, we arrive at the pushed arguments. Hence, the fourth and other arguments can be found at rbp+48 and higher.
When we return to main, the stack is aligned again, and printf is called.
Figure 41-3 shows the output, which is of course the same as before.
Working with Floating Points
Floating points are another story. Listing 41-4 shows some example code.
; stack_float.asm
extern printf
section .data
zero dq 0.0 ;0x0000000000000000
one dq 1.0 ;0x3FF0000000000000
two dq 2.0 ;0x4000000000000000
three dq 3.0 ;0x4008000000000000
four dq 4.0 ;0x4010000000000000
five dq 5.0 ;0x4014000000000000
six dq 6.0 ;0x4018000000000000
seven dq 7.0 ;0x401C000000000000
eight dq 8.0 ;0x4020000000000000
nine dq 9.0 ;0x4022000000000000
section .bss
section .text
global main
main:
push rbp
mov rbp,rsp
movq xmm0, [zero]
movq xmm1, [one]
movq xmm2, [two]
movq xmm3, [three]
movq xmm4, [nine]
sub rsp, 8
movq [rsp], xmm4
movq xmm4, [eight]
sub rsp, 8
movq [rsp], xmm4
movq xmm4, [seven]
sub rsp, 8
movq [rsp], xmm4
movq xmm4, [six]
sub rsp, 8
movq [rsp], xmm4
movq xmm4, [five]
sub rsp, 8
movq [rsp], xmm4
movq xmm4, [four]
sub rsp, 8
movq [rsp], xmm4
sub rsp,32 ; shadow
call lfunc
add rsp,32
leave
ret
;------------------------------------------------
lfunc:
push rbp
mov rbp,rsp
movsd xmm4,[rbp+8+8+32]
movsd xmm5,[rbp+8+8+32+8]
movsd xmm6,[rbp+8+8+32+16]
movsd xmm7,[rbp+8+8+32+24]
movsd xmm8,[rbp+8+8+32+32]
movsd xmm9,[rbp+8+8+32+40]
leave
ret
Listing 41-4
stack_float.asm
There is no output for this little program because there is an oddity that we will explain in the next chapter. You will have to use a debugger to look at the xmm registers. For your convenience, we have provided the floating-point values in hexadecimal in the comments. The first four values are passed to the function in the xmm0 to xmm3 registers. The remaining arguments will be stored on the stack. Remember that the xmm registers can contain one scalar double-precision value, two packed double-precision values, or four packed single-precision values. In this case, we use one scalar double-precision value, and for the sake of the demonstration we stored the values on the stack without using a push instruction. This would be the way to store packed values on the stack, adjusting rsp every time with the appropriate amount. A more efficient way would be to push the scalar value directly from memory to the stack, as shown here:
push qword[nine]
In the function, we have to copy the values from the stack into the xmm registers, where we can process them further.
Summary
In this chapter, you learned about the following:
How to pass arguments to functions in registers and the stack