views:

278

answers:

3

Hi, 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.

Thanks a lot,

A: 

The easy way: For a text-mode program, use libncurses to access the keyboard; for a graphical program, use Gtk+.

The hard way: Assuming a text-mode program, you have to tell the kernel that you want single-character input, and then you have to do a whole lot of bookkeeping and decoding. It's really complicated. There is no equivalent of the good old DOS getch() routine. You can start learning about how to do it here: Terminal I/O. Graphical programs are even more complicated; the lowest-level API for that is Xlib.

Either way, you're going to go mad coding whatever this is in assembly; use C instead.

Zack
While everything you said is correct for C, it's not really a relevant answer if the OP is trying to learn assembly.
Tyler McHenry
That's because the OP *should not be programming in assembly language*. The only good reason to hand-code anything in assembly language anymore is if it's a performance-critical computational subroutine, or one of the very few low-level pieces of an operating system kernel that *can't* be coded any other way. User interaction does not qualify. What the OP is trying to do is not even a good *learning exercise* under Unix.
Zack
Having said that, there is nothing stopping the OP from writing assembly language that calls libncurses, although it seems like a profound waste of time and sanity points to me (but it wouldn't be as bad as assembly language that does Unix terminal I/O by hand).
Zack
@Zack I wouldn't call this a good learning exercise for Unix programming, or for assembly, but it's certainly a good learning exercise. While assembly is not a practical choice for the vast, vast majority of applications, I don't think that learning how to do things you wouldn't normally do in assembly is a waste of time. It gives you an interesting perspective on what is possible on what levels of abstraction, and on how your machine and OS work together.
jeremiahd
+3  A: 

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.

Callum
A: 

I needed to do this recently, and inspired by Callum's excellent answer, I wrote the following:

termios:        times 36 db 0
stdin:          equ 0
ICANON:         equ 1<<1
ECHO:           equ 1<<3

canonical_off:
        call read_stdin_termios

        ; clear canonical bit in local mode flags
        push rax
        mov eax, ICANON
        not eax
        and [termios+12], eax
        pop rax

        call write_stdin_termios
        ret

echo_off:
        call read_stdin_termios

        ; clear echo bit in local mode flags
        push rax
        mov eax, ECHO
        not eax
        and [termios+12], eax
        pop rax

        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

read_stdin_termios:
        push rax
        push rbx
        push rcx
        push rdx

        mov eax, 36h
        mov ebx, stdin
        mov ecx, 5401h
        mov edx, termios
        int 80h

        pop rdx
        pop rcx
        pop rbx
        pop rax
        ret

write_stdin_termios:
        push rax
        push rbx
        push rcx
        push rdx

        mov eax, 36h
        mov ebx, stdin
        mov ecx, 5402h
        mov edx, termios
        int 80h

        pop rdx
        pop rcx
        pop rbx
        pop rax
        ret

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.

Richard Fearn