Topleaked: una herramienta para detectar pérdidas de memoria

imagen

La historia, como suele suceder, comenzó con el hecho de que uno de los servicios en el servidor cayó. Más precisamente, el proceso fue eliminado al monitorear el uso excesivo de memoria. El stock debería haber sido múltiple, lo que significa que tenemos una pérdida de memoria.
Hay un volcado de memoria completo con información de depuración, hay registros, pero no se pueden reproducir. O la fuga es increíblemente lenta, o el escenario depende del clima en Marte. En una palabra, otro error que no se reproduce mediante pruebas, pero se encuentra en estado salvaje. Queda la única pista real: un volcado de memoria.


Idea


El servicio original fue escrito en C ++ y Perl, aunque esto no juega un papel especial. Todo lo que se describe a continuación se aplica a casi cualquier idioma.


Nuestro proceso desde el enunciado del problema fue encajar en un par de cientos de megabytes de RAM, y se completó por más de 6 gigabytes. Por lo tanto, la mayor parte de la memoria del proceso son objetos filtrados y sus datos. Solo es necesario averiguar qué tipos de objetos estaban más en la memoria. Por supuesto, no hay una lista de objetos con información de tipo en el volcado. El seguimiento de las relaciones y la creación de un gráfico como lo hacen los recolectores de basura es prácticamente imposible. Pero no necesitamos entender este hash binario, sino calcular qué objetos son más. Los objetos de clases no triviales tienen un puntero a una tabla de métodos virtuales, y todos los objetos de la misma clase tienen el mismo puntero. Cuántas veces se encuentra un puntero a una clase vtbl en la memoria, se han creado tantos objetos de esta clase.


Además de vtbl, existen otras secuencias frecuentes: constantes que inicializan campos, encabezados HTTP en fragmentos de cadena, punteros a funciones.
Si tiene la suerte de encontrar un puntero, entonces podemos usar gdb para comprender a qué apunta (a menos, por supuesto, que haya caracteres de depuración). En el caso de los datos, puede intentar verlos y comprender dónde se utiliza. Mirando hacia el futuro, noto que sucede tanto eso como otro y, a partir de un fragmento de una línea, es muy posible comprender qué es esta parte del protocolo y dónde es necesario profundizar más.


La idea fue espiada, y la primera implementación fue copiada de manera insoportable de stackoverflow. https://stackoverflow.com/questions/7439170/is-there-a-way-to-find-leaked-memory-using-a-core-file


hexdump core.10639 | awk '{printf "%s%s%s%s\n%s%s%s%s\n", $5,$4,$3,$2,$9,$8,$7,$6}' | sort | uniq -c | sort -nr | head 

El guión funcionó durante unos 15 minutos en nuestro basurero, devolvió un montón de líneas y ... nada. Ni un solo puntero, nada útil.


Resuelto


El desarrollo impulsado por Stackoverflow tiene sus inconvenientes. No puedes simplemente copiar el guión y esperar que todo funcione. En este script en particular, algún tipo de reordenamiento de bytes llama inmediatamente la atención. También surge la pregunta de por qué las permutaciones en 4. No es necesario ser un súper especialista para comprender que tales permutaciones dependen de la plataforma: bitness y orden de bytes.


Para comprender exactamente cómo mirar, debe comprender el formato de archivo del volcado de memoria, LITTLE- y BIG-endian, o simplemente puede reorganizar los bytes en las piezas encontradas de diferentes maneras y dar gdb. ¡Oh milagro! En orden directo, el byte gdb ve el carácter y dice que es un puntero a una función.


En nuestro caso, fue un puntero a una de las funciones de lectura y escritura en búferes openssl. Para personalizar la entrada y la salida, se utiliza el enfoque del sistema OOP: una estructura con un conjunto de punteros a las funciones, que es una especie de interfaz o más bien vtbl. Estas estructuras con punteros resultaron ser increíblemente numerosas. Una mirada cercana al código responsable de establecer estas estructuras y crear buffers nos permitió encontrar rápidamente el error. Al final resultó que, en la unión de C ++ y C no había objetos RAII y, en caso de error, el retorno temprano no dejaba la oportunidad de liberar recursos. Nadie adivinó cargar el servicio con apretones de manos SSL incorrectos de manera oportuna, por lo que se lo perdieron. Cómo marcar 6 gigabytes de apretones de manos SSL incorrectos también es interesante, pero como dicen, esta es una historia completamente diferente. El problema está resuelto.


topleaked


El script resultó ser útil, pero aún tiene serios inconvenientes para el uso frecuente: es muy lento, depende de la plataforma, luego resulta que los archivos de volcado también tienen diferentes compensaciones, es difícil interpretar los resultados. La tarea de excavar en un volcado binario no encaja bien con bash, por lo que cambié el lenguaje de programación a D. La elección del idioma se debe realmente al deseo egoísta de escribir en su idioma favorito. Bueno, la racionalización de la elección es la siguiente: la velocidad y el consumo de memoria son críticos, por lo que necesita un lenguaje compilado nativo, y es banal escribir D más rápido que C o C ++. Más adelante en el código será claramente visible. Así nació el proyecto topleaked .


Instalación


No hay ensamblados binarios, por lo que de una forma u otra necesitará ensamblar el proyecto desde el origen. Para hacer esto, necesita el compilador D. Hay tres opciones: dmd es el compilador de referencia, ldc se basa en llvm y gdc, incluidos en gcc, a partir de la versión 9. Por lo tanto, es posible que no tenga que instalar nada si tiene la última versión de gcc. Si lo instala, le recomiendo ldc, ya que se optimiza mejor. Los tres se pueden encontrar en el sitio web oficial .
El gestor de paquetes dub se suministra con el compilador. Al usarlo, topleaked se instala con un comando:


 dub fetch topleaked 

En el futuro, usaremos el comando para comenzar:


 dub run topleaked -brelease-nobounds -- <filename> [<options>...] 

Para no repetir la ejecución de dub y el argumento del compilador brelease-nobounds, puede descargar las fuentes del github y recopilar el archivo ejecutable:


 dub build -brelease-nobounds 

En la raíz de la carpeta del proyecto aparecerá topleaked.


Uso


Tomemos un programa simple de C ++ con una pérdida de memoria.


 #include <iostream> #include <assert.h> #include <unistd.h> class A { size_t val = 12345678910; virtual ~A(){} }; int main() { for (size_t i =0; i < 1000000; i++) { new A(); } std::cout << getpid() << std::endl; sleep(200); } 

Lo completamos a través de kill -6, luego obtenemos un volcado de memoria. Ahora puede ejecutar topleaked y ver los resultados

 ./toleaked -n10 leak.core 

La opción -n es el tamaño de la parte superior que necesitamos. Por lo general, los valores entre 10 y 200 tienen sentido, dependiendo de la cantidad de "basura" que haya. El formato de salida predeterminado es una línea por línea en forma legible por humanos.


 0x0000000000000000 : 1050347 0x0000000000000021 : 1000003 0x00000002dfdc1c3e : 1000000 0x0000558087922d90 : 1000000 0x0000000000000002 : 198 0x0000000000000001 : 180 0x00007f4247c6a000 : 164 0x0000000000000008 : 160 0x00007f4247c5c438 : 153 0xffffffffffffffff : 141 

Es de poca utilidad, excepto que podemos ver el número 0x2dfdc1c3e, que también es 12345678910, que ocurre un millón de veces. Esto ya podría ser suficiente, pero quiero más. Para ver los nombres de clase de los objetos filtrados, puede enviar el resultado a gdb simplemente redirigiendo la secuencia de salida estándar a la entrada de gdb con un archivo de volcado abierto. -ogdb: opción para cambiar el formato a gdb comprensible.


 $ ./topleaked -n10 -ogdb /home/core/leak.1002.core | gdb leak /home/core/leak.1002.core ...<   gdb  > #0 0x00007f424784e6f4 in __GI___nanosleep (requested_time=requested_time@entry=0x7ffcfffedb50, remaining=remaining@entry=0x7ffcfffedb50) at ../sysdeps/unix/sysv/linux/nanosleep.c:28 28 ../sysdeps/unix/sysv/linux/nanosleep.c: No such file or directory. (gdb) $1 = 1050347 (gdb) 0x0: Cannot access memory at address 0x0 (gdb) No symbol matches 0x0000000000000000. (gdb) $2 = 1000003 (gdb) 0x21: Cannot access memory at address 0x21 (gdb) No symbol matches 0x0000000000000021. (gdb) $3 = 1000000 (gdb) 0x2dfdc1c3e: Cannot access memory at address 0x2dfdc1c3e (gdb) No symbol matches 0x00000002dfdc1c3e. (gdb) $4 = 1000000 (gdb) 0x558087922d90 <_ZTV1A+16>: 0x87721bfa (gdb) vtable for A + 16 in section .data.rel.ro of /home/g.smorkalov/dlang/topleaked/leak (gdb) $5 = 198 (gdb) 0x2: Cannot access memory at address 0x2 (gdb) No symbol matches 0x0000000000000002. (gdb) $6 = 180 (gdb) 0x1: Cannot access memory at address 0x1 (gdb) No symbol matches 0x0000000000000001. (gdb) $7 = 164 (gdb) 0x7f4247c6a000: 0x47ae6000 (gdb) No symbol matches 0x00007f4247c6a000. (gdb) $8 = 160 (gdb) 0x8: Cannot access memory at address 0x8 (gdb) No symbol matches 0x0000000000000008. (gdb) $9 = 153 (gdb) 0x7f4247c5c438 <_ZTVN10__cxxabiv120__si_class_type_infoE+16>: 0x47b79660 (gdb) vtable for __cxxabiv1::__si_class_type_info + 16 in section .data.rel.ro of /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (gdb) $10 = 141 (gdb) 0xffffffffffffffff: Cannot access memory at address 0xffffffffffffffff (gdb) No symbol matches 0xffffffffffffffff. (gdb) quit 

Leer no es muy simple, pero es posible. Las líneas de la forma $ 4 = 1,000,000 reflejan la posición en la parte superior y el número de ocurrencias encontradas. A continuación se muestran los resultados de ejecutar x e símbolo de información para el valor. Aquí podemos ver que vtable para A ocurre un millón de veces, lo que corresponde a un millón de objetos filtrados de la clase A.

Para analizar parte del archivo (si es demasiado grande), se agregan las opciones de desplazamiento y límite, comenzando desde dónde y cuántos bytes leer.


Resultado


La utilidad resultante es notablemente más rápida que el script. Todavía tiene que esperar, pero no en la escala de una caminata para el té, sino unos segundos antes de que aparezca la parte superior en la pantalla. Estoy absolutamente seguro de que el algoritmo se puede mejorar significativamente, y las operaciones pesadas de entrada y salida se pueden optimizar significativamente. Pero esto es una cuestión de desarrollo futuro, ahora todo está funcionando bien.


Gracias a la opción -ogdb y la redirección en gdb, obtenemos inmediatamente nombres y valores, a veces incluso números de línea, si tenemos la suerte de llegar a la función.


La consecuencia obvia, pero muy inesperada, de la solución frontal fue multiplataforma. Sí, topleaked no conoce el orden de los bytes, pero dado que no analiza el formato del archivo, sino que simplemente lee el archivo byte por byte, puede usarse en Windows o en cualquier sistema con cualquier formato de volcado de memoria. Solo se requiere que los datos estén alineados dentro del archivo.


Lenguaje D


Me gustaría señalar por separado la experiencia de desarrollar un programa de este tipo en D. La primera versión de trabajo se escribió en cuestión de minutos. Debo decir que hasta ahora el algoritmo principal solo toma tres líneas:


 auto all = input.sort; ValCount[] res = new ValCount[min(all.length, maxSize)]; return all.group.map!((p) => ValCount(p[0],p[1])) .topNCopy!"a.count>b.count"(res, Yes.sortOutput); 

Todo gracias a los rangos diferidos y la presencia de algoritmos listos para usar sobre ellos en la biblioteca estándar, como group y topN.


Más tarde, el análisis de los argumentos de la línea de comandos, el formato de la salida y todo lo que es detallado, pero también escrito rápidamente, creció en la parte superior. A menos que la lectura del archivo resultara de alguna manera extraña, fuera del estilo general.


En la última versión en este momento, el indicador --find apareció para la búsqueda habitual de una subcadena, que no está relacionada en absoluto con la frecuencia. Debido a este problema, el código ha aumentado notablemente de tamaño, pero con altas posibilidades, la función se eliminará y el código volverá a su estado simple original.


En total, los costos de mano de obra son comparables a los lenguajes de scripting y mucho mejores en rendimiento. Potencialmente, puede llevarlo al máximo posible, ya que el mismo código en C y D funcionará igual a la misma velocidad.


Indicaciones y contraindicaciones de uso.


  • Se necesita Topleaked para buscar fugas cuando solo hay un volcado de la memoria del proceso actual, pero no hay forma de reproducirlo bajo el desinfectante.
  • Este no es otro valgrind y no pretende ser un análisis dinámico.
  • Una excepción interesante al comentario anterior puede ser fugas temporales. Es decir, la memoria se libera, pero demasiado tarde (cuando el servidor se detiene, por ejemplo). Luego puede eliminar el volcado en el momento adecuado y analizar. Valgrind o asan, trabajando en el momento en que finaliza el proceso, pueden empeorar esto.
  • Solo modo de 64 bits. El soporte para otros bits y el orden de bytes se pospone para el futuro.

Problemas conocidos


Durante las pruebas, se utilizaron archivos de volcado que se recibieron enviando una señal al proceso. Con tales archivos, todo funciona bien. Cuando se elimina un volcado, el comando gcore escribe algunos otros encabezados ELF y se produce un desplazamiento por un número indefinido de bytes. Es decir, los valores de los punteros no están alineados a 8 en el archivo, por lo que se obtienen resultados sin sentido. Para la solución, se introdujo la opción de desplazamiento: leer el archivo no primero, sino desplazado por bytes de desplazamiento (generalmente 4).
Para resolver esto, planeo agregar la lectura del resultado de objdump -s de stdin. Bueno, conecte libelf y analícelo usted mismo, pero matará "multiplataforma", y stdout es más flexible y más cercano a la forma de Unix.


Referencias


Proyecto Github
Compiladores D
Pregunta original sobre stackoverflow

Source: https://habr.com/ru/post/485102/


All Articles