Non-canonical terminal mode and non-blocking input to nasm

The idea of ​​writing a game in assembler language, of course, is unlikely to come to anyone by itself, but it is such a sophisticated form of reporting that has long been practiced in the first year of the MSU MSU. But as progress does not stand still, both DOS and masm become history, and nasm and Linux come to the forefront of training bachelors. Perhaps in ten years the management of the faculty will discover python, but this is not the issue now.
 
 
Programming in assembler for Linux, with all its advantages, makes it impossible to use BIOS interrupts and as a result impairs functionality. Instead, you have to use system calls and contact the terminal's api. Therefore, writing a simulator of blackjack or sea battle does not cause much difficulty, and with the most common snake there are problems. The point is that the I /O system is controlled by the terminal, and the C system functions can not be used directly. Therefore, when writing even fairly simple games, two stumbling blocks are born: how to switch the terminal to non-canonical mode and how to make input from the keyboard non-blocking. This will be discussed in the article.
 
gives us. an example from the official GNU documentation , if you remove the helper code from it:
 
 
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. * /
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 this code, STDIN_FILENO means the input stream descriptor we are working with (default is 0), ICANON is the flag for the inclusion of the canonical input itself, ECHO is the display flag for input characters on the screen, and TCSANOW and TCSAFLUSH are the macros defined by the library. Thus, the "bare" algorithm, devoid of security checks, looks like this:
 
 
 
preserve the original termios structure;
 
copy its contents with the change of flags ICANON and ECHO;
 
send the modified structure to the terminal;
 
Upon termination of work to return to the terminal the saved structure.
 
 
It remains to understand what the library functions tcsetattr and tcgetattr do. Actually they do a lot of things, but the key in their work is the system call ioctl . The first argument is the stream's descriptor (0 in our case), the second is the set of flags that are defined by the TCSANOW and TCSAFLUSH macros, and the third is the pointer to the structure (in our case, termios). On the syntax nasm and under the convention of system calls on linux it will take the following form:
 
 
mov rax, 16; number of the system call ioctl
mov rdi, 0; number of the standard input descriptor
mov rsi, TCGETS; set of flags
mov rdx, tattr; address of memory area with structure
syscall

 
In general, this is the whole essence of the functions tcsetattr and tcgetattr. For the rest of the code, we need to know the size and structure of the termios structure, which is also easy to find in official documentation . Its default size is 60 bytes, and the array of flags we need is 4 bytes in size and is the fourth one by the count. It remains to write two procedures and merge into one code.
 

 
Under the spoiler the simplest implementation of it, far from being the most secure, but completely working on any OS that supports POSIX standards. The values ​​of the macros were taken from the above sources of the standard C library.
 

 
Translation into non-canonical mode [/b]
    % define ICANON 2
% define ECHO 8
% define TCGETS 21505; attribute to get the structure
% define TCPUTS 21506; attribute to send structure to
global setcan; procedure for switching to the canonical mode
global setnoncan; procedure for switching to non-canonical mode
section .bss
stty resb 12; termios size - 60 bytes
slflag resb 4; slflag is the fourth after 3 * 4 bytes of memory
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. Non-blocking input in the terminal


 
For non-blocking input of means of the terminal to us will not suffice. We will write a function that will check the standard stream buffer for readiness to transmit information: if there is a symbol in the buffer, it will return its code; if the buffer is empty, it returns 0. For this purpose, you can use two system calls - poll () or select (). They are both able to view different I /O streams for the fact of an event. For example, if information is received in one of the streams, then both of these system calls are able to skip this and display it in the returned data. However, the second one is essentially an improved version of the first one and is useful when working with multiple threads. We do not have such a goal (we work only with the standard thread), so let's use the poll () call.
 

 
It also takes three parameters into the input:
 

 
  1.  
  2. a pointer to the data structure, which contains information about the descriptors of the monitored flows (discussed below);  
  3. the number of processed threads (we have one);  
  4. time in milliseconds, during which you can expect an event (we need it to come immediately, so this parameter is 0).  

 
From documentation you can find out that the desired data structure has the following device:
 

 
    struct pollfd {
int fd; /* file descriptor * /
short events; /* Requested events * /
short revents; /* returned events * /
};};

 
The descriptor is used as a file descriptor (we work with the standard stream, so it is equal to 0), and as the requested events - various flags, from which we need only the flag of data availability in the buffer. It has the name POLLIN and is equal to 1. The field of the returned events is ignored, because we do not give any information to the input stream. Then the required system call will look like this:
 

 
    section .data
fd dd 0; descriptor of the standard input stream
eve dw 1; only one attribute is POLLIN
rev dw 0; not used
section .text
poll: nop
push rbx
push rcx
push rdx
push rdi
push rsi
mov rax, 7; the number of the poll system call
mov rdi, fd; pointer to the structure
mov rsi, 1; monitor one stream
mov rdx, 0; we do not give time to wait for
syscall

 
The poll () system call returns the number of threads in which "interesting" events occurred. Since we only have one thread, the return value is either 1 (there are entered data) or 0 (there are none). If the buffer is nonempty, we immediately make another system call - read - and read the code of the entered character. In the end, we get the following code.
 

 
Non-blocking input in the terminal [/b]
    section .data
fd dd 0; descriptor of the standard input stream
eve dw 1; only one attribute is POLLIN
rev dw 0; not used
sym db 1
section .text
poll: nop
push rbx
push rcx
push rdx
push rdi
push rsi
mov rax, 7; the number of the poll system call
mov rdi, fd; pointer to the structure
mov rsi, 1; monitor one stream
mov rdx, 0; we do not give time to wait for
syscall
test rax, rax; check the returned value to 0
jz .e
mov rax, 0
mov rdi, 0; if the data is
mov rsi, sym; then make the call read
mov rdx, 1
syscall
xor rax, rax
mov al, byte[sym]; return the character code if it was read
. e: pop rsi
pop rdi
pop rdx
pop rcx
pop rbx
ret

 
 
Thus, now you can use the poll function to read the information. If there are no entered data, that is, no button was pressed, it will return 0 and thus not block our process. Of course, for a given implementation, if the flaws, in particular, it can only work with the symbols of ascii, but it easily changes depending on the task.
 
 
The three functions described above (setcan, setnoncan and poll) are sufficient to adjust the terminal input to suit your needs. They are prohibitively simple for both understanding and use. However, in a real game it would be nice to secure them in accordance with the usual approach to C, but this is already a programmer's business.
 
 

Sources


 
1) The sources of the functions tcgetattr and tcsetattr ;
 
2) Documentation for the system call ioctl ;
 
3) Documentation of the system call poll ;
 
4) Documentation for termios ;
 
5) Table of system calls for Linux x64 .
+ 0 -

Add comment