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 Floats 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.