Так ли прост вопрос: "Как запускается функция main() в Linux"? Для ответа на него я возьму, в качестве примера, простенькую программу на языке C -- "simple.c"
main() { return(0); }
gcc -o simple simple.c
Для того, чтобы рассмотреть внутреннее устройство исполняемого файла воспользуемся утилитой "objdump"
objdump -f simpleОтсюда видно, что файл, во-первых, имеет формат "ELF32", а во-вторых -- адрес запуска программы "0x080482d0"
simple: file format elf32-i386
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x080482d0
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" содержит адрес запуска программы.
Для ответа на этот вопрос попробуем дизассемблировать программу "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 -------------------
Теперь вопросов становится еще больше
Попробуем ответить на все эти вопросы.
Если внимательно просмотреть весь листинг, создаваемый утилитой objdump, то можно легко найти ответ
Вот он:
0x80483d0 : Это адрес функции main().
0x8048274 : адрес функции _init.
0x8048420 : адрес функции _fini. Функции _init и _fini -- это функции
инициализации и финализации (завершения) приложения, генерируемые компилятором
GCC.
Таким образом все приведенные числа являются указателями на функции (точнее -- адресами функций прим. перев.)
Снова обратимся к листингу.
80482bc: ff 25 48 95 04 08 jmp *0x8049548
Формат ELF предполагает возможность динамического связывания исполняемой
программы с библиотеками.
Где под словами "динамическое связывание" следует
понимать то, что связывание производится во время исполнения. В
противоположность динамическому связыванию существует "статическое связывание",
т.е. когда связывание с библиотеками происходит на этапе сборки программы, что,
как правило, приводит к "раздуванию" исполняемого файла до огромных размеров.
Если вы запустите команду:
"ldd simple" libc.so.6 => /lib/i686/libc.so.6 (0x42000000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)Вы сможете увидеть полный список библиотек, связанных с программой simple динамически.
objdump -R simpleЗдесь адрес 0x8049548 называется "jump slot" и имеет определенный смысл. В соответствии с таблицей он означает вызов __libc_start_main.
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
Теперь "карты сдает" библиотека 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.
Дно стека ------------------ 0x80483d0 main ------------------ esi argc ------------------ ecx argv ------------------ 0x8048274 _init ------------------ 0x8048420 _fini ------------------ edx _rtlf_fini ------------------ esp stack_end ------------------ eax это ноль (0) ------------------
Когда программа запускается из командной строки, выполняются следующие действия.
Когда управление передается в точку _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.
Итак, выводы следующие.
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)