Nos sistemas operativos tipo Unix, a comunicación dun programa co mundo exterior e co sistema operativo prodúcese a través dun pequeno conxunto de funcións: chamadas ao sistema. Isto significa que para fins de depuración pode ser útil espiar as chamadas do sistema que se executan polos procesos.
Unha utilidade axúdache a supervisar a "vida íntima" dos programas en Linux strace, que é o tema deste artigo. Os exemplos do uso de equipos de espionaxe van acompañados dunha breve historia strace e unha descrición do deseño destes programas.
A interface principal entre os programas e o núcleo do sistema operativo en Unix son as chamadas ao sistema. chamadas ao sistema, syscalls), a interacción dos programas co mundo exterior prodúcese exclusivamente a través deles.
Pero na primeira versión pública de Unix (Versión 6 Unix, 1975) non había formas convenientes de rastrexar o comportamento dos procesos dos usuarios. Para resolver este problema, Bell Labs actualizará á seguinte versión (Versión 7 Unix, 1979) propuxo unha nova convocatoria do sistema - ptrace.
ptrace desenvolveuse principalmente para depuradores interactivos, pero a finais dos 80 (na era do System V versión 4) sobre esta base, apareceron depuradores limitadamente focalizados (trazadores de chamadas de sistema) e foron moi utilizados.
Primeira a mesma versión de strace foi publicada por Paul Cronenburg na lista de correo comp.sources.sun en 1992 como alternativa a unha utilidade pechada trace do Sol. Tanto o clon como o orixinal estaban destinados a SunOS, pero en 1994 strace foi portado a System V, Solaris e o cada vez máis popular Linux.
Hoxe, strace só admite Linux e confía no mesmo ptrace, cuberto de moitas extensións.
Mantedor moderno (e moi activo). strace - Dmitry Levin. Grazas a el, a utilidade adquiriu funcións avanzadas como a inxección de erros nas chamadas do sistema, soporte para unha ampla gama de arquitecturas e, o máis importante, mascota. Fontes non oficiais afirman que a elección recaeu no avestruz debido á consonancia entre a palabra rusa "avestruz" e a palabra inglesa "strace".
Tamén é importante que a chamada do sistema ptrace e os trazadores nunca se incluíron en POSIX, a pesar dunha longa historia e implementación en Linux, FreeBSD, OpenBSD e Unix tradicional.
Dispositivo Strace en poucas palabras: Piglet Trace
"Non se espera que entendas isto" (Dennis Ritchie, comenta no código fonte de Unix da versión 6)
Desde a primeira infancia, non soporto as caixas negras: non xoguei con xoguetes, senón que tratei de comprender a súa estrutura (os adultos usaban a palabra "rompeu", pero non cren nas malas linguas). Quizais sexa por iso que a cultura informal do primeiro Unix e do movemento moderno de código aberto está tan preto de min.
Para os efectos deste artigo, non é razoable desmontar o código fonte de strace, que creceu ao longo de décadas. Pero non debería quedar ningún segredo para os lectores. Polo tanto, para mostrar o principio de funcionamento destes programas de traza, proporcionarei o código para un rastreador en miniatura: Rastro de porquiño (ptr). Non sabe como facer nada especial, pero o principal son as chamadas ao sistema do programa: dá como resultado:
Piglet Trace recoñece preto de centos de chamadas ao sistema Linux (ver. mesa) e só funciona na arquitectura x86-64. Isto é suficiente para fins educativos.
Vexamos o traballo do noso clon. No caso de Linux, os depuradores e os rastreadores usan, como se mencionou anteriormente, a chamada ao sistema ptrace. Funciona pasando no primeiro argumento os identificadores de comandos, dos que só necesitamos PTRACE_TRACEME, PTRACE_SYSCALL и PTRACE_GETREGS.
O rastreador comeza no estilo habitual de Unix: fork(2) inicia un proceso fillo, que á súa vez usa exec(3) pon en marcha o programa en estudo. A única sutileza aquí é o desafío ptrace(PTRACE_TRACEME) antes exec: O proceso fillo espera que o proceso principal o supervise:
pid_t child_pid = fork();
switch (child_pid) {
case -1:
err(EXIT_FAILURE, "fork");
case 0:
/* Child here */
/* A traced mode has to be enabled. A parent will have to wait(2) for it
* to happen. */
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
/* Replace itself with a program to be run. */
execvp(argv[1], argv + 1);
err(EXIT_FAILURE, "exec");
}
O proceso parental debería chamar agora wait(2) no proceso fillo, é dicir, asegúrese de que se produciu o cambio ao modo de rastrexo:
/* 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");
Neste punto, os preparativos están completos e pode proceder directamente ao seguimento das chamadas do sistema nun ciclo interminable.
Chamar ptrace(PTRACE_SYSCALL) garante que posteriormente wait o pai completará antes de que se execute a chamada ao sistema ou inmediatamente despois de que se complete. Entre dúas chamadas pode realizar calquera acción: substituír a chamada por outra alternativa, cambiar os argumentos ou o valor de retorno.
Só necesitamos chamar ao comando dúas veces ptrace(PTRACE_GETREGS)para obter o estado de rexistro rax antes da chamada (número de chamada do sistema) e inmediatamente despois (valor de retorno).
En realidade, o ciclo:
/* 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);
}
Ese é todo o trazador. Agora xa sabes por onde comezar a próxima portada DTrace en Linux.
Conceptos básicos: executar un programa executando strace
Como primeiro caso de uso strace, quizais paga a pena citar o xeito máis sinxelo: lanzar unha aplicación en execución strace.
Para non afondar na lista interminable de chamadas dun programa típico, escribimos programa mínimo ao redor write:
int main(int argc, char *argv[])
{
char str[] = "write me to stdoutn";
/* write(2) is a simple wrapper around a syscall so it should be easy to
* find in the syscall trace. */
if (sizeof(str) != write(STDOUT_FILENO, str, sizeof(str))){
perror("write");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
Imos construír o programa e asegúrese de que funciona:
$ gcc examples/write-simple.c -o write-simple
$ ./write-simple
write me to stdout
E, finalmente, executémolo baixo control de strace:
Moi "propaganda" e pouco educativo. Aquí hai dous problemas: a saída do programa mestúrase coa saída strace e unha abundancia de chamadas de sistema que non nos interesan.
Pode separar o fluxo de saída estándar do programa e a saída do erro de traza mediante o interruptor -o, que redirixe a lista de chamadas ao sistema a un ficheiro de argumentos.
Queda por tratar o problema das chamadas "extra". Supoñamos que só nos interesan as chamadas write. Chave -e permítelle especificar expresións polas que se filtrarán as chamadas do sistema. A opción de condición máis popular é, naturalmente, trace=*, co que podes deixar só as chamadas que nos interesen.
Cando se usa simultaneamente -o и -e teremos:
$ strace -e trace=write -owrite-simple.log ./write-simple
write me to stdout
$ cat write-simple.log
write(1, "write me to stdoutn", 20
) = 20
+++ exited with 0 +++
Entón, xa ves, é moito máis fácil de ler.
Tamén pode eliminar as chamadas do sistema, por exemplo as relacionadas coa asignación e liberación de memoria:
$ strace -e trace=!brk,mmap,mprotect,munmap -owrite-simple.log ./write-simple
write me to stdout
$ cat write-simple.log
execve("./write-simple", ["./write-simple"], 0x7ffe9972a498 /* 69 vars */) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=124066, ...}) = 0
close(3) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF21133>1260342"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2030544, ...}) = 0
close(3) = 0
arch_prctl(ARCH_SET_FS, 0x7f00f0be74c0) = 0
write(1, "write me to stdoutn", 20) = 20
exit_group(0) = ?
+++ exited with 0 +++
Teña en conta o signo de admiración escapado na lista de chamadas excluídas: isto é necesario polo shell de comandos. cuncha).
Na miña versión de glibc, unha chamada ao sistema finaliza o proceso exit_group, non tradicional _exit. Esta é a dificultade de traballar coas chamadas do sistema: a interface coa que traballa o programador non está directamente relacionada coas chamadas do sistema. Ademais, cambia regularmente dependendo da implementación e da plataforma.
Fundamentos: incorporarse ao proceso sobre a marcha
Inicialmente, a chamada do sistema ptrace na que se construíu strace, só se pode usar cando se executa o programa nun modo especial. Esta limitación puido parecer razoable nos días da versión 6 de Unix. Hoxe en día, isto xa non é suficiente: ás veces cómpre investigar os problemas dun programa de traballo. Un exemplo típico é un proceso bloqueado nun asa ou durmido. Polo tanto moderno strace pode unirse a procesos sobre a marcha.
$ ./write-sleep &
[1] 15329
write me
$ strace -p 15329
strace: Process 15329 attached
pause(
^Cstrace: Process 15329 detached
<detached ...>
Programa bloqueado por chamada pause. Vexamos como reacciona ela ante os sinais:
$ strace -o write-sleep.log -p 15329 &
strace: Process 15329 attached
$
$ kill -CONT 15329
$ cat write-sleep.log
pause() = ? ERESTARTNOHAND (To be restarted if no handler)
--- SIGCONT {si_signo=SIGCONT, si_code=SI_USER, si_pid=14989, si_uid=1001} ---
pause(
$
$ kill -TERM 15329
$ cat write-sleep.log
pause() = ? ERESTARTNOHAND (To be restarted if no handler)
--- SIGCONT {si_signo=SIGCONT, si_code=SI_USER, si_pid=14989, si_uid=1001} ---
pause() = ? ERESTARTNOHAND (To be restarted if no handler)
--- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=14989, si_uid=1001} ---
+++ killed by SIGTERM +++
Lanzamos o programa conxelado e unímonos a el usando strace. Dúas cousas quedaron claras: a chamada do sistema de pausa ignora os sinais sen controladores e, o que é máis interesante, o trace monitor non só as chamadas do sistema, senón tamén os sinais entrantes.
Exemplo: Seguimento de procesos secundarios
Traballar con procesos a través dunha chamada fork - a base de todos os Unix. Vexamos como funciona strace cunha árbore de procesos usando o exemplo dunha simple "creación" programas:
int main(int argc, char *argv[])
{
pid_t parent_pid = getpid();
pid_t child_pid = fork();
if (child_pid == 0) {
/* A child is born! */
child_pid = getpid();
/* In the end of the day printf is just a call to write(2). */
printf("child (self=%d)n", child_pid);
exit(EXIT_SUCCESS);
}
printf("parent (self=%d, child=%d)n", parent_pid, child_pid);
wait(NULL);
exit(EXIT_SUCCESS);
}
Aquí o proceso orixinal crea un proceso fillo, ambos escribindo na saída estándar:
A bandeira axúdache a seguir toda a árbore do proceso -f, que strace supervisa as chamadas do sistema en procesos fillos. Isto engádese a cada liña de saída pid proceso que fai unha saída do sistema:
Por certo, que chamada de sistema se usa para crear un novo proceso?
Exemplo: rutas de ficheiros en lugar de identificadores
Coñecer os descritores de ficheiros é certamente útil, pero os nomes dos ficheiros específicos aos que accede un programa tamén poden ser útiles.
o seguinte programa escribe a liña no ficheiro temporal:
void do_write(int out_fd)
{
char str[] = "write me to a filen";
if (sizeof(str) != write(out_fd, str, sizeof(str))){
perror("write");
exit(EXIT_FAILURE);
}
}
int main(int argc, char *argv[])
{
char tmp_filename_template[] = "/tmp/output_fileXXXXXX";
int out_fd = mkstemp(tmp_filename_template);
if (out_fd == -1) {
perror("mkstemp");
exit(EXIT_FAILURE);
}
do_write(out_fd);
return EXIT_SUCCESS;
}
Durante unha chamada normal strace mostrará o valor do número de descritor pasado á chamada do sistema:
$ strace -e trace=write -o write-tmp-file.log ./write-tmp-file
$ cat write-tmp-file.log
write(3, "write me to a filen", 20) = 20
+++ exited with 0 +++
Cunha bandeira -y A utilidade mostra o camiño ao ficheiro ao que corresponde o descritor:
$ strace -y -e trace=write -o write-tmp-file.log ./write-tmp-file
$ cat write-tmp-file.log
write(3</tmp/output_fileCf5MyW>, "write me to a filen", 20) = 20
+++ exited with 0 +++
Exemplo: Seguimento de acceso a ficheiros
Outra característica útil: mostrar só as chamadas do sistema asociadas a un ficheiro específico. A continuación programa engade unha liña a un ficheiro arbitrario pasado como argumento:
void do_write(int out_fd)
{
char str[] = "write me to a filen";
if (sizeof(str) != write(out_fd, str, sizeof(str))){
perror("write");
exit(EXIT_FAILURE);
}
}
int main(int argc, char *argv[])
{
/*
* Path will be provided by the first program argument.
* */
const char *path = argv[1];
/*
* Open an existing file for writing in append mode.
* */
int out_fd = open(path, O_APPEND | O_WRONLY);
if (out_fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
do_write(out_fd);
return EXIT_SUCCESS;
}
Por defecto strace mostra moita información innecesaria. Bandeira -P cun argumento fai que strace imprima só as chamadas ao ficheiro especificado:
$ strace -y -P/tmp/test_file.log -o write-file.log ./write-file /tmp/test_file.log
$ cat write-file.log
openat(AT_FDCWD, "/tmp/test_file.log", O_WRONLY|O_APPEND) = 3</tmp/test_file.log>
write(3</tmp/test_file.log>, "write me to a filen", 20) = 20
+++ exited with 0 +++
Exemplo: Programas multiproceso
Utilidade strace tamén pode axudar cando se traballa con fíos múltiples o programa. O seguinte programa escribe na saída estándar de dous fluxos:
void *thread(void *arg)
{
(void) arg;
printf("Secondary thread: workingn");
sleep(1);
printf("Secondary thread: donen");
return NULL;
}
int main(int argc, char *argv[])
{
printf("Initial thread: launching a threadn");
pthread_t thr;
if (0 != pthread_create(&thr, NULL, thread, NULL)) {
fprintf(stderr, "Initial thread: failed to create a thread");
exit(EXIT_FAILURE);
}
printf("Initial thread: joining a threadn");
if (0 != pthread_join(thr, NULL)) {
fprintf(stderr, "Initial thread: failed to join a thread");
exit(EXIT_FAILURE);
};
printf("Initial thread: done");
exit(EXIT_SUCCESS);
}
Por suposto, debe compilarse cun saúdo especial para o enlazador: a bandeira -pthread:
$ gcc examples/thread-write.c -pthread -o thread-write
$ ./thread-write
/thread-write
Initial thread: launching a thread
Initial thread: joining a thread
Secondary thread: working
Secondary thread: done
Initial thread: done
$
Bandeira -f, como no caso dos procesos habituais, engadirá o pid do proceso ao comezo de cada liña.
Por suposto, non estamos a falar dun identificador de fío no sentido da implementación do estándar POSIX Threads, senón do número que usa o planificador de tarefas en Linux. Desde o punto de vista deste último, non hai procesos nin fíos; hai tarefas que deben ser distribuídas entre os núcleos dispoñibles da máquina.
Cando se traballa en varios fíos, as chamadas ao sistema vólvense demasiadas:
Por certo, preguntas. Que chamada do sistema se usa para crear un novo fío? En que se diferencia esta convocatoria de fíos da convocatoria de procesos?
Clase maxistral: pila de procesos no momento dunha chamada ao sistema
Unha das que apareceu recentemente strace capacidades: mostra a pila de chamadas de funcións no momento da chamada do sistema. Simple exemplo:
void do_write(void)
{
char str[] = "write me to stdoutn";
if (sizeof(str) != write(STDOUT_FILENO, str, sizeof(str))){
perror("write");
exit(EXIT_FAILURE);
}
}
int main(int argc, char *argv[])
{
do_write();
return EXIT_SUCCESS;
}
Por suposto, a saída do programa faise moi voluminosa e, ademais da bandeira -k (visualización da pila de chamadas), ten sentido filtrar as chamadas do sistema polo nome:
$ gcc examples/write-simple.c -o write-simple
$ strace -k -e trace=write -o write-simple.log ./write-simple
write me to stdout
$ cat write-simple.log
write(1, "write me to stdoutn", 20) = 20
> /lib/x86_64-linux-gnu/libc-2.27.so(__write+0x14) [0x110154]
> /home/vkazanov/projects-my/strace-post/write-simple(do_write+0x50) [0x78a]
> /home/vkazanov/projects-my/strace-post/write-simple(main+0x14) [0x7d1]
> /lib/x86_64-linux-gnu/libc-2.27.so(__libc_start_main+0xe7) [0x21b97]
> /home/vkazanov/projects-my/strace-post/write-simple(_start+0x2a) [0x65a]
+++ exited with 0 +++
Clase maxistral: inxección de erros
E unha característica máis nova e moi útil: a inxección de erros. Aquí programa, escribindo dúas liñas no fluxo de saída:
É interesante que erros se devolven todo retos write, incluída a chamada oculta detrás do perror. Só ten sentido devolver un erro para a primeira das chamadas:
$ strace -e trace=write -e inject=write:error=EBADF:when=1 -owrite-twice.log ./write-twice
write: Bad file descriptor
$ cat write-twice.log
write(1, "write me 1n", 12) = -1 EBADF (Bad file descriptor) (INJECTED)
write(3, "write: Bad file descriptorn", 27) = 27
+++ exited with 1 +++
Ou a segunda:
$ strace -e trace=write -e inject=write:error=EBADF:when=2 -owrite-twice.log ./write-twice
write me 1
write: Bad file descriptor
$ cat write-twice.log
write(1, "write me 1n", 12) = 12
write(1, "write me 2n", 12) = -1 EBADF (Bad file descriptor) (INJECTED)
write(3, "write: Bad file descriptorn", 27) = 27
+++ exited with 1 +++
Non é necesario especificar o tipo de erro:
$ strace -e trace=write -e fault=write:when=1 -owrite-twice.log ./write-twice
$ cat write-twice.log
write(1, "write me 1n", 12) = -1 ENOSYS (Function not implemented) (INJECTED)
write(3, "write: Function not implementedn", 32) = 32
+++ exited with 1 +++
En combinación con outras marcas, pode "interromper" o acceso a un ficheiro específico. Exemplo:
$ strace -y -P/tmp/test_file.log -e inject=file:error=ENOENT -o write-file.log ./write-file /tmp/test_file.log
open: No such file or directory
$ cat write-file.log
openat(AT_FDCWD, "/tmp/test_file.log", O_WRONLY|O_APPEND) = -1 ENOENT (No such file or directory) (INJECTED)
+++ exited with 1 +++
Ademais da inxección de erros, unha lata introducir atrasos ao facer chamadas ou recibir sinais.
Posterior
Utilidade strace - unha ferramenta sinxela e fiable. Pero ademais das chamadas ao sistema, pódense depurar outros aspectos do funcionamento dos programas e do sistema operativo. Por exemplo, pode rastrexar as chamadas a bibliotecas vinculadas dinámicamente. ltrace, poden analizar o funcionamento do sistema operativo SystemTap и ftrace, e permítelle investigar en profundidade o rendemento do programa perfecto. Con todo, é strace - a primeira liña de defensa en caso de problemas cos programas propios e alleos, e úsoa polo menos un par de veces á semana.
En resumo, se che gusta Unix, le man 1 strace e non dubides en mirar os teus programas!