Mode terminal non-kanonik dan input non-blocking pada nasm

Gagasan menulis game dalam bahasa rakitan, tentu saja, tidak mungkin muncul di benak seseorang dengan sendirinya, namun, ini adalah bentuk pelaporan yang canggih yang telah lama dipraktikkan di tahun pertama VMK Universitas Negeri Moskow. Tetapi karena kemajuan tidak berhenti, baik DOS dan masm menjadi sejarah, dan nasm dan Linux menjadi yang terdepan dalam mempersiapkan para bujangan. Mungkin dalam sepuluh tahun, kepemimpinan fakultas akan menemukan python, tetapi ini bukan tentang itu sekarang.

Pemrograman Assembler di Linux, dengan semua kelebihannya, membuatnya tidak mungkin untuk menggunakan interupsi BIOS dan, sebagai akibatnya, menghilangkan fungsionalitasnya. Sebaliknya, mereka harus menggunakan panggilan sistem dan menghubungi terminal api. Karena itu, menulis simulator blackjack atau pertempuran laut tidak menyebabkan kesulitan besar, dan ada masalah dengan ular yang paling biasa. Faktanya adalah bahwa sistem input-output dikendalikan oleh terminal, dan fungsi sistem C tidak dapat digunakan secara langsung. Oleh karena itu, ketika menulis game yang bahkan cukup sederhana, lahir dua batu sandungan: cara mengganti terminal ke mode non-kanonik dan bagaimana membuat input keyboard non-pemblokiran. Ini akan dibahas dalam artikel.

1. Moda terminal non-kanonik


Seperti yang Anda ketahui, untuk memahami apa fungsi dalam C, Anda harus berpikir seperti fungsi dalam C. Untungnya, mengalihkan terminal ke mode non-kanonik tidak begitu sulit. Inilah yang diberikan contoh dalam dokumentasi resmi GNU kepada kami jika Anda menghapus kode pembantu dari kode tersebut:

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

Dalam kode ini, STDIN_FILENO berarti pegangan ke aliran input yang sedang kami kerjakan (secara default adalah 0), ICANON adalah bendera yang diaktifkan untuk input kanonik yang sama, ECHO adalah bendera untuk menampilkan karakter input pada layar, dan TCSANOW dan TCSAFLUSH adalah makro yang ditentukan perpustakaan. Dengan demikian, algoritma "telanjang", tanpa pemeriksaan keamanan, terlihat seperti ini:

  1. menjaga struktur termios asli;
  2. salin isinya dengan perubahan bendera ICANON dan ECHO;
  3. kirim struktur yang diubah ke terminal;
  4. Setelah selesai bekerja, kembalilah ke terminal struktur yang disimpan.

Masih memahami apa fungsi perpustakaan tcsetattr dan tcgetattr lakukan. Sebenarnya, mereka melakukan banyak hal, tetapi panggilan sistem ioctl adalah kunci untuk pekerjaan mereka. Argumen pertama yang dibutuhkan adalah deskriptor aliran (0 dalam kasus kami), yang kedua adalah serangkaian flag yang baru saja didefinisikan oleh makro TCSANOW dan TCSAFLUSH, dan yang ketiga adalah pointer ke struktur (dalam termios kasus kami). Pada sintaks nasm dan di bawah konvensi pemanggilan sistem di linux, ia akan mengambil bentuk berikut:

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

Secara umum, ini adalah inti dari fungsi tcsetattr dan tcgetattr. Untuk sisa kode, kita perlu mengetahui ukuran dan struktur struktur termios, yang juga mudah ditemukan dalam dokumentasi resmi . Ukurannya secara default adalah 60 byte, dan array flag yang kita butuhkan berukuran 4 byte dan terletak keempat berturut-turut. Tetap menulis dua prosedur dan menggabungkannya menjadi satu kode.

Di bawah spoiler, implementasinya yang paling sederhana bukanlah cara yang paling aman, tetapi ia bekerja dengan cukup baik pada OS yang mendukung standar POSIX. Nilai makro diambil dari sumber-sumber pustaka C standar yang disebutkan di atas.

Transfer ke mode nonkanonik
 %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. Input non-blocking di terminal


Untuk input dana yang tidak menghalangi, terminal tidak cukup untuk kami. Kami akan menulis fungsi yang akan memeriksa buffer aliran standar untuk kesiapan mengirimkan informasi: jika ada simbol di buffer, ia akan mengembalikan kode-nya; jika buffer kosong, itu akan mengembalikan 0. Untuk tujuan ini, Anda dapat menggunakan dua panggilan sistem - polling () atau pilih (). Keduanya mampu melihat berbagai aliran input-output pada fakta peristiwa apa pun. Misalnya, jika informasi telah tiba di salah satu aliran, maka kedua panggilan sistem ini dapat menangkap ini dan menampilkannya dalam data yang dikembalikan. Namun, yang kedua dari mereka pada dasarnya adalah versi yang ditingkatkan dari yang pertama dan berguna ketika bekerja dengan banyak utas. Kami tidak memiliki tujuan seperti itu (kami hanya bekerja dengan aliran standar), jadi kami akan menggunakan polling () panggilan.

Itu juga menerima tiga parameter sebagai input:

  1. pointer ke struktur data, yang berisi informasi tentang deskriptor dari aliran yang dipantau (kami akan membahasnya di bawah);
  2. jumlah utas yang diproses (kami memilikinya);
  3. waktu dalam milidetik di mana suatu peristiwa dapat diharapkan (kita perlu itu terjadi segera, jadi parameter ini adalah 0).

Dari dokumentasi Anda dapat mengetahui bahwa struktur data yang diperlukan memiliki perangkat berikut:

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

Deskriptornya digunakan sebagai deskriptor file (kami bekerja dengan aliran standar, oleh karena itu 0), dan flag yang diminta adalah berbagai flag, di mana kami hanya membutuhkan flag untuk kehadiran data di buffer. Itu memiliki nama POLLIN dan sama dengan 1. Kami mengabaikan bidang peristiwa yang dikembalikan, karena kami tidak memberikan informasi apa pun ke aliran input. Maka panggilan sistem yang diinginkan akan terlihat seperti ini:

 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 

Polling () system call mengembalikan jumlah utas tempat peristiwa "menarik" terjadi. Karena kami hanya memiliki satu utas, nilai kembalinya adalah 1 (ada data yang dimasukkan) atau 0 (tidak ada). Namun, jika buffer tidak kosong, maka kami segera membuat panggilan sistem lain - baca - dan baca kode karakter yang dimasukkan. Hasilnya, kami mendapatkan kode berikut.

Input non-blocking di 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 


Dengan demikian, sekarang Anda dapat menggunakan fungsi polling untuk membaca informasi. Jika tidak ada data yang dimasukkan, yaitu, tidak ada tombol yang ditekan, maka itu akan mengembalikan 0 dan dengan demikian tidak akan memblokir proses kami. Tentu saja, implementasi ini memiliki kekurangan, khususnya, ia hanya dapat bekerja dengan karakter ascii, tetapi dapat dengan mudah diubah tergantung pada tugasnya.

Tiga fungsi yang dijelaskan di atas (setcan, setnoncan, dan polling) sudah cukup untuk menyempurnakan input terminal untuk Anda dan Anda sendiri. Mereka sangat sederhana baik untuk pemahaman maupun untuk digunakan. Namun, dalam permainan nyata, akan lebih baik untuk mengamankan mereka sesuai dengan pendekatan C yang biasa, tetapi ini sudah menjadi bisnis programmer.

Sumber


1) Sumber fungsi tcgetattr dan tcsetattr ;
2) dokumentasi panggilan sistem ioctl ;
3) dokumentasi panggilan sistem pemungutan suara ;
4) Dokumentasi tentang termios ;
5) Tabel panggilan sistem di Linux x64 .

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


All Articles