1

DOS has int 21h / AH=08H: Console input without echo.

Is there something similar for Linux? If I need to process the entered value before it is displayed in the terminal.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
  • It is possible to do this on Linux. You have to configure termios into a different mode. Or use the curses library which provides functionality like this in an easily accessible way. – fuz Mar 25 '21 at 10:42
  • 1
    It's not "interrupt 08h" it is "(DOS) interrupt 21h service 08h". – ecm Mar 25 '21 at 11:17

1 Answers1

5

Under Linux, it is the tty that buffers the typed chars before "sending" them to the requesting program.
This is controlled through the terminal mode: raw (no buffering) or cooked (also known respectively as non-canonical and canonical mode).

These modes are actually attributes of the tty, which can be controlled with tcgetattr and tcsetattr.

The code to set the terminal in non-canonical mode without echo can be found, for example, here (more info on the VTIME and VMIN control chars can be found here).

That's C, so we need to translate it into assembly. From the source of tcgetattr we can see that the tty attributes are retrieved through an IOCTL to stdin with the command TCGETS (value 0x5401) and, similarly, they are set with an IOCTL with the command TCSETS (value 0x5402).
The structure read is not the struct termios but struct __kernel_termios which is basically a shortened version of the former. The IOCTL must be sent to the stdin file (file descriptor STDIN_FILENO of value 0).
Knowing how to implement tcgetattr and tcsetattr we only need to get the value of the constants (like ICANON and similar).
I advise using a compiler (e.g. here) to find the values of the public constants and to check the structure's offsets.
For non-public constants (not visible outside their translation units) we must resort to reading the source (this is not particularly hard, but care must be taken to find the right source).

Below a 64-bit program that invokes the IOCTLs to get-modify-set the TTY attribute in order to enable the raw mode.
Then the program waits for a single char and displays it incremented (e.g. a -> b).
Note that this program has been tested under Linux (5.x) and, as the various constants change values across different clones of Unix, it is not portable.
I used NASM and defined a structure for struct __kernel_termios, I also used a lot of symbolic constants to make the code more readable. I don't really like using structures in assembly but NASM ones are just a thin macro layer (it's better to get used to them if you aren't already).
Finally, I assume familiarity with 64-bit Linux assembly programming.

BITS 64

GLOBAL _start

;
; System calls numbers
;
%define SYS_READ    0
%define SYS_WRITE   1
%define SYS_IOCTL   16
%define SYS_EXIT    60

;
; __kernel_termios structure
;
%define KERNEL_NCC 19
struc 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    KERNEL_NCC      ;control characters
endstruc

;
; IOCTL commands
;
%define TCGETS      0x5401
%define TCSETS      0x5402

;
; TTY local flags
;
%define ECHO        8
%define ICANON      2

;
; TTY control chars
;
%define VMIN        6
%define VTIME       5

;
; Standard file descriptors
;
%define STDIN_FILENO    0
%define STDOUT_FILENO   1


SECTION .bss

    ;The char read (reserve a DWORD to make termios_data be aligned on DWORDs boundary)
    data        resd 1
    
    ;The TTY attributes
    termios_data    resb termios_size
    
SECTION .text

_start:
    ;
    ;Get the terminal settings by sending the TCGETS IOCTL to stdin
    ;
    mov edi, STDIN_FILENO       ;Send IOCTL to stdin (Less efficient but more readable)
    mov esi, TCGETS         ;The TCGETS command
    lea rdx, [REL termios_data] ;The arg, the buffer where to store the TTY attribs
    
    mov eax, SYS_IOCTL      ;Do the syscall     
    syscall

    ;
    ;Set the raw mode by clearing ECHO and ICANON and setting VMIN = 1, VTIME = 0
    ;
    and DWORD [REL termios_data + termios.c_lflag], ~(ICANON | ECHO)    ;Clear ECHO and ICANON
    mov BYTE [REL termios_data + termios.c_cc + VMIN], 1
    mov BYTE [REL termios_data + termios.c_cc + VTIME], 0
    
    ;
    ;Set the terminal settings
    ;
    mov edi, STDIN_FILENO       ;Send to stdin (Less efficient but more readable)
    mov esi, TCSETS         ;Use TCSETS as the command
    lea rdx, [REL termios_data] ;Use the same data read (and altered) before
    
    mov eax, SYS_IOCTL      ;Do the syscall
    syscall

    ;
    ;Read a char
    ;
    mov edi, STDIN_FILENO       ;Read from stdin (Less efficient but more readable)
    lea rsi, [REL data]     ;Read into data
    mov edx, 1          ;Read only 1 char
    
    mov eax, SYS_READ       ;Do the syscall (Less efficient but more readable)
    syscall
    
    ;
    ;Increment the char (as an example)
    ;
    inc BYTE [REL data]
    
    ;
    ;Print the char
    ;
    mov edi, STDOUT_FILENO      ;Write to stdout
    lea rsi, [REL data]     ;Write the altered char
    mov edx, 1          ;Only 1 char to write
    
    mov eax, SYS_WRITE      ;Do the syscall
    syscall
    
    ;
    ;Restore the terminal settins (similar to the code above)
    ;
    mov edi, STDIN_FILENO
    mov esi, TCGETS
    lea rdx, [REL termios_data]
    mov eax, SYS_IOCTL
    syscall

    ;Set ECHO and ICANON
    or DWORD [REL termios_data + termios.c_lflag], ICANON | ECHO

    mov edi, STDIN_FILENO   
    mov esi, TCSETS
    lea rdx, [REL termios_data]
    mov eax, SYS_IOCTL
    syscall 
    
    ;
    ;Exit
    ;
    xor edi, edi
    mov eax, SYS_EXIT
    syscall
Margaret Bloom
  • 41,768
  • 5
  • 78
  • 124
  • 1
    1. "make termios_data be aligned on DWORDs boundary" You can use `alignb 4` to explicitly align the following variable. 2. You can use `default rel` if you want all disp32-only memory accesses to use rip-relative addressing, instead of having to specify for every access. – ecm Mar 25 '21 at 16:20
  • 1
    @ecm I already know that, thanks. `alignb 4` is longer to type than writing 4 instead of 1. I didn't use `DEFAULT REL` for no good reason. I just didn't anticipate I was going to use so "many" memory access and later on didn't feel like removing all those `REL` in favour of `DEFAULT REL`. – Margaret Bloom Mar 25 '21 at 17:10
  • 1
    `resd 1` doesn't imply alignment; maybe you're thinking of classic MIPS assembly (like MARS) where `.word` magically aligns? Example: https://godbolt.org/z/fbe69ns5a shows `resb 1` / `resd 1` ending at address `5`, not `8`. OTOH, it's probably safe to assume that the start of this file's `.bss` is 4-byte aligned. Yup, just tried linking together two files with those 5-byte BSS sections, and the `foo2:` was at address 8, not the same as `bar1:` at address 5, so `ld` padded to align the start of the BSS section of the second `.o`. – Peter Cordes Mar 25 '21 at 18:02
  • @PeterCordes I don't get your point. If x % 4 == 0 then (x+4) % 4 == 0. The `.bss` is aligned on 4-bytes on NASM (see [this](https://github.com/netwide-assembler/nasm/blob/858bc9d6b9d8d77961a375e792e69666d613383f/output/outelf.c#L255)) and I reserve 4 bytes, so `termios_data` is still aligned on 4-bytes. Am I missing something? – Margaret Bloom Mar 25 '21 at 20:11
  • Oh, I see what you mean now. Given known starting alignment, you just manually reserve 4 bytes, instead of reserving one and letting `align` pad for you. That's fine, but `align 4` would be more compact and clear to humans than the whole long comment. (Especially since I (and perhaps others) misinterpreted it as a claim that `resd 1` would force alignment where `resb 4` or `resb termios_size` wouldn't have.) – Peter Cordes Mar 25 '21 at 20:39