Если при загрузке программы память по этому адресу недоступна, загрузчик должен переместить загруженную программу, чтобы отразить фактический адрес загрузки.
Не все форматы файлов поддерживают это:
GCC для 32-битной Windows добавит информацию, необходимую для загрузчика в случае динамических библиотек (.dll
). Однако в исполняемые файлы (.exe
) информация не добавляется, поэтому такой исполняемый файл должен загружаться по фиксированному адресу.
В Linux это немного сложнее; однако также невозможно загрузить много (обычно более старых 32-разрядных) исполняемых файлов по разным адресам, в то время как динамические библиотеки (.so
) можно загрузить по разным адресам.
Предположим, у меня есть другой исполняемый файл, который в данный момент работает на моей машине и уже использует пространство памяти между 400540
и 601040
...
Современные компьютеры (все 32-разрядные компьютеры с архитектурой x86) имеют MMU подкачки, который используется большинством современных операционных систем. Это некоторая схема (обычно в ЦП), которая переводит адреса, видимые программным обеспечением, в адреса, видимые ОЗУ. В вашем примере 400540
может быть переведено в 1234000
, поэтому доступ к адресу 400540
фактически приведет к доступу к адресу 1234000
в ОЗУ.
Дело в том, что современные ОС используют разные конфигурации MMU для разных задач. Поэтому, если вы снова запустите свою программу, будет использоваться другая конфигурация MMU, которая преобразует адрес 400540
, видимый программным обеспечением, в адрес 2345000
в ОЗУ. Обе программы, использующие адрес 400540
, могут работать одновременно, потому что одна программа будет фактически обращаться к адресу 1234000
, а другая будет обращаться к адресу 2345000
в ОЗУ, когда программы будут обращаться к адресу 400540
.
Это означает, что какой-либо адрес (например, 400540
) никогда не будет уже использоваться при загрузке исполняемого файла.
Адрес может уже использоваться, когда загружается динамическая библиотека (.so
/.dll
), поскольку эти библиотеки совместно используют память с исполняемым файлом.
... как решается, где запустить мой новый исполняемый файл Linux?
В Linux исполняемый файл будет загружен по фиксированному адресу, если он был связан таким образом, что его нельзя переместить на другой адрес. (Как уже было сказано: это было типично для старых 32-битных файлов.) В вашем примере строка Hello world будет располагаться по адресу 0x601040
если ваш компилятор и компоновщик создали исполняемый файл таким образом.
Однако большинство 64-разрядных исполняемых файлов можно загрузить по другому адресу. Linux загрузит их по какому-то случайному адресу из соображений безопасности, что затрудняет атаку вирусов или других вредоносных программ на программу.
... так что стек может расти ниже текстового сегмента ...
Я никогда не видел такой схемы памяти ни в одной операционной системе:
И под Linux, и под Solaris стек располагался в конце адресного пространства (где-то около 0xBFFFFF00
), а текстовый сегмент загружался довольно близко к началу памяти (возможно, адрес 0x401000
).
... и куча может расти с конца данных, ...
допустим наползает куча предыдущего приложения...
Многие реализации с конца 1990-х больше не используют кучу. Вместо этого они используют mmap()
для резервирования новой памяти.
Согласно странице руководства brk()
, куча была объявлена устаревшей функцией в 2001 году, поэтому она больше не должна использоваться новыми программами.
(Однако, по словам Питера Кордеса, malloc()
в некоторых случаях по-прежнему использует кучу.)
В отличие от простых операционных систем, таких как MS-DOS, Linux не позволяет вам просто использовать кучу, но вы должны вызвать функцию brk()
, чтобы сообщить Linux, какой объем кучи вы хотите использовать.
Если программа использует кучу и использует больше кучи, чем доступно, функция brk()
возвращает некоторый код ошибки, а функция malloc()
просто возвращает NULL
.
Однако такая ситуация обычно возникает из-за нехватки оперативной памяти, а не из-за того, что куча перекрывается какой-либо другой областью памяти.
... в то время как стек только что запущенного Linux вырос вниз до ...
Вскоре произойдет конфликт/перекрытие адресов памяти. Что происходит, когда столкновение в конце концов происходит?
Действительно, размер стека ограничен.
Если вы используете слишком много стека, у вас возникает переполнение стека.
Эта программа намеренно использует слишком много стека — просто чтобы посмотреть, что произойдет:
.globl _start
_start:
sub $0x100000, %rsp
push %rax
push %rax
jmp _start
В случае операционной системы с MMU (например, Linux) ваша программа вылетит с сообщением об ошибке:
~$ ./example_program
Segmentation fault (core dumped)
~$
ИЗМЕНИТЬ/ДОБАВИТЬ
Стек для всех запущенных программ находится в конце?
В более старых версиях Linux стек располагался рядом (но не точно) с концом виртуальной памяти, доступной программе: программы могли получить доступ к диапазону адресов от 0
до 0xBFFFFFFF
в этих версиях Linux. Начальный указатель стека располагался вокруг 0xBFFFFE00
. (Аргументы командной строки и переменные окружения идут после стека.)
И это конец фактической физической памяти? Не перемешается ли тогда стек разных запущенных программ? У меня сложилось впечатление, что весь стек и память программы остаются непрерывными в реальной физической памяти,...
На компьютере с MMU программа никогда не видит физическую память:
Когда программа загружается, ОС будет искать какую-то свободную область ОЗУ - возможно, она найдет что-то по физическому адресу 0xABC000
. Затем он настраивает MMU таким образом, что виртуальные адреса 0xBFFFF000-0xBFFFFFFF
преобразуются в физические адреса 0xABC000-0xABCFFF
.
Это означает: Всякий раз, когда программа обращается к адресу 0xBFFFFE20
(например, используя операцию push
), фактически осуществляется доступ к физическому адресу 0xABCE20
в ОЗУ.
У программы вообще нет возможности получить доступ к определенному физическому адресу.
Если у вас запущена другая программа, MMU настроен таким образом, что адреса 0xBFFFF000-0xBFFFFFFF
преобразуются в адреса 0x345000-0x345FFF
при выполнении другой программы.
Таким образом, если одна из двух программ выполнит операцию push
, а указатель стека равен 0xBFFFFE20
, будет осуществлен доступ к адресу 0xABCE20
в ОЗУ; если другая программа выполняет операцию push
(с тем же значением указателя стека), будет осуществлен доступ к адресу 0x345E20
.
Поэтому стеки не будут смешиваться.
ОС, не использующие MMU, но поддерживающие многозадачность (примерами являются Amiga 500 или ранние Apple Macintosh), конечно, не будут работать таким образом. Такие ОС используют специальные форматы файлов (а не ELF), которые оптимизированы для запуска нескольких программ без MMU. Компиляция программ для таких ОС намного сложнее, чем компиляция программ для Linux или Windows. И даже есть ограничения для разработчика ПО (пример: функции и массивы не должны быть слишком длинными).
Кроме того, есть ли у каждой программы собственный указатель стека, базовый указатель, регистры и т. д.? Или у ОС есть только один набор этих регистров, который будет использоваться всеми программами?
(Предполагая одноядерный ЦП), ЦП имеет один набор регистров; и одновременно может работать только одна программа.
Когда вы запускаете несколько программ, ОС будет переключаться между ними. Это означает, что программа А работает (например) 1/50 секунды, затем программа В работает 1/50 секунды, затем программа А работает 1/50 секунды и так далее. Вам кажется, что программы работают одновременно.
Когда ОС переключается с программы А на программу Б, она должна сначала сохранить значения регистров (программы А). Затем он должен изменить конфигурацию MMU. Наконец, он должен восстановить значения регистров программы B.
person
Martin Rosenau
schedule
06.08.2020