How to Read Single Character Input from Keyboard Using Nasm (Assembly) Under Ubuntu

How do i read single character input from keyboard using nasm (assembly) under ubuntu?

It can be done from assembly, but it isn't easy. You can't use int 21h, that's a DOS system call and it isn't available under Linux.

To get characters from the terminal under UNIX-like operating systems (such as Linux), you read from STDIN (file number 0). Normally, the read system call will block until the user presses enter. This is called canonical mode. To read a single character without waiting for the user to press enter, you must first disable canonical mode. Of course, you'll have to re-enable it if you want line input later on, and before your program exits.

To disable canonical mode on Linux, you send an IOCTL (IO ControL) to STDIN, using the ioctl syscall. I assume you know how to make Linux system calls from assembler.

The ioctl syscall has three parameters. The first is the file to send the command to (STDIN), the second is the IOCTL number, and the third is typically a pointer to a data structure. ioctl returns 0 on success, or a negative error code on fail.

The first IOCTL you need is TCGETS (number 0x5401) which gets the current terminal parameters in a termios structure. The third parameter is a pointer to a termios structure. From the kernel source, the termios structure is defined as:

struct termios {
tcflag_t c_iflag; /* input mode flags */
tcflag_t c_oflag; /* output mode flags */
tcflag_t c_cflag; /* control mode flags */
tcflag_t c_lflag; /* local mode flags */
cc_t c_line; /* line discipline */
cc_t c_cc[NCCS]; /* control characters */
};

where tcflag_t is 32 bits long, cc_t is one byte long, and NCCS is currently defined as 19. See the NASM manual for how you can conveniently define and reserve space for structures like this.

So once you've got the current termios, you need to clear the canonical flag. This flag is in the c_lflag field, with mask ICANON (0x00000002). To clear it, compute c_lflag AND (NOT ICANON). and store the result back into the c_lflag field.

Now you need to notify the kernel of your changes to the termios structure. Use the TCSETS (number 0x5402) ioctl, with the third parameter set the the address of your termios structure.

If all goes well, the terminal is now in non-canonical mode. You can restore canonical mode by setting the canonical flag (by ORing c_lflag with ICANON) and calling the TCSETS ioctl again. always restore canonical mode before you exit

As I said, it isn't easy.

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


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:

  • What happens if you use the 32-bit int 0x80 Linux ABI in 64-bit code?
  • Why does the sys_read system call end when it detects a new line?
  • How do i read single character input from keyboard using nasm (assembly) under ubuntu?
  • Using the raw keyboard mode under Linux (external site with example in 32-bit assembly)

Basic input with x64 assembly code

In your first code section you have to set the SYS_CALL to 0 for SYS_READ (as mentioned rudimentically in the other answer).

So check a Linux x64 SYS_CALL list for the appropriate parameters and try

_start:
mov rax, 0 ; set SYS_READ as SYS_CALL value
sub rsp, 8 ; allocate 8-byte space on the stack as read buffer
mov rdi, 0 ; set rdi to 0 to indicate a STDIN file descriptor
lea rsi, [rsp] ; set const char *buf to the 8-byte space on stack
mov rdx, 1 ; set size_t count to 1 for one char
syscall

How to read input from stdin in assembly, character by character

First, when the user hits Enter, you will see LF (\n, 0xa), not CR (\r, 0xd). This may explain why your program doesn't exit when you think it should.

As far as why extra characters go to the shell, this is about how the OS does terminal input. It accumulates keystrokes from the terminal into a kernel buffer until Enter is pressed, then makes the whole buffer available to be read by read(). This allows things like backspace to work transparently without requiring the application to explicitly code it, but it does mean that you can't literally read one keystroke at a time, as you're noticing.

If your program exits while the buffer still contains characters, then those characters will be read by the next program which attempts to read from the device, which in your case will be the shell. Most programs that read stdin avoid this by continuing to read and process data until end-of-file is seen (read() returning 0) which happens for a terminal when the user presses Ctrl-D.

If you really need to process input character-by-character, you need to set the terminal to non-canonical mode, but many things will be different in this case.

PHP CLI: How to read a single character of input from the TTY (without waiting for the enter key)?

The solution for me was to set -icanon mode on the TTY (using stty). Eg.:

stty -icanon

So, the the code that now works is:

#!/usr/bin/php
<?php
system("stty -icanon");
echo "input# ";
while ($c = fread(STDIN, 1)) {
echo "Read from STDIN: " . $c . "\ninput# ";
}
?>

Output:

input# fRead from STDIN: f
input# oRead from STDIN: o
input# oRead from STDIN: o
input#
Read from STDIN:

input#

Props to the answer given here:

Is there a way to wait for and get a key press from a (remote) terminal session?

For more information, see:

http://www.faqs.org/docs/Linux-HOWTO/Serial-Programming-HOWTO.html#AEN92

Don't forget to restore the TTY when you're done with it...

Restoring the tty configuration

Resetting the terminal back to the way it was can be done by saving the tty state before you make changes to it. You can then restore to that state when you're done.

For example:

<?php

// Save existing tty configuration
$term = `stty -g`;

// Make lots of drastic changes to the tty
system("stty raw opost -ocrnl onlcr -onocr -onlret icrnl -inlcr -echo isig intr undef");

// Reset the tty back to the original configuration
system("stty '" . $term . "'");

?>

This is the only way to preserve the tty and put it back how the user had it before you began.

Note that if you're not worried about preserving the original state, you can reset it back to a default "sane" configuration simply by doing:

<?php

// Make lots of drastic changes to the tty
system("stty raw opost -ocrnl onlcr -onocr -onlret icrnl -inlcr -echo isig intr undef");

// Reset the tty back to sane defaults
system("stty sane");

?>


Related Topics



Leave a reply



Submit