Выполнение init и fini

Я только что прочитал разделы init и fini в файлах ELF и дал ему попробовать:

#include <stdio.h>
int main(){
  puts("main");
  return 0;
}

void init(){
  puts("init");
}
void fini(){
  puts("fini");
}

Если я выполняю gcc -Wl,-init,init -Wl,-fini,fini foo.c и запускаю результат, часть "init" не печатается:

$ ./a.out
main
fini

Не запущена ли часть init, или она не может каким-то образом напечатать?

Есть ли какая-либо "официальная" документация об элементах init/fini?

man ld говорит:

 -init=name
     When creating an ELF executable or shared object, call
     NAME when the executable or shared object is loaded, by
     setting DT_INIT to the address of the function.  By
     default, the linker uses "_init" as the function to call.

Не означает ли это, что достаточно назвать функцию init _init? (Если я gcc жалуется на несколько определений.)

Ответы

Ответ 1

Не делай этого; пусть ваш компилятор и компоновщик заполняют разделы по своему усмотрению.

Вместо этого отметьте свои функции соответствующими атрибутами , чтобы компилятор и компоновщик поместили их в правильные разделы.

Например,

static void before_main(void) __attribute__((constructor));
static void after_main(void) __attribute__((destructor));

static void before_main(void)
{
    /* This is run before main() */
}

static void after_main(void)
{
    /* This is run after main() returns (or exit() is called) */
}

Вы также можете назначить приоритет (скажем, __attribute__((constructor (300)))), целое число от 101 до 65535 включительно, причем функции, имеющие номер меньшего приоритета, запускаются первыми.

Обратите внимание, что для иллюстрации я отметил функции static. То есть, функции не будут видны вне области файла. Функции не обязательно должны быть экспортированы для автоматического вызова символов.


Для тестирования я предлагаю сохранить следующее в отдельном файле, например tructor.c:

#include <unistd.h>
#include <string.h>
#include <errno.h>

static int outfd = -1;

static void wrout(const char *const string)
{
    if (string && *string && outfd != -1) {
        const char       *p = string;
        const char *const q = string + strlen(string);

        while (p < q) {
            ssize_t n = write(outfd, p, (size_t)(q - p));
            if (n > (ssize_t)0)
                p += n;
            else
            if (n != (ssize_t)-1 || errno != EINTR)
                break;
        }
    }
}

void before_main(void) __attribute__((constructor (101)));
void before_main(void)
{
    int saved_errno = errno;

    /* This is run before main() */
    outfd = dup(STDERR_FILENO);
    wrout("Before main()\n");

    errno = saved_errno;
}

static void after_main(void) __attribute__((destructor (65535)));
static void after_main(void)
{
    int saved_errno = errno;

    /* This is run after main() returns (or exit() is called) */
    wrout("After main()\n");

    errno = saved_errno;
}

чтобы вы могли компилировать и связывать его как часть любой программы или библиотеки. Чтобы скомпилировать его как общую библиотеку, используйте, например,

gcc -Wall -Wextra -fPIC -shared tructor.c -Wl,-soname,libtructor.so -o libtructor.so

и вы можете вставить его в любую динамически связанную команду или двоичный код с помощью

LD_PRELOAD=./libtructor.so some-command-or-binary

Функции сохраняют errno неизменными, хотя на практике это не имеет значения, и используйте syscall для низкоуровневого write() для вывода сообщений на стандартную ошибку. Начальная стандартная ошибка дублируется на новый дескриптор, потому что во многих случаях стандартная ошибка сама закрывается до того, как последний глобальный деструктор - наш деструктор здесь - запускается.

(Некоторые параноидальные двоичные файлы, обычно чувствительные к безопасности, закрывают все дескрипторы, о которых они не знают, поэтому вы можете не видеть сообщение After main() во всех случаях.)

Ответ 2

Это не ошибка в ld, а в стартовом коде glibc для основного исполняемого файла. Для общих объектов вызывается функция, заданная опцией -init.


Это является фиксацией для ld добавления параметров -init и -fini.
Функция _init программы не вызывается из файла glibc-2.21/elf/dl-init.c:58 в записи DT_INIT динамическим компоновщиком, но вызывается из __libc_csu_init в файле glibc-2.21/csu/elf-init.c:83 с помощью основного исполняемого файла.

То есть, указатель функции в DT_INIT программы игнорируется при запуске.

Если вы компилируете с помощью -static, fini также не вызывается.

DT_INIT и DT_FINI определенно не должны использоваться, поскольку они old-style, см. строку 255.

Следующие работы:

#include <stdio.h>

static void preinit(int argc, char **argv, char **envp) {
    puts(__FUNCTION__);
}

static void init(int argc, char **argv, char **envp) {
    puts(__FUNCTION__);
}

static void fini(void) {
    puts(__FUNCTION__);
}


__attribute__((section(".preinit_array"), used)) static typeof(preinit) *preinit_p = preinit;
__attribute__((section(".init_array"), used)) static typeof(init) *init_p = init;
__attribute__((section(".fini_array"), used)) static typeof(fini) *fini_p = fini;

int main(void) {
    puts(__FUNCTION__);
    return 0;
}

$ gcc -Wall a.c
$ ./a.out
preinit
init
main
fini
$