Swift Calling Conventions on ARM64: Float / Double
Learn what registers Swift uses for floating point numbers
This is the 2nd post in a series on how Swift’s calling conventions work on ARM64. If you have not read my first post on Swift’s calling conventions for Int
and Bool
types, please read that first.
To try out the examples in this article, please make sure you have done the following set up first:
Item #3 is very important. If you set a breakpoint in Xcode’s UI the following steps may not work.
The Code We Will Investigate
In this post we will investigate the calling conventions used in the following code. In the iOS app project you created, replace the contents of ViewController.swift
with the following:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
print(testFloat(a: 100.111, b: 100.222))
print(testDouble(a: 200.111, b: 200.222))
print(testFloatInt(a: 300.111, b: 300.222, c: 300333, d: 300444))
print(testIntFloatDoubleInt(a: 400111, b: 400.222, c: 400.333, d: 400444))
}
}
func testFloat(a: Float, b: Float) -> Float {
return a + b
}
func testDouble(a: Double, b: Double) -> Double {
return a + b
}
func testFloatInt(a: Float, b: Float, c: Int, d: Int) -> Float {
return a + b + Float(c + d)
}
func testIntFloatDoubleInt(a: Int, b: Float, c: Double, d: Int) -> Double {
return Double(a) + Double(b) + c + Double(d)
}
Before continuing, make sure you can build and run the project.
Background Information
The “Procedure Call Standard for the ARM 64-bit Architecture” states:
The base standard provides for passing arguments in general-purpose registers (
r0
-r7
), SIMD/floating-point registers (v0
-v7
) and on the stack. For subroutines that take a small number of small parameters, only registers are used.
Technically the document does say that floating-point arguments will be stored in registers v0
-v7
, but its actually a little more complicated than that. Each v<n>
register holds 128bits, and there are different registers which reference different chunks of this memory.
These docs explain how these other registers work:
The mapping between the registers is as follows:
d<n>
maps to the least significant half ofv<n>
s<n>
maps to the least significant half ofd<n>
h<n>
maps to the least significant half ofs<n>
b<n>
maps to the least significant half ofh<n>
This means that the register d0
maps to the least significant 64 bits of v0
. Similarly s0
maps to the least significant 32 bits of d0
(transitively, s0
also maps the least significant 32 bits of v0
).
Since Float
s only need 32 bits, Swift will store them in s<n>
registers. Similarly Swift will use d<n>
registers to store Doubles
since they need 64 bits.
Armed with this knowledge, we should expect that Float
arguments will be stored in registers s0
-s7
. Let’s try it out.
Float
Argument Values
Use the approach described here to place a breakpoint on testFloat
, and run the project until the breakpoint triggers. Then run the following commands in LLDB:
(lldb) register read s0 s1 -f f
s0 = 100.111
s1 = 100.222
This confirms our hypothesis! We can find Float
arguments in the s0
-s7
registers.
We can also confirm the relationship between s0
, d0
, and v0
below:
(lldb) register read s0 -f h
s0 = 0x42c838d5
(lldb) register read d0 -f h
d0 = 0x0000000042c838d5
(lldb) register read v0 -f h
v0 = 0x00000000000000000000000042c838d5
Notice how the larger registers contain the same data, just padded.
Return Value
Just like we learned in my previous article, Float
return values will be stored the same way that the first Float
argument is stored. This means that a Float
return value should be stored in s0
.
Let’s confirm:
(lldb) thread step-out
(lldb) register read s0 -f f
s0 = 200.333
Great! This is exactly what we expect. Just to remind you, thread step-out
will execute the rest of the current function, pop the call stack, and then pause the debugger. At this point, the return value should be correctly stored in the intended register.
Double
Argument Values
Double
works exactly the same as Float
execpt that values are stored in the 64bit d0
-d7
registers. This means that the arguments to testDouble
will be stored in d0
and d1
.
You can verify this by setting a breakpoint on testDouble
and running:
(lldb) register read d0 d1 -f f
d0 = 200.111
d1 = 200.222
Return Value
The return value works exactly the same as with Float
, execpt it uses the d0
register.
You can use these commands to try it out:
(lldb) thread step-out
(lldb) register read d0 -f f
d0 = 400.333
Mixed Argument Types
What happens if a function has mixed argument types?
This works as you would probably expect. Float
/Double
arguments are stored in the floating point registers, and Int
/Bool
arguments are stored in the general purpose registers.
Let’s try it out to see what happens:
Float, Int
Set a breakpoint on testFloatInt
and run the following commands.
(lldb) register read s0 s1 -f f
s0 = 300.111
s1 = 300.222
(lldb) register read x0 x1 -f d
x0 = 300333
x1 = 300444
Notice how the Int
arguments are stored in registers x0
, and x1
even though they are the 3rd, and 4th arguments respectively. This is because they are the 1st and 2nd Int
arguments.
Also notice how Swift chose to put the floaing point values in the floating point registers s<n>
even though the general purpose registers x<n>
would have had enough space to store these values.
Int, Float, Double Int
Let’s try out a more complex example in which integer and floating point values are interleaved.
Set a breakpoint on testIntFloatDoubleInt
and run the following commands.
(lldb) register read x0 -f d
x0 = 400111
(lldb) register read s0 -f f
s0 = 400.222
(lldb) register read d1 -f f
d1 = 400.333
(lldb) register read x1 -f d
x1 = 400444
Again, Swift puts the 1st and 2nd Int
values in x0
and x1
respectively.
Also notice how the first floating point value is in s0
, but the 2nd one is in d1
. This is beacause the s<n>
, and d<n>
registers actually just represent a smaller section of a v<n>
register. Once Swift uses s0
for one argument it means that d0
, and v0
are used up too. It must store the next floaing point argument in s1
/ d1
, etc.
Summary
I hope this article helped explain how Swift stores its floating point arguments on ARM64! If you’ve read my first article too you should now understand how Swift stores Int
, Bool
, Float
, and Double
types as arguments and return values.
Int
and Bool
values are stored in the x<n>
registers, and floaing point values are stored in the v<n>
registers (or the smaller variants if possible).
In my next article I’m planning to cover how Swift stores Strings
on ARM64.
Troubleshooting
Make sure you use the approach desribed here to set breakpoints. Setting breakpoints using the UI or using
breakpoint set --name <func_name>
may not work because LLDB may not pause the function on the first instruction. If other instructions execute, there’s a chance that the register values may get overwritten.Make sure you are running this code on a real iOS device. If you run this code on the iOS simulator, Swift will likely be running on a x86 CPU which has a different calling convention.
If something is still unclear or not working, I would be happy to help! Feel free to DM me on twitter or send me an email.