System Linux, jak każdy system operacyjny, udostępnia programom różnorodne funkcje, takie jak dostęp do plików, komunikacja czy zarządzanie procesami. Te funkcje są realizowane przez wywołania systemowe (ang. system calls), które pozwalają aplikacjom komunikować się bezpośrednio z jądrem systemu.
W tym artykule pokażę, jak działają wywołania systemowe w Linuxie na architekturze x86-64, na przykładzie prostego programu w asemblerze NASM, który:
- otwiera plik,
- odczytuje pierwszą linię zawierającą ciąg znaków
'0'
i'1'
, - konwertuje ją na wartość bitową,
- wyświetla oryginalną linię i jej wartość szesnastkową na standardowe wyjście.
Co to są wywołania systemowe?
Wywołania systemowe to specjalne funkcje jądra systemu operacyjnego, które umożliwiają programom użytkownika wykonywanie operacji niedostępnych bezpośrednio z poziomu kodu aplikacji, np. odczyt z dysku, zapis na ekran, zarządzanie procesami.
W Linuksie, na architekturze x86-64, wywołania systemowe uruchamiamy poprzez instrukcję syscall
. Argumenty przekazujemy w rejestrach procesora:
Rejestr | Znaczenie |
---|---|
rax | numer wywołania systemowego (syscall number) |
rdi | 1. argument |
rsi | 2. argument |
rdx | 3. argument |
r10 | 4. argument |
r8 | 5. argument |
r9 | 6. argument |
Wynik działania syscall zwracany jest w rejestrze rax
.
Nasz przykład: odczyt i wyświetlanie flag z pliku
Cel programu
- Otworzyć plik
flags.txt
do odczytu. - Wczytać pierwszą linię, która jest ciągiem zer i jedynek (np.
"10101101"
). - Zamienić ją na bajt bitowy, gdzie
'0'
to bit 0, a'1'
to bit 1. - Wypisać na ekran tekst z pliku oraz wartość hex tego bajtu.
Kluczowe wywołania systemowe użyte w programie
Syscall | Numer | Funkcja |
---|---|---|
open | 2 | otwarcie pliku |
read | 0 | odczyt danych z pliku |
close | 3 | zamknięcie pliku |
write | 1 | wypisanie danych na standardowe wyjście |
exit | 60 | zakończenie programu |
Fragmenty kodu i ich wyjaśnienie
Otwieranie pliku
mov rax, 2 ; syscall: open
mov rdi, filename ; nazwa pliku (adres w pamięci)
mov rsi, 0 ; tryb tylko do odczytu (O_RDONLY)
syscall
test rax, rax ; sprawdź czy otwarcie powiodło się
js open_error ; jeśli rax < 0, to błąd
mov [filedesc], eax ; zachowaj deskryptor pliku
open
zwraca deskryptor pliku (liczba całkowita) lub błąd (<0).
Odczyt danych z pliku
mov rax, 0 ; syscall: read
mov edi, [filedesc] ; deskryptor pliku
mov rsi, buffer ; adres bufora do wczytania danych
mov rdx, 64 ; ile bajtów czytać
syscall
test rax, rax
jle read_error ; jeśli 0 lub błąd - problem
mov rcx, rax ; zapisz ile bajtów wczytano
Wypisywanie na ekran
mov rax, 1 ; syscall: write
mov rdi, 1 ; stdout
mov rsi, text_label ; wskaźnik na napis "Flagi: "
mov rdx, 7 ; długość napisu
syscall
Cały kod programu (NASM x86-64)
; -- kod jak w pełnej wersji, którą podałem wcześniej --
Pełna wersja kodu w języku Assembler x86_64 poniżej:
section .data
filename db "flags.txt", 0
text_label db "Flagi: ", 0
hex_label db "HEX: 0x", 0
error_open db "Blad: nie mozna otworzyc pliku.", 10, 0
error_read db "Blad: nie mozna odczytac pliku.", 10, 0
newline db 10, 0
section .bss
buffer resb 64
bit_value resb 1
filedesc resd 1
section .text
global _start
_start:
; === open("flags.txt", O_RDONLY)
mov rax, 2 ; sys_open
mov rdi, filename
mov rsi, 0
syscall
test rax, rax
js open_error
mov [filedesc], eax
; === read(fd, buffer, 64)
mov rax, 0 ; sys_read
mov edi, [filedesc]
mov rsi, buffer
mov rdx, 64
syscall
test rax, rax
jle read_error
mov rcx, rax ; liczba bajtów wczytanych
; === Parsowanie "1010..." do bajtu
xor rbx, rbx ; accumulator na bit_value
xor rdi, rdi ; indeks do parsowania
parse_loop:
cmp rdi, rcx
jge parse_done
mov al, [buffer + rdi]
cmp al, 10 ; znak nowej linii?
je parse_done
shl rbx, 1
cmp al, '1'
jne .skip_set_bit
or rbx, 1
.skip_set_bit:
inc rdi
jmp parse_loop
parse_done:
mov [bit_value], bl
mov r8, rdi ; zachowujemy długość tekstu w r8
; === close file
mov rax, 3 ; sys_close
mov edi, [filedesc]
syscall
; === write(1, "Flagi: ", 7)
mov rax, 1 ; sys_write
mov rdi, 1 ; stdout
mov rsi, text_label
mov rdx, 7
syscall
; === write(1, buffer, r8) - oryginalny tekst
mov rax, 1
mov rdi, 1
mov rsi, buffer
mov rdx, r8
syscall
; === write newline
mov rax, 1
mov rdi, 1
mov rsi, newline
mov rdx, 1
syscall
; === write(1, "HEX: 0x", 7)
mov rax, 1
mov rdi, 1
mov rsi, hex_label
mov rdx, 7
syscall
; === konwersja bit_value do HEX i wypisanie
movzx rax, byte [bit_value]
mov rbx, rax
shr rbx, 4
call print_hex_digit
movzx rbx, byte [bit_value]
and rbx, 0x0F
call print_hex_digit
; === write newline
mov rax, 1
mov rdi, 1
mov rsi, newline
mov rdx, 1
syscall
exit:
mov rax, 60 ; sys_exit
xor rdi, rdi
syscall
; === Obsługa błędów ===
open_error:
mov rax, 1
mov rdi, 1
mov rsi, error_open
mov rdx, 32
syscall
mov rax, 60
mov rdi, 1
syscall
read_error:
mov rax, 1
mov rdi, 1
mov rsi, error_read
mov rdx, 32
syscall
mov rax, 60
mov rdi, 2
syscall
; === Funkcja pomocnicza do wypisania jednej cyfry HEX (rbx=0..15) ===
print_hex_digit:
cmp rbx, 9
jbe .digit
add rbx, 87 ; 'a' = 97, 10 + 87 = 97
jmp .print
.digit:
add rbx, 48 ; '0' = 48
.print:
mov rax, 1
mov rdi, 1
sub rsp, 8 ; zarezerwuj miejsce na stosie
mov byte [rsp], bl
mov rsi, rsp
mov rdx, 1
syscall
add rsp, 8
ret
Kompilacja i uruchomienie pod Linuxem
$ cat "00000011" > flags.txt
$ nasm -f elf64 -o flags.o flags.asm
$ ld -o flags flags.o
$ ./flags
Flagi: 00000011
HEX: 0x03
Podsumowanie i wnioski
- Wywołania systemowe są podstawą interakcji programu z systemem operacyjnym.
- W asemblerze na Linux x86-64 wywołujemy je instrukcją
syscall
z odpowiednimi rejestrami. - Nawet prosty program potrafi efektywnie korzystać z systemu plików i wyświetlać dane.
- Pisanie kodu w asemblerze wymaga dokładności, m.in. w obsłudze rejestrów i długości danych.
Co dalej?
Jeśli zainteresował Cię temat programowania niskopoziomowego w Linuxie, możesz:
- Poznać więcej syscalli (np.
mmap
,fork
,execve
). - Napisać własny loader lub prosty shell.
- Eksperymentować z asemblerem i C, łącząc wygodę z wydajnością.