Рабочий адрес приложения с последующими расширениями кучи и стека - PullRequest
1 голос
/ 06 августа 2020

У меня есть m.c:

extern void a(char*);

int main(int ac, char **av){
    static char string [] = "Hello , world!\n";
    a(string);
}

и a.c:

#include <unistd.h>
#include <string.h>

void a(char* s){
    write(1, s, strlen(s));
}

Я компилирую и собираю их как:

g++ -c -g -std=c++14 -MMD -MP -MF "m.o.d" -o m.o m.c
g++ -c -g -std=c++14 -MMD -MP -MF "a.o.d" -o a.o a.c
g++ -o linux m.o a.o -lm -lpthread -ldl

Затем я проверяю исполняемый файл linux таким образом:

objdump -drwxCS -Mintel linux

Результат этого на моем Ubuntu 16.04.6 начинается с:

start address 0x0000000000400540

затем, позже, это раздел init:

00000000004004c8 <_init>:
  4004c8:   48 83 ec 08             sub    rsp,0x8

И, наконец, раздел fini:

0000000000400704 <_fini>:
  400704:   48 83 ec 08             sub    rsp,0x8
  400708:   48 83 c4 08             add    rsp,0x8
  40070c:   c3                      ret 

Программа ссылается на строку Hello , world!\n который находится в разделе .data, полученном командой:

objdump -sj .data linux

Contents of section .data:
 601030 00000000 00000000 00000000 00000000  ................
 601040 48656c6c 6f202c20 776f726c 64210a00  Hello , world!..

Все это говорит мне, что исполняемый файл был создан таким образом, чтобы быть загруженным по фактическому адресу памяти начиная примерно с 0x0000000000400540 (адрес .init), и программа обращается к данным в фактическом адресе памяти, расширяясь как минимум до 601040 (адрес .data)

Я основываю это на главе 7 «Компоновщики и загрузчики» Джона Р. Левина , где он заявляет:

Компоновщик объединяет набор входных файлов в один выходной файл, который готов к загрузке в указанное время. i c адрес.

Мой вопрос касается следующей строки.

Если при загрузке программы память по этому адресу недоступна, загрузчик должен переместить загруженную программу, чтобы отразить фактический адрес загрузки.

(1) Предположим, у меня есть другой исполняемый файл, который в настоящее время работает на моем компьютере, уже использующий пространство памяти между 400540 и 601040, как определяется, где запустить мой новый исполняемый файл linux?

(2) В связи с этим в главе 4 сказано:

.. Объекты ELF ... загружаются примерно в середине адресного пространства, поэтому стек может расти ниже текстового сегмента, а куча может расти с конца данных, сохраняя общее используемое адресное пространство относительно компактным.

Предположим, что предыдущее запущенное приложение было запущено, скажем, с 200000, а теперь linux начинается с 400540. Нет cla sh или перекрытия адресов памяти. Но по мере продолжения выполнения программ предположим, что куча предыдущего приложения увеличилась до 300000, а стек только что запущенного linux вырос вниз до 310000. Вскоре произойдет столкновение / перекрытие адресов памяти. Что произойдет, когда в конце концов произойдет cla sh?

Ответы [ 2 ]

2 голосов
/ 06 августа 2020

Если при загрузке программы память по этому адресу недоступна, загрузчик должен переместить загруженную программу, чтобы отразить фактический адрес загрузки.

Не весь файл форматы поддерживают это:

G CC для 32-битных Windows добавит информацию, необходимую для загрузчика в случае динамических c библиотек (.dll). Однако информация не добавляется в исполняемые файлы (.exe), поэтому такой исполняемый файл должен быть загружен по фиксированному адресу.

Под Linux это немного сложнее; однако также невозможно загрузить многие (обычно более старые 32-разрядные) исполняемые файлы по разным адресам, в то время как библиотеки Dynami c (.so) могут быть загружены по разным адресам.

Предположим У меня есть еще один исполняемый файл, который в настоящее время работает на моем компьютере, уже использующий пространство памяти между 400540 и 601040 ...

Современные компьютеры (все 32-разрядные компьютеры x86) имеют MMU подкачки который используется в большинстве современных операционных систем. Это некая схема (обычно в ЦП), которая переводит адреса, видимые программным обеспечением, в адреса, видимые ОЗУ. В вашем примере 400540 может быть преобразовано в 1234000, поэтому доступ к адресу 400540 фактически приведет к доступу к адресу 1234000 в ОЗУ.

Дело в том, что современные ОС используют разные конфигурации MMU для разные задачи. Таким образом, если вы снова запустите свою программу, используется другая конфигурация MMU, которая преобразует адрес 400540, видимый программным обеспечением, в адрес 2345000 в ОЗУ. Обе программы, использующие адрес 400540, могут работать одновременно, поскольку одна программа будет обращаться к адресу 1234000, а другая - к адресу 2345000 в ОЗУ, когда программы обращаются к адресу 400540.

Это означает, что некоторый адрес (например, 400540) никогда не будет «уже занят», когда исполняемый файл загружен.

Адрес может уже использоваться, когда динамическая c библиотека (.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 вырос вниз до ...

Скоро будет конфликт / перекрытие адресов памяти. Что происходит, когда в конечном итоге происходит cla sh?

Действительно, размер стека ограничен.

Если вы используете слишком много стека, вы получаете «переполнение стека» .

Эта программа намеренно использует слишком много стека - просто чтобы увидеть, что произойдет:

.globl _start
_start:
    sub $0x100000, %rsp
    push %rax
    push %rax
    jmp _start

В случае операционной системы с MMU (например, Linux) ваш программа выдаст sh с сообщением об ошибке:

~$ ./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. И есть даже ограничения для разработчика программного обеспечения (пример: функции и массивы не должны быть слишком длинными).

Кроме того, каждая программа имеет свой собственный указатель стека, базовый указатель, регистры и т. Д. c.? Или в ОС есть только один набор этих регистров, который будет использоваться всеми программами?

(Предполагая одноядерный ЦП), ЦП имеет один набор регистров; и только одна программа может работать одновременно.

Когда вы запускаете несколько программ, ОС будет переключаться между программами. Это означает, что программа A выполняется (например) 1/50 секунды, затем программа B выполняется в течение 1/50 секунды, затем программа A выполняется в течение 1/50 секунды и так далее. Вам кажется, что программы выполняются одновременно.

Когда ОС переключается с программы A на программу B, она должна сначала сохранить значения регистров (программы A). Затем он должен изменить конфигурацию MMU. Наконец, он должен восстановить значения регистров программы B.

2 голосов
/ 06 августа 2020

Да, objdump в этом исполняемом файле показывает адреса, по которым будут отображаться его сегменты. (Связывание объединяет разделы в сегменты: В чем разница раздела и сегмента в формате файла ELF ) .data и .text связываются с разными разделами с разными разрешениями (чтение + запись против чтения + exe c).

Если при загрузке программы память по этому адресу недоступна

Это могло произойти только при загрузке библиотека Dynami c, а не сам исполняемый файл. Виртуальная память означает, что каждый процесс имеет собственное частное виртуальное адресное пространство, даже если они были запущены из одного и того же исполняемого файла. (Вот почему ld всегда может выбрать один и тот же базовый адрес по умолчанию для сегментов text и data, не пытаясь разместить каждый исполняемый файл и библиотеку в системе в разных местах в одном адресном пространстве.)

Исполняемый файл - это первое, что может претендовать на части этого адресного пространства, когда он загружается / отображается загрузчиком программы ELF ОС. Вот почему традиционные (не P IE) исполняемые файлы ELF могут быть не перемещаемыми, в отличие от общих объектов ELF, таких как /lib/libc.so.6

. Если вы выполняете пошаговую программу с помощью отладчика или включаете режим сна, вы будет время посмотреть less /proc/<PID>/maps. Или cat /proc/self/maps, чтобы кошка показывала вам свою карту. (Также /proc/self/smaps для получения более подробной информации о каждом отображении, например, насколько оно загрязнено, с использованием огромных страниц и т. Д. c.)

(Новые дистрибутивы GNU / Linux настраивают G CC на сделать P IE исполняемыми файлами по умолчанию: 32-битные абсолютные адреса больше не разрешены в x86-64 Linux? . В этом случае objdump будет видеть только адреса относительно базы 0 или 1000 или что-то в этом роде. А asm, сгенерированный компилятором, использовал бы P C -относительную адресацию, а не абсолютную.)

...