c++ - LD_PRELOAD no funciona como se esperaba
linux gcc (3)
Como han dicho otros, un programa puede llamar a través de _exit()
, _Exit()
o abort()
y sus destructores ni siquiera lo notarán. Para resolver estos casos, puede anular estas funciones simplemente escribiendo una envoltura como la siguiente en el siguiente ejemplo:
void
_exit(int status)
{
void (*real__exit)(int) __attribute__((noreturn));
const char *errmsg;
/* Here you should call your "destructor" function. */
destruct();
(void)dlerror();
real__exit = (void(*)(int))dlsym(RTLD_NEXT, "_exit");
errmsg = dlerror();
if (errmsg) {
fprintf(stderr, "dlsym: _exit: %s/n", errmsg);
abort();
}
real__exit(status);
}
Pero esto no resolvería todas las posibilidades de que un programa se escape sin el conocimiento de su biblioteca, porque esos no son los únicos puntos de salida que podría tener una aplicación. También podría desencadenar la llamada al sistema de exit
a través de la función syscall()
y para evitarlo, también tendría que syscall()
.
Otra forma en que un programa podría salir es mediante la recepción de una señal no controlada, por lo que también debería manejar (o ajustar) todas las señales que podrían desencadenar la muerte de un programa. Lea la página del manual de signal(2)
para obtener más información, pero tenga en cuenta que señales como SIGKILL
(9) no se pueden manejar y que una aplicación podría destruirse llamando a kill()
. Habiendo dicho eso, ya menos que no esperes manejar aplicaciones de locos escritas por monos locos, deberías envolver también kill()
.
Otra llamada al sistema que tendrías que ajustar es execve()
.
De todos modos, una llamada al sistema (como _exit
) también podría ser activada directamente a través de una instrucción int 0x80
o la macro _syscallX()
obsoleta. ¿Cómo lo envolviste si no fuera de la aplicación (como strace
o valgrind
)? Bueno, si esperas este tipo de comportamiento en tus programas, te sugiero que abandones la técnica LD_PRELOAD
y comiences a pensar en hacer como strace
y valgrind
do (usando ptrace()
de otro proceso) o crear un módulo del kernel de Linux para rastrearlo .
Considere la siguiente biblioteca que se puede precargar antes de cualquier ejecución del programa:
// g++ -std=c++11 -shared -fPIC preload.cpp -o preload.so
// LD_PRELOAD=./preload.so <command>
#include <iostream>
struct Goodbye {
Goodbye() {std::cout << "Hello/n";}
~Goodbye() {std::cout << "Goodbye!/n";}
} goodbye;
El problema es que, si bien siempre se llama al constructor de la variable global goodbye
se llama al destructor para algunos programas, como ls
:
$ LD_PRELOAD=./preload.so ls
Hello
Para algunos otros programas, el destructor se llama como se espera:
$ LD_PRELOAD=./preload.so man
Hello
What manual page do you want?
Goodbye!
¿Puedes explicar por qué no se llama al destructor en el primer caso? EDITAR: la pregunta anterior ya ha sido respondida, es decir, un programa bien podría usar _exit (), abort () para salir.
Sin embargo:
¿Hay alguna forma de forzar la llamada de una función determinada cuando se cierra un programa precargado?
Si el programa sale a través de _exit
(POSIX) o _Exit
(C99) o terminación anormal del programa ( abort
, señales fatales, etc.), entonces no hay forma de llamar a los destructores. No veo ninguna manera de evitar esto.
ls
tiene atexit (close_stdout);
como su código de inicialización. Cuando finaliza, cierra stdout (es decir, close(1)
), por lo que sus operaciones cout
, printf
o write(1, ...
no imprimirán nada. No significa que no se haya llamado a destructor. Puede verificar esto con Por ejemplo, creando un nuevo archivo en tu destructor.
http://git.savannah.gnu.org/cgit/coreutils.git/tree/src/ls.c#n1285 aquí está la línea en GNU coreutils ls.
No es solo ls
, la mayoría de los coreutils hacen eso. Desafortunadamente, no sé la razón exacta por la que prefieren cerrarla.
Nota al margen sobre cómo se pudo encontrar esto (o al menos lo que hice): puede ser útil la próxima vez o con un programa sin acceso al código fuente:
El mensaje del destructor se imprime con /bin/true
(el programa más simple que se me /bin/true
), pero no se imprime con ls
o df
. Comencé con strace /bin/true
y strace /bin/ls
y strace /bin/ls
últimas llamadas al sistema. Se muestra close(1)
y close(2)
para ls
, pero ninguno para true
. Después de eso, las cosas empezaron a tener sentido y simplemente tuve que verificar que se llamaba al destructor.