How to print a string to the terminal in x86-64 assembly (NASM) without syscall?

28,379

This is a good exercise. You will use syscall (you cannot access stdout otherwise), but you can do a "bare-metal" write without any external library providing the output routine (like calling printf). As an example of the basic "bare-metal" write to stdout in x86_64, I put together a example without any internal or system function calls:

section .data
    string1 db  0xa, "  Hello StackOverflow!!!", 0xa, 0xa, 0

section .text
    global _start

    _start:
        ; calculate the length of string
        mov     rdi, string1        ; string1 to destination index
        xor     rcx, rcx            ; zero rcx
        not     rcx                 ; set rcx = -1
        xor     al,al               ; zero the al register (initialize to NUL)
        cld                         ; clear the direction flag
        repnz   scasb               ; get the string length (dec rcx through NUL)
        not     rcx                 ; rev all bits of negative results in absolute value
        dec     rcx                 ; -1 to skip the null-terminator, rcx contains length
        mov     rdx, rcx            ; put length in rdx
        ; write string to stdout
        mov     rsi, string1        ; string1 to source index
        mov     rax, 1              ; set write to command
        mov     rdi,rax             ; set destination index to rax (stdout)
        syscall                     ; call kernel

        ; exit 
        xor     rdi,rdi             ; zero rdi (rdi hold return value)
        mov     rax, 0x3c           ; set syscall number to 60 (0x3c hex)
        syscall                     ; call kernel

; Compile/Link
;
; nasm -f elf64 -o hello-stack_64.o hello-stack_64.asm
; ld  -o hello-stack_64 hello-stack_64.o

output:

$ ./hello-stack_64

  Hello StackOverflow!!!

For general use, I split the process into two parts (1) getting the length and (2) writing to stdout. Below the strprn function will write any string to stdout. It calls strsz to get the length while preserving the destination index on the stack. This reduces the task of writing a string to stdout and prevents a lot of repitition in your code.

; szstr computes the lenght of a string.
; rdi - string address
; rdx - contains string length (returned)
section .text
        strsz:
                xor     rcx, rcx                ; zero rcx
                not     rcx                     ; set rcx = -1 (uses bitwise id: ~x = -x-1)
                xor     al,al                   ; zero the al register (initialize to NUL)
                cld                             ; clear the direction flag
                repnz scasb                     ; get the string length (dec rcx through NUL)
                not     rcx                     ; rev all bits of negative -> absolute value
                dec     rcx                     ; -1 to skip the null-term, rcx contains length
                mov     rdx, rcx                ; size returned in rdx, ready to call write
                ret

; strprn writes a string to the file descriptor.
; rdi - string address
; rdx - contains string length
section .text
        strprn:
                push    rdi                     ; push string address onto stack
                call    strsz                   ; call strsz to get length
                pop     rsi                     ; pop string to rsi (source index)
                mov     rax, 0x1                ; put write/stdout number in rax (both 1)
                mov     rdi, rax                ; set destination index to rax (stdout)
                syscall                         ; call kernel
                ret

To further automate general output to stdout NASM macros provide a convenient solution. Example strn (short for string_n). It takes two arguments, the addresses of the string, and the number of characters to write:

%macro  strn    2
        mov     rax, 1
        mov     rdi, 1
        mov     rsi, %1
        mov     rdx, %2
        syscall
%endmacro

Useful for indents, newlines or writing complete strings. You could generalize further by passing 3 arguments including the destination for rdi.

Share:
28,379
Lance
Author by

Lance

I read JavaScript and am attempting to emit Assembly from it.

Updated on October 09, 2020

Comments

  • Lance
    Lance over 3 years

    I am new to assembly, and want to first try to get an intuitive feel for how printing a string to the terminal would work, without going through the operating system abstraction (Linux or OSX).

    (Editor's note: the accepted answer only covers Linux. x86-64 MacOS uses a similar system-calling convention but different call numbers.)

    tl;dr How do you write to stdout (print to the terminal) in x86-64 assembly with NASM on OSX, at the lowest level possible (i.e. without syscall)? How is BareMetal OS doing this?

    Most examples show something like this:

    global start
    
    section .text
    start:
      mov rax, 1
      mov rdi, 1
      mov rsi, message
      mov rdx, 13
      syscall
    
      mov eax, 60
      xor rdi, rdi
      syscall
    
    message:
      db "Hello world", 10
    

    In there, they are using syscall to print the string, which is relying on the operating system. I am not looking for that, but for how to write a string to stdout directly, at the lowest level possible.

    There is this exokernel project, BareMetal OS that I think is doing this. Though since I am new to assembly, I don't know enough yet to figure out how they accomplish this. From what it seems though, the two important files are:

    It seems the relevant code to print is this (extracted from those two files):

    ;
    ; Display text in terminal.
    ;
    ;  IN:  RSI = message location (zero-terminated string)
    ; OUT:  All registers preserved
    ;
    
    os_output:
      push rcx
    
      call os_string_length
      call os_output_chars
      
      pop rcx
      ret
    
    ; 
    ; Displays text.
    ;
    ;  IN:  RSI = message location (an ASCII string, not zero-terminated)
    ; RCX = number of chars to print
    ; OUT:  All registers preserved
    ;
    
    os_output_chars:
      push rdi
      push rsi
      push rcx
      push rax
    
      cld ; Clear the direction flag.. we want to increment through the string
      mov ah, 0x07 ; Store the attribute into AH so STOSW can be used later on
    
    ;
    ; Return length of a string.
    ;
    ;  IN:  RSI = string location
    ; OUT:  RCX = length (not including the NULL terminator)
    ;
    ; All other registers preserved
    ;
    
    os_string_length:
      push rdi
      push rax
    
      xor ecx, ecx
      xor eax, eax
      mov rdi, rsi
      not rcx
      cld
      repne scasb ; compare byte at RDI to value in AL
      not rcx
      dec rcx
    
      pop rax
      pop rdi
      ret
    

    But that doesn't look complete to me (though I wouldn't know yet since I'm new).

    So my question is, along the lines of that BareMetal OS snippet, how do you write to stdout (print to the terminal) in x86-64 assembly with NASM on OSX?

  • Seva Alekseyev
    Seva Alekseyev over 9 years
    Hardly bare metal, but the lowest level possible on a bona fide protected mode OS such as Linux.