En este capítulo discutimos las decisiones tomadas y mecanismos usados para obtener un código altamente portable.
Se comienza por exponer las diferencias y similitudes entre distintas plataformas, tanto por sistema operativo como por arquitectura. Se exponen las plataformas soportadas actualmente: GNU/Linux™ sobre INTEL® 386, PowerPC™ y PowerPC™ 64; Solaris sobre SPARC; Irix sobre MIPS®.
De exponer estas plataformas, escribimos una sección dedicada a ver cuales son las diferencias y similitudes entre ellas. Explicaremos, muy por encima, las funcionalidades básicas que debe ofrecer la parte dependiente de la arquitectura para que pDI-Tools pueda desarrollar sus funciones. Esta capa recibirá el nombre de "Elf Backend" y ofrecerá un API a pDI-Tools que le permitirá desempeñar sus funciones sin preocuparse de que tipo de arquitectura o sistema tiene debajo.
En la siguiente sección se explican las decisiones tomadas para hacer el código portable. Buscamos portabilidad tanto a nivel de código como de herramientas de programación. Un programa fácilmente portable entre diferentes sistemas se consigue creando capas de abstracción, organizando bien el código, procurando no usar muchas estructuras e interfaces específicas de la arquitectura. Pero también se tiene que escribir el programa de tal forma que sea fácilmente compilable con otras herramientas.
De hecho el primer problema que se afronta al portar un programa es la disponibilidad de las herramientas con que se creo. Por ejemplo, a la hora de portar un programa desde Mac OS™ X a Solaris el primer problema es la inexistencia de las herramientas y librerías necesarias para SPARC. Esto significa un trabajo extra para crear el entorno de trabajo, los "scripts" de construcción del programa y encontrar herramientas equivalentes a las usadas en el sistema operativo de Apple Computer, Inc..
En la sección Políticas seguidas para obtener un código portable se discute que decisiones se han tomado para llegar a obtener un código portable tanto a nivel de compilador como de arquitectura.
En la sección Organización del código según la plataforma se discute desde como se organiza el código hasta que funcionalidades se implementan.
Finalmente, en las sección Herramientas explicamos que herramientas se usaron para desarrollar pDI-Tools, y en especial, en que contribuyen a que sea más portable.
pDI-Tools no trabaja directamente con el "hardware", aunque si es dependiente de tres componentes: el formato y suplemento ELF específicos de la arquitectura, la CPU para los fragmentos en ensamblador de pDI-Tools y como organiza la información del "Runtime Linker" el sistema operativo.
Por ello, en las siguientes secciones describiremos las principales características y diferencias de las arquitecturas sobre las que funciona pDI-Tools.
Veremos que hay básicamente tres tipos de mecanismos de enlace dinámico (todos ellos se tratarán con más detenimiento en las siguientes secciones):
Enlaces basados en una tabla GOT ("Global Offset Table"). Esta tabla contiene las direcciones de los símbolos externos usados por el DSO. Este es el caso de MIPS®.
Enlaces basados en una tabla PLT ("Procedure Linkage Table"). Esta tabla contiene "stubs" que ejecutan en enlace. Este es el caso de SPARC y PowerPC™ 32.
Por último existen combinaciones de ambos, como el caso de INTEL® 386.
Se mostrará que el formato ELF, a pesar de establecer con más o menos exactitud que estructuras deben usarse para organizar la información de un objeto, no explica como implementar el enlace dinámico en cada arquitectura. Además deja cabos en el aire que deben resolverse según las necesidades del fabricante. Esto es debido a que la especificación ELF no indica ninguna interfaz genérica para los "Runtime Linker" ni se ha llegado a ningún tipo de acuerdo entre fabricantes para producirla.
Con estas explicaciones reuniremos la suficiente información para poder entender, en la siguiente sección, como se estructuró la capa dependiente de la arquitectura de pDI-Tools.
Linux™ es un "kernel" libre de licencia GPL, creado por Linus Torvalds y una amplia comunidad de usuarios, que ha evolucionado mucho a lo largo de su vida. Linux™ fue inicialmente escrito exclusivamente para la plataforma INTEL® 386. Durante sus primeros años de vida básicamente creció hasta casi ofrecer las mismas funcionalidades que un "kernel" comercial de la época. Fue en la rama 2.2.x cuando se hizo un gran esfuerzo para crear un "kernel" fácilmente portable capaz de competir con otros sistemas estilo Unix™. Con esta versión aparecieron implementaciones de Linux™ para múltiples arquitecturas. Esto provocó que se empezasen a depurar mucho las interfaces del sistema operativo, cumpliendo cada día más los estándares. La versión 2.4.x se considera un "kernel" muy depurado, eficiente y apto para entornos de trabajo intenso, además de funcionar en un gran número de arquitecturas. Y ahora en la versión 2.6.x se está empezando a usar bastante en grandes computadoras y cálculo intensivo.
Linux™ además está íntimamente ligado a la librería de C GNU C Library (por ello suele recibir el nombre de GNU/Linux™). Ésta, al igual que el "kernel", ha cambiado mucho a lo largo de su historia, soportando cada día mejor los estándares y mejorando su calidad y portabilidad.
El resultado, especialmente por los orígenes INTEL® 386 de Linux™, es una de las implementaciones de ELF más cuidadas y respetuosas con la especificación [Sys5ABI].
Sólo ha sido ahora con la versión 2.6.x del "kernel" y la versión 2.3.x de GNU C Library cuando se han introducido algunos cambios que rompen un poco la especificación ELF. No obstante los cambios no son muy bruscos y se resuelven con unas pocas condiciones.
El "Runtime Linker" de GNU/Linux™ ofrece un protocolo de depuración que
permite acceder a la lista de objetos que él maneja. Esta lista es
accesible desde la estructura global _r_debug
. En esta estructura
encontramos una lista encadenada de objetos, donde el primero es el
binario de la aplicación y el resto las librerías y objetos dinámicos
que ésta usa. Esta estructura y lista no forman parte del estándar y es
el mecanismo que ld-linux.so (el "Runtime Linker") pone a nuestra
diposición para acceder a la información sobre los DSO en memoria.
Cada elemento de la lista es una estructura _r_linkmap
que nos
permite llegar a la información ELF de un DSO. Examinando las
estructuras ELF la primera sorpresa que tenemos es que en la
información del ejecutable no tenemos el nombre de la aplicación. Por
este motivo en GNU/Linux™ sobre INTEL® 386 pDI-Tools siempre se refiere al
programa principal con el alias MAIN.
El suplemento del ABI para INTEL® 386 [Sys5i386] indica que en esta arquitectura siempre se ha de usar estructuras de reubicación con un "addend" implícito, o lo que es lo mismo, estructuras ElfW(Rel).
Esto implica que encontraremos seguro las secciones DT_REL
,
DT_RELSZ
y DT_RELENT
.
No obstante no las usaremos ya que GNU/Linux™ separa todas las
reubicaciones de función en una tabla a parte: DT_JMPREL
. Esta
tabla está ordenada ascendentemente por el índice de símbolo de cada
reubicación. Esto es muy práctico ya que nos permite hacer las
búsquedas en esta tabla dicotómicamente.
Todas las entradas en la sección DT_JMPREL
son del tipo
R_386_JMP_SLOT
. Este tipo de reubicación apunta a una entrada del
GOT de la sección DT_PLTGOT
del objeto. Esta entrada
contiene la dirección real del objeto en memoria, o en su defecto un
puntero a un "stub" encargado de llamar al "Runtime Linker" para que la
resuelva.
Esta tabla se usa por unos "stubs" de reenlace generados en tiempo de compilación. Un "stub" se divide en dos partes: una que trata de transferir el control a la verdadera definición de la función, y otra encargada de transferir el control al "Runtime Linker" si la parte anterior no tuvo éxito.
Estos "stubs" están también contenidos en la sección DT_PLTGOT
. Esta
parte de la sección recibe el nombre de "Procedure Linkage Table". El
aspecto del PLT es el siguiente:
.PLT0: pushl got_plus_4 jmp *got_plus_8 nop ; nop nop ; nop .PLT1: jmp *name1_in_got pushl $offset jmp .PLT0 .PLT2: jmp *name2_in_got pushl $offset jmp .PLT0 ...
De este código podemos deducir que las tres primeras entradas del GOT
están reservadas y su contenido es especial. La primera entrada, que no
se usa en este código, contiene la dirección de la estructura dinámica
_DYNAMIC
. La segunda entrada sirve para dar información de
identificación al "Runtime Linker", mientras que la tercera es un
puntero a una función del "Runtime Linker" encargada de realizar las
resoluciones en tiempo de ejecución.
Cuando el programa arranca la tabla GOT está inicializada de tal forma que la entrada name1_in_got contiene la dirección de la siguiente instrucción a .PLT1, la entrada name2_in_got contiene la dirección de la siguiente instrucción a .PLT2, y así sucesivamente.
Esto hace que la primera vez que usamos una función, el "stub"
.PLTn
provoque que se
termine saltando a .PLT0 y de este al
"Runtime Linker". El "Runtime Linker" alterará la entrada
n del GOT con la verdadera dirección de la
función para saltar directamente a ella en próximas llamadas.
El PLT se presenta en INTEL® 386 de dos formas: con direcciones estáticas para objetos no reubicables, como es el caso del binario ejecutable, o con "offsets" relativos, que es el caso de la mayor parte de las librerías compartidas.
Una última diferencia respecto al estándar, pero que no nos afecta muy seriamente, es el "Runtime Linker". La especificación recomienda que sea la misma librería de C el "Runtime Linker". Sin embargo, GNU/Linux™ usa el objeto compartido ld-linux.so como "Runtime Linker".
Realmente no es técnicamente más complejo darle el rol de "Runtime Linker" a GNU C Library. Los motivos que han llevado a separar esta funcionalidad a parte no están claros, aunque si es útil esta separación en algunos casos especiales. En concreto se nos ocurren dos posibles motivos:
Supongamos que escribimos una aplicación íntegramente en ensamblador y totalmente autosuficiente (no recurre a otras librerías, funciona exclusivamente mediante llamadas al sistema). Si el "Runtime Linker" está integrado en la librería de C obligamos a estas aplicaciones supercompactas a cargar esta macro librería, aumentando su consumo de memoria y tiempo de carga.
En el caso de los binarios enlazados estáticamente no es necesario usar la librería de C del sistema (ver [Teensy]). De hecho aplicaciones como versiones antiguas de Acrobat Reader vienen enlazadas estáticamente para evitar usar las librerías de sistema y así funcionar sobre casi cualquier distribución de GNU/Linux™ sin ser necesaria una recompilación. Estos binarios de por sí suelen ser muy grandes y consumen mucha memoria al incluir toda la librería de C, y además pueden dejar de funcionar correctamente si las llamadas al sistema operativo cambian.
El sistema operativo Solaris sobre SPARC tiene una implementación muy correcta del estándar ELF al igual que GNU/Linux™ sobre INTEL® 386.
Solaris es un sistema operativo desarrollado por Sun Microsystems™ a partir de la rama BSD, pero con muchas influencias de "System V" (de hecho ELF es parte de "System V"). Al contrario que GNU/Linux™, Solaris es un sistema operativo Unix™ y no un clon.
La arquitectura SPARC es el buque insignia de Solaris. Sun Microsystems™ ha usado mucho y aún usa este tipo de CPU RISC. Se caracteriza por ser una arquitectura eficiente, clara, con un código ensamblador elegante y fácil de mantener. De hecho gracias a esto las SPARC modernas ofrecen total compatibilidad hacia atrás con CPU's más antiguas.
La arquitectura SPARC prácticamente solo ha sido usada por Sun Microsystems™ para sus estaciones de trabajo y servidores. Debido a esto el suplemento para SPARC de la ABI ha sido escrito casi por entero por Sun Microsystems™, a la vez que la implementación en Solaris. El resultado es una implementación bastante rigurosa de la misma.
A pesar de ello, en Solaris hemos encontrado algunos pequeños detalles que incumplen la especificación ELF. No parecen casuales ya que tienen la pinta de ser pequeñas optimizaciones. Por ejemplo, al igual que en GNU/Linux™, encontramos que el "Runtime Linker" es una librería a parte llamada ld.so.1.
Para poder acceder a los datos de los objetos en memoria el
"Runtime Linker" ofrece un protocolo de depuración basado en una
estructura muy similar a _r_debug
de GNU/Linux™. Esta estructura
recibe el nombre de r_debug
.
No obstante esta estructura no está exportada globalmente y se ha de
buscar en la sección dinámica DT_DEBUG
de algún objeto. Esto nos
lleva a una recurrencia: necesitamos consultar la sección DT_DEBUG
pero no tenemos como llegar a las estructuras ELF de un objeto.
La especificación ELF, previsora, obliga a exportar la variable
global _DYNAMIC
a todos los objetos para resolver estas
situaciones. Esta variable global es un puntero a la lista de secciones
dinámicas del ejecutable. En esta lista precisamente encontraremos la
sección DT_DEBUG
que nos indicará la posición de la estructura
r_debug
que buscamos.
Esta estructura, al igual que su homóloga en GNU/Linux™, contiene un puntero a una lista encadenada de objetos. Dentro de cada elemento de la lista encontraremos un puntero a la lista de estructuras dinámicas.
También encontramos que en algunos objetos faltan secciones como
DT_RELA
. Esto sucede en objetos especiales que no exportan ni
usan símbolos externos. Estos objetos se encuentran en Solaris y en
las nuevas versiones de GNU/Linux™, y se desconoce su papel en el sistema,
aunque tampoco afectan a nuestros propósitos. Estos objetos son
marcados por pDI-Tools como Broken Objects (objetos rotos) para no ser
manipulados.
Otra característica de esta arquitectura es que usa reubicaciones relativas, o lo que es lo mismo, del tipo ElfW(Rela). Estas estructuras se diferencian de ElfW(Rel) en que tienen un "addend" explícito. Este valor debe usarse con el puntero contenido en la estructura según el tipo de reubicación que sea.
Al usar estructuras ElfW(Rela) se deben usar las secciones
DT_RELA
, DT_RELASZ
y DT_RELAENT
. Estas, al igual que en
INTEL® 386, contienen punteros al interior de la sección DT_PLTGOT
. En
concreto nos interesan las estructuras del tipo R_SPARC_JMP_SLOT
,
que son las encargadas de reubicar las funciones externas. No obstante
esta vez los punteros de las estructuras ElfW(Rela) no apuntan a otros
punteros, sino directamente a código.
La especificación de la arquitectura SPARC no usa en ningún momento tablas GOT, sino que reescribe en tiempo de ejecución el código de la tabla PLT.
Cada entrada del PLT es un "stub" encargado de realizar un salto a la verdadera definición de la función, o al "Runtime Linker" en caso de no conocerse todavía. Un "stub", debido a las limitaciones que impone el juego de instrucciones, se compone de unas cuantas instrucciones aritméticas que generan la dirección donde saltar y una instrucción de salto sin retorno.
Seguidamente copiamos un código de ejemplo de la tabla PLT ya inicializado por el "Runtime Linker":
.PLT0: save %sp, -64, %sp call runtime_linker nop .PLT1: .word obj_identification unimp unimp ... .PLT101: sethi %hi(name1), %g1 jmpl %g1+%lo(name1), %g0 nop .PLT102: sethi (.-.PLT0), %g1 ba,a .PLT0 nop .PLT103: sethi %hi(name3), %g1 jmpl %g1+%lo(name3), %g0 nop .PLTN: sethi %hi(nameN), %g1 jmpl %g1+%lo(nameN), %g0 nop
El código en .PLT0 es el encargado de resolver un símbolo en tiempo de ejecución. Inicialmente los "stubs" tienen el aspecto del "stub" .PLT102. Este transfiere el control a .PLT0 junto a su posición en %g1. .PLT0 a su vez transferirá el control al "Runtime Linker". Éste se encargará de resolver la función, alterar el código en la posición .PLT102 y transferir el control a dicha función.
A partir de entonces los "stubs" toman el aspecto de los ejemplos .PLT101 y .PLT103. Estos "stubs" simplemente realizan un salto a la verdadera definición de la función.
En el caso de SPARC 64 bits, los "stubs" son distintos, además de más grandes. No obstante es prácticamente el mismo mecanismo.
Irix es el sistema operativo de SGI®. Está presente en muchas plataformas, entre ellas Itanium®. No obstante la más extendida y usada es para MIPS®. MIPS® es una CPU RISC de alto rendimiento, para 32 y 64 bits.
Este sistema operativo tiene una larga historia y soporta bastantes ABI. Actualmente sólo se usa, o se recomienda usar, el ABI de ELF en sus formas N32 (32 bits ELF) o 64 (64 bits ELF).
Por compatibilidad hacia atrás el "Runtime Linker", al contrario que SPARC o INTEL® 386, ofrece varios métodos para acceder a los objetos en memoria.
El que nos interesa es el acceso mediante la información de depuración en la estructura ElfW(Obj_Info). Esta estructura, a pesar de tener un nombre del estilo de los tipos de ELF, no forma parte del estándar ELF y es totalmente exclusiva de Irix. Esta estructura ofrece mucha información de depuración junto una lista encadenada con información sobre los objetos en memoria.
Y es en los elementos de esa lista donde encontramos una primera
particularidad de este sistema operativo: los campos
oi_ehdr
y
oi_orig_ehdr
. Estos campos indican la
dirección donde espera cargarse el objeto compartido, y la dirección
donde realmente ha sido ubicado. La diferencia entre ambos campos se
llama desplazamiento delta y se usa para
recalcular los "offsets" indicados en algunas
estructuras ELF.
De hecho el desplazamiento delta sólo se debe usar en las estructuras ELF que se han leído directamente de disco sin alterarse, como la tabla de símbolos o las tablas de reubicación. En otras estructuras generadas por el "Runtime Linker" como sería la tabla GOT se usan punteros absolutos.
Estos valores no se reajustan debido a que los ficheros binarios se mapean en memoria. De esta forma el programa no se copia entero a memoria sino que se consulta de disco de una forma transparente y sin consumir apenas memoria. Esto permite ahorrar bastante memoria, y además ya que, si están funcionando varias copias, todas comparten páginas cacheadas y por lo tanto se gana también rendimiento. No obstante es posible escribir en dichas páginas ya que por defecto están marcadas como páginas COW ("Copy-On-Write"), o lo que es lo mismo, cuando se modifican se reserva una página de memoria virtual anónima donde copiar el contenido y la modificación.
Por ello se debe detectar y tener en cuenta en que estructuras debemos usar el desplazamiento delta o no para interpretar sus valores.
La arquitectura MIPS® usa reubicaciones con "addends" implícitos, o lo
que es lo mismo la estructura ElfW(Rel) y las secciones dinámicas
DT_REL
, DT_RELSZ
y DT_RELENT
.
Otra gran diferencia es la ausencia de una sección DT_JMPREL
que
acelere las búsquedas de reenlaces, pero como comentamos seguidamente,
no es necesaria por la disposición que tienen las reubicaciones en la
sección DT_REL
.
De hecho en esta arquitectura pDI-Tools nunca llega a usar la tabla
DT_REL
. El suplemento ELF de MIPS® introduce muchos detalles sobre
como debe organizarse la tabla de símbolos y la tabla GOT. Además
añade nuevas secciones dinámicas con información para poder explotar
dicha organización. Las estructuras dinámicas nuevas más importantes:
DT_MIPS_RLD_VERSION
.
Este elemento contiene la versión de la API del
"Runtime Linker". Puede ser necesaria en el futuro para distinguir
versiones de Irix que presenten alguna incompatibilidad con la
versión actual.
DT_MIPS_LOCAL_GOTNO
.
Este elemento nos indica cuantas entradas del GOT pertenecen a
símbolos locales. Es importante ya que nos permite dividir el
GOT en dos partes claras, de las cuales sólo una debemos
manipular.
DT_MIPS_GOTSYM
.
Por último este elemento nos da el índice del primer símbolo de
la tabla de símbolos que tiene una entrada en la
"Global Offset Table".
Las sección DT_PLTGOT
se divide en dos partes lógicas:
local y externa. Las entradas
locales residen en la primera parte de la tabla. Estas entradas sólo
requieren ser reubicadas si ocurren en un objeto compartido y la
dirección donde se ha cargado dicho objeto no es donde esperaba ser
cargado.
La sección de entradas externas reside en la segunda parte de la tabla
GOT. La estructura dinámica DT_MIPS_LOCAL_GOTNO
indica cuantos
elementos contiene la tabla GOT local, y por lo tanto, donde comienza
la tabla GOT externa.
Cada entrada en la parte externa del GOT se corresponde directamente
con una entrada de una parte especial de la tabla de símbolos. Esta
parte de la tabla de símbolos la llamamos .dynsym. La tabla de
símbolos, de forma similar al GOT, se divide en dos partes: símbolos
que no tienen una entrada en el GOT y símbolos que sí. Con ayuda de
la información que nos da DT_MIPS_GOTSYM
podemos conocer cual es la
primera entrada de la tabla de símbolos que forma parte de .dynsym.
Toda esta información nos lleva a una conclusión: podemos conocer la
posición en el GOT de una función tan sólo sabiendo la información de
símbolo sin ser necesario consultar las estructuras DT_REL
. Además el
acceso será prácticamente directo debido al orden que hay entre
DT_SYMTAB
y DT_PLTGOT
.
Por ejemplo, supongamos que queremos modificar la entrada del GOT del símbolo externo con índice symndx. Aplicando la siguiente operación aritmética obtenemos el índice de la entrada del GOT que debemos modificar:
gotndx = val(
DT_MIPS_LOCAL_GOTNO
) + symndx - val(DT_MIPS_GOTSYM
)
Esto sin duda es mucho más rápido que una búsqueda en la tabla de reubicación, aunque ésta sea dicotómica.
Además el "Runtime Linker", que en Irix es una aplicación llamada rld(5), ofrece una completa API para manipular los objetos y símbolos en memoria. No obstante en pDI-Tools no se ha aprovechado esta API ya que parece muy dependiente de la versión de Irix y puede ser que no este presente en próximas versiones del sistema operativo. Manipulándolos con las funciones dlopen(3), dlsym(3) y dlclose(3) garantizamos que esta parte del código sea altamente portable sin perder apenas eficiencia (se supone que la implementación de estas funciones debe usar la API descrita).
Particularidades como esta hicieron tomar la decisión de separar el código para cada sistema operativo y arquitectura. No aprovechar algunas ayudas que brinda la plataforma Irix sobre MIPS® sería un error ya que obtendríamos soluciones menos eficientes.
Con esto se han explicado las diferencias más importantes de esta plataforma. Como se ha podido ver Irix sobre MIPS® es una plataforma con una implementación de ELF muy peculiar, tanto por sus optimizaciones como por las funcionalidades ofrecidas por el "Runtime Linker".
Esta explicación la dividimos en dos partes, la primera para PowerPC™ y la segunda para PowerPC™ 64 bits, debido no sólo a que son dos ABI completamente distintos, sino que además el segundo no forma parte del estándar ELF aunque sigue sus indicaciones.
PowerPC™ es una de las principales plataformas sobre las que corre GNU/Linux™. Tiene una larga historia junto a una gran comunidad encargada de soportarla y mejorarla.
Debido a la buena portabilidad del sistema GNU/Linux™, las diferencias
entre el "Runtime Linker" de INTEL® 386 y PowerPC™ no son muchas, y
casi todas las operaciones y funcionalidades se mantienen intactas de
una implementación a la otra. Las estructuras de depuración
_r_debug
son casi idénticas y no presentan diferencias a la
hora de manejarlas.
Encontramos también algunas discrepancias comunes respecto al estándar en ambas versiones (INTEL® 386 y PowerPC™). Por ejemplo, el objeto compartido ld-linux.so en ambas versiones asume el rol de "Runtime Linker", en lugar de la librería de C tal como aconseja el estándar.
Respecto a la implementación del ABI ELF para PowerPC™ podemos decir que está muy cuidada y es respetuosa con la especificación, a excepción de los aspectos que comentaremos seguidamente.
En esta arquitectura también nos encontramos con Broken Objects (ver GNU/Linux™/INTEL® 386 o Solaris). Estos objetos tampoco son tratados en pDI-Tools y pasan desapercibidos durante la instrumentación.
El suplemento ELF de esta arquitectura es parecido a SPARC, pero con características de INTEL® 386. Se basa, al igual que INTEL® 386 en tener una a la vez una tabla PLT y una tabla GOT, pero se modifica el código en tiempo de ejecución al igual que SPARC. No obstante GNU/Linux™ no se mantiene fiel a la especificación ELF en este aspecto, e implementa un mecanismo de enlace algo distinto, aunque coherente con la especificación.
La implementación de los enlaces es casi idéntica a la implementación de SPARC en 32 bits. Sólo hay dos diferencias significativas: el nuevo juego de instrucciones de PowerPC™ y que las estructuras de reubicación no usan un "addend" explicito, es decir, se usan estructuras ElfW(Rel).
También encontramos que la sección DT_JMPREL
también está
presente, al igual que en GNU/Linux™ para INTEL® 386. En esta sección
encontramos solamente estructuras del tipo R_PPC_JMP_SLOT
.
Estas estructuras son las encargadas de reubicar las funciones que
usa o implementa un objeto.
Al igual que en SPARC, no usa en ningún momento tablas GOT, sino que reescribe en tiempo de ejecución el código de la tabla PLT.
Cada entrada del PLT es un "stub" encargado de realizar un salto a la verdadera definición de la función, o al "Runtime Linker" en caso de no conocerse todavía. Seguidamente mostramos el aspecto que puede tener el PLT de MAIN sólo iniciarse el programa en GNU/Linux™ sobre PowerPC™:
.PLTcall: addis r11, r11, .PLTtable@ha lwz r11, .PLTtable@l(r11) mtctr r11 bctr .PLTresolve: addis r12, r0, dynamic_linker@ha addi r12, r12, dynamic_linker@l mtctr r12 addis r12, r0, symtab_addr@ha addi r12, r12, symtab_addr@l bctr nop nop nop nop nop nop nop nop .PLT1: addi r11, r0, 4*0 b .PLTresolve .PLT2: addi r11, r0, 4*1 b .PLTresolve .PLT3: addi r11, r0, 4*2 b .PLTresolve ... .PLTN: addi r11, r0, 4*(N-1) b .PLTresolve
Cuando llamamos a una función, el control se transfiere a un "stub" como .PLT1, .PLT2... Estos "stubs" escriben su índice en el registro r11 y transfieren el control a .PLTresolve. Esta rutina llama a "Runtime Linker", pasándole el índice de la función en la tabla de símbolos y un puntero a dicha tabla. Con esta información el "Runtime Linker" resolverá la dirección del símbolo, y cambiará consecuentemente el contenido de la tabla PLT y GOT. Acto seguido transfiere el control a la función resuelta.
Pero GNU/Linux™ en esta ocasión no se mantiene fiel a la especificación
y e introduce aquí una optimización copiada de la plataforma SPARC.
La rutina .PLTcall está implementada en esta
plataforma al principio de la sección DT_PLTGOT
, al contrarió del
estándar, que indica que tendría que ser la segunda rutina. Además,
si se puede, el código que se copia es distinto del expuesto:
transfiere el control directamente a la función sin usar la tabla
GOT. Seguidamente se explica cuando se usa este código o el otro.
Si la diferencia entre el punto de entrada de la función desde la posición y .PLTi está en un rango de 0x03ffffff bytes se realiza un salto relativo a la función, y por lo tanto se escribe esta entrada en el PLT:
PLTi: b (@target - .PLTi)
Si la dirección de la función está demasiado lejos, es decir, no entra en el rango antes descrito, se realiza el enlace mediante una consulta al GOT y un salto a a función .PLTcall, tal como indica el estándar:
PLTi: addi r11, r0, 4*(i-1) b .PLTcall
En próximas llamadas, el "stub" transferirá el control directamente si es un salto cercano, o mediante la rutina .PLTcall a la dirección de memoria almacenada en la posición i-1 de la tabla GOT.
Con esta optimización se consigue acelerar notablemente la velocidad del enlace en muchísimos casos sin romper la compatibilidad con el estándar.
GNU/Linux™ sobre PowerPC™ 64 es un "porting" relativamente reciente. Está dirigido considerablemente por IBM® e implementa soluciones similares a las de IBM AIX™ en el "kernel". Debido a que este formato no se considera parte de ELF no se entrará en mucho detalle sobre él.
IBM AIX™ originalmente usaba un ABI distinto de ELF llamado PowerOpen™. El formato de los ejecutables PowerOpen™ era una variación del formato COFF y adaptado a PowerPC™. Posteriormente se adaptó el "Runtime Linker" de IBM AIX™ para que pudiera usar binarios ELF.
De esto resultó unas indicaciones, que no se consideran parte del estándar ELF, sobre como implementar algo parecido a ELF siguiendo un ABI similar al de PowerOpen™ y por lo tanto un esquema de enlace también similar.
El resultado final es una especificación muy distinta a todas las anteriores, y a la vez muy vaga e inexacta.
Con el desarrollo del sistema operativo GNU/Linux™ se ha hecho un esfuerzo por retomar este formato de enlace e intentar convertirlo en algo parecido a ELF. No obstante la implementación no sigue ni la especificación resultante ni tampoco las antiguas especificaciones ELF de IBM AIX™.
De hecho en esta arquitectura, a pesar de que existe una sección
DT_PLTGOT
, su contenido no es útil. Los enlaces se realizan
todos en tiempo de carga mediante la construcción de descriptores de
función.
Un descriptor de función es una estructura de esta forma:
typedef struct function_descriptor { void *addr; unsigned long toc; unsigned long env; } f_desc_t;
El campo addr
contiene la dirección del
punto de entrada de la función.
El campo toc
contiene la dirección base
del TOC para esta función.
El campo env
contiene el puntero de
entorno para lenguajes como Pascal o PL/1.
Estos descriptores tendrían que estar contenidos en una tabla llamada
TOC contenida en la sección DT_PLTGOT
.
Cuando un programa quiere llamar a una función, este consulta el descriptor de la función, carga en el registro r2 (o registro TOC) el valor correcto del TOC para esta función, consulta su dirección en el descriptor y salta a ella.
Aprovechando esto se construyen los reenlaces: para reenlazar una función a otra simplemente tenemos que copiar su descriptor encima del descriptor de la función interpuesta.
Este mecanismo es el único posible ya que la mayoría de estructuras ELF están corruptas en esta arquitectura. Con esto quiero decir que las tablas de símbolos están vacías o faltan símbolos, punteros que no apuntan a ninguna parte e incoherencias.
Respecto al "Runtime Linker", éste tiene el mismo comportamiento que en PowerPC™ 32 bits. Por ello el "driver" de sistema apenas se ve afectado por esta nueva arquitectura.
Las descripciones anteriores nos muestran que las diferencias generalmente se suelen dar a nivel del "Runtime Linker". Respecto a las implementaciones de ELF, con mayores o menores innovaciones, éstas se suelen respetar bastante la especificación ELF: tabla de símbolos, tabla de cadenas, formato de las estructuras, etc.
Rara poder manejar todas estas diferencias se debe decidir como se organizarán los "Elf Backend" para poder obtener una portabilidad óptima sin sacrificar la legibilidad del programa.
La idea más inmediata es dividir los "Elf Backend" en unidades, que llamaremos "drivers". Es fácil pensar en dos tipos de "drivers": los dedicados a interaccionar con el "Runtime Linker" y por lo tanto dependientes del sistema operativo, y los dedicados a realizar los enlaces en una cierta arquitectura (dependientes de la CPU).
A priori la organización más sencilla sería una comunicación directa entre todos los "drivers" y la parte independiente de la arquitectura, como se puede ver en el siguiente diagrama:
En este diagrama mostramos como todas las comunicaciones serían a un mismo nivel. Es decir, pDI-Tools usaría un "driver" del sistema operativo para interaccionar con el "Runtime Linker", y otro de CPU para realizar las interposiciones.
Pero a pesar de que los mecanismos de enlace (dependientes de la CPU) parecen a priori muy portables entre sistemas operativos, hay ligeras diferencias impuestas por los sistemas operativos que hacen que sean difíciles de reutilizar en su totalidad.
Además apostar por fijar un API demasiado estricta para ELF haría que ciertos casos como PowerPC™ 64 fuesen muy difíciles de resolver, además de en el futuro vetar el soporte a otros ABI que no sean ELF.
Por ello, aprovechando que los sistemas operativos suelen ir muy ligados a las arquitecturas sobre las que funcionan, se hará una implementación de estos "drivers" menos portable, pero muchísimo más compacta. Y por ello estos deberán replicarse para cada sistema operativo, aunque por su reducido tamaño (unas 200 líneas de código en el caso de GNU/Linux™) no implicará un gran problema.
De este modo la comunicación se realizará de la siguiente forma: la parte independiente solicitará operaciones al "Elf Backend", que en realidad será directamente un "driver" para el sistema operativo. Este llamará posteriormente al "driver" de CPU adecuado para ejecutar las interposiciones:
En este diagrama podemos ver como del "driver" de un sistema operativo penden sus "drivers" específicos para CPU.
De esta forma podemos camuflar el tratamiento de ABI's algo distintas de ELF como sería el caso de PowerPC™ 64.
Además se reduce notablemente el API que expondrá el "Elf Backend" a la parte no dependiente de la arquitectura y a los "backend" de usuario. Esta fuerte separación reducirá la cantidad de condicionales en el código, haciéndolo más compacto, legible y eficiente.
A la hora de implementar el "driver" de sistema operativo nos encontramos con diferencias entre sistemas, pero no demasiado fuertes, ya que se puede seguir un hilo general. Por ello se traza un esquema que se seguirá en todos los "drivers" de los diferentes sistemas operativos.
Hacer esto facilita el trabajo de crear un "driver" para un nuevo sistema operativo a partir de otro ya existente. El "driver" de sistema operativo debe facilitar a pDI-Tools la gestión de los objetos en memoria, y además aislar los mecanismos de reenlace usando los servicios del "driver" de CPU.
Vistas las secciones anteriores observamos que la primera tarea que debe resolverse es como acceder a las estructuras ELF. Para ello el "Elf Backend" debe ser responsable de construir una lista de objetos, para que la parte independiente pueda trabajar con ellos sin preocuparse de como se organiza el "Runtime Linker".
Cada nodo de la lista de objetos de pDI-Tools contendrá información sobre el objeto. Esta información será recogida de los "Dynamic Tags" de los objetos en memoria, tratada si es necesario y precalculada en algunos casos. Como veremos más adelante, la información sobre cada objeto en memoria, irá contenida en una estructura PDI_ELFOBJ, donde cada estructura contiene un enlace a la información de objeto anterior y posterior, según el orden de la lista de objetos del "Runtime Linker".
Parte de esta información se presentará al código no dependiente de la plataforma, mientras que el resto será para uso interno del "Elf Backend".
Esta información no sólo acelerará y simplificará la obtención de un cierto dato durante la ejecución, sino que además centrará el proceso de los "Dynamic Tags" a un sólo punto del programa.
Como hemos visto, por la presencia o ausencia de los "Dynamic Tags"
cuando se dan diferencias incluso entre diferentes versiones de un
mismo sistema operativo. Por ejemplo, en GNU/Linux™ 2.6.x con GNU C Library
2.3.x, nos encontraremos con objetos que carecen del "Dynamic Tag"
DT_REL
y DT_RELA
, mientras que con GNU C Library 2.2.x siempre
está presente. En estos casos se adjunta en la información de objeto
una marca, que llamaremos "BrokenObject", que
indica que el objeto no se puede instrumentar.
Tambien encontramos otras diferencias que no quebrantan el estándar
pero implican muchas distinciones posteriores. Por ejemplo en Irix la
la sección DT_JMPREL
no existe. Esta sección es opcional, según
el estándar, aunque por regla general está presente en el resto de
sistemas operativos. Dejando los mecanismos de búsqueda dentro del
"Elf Backend" no sólo aislamos la parte independiente un poco más de
ELF, sino que obtenemos un código más simple, más específico y más
rápido.
El resto de diferencias afecta directamente al contenido de las secciones ELF. Algunos "Runtime Linker" a veces modifican el contenido de las secciones (respecto a su aspecto en disco) por motivos de optimización. Esto implica que el código específico para un determinado sistema operativo, aunque no sea excesivamente diferente al de otro sistema operativo, esté lleno de pequeños ajustes para poder utilizar dichos datos.
Por último encontramos que algunas implementaciones proporcionan nuevos "flags" o datos que pueden facilitar y acelerar algunos mecanismos y que por lo tanto vale la pena aprovechar.
PDI_ELFOBJ. Estas estructuras contienen mucha información precalculada y con ella se consigue acelerar notablemente la ejecución de pDI-Tools ya que reduce la cantidad de código replicado y cálculo.
Ejemplos de esta información son punteros a las principales secciones ELF, valores extraídos de las mismas, nombre del DSO, su alias, punto de carga del DSO, etc.
Pero no en todas las plataformas nos interesa la misma información. Por ello esta estructura de información se divide en una parte común para todas las plataformas (no dependiente de la arquitectura) y una parte específica, establecida en tiempo de compilación, totalmente dependiente de la arquitectura.
Por último explicamos que labores debe cumplir la capa encargada de gestionar la CPU.
El "driver" de CPU puede realizar tres posibles operaciones: establecer los reenlaces, establecer las redefiniciones y establecer las interposiciones mediante "callback". De estas tres sólo son frecuentes las dos primeras.
No se establece un API común para estas operaciones, y se deja libre frente a cualquier necesidad del "Elf Backend". No obstante, hasta la fecha se sigue el mismo esquema para todas las implementaciones.
En la siguiente sección describimos a grandes rasgos el API que ofrece el "Elf Backend" para realizar todas las operaciones antes citadas.
En esta sección enumeramos una por una las funciones que debe implementar cara a pDI-Tools el API de un "Elf Backend". Antes de comenzar a enumerarlas, damos unas pinceladas generales para entender mejor este API.
Se pretende mantener un API lo más reducida y simple posible. Se observa que cada punto de entrada tiene un objetivo claramente definido aunque en ningún momento se marca el formato del ABI que se trata por debajo.
Pero se hace una excepción con la gestión de los "callback". Debido a la complejidad de los "callback" es muy difícil decidir una única estructura que deban seguir todas las arquitecturas. De hecho puede ser hasta contraproducente ya que implicaría mayores niveles de abstracción, más complejidad y consiguientemente más código y lentitud. Por lo tanto se deja en manos del programador de la arquitectura como se implementará el mecanismo de "callback" y todas las estructuras en memoria que este requiera.
Respecto al resto el funcionamiento es bastante claro, y como veremos el "Elf Backend" implementa funcionalidades básicas como su propia inicialización y finalización, resolución de símbolos, aplicar y deshacer las interposiciones de código y gestión de los "callback".
Descripción. Estas funciones se encargan de inicializar y finalizar el "Elf Backend", respectivamente. La primera se llama durante la inicialización de pDI-Tools y la segunda durante su finalización.
Generalmente un "Elf Backend" no suele requerir una
inicialización previa. Sin embargo hay casos como Solaris
donde se deben realizar unas cuantas operaciones para poder
obtener información clave para poder trabajar con las
estructuras ELF. O el caso de GNU/Linux™, que debemos averiguar
antes que versión de GNU C Library se está ejecutando ya que en la
versión 2.3.x se han introducido cambios importantes en el
"Runtime Linker". Para inicializar y finalizar el "Elf Backend"
se deben implementar las funciones _pdi_arch_init()
y
_pdi_arch_fini()
.
Parámetros. Estas funciones no reciben parámetros de entrada.
Valor devuelto. 0 si todo fue bien, -1 en caso de error.
Descripción. Esta función se encarga de obtener punteros reales a las funciones que se usarán para evitar que pDI-Tools sea instrumentado accidentalmente.
Parámetros. Esta función no recibe parámetros de entrada.
Valor devuelto. 0 si todo fue bien, -1 en caso de error.
Descripción.
La función _pdi_arch_initObjectList()
crea una copia
independiente de la plataforma de la lista de objetos del
"Runtime Linker". La función _pdi_arch_refreshObjectList()
nos permitirá resincronizar esta copia con la lista actual
del "Runtime Linker".
Con estas funciones podemos implementar casi toda la lógica de la aplicación en la parte no dependiente de la arquitectura y reducir notablemente la cantidad de código en los "Elf Backends". La mayor parte de pDI-Tools es código de gestión de las estructuras en memoria y comprobaciones simples como si está o no cargado un DSO en memoria, resolver referencias a objetos tanto por nombre como por alias, comprobar que las interposiciones no se interfieren, etc.
Parámetros. Estas funciones no reciben parámetros de entrada.
Valor devuelto. 0 si todo fue bien, -1 en caso de error.
Descripción. Esta función se encarga de liberar la memoria de la lista de objetos independiente de la plataforma. Se le llama durante la finalización de pDI-Tools.
Parámetros. Esta función no recibe parámetros de entrada.
Valor devuelto. Esta función no devuelve nada.
Descripción.
Para resolver un símbolo el "Elf Backend" debe ofrecer las
funciones _pdi_arch_resolveSymbol()
y
_pdi_arch_resolveSymbolInObject()
. La primera función busca
entre todos los objetos en memoria un puntero al símbolo en
el objeto que lo exporta. La segunda busca la información de
un símbolo en un determinado objeto. No debe interpretarse
como "buscar un símbolo en el espacio de nombres de un
determinado objeto": simplemente busca la
implementación del símbolo en el objeto, y si fracasa
devuelve NULL
.
Parámetros.
obj
.
Indica en que objeto debemos buscar el símbolo
indicado por el parámetro
name
.
name
.
Es el nombre del símbolo que pretendemos resolver.
Valor devuelto.
Se devuelve un puntero al punto de entrada de la función en
caso de éxito, o NULL
en caso de no poder resolverse.
Descripción.
Esta función indica si el símbolo de nombre
name
es referenciado o implementado
por el objeto obj
(o ninguno de los
dos).
Parámetros.
obj
.
Indica en que objeto debemos buscar la información
del símbolo indicado por el parámetro
name
.
name
.
Es el nombre del símbolo que pretendemos examinar.
Valor devuelto.
Devuelve 0 si el símbolo name
no se
usa ni se implementa por obj
. Devuelve
un valor positivo si está implementado por el objeto, o un
valor negativo en caso de sólo usarse por él.
Descripción. Estas funciones se encargan de inicializar y finalizar el sistema de gestión de interposiciones por "callback" dependiente de la arquitectura.
Parámetros. Esta función no recibe parámetros de entrada.
Valor devuelto. Esta función devuelve un número natural.
Descripción.
Esta función da de alta la información dependiente de la
arquitectura en una interposición. Sólo se actualizan y
preparan las estructuras de datos, no se instala la
interposición. Para instalar la interposición se deberá
llamar posteriormente a la función _pdi_arch_callback()
,
_pdi_arch_redefine()
o _pdi_arch_relink()
.
Parámetros.
eo
.
Objeto en el que damos de alta la interposición.
wname
.
Nombre de la función "wrapper", o lo que es lo mismo,
nombre de la función que interponemos.
i
.
Interposición donde añadimos la información
dependiente de la arquitectura.
Valor devuelto. Devuelve 0 en caso de éxito, o -1 en caso de algún error.
Descripción.
Esta función realiza la acción de instalar una interposición
de código mediante el mecanismo de "callback" el objeto
eo
.
Parámetros.
eo
.
Objeto sobre el que estableceremos las
interposiciones por "callback".
i
.
Estructura que define la interposición que se va a
instalar, es decir, se indica el tipo de
interposición y sus parámetros.
Valor devuelto. 0 en caso de éxito, -1 en caso de error.
Descripción.
Estas función instala una redefinición sobre la función
implementada en el objeto eo
.
Parámetros.
eo
.
Objeto que contiene la implementación de la función a
redefinir.
i
.
Estructura que define la interposición que se va a
instalar, es decir, se indica el tipo de
interposición y sus parámetros.
Valor devuelto. 0 en caso de éxito, -1 en caso de error.
Descripción.
Esta función se encarga de instalar un reenlace sobre las
llamadas a una función desde el objeto
eo
.
Parámetros.
eo
.
Objeto sobre el que instalaremos el reenlace.
i
.
Estructura que define la interposición que se va a
instalar, es decir, se indica el tipo de
interposición y sus parámetros.
Valor devuelto. 0 en caso de éxito, -1 en caso de error.
Descripción. Estas funciones sirven para desinstalar los diferentes tipos de interposiciones de código.
Parámetros.
eo
.
Objeto al que pertenece esta interposición.
i
.
Interposición a desinstalar.
Valor devuelto. 0 en caso de éxito, -1 en caso de error.
Descripción.
Desactiva la redefinición i
en el
objeto eo
. Esta función se usa cuando
se carga un "backend" en memoria en tiempo de ejecución para
que no se vea afectado por las redefiniciones instaladas.
Parámetros.
i
.
Redefinición que desactivamos.
eo
.
Objeto en el que desactivamos la redefinición.
Valor devuelto. 0 en caso de éxito, -1 en caso de error.
Descripción.
Esta función libera la memoria usada por la información
dependiente de la arquitectura de la interposición
i
instalada en el objeto
eo
. No desinstala la interposición,
para ello ha de usarse una de las funciones
_pdi_arch_undoCallback()
_pdi_arch_undoRedefine()
o
_pdi_arch_undoRelink()
.
Parámetros.
eo
.
Objeto en el que está dada de alta la interposición.
i
.
Interposición en la que quitaremos la información
dependiente de la arquitectura, liberando memoria.
Valor devuelto. Devuelve 0 en caso de éxito, o -1 en caso de algún error.
Descripción. Esta función aconseja el tamaño del "pool" de "stubs" por defecto para esta plataforma. Generalmente la máxima cantidad de "stubs" que quepan en una página de memoria.
Parámetros. Esta función no recibe parámetros de entrada.
Valor devuelto. Esta función devuelve un número natural.
Descripción.
Esta función reserva una estructura de información especifica
de la arquitectura sobre un objeto (una estructura
PDI_ARCH_ELFOBJ) y la asigna al objeto
eo
.
pDI-Tools no reserva memoria dinámicamente para las estructuras de información sobre los objetos independiente de la arquitectura. En su lugar, al iniciarse, reserva un "pool" de objetos estático. Por ello, y en caso del que "Elf Backend" quisiese organizar de la misma manera, se le pasa el índice de la información no dependiente en el "pool" de objetos.
Parámetros.
eo
.
Información no dependiente de la arquitectura del
objeto que se le quiere añadir la información
dependiente.
objpool_index
.
Índice en el "pool" de objetos del objeto
eo
.
Valor devuelto. Devuelve 0 en caso de éxito, o -1 en caso de algún error.
Descripción. Libera la memoria ocupada por la parte dependiente de la arquitectura en una estructura PDI_ARCH_ELFOBJ.
Parámetros.
eo
.
Objeto en el que liberamos memoria.
objpool_index
.
Índice en el "pool" de objetos del objeto
eo
.
Valor devuelto. Devuelve 0 en caso de éxito, o -1 en caso de algún error.