1. Cargar a pedido
Comparemos los tres momentos en que un programa puede «tomar» el código de una biblioteca:
| Forma | Quién la decide | Cuándo se carga | Si falta |
|---|---|---|---|
| Estática (tema 32) | El enlazador | Nunca: se copió al ejecutable | Error al compilar |
| Dinámica (tema 33) | El enlazador anota, el cargador trae | Antes de main | El programa no arranca |
| Carga dinámica (este tema) | El propio programa | Cuando el código llama a dlopen | Una 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.
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ón | Qué 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:
#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;
}
$ 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:
- El nombre. Se pasa
"libm.so.6", el soname completo, no"libm.so": el enlace sin versión suele pertenecer al paquete de desarrollo y puede no existir en una máquina que solo ejecuta. Un programa que carga a pedido pide siempre el soname (tema 33). La búsqueda sigue el orden del cargador:RUNPATH,LD_LIBRARY_PATH, la caché deldconfigy los directorios del sistema. - El manejador. Es un valor opaco que solo sirve para pasarlo a
dlsymydlclose. No es un puntero a nada que se pueda leer. - La conversión del puntero.
dlsymdevuelvevoid *, pero lo que se quiere es un puntero a función, y el lenguaje C no define la conversión entre ambos (POSIX sí obliga a que funcione). Con-Wpedanticel cast directo da la advertenciaISO C forbids conversion of object pointer to function pointer type. El modismo*(void **)(&cf) = dlsym(...)escribe la dirección sobre el puntero a función sin pasar por la conversión y no genera avisos; el cast directo,(plugin_info_fn)dlsym(…), es lo que verás en la mayoría del código y funciona igual en cualquier plataforma POSIX.
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):
$ 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:
dlerrordevuelve el error una sola vez: después de leerlo, la siguiente llamada devuelveNULLhasta que ocurra otro fallo. En la salida anterior, el segundodlerrordio(null). Hay que copiar o imprimir el texto de inmediato, y no llamarlo «para ver si hubo error» antes de tiempo.- Un
NULLdedlsymno siempre es un error: un símbolo puede tener, legítimamente, el valor cero (una variable puntero inicializada enNULL, por ejemplo). La forma estrictamente correcta de distinguirlo es limpiar el estado con una llamada previa adlerror(), llamar adlsymy volver a mirardlerror(). Para funciones, que nunca valen cero, basta con comprobar el resultado, como hicimos.
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:
#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 */
#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:
#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.
$ 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:
$ 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:
- Si contiene una
/se toma como ruta: absoluta o relativa al directorio actual, sin más búsqueda."./plugins/saludo.so"y"plugins/saludo.so"funcionan igual. - Si no la contiene, se trata como un nombre de biblioteca y se busca como lo hace el cargador al arrancar (
RUNPATH,LD_LIBRARY_PATH, caché, directorios del sistema). El directorio actual no forma parte de esa búsqueda.
$ 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:
$ 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:
$ 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:
RTLD_NOW: todos los símbolos pendientes se resuelven en eldlopen. Si falta alguno, la llamada falla y devuelveNULL.RTLD_LAZY: las funciones se resuelven la primera vez que se llaman (enlace perezoso). La carga puede salir bien y fallar más tarde.
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: 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 */
#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;
}
$ 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:
- El constructor corre una sola vez, en el primer
dlopen; el segundo solo suma una referencia y devuelve el mismo manejador. - Tras el primer
dlclosela biblioteca sigue cargada ydlsymsobre el otro manejador sigue funcionando. El destructor corre recién con el segundodlclose, y en ese momento la biblioteca desaparece del proceso. RTLD_NOLOAD(extensión de glibc, que requiere_GNU_SOURCE) no carga nada: pregunta si ya está cargada y devuelve su manejador oNULL. Pero cuenta como una referencia más, por eso la funcióncargadala cierra de inmediato. Sin esedlclosela biblioteca nunca se descargaría y el destructor no correría hasta el final del programa.
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: busca el símbolo en el orden normal del cargador, como si lo hubiera usado el programa desde el comienzo, entre el ejecutable y todas las bibliotecas ya cargadas.RTLD_NEXT: busca el símbolo en las bibliotecas que van después de la que hace la llamada. Sirve para que una función que reemplaza a otra pueda llamar a la original.
RTLD_DEFAULT confirma qué función es «la de verdad» para el programa:
#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 */
#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
| Error | Qué pasa | Corrección |
|---|---|---|
No comprobar el resultado de dlopen o dlsym | Desreferenciar 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 actual | cannot 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 plugins | undefined symbol: … al cargar el plugin. | -rdynamic (o --dynamic-list, o visibilidad explícita). |
Llamar a dlerror() dos veces esperando el mismo texto | La segunda devuelve NULL. | Guardar o imprimir el texto de inmediato. |
Usar el puntero devuelto por dlsym después de dlclose | Puede apuntar a memoria ya desmapeada: SIGSEGV. | No usar nada de la biblioteca tras cerrar el último manejador. |
Usar RTLD_LAZY con plugins de terceros | Un 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 referencia | La biblioteca nunca se descarga. | Un dlclose por cada dlopen exitoso. |
Pedir "libm.so" en un programa que se instala en otras máquinas | Ese 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 real | Comportamiento 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 otros | Ejecutan código ajeno con tus permisos. | Directorios propios del programa, con permisos restringidos. |
Pedir RTLD_NEXT o RTLD_NOLOAD sin _GNU_SOURCE | Error 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 reemplaza | Recursión infinita. | Evitar llamar a funciones envueltas dentro del envoltorio. |
| Cargar y descargar plugins desde varios hilos sin coordinar | Un 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
- La carga dinámica permite que el programa decida en ejecución qué biblioteca abrir; la ausencia de una biblioteca deja de ser un error fatal y pasa a ser un caso que se maneja. La biblioteca no figura en
NEEDED. - El ciclo es
dlopen(devuelve un manejador),dlsym(devuelve la dirección de un símbolo) ydlclose;dlerrordevuelve el último error como texto, una sola vez. Se incluye<dlfcn.h>;-ldlya no es necesario en glibc 2.34 o posterior. - Un nombre con
/se toma como ruta; sin/, se busca como lo hace el cargador. Se pide siempre el soname (libm.so.6), no el enlace de desarrollo. - Un esquema de plugins necesita un contrato: un encabezado común, un símbolo de entrada con nombre fijo (
plugin_info) y untypedefdel tipo de función. Cada paso que puede fallar se comprueba. - Si los plugins llaman a funciones del ejecutable, éste se enlaza con
-rdynamicpara exportarlas; si no, eldlopenfalla conundefined symbol. RTLD_NOWresuelve todo en eldlopeny permite rechazar un plugin roto;RTLD_LAZYdifiere la resolución y un símbolo faltante puede terminar el proceso después.RTLD_LOCAL/RTLD_GLOBALcontrolan si los símbolos se publican a las bibliotecas siguientes.- Cada
dlopensuma una referencia y cadadlclosela resta; la biblioteca se descarga al llegar a cero, y entonces corre su destructor (el constructor corre solo en la primera carga).RTLD_NOLOADconsulta sin cargar, pero también suma. dlsym(RTLD_NEXT, …)permite que una función reemplazante llame a la original, la pieza que faltaba para envolver funciones conLD_PRELOAD;RTLD_DEFAULTbusca como lo haría el cargador.
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
- Escribe un programa que cargue
libm.so.6y, condlsym, calculesqrt,sinypowa partir de sus nombres recibidos porargv. ¿Qué pasa si el nombre no existe? - 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. - Haz que el anfitrión lea un directorio con
opendir/readdiry cargue todos los.soque encuentre. Ignora con un mensaje los que no cumplan el contrato. - Agrega al contrato un campo
abienstruct pluginy haz que el anfitrión rechace los plugins con una versión distinta. Prueba con uno compilado contra una versión anterior del encabezado. - Compila el anfitrión sin
-rdynamicy el pluginmayus; observa el error. Luego pasa la función a un puntero que el anfitrión entrega al plugin como argumento (por ejemplo, con un campologen una estructura de contexto) y comprueba que ya no hace falta-rdynamic. ¿Qué diseño prefieres? - 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?
- Envuelve
mallocconLD_PRELOADyRTLD_NEXTpara contar las llamadas, mostrando el total al final con un destructor. Cuida de no usarprintfdentro del envoltorio. Prueba conlso con un programa tuyo. - Con
RTLD_LAZYy-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.