How do I print a string to the terminal in x86-64 assembly (NASM) without syscall?
I'm new to assembly and want to first try to get an intuitive idea of how printing a line on the terminal will work, without going through the operating system (Linux or OSX) abstraction.
tl; dr How do you write to stdout (print to terminal) in x86-64 assembly with NASM on OSX, at the lowest level (no syscall)? How does BareMetal OS work?
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
There they use a syscall
string to print, which relies on the operating system . I'm not looking for this, but for how to write a string to stdout directly, at the lowest level.
There is an exokernel project, BareMetal OS , which I think does this. Although, since I'm new to assembly, I don't know enough yet to understand how they do it. From what appears to be two important files:
It seems that the relevant code for printing is this (extracted from these 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 (although I don't know yet, since I'm a beginner).
So my question is, according to this BareMetal OS snippet, how do you write to stdout (print to terminal) in x86-64 build with NASM on OSX?
source to share
This is a good exercise. You will use syscall
(you cannot stdout
otherwise access ), but you can write bare metal without an external library providing an inference routine (like a call printf
). As an example of basic "bare metal" write in stdout
x86_64, I put together an 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 in stdout
. strprn
Any string will be written below the function stdout
. It calls strsz
to get the length while keeping the destination index on the stack. This reduces the task of writing the string to stdout
and prevents a lot of repetition 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
macros, NASM provides a convenient solution. Example strn
(short for string_n
). It takes two arguments, the address 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 indenting, newlines, or writing full lines. You can generalize it by passing 3 arguments including the destination for rdi
.
source to share