La función final de pDI-Tools es interceptar llamadas a funciones entre objetos en memoria (por ejemplo el ejecutable y las librerías que usa) y redireccionarlas hacia el código de instrumentación. El código de instrumentación está siempre contenido en unos objetos llamados "backends".
Un "backend" es un objeto compartido dinámicamente, o también llamado DSO ("Dynamic Shared Object") o DLL ("Dynamic Link Library"). Estos objetos de por si no indican que código y como se ha de interceptar. Estos ficheros tan sólo contienen el código responsable de manejar las intercepciones y que será de hecho el código que se interpone.
Este capítulo mostramos la sintaxis y funcionamiento de los ficheros de comandos y como se resuelven las dependencias entre "backends" que aparecen en ellos. Seguidamente se describe como gestionar los distintos tipos de interposiciones y se finaliza explicando como se estructura un "backend" generalmente.
Los ficheros de comandos indican como deben realizarse las interposiciones de código y sobre que DSO deben realizarse. Son ficheros de texto que contienen básicamente reglas en las que explican los "backend" que intervienen y como intervienen sobre los DSO.
Un fichero de comandos se divide en dos partes:
Lista de objetos y "backend". Al principio de un fichero de comandos se puede indicar todos los DSO (y "backend") que intervendrán en los comandos de la siguiente sección.
Declarándolos previamente se obtiene una serie de ventajas: de entrada, concentrando dicha lista al principio del fichero se puede saber a primera vista que DSO intervienen en un fichero de comandos.
Otra gran ventaja es que se debe indicar si el fichero es un DSO normal o un "backend". De esta forma pDI-Tools puede detectar reglas equivocadas debido a descuidos o distracciones.
Por último, se permite asociar un alias a cada objeto (ya sea un DSO normal o un "backend") y así hacer luego los comandos de interposición de código más legibles y más fáciles de modificar. Por ejemplo, en GNU/Linux™ con GNU C Library 2.5.x la librería matemática suele estar en /lib/libm.so.6, mientras que en las últimas versiones está en /lib/tls/libm.so.6. Trabajando con un alias modificar todas las reglas que afectan a esta librería es tan sencillo como alterar la línea donde se define.
Hay tres alias predefinidos: LIBC, MAIN y PDI. El
primero se asigna a la librería de C (por ejemplo a
/lib/libc.so si fuese el
caso), la siguiente se asigna al DSO del programa en ejecución y
el último a la librería que implementa pDI-Tools. Estos alias se
corresponden con las constantes PDI_ALIAS_LIBC
,
PDI_ALIAS_MAIN
y PDI_ALIAS_PDI
definidas en el
fichero de cabecera ebeif.h de pDI-Tools.
Comandos. En esta sección simplemente se encuentran, uno tras otro, los comandos de interposición de código. Se pueden usar en ellos los alias creados en la sección anterior.
La sintaxis de un fichero de comandos es muy sencilla. El elemento más simple de un fichero de comandos es el comentario: cualquier línea que comienza por un punto y coma (;) o blancos seguidos de un punto y coma (;) es ignorada hasta el próximo salto de línea.
Como ya hemos dicho, un fichero de comandos comienza por la lista de objetos y "backend". En esta sección, cada línea no vacía ni con un comentario, representa un DSO normal o un "backend". La sintaxis es la siguiente:
[#backend | #object | #define] {path_dso} [alias]
Obsérvese que la línea puede comenzar por #backend, #define, #object o directamente con el path a un DSO. El significado de cada opción es el siguiente:
#backend. Se va a listar un "backend".
#object. Indica que se va a listar, y seguramente asignarle un alias a, un DSO que no es un "backend" y que vamos a instrumentar. Se mantiene por compatibilidad con el antiguo DITools la directiva #define.
Si una la línea comienza directamente por la ruta de acceso al DSO se asume que es un DSO normal que no es un "backend". Esta sintaxis se mantiene por compatibilidad con el antiguo DITools.
La ruta al DSO puede escribirse directamente o encerrarla entre dobles comillas si fuese el caso de que ésta contuviese espacios.
Después de la ruta al DSO podemos definir, opcionalmente, un alias al objeto o "backend". Este no puede contener espacios.
Esta sección se acaba, dando comienzo a la sección de comandos mediante las marcas #commands o #relinks.
En esta sección cada línea no vacía ni comentario representa un comando que explica como debe instalarse una interposición de código. La sintaxis de cada línea varía según el tipo de interposición. En las siguientes secciones comentamos la sintaxis de cada una de las interposiciones.
La sintaxis de un comando de reenlace es la siguiente:
{F | R} {trg_obj} [ {trg_func} {backend} {be_func} ]
El primer parámetro es una letra y puede ser F o R. No hay diferencia entre usar una u otra ya que ambas indican que este comando instalará un reenlace. Se mantienen dos opciones por compatibilidad con DITools.
El siguiente par de parámetros (trg_obj
y
trg_func
) indican que llamadas a una función,
desde un determinado objeto, se deben interceptar.
El último par de parámetros (backend
y
be_func
) indican la función del "backend"
que recibirá el control cuando se ejecute la llamada a la función
el control.
Es posible asignarle un comodín al parámetro
trg_obj
. Asignándole a dicho parámetro un
asterisco se instalará el reenlace sobre todos los DSO
instrumentables (ver los parámetros de configuración de pDI-Tools:
donttouch_backends
y donttouch_pdi
).
La sintaxis de un comando de redefinición es la siguiente:
D {trg_obj} {trg_func} {backend} {be_func}
La primera letra indica que se va a instalar una redefinición.
El siguiente par de parámetros (trg_obj
y
trg_func
) indican la función que redefinimos
en el objeto DSO. Evidentemente las redefiniciones no admiten ni
pueden admitir comodines en ninguno de los dos parámetros.
El último par de parámetros (backend
y
be_func
) indican la función del "backend"
que recibirá el control cuando se ejecute la llamada a la función
redefinida.
Las dos posibles sintaxis de un comando de "callback" son las siguientes:
{ [C] [F] [R] } {trg_obj} * {backend} [be_func]
En el caso de indicarse en el primer parámetro la letra C se deja claro que la intención es instalar un "callback". En cambio se ofrece, por compatibilidad con el antiguo DITools, la posibilidad de usar las letras F o R. pDI-Tools deducirá del asterisco en el tercer parámetro (función interceptada) que se desea instalar en realidad un "callback".
El siguiente parámetro trg_obj
indica el
objeto del cual interceptaremos todas las llamadas. Este parámetro va
seguido siempre de un asterisco, que indica que se instalará el
"callback" sobre todas las funciones del objeto[1].
El parámetro backend
indica el "backend" que
gestionará el "callback".
También está el parámetro be_func
, que sólo se
puede usar si la opción de configuración cb_allow_handler
está activada. Si se indica este parámetro se usará un
"callback handler" alternativo en lugar del que viene de serie con
pDI-Tools. Generalmente se indica NULL
o ningún valor.
Como se ha visto hasta ahora, con las opciones de configuración
runtime
y config
es posible indicar más de dos
ficheros de comandos. Estos ficheros de comandos establecen un orden de
carga de los "backend" y de aplicación de los comandos que contienen.
Como a veces un determinado "backend" requiere que se inicialize antes
otro determinado "backend" esta lista de ficheros de configuración se
ha de procesar de manera adecuada para respetar las ficheros.
El problema aparece cuando en varios ficheros de comandos se establecen distintos ordenes. pDI-Tools al recoger todos los ordenes de estos ficheros de comandos debe finalmente establecer un orden único de carga en el cual se respeten dichas dependencias. Así si por ejemplo, tenemos la lista de "backend" A.so, B.so y C.so, y el fichero de comandos L1.CFG:
; Fichero L1.CFG ; lista de backends y objetos #backend A.so A #backend B.so B ; .. otros backends y objetos .. #commands ; .. comandos ..
y el fichero de comandos L2.CFG:
; Fichero L2.CFG ; lista de backends y objetos #backend B.so B #backend C.so C ; .. otros backends y objetos .. #commands ; .. comandos ..
pDI-Tools deduce de aquí el siguiente grafo de dependencias:
El "backend" B.so debe inicializarse después de A.so, por lo tanto B depende de A.
Igualmente "backend" C.so debe inicializarse después de B.so, por lo tanto C depende de B.
Esto nos da a entender que los ficheros de comandos como mucho establecen grafos de dependencia lineales que posteriormente debemos combinar en un único grafo del que debemos deducir un orden de carga.
Siguiendo con el ejemplo, si combinamos ambos grafos y expresamos las dependencias como una flecha desde el objeto al objeto del que depende obtenemos el siguiente grafo:
A ← B ← C
De este grafo se puede deducir un único orden de carga de los "backend" en el cual se respetan las dependencias: si recorremos el grafo comenzando por un nodo que no depende de otro nodo obtenemos el orden: A, B, C. No obstante no podemos fiarnos de que siempre el orden sea único ya que no siempre se obtienen grafos lineales. Por ejemplo, supongamos dos ficheros de configuración que generan estos dos grafos:
A ← B
A ← C
al unirlos obtenemos el siguiente grafo:
B → A ← C
del cual surgen dos posibles ordenes, ambos correctos:
A, B, C
A, C, B
Como se puede observar, si dos nodos están a la misma profundidad respeto a un nodo padre de ambos, estos se pueden cargar en distinto orden. Esto también afecta a nodos que no tienen padre. Por ejemplo la unión de:
B ← A
C ← A
genera este grafo de dependencias:
B ← A → C
Los dos posibles ordenes que surgen, son:
B, C, A
C, B, A
La pregunta inmediata: ¿que pasa si surge un bucle? Por ejemplo:
A ← B ← C
B ← C ← A
Generaría un grafo con la siguiente forma:
C ← A ← B ← C
Obsérvese que en este caso no hay ningún nodo que no dependa de otro: no sería posible elegir un primer fichero a cargar y la ejecución de pDI-Tools terminaría ipso-facto. Por regla generalmente es difícil encontrar dependencias circulares entre ellos, aunque no es imposible.
[1] | Se fuerza a esta sintaxis para permitir que en el futuro se pueda implementar la posibilidad de instalar un "callback" sobre un conjunto de funciones y no necesariamente sobre todas. |