La instrumentación de código es una tecnología que permite analizar o variar el comportamiento de un programa durante su ejecución. Mediante este análisis se pueden descubrir muchas cosas del programa objetivo o incluso del sistema operativo que se está usando.
Hay muchísimas técnicas de instrumentación de código, desde las más sencillas como sería instrumentar estáticamente el mismo código fuente y luego compilar, hasta complejas técnicas de instrumentación en tiempo de ejecución (técnicas conocidas como instrumentación dinámica).
Cuando un programa se carga en memoria este suele requerir la cooperación de otras librerías para poder realizar su trabajo. Al conjunto del programa junto a las librerías se les conoce como objetos compartidos dinámicamente (DSO). Cuando el sistema operativo ejecuta un programa carga todos estos DSO en memoria y, mediante unos mecanismos definidos por el estándar ELF, establece como se han de llamar estos entre si.
En el siguiente documento presentamos el proyecto pDI-Tools. pDI-Tools es un mecanismo creado para poder realizar sesiones de instrumentación sobre un binario, incluso sin información de depuración del mismo, dinámicamente. Este programa interpone el código mediante mecanismos basados en interceptar las llamadas entre objetos compartidos dinámicamente. Estos mecanismos se basan en explotar la información de ELF y así interponer código creando un entorno apto para desarrollar herramientas de instrumentación.
pDI-Tools fue diseñado basándose en los mecanismos de interposición de DITools, herramienta desarrollada por el CEPBA para Irix sobre MIPS®. pDI-Tools, a pesar de inspirarse en ella, se basa en un nuevo diseño orientado a obtener una gran portabilidad sin apenas sacrificar rendimiento y eficiencia.
Aunque el programador haga un gran esfuerzo por obtener un código óptimo y correcto, hay muchos factores externos e internos que dificultan esta labor: ordenadores complejos, recursos cuyo acceso no es uniforme, sistemas operativos de tiempo compartido, sistemas de comunicación con singularidades, miles de opciones de configuración (y de configuraciones posibles) y un largo etcétera, que añadido a la complejidad del software y/o de los datos con los que trabaja hacen que el comportamiento de un programa sea algo muy difícil de predecir. Y nos podemos encontrar en alguna de estas situaciones:
El rendimiento del software desarrollado no se corresponde en absoluto con el rendimiento teórico calculado para la máquina.
Cuesta demasiado imaginar la ejecución del programa y por lo tanto determinar dónde están exactamente los verdaderos cuellos de botella.
Ocurren cosas raras en un determinado sistema y en otros no.
Sabemos que los algoritmos que usamos ya son óptimos, pero aún así creemos que se puede mejorar la velocidad, ¿cómo averiguar dónde debemos optimizar?
Aún peor: no se dispone del código fuente, sólo de una caja negra que está dando un rendimiento pésimo debido a alguna característica del sistema que no se consigue identificar.
O simplemente: no se sabe que está pasando ahí dentro, pero pasa algo inesperado.
La instrumentación de código es una herramienta ideal en estas situaciones: nos permite analizar la ejecución del programa en los puntos clave sin llegar al extremo nivel de detalle e interferencia de las herramientas de depuración, sin sufrir la rigidez de las herramientas de "profiling". Además la instrumentación de código, al poder variar fácilmente la ejecución del programa, no se limita tan sólo al análisis de software: sus aplicaciones son virtualmente infinitas.
No obstante la flexibilidad ofrecida varía mucho según el tipo de instrumentación usada. A grandes rasgos hay los siguientes tipos:
Instrumentación estática. Se puede realizar de dos maneras:
Sobre el código fuente. Consiste en insertar el código de instrumentación en el código fuente. Es necesario compilar de nuevo para obtener una versión instrumentada de la aplicación.
Sobre el binario. Consiste en usar una herramienta especial que inserta el código de instrumentación en el binario ya compilado. No es necesario recompilar, pero si puede ser necesario disponer de información de depuración o similar.
Instrumentación dinámica. Consiste en alterar el código del ejecutable (o al menos las tablas de símbolos) en tiempo de ejecución para redirigir y controlar la ejecución del programa. No es necesaria la información de depuración aunque si se dispone de ella es posible aprovecharla.
Instrumentar estáticamente el código fuente es la técnica más potente ya que permite realizar cualquier tipo de estudio sobre el programa; en contrapartida, al introducir cambios en los fuentes, con la recompilación, a veces, se obtiene binarios muy distintos del original, obteniendo mediciones bastante trastocadas al instrumentar[1].
La instrumentación estática sobre el binario permite instrumentar con menos potencia que con la técnica anterior pero a cambio no altera tanto el resultado de la compilación e introduce menos ruido en la instrumentación. No obstante sigue alterando el ejecutable final (lo cual es un problema en caso de que el ejecutable vaya firmado y/o tenga algún mecanismo de protección). Otro problema es que esta técnica suele requerir información de la que no se suele disponer (código fuente, información de depuración, tablas de símbolos, ...).
La instrumentación dinámica se realiza en tiempo de ejecución y sin modificar el binario. Esta instrumentación se puede realizar de diversas formas: modificando el código en memoria (este caso es similar a la instrumentación estática pero en tiempo de ejecución) o trabajando directamente en las tablas de enlace dinámico del programa. La primera solución es algo lenta aunque permite instrumentar con mucha potencia el código, sin embargo necesita información detallada sobre el ejecutable. La segunda es el tipo de instrumentación más rápida, menos intrusiva y más ágil pero se limita tan sólo a las llamadas entre objetos compartidos (entre librerías y desde el ejecutable a librerías), pero a cambio no requiere ningún tipo de información de depuración.
[1] | Aun así existen técnicas basadas en el uso de macros y librerías compartidas para intentar insertar código alterando el binario resultante lo menos posible. |