Odpalasz strace na procesie, który intensywnie wywołuje syscalle, i throughput wyraźnie spada. Podpinasz gdb do działającej usługi — i proces staje. To koszt zatrzymywania wykonania i przekazywania kontroli debuggerowi przez ptrace. Jego skala zależy od programu i sposobu śledzenia.
Zrozumienie mechaniki ptrace odpowiada na trzy pytania, które prędzej czy później zadaje sobie każdy, kto debuguje na Linuksie: dlaczego śledzenie syscalli jest tak kosztowne, dlaczego gdb -p potrafi odbić się od systemu z Operation not permitted, i czym właściwie jest breakpoint, skoro procesor nie ma pojęcia o istnieniu twojego kodu źródłowego.
Jeden syscall, cały ekosystem debuggerów
ptrace dostarcza zestaw operacji do kontroli śledzonego wątku. Narzędzia diagnostyczne korzystają też z innych interfejsów, między innymi perf, process_vm_readv() i eBPF.
Model jest asymetryczny: tracer i tracee. Relacja działa per-thread — każdy wątek śledzony jest osobno — i jest wyłączna: jeden tracee ma dokładnie jednego tracera. Dlatego nie podepniesz gdb do procesu, który już wisi pod strace; kernel zwróci EPERM i koniec dyskusji. Komunikacja jest brutalnie prosta: tracee zatrzymuje się w jednym z kilku rodzajów stopów, kernel powiadamia tracera przez waitpid(), tracer grzebie w zamrożonym procesie — rejestry, pamięć, sygnały — i decyduje, jak go wznowić. Wszystko, co robi debugger, od wypisywania syscalli po step-debugging, to kompozycja tej jednej pętli: stop, inspekcja, wznowienie.
Trzy drogi przejęcia procesu
PTRACE_TRACEME + execve()
Klasyczny wzorzec „uruchom pod debuggerem”. Proces potomny po fork() i copy-on-write woła ptrace(PTRACE_TRACEME, ...) — deklaruje: zgadzam się być śledzony przez rodzica — a następnie wykonuje execve(). Po udanym execve kernel dostarcza dziecku SIGTRAP, więc nowy program zatrzymuje się, zanim wykona pierwszą własną instrukcję. Dokładnie w tym punkcie strace ./app i gdb ./app zaczynają sesję: mają proces zamrożony na starcie, z pełną kontrolą, zanim cokolwiek się wydarzy.
PTRACE_ATTACH
Podpięcie do działającego wątku. PTRACE_ATTACH wysyła mu SIGSTOP, ale samo wywołanie nie oznacza jeszcze, że wątek się zatrzymał — tracer musi odebrać zdarzenie przez waitpid(). Signal-delivery-stop i późniejszy group-stop to odrębne zdarzenia, co komplikuje obsługę konkurujących sygnałów.
PTRACE_SEIZE
Nowszy interfejs, zaprojektowany po lekcjach z ATTACH: przejmuje tracee bez zatrzymywania go. Proces działa dalej, a tracer zatrzymuje go dopiero wtedy, gdy faktycznie chce — jawnym PTRACE_INTERRUPT. SEIZE raportuje też stopy w sposób jednoznacznie rozróżnialny (PTRACE_EVENT_STOP), co eliminuje całą klasę pytań „czy to był mój SIGSTOP, czy cudzy”. Współczesny strace -p pod spodem używa właśnie SEIZE. Jeżeli piszesz własny tracer w 2026 i zaczynasz od ATTACH — zaczynasz od legacy.
Signal-delivery-stop — tracer widzi sygnały pierwszy
Gdy niezablokowany sygnał ma zostać dostarczony śledzonemu wątkowi, zwykle pojawia się signal-delivery-stop. Tracer może wtedy przekazać sygnał dalej, zastąpić go innym lub wyciszyć. SIGKILL nie daje takiej możliwości. Sygnał pozostający w masce blocked nie wywołuje jeszcze tego stopu.
Tracer nie może przechwycić ani wyciszyć SIGKILL. Nie znaczy to jednak, że proces zniknie natychmiast w każdym stanie kernela — nieprzerywalne oczekiwanie może opóźnić zakończenie.
Syscall-stopy, czyli minimalny strace w C
W typowej sekwencji PTRACE_SYSCALL obserwujesz wejście i wyjście z syscalla. Nie wolno jednak zakładać ślepej naprzemienności: proces może zakończyć się w syscallu, a execve(), sygnały i zdarzenia ptrace komplikują przebieg. Rodzaj stopu można odczytać przez PTRACE_GET_SYSCALL_INFO (Linux 5.3+). Opcja PTRACE_O_TRACESYSGOOD pozwala odróżnić syscall-stop od zwykłego SIGTRAP.
Poniższy szkic pokazuje pojedynczy śledzony wątek na Linux 5.3+. Rozpoznaje wejście i wyjście po typie zdarzenia, zamiast liczyć stopy parami. Nie śledzi potomków ani nie implementuje pełnej obsługi group-stopów.
Zakres weryfikacji: przykład przeszedł kompilację z gcc -Wall -Wextra. Uruchomienia tracera nie udało się potwierdzić w środowisku przygotowania tekstu; przed użyciem sprawdź go na kontrolowanym hoście Linux z dozwolonym ptrace.
#define _GNU_SOURCE
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ptrace.h>
#include <linux/ptrace.h>
#include <sys/wait.h>
#include <unistd.h>
int main(int argc, char **argv) {
if (argc < 2) return 2;
pid_t child = fork();
if (child == -1) { perror("fork"); return 1; }
if (child == 0) {
if (ptrace(PTRACE_TRACEME, 0, 0, 0) == -1) _exit(127);
execvp(argv[1], argv + 1);
_exit(127);
}
int status;
if (waitpid(child, &status, 0) != child || !WIFSTOPPED(status)) return 1;
if (ptrace(PTRACE_SETOPTIONS, child, 0,
PTRACE_O_TRACESYSGOOD | PTRACE_O_EXITKILL) == -1) {
perror("PTRACE_SETOPTIONS"); return 1;
}
int sig = 0, have_entry = 0;
unsigned long long nr = 0;
for (;;) {
if (ptrace(PTRACE_SYSCALL, child, 0, sig) == -1) {
perror("PTRACE_SYSCALL"); return 1;
}
do {
if (waitpid(child, &status, 0) == child) break;
if (errno != EINTR) { perror("waitpid"); return 1; }
} while (1);
if (WIFEXITED(status) || WIFSIGNALED(status)) break;
if (!WIFSTOPPED(status)) continue;
sig = 0;
if (WSTOPSIG(status) == (SIGTRAP | 0x80)) {
struct ptrace_syscall_info info = {0};
if (ptrace(PTRACE_GET_SYSCALL_INFO, child, sizeof(info), &info) == -1) {
perror("PTRACE_GET_SYSCALL_INFO"); return 1;
}
if (info.op == PTRACE_SYSCALL_INFO_ENTRY) {
nr = info.entry.nr; have_entry = 1;
} else if (info.op == PTRACE_SYSCALL_INFO_EXIT && have_entry) {
fprintf(stderr, "syscall(%llu) = %lld\n", nr,
(long long)info.exit.rval);
have_entry = 0;
}
} else {
siginfo_t si;
if (ptrace(PTRACE_GETSIGINFO, child, 0, &si) == -1) {
fprintf(stderr, "Stop poza zakresem tego prostego tracera\n");
return 1; /* EXITKILL zakończy tracee */
}
sig = WSTOPSIG(status);
}
}
return 0;
}PTRACE_O_EXITKILL kończy tracee, gdy tracer umrze. PTRACE_GET_SYSCALL_INFO rozróżnia wejście i wyjście również wtedy, gdy pierwszy zaobserwowany syscall-stop jest wyjściem z execve(). Numery syscalli nadal zależą od architektury. Kod celowo kończy pracę przy nieobsługiwanym stopie; nie jest zamiennikiem strace.
Jak gdb ustawia breakpoint
Typowy programowy breakpoint na x86 polega na podmianie pierwszego bajtu instrukcji na 0xCC, czyli int3. Kernel raportuje pułapkę debuggerowi jako SIGTRAP. Procesory mają też sprzętowe breakpointy i watchpointy — opis poniżej dotyczy wariantu programowego.
Dwie rzeczy z tego wynikają. Po pierwsze: sekcja .text jest zmapowana bez prawa zapisu, ale ptrace pisze przez interfejs kernela, z pominięciem ochrony stron widzianej przez sam proces — to ten sam mechanizm ochrony pamięci, który przy zwykłym zapisie z userspace skończyłby się tym, co opisuje anatomia segfaulta od MMU po core dump. Po drugie: adres breakpointu trzeba najpierw znaleźć, a między adresem w binarce a adresem w pamięci stoi ASLR, relokacja i dynamic linker — jak segmenty trafiają do pamięci, zanim ruszy main(), rozbiera ELF i dynamic linking.
Skąd bierze się narzut ptrace
Śledzenie przez PTRACE_SYSCALL zwykle wymaga zatrzymań na wejściu i wyjściu. Tracer musi dostać czas CPU, odczytać zdarzenie dodatkowymi wywołaniami i wznowić tracee. Czytanie pamięci dokłada pracę: przy 64-bitowym ABI odczyt 4 KiB słowo po słowie przez PTRACE_PEEKDATA oznacza 512 wywołań. process_vm_readv() może skopiować większy blok naraz, ale samo nie usuwa zatrzymań śledzenia.
W syscall-heavy workloadzie narzut może być duży. Dlatego zakres i czas śledzenia dobierasz do konkretnego pytania; wyników z strace nie traktujesz jak pomiaru wydajności bez instrumentacji.
| Metryka | ptrace (PTRACE_SYSCALL) | ptrace + seccomp-BPF | eBPF (tracepointy) |
|---|---|---|---|
| Koszt jednego zdarzenia | Zatrzymania, przełączenia i dodatkowe syscalle tracera | Jak ptrace, ale tylko dla filtrowanych syscalli | Wykonanie programu BPF w kernelu; koszt zależy od pomiaru |
| Wpływ na tracee | Zatrzymanie śledzonego wątku na granicach syscalla | Stop wyłącznie na dopasowanych wywołaniach | Brak zatrzymań; mały, mierzalny narzut |
| Modyfikacja stanu (rejestry, pamięć, sygnały) | Pełna | Pełna dla zatrzymanych wywołań | Brak — obserwacja read-only |
| Wymagane uprawnienia | YAMA ptrace_scope lub CAP_SYS_PTRACE | Jak ptrace + instalacja filtra seccomp | CAP_BPF + CAP_PERFMON albo root |
YAMA, czyli dlaczego gdb -p rzuca „Operation not permitted”
Sam zgodny UID nie wystarcza do opisu uprawnień: znaczenie mają też stan dumpable, capabilities i polityki LSM. Yama może nałożyć kolejne ograniczenia przez kernel.yama.ptrace_scope: 0 — bez dodatkowych ograniczeń Yama; 1 — zwykle relacja przodek–potomek lub jawne PR_SET_PTRACER; 2 — wymagane CAP_SYS_PTRACE; 3 — zakaz attach i TRACEME, którego nie cofniesz do restartu.
$ cat /proc/sys/kernel/yama/ptrace_scope
1
$ gdb -p "$(pidof myapp)"
ptrace: Operation not permitted.
$ sudo sysctl -w kernel.yama.ptrace_scope=0 # doraźnie i świadomie, nie na stałe
Przy ptrace_scope=1 uruchomienie własnego dziecka pod debuggerem zwykle działa, choć attach do obcego procesu może zostać odrzucony. Nie jest to reguła „działa zawsze”: poziomy 2 i 3 oraz inne mechanizmy bezpieczeństwa mogą ograniczać także PTRACE_TRACEME.
Kiedy ptrace, a kiedy nie
ptrace to chirurgia na żywym organizmie: pełna kontrola, dotykanie stanu, zatrzymany pacjent. Ten poziom inwazyjności jest właściwy w debugowaniu incydentu — i niewłaściwy jako stały element systemu. Reguły, które się bronią: strace i gdb do weryfikacji konkretnej hipotezy o konkretnym procesie, z akceptacją kosztu; ciągła obserwacja syscalli na produkcji — eBPF, nie ptrace. Jeżeli twój „observability agent” trzyma procesy pod ptrace, to nie jest observability, tylko permanentny debugger na produkcji — ze wszystkimi stopami i narzutami, o których była mowa wyżej.
I sprawa metodologiczna: ptrace daje ci mikroskop, nie kompas. Zatrzymywanie procesu i oglądanie rejestrów ma sens dopiero wtedy, gdy wiesz, czego szukasz — narzędzie pomaga zbierać obserwacje i sprawdzać hipotezy, co rozkłada na czynniki debugowanie hipotezami zamiast zgadywania.
Gdy śledzenie zmienia zachowanie programu, wróć do tej pętli: zatrzymanie, inspekcja, ewentualna zmiana i wznowienie. Właśnie tam pojawia się duża część kosztów i efektów ubocznych debuggera.
Weryfikacja techniczna: ptrace(2).



Widać, że powstał przez AI od pierwszego zdania. Zaczyna się jak wysryw z linkedin w stylu
Zaczynasz od X i robi się Y. To nie Z tylko T. Blablabla.
To po co czytać u was, jak sam mogę sobie to wygenerować xD
Rozumiem niezadowolenie. Prawda, że każdy może wygenerować sobie taki artykuł w modelu AI, z którego sam korzysta. Jednak u mnie wygląda to zupełnie inaczej, niż może to wyglądać u niektórych użytkowników sztucznej inteligencji. Posiadam założony projekt z plikami pamięci na claude.ai od anthropic, korzystam z płatnej subskrypcji, aby mieć dostęp do najlepszych modeli oraz MCP, które zarządza całym blogiem DevMindset. W każdym razie, jeśli to niezadowala niektórych czytelników, zawsze mogą skorzystać z innych źródeł.