This is the fourth post about x86 assembly. In this post I will show an example how to call the printf function from the C standard library in assembly code.

section .text
    default rel
    extern printf
    global main
main:
    ; Create a stack-frame, re-aligning the stack to 16-byte alignment before calls
    push rbp

    mov	rdi, fmt
    mov	rsi, message
    mov	rax, 0

    ; Call printf
    call printf wrt ..plt
    
    pop	rbp		; Pop stack

    mov	rax,0	; Exit code 0
    ret			; Return
section .data
    message:  db        "Hello, World", 10, 0
    fmt:    db "%s", 10, 0
$ nasm printf.asm -f elf64 -o printf.o
$ gcc printf.o
$ ./a.out
# Hello world

The first part

  default rel
  extern printf
  global main

The default rel is a nasm assembly directive. It tells nasm to use rip relative adressing. In short it tells the assembler to rewrite the references in instructions that uses our fmt and message constants relative to the instruction pointer. This is needed because default for the linker in 64-bit linux is to use position-independent code.

The extern printf part tells the assembler that this symbol exists outside of this file and needs to be referenced at a later stage.

Last row here is global main which is needed for gcc and it’s the entrypoint for libc.

main

First we need to align the stack because the x86_64 ABI requires the stackpointer to always be 16-byte aligned so therefor we push a value to it.

Then we prepare our registers for the function call to printf and then we call printf wrt ..plt. What happens here is that we load printf from the libc shared library and this is a little bit complicated. But very briefly it says call printf with relation to procedure linkage table. The PCL will then the first time printf is called resolve where printf is in memory with the help of the dynamic link loader in linux. It then stores that adress for future calls. We could link printf statically which would copy the code of printf into the executable.

Lastly we need to set return value of main to zero and do a ret.

call and ret

When you do a call operation two things happen. First the instruction on the next instruction is pushed onto the stack. In our example above it will be the memory adress of the instruction pop rbp. Secondly it will jump to the memory adress where printf starts. When printf is done it will do a ret instruction which will pop our memory adress from the stack and jump back to our main function.