In this segment we'll look at floating point instructions in ARM.
Floating point computations are considerably harder than integer computations. Operations must deal with both the mantissa and exponent, and addition/subtraction must do major bit-shifting to line up both operands. Also, most modern processors perform more complex operations like powers, roots, logarithms, and trig functions in hardware.
The hardware to do all this is called a floating point unit or FPU. A decade or two, it would have resided on a separate chip nearly as complex as the CPU itself. In modern processors it's part of the CPU die, but still a separate unit that interoperates with the CPU.
For this reason, ARM assembly has special instructions for floating point operations, and a set of floating point registers (FP registers) specifically for such operations. The FP registers are actually in the FPU, and the floating point operations are performed by the FPU under direction from the CPU.
An image of a typical FP processor is here.
There are 16 floating point registers, designated d0 - d15. These are 64-bit registers, so they hold double precision values. Each register can also be divided into two 32-bit single precision registers labelled s0 - s32. So for instance d0 is also s0 and s1. You may use it either as one double precision register or two single precision registers, though not both at the same time.
The FP registers have their own load and store operations, which work like the general purpose ones, but load and store to/from the FP registers.
Our example program Quadratic.c declares four doubles, reads them, and then computes the solution for a quadratic equation with a, b, and c as coefficients and prints the result.
The compiler-generated code for this is in Quadratic.s. On lines 19-23, it starts off by computing offsets for the three locals a, b, and c and passing them to scanf, much as the max-computing Prg5 did.
Question 1
But there's an interesting difference from the earlier program. What are the addresses (relative to fp) of those variables, and what do you notice regarding their size?
Answer 1
After the scanf, those three variables will have values entered into them. Lines 24-41 do the quadratic computation, and are almost entirely composed of floating-point instructions, which is why the opcodes all begin with "f". And they operate on the double-precision registers, which is why they end with "d" (single-precision instructions would end with "s")
The comments provided with the code indicate what the instructions do. fldd for instance is a load instruction like ldr, but for the FP registers. So lines 24-25 load b into both d6 and d7. The fmuld on line 26 multiplies them to compute b*b, leaving that in d6, etc. Lines 27-28 load a and the value 4.0 into d7 and d5 respectively.
Line 28 needs more explanation. First, there is a fcpyd instruction that works like mov, and is used on line 33 to copy one register into another. But, it does not permit assignment of constants, only other FP registers. So line 28 loads the constant from memory, just as we would do for a constant too large to fit into a mov instruction.
The fldd loads from .L3, and is of course a pseudoinstruction, converted to a pc-relative load. And since it's loading a double-precision value, it loads two words at .L3, not just one.
So, consulting .L3 it looks like it would load 0 (line 53) for the first four bytes and then the value on line 54 for the last four. But that would imply all zeros for the exponent which is at the start of the number. That can't be right. What's going on?
This is the first time we've encountered what is usually termed *endianness.
When a value, e.g. an integer, occupies multiple bytes in memory, it's natural to assume that the bytes are placed in memory in the left to right order we naturally write them. If an integer like 000010001100101011101011011111111 is at address 100, we assume its bytes are placed in addresses as shown, with the most significant byte first, and the least significant one last.
00001000 11001010 11101011 01111111
100 101 102 103
This is true for many CPUs. But, some CPUs place the bytes in the opposite order, with the least significant first:
01111111 11101011 11001010 00001000
100 101 102 103
These two storage approaches are respectively called big-endian and little-endian. The terms are references from Swift's Gulliver's Travels, which is a political satire, not just a children's book. Gulliver at one point travels to a land where those who eat softboiled breakfast eggs from the little end first (the "Little Endians") are at war with those who eat them from the big end first (the "Big Endians").
The reasons for the difference are not quite as silly as the Swift reference might suggest. When the CPU adds or subtracts multibyte values, it does so from right to left. So little-endian order positions bytes so that they are most naturally drawn from memory in the order needed for right-to-left mathematical operations. That assumes they're being accessed one byte at a time, though. This is not true in a modern 32 or 64-bit CPU, which draws values from memory multiple bytes at a time. But the earliest 8-bit microprocessors did fetch data one byte at a time, and it was a struggle to get all the needed circuitry for a single CPU onto one chip, so the little-endian design was worth it to save circuitry and complexity. In particular, Intel used this standard in its early microprocessors. Once the little-endian standard was set, all future Intel processors followed it for backward compatibility, and Intel is stuck with it to this day. Most non-Intel CPUs are big-endian, and the ARM standard actually allows the user to choose which ordering to use.
It's not obvious when you're dealing with a little-endian CPU. It's irrelevant in registers, which are always in the expected left-to-right order. The ldr and str instructions silently adjust for endianness when going between register and memory, and any multibyte constant you provide in the assembly code is put into memory in the right order by the assembler. You only see the byte ordering when you deal with a multibyte quantity in pieces, e.g. loading individual bytes from an integer, or, as in our case, specifying two words for an 8-byte quantity.
Returning then to lines 53-54, the words 0 and 0x40100000 specify a little-endian 8-byte double.
0x4010000000000000
Question 2
Convert that hex to binary form, and explain why it represents 4.0 in IEEE floating point.
Answer 2
Breaking it into sign, exponent, and mantissa, we have: 0 10000000001 0000000000000000000000000000000000000000000000000000 That's a positive value, with exponent 1025-1023 = 2, and a mantissa of 1.0 (remember the 1. is assumed). 1.0 x 2^2 = 4.0
Question 3 When a binary value is transmitted between machines on the internet, is it sent in little-endian or big-endian order? Do a bit of online research -- it won't take much.
Answer 3
And one more question regarding the line 28 that started this big digression:
Question 4
We had to load the 4.0 because a fcpyd instruction can't move a floating point constant into a register. Why can't it do so?
Answer 4
Lines 29-32 are easy to follow; please just read them and their comments. Line 33, however, is more interesting. It's preparing for the sqrt function call. We know from prior examples that r0 holds the first parameter in a function call. But this works only for integer values. If the parameter is a double, then d0 holds the first parameter, since d0 has 64 bits, and any operations on the parameter will use the FP registers anyway. Similarly, d0 holds the return value from the function call, as we see on line 35 after the sqrt call.
Lines 36-39 are unremarkable; again, read them and their comments so you see what they do. Line 40 performs a division, per the comment, and is interesting because there is a division instruction for floating point, even though there is none for integers.
Line 41 stores the final result in x, line 42 sets up the format string parameter in r0.
But, line 43 is not so obvious. An ldrd instruction loads from memory into general registers, but it loads 8 bytes into two successive general registers. The memory must start at an 8-byte divisible address, and the specified register must be even, with the implied second one odd. For instance, ldrd r2, [fp, #-12] loads x into r2 and r3.
Because printf is declared as a variadic function, and the parameter types after the format string are unpredictable. So, it doesn't follow the usual rule that doubles are passed in FP registers. It will expect x in registers r2 and r3.
The remaining lines return a 0 from main and deconstruct the stack frame.
Question 5
Why is there an .align 3 on line 51?
Answer 5
.align 3 means pad to an 8-byte boundary. The double stored at .L3 needs to be on such a boundary per the requirements of ldrd stated above.