There are three different types of variables that we need to be able to print. They are strings, integers, and floats
We will start with printing string variables.
The scenario is printing a variable as part of a string. The code is very similar to the Hello World code shown previously.
// print_a_var_1.s
.global main
.extern printf
.data
output:
.asciz "The word is %s.\n" // The %s will be replaced with the var in x1
word:
.asciz "vanilla"
.text
main:
//prolog
stp x29, x30, [sp, -16]!
mov x29, sp
//main code
ldr x0, =output // The output string goes into x0
ldr x1, =word // The variable goes into x1
bl printf // The printf function inserts the variable into the output string
//cleanup
mov x0, #0
ldp x29, x30, [sp], 16
RET
The declaration of "output" has a placeholder withing the string denoted by the %s symbol. When the printf function
encounters the %s placeholder it looks in x1 expecting to find the address of a string. It will then insert that string
variable into the string stored in x0.
Printing integers is not dissimilar to printing strings. The placeholder(s) are different and with that is the one aspect that needs to be discussed.
// print_int.s
.global main
.extern printf
.data
output:
.asciz "The number is %d.\n" // The %s will be replaced with the var in x1
number:
.word 32
.text
main:
//prolog
stp x29, x30, [sp, -16]!
mov x29, sp
//main code
ldr x0, =output // The output string goes into x0
ldr x1, =number // The address of the variable goes into x1
ldr x1, [x1] // Load x1 with the value stored at the address in x1
bl printf // The printf function inserts the variable into the output string
//cleanup
mov x0, #0
ldp x29, x30, [sp], 16
RET
The .data section has two aspects that need highlighting.
The %d placeholder specifies a 32 bit signed integer. The .word declares "number" to be a 32 bit signed integer as well. When the ldr x1, [x1] instruction is executed the machine will locate the 64 bit address in memory and take the 32 bit value stored there. It will then be loaded into the 64 bit x1 register with the upper 32 bits set to zero.
Consider what happens when this code is used:
.data
output:
.asciz "The word is %d.\n" // The %s will be replaced with the var in x1
number:
.word 4294967296
The number 4,294,967,296 is also 1 0000 0000 0000 0000 0000 0000 0000 0000 which is a 33 bit numnber. If this code is assembled the assembler returns the following message:
andrew@master:~/assm2 $ gcc -g print_int.s -o print_int
print_int.s: Assembler messages:
print_int.s:10: Warning: value 0x100000000 truncated to 0x0
Only 32 bit of memory were allocated to the .word variable and so only the lower 32 bits were stored resulting in the truncation. To store 64 bit integers the variable must be declared as .quad rather than .word.
The %d placeholder is also only 32 bits. If the .data section is changed to this:
.data
output:
.asciz "The number is %d.\n" // The %s will be replaced with the var in x1
number:
.quad 4294967296
The code will assemble and run without error but will report 0 as the value:
andrew@master:~/assm2 $ ./print_a_var_1
The number is 0.
The correct way to print a 64 bit integer is to use the %ld (long decimal) placeholder. This will result in the number being displayed correctly.
If the number to be printed is known prior to runtime its value can be loaded directly into a register without the need for a variable. This is called an immediate load.
The following code shown an immediate load.
// print_a_var_1.s
.global main
.extern printf
.data
output:
.asciz "The number is %d.\n" // The %s will be replaced with the var in x1
.text
main:
//prolog
stp x29, x30, [sp, -16]!
mov x29, sp
//main code
ldr x0, =output // The output string goes into x0
mov x1, #32 // Move the value 32 into x1 directly
bl printf // Tye printf function inserts the variable into the output string
//cleanup
mov x0, #0
ldp x29, x30, [sp], 16
RET
As a final note, any integer can be loaded into a register from memory or immediately.
Floating point numbers cannot be printed from the x registers. Instead they have to be placed in dedicated floating point registers known as d registers. Note that the d registers are 64 bits. While it is possible to work with 32 bit values in the single point, or s registers, they must be converted to 64 bit and placed in a d register for printing with printf. So there is only one code example to look at.
// printing_floats
.global main
.extern printf
.section .data
output:
.asciz "Value = %f\n"
pi:
.double 3.14159
.section .text
main:
// Prolog
stp x29, x30, [sp, -16]!
mov x29, sp
// Main code
ldr x0, =output
ldr x1, =pi
ldr d0, [x1]
bl printf
// Cleanup
mov x0, #0
ldp x29, x30, [sp], 16
RET
The only thing to point out in this code is the %f placeholder in the output variable. The %f, float placeholder is for a 32 bit IEEE754 encoded floating point value. The pi variable is declared as .double and printed from d0 which are both 64 bits. The printf function automatically promotes %f to %lf, long float, which is 64 bits for printing and that evens everything up.
While it is possible to use the fmov instruction to do an immediate load to a d register, there are only a few, 256 actually, floating point values that can be moved directly.
Consider this code:
// printing_floats
.global main
.extern printf
.section .data
output:
.asciz "Value = %f\n"
pi:
.double 3.14159
.section .text
main:
// Prolog
stp x29, x30, [sp, -16]!
mov x29, sp
// Main code
ldr x0, =output
fmov d0, #12.5
bl printf
// Cleanup
mov x0, #0
ldp x29, x30, [sp], 16
RET
This code assembles and runs perfectly:
andrew@master:~/assm2 $ ./printing_floats
Value = 12.500000
andrew@master:~/assm2 $
If the value 12.5 is replaced with 12.6, the code does not assemble:
andrew@master:~/assm2 $ gcc -g printing_floats.s -o printing_floats
printing_floats.s: Assembler messages:
printing_floats.s:22: Error: invalid floating-point constant at operand 2 -- `fmov d0,#12.6'
andrew@master:~/assm2 $
The reason is that the only values that can be loaded immediately are those that can be represented using the imm8 format.
This is an eight bit format of the form a:b:c:d:e:f:g:h where a is the sign bit, bcd are the exponent shown in the table below, and efgh is the mantissa.
| Bit pattern | Exponent | Value |
|---|---|---|
| 000 | 2-3 | 0.125 |
| 001 | 2-2 | 0.25 |
| 010 | 2-1 | 0.5 |
| 011 | 20 | 1 |
| 100 | 21 | 2 |
| 101 | 22 | 4 |
| 110 | 23 | 8 |
| 111 | 24 | 16 |
For a value to be encodable to imm8 it must be able to be represented as: 1.n × 2m where m is a value from -3 to 4 and n is a factor 16.
In the case of 12.5:
12.5 is positive so the a bit is zero.
12.5 ÷ 8 = 1.5625
Since 8 = 23, therefore the bcd bits are 110
Finally, 0.5625 × 16 = 9, therefore, the efgh bits are 1001.
The imm8 representation of 12.5 is 01101001.
If we consider 12.6, 12.6 ÷ 8 = 1.575, but 0.575 × 16 = 9.2 which cannot be represented as a four bit binary value.
As another example we can work backwards from an 8 bit binary value: 10100110.
The 1 in the a position indicates this is a negative number.
The 010 in the bcd position indicates the exponent is s-1 or 0.5.
The 0110 in eht efgh position indicates a value of 6.
6 × 0.5 = 3. That means that -3.0 should be a value that can be immediately loaded.
We test:
// Main code
ldr x0, =output
fmov d0, #-3.0
When assembled and run we get the output of -3.0.