From man 2 write
, you can see the signature of write
is,
ssize_t write(int fd, const void *buf, size_t count);
It takes a pointer (const void *buf
) to a buffer in memory. You can't pass it a char
by value, so you have to store it to memory and pass a pointer.
(Don't print one char at a time unless you only have one to print, that's really inefficient. Construct a buffer in memory and print that. e.g. this x86-64 Linux NASM function: How do I print an integer in Assembly Level Programming without printf from the c library? (itoa, integer to decimal ASCII string))
A NASM version of GCC: putchar(char) in inline assembly:
; x86-64 System V calling convention: input = byte in DIL
; clobbers: RDI, RSI, RDX, RCX, R11 (last 2 by syscall itself)
; returns: RAX = write return value: 1 for success, -1..-4095 for error
writechar:
mov byte [rsp-4], dil ; store the char from RDI into the red zone (below RSP)
mov eax, 1 ; __NR_write syscall number from unistd_64.h
mov edi, 1 ; EDI = fd=1 = stdout
lea rsi, [rsp-4] ; RSI = buf
mov edx, edi ; RDX = len = 1 happens to be the same as fd and call #
syscall ; rax = write(1, buf, 1)
ret
If you do pass an invalid pointer in RSI, such as '2'
(integer 50
), the system call will return -EFAULT
(-14
) in RAX. (The kernel returns error codes on bad pointers to system calls, instead of delivering a SIGSEGV like it would if you deref in user-space).
See also What are the return values of system calls in Assembly?
Instead of writing code to check return values, in toy programs / experiments you should just run them under strace ./a.out
. If you're writing your own _start
without libc there won't be any other system calls during startup that you don't make yourself, so it's very easy to read the output, otherwise there are a bunch of startup system calls made by libc before your code. How should strace be used?