Accessing the screen under Linux

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.

Using the terminal's control sequences

(skip control sequences)

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.

Colouring the text

(skip text colouring)

The control sequence responsible for changing the text and background colours looks like this:

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".

Changing the current cursor position

(skip to the program)

The control sequence responsible for placing the cursor looks like this:
ESC [ r ; c H,

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:

(skip the program)
; 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

	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


; 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
	inc	bx
	cmp	bx, si
	je	.dalej
	call	znak
	jmp	short .rysuj_gora

	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
	inc	cx
	cmp	cx, bp
	je	.ostatni

	mov	dl, r_l
	call	znak			; start with left side

	push	bx
	mov	dl, spacja		; spaces inside
	inc	bx
	cmp	bx, si		; while BX<SI, draw the interior (spaces)
	je	.dalej2
	call	znak
	jmp	short .rysuj_srodek2

	mov	dl, r_p
	call	znak			; draw right side
	pop	bx

	jmp	short .rysuj_srodek

	mov	dl, r_ld
	call	znak			; draw B-L corner
	pop	bx

	mov	dl, r_d		; will draw the bottom edge of the frame
	inc	bx
	cmp	bx, si			;while BX<SI, draw the bottom edge
	je	.dalej3
	call	znak
	jmp	short .rysuj_dol

	mov	dl, r_pd
	call	znak			; draw B-R corner

	pop	cx
	pop	bx



;  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


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

Using the /dev/vcsaN character devices

(skip vcsa)

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:

(skip the vcsa 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

	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

	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:
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

Using memory mapping

(skip memory mapping)

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:

  1. EBX = the address under which we want the file to be mapped to. Best is to specify zero, so the system chooses a suitable address
  2. ECX = the length of the mapped file area, in bytes. We will specify 100000h here, to be sure that have the area starting at B8000 and of length 4000 bytes (the number of bytes required for one screen in text mode, for the characters and their attributes)
  3. EDX = the access mode to the shared memory. If we want read and write access, we give PROT_READ=1 + PROT_WRITE=2 here
  4. ESI = sharing mode for the mapped memory. We set this to MAP_SHARED=1 (shared, not private)
  5. EDI = the descriptor of the open file to be mapped
  6. EBP = the starting offset in the file to be mapped. This address is given in system page size units, which can vary on different systems. The easiest is to put zero here and add B8000 to the addresses later

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:

  1. EBX = start address of the data to synchronize
  2. ECX = the number of bytes to synchronize
  3. EDX = 0 or ORed flags: MS_ASYNC=1 (perform asynchronously), MS_INVALIDATE=2 (invalidate the area after writing), MS_SYNC (perform synchronously)

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.

On-line contents (Alt+2)
Helpers for people with disabilities (Alt+0)