Poznaj wywołania systemowe Linuxa na przykładzie asemblera x86-64

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:

RejestrZnaczenie
raxnumer wywołania systemowego (syscall number)
rdi1. argument
rsi2. argument
rdx3. argument
r104. argument
r85. argument
r96. 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

SyscallNumerFunkcja
open2otwarcie pliku
read0odczyt danych z pliku
close3zamknięcie pliku
write1wypisanie danych na standardowe wyjście
exit60zakoń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ą.
TUX - maskotka systemu Linux

About the author

Autor "BIELI" to zapalony entuzjasta otwartego oprogramowania, który dzieli się swoją pasją na blogu poznajlinuxa.pl. Jego wpisy są skarbnicą wiedzy na temat Linuxa, programowania oraz najnowszych trendów w świecie technologii. Autor "BIELI" wierzy w siłę społeczności Open Source i zawsze stara się inspirować swoich czytelników do eksplorowania i eksperymentowania z kodem.