Printing variables

Printing strings

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

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.

Printing floating point values

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 patternExponentValue
0002-30.125
0012-20.25
0102-10.5
011201
100212
101224
110238
1112416

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.



Hello World ← Previous | Next → User input