Assembly course · Lesson 2

Hello, World! with printf and formats

At the end you can print text, pass arguments to a function and write numbers with printf as a decimal number, a hex number and a character. Prerequisite: lesson 1.

Architecture:
The video for this lesson is coming.

The idea: using a ready-made function

A program that writes something on the screen would have to talk to the operating system itself. It is easier to use a ready-made function. We take printf from the C runtime library msvcrt.dll, which exists on every Windows. With .import msvcrt.dll printf you tell the assembler that your program needs this function. It creates an import table in which Windows enters the address of printf when the program starts. The function is called through this entry.

You need two things for that: the text in memory and the rules for how to give a function arguments.

Text in memory

In the section .data you store data. msg db 'Hello, World!', 10, 0 means: db (define byte) puts bytes one after the other in memory; the text in quotes becomes its character codes (one byte per character, H is 72). 10 is the line break, 0 marks the end of the text – printf reads until it finds a 0. The label msg stands for the address where the text begins. So you do not give printf the text but the address where it is.

Where do the arguments go?

The rule which argument is passed where is called the calling convention. It applies to every function you call, not just printf.

On the i386 (cdecl) the arguments go on the stack, the last one first: push puts a value on top of the stack. After the call the caller must remove them itself (add esp, 4 for one argument of 4 bytes). The result of the function is in eax.

On the x64 (Windows) the first four arguments go in rcx, rdx, r8 and r9, the first in rcx. More would go on the stack. In addition the caller must leave 32 bytes of room on the stack (shadow space) and keep the stack 16-byte aligned – hence the sub rsp, 40 (32 + 8: when the program starts, the 8-byte return address is already on the stack, and the 8 bytes make it divisible by 16 again). The result is in rax.

On ARM 32 the first four arguments go in r0, r1, r2 and r3, the first in r0. The call blx jumps into the function and puts the return address in lr. Because the call overwrites lr, a program that calls functions itself must save lr on the stack first (push {r4, lr}) and get it back into pc at the end (pop {r4, pc}). The result is in r0. Any function may change r0 to r3; it must return r4 to r11 unchanged.

On ARM 64 the first eight arguments go in x0 to x7, the first in x0. The call blr jumps into the function and puts the return address in x30. Because the call overwrites x30, a program that calls functions itself must save x30 on the stack first (stp x29, x30, [sp, #-16]!) and restore it at the end (ldp). The result is in x0. Any function may change x0 to x17; it must return x19 to x28 unchanged.

Hello, World!

; Lesson 2: Hello, World! with printf (i386, cdecl: the arguments go on the stack).

.format pe console
.arch i386
.import msvcrt.dll printf

.code
start:
  push dword msg          ; the one argument: the address of the text
  call [printf]           ; through the import table
  add esp, 4              ; cdecl: the caller takes the argument off the stack
  xor eax, eax            ; exit code 0
  ret

.data
msg db 'Hello, World!', 10, 0     ; 10 = new line, 0 = end of the text
; Lesson 2: Hello, World! with printf (x64, Windows: the first arguments are in rcx, rdx, r8, r9).

.format pe console
.arch x64
.import msvcrt.dll printf

.code
start:
  sub rsp, 40             ; 32 bytes shadow space for printf + 8 to align the stack
  lea rcx, [msg]          ; first argument: the address of the text
  call [printf]
  xor eax, eax            ; exit code 0
  add rsp, 40
  ret

.data
msg db 'Hello, World!', 10, 0     ; 10 = new line, 0 = end of the text
; Lesson 2: Hello, World! with printf (ARM 32, Thumb-2: the first arguments are in r0 to r3).

.format pe console
.arch thumb
.import msvcrt.dll printf

.code
start:
  push {r4, lr}           ; save lr (the way back); r4 keeps the stack 8-byte aligned
  adr r0, msg             ; first argument: the address of the text (relative to pc)
  ldr r3, printf          ; the address of printf from the import table
  blx r3                  ; call it (lr = the next instruction)
  movs r0, #0             ; exit code 0
  pop {r4, pc}            ; return: the saved lr goes into pc

.data
msg db 'Hello, World!', 10, 0     ; 10 = new line, 0 = end of the text
; Lesson 2: Hello, World! with printf (ARM 64: the first arguments are in x0 to x7).

.format pe console
.arch arm64
.import msvcrt.dll printf

.code
start:
  stp x29, x30, [sp, #-16]!     ; save the way back (x30 = lr)
  adrp x0, msg                  ; the page of the text ...
  add x0, x0, :lo12:msg         ; ... plus the position in the page: first argument
  adrp x16, printf
  ldr x16, [x16, :lo12:printf]  ; the address of printf from the import table
  blr x16
  mov x0, #0                    ; exit code 0
  ldp x29, x30, [sp], #16
  ret

.data
msg db 'Hello, World!', 10, 0     ; 10 = new line, 0 = end of the text

Formats

The first argument of printf is a format text. Where it contains a %, printf inserts the next argument:

Formatwrites the argument asExample for 42
%ddecimal number with sign42
%xhex number (%X: with capital letters)2a
%cone character (the value is the character code)*
%stext; the argument is the address of the text–
%%the character % itself%

The program calculates 20 + 22 and prints three lines. Pay attention to how the values get to printf in each architecture.

; Lesson 2: printf with formats (i386). %d = decimal, %x = hex, %c = character, %s = text.

.format pe console
.arch i386
.import msvcrt.dll printf

.code
start:
  mov eax, 20
  add eax, 22             ; eax = 42
  push eax                ; the arguments from right to left: the last one first
  push 22
  push 20
  push dword f_sum        ; and the format at the end (it is the first argument)
  call [printf]
  add esp, 16             ; 4 arguments of 4 bytes

  push 42                 ; %c
  push 42                 ; %x
  push 42                 ; %d
  push dword f_same
  call [printf]
  add esp, 16

  push 8                  ; %d
  push dword name         ; %s: the address of the text
  push dword f_text
  call [printf]
  add esp, 12

  xor eax, eax
  ret

.data
f_sum  db '%d + %d = %d', 10, 0
f_same db '%d is %x in hex and the character %c', 10, 0
f_text db 'The name %s has %d letters', 10, 0
name   db 'Asmonite', 0
; Lesson 2: printf with formats (x64). %d = decimal, %x = hex, %c = character, %s = text.

.format pe console
.arch x64
.import msvcrt.dll printf

.code
start:
  sub rsp, 40
  mov edx, 20             ; second argument: rdx (edx is its lower half)
  mov r8d, 22             ; third argument: r8
  mov r9d, edx
  add r9d, r8d            ; fourth argument: r9 = 20 + 22 = 42
  lea rcx, [f_sum]        ; first argument: the format
  call [printf]

  lea rcx, [f_same]
  mov edx, 42             ; %d
  mov r8d, 42             ; %x
  mov r9d, 42             ; %c
  call [printf]

  lea rcx, [f_text]
  lea rdx, [name]         ; %s: the address of the text
  mov r8d, 8              ; %d
  call [printf]

  xor eax, eax
  add rsp, 40
  ret

.data
f_sum  db '%d + %d = %d', 10, 0
f_same db '%d is %x in hex and the character %c', 10, 0
f_text db 'The name %s has %d letters', 10, 0
name   db 'Asmonite', 0
; Lesson 2: printf with formats (ARM 32, Thumb-2). %d = decimal, %x = hex, %c = character, %s = text.
; The arguments: r0 = format, r1, r2, r3 = the values after it.

.format pe console
.arch thumb
.import msvcrt.dll printf

.code
start:
  push {r4, lr}
  ldr r4, printf          ; r4 keeps the address of printf: r4 stays unchanged across calls

  movs r1, #20
  movs r2, #22
  adds r3, r1, r2         ; r3 = 20 + 22 = 42
  adr r0, f_sum           ; the format
  blx r4

  adr r0, f_same
  movs r1, #42            ; %d
  movs r2, #42            ; %x
  movs r3, #42            ; %c
  blx r4

  adr r0, f_text
  adr r1, name            ; %s: the address of the text
  movs r2, #8             ; %d
  blx r4

  movs r0, #0
  pop {r4, pc}

.data
align 4
f_sum  db '%d + %d = %d', 10, 0
align 4
f_same db '%d is %x in hex and the character %c', 10, 0
align 4
f_text db 'The name %s has %d letters', 10, 0
align 4
name   db 'Asmonite', 0
; Lesson 2: printf with formats (ARM 64). %d = decimal, %x = hex, %c = character, %s = text.
; The arguments: x0 = format, x1, x2, x3, ... = the values after it (w1 is the lower half of x1).

.format pe console
.arch arm64
.import msvcrt.dll printf

.code
start:
  stp x29, x30, [sp, #-32]!
  str x19, [sp, #16]
  adrp x19, printf
  ldr x19, [x19, :lo12:printf]  ; x19 keeps the address of printf: x19 stays unchanged across calls

  mov w1, #20
  mov w2, #22
  add w3, w1, w2                ; w3 = 20 + 22 = 42
  adrp x0, f_sum
  add x0, x0, :lo12:f_sum       ; the format
  blr x19

  adrp x0, f_same
  add x0, x0, :lo12:f_same
  mov w1, #42                   ; %d
  mov w2, #42                   ; %x
  mov w3, #42                   ; %c
  blr x19

  adrp x0, f_text
  add x0, x0, :lo12:f_text
  adrp x1, name
  add x1, x1, :lo12:name        ; %s: the address of the text
  mov w2, #8                    ; %d
  blr x19

  mov x0, #0
  ldr x19, [sp, #16]
  ldp x29, x30, [sp], #32
  ret

.data
f_sum  db '%d + %d = %d', 10, 0
f_same db '%d is %x in hex and the character %c', 10, 0
f_text db 'The name %s has %d letters', 10, 0
name   db 'Asmonite', 0

The arguments are pushed from right to left: the last value first, the format last. That way the format is on top of the stack and is the first argument for printf. Then add esp, 16 removes four arguments (4 bytes each); the third output has three, so add esp, 12. Recount the number when you change something – a wrong add esp destroys the stack.

For each call you load anew: the format into rcx, the values into rdx, r8, r9. The function may change these registers, so you set them again before every call. With edx, r8d and r9d you write the lower 32 bits; the upper ones are set to 0, which is enough for %d and %x. For %s, rdx holds a full 64-bit address, hence lea rdx, [name].

The format is in r0, the values in r1 to r3. printf changes these registers, so you set them again before every call. The address of printf is in r4 – that register survives the call unchanged, so it need not be loaded every time. adds r3, r1, r2 adds and writes the result into a third register without changing the summands. align 4 makes every text start at an address divisible by 4; adr requires that.

The format is in x0, the values in w1 to w3 (the lower 32 bits of x1 to x3). printf changes these registers, so you set them again before every call. The address of printf is in x19 – that register survives the call unchanged. For that the program saves the old x19 on the stack at the start and restores it at the end. add w3, w1, w2 adds and writes the result into a third register.

Try it in Asmonite

  1. Open a program (Hello, World! or formats) and assemble it with Ctrl+F9.
  2. Set a breakpoint with F5 on the line with call [printf] (on ARM: on blx or blr) and start with F9. The program stops before the call.
  3. Look at the stack on the right: on top is the address of the text, below it the values. That is exactly what you put there with push.
  4. Look at the registers rcx, rdx, r8 and r9: the arguments are there. In the memory window you see the text at the address in rcx.
  5. Look at the registers r0 to r3: the arguments are there. In the memory window you see the text at the address in r0.
  6. Look at the registers x0 to x3: the arguments are there. In the memory window you see the text at the address in x0.
  7. Press F8: the call runs, and the text appears in the output window. With F8 you go on through the next calls.
  8. Press F1 on the line with the call: on the right the help explains printf with its parameters.

What you see

Hello, World!
20 + 22 = 42
42 is 2a in hex and the character *
The name Asmonite has 8 letters

The number 42 reaches printf three times; what becomes of it is decided by the format alone: 42, 2a or the character with the code 42, an asterisk. The CPU does not know what a number "means" – the program decides that.

Exercises

1. Your name

In the formats example, change the name to your own. The output still gives the old number of letters. Why is that, and what would you have to do to make it right?

Solution

The number is a constant in the instruction (8); the program counts nothing. You have to adjust the number yourself. The program can count by itself with a loop that walks through the text up to the 0 – we will do that in a later lesson (loops, strings).

2. Negative numbers

With the same formats %d and %x, print the number −1. What appears, and why?

Solution

%d prints -1, %x prints ffffffff. In the register the same 32 bits are there in both cases, all of them 1. %d reads them as a number with sign (−1), %x as a number without sign (4294967295 = ffffffff). How negative numbers are stored comes in the lesson about bits.

3. Brain teaser: leading zeros

Print the number 255 in the form 0x000000FF – with a single format, without writing the zeros yourself. The format %x knows a width and a padding character for that.

Solution

The format 0x%08X: the characters 0x are written unchanged, %08X writes the number in capital hex digits, at least 8 places wide (8), padded on the left with zeros (0).

Remember