pDI-Tools: Mecanismo de interposición dinámica de código | ||
---|---|---|
Anterior | Capítulo 2. Trabajo realizado | Siguiente |
En esta sección se discuten decisiones que se tuvieron que tomar antes y durante el desarrollo del software. Desde el sistema de compilación, pasando por el lenguaje, hasta las convenciones de programación usadas.
Todas las decisiones se hacen entorno a unas cuantas premisas y/o objetivos marcados que ya se han comentado en el capítulo de introducción. Aún así recordemos que los cuatro objetivos principales son:
Ha de ser una aplicación muy veloz y ligera.
Altamente portable manteniéndose simple.
Sencillo de compilar, configurar e instalar.
Debe ofrecer una API pero que esta no implique trabajar sólo con un determinado lenguaje.
En las siguientes subsecciones se discuten las principales decisiones tomadas para lograr estas metas.
Debido a que se decidió no reusar el código de DITools en el desarrollo de pDI-Tools la primera decisión que se tuvo que tomar fue que lenguaje usar, y consecuentemente, el paradigma de programación adecuado.
El lenguaje escogido fue el C con un diseño de programación descendente. Los lenguajes C y C++ son idóneos para programar a bajo nivel. No obstante la programación orientada a objetos presenta varios inconvenientes en un proyecto de esta índole. Los motivos para desestimar el lenguaje C++ y la programación orientada a objetos se enumeran a continuación:
Excesivo código "overhead". El binario de un programa compilado por C++ incluye mucho código necesario para implementar las características del lenguaje. Esto implica un importante "overhead" que se traduce en una ejecución más lenta. Esta aplicación va destinada principalmente a proyectos de instrumentación. La instrumentación de por sí introduce un importante "overhead". Si encima añadimos el "overhead" que implica usar C++ la instrumentación se ralentiza y podemos introducir demasiado ruido en los resultados de la instrumentación.
La complejidad del programa y sus datos no requieren la potencia de la OOP. Es posible que el programa hubiese podido ser un poco más fácil de leer si hubiera sido escrito en C++, pero en realidad hubiera sido muy poco más. La cantidad de estructuras de datos que hay es muy reducida. Además estas estructuras son muy sencillas e inconexas entre ellas, lo cual nos hace prescindir de muchas de las ventajas de la orientación a objetos como la herencia, la sobrecarga de operadores o el polimorfismo.
También es verdad que es posible implementar un programa orientado a objetos en lenguaje C puro[1]. No obstante esto hubiera aumentado la complejidad del programa, hubiera consumido más memoria y además hubiera hecho todo un poco más difícil de entender.
Obliga a los "backend" a usar C++. Si se hubiese programado pDI-Tools con C++ esto obligaría a migrar todo el software existente para DITools a C++. Además nos obligaría a usar C++ en futuros proyectos. En cambio, al estar escrito en C, pDI-Tools puede usarse tanto con "backend" escritos en C como en C++ o cualquier otro lenguaje ya que nos libramos de toda la complejidad de interacción con C++.
Diferencias entre implementaciones de C++. Las diferencias entre compiladores de C++ son mucho más acentuada que las que hay entre compiladores de C. A veces incluso encontramos serias diferencias entre versiones distintas de un mismo compilador para diferentes plataformas. Usar C++ hubiera ligado excesivamente pDI-Tools a un determinado compilador. Usando el lenguaje ANSI-C garantizamos una mayor portabilidad (tanto entre compiladores como entre plataformas) y a la vez evitamos desagradables sorpresas debidas a dichas diferencias.
C++ no siempre está disponible. En entornos de desarrollo antiguos, o incluso algunos entornos de desarrollo modernos, no disponemos de C++. Por ejemplo el lenguaje C++ en un entorno GNU/Linux™ con GNU C Compiler, a pesar de ser algo instalado normalmente, no deja de ser opcional.
Usando sólo C ANSI garantizamos que la aplicación se podrá compilar en casi cualquier sistema.
También se usan las aplicaciones Autoconf y Automake para gestionar la compilación del software. La herramienta Autoconf permite despreocuparse de las peculiaridades del compilador, de las librerías y de la arquitectura ya que es él quien se encarga de detectarlas y, en muchas ocasiones, ocultarlas. Automake automatiza la creación de los ficheros makefile, generándolos específicos para la versión de make instalada.
El motivo de escoger el par de aplicaciones Autoconf y Automake en lugar de otro equivalente como podría ser Imake es debido a su gran flexibilidad. Autoconf frecuentemente ha funcionado en sistemas en los que nunca se había ejecutado antes, lo cual es directamente imposible en un sistema de base de datos como Imake. Esto hace que este par funcione sobre una gran variedad de sistemas POSIX. Además, una vez construido el "script" configure.in, sólo es necesario disponer de una "shell" sh(1), un compilador de C y la herramienta make para poder compilar el programa. Imake, por el contrario, necesita estar instalado para poder realizar la compilación. Por último es mucho más difícil extender las funcionalidades de Imake debido a su naturaleza basada en una gigantesca base de datos: no es posible distribuir sólo los cambios y usarlos sin instalarlos en la base de datos de Imake.
Todas las funciones y símbolos públicos de pDI-Tools van precedidos del prefijo _pdi_. Esto reduce la posibilidad de que haya una colisión de nombres con los del programa a instrumentar.
En determinadas ocasiones se añade un infijo para agrupar determinadas funciones de tal forma que nos permita, sólo mediante el nombre, deducir a que parte del programa pertenece. Así tenemos el prefijo _pdi_bcfg_ para las funciones encargadas de manejar los ficheros de comandos, el prefijo _pdi_linux_ para todo los símbolos específicos para GNU/Linux™ y así sucesivamente.
Los prefijos registrados actualmente son los siguientes:
Tabla 2-1. Prefijos de los símbolos de pDI-Tools
Prefijo | Descripción |
---|---|
_pdi_ | Prefijo genérico. Todos los símbolos públicos de pDI-Tools deben estar precedidos por este prefijo. |
_pdi_becfg_ | Este prefijo agrupa todas las funciones relacionadas directamente con la gestión de los ficheros de comandos. |
_pdi_ebe_ | El infijo "ebe" se traduce como "Elf Backend". Todas las funciones con este infijo acceden directa o indirectamente a las estructuras de información ELF tanto para consultas como para realizar modificaciones. No obstante su implementación es muy genérica o totalmente desligada de la arquitectura subyacente, así que se trata de un código altamente portable. Estas funciones logran esta independencia mediante una capa de abstracción específica para la plataforma. |
_pdi_arch_ |
Todas las funciones precedidas por este prefijo son totalmente
dependientes de la arquitectura y deben ser implementadas por
el código específico para la plataforma. En realidad nunca
encontraremos símbolos exportados con este prefijo. Este
prefijo en realidad es una macro que se traducirá al prefijo
específico para la plataforma: así si llamamos a la función
_pdi_arch_init en realidad estamos
llamando a _pdi_linux_init en GNU/Linux™, o a
_pdi_irix_init en Irix, etc.
|
_pdi_safe_ |
Este grupo de símbolos son en realidad punteros a funciones.
Estos punteros apuntan a funciones de la librería de C. Así por
ejemplo _pdi_safe_fputc es un puntero a la
función fputc . Estos punteros nos permiten
llamar a funciones de C sin vernos afectados por
instrumentaciones agresivas.
|
_pdi_irix_ | Funciones y símbolos específicos para la implementación de pDI-Tools para Irix. |
_pdi_linux_ | Funciones y símbolos específicos para la implementación de pDI-Tools para GNU/Linux™. |
_pdi_solaris_ | Funciones y símbolos específicos para la implementación de pDI-Tools para Solaris. |
La convención usada para los tipos, constantes y variables es la siguiente:
Las variables y funciones globales se distinguen mediante el prefijo _pdi_. La variable debe estar escrita toda en minúsculas, a no ser que este formada por varias palabras. En ese caso la primera letra de cada palabra, a excepción de la primera, estará en mayúsculas. Por ejemplo:
PDI_ELF_OBJ *_pdi_objList; int _pdi_init(void); int _pdi_initSafeFuncs(void);
Las variables locales y funciones locales no deben ir nunca precedidas del prefijo _pdi_. No obstante su identificador debe comenzar siempre por minúsculas, y si es un identificador formado por múltiples palabras la primera letra de cada palabra estará en mayúsculas a excepción de la primera, o se separarán las palabras mediante un signo de subrayado. Por ejemplo:
int myLocalVar; int my_var; int refreshObjects(void);
Las constantes del preprocesador y los tipos de datos siempre están en mayúsculas. Además siempre llevan el prefijo PDI_. Si además la constante o tipo está ligado a la arquitectura se le añade un infijo que la identifique como tal. Así las constantes y tipos específicos de GNU/Linux™ llevan el prefijo PDI_LINUX_, mientras que los de Irix se identifican por el prefijo PDI_IRIX_. El uso del signo de subrayado para aumentar la legibilidad del identificador es arbitrario, es decir, no hay una regla fija.
Nunca se nombra directamente los tipos de datos ligados a la plataforma (por ejemplo los declarados por el fichero elf.h). En su lugar se accede a ellos mediante macros adecuadas. Por ejemplo, si se usa tal cual el tipo Elf32_Addr se comete un error ya que el código queda totalmente ligado a plataformas de 32 bits. La forma correcta de referirse a estos tipos es mediante la macro ElfW(X), donde X es el tipo que queremos usar. En el ejemplo anterior usaríamos el tipo ElfW(Addr). De esta forma es el programa configure.in quien decide si el tipo debe traducirse a la versión de 32 o 64 bits.
Se evitan en la medida de lo posible las construcciones #if ... #else ... #endif complejas, ya que estas construcciones, sin ningún tipo de control, llevan a códigos ilegibles y muy propensos a errores.
pDI-Tools se distribuye en un fichero .tar.gz con un nombre de la forma pditools-VERSIÓN.tar.gz donde VERSIÓN indica la versión que contiene de pDI-Tools.
La estructura de directorios en la distribución de pDI-Tools obedece a las indicaciones hechas por GNU. En el directorio principal se encuentra el "script" de configuración configure.in y ficheros de texto describiendo el contenido del paquete. También encontramos los siguientes subdirectorios:
Directorios contenidos en la distribución de pDI-Tools
En este directorio se encuentran todos los "script" y ficheros fuente XML DocBook necesarios para construir la documentación. Además se incluye la documentación ya generada en diversos formatos (entre otros HTML, PDF y PostScript®).
Aquí se encuentran los ficheros de configuración independientes de la plataforma.
Separamos en este directorio todos los ficheros de cabecera necesarios para poder programar con pDI-Tools. Son útiles para la programación de objetos "backend" o aplicaciones que interaccionen con pDI-Tools.
Dentro de este directorio encontramos más subdirectorios donde cada uno contiene los ficheros de cabecera específicos para una plataforma. Así por ejemplo encontramos los directorios ./include/irix/, ./include/linux/ y ./include/solaris/.
Dentro de este directorio encontramos otros subdirectorios con el código fuente necesario para construir pDI-Tools, sus utilidades y programas de "test". Y es en el directorio ./src/link/ donde está el código fuente que termina generando el núcleo de pDI-Tools: libpdi.so.
Dentro del directorio ./src/link/, al igual que pasaba en el directorio ./include/, encontramos varios subdirectorios donde cada uno contiene el código específico para una determinada plataforma.
[1] | Es posible con algunos "hacks" basados en declarar punteros de funciones en las estructuras, el uso de union y algunas macros |