Printing integers and characters
prerequisite
Read these first:
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:
- Define a format string containing the appropriate format specifier.
- Load the address of the format string into
x0. - Place the value to be printed into the next argument register.
- 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 characterIn 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 callIn 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 printfNotice 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.
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.
| Specifier | Prints | Fill |
|---|---|---|
%d | a signed integer in decimal | a w register |
%u | an unsigned integer in decimal | a w register |
%x | an integer in hexadecimal | a w register |
%c | the character with that code | a w register |
%ld | a 64-bit signed integer in decimal | an x register |
%lx | a 64-bit integer in hexadecimal | an x register |
%s | the string at that address | an x register holding an address |
%f | a double | a 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.
pitfall
A common mistake from this lesson, with a broken program and its fix that you can run:
Check yourself
- A format string holds two specifiers. Which registers carry their values?
- You want to print the 64-bit value in
x19. Which specifier do you use, and which register do you fill? - What does
mov w1, 'A'put inw1, and what do%dand%cprint for it?
answers
show answers
w1(orx1) for the first andw2(orx2) for the second; the format string itself is inx0.%ld, withmov x1, x19.- 65, the code for A:
%dprints 65 and%cprints A.
Practice
- Warm up the registers: prints its answer through a format string.
- Name tag: print two characters and a number with one format string,
%cand%d. - Intermediate quiz: ARMv8 assembly: covers the argument registers.