Бесплатная zero-инициализация
Вчера я сказал, что иногда в самой программе может попросту отсутствовать код по занулению неинициализированных глобальных переменных. Сегодня разберем, за счет чего это может достигаться.
Во время старта программы ей необходимо выделить память под такие вещи, как стек, кучу, код самой программы и глобальные переменные. Память программе предоставляет операционная система. Ну и естественно, что в эту память раньше была записана какая-то информация. Вообще говоря, потенциально конфиденциальная. То есть раньше был какой-то процесс, который писал информацию в память, завершился, и теперь ее отдают другому процессу.
И что получается, наш новорожденный процесс может видеть какую-то конфиденциальную информацию? Это же большая уязвимость.
Может ли операционная система опираться на честность человека, написавшего код, или на компилятор, что кто-то из них останется приличным парнем и сам занулит всю выданную программе память? В большинстве случаев может. Но здесь очень важны исключения, которых быть не должно.
Поэтому ОС никому не доверяет и сама зануляет всю память, которую выдает новому процессу.
Компилятор/линкер при формировании бинарника собирает все неинициализированные переменные вместе в одну секцию с названием .bss.
Получается, при старте программы у ОС запрашивается память в том числе под секцию .bss, и эта память уже аллоцируется зануленной! И никакого кода не нужно, за нас все делает операционка.
Важное уточнение, что такое поведение наблюдается не у всех операционок. Да, все эти ваши винды, линуксы и прочие макоси зануляют память перед ее передачей другому процессу. Но для каких-нибудь микроконтроллеров это может быть неактуально и компилятор должен честно вставить код зануления для того, чтобы соблюсти требования стандарта.
В чате последние пару дней были бурные обсуждения того, что этого зануления может и не быть. Ну как бы, может и не быть. Только тогда компилятор будет противоречить стандарту. И пользоваться им можно на свой страх и риск.
Don't reveal secrets. Stay cool.
#OS #compiler #cppcore
Вчера я сказал, что иногда в самой программе может попросту отсутствовать код по занулению неинициализированных глобальных переменных. Сегодня разберем, за счет чего это может достигаться.
Во время старта программы ей необходимо выделить память под такие вещи, как стек, кучу, код самой программы и глобальные переменные. Память программе предоставляет операционная система. Ну и естественно, что в эту память раньше была записана какая-то информация. Вообще говоря, потенциально конфиденциальная. То есть раньше был какой-то процесс, который писал информацию в память, завершился, и теперь ее отдают другому процессу.
И что получается, наш новорожденный процесс может видеть какую-то конфиденциальную информацию? Это же большая уязвимость.
Может ли операционная система опираться на честность человека, написавшего код, или на компилятор, что кто-то из них останется приличным парнем и сам занулит всю выданную программе память? В большинстве случаев может. Но здесь очень важны исключения, которых быть не должно.
Поэтому ОС никому не доверяет и сама зануляет всю память, которую выдает новому процессу.
Компилятор/линкер при формировании бинарника собирает все неинициализированные переменные вместе в одну секцию с названием .bss.
Получается, при старте программы у ОС запрашивается память в том числе под секцию .bss, и эта память уже аллоцируется зануленной! И никакого кода не нужно, за нас все делает операционка.
Важное уточнение, что такое поведение наблюдается не у всех операционок. Да, все эти ваши винды, линуксы и прочие макоси зануляют память перед ее передачей другому процессу. Но для каких-нибудь микроконтроллеров это может быть неактуально и компилятор должен честно вставить код зануления для того, чтобы соблюсти требования стандарта.
В чате последние пару дней были бурные обсуждения того, что этого зануления может и не быть. Ну как бы, может и не быть. Только тогда компилятор будет противоречить стандарту. И пользоваться им можно на свой страх и риск.
Don't reveal secrets. Stay cool.
#OS #compiler #cppcore
🔥45👍19❤12❤🔥2😁1
Почему тогда локальные переменные не зануляются?
Вчера мы разобрали, что когда операционка выдает процессу память, она ее зануляет. Тогда получается, что сегмент глобальных данных автоматически заполнен нулями.
Но возникает вопрос: раз ОС такая молодец и зануляет всю память, то почему локальные переменные и куча заполнены мусором? Какие-то двойные стандарты.
Все на самом деле немножко сложнее.
Есть такое понятие, как "zero-fill on demand". Заполнение нулями по требованию.
Когда процесс запрашивает память под свои сегменты, стек и кучу, ОС на самом деле не дает ему реальные страницы памяти. А дает "виртуальные". То есть ничего не аллоцирует по факту. Такие страницы заполнены нулями.
Процесс может свободно читать эти страницы и будет действительно видеть там нули. Однако это не будет физической памятью. Как только процесс захочет что-то записать в нее, только тогда операционка разрождается, реально аллоцирует физическую страницу и копирует в нее содержимое той виртуальной страницы. То есть заполняет физическую нулями.
И так она делает один раз на каждую физическую страницу.
Вот как появляются нули в реальной памяти. Теперь почему они не остаются навсегда.
Дело в том, что процесс переиспользует свою память. Программа в течение всей своей жизни использует один и тот же стек и кучу.
Мы выделили маллоком массив байт, попользовали его и освободили. И эта память не вернулась операционке. Процесс может ее переиспользовать. Да, изначально, при попытке записи в эти байты, ОС выдавала зануленные страницы. Но после того, как мы ими попользовались, там уже лежат наши данные. И с точки зрения куска программы, которая в следующий раз получит эту память, там уже лежит "мусор". Но это просто данные из предыдущей аллокации.
Также и локальные переменные. Мы выполнили одну функцию, вернулись обратно, и выполняя следующую функцию, мы будем переиспользовать память стека под локальные переменные.
Именно поэтому кстати, мы можем очень легко получить доступ к данным, которые лежали на стеке ранее:
Возможный вывод такого кода:
Обратите внимание, что, вызывая функцию с переменной uninitialize в первый раз, мы получили мусор. Однако после вызова func1, где переменная инициализирована, в памяти стека на месте, где лежала initialize будет лежать число 10. Так как сигнатуры и содержимое функций в целом идентичны, то uninitialize во второй раз будет располагаться на том же самом месте, где и была переменная initialize. Соответственно, она будет содержать то же значение.
А учитывая, что до пользовательского кода выполняется некий "скрытый код", то даже в "начале" программы вы будете видеть на стеке мусор.
Reuse resources. Stay cool.
#OS #compiler
Вчера мы разобрали, что когда операционка выдает процессу память, она ее зануляет. Тогда получается, что сегмент глобальных данных автоматически заполнен нулями.
Но возникает вопрос: раз ОС такая молодец и зануляет всю память, то почему локальные переменные и куча заполнены мусором? Какие-то двойные стандарты.
Все на самом деле немножко сложнее.
Есть такое понятие, как "zero-fill on demand". Заполнение нулями по требованию.
Когда процесс запрашивает память под свои сегменты, стек и кучу, ОС на самом деле не дает ему реальные страницы памяти. А дает "виртуальные". То есть ничего не аллоцирует по факту. Такие страницы заполнены нулями.
Процесс может свободно читать эти страницы и будет действительно видеть там нули. Однако это не будет физической памятью. Как только процесс захочет что-то записать в нее, только тогда операционка разрождается, реально аллоцирует физическую страницу и копирует в нее содержимое той виртуальной страницы. То есть заполняет физическую нулями.
И так она делает один раз на каждую физическую страницу.
Вот как появляются нули в реальной памяти. Теперь почему они не остаются навсегда.
Дело в том, что процесс переиспользует свою память. Программа в течение всей своей жизни использует один и тот же стек и кучу.
Мы выделили маллоком массив байт, попользовали его и освободили. И эта память не вернулась операционке. Процесс может ее переиспользовать. Да, изначально, при попытке записи в эти байты, ОС выдавала зануленные страницы. Но после того, как мы ими попользовались, там уже лежат наши данные. И с точки зрения куска программы, которая в следующий раз получит эту память, там уже лежит "мусор". Но это просто данные из предыдущей аллокации.
Также и локальные переменные. Мы выполнили одну функцию, вернулись обратно, и выполняя следующую функцию, мы будем переиспользовать память стека под локальные переменные.
Именно поэтому кстати, мы можем очень легко получить доступ к данным, которые лежали на стеке ранее:
void fun1() {
int initialize = 10;
std::cout << initialize << std::endl;
}
void fun2() {
int uninitialize;
std::cout << uninitialize << std::endl;
}
int main() {
fun2();
fun1();
fun2();
}Возможный вывод такого кода:
32760
10
10
Обратите внимание, что, вызывая функцию с переменной uninitialize в первый раз, мы получили мусор. Однако после вызова func1, где переменная инициализирована, в памяти стека на месте, где лежала initialize будет лежать число 10. Так как сигнатуры и содержимое функций в целом идентичны, то uninitialize во второй раз будет располагаться на том же самом месте, где и была переменная initialize. Соответственно, она будет содержать то же значение.
А учитывая, что до пользовательского кода выполняется некий "скрытый код", то даже в "начале" программы вы будете видеть на стеке мусор.
Reuse resources. Stay cool.
#OS #compiler
👍54❤🔥28🔥15❤4⚡3👎1