1

I am trying to use the write syscall in order to reproduce the putchar function behavior which prints a single character. My code is as follows,

asm_putchar:
  push    rbp
  mov     rbp, rsp

  mov     r8, rdi

call:
  mov     rax, 1
  mov     rdi, 1
  mov     rsi, r8
  mov     rdx, 1
  syscall

return:
  mov     rsp, rbp
  pop     rbp
  ret
Evan Carroll
  • 78,363
  • 46
  • 261
  • 468
Lucas
  • 33
  • 7
  • Have you verified that `rdi` contains a valid string address? Also, you're running this on a 64-bit Linux system, right? – Michael Jun 04 '18 at 13:08
  • 1
    Can you write it in C? Note that `putchar` accepts a character but `write` expects a pointer. – Jester Jun 04 '18 at 13:11
  • Yes I'm running on a 64-bit Linux, I know that write takes a pointer but most of people using this syscall use a data section to store strings – Lucas Jun 04 '18 at 13:25
  • Can't you use the stack instead? – Michael Jun 04 '18 at 13:28
  • It consists to push rax and use the syscall again ? – Lucas Jun 04 '18 at 13:31
  • 2
    @Lucas Push the desired character on the stack, then pass a pointer to that character to the system call. – fuz Jun 04 '18 at 13:35
  • thanks, i'll try that – Lucas Jun 04 '18 at 13:38
  • Just to be sure, can I use the rbp pointer register (pushed at begining) and move rbp, rdi. And so use rbp instead of r8 in my previous exemple ? – Lucas Jun 04 '18 at 13:47
  • 1
    you don't need additional register. You need memory. Also you don't need to touch `rbp` at all (including push/pop). You can just `push rdi` at the beginning to store the character into the stack (= memory), then `mov rsi,rsp` will get the memory address of stored char into `rsi`. And remember to release the stored character after `sys_write`, like `add rsp,8` (or `pop` into some register which you don't need any more, `pop rcx` is probably good candidate, as `rcx` was modified by `syscall` already). – Ped7g Jun 04 '18 at 14:28
  • Related: [GCC: putchar(char) in inline assembly](https://stackoverflow.com/a/50691215). See the compiler asm output in my answer for a complete asm function that implements `return sys_write(1, &my_char, 1);`. On the Godbolt link, you can click the "intel" button to get Intel syntax instead of AT&T. – Peter Cordes Jun 05 '18 at 02:42
  • Strangely I couldn't find an exact duplicate of this very simple question. It's been asked and answered many times for the `int 0x80` 32-bit ABI, but possibly not for x86-64. – Peter Cordes Jun 05 '18 at 02:51

1 Answers1

6

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?

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847