2.5. Validación

Para demostrar que pDI-Tools se ejecuta correctamente se hace una serie de pruebas que a continuación comentaremos. Estas pruebas garantizan la correcta ejecución de las interposiciones de código, una interpretación adecuada de los ficheros de comandos de interposición, que se gestionan correctamente las estructuras de memoria, etc. Las pruebas se dividen en dos tipos: automáticas y manuales.

Las pruebas automáticas están dedicadas especialmente al código no dependiente de la arquitectura, aunque algunas de ellas comprueban por encima el funcionamiento de las interposiciones. Normalmente éstas ponen a prueba el código de gestión de las estructuras de pDI-Tools.

Las pruebas manuales se realizaban sobre pDI-Tools cada vez que se portaba a una nueva arquitectura. Están orientadas a comprobar que los mecanismos de interposición y gestión del "Elf Backend" funcionan correctamente.

2.5.1. Pruebas automáticas

Las pruebas automáticas forman parte de la distribución y utilizan el banco de pruebas de Autoconf y Automake para realizarse. Por ello, si se desean realizarse las pruebas tan sólo hay que llamar a make desde el directorio de la distribución de pDI-Tools con la orden make check. Esto hará que se ejecuten, una tras otra, las pruebas que describimos a continuación.

Este chequeo no sólo sirve para comprobar la corrección de pDI-Tools cuando está en desarrollo, sino que también es útil para un administrador que va a instalar el "software" y desea comprobar que las librerías se han generado correctamente.

Cada prueba es en realidad un pequeño "shell script" que genera automáticamente ficheros de configuración, de comandos de reenlace junto a pequeños programas en C que, o bien importan parte del código de pDI-Tools, o bien lo usan directamente como un objeto dinámico. Por ello es necesario haber compilado pDI-Tools antes de ejecutar los "tests". Estos ficheros generados serán usados por el "shell script" para realizar uno o varios chequeos.

Se realizan diversas comprobaciones, pero la mayor parte van dirigidas a comprobar el correcto funcionamiento de la parte no dependiente de la arquitectura de pDI-Tools.

El primer "script" comprueba el sistema de recogida de parámetros de configuración. Para ello crea unos programas en C, los ficheros de configuración necesarios y establece las variables de entorno necesarias. Las pruebas que realiza son las siguientes:

En el siguiente "shell script" se prueba el algoritmo de la resolución de dependencias entre "backends". En esta ocasión se pone a prueba el algoritmo de resolución de dependencias y los algoritmos de combinación de ficheros de comandos de interposición:

Por último se realizan unas pruebas básicas con los reenlaces y redefiniciones, y en caso de estar disponibles en esta arquitectura, las interposiciones mediante "callback". Debido a que sería muy complejo hacer un "software" que chequease los casos especiales, según la arquitectura, estas pruebas se realizan manualmente tal como se explica en la siguiente sección.

2.5.2. Testeo y comprobación manual

Cada vez que se acababa un "porting" de pDI-Tools a una nueva plataforma se sometían los mecanismos de interposición de código a una serie de pruebas que dan una cierta garantía de que la implementación es correcta y da exactamente el resultado esperado.

Para realizar estos chequeos se escribieron algunas librerías dinámicas, dos "backends" y un pequeño programa de "test". También se entrega un fichero de comandos para cada arquitectura, pero al no ser una prueba automática, debemos editarlo a mano para probar las diferentes interposiciones.

Este código se entrega con la distribución de pDI-Tools, pero no se compila en la configuración por defecto. Si se desea trastear con él debe indicar el parámetro --enable-debug al llamar a configure. El código fuente reside en el directorio PREFIX/src/link, junto al código fuente de libpdi.so. Se eligió esta ubicación por comodidad: es el directorio donde se genera el binario y permite ejecutar en un mismo comando la reconstrucción de pDI-Tools y el programa de "test".

El programa de "test" simplemente ejecuta unas cuantas funciones sencillas como printf(2), fputc(3), puts(3), etc. y termina. Además requiere las librerías libtest.so y libdyn.so. La primera es una dependencia de la aplicación, y que por tanto carga automáticamente el "Runtime Linker", que contiene una función que imprime cadenas con las funciones printf(2) y fputc(3). La segunda se carga dinámicamente por el programa principal y contiene una función muy parecida a la de la otra librería.

Se entrega otro programa, llamada testth, pensado para testear los "callback" en una aplicación paralela.

Los "backends" son testbe.so y testcb.so. El primero es un "backend" muy completo con "wrappers" y un gestor de "callback". El segundo es un caso sencillo de "backend" sólo orientado a interposiciones mediante "callback".

Por último se entrega un pequeño runtime, runtime.so, de ejemplo que instala un "thread id resolver" para las librerías pthread. Es esencial para que el programa testth funcione correctamente.

Seguidamente describimos que pruebas se han realizado con cada interposición.

2.5.2.1. Reenlaces

Para probar los reenlaces se comienza por probar a interceptar la función fputc(3) en el ejecutable (MAIN). Este es reenlace más sencillo ya que la función sólo recibe dos parámetros y devuelve un valor fácilmente predecible en todas las arquitecturas: el carácter que imprime. De esta forma se prueba el envío de parámetros y la entrega del valor de retorno.

La siguiente prueba consiste en interceptar la misma función, pero esta vez en los objetos compartidos (libtest.so y libdyn.so). Se realiza esta prueba ya que los ejecutables se enlazan siempre estáticamente y se instalan siempre en una determinada dirección base de memoria. En cambio los objetos compartidos trabajan con punteros relativos y ello implica, en algunas plataformas, que se tenga que hacer un pequeño tratamiento previo antes de instalar la interposición.

De nuevo trabajando sobre el objeto libtest.so probamos a interceptar una llamada desde él hacía el ejecutable MAIN. Con esto comprobamos que se realiza también correctamente el caso inverso al anterior. En algunas plataformas como PowerPC™ 64 estas dos últimas pruebas son importantes.

Antes de acabar se intercepta en cada objeto en memoria llamadas a printf(2). Debido a la existencia de funciones como esta, que pueden recibir un número de parámetros variable, se ha de comprobar que los "wrapper" puedan usar la interfaz stdarg.h de C. Con esta prueba se garantiza que se deja la pila en un estado coherente, sin afectar para nada al paso de parámetros y al valor de retorno de la función.

Para finalizar se prueban reenlaces según las limitaciones o características de la plataforma. Por ejemplo en PowerPC™ se probaron reenlaces a funciones cercanas y lejanas (ver más sobre esto en la sección PowerPC sobre GNU/Linux del capítulo Portabilidad e implementación en las diferentes plataformas.

2.5.2.2. Redefiniciones

Las redefiniciones, al reutilizar código usado por el mecanismo de reenlace, se suelen probar una vez que los reenlaces funcionan correctamente.

Para probar las redefiniciones el programa redefine la función fputc(3) de la librería de C. Esto afecta a todas las llamadas realizadas a fputc(3) desde cualquier DSO de la aplicación. Comprobamos que esto es así, pero nos centramos especialmente en la llamada a fputc(3) desde la librería libdyn.so. Esta librería se carga dinámicamente y por lo tanto cuando acceda a fputc(3) el "Runtime Linker" tendrá que resolver la función, en tiempo de ejecución, con las estructuras de datos alteradas por pDI-Tools (en concreto la información de símbolo de fputc(3) en C Library).

Se hace también una prueba de redefinición con una librería que no sea C Library ya que este objeto, en la mayoría de plataformas, es especial y toma como valor base la dirección 0x00000000. Por ello redefinimos la función lib_function del objeto libtest.so de un modo incompleto: forzamos (comentando código) a pDI-Tools para que no realice un reenlace masivo de esta función en todos los objetos. De este modo comprobamos si el "Runtime Linker" resuelve correctamente símbolos de otros objetos.

2.5.2.3. Interposiciones mediante "callback"

Este mecanismo también requiere que funcionen bien los reenlaces, ya que se basa totalmente en este mecanismo para realizar su cometido.

Estas pruebas comienzan por instalar un "backend" sencillo (testcb.so) sobre el ejecutable. Este "backend" simplemente intercepta un pequeño conjunto de funciones. Cada vez que intercepta una función escribe un mensaje antes de ejecución, con su nombre, y un mensaje al finalizar la función. De esta forma comprobamos que lo más básico del mecanismo de "callback", que es la invocación de las funciones de control y la función interceptada, funciona correctamente.

En la siguiente prueba se usaba el "backend" testbe.so. Este "backend" es aún más complejo que el anterior y no sólo intercepta las llamadas a las funciones, sino que opera con sus parámetros y comprueba su valor de retorno. Con este "test" comprobamos que el "generic wrapper" deja la pila en un estado correcto que permita usar la interfaz stdarg.h para recoger una cantidad variable de parámetros.

Por último se ejecutó el programa testth, que ejecuta muchos "threads" en paralelo que escriben un mensaje por pantalla y al cabo de un tiempo aleatorio mueren. Para ejecutar esta prueba se implementa un "thread id resolver" en el "backend" runtime.so. Con este chequeo comprobamos la robustez y buen funcionamiento del mecanismo de "callback".

Todas estas pruebas sólo se han realizado sobre pDI-Tools para Linux™/INTEL® 386 ya que es la única implementación que presenta este mecanismo.