Справочник функций

Ваш аккаунт

Войти через: 
Забыли пароль?
Регистрация
Информацию о новых материалах можно получать и без регистрации:
реклама
Купить pop up стенды в Москве.

Почтовая рассылка

Подписчиков: -1
Последний выпуск: 19.06.2015

Как запускается функция main() в Linux

Источник: http://gazette.linux.ru.net/ Автор: Hyouck "Hawk" Kim
Перевод: Андрей Киселев

Вступление

Так ли прост вопрос: "Как запускается функция main() в Linux"? Для ответа на него я возьму, в качестве примера, простенькую программу на языке C — "simple.c"

main()
{
   return(0);
}

Сборка:

gcc -o simple simple.c

Что находится внутри исполняемого файла?

Для того, чтобы рассмотреть внутреннее устройство исполняемого файла воспользуемся утилитой "objdump"

objdump -f simple

simple:     file format elf32-i386

architecture: i386, flags 0x00000112:

EXEC_P, HAS_SYMS, D_PAGED

start address 0x080482d0

Отсюда видно, что файл, во-первых, имеет формат "ELF32", а во-вторых - адрес запуска программы "0x080482d0"

Что такое ELF?

ELF — это аббревиатура от английского Executable and Linking Format (Формат Исполняемых и Связываемых файлов). Это одна из разновидностей форматов для исполняемых и объектных файлов, используемых в UNIX-системах. Для нас особый интерес будет представлять заголовок файла. Каждый файл формата ELF имеет ELF-заголовок следующей структуры:

typedef struct
{
        unsigned char   e_ident[EI_NIDENT]; /* Сигнатура и прочая информация */
        Elf32_Half      e_type;             /* Тип объектного файла */
        Elf32_Half      e_machine;          /* Аппаратная платформа (архитектура) */
        Elf32_Word      e_version;          /* Номер версии */
        Elf32_Addr      e_entry;            /* Адрес точки входа (стартовый адрес программы) */
        Elf32_Off       e_phoff;            /* Смещение от начала файла таблицы программных заголовков */
        Elf32_Off       e_shoff;            /* Смещение от начала файла таблицы заголовков секций */
        Elf32_Word      e_flags;            /* Специфичные флаги процессора */
                                            /* (не используется в архитектуре i386) */
        Elf32_Half      e_ehsize;           /* Размер ELF-заголовка в байта х */
        Elf32_Half      e_phentsize;        /* Размер записи в таблице программных заголовков */
        Elf32_Half      e_phnum;            /* Количество записей в таблице */
                                            /* программных заголовков */
        Elf32_Half      e_shentsize;        /* Размер записи в таблице заголовков секций */
        Elf32_Half      e_shnum;            /* Количество записей в таблице */
                                            /* заголовков секций */
        Elf32_Half      e_shstrndx;         /* Расположение сегмента, содержащего таблицy стpок */
} Elf32_Ehdr;

В этой структуре, поле "e_entry" содержит адрес запуска программы.

Что находится по адресу "0x080482d0", то есть по адресу запуска (starting

address)?

Для ответа на этот вопрос попробуем дизассемблировать программу "simple". Для дизассемблирования исполняемых файлов я использую objdump.

objdump —disassemble simple

Утилита objdump выдаст очень много информации, поэтому я не буду приводить её всю. Нас интересует только адрес 0x080482d0. Вот эта часть листинга:

080482d0 <_start>:
 80482d0:       31 ed                   xor    %ebp,%ebp
 80482d2:       5e                      pop    %esi
 80482d3:       89 e1                   mov    %esp,%ecx
 80482d5:       83 e4 f0                and    $0xfffffff0,%esp
 80482d8:       50                      push   %eax
 80482d9:       54                      push   %esp
 80482da:       52                      push   %edx
 80482db:       68 20 84 04 08          push   $0x8048420
 80482e0:       68 74 82 04 08          push   $0x8048274
 80482e5:       51                      push   %ecx
 80482e6:       56                      push   %esi
 80482e7:       68 d0 83 04 08          push   $0x80483d0
 80482ec:       e8 cb ff ff ff          call   80482bc <_init+0x48>
 80482f1:       f4                      hlt
 80482f2:       89 f6                   mov    %esi,%esi

Похоже на то, что первой запускается процедура "_start". Все, что она делает — это очищает регистр ebp, "проталкивает" какие-то значения в стек и вызывает подпрограмму. Согласно этим инструкциям содержимое стека должно выглядеть так:

         -----Дно стека-----
         0x80483d
         -------------------
         esi
         -------------------
         ecx
         -------------------
         0x8048274
         -------------------
         0x8048420
         -------------------
         edx
         -------------------
         esp
         -------------------
         eax
         -------------------

Теперь вопросов становится еще больше

  • Что за числа кладутся в стек?
  • Что находится по адресу 80482bc, который вызывается инструкцией call в процедуре _start?
  • В приведенном листинге отсутствуют инструкции, инициализирующие регистры (имеются ввиду eax, ecx, edx прим. перев.). Где они инициализируются?

Попробуем ответить на все эти вопросы.

Вопрос 1> Что за числа кладутся в стек?

Если внимательно просмотреть весь листинг, создаваемый утилитой objdump, то можно легко найти ответ

Вот он:

0x80483d0 : Это адрес функции main().

0x8048274 : адрес функции _init.

0x8048420 : адрес функции _fini. Функции _init и _fini — это функции инициализации и финализации (завершения) приложения, генерируемые компилятором GCC.

Таким образом все приведенные числа являются указателями на функции (точнее — адресами функций прим. перев.)

Вопрос 2> Что находится по адресу 80482bc?

Снова обратимся к листингу.

80482bc:   ff 25 48 95 04 08       jmp    *0x8049548

Здесь *0x8049548 означает указатель.

Это просто косвенный переход по адресу, хранящемуся в памяти по адресу 0x8049548.

Дополнительно о формате ELF и динамическом связывании

Формат ELF предполагает возможность динамического связывания исполняемой программы с библиотеками. Где под словами "динамическое связывание" следует понимать то, что связывание производится во время исполнения. В противоположность динамическому связыванию существует "статическое связывание", т.е. когда связывание с библиотеками происходит на этапе сборки программы, что, как правило, приводит к "раздуванию" исполняемого файла до огромных размеров. Если вы запустите команду:

"ldd simple"

    libc.so.6 => /lib/i686/libc.so.6 (0x42000000)
    /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)

Вы сможете увидеть полный список библиотек, связанных с программой simple динамически. Вкратце, концепция динамического связывания выглядит так.

  1. На этапе сборки программы адреса переменных и функций в динамической библиотеке не известны. Они становятся известны только на этапе исполнения
  2. Для того, чтобы иметь возможность обращаться к компонентам динамической библиотеки (переменные, функции и т.д. прим. перев.) необходимо предусмотреть указатели на эти компоненты Указатели заполняются фактическими адресами во время загрузки.
  3. Приложение может обращаться к динамическим компонентам только косвенно, используя для этого указатели. Пример такой косвенной адресации можно увидеть в листинге, приведенном выше, по адресу 80482bc, когда осуществляется косвенный переход. Фактический адрес перехода сохраняется по адресу 0x8049548 во время загрузки программы.

Косвенные ссылки можно посмотреть, выполнив команду

objdump -R simple


      simple:     file format elf32-i386

        DYNAMIC RELOCATION RECORDS
        OFFSET   TYPE              VALUE
        0804954c R_386_GLOB_DAT    __gmon_start__
        08049540 R_386_JUMP_SLOT   __register_frame_info
        08049544 R_386_JUMP_SLOT   __deregister_frame_info
        08049548 R_386_JUMP_SLOT   __libc_start_main
       Здесь адрес 0x8049548 называется "jump slot" и имеет определенный
       смысл. В соответствии с таблицей он означает вызов
       __libc_start_main.

Что такое __libc_start_main?

Теперь "карты сдает" библиотека libc. __libc_start_main — это функция из библиотеки libc.so.6. Если отыскать функцию __libc_start_main в исходном коде библиотеки glibc, то увидите примерно такое объявление.

extern int BP_SYM (__libc_start_main) (int (*main) (int, char **, char **),
        int argc,
        char *__unbounded *__unbounded ubp_av,
        void (*init) (void),
        void (*fini) (void),
        void (*rtld_fini) (void),
        void *__unbounded stack_end)
        __attribute__ ((noreturn));

Теперь становится понятен смысл ассемблерных инструкций из листинга, приведенного выше — они кладут на стек входные параметры и вызывают функцию __libc_start_main.

В задачу этой функции входят некоторые действия по инициализации среды исполнения и вызов функции main().

Рассмотрим содержимое стека с новых позиций.

Дно стека  ------------------
           0x80483d0             main
           ------------------
           esi                   argc
           ------------------
           ecx                   argv
           ------------------
           0x8048274             _init
           ------------------
           0x8048420             _fini
           ------------------
           edx                   _rtlf_fini
           ------------------
           esp                   stack_end
           ------------------
           eax                   это ноль (0)
           ------------------

Согласно такому представлению стека, понятно, что перед вызовом __libc_start_main() в регистры esi, ecx, edx, esp и eax должны быть записаны соответствующие значения. Совершенно очевидно, что дизассемблированный код, показанный выше, ничего в эти регистры не пишет. Тогда кто? Остается только одно предположение — ядро. А теперь перейдем к третьему вопросу.

Вопрос 3> Что делает ядро?

Когда программа запускается из командной строки, выполняются следующие действия.

  1. Командная оболочка (shell) делает системный вызов "execve" с параметрами argc/argv.
  2. Обработчик системного вызова в ядре получает управление и начинает его обработку. В ядре обработчик называется "sys_execve". На платформе x86, пользовательское приложение передает аргументы вызова в ядро через регистры.
    • ebx : указатель на строку с именем программы
    • ecx : указатель на массив argv
    • edx : указатель на массив переменных окружения
  3. Универсальный обработчик системного вызова в ядре называется do_execve. Он создает и заполняет определенные структуры данных, копирует необходимую информацию из пространства пользователя в пространство ядра и, наконец, вызывает search_binary_handler().

Linux поддерживает множество форматов исполняемых файлов, например a.out и ELF. Для обеспечения такой поддержки в ядре имеется структура "struct linux_binfmt", которая содержит указатели на загрузчики каждого из поддерживаемых форматов. Таким образом, search_binary_handler() просто отыскивает нужный загрузчик и вызывает его. В нашем случае — это load_elf_binary(). Описывать эту функцию в подробностях слишком долгая и нудная работа, так что я не буду заниматься этим здесь. За подробностями обращайтесь к специальной литературе по данной тематике. (от себя могу предложить ссылку на статью "Внутреннее устройство ядра Linux 2.4" прим. перев. )

Вкратце процесс загрузки выглядит примерно так.

Сначала создаются и заполняются структуры в пространстве ядра и файл программы считывается в память. Затем производится установка дополнительных значений — определяется размер сегмента кода, определяется начало сегмента данных и сегмента стека и т.д.. В пользовательском режиме выделяется память, в которую копируются входные параметры (argv) и переменные окружения. Затем функция create_elf_tables(), в пользовательском режиме, кладет на стек argc, указатели на argv и массив переменных окружения, после чего start_thread() запускает программу на исполнение.

Когда управление передается в точку _start, стек выглядит примерно так:

Дно стека       -------------
                argc
                -------------
                указатель на argv
                -------------
                указатель на env
                -------------

Теперь наш дизассемблированный листинг выглядит еще более определенным.

pop %esi           <--- со стека снимается argc
move %esp, %ecx    <--- argv
                      т.е. теперь, фактически, адрес argv совпадает с
                      указателем стека

Теперь все готово к запуску программы.

Что можно сказать по-поводу остальных регистров?

esp используется для указания вершины стека в прикладной программе. После того как со стека будет снята вся необходимая информация, процедура _start просто скорректирует указатель стека (esp), сбросив 4 младших бита в регистре esp. В регистр edx заносится указатель на, своего рода деструктор приложения — rtlf_fini. На платформе x86 эта особенность не поддерживается, поэтому ядро заносит туда число 0 макрокомандой.

#define ELF_PLAT_INIT(_r)  do { \
        _r->ebx = 0; _r->ecx = 0; _r->edx = 0; \
        _r->esi = 0; _r->edi = 0; _r->ebp = 0; \
        _r->\eax = 0; \
} while (0)

Откуда взялся весь этот дополнительный код

   Откуда взялся весь этот дополнительный код? Он входит в состав
   компилятора GCC. Вы можете найти его в
   /usr/lib/gcc-lib/i386-redhat-linux/XXX и
   /usr/lib где XXX — номер версии gcc.
   Файлы называются crtbegin.o,crtend.o, gcrt1.o.

Подведение итогов

Итак, выводы следующие.

  1. При сборке программы, GCC присоединяет к ней код из объектных модулей crtbegin.o/crtend.o/gcrt1.o а другие библиотеки, по-умолчанию, связывает динамически. Адрес запуска приложения (в ELF-заголовке прим. перев.) указывает на точку _start.
  2. Ядро загружает программу и устанавливает сегменты text/data/bss/stack, распределяет память для входных параметров и переменных окружения и помещает на стек всю необходимую информацию.
  3. Управление передается в точку _start. Здесь информация снимается со стека, на стеке размещаются входные параметры для функции __libc_start_main, после чего ей передается управление.
  4. Функция __libc_start_main выполняет все необходимые действия по инициализации среды исполнения, особенно это касается библиотеки C (malloc и т.п.) и вызывает функцию main() программы.
  5. Функции main() передаются входные аргументы — main(argc, argv). Здесь есть один интересный момент. __libc_start_main "представляет" себе сигнатуру функции main() как main(int, char **, char **). Если вам это любопытно, то попробуйте запустить следующую программу:
main(int argc, char** argv, char** env)
{
    int i = 0;
    while(env[i] != 0)
    {
       printf("%s\n", env[i++]);
    }
    return(0);
}

Заключение

В Linux запуск функции main() является результатом взаимодействия GCC, libc и загрузчика.

Оставить комментарий

Комментарий:
можно использовать BB-коды
Максимальная длина комментария - 4000 символов.
 
Реклама на сайте | Обмен ссылками | Ссылки | Экспорт (RSS) | Контакты
Добавить статью | Добавить исходник | Добавить хостинг-провайдера | Добавить сайт в каталог