Modo não canônico do terminal e entrada sem bloqueio no nasm

A idéia de escrever um jogo em linguagem assembly, é claro, dificilmente virá à mente de alguém, no entanto, uma forma tão sofisticada de relatório é praticada há muito tempo no primeiro ano da VMK da Universidade Estadual de Moscou. Mas, como o progresso não pára, o DOS e o masm se tornam história, e o nasm e o Linux entram na vanguarda da preparação de solteiros. Talvez em dez anos, a liderança da faculdade descubra python, mas isso não é mais o que acontece agora.

A programação de assembler no Linux, com todas as suas vantagens, impossibilita o uso de interrupções do BIOS e, como resultado, as priva da funcionalidade. Em vez disso, eles precisam usar as chamadas do sistema e entrar em contato com a API do terminal. Portanto, escrever um simulador de blackjack ou batalha no mar não causa grandes dificuldades e há problemas com a cobra mais comum. O fato é que o sistema de entrada e saída é controlado pelo terminal e as funções do sistema C não podem ser usadas diretamente. Portanto, ao escrever mesmo jogos bastante simples, nascem dois obstáculos: como alternar o terminal para o modo não canônico e como tornar a entrada do teclado sem bloqueio. Isso será discutido no artigo.

1. Modo não canônico do terminal


Como você sabe, para entender o que uma função em C faz, você precisa pensar como uma função em C. Felizmente, mudar o terminal para o modo não canônico não é tão difícil. Isto é o que o exemplo da documentação oficial do GNU nos fornece se você remover o 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); } 

Nesse código, STDIN_FILENO significa o identificador para o fluxo de entrada com o qual estamos trabalhando (por padrão é 0), ICANON é o sinalizador de habilitação para a mesma entrada canônica, ECHO é o sinalizador para exibir caracteres de entrada na tela e TCSANOW e TCSAFLUSH são macros definidas pela biblioteca. Assim, o algoritmo “bare”, desprovido de verificações de segurança, fica assim:

  1. manter a estrutura original dos termios;
  2. copie seu conteúdo com a alteração das bandeiras ICANON e ECHO;
  3. envie a estrutura alterada para o terminal;
  4. Após a conclusão do trabalho, retorne ao terminal a estrutura salva.

Resta entender o que as funções da biblioteca tcsetattr e tcgetattr fazem. De fato, eles fazem muitas coisas, mas a chamada do sistema ioctl é a chave do trabalho deles. O primeiro argumento usado é um descritor de fluxo (0 no nosso caso), o segundo é um conjunto de sinalizadores que são definidos apenas pelas macros TCSANOW e TCSAFLUSH, e o terceiro é um ponteiro para a estrutura (em nossos termos de caso). Na sintaxe do nasm e sob a convenção de chamadas do sistema no linux, o formato será o seguinte:

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

Em geral, esse é o objetivo das funções tcsetattr e tcgetattr. Para o restante do código, precisamos saber o tamanho e a estrutura da estrutura termios, o que também é fácil de encontrar na documentação oficial . Seu tamanho, por padrão, é de 60 bytes, e a matriz de sinalizadores de que precisamos é de 4 bytes e está localizada em quarto lugar consecutivo. Resta escrever dois procedimentos e combiná-los em um código.

Sob o spoiler, sua implementação mais simples não é de forma alguma a mais segura, mas funciona muito bem em qualquer sistema operacional compatível com os padrões POSIX. Os valores macro foram retirados das fontes acima mencionadas da biblioteca C padrão.

Transferir para o modo não-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 sem bloqueio no terminal


Para entrada de fundos sem bloqueio, o terminal não é suficiente para nós. Escreveremos uma função que verificará o buffer de fluxo padrão quanto à disponibilidade para transmitir informações: se houver um símbolo no buffer, ele retornará seu código; se o buffer estiver vazio, ele retornará 0. Para esse propósito, você pode usar duas chamadas do sistema - poll () ou selecione (). Ambos são capazes de visualizar vários fluxos de entrada e saída no fato de qualquer evento. Por exemplo, se as informações chegaram em qualquer um dos fluxos, as duas chamadas do sistema podem capturar e exibir nos dados retornados. No entanto, o segundo deles é essencialmente uma versão aprimorada do primeiro e é útil ao trabalhar com vários threads. Como não temos esse objetivo (trabalhamos apenas com o fluxo padrão), usaremos a chamada de poll ().

Também aceita três parâmetros como entrada:

  1. um ponteiro para a estrutura de dados, que contém informações sobre os descritores dos fluxos monitorados (discutiremos a seguir);
  2. o número de threads processados ​​(temos um);
  3. tempo em milissegundos durante o qual um evento pode ser esperado (precisamos que ele ocorra imediatamente, portanto esse parâmetro é 0).

Na documentação, você pode descobrir que a estrutura de dados necessária possui o seguinte dispositivo:

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

Seu descritor é usado como um descritor de arquivo (trabalhamos com um fluxo padrão, portanto, é 0) e, como eventos solicitados, usamos vários sinalizadores, dos quais precisamos apenas do sinalizador para a presença de dados no buffer. Ele tem o nome POLLIN e é igual a 1. Ignoramos o campo de eventos retornados, porque não fornecemos nenhuma informação ao fluxo de entrada. Em seguida, a chamada do sistema desejada será assim:

 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 

A chamada do sistema poll () retorna o número de threads nos quais ocorreram eventos "interessantes". Como temos apenas um encadeamento, o valor retornado é 1 (há dados inseridos) ou 0 (não há nenhum). Se, no entanto, o buffer não estiver vazio, faremos imediatamente outra chamada do sistema - ler - e ler o código do caractere inserido. Como resultado, obtemos o seguinte código.

Entrada sem bloqueio no 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 


Portanto, agora você pode usar a função de pesquisa para ler informações. Se não houver dados inseridos, ou seja, nenhum botão foi pressionado, ele retornará 0 e, portanto, não bloqueará nosso processo. Obviamente, essa implementação possui falhas, em particular, ela só pode funcionar com caracteres ascii, mas pode ser facilmente alterada dependendo da tarefa.

As três funções descritas acima (setcan, setnoncan e poll) são suficientes para ajustar a entrada do terminal para você e para os seus. Eles são extremamente simples, tanto para compreensão quanto para uso. No entanto, em um jogo real, seria bom protegê-los de acordo com a abordagem C usual, mas isso já é da conta de um programador.

Fontes


1) As fontes das funções tcgetattr e tcsetattr ;
2) documentação de chamada do sistema ioctl ;
3) documentação sobre chamada do sistema de votação ;
4) documentação sobre termios ;
5) Tabela de chamada do sistema no Linux x64 .

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


All Articles