6.2 KiB
Section 1 / Floating Point Literals
Recall that all AARCH64 instructions are 4 bytes long. Recall also that this means that there are constraints on what can be specified as a literal since the literal must be encoded into the 4 byte instruction. If the literal is too large, an assembler error will result.
Given that floating point values are always at least 4 bytes long themselves, using floating point literals is extremely constrained. For example:
fmov d0, 1 // 1
fmov d0, 1.1 // 2
Line 1 will pass muster but Line 2 will cause an error.
To load a float, you could translate the value to binary and do
as the following:
.text // 1
.global main // 2
.align 2 // 3
// 4
main: str x30, [sp, -16]! // 5
ldr s0, =0x3fc00000 // 6
fcvt d0, s0 // 7
ldr x0, =fmt // 8
bl printf // 9
ldr x30, [sp], 16 // 10
mov w0, wzr // 11
ret // 12
// 13
.data // 14
fmt: .asciz "%f\n" // 15
.end // 16
The above code is found here.
Line 6 puts the translated value of 1.5 into s0 (since the value
is a float it goes in an s register). The assembler performs the
magic of treating the literal as if it were an address and puts it into
s0.
Line 7 converts the single precision number into a double precision
number for printing.
printf() only knows how to print double precision values. When you
specify a float, it will convert it to a double before emitting it.
Translating floats and doubles by hand isn't a common practice for
humans, though compilers are happy to do so.
Instead for us humans, the assembler directives .float and .double
are used more frequently to specify float and double values putting
them into RAM.
The following example prints an array of floats and doubles:
.global main // 1
.text // 2
.align 2 // 3
// 4
counter .req x20 // 5
dptr .req x21 // 6
fptr .req x22 // 7
.equ max, 4 // 8
// 9
main: stp counter, x30, [sp, -16]! // 10
stp dptr, fptr, [sp, -16]! // 11
ldr dptr, =d // 12
ldr fptr, =f // 13
mov counter, xzr // 14
// 15
1: cmp counter, max // 16
beq 2f // 17
// 18
ldr d0, [dptr, counter, lsl 3] // 19
ldr s1, [fptr, counter, lsl 2] // 20
fcvt d1, s1 // 21
ldr x0, =fmt // 22
add counter, counter, 1 // 23
mov x1, counter // 24
bl printf // 25
b 1b // 26
// 27
2: ldp dptr, fptr, [sp], 16 // 28
ldp counter, x30, [sp], 16 // 29
mov w0, wzr // 30
ret // 31
// 32
.data // 33
fmt: .asciz "%d %f %f\n" // 34
d: .double 1.111111, 2.222222, 3.333333, 4.444444 // 35
f: .float 1.111111, 2.222222, 3.333333, 4.444444 // 36
// 37
.end // 38
The above code is found here.
A number of interesting things in this source code:
-
We use
.reqto give symbolic names to various registers. This can help you in remembering which register is being used for what purpose. -
We use
.equto encode a small integer literal value to give it a symbolic name, eliminating the use of a "magic number." -
Lines 19and20use address arithmetic to march through an array of doubles (8 bytes each) and an array of floats (4 bytes each).
Line 19 is equivalent to:
// ldr d0, [dptr, counter, lsl 3]
d0 = dptr[counter];
counter is multiplied by 8 then added to dptr.
Line 20 is equivalent to:
counter is multiplied byb 4 then added to fptr.
// ldr s1, [fptr, counter, lsl 2]
s1 = fptr[counter];
Cool huh?