Якщо купа нульово ініціалізується для безпеки, то чому стек просто неініціалізований?


15

У моїй системі Debian GNU / Linux 9, коли виконується бінарний файл,

  • стек неініціалізований, але
  • купа ініціалізується нулем.

Чому?

Я припускаю, що нульова ініціалізація сприяє безпеці, але, якщо для купи, то чому б і не для стека? Чи стек теж не потребує безпеки?

Наскільки я знаю, моє питання не стосується Debian.

Зразок коду С:

#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 

Звичайно, стандарт C не вимагає malloc()очищення пам'яті, перш ніж розподіляти її, але моя програма C лише для ілюстрації. Питання - це не питання про С або про стандартну бібліотеку С. Скоріше, питання - це питання про те, чому ядро ​​та / або завантажувач, який працює під час запуску, нулюють купу, а не стек.

ІНШИЙ ДОСВІД

Моє запитання стосується спостережуваної поведінки GNU / Linux, а не вимог стандартних документів. Якщо ви не впевнені, що я маю на увазі, спробуйте цей код, який викликає подальше невизначене поведінку ( невизначене, тобто, що стосується стандарту С), щоб проілюструвати точку:

#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

Що стосується стандарту С, то поведінка не визначена, тому моє питання не стосується стандарту С. Заклик не malloc()потрібно повертати одну і ту ж адресу кожен раз, але, оскільки цей дзвінок malloc()дійсно трапляється щоразу повертати одну і ту ж адресу, цікаво помітити, що пам'ять, що знаходиться в купі, щоразу нулюється.

Стек, навпаки, не здавався нульовим.

Я не знаю, що останній код буде робити на вашій машині, оскільки я не знаю, який шар системи GNU / Linux викликає спостережувану поведінку. Можна, але спробуйте.

ОНОВЛЕННЯ

@Kusalananda помітив у коментарях:

Оскільки це варте, ваш останній код повертає різні адреси та (випадкові) неініціалізовані (ненульові) дані під час запуску на OpenBSD. Це, очевидно, нічого не говорить про поведінку, яку ви спостерігаєте в Linux.

Те, що мій результат відрізняється від результату на OpenBSD, справді цікаво. Мабуть, мої експерименти виявили не протокол безпеки ядра (або лінкера), як я думав, а лише артефакт впровадження.

У цьому світлі я вважаю, що разом відповіді нижче на @mosvy, @StephenKitt та @AndreasGrapentin вирішують моє питання.

Дивіться також про переповнення стека: Чому malloc ініціалізує значення 0 у gcc? (кредит: @bta).


2
Оскільки це варте, ваш останній код повертає різні адреси та (випадкові) неініціалізовані (ненульові) дані під час запуску на OpenBSD. Це, очевидно, нічого не говорить про поведінку, яку ви спостерігаєте в Linux.
Kusalananda

Будь ласка, не змінюйте обсяг свого питання і не намагайтеся його редагувати, щоб зробити відповіді та коментарі зайвими. У C "купа" - це не що інше, як пам'ять, повернута malloc () та calloc (), і лише остання обнуляє пам'ять; newоператор в C ++ (також «купа») на Linux просто обгортка для Танос (); ядро не знає і не хвилює, що таке "купа".
mosvy

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

@Kusalananda Я бачу. Те, що мій результат відрізняється від результату на OpenBSD, справді цікаво. Мабуть, ви та Мосві показали, що мої експерименти виявили не протокол безпеки ядра (або лінкера), як я думав, а просто артефакт впровадження.
вт

@thb Я вважаю, що це може бути правильним спостереженням, так.
Kusalananda

Відповіді:


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

2
Ну, так, але саме тому я задав питання тут, а не про Stack Overflow. Моє запитання стосувалося не стандарту C, а того, як сучасні системи GNU / Linux зазвичай пов'язують і завантажують бінарні файли. Ваш LD_PRELOAD жартівливий, але відповідає на інше питання, ніж на питання, яке я мав намір задати.
вт

19
Я щасливий, що змусив тебе сміятися, але твої припущення та забобони зовсім не смішні. У "сучасній системі GNU / Linux" двійкові файли, як правило, завантажуються динамічним лінкером, який запускає конструктори з динамічних бібліотек, перш ніж потрапити на головну функцію () з вашої програми. У вашій самій системі Debian GNU / Linux 9, і malloc (), і free () будуть викликатись не один раз перед функцією main () з вашої програми, навіть коли не використовується жодна попередньо завантажена бібліотека.
mosvy

23

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

У бібліотеці GNU C на x86-64 виконання починається в точці входу _start , яка закликає __libc_start_mainналаштувати речі, а остання закінчує виклик main. Але перед тим, як зателефонувати main, він викликає ряд інших функцій, через що в стек записуються різні фрагменти даних. Вміст стеку не очищається між викликами функцій, тому коли ви входитеmain , ваш стек містить залишки від попередніх викликів функцій.

Це пояснює лише результати, отримані від стеку, дивіться інші відповіді щодо вашого загального підходу та припущень.


Зауважте, що до моменту main()виклику підпрограми ініціалізації можуть дуже добре змінити пам'ять, повернуту malloc()- особливо якщо бібліотеки C ++ пов'язані. Припустимо, що "купа" ініціалізована до чогось, це дійсно, дуже погане припущення.
Ендрю Генле

Ваша відповідь разом із московськими вирішить моє запитання. На жаль, система дозволяє мені прийняти лише одне з двох; інакше я б прийняв і те, і інше.
вт

18

В обох випадках ви неініціалізовані пам'ять, і ви не можете робити жодних припущень щодо її вмісту.

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

Якщо ви malloc()зможете задовольнити запит із пам'яті, яка вже використовується та звільнена цим процесом, його вміст не буде очищено (насправді очищення не має нічого спільного malloc()і цього не може бути - це повинно відбуватися до того, як пам'ять відображається в ваш адресний простір). Ви можете отримати пам'ять, яка раніше була написана вашим процесом / програмою (наприклад, ранішеmain() ).

У вашій прикладі програми ви бачите malloc()регіон, який ще не був записаний цим процесом (тобто це прямо з нової сторінки) та стек, до якого було записано (попередньоmain() кодом у вашій програмі). Якщо вивчити більше стека, ви побачите, що він заповнений нулем далі (в напрямку його зростання).

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


1
Тиждень-два тому я спробував інший експеримент, дзвонив malloc()і free()повторював. Хоча нічого не вимагає malloc()повторно використовувати той самий сховище, яке нещодавно звільнили, в експерименті malloc()це сталося. Бувало, щоразу повертати одну і ту ж адресу, але також нівелювати пам’ять кожного разу, чого я не очікував. Це мені було цікаво. Подальші експерименти призвели до сьогоднішнього питання.
вт

1
@thb, Можливо, я недостатньо зрозумілий - більшість реалізацій не malloc()роблять абсолютно нічого з пам'яті, яку вони вам передають - це або раніше використовувана, або щойно призначена (і тому ОС нульова). У вашому тесті ви, очевидно, отримали останнє. Так само пам'ять стека надається вашому процесу в очищеному стані, але ви не вивчаєте його досить далеко, щоб побачити частини, які ваш процес ще не торкнувся. Ваш стек пам'яті буде очищений , перш ніж він дав вашому процесу.
Toby Speight

2
@TobySpeight: brk та sbrk застаріли mmap. pubs.opengroup.org/onlinepubs/7908799/xsh/brk.html говорить ЛЕГАЦІЯ вгорі.
Джошуа

2
Якщо вам потрібна ініціалізована пам'ять з використанням, callocможливо, є опція (замість memset)
eckes

2
@thb і Toby: цікавий факт: нові сторінки з ядра часто ліниво виділяються, а просто копіюється при записі, відображається на спільній нульовій сторінці. Це відбувається, mmap(MAP_ANONYMOUS)якщо ви MAP_POPULATEтакож не використовуєте . Сподіваємось, нові сторінки стеків підкріплюються новими фізичними сторінками та з'єднуються (відображаються в таблицях апаратних сторінок, а також у списку покажчиків / довжини ядра відображення) при зростанні, оскільки зазвичай нова пам'ять стека записується при першому торканні. . Але так, ядро ​​повинно якось уникати протікання даних, а нулювання - найдешевше і найкорисніше.
Пітер Кордес

9

Ваша передумова неправильна.

Те, що ви описуєте як "безпека" - це справді конфіденційність , тобто жоден процес не може читати пам'ять інших процесів, якщо ця пам'ять явно не поділяється між цими процесами. В операційній системі це один з аспектів ізоляції одночасних дій або процесів.

Що операційна система робить для забезпечення цієї ізоляції, це щоразу, коли процес запитує пам'ять для накопичення купи або стека, ця пам'ять або надходить з області фізичної пам'яті, яка заповнена нулями, або заповнюється сміттям, тобто що випливає з того самого процесу .

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

Ви занадто багато читаєте у своїх вимірах.


1
Зараз у розділі "Оновлення запитання" явно посилається на вашу освітлюючу відповідь.
вт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.