Linux main return value

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

Вступление

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

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

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

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

Что такое ELF?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Что такое __libc_start_main?

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

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

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

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

Согласно такому представлению стека, понятно, что перед вызовом __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, стек выглядит примерно так:

Читайте также:  Очень сильно загружена оперативная память windows 10

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

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

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

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

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

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

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

  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 **). Если вам это любопытно, то попробуйте запустить следующую программу:

Заключение

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

Источник

Несколько подробностей о функции main

Однажды заинтересовался, содержимым стека функции main процесса в linux. Провел некоторые изыскания и теперь представляю вам результат.

Варианты описания функции main:
1. int main()
2. int main(int argc, char **argv)
3. int main(int argc, char **argv, char **env)
4. int main(int argc, char **argv, char **env, ElfW(auxv_t) auxv[])
5. int main(int argc, char **argv, char **env, char **apple)

argc — число параметров
argv — нуль-терминальный массив указателей на строки параметров командной строки
env — нуль-терминальный массив указателей на строки переменных окружения. Каждая строка в формате ИМЯ=ЗНАЧЕНИЕ
auxv — массив вспомогательных значение (доступно только для PowerPC [1])
apple — путь к исполняемому файлу (в MacOS и Darwin [2])
Вспомогательный вектор — массив с различной дополнительной информацией, такой как эффективный идентификатор пользователя, признак setuid бита, размер страницы памяти и т.п.

Далее о том как получить массив вспомогательных значений для i386 и x86_64, а также об остальном содержимом «сегмента» стека.

Размер сегмента стека можно глянуть в файле maps:
cat /proc/10918/maps

7ffffffa3000-7ffffffff000 rw-p 00000000 00:00 0 [stack]

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

1. 0x7ffffffff000 Верхняя точка сегмента стека. Обращение вызывает segfault
0x7ffffffff0f8 NULL void* 8 0x00′
2. filename[0] char 1+ «/tmp/a.out»
char 1 0x00
.
env[1][0] char 1 0x00
.
char 1 0x00
3. 0x7fffffffe5e0 env[0][0] char 1 ..
char 1 0x00
.
argv[1][0] char 1 0x00
.
char 1 0x00
4. 0x7fffffffe5be argv[0][0] char 1+ «/tmp/a.out»
5. Массив случайной длины
6. данные для auxv void*[] 48′
AT_NULL Elf64_auxv_t 16
.
auxv[1] Elf64_auxv_t 16
7. auxv[0] Elf64_auxv_t 16 Ex.:
NULL void* 8 0x00
.
env[1] char* 8
8. 0x7fffffffe308 env[0] char* 8 0x7fffffffe5e0
NULL void* 8 0x00
.
argv[1] char* 8
9. 0x7fffffffe2f8 argv[0] char* 8 0x7fffffffe5be
10. 0x7fffffffe2f0 argc long int 8′ число аргументов + 1
11. Локальные переменные и аргументы, функций вызываемых до main
12. Локальные переменные main
13. 0x7fffffffe1fc argc int 4 число аргументов + 1
0x7fffffffe1f0 argv char** 8 0x7fffffffe2f8
0x7fffffffe1e8 env char** 8 0x7fffffffe308
14. Переменные локальных функций

‘ — описания полей в документах не нашел, но в дампе явно видны.

Для 32 битов не проверял, но скорее всего достаточно только разделить размеры на два.

1. Обращение к адресам, выше верхней точки, вызывает Segfault.
2. Строка, содержащая путь к исполняемому файлу.
3. Массив строк с переменными окружения
4. Массив строк с параметрами командной строки
5. Массив случайной длинны. Его выделение можно отключить командами
sysctl -w kernel.randomize_va_space=0
echo 0 > /proc/sys/kernel/randomize_va_space
6. Данные для вспомогательного вектора (например строка «x86_64»)
7. Вспомогательный вектор. Подробнее ниже.
8. Нуль-терминальный массив указателей на строки переменных окружения
9. Нуль-терминальный массив указателей на строки параметров командной строки
10.Машинное слово, содержащее число параметров командной строки (один из аргументов «старших» функций см. п. 11)
11.Локальные переменные и аргументы, функций вызываемых до main(_start,__libc_start_main..)
12.Переменные, объявленные в main
13.Аргументы функции main
14.Переменные и аргументы локальных функций.

Вспомогательный вектор
Для i386 и x86_64 нельзя получить адрес первого элемента вспомогательного вектора, однако содержимое этого вектора можно получить другими способами. Один из них — обратиться к области памяти, лежащей сразу за массивом указателей на строки переменных окружения.
Это должно выглядеть примерно так:

Структуры Elf<32,64>_auxv_t описаны в /usr/include/elf.h. Функции заполнения структур в linux-kernel/fs/binfmt_elf.c

Второй способ получить содержимое вектора:
hexdump /proc/self/auxv

Самый удобочитаемое представление получается установкой переменной окружения LD_SHOW_AUXV.

LD_SHOW_AUXV=1 ls
AT_HWCAP: bfebfbff //возможности процессора
AT_PAGESZ: 4096 //размер страницы памяти
AT_CLKTCK: 100 //частота обновления times()
AT_PHDR: 0x400040 //информация о заголовке
AT_PHENT: 56
AT_PHNUM: 9
AT_BASE: 0x7fd00b5bc000 //адрес интерпретатора, то бишь ld.so
AT_FLAGS: 0x0
AT_ENTRY: 0x402490 //точка входа в программу
AT_UID: 1000 //идентификаторы пользователя и группы
AT_EUID: 1000 //номинальные и эффективные
AT_GID: 1000
AT_EGID: 1000
AT_SECURE: 0 //поднят ли setuid флаг
AT_RANDOM: 0x7fff30bdc809 //адрес 16 случайных байт,
генерируемых при запуске
AT_SYSINFO_EHDR: 0x7fff30bff000 //указатель на страницу, используемую для
//системных вызовов
AT_EXECFN: /bin/ls
AT_PLATFORM: x86_64
Слева — название переменной, справа значение. Все возможные названия переменных и их описание можно глянуть в файле elf.h. (константы с префиксом AT_)

Возвращение из main()
После инициализации контекста процесса управление передается не в main(), а в функцию _start().
main() вызывает уже из __libc_start_main. Эта последняя функция имеет интересную особенность — ей передается указатель на функцию, которая должна быть выполнена после main(). И указатель этот передается естественно через стек.
Вообще аргументы __libc_start_main имеют вид, согласно файла glibc-2.11/sysdeps/ia64/elf/start.S
/*
* Arguments for __libc_start_main:
* out0: main
* out1: argc
* out2: argv
* out3: init
* out4: fini //функция вызываемая после main
* out5: rtld_fini
* out6: stack_end
*/
Т.е. чтобы получить адрес указателя fini нужно сместиться на два машинных слова от последней локальной переменной main.
Вот что получилось(работоспособность зависит от версии компилятора):

Надеюсь, было интересно.
Удач.

Спасибо пользователю Xeor за полезную наводку.

Источник

Читайте также:  Windows 10 не открывает центр уведомлений
Оцените статью