Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I'm using nasm under ubuntu. By the way i need to get single input character from user's keyboard (like when a program ask you for y/n ?) so as key pressed and without pressing enter i need to read the entered character. I googled it a lot but all what i found was somehow related to this line (int 21h) which result in "Segmentation Fault". Please help me to figure it out how to get single character or how to over come this segmentation fault.

like image 747
Roozbeh Sharafi Avatar asked Jul 22 '10 00:07

Roozbeh Sharafi


2 Answers

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.

like image 141
Callum Avatar answered Jan 04 '23 05:01

Callum


I needed to do this recently, and inspired by Callum's excellent answer, I wrote the following (NASM for x86-64):

DEFAULT REL

section .bss
termios:        resb 36

stdin_fd:       equ 0           ; STDIN_FILENO
ICANON:         equ 1<<1
ECHO:           equ 1<<3

section .text
canonical_off:
        call read_stdin_termios

        ; clear canonical bit in local mode flags
        and dword [termios+12], ~ICANON

        call write_stdin_termios
        ret

echo_off:
        call read_stdin_termios

        ; clear echo bit in local mode flags
        and dword [termios+12], ~ECHO

        call write_stdin_termios
        ret

canonical_on:
        call read_stdin_termios

        ; set canonical bit in local mode flags
        or dword [termios+12], ICANON

        call write_stdin_termios
        ret

echo_on:
        call read_stdin_termios

        ; set echo bit in local mode flags
        or dword [termios+12], ECHO

        call write_stdin_termios
        ret

; clobbers RAX, RCX, RDX, R8..11 (by int 0x80 in 64-bit mode)
; allowed by x86-64 System V calling convention    
read_stdin_termios:
        push rbx

        mov eax, 36h
        mov ebx, stdin_fd
        mov ecx, 5401h
        mov edx, termios
        int 80h            ; ioctl(0, 0x5401, termios)

        pop rbx
        ret

write_stdin_termios:
        push rbx

        mov eax, 36h
        mov ebx, stdin_fd
        mov ecx, 5402h
        mov edx, termios
        int 80h            ; ioctl(0, 0x5402, termios)

        pop rbx
        ret

(Editor's note: don't use int 0x80 in 64-bit code: What happens if you use the 32-bit int 0x80 Linux ABI in 64-bit code? - it would break in a PIE executable (where static addresses aren't in the low 32 bits), or with the termios buffer on the stack. It does actually work in a traditional non-PIE executable, and this version can be easily ported to 32-bit mode.)

You can then do:

call canonical_off

If you're reading a line of text, you probably also want to do:

call echo_off

so that each character isn't echoed as it's typed.

There may be better ways of doing this, but it works for me on a 64-bit Fedora installation.

More information can be found in the man page for termios(3), or in the termbits.h source.

like image 28
Richard Fearn Avatar answered Jan 04 '23 06:01

Richard Fearn