Адрес логической памяти для переменных в C - PullRequest
2 голосов
/ 20 октября 2019

Рассмотрим следующий фрагмент кода:

#include <stdio.h>
int main() {
  int a = 10;
  printf("%d %p\n", a, &a);
}

Если я несколько раз скомпилирую и выполню приведенный выше код, он напечатает разные значения для адресной части оператора printf.

Если объем логической памяти 16 бит, адрес оператора & должен быть в диапазоне от 0x0000 до 0xFFFF. Мы знаем, что адрес оператора & не одинаков для разных исполнений. Мой вопрос - каковы причины, ведущие к этой неопределенности в назначении адресов памяти? Поскольку логический адрес сопоставлен с физическим адресом, не должно ли быть возможным иметь согласованные значения логических адресов, даже если физические адреса меняются?

Кроме того, если я разветвляю процесс, дочерний процесс и родительский процесс напечатаютточно такой же вывод для оператора printf. Почему вышеописанное поведение не происходит, когда мы разветвляем дочерний процесс, даже если он порождает новый процесс?

1 Ответ

1 голос
/ 20 октября 2019

Цитируется из ответа , связанного @tpr в комментариях, наблюдаемая вами разница в адресах обусловлена ​​ рандомизацией расположения адресного пространства :

Локальные переменные размещаются в стеке. Традиционно распределение стека повторяется, но в последние годы ситуация изменилась. Рандомизация размещения адресного пространства (ASR) - сравнительно недавнее нововведение в управлении памятью ОС, которое намеренно делает адреса памяти в распределениях стека (например, те, которые вы наблюдали) максимально недетерминированными во время выполнения. Это функция безопасности: это не позволяет плохим субъектам использовать переполнения буфера кучи, потому что, если реализация ASLR достаточно энтропийна, кто знает, что будет в конце переполненного буфера?

Важно, что ASLR применяется для выделения самого стека (вместе с другими областями данных, подключенными к исполняемому файлу). Как кратко сказано в Википедии:

Чтобы предотвратить надежный переход злоумышленника, например, к определенной эксплуатируемой функции в памяти, ASLR случайным образом размещает позиции в адресном пространстве ключевых областей данных процесса. , включая базу исполняемого файла и позиции стека, кучи и библиотек.

В разветвленном процессе адрес один и тот же: , а не из-за копирование при записи , как я первоначально ответил. Даже если вы измените переменную в разветвленном процессе, адрес останется прежним (хотя будет сделана копия переменной). Попробуйте запустить следующий код:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main() {
  int a = 10;
  int status;
  printf("%d %p\n", a, &a);
  pid_t pid = fork();
  if (pid == 0)
  {
    printf("FORKED: %d %p\n", a, &a);
    a = 11;
    printf("FORKED: %d %p\n", a, &a);
    return 0;
  } else {
  wait(&status);
  printf("%d %p\n", a, &a);
  return 0;
  }
}

, и вы увидите, что a изменяется только в разветвленном процессе, но родительский процесс напечатает его без изменений. Однако адрес остается одинаковым во всех напечатанных строках. При написании этого ответа это стало неожиданностью, поэтому после поиска я нашел этот вопрос . Ответ довольно прост:

Каждый отдельный процесс получает свое собственное 4G виртуальное адресное пространство, и задача менеджеров операционной системы и аппаратной памяти состоит в том, чтобы сопоставить ваши виртуальные адреса с физическимииз них.

Итак, хотя может показаться, что два процесса имеют один и тот же адрес для переменной, это всего лишь виртуальный адрес.

Диспетчер памяти отобразит его всовершенно другой физический адрес

Следующие две цитаты взяты из fork(2) справочных страниц:

Дочерний процесс создается с помощью одного потока- тот, который называется fork (). Все виртуальное адресное пространство родительского объекта реплицируется в дочернем объекте, включая состояния мьютексов, переменных условий и других объектов pthreads

[...]

Под Linux fork ()реализуется с использованием страниц копирования при записи, поэтому единственное наказание, которое оно несет, - это время и память, необходимые для дублирования таблиц страниц родителя и создания уникальной структуры задач для дочернего элемента.

Из-за copy-on-write , упомянутого во второй цитате, базовый физический адрес может быть одинаковым для равного виртуального адреса памяти в разветвленном процессе и его родительском процессе. Из Википедии:

Копирование при записи ( CoW или COW ), иногда упоминаемое как неявное совместное использование или shadowing - это метод управления ресурсами, используемый в компьютерном программировании для эффективной реализации операции «дублировать» или «копировать» на изменяемых ресурсах. Если ресурс дублирован, но не изменен, нет необходимости создавать новый ресурс;ресурс может быть разделен между копией и оригиналом. Модификации все равно должны создавать копию, отсюда и техника: операция копирования откладывается до первой записи.

Следовательно, до тех пор, пока переменная не будет изменена (или не будет назван член семейства exec*), одни и те же виртуальные адреса, скорее всего, будут соответствовать одному и тому же физическому адресу (исключения см. В man-страницах).

...