← Programación en C para Sistemas Operativos Bloque 8 · Compilación, enlazado y bibliotecas · Tema 34 de 42

Tema 34

Carga dinámica de bibliotecas

En el tema 33 las bibliotecas dinámicas se decidían al compilar: el ejecutable llevaba anotado el nombre de cada una (NEEDED) y el cargador las traía antes de llegar a main. Hay programas que no pueden saber de antemano qué van a necesitar: un editor con extensiones, un servidor que carga módulos según su configuración, un reproductor que descubre los códecs instalados. Para ellos existe una segunda forma de usar una biblioteca dinámica, a pedido y mientras el programa corre: se abre con dlopen, se busca una función por su nombre con dlsym y se suelta con dlclose. Este tema enseña esas tres llamadas y la que informa los errores (dlerror), construye un pequeño programa con plugins y explica los detalles que más sorprenden: por qué el programa principal necesita -rdynamic, qué cuenta dlclose y cómo RTLD_NEXT permite envolver una función existente.

1. Cargar a pedido

Comparemos los tres momentos en que un programa puede «tomar» el código de una biblioteca:

FormaQuién la decideCuándo se cargaSi falta
Estática (tema 32)El enlazadorNunca: se copió al ejecutableError al compilar
Dinámica (tema 33)El enlazador anota, el cargador traeAntes de mainEl programa no arranca
Carga dinámica (este tema)El propio programaCuando el código llama a dlopenUna llamada devuelve NULL y el programa decide qué hacer

La última fila es la que importa: la ausencia de una biblioteca deja de ser un error fatal y pasa a ser una situación que el programa puede manejar (ofrecer menos funciones, probar otra alternativa, avisar al usuario). La biblioteca cargada tampoco necesita existir al compilar: el programa solo conoce su nombre y la forma de lo que va a buscar. Eso es lo que permite que un tercero escriba una extensión años después de publicado el programa.

Línea de tiempo de un proceso que usa dlopen. El programa arranca con el cargador, que trae las bibliotecas NEEDED como libc.so.6. Después main corre. En medio de la ejecución, una llamada a dlopen carga plugin.so, dlsym devuelve la dirección de una función, el programa la llama y dlclose suelta la biblioteca. El archivo plugin.so no aparece en la lista NEEDED del ejecutable ld.so carga NEEDED (libc) main() corre el programa decide cuándo y qué cargar dlopen abre dlsym busca llamada usa dlclose suelta plugin.so no figura en NEEDED: se pide mientras corre.
El cargador del sistema trae lo anotado; dlopen trae lo que el programa pide en pleno main.

2. El ciclo básico: dlopen, dlsym, dlclose

Las funciones se declaran en <dlfcn.h> y pertenecen a POSIX:

FunciónQué hace
void *dlopen(const char *archivo, int modo)Carga la biblioteca y devuelve un manejador opaco, o NULL si falla.
void *dlsym(void *h, const char *nombre)Busca un símbolo (función o variable) por nombre y devuelve su dirección, o NULL.
int dlclose(void *h)Cierra el manejador; descarga la biblioteca cuando ya nadie la usa.
char *dlerror(void)Describe en texto el último error de las tres anteriores, o NULL si no hubo.

El ejemplo más pequeño posible usa una biblioteca que ya existe en el sistema, libm, para calcular un coseno sin que el programa la tenga en NEEDED:

coseno.c
#define _POSIX_C_SOURCE 200809L
#include <dlfcn.h>
#include <stdio.h>

int main(void)
{
    void *h = dlopen("libm.so.6", RTLD_NOW);
    if (!h) {
        fprintf(stderr, "%s\n", dlerror());
        return 1;
    }

    double (*cf)(double);
    *(void **)(&cf) = dlsym(h, "cos");
    if (!cf) {
        fprintf(stderr, "%s\n", dlerror());
        return 1;
    }
    printf("cos(0) = %.1f\n", cf(0.0));
    printf("cos(3.14159265) = %.4f\n", cf(3.14159265));

    dlsym(h, "no_hay");
    const char *e = dlerror();
    printf("dlerror: %s\n", e ? e : "(null)");
    printf("segundo dlerror: %s\n", dlerror() ? "texto" : "(null)");
    dlclose(h);
    return 0;
}
terminal
$ gcc -std=c17 -Wall -Wextra -o coseno coseno.c
$ ./coseno
cos(0) = 1.0
cos(3.14159265) = -1.0000
dlerror: /usr/lib/x86_64-linux-gnu/libm.so.6: undefined symbol: no_hay
segundo dlerror: (null)
$ readelf -d coseno | grep NEEDED
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]

Tres detalles de este programa merecen explicación:

Una nota histórica sobre la compilación: antes de glibc 2.34 las funciones dl* vivían en una biblioteca aparte y había que enlazar con -ldl. Hoy están dentro de libc y se puede omitir. Se comprueba que el resultado es el mismo (la opción se acepta y no agrega ninguna dependencia):

terminal
$ gcc -std=c17 -Wall -Wextra -o coseno2 coseno.c -ldl
$ readelf -d coseno2 | grep NEEDED
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]

Si el código tiene que compilar también en sistemas más antiguos, se deja -ldl, que no hace daño.

3. Cuando falla: dlerror

Las funciones de este grupo no usan errno: avisan el fallo con NULL y dejan el motivo en un texto que se obtiene con dlerror. Hay dos reglas que conviene recordar:

4. Un programa con plugins

Un esquema de extensiones necesita un contrato entre el programa (el anfitrión) y cada módulo. El más simple es una función con un nombre fijo que devuelve una estructura de descripción. Lo ponemos en un encabezado que ven los dos lados:

inc/plugin.h
#ifndef PLUGIN_H
#define PLUGIN_H
#include <stddef.h>

struct plugin {
    const char *nombre;
    int version;
    int (*ejecutar)(const char *entrada, char *salida, size_t n);
};

typedef const struct plugin *(*plugin_info_fn)(void);

/* la ofrece el anfitrión */
void host_log(const char *msg);
#endif

Cada plugin exporta plugin_info y lo demás es privado (static). Dos plugins mínimos: saludo antepone «hola, » y mayus pasa a mayúsculas y le avisa al anfitrión, con host_log, de que se ejecutó:

saludo.c y mayus.c
/* saludo.c */
#include <stdio.h>
#include "plugin.h"

static int ejecutar(const char *entrada, char *salida, size_t n)
{
    return snprintf(salida, n, "hola, %s", entrada);
}

static const struct plugin info = {"saludo", 1, ejecutar};

const struct plugin *plugin_info(void)
{
    return &info;
}

/* mayus.c */
#include <ctype.h>
#include <stdio.h>
#include "plugin.h"

static int ejecutar(const char *entrada, char *salida, size_t n)
{
    size_t i = 0;

    host_log("mayus: ejecutando");
    for (; entrada[i] && i + 1 < n; i++)
        salida[i] = (char)toupper((unsigned char)entrada[i]);
    salida[i] = '\0';
    return (int)i;
}

static const struct plugin info = {"mayus", 2, ejecutar};

const struct plugin *plugin_info(void)
{
    return &info;
}

El anfitrión recibe por la línea de órdenes los plugins a cargar. Para cada uno: abre, busca plugin_info, la llama, usa el plugin y lo cierra. Cada paso que puede fallar se comprueba, porque aquí un fallo es normal y esperable:

host.c
#define _POSIX_C_SOURCE 200809L
#include <dlfcn.h>
#include <stdio.h>
#include "plugin.h"

void host_log(const char *msg)
{
    fprintf(stderr, "[host] %s\n", msg);
}

int main(int argc, char **argv)
{
    for (int i = 1; i < argc; i++) {
        void *h = dlopen(argv[i], RTLD_NOW);
        if (!h) {
            fprintf(stderr, "no se pudo cargar: %s\n", dlerror());
            continue;
        }

        plugin_info_fn info = (plugin_info_fn)dlsym(h, "plugin_info");
        if (!info) {
            fprintf(stderr, "%s no es un plugin: %s\n", argv[i], dlerror());
            dlclose(h);
            continue;
        }

        const struct plugin *p = info();
        char salida[64];
        p->ejecutar("mundo", salida, sizeof salida);
        printf("[%s v%d] %s\n", p->nombre, p->version, salida);
        dlclose(h);
    }
    return 0;
}

Los plugins se compilan como bibliotecas dinámicas (-fPIC -shared, tema 33) y el anfitrión como un programa común. No hay ninguna relación en tiempo de enlace entre ellos: solo comparten el encabezado.

terminal
$ mkdir plugins
$ gcc -std=c17 -Wall -Wextra -Iinc -fPIC -shared -o plugins/saludo.so saludo.c
$ gcc -std=c17 -Wall -Wextra -Iinc -fPIC -shared -o plugins/mayus.so mayus.c
$ gcc -std=c17 -Wall -Wextra -Iinc -rdynamic -o host host.c
$ ./host ./plugins/saludo.so ./plugins/mayus.so
[host] mayus: ejecutando
[saludo v1] hola, mundo
[mayus v2] MUNDO
$ readelf -d host | grep NEEDED; ldd host | grep -c saludo
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
0

El anfitrión funciona con los plugins que se le indiquen y no tiene ninguna dependencia de ellos: readelf y ldd no los mencionan. (La línea [host] sale primero porque va a stderr, sin búfer, mientras que stdout conectada a una tubería se vacía al final.) Agregar un tercer plugin no exige recompilar nada del anfitrión: basta con otro .so que cumpla el contrato. En un programa real, en lugar de recibir los nombres por argv, el anfitrión recorrería un directorio de plugins con opendir y readdir y cargaría cada .so que encuentre.

5. El anfitrión también ofrece símbolos: -rdynamic

En la compilación anterior hay una opción que no se explicó: -rdynamic. El plugin mayus llama a host_log, una función que está en el ejecutable, no en una biblioteca. Para que el cargador pueda resolver esa referencia al abrir el plugin, el ejecutable tiene que haber puesto host_log en su tabla de símbolos dinámicos, y por defecto no lo hace: solo exporta lo que alguna biblioteca ya enlazada le pide. Sin la opción:

terminal · el mismo anfitrión, compilado sin y con -rdynamic
$ gcc -std=c17 -Wall -Wextra -Iinc -o host0 host.c
$ ./host0 ./plugins/saludo.so ./plugins/mayus.so
no se pudo cargar: ./plugins/mayus.so: undefined symbol: host_log
[saludo v1] hola, mundo
$ nm -D host0 | grep ' T host_log'; echo "rc=$?"
rc=1
$ nm -D host | grep -E ' T host_log| T main'
0000000000001209 T host_log
000000000000123e T main

Con host0, saludo se carga (no necesita nada del anfitrión) y mayus no: undefined symbol: host_log. La tabla dinámica de host0 no contiene host_log; la de host, sí, junto con main y todo lo demás. -rdynamic es el nombre de gcc para -Wl,--export-dynamic: exporta todos los símbolos globales del ejecutable.

Cuando se quiere exponer solo una interfaz acotada (lo sano en un programa grande, para no revelar ni comprometer todo el programa), se puede usar una lista explícita con -Wl,--dynamic-list=archivo, o marcar las funciones con __attribute__((visibility("default"))) compilando todo lo demás con -fvisibility=hidden. La idea es siempre la misma: el contrato del anfitrión con sus plugins es una API pública, y hay que decidir qué entra.

6. Nombre o ruta, y los demás errores de carga

El primer argumento de dlopen se interpreta de dos maneras según tenga o no una barra:

terminal · el mismo archivo con y sin barra
$ cp plugins/saludo.so saludo.so
$ ./host saludo.so
no se pudo cargar: saludo.so: cannot open shared object file: No such file or directory
$ ./host ./saludo.so
[saludo v1] hola, mundo
$ LD_LIBRARY_PATH=. ./host saludo.so
[saludo v1] hola, mundo

Para un plugin, que se identifica por su ubicación, lo normal es construir la ruta completa (directorio de plugins y nombre) y pasarla con barra. Una trampa de seguridad: si el programa llama a dlopen("algo.so") con un nombre sin ruta, el código que se ejecuta depende de variables de entorno y de lo que haya en los directorios de búsqueda. Cargar un módulo desde un directorio donde otros usuarios pueden escribir es ejecutar código ajeno con tus permisos.

La otra familia de errores es la de los archivos que existen pero no sirven. dlerror los distingue:

terminal · un programa mínimo que solo hace dlopen y muestra dlerror
$ echo "texto cualquiera" > falso.so; : > vacio.so
$ ./cargar ./falso.so
./falso.so: file too short
$ ./cargar ./vacio.so
./vacio.so: file too short
$ ./host ./plugins/noexiste.so
no se pudo cargar: ./plugins/noexiste.so: cannot open shared object file: No such file or directory

Y el caso de un .so válido que no cumple el contrato, porque no tiene plugin_info. Se carga bien, pero dlsym falla, por eso el anfitrión comprueba el segundo paso:

terminal
$ echo 'int otra(void) { return 1; }' > vacio.c
$ gcc -shared -fPIC -o plugins/vacio.so vacio.c
$ ./host ./plugins/vacio.so
./plugins/vacio.so no es un plugin: ./plugins/vacio.so: undefined symbol: plugin_info

7. RTLD_NOW y RTLD_LAZY

El segundo argumento de dlopen es obligatorio e indica cuándo se resuelven los símbolos de la biblioteca que apuntan al exterior:

La diferencia se ve con un plugin defectuoso, que llama a una función que nadie define. Para el ejemplo se enlaza con -Wl,-z,lazy, porque en una distribución como Ubuntu el enlazador pide por defecto resolver todo al cargar, y eso anularía el modo perezoso:

roto.c y terminal
/* roto.c: ejecutar() llama a no_existe(), que no está en ninguna parte */
int no_existe(void);
static int ejecutar(const char *e, char *s, size_t n)
{
    (void)e; (void)s; (void)n;
    return no_existe();
}

$ gcc -std=c17 -Iinc -fPIC -shared -Wl,-z,lazy -o plugins/roto.so roto.c
$ ./lazy ./plugins/roto.so; echo "rc=$?"
dlopen: ./plugins/roto.so: undefined symbol: no_existe
rc=1
$ ./lazy ./plugins/roto.so lazy; echo "rc=$?"
./lazy: symbol lookup error: ./plugins/roto.so: undefined symbol: no_existe
rc=127

(El programa lazy.c es el anfitrión reducido: usa RTLD_NOW o, si hay un segundo argumento, RTLD_LAZY; llama a ejecutar y escribe «dlopen ok» y «ejecutar ok».) Con RTLD_NOW el problema se detecta en la carga, donde el anfitrión puede manejarlo. Con RTLD_LAZY el dlopen tiene éxito, y el fallo llega cuando ejecutar llama a no_existe: el cargador no puede devolver un error a esa llamada, y termina el proceso con symbol lookup error y estado 127. Para un programa con plugins de terceros, RTLD_NOW es la elección segura: un plugin roto es rechazado y el anfitrión sigue.

Hay además dos indicadores que se combinan con los anteriores con |. RTLD_LOCAL (el valor por defecto) mantiene los símbolos de la biblioteca privados: solo se obtienen con dlsym sobre su manejador. RTLD_GLOBAL los publica para las bibliotecas que se carguen después, lo cual es útil cuando un plugin depende de otra biblioteca que se cargó antes, y arriesgado, porque dos módulos pueden terminar pisándose nombres.

8. Descargar: contador de referencias y constructores

Cargar la misma biblioteca dos veces no la carga dos veces: dlopen devuelve el mismo manejador y aumenta un contador de referencias, y dlclose lo disminuye. La biblioteca solo se descarga cuando el contador llega a cero. Para verlo hace falta que la biblioteca avise cuando se carga y se descarga, y el compilador lo permite con funciones marcadas como constructor y destructor: gcc las ejecuta justo después de cargar la biblioteca y justo antes de descargarla.

cuenta.c y ref.c
/* cuenta.c */
#include <stdio.h>

__attribute__((constructor))
static void al_cargar(void)
{
    puts("  [cuenta.so] constructor");
}

__attribute__((destructor))
static void al_descargar(void)
{
    puts("  [cuenta.so] destructor");
}

int doble(int x) { return 2 * x; }

/* ref.c */
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>

static int cargada(void)
{
    void *h = dlopen("./cuenta.so", RTLD_NOW | RTLD_NOLOAD);

    if (h)
        dlclose(h);
    return h != NULL;
}

int main(void)
{
    printf("al inicio, cargada: %s\n", cargada() ? "si" : "no");
    void *a = dlopen("./cuenta.so", RTLD_NOW);
    void *b = dlopen("./cuenta.so", RTLD_NOW);
    printf("mismo manejador: %s\n", a == b ? "si" : "no");

    puts("primer dlclose");
    dlclose(a);
    printf("cargada: %s\n", cargada() ? "si" : "no");
    int (*f)(int);
    *(void **)(&f) = dlsym(b, "doble");
    printf("doble(21) = %d\n", f(21));
    puts("segundo dlclose");
    dlclose(b);
    printf("cargada: %s\n", cargada() ? "si" : "no");
    puts("fin");
    return 0;
}
terminal
$ gcc -std=c17 -Wall -Wextra -fPIC -shared -o cuenta.so cuenta.c
$ gcc -std=c17 -Wall -Wextra -o ref ref.c && ./ref
al inicio, cargada: no
  [cuenta.so] constructor
mismo manejador: si
primer dlclose
cargada: si
doble(21) = 42
segundo dlclose
  [cuenta.so] destructor
cargada: no
fin

La secuencia cuenta la historia completa:

Los constructores son el lugar natural para que un plugin se registre en el anfitrión o inicialice su estado, y los destructores para liberar recursos. Tiene un límite práctico: dlclose no promete que el código desaparezca de la memoria (hay bibliotecas que se marcan como no descargables, y las que dejan hilos o funciones de callback registradas no pueden irse sin riesgo). Por eso, tras el último dlclose se deja de usar todo lo que vino de la biblioteca (punteros a funciones, a datos y a textos), sin importar si el código sigue o no en memoria.

9. RTLD_NEXT y RTLD_DEFAULT

Además de un manejador real, dlsym acepta dos valores especiales (requieren _GNU_SOURCE en glibc):

RTLD_DEFAULT confirma qué función es «la de verdad» para el programa:

def.c y terminal
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>

int main(void)
{
    void *f = dlsym(RTLD_DEFAULT, "printf");
    printf("printf encontrada: %s\n", f ? "si" : "no");
    printf("es la misma: %s\n", f == (void *)printf ? "si" : "no");
    printf("no_hay: %s\n", dlsym(RTLD_DEFAULT, "no_hay") ? "si" : "no");
    return 0;
}

$ gcc -std=c17 -Wall -Wextra -o def def.c && ./def
printf encontrada: si
es la misma: si
no_hay: no

RTLD_NEXT completa la técnica del tema 33 (LD_PRELOAD), que dejaba un problema: la función sustituta ganaba al original, pero ¿cómo se llama a la original desde ella? Con RTLD_NEXT. Un ejemplo sencillo envuelve puts para que anteponga una línea de traza, sin tocar el programa:

traza.c, hola.c y terminal
/* traza.c */
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>

int puts(const char *s)
{
    static int (*real)(const char *);

    if (!real)
        *(void **)(&real) = dlsym(RTLD_NEXT, "puts");
    real("[traza]");
    return real(s);
}

/* hola.c */
#include <stdio.h>
int main(void) { puts("hola"); return 0; }

$ gcc -std=c17 -Wall -Wextra -fPIC -shared -o traza.so traza.c
$ gcc -std=c17 -Wall -Wextra -o hola hola.c
$ ./hola
hola
$ LD_PRELOAD=./traza.so ./hola
[traza]
hola

Sin LD_PRELOAD el programa imprime solo hola. Con la biblioteca precargada, la llamada a puts del programa llega a la versión de traza.so, que busca con RTLD_NEXT la siguiente definición (la de libc), imprime la traza con ella y luego imprime el mensaje original. Es la base de herramientas que registran llamadas a malloc, open o write, de los depuradores de memoria y de los simuladores de fallos. Hay que tener cuidado con la recursión (en un envoltorio de malloc, usar printf puede volver a llamar a malloc) y con que el envoltorio sea reentrante y seguro frente a hilos.

10. Errores frecuentes

ErrorQué pasaCorrección
No comprobar el resultado de dlopen o dlsymDesreferenciar NULL y caer con SIGSEGV si el plugin falta o no cumple el contrato.Comprobar cada paso y mostrar dlerror().
Pasar "saludo.so" esperando que busque en el directorio actualcannot open shared object file: sin barra se busca como biblioteca del sistema."./saludo.so" o una ruta completa.
Olvidar -rdynamic en el anfitrión que ofrece funciones a los pluginsundefined symbol: … al cargar el plugin.-rdynamic (o --dynamic-list, o visibilidad explícita).
Llamar a dlerror() dos veces esperando el mismo textoLa segunda devuelve NULL.Guardar o imprimir el texto de inmediato.
Usar el puntero devuelto por dlsym después de dlclosePuede apuntar a memoria ya desmapeada: SIGSEGV.No usar nada de la biblioteca tras cerrar el último manejador.
Usar RTLD_LAZY con plugins de tercerosUn símbolo faltante mata el proceso con symbol lookup error, mucho después del dlopen.RTLD_NOW.
Olvidar que RTLD_NOLOAD y cada dlopen suman una referenciaLa biblioteca nunca se descarga.Un dlclose por cada dlopen exitoso.
Pedir "libm.so" en un programa que se instala en otras máquinasEse enlace suele estar solo con los paquetes de desarrollo.Pedir el soname: "libm.so.6".
Convertir el void * con un cast a un tipo de función distinto del realComportamiento indefinido; no hay comprobación de tipos en dlsym.Un solo typedef compartido en el encabezado del contrato.
Cargar plugins desde un directorio donde escriben otrosEjecutan código ajeno con tus permisos.Directorios propios del programa, con permisos restringidos.
Pedir RTLD_NEXT o RTLD_NOLOAD sin _GNU_SOURCEError de compilación: el identificador no existe.Definir _GNU_SOURCE antes de los #include (no es parte de POSIX).
Un envoltorio con RTLD_NEXT que usa funciones que él mismo reemplazaRecursión infinita.Evitar llamar a funciones envueltas dentro del envoltorio.
Cargar y descargar plugins desde varios hilos sin coordinarUn hilo puede usar un plugin que otro acaba de cerrar.Proteger con un mutex la lógica propia de carga y descarga (en glibc dlerror es por hilo).

11. Resumen

12. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿En qué se diferencia enlazar una biblioteca dinámica con -l de cargarla con dlopen?

Con -l, el nombre queda anotado en el ejecutable (NEEDED) y el cargador la trae antes de main; si falta, el programa no arranca. Con dlopen, el programa pide la biblioteca mientras corre, no figura en NEEDED, puede no existir al compilar, y si falta recibe NULL y decide qué hacer.

2. ¿Qué devuelve dlopen y para qué se usa?

Un manejador opaco (void *), o NULL si falla. Solo sirve para pasarlo a dlsym, para buscar símbolos en esa biblioteca, y a dlclose, para soltarla.

3. ¿Por qué la conversión de lo que devuelve dlsym a un puntero a función es delicada en C?

Porque devuelve void * y el estándar de C no define la conversión entre un puntero a objeto y un puntero a función (con -Wpedantic da una advertencia). POSIX sí exige que funcione. El modismo *(void **)(&f) = dlsym(…) evita la advertencia; y el tipo de la función se declara una vez en un typedef compartido, porque dlsym no comprueba que coincida.

4. ¿Cómo se sabe por qué falló dlopen, y qué particularidad tiene dlerror?

Con dlerror(), que devuelve un texto como cannot open shared object file, undefined symbol: … o file too short. Devuelve el error una sola vez: la llamada siguiente da NULL hasta que haya otro fallo, así que hay que usar el texto de inmediato.

5. ¿Cuál es la diferencia entre dlopen("saludo.so", …) y dlopen("./saludo.so", …)?

Con barra es una ruta y se abre ese archivo directamente. Sin barra es un nombre de biblioteca y se busca por RUNPATH, LD_LIBRARY_PATH, la caché de ldconfig y los directorios del sistema, sin mirar el directorio actual; por eso el primero falla con cannot open shared object file aunque el archivo esté al lado.

6. Un plugin llama a una función del programa principal y dlopen falla con undefined symbol. ¿Qué falta?

El ejecutable no exportó esa función a la tabla de símbolos dinámicos. Se compila el anfitrión con -rdynamic (equivale a -Wl,--export-dynamic), o se exporta solo lo necesario con --dynamic-list o con visibilidad explícita.

7. ¿Qué diferencia hay entre RTLD_NOW y RTLD_LAZY, y cuál conviene para plugins de terceros?

RTLD_NOW resuelve todos los símbolos pendientes durante dlopen, y si falta alguno la llamada falla de forma controlada. RTLD_LAZY los resuelve cuando se llaman por primera vez, y un símbolo faltante termina el proceso con symbol lookup error. Para plugins de terceros conviene RTLD_NOW, porque un plugin roto se rechaza sin afectar al anfitrión.

8. Se llama dos veces a dlopen con la misma biblioteca y una vez a dlclose. ¿Sigue cargada? ¿Cuándo corre su destructor?

Sigue cargada: dlopen devolvió el mismo manejador con el contador en 2 y dlclose lo dejó en 1. El destructor corre en el segundo dlclose, cuando el contador llega a cero. El constructor, en cambio, corrió una sola vez, en el primer dlopen.

9. ¿Qué hace RTLD_NOLOAD y qué debe tenerse en cuenta al usarlo?

Pregunta si la biblioteca ya está cargada: devuelve su manejador o NULL, sin cargarla. El manejador cuenta como una referencia más, por lo que hay que cerrarlo con dlclose si no se quiere impedir la descarga.

10. ¿Para qué sirve __attribute__((constructor)) en una biblioteca cargada con dlopen?

Marca una función que se ejecuta automáticamente al cargarse la biblioteca, antes de que dlopen retorne. Sirve para inicializar su estado o registrarse en el anfitrión. Con destructor se hace lo opuesto, al descargarla.

11. ¿Qué es RTLD_NEXT y qué problema resuelve con LD_PRELOAD?

Es un valor especial para dlsym que busca el símbolo en las bibliotecas posteriores a la que hace la llamada. Permite que una función precargada que reemplaza a otra (por ejemplo puts) llame a la original y así sólo agregue su comportamiento, sin duplicar el de la biblioteca real.

12. Se escribe un programa con plugins: ¿qué debe tener el contrato entre el anfitrión y cada módulo?

Un encabezado común con las estructuras y tipos de función; un símbolo de entrada con nombre fijo que el anfitrión busca con dlsym; reglas claras sobre quién reserva y libera la memoria; y, si los plugins llaman al anfitrión, una lista de las funciones que éste exporta. Una versión del contrato en la estructura permite rechazar plugins viejos.

13. ¿Por qué se pide "libm.so.6" y no "libm.so"?

Porque libm.so.6 es el soname y existe en cualquier instalación que ejecute programas; libm.so es el nombre del enlazador, que normalmente trae el paquete de desarrollo y puede no estar en la máquina de destino.

14. ¿Por qué no basta con cerrar la biblioteca y seguir usando un puntero obtenido con dlsym?

Porque al llegar el contador a cero la biblioteca puede desmapearse de la memoria y el puntero queda colgando; usarlo produce una caída. Tras el último dlclose no se usa nada de lo que vino de la biblioteca.

Para practicar

  1. Escribe un programa que cargue libm.so.6 y, con dlsym, calcule sqrt, sin y pow a partir de sus nombres recibidos por argv. ¿Qué pasa si el nombre no existe?
  2. Agrega al anfitrión de este tema un tercer plugin, cuenta_letras, que devuelva en la salida la cantidad de caracteres de la entrada. Comprueba que no hubo que recompilar el anfitrión.
  3. Haz que el anfitrión lea un directorio con opendir/readdir y cargue todos los .so que encuentre. Ignora con un mensaje los que no cumplan el contrato.
  4. Agrega al contrato un campo abi en struct plugin y haz que el anfitrión rechace los plugins con una versión distinta. Prueba con uno compilado contra una versión anterior del encabezado.
  5. Compila el anfitrión sin -rdynamic y el plugin mayus; observa el error. Luego pasa la función a un puntero que el anfitrión entrega al plugin como argumento (por ejemplo, con un campo log en una estructura de contexto) y comprueba que ya no hace falta -rdynamic. ¿Qué diseño prefieres?
  6. Escribe un plugin con un constructor que imprima un mensaje. Cárgalo, ciérralo y vuelve a cargarlo en el mismo programa; ¿cuántas veces corre el constructor? ¿Y si lo cargas dos veces seguidas?
  7. Envuelve malloc con LD_PRELOAD y RTLD_NEXT para contar las llamadas, mostrando el total al final con un destructor. Cuida de no usar printf dentro del envoltorio. Prueba con ls o con un programa tuyo.
  8. Con RTLD_LAZY y -Wl,-z,lazy, prepara un plugin que solo falle si se llama una de sus dos funciones, y muestra que la otra funciona sin problemas.