Nicht-kanonischer Modus des Terminals und nicht blockierender Eingang auf Nasm

Die Idee, ein Spiel in Assemblersprache zu schreiben, wird natürlich kaum von jemandem in den Sinn kommen. Es handelt sich jedoch um eine so ausgefeilte Form der Berichterstattung, die seit langem im ersten Jahr des VMK der Moskauer Staatsuniversität praktiziert wird. Da der Fortschritt jedoch nicht zum Stillstand kommt, werden sowohl DOS als auch masm Geschichte, und nasm und Linux stehen bei der Vorbereitung von Junggesellen an vorderster Front. Vielleicht wird die Leitung der Fakultät in zehn Jahren Python entdecken, aber darum geht es jetzt nicht.

Die Assembler-Programmierung unter Linux mit all ihren Vorteilen macht die Verwendung von BIOS-Interrupts unmöglich und beraubt sie daher der Funktionalität. Stattdessen müssen sie Systemaufrufe verwenden und die Terminal-API kontaktieren. Daher verursacht das Schreiben eines Simulators für Blackjack oder Seeschlacht keine großen Schwierigkeiten, und es gibt Probleme mit der gewöhnlichsten Schlange. Tatsache ist, dass das Eingabe-Ausgabe-System vom Terminal gesteuert wird und die Funktionen des C-Systems nicht direkt verwendet werden können. Wenn Sie also auch relativ einfache Spiele schreiben, entstehen zwei Stolpersteine: wie Sie das Terminal in den nicht-kanonischen Modus schalten und wie Sie die Tastatureingabe nicht blockieren. Dies wird im Artikel besprochen.

1. Nicht-kanonischer Modus des Terminals


Wie Sie wissen, müssen Sie, um zu verstehen, was eine Funktion in C bewirkt, wie eine Funktion in C denken. Glücklicherweise ist es nicht so schwierig, das Terminal in den nicht-kanonischen Modus zu schalten. Das Beispiel in der offiziellen GNU-Dokumentation zeigt uns Folgendes, wenn Sie den Hilfecode daraus entfernen:

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

In diesem Code bedeutet STDIN_FILENO das Handle für den Eingabestream, mit dem wir arbeiten (standardmäßig ist es 0), ICANON ist das Aktivierungsflag für dieselbe kanonische Eingabe, ECHO ist das Flag für die Anzeige von Eingabezeichen auf dem Bildschirm und TCSANOW und TCSAFLUSH sind bibliotheksdefinierte Makros. Der „nackte“ Algorithmus ohne Sicherheitsüberprüfungen sieht also folgendermaßen aus:

  1. Behalten Sie die ursprüngliche Termios-Struktur bei.
  2. Kopieren Sie den Inhalt mit der Änderung der ICANON- und ECHO-Flags.
  3. Senden Sie die geänderte Struktur an das Terminal.
  4. Geben Sie nach Abschluss der Arbeiten die gespeicherte Struktur an das Terminal zurück.

Es bleibt zu verstehen, was die Bibliotheksfunktionen tcsetattr und tcgetattr tun. Tatsächlich machen sie viele Dinge, aber der ioctl -Systemaufruf ist der Schlüssel zu ihrer Arbeit. Das erste Argument ist ein Stream-Deskriptor (in unserem Fall 0), das zweite ist eine Reihe von Flags, die nur durch die Makros TCSANOW und TCSAFLUSH definiert werden, und das dritte ist ein Zeiger auf die Struktur (in unserem Fall termios). In der Nasm-Syntax und unter der Konvention von Systemaufrufen unter Linux wird die folgende Form angenommen:

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

Im Allgemeinen ist dies der springende Punkt der Funktionen tcsetattr und tcgetattr. Für den Rest des Codes müssen wir die Größe und Struktur der Termios-Struktur kennen, die auch in der offiziellen Dokumentation leicht zu finden ist. Die Größe beträgt standardmäßig 60 Byte, und das Array der benötigten Flags ist 4 Byte groß und befindet sich an vierter Stelle in Folge. Es bleiben zwei Prozeduren zu schreiben und sie zu einem Code zu kombinieren.

Unter dem Spoiler ist seine einfachste Implementierung keineswegs die sicherste, funktioniert aber auf jedem Betriebssystem, das POSIX-Standards unterstützt, recht gut. Die Makrowerte wurden aus den oben genannten Quellen der Standard-C-Bibliothek entnommen.

In den nichtkanonischen Modus wechseln
 %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. Nicht blockierender Eingang im Terminal


Für die nicht blockierende Eingabe von Geldern reicht uns das Terminal nicht aus. Wir werden eine Funktion schreiben, die den Standard-Stream-Puffer auf die Bereitschaft zur Übertragung von Informationen überprüft: Wenn sich ein Symbol im Puffer befindet, gibt er seinen Code zurück; Wenn der Puffer leer ist, wird 0 zurückgegeben. Zu diesem Zweck können Sie zwei Systemaufrufe verwenden - poll () oder select (). Beide sind in der Lage, verschiedene Eingabe-Ausgabe-Streams zu jedem Ereignis anzuzeigen. Wenn beispielsweise Informationen in einem der Streams angekommen sind, können diese beiden Systemaufrufe diese erfassen und in den zurückgegebenen Daten anzeigen. Die zweite ist jedoch im Wesentlichen eine verbesserte Version der ersten und ist nützlich, wenn Sie mit mehreren Threads arbeiten. Wir haben kein solches Ziel (wir arbeiten nur mit dem Standard-Stream), daher verwenden wir den Aufruf poll ().

Es werden auch drei Parameter als Eingabe akzeptiert:

  1. einen Zeiger auf die Datenstruktur, der Informationen über die Deskriptoren der überwachten Flüsse enthält (wir werden es unten diskutieren);
  2. die Anzahl der verarbeiteten Threads (wir haben einen);
  3. Zeit in Millisekunden, in der ein Ereignis erwartet werden kann (es muss sofort auftreten, daher ist dieser Parameter 0).

Aus der Dokumentation können Sie entnehmen, dass die erforderliche Datenstruktur über das folgende Gerät verfügt:

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

Sein Deskriptor wird als Dateideskriptor verwendet (wir arbeiten mit einem Standard-Stream, daher ist er 0), und die angeforderten Flags sind verschiedene Flags, von denen wir das Flag nur für das Vorhandensein von Daten im Puffer benötigen. Es hat den Namen POLLIN und ist gleich 1. Wir ignorieren das Feld der zurückgegebenen Ereignisse, da wir dem Eingabestream keine Informationen geben. Dann sieht der gewünschte Systemaufruf folgendermaßen aus:

 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 

Der Systemaufruf poll () gibt die Anzahl der Threads zurück, in denen "interessante" Ereignisse aufgetreten sind. Da wir nur einen Thread haben, ist der Rückgabewert entweder 1 (es werden Daten eingegeben) oder 0 (es gibt keine). Wenn der Puffer dennoch nicht leer ist, führen wir sofort einen weiteren Systemaufruf durch - lesen - und lesen den Code des eingegebenen Zeichens. Als Ergebnis erhalten wir den folgenden Code.

Nicht blockierender Eingang im 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 


Somit können Sie jetzt die Abfragefunktion zum Lesen von Informationen verwenden. Wenn keine Daten eingegeben wurden, dh keine Taste gedrückt wurde, wird 0 zurückgegeben und somit unser Prozess nicht blockiert. Natürlich weist diese Implementierung Mängel auf, insbesondere kann sie nur mit ASCII-Zeichen arbeiten, sie kann jedoch je nach Aufgabe leicht geändert werden.

Die drei oben beschriebenen Funktionen (setcan, setnoncan und poll) reichen aus, um den Terminaleingang für sich und Ihre eigenen zu optimieren. Sie sind sowohl zum Verständnis als auch zur Verwendung äußerst einfach. In einem echten Spiel wäre es jedoch schön, sie gemäß dem üblichen C-Ansatz zu sichern, aber dies ist bereits ein Programmierergeschäft.

Quellen


1) Die Quellen der Funktionen tcgetattr und tcsetattr ;
2) Dokumentation des ioctl-Systemaufrufs ;
3) Dokumentation zum Aufruf des Umfragesystems ;
4) Dokumentation zu Termios ;
5) Systemaufruftabelle unter Linux x64 .

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


All Articles