Search code examples
linuxassemblyx86-64nasmtty

Reading a single-key input on Linux (without waiting for return) using x86_64 sys_call


I want to make Linux just take 1 keystroke from keyboard using sys_read, but sys_read just wait until i pressed enter. How to read 1 keystroke ? this is my code:

Mov EAX,3
Mov EBX,0
Mov ECX,Nada
Mov EDX,1
Int 80h

Cmp ECX,49
Je Do_C
Jmp Error

I already tried using BIOS interrupt but it's failed (Segmentation fault), I want capture number 1 to 8 input from keyboard.


Solution

  • Syscalls in 64-bit linux

    The tables from man syscall provide a good overview here:

    arch/ABI   instruction          syscall #   retval Notes
    ──────────────────────────────────────────────────────────────────
    i386       int $0x80            eax         eax
    x86_64     syscall              rax         rax    See below
    
    arch/ABI      arg1  arg2  arg3  arg4  arg5  arg6  arg7  Notes
    ──────────────────────────────────────────────────────────────────
    i386          ebx   ecx   edx   esi   edi   ebp   -
    x86_64        rdi   rsi   rdx   r10   r8    r9    -
    

    I have omitted the lines that are not relevant here. In 32-bit mode, the parameters were transferred in ebx, ecx, etc and the syscall number is in eax. In 64-bit mode it is a little different: All registers are now 64-bit wide and therefore have a different name. The syscall number is still in eax, which now becomes rax. But the parameters are now passed in rdi, rsi, etc. In addition, the instruction syscall is used here instead of int 0x80 to trigger a syscall.

    The order of the parameters can also be read in the man pages, here man 2 ioctl and man 2 read:

    int ioctl(int fd, unsigned long request, ...);
    ssize_t read(int fd, void *buf, size_t count);
    

    So here the value of int fd is in rdi, the second parameter in rsi etc.

    How to get rid of waiting for a newline

    Firstly create a termios structure in memory (in .bss section):

    termios:
      c_iflag resd 1   ; input mode flags
      c_oflag resd 1   ; output mode flags
      c_cflag resd 1   ; control mode flags
      c_lflag resd 1   ; local mode flags
      c_line  resb 1   ; line discipline
      c_cc    resb 19  ; control characters
    

    Then get the current terminal settings and disable canonical mode:

    ; Get current settings
    mov  eax, 16             ; syscall number: SYS_ioctl
    mov  edi, 0              ; fd:      STDIN_FILENO
    mov  esi, 0x5401         ; request: TCGETS
    mov  rdx, termios        ; request data
    syscall
    
    ; Modify flags
    and byte [c_lflag], 0FDh  ; Clear ICANON to disable canonical mode
    
    ; Write termios structure back
    mov  eax, 16             ; syscall number: SYS_ioctl
    mov  edi, 0              ; fd:      STDIN_FILENO
    mov  esi, 0x5402         ; request: TCSETS
    mov  rdx, termios        ; request data
    syscall
    

    Now you can use sys_read to read in the keystroke:

    mov  eax, 0              ; syscall number: SYS_read
    mov  edi, 0              ; int    fd:  STDIN_FILENO
    mov  rsi, buf            ; void*  buf
    mov  rdx, len            ; size_t count
    syscall
    

    Afterwards check the return value in rax: It contains the number of characters read.

    (Or a -errno code on error, e.g. if you closed stdin by running ./a.out <&- in bash. Use strace to print a decoded trace of the system calls your program makes, so you don't need to actually write error handling in toy experiments.)


    References: