Like others said, a program might call via _exit()
, _Exit()
or abort()
and your destructors won't even notice. To solve these cases you could override these functions by just writing a wrapper like the following the example below:
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);
}
But this wouldn't solve all the possibilities of a program escaping without your library's knowledge, because those are not the only exit points an application could have. It could also trigger the exit
system call via the syscall()
function and to avoid it you would have to wrap it too.
Another way a program could exit is by receiving an unhandled signal, so you should also handle (or wrap?) all signals that could trigger the death of a program. Read the signal(2)
man page for more information but please be aware that signals like SIGKILL
(9) cannot be handled and an application could destroy itself by calling kill()
. With that being said and unless you don't expect to handle insane applications written by crazy monkeys you should wrap kill()
too.
Another system call you'd have to wrap is execve()
.
Anyway, a system call (like _exit
) could also be triggered directly via an assembly int 0x80
instruction or the obsolete _syscallX()
macro. How'd you wrap it if not from outside the application (like strace
or valgrind
)? Well, if you expect this kind of behaviour in you programs I suggest you drop the LD_PRELOAD
technique and start thinking about doing like strace
and valgrind
do (using ptrace()
from another process) or creating a Linux kernel module to trace it.