
I Unix-lignende operativsystemer foregår kommunikationen mellem programmer og omverdenen samt operativsystemet gennem en lille samling funktioner — systemopkald. Det betyder, at det kan være nyttigt i fejlsøgningsformål at kunne snige sig ind for at se, hvilke systemopkald der udføres af processerne.
At følge programmets 'intime liv' på Linux hjælper et værktøj strace, som denne artikel er dedikeret til. Til eksempler på brugen af 'spionage' udstyr vedlægges en kort historie strace og beskrivelse af strukturen af sådanne programmer.
Indhold
Arternes oprindelse
Hovedinterfacet mellem programmer og kernen i OS i Unix er systemopkaldene (eng. system calls, syscalls), samspillet mellem programmer og omverdenen sker udelukkende igennem dem.
Men i den første offentlige version af Unix (, 1975) var der ikke nogen behagelige måder at spore brugerprocessernes adfærd. For at løse dette problem foreslog Bell Labs i den næste version (, 1979) et nyt systemopkald — ptrace.
ptrace blev først og fremmest udviklet til interaktive debugger, men ved slutningen af 80'erne (i den kommercielle æra af ) opstod der på denne baggrund og fik en bred udbredelse specialiserede debuggere — systemopkalds trace-r.
version af strace blev offentliggjort af Paul Kronenburg i mailinglisten comp.sources.sun i 1992 som et alternativ til det lukkede værktøj trace fra Sun. Både klonen og originalen var beregnet til SunOS, men i 1994 var strace den blevet portet til System V, Solaris og det hastigt voksende Linux.
I dag understøtter strace kun Linux og bygger videre på den samme ptrace, som er blevet udvidet med mange funktioner.
Den moderne (og meget aktive) vedligeholder strace — . Takket være ham har værktøjet fået avancerede funktioner som fejlindsprøjtning i systemopkald, støtte til et bredt spektrum af arkitekturer og, vigtigst af alt, . Uofficielle kilder hævder, at valget faldt på strudsen på grund af ligheden mellem det russiske ord «страус» og det engelske "strace".
Det er også bemærkelsesværdigt, at systemopkaldet ptrace og debuggerne aldrig er blevet inkluderet i POSIX, på trods af den lange historie og eksistensen af implementeringer i Linux, FreeBSD, OpenBSD og klassisk Unix.
Strukturen af strace i to ord: Piglet Trace
"You are not expected to understand this" (Dennis Ritchie, kommentar i kildekoden til Version 6 Unix)
Fra en tidlig alder har jeg aldrig kunnet udstå sorte bokse: jeg legede ikke med legetøj, men forsøgte at forstå deres indretning (voksne brugte ordet «brækkede», men tro ikke på de onde tunger). Måske er det derfor, jeg føler mig så tæt på den uformelle kultur i de tidlige Unix-oplevelser og den moderne open-source-bevægelse.
I denne artikel er det urimeligt at gennemgå kildekoden til det veludviklede strace over årtier. Men der skal heller ikke være nogen hemmeligheder for læserne. Derfor vil jeg for at vise, hvordan sådanne strace-programmer fungerer, give koden til en miniature-debugger — (ptr). Det kan ikke gøre noget særligt, men det vigtigste — systemopkaldene i programmet — viser det:
$ 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 terminatedPiglet Trace genkender omkring hundrede systemopkald i Linux (se. ) og fungerer kun på x86-64-arkitekturen. Til undervisningsformål er dette tilstrækkeligt.
Lad os gennemgå funktionen af vores klon. I tilfælde af Linux benyttes som nævnt tidligere systemopkaldet ptrace til debuggere og debuggerprogrammer. Det fungerer ved at sende kommandoidentifikatorer i det første argument, hvoraf vi kun har brug for PTRACE_TRACEME, PTRACE_SYSCALL og PTRACE_GETREGS.
Funktionen af debuggere begynder i klassisk Unix-stil: fork(2) starter en datterproces, og den anvender derefter exec(3) til at starte det undersøgte program. Den eneste finesse her er kaldet ptrace(PTRACE_TRACEME) før exec: datterprocessen venter på, at forældreprocessen vil overvåge den:
pid_t child_pid = fork();
switch (child_pid) {
case -1:
err(EXIT_FAILURE, "fork");
case 0:
Barn her
Et tracere modet, skal være aktiveret. En forælder skal vente(2) på, at det
for at ske.
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
Erstat sig selv med et program, der skal køres.
execvp(argv[1], argv + 1);
err(EXIT_FAILURE, "exec");
}Forældreprocessen skal nu kalde wait(2) i børneprocessen, hvilket betyder at sikre, at overgangen til sporingsmode er sket:
/* 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");Nu er forberedelserne færdige, og vi kan begynde at spore systemopkald i en uendelig løkke.
Kald ptrace(PTRACE_SYSCALL) garanterer, at den efterfølgende vent af forældreprocessen enten afsluttes før eksekvering af systemopkaldet, eller straks efter det er afsluttet. Mellem de to opkald kan der foretages handlinger: erstatte opkaldet med et alternativt, ændre argumenter eller returværdi.
Vi behøver blot at kalde kommandoen ptrace(PTRACE_GETREGS), for to gange at få registreringens tilstand rax før opkaldet (nummeret på systemopkaldet) og straks efter (returværdien).
Faktisk er løkken:
/* 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);
}Det er hele sporeren. Nu ved du, hvordan du starter en ny portering til Linux.
Grundlæggende: start programmet under strace
Som det første eksempel på brug strace, bør den enkleste metode muligvis være at køre et program under strace.
For ikke at dykke ind i en endeløs liste over opkald fra et typisk program, lad os skrive omkring write:
int main(int argc, char *argv[])
{
char str[] = "skriv mig til stdoutn";
write(2) er et simpelt wrapper omkring et syscall, så det burde være let at
finder i syscall-sporet.
if (sizeof(str) != write(STDOUT_FILENO, str, sizeof(str))){
perror("write");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
Lad os bygge programmet og sikre os, at det virker:
$ gcc examples/write-simple.c -o write-simple
$ .\/write-simple
skriv mig til stdoutOg lad os endelig køre det under strace:
$ strace .\/write-simple
pexecve(".\/write", [".\/write"], 0x7ffebd6145b0 71 vars) = 0
brk(NULL) = 0x55ff5489e000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (Ingen sådan fil eller katalog)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (Ingen sådan fil eller katalog)
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 (Ingen sådan fil eller katalog)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF2113 3 >