Mirar en ejecutables y bibliotecas para facilitar la depuración

A primera vista, tanto las compilaciones ejecutables producibles como las bibliotecas utilizadas durante el proceso de construcción parecen no ser muy accesibles. Son estas cajas negras las que hacen una aplicación, o hacen feliz el enlace, cuando le da el archivo de biblioteca "correcto". También hay mucho que decir porque no ha profundizado demasiado en ambos, ya que normalmente las cosas funcionarán de forma sencilla sin tener que preocuparse por esos detalles adicionales.

La cuestión es que tanto los ejecutables como las bibliotecas contienen mucha información que normalmente solo utiliza el sistema operativo, la caja de herramientas, los depuradores y herramientas similares. ¿Están estos archivos en formato Windows PE, Linux de la vieja escuela? a.out o contemporáneo .elfCuando las cosas van mal durante el desarrollo, a veces hay que aprovechar las herramientas adecuadas para inspeccionarlas y dar sentido a lo que está sucediendo.

Este artículo se centrará principalmente en la plataforma Linux, aunque la mayoría también se aplica a BSD y MacOS, y hasta cierto punto a Windows.

Abriendo la Caja Negra

Independientemente de la plataforma en la que se encuentre, los formatos ejecutables y de biblioteca tienen muchas secciones en común. Por supuesto, existe la sección con las instrucciones reales, así como la sección con todas las cadenas de texto y valores constantes que ingresamos en el código antes de compilarlo. Si ordenamos al compilador que genere símbolos de depuración y le dijimos al enlace que los deje en su lugar, también tenemos los símbolos de depuración en su propia sección. Los veremos más adelante en este artículo.

En ELF (formato ejecutable y enlazable), que se usa comúnmente en Linux y muchos otros sistemas operativos, el diseño preliminar sigue este diagrama. No todas estas secciones son necesarias y su inclusión depende de las opciones que se eligieron cuando se creó el archivo ejecutable.

Se puede obtener una descripción general rápida de las propiedades de un archivo ejecutable con la herramienta de archivos:

ELF 32-bit LSB shared object, Intel 80386, version 1 (GNU/Linux), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=0558c7ef0f6845826d012b4ccc14948a2ffe8277, stripped

Esta salida nos dice que estamos tratando con un binario de 32 bits, compilado para la arquitectura x86, que usa varias bibliotecas comunes y cuyos símbolos de depuración están en desuso.

Si los símbolos de depuración todavía están presentes, obtenemos:

ELF 32-bit LSB shared object, Intel 80386, version 1 (GNU/Linux), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=0558c7ef0f6845826d012b4ccc14948a2ffe8277, with debug_info, not stripped

En este caso particular, estamos ante una compilación binaria en Raspbian Buster para x86, que es una versión de 32 bits de Linux, para que todos sean compatibles.

Para un archivo ejecutable de Windows obtenemos el siguiente resultado menos extenso:

PE32+ executable (GUI) x86-64, for MS Windows

Esto nos dice que estamos tratando con un PE implementable (Windows) compilado para la arquitectura x86-64 de 64 bits.

Como uno podría haber adivinado en este punto, las bibliotecas, tanto dinámicas como compartidas, usan el mismo formato que los archivos ejecutables, por ejemplo, examinando .so un archivo de biblioteca común en Linux generaría casi el mismo resultado cuando usamos el expediente pedido.

Comparte responsablemente

Exclusivo de los sistemas operativos (de escritorio) es la capacidad de cargar bibliotecas dinámicas (compartidas) cuando se inicia la aplicación. Aquí se supone que las bibliotecas necesarias están presentes en el sistema de alojamiento y en la ruta de búsqueda del cargador de bibliotecas (componente del SO). Las bibliotecas también se pueden versionar para indicar diferentes revisiones. Esto suele suceder con el nombre del archivo, con el nombre del género (p. Ej. libfoo.so) simulado al archivo real (libfoo.so.0.1). Si hay una falta de compatibilidad con la versión, esto puede resultar en un error simbólico, que veremos en la siguiente sección.

Al ejecutar archivos de una biblioteca compartida, es fácil comprobar qué dependencias directas (codificadas en el archivo ejecutable) utiliza, comprobando el ejecutable con la herramienta ldd, que tiene que no funciona bien con la anterior. a.out formato. No se trata realmente del desarrollo actual en Windows, Linux / BSD y MacOS, que utilizan los formatos PE (PE32 +), ELF y Mach-O respectivamente. Para el desarrollo integrado (por ejemplo, ARM Cortex-M), el formato ELF también se utiliza como formato de mediación antes de generar la imagen binaria.

Listado de dependencias

La salida básica de ldd muestra dónde están las dependencias directas en el sistema de archivos y qué dependencias no se encuentran. Por ejemplo, esta es la salida (muy) abreviada de ldd por ffplay.exe en MSYS2 en Windows:

$ ldd /mingw64/bin/ffplay.exe
        ntdll.dll => /c/Windows/SYSTEM32/ntdll.dll (0x77780000)
        kernel32.dll => /c/Windows/system32/kernel32.dll (0x77660000)
        KERNELBASE.dll => /c/Windows/system32/KERNELBASE.dll (0x7fefd730000)
        msvcrt.dll => /c/Windows/system32/msvcrt.dll (0x7fefed80000)
        SHELL32.dll => /c/Windows/system32/SHELL32.dll (0x7fefdab0000)
        SHLWAPI.dll => /c/Windows/system32/SHLWAPI.dll (0x7fefda10000)
        GDI32.dll => /c/Windows/system32/GDI32.dll (0x7feff0e0000)
        USER32.dll => /c/Windows/system32/USER32.dll (0x77560000)
        LPK.dll => /c/Windows/system32/LPK.dll (0x7fefeb30000)
        USP10.dll => /c/Windows/system32/USP10.dll (0x7feff6e0000)
        SDL2.dll => /mingw64/bin/SDL2.dll (0x644c0000)
        [...]

Las dependencias mostradas para el ejecutable promedio pueden ser bastante masivas (la lista completa es aproximadamente ocho veces más larga que esta longitud), pero sirve como una verificación prudencial rápida para ver no solo si se ha cumplido una dependencia, sino también si el cargador de aplicaciones elegido el correcto. biblioteca. Puede suceder, por ejemplo, que un sistema tenga dos versiones diferentes de una biblioteca (por ejemplo, en / usr / shared / bin y / usr / bin), lo que puede llevar a la ridícula situación en la que pasas medio día depurando varias bibliotecas y versiones de aplicaciones, devolviendo versiones de código "que se sabe que funcionan" y perdiendo la cordura.

Otra cosa, como herramienta ldd muestra es en qué dirección se cargó la biblioteca, pero eso solo sirve para niveles realmente altos de depuración y optimización.

Cuando los símbolos se ausentan

Las cosas son divertidas cuando hablamos de símbolos en el contexto de formatos ejecutables y de biblioteca. No se trata de depurar símbolos, que son un tema completamente diferente, sino de los símbolos que son posibles para encontrar secciones de código, ya sea durante la ejecución o al vincular archivos de objetos y bibliotecas estáticas. Los símbolos que faltan también provocan errores de ejecución divertidos cuando no se encuentra un "punto de entrada" en ninguna biblioteca común.

Una forma rápida de resolver estos problemas suele ser asegurarse de tener las versiones compatibles de las bibliotecas para el código o el archivo ejecutable. A veces, esto verifica todo, y el cargador de la aplicación o el enlace aún te da una pista sobre los símbolos que faltan, entonces, ¿qué pasa?

En el caso del código de enlace, puede ser tan simple como el orden de enlace incorrecto, ya que las cadenas de herramientas para la mayoría de los idiomas utilizan un estilo de enlace oportunista que recuerda los símbolos que faltan pero no recuerda los símbolos que ya ha visto. Mientras que en lenguajes como Ada esto no es un problema, en lenguajes de estilo C, determinar el enlace en los comandos dados al enlace es esencial.

Otro problema es cuando un lenguaje (como C ++) admite funciones de sobrecarga para admitir diferentes argumentos y tipos de retorno, y se utiliza la alimentación de nombres (para obtener un símbolo único). Si un archivo de encabezado se compiló en modo C ++, cuando se supone que debe estar vinculado a una biblioteca compilada como código C, sin una fuente, esto provocaría que el enlace diera el error "símbolo faltante" para esas funciones.

Para averiguar si un símbolo faltante realmente falta, se maneja mal, se deja sin mezclar o en otra biblioteca o archivo de objeto, se puede usar una utilidad como un archivo de lectura para verificar qué símbolos están realmente en el archivo. Tenga en cuenta que (obviamente) readelf solo admite archivos de estilo ELF. Una utilidad más general que se centra solo en símbolos en varios formatos es nm. Por ejemplo, esta salida de la entrada de Wikipedia en nm:

# nm test.o
0000000a T _Z15global_functioni
00000025 T _Z16global_function2v
00000004 b _ZL10static_var
00000000 t _ZL15static_functionv
00000004 d _ZL15static_var_init
00000008 b _ZZ15global_functioniE16local_static_var
00000008 d _ZZ15global_functioniE21local_static_var_init
         U __gxx_personality_v0
00000000 B global_var
00000000 D global_var_init
0000003b T main
00000036 T non_mangled_function

Esto muestra cómo se ve la salida de nm cuando se usa un compilador de C ++. Se puede enseñar a Nm a desmontar símbolos para facilitar su lectura, si es necesario. De todos modos, su salida nos dice si un símbolo existe en el archivo o es indefinido ('U'). También detallará dónde se define el símbolo (qué sección) y qué tipo de símbolo es (si corresponde). En el ejemplo anterior, vemos un símbolo indefinido ('U'), varios símbolos de sección de texto (código) ('T' y 't'), un símbolo en la sección de datos no inicializados (BSS, 'B' y 'b') y dos en la sección de datos inicializados ('D' y 'd').

De estos, solo necesitaremos darle al enlace una biblioteca o archivo objeto que contenga el símbolo indefinido para hacer que este código se enlace y produzca un ejecutable.

Última salida: inicio de la aplicación de seguimiento

Es molesto, a veces todo parece estar en orden, sin embargo, la aplicación no se inicia o se detiene a la mitad de un mensaje misterioso. Esta utilidad como strace puede ser extremadamente útil ya que rastrea todas las llamadas al sistema con la aplicación desde el inicio de la aplicación. A menudo, el problema de una aplicación descargada se debe a una dependencia indirecta que no se puede cargar, a una configuración ambiental inadecuada oa un archivo que resultó ser ilegible.

Simplemente iluminar una calle con la aplicación como argumento mostrará una lista de las llamadas al sistema realizadas por la aplicación, incluidos los errores, como un archivo faltante:

open("/foo/bar", O_RDONLY) = -1 ENOENT (No such file or directory)

O falta la dependencia de la biblioteca:

open("/usr/lib/libfoo.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)

Envolvente

Obviamente, nada de esto es la depuración final pendiente del enlace y el funcionamiento de ejecutables, binarios y una variedad de problemas relacionados. Como sucede con tantas cosas en la vida, en última instancia, la mayoría de las experiencias son importantes. Con el tiempo, se desarrollará una intuición sobre dónde probablemente radica el problema, así como sobre cómo descubrir al culpable lo más rápido posible.

Después de haber pasado muchos años en el desarrollo de programas de negocios y haber sobrevivido a una variedad de proyectos de pasatiempos (demasiado) ambiciosos, ciertamente puedo decir que hay muchos conocimientos que me gustaría tener antes. Por otro lado, el acto de descubrir por qué ciertas cosas no funcionaron y corregir esta injusticia contra el orden del mundo solía ser gratificante en sí mismo.

Dicho esto, hay que elegir sabiamente sus batallas. A veces, aprender cosas desde cero no vale la pena, y confiar en el conocimiento de los demás no es nada de lo que avergonzarse. Especialmente cuando el viernes por la tarde y el cliente espera la entrega de la nueva versión el lunes. Ojalá este artículo haya ayudado en este sentido.

  • blanco dice:

    Solo para decir que a.out es anterior a Linux, o incluso a Minix (¿recuerdas eso?) Pero estaba en Unix, y 4.2 BSD y System V, y probablemente también en Unix anterior.

    • huele a bicicletas dice:

      Desde 3BSD / VMUNIX en 1979, al menos.

  • Geremy Condra dice:

    Si la gente siente curiosidad acerca de cuánta información se extrae de un binario compilado, recientemente escribí un rifle que usa información libre de errores para inferir el sistema típico completo de un programa en lenguaje C y rifles API generados automáticamente para él. Puede encontrarlo en GitHub aquí: https://github.com/intel/fffc

  • codificación posterior a la hora dice:

    ¿Podría un método readelf / nm, por ejemplo, ayudar a resolver los excedentes de pila o acumular problemas de asignación?

    • Ricardo dice:

      Probablemente no, aunque si te esfuerzas lo suficiente, es posible crear un ejemplo en el que puedan ser un poco útiles.

      Los desbordamientos de pila y los problemas de asignación masiva son generalmente problemas de tiempo de ejecución, a menudo causados ​​por fallas en la lógica del programa, mientras que readelf y nm son herramientas para mostrar información de matriz simbólica durante el tiempo de compilación.

      El ejemplo más fácil que se me ocurre donde podrían ser útiles es si tiene dos programas llamados "foo", en diferentes bibliotecas, y su programa principal se llama "foo" con la expectativa de que se comporte como el "foo" " en el que escribió la biblioteca A, pero en su lugar selecciona un "foo" diferente de la biblioteca B, y la diferencia de comportamiento entre los dos programas hace que la pila sea un síntoma superfluo.

      Para resolver problemas de enlaces, como referencias externas no resueltas (o el problema que acabo de describir, donde desea saber qué versión de una rutina de biblioteca particular usa su programa), readelf / nm son buenos puntos de partida. Para resolver los excedentes de pila o los problemas de corrupción masiva, un depurador simbólico interactivo suele ser la herramienta opcional para comenzar.

Matías Jiménez
Matías Jiménez

Deja una respuesta

Tu dirección de correo electrónico no será publicada.