Если куча инициализируется нулями для безопасности, то почему стек просто неинициализирован?

15

В моей системе Debian GNU / Linux 9, когда исполняется двоичный файл,

  • стек неинициализирован, но
  • куча инициализируется нулями.

Почему?

Я предполагаю, что нулевая инициализация способствует безопасности, но если для кучи, то почему не для стека? Стек тоже не нуждается в безопасности?

Насколько я знаю, мой вопрос не является специфичным для Debian.

Пример кода C:

#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>

const size_t n = 8;

// --------------------------------------------------------------------
// UNINTERESTING CODE
// --------------------------------------------------------------------
static void print_array(
  const int *const p, const size_t size, const char *const name
)
{
    printf("%s at %p: ", name, p);
    for (size_t i = 0; i < size; ++i) printf("%d ", p[i]);
    printf("\n");
}

// --------------------------------------------------------------------
// INTERESTING CODE
// --------------------------------------------------------------------
int main()
{
    int a[n];
    int *const b = malloc(n*sizeof(int));
    print_array(a, n, "a");
    print_array(b, n, "b");
    free(b);
    return 0;
}

Выход:

a at 0x7ffe118997e0: 194 0 294230047 32766 294230046 32766 -550453275 32713 
b at 0x561d4bbfe010: 0 0 0 0 0 0 0 0 

malloc()Конечно, стандарт C не требует очистки памяти перед ее выделением, но моя программа на C предназначена только для иллюстрации. Вопрос не в вопросе о Си или о стандартной библиотеке Си. Скорее, вопрос заключается в том, почему ядро ​​и / или загрузчик времени выполнения обнуляют кучу, а не стек.

ДРУГОЙ ЭКСПЕРИМЕНТ

Мой вопрос касается наблюдаемого поведения GNU / Linux, а не требований стандартов. Если вы не уверены, что я имею в виду, попробуйте этот код, который вызывает дальнейшее неопределенное поведение ( неопределенное, то есть, что касается стандарта C), чтобы проиллюстрировать это:

#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>

const size_t n = 4;

int main()
{
    for (size_t i = n; i; --i) {
        int *const p = malloc(sizeof(int));
        printf("%p %d ", p, *p);
        ++*p;
        printf("%d\n", *p);
        free(p);
    }
    return 0;
}

Выход из моей машины:

0x555e86696010 0 1
0x555e86696010 0 1
0x555e86696010 0 1
0x555e86696010 0 1

Что касается стандарта C, поведение не определено, поэтому мой вопрос не касается стандарта C. Вызов malloc()необязательно возвращать один и тот же адрес каждый раз, но, поскольку этот вызов malloc()действительно каждый раз возвращает один и тот же адрес, интересно отметить, что память, находящаяся в куче, каждый раз обнуляется.

Стек, напротив, не был обнулен.

Я не знаю, что будет делать последний код на вашей машине, поскольку я не знаю, какой уровень системы GNU / Linux вызывает наблюдаемое поведение. Вы можете попробовать это.

ОБНОВИТЬ

@Kusalananda заметил в комментариях:

Для чего он стоит, ваш последний код возвращает разные адреса и (иногда) неинициализированные (ненулевые) данные при запуске на OpenBSD. Это, очевидно, ничего не говорит о поведении, которое вы наблюдаете в Linux.

То, что мой результат отличается от результата на OpenBSD, действительно интересно. Судя по всему, в ходе моих экспериментов я обнаружил не протокол безопасности ядра (или компоновщика), как я думал, а простой артефакт реализации.

В этом свете я считаю, что вместе приведенные ниже ответы @mosvy, @StephenKitt и @AndreasGrapentin решают мой вопрос.

См. Также Переполнение стека: почему malloc инициализирует значения в 0 в gcc? (кредит: @bta).

THB
источник
2
Для чего он стоит, ваш последний код возвращает разные адреса и (иногда) неинициализированные (ненулевые) данные при запуске на OpenBSD. Это, очевидно, ничего не говорит о поведении, которое вы наблюдаете в Linux.
Кусалананда
Пожалуйста, не изменяйте объем своего вопроса и не пытайтесь редактировать его, чтобы сделать ответы и комментарии излишними. В C «куча» - это не что иное, как память, возвращаемая malloc () и calloc (), и только последняя обнуляет память; newоператор в C ++ (также «куча») на Linux просто обертка для таНоса (); ядро не знает и не заботится о том, что такое «куча».
Мосви
3
Ваш второй пример - просто разоблачение артефакта реализации malloc в glibc; если вы сделаете это повторно с помощью malloc / free с буфером размером более 8 байт, вы четко увидите, что только первые 8 байт обнуляются.
Мосви
@ Кусалананда, я вижу. То, что мой результат отличается от результата на OpenBSD, действительно интересно. По всей видимости, вы и Мосви показали, что в ходе моих экспериментов был обнаружен не протокол безопасности ядра (или компоновщика), как я думал, а простой артефакт реализации.
THB
@ thb Я считаю, что это может быть правильным наблюдением, да.
Кусалананда

Ответы:

28

Хранилище, возвращаемое функцией malloc (), не инициализируется нулями. Никогда не предполагайте, что это так.

В вашей тестовой программе это просто случайность: я думаю, что malloc()только что получил новый блок mmap(), но также не полагайтесь на это.

Например, если я запускаю вашу программу на моем компьютере следующим образом:

$ echo 'void __attribute__((constructor)) p(void){
    void *b = malloc(4444); memset(b, 4, 4444); free(b);
}' | cc -include stdlib.h -include string.h -xc - -shared -o pollute.so

$ LD_PRELOAD=./pollute.so ./your_program
a at 0x7ffd40d3aa60: 1256994848 21891 1256994464 21891 1087613792 32765 0 0
b at 0x55834c75d010: 67372036 67372036 67372036 67372036 67372036 67372036 67372036 67372036

Ваш второй пример - просто разоблачение артефакта mallocреализации в glibc; если вы сделаете это повторно malloc/ freeс буфером больше 8 байт, вы ясно увидите, что только первые 8 байт обнуляются, как в следующем примере кода.

#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>

const size_t n = 4;
const size_t m = 0x10;

int main()
{
    for (size_t i = n; i; --i) {
        int *const p = malloc(m*sizeof(int));
        printf("%p ", p);
        for (size_t j = 0; j < m; ++j) {
            printf("%d:", p[j]);
            ++p[j];
            printf("%d ", p[j]);
        }
        free(p);
        printf("\n");
    }
    return 0;
}

Выход:

0x55be12864010 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 
0x55be12864010 0:1 0:1 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 
0x55be12864010 0:1 0:1 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 
0x55be12864010 0:1 0:1 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4
mosvy
источник
2
Ну да, но именно поэтому я задал вопрос здесь, а не о переполнении стека. Мой вопрос был не о стандарте C, а о том, как современные системы GNU / Linux обычно связывают и загружают двоичные файлы. Ваш LD_PRELOAD шутит, но отвечает на другой вопрос, кроме вопроса, который я хотел задать.
THB
19
Я рад, что заставил вас смеяться, но ваши предположения и предубеждения совсем не смешны. В «современной системе GNU / Linux» двоичные файлы обычно загружаются динамическим компоновщиком, который запускает конструкторы из динамических библиотек, прежде чем перейти к функции main () из вашей программы. В вашей самой системе Debian GNU / Linux 9 и malloc (), и free () будут вызываться более чем один раз перед функцией main () из вашей программы, даже если вы не используете предварительно загруженные библиотеки.
мосвы
23

Независимо от того, как инициализируется стек, вы не видите нетронутый стек, потому что библиотека C делает множество вещей перед вызовом main, и они касаются стека.

С библиотекой GNU C, в x86-64, выполнение начинается с точки входа _start , которая вызывает __libc_start_mainнастройку, а последняя в итоге вызывает main. Но перед вызовом mainон вызывает ряд других функций, что приводит к записи в стек различных фрагментов данных. Содержимое стека не очищается между вызовами функций, поэтому, когда вы входите main, ваш стек содержит остатки от предыдущих вызовов функций.

Это только объясняет результаты, которые вы получаете из стека, смотрите другие ответы относительно вашего общего подхода и предположений.

Стивен Китт
источник
Обратите внимание, что к тому времени, когда main()вызывается, подпрограммы инициализации вполне могут иметь измененную память, возвращаемую malloc()- особенно если библиотеки C ++ связаны. Предположение, что «куча» инициализируется чем-либо, является действительно очень плохим предположением.
Эндрю Хенле
Ваш ответ вместе с Мосви разрешит мой вопрос. Система, к сожалению, позволяет мне принять только один из двух; в противном случае я бы принял оба.
THB
18

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

Когда ОС должна распределить новую страницу в вашем процессе (будь то для своего стека или для используемой арены malloc()), она гарантирует, что не будет представлять данные из других процессов; обычный способ убедиться, что это заполнить его нулями (но в равной степени допустимо перезаписывать что-либо еще, включая даже ценность страницы /dev/urandom- на самом деле некоторые malloc()реализации отладки пишут ненулевые шаблоны, чтобы отлавливать ошибочные предположения, такие как ваше).

Если malloc() можно удовлетворить запрос из памяти, уже использованной и освобожденной этим процессом, его содержимое не будет очищено (фактически, очистка не имеет ничего общего malloc()и не может быть - это должно произойти до того, как память будет отображена в Ваше адресное пространство). Вы можете получить память, которая была ранее записана вашим процессом / программой (например, раньше main()).

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

Если вы действительно хотите понять, что происходит на уровне ОС, я рекомендую вам обойти уровень библиотеки C и взаимодействовать с помощью системных вызовов, таких как brk()и mmap()вместо.

Тоби Спейт
источник
1
Неделю или две назад я пробовал другой эксперимент, звонил malloc()и free()неоднократно. Хотя ничто не требует malloc()повторного использования того же хранилища, которое было недавно освобождено, в ходе эксперимента malloc()это произошло. Случалось, что каждый раз возвращался один и тот же адрес, но также каждый раз обнулялась память, чего я не ожидал. Это было интересно для меня. Дальнейшие эксперименты привели к сегодняшнему вопросу.
THB
1
@ thb, Возможно, я не достаточно ясен - большинство реализаций malloc()абсолютно ничего не делают с памятью, которую они вам вручают - она ​​либо используется ранее, либо назначается заново (и поэтому обнуляется ОС). В вашем тесте вы, очевидно, получили последнее. Точно так же память стека передается вашему процессу в очищенном состоянии, но вы недостаточно изучите его, чтобы увидеть части, которых ваш процесс еще не коснулся. Ваш стек памяти будет очищен , прежде чем он дал вашему процессу.
Тоби Спейт
2
@TobySpeight: brk и sbrk устарели из mmap. pubs.opengroup.org/onlinepubs/7908799/xsh/brk.html говорит, что LEGACY прямо вверху.
Джошуа
2
Если вам нужна инициализированная память, callocможно использовать опцию (вместо memset)
eckes
2
@thb и Toby: забавный факт: новые страницы из ядра часто распределяются лениво и просто копируются при записи, сопоставляются с общей обнуленной страницей. Это происходит, mmap(MAP_ANONYMOUS)если вы не используете, MAP_POPULATEа также. Надеемся, что новые страницы стека поддерживаются свежими физическими страницами и подключаются (отображаются в таблицах аппаратных страниц, а также в списке отображений указателя / длины ядра) при увеличении, потому что обычно новая стековая память записывается при первом прикосновении , Но да, ядро ​​должно как-то избегать утечки данных, и обнуление является самым дешевым и наиболее полезным.
Питер Кордес
9

Ваша предпосылка неверна.

То, что вы описываете как «безопасность», на самом деле является конфиденциальностью , что означает, что ни один процесс не может считывать память других процессов, если только эта память явно не используется этими процессами. В операционной системе это один из аспектов изоляции одновременных действий или процессов.

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

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

Вы слишком много читаете по своим измерениям.

Андреас Грапентин
источник
1
Раздел «Обновление вопроса» теперь явно ссылается на ваш яркий ответ.
THB