- Как запускается функция main() в Linux
- Вступление
- Что находится внутри исполняемого файла?
- Что такое ELF?
- Что находится по адресу «0x080482d0», то есть по адресу запуска (starting
- Вопрос 1> Что за числа кладутся в стек?
- Вопрос 2> Что находится по адресу 80482bc?
- Дополнительно о формате ELF и динамическом связывании
- Что такое __libc_start_main?
- Вопрос 3> Что делает ядро?
- Что можно сказать по-поводу остальных регистров?
- Подведение итогов
- Заключение
- Linux in function main
- Вступление
- Сборка
- Что находится внутри исполняемого файла?
- Что такое ELF?
- Что находится по адресу «0x080482d0», то есть по адресу запуска (starting address)?
- Вопрос 1> Что за числа кладутся в стек?
- Вопрос 2> Что находится по адресу 80482bc?
- Дополнительно о формате ELF и динамическом связывании
- Что такое __libc_start_main?
- Вопрос 3> Что делает ядро?
- Что можно сказать по-поводу остальных регистров?
- Откуда взялся весь этот дополнительный код
- Подведение итогов
- Заключение
Как запускается функция 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 динамически. Вкратце, концепция динамического связывания выглядит так.
- На этапе сборки программы адреса переменных и функций в динамической библиотеке не известны. Они становятся известны только на этапе исполнения
- Для того, чтобы иметь возможность обращаться к компонентам динамической библиотеки (переменные, функции и т.д. прим. перев.) необходимо предусмотреть указатели на эти компоненты Указатели заполняются фактическими адресами во время загрузки.
- Приложение может обращаться к динамическим компонентам только косвенно, используя для этого указатели. Пример такой косвенной адресации можно увидеть в листинге, приведенном выше, по адресу 80482bc, когда осуществляется косвенный переход. Фактический адрес перехода сохраняется по адресу 0x8049548 во время загрузки программы.
Косвенные ссылки можно посмотреть, выполнив команду
Что такое __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> Что делает ядро?
Когда программа запускается из командной строки, выполняются следующие действия.
- Командная оболочка (shell) делает системный вызов «execve» с параметрами argc/argv.
- Обработчик системного вызова в ядре получает управление и начинает его обработку. В ядре обработчик называется «sys_execve». На платформе x86, пользовательское приложение передает аргументы вызова в ядро через регистры.
- ebx : указатель на строку с именем программы
- ecx : указатель на массив argv
- edx : указатель на массив переменных окружения
- Универсальный обработчик системного вызова в ядре называется 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, стек выглядит примерно так:
Теперь наш дизассемблированный листинг выглядит еще более определенным.
Теперь все готово к запуску программы.
Что можно сказать по-поводу остальных регистров?
esp используется для указания вершины стека в прикладной программе. После того как со стека будет снята вся необходимая информация, процедура _start просто скорректирует указатель стека (esp), сбросив 4 младших бита в регистре esp. В регистр edx заносится указатель на, своего рода деструктор приложения rtlf_fini. На платформе x86 эта особенность не поддерживается, поэтому ядро заносит туда число 0 макрокомандой.
Откуда взялся весь этот дополнительный код
Подведение итогов
Итак, выводы следующие.
- При сборке программы, GCC присоединяет к ней код из объектных модулей crtbegin.o/crtend.o/gcrt1.o а другие библиотеки, по-умолчанию, связывает динамически. Адрес запуска приложения (в ELF-заголовке прим. перев.) указывает на точку _start.
- Ядро загружает программу и устанавливает сегменты text/data/bss/stack, распределяет память для входных параметров и переменных окружения и помещает на стек всю необходимую информацию.
- Управление передается в точку _start. Здесь информация снимается со стека, на стеке размещаются входные параметры для функции __libc_start_main, после чего ей передается управление.
- Функция __libc_start_main выполняет все необходимые действия по инициализации среды исполнения, особенно это касается библиотеки C (malloc и т.п.) и вызывает функцию main() программы.
- Функции main() передаются входные аргументы main(argc, argv). Здесь есть один интересный момент. __libc_start_main «представляет» себе сигнатуру функции main() как main(int, char **, char **). Если вам это любопытно, то попробуйте запустить следующую программу:
Заключение
В Linux запуск функции main() является результатом взаимодействия GCC, libc и загрузчика.
Источник
Linux in function main
Как запускается функция main() в Linux
Автор (c): Hyouck «Hawk» Kim
Перевод (c): Андрей Киселев
Вступление
Так ли прост вопрос: «Как запускается функция main() в Linux»? Для ответа на него я возьму, в качестве примера, простенькую программу на языке C — «simple.c»
Сборка
Что находится внутри исполняемого файла?
Для того, чтобы рассмотреть внутреннее устройство исполняемого файла воспользуемся утилитой «objdump»
Отсюда видно, что файл, во-первых, имеет формат «ELF32», а во-вторых — адрес запуска программы «0x080482d0»
Что такое ELF?
ELF — это аббревиатура от английского Executable and Linking Format (Формат Исполняемых и Связываемых файлов). Это одна из разновидностей форматов для исполняемых и объектных файлов, используемых в UNIX-системах. Для нас особый интерес будет представлять заголовок файла. Каждый файл формата ELF имеет ELF-заголовок следующей структуры:
В этой структуре, поле «e_entry» содержит адрес запуска программы.
Что находится по адресу «0x080482d0», то есть по адресу запуска (starting address)?
Для ответа на этот вопрос попробуем дизассемблировать программу «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 динамически.
Вкратце, концепция динамического связывания выглядит так.
- На этапе сборки программы адреса переменных и функций в динамической библиотеке не известны. Они становятся известны только на этапе исполнения
- Для того, чтобы иметь возможность обращаться к компонентам динамической библиотеки (переменные, функции и т.д. прим. перев.) необходимо предусмотреть указатели на эти компоненты
Указатели заполняются фактическими адресами во время загрузки. - Приложение может обращаться к динамическим компонентам только косвенно, используя для этого указатели. Пример такой косвенной адресации можно увидеть в листинге, приведенном выше, по адресу 80482bc, когда осуществляется косвенный переход.
Фактический адрес перехода сохраняется по адресу 0x8049548 во время загрузки программы.
Косвенные ссылки можно посмотреть, выполнив команду Здесь адрес 0x8049548 называется «jump slot» и имеет определенный смысл. В соответствии с таблицей он означает вызов __libc_start_main.
Что такое __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> Что делает ядро?
Когда программа запускается из командной строки, выполняются следующие действия.
- Командная оболочка (shell) делает системный вызов «execve» с параметрами argc/argv.
- Обработчик системного вызова в ядре получает управление и начинает его обработку. В ядре обработчик называется «sys_execve». На платформе x86, пользовательское приложение передает аргументы вызова в ядро через регистры.
- ebx : указатель на строку с именем программы
- ecx : указатель на массив argv
- edx : указатель на массив переменных окружения
- Универсальный обработчик системного вызова в ядре называется 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, стек выглядит примерно так:
Теперь наш дизассемблированный листинг выглядит еще более определенным.
Теперь все готово к запуску программы.
Что можно сказать по-поводу остальных регистров?
esp используется для указания вершины стека в прикладной программе. После того как со стека будет снята вся необходимая информация, процедура _start просто скорректирует указатель стека (esp), сбросив 4 младших бита в регистре esp. В регистр edx заносится указатель на, своего рода деструктор приложения — rtlf_fini. На платформе x86 эта особенность не поддерживается, поэтому ядро заносит туда число 0 макрокомандой.
Откуда взялся весь этот дополнительный код
Откуда взялся весь этот дополнительный код? Он входит в состав компилятора GCC. Вы можете найти его в
/usr/lib/gcc-lib/i386-redhat-linux/XXX и
/usr/lib где XXX — номер версии gcc.
Файлы называются crtbegin.o,crtend.o, gcrt1.o.
Подведение итогов
Итак, выводы следующие.
- При сборке программы, GCC присоединяет к ней код из объектных модулей crtbegin.o/crtend.o/gcrt1.o а другие библиотеки, по-умолчанию, связывает динамически. Адрес запуска приложения (в ELF-заголовке прим. перев.) указывает на точку _start.
- Ядро загружает программу и устанавливает сегменты text/data/bss/stack, распределяет память для входных параметров и переменных окружения и помещает на стек всю необходимую информацию.
- Управление передается в точку _start. Здесь информация снимается со стека, на стеке размещаются входные параметры для функции __libc_start_main, после чего ей передается управление.
- Функция __libc_start_main выполняет все необходимые действия по инициализации среды исполнения, особенно это касается библиотеки C (malloc и т.п.) и вызывает функцию main() программы.
- Функции main() передаются входные аргументы — main(argc, argv). Здесь есть один интересный момент. __libc_start_main «представляет» себе сигнатуру функции main() как main(int, char **, char **). Если вам это любопытно, то попробуйте запустить следующую программу:
Заключение
В Linux запуск функции main() является результатом взаимодействия GCC, libc и загрузчика.
Источник