7.3. Estructura de un backend

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:

7.3.1. El fichero backend.h

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ónaliasstring
di_init_backend()PDI_BE_FUNC_INITPDI_STR_BE_FUNC_INIT
di_fini_backend()PDI_BE_FUNC_FINIPDI_STR_BE_FUNC_FINI
di_callback_required()PDI_BE_FUNC_CB_REQPDI_STR_BE_FUNC_CB_REQ
di_pre_event_callback()PDI_BE_FUNC_PRE_CBPDI_STR_BE_FUNC_PRE_CB
di_post_event_callback()PDI_BE_FUNC_POST_CBPDI_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.

Tabla 7-2. Prototipos de funciones en backend.h

prototipo de función

int PDI_BE_FUNC_INIT(void);

int PDI_BE_FUNC_FINI(void);

int PDI_BE_FUNC_CB_REQ(char *func_name);

void PDI_BE_FUNC_PRE_CB(int virtual_processor, int event_id, ...);

void PDI_BE_FUNC_POST_CB(int virtual_processor, int event_id, int retval);

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.

7.3.2. Inicialización y finalización

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.

7.3.3. Wrappers

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.

7.3.4. Callbacks

Si un "backend" desea interceptar llamadas mediante el mecanismo de "callback" debe implementar las siguientes funciones (públicamente):

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'                          (1)
Identificado 'fputc'                                      (2)
Se va a ejecutar una llamada a 'fputc'
+La llamada a 'fputc' a devuelto 43.
Identificado 'fputc'                                      (3)
Se va a ejecutar una llamada a 'fputc'
*La llamada a 'fputc' a devuelto 42.
Identificado 'puts'                                       (4)
Se va a ejecutar una llamada a 'puts'

La función 'fputc()' parece funcionar bien
La llamada a 'puts' a devuelto 44.
Identificado 'exit'                                       (5)
Cerrando el backend.
gerardo@kobetai:~/pdi/src/link$
        
(1)
La primera sorpresa que nos encontramos es una llamada a una función interna de C. Esta función es la encargada de inicializar las librerías de C libc.so y preparar el entorno de ejecución del programa y llamarlo. Esta función nunca vuelve, es decir, que si la instrumentamos con un "callback" sólo se ejecuta el pre-tratamiento y nunca el post-tratamiento. No vuelve debido a que al acabar ejecuta la llamada al sistema _exit(2).
(2)
En este punto se ha ejecutado la función fputc('+');
(3)
En este punto se ha ejecutado la función fputc('*');
(4)
Otra sorpresa que nos llevamos es que en este punto no se ejecuta la función printf(2), sino un puts(3). El motivo de esto es que el optimizador del compilador ha decidido que para este caso es más eficiente llamar a esta función. Este tema es muy delicado ya que no depende ya ni de pDI-Tools ni del sistema operativo, sino totalmente de las decisiones del compilador.
(5)
Si 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.

7.3.5. Código de gestión interna del "backend"

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.