Pisanie programów rezydentnych (tzw. demonów) pod Linuksem

W Linuksie są co najmniej dwa sposoby na uruchomienie programu w tle:

  1. dodanie znaczka ampersand (&) do uruchomienia programu, na przykład program param1 param2 param3 &.
  2. skorzystanie z programu screen, na przykład screen -m -d program param1 param2.

Ale nie jesteśmy programistami po to, by liczyć, że nasz program zostanie właśnie tak uruchomiony. Teraz pokażę, jak samemu zadbać o działanie swojego programu w tle. Najpierw posłuży nam do tego funkcja daemon (patrz: man 3 daemon) z biblioteki języka C. Dlatego sposób pisania programu będzie troszkę inny niż zwykle:

  1. zamiast LD do łączenia programu, skorzystamy z GCC (który widząc rozszerzenie .o naszego pliku, połączy go odpowiednio z biblioteką języka C bez kompilowania)
  2. skoro korzystamy z GCC i biblioteki C (która ma już własny symbol _start), to nasz kod będzie się zaczynał etykietą main (taką samą, jak programy w języku C)
  3. funkcja daemon musi być zadeklarowana jako zewnętrzna (extern)

Aby było widać, że nasz demon (odpowiednik TSR w Linuksie) rzeczywiście działa, umieścimy w nim pętlę co jakiś czas wyświetlającą jakiś napis. W celu odmierzania przerwy skorzystamy z funkcji systemowej sys_nanosleep (numer 162).

Jak widać ze strony podręcznika, funkcja daemon przyjmuje 2 argumenty w postaci liczb całkowitych (DWORD):

Po omówieniu tego wszystkiego, przejdźmy wreszcie do przykładowego programu. Jego zadaniem jest przejście w tryb demona i wyświetlanie co 5 sekund wskazanego tekstu w nieskończoność. Działanie będzie więc widoczne na terminalu, z którego uruchamiacie program. Sam program można będzie oczywiście zobaczyć na liście działających procesów (komenda ps -A). Jedynym sposobem na zakończenie programu jest zabicie go poleceniem kill.

A oto kod dla NASMa:


(przeskocz program)
; Program przechodzący w tryb demona systemowego
;
; autor: Bogdan D., bogdandr (at) op.pl
;
; kompilacja:
; nasm -f elf -o demon.o demon.asm
; gcc -o demon demon.o


extern daemon			; deklaracja funkcji zewnętrznej

section .text			; początek sekcji kodu
global main			;symbol "main" musi być globalny dla GCC

main:
	push	dword 1		; drugi argument
	push	dword 1		; pierwszy argument
	call	daemon		; uruchomienie funkcji daemon
	add	esp, 8		; usunięcie argumentów ze stosu

			; przerwa między kolejnymi napisami
			; będzie trwać 5 sekund i 0 nanosekund:
	mov	dword [t1+timespec.tv_nsec], 0
	mov	dword [t1+timespec.tv_sec], 5

.petla:
	mov	eax, 4		; funkcja zapisywania do pliku
	mov	ebx, 1		; standardowe wyjście
	mov	ecx, napis	; co wypisać
	mov	edx, napis_dl	; długość napisu
	int	80h

	mov     eax, 162	; funkcja sys_nanosleep
	mov     ebx, t1		; tyle czekać
	mov     ecx, 0	; ewentualny adres drugiej struktury timespec
	int     80h		;  robimy przerwę...

	jmp	.petla		; i od nowa....

				; poniższy kod nie będzie wykonany
	mov	eax, 1
	xor	ebx, ebx
	int	80h		; wyjście z programu

section .data

napis		db	"Tu Twoj demon mowi.", 10
napis_dl	equ	$ - napis

struc timespec			; definicja struktury timespec
				; (tylko jako typ danych)
	.tv_sec:	resd 1
	.tv_nsec:	resd 1
endstruc

t1 istruc timespec		; tworzymy zmienną t1 jako całą
				; strukturę timespec

Kompilacja i łączenie odbywa się tak:

	nasm -f elf -o demon.o demon.asm
	gcc -o demon demon.o

O jednej rzeczy należy wspomnieć: sam fakt, że program jest demonem NIE musi oznaczać, że działa na prawach administratora (i całe zabezpieczenie systemu jest do niczego).


Programy rezydentne z wykorzystaniem int 80h

Funkcja daemon jest co prawda z biblioteki C, ale można ją przerobić na kod korzystający wyłącznie z przerwania int 80h. Sam kod funkcji jest w pliku misc/daemon.c w źródłach biblioteki glibc. Nie jest on za długi i dość łatwo można go przerobić na takie oto makro:


(przeskocz makro)
%macro daemon 2
	; pierwszy parametr: nochdir - czy nie zmieniać katalogu na główny?
	; drugi parametr: noclose - czy nie zamykać stdin i stdout?

	mov	eax, 2
	int	80h		; sys_fork

	cmp	eax, 0
	jl	%%koniec	; EAX < 0 oznacza błąd

	test	eax, eax
	jz	%%dalej		; EAX = 0 w procesie potomnym
				; EAX > 0 w procesie rodzica

	mov	eax, 1
	xor	ebx, ebx
	int	80h		; sys_exit - rodzic kończy pracę.

 %%glowny:	db	"/", 0
 %%devnull:	db	"/dev/null", 0

 %%dalej:
	mov	eax, 66		; sys_setsid
	int	80h		; tworzymy nową sesję i ustawiamy GID

	cmp	eax, 0
	jl	%%koniec	; EAX < 0 oznacza błąd

	%if %1 = 0

		mov	eax, 12		; sys_chdir
		mov	ebx, %%glowny
		int	80h		; zmieniamy katalog na główny
	%endif

	%if %2 = 0

		; otwieramy /dev/null:
		mov	eax, 5
		mov	ebx, %%devnull
		mov	ecx, 2
		mov	edx, 0
		int	80h

		cmp	eax, 0
		jl	%%koniec	; EAX < 0 oznacza błąd

		mov	ebx, eax	; EBX = deskryptor /dev/null

		;duplikujemy deskryptory standardowego wejścia, wyjścia i
		;wyjścia błędów do deskryptora otwartego /dev/null, po czym
		; ten /dev/null zamkamy

		mov	eax, 63
		mov	ecx, 0		; wejście
		int	80h

		mov	eax, 63
		mov	ecx, 1		; wyjście
		int	80h

		mov	eax, 63
		mov	ecx, 2		; wyjście błędów
		int	80h

		mov	eax, 6
		int	80h		; zamykamy /dev/null

	%endif
 %%koniec:

%endmacro

; użycie:
	daemon 1, 1	; bez żadnych PUSH ani ADD ESP

Teraz można już powrócić do starego schematu programu, gdzie symbolem startowym był _start, a łączenie odbywało się za pomocą LD, a nie GCC z biblioteką C.


Spis treści off-line (klawisz dostępu 1)
Spis treści on-line (klawisz dostępu 2)
Ułatwienia dla niepełnosprawnych (klawisz dostępu 0)