Home > Software engineering >  `__attribute__((destructor))` not running in some cases?
`__attribute__((destructor))` not running in some cases?

Time:05-11

I am writing a toy malloc(3) implementation, loaded with LD_PRELOAD, as an exercise. I have a function annotated with __attribute__((destructor)) to dump a list of allocations and their status on exit for debugging purposes but I found it doesn't run in some cases. Specifically, it does run on locally-compiled code but not against system binaries like /bin/ls (on Arch Linux). A constructor-tagged function does work for both cases, though.

A simple repro of the problem is:

main.c:

#include <stdio.h>

// compile with: clang -o main main.c

int main() {
    printf("main\n");
}

wrap.c:

#include <stdio.h>

// compile with: clang -o wrap -shared -fPIC wrap.c

void __attribute__((constructor)) say_hi() {
    printf("hi y'all\n");
}

void __attribute__((destructor)) say_bye() {
    printf("bye y'all\n");
}

Destructor runs with main.c:

$ LD_PRELOAD=./wrap ./main
hi y'all
main
bye y'all

Destructor doesn't run with /bin/ls:

$ LD_PRELOAD=./wrap /bin/ls
hi y'all
main  main.c  wrap  wrap.c

I can't work out how to debug this. LD_DEBUG=all doesn't show anything useful. ldd main and ldd /bin/ls look comparable. Both binaries are dynamically linked. The GCC docs don't list any caveats I should be aware of, as far as I can see.

CodePudding user response:

For some reason, GNU ls closes stdout before it exits, via an atexit handler that presumably gets run before your destructor.

So probably your destructor runs just fine, but doesn't print anything because you are writing to a stream that is closed.

You'll probably need to have your destructor, and your wrapper in general, do its logging in some more robust fashion. Opening a raw fd and write()ing to it would be better, but even so, some programs will do for (i = 0; i < 1024; i ) close(i); which would close your log fd as well. The only really safe way might be to open/write/close every time, or to maintain your own log buffer somewhere in memory and manually write it out when full.


From comments, the likely reason is that stdout may be directed to a file, and since it is buffered, some of the data may not be written out until the stream is closed. An error could occur at that point (full disk, I/O error, etc). If ls were to leave the closing of stdout to the C library startup/shutdown code as your simple main.c does, then this error would go undetected and ls would still exit with status 0; there would be no way for the parent process to know that the output file was incomplete. So by calling fclose(stdout) explicitly, ls can handle the failure, report it if possible, and exit with nonzero status to ensure that the parent knows not to trust the output.

As ShadowRanger points out, all the GNU coreutils have this behavior.

  • Related