Пост создан для публикации одной единственной картинки (кликабельна). На мой взгляд, она как нельзя лучше передаёт атмосферу того как устроена память в компьютере. Я изобразил её на 3-4 курсе института на A4 при объяснении положения дел товарищу. Так она мне тогда понравилась, что дал себе обещание нарисовать её в электронном виде.
Исходник в формате VSD (Microsoft Visio) лежит у меня в DropBox. Если имеются уточнения, поправки, комментарии — прошу под кат.
Отношение между ВАП и ВП:
Образ загружается в виртуальную память системы (ВП) единожды и проецируется в те виртуальные адресные пространства (ВАП), в которых он востребован. Образ EXE или DLL идентифицируется уникальным путём к файлу. Соответственно, если к одному и тому же файлу ведут несколько путей, можно произвести множественную загрузку одного и того же модуля. В системе имеется набор DLL, которые подгружаются во все программы, такие как kernel32.dll и прочие…
Отношение между ВП и ФП:
Физическая память (ФП) состоит из страниц. Любой модуль EXE или DLL занимает некоторое число таких страниц. При загрузке системой модулей для них ищутся свободные страницы, в которые загружается образ файла. Модули проецируются в виртуальную память и загружаются в физическую по мере необходимости средствами ОС. Также проецирование можно использовать для пользовательских файлов (также называется маппинг файлов — mapping).
Отношение между ФП и железом:
Операционная система может по своему усмотрению перемещать страницы физической памяти в файл подкачки и обратно. При обращении исполняемого кода к странице, находящейся в файле подкачки, производится выгрузка страницы в ОЗУ. Если к странице долгое время не осуществлялся доступ и возникла потребность в памяти, ОС может перемещение страницы в файл подкачки.
В прошлый раз мы рассмотрели вопросы использования глобальных переменных. Прежде, чем двинуться дальше, нужно дать небольшой вводный курс по памяти в Windows.
Любая вещь в вашей программе занимает «память компьютера». Это может быть строка, число, открытый файл, запись, объект, форма и даже сам код. Даже хотя вы явно никого не просили выделять память, она всё равно выделена автоматически — либо компилятором, либо операционной системой.
Адресное пространство и все, все, все…
Кратко говоря, память программы может рассматриваться как один очень-очень длинный ряд байтов. Байт — это единица измерения количества информации, в мире Delphi и Windows он равен восьми битам и может хранить одно из 256
различных значений (от 0
до 255
). На память можно смотреть как на массив байт. Что именно содержат эти байты — зависит от того, как интерпретировать их содержимое, т.е. от того, как их используют. Значение 97 может означать число 97
, или же ANSI букву 'a'
. Если вы рассматриваете вместе несколько байт, то вы можете хранить и большие значения. Например, в 2-х байтах вы можете хранить одно из 256*256 = 65536
различных значений, две ANSI буквы 'ab'
или Unicode букву 'a'
— и т.д.
Чтобы обратиться к конкретному байту в памяти (адресовать его), можно присвоить каждому байту номер, пронумеровав их целыми положительными числами, включив ноль за начало отсчёта. Индекс байта в этом огромном массиве и называется его адресом, а весь массив целиком — памятью программы. Диапазон адресов от 0
до максимума называется адресным пространством программы. А максимум (длина) массива называется размером адресного пространства.
(примечание: ну, на самом деле, есть тысяча и один способ адресовать память, но в рамках современного мира и этой статьи мы ограничимся только этим способом).
Адресное пространство (вернее, его размер) определяет способность программы работать с данными. Чем оно больше — тем с большим количеством данных программа сможет работать (в один момент времени). Если у программы заканчивается свободное место в адресном пространстве (т.е. все адреса в нём выделены под какие-то объекты в программе) — то у программы заканчивается память (out of memory).
Как адресное пространство соотносится с вашим исходным кодом
С точки зрения языка высокого уровня (Паскаль) все вещи в вашей программе характеризуются именем (идентификатором), типом («сколько памяти выделять») и семантикой («что с этим можно делать»). Например, целое число занимает 4
байта и их можно читать, писать, складывать и т.п. И число A — это не то же самое, что число B. Строки же занимают переменный объём памяти, их, к примеру, можно соединять и редактировать. И так далее.
Но на уровне машинного языка, железа и операционной системы все они характеризуются только местоположением, размером (в байтах) и атрибутами доступа. Местоположение — это адрес объекта. К примеру, число A может иметь адрес 1234
, а число B — 1238
. И поэтому это два разных числа — потому что у них разный адрес, т.е. они лежат в разных местах. Атрибут доступа является упрощённой «семантикой», которая определяет то, что можно делать с памятью. А таких вещей всего три: читать, писать и выполнять. Последнее означает исполнение машинного кода. Тут нужно пояснить, что ваши данные (числа, строки, формы и т.п.) находятся в одном «контейнере» (том самом «массиве памяти из байт») вместе с кодом программы — .exe файлом. Иными словами, код рассматривается наравне с данными, а чтобы их отличать и служат атрибуты доступа.
Можно увидеть, как понятия языка высокого уровня («имя», «тип» и «семантика») проецируются в понятия низкого уровня («адрес», «размер» и «атрибуты доступа»).
Древний мир
В давние времена память программы была тождественно равна оперативной памяти машины (т.н. ОЗУ или RAM — Random Access Memory). Иными словами, размер адресного пространства программы был равен размеру установленной оперативной памяти. Вот, установлено на вашей машине две планки памяти по 64
Кб — значит, у вашей программы есть 128
Кб памяти. Ну, за вычетом той памяти, что уже занята, конечно же. Адрес объекта программы был равен адресу физической ячейке оперативной памяти (физическому адресу). И если у вас заканчивалось место в ОЗУ, то у вас заканчивалась память в программе.
Конечно, такой способ хотя и весьма прост, имеет две проблемы:
- Память программы ограничена оперативной памятью. А раньше эта память была дорогой и её было очень мало.
- Если нужно запустить две программы, то они будут работать «в одной песочнице»: и первая и вторая программа будут размещать свои данные в одном месте — оперативной памяти. И если первая программа по ошибке запишет что-то в данные второй, то… ой.
Виртуальная память и виртуальное адресное пространство
Поэтому в современном мире используется совершенно другая схема: во-первых, память программы теперь больше не тождественна оперативной памяти. Теперь программа работает исключительно с так называемой «виртуальной памятью». Виртуальная память — это имитация реальной памяти. Она позволяет каждой программе:
- считать, что установлено максимальное теоретически возможное количество оперативной памяти;
- считать, что она является единственной программой, запущенной на машине.
Иными словами, адресное пространство программы более не ограничено размером физической памяти (так называют оперативную память компьютера, чтобы специально указать на её отличие от виртуальной памяти) — адресное пространство имеет теперь максимально возможный размер. К примеру, если для адресации используются 32-битные указатели (4 байта), то размер адресного пространства равен 2^32 = 4'294'967'296
байт. Т.е. 4 миллиарда (если угодно: биллионов) или 4 Гб. А размерность адресного пространства — равна 32
.
В связи с новомодным «переходом на 64
бита» нужно упомянуть, что этот переход заключается в замене 4
-байтных (32
-битных) указателей на 8-байтные (64-битные) — что увеличивает размер адресного пространства программы аж до 2^64 = 18'446'744'073'709'551'616
байт. Т.е. 18
с лишним квинтиллионов байт или 16
Эб (эксабайт) для краткости. Соответственно, 32
-битный указатель может быть любым числом от 0
до 4'294'967'296
(от $00000000
до $FFFFFFFF
). 64
-разрядный указатель может варьироваться от $00000000'00000000
до $FFFFFFFF'FFFFFFFF
.
А из второго пункта следует, что 4
Гб или 16
Эб есть у каждой программы. Т.е. каждой программе отводится своё личное закрытое адресное пространство. Такая изолированность означает, что программа А в своем адресном пространстве может хранить какую-то запись данных по адресу $12345678
, и одновременно у программы В по тому же адресу $12345678
(но уже в его адресном пространстве) может находиться совершенно иная запись данных. Если программа A попробует прочитать данные по адресу $12345678
, то она получит доступ к своей записи (записи программы A), а не данным программы B. Но если к адресу $12345678
обратится программа B, то она получит свою запись, а не запись программы А. Иными словами, программа A не может обратиться в памяти (адресному пространству) программы B и наоборот.
Таким образом, при использовании виртуальной памяти упрощается программирование, так как программисту больше не нужно учитывать ограниченность памяти, или согласовывать использование памяти с другими приложениями. Для программы выглядит доступным и непрерывным всё допустимое адресное пространство, вне зависимости от наличия в компьютере соответствующего объема ОЗУ. Если программы выделяют в их адресных пространствах больше памяти, чем есть в системе физической памяти, то часть памяти из ОЗУ переносится на диск («винчестер») — в т.н. файл подкачки (его ещё называют страничным файлом, page file, SWAP-файлом или «свопом»). Когда программа обращается к своим данным, которые были выгружены на диск, то операционная система автоматически загрузит данные из файла подкачки в ОЗУ. И всё это происходит под капотом — т.е. совершенно незаметно для программы. С точки зрения программы, ей кажется, что она работает с 4
Гб или 16
Эб RAM.
Применение механизма виртуальной памяти позволяет:
- упростить адресацию памяти программами;
- рационально управлять оперативной памятью компьютера (хранить в ней только активно используемые области памяти);
- изолировать программы друг от друга (программа полагает, что монопольно владеет всей памятью).
А теперь, пока вы не перевозбудились от колоссального объема адресного пространства, предоставляемого вашей программе: вспомните, что оно — виртуальное, а не физическое. Другими словами, (виртуальное) адресное пространство — всего лишь диапазон адресов памяти. Конечно, нехватка памяти теперь не происходит, когда заканчивается свободное место в оперативной памяти. И на машине с 256
Мб ОЗУ, любая программа может выделить, скажем, один кусок в 512
Мб памяти. Конечно же, это не означает, что вы можете выделить аж 16
эксабайт — ведь реальный размер ограничен размером диска. И не факт, что в системе будет диск на 16
эксабайт. Тем не менее, это значительно лучше, чем просто 256
Мб оперативной памяти, установленные на вашем «старичке».
(примечание: по непонятной мне причине, некоторые люди не верят в тот простой факт, что программа может спокойно выделить больше памяти, чем установлено физической памяти в системе; звучит как сюжет для разрушителей легенд (MythBusters)).
Чем чаще системе приходится копировать данные из оперативной памяти в файл подкачки и наоборот, тем больше нагрузка на жесткий диск и тем медленнее работает операционная система (при этом может получиться так, что операционная система будет тратить всё свое время на подкачку памяти, вместо выполнения программ). Поэтому, добавив компьютеру оперативной памяти, вы снизите частоту обращения к жёсткому диску и, тем самым, увеличите общую производительность системы. Кстати, во многих случаях увеличение оперативной памяти дает больший выигрыш в производительности, чем замена старого процессора на новый. А с падением цен на память уже не проблема собрать систему с 16
или 32
Гб оперативной памяти по доступной цене.
Факты о виртуальном адресном пространстве
Хотя в самом начале мы рассматривали память программы (адресное пространство) как один непрерывный однородный блок, сейчас настало время сделать уточнение, что я вам наврал: таковым он не является. Адресное пространство, хотя действительно однородно и непрерывно более чем на 99%
, но в нём есть несколько специальных областей. Я не буду подробно разбирать их все, скажу только о самых важных.
Во-первых, это область для отлова нулевых указателей. Это, определённо, самая важная специальная часть адресного пространства. Начинается она в нуле и заканчивается на адресе 65'535
. Т.е. имеет размер в 64
Кб и расположена в диапазоне $00000000-$0000FFFF
— самом начале адресного пространства. Специальна эта область тем, что она всегда заблокирована: в ней нельзя выделить память, а любое обращение по этим адресам всегда возбуждает исключение access violation (примечание: это не единственная причина возбуждения access violation). Эта область сделана исключительно для нашего удобства. Как вы узнаете потом (или уже знаете), нулевой указатель nil
по числовому значению равен 0
. И если вы случайно (по ошибке) обратитесь к нулевому указателю — то эта область поможет вам возбудить исключение и поймать вашу ошибку.
А что такого особенного в числе 65'535
? Ну, 64
Кб — это гранулярность выделения памяти. Гранулярность выделения памяти определяет, блоками каких размеров вы можете оперировать при выделении и освобождении памяти. Т.е. гранулярность выделения памяти в 64
Кб означает, что вы можете выделять только блоки памяти, размер которых кратен 64
Кб. Зачем так делается? Ну, если вы попробуете вести учёт «выделенности» каждого байта в программе, то размер управляющих структур у вас превысит размер самих данных. Поэтому память выделяют «кластерами». Иными словами, если вы хотите расположить область в начале адресного пространства, то вы не можете выделить меньше, чем 64
Кб. А больше? Больше — можно. Например, 64 + 64 = 128
Кб. Но большого смысла в этом нет.
Почему гранулярность выделения памяти равна именно 64
Кб, а не, скажем, 8
Кб? Ну, на это есть исторические причины.
(примечание: полностью аналогичный блок расположен на границе 2
Гб — но уже по совершенно другим причинам).
Далее, что вам ещё нужно знать про виртуальное адресное пространство — оно доступно вам не полностью. Грубо говоря, в виртуальном адресном пространстве каждой программы сосуществуют сама программа и операционная система. Та часть, где работает ваша программа (и о котором мы говорили всё это время выше), называется разделом для кода и данных пользовательского режима (user mode). Та часть, где работает операционная система, называется разделом для кода и данных режима ядра (kernel mode). Обе эти части находятся в едином адресном пространстве программы.
Чем они отличаются? Про пользовательский раздел мы уже много чего сказали: он свой у каждой программы и это полностью ваш раздел — делайте что хотите. Раздел ядра является здесь особенным в двух моментах: во-первых, у вашей программы нет к нему никакого доступа. Вообще и в принципе это невозможно. Там орудует только операционная система, но не вы. Если вы попробуете обратиться к памяти в этом разделе, то получите просто access violation. Во-вторых, особенность раздела в том, что он разделяется между всеми программами. Да, вот так: пользовательская часть у каждого адресного пространства своя, но часть ядра — одна и та же, общая. По сути, раздел ядра является «адресным пространством режима ядра».
Какой размер имеют эти две части адресного пространства? Ну, если мы говорим про 32
-разрядную программу, то пользовательский раздел занимает от 2
до 4
Гб (по умолчанию — 2
Гб). Соответственно, режим ядра занимает от 0
до 2
Гб (ибо суммарно должно быть 4
Гб). Конечно же, это за вычетом уже упоминаемых специальных областей. Итого: по умолчанию адресное пространство 32
-разрядной программы делится пополам. Половина — вам, и половина — операционной системе.
(примечание: 0 Гб под режим ядра — это специальный особый случай, достижимый только при запуске 32-битной программы на 64-битной машине. В обычных условиях граница между разделами может двигаться от 2 до 3 Гб).
Если говорить совсем точно, то раздел для ваших данных в случае 32
-х бит имеет диапазон $0000FFFF-$7FFEFFFF
(или $BFFFFFFF
в максимуме на 3 Гб, с дыркой на 64
Кб в районе 2
Гб), а раздел режима ядра — $80000000-$FFFFFFFF
(или $C0000000-$FFFFFFFF
в максимуме для user mode). В случае 64
-разрядной программы ситуация будет несколько иная. На сегодняшний день в Windows соотношение выглядит так: user mode — $00000000'00010000-$000003FF'FFFEFFFF
(8
Тб); kernel mode — $00000400'00000000-$FFFFFFFF'FFFFFFFF
. Ну, это всё ещё недостаточно точно, ведь, на самом деле, режим ядра в случае 64
-х бит использует только максимум несколько сотен Гб, оставляя большую часть адресного пространства попросту неиспользуемой. Т.е. у нас в дополнение к двум областям (user mode и kernel mode) появляется ещё и третья: зарезервированная область. Которую, впрочем, со стороны user mode удобно считать частью kernel mode. Сделано это по той простой причине, что 64
-битное адресное пространство настолько огромно, что user mode и kernel mode выглядели бы в нём тонюсенькими полосочками, вздумай бы вы изобразить их графически и в масштабе. А если место просто зарезервировано, то и не нужно делать для него управляющих данных. Даже 8
Тб памяти для user mode — это очень много. Если бы вы выделяли мегабайт памяти в секунду, у вас бы ушло три месяца, чтобы исчерпать такое адресное пространство.
Это что касается изолированности одной программы от других и от операционной системы. Внутри программы её модули (exe, DLL, bpl) друг от друга, вообще говоря, никак не изолированы. Однако на практике граница всё же появляется, но связана она с языковыми различиями и особенностью управления памятью в разных языках программирования. Но это разговор для другого раза.
Если вы забудете всё то, что я тут говорил, то вот факт, который вы должны вынести из этого обсуждения: размер памяти программы ограничен 2 Гб (32-битная программа) или 8 Тб (64-битная программа), либо суммарным размером оперативной памяти и файлом подкачки — смотря что меньше, а что больше. Т.е. на практике вы получаете «out of memory» только когда превышаете размер в 2 Гб.
Операции, производимые с виртуальной памятью
Ну, вполне очевидно, что прежде чем использовать память, вы должны её выделить (commit), а после окончания работы — освободить (release). Конечно, вам не обязательно делать это в явном виде — как я уже сказал, часто за вас это делает кто-то другой автоматически. Но об этом в следующий раз.
Помимо двух операций (выделения и освобождения памяти) существует и третья операция — резервирование (reserve) памяти. Смысл её заключается в том, что под зарезервированную виртуальную память не выделяется никакой реальной памяти (будь то оперативная память или файл подкачки), но при этом память считается занятой, как если бы она была выделена. Позднее, вы можете выделить реальную память этому зарезервированному блоку (полностью или частями).
Зачем нужна такая операция? Ну, предположим, вам нужен непрерывный блок памяти для работы (к примеру, чтобы обработать его за один проход одним циклом), но вы выделяете память не сразу а частями — по мере надобности. Вы не можете просто выделить первый блок, а потом — второй: ведь тогда нет гарантии, что они будут идти друг за другом. Вот поэтому и придумали операцию резервирования: вы резервируете достаточно большой регион. Это — «бесплатно». Потом вы выделяете в нём реальную память. Обе цели достигнуты: вы и выделяете память по мере необходимости (а не сразу целиком), и вы получаете свою непрерывную область памяти.
Кстати, все три операции выполняются функциями VirtualAlloc
и VirtualFree
. Не забудьте только, что мы говорили про гранулярность выделения памяти в 64
Кб.
И снова: какое это имеет отношение к Delphi?
Ну, почти самое прямое. Ведь программа на Delphi должна выделять и освобождать память. Это значит, что ей нужно вызывать функции VirtualAlloc
и VirtualFree
. А выделять память она будет в своём (виртуальном) адресном пространстве — причём, только в пользовательской его части.
Операции с памятью в Delphi проводятся через функции GetMem
и FreeMem
. Конечно же, кроме этих функций в Delphi существует и много других — но они являются лишь обёртками или переходниками к GetMem
и FreeMem
. Эти обёртки (например: AllocMem
, New
, Dispose
, SetLength
и т.п.) предоставляют дополнительную функциональность и удобство (кстати, в системе тоже есть обёртки к вызовам VirtualAlloc
и VirtualFree
). В некоторых случаях, эти вызовы и вовсе скрыты и происходят автоматически под капотом языка. Например, при сложении двух строк:
var S1, S2, S3: String; begin S1 := S2 + S3;
вы не видите вызов GetMem
, но он здесь есть.
Зачем нужны «свои» подпрограммы управления памятью? Почему нельзя просто использовать VirtualAlloc
и VirtualFree
? Ну, Delphi тут не уникальна — большинство языков используют т.н. менеджеры памяти — это код, который в Delphi стоит за вызовами GetMem
и FreeMem
, который служит переходником к VirtualAlloc
и VirtualFree
. А делается это по причине всё той же гранулярности выделения в 64
Кб. Т.е. если вы создаёте 100
объектов по, скажем, 12
байт, то вместо двух килобайт (12
б * 100 = 1.2
Кб + служебные данные менеджера памяти) вы занимаете уже почти 6.5
Мб (64 * 100 = 6'400
Кб) — на несколько порядков больше! Использовали бы вы VirtualAlloc — вы бы очень быстро исчерпали свободную память. Менеджер памяти же «упаковывает» несколько запросов на выделение памяти в один блок.
(примечание: «упаковка» ни в коем случае не означает «сжатие» или «кодирование» — это просто размещение нескольких маленьких кусочков памяти в одном 64 Кб блоке).
Заметьте, что операции резервирования памяти у Delphi нет, т.к. подобная операции не имеет большого смысла при «упаковке» запросов менеджером памяти. Для работы с резервированием используются функции операционной системы.
Продолжение следует…
Вот и все базовые сведения про устройство памяти в Windows, которые вам нужно знать для начала. В следующий раз мы более близко посмотрим на то, как архитектура памяти соотносится с переменными в ваших программах.
См. также: Архитектура памяти в Windows: мифы и легенды (spin-off).
Читать далее: Адресное пространство под микроскопом.
Виртуальная память
Виртуальная память — это важная часть операционных систем, включая Windows. Она представляет собой механизм, позволяющий приложениям, работать с большими объемами памяти, чем физически доступно на компьютере, и обеспечивает изоляцию процессов друг от друга. Вот основные аспекты виртуальной памяти в Windows:
Виртуальная адресация
Каждому процессу в Windows предоставляется свое собственное виртуальное адресное пространство. Это означает, что каждый процесс видит свою собственную непрерывную область адресов, начиная с нуля. Этот механизм позволяет изолировать процессы друг от друга, так что один процесс не может напрямую обратиться к памяти другого процесса.
Физическая память и страничный файл
Виртуальная память Windows состоит из физической оперативной памяти (RAM) и страничного файла на диске. Если физическая память заполняется, то часть данных может быть перемещена в страничный файл, освобождая место для новых данных. Этот процесс называется “подкачкой” (paging).
Страницы памяти
Виртуальная память разбивается на небольшие блоки, называемые страницами памяти. Размер страницы обычно составляет 4 КБ. Windows использует систему управления таблицами страниц (Page Table) для отображения виртуальных адресов на физические адреса или на адреса в страничном файле.
Отображение виртуальной памяти
Когда процесс обращается к виртуальной памяти, операционная система Windows преобразует виртуальный адрес в соответствующий физический адрес. Если требуемая страница находится в физической памяти, это происходит незаметно. Если страница находится в страничном файле, она должна быть загружена в физическую память перед доступом к ней.
Защита памяти
Виртуальная память Windows также обеспечивает механизмы защиты. Каждая страница памяти может иметь разрешения на чтение, запись и выполнение. Это позволяет операционной системе и программам контролировать доступ к памяти и предотвращать некорректное или вредоносное поведение.
Управление виртуальной памятью
Операционная система Windows автоматически управляет виртуальной памятью, включая подкачку данных между физической памятью и страничным файлом. Программисты обычно не заботятся о деталях управления виртуальной памятью, но могут использовать API для запроса дополнительной памяти (например, функции VirtualAlloc
) и управления защитой памяти (например, функции VirtualProtect
).
Управление динамической памятью
Управление памятью в Windows может быть выполнено с использованием различных функций и API операционной системы. Давайте рассмотрим несколько примеров кода на языке C/C++ для выделения и освобождения памяти в Windows.
Выделение памяти с использованием malloc
и free
(C/C++)
#include <stdio.h>
#include <stdlib.h>
int main() {
// Выделение памяти под массив целых чисел
int *arr = (int*)malloc(5 * sizeof(int));
if (arr == NULL) {
printf("Не удалось выделить память\n");
return 1;
}
// Использование выделенной памяти
for (int i = 0; i < 5; i++) {
arr[i] = i * 10;
}
// Освобождение памяти после использования
free(arr);
return 0;
}
В этом примере мы используем функции malloc
для выделения памяти под массив целых чисел и free
для освобождения этой памяти после ее использования.
Выделение памяти с использованием функции VirtualAlloc
(WinAPI)
#include <Windows.h>
#include <stdio.h>
int main() {
// Выделение 1 мегабайта (1048576 байт) виртуальной памяти
LPVOID mem = VirtualAlloc(NULL, 1048576, MEM_COMMIT, PAGE_READWRITE);
if (mem == NULL) {
printf("Не удалось выделить виртуальную память\n");
return 1;
}
// Использование выделенной виртуальной памяти
// Освобождение виртуальной памяти
VirtualFree(mem, 0, MEM_RELEASE);
return 0;
}
Здесь мы используем функцию VirtualAlloc
из библиотеки WinAPI для выделения виртуальной памяти. После использования памяти мы освобождаем ее с помощью функции VirtualFree
.
Выделение и освобождение памяти с использованием C++ операторов new
и delete
#include <iostream>
#include <windows.h>
int main() {
SetConsoleOutputCP(1251);
// Выделение памяти под одно целое число
int *num = new int;
// Использование выделенной памяти
*num = 42;
std::cout << "Значение: " << *num << std::endl;
// Освобождение памяти
delete num;
return 0;
}
Стек и куча
Стек и куча — это две основные области памяти, используемые в программах для хранения данных и управления памятью. Они имеют разные характеристики и предназначены для разных целей. Давайте рассмотрим их более подробно:
Стек (Stack)
- Характеристики:
- Ограниченный по размеру.
- Доступ к данным выполняется в порядке “первым вошел, последним вышел” (LIFO — Last-In, First-Out).
- Часто фиксированный размер стека определяется на этапе компиляции.
- Использование:
- Хранит локальные переменные функций и адреса возврата после вызова функций.
- Используется для управления вызовами функций (стек вызовов).
- Жизненный цикл данных:
- Данные, хранящиеся в стеке, автоматически удаляются при завершении функции, в которой они определены.
- Ограниченное время жизни.
- Примеры языков:
- Стек используется в C, C++, Java (для вызовов методов), Python (для вызовов функций).
Куча (Heap)
- Характеристики:
- Динамически расширяемая область памяти.
- Доступ к данным происходит в произвольном порядке.
- Размер кучи ограничен объемом доступной физической и виртуальной памяти.
- Использование:
- Хранит данные, которые могут иметь долгий или неопределенный срок жизни, такие как объекты, созданные динамически.
- Жизненный цикл данных:
- Данные, хранящиеся в куче, существуют до тех пор, пока на них есть указатели, и могут быть освобождены вручную (например, с помощью
free
в C/C++ или сборщика мусора в других языках).
- Данные, хранящиеся в куче, существуют до тех пор, пока на них есть указатели, и могут быть освобождены вручную (например, с помощью
- Примеры языков:
- Куча используется в C, C++, C#, Java (для объектов, созданных с помощью
new
), Python (с использованием модуляgc
для сборки мусора).
- Куча используется в C, C++, C#, Java (для объектов, созданных с помощью
Сравнение стека и кучи
-
Стек обычно быстрее доступен для чтения и записи, чем куча.
-
Куча предоставляет более гибкое управление памятью, но требует явного освобождения ресурсов.
-
Стек обеспечивает управление временем жизни данных автоматически, в то время как в куче это делается вручную.
-
Использование стека ограничено, поэтому он лучше подходит для хранения данных с известным временем жизни, в то время как куча подходит для данных с неопределенным или долгим временем жизни.
-
Оба механизма имеют свои применения и зависят от конкретных требований программы.
Функции для работы со стеком
Windows предоставляет набор функций и API для работы со стеком приложения. Эти функции позволяют программам управлять стеком вызовов функций, а также получать информацию о текущем состоянии стека. Вот некоторые из наиболее часто используемых функций Windows для работы со стеком:
GetCurrentThreadStackLimits (Windows 8.1 и более поздние версии)
Эта функция позволяет получить информацию о границах стека текущего потока. Она возвращает указатель на начало и конец стека текущего потока. Это может быть полезно, например, для отслеживания использования стека и предотвращения переполнения стека.
Пример использования:
void GetStackLimits() {
ULONG_PTR lowLimit, highLimit;
GetCurrentThreadStackLimits(&lowLimit, &highLimit);
printf("Low Limit: 0x%llx\n", lowLimit);
printf("High Limit: 0x%llx\n", highLimit);
}
RtlCaptureContext (Windows XP и более поздние версии)
Эта функция захватывает текущий контекст выполнения, включая информацию о регистрах и указателях стека. Это может быть полезно при анализе стека или сохранении контекста выполнения для последующего использования.
Пример использования:
CONTEXT context;
RtlCaptureContext(&context);
// Теперь у вас есть информация о контексте выполнения текущего потока
VirtualQuery (Windows XP и более поздние версии)
Эта функция позволяет получить информацию о виртуальной памяти, включая стек. Вы можете использовать ее для определения границ стеков разных потоков или для анализа виртуальной памяти вашего процесса.
Пример использования:
MEMORY_BASIC_INFORMATION mbi;
VirtualQuery(&someAddress, &mbi, sizeof(mbi));
// Теперь вы можете получить информацию о найденной памяти, включая стек
SetThreadStackGuarantee (Windows 8 и более поздние версии)
Эта функция позволяет установить минимальный размер стека для потока. Это может быть полезно, чтобы предотвратить переполнение стека в потоках с большой глубиной вызовов.
Пример использования:
DWORD stackSize = 0x10000; // 64 КБ
SetThreadStackGuarantee(&stackSize);
StackWalk64 (DbgHelp API)
Эта функция из библиотеки DbgHelp API позволяет выполнять обход стека вызовов функций для получения информации о вызовах и адресах функций. Она полезна при создании отладочных и профилирующих инструментов.
Пример использования:
STACKFRAME64 stackFrame;
// Настройка параметров и выполнение обхода стека
Функции для работы с кучей
WinAPI предоставляет ряд функций для работы с кучей (памятью, выделяемой в куче). Основные функции включают в себя HeapCreate
, HeapAlloc
, HeapFree
, HeapReAlloc
и HeapDestroy
. Давайте рассмотрим эти функции более подробно:
HeapCreate
-
Создает новую кучу.
-
Синтаксис:
HANDLE HeapCreate(DWORD flOptions, SIZE_T dwInitialSize, SIZE_T dwMaximumSize);
-
Пример:
HANDLE hHeap = HeapCreate(0, 0, 0);
HeapAlloc
-
Выделяет блок памяти из кучи.
-
Синтаксис:
LPVOID HeapAlloc(HANDLE hHeap, DWORD dwFlags, SIZE_T dwBytes);
-
Пример:
int* pData = (int*)HeapAlloc(hHeap, 0, sizeof(int) * 10);
HeapFree
-
Освобождает блок памяти, выделенный ранее с помощью
HeapAlloc
. -
Синтаксис:
BOOL HeapFree(HANDLE hHeap, DWORD dwFlags, LPVOID lpMem);
-
Пример:
HeapFree(hHeap, 0, pData);
HeapReAlloc
-
Изменяет размер выделенного блока памяти в куче.
-
Синтаксис:
LPVOID HeapReAlloc(HANDLE hHeap, DWORD dwFlags, LPVOID lpMem, SIZE_T dwBytes);
-
Пример:
pData = (int*)HeapReAlloc(hHeap, 0, pData, sizeof(int) * 20);
HeapDestroy
-
Уничтожает кучу и освобождает все связанные с ней ресурсы.
-
Синтаксис:
BOOL HeapDestroy(HANDLE hHeap);
-
Пример:
HeapSize
-
Возвращает размер выделенного блока памяти в куче.
-
Синтаксис:
SIZE_T HeapSize(HANDLE hHeap, DWORD dwFlags, LPCVOID lpMem);
-
Пример:
SIZE_T size = HeapSize(hHeap, 0, pData);
HeapValidate
-
Проверяет целостность кучи и выделенных блоков.
-
Синтаксис:
BOOL HeapValidate(HANDLE hHeap, DWORD dwFlags, LPCVOID lpMem);
-
Пример:
if (HeapValidate(hHeap, 0, pData)) { printf("Куча валидна.\n"); } else { printf("Куча повреждена.\n"); }
Пример 1: Создание кучи и выделение памяти
#include <Windows.h>
#include <stdio.h>
int main() {
SetConsoleOutputCP(1251);
// Создание кучи
HANDLE hHeap = HeapCreate(0, 0, 0);
if (hHeap == NULL) {
printf("Не удалось создать кучу\n");
return 1;
}
// Выделение памяти из кучи
int *data = (int*)HeapAlloc(hHeap, 0, sizeof(int) * 5);
if (data == NULL) {
printf("Не удалось выделить память из кучи\n");
HeapDestroy(hHeap);
return 1;
}
// Использование выделенной памяти
for (int i = 0; i < 5; i++) {
data[i] = i * 10;
}
// Освобождение памяти
HeapFree(hHeap, 0, data);
// Уничтожение кучи
HeapDestroy(hHeap);
return 0;
}
В этом примере мы создаем кучу с помощью HeapCreate
, выделяем память из кучи с помощью HeapAlloc
, используем эту память и освобождаем ее с помощью HeapFree
, а затем уничтожаем кучу с помощью HeapDestroy
.
Пример 2: Выделение строки в куче
#include <Windows.h>
#include <stdio.h>
int main() {
SetConsoleOutputCP(1251);
// Создание кучи
HANDLE hHeap = HeapCreate(0, 0, 0);
if (hHeap == NULL) {
printf("Не удалось создать кучу\n");
return 1;
}
// Выделение строки в куче
char *str = (char*)HeapAlloc(hHeap, 0, 256);
if (str == NULL) {
printf("Не удалось выделить память для строки\n");
HeapDestroy(hHeap);
return 1;
}
// Копирование строки в выделенную память
strcpy_s(str, 256, "Пример строки в куче");
// Использование строки
// Освобождение памяти
HeapFree(hHeap, 0, str);
// Уничтожение кучи
HeapDestroy(hHeap);
return 0;
}
В этом примере мы выделяем память для строки в куче, копируем строку в эту память, используем ее и освобождаем память.
Отображение файлов на адресное пространство
File mapping (сопоставление файла) в WinAPI — это механизм, который позволяет отображать содержимое файла в виртуальную память процесса. Это может быть полезно для обмена данными между процессами, создания разделяемой памяти или для улучшения производительности при доступе к большим файлам. Давайте рассмотрим основы использования file mapping в WinAPI:
Создание файла для сопоставления
Сначала необходимо создать или открыть файл, который вы хотите сопоставить. Это можно сделать с помощью функций, таких как CreateFile
или OpenFile
. Например:
HANDLE hFile = CreateFile(
L"C:\\example.txt", // Имя файла
GENERIC_READ | GENERIC_WRITE, // Режим доступа
0, // Атрибуты файла
NULL, // Дескриптор безопасности
OPEN_ALWAYS, // Действие при открытии (создать, если не существует)
FILE_ATTRIBUTE_NORMAL, // Атрибуты файла
NULL // Шаблон для атрибутов
);
Создание отображения файла в памяти
Затем создайте отображение файла в виртуальную память с помощью функции CreateFileMapping
. Это создает объект отображения файла, который может быть использован для доступа к содержимому файла:
HANDLE hMapFile = CreateFileMapping(
hFile, // Дескриптор файла
NULL, // Атрибуты безопасности (можно использовать NULL)
PAGE_READWRITE, // Режим доступа к файлу в отображении
0, // Размер отображения файла (0 - весь файл)
0, // Высший значащий байт размера файла
NULL // Имя отображения файла (можно использовать NULL)
);
Отображение файла в виртуальную память
Завершите процесс сопоставления файла, отображая его в виртуальную память с помощью функции MapViewOfFile
:
LPVOID pData = MapViewOfFile(
hMapFile, // Дескриптор отображения файла
FILE_MAP_ALL_ACCESS, // Режим доступа к отображению
0, // Смещение в файле
0, // Начальный байт отображения
0 // Размер отображения (0 - весь файл)
);
Использование данных
Теперь pData
указывает на начало отображения файла в виртуальной памяти. Вы можете работать с данными, как с обычной памятью.
Освобождение ресурсов
После завершения работы с данными не забудьте освободить ресурсы:
UnmapViewOfFile(pData); // Освобождение отображения файла
CloseHandle(hFile); // Закрытие дескриптора файла
CloseHandle(hMapFile); // Закрытие дескриптора отображения файла
Наверх
Аннотация: Виртуальная память. Реализация виртуальной памяти в Windows. Структура виртуального адресного пространства. Выделение памяти процессам. Дескрипторы виртуальных адресов. Трансляция адресов. Ошибки страниц. Пределы памяти.
Виртуальная память
Всем процессам в операционной системе Windows предоставляется важнейший ресурс – виртуальная память (virtual memory). Все данные, с которыми процессы непосредственно работают, хранятся именно в виртуальной памяти.
Название «виртуальная» произошло из-за того что процессу неизвестно реальное (физическое) расположение памяти – она может находиться как в оперативной памяти (ОЗУ), так и на диске. Операционная система предоставляет процессу виртуальное адресное пространство (ВАП, virtual address space) определенного размера и процесс может работать с ячейками памяти по любым виртуальным адресам этого пространства, не «задумываясь» о том, где реально хранятся данные.
Размер виртуальной памяти теоретически ограничивается разрядностью операционной системы. На практике в конкретной реализации операционной системы устанавливаются ограничения ниже теоретического предела. Например, для 32-разрядных систем (x86), которые используют для адресации 32 разрядные регистры и переменные, теоретический максимум составляет 4 ГБ (232 байт = 4 294 967 296 байт = 4 ГБ). Однако для процессов доступна только половина этой памяти – 2 ГБ, другая половина отдается системным компонентам. В 64 разрядных системах (x64) теоретический предел равен 16 экзабайт (264 байт = 16 777 216 ТБ = 16 ЭБ). При этом процессам выделяется 8 ТБ, ещё столько же отдается системе, остальное адресное пространство в нынешних версиях Windows не используется.
Введение виртуальной памяти, во-первых, позволяет прикладным программистам не заниматься сложными вопросами реального размещения данных в памяти, во-вторых, дает возможность операционной системе запускать несколько процессов одновременно, поскольку вместо дорогого ограниченного ресурса – оперативной памяти, используется дешевая и большая по емкости внешняя память.
Реализация виртуальной памяти в Windows
Схема реализации виртуальной памяти в 32-разрядной операционной системе Windows представлена на рис.11.1. Как уже отмечалось, процессу предоставляется виртуальное адресное пространство размером 4 ГБ, из которых 2 ГБ, расположенных по младшим адресам (0000 0000 – 7FFF FFFF), процесс может использовать по своему усмотрению (пользовательское ВАП), а оставшиеся два гигабайта (8000 0000 – FFFF FFFF) выделяются под системные структуры данных и компоненты (системное ВАП)1Специальный ключ /3GB в файле boot.ini увеличивает пользовательское ВАП до 3 ГБ, соответственно, уменьшая системное ВАП до 1 ГБ. Начиная с Windows Vista вместо файла boot.ini используется утилита BCDEDIT. Чтобы увеличить пользовательское ВАП, нужно выполнить следующую команду: bcdedit /Set IncreaseUserVa 3072. При этом, чтобы приложение могло использовать увеличенное ВАП, оно должно компилироваться с ключом /LARGEADDRESSAWARE.. Отметим, что каждый процесс имеет свое собственное пользовательское ВАП, а системное ВАП для всех процессов одно и то же.
Рис.
11.1.
Реализация виртуальной памяти в 32-разрядных Windows
Виртуальная память делится на блоки одинакового размера – виртуальные страницы. В Windows страницы бывают большие (x86 – 4 МБ, x64 – 2 МБ) и малые (4 КБ). Физическая память (ОЗУ) также делится на страницы точно такого же размера, как и виртуальная память. Общее количество малых виртуальных страниц процесса в 32 разрядных системах равно 1 048 576 (4 ГБ / 4 КБ = 1 048 576).
Обычно процессы задействуют не весь объем виртуальной памяти, а только небольшую его часть. Соответственно, не имеет смысла (и, часто, возможности) выделять страницу в физической памяти для каждой виртуальной страницы всех процессов. Вместо этого в ОЗУ (говорят, «резидентно») находится ограниченное количество страниц, которые непосредственно необходимы процессу. Такое подмножество виртуальных страниц процесса, расположенных в физической памяти, называется рабочим набором процесса (working set).
Те виртуальные страницы, которые пока не требуются процессу, операционная система может выгрузить на диск, в специальный файл, называемый файлом подкачки (page file).
Каким образом процесс узнает, где в данный момент находится требуемая страница? Для этого служат специальные структуры данных – таблицы страниц (page table).
Структура виртуального адресного пространства
Рассмотрим, из каких элементов состоит виртуальное адресное пространство процесса в 32 разрядных Windows (рис.11.2).
В пользовательском ВАП располагаются исполняемый образ процесса, динамически подключаемые библиотеки (DLL, dynamic-link library), куча процесса и стеки потоков.
При запуске программы создается процесс (см. лекцию 6 «Процессы и потоки»), при этом в память загружаются код и данные программы (исполняемый образ, executable image), а также необходимые программе динамически подключаемые библиотеки (DLL). Формируется куча (heap) – область, в которой процесс может выделять память динамическим структурам данных (т. е. структурам, размер которых заранее неизвестен, а определяется в ходе выполнения программы). По умолчанию размер кучи составляет 1 МБ, но при компиляции приложения или в ходе выполнения процесса может быть изменен. Кроме того, каждому потоку предоставляется стек (stack) для хранения локальных переменных и параметров функций, также по умолчанию размером 1 МБ.
Рис.
11.2.
Структура виртуального адресного пространства
В системном ВАП расположены:
- образы ядра (ntoskrnl.exe), исполнительной системы, HAL (hal.dll), драйверов устройств, требуемых при загрузке системы;
- таблицы страниц процесса;
- системный кэш;
- пул подкачиваемой памяти (paged pool) – системная куча подкачиваемой памяти;
- пул подкачиваемой памяти (nonpaged pool) – системная куча неподкачиваемой памяти;
- другие элементы (см. [5]).
Переменные, в которых хранятся границы разделов в системном ВАП, приведены в [5, стр. 442]. Вычисляются эти переменные в функции MmInitSystem (файл base\ntos\mm\mminit.c, строка 373), отвечающей за инициализацию подсистемы памяти. В файле base\ntos\mm\i386\mi386.h приведена структура ВАП и определены константы, связанные с управлением памятью (например, стартовый адрес системного кэша MM_SYSTEM_CACHE_START, строка 199).
Выделение памяти процессам
Существует несколько способов выделения виртуальной памяти процессам при помощи Windows API2См. обзор в MSDN «Comparing Memory Allocation Methods» (http://msdn.microsoft.com/en-us/library/windows/desktop/aa366533(v=vs.85).aspx).. Рассмотрим два основных способа – с помощью функции VirtualAlloc и с использованием кучи.
1. WinAPI функция VirtualAlloc позволяет резервировать и передавать виртуальную память процессу. При резервировании запрошенный диапазон виртуального адресного пространства закрепляется за процессом (при условии наличия достаточного количества свободных страниц в пользовательском ВАП), соответствующие виртуальные страницы становятся зарезервированными (reserved), но доступа к этой памяти у процесса нет – при попытке чтения или записи возникнет исключение. Чтобы получить доступ, процесс должен передать память зарезервированным страницам, которые в этом случае становятся переданными (commit).
Отметим, что резервируются участки виртуальной памяти по адресам, кратным значению константы гранулярности выделения памяти MM_ALLOCATION_GRANULARITY (файл base\ntos\inc\mm.h, строка 54). Это значение равно 64 КБ. Кроме того, размер резервируемой области должен быть кратен размеру страницы (4 КБ).
WinAPI функция VirtualAlloc для выделения памяти использует функцию ядра NtAllocateVirtualMemory (файл base\ntos\mm\allocvm.c, строка 173).
2. Для более гибкого распределения памяти существует куча процесса, которая управляется диспетчером кучи (heap manager). Кучу используют WinAPI функция HeapAlloc, а также оператор языка C malloc и оператор C++ new. Диспетчер кучи предоставляет возможность процессу выделять память с гранулярностью 8 байтов (в 32-разрядных системах), а для обслуживания этих запросов использует те же функции ядра, что и VirtualAlloc.
Дескрипторы виртуальных адресов
Для хранения информации о зарезервированных страницах памяти используются дескрипторы виртуальных адресов (Virtual Address Descriptors, VAD). Каждый дескриптор содержит данные об одной зарезервированной области памяти и описывается структурой MMVAD (файл base\ntos\mm\mi.h, строка 3976).
Границы области определяются двумя полями – StartingVpn (начальный VPN) и EndingVpn (конечный VPN). VPN (Virtual Page Number) – это номер виртуальной страницы; страницы просто нумеруются, начиная с нулевой. Если размер страницы 4 КБ (212 байт), то VPN получается из виртуального адреса начала страницы отбрасыванием младших 12 бит (или 3 шестнадцатеричных цифр). Например, если виртуальная страница начинается с адреса 0x340000, то VPN такой страницы равен 0x340.
Дескрипторы виртуальных адресов для каждого процесса организованы в сбалансированное двоичное АВЛ дерево3АВЛ дерево – структура данных для организации эффективного поиска; двоичное дерево, сбалансированное по высоте. Названо в честь разработчиков – советских ученых Г. М. Адельсон Вельского и Е. М. Ландиса. (AVL tree). Для этого в структуре MMVAD имеются поля указатели на левого и правого потомков: LeftChild и RightChild.
Для хранения информации о состоянии области памяти, за которую отвечает дескриптор, в структуре MMVAD содержится поле флагов VadFlags.
Для рядовых пользователей работа с ПК под управлением Windows — это как полёт в самолёте. С одной стороны дико тошнит от багов и глюков, а с другой – выйти всё равно некуда. Zip File, мамкины хаЦкеры. С вами Денчик и нынче мы наконец-то обсудим верхние уровни устройства операционной системы Windows. Рассмотрим детально процесс загрузки, архитектурные особенности и нюансы. Ну и конечно же разберём потенциальные уязвимости, которые могут встречаться в операционных процессах данной системы. Если вам интересна данная тема и вы давненько хотите узнать, что же скрывается в неё под капотом. Тогда устраивайтесь по удобней, наливайте свежую порцию чего-нибудь по забористей и приготовьтесь к путешествию в полную Виндузятню. Погнали.
Но перед тем, как мы начнём обсуждение основной темы, я бы хотел рассказать вам о партнёрах данного выпуска, хостинг-провайдере FirstVDS. FirstVDS — это крупный хостинг-провайдер, который на рынке уже 20 лет. 6 декабря ребята начали отмечать юбилей, и в честь этого праздника запустили крутейшую акцию. Что же будет 6 декабря? Будут скидки, занимательная статистика для клиентов, розыгрыш техники Apple и игра FirstRunner. Игра FirstRunner была создана разработчиками специально к 20-летию FirstVDS. Участникам предлагается помочь Ферст Джону пробежать от медленного 2002 до сверхбыстрого 2022. Играйте, ищите пасхалки, входите в ТОП и получайте дополнительный подарки. Каждому клиенту, который поиграет в игру, выпадает возможность выиграть макбук, айфон, плейстейшн или сертификаты на баланс. FirstVDS будет ждать всех на странице акции с 6 по 13 декабря! Присоединяйтесь по ссылке в описании к видео.
Стандартное устройство машины
Ну а мы возвращаемся к основной теме нашего выпуска. Как вы помните, эталонно любая машина состоит у нас из процессора, исполняющего команды программ, быстрой памяти (ОЗУ), дискового пространства для долговременного хранения и подключения к сетке.
Касательно этих терминов вроде бы всё просто и очевидно, однако и по сей день многие ITшники называют «программами» то, что на поверку является приложением. Не путайте пожалуйста. Это совершенно разные вещи.
Окей. В целом картина выглядит следующим образом. На прикладном уровне находятся вышеупомянутые приложения. Они взаимодействуют непосредственно с операционной системой.
В данном случае под операционной системой я подразумеваю совокупность ядра (Kernel) и драйверов устройств. Последние соответственно относятся к самому нижнему, так называемому, железному уровню.
Сегодня мы будем акцентировать внимание на среднем, операционном уровне, который позволяет железу работать с протоколами, методами, периферийной историей и прочими интересными штуками.
Для того, чтобы не писать драйвера для каждого мало-мальски значимого устройства посредством ассемблера, умные дядьки придумали операционные системы.
Ключевые версии Windows
Если речь заходит о Windows, то тут можно выстроить поистине гигантский таймлайн из версий. Я специально включил в подборку не все, а только наиболее значимые версии мелкомягкой ОСи.
Из тех, с которыми вы ещё можете столкнуться тут Windows XP. Я буквально пару лет назад работал в крупной конторе, где 90% парка состояло из ХРюш и никого это особо не парило. Как говорится, лучшее, враг хорошего.
Windows Server 2003 был весьма прорывным и дико сложным для освоения на то время. Именно с него начинается эпоха сисадминства в России. Восьмой сервер в свою очередь был чутка дружелюбнее.
Однако почему-то дико тяжёлым и ел столько оперативы, что запустить его на одной физической тачке с Касперским было практически нереально.
А учитывая то, что SSDшников ещё не было от слова совсем, удовольствия админы получили изрядную порцию.
Седьмая Винда имела кучу проблем с совместимостью. Хотя со временем с помощью обнов и сервис-паков это исправили. Точно также Мелкомягкие допилили и Восьмой сервер выпустив R2 версию, которая, как по мне и по сей день является практически идеальным решением для мелких и средних контор.
Ну про остальные ОСи говорить в целом особо нечего, ибо вы и сами можете попробовать их в деле у себя дома или на рабочих местах.
По 16 серваку в связке с 10 виндой в роли клиента у меня кстати есть целый авторский видеокурс. Можете чекнуть как-нибудь на досуге, если любите иногда развиваться, а не только писю гонять.
Также для развития очень полезно ежедневно учить команды для оперативного взаимодействия с командной строкой системы.
Как показывает практика, если вы шарите, то набрать команду можно в разы быстрее, нежели тыкать мышью в иконки. Рекомендую.
Application Programming Interface (API)
Интерфейс программного взаимодействия или API позволяет одной программе взаимодействовать с другой. Например, приложению с Windows.
API также имеют разные версии. Для 32 разрядных ОС они одни, для 64 разрядных другие.
Если в теме, напишите в комментах по каким причина 32 разрядные операционки до сих пор существуют и почему в самом ближайшем будущем их исчезновение в принципе невозможно.
Даю подсказку. Это как-то связано с особенностями программ. Как вы помните, программа – это набор инструкций для выполнения. Тут всё логично. Однако давайте помимо программы введём ещё такое понятие, как процесс.
Процесс – это совокупность из загруженного и исполняемого набора инструкций и контейнера для ресурсов. Ни больше ни меньше.
Любой процесс обладает рядом особенностей. Наиболее важным для вас из этого списка является PID. Он же Process ID. Он же идентификатор процесса.
Давайте сразу рассмотрим пример. Как видно в таскменеджере, запущенная программа, в данном случае блокнот, может в момент работы создавать несколько разных процессов.
Один процесс может запускать целое дерево из созависимых процессов. И каждый процесс в этом дереве будет иметь равные права. Это же работает в обратную сторону.
Т.е. если вы хлопните какой-нибудь процесс Explorer, всё что так или иначе связано с интерфейсом у вас отвалится. Это в целом достаточно удобная штука. Также для расширенной работы с процессами рекомендую юзать Sysinternals.
Это такой набор расширенных системных инструментов Windows от Марка Руссиновича, позволяющий получить больше информации, чем при апеллировании стандартными инструментами.
Внутри процессов у нас существуют потоки исполнения (threads). Т.е. то, что Windows может запускать на ядре процессора на исполнение.
Также внутри работающего процесса есть как минимум один поток. Windows выделяет каждому потоку квант времени для выполнения на процессоре и быстро переключает исполняющиеся потоки.
Именно это и создаёт так называемую иллюзию «параллельности» работы приложений. Ключевая идея тут заключается в разделении задач на разные потоки, чтобы не было «подвисаний».
Например, один поток рисует графический интерфейс, а другой — выполняет сложную работу. Всё, как в жизни. От каждого по возможностям на благо общего дела.
Архитектура
Windows и приложения – это, как мы знаем исполняемый код, поэтому существует задача ограничения возможностей приложений. В современных процессорах (речь про x64) по дефолту определены 4 уровня привилегий.
Про UserMode мы с вами уже поговорили в общих чертах. Kernel же, являясь по сути ядром, даёт доступ к процессору и всей оперативной памяти.
Т.е. когда пользовательскому процессу необходимо выполнить операцию, требующую повышенных привилегий, например, блокнот хочет сохранить файл на диск.
Наш процесс самостоятельно вызывает соответствующий сервис в ядре. Там выполняется специальная команда, переводящая вызывающий поток в kernel mode, а после завершения возвращающая его обратно в user.
Именно поэтому все путные вирусы хотят заломиться именно в Kernel. Ибо доступ к железу возможен только на уровне ядра, а значит для какой-то реальной пакости требуются повышенные привилегии.
Память
Фундаментально вся память представляется, как непрерывная адресуемая последовательность байт, где операционная система занимает верхние адреса, к которым у пользовательских процессов доступа нет.
Поскольку процессов много, Windows распределяет между ними участки памяти так, что для процесса они как бы непрерывные, однако на самом деле это не так.
Т.е. в моменте процессор не видит этих пробелов. Для него есть только синенькие полосочки или только зелёненькие. Такие вот специфические особенности области видимости.
Если есть нужда посмотреть более детальную информацию о карте памяти процесса, то можно воспользоваться ещё одной утилитой от Руссиновича под названием VMMap.
Для примера я, как обычно, запустил стандартный блокнот. С помощью данной программы наглядно видно, что помимо самого файла notepad.exe (он будет в самом низу списка), загружается много dll файлов.
Библиотеки DLL
DLL (они же Dynamic-link library) – это специальный формат файлов, позволяющий хранить исполняемый код (т.е. инструкции), которые могут использоваться различного рода процессами.
Процессы подгружают библиотеки и используют описанные в ней функции. Поэтому если мы в VMMap’е прочекаем разные приложения, то увидим, что стандартные библиотеки используются одни и те же.
В основном это будут Кернелы. Именно эти библиотеки служат своеобразным слоем, который транслирует документированные вызовы функций в вызовы к сервисам Windows.
Глобально разделение на Kernel Mode и User Mode со стороны выглядит следующим образом. В самом низу мы видим вариант Мелкомягкого гипервизора. Эта деталь не является обязательной.
Если точнее, то она актуальна для ситуаций, когда на одном железе крутится несколько операционных систем. Само ядро, согласно схеме, находится над ХАЛом (набором общих инструкций).
При этом ядро загружается при старте машины и берёт управление на себя. А гипервизор, хоть и исполняется в 0-ом кольце, но при этом изолирует себя от ядра и может как бы «наблюдать» за всей ситуацией со стороны.
Или как говорят умные дядьки, осуществляет мониторинг. В самом User Mode выделяется 4 типа процессов: пользовательские — процессы, получаемые из обычных (т.е. устанавливаемых пользователем или предустановленных) приложений;
Сервисы (они же службы) — чаще всего процессы, которые выполняются в «фоновом режиме», например, службы печати, службы индексирования.
Environment Subsystems — поддержка различных окружений (ранее поддерживалось POSIX, сейчас только Windows). Поэтому обратной совместимости нет.
Ну и само-собой различного рода системные процыки. Уже упомянутый POSIX (Portable Operating System Interface) — это набор стандартов, предназначенный для организации совместимости между ОС.
Начиная с Windows 10, в состав операционки вошла подсистема WSL (Windows Subsystem for Linux). И как понятно из названия она предоставляет возможность запуска Linux-приложений из командной строки.
Вернее, так работала первая версия. WSL 2 уже представляет собой отдельную виртуалку на гипервизоре и даёт гораздо больше возможностей для сисадминов и безопасников в плане доступа к кишкам ОСи.
Ключевые файлы и драйвера
Теперь что касается ключевых файлов в системе. На слайде представлены основные каталоги, которые необходимо запомнить. Современные версии ОС Windows не позволяют как-либо работать с ними.
Однако во времена Windows 2000 вы могли удалить с компьютера папку system32 и тем самым провести небольшой саботаж. Папка окажется в корзине, восстановить вы её не сможете, винда зависнет, но при этом не выключится.
Короче, ад и Израиль. Благо, что мелкомягкие пофиксили этот забавный баг. Но сам факт существования такой вот хурмы заставляет задуматься о тщетности бытия.
Ладненько, помимо файлов есть ещё драйвера, которые представляют собою программный код, обеспечивающий поддержку той или иной функциональности устройств, протоколов и файл-систем.
Системные драйвера располагаются в каталоге System32\Drivers, а пользовательские — в произвольных каталогах, выбираемых в момент инсталляции.
Загрузка
Процесс загрузки компьютера начинается не с работы операционной системы Windows, а с работы встроенного ПО — BIOS.
BIOS зашит в материнскую плату и отвечает за базовую инициализацию оборудования и процедуру самотестирования (она же POST).
BIOS анализирует диски в установленном порядке для поиска MBR (Master Boot Record) — специальным образом оформленной области на диске.
Сам MBR загружает Boot Manager, который уже непосредственно и запускает ОС.
Для Windows это каталог %SystemDrive%\bootmgr (к слову в файловой системе он не отображается).
Ну а дальше Boot Manager загружает так называемый Windows Loader (файлик winload.exe), который уже будит наш Kernel, т.е. загружает ядро Винды.
Вот такой вот хитро**ный процесс. А вы это даже не цените. Давайте резюмирую. Сначала BIOS, затем Boot Manager, далее Loader и только затем ядро, а после уж и рабочий стол с пышногрудой девицей.
Есть ещё вариант с UEFI. Это такой интерфейс, пришедший на смену BIOS, который позволяет писать приложения, подписывать их и проверять конечную подпись.
Собственные UEFI есть у Samsung, ASUS и других популярных вендеров. Схематически работа этой истории выглядит следующим образом.
Да, возможно чутка сложнее, чем в случае с классическим BIOS, зато в разы дружелюбнее для конечного пользователя.
Помимо прочего есть ещё утилиты позволяющие модифицировать BOOT-систему. Например, bcdedit. Либо msconfig, если предпочитаете графический интерфейс. Если захотите поковырять, рекомендую делать это на виртуалке.
И последнее о чём мне бы хотелось сегодня поговорить – это процесс smss.exe. Данный процесс запускает ядро session manager subsystem.
Он же первый процесс в user mode. Который в свою очередь загружает цепочку процессов, отвечающих за выполнение дальнейшей процедуры инициализации.
Ее мы с вами подробно разберём в следующем видео из цикла информационная безопасность с нуля до джуна.
Так что, если не хочешь пропустить это дело и более детально изучить механизмы безопасности операционной системы Windows – обязательно подпишись на канал кликнув на колокольчик.
Не пойму правда, какого лешего ты не сделал этого раньше, но всё-таки дам шанс и возможность исправить карму по-братски, раз уж ты так напрягся и досмотрел ролик до этой минуты.
Окей, друзья. Нынче мы рассмотрели общую архитектуру ОС Windows и базовый процесс загрузки. Тот, что происходит непосредственно до загрузки ядра.
На следующей лекции мы с вами уже подробно поговорим о процессе загрузки и механизмах безопасности, предоставляемых данной операционной системой.
Не забываем сделать домашнее задание по теме лекции. Ссылочка, как обычно, будет закреплена в описании. Ну и если урок зашёл – не пожидитесь и отблагодарите жирнейшим лайкосиком.
Вам не напряжно пару раз по экранчику тапнуть, а мне дико приятно. Приятно осознавать, что работа над контентом происходит не зря и среди современных ITшников есть спрос на инфу с уклоном в ИБ.
Ладненько. С вами, как обычно, был Денчик. В заключении, по традиции, желаю всем удачи, успеха и самое главное, отличного настроения.
Берегите себя и данные своих пользователей. Не позволяйте криворуким ломать винду. Для этого регулярно делайте бэкапы на сервер с наиболее важных тачек. И будем вам счастье.
Помните, технологии – это весело. Во всяком случае, если речь идёт об IT. Тут без креативности, улыбки и хорошего чувства юмора в принципе никуда. Унынение – главный враг любого развития.
Капец, я, как всегда, под конец видео ударяюсь в никому не нужную диванную философию. Всё короче. До новых встреч, мои кайфные друже. Всем пока.