В моей системе 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).
new
оператор в C ++ (также «куча») на Linux просто обертка для таНоса (); ядро не знает и не заботится о том, что такое «куча».Ответы:
Хранилище, возвращаемое функцией malloc (), не инициализируется нулями. Никогда не предполагайте, что это так.
В вашей тестовой программе это просто случайность: я думаю, что
malloc()
только что получил новый блокmmap()
, но также не полагайтесь на это.Например, если я запускаю вашу программу на моем компьютере следующим образом:
Ваш второй пример - просто разоблачение артефакта
malloc
реализации в glibc; если вы сделаете это повторноmalloc
/free
с буфером больше 8 байт, вы ясно увидите, что только первые 8 байт обнуляются, как в следующем примере кода.Выход:
источник
Независимо от того, как инициализируется стек, вы не видите нетронутый стек, потому что библиотека C делает множество вещей перед вызовом
main
, и они касаются стека.С библиотекой GNU C, в x86-64, выполнение начинается с точки входа _start , которая вызывает
__libc_start_main
настройку, а последняя в итоге вызываетmain
. Но перед вызовомmain
он вызывает ряд других функций, что приводит к записи в стек различных фрагментов данных. Содержимое стека не очищается между вызовами функций, поэтому, когда вы входитеmain
, ваш стек содержит остатки от предыдущих вызовов функций.Это только объясняет результаты, которые вы получаете из стека, смотрите другие ответы относительно вашего общего подхода и предположений.
источник
main()
вызывается, подпрограммы инициализации вполне могут иметь измененную память, возвращаемуюmalloc()
- особенно если библиотеки C ++ связаны. Предположение, что «куча» инициализируется чем-либо, является действительно очень плохим предположением.В обоих случаях вы получаете неинициализированную память и не можете делать никаких предположений относительно ее содержимого.
Когда ОС должна распределить новую страницу в вашем процессе (будь то для своего стека или для используемой арены
malloc()
), она гарантирует, что не будет представлять данные из других процессов; обычный способ убедиться, что это заполнить его нулями (но в равной степени допустимо перезаписывать что-либо еще, включая даже ценность страницы/dev/urandom
- на самом деле некоторыеmalloc()
реализации отладки пишут ненулевые шаблоны, чтобы отлавливать ошибочные предположения, такие как ваше).Если
malloc()
можно удовлетворить запрос из памяти, уже использованной и освобожденной этим процессом, его содержимое не будет очищено (фактически, очистка не имеет ничего общегоmalloc()
и не может быть - это должно произойти до того, как память будет отображена в Ваше адресное пространство). Вы можете получить память, которая была ранее записана вашим процессом / программой (например, раньшеmain()
).В вашем примере программы вы видите
malloc()
регион, который еще не был записан этим процессом (т. Е. Прямо с новой страницы), и стек, в который был записан (с помощью предварительногоmain()
кода в вашей программе). Если вы изучите больше стека, вы обнаружите, что он заполнен нулями дальше вниз (в направлении роста).Если вы действительно хотите понять, что происходит на уровне ОС, я рекомендую вам обойти уровень библиотеки C и взаимодействовать с помощью системных вызовов, таких как
brk()
иmmap()
вместо.источник
malloc()
иfree()
неоднократно. Хотя ничто не требуетmalloc()
повторного использования того же хранилища, которое было недавно освобождено, в ходе экспериментаmalloc()
это произошло. Случалось, что каждый раз возвращался один и тот же адрес, но также каждый раз обнулялась память, чего я не ожидал. Это было интересно для меня. Дальнейшие эксперименты привели к сегодняшнему вопросу.malloc()
абсолютно ничего не делают с памятью, которую они вам вручают - она либо используется ранее, либо назначается заново (и поэтому обнуляется ОС). В вашем тесте вы, очевидно, получили последнее. Точно так же память стека передается вашему процессу в очищенном состоянии, но вы недостаточно изучите его, чтобы увидеть части, которых ваш процесс еще не коснулся. Ваш стек памяти будет очищен , прежде чем он дал вашему процессу.calloc
можно использовать опцию (вместоmemset
)mmap(MAP_ANONYMOUS)
если вы не используете,MAP_POPULATE
а также. Надеемся, что новые страницы стека поддерживаются свежими физическими страницами и подключаются (отображаются в таблицах аппаратных страниц, а также в списке отображений указателя / длины ядра) при увеличении, потому что обычно новая стековая память записывается при первом прикосновении , Но да, ядро должно как-то избегать утечки данных, и обнуление является самым дешевым и наиболее полезным.Ваша предпосылка неверна.
То, что вы описываете как «безопасность», на самом деле является конфиденциальностью , что означает, что ни один процесс не может считывать память других процессов, если только эта память явно не используется этими процессами. В операционной системе это один из аспектов изоляции одновременных действий или процессов.
Операционная система делает это для обеспечения такой изоляции: всякий раз, когда процесс запрашивает память для выделения кучи или стека, эта память либо поступает из области физической памяти, которая заполнена нулями, либо заполняется ненужными файлами, которые исходя из того же процесса .
Это гарантирует , что вы только когда - либо видеть нули, или свой собственный мусор, так что конфиденциальность гарантируется, и как кучного и стека «обеспечить», хотя и не обязательно (нулевой) инициализируется.
Вы слишком много читаете по своим измерениям.
источник