Почему виртуальный адрес точки входа ELF имеет вид 0x80xxxxx, а не ноль 0x0? - PullRequest
19 голосов
/ 02 февраля 2010

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

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

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

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

Ответы [ 2 ]

33 голосов
/ 03 февраля 2010

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

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

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

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

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

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

6 голосов
/ 02 февраля 2010

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

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

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

...