
In Unix-achtige besturingssystemen vindt de communicatie tussen programma's en de externe wereld en het besturingssysteem plaats via een kleine set functies - systeemaanroepen. Dit betekent dat het voor debugdoeleinden nuttig kan zijn om de systeemaanroepen die door lopende processen worden uitgevoerd, te observeren.
Het volgen van de 'intieme levens' van programma's op Linux wordt ondersteund door de utility strace, waar dit artikel aan is gewijd. Bij de voorbeelden van het gebruik van de 'spionage' apparatuur is een beknopte geschiedenis strace en de beschrijving van het apparaat van dergelijke programma's inbegrepen.
Inhoud
De oorsprong van soorten
De belangrijkste interface tussen programma's en de kernel van het OS in Unix zijn de systeemaanroepen (Eng. system calls, syscalls), de interactie van programma's met de externe wereld gebeurt uitsluitend via deze.
In de eerste openbare versie van Unix (, 1975) waren er echter geen handige manieren om het gedrag van gebruikersprocessen te volgen. Om dit probleem op te lossen, stelde Bell Labs in de volgende versie (, 1979) een nieuwe systeemaanroep voor - ptrace.
ptrace werd in de eerste plaats ontwikkeld voor interactieve debuggers, maar aan het einde van de jaren '80 (in het commerciële tijdperk van ) werden op deze basis de specialiseerde debuggers - tracer van systeemaanroepen - geïntroduceerd.
De versie van strace werd in 1992 door Paul Kronenburg gepubliceerd in de nieuwsbrief comp.sources.sun als alternatief voor de gesloten utility trace van Sun. Zowel de kloon als het origineel waren bedoeld voor SunOS, maar tegen 1994 strace was het geporteerd naar System V, Solaris en de opkomende populaire Linux.
Vandaag de dag ondersteunt strace alleen Linux en vertrouwt het nog steeds op dezelfde ptrace, die is verrijkt met tal van uitbreidingen.
De moderne (en zeer actieve) maintainer strace — . Dankzij hem heeft de utility geavanceerde mogelijkheden gekregen zoals injectiefouten in systeemaanroepen, ondersteuning voor een breed scala aan architecturen en, vooral, . Ongeofficiale bronnen beweren dat de keuze gevallen is op de struisvogel vanwege de gelijkenis tussen het Russische woord «страус» en het Engelse "strace".
Het is ook belangrijk op te merken dat de systeemaanroep ptrace en traceerprogramma's nooit in POSIX zijn opgenomen, ondanks hun lange geschiedenis en de implementatie in Linux, FreeBSD, OpenBSD en traditionele Unix.
Het apparaat strace in het kort: Piglet Trace
"You are not expected to understand this" (Dennis Ritchie, commentaar in de broncode van Version 6 Unix)
Van jongs af aan heb ik een hekel aan zwarte dozen: ik speelde niet met speelgoed, maar probeerde te begrijpen hoe ze werkten (volwassenen gebruikten het woord "kapot maken", maar geloof niet de slechte tongen). Misschien daarom voel ik me zo verbonden met de informele cultuur van de eerste Unix en de moderne open-sourcebeweging.
In het kader van dit artikel is het onredelijk om de broncode van strace, die in de loop der jaren is verlevendigd, te onderzoeken. Maar er mogen ook geen geheimen blijven voor de lezers. Daarom, om het principe van dergelijke strace-programma's te tonen, zal ik de code van een miniatuur traceerder geven — (ptr). Het doet niets bijzonders, maar het belangrijkste — het geeft de systeemaanroepen van het programma weer:
$ gcc examples/piglet-trace.c -o ptr
$ ptr echo test > /dev/null
BRK(12) -> 94744690540544
ACCESS(21) -> 18446744073709551614
ACCESS(21) -> 18446744073709551614
unknown(257) -> 3
FSTAT(5) -> 0
MMAP(9) -> 140694657216512
CLOSE(3) -> 0
ACCESS(21) -> 18446744073709551614
unknown(257) -> 3
READ(0) -> 832
FSTAT(5) -> 0
MMAP(9) -> 140694657208320
MMAP(9) -> 140694650953728
MPROTECT(10) -> 0
MMAP(9) -> 140694655045632
MMAP(9) -> 140694655070208
CLOSE(3) -> 0
unknown(158) -> 0
MPROTECT(10) -> 0
MPROTECT(10) -> 0
MPROTECT(10) -> 0
MUNMAP(11) -> 0
BRK(12) -> 94744690540544
BRK(12) -> 94744690675712
unknown(257) -> 3
FSTAT(5) -> 0
MMAP(9) -> 140694646390784
CLOSE(3) -> 0
FSTAT(5) -> 0
IOCTL(16) -> 18446744073709551591
WRITE(1) -> 5
CLOSE(3) -> 0
CLOSE(3) -> 0
unknown(231)
Tracee beëindigdPiglet Trace herkent ongeveer honderd systeemaanroepen van Linux (zie ) en werkt alleen op de x86-64 architectuur. Voor educatieve doeleinden is dit voldoende.
Laten we de werking van onze kloon bekijken. In het geval van Linux wordt voor debuggers en traceerprogramma's, zoals hierboven vermeld, de systeemaanroep ptrace gebruikt. Dit werkt door het doorgeven van commando-IDs als eerste argument, waarvan we alleen nodig hebben PTRACE_TRACEME, PTRACE_SYSCALL en PTRACE_GETREGS.
De werking van de traceerder begint in de gebruikelijke Unix-stijl: fork(2) start een kindproces, dat op zijn beurt met behulp van exec(3) de te onderzoeken programma start. De enige nuance hier is de aanroep ptrace(PTRACE_TRACEME) voordat exec: het kindproces verwacht dat het ouderproces het zal volgen:
pid_t child_pid = fork();
switch (child_pid) {
case -1:
err(EXIT_FAILURE, "fork");
case 0:
/* Kind hier * /
/* Een traced mode moet worden ingeschakeld. Een ouder moet wachten(2) totdat dit
* gebeurt. * /
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
/* Vervang zichzelf door een programma dat moet worden uitgevoerd. * /
execvp(argv[1], argv + 1);
err(EXIT_FAILURE, "exec");
}Het ouderproces moet nu aanroepen wait(2) in het kindproces, dat wil zeggen ervoor zorgen dat de overschakeling naar de traceermodus heeft plaatsgevonden:
/* Parent */
/* First we wait for the child to set the traced mode (see
* ptrace(PTRACE_TRACEME) above) */
if (waitpid(child_pid, NULL, 0) == -1)
err(EXIT_FAILURE, "traceme -> waitpid");Daardoor zijn de voorbereidingen afgerond en kan de daadwerkelijke tracing van systeemaanroepen in een oneindige lus beginnen.
Aanroep ptrace(PTRACE_SYSCALL) garandeert dat het volgende wachten ouderproces wordt beëindigd of vóór de uitvoering van de systeemaanroep of onmiddellijk erna. Tussen twee aanroepen kunnen verschillende acties worden uitgevoerd: de aanroep vervangen door een alternatieve, de argumenten of de teruggeefwaarde wijzigen.
Voor ons is het voldoende om het commando twee keer aan te roepen ptrace(PTRACE_GETREGS), om de status van de register rax te krijgen voor de aanroep (nummer van de systeemaanroep) en onmiddellijk erna (teruggeefwaarde).
Eigenlijk de lus:
/* A system call tracing loop, one interation per call. */
for (;;) {
/* A non-portable structure defined for ptrace/GDB/strace usage mostly.
* It allows to conveniently dump and access register state using
* ptrace. */
struct user_regs_struct registers;
/* Enter syscall: continue execution until the next system call
* beginning. Stop right before syscall.
*
* It's possible to change the system call number, system call
* arguments, return value or even avoid executing the system call
* completely. */
if (ptrace(PTRACE_SYSCALL, child_pid, NULL, NULL) == -1)
err(EXIT_FAILURE, "enter_syscall");
if (waitpid(child_pid, NULL, 0) == -1)
err(EXIT_FAILURE, "enter_syscall -> waitpid");
/* According to the x86-64 system call convention on Linux (see man 2
* syscall) the number identifying a syscall should be put into the rax
* general purpose register, with the rest of the arguments residing in
* other general purpose registers (rdi,rsi, rdx, r10, r8, r9). */
if (ptrace(PTRACE_GETREGS, child_pid, NULL, ®isters) == -1)
err(EXIT_FAILURE, "enter_syscall -> getregs");
/* Note how orig_rax is used here. That's because on x86-64 rax is used
* both for executing a syscall, and returning a value from it. To
* differentiate between the cases both rax and orig_rax are updated on
* syscall entry/exit, and only rax is updated on exit. */
print_syscall_enter(registers.orig_rax);
/* Exit syscall: execute of the syscall, and stop on system
* call exit.
*
* More system call tinkering possible: change the return value, record
* time it took to finish the system call, etc. */
if (ptrace(PTRACE_SYSCALL, child_pid, NULL, NULL) == -1)
err(EXIT_FAILURE, "exit_syscall");
if (waitpid(child_pid, NULL, 0) == -1)
err(EXIT_FAILURE, "exit_syscall -> waitpid");
/* Retrieve register state again as we want to inspect system call
* return value. */
if (ptrace(PTRACE_GETREGS, child_pid, NULL, ®isters) == -1) {
/* ESRCH is returned when a child terminates using a syscall and no
* return value is possible, e.g. as a result of exit(2). */
if (errno == ESRCH) {
fprintf(stderr, "nTracee terminatedn");
break;
}
err(EXIT_FAILURE, "exit_syscall -> getregs");
}
/* Done with this system call, let the next iteration handle the next
* one */
print_syscall_exit(registers.rax);
}Dat is de hele tracer. Nu weet je waar te beginnen met de volgende portering naar Linux.
Basisprincipes: het starten van een programma onder strace
Als eerste voorbeeld van gebruik strace, laten we misschien de eenvoudigste manier noemen - het uitvoeren van een applicatie onder beheer van strace.
Om niet door de eindeloze lijst met aanroepen van een typische programma te hoeven zoeken, zullen we schrijven rondom write:
int main(int argc, char *argv[])
{
char str[] = "schrijf me naar stdoutn";
/* write(2) is een eenvoudige wrapper rond een syscall zodat het eenvoudig moet zijn om
* te vinden in de syscall trace. * /
if (sizeof(str) != write(STDOUT_FILENO, str, sizeof(str))){
perror("write");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
Laten we het programma compileren en controleren of het werkt:
$ gcc examples/write-simple.c -o write-simple
$ . /write-simple
schrijf me naar stdoutEn laten we het tenslotte uitvoeren met strace:
$ strace . /write-simple
pexecve("./write", ["./write"], 0x7ffebd6145b0 /* 71 vars */) = 0
brk(NULL) = 0x55ff5489e000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (Bestand of map bestaat niet)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (Bestand of map bestaat niet)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=197410, ...}) = 0
mmap(NULL, 197410, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f7a2a633000
close(3) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (Bestand of map bestaat niet)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF2113 3 >