The OpenNET Project / Index page

[ новости /+++ | форум | теги | ]

Внутренние устройство исполняемых файлов формата ELF. (gcc elf assembler)


<< Предыдущая ИНДЕКС Поиск в статьях src Установить закладку Перейти на закладку Следующая >>
Ключевые слова: gcc, elf, assembler,  (найти похожие документы)
From: Андрей Киселев <kis_an@mail.ru> Newsgroups: http://gazette.linux.ru.net Date: Mon, 14 Apr 2003 13:01:37 +0000 (UTC) Subject: Внутренние устройство исполняемых файлов формата ELF. Оригинал: http://gazette.linux.ru.net/lg84/kim.html Как запускается функция main() в Linux Автор (c): Hyouck "Hawk" Kim <http://gazette.linux.ru.net/authors/hawk.html>; Перевод (c): Андрей Киселев <kis_an@mail.ru> _________________________________________________________________ Вступление Так ли прост вопрос: "Как запускается функция 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 ------------------- Теперь вопросов становится еще больше 1. Что за числа кладутся в стек? 2. Что находится по адресу 80482bc, который вызывается инструкцией call в процедуре _start? 3. В приведенном листинге отсутствуют инструкции, инициализирующие регистры (имеются ввиду 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" прим. перев. https://www.opennet.ru/docs/RUS/lki/) Вкратце процесс загрузки выглядит примерно так. Сначала создаются и заполняются структуры в пространстве ядра и файл программы считывается в память. Затем производится установка дополнительных значений -- определяется размер сегмента кода, определяется начало сегмента данных и сегмента стека и т.д.. В пользовательском режиме выделяется память, в которую копируются входные параметры (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 и загрузчика. Ссылки objdump -- "man objdump" ELF-заголовок -- /usr/include/elf.h __libc_start_main -- исходный код glibc (./sysdeps/generic/libc-start.c) sys_execve -- исходный код ядра linux (arch/i386/kernel/process.c) do_execve -- исходный код ядра linux (fs/exec.c) struct linux_binfmt -- исходный код ядра linux (include/linux/binfmts.h) load_elf_binary -- исходный код ядра linux (fs/binfmt_elf.c) create_elf_tables -- исходный код ядра linux (fs/binfmt_elf.c) start_thread -- исходный код ядра linux (include/asm/processor.h) Copyright (C) 2002, Hyouck "Hawk" Kim. Copying license http://www.linuxgazette.com/copying.html Published in Issue 84 of Linux Gazette, November 2002

<< Предыдущая ИНДЕКС Поиск в статьях src Установить закладку Перейти на закладку Следующая >>

Обсуждение [ RSS ]
  • 1.1, Вера (?), 13:36, 06/12/2005 [ответить]  
  • +/
    Подробно и толково
     
  • 1.2, Reader (?), 14:18, 29/08/2008 [ответить]  
  • +/
    Спасибо огромное
     
  • 1.3, Zick (?), 18:38, 17/12/2008 [ответить]  
  • +/
    Просто суперполезный текст!
     
  • 1.4, Hormahis (?), 23:01, 15/11/2010 [ответить]  
  • +/
    Спасибо! Было очень познавательно! :)
     
  • 1.5, zakhej (?), 20:00, 09/07/2011 [ответить]  
  • +/
    Спасибо за статью, многое полезного!
     
  • 1.6, Сергей (??), 16:28, 20/06/2012 [ответить]  
  • +/
    Спасибо, мне никто более доходчиво это бы не рассказал...
     
  • 1.7, afi (?), 21:36, 05/05/2014 [ответить]  
  • +/
    Спасибо. Много интересного узнал.
     
  • 1.8, Пиццамейкер (?), 01:12, 11/07/2016 [ответить]  
  • +/
    Впервые вижу, чтобы вершину стека называли дном
     
  • 1.9, Кузя (?), 18:25, 10/08/2020 [ответить]  
  • +/
    В 2020-м это всё ещё самая понятная статья про ELF.
     

     Добавить комментарий
    Имя:
    E-Mail:
    Заголовок:
    Текст:




    Партнёры:
    PostgresPro
    Inferno Solutions
    Hosting by Hoster.ru
    Хостинг:

    Закладки на сайте
    Проследить за страницей
    Created 1996-2024 by Maxim Chirkov
    Добавить, Поддержать, Вебмастеру