Author Topic: Linux x64 - Write to STDOUT while waiting for user input  (Read 18358 times)

Offline isc.jcl.apex

  • Jr. Member
  • *
  • Posts: 4
Linux x64 - Write to STDOUT while waiting for user input
« on: May 05, 2016, 03:41:22 AM »
Good night, I need some help.

I won't lie, this is an assignment for my university, but I haven't found anything related to.

What I need it's to print messages to the terminal (in a loop style) until the user "halts" the execution pressing ESC key.

This is what I have so far, I'm not asking for anyone to solve my issues, but it would be nice to point me into the right direction.

Code: [Select]
section .data
    ICANON:     equ     1<<1       
    TCGETS:     equ     0x5401     
    TCSETS      equ     0x5402     
    sys_exit:   equ     60
    sys_read:   equ     0
    sys_write:  equ     1
    sys_open:   equ     2
    sys_close:  equ     3
    sys_ioctl:  equ     16
    stdin:      equ     0
    stdout:     equ     1
    s1:         db      "Cadena: 1", 10
    s1l:        equ     $-s1
    s2:         db      "Cadena: 2", 10
    s2l:        equ     $-s2

section .bss
    buffer:     resb    2
    termios:    resb    36

section .text

write_stdout:
    mov rax, sys_write
    mov rdi, stdout
    syscall
    ret

get_keycode:
    mov rax, sys_read
    mov rdi, stdin
    mov rsi, buffer
    mov rdx, 1
    syscall
    ret

write_stdin_termios:
    mov rax, sys_ioctl
    mov rdi, stdin
    mov rsi, TCSETS
    mov rdx, termios
    syscall
    ret

read_stdin_termios:
    mov rax, sys_ioctl
    mov rdi, stdin
    mov rsi, TCGETS
    mov rdx, termios
    syscall
    ret

canonical_off:
    call read_stdin_termios
    mov rax, ICANON
    not rax
    and [termios+12], rax
    call write_stdin_termios
    ret

canonical_on:
    call read_stdin_termios
    or dword [termios+12], ICANON
    call write_stdin_termios
    ret

GLOBAL _start

_start:
    nop
    nop
    call canonical_off
    xor rsi, rsi

    read_code:
        mov rsi, s1
        mov rdx, s1l
        call write_stdout
        cmp rsi, 24
        je end
        call get_keycode

    mov al, byte[buffer]
    cmp al, "e"
    je end
    jmp read_code

end:
    mov rax, sys_exit
    mov rdi, 0
    syscall

Thanks in advance.

Offline Frank Kotler

  • NASM Developer
  • Hero Member
  • *****
  • Posts: 2667
  • Country: us
Re: Linux x64 - Write to STDOUT while waiting for user input
« Reply #1 on: May 05, 2016, 05:43:34 AM »
Hi isc.jcl.apex,

Welcome to the Forum.  I'm not very familiar with 64-bit code, and can't test it for ya. I've done similar things in 32-bit code. I don't understand this part:

Code: [Select]
_start:
    nop
    nop
    call canonical_off
    xor rsi, rsi

    read_code:
        mov rsi, s1
        mov rdx, s1l
        call write_stdout
        cmp rsi, 24
        je end

What's 24? I don't think rsi is likely to have that value. So you go on to check for the letter 'e'. That part looks like it should work. Do you need this to be ESC instead of 'e'? Look for 0x1B or 27 decimal. ESC may generate more than one character. I don't know what that's going to do with I_CANON turned off. You might need to flush STDIN to clean up any mess. You may need to turn I_CANON back on before you exit (I think it 'll be alright?). It looks like you're off to a pretty good start.

Tell us more about what you need this to do... or not do... Sorry I can't test it...

Best,
Frank


Offline isc.jcl.apex

  • Jr. Member
  • *
  • Posts: 4
Re: Linux x64 - Write to STDOUT while waiting for user input
« Reply #2 on: May 05, 2016, 01:39:18 PM »
That bit of a part is a victim of copy/paste (my fault, here's where I took almost all of the code http://forum.nasm.us/index.php?topic=1714.0).

By now, the "e" key was just to test if it worked, ESC key returns me "1b" so that's what finally I would be looking for.

What this "program" needs to do (in it's fullness) it's:

1° Read two 8 bit binary numbers (Something like 10101010).
2° Realize bitwise operations between them
3° Print result to terminal in a loop way (printing the same results over and over)
4° Halt execution when the user hits ESC key

By now, my code is working in the other topics (I know how to read the numbers, translate them to real binary digits, do bitwise operations and convert them again to ASCII chars), but the issue here it's what I don't know how to "print" and "wait for user input".

I've been reading that I need to get terminal in RAW mode, so that way I implement my own "key handler", in this way, all I'd need is wait for the "ESC" keycode and halt execution.


Last night (I'm from méxico, time now is 8:22 AM) I keep up with something and looked up how to put terminal in RAW mode in C (Because C it's like a not-so-high abstraction of assembly and linux syscalls are just like in C) and I achieved how to put the terminal in RAW mode and print in a loop style, now my issue trascends to bringing back the terminal to normal state.

Here's my last updated code
Code: [Select]
section .data
    ICANON:     equ     1<<1       
    TCGETS:     equ     0x5401     
    TCSETS:     equ     0x5402
    TCGETA:     equ     0x5405
    TCSETA:     equ     0x5406
    sys_exit:   equ     60
    sys_read:   equ     0
    sys_write:  equ     1
    sys_open:   equ     2
    sys_close:  equ     3
    sys_ioctl:  equ     16
    stdin:      equ     0
    stdout:     equ     1
    s1:         db      "Cadena: 1", 10
    s1l:        equ     $-s1
    s2:         db      "Cadena: 2", 10
    s2l:        equ     $-s2

section .bss
    buffer:     resb    2
    termios:    resb    36

section .text

write_stdout:
    mov rax, sys_write
    mov rdi, stdout
    syscall
    ret

get_keycode:
    mov rax, sys_read
    mov rdi, stdin
    mov rsi, buffer
    mov rdx, 1
    syscall
    ret


write_stdin_termios:
    mov rax, sys_ioctl
    mov rdi, stdin
    mov rsi, TCSETA
    mov rdx, termios
    syscall
    ret

read_stdin_termios:
    mov rax, sys_ioctl
    mov rdi, stdin
    mov rsi, TCGETS
    mov rdx, termios
    syscall
    ret

canonical_off:
    call read_stdin_termios
    mov rax, ICANON
    not rax
    and [termios+12], rax
    call write_stdin_termios
    ret

canonical_on:
    call read_stdin_termios
    or dword [termios+12], ICANON
    call write_stdin_termios
    ret

GLOBAL _start

_start:
    nop
    nop
    call canonical_off

    read_code:
        mov rsi, s1
        mov rdx, s1l
        call write_stdout
        call get_keycode

    mov al, byte[buffer]
    cmp al, "e"
    je end
    jmp read_code

end:
    call canonical_on
    mov rax, sys_exit
    mov rdi, 0
    syscall

By now, it prints to STDOUT and no matter which key you press (unless it's e) it keeps going, so when you press e, it stops, but it lefts the terminal in RAW and ECHO OFF.

Here you can download my code and a Makefile so you can assembly/link it to test. https://drive.google.com/file/d/0B2iYnMM5TaIUbWVpM3h1Q2YzNEk/view?usp=sharing

Offline Frank Kotler

  • NASM Developer
  • Hero Member
  • *****
  • Posts: 2667
  • Country: us
Re: Linux x64 - Write to STDOUT while waiting for user input
« Reply #3 on: May 05, 2016, 09:26:18 PM »
So... is your problem that canonical_on isn't working? Or do you need it to keep printing without having to press a key? (which does not seem useful, but seems to be what you describe) I think for that, you need sys_fcntl to make stdin non-blocking. I don't remember exactly how I did that, but I remember it was a mess not to undo it! This used to be very easy in DOS, but a PITA in Linux.

Best,
Frank


Offline isc.jcl.apex

  • Jr. Member
  • *
  • Posts: 4
Re: Linux x64 - Write to STDOUT while waiting for user input
« Reply #4 on: May 05, 2016, 09:36:55 PM »
I need it to keep printing until the user presses ESC (I know it's not useful, but whatever the teacher wants....).

I'll read about sys_fcntl to see if that's what I should be doing.

As a side note, I've been testing other stuff and I think I've found something, in C you can get a struct of termios, that represents practically the configuration of the tty, and there are two values, specifically at termios.c_cc[VTIME] and termios.c_cc[VMIN] that can be configured to specify time and bytes needed to return after a read call, but so far, I haven't found how to set up this values in assembly.

But thank you very much for helping me, and yes, it really is a PITA.

Offline isc.jcl.apex

  • Jr. Member
  • *
  • Posts: 4
Re: Linux x64 - Write to STDOUT while waiting for user input
« Reply #5 on: May 05, 2016, 10:43:06 PM »
Well, I solved my problems.

I find out how to modify correctly the termios struct after all, I just needed a sample C program, a debugger and some insight...

Well, in case someone else searches for this, here's what I've found.



In termios(3), we can find this:
Quote
MIN == 0, TIME == 0 (polling read)
              If  data is available, read(2) returns immediately, with the lesser of the number of bytes available, or the number of bytes
              requested.  If no data is available, read(2) returns 0.

That be it, we only need to fetch the termios struct for our current tty, modify these values, and we get a looping console (because read will always return even if there were no bytes read).

Here's my final code, working, and ready to deliver.
PS: Sorry for spanish comments, I have to deliver this and others in a couple of hours.

Code: [Select]
; IMPORTANTE:
; http://stackoverflow.com/questions/3305005/how-do-i-read-single-character-input-from-keyboard-using-nasm-assembly-under-u
; https://filippo.io/linux-syscall-table/
; http://forum.nasm.us/index.php?topic=1714.0
; https://msdn.microsoft.com/en-us/library/windows/hardware/ff561499(v=vs.85).aspx
; https://github.com/torvalds/linux/blob/9256d5a308c95a50c6e85d682492ae1f86a70f9b/include/uapi/asm-generic/ioctls.h

; Constantes definidas que no se tienen porque guardar en memoria

%define SYS_READ    0
%define SYS_WRITE   1
%define SYS_EXIT    0x3c
%define STDIN       0
%define STDOUT      1
%define IOCTL       16
; T (0x54) = Terminal | GET STRUCT / SET STRUCT = GETS /SETS
%define TCGETS      0x5401
%define TCSETS      0x5402
%define ICANON      2       ; Para la bandera canonical
%define ECHO        4       ; Para la bandera de echo
%define VTIME       5       ; Posición de VTIME en CC_C
%define VMIN        6       ; Posición de VMIN en CC_C
%define CC_C        18      ; Offset para localizar CC_C

; Para más información, ver termios(3)

section .data
    b1mes:      db  "Número binario 1:",10
    b1mesl:     equ $-b1mes
    b2mes:      db  "Número binario 2:", 10
    b2mesl:     equ $-b2mes
    ormsg:      db  "Operación OR: 00000000",10
    ormgsl:     equ $-ormsg
    andmsg:     db  "Operación AND: 00000000",10
    andmgsl:    equ $-andmsg
    xormsg:     db  "Operación XOR: 00000000",10
    xormsgl:    equ $-xormsg
    notmsg:     db  "Operación NOT (1): 00000000", 10
    notmsgl:    equ $-notmsg
    notmsg2:    db  "Operación NOT (2): 00000000", 10
    notmsgl2:   equ $-notmsg2
    splitter:   db  "-----------------------------", 10
    splitterl:  equ $-splitter


section .bss
    ; El número es de 8 bits, pero cada caracter es 1 byte
    b1:         resb    9   ; Necesitamos 1 byte por cada caracter
    b2:         resb    9   ; por eso reservamos 8
    size:       equ     9
    ; Se están leyendo 9 caracteres, ya que se tiene que procesar el LF
        ; de caso contrario, al hacer otra llamada a READ se leerá el LF y se
        ; entenderá como que ya fue otra linea, aunque no fue así.
    buffer:     resb    2
    termios:    resb    46

section .text

GLOBAL _start


; Procedimientos tomados del primer enlace en el encabezado.
; Gracias a los usuarios de StackOverflow: Callum y Richard Fearn

canonical_off:
    call read_stdin_termios
    ; Limpiamos la bandera de ICANON
    push rax
    mov rax, ICANON
    not rax
    and [termios+12], rax
    mov byte[termios+CC_C+VTIME], 0     ; Ponemos el tiempo de espera a 0
    mov byte[termios+CC_C+VMIN], 0      ; Ponemos la cantidad mínima a 0
    pop rax
    call write_stdin_termios
    ret

echo_off:
    call read_stdin_termios
    ; Limpiamos la bandera de ECHO
    push rax
    mov rax, ECHO
    not rax
    and [termios+12], rax
    pop rax
    call write_stdin_termios
    ret

canonical_on:
    call read_stdin_termios
    ; Restauramos la bandera de CANONICAL
    or dword [termios+12], ICANON
    mov byte[termios+CC_C+VTIME], 0     ; Tiempo espera = 0
    mov byte[termios+CC_C+VMIN], 1      ; Bytes mínimos = 1(default)
    call write_stdin_termios
    ret

echo_on:
    call read_stdin_termios
    ; Restauramos la bandera de ECHO
    or dword [termios+12], ECHO
    call write_stdin_termios
    ret

read_stdin_termios:
    push rax
    push rbx
    push rcx
    push rdx
    mov rax, IOCTL
    mov rdi, STDIN
    mov rsi, TCGETS
    mov rdx, termios
    syscall
    pop rdx
    pop rcx
    pop rbx
    pop rax
    ret

write_stdin_termios:
    push rax
    push rbx
    push rcx
    push rdx
    mov rax, IOCTL
    mov rdi, STDIN
    mov rsi, TCSETS
    mov rdx, termios
    syscall
    pop rdx
    pop rcx
    pop rbx
    pop rax
    ret


; Se encargará de hacer las operaciones entre los números
; guardarlos en memoria para después imprimir de manera cíclica
; Recibe:   r12 = Numero 1
;           r13 = Número 2
; Modifica: rax, rbx, rcx

bitwise:
    ; OR
    mov rax, r12    ; Movemos el número designado para la operación
    or rax, r13     ; 1er Operación: OR Número 1 con Número 2
    push r12        ; Guardamos los valores, ya que es un estandar el guardar
    push r13            ;los registros que usamos antes de ejecutar un proc.
    mov rbx, ormsg  ; Movemos la cadena en la cual se guardará el número
    mov rcx, ormgsl ; Indicamos el tamaño de la cadena
    call number_to_memory   ; Llamamos el procedimiento.
    pop r13
    pop r12
    ; AND - Se repite el procedimiento, pero ahora para la instrucción AND
    mov rax, r12    ; Movemos el número designado para la operación
    and rax, r13    ; 2da Operación: AND Número 1 con Número 2
    push r12        ; Guardamos los valores, ya que es un estandar el guardar
    push r13            ;los registros que usamos antes de ejecutar un proc.
    mov rbx, andmsg ; Movemos la cadena en la cual se guardará el número
    mov rcx, andmgsl ; Indicamos el tamaño de la cadena
    call number_to_memory   ; Llamamos el procedimiento.
    pop r13
    pop r12
    ; XOR
    mov rax, r12    ; Movemos el número designado para la operación
    xor rax, r13    ; 3er Operación: XOR Número 1 con Número 2
    push r12        ; Guardamos los valores, ya que es un estandar el guardar
    push r13            ;los registros que usamos antes de ejecutar un proc.
    mov rbx, xormsg ; Movemos la cadena en la cual se guardará el número
    mov rcx, xormsgl ; Indicamos el tamaño de la cadena
    call number_to_memory   ; Llamamos el procedimiento.
    pop r13
    pop r12
    ; NOT Número 1
    mov rax, r12    ; Movemos el número designado para la operación
    not rax         ; NOT Número 1
    push r12        ; Guardamos los valores, ya que es un estandar el guardar
    push r13            ;los registros que usamos antes de ejecutar un proc.
    mov rbx, notmsg ; Movemos la cadena en la cual se guardará el número
    mov rcx, notmsgl ; Indicamos el tamaño de la cadena
    call number_to_memory   ; Llamamos el procedimiento.
    pop r13
    pop r12
    ; NOT Número 2
    mov rax, r13    ; Movemos el número designado para la operación
    not rax         ; NOT Número 2
    push r12        ; Guardamos los valores, ya que es un estandar el guardar
    push r13            ;los registros que usamos antes de ejecutar un proc.
    mov rbx, notmsg2    ; Movemos la cadena en la cual se guardará el número
    mov rcx, notmsgl2   ; Indicamos el tamaño de la cadena
    call number_to_memory   ; Llamamos el procedimiento.
    pop r13
    pop r12
    ret

; Mueve el resultado a la cadena en memoria especificada
; El tamaño es fijo a 8 bytes
; Recibe: RAX = Número, RBX = Cadena en memoria, RCX = Tamaño cadena
; Regresa: Nada
; Modifica: RAX, RBX, RCX, R14

number_to_memory:
    add rbx, rcx            ; Le sumamos el tamaño de la cadena
    dec rbx                 ; Para omitir el salto de línea
    mov rcx, 0              ; Inicializamos el desplazamiento
    mov r14, rax            ; Movemos el valor para respaldarlo
    cicle:
        inc rcx             ; Incrementamos el contador para la posición
        and rax, 00000001b  ; Una máscara para sólo obtener el primer número
        dec rbx             ; Le restamos 1 a la posición en memoria
        add byte[rbx], al   ; Le sumamos el valor al caracter en memoria
        shr r14, 1          ; Eliminamos el número que ya pasamos a memoria
        mov rax, r14        ; Movemos el nuevo número al registro r14
        cmp rcx, 8
        jne cicle           ; Mientras no sean 8, seguimos pasando a memoria
    ret

; Para leer los caracteres

get_keycode:
    mov rax, SYS_READ
    mov rdi, STDIN
    mov rsi, buffer
    mov rdx, 1
    syscall
    ret

; Imprime un mensaje a STDOUT
;
; Registros:
;   r12 - Direccio? Memoria Mensaje
;   r13 - Longitud mensaje
;

print:
    mov rax, SYS_WRITE      ; Syscall 1 = Escritura
    mov rdi, STDOUT         ; FD: Descriptor a Salida Estandar
    mov rsi, r12            ; Apuntador al buffer
    mov rdx, r13            ; Apuntador al tamaño del buffer
    syscall
    ret

; Lee desde STDIN un valor, el cual se guarda en el buffer designado
;   Registros:
;       r12 - Dirección del buffer
;       r13 - Cantidad de bytes a leer

read_stdint:
    mov rax, SYS_READ   ; RAX 0 = Syscall (Read)
    mov rdi, STDIN      ; RDI 0 = FD (STDIN)
    mov rsi, r12        ; RDI = dirección buffer
    mov rdx, r13        ; RDX = Bytes a leer
    syscall
    ret

; Convierte un número ascii (representación binaria) a su forma correcta
;   Registros:
;       r12 - Dirección del número
;       r13 - Tamaño
;   Regresa:
;       rax = numero en binario

default_size:   equ     8       ; Un tamaño estandar para este programa

ascii_to_binary:        ; Guardamos los registros
    xor rax, rax        ; Limpiamos RAX
    xor rbx, rbx        ; Limpiamos RBX
    xor rcx, rcx        ; Ponemos en 0 el valor que servirá para offset
    iter:
        shl rbx, 1              ; Hacemos un shift izquierdo (para posicionar
        mov al, byte[r12+rcx]   ; Movemos de memoria el dígito
        sub rax, 30h            ; Restamos 30h = Valor del 0 en ASCII
        add rbx, rax            ; Movemos el nuevo valor a RBX
        inc rcx                 ; correctamente el bit)
        cmp rcx, r13
        jne iter                ; Verifica si terminó de leer el buffer
    mov rax, rbx
    ret

_start:
    nop
    nop
    ; Primero hay que mostrar el primer mensaje
    mov r12, b1mes          ; Ponemos en los registros los valores esperados
    mov r13, b1mesl           ; los cuales son solicitados por el procedimiento
    call print                ; llamado print.
    ; Leemos el primer número
    mov r12, b1             ; Dirección memoria para el primer número
    mov r13, size           ; Leeremos 8 bytes
    call read_stdint        ; Leemos
    ; Segundo número
    mov r12, b2mes          ; Ponemos en los registros los valores esperados
    mov r13, b2mesl           ; los cuales son solicitados por el procedimiento
    call print                ; llamado print.
    ; Leemos el primer número
    mov r12, b2             ; Dirección memoria para el primer número
    mov r13, size           ; Leeremos 8 bytes
    call read_stdint        ; Leemos
    ; Conversion
    mov r12, b1
    mov r13, default_size
    call ascii_to_binary
    ; Guardamos el número y convertimos el siguiente
    push rax
    mov r12, b2
    mov r13, default_size
    call ascii_to_binary
    pop r12             ; r12 = Número 1
    mov r13, rax        ; r13 = Número 2
    call bitwise
    mov r12, splitter   ; Imprimimos el divisor de línea
    mov r13, splitterl
    call print
    call canonical_off
    call main_cicle
    ; Ahora que tenemos ambos, procedemos a hacer la comparación
    jmp end                 ; Saltamos a la salida del programa

; Aquí se imprimirán los números a consola
; esperando a que el usuario detenga la impresión al presionar la tecla ESC
main_cicle:
    mov r12, ormsg
    mov r13, ormgsl
    call print
    mov r12, andmsg
    mov r13, andmgsl
    call print
    mov r12, xormsg
    mov r13, xormsgl
    call print
    mov r12, notmsg
    mov r13, notmsgl
    call print
    mov r12, notmsg2
    mov r13, notmsgl2
    call print
    ; Listen for ESC char
    call get_keycode
    mov al, byte[buffer]
    cmp al, 0x1b
    je end
    jmp main_cicle
    ret

end:
    call canonical_on
    mov rax, SYS_EXIT
    mov rdi, 0
    syscall

Offline Frank Kotler

  • NASM Developer
  • Hero Member
  • *****
  • Posts: 2667
  • Country: us
Re: Linux x64 - Write to STDOUT while waiting for user input
« Reply #6 on: May 05, 2016, 11:22:14 PM »
Sounds good. If it works, don't worry about what language the comments are in!

Best,
Frank