Como ya se ha dicho en la introducción, un "backend" es una librería
compartida y como tal nunca será un programa completo y carecerá de una
función main()
. El código de un "backend" en su
mayor parte está orientado a tomar el control y gestionar cada llamada
que pDI-Tools intercepte.
Un "backend" se suele dividir en las siguientes partes:
Inclusión del fichero backend.h. Todo "backend" debería incluir el fichero backend.h al principio. Este fichero define constantes, macros, símbolos y nombres de funciones especialmente útiles a la hora de escribir un "backend".
Código de inicialización y finalización del "backend". Consiste en dos funciones, totalmente opcionales. La primera, la de inicialización, se ejecuta antes del código a instrumentar y antes de que se instalen sus propios reenlaces. La segunda se ejecuta una vez finalizado el programa a instrumentar, y una vez deshechos todos los reenlaces. El código de inicialización generalmente son operaciones de reserva de memoria, inicialización de estructuras, etc. Mientras que el de finalización realiza las operaciones de terminación del "backend".
Funciones de interceptación o funciones "wrapper". Las interposiciones de código más sencillas, que son los reenlaces y redefiniciones, consisten en redirigir la ejecución hacia una función de un "backend" cuando interceptan una llamada a un determinado DSO. Estas funciones reciben el nombre de "wrapper".
Gestión de los "callback". Es un tipo de interposición de código especial, no siempre disponible en todas las arquitecturas debido a su complejidad, que sirve para interponer el mismo sobre todas las funciones de un DSO.
Código de gestión interna del "backend". Un "backend", a pesar de no ser un programa tradicional, puede llegar a ser bastante complejo. Exceptuando los "backend" de ejemplo, en la mayoría de ocasiones un "backend" llega a ser tan complejo como cualquier otra aplicación, necesitando usar estructuras de datos, funciones, variables globales... y no sólo las funciones "wrapper" y/o funciones de gestión de los "callback".
La principal función de este fichero es definir los prototipos de funciones que usa pDI-Tools para interaccionar con el "backend". De esta forma hacemos que el compilador compruebe por nosotros que la signatura de las funciones que implementamos sea correcta.
No obstante no se recomienda usar las funciones directamente, sino
mediante un alias que les asignamos en el mismo fichero backend.h.
Los alias son simples constantes del preprocesador con el prefijo
PDI_BE_FUNC_. Cada una de ellas se sustituye por el
verdadero nombre de la función en tiempo de compilación. Por ejemplo la
constante PDI_BE_FUNC_INIT
se traducirá en el
nombre de función di_init_backend()
.
Los motivos de recomendar usar estos alias en lugar del verdadero nombre de la función son varios.
El primer motivo viene de intentar mantener la nomenclatura usada en
todo el programa intentando romper lo menos posible la compatibilidad
hacia atrás con DITools. DITools espera encontrar funciones con
nombres como di_init_backend()
, di_callback_required()
o
di_post_event_callback()
. Si se observan detenidamente se verá que no
siguen las convenciones decididas para los nombres de funciones de
pDI-Tools (ver las convenciones en Sección 2.2.2).
Ocultando los nombres de estas funciones al programador mediante alias
que siguen las convenciones usadas se simplifica la labor del
programador sin sacrificar la compatibilidad directa con antiguos
"backends" de DITools.
Otro motivo es que si se diese una situación donde es necesario usar otros nombres de función para estas funciones, cambiarlos sería tan fácil como editar el fichero backend.h y recompilar pDI-Tools.
Un último motivo es que con el tiempo se dejarán de usar estos nombres de función por los motivos antes expuestos. Si los "backend" usan los alias en lugar de los nombres de función la transición será transparente.
Por último el fichero backend.h añade otro juego de constantes con el prefijo PDI_STR_BE_FUNC_. Cada constante es un string con el nombre de la función contenido en la constante con prefijo PDI_BE_FUNC_. Estas constantes son usadas por pDI-Tools para realizar búsquedas e imprimir errores referentes a estas funciones. Se ponen también a disposición del usuario para que haga con ellas el uso que más le convenga.
Un inventario de las funciones y sus constantes se puede ver en la Tabla 7-1. La primera columna es el nombre de función por defecto, la segunda es la constante que contiene el verdadero nombre de la función, y la tercera es otra constante calculada a partir de la anterior con un "string" con el nombre de función.
Tabla 7-1. Funciones y constantes en backend.h
función | alias | string |
---|---|---|
di_init_backend() | PDI_BE_FUNC_INIT | PDI_STR_BE_FUNC_INIT |
di_fini_backend() | PDI_BE_FUNC_FINI | PDI_STR_BE_FUNC_FINI |
di_callback_required() | PDI_BE_FUNC_CB_REQ | PDI_STR_BE_FUNC_CB_REQ |
di_pre_event_callback() | PDI_BE_FUNC_PRE_CB | PDI_STR_BE_FUNC_PRE_CB |
di_post_event_callback() | PDI_BE_FUNC_POST_CB | PDI_STR_BE_FUNC_POST_CB |
Como hemos dicho sólo empezar se incluyen sus prototipos de función. Así el compilador puede comprobar que la implementación del "backend" coincide con los prototipos de funciones que pDI-Tools espera encontrar. Estos prototipos de funciones se pueden ver en la Tabla 7-2.
Por último notar que al incluir el fichero backend.h no se incluye automáticamente ningún otro fichero de cabecera que importe otros símbolos de pDI-Tools. Si se desea usar alguna función de la API que ofrece pDI-Tools deberá incluirse explícitamente el fichero de cabecera adecuado.
El código de inicialización y finalización de un "backend" es algo totalmente opcional. Además la existencia del código de inicialización no fuerza a que exista un código de finalización o viceversa.
El código de inicialización se debe encapsular dentro de una función
exportada públicamente (es decir, visible desde el exterior del módulo)
con un determinado nombre. Por defecto recibe el nombre
di_init_backend()
, aunque se debería referir siempre a ella a
través de la constante PDI_BE_FUNC_INIT
(ver los motivos más
arriba).
La inicialización del "backend" sucede después de la carga de los ficheros de configuración, pero antes de aplicar los ficheros de comandos. Evidentemente esto sucede antes de comenzar la ejecución del programa a instrumentar.
Los "backend" se inicializan según el orden de aparición en los ficheros de comandos, y en caso de haber varios ficheros de comandos, según el orden de aparición y dependencias establecidas entre ellos. Para saber más sobre el orden de carga e inicialización vea la Sección 7.1.2.
El código de finalización, al igual que el de inicialización, debe
estar en una función de un determinado nombre: di_fini_backend()
,
aunque por los mismos motivos el programador debe referirse a ella
mediante la constante PDI_BE_FUNC_FINI
.
El código de finalización de los "backend" sólo se ejecuta una vez el programa ha finalizado y después de que pDI-Tools deshaga todas las interposiciones.
Por último comentar que es posible aprovechar el segmento de código de inicialización y finalización del estándar ELF (.init y .fini) para ejecutar un código de inicialización justo cuando pDI-Tools ha cargado el "backend" en memoria, o el de finalización cuando este "backend" sea descargado. No obstante esto nunca se ha probado y el resultado no es fácilmente predecible. Aunque en principio debería funcionar, se desaconseja seriamente.
Conceptualmente, el modo más sencillo de interceptar una llamada a una función sería poniendo en el lugar de la función original la función interceptadora. Esta estrategia es la que siguen aproximadamente los reenlaces y las redefiniciones: llamar a una función del "backend", llamada "wrapper", en lugar de la original. Cuando termina la ejecución de dicha función se devuelve el control al programa instrumentado.
Debido a los motivos comentados en el párrafo anterior, los "wrapper" deben tener el mismo aspecto (signatura) que la función a la que interceptan.
Los "wrapper" permiten, de una forma sencilla, alterar o registrar el resultado de una función de una librería. Generalmente suelen ser funciones que, a parte de sus tareas específicas, durante su ejecución suelen transferir el control a la función que están interceptando.
Por último remarcar que los "wrapper" deben ser visibles desde fuera del "backend", para poder ser referenciados por pDI-Tools.
Un ejemplo de código de un "backend" con funciones "wrapper":
/* backend de ejemplo: 'myfirstbe.so' */ #include<stdio.h> #include<unistd.h> #include<backend.h> #define TRUE (1==1) static int read_no; static int write_no; ssize_t read_wrapper(int fd, void *buf, size_t nbyte) { ++read_no; fprintf(stderr, "read()\n"); return read(fd, buf, nbyte); } ssize_t write_wrapper(int fd, const void *buf, size_t nbyte) { ++write_no; fprintf(stderr, "write()\n"); return write(fd, buf, nbyte); } int di_init_backend(void) { printf("Inicializando backend.\n"); read_no = write_no = 0; return TRUE; } void di_fini_backend(void) { printf("Cerrando el backend.\n" " number of reads = %d\n" " number of writes = %d\n", read_no, write_no); }
Y este "backend" se usaría ejecutando los siguientes comandos, por ejemplo:
; Fichero: 'commands.cfg' ; Definimos los alias a las librerías y los backends que usamos #backend BACKEND ./myfirstbe.so ; Comandos de reenlace # commands R MAIN read BACKEND read_wrapper R MAIN write BACKEND write_wrapper
Por último se puede usar este fichero de comandos tan sólo asignando el
nombre del fichero de comandos (commands.cfg) a la
variable de entorno DI_CONFIG_FILE
o mediante un fichero de
comandos como el siguiente:
# Fichero de configuración 'pdi.cfg' config = commands.cfg
El resultado de la ejecución sería un mensaje por cada read(2) y write(2) del programa principal y, al finalizar la aplicación, un resumen con el total de las veces que han sido ejecutadas cada una de ellas.
Si un "backend" desea interceptar llamadas mediante el mecanismo de "callback" debe implementar las siguientes funciones (públicamente):
Una función con un nombre determinado (definido por la constante
PDI_BE_FUNC_CB_REQ
) que al ejecutarse decida si la llamada
interceptada desea procesarse o no.
Dos funciones encargadas (ver constantes PDI_BE_FUNC_PRE_CB
y
PDI_BE_FUNC_POST_CB
) que se ejecutan sólo cuando se intercepta
una llamada y la anterior indica que debe procesarse. Una que se
ejecuta antes que se ejecute el código interceptado, y otra que se
ejecuta después. Se puede implementar una de las dos o las dos a la
vez: son independientes y opcionales.
Debe tenerse en cuenta que estas funciones han de ser visibles desde fuera del "backend", para poder ser referenciadas por pDI-Tools.
Terminamos esta descripción de los "callback" con un ejemplo de "backend" que los usa:
/* backend de ejemplo: 'callbacks.so' */ #include<stdio.h> #include<unistd.h> #include<backend.h> #define TRUE (1==1) /* Lista de funciones que procesará el callback */ char *funcs[] = { "fputc", "printf", "fprintf", "puts", "tell", NULL }; int PDI_BE_FUNC_INIT(void) { printf("Inicializando el backend.\n"); return TRUE; } void PDI_BE_FUNC_FINI(void) { printf("Cerrando el backend.\n"); } int PDI_BE_FUNC_CB_REQ(char *funcname) { int i; printf("Identificado '%s'\n", funcname); for(i = 0; funcs[i]; i++) if(!strcmp(funcname, funcs[i])) return i+1; return 0; } void PDI_BE_FUNC_PRE_CB(int virtual_processor, int event_id, ...) { printf("Se va a ejecutar una llamada a '%s'\n", funcs[event_id-1]); } void PDI_BE_FUNC_POST_CB(int virtual_processor, int event_id, int retval) { printf("La llamada a '%s' a devuelto %d.\n", funcs[event_id-1], retval); }
Y este "backend" se usaría ejecutando los siguientes comandos:
; Definimos los alias a las librerías y los backends que usamos #backend BACKEND ./callbacks.so ; Comandos de reenlace # commands C MAIN * BACKEND
De nuevo usaríamos este fichero de comandos con la variable de entorno
DI_CONFIG_FILE
o mediante un fichero de configuración.
Si el programa principal consiste en:
/* Programa de ejemplo */ #include<stdio.h> #include<stdlib.h> int main(int argc, char **argv) { fputc('+', stdout); if(fputc('*', stdout) != (int) '*') printf("\nLa función 'fputc()' funcional mal!\n"); else printf("\nLa función 'fputc()' parece funcionar bien\n"); }
El resultado de la instrumentación sería:
gerardo@kobetai:~/pdi/src/link$ ./runtest Inicializando el backend. Identificado '__libc_start_main'Identificado 'fputc'
Se va a ejecutar una llamada a 'fputc' +La llamada a 'fputc' a devuelto 43. Identificado 'fputc'
Se va a ejecutar una llamada a 'fputc' *La llamada a 'fputc' a devuelto 42. Identificado 'puts'
Se va a ejecutar una llamada a 'puts' La función 'fputc()' parece funcionar bien La llamada a 'puts' a devuelto 44. Identificado 'exit'
Cerrando el backend. gerardo@kobetai:~/pdi/src/link$
main()
no llama explícitamente a
exit(3), el código de finalización de la aplicación se encarga
de hacerlo por él implícitamente. Este es el motivo de que se
haya detectado esta llamada.
Se recomienda que un "backend" sólo exporte los símbolos necesarios y así evitar posibles colisiones con símbolos públicos de la aplicación instrumentada. Siempre es una buena política declarar como static cualquier símbolo global que no sea necesario ver desde fuera del "backend". Esta palabra reservada de C, usada en el ámbito global, impide que un símbolo pueda ser visto desde fuera del fichero objeto al que pertenece.