When it comes to writing on the screen, we aren't limited to writing where the cursor currently is. You surely have seen a program, which despite of its textual interface, was able to write on the screen wherever it wanted. This is just the thing we're going to do now.
Each terminal program has different control sequences and if you want to write programs which will work on every terminal, you should learn the ncurses library. I'm going to describe only a few of the sequences of the standard terminal program, xterm.
First thing: what exactly is a control character (control sequence) anyway?
It is a special sequence of characters describing the behaviour of the terminal.
You surely know at leas a few already: BEL (beep), CR/LF (new line), TAB (the tabulation character).
Now you're going to learn two more: changing the text and background colour and moving
the cursor to another location on the screen.
I've used the file xterm_controls.txt. You can also use that file or read the information available in the manual - man 4 console_codes.
The control sequence responsible for changing the text and background colours looks like this:
ESC[(attr);(txt);(bg)m
,
where:
If, for example, you want to write something in red and then restore the original console settings, you need to normally (using int 80h, EAX=4, EBX=1, ECX=address, EDX=length) display the following sequence:
1bh, "[0;31;40m Your message", 1bh, "[0;37;40m"
.
The last sequence restores the default terminal settings (grey on black). If you're using a terminal with different colour settings than grey on black, you can set your values the same way as for text - the terminal will remember the settings. You can also try the following sequence:
1bh, "[0;31;40m Your message", 1bh, "[0;39;49m"
.
The values 39 and 49 restore the default values for the foreground and background colours,
respectively.
You can also try restoring all attributes' default values (not just colour) without setting
new values:
1bh, "[0;31;40m Your message", 1bh, "[0m"
.
The control sequence responsible for placing the cursor looks like this:
ESC [ r ; c H
,
where:
If, for example, you want to write something in the ninth row of the tenth column, you should normally (using int 80h, EAX=4, EBX=1, ECX=address, EDX=length) display the following sequence:
1bh, "[10;10HMessage"
Here's the promised program for drawing frames:
; Drawing windows with frames around them ; ; Author: Bogdan D. ; ; nasm -O999 -o ramki.o -f elf ramki.asm ; ld -s -o ramki ramki.o section .text global _start _start: mov eax, 4 mov ebx, 1 mov ecx, czysc mov edx, czysc_dl int 80h ; display screen clearing sequence mov ax, (36<<8)+44 ; text and background colour: ; yellow on blue mov bx, 1 ; Upper-Left (U-L) column mov cx, 1 ; U-L row mov si, 9 ; Bottom-Right (B-R) column mov bp, 9 ; B-R row call rysuj_okienko mov ax, (37<<8)+40 ; white on black mov bx, 10 mov cx, 10 mov si, 20 mov bp, 16 call rysuj_okienko mov eax, 4 mov ebx, 1 mov ecx, nwln mov edx, 1 int 80h ; print a newline character mov eax, 1 xor ebx, ebx int 80h ; exit the program rysuj_okienko: ; input: ; ; AH = character attribute (colour) ; AL = background colour ; BX = column of the U-L window corner ; CX = row of the U-L window corner ; SI = colmun of the B-R window corner ; BP = row of the B-R window corner ; ; output: ; nothing ; double ASCII frames ;r_p equ 0bah ; right side ;r_pg equ 0bbh ; top right (corner) ;r_pd equ 0bch ; bottom right ;r_g equ 0cdh ; top ;r_d equ r_g ; bottom ;r_l equ r_p ; left side ;r_lg equ 0c9h ; left top ;r_ld equ 0c8h ; left bottom r_p equ "|" ; right side r_pg equ "\" ; top right (corner) r_pd equ "/" ; bottom right r_g equ "=" ; top r_d equ r_g ; bottom r_l equ r_p ; left side r_lg equ "/" ; left top r_ld equ "\" ; left bottom spacja equ 20h push bx push cx mov dl, r_lg call znak ; draw U-L corner push bx mov dl, r_g ; will draw the top edge ;while BX<SI, draw the top edge .rysuj_gora: inc bx cmp bx, si je .dalej call znak jmp short .rysuj_gora .dalej: mov dl, r_pg call znak ; draw U-R corner pop bx push bx ; draw the middle ;while CX<BP, draw the interior of the frame .rysuj_srodek: inc cx cmp cx, bp je .ostatni mov dl, r_l call znak ; start with left side push bx mov dl, spacja ; spaces inside .rysuj_srodek2: inc bx cmp bx, si ; while BX<SI, draw the interior (spaces) je .dalej2 call znak jmp short .rysuj_srodek2 .dalej2: mov dl, r_p call znak ; draw right side pop bx jmp short .rysuj_srodek .ostatni: mov dl, r_ld call znak ; draw B-L corner pop bx mov dl, r_d ; will draw the bottom edge of the frame .rysuj_dol: inc bx cmp bx, si ;while BX<SI, draw the bottom edge je .dalej3 call znak jmp short .rysuj_dol .dalej3: mov dl, r_pd call znak ; draw B-R corner pop cx pop bx ret znak: ; AH = character attribute (colour) ; AL = background colour ; BX = character column ; CX = character row ; DL = the character to write push eax push ebx push ecx push edx push ax mov dh, 10 shr ax, 8 ; AX = character colour div dh ; AL = AL/10, AH = AL mod 10 add ax, "00" ; add to the quotient and remainder ; the ASCII code of the '0' digit mov [fg], ax ; put the character colour ; number into [fg] pop ax and ax, 0FFh ; AX = background colour div dh ; divide by 10 add ax, "00" mov [bg], ax mov ax, bx ; AX = character column and ax, 0FFh div dh ; divide by 10 add ax, "00" mov [kolumna], ax mov ax, cx ; AX = character row and ax, 0FFh div dh ; divide by 10 add ax, "00" mov [wiersz], ax mov [znaczek], dl ; save what character to display mov eax, 4 mov ebx, 1 mov ecx, pozycja mov edx, napis_dl int 80h ; print the string together with the ; positioning code pop edx pop ecx pop ebx pop eax ret section .data ESC equ 1Bh pozycja db ESC, "[" ; cursor positioning sequence wiersz db "00;" kolumna db "00H" napis db ESC, "[" ; colour change sequence atr db "0;" fg db "00;" bg db "00m" znaczek db "x" ; character to print napis_dl equ $ - pozycja czysc db ESC, "[2J" ; screen clearing sequence czysc_dl equ $ - czysc nwln db 10
Another way to move on the screen is to write to special character devices - the /dev/vcsaN files (root privileges may be required).
The man vcsa page (the example program, strictly speaking) shows that the contents of these files is simple: there are 4 bytes at the beginning, meaning the number of rows, the number of columns (different resolutions are available) and the X and Y cursor coordinates. After these, the screen contents starts (starting with the upper-left corner) - characters and their attributes. The attribute bytes are the same as under DOS:
Here are the colours:
Black - 0, blue - 1, green - 2, skyblue - 3, red - 4, magenta - 5,
brown - 6, light grey (the standard) - 7, dark grey - 8, light blue - 9,
light green - 10, light skyblue - 11, light red - 12, light magenta - 13,
yellow - 14, white - 15.
Now you see there's nothing hard in it - all you have to do is open the file, read the
screen size and write the correct bytes in the correct places (using the seeking function).
Here's an example:
; Program for writing directly to the console file ; ; Author: Bogdan D., bogdandr // op .. pl ; ; assemble: ; ; nasm -O999 -f elf -o konsola.o konsola.asm ; ld -s -o konsola konsola.o %idefine sys_exit 1 %idefine sys_read 3 %idefine sys_write 4 %idefine sys_open 5 %idefine sys_close 6 %idefine sys_lseek 19 %define SEEK_SET 0 %define O_RDWR 02o ; position to display %define nasz_wiersz 10 %define nasza_kolumna 10 section .text global _start _start: mov eax, sys_open ; open the file mov ebx, plik ; file name mov ecx, O_RDWR ; read and write mov edx, 600q ; read and write for the user int 80h ; open cmp eax, 0 jl .koniec mov ebx, eax ; file handle mov eax, sys_read ; read the file (console ; attributes first) mov ecx, konsola ; where to read mov edx, 4 ; how many bytes to read int 80h mov eax, sys_lseek ; seek to the correct position movzx ecx, byte [l_kolumn] imul ecx, nasz_wiersz add ecx, nasza_kolumna ;ECX=row*row length + column shl ecx, 1 ; ECX *= 2, because there are 2 bytes ; on the screen for each character: ; the character and its attribute add ecx, 4 ; +4, because we're moving from ; the beginning of the file mov edx, SEEK_SET ; start at the beginning of the file int 80h mov eax, sys_write ; writing to the file mov ecx, znak ; what to write mov edx, 2 ; how many bytes to write int 80h mov eax, sys_close ; close the file int 80h xor eax, eax ; EAX = 0 = no errors .koniec: mov ebx, eax mov eax, sys_exit int 80h ; exit with zero code or the error, ; from opening the file section .data plik db "/dev/vcsa1", 0 ; first text console file ; attributes of the console: konsola: l_wierszy db 0 l_kolumn db 0 kursor_x db 0 kursor_y db 0 ; character with the attribute we're going to display: znak db "*" atrybut db 43h ; skyblue on red
Another way to write to the screen is to write directly to the text mode memory. This memory is located in segment B800, which corresponds to the linear address B8000, starting with 0. Of course, because of security reasons, the system won't let us write to the address directly, so we have to use some other method. This method is opening a device special file, which symbolizes the computer's whole memory - /dev/mem. On most systems opening this file requires root privileges.
After opening the file we have 2 possibilities. The first one is to use file seeking functions to move around in the file and use the file reading and writing functions to read and write data. This can be slow, but it's always an option. The second possibility is to map the file memory and use it like a plain array. This method will be discussed here in more detail.
To open the file, use a traditional system call:
mov eax, 5 ; sys_open mov ebx, pamiec ; the address of the file name "/dev/mem", 0 mov ecx, 2 ; O_RDWR, read and write mov edx, 666o ; full access int 80h ... pamiec db "/dev/mem", 0
The second step is to map our open file to memory. To do this, use the system call sys_mmap2. It takes 6 arguments:
After a successful call, the system will return in EAX the address of mapped memory area, which we can use (in case of error, we get a value between -4096 and -1 inclusive). An example call looks like this:
mov eax, 192 ; sys_mmap2 xor ebx, ebx ; the system will choose the address mov ecx, 100000h ; the length of the mapped area mov edx, 3 ; PROT_READ | PROT_WRITE, read + write mov esi, 1 ; MAP_SHARED - sharing mode mov edi, [deskryptor] ; the descriptor of the memory file ; from sys_open in the previous step mov ebp, 0 ; starting file offset int 80h
All we have to do now is to use the obtained pointer, for example:
mov byte [eax+0b8000h], 'A'
The screen in text mode consists of 80*25=2000 characters and each of them has an attribute byte following it, which tells the foreground and background colours:
b8000 - character 1, in the top left corner
b8001 - character 1's attribute
b8002 - character 2, one to the right of the character 1
b8003 - character 2's attribute
and so on
What is the attribute?
It's a byte telling the foreground and background colours of a given character. The bits in
this byte mean:
Here are the colours:
Black - 0, blue - 1, green - 2, skyblue - 3, red - 4, magenta - 5,
brown - 6, light grey (the standard) - 7, dark grey - 8, light blue - 9,
light green - 10, light skyblue - 11, light red - 12, light magenta - 13,
yellow - 14, white - 15.
The changes that we make may not immediately appear in the file (on the screen in this case). To force the physical writing of the data, use the sys_msync function. It takes 3 arguments:
An example call looks like this:
mov eax, 144 ; sys_msync mov ebx, 0b8000h ; starting address mov ecx, 4000 ; how many to synchronize mov edx, 0 ; flags int 80h
After finishing working with the file, we can unmap it:
mov eax, 91 ; sys_munmap mov ebx, [pointer] ; pointer obtained from sys_mmap2 mov ecx, 100000h ; the number of bytes int 80h
and close it:
mov eax, 6 ; sys_close mov ebx, [descriptor] ; descriptor of the file "/dev/mem" int 80h
As you can see, mapping files to memory is convenient, because you don't have to seek in the file all the time with the sys_lseek function and perform other time-costly system calls. It's worth to know this. But you have to remember that not all files or devices can be mapped to memory - you shouldn't close your program with an error massage in such case, but use the traditional interface of the file functions.