Почему виртуальный адрес ввода точки входа ELF формы 0x80xxxxx, а не ноль 0x0?

При выполнении программа начнет работать с виртуального адреса 0x80482c0. Этот адрес не указывает на нашу процедуру main(), а на процедуру с именем _start, которая создается компоновщиком.

Мое исследование Google до сих пор просто привело меня к некоторым (неопределенным) историческим спекуляциям вроде этого:

Существует фольклор, в котором 0x08048000 был STACK_TOP (т.е. стек вырос вниз от 0x08048000 до 0) в порту от * NIX до i386, который был обнародован группой из Санта-Крус, штат Калифорния. Это было тогда, когда 128 МБ ОЗУ были дорогими, и 4 ГБ ОЗУ немыслимо.

Кто-нибудь может подтвердить или опровергнуть это?

Ответ 1

Как указывал Mads, чтобы уловить большинство запросов с помощью нулевых указателей, Unix-подобные системы, как правило, делают страницу с нулевым адресом "unmapped". Таким образом, доступ немедленно вызывает исключение CPU, другими словами, segfault. Это намного лучше, чем позволить запуску приложения. Однако таблица векторов исключений может быть по любому адресу, по крайней мере, на процессорах x86 (для этого есть специальный регистр, загруженный кодом операции lidt).

Адрес начальной точки является частью набора соглашений, которые описывают, как выкладывается память. Компилятор, когда он создает исполняемый двоичный файл, должен знать эти соглашения, поэтому они вряд ли изменятся. В принципе, для Linux соглашения о компоновке памяти наследуются от самых первых версий Linux, в начале 90-х. Процесс должен иметь доступ к нескольким областям:

  • Код должен быть в диапазоне, который включает начальную точку.
  • Должен быть стек.
  • Должна быть куча с пределом, который увеличивается с системными вызовами brk() и sbrk().
  • Должно быть место для системных вызовов mmap(), включая загрузку разделяемой библиотеки.

В настоящее время куча, где malloc() идет, поддерживается mmap() вызовами, которые получают куски памяти по любому адресу, которое ядро ​​считает нужным. Но в более старые времена Linux был похож на предыдущие Unix-подобные системы, и его куча требовала большой площади в одном непрерывном куске, который мог бы увеличиться по отношению к увеличению адресов. Таким образом, независимо от того, что было в соглашении, он должен был набивать код и стекать по низким адресам, а каждый кусок адресного пространства после заданной точки - в кучу.

Но есть также стек, который обычно довольно мал, но может расти довольно резко в некоторых случаях. Стек растет, и когда стек заполнен, мы действительно хотим, чтобы процесс предсказуемо разбился, а не перезаписал некоторые данные. Таким образом, для стека должна быть широкая область, а на нижнем конце этой области - открытая страница. И вот! На нулевом адресе есть неотображаемая страница, чтобы перехватывать нулевые указатели указателя. Следовательно, было определено, что стек получит первые 128 МБ адресного пространства, за исключением первой страницы. Это означает, что код должен был идти после этих 128 МБ по адресу, подобному 0x080xxxxx.

Как отмечает Майкл, "потерять" 128 МБ адресного пространства не имеет большого значения, потому что адресное пространство было очень большим в отношении того, что можно было бы фактически использовать. В то время ядро ​​Linux ограничивало адресное пространство для одного процесса до 1 ГБ, максимально допустимое аппаратным обеспечением на 4 ГБ, и это не считалось большой проблемой.

Ответ 2

Почему бы не начать с адреса 0x0? Для этого есть как минимум две причины:

  • Поскольку нулевой адрес известен как NULL-указатель и используется языком программирования для правильных указателей. Вы не можете использовать для этого значение адреса, если вы собираетесь выполнять там код.
  • Фактическое содержимое по адресу 0 часто (но не всегда) содержит таблицу векторов исключений и, следовательно, недоступно в непривилегированных режимах. Проконсультируйтесь с документацией по вашей конкретной архитектуре.

Что касается точки входа _start vs main: Если вы ссылаетесь на среду выполнения C (стандартные библиотеки C), библиотека обертывает функцию с именем main, поэтому она может инициализировать среду до вызова main. В Linux это параметры argc и argv для приложения, переменные env и, возможно, некоторые примитивы синхронизации и блокировки. Он также гарантирует, что возврат из основных проходов в код состояния и вызывает функцию _exit, которая завершает процесс.