Modo no canónico del terminal y entrada sin bloqueo en nasm

La idea de escribir un juego en lenguaje ensamblador, por supuesto, es poco probable que se le ocurra a alguien por sí misma, sin embargo, es una forma tan sofisticada de informes que se ha practicado durante el primer año del VMK de la Universidad Estatal de Moscú. Pero como el progreso no se detiene, tanto DOS como masm se convierten en historia, y nasm y Linux se ponen a la vanguardia en la preparación de solteros. Quizás en diez años, el liderazgo de la facultad descubra Python, pero no se trata de eso ahora.

La programación del ensamblador en Linux, con todas sus ventajas, hace que sea imposible usar interrupciones del BIOS y, como resultado, les priva de funcionalidad. En cambio, tienen que usar las llamadas del sistema y comunicarse con la API del terminal. Por lo tanto, escribir un simulador de blackjack o batalla naval no causa grandes dificultades, y existen problemas con la serpiente más común. El hecho es que el sistema de entrada-salida está controlado por el terminal, y las funciones del sistema C no se pueden usar directamente. Por lo tanto, cuando se escriben juegos bastante simples, nacen dos escollos: cómo cambiar el terminal al modo no canónico y cómo hacer que la entrada del teclado no se bloquee. Esto se discutirá en el artículo.

1. Modo no canónico del terminal


Como sabes, para entender lo que hace una función en C, debes pensar como una función en C. Afortunadamente, cambiar el terminal al modo no canónico no es tan difícil. Esto es lo que nos da el ejemplo en la documentación oficial de GNU si elimina el código auxiliar:

struct termios saved_attributes; void reset_input_mode (void) { tcsetattr (STDIN_FILENO, TCSANOW, &saved_attributes); } void set_input_mode (void) { struct termios tattr; /* Save the terminal attributes so we can restore them later. */ tcgetattr (STDIN_FILENO, &saved_attributes); /* Set the funny terminal modes. */ tcgetattr (STDIN_FILENO, &tattr); tattr.c_lflag &= ~(ICANON|ECHO); /* Clear ICANON and ECHO. */ tcsetattr (STDIN_FILENO, TCSAFLUSH, &tattr); } 

En este código, STDIN_FILENO significa el identificador de la secuencia de entrada con la que estamos trabajando (por defecto es 0), ICANON es el indicador de habilitación para la misma entrada canónica, ECHO es el indicador para mostrar caracteres de entrada en la pantalla, y TCSANOW y TCSAFLUSH son macros definidas por la biblioteca. Por lo tanto, el algoritmo "desnudo", sin controles de seguridad, se ve así:

  1. mantener la estructura original de termios;
  2. copie su contenido con el cambio de las banderas ICANON y ECHO;
  3. enviar la estructura modificada a la terminal;
  4. Al finalizar el trabajo, devuelva a la terminal la estructura guardada.

Queda por entender para qué sirven las funciones de biblioteca tcsetattr y tcgetattr. De hecho, hacen muchas cosas, pero la llamada al sistema ioctl es la clave de su trabajo. El primer argumento que toma es un descriptor de flujo (0 en nuestro caso), el segundo es un conjunto de indicadores que están definidos por las macros TCSANOW y TCSAFLUSH, y el tercero es un puntero a la estructura (en nuestro caso, termios). En la sintaxis nasm y bajo la convención de llamadas al sistema en linux, tomará la siguiente forma:

  mov rax, 16 ;   ioctl mov rdi, 0 ;    mov rsi, TCGETS ;  mov rdx, tattr ;     syscall 

En general, este es el objetivo de las funciones tcsetattr y tcgetattr. Para el resto del código, necesitamos conocer el tamaño y la estructura de la estructura termios, que también es fácil de encontrar en la documentación oficial . Su tamaño por defecto es de 60 bytes, y la matriz de banderas que necesitamos tiene un tamaño de 4 bytes y se ubica en cuarto lugar. Queda por escribir dos procedimientos y combinarlos en un código.

Bajo el spoiler, su implementación más simple no es la más segura, pero funciona bastante bien en cualquier sistema operativo que soporte los estándares POSIX. Los macro valores fueron tomados de las fuentes mencionadas de la biblioteca estándar de C.

Transferencia al modo no canónico
 %define ICANON 2 %define ECHO 8 %define TCGETS 21505 ;    %define TCPUTS 21506 ;    global setcan ;     global setnoncan ;     section .bss stty resb 12 ; termios - 60  slflag resb 4 ;slflag    3*4   srest resb 44 tty resb 12 lflag resb 4 brest resb 44 section .text setnoncan: push stty call tcgetattr push tty call tcgetattr and dword[lflag], (~ICANON) and dword[lflag], (~ECHO) call tcsetattr add rsp, 16 ret setcan: push stty call tcsetattr add rsp, 8 ret tcgetattr: mov rdx, qword[rsp+8] push rax push rbx push rcx push rdi push rsi mov rax, 16 ;ioctl system call mov rdi, 0 mov rsi, TCGETS syscall pop rsi pop rdi pop rcx pop rbx pop rax ret tcsetattr: mov rdx, qword[rsp+8] push rax push rbx push rcx push rdi push rsi mov rax, 16 ;ioctl system call mov rdi, 0 mov rsi, TCPUTS syscall pop rsi pop rdi pop rcx pop rbx pop rax ret 


2. Entrada sin bloqueo en el terminal


Para la entrada de fondos sin bloqueo, el terminal no es suficiente para nosotros. Escribiremos una función que verificará que el búfer de flujo estándar esté listo para transmitir información: si hay un símbolo en el búfer, devolverá su código; si el búfer está vacío, devolverá 0. Para este propósito, puede usar dos llamadas al sistema: poll () o select (). Ambos son capaces de ver varios flujos de entrada-salida en el caso de cualquier evento. Por ejemplo, si la información ha llegado a alguna de las transmisiones, ambas llamadas al sistema pueden capturar esto y mostrarlo en los datos devueltos. Sin embargo, el segundo de ellos es esencialmente una versión mejorada del primero y es útil cuando se trabaja con múltiples hilos. No tenemos ese objetivo (solo trabajamos con la transmisión estándar), por lo que utilizaremos la llamada poll ().

También acepta tres parámetros como entrada:

  1. un puntero a la estructura de datos, que contiene información sobre los descriptores de los flujos monitoreados (lo discutiremos a continuación);
  2. el número de hilos procesados ​​(tenemos uno);
  3. tiempo en milisegundos durante el cual se puede esperar un evento (necesitamos que ocurra de inmediato, por lo que este parámetro es 0).

De la documentación puede descubrir que la estructura de datos requerida tiene el siguiente dispositivo:

 struct pollfd { int fd; /*   */ short events; /*   */ short revents; /*   */ }; 

Su descriptor se usa como un descriptor de archivo (trabajamos con una secuencia estándar, por lo tanto, es 0), y los indicadores solicitados son varios indicadores, de los cuales solo necesitamos el indicador para la presencia de datos en el búfer. Tiene el nombre POLLIN y es igual a 1. Ignoramos el campo de eventos devueltos, porque no damos ninguna información al flujo de entrada. Entonces la llamada al sistema deseada se verá así:

 section .data fd dd 0 ;    eve dw 1 ;   - POLLIN rev dw 0 ;  section .text poll: nop push rbx push rcx push rdx push rdi push rsi mov rax, 7 ;   poll mov rdi, fd ;   mov rsi, 1 ;   mov rdx, 0 ;     syscall 

La llamada al sistema poll () devuelve el número de subprocesos en los que se han producido eventos "interesantes". Como solo tenemos un hilo, el valor de retorno es 1 (hay datos ingresados) o 0 (no hay ninguno). Sin embargo, si el búfer no está vacío, inmediatamente hacemos otra llamada al sistema, leer, y leemos el código del carácter ingresado. Como resultado, obtenemos el siguiente código.

Entrada sin bloqueo en el terminal
 section .data fd dd 0 ;    eve dw 1 ;   - POLLIN rev dw 0 ;  sym db 1 section .text poll: nop push rbx push rcx push rdx push rdi push rsi mov rax, 7 ;   poll mov rdi, fd ;   mov rsi, 1 ;   mov rdx, 0 ;     syscall test rax, rax ;    0 jz .e mov rax, 0 mov rdi, 0 ;   mov rsi, sym ;   read mov rdx, 1 syscall xor rax, rax mov al, byte[sym] ;  ,     .e: pop rsi pop rdi pop rdx pop rcx pop rbx ret 


Por lo tanto, ahora puede usar la función de sondeo para leer información. Si no hay datos ingresados, es decir, no se ha presionado ningún botón, entonces devolverá 0 y, por lo tanto, no bloqueará nuestro proceso. Por supuesto, esta implementación tiene fallas, en particular, solo puede funcionar con caracteres ascii, pero se puede cambiar fácilmente dependiendo de la tarea.

Las tres funciones descritas anteriormente (setcan, setnoncan y poll) son suficientes para ajustar la entrada del terminal para usted y los suyos. Son extremadamente simples tanto para la comprensión como para el uso. Sin embargo, en un juego real, sería bueno asegurarlos de acuerdo con el enfoque C habitual, pero esto ya es asunto de un programador.

Fuentes


1) Las fuentes de las funciones tcgetattr y tcsetattr ;
2) documentación de la llamada al sistema ioctl ;
3) documentación sobre la llamada al sistema de votación ;
4) Documentación sobre termios ;
5) Tabla de llamadas del sistema en Linux x64 .

Source: https://habr.com/ru/post/es414309/


All Articles