AArch64 Playground
4.6 · Printing integers and characters

Printing integers and characters

prerequisite

Printing an integer takes one step more than printing a string: the format string names a type, and the value goes in the next argument register.

In C, you print a value by passing printf a format specifier along with the value. A format specifier is a placeholder in the format string: a % followed by a letter that tells printf how to show the next value. For example, printing an integer requires the integer format specifier (%d), while printing a character requires the character format specifier (%c). The following C code prints one of each:

#include <stdio.h>int main() {    int myInteger = 42;    char myCharacter = 'A';    printf("Integer: %d\n", myInteger);    // Output:    // Integer: 42    printf("Character: %c\n", myCharacter);    // Output:    // Character: A    return 0;}

The format specifier tells printf how to read and show the value it is given. Assembly works the same way: give printf the right format specifier and put the value in the right register.

The big picture

Just like printing strings, printing other data types in AArch64 assembly uses the printf function. The general process is:

  1. Define a format string containing the appropriate format specifier.
  2. Load the address of the format string into x0.
  3. Place the value to be printed into the next argument register.
  4. Call printf.

The example below prints the integer 67 and the character L.

Defining the format string

The first step in printing an integer and a character is to define format strings containing the appropriate format specifiers. Since we are printing both an integer and a character, we need the %d format specifier for the integer and the %c format specifier for the character.

The following code declares a format string for each in the .data section:

.datafmt_int:    .string "Integer: %d\n"        // for printing an integerfmt_char:   .string "Character: %c\n"      // for printing a character

In the above code, the variable fmt_int serves as the format string for printing an integer using the %d format specifier. Similarly, the variable fmt_char serves as the format string for printing a character using the %c format specifier. Both strings are defined using the .string directive.

note

You can print other data types in assembly by defining a format string with the matching format specifier. The specifiers are the same ones C uses; the table further down lists the common ones.

Loading the format string address

Loading the format string address is done in the same way as loading the address of a string variable when printing a string. The ldr instruction loads the address of the format string into x0:

        ldr     x0, =fmt_int                // before the first call        ldr     x0, =fmt_char               // before the second call

In the code above, the address of each format string is loaded into x0: fmt_int for the integer and fmt_char for the character. Each one is loaded just before its own printf call, because the integer and the character are printed with different format strings.

note

The format string is always loaded into the first argument register (x0) before calling printf, regardless of the data type being printed. This is because printf expects the format string to be passed as its first argument.

The corresponding values are then passed in the next argument registers (x1, x2, and so on). If the format string contains multiple format specifiers, the values must be passed in the order they appear in the format string.

Floating-point values are the exception. They travel in the floating-point registers, which are counted separately, so the first one goes in d0 even when x1 already holds an integer. printf always receives a floating-point value as a 64-bit double, so a 32-bit float in s0 must be widened first with fcvt d0, s0. The floating-point lesson covers this.

Passing the values to print

The corresponding values are passed by placing them in the next available argument register. The format string already fills x0, so the next one is x1: the integer value 67 and the character L are each placed in w1 before their respective printf calls. w1 is the 32-bit view of x1, and it is the right size here because %d and %c both read a 32-bit int.

'L' is a character constant: the assembler replaces it with the character's code in ASCII, the standard table that gives each character a number. The code for L is 76, and %c prints the character whose code the register holds.

The following code puts each value in place:

        ldr     x0, =fmt_int        mov     w1, 67        ldr     x0, =fmt_char        mov     w1, 'L'

In the code above, the integer value 67 and the character L are placed in w1 before their respective printf calls.

Calling printf

With the arguments in place, the last step is to call printf with the bl instruction:

        ldr     x0, =fmt_int        mov     w1, 67        bl      printf        ldr     x0, =fmt_char        mov     w1, 'L'        bl      printf

Notice that x0 and w1 are both set again before the second call. printf is allowed to change any of x0 to x18, so neither value is guaranteed to survive the first call.

note

In assembly you call any function with bl followed by the function name.

The whole program

The following program prints an integer and a character using printf. It prints Integer: 67 on the first line and Character: L on the second.

Change the %d in fmt_int to %c and run it again. The first line becomes Integer: C, because 67 is the code for the letter C: the specifier, not the register, decides how a value is shown.

loading editor...

regfile

N clearZ clearC clearV clear

x0–x30 are the integer registers.

X0arg00x0000000000000000
X1arg10x0000000000000000
X2arg20x0000000000000000
X3arg30x0000000000000000
X4arg40x0000000000000000
X5arg50x0000000000000000
X6arg60x0000000000000000
X7arg70x0000000000000000
X8ind0x0000000000000000
X90x0000000000000000
X100x0000000000000000
X110x0000000000000000
X120x0000000000000000
X130x0000000000000000
X140x0000000000000000
X150x0000000000000000
X16ip00x0000000000000000
X17ip10x0000000000000000
X18pr0x0000000000000000
X190x0000000000000000
X200x0000000000000000
X210x0000000000000000
X220x0000000000000000
X230x0000000000000000
X240x0000000000000000
X250x0000000000000000
X260x0000000000000000
X270x0000000000000000
X280x0000000000000000
X29fp0x0000000000000000
X30lr0x0000000000000000
SP0x0000000080000000
PC0x0000000000400000
console

Output prints here as your program runs.

Press step or run under the editor, or feed stdin from the box below.

not assembled

example 1try it: run it, or step one instruction at a timeOpen in playground

Matching the specifier to the register width

Each specifier also says how wide its value is, and the register you fill has to match. %d reads a 32-bit int, so it takes a w register; %ld reads a 64-bit long, so it takes an x register.

SpecifierPrintsFill
%da signed integer in decimala w register
%uan unsigned integer in decimala w register
%xan integer in hexadecimala w register
%cthe character with that codea w register
%lda 64-bit signed integer in decimalan x register
%lxa 64-bit integer in hexadecimalan x register
%sthe string at that addressan x register holding an address
%fa doublea d register

When the widths disagree, printf reads the wrong number of bits. With 5000000000 in x1, %ld prints 5000000000, but %d reads only the low 32 bits and prints 705032704.

Several values in one call

One format string can hold several specifiers. printf fills them from left to right: the first specifier takes w1, the second w2, the third w3, and so on up to x7. Values past the seventh go on the stack, which a later lesson covers.

The program below prints Seat B12 costs 45 dollars. from a single call. Swap the numbers in w2 and w3 and run it again: the line becomes Seat B45 costs 12 dollars., because printf matches values to specifiers by position alone.

loading editor...

regfile

N clearZ clearC clearV clear

x0–x30 are the integer registers.

X0arg00x0000000000000000
X1arg10x0000000000000000
X2arg20x0000000000000000
X3arg30x0000000000000000
X4arg40x0000000000000000
X5arg50x0000000000000000
X6arg60x0000000000000000
X7arg70x0000000000000000
X8ind0x0000000000000000
X90x0000000000000000
X100x0000000000000000
X110x0000000000000000
X120x0000000000000000
X130x0000000000000000
X140x0000000000000000
X150x0000000000000000
X16ip00x0000000000000000
X17ip10x0000000000000000
X18pr0x0000000000000000
X190x0000000000000000
X200x0000000000000000
X210x0000000000000000
X220x0000000000000000
X230x0000000000000000
X240x0000000000000000
X250x0000000000000000
X260x0000000000000000
X270x0000000000000000
X280x0000000000000000
X29fp0x0000000000000000
X30lr0x0000000000000000
SP0x0000000080000000
PC0x0000000000400000
console

Output prints here as your program runs.

Press step or run under the editor, or feed stdin from the box below.

not assembled

example 2try it: run it, or step one instruction at a timeOpen in playground

pitfall

A common mistake from this lesson, with a broken program and its fix that you can run:

Check yourself

  1. A format string holds two specifiers. Which registers carry their values?
  2. You want to print the 64-bit value in x19. Which specifier do you use, and which register do you fill?
  3. What does mov w1, 'A' put in w1, and what do %d and %c print for it?

answers

show answers
  1. w1 (or x1) for the first and w2 (or x2) for the second; the format string itself is in x0.
  2. %ld, with mov x1, x19.
  3. 65, the code for A: %d prints 65 and %c prints A.

Practice