终端的非规范模式和nasm上的非阻塞输入

当然,用汇编语言编写游戏的想法本身不可能让人想到,但是,这种复杂的报告形式早在莫斯科国立大学VMK的第一年就已实行。 但是,由于进展不会停滞不前,因此DOS和masm都已成为历史,nasm和Linux成为准备单身汉的最前沿。 也许十年后,教职员工的领导层将发现python,但这与现在无关。

Linux下的汇编程序编程具有所有优点,因此无法使用BIOS中断,因此剥夺了它们的功能。 相反,他们必须使用系统调用并联系终端api。 因此,编写二十一点或海战模拟器不会造成很大的困难,而且最普通的蛇也有问题。 事实是输入输出系统由终端控制,C系统功能不能直接使用。 因此,即使编写相当简单的游戏,也有两个绊脚石:如何将终端切换到非规范模式,以及如何使键盘输入成为非障碍物。 这将在本文中讨论。

1.终端的非规范模式


如您所知,要了解C中的函数的功能,您需要像C中的函数一样思考。 幸运的是,将终端切换到非规范模式并不是那么困难。 如果您从中删除了辅助代码,这就是GNU官方文档中示例

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

在此代码中,STDIN_FILENO表示我们正在使用的输入流的句柄(默认为0),ICANON是同一规范输入的启用标志,ECHO是用于在屏幕上显示输入字符的标志,TCSANOW和TCSAFLUSH是库定义的宏。 因此,没有安全检查的“裸”算法如下所示:

  1. 保留原始termios结构;
  2. 使用ICANON和ECHO标志的更改复制其内容;
  3. 将改变后的结构发送给终端;
  4. 工作完成后,将保存的结构返回到终端。

仍然需要了解库函数tcsetattr和tcgetattr的功能。 实际上,他们做了很多事情,但是ioctl系统调用是他们工作的关键。 它使用的第一个参数是流描述符(在我们的例子中为0),第二个参数是一组仅由TCSANOW和TCSAFLUSH宏定义的标志,第三个是指向结构的指针(在我们的例子中为termios)。 在nasm语法上以及在Linux上的系统调用约定下,它将采用以下形式:

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

通常,这是tcsetattr和tcgetattr函数的重点。 对于其余的代码,我们需要知道termios结构的大小和结构,这在官方文档中也很容易找到。 默认情况下,它的大小是60个字节,我们需要的标志数组的大小是4个字节,位于行的第四位。 仍然需要编写两个过程并将它们组合为一个代码。

在扰流器下,其最简单的实现绝不是最安全的,但在任何支持POSIX标准的操作系统上都可以很好地工作。 宏值取自上述标准C库的来源。

转移到非规范模式
 %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.终端中的非阻塞输入


对于无阻碍的资金投入,终端对我们来说还不够。 我们将编写一个函数,该函数将检查标准流缓冲区是否准备好传输信息:如果缓冲区中有符号,它将返回其代码; 如果缓冲区为空,则将返回0。为此,您可以使用两个系统调用-poll()或select()。 它们都能够根据任何事件的事实查看各种输入输出流。 例如,如果信息已到达任何流中,则这两个系统调用都可以捕获此信息并将其显示在返回的数据中。 但是,它们中的第二个本质上是第一个的改进版本,在使用多个线程时很有用。 我们没有这样的目标(我们仅适用于标准流),因此我们将使用poll()调用。

它还接受三个参数作为输入:

  1. 指向数据结构的指针,该数据结构包含有关被监视流的描述符的信息(我们将在下面讨论);
  2. 处理的线程数(我们有一个);
  3. 可以预计发生事件的时间(以毫秒为单位)(我们需要立即发生该事件,因此此参数为0)。

文档中可以发现所需的数据结构具有以下设备:

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

它的描述符用作文件描述符(我们使用标准流,因此它为0),并且请求的标志是各种标志,对于缓冲区中是否存在数据,我们仅需要这些标志。 它的名称为POLLIN且等于1。我们忽略返回事件的字段,因为我们没有向输入流提供任何信息。 然后,所需的系统调用将如下所示:

 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 

poll()系统调用返回发生“有趣”事件的线程数。 由于我们只有一个线程,因此返回值为1(输入了数据)或0(没有)。 但是,如果缓冲区不为空,那么我们将立即进行另一个系统调用-读取-并读取输入字符的代码。 结果,我们得到以下代码。

终端中的非阻塞输入
 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 


因此,现在您可以使用轮询功能来读取信息。 如果没有输入任何数据,即没有按下任何按钮,则它将返回0,因此不会阻塞我们的过程。 当然,此实现有缺陷,特别是它只能与ASCII字符一起使用,但是可以根据任务轻松进行更改。

上述三个功能(setcan,setnoncan和poll)足以为您和您自己调整终端输入。 它们对于理解和使用都非常简单。 但是,在真实游戏中,按照常规的C方法保护它们会很好,但这已经是程序员的工作了。

资料来源


1) tcgetattr和tcsetattr函数来源
2) ioctl系统调用文档
3) 轮询系统调用文档
4) 关于termios的文档
5) Linux x64下的系统调用表

Source: https://habr.com/ru/post/zh-CN414309/


All Articles