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

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

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

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

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


person Michael L.    schedule 02.02.2010    source источник
comment
Если 0x08048000 когда-либо был STACK_TOP, это было очень давным-давно. Последний TASK_SIZE полностью до 2.0.40.   -  person ivan_pozdeev    schedule 28.09.2016
comment
x86-64 Linux выбирает низкий адрес (Почему адрес 0x400000 выбран в качестве начала текстового сегмента в x86_64 ABI?): избегая wiki.debian.org/mmap_min_addr и выбор начала группы страниц размером 2 МБ рядом с началом низкий 1ГиБ. Почему адрес 0x400000 выбран в качестве начала текстового сегмента в x86_64 ABI? также объясняет некоторые мотивы выбора i386 0x080xxxxx.   -  person Peter Cordes    schedule 25.05.2021


Ответы (2)


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

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

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

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

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

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

person Thomas Pornin    schedule 02.02.2010

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

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

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

person Community    schedule 02.02.2010
comment
В C нулевые указатели могут иметь совершенно другое значение, чем 0 на самом низком уровне. В рамках C (исходный код) недопустимое значение указателя машины должно отображаться в 0. Технически не требуется, чтобы нулевой указатель C фактически отображался в адрес ноль. - person datenwolf; 01.08.2011
comment
_GLOBAL_OFFSET_TABLE_ также указывает на 0x200XXX диапазон в Binutils 2.24. - person Ciro Santilli 新疆再教育营六四事件ۍ 01.06.2015
comment
@datenwolf Это спорный вопрос, все современные процессоры представляют адреса как два дополнительных целых числа, представляя NULL как что-либо, кроме 0, в этом случае было бы бессмысленным падением производительности. То, что это разрешено стандартом, не означает, что это хорошая идея. Даже во встроенных средах с очень ограниченным объемом памяти адрес 0x00 обычно зарезервирован для NULL. - person yyny; 02.07.2020
comment
@yyny: Что-то, что-то 8086 в реальном режиме ... Также я имел в виду не дополнение 2s и другие виды числового представления, а значения trap. - person datenwolf; 02.07.2020
comment
@datenwolf Я не знаю компиляторов C, предназначенных для 8086. Это вся моя точка зрения. Стандарт ANSI C был написан с учетом прямой совместимости в то время, когда еще можно было представить, что сегментированная память станет обычным явлением. В настоящее время практически каждый процессор использует адресацию на основе целых чисел с дополнением до двух, что означает, что нет практических причин представлять NULL как ненулевое значение. На данный момент C более 30 лет, и все согласны с тем, что преобразование константы указателя NULL в целое число приводит к значению 0. - person yyny; 02.07.2020
comment
Или, с другой стороны, с помощью страничной памяти адрес 0x00 может быть сопоставлен с любой таблицей страниц, которую пожелает ядро. Фактически, на некоторых материнских платах и ​​с некоторыми загрузчиками можно оставить первый слот ОЗУ пустым, что означает, что в каком-то смысле это даже не физический адрес 0. Однако все это остается спорным. На самом деле каждый мыслимый процессор, на котором работает C, может представлять адрес 0x00, и каждое мыслимое ядро ​​и приложение ожидают, что это будет так. Ссылка на стандарт C как на истину в последней инстанции контрпродуктивна и вредна для начинающих программистов. - person yyny; 02.07.2020