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.
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
push dword msgputs the address of the text on the stack as the argument.call [printf]calls the function through the import table; the square brackets mean "the value at this address", that is the address ofprintfthat Windows entered there.add esp, 4cleans up the argument.xor eax, eaxsetseaxto 0 (shorter thanmov eax, 0: "exclusive or" gives 0 when both bits are equal, and a register always equals itself): exit code 0.sub rsp, 40makes room on the stack.lea rcx, [msg](load effective address) loads the address ofmsgintorcx, the first argument.call [printf]calls the function through the import table.xor eax, eaxsetseaxto 0 (exit code 0),add rsp, 40gives the room back beforeretreturns.push {r4, lr}saveslr(andr4, to keep the stack aligned).adr r0, msgloads the address of the text intor0, relative topc– the program has no fixed address.ldr r3, printffetches the address ofprintffrom the import table,blx r3calls it.movs r0, #0is the exit code,pop {r4, pc}restoresr4and returns.stp x29, x30, [sp, #-16]!saves two registers on the stack. The address of the text is made in two steps:adrploads the address of the 4 KB page on whichmsglies,add ... :lo12:msgadds the position within the page. The address ofprintfcomes from the import table in the same way.blr x16calls it.mov x0, #0is the exit code,ldprestores the saved registers,retreturns.
Formats
The first argument of printf is a format text. Where it contains a %, printf inserts the next argument:
| Format | writes the argument as | Example for 42 |
|---|---|---|
%d | decimal number with sign | 42 |
%x | hex number (%X: with capital letters) | 2a |
%c | one character (the value is the character code) | * |
%s | text; 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
- Open a program (Hello, World! or formats) and assemble it with Ctrl+F9.
- Set a breakpoint with F5 on the line with
call [printf](on ARM: onblxorblr) and start with F9. The program stops before the call. - 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. - Look at the registers
rcx,rdx,r8andr9: the arguments are there. In the memory window you see the text at the address inrcx. - Look at the registers
r0tor3: the arguments are there. In the memory window you see the text at the address inr0. - Look at the registers
x0tox3: the arguments are there. In the memory window you see the text at the address inx0. - Press F8: the call runs, and the text appears in the output window. With F8 you go on through the next calls.
- Press F1 on the line with the call: on the right the help explains
printfwith 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
- Text in memory is bytes; a label in front of the text is its address, a 0 at the end marks the end.
- A function gets its arguments at fixed places – where is decided by the calling convention of the architecture (stack on the i386, registers on x64 and ARM).
- The caller must know which registers the function may change and which it may not.
printfdecides with the format what a value means:%d,%x,%c,%s.