En los sistemas operativos tipo Unix, la comunicación de un programa con el mundo exterior y el sistema operativo se produce a través de un pequeño conjunto de funciones: llamadas al sistema. Esto significa que, con fines de depuración, puede resultar útil espiar las llamadas al sistema que ejecutan los procesos.
Una utilidad te ayuda a monitorear la “vida íntima” de los programas en Linux strace, que es el tema de este artículo. Ejemplos del uso de equipos de espionaje van acompañados de una breve historia. strace y una descripción del diseño de dichos programas.
La interfaz principal entre los programas y el kernel del sistema operativo en Unix son las llamadas al sistema. llamadas al sistema, llamadas al sistema), la interacción de los programas con el mundo exterior se produce exclusivamente a través de ellos.
Pero en la primera versión pública de Unix (Versión 6 Unix, 1975) no había formas convenientes de rastrear el comportamiento de los procesos de los usuarios. Para resolver este problema, Bell Labs actualizará a la siguiente versión (Versión 7 Unix, 1979) propuso una nueva llamada al sistema: ptrace.
ptrace se desarrolló principalmente para depuradores interactivos, pero a finales de los años 80 (en la era del comercio Versión 4 del sistema V) sobre esta base, aparecieron y se utilizaron ampliamente depuradores con un enfoque limitado (rastreadores de llamadas al sistema).
primero Paul Cronenburg publicó la misma versión de strace en la lista de correo comp.sources.sun en 1992 como alternativa a una utilidad cerrada. trace del sol. Tanto el clon como el original estaban destinados a SunOS, pero en 1994 strace fue portado a System V, Solaris y el cada vez más popular Linux.
Hoy en día strace solo soporta Linux y depende del mismo ptrace, cubierto de muchas extensiones.
Mantenedor moderno (y muy activo) strace - Дмитрий левин. Gracias a él, la utilidad adquirió funciones avanzadas como la inyección de errores en las llamadas al sistema, soporte para una amplia gama de arquitecturas y, lo más importante, mascota. Fuentes no oficiales afirman que la elección recayó en el avestruz debido a la consonancia entre la palabra rusa "avestruz" y la palabra inglesa "strace".
También es importante que la llamada al sistema ptrace y los rastreadores nunca se incluyeron en POSIX, a pesar de una larga historia e implementación en Linux, FreeBSD, OpenBSD y Unix tradicional.
Dispositivo Strace en pocas palabras: Piglet Trace
"No se espera que entiendas esto" (Dennis Ritchie, comentario en la versión 6 del código fuente de Unix)
Desde pequeño no soporto las cajas negras: no jugaba con juguetes, pero intentaba entender su estructura (los adultos usaban la palabra "roto", pero no creen en las malas lenguas). Quizás es por eso que la cultura informal del primer Unix y el movimiento moderno de código abierto me resultan tan cercanas.
A los efectos de este artículo, no es razonable desmontar el código fuente de strace, que ha crecido a lo largo de décadas. Pero no debería haber secretos para los lectores. Por lo tanto, para mostrar el principio de funcionamiento de dichos programas strace, proporcionaré el código para un rastreador en miniatura: Rastro de lechón (ptr). No sabe cómo hacer nada especial, pero lo principal son las llamadas al sistema del programa: genera:
Piglet Trace reconoce alrededor de cientos de llamadas al sistema Linux (consulte. la mesa) y solo funciona en arquitectura x86-64. Esto es suficiente para fines educativos.
Veamos el trabajo de nuestro clon. En el caso de Linux, los depuradores y rastreadores utilizan, como se mencionó anteriormente, la llamada al sistema ptrace. Funciona pasando en el primer argumento los identificadores de comando, de los cuales solo necesitamos PTRACE_TRACEME, PTRACE_SYSCALL и PTRACE_GETREGS.
El rastreador comienza en el estilo habitual de Unix: fork(2) lanza un proceso hijo, que a su vez utiliza exec(3) lanza el programa en estudio. La única sutileza aquí es el desafío. ptrace(PTRACE_TRACEME) antes exec: El proceso hijo espera que el proceso padre lo 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");
}
El proceso padre ahora debería llamar wait(2) en el proceso hijo, es decir, asegúrese de que se haya producido el cambio al modo de seguimiento:
/* 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");
En este punto, los preparativos están completos y puede proceder directamente al seguimiento de las llamadas al sistema en un bucle sin fin.
Llamar ptrace(PTRACE_SYSCALL) garantiza que las posteriores wait parent se completará antes de que se ejecute la llamada al sistema o inmediatamente después de que se complete. Entre dos llamadas puedes realizar cualquier acción: reemplazar la llamada por una alternativa, cambiar los argumentos o el valor de retorno.
Solo necesitamos llamar el comando dos veces. ptrace(PTRACE_GETREGS)para obtener el estado de registro rax antes de la llamada (número de llamada del sistema) e inmediatamente después (valor de retorno).
En realidad, el 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 es todo el rastreador. Ahora ya sabes por dónde empezar la próxima transferencia. DTrace en Linux.
Conceptos básicos: ejecutar un programa en ejecución strace
Como primer caso de uso strace, quizás valga la pena citar el método más simple: iniciar una aplicación en ejecución strace.
Para no ahondar en la interminable lista de llamadas de un programa típico, escribimos programa minimo vokrug 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;
}
Construyamos el programa y asegurémonos de que funcione:
$ gcc examples/write-simple.c -o write-simple
$ ./write-simple
write me to stdout
Y finalmente, ejecutémoslo bajo control de strace:
Muy “prolijo” y poco educativo. Hay dos problemas aquí: la salida del programa se mezcla con la salida strace y una gran cantidad de llamadas al sistema que no nos interesan.
Puede separar el flujo de salida estándar del programa y la salida de error de seguimiento utilizando el modificador -o, que redirige la lista de llamadas al sistema a un archivo de argumentos.
Queda por abordar el problema de las llamadas “extra”. Supongamos que solo nos interesan las llamadas. write... Llave -e le permite especificar expresiones por las cuales se filtrarán las llamadas al sistema. La opción de condición más popular es, naturalmente, trace=*, con el que podrás dejar sólo las llamadas que nos interesen.
Cuando se usa simultáneamente -o и -e obtendremos:
$ 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 +++
Entonces, verás, es mucho más fácil de leer.
También puede eliminar llamadas al sistema, por ejemplo aquellas relacionadas con la asignación y 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 +++
Tenga en cuenta el signo de exclamación escapado en la lista de llamadas excluidas: esto lo requiere el shell de comandos. shell).
En mi versión de glibc, una llamada al sistema finaliza el proceso exit_group, no tradicional _exit. Ésta es la dificultad de trabajar con llamadas al sistema: la interfaz con la que trabaja el programador no está directamente relacionada con las llamadas al sistema. Además, cambia periódicamente según la implementación y la plataforma.
Conceptos básicos: unirse al proceso sobre la marcha
Inicialmente, la llamada al sistema ptrace en el que se construyó strace, solo se puede utilizar cuando se ejecuta el programa en un modo especial. Esta limitación puede haber parecido razonable en los días de la Versión 6 de Unix. Hoy en día esto ya no es suficiente: a veces es necesario investigar los problemas de un programa en funcionamiento. Un ejemplo típico es un proceso bloqueado en un mango o en suspensión. Por lo tanto moderno strace puede unirse a procesos sobre la marcha.
$ ./write-sleep &
[1] 15329
write me
$ strace -p 15329
strace: Process 15329 attached
pause(
^Cstrace: Process 15329 detached
<detached ...>
Programa bloqueado por llamada pause. Veamos cómo reacciona a las señales:
$ 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 el programa congelado y nos unimos a él usando strace. Dos cosas quedaron claras: la llamada al sistema de pausa ignora las señales sin controladores y, lo que es más interesante, strace monitorea no solo las llamadas al sistema, sino también las señales entrantes.
Ejemplo: seguimiento de procesos secundarios
Trabajar con procesos a través de una llamada. fork - la base de todos los Unixes. Veamos cómo funciona strace con un árbol de procesos usando el ejemplo de un simple “breeding” programa:
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í el proceso original crea un proceso hijo, y ambos escriben en la salida estándar:
La bandera le ayuda a realizar un seguimiento de todo el árbol de procesos. -fcon la cual strace monitorea las llamadas al sistema en procesos secundarios. Esto se suma a cada línea de salida. pid proceso que genera una salida del sistema:
Por cierto, ¿qué llamada al sistema se utiliza para crear un nuevo proceso?
Ejemplo: rutas de archivos en lugar de identificadores
Conocer los descriptores de archivos es ciertamente útil, pero los nombres de los archivos específicos a los que accede un programa también pueden resultar útiles.
el próximo programa escribe la línea en el archivo 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 una llamada normal strace mostrará el valor del número de descriptor pasado a la llamada al 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 +++
con una bandera -y La utilidad muestra la ruta al archivo al que corresponde el descriptor:
$ 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 +++
Ejemplo: seguimiento de acceso a archivos
Otra característica útil: muestra solo las llamadas al sistema asociadas con un archivo específico. Próximo programa agrega una línea a un archivo 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 muestra mucha información innecesaria. Bandera -P con un argumento hace que strace imprima solo llamadas al archivo 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 +++
Ejemplo: programas multiproceso
Utilidad strace También puede ayudar cuando se trabaja con subprocesos múltiples. el programa. El siguiente programa escribe en la salida estándar de dos flujos:
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);
}
Naturalmente, debe compilarse con un saludo especial al vinculador: la bandera -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
$
Bandera -f, como en el caso de los procesos regulares, agregará el pid del proceso al comienzo de cada línea.
Naturalmente, no estamos hablando de un identificador de hilo en el sentido de la implementación del estándar POSIX Threads, sino del número utilizado por el programador de tareas en Linux. Desde el punto de vista de este último, no hay procesos ni subprocesos: hay tareas que deben distribuirse entre los núcleos disponibles de la máquina.
Cuando se trabaja en varios subprocesos, las llamadas al sistema se vuelven demasiadas:
Por cierto, preguntas. ¿Qué llamada al sistema se utiliza para crear un nuevo hilo? ¿En qué se diferencia esta convocatoria de subprocesos de la convocatoria de procesos?
Clase magistral: pila de procesos en el momento de una llamada al sistema
Uno de los recientemente aparecidos. strace capacidades: muestra la pila de llamadas a funciones en el momento de la llamada al sistema. Simple ejemplo:
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;
}
Naturalmente, la salida del programa se vuelve muy voluminosa y, además de la bandera -k (visualización de la pila de llamadas), tiene sentido filtrar las llamadas al sistema por nombre:
$ 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 magistral: inyección de errores
Y una característica más nueva y muy útil: la inyección de errores. Aquí programa, escribiendo dos líneas en el flujo de salida:
Es interesante qué errores se devuelven. todos desafíos write, incluida la llamada escondida detrás de perror. Sólo tiene sentido devolver un error para la primera de las llamadas:
$ 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 +++
O el segundo:
$ 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 +++
No es necesario especificar el tipo de error:
$ 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 otras banderas, puede "interrumpir" el acceso a un archivo específico. Ejemplo:
$ 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 +++
Además de la inyección de errores, uno puede introducir retrasos al realizar llamadas o recibir señales.
Epílogo
Utilidad strace - una herramienta sencilla y fiable. Pero además de las llamadas al sistema, se pueden depurar otros aspectos del funcionamiento de los programas y del sistema operativo. Por ejemplo, puede rastrear llamadas a bibliotecas vinculadas dinámicamente. traza, pueden investigar el funcionamiento del sistema operativo. toque del sistema и seguimientoy le permite investigar en profundidad el rendimiento del programa Perf. Sin embargo, es strace - la primera línea de defensa en caso de problemas con mis programas y los de otras personas, y lo uso al menos un par de veces a la semana.
En resumen, si te encanta Unix, lee man 1 strace ¡Y siéntete libre de echar un vistazo a tus programas!