Mode non canonique du terminal et entrée non bloquante sur nasm

L'idée d'écrire un jeu en langage assembleur, bien sûr, est peu susceptible de venir à l'esprit de quelqu'un par lui-même, cependant, une telle forme sophistiquée de reportage est pratiquée depuis longtemps au cours de la première année du VMK de l'Université d'État de Moscou. Mais comme les progrès ne s'arrêtent pas, DOS et Masm entrent dans l'histoire, et Nasm et Linux arrivent au premier plan de la préparation des bachelors. Peut-être que dans dix ans, la direction de la faculté découvrira le python, mais ce n'est plus le sujet maintenant.

La programmation de l'assembleur sous Linux, avec tous ses avantages, rend impossible l'utilisation des interruptions du BIOS et, par conséquent, les prive de fonctionnalités. Au lieu de cela, ils doivent utiliser les appels système et contacter l'API du terminal. Par conséquent, écrire un simulateur de blackjack ou de bataille navale ne pose pas de grandes difficultés, et il y a des problèmes avec le serpent le plus ordinaire. Le fait est que le système d'entrée-sortie est contrôlé par le terminal, et les fonctions du système C ne peuvent pas être utilisées directement. Par conséquent, lors de l'écriture de jeux même assez simples, deux pierres d'achoppement sont nées: comment faire passer le terminal en mode non canonique et comment rendre l'entrée clavier non bloquante. Cela sera discuté dans l'article.

1. Mode non canonique du terminal


Comme vous le savez, pour comprendre ce que fait une fonction en C, vous devez penser comme une fonction en C. Heureusement, le passage du terminal en mode non canonique n'est pas si difficile. Voici ce que l' exemple de la documentation officielle de GNU nous donne si vous en supprimez le code d'assistance:

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); } 

Dans ce code, STDIN_FILENO signifie la poignée du flux d'entrée avec lequel nous travaillons (par défaut, il est 0), ICANON est le drapeau d'activation pour la même entrée canonique, ECHO est le drapeau pour afficher les caractères d'entrée à l'écran et TCSANOW et TCSAFLUSH sont des macros définies par la bibliothèque. Ainsi, l'algorithme «nu», dépourvu de contrôles de sécurité, ressemble à ceci:

  1. conserver la structure d'origine des termios;
  2. copier son contenu avec le changement des drapeaux ICANON et ECHO;
  3. envoyer la structure modifiée au terminal;
  4. à la fin des travaux, restituer au terminal la structure enregistrée.

Reste à comprendre ce que font les fonctions de bibliothèque tcsetattr et tcgetattr. En fait, ils font beaucoup de choses, mais l' appel système ioctl est la clé de leur travail. Le premier argument qu'il prend est un descripteur de flux (0 dans notre cas), le second est un ensemble d'indicateurs qui sont juste définis par les macros TCSANOW et TCSAFLUSH, et le troisième est un pointeur sur la structure (dans notre cas termios). Sur la syntaxe nasm et sous la convention des appels système sur linux, elle prendra la forme suivante:

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

En général, c'est tout l'intérêt des fonctions tcsetattr et tcgetattr. Pour le reste du code, nous devons connaître la taille et la structure de la structure termios, qui est également facile à trouver dans la documentation officielle . Sa taille par défaut est de 60 octets, et le tableau d'indicateurs dont nous avons besoin est de 4 octets et est situé quatrième dans une rangée. Il reste à écrire deux procédures et à les combiner en un seul code.

Sous le spoiler, sa mise en œuvre la plus simple n'est en aucun cas la plus sécurisée, mais elle fonctionne assez bien sur tout système d'exploitation prenant en charge les normes POSIX. Les valeurs macro ont été tirées des sources susmentionnées de la bibliothèque C standard.

Transfert en mode non canonique
 %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. Entrée non bloquante dans le terminal


Pour une entrée de fonds non bloquante, le terminal ne nous suffit pas. Nous allons écrire une fonction qui vérifiera si le tampon de flux standard est prêt à transmettre des informations: s'il y a un symbole dans le tampon, il retournera son code; si le tampon est vide, il retournera 0. Pour cela, vous pouvez utiliser deux appels système - poll () ou select (). Les deux sont capables de visualiser divers flux d'entrée-sortie sur le fait de n'importe quel événement. Par exemple, si des informations sont arrivées dans l'un des flux, ces deux appels système peuvent les capturer et les afficher dans les données renvoyées. Cependant, le second d'entre eux est essentiellement une version améliorée du premier et est utile lorsque vous travaillez avec plusieurs threads. Nous n'avons pas un tel objectif (nous ne travaillons qu'avec le flux standard), nous allons donc utiliser l'appel poll ().

Il accepte également trois paramètres en entrée:

  1. un pointeur sur la structure des données, qui contient des informations sur les descripteurs des flux surveillés (nous en discuterons ci-dessous);
  2. le nombre de threads traités (nous en avons un);
  3. temps en millisecondes pendant lequel un événement peut être attendu (nous avons besoin qu'il se produise immédiatement, donc ce paramètre est 0).

Dans la documentation, vous pouvez découvrir que la structure de données requise possède le périphérique suivant:

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

Son descripteur est utilisé comme descripteur de fichier (nous travaillons avec un flux standard, donc c'est 0), et comme événements demandés nous utilisons différents drapeaux, dont nous n'avons besoin que du drapeau pour la présence de données dans le tampon. Il a le nom POLLIN et est égal à 1. Nous ignorons le champ des événements retournés, car nous ne donnons aucune information au flux d'entrée. L'appel système souhaité ressemblera alors à ceci:

 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 

L'appel système poll () renvoie le nombre de threads dans lesquels des événements "intéressants" se sont produits. Comme nous n'avons qu'un seul thread, la valeur de retour est soit 1 (il y a des données entrées) ou 0 (il n'y en a pas). Si, néanmoins, le tampon n'est pas vide, alors nous faisons immédiatement un autre appel système - lire - et lire le code du caractère entré. En conséquence, nous obtenons le code suivant.

Entrée non bloquante dans le 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 


Ainsi, vous pouvez maintenant utiliser la fonction d'interrogation pour lire des informations. Si aucune donnée n'est entrée, c'est-à-dire qu'aucun bouton n'a été enfoncé, alors il retournera 0 et ne bloquera donc pas notre processus. Bien sûr, cette implémentation a des défauts, en particulier, elle ne peut fonctionner qu'avec des caractères ascii, mais elle peut facilement être modifiée en fonction de la tâche.

Les trois fonctions décrites ci-dessus (setcan, setnoncan et poll) suffisent pour affiner l'entrée du terminal pour vous et les vôtres. Ils sont extrêmement simples à comprendre et à utiliser. Cependant, dans un vrai jeu, ce serait bien de les sécuriser conformément à l'approche C habituelle, mais c'est déjà l'affaire d'un programmeur.

Les sources


1) Les sources des fonctions tcgetattr et tcsetattr ;
2) documentation des appels système ioctl ;
3) documentation sur l'appel du système de sondage ;
4) Documentation sur les termios ;
5) Table d'appels système sous Linux x64 .

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


All Articles