pDI-Tools incorpora tres mecanismos distintos de interposición de código. Los dos principales son los más sencillos de usar y están disponibles en todas las plataformas: los reenlaces y las redefiniciones. El otro mecanismo son los "callback" que permiten instrumentar una función de una forma totalmente invisible (ver motivos en la sección callback. En contrapartida es el mecanismo menos portable de los tres y en ocasiones es bastante complejo de usar.
En las secciones siguientes se comenta y explica como se implementa y funciona cada uno de ellos, dando también ejemplos de su uso.
Un reenlace o "relink" es la interposición de código más simple que se puede realizar, y a la vez la más fácil de controlar y que menos problemas causa.
Para especificar un reenlace se necesitan los siguientes parámetros:
DSO objeto. Es el objeto sobre el que se instala el "relink". Generalmente se le llama "target object".
Función objeto. Se interceptarán las llamadas desde el objeto "target object" a esta función. Suele recibir el nombre de "target function".
El "backend" que gestionará el reenlace.
La función de dicho "backend" que tomará el control.
Un reenlace consiste en sustituir la dirección la función "target function" en el GOT del objeto "target object", por la dirección del "wrapper" del "backend". De esta forma cada vez que el "target object" haga una llamada a la "target function", se transferirá el control al "backend". Evidentemente ningún otro DSO se verá afectado por esta operación ya que sólo se modifica la entrada del GOT que afecta al "target object".
En este esquema vemos como se instala un reenlace en la aplicación, exactamente sobre el programa principal, sin afectar al resto de objetos en memoria.
Las redefiniciones se usan y configuran de un modo muy similar a los reenlaces. Sin embargo su semántica y efectos son muy distintos.
La idea de un reenlace es introducir una única interposición de código. En cambio una redefinición sustituye una función por otra.
Para especificar un reenlace se necesitan los siguientes parámetros:
DSO objeto. El objeto que contiene la implementación de la función que queremos redefinir. Este objeto recibe el nombre de "target object".
Función a redefinir. Nombre de la función que se redefine. Recibe el nombre de "target function".
El "backend" que contiene la nueva implementación de la función a redefinir.
El nombre de la función del "backend" que usaremos.
La redefinición consiste en modificar en la tabla de símbolos de "target object" la entrada de la función a redefinir. En dicha entrada indicamos la dirección de nuestra implementación de la función. A partir de ahora, cada vez que un objeto use el símbolo redefinido en realidad se dará el control a la nueva definición.
En este esquema vemos como se sustituye la función read(2) de libc.so por nuestra versión de la función. Como se puede ver, esta sustitución afecta a todos los DSO de la aplicación.
![]() |
Si el parámetro de configuración |
El "callback", al contrario que los reenlaces y las redefiniciones, desde el punto de vista de un DSO no actúa sobre una única función sino sobre todas. Un "callback" permite instrumentar todas las llamadas que realiza un DSO a otros DSO.
Con los reenlaces o las redefiniciones, durante la ejecución del
programa instrumentado, sólo se ejecuta código de los "backend" ya que
pDI-Tools se ha encargado de redirigir el control hacia el código de los
"backend". En el caso del "callback" interviene tanto código del
"backend" como de pDI-Tools y ambos cooperan para realizar la
interposición. En estas interposiciones quien recibe primero el control
es la rutina "callback handler" (también llamada en ocasiones
"generic wrapper"). Éste es un código que forma parte de pDI-Tools y se
encarga de gestionar la intercepción. Este código exige al "backend"
tener implementadas dos o tres funciones. La primera de todas,
PDI_BE_FUNC_CB_REQ
, debe decidir si la intercepción que ha
realizado pDI-Tools interesa o no. Y las otras dos,
PDI_BE_FUNC_PRE_CB
y PDI_BE_FUNC_POST_CB
, aportan el código
a interponer antes y después de la llamada.
En el siguiente gráfico mostramos como cooperan entre si pDI-Tools y el "backend" para ejecutar la interposición de código.
En este diagrama se observa como al llamar a la función printf(2) desde MAIN en realidad se transfiere el control al "generic wrapper". Éste se encarga de transferir el control a las funciones de control de "callback" del "backend" y al verdadero código de printf(2).
El "callback handler" es una función, también llamada "generic wrapper",
encargada de llamar a la función interceptada y a las funciones del
"backend" encargadas de gestionar el "callback": PDI_BE_FUNC_CB_REQ
,
PDI_BE_FUNC_PRE_CB
y PDI_BE_FUNC_POST_CB
.
La función "callback handler" está escrita en ensamblador y es muy compleja y específica para la arquitectura. Es la encargada de realizar una interposición de tal forma que no haga falta copiar los parámetros de nuevo para poder ejecutar el código de la función interceptada, especialmente porque no se conocen. Esto nos obliga a salvar el estado de la pila y los registros. Salvando esta información es posible ejecutar el código pre-interpuesto, luego restaurar el estado inicial, y acto seguido saltar a la función interceptada. Al restaurar el estado exacto la función podrá recoger todos sus parámetros (sean por pila o registros) sin verse afectada por nuestras operaciones.
Al terminar la ejecución de la función el "callback handler" retomará el control, salvará el estado de los registros y la pila, ejecutará el código post-interpuesto, finalmente restaurará el estado final y retomará la ejecución donde tendría que haber vuelto la función.
Observemos que salvamos el estado de la aplicación antes de ejecutar el
código interpuesto por las funciones PDI_BE_FUNC_PRE_CB
y
PDI_BE_FUNC_PRE_CB
. Esto implica que estas funciones no pueden
modificar los parámetros de entrada ni el valor retornado por la
función.
Por último queda explicar la relación entre el mecanismo de "callback" y algunos parámetros de configuración que seguidamente enumeramos.
Antes de usar el mecanismo de "callback" debemos tener claro cuantos
"threads" se usarán. Por cada "thread" el mecanismo de "callback"
necesita una pila y estructuras de información sobre el "thread".
Debido a ello debemos inicializar los parámetros max_threads
y num_threads
. El primero indica cuantos "threads" podemos
instrumentar como mucho, mientras que el segundo indica cuantos
"threads" esperamos usar.
La idea es que el parámetro max_threads
se prepara para manejar
un número máximo de "threads", mientras que el segundo,
num_threads
decide cuantos "threads" pre-inicializamos antes de
comenzar la instrumentación. Si no se tiene claro cuantos "threads" se
van a usar se aconseja un número alto en max_threads
y 0 en
num_threads
.
Cada "thread" (una vez inicializado) tendrá asociado una pila donde
guardará información de estado. El tamaño de esta pila viene
determinado por el parámetro cb_stack_size
. Por defecto se
inicializa con un tamaño de 1024 bytes, que es más que suficiente para
la mayoría de los casos. Sólo debería aumentarse si se realizan
instrumentaciones que pueden desembocar en varios niveles de
profundidad.
Un parámetro muy importante es cb_max_stubs
. Este parámetro
indica la cantidad de "stubs" para los que se reservará memoria. Por
cada función de un DSO que se instrumenta se consume un "stub". Así
si instrumentamos dos librerías, una con 100 funciones y la otra con
200, este parámetro debe valer como mínimo 300.
Por último tenemos el parámetro cb_allow_handler
que permite
usar, si lo deseamos, nuestro propio "generic wrapper" en las
interposiciones mediante "callback". Evidentemente esta opción especial
para usuarios muy avanzados.
![]() | Si se instala un "callback" sobre un DSO luego ya no se puede instalar ningún otro tipo de interposición sobre cualquier función del mismo DSO ya que ambas entrarían en conflicto. |