Capítulo 2. Trabajo realizado

Tabla de contenidos
2.1. Funcionamiento
2.2. Estructura y decisiones de diseño
2.2.1. Lenguaje y sistema de compilación
2.2.2. Organización del código
2.2.3. Distribución de los ficheros
2.3. Tipos de interposiciones
2.3.1. Conceptos básicos para crear una interposición de código
2.3.2. Mecanismo de reenlace
2.3.3. Mecanismo de redefinición
2.3.4. Mecanismo de "callback"
2.4. Mejoras y extensiones
2.5. Validación
2.5.1. Pruebas automáticas
2.5.2. Testeo y comprobación manual
2.5.2.1. Reenlaces
2.5.2.2. Redefiniciones
2.5.2.3. Interposiciones mediante "callback"

En este capítulo documentamos el funcionamiento, decisiones tomadas y tareas realizadas sobre pDI-Tools a lo largo del proyecto.

El capítulo comienza por una descripción global de pDI-Tools que permite construir una imagen global sobre su funcionamiento.

Seguidamente describimos las decisiones de implementación tomadas, hablando a grandes rasgos sobre el flujo de ejecución que seguirá pDI-Tools hasta los mínimos detalles como los nombres de variables.

Todo ello finaliza en una última parte donde se explica la implementación de los mecanismos de interposición. Partiendo de los conceptos más básicos para luego ir aumentando la complejidad hasta llegar a explicar con detalle los "callback" sobre INTEL® 386.

2.1. Funcionamiento

El funcionamiento de pDI-Tools, observado superficialmente, es muy similar al de DITools. La primera diferencia más evidente es que pDI-Tools está implementado todo en una única librería libpdi.so. Los mecanismos de interposición son esencialmente idénticos, a excepción del "callback", el cual se implementa de un modo distinto, un poco más robusto y más eficiente.

El esquema de funcionamiento es muy similar al de DITools como se puede observar en este diagrama que muestra la interacción de pDI-Tools con los objetos de la aplicación instrumentada.

En este esquema observamos varias aplicaciones, más una instrumentada por pDI-Tools. Los objetos compartidos (DSO) de la aplicación se representan como rectángulos. En cambio, los DSO de DITools se muestran como dos elipses.

Al igual que su antecesor para usarlo debemos aprovechar la variable de entorno LD_PRELOAD (_RLD_LIST (rld(5)) en caso de Irix). Esta variable de entorno nos permite añadir un nuevo objeto compartido en el espacio de trabajo de una aplicación que instrumentamos. Durante su inicialización, que siempre es antes de comenzar la ejecución de la aplicación, se realizan todas las tareas de inicialización, configuración e interposición de código.

Seguidamente explicamos el proceso que sigue habitualmente pDI-Tools, desde su inicialización hasta que empieza a ejecutarse el programa instrumentado.

La inicialización de pDI-Tools comienza por recoger una serie de punteros reales a muchas funciones de las librerías de C. Cada puntero es absoluto a la definición de una función, libre de todos los mecanismos de enlace de ELF. Estos punteros permitirán a pDI-Tools acceder a las funciones de la librería de C sin verse afectado por interposiciones accidentales.

Acto seguido se recoge la configuración de pDI-Tools. pDI-Tools permite variar notablemente su comportamiento y funcionalidades sin ser necesaria una recompilación. pDI-Tools incorpora un sistema más potente de configuración que DITools. Este nuevo mecanismo de configuración se basa en ficheros de configuración y, por compatibilidad con DITools, acepta también variables de entorno. Los ficheros de configuración son sencillos ficheros de texto divididos en secciones en las cuales encontramos asignaciones a parámetros.

La configuración a su vez determina el siguiente paso de ejecución: la carga e interpretación de los comandos de interposición de código. Durante la configuración se indica que ficheros de comandos se usarán, que a diferencia de DITools estos pueden ser más de dos.

Pero permitir más de dos ficheros de comandos de interposición de código aumenta la complejidad a la hora de combinar los "backends" y los comandos. Se deben resolver posibles dependencias entre "backends". La resolución de estas posibles dependencias resultará en un orden de carga e inicialización de los "backends" junto a un guión final de comandos de interposición.

Una vez completado el guión final se realizan muchas comprobaciones. Lo primero es buscar los "backend" que intervendrán en esta sesión. Luego se comprueba si los objetos en memoria se corresponden con los indicados en los ficheros de comandos. También se comprueba que no haya interacción entre los diferentes comandos.

En estos momentos pDI-Tools está en condiciones de ejecutar los comandos. Al igual que en el antiguo DITools primero se cargan e inicializan los "backend" y posteriormente se instalan las interposiciones de código. La carga de los "backend" y su inicialización se realiza en el orden calculado anteriormente. Es en este punto donde se ejecuta la mayor parte del código dependiente de la arquitectura.

Llegados a este punto sólo queda realizar algunas tareas de limpieza y liberación de memoria y se transfiere el control al programa principal. A partir de aquí no se vuelve a retomar el control a no ser que la aplicación lo entregue, ya sea mediante su propia terminación o mediante una llamada explicita a la API ofrecida.

Generalmente durante la ejecución de la aplicación no se ejecuta código de pDI-Tools a excepción de los "stubs" de interposición, si estos existen. Es en este momento cuando se ejecuta el código interpuesto por los "backend".

Una vez el programa termina su ejecución (normalmente mediante una llamada a la función exit(3) o por la finalización de la función main) el código de finalización de pDI-Tools vuelve a retomar el control.

La finalización comienza por deshacer todas las interposiciones realizadas antes y durante la ejecución del programa. Esto nos permitirá ejecutar con tranquilidad y sin interferencias el código de finalización de los "backend".

Posteriormente se ejecuta el código de finalización de los diferentes "backend". Las rutinas de finalización de los "backends" se ejecutan en el orden inverso al de las rutinas de inicialización.

Acto seguido se descargan explícitamente los "backend" de memoria. Esto se realiza por cuestiones de corrección, ya que de todas formas esta operación la hace el sistema operativo al finalizar la ejecución de una aplicación.

Finalmente se imprime el último mensaje de "log", se cierran todos los ficheros abiertos y se libera toda la memoria usada.