МЕНЕДЖЕР ПАМЯТИ И ПРОЦЕССЫ ========================== But I fear tomorrow I'll be crying, Yes I fear tomorrow I'll be crying. King Crimson'69 -Epitaph Высокоуровневой информации о менеджере памяти Windows NT достаточно много, поэтому здесь не будут описываться такие вещи как FLAT модель, виртуальная память и др. Здесь описываются только конкретные низкоуровневые вещи. Предпологается знание архитектуры процессоров >i386. ОГЛАВЛЕНИЕ ========== 00.Структура потока и процесса ядра 01.Таблицы страниц 02.Hyper Space 03.System PTE'S 04.База данных фрэймов (MmPfnDatabase) 05.Working Set 06.Вытеснение страниц в pagefile'ы 07.Обработка страничных ошибок 08.Создание процесса с точки зрения менеджера памяти 09.Переключение контекстов 0a.Некоторые недокументированные функции менеджера памяти. 0b.Заключение ПРИЛОЖЕНИЕ 0c.Некоторые недокументированные системные вызовы 0d.Замечания и черновые наброски 00.Структура потока и процесса ядра =================================== Каждый процесс в ОС Windows NT представлен структурой EPROCESS. Кроме атрибутов процесса эта структура ссылается на несколько других структур связанных с выполняющимся процессом. Например, с каждым процессом связан один или несколько потоков, представленных в системе структурой ETHREAD. Кратко опишу основную информацию, которая присутствует в этой структуре и которая стала известна благодаря исследованию функций ядра. В голове структуры находится структура KPROCESS - с этой структурой работает планировщик NT. В ней находятся указатель на список потоков ядра (KTHREAD) связанных с этим процессом (разделяющих адресное пространство процесса). Базовый приоритет. Общие времена проведенные потоками процесса в режиме ядра и режиме пользователя. Процессорное сродство (маска, определяющая на каких процессорах могут выполняться потоки процесса). Значение кванта. В структуре ETHREAD также присутствует такая информация как: идентификатор процесса, идентификатор родителя, имя образа процесса, ссылка на секцию, представляющий выполняемый файл. Блок квот определяет лимиты на использование вытесняемого и не вытесняемого пулов, страничного файла. Дерево VAD (virtual address descriptors) определяет состояние регионов памяти пользовательского адресного пространства. Информация о рабочем наборе (Working Set) определяет какие именно физические страницы в данный момент принадлежат процессу. А также лимиты и статистику. Маркер ( токен ) доступа (ACCESS TOKEN) описывает атрибуты безопасности связанные с текущим процессом. Таблица дескрипторов описывает дескрипторы открытых процессом объектов. Таблица позволяет не проверять каждый раз права доступа к объекту. В структуре EPROCESS также содержится указатель на PEB. Структура, представляющая в системе поток, содержит в себе времена создания и выхода из потока, ID процесса и указатель на EPROCESS, стартовый адрес, список ожидающих запросов ввода/вывода (IRP) а также структуру KTHREAD с которой работает планировщик. В KTHREAD содержится следующая информация: время выполнения потока в режиме ядра и пользователя, указатели на базу и вершину стека ядра. Указатель на таблицу сервисов. Базовый и текущий приоритет, состояние и другую информацию для планировщика. Список на APC и указатель на TEB. Это основная информация, KTHREAD содержит в себе множество других данных, посмотреть которые можно рассматривая структуру KTHREAD. 01.Таблицы страниц ================== Обычно, ОС, как и аппаратура, оперирует таким понятием, как таблица страниц. В ОС Windows NT каждый процесс имеет свои личные таблицы страниц (все потоки процесса разделяют эти таблицы). Соответственно, при переключении процессов происходит переключение таблиц. Часто, для ускорения доступа к таблицам аппаратура поддерживает translation lookaside buffer (TLB). В Windows NT реализована 2-х уровневый механизм страниц. Трансляция виртуального адреса в физический для процессоров i386+ (без учета сегментной организации) выглядит так: Virtual Address +-------------------+-------------------+-----------------------+ |3 3 2 2 2 2 2 2 2 2|2 2 1 1 1 1 1 1 1 1|1 1 | |1 0 9 8 7 6 5 4 3 2|1 0 9 8 7 6 5 4 3 2|1 0 9 8 7 6 5 4 3 2 1 0| +-------------------+-------------------+-----------------------+ | Directory index | Page Table index | Offset in page | +-+-----------------+----+--------------+-----+-----------------+ | | | | | | | Page Directory (4Kb)| Page Table (4Kb) | Frame(4Kb) | +-------------+ | +-------------+ | +-------------+ | | 0 | | | 0 | | | | | +-------------+ | +-------------+ | | | | | 1 | | | 1 | | | | | +-------------+ | +-------------+ | | | | | | +->| PTE +-+ | | | | +-------------+ +-------------+ | | | ----------- | +->| PDE +-+ | | | +->| byte | +-------------+ | +-------------+ | | ----------- | | | | | | | | | +-------------+ | +-------------+ | | | | | | | | | | | ... | ... | | | | 1023 | | | 1023 | | | | CR3->+-------------+ +----->+-------------+ +--->+-------------+ ОС Windows NT 4.0 все еще 32р ОС, в плане адресации. Адресное пространство NT - это 4Гб. Из них, обычно 2 младшие Гб (адреса 0-0x7fffffff) принадлежат текущему пользовательскому процессу, а старшие 2 Гб (0x80000000-0xffffffff) принадлежат ядру. При переключении контекстов, перегружается регистр CR3, в результате чего трансляция адресов пользователя меняется, этим достигается изолированность процессов. Замечание: в ОС Windows NT начиная с 4-й версии помимо 4-Kb также используются 4-Mb страницы (для Pentium и выше), при отображении кода ядра. Однако, реальной поддержки переменного размера страниц в Windows NT нет . Форматы PTE и PDE практически идентичны. PTE +---------------+---------------+---------------+---------------+ |3 3 2 2 2 2 2 2|2 2 2 2 1 1 1 1|1 1 1 1 1 1 | | |1 0 9 8 7 6 5 4|3 2 1 0 9 8 7 6|5 4 3 2 1 0 9 8|7 6 5 4 3 2 1 0| +---------------------------------------+-----------------------+ | |T P C U R D A P P U R P| | Base address 20 bits |R P W C W S W | | |N T D T | +---------------------------------------+-----------------------+ Важнейшие биты, определяемые архитектурой i386+ следующие: --------------------------------------------------------------------------- P - бит присутствия. Если он сброшен, то при трансляции адреса произойдет исключение. Вообще, PTE со сброшенным битом используется ядром NT в различных случаях. Например, если странца вытеснена в pagefile, оставшиеся биты описывают ее положение в файле и номер pagefile. U/S - доступна ли страница из user режима. Именно с помошью этого бита защищено пространство ядра (обычно, старшие 2Гб) RW - доступность для записи Выделеннные свободные биты для разработчиков ОС, используемые в NT --------------------------------------------------------------------------- PPT - proto pte TRN - transition pte Биты с 5 по 9 используются в PTE со сброшенным P битом. (При обработке страничных фоултов.) Они носят название Protection Mask и выглядят следующим образом. -------------------------------------------------------------------------------------- * MiCreatePagingFileMap 9 8 7 6 5 --------- | | | | | | | | | +- Write Copy | | | +--- Execute | | +----- Write | +------- NO CACHE +--------- Guard Комбинация GUARD | NOCACHE называется NO ACCESS * MmGetPhysicalAddress Функция очень короткая, но позволила получить много информации. По виртуальным адресам 0xc0000000 - 0xc03fffff отображаются таблицы страниц процесса. Причем, механизм отображения очень изящный. В таблице директорий (Directory Table, далее DT) 1100000000b-й вход (соответствует адресам 0xc000..-0xc03ff..) указывает на саму DT. Т.е. DT играет роль таблицы страниц (Page Table) для этих адресов! Что это дает. Если мы обратимся, скажем, по адресу (для удобства в двоичной системе) 1100000000.0000000101.0000001001.00b ---------- ---------- -------------- 0xc0... Выбор Смещение в таблице страниц Каталог таблицы страниц страниц То получим PTE с номером 1001b в таблице страниц 101b. Но это еще не все - сама DT отображается по адресам 0xc0300000-0xc0300ffc. В MmSystemPteBase лежит как раз 0xc0300000 Почему так - ясно если посмотреть на пример: 1100000000.1100000000.0000001001.00b ---------- ---------- -------------- 0xc0... 0xc0... Смещение в каталоге страниц Каталог Выбор таблицы страниц страниц- каталог страниц Ну и наконец, по c0300c00 содержится PDE для самого каталога Значение базового адреса этого PDE хранится в MmSystemPageDirectory.Также система резервирует один PTE для отображения на физическую страницу MmSystemPageDirectory. Это MmSystemPagePtes. Такая организация позволяет легко манипулировать адресам. Например, если имеется адрес PTE, то адрес страницы, которую он описывает равна PTE<<10. И наоборот: PTE=(Addr>>10)+0xc0000000; Кроме того, в ядре присутствует глобальная переменная MmKseg2Frame = 0x20000. Она показывает, какой диапазон адресов непосредственно отображается на физическую память, начиная от 0x80000000... Т. е. в данном случае виртуальные адреса 0x80000000 - 0x9fffffff отображаются на физические 00000000-1f000000 Получено еще несколько интересных выводов. Начиная с c0000000 находятся 0x1000*0x200=0x200000=2M таблиц описывающие адреса (0-7fffffff) (c0000000-c01ffffc). PDE, описывающие эти таблицы лежат по адресам (c0300000-0xc03007fc). По адресам, (c0200000-c027fffc) для i486 должны быть таблицы, описывающие 512MB от 80000000 До a0000000, но для Pentium в области 0xc0300800-0xc03009fc находятся 4MB PDE и описывают физические страницы по 4m от 0 до 1fc00000 с шагом 00400000. Т.е. выбирается механизм 4mb страниц. Виртуальные адреса соответствующие этим PDE - 80000000, 9fffffff. Таким образом получаем такую карту таблиц страниц (следует иметь в виду, что описанная ниже карта не универсальна. Но она имеет такой вид в большинстве случаев): Диапазон c0000000 - c01ffffc таблицы для 00000000-7fffffff диапазон с0200000 - с027ffff "съеденные" адреса 4m страничной адресацией Диапазон c0280000 - c02ffffc содержит таблицы для a0000000 - bfffffff Диапазон c0300000 - c0300ffc сама PD (описывает диапазон от c0000000 - c03fffff Диапазон c0301000 - c03013fc c0400000 - c04fffff HyperSpace (вернее, 1/4 часть hyper space) Диапазон с0301400 - с03fffff содержит таблицы для c050000 - ffffffff Замечание: По 0xc0301000-0xc0301ffc содержится таблица страниц, описывающая гиперпространство (hyper space). Это пространство адресов ядра, которое отображается по разному для каждого процесса (в остальном, пространство ядра постоянно в контексте каждого пользовательского процесса). Это как бы приватная область для процесса. Например, в hyper space располагается working set. Первые 256 PTE таблиц (1/4 от hyper space) зарезервированы для нужд ядра и используются, когда надо быстро отобразить какой-либо виртуальный адрес на фрейм. Приведу пример отображения одного из адресов области 0xc0200000-0xc027f000. 1100000000.1000000000.000000000000 = 0xc0200000 1) Берется PDE #1100000000 (4k страница) и выбирается сама PageDirectory 2) В Directory выбирается PTE #1000000000 (c0300800) Это 4MB PDE - но в данном случае бит размера игнорируется, так как PDE играет роль PTE. В итоге c0200000 - c0200fff отображается туда же куда и 80000000-80000fff с0201000 отображается в следующие - 80400000- 80400fff. и тд. до c027f000 - 9fc00000 PTE, которые располагаются от c0200000 до c027fffc - описывают область от 80000000 - до 9ffffc00 (512m) 02.Hyper Space ============== HyperSpace это область в пространстве ядра (4mb), которая отображается для каждого из процесса по разному. Для преобразования 4mb достаточно одной полной таблицы страниц. Эта таблица находится по адресам 0xc0301000 - 0xc0301ffc. ( 0 вход PDE по адресу 0xc0300c04) Внутри, для отображения физической страницы в область HyperSpace (в случаях когда нужно быстро сформировать виртуальный адрес для какого-нибудь фрэйма) используется функция: DWORD MiMapPageInHyperSpace(DWORD BaseAddr,OUT PDWORD Irql); Она возвращает виртуальный адрес в HyperSpace, который отображается на желаемую физическую страницу. Как и с чем работает эта функция? В ядре есть такие переменные: MmFirstReservedMappingPte=0xc0301000 MmLastReservedMappingPte=0xc03013fc Это описывает 255 pte's. Которые описывают диапазон: 0xc0400000-0xc04fffff (1/4 HyperSpace) По MmFirstReservedMappingPte стоит pte, базовый адрес в котором играет роль счетчика (от 0 до 255). (Конечно, pte является инвалидным, со сброшенным битом P). В зависимости от текущего значения счетчика добавляется pte для желаемого адреса... Причем счетчик работает по принципу стека - вниз. Начиная от ff. Вообще, это не единственный случай, когда PTE в таблице служат информационным целям. 03.System PTE'S =============== В ядре есть такое понятие - системные pte. Что это и как с этим работает ядро? *См. функцию MiReserveSystemPtes(...) Для свободных PTE система поддерживают несколько структур. Во-первых для быстрого удовлетворения частых запросов (имеется в виду, когда ядру нужен pte для отображения какой-либо физической страницы) поддерживается Sytem Ptes Pool. Причем пул поддерживает следующие БЛОКИ pte. (Блоки - это означает что запросы удовлетворяются БЛОЧНО, по несколько pte) 1, 2 ,4 ,8 и 16 pte. Есть таблицы: BYTE MmSysPteTables[16]={0,0,1,2,2,3,3,3,3,4,4,4,4,4,4,4,4}; DWORD MmSysPteIndex[5]={1,2,4,8,16}; DWORD MmFreeSysPteListBySize[5]; PPTE MmLastSysPteListBySize[5]; DWORD MmSysPteListBySizeCount[5]; DWORD MmSysPteMinimumFree[5]={100,50,30,20,20} PVOID MmSystemPteBase;// 0xc0200000 Блоки свободных PTE в пуле (конечно, они расположены в таблицах страниц, т.е. структура списков находится в таблицах страниц и это правильно) организованы в списки. Элементы списка: typedef struct _FREE_SYSTEM_PTES_BLOCK{ /*pte0*/ SYSPTE_REF NextRef; // на следующий блок /*pte1*/ DWORD FlushUnkn; // как-то используется при Flush /*pte2*/ DWORD ArrayOfNulls[ANY_SIZE_ARRAY]; // свободные PTE }FREE_SYSTEM_PTES_BLOCK PFREE_SYSTEM_PTES_BLOCK; Адрес на PTE из ссылки получается как : VA=(NextRef>>10)+MmSystemPteBase (Младшие 10 бит всегда 0, соответственно с ними равен 0 и P бит) Последний элемент списка в поле NextRef содержит 0xfffff000 (-1) Соответственно, поддерживается 5 списков. (Для блоков из 1,2,4,8 и 16 pte). *См. функции MiReserveSystemPtes2(...) / MiInitializeSystemPtes Кроме пула - существует еще _неорганизованные_ списки свободных системных pte. PPTE MmSystemPtesStart[2]; PPTE MmSystemPtesEnd[2]; SYSPTE_REF MmFirstFreeSystemPte[2]; DWORD MmTotalFreeSystemPtes[2]; Содержится две ссылки на два списка. Каждый элемент списков: typedef struct _FREE_SYSTEM_PTES{ SYSPTE_REF Next; // #define ONLY_ONE_PTE_FLAG 2, last = 0xfffff000 DWORD NumOfFreePtes; }FREE_SYSTEM_PTES PFREE_SYSTEM_PTES; Причем, 1 список - неорганизован в принципе. При освобождении pte's для 0 списка (MiReleaseSystemPtes). По возможности pte попадают в System Ptes Pool. При запросах к MiReserveSystemPtes(...) с желаемым числом pte больше 16, pte выделяются также из 0 списка. Т.е. 0 связан с пулом, а 1 нет. Для того что бы результаты работы не противоречили TLB используется либо перегрузка cr3 (медленно) либо команда invlpg. "Высокоуровневая" функция MiFlushPteList(PTE_LIST* PteList, BOOLEAN bFlushCounter, DWORD PteValue); занимается следующим: Инициализирует PTE's и делает invlpg (ассемблерная инструкция). typedef struct PTE_LIST{ DWORD Counter; // max equ 15 PVOID PtePointersInTable[15]; PVOID PteMappingAddresses[15]; }; Если Counter больше 15 то происходит вызов KeFlushCurrentTb (просто перезагрузка CR3) и увеличивается MmFlushCounter на 0x1000 если bFlushCounter установлен. 04.База данных фрэймов (MmPfnDatabase) ====================================== Ядро хранит информацию о физических страницах в базе данных фреймов (MmPfnDatabase). По существу, это просто массив из структур размером 0x18 байт. Каждая структура соответствует физической странице. (В порядке следования, поэтому часто на элемент массива ссылаются как Pfn- page frame number) Количество структур соответствует количеству 4Кб страниц в системе ( или количеству страниц, которые видны ядру. При желании можно использовать соответствующую опцию в boot.ini для того, чтобы сделать "плохими" часть страниц для ядра NT). В зависимости от контекста по разному интерпретируются поля записей структур. Обычно, структуру можно представить так. typedef struct _PfnDatabaseEntry { union { DWORD NextRef; // 0x0 если фрэйм в списке - то это номер сл. фрэйма // -1 - последний DWORD Misc; // также другая информация, в зависимости от контекста // см. псевдокод (обычно TmpPfn->0...) // обычно здесь может находиться *KTHREAD, *KPROCESS, // *PAGESUPPORT_BLOCK... }; PPTE PtePpte; // 0x4 указатель на pte или ppte union { // 0x8 DWORD PrevRef; // предыдущий фрэйм или (-1, первый) DWORD ShareCounter; // Share счетчик }; WORD Flags; // 0xc см. ниже WORD RefCounter; // 0xe счетчик ссылок DWORD Trans; // 0x10 ?? см ниже. Понял использование для pagefile DWORD ContFrame;//ContainingFrame; // 14 }PfnDatabaseEntry; /* Flags (имена взяты из результатов вывода команды KD расширения windbg !pfn) Маска Биты Имя Значение ----- ---- --- -------- 0001 0 M Modifyied 0002 1 R Read In Progress 0004 2 W WriteInProgress 0008 3 P Shared 0070 [4:6] Color Color (In fact Always null for x86) 0080 7 X Parity Error 0700 [8:10] State 0- Zeroed /List 1- Free 2- StandBy 3- Modified 4- ModifiedNoWrite 5- BadPage 6- Active 7- Trans 0800 11 E InPageError Значение поля Trans для фрейма содержимое которого находится в PageFile или другом отображаемом файле это Page File PTE. Приведу форматы PTE со сброшенным битом P. (такие PTE определяются не архитектурой платформы а ОС) * из @MiReleasePageFileSpace (Trans) Page File PTE +---------------------------------------+-+-+---------+-------+-+ |3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1|1|1|0 0 0 0 0|0 0 0 0|0| |1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2|1|0|9 8 7 6 5|4 3 2 1|0| +---------------------------------------+-+-+---------+-------+-+ | offset |T|P|Protect. |page |0| | |R|P|mask |file | | | |N|T| |Num | | +---------------------------------------+-+-+---------+-------+-+ Transition PTE +---------------------------------------+-+-+---------+-------+-+ |3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1|1|1|0 0 0 0 0|0 0 0 0|0| |1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2|1|0|9 8 7 6 5|4 3 2 1|0| +---------------------------------------+-+-+---------+-------+-+ | PFN |T|P|Protect. |C W O W|0| | |R|P|mask |D T | | | |N|T| | | | +---------------------------------------+-+-+---------+-------+-+ W - write O - owner WT - write throuth CD - cache disable Возможно, пока не ясно что это все значит, но это будет понятно ниже. Конечно, структура не документирована. Видно, что структуры (в дальнейшем фреймы) могут организовываться в списки. Для фреймов поддерживается структура: struct _MmPageLocationList{ PPfnListHeader ZeroedPageListhead; //&MmZeroedPageListhead PPfnListHeader FreePageListHead; //&MmFreePageListHead PPfnListHeader StandbyPageListHead; //&MmStandbyPageListHead PPfnListHeader ModifiedPageListHead; //&MmModifiedPageListHead PPfnListHeader ModifiedNoWritePageListHead;//&MmModifiedNoWritePageListHead PPfnListHeader BadPageListHead; //&MmBadPageListHead }MmPageLocationList; Названия и высокоуровневая информация о NT говорят сами за себя. Итак поддерживается 6 списков. Состояние фрейма (соответствующие биты в поле Flags) тесно связано с списком. Ниже перечислены состояния фрейма: +---------------+----------------------------------------------------+------+ |Состояние |Описание |Список| +---------------+----------------------------------------------------+------+ |Zero | страница свободна для использована и очищена | 0 | | | нулями | | |Free | страница свободна для использования | 1 | |Standby | страница не используется но может быть легко | 2 | | | восстановлена | | |Modified | страница грязная и предназначена для вытеснения | 3 | |ModifiedNoWrite| страница грязная но (пока) не предназначена | 4 | | | для вытеснения | | |Bad | страница не используется (была ошибка) | 5 | |Active | страница активна и отображается по крайней мере по | - | | | одному виртуального адресу | | +---------------+----------------------------------------------------+------+ Фрейм может быть в одном из списков, или не быть вообще (состояние Active). Если страница принадлежит какому-либо процессу, то это отслеживается с помощью таблицы Working Set (см. дальше). Также, фрейм может вообще нигде не учитываться, если он используется самим менеджером памяти. Каждый из заголовков списков это структура вида: typedef struct _PfnListHeader{ DWORD Counter; // число фрэймов в списке DWORD LogNum; // номер списка.0 - zeroed, 1- Free etc... DWORD FirstFn; // Первый фрэйм (номер) в MmPfnDatabase DWORD LastFn; // --//--- последний. }PfnListHeader PPfnListHeader; Кроме того, можно адресовать свободные (zeroed или free) фреймы по "цветам" (это хэш). Как - будет ясно если просмотреть псевдокод в приложении. Приведу сопровождающие структуры: struct { ColorHashItem* Zeroed; //(-1) нет ColorHashItem* Free; }MmFreePagesByColor; typedef struct _ColorHashItem{ DWORD FrameNum; PfnDatabaseEntry* Pfn; } ColorHashItem; Имеется набор функций, для работы с фреймами по цветам (работа с хэшем) или без учета цветов. Например, MiRemovePageByColor(FrameNum, Color); Эти функции и их аргументы как и возвращаемые значения легко угадать, смотря на их названия и проглядывая их дизассемблированный код, поэтому они здесь не описываются, тем более, они не экспортируются. При работе с цветами, учитываются маска цветов, и последний выбранный цвет. Windows NT удовлетворяет классу защиты C2, и поэтому должна при выделении страниц пользовательским процессам чистить их. Рассмотрим поток системного процесса, который занимается очисткой фреймов. Последнее, что делается в Phase1Initialization() это вызов функции MmZeroPageThread. Как не трудно догадаться - поток предназначен для очистки свободных страниц и перемещении их в список нулевых страниц. MmZeroPageThread { // //.... неинтересные вещи пропущены ;) // while(1) { KeWaitForSingleObject(MmZeroingPageEvent,8,0,0,0); // ждем события while(!KeTryToAcquireSpinLock(MmPfnLock,&OldIrql)); // захватить PfnDatabase while(MmFreePageListHead.Count){ MiRemoveAnyPage(MmFreePageListHead.FirstFn&MmSecondaryColorMask); // вытащить страницу из списка свободных Va=MiMapPageToZeroInHyperSpace(MmFreePageListHead.FirstFn); KeLowerIrql(OldIrql); memset(Va,0,0x1000); // clear page while(!KeTryToAcquireSpinLock(MmPfnLock,&OldIrql); MiInsertPageInList(&MmZeroedpageListHead,FrameNum); // вставить очищенную страницу в список Zero } MmZeroingPageThreadActive=0; // очистить флаг KeLowerIrql(OldIrql); } // никогда не выходит. } // функция просто отображает фрэйм на определенный адрес // чтобы его можно было очистить DWORD MiMapPageToZeroInHyperSpace(FrameNum) { if(FrameNum=MmMinimumFreePagesToZero&& !MmZeroingPageThreadActive) { MmZeroingPageThreadActive=1; KeSetEvent(&MmZeroingPageEvent,0,0); } .... } Замечание: Ядро не всегда пологается на этот поток, иногда, встречается код, который забирает одну из свободных страниц, а потом сам ее чистит. 05.Working Set ============== Working Set - рабочий набор - это набор физических страниц принадлежащих к.л. процессу в данный момент. С помощью определенных механизмов менеджер памяти отслеживает рабочие наборы процессов. С working set связаны две квоты максимальный размер рабочего набора ( maximum working set ) и минимальный размер рабочего набора ( minimum working set) . Это максимальный и минимальный размеры рабочего набора. Основываясь на этих величинах, менеджер памяти поддерживает рабочие наборы процессов (пытается поддерживать размер рабочего набора не меньше минимума, но не больше максимума). При определенных условиях, рабочий набор урезается, при этом фреймы набора попадают в список свободных фреймов. Для ядра рабочий набор - это совокупность структур. По смещению 0xc8 (для ядра ОС NT4.0) в структуре процесса (ETHREAD) расположена следующая структура. typedef struct _VM{ /* C8*/ LARGE_INTEGER UpdateTime; //0 /* D0*/ DWORD Pages; //8 called so, by S-Ice authors /* D4*/ DWORD PageFaultCount //0c faults; // in fact number of MiLocateAndReserveWsle calls /* D8*/ DWORD PeakWorkingSetSize; //10 all /* DC*/ DWORD WorkingSetSize; //14 in /* E0*/ DWORD MinimumWorkingSet; //18 pages, not in /* E4*/ DWORD MaximumWorkingSet; //1c bytes /* E8*/ PWS_LIST WorkingSetList; //20 data table /* EC*/ LIST_ENTRY WorkingSetExpansion; //24 expansion /* F4*/ BYTE fl0; // Operation??? //2c BYTE fl1; // always 2??? //2d BYTE fl2; // reserved??? always 0 //2e BYTE fl3; // //2f }VM *PVM; VM она называется в команде расширения для WinDbg !procfields. Важно здесь то, что отслеживается число страничных ошибок (PageFaultCount), MaximumWorkingSet и MinimumWorkingSet, на основе чего менеджер и поддерживает рабочий набор. Замечание: На самом деле, PageFaultCount не строго счетчик ошибок. Этот счетчик увеличивается в функции MiLocateAndReserveWsle, которая вызывается не только при страничной ошибке, но и в некоторых других случаях (правда, гораздо реже). Следующие структуры описывают таблицу, содержащую страницы рабочего набора. typedef struct _WS_LIST{ DWORD Quota; //0 ??? i'm not shure.... DWORD FirstFreeWsle; // 4 start of indexed list of free items DWORD FirstDynamic; // 8 Num of working set wsle entries in the start // FirstDynamic DWORD LastWsleIndex; // c above - only empty items DWORD NextSlot; // 10 in fact always == FirstDynamic // NextSlot PWSLE Wsle; // 14 pointer to table with Wsle DWORD Reserved1 // 18 ??? DWORD NumOfWsleItems; // 1c Num of items in Wsle table // (last initialized) DWORD NumOfWsleInserted; // 20 of Wsle items inserted (WsleInsert/ // WsleRemove) PWSHASH_ITEM HashPtr; // 24 pinter to hash, now we can get index of // Wsle item by address. Present only if // NumOfWsleItems>0x180 DWORD HashSize; // 28 hash size DWORD Reserved2; // 2c ??? }WS_LIST *PWS_LIST; typedef struct _WSLE{ // элемент таблицы рабочего набора DWORD PageAddress; }WSLE *PWSLE; // PageAddress представляет собой виртуальный адрес страницы рабочего набора // Младшие 12 бит используются как атрибуты (виртуальный адрес страницы // всегда кратен 4K) #define WSLE_DONOTPUTINHASH 0x400 // не помещать в хэш #define WSLE_PRESENT 0x1 // не пустой элемент #define WSLE_INTERNALUSE 0x2 // фрэйм используется менеджером памяти // Свободный WSLE со сброшенным битом WSLE_PRESENT представляет собой // индекс на следующий свободный WSLE. Таким образом свободные WSLE // организованы в список. Последний свободный WSLE ссылается на -1 #define EMPTY_WSLE (next_emty_wsle_index) (next_emty_wsle_index<<4) #define LAST_EMPTY_WSLE 0xfffffff0 typedef struct _WSHASH_ITEM{ DWORD PageAddress; //Value DWORD WsleIndex; //index in Wsle table }WSHASH_ITEM *PWSHASH_ITEM; //Хэш-функция очень простая. Псевдокод внутренней функции: //MiLookupWsleHashIndex(Value,WorkingSetList) //{ //Val=value&0xfffff000; //TmpPtr=WorkingSetList->HashPtr; //Mod=(Val>>0xa)%(WorkingSetList->HashSize-1); //if(*(TmpPtr+Mod*8)==Val)return Mod; //while(*(TmpPtr+Mod*8)!=Val)){ // Mod++; // if(WorkingSetList->HashSize>Mod)continue; // Mod=0; // if(fl)KeBugCheckEx(0x1a,0x41884,Val,Value,WorkingSetList); // fl=1; // } //return Mod; //} Рассмотрим working set типичного процесса. WorkingSetList находится по адресу MmWorkingSetList (0xc0502000) Это область hyper space, и поэтому при переключении между процессами, меняется преобразование этих виртуальных адресов, таким образом, каждый процесс имеет свои структуры рабочего набора. По адресу MmWsle (0xc0502690) находится начало динамической таблицы Wsle. Окончание таблицы всегда кратно 0x1000. Т.е. таблица может заканчиваться по адресу 0xc0503000, 0xc0504000 и т.д. (Это сделано в целях упрощения манипулирования размером таблицы Wsle). Хэш (если он есть) находится по такому смещению, к которому никогда не вырастет таблица Wsle. Посмотрим на таблицу Wsle подробнее: // WsList-0xc0502000--- // .... // -------0xc0502030---- // pde 00 fault counter // pde 01 fault counter // pde 02 fault counter // // +-Wsle==0xc0502690--- +--Pde/pte +-----Pfn[0]------ // |0 c0300000|403 Page Directory |c0300c00 pde |pProcess // |4 c0301000|403 Hyper Space |c0300c04 pte |1 // |8 MmWorkingSetList(c0502000)|403 |c0301408 pte |2 // |c MmWorkingSetList+0x1000 | 403 |. |3 // |10 MmWorkingSetList+0x2000 | 403 |. . // | .... // |FirstDynamic*4 FrameN // |.... |. . // . // |LastWsleIndex*4 FrameM // +-------- +------ +------- // | free items // .... // | 0xfffffff0 // +------------------- // Хэш // .... Интересно здесь то, что в начале таблицы собрано FirstDynamic страниц, используемых для построения самой таблицы Wsle, WorkingSetList и хэша. Также тут есть фрэйм каталога страниц, HyperSpace и некоторые другие страницы, которые нужны самому менеджеру памяти и они не могут быть удалены из рабочего набора. (флаг WSLE_INTERNALUSE). Потом, тут мы видим еще два варианта использование поля фрэйма Pfn по смещению 0. Для фрэйма каталога страниц это указатель на проуесс, для обычной страницы принадлежащей набору - это индекс в таблице. Между WorkingSetList и началом таблицы Wsle есть небольшое свободное пространство в 0x660 байт. Нет информации как распределяется все это пространство, но сразу же за WorkingSetList начинается массив (WORD) счетчиков страничных ошибок по PDE для пространства пользователя (обычно, младшие 2Гб). Т.е. если, скажем, элемент с индексом 0x100 содержит 3, то это означает, что произошло 3 (если не учитывать возможное переполнение) страничных ошибки для страниц диапазона [0x40000000-0x403fffff]. Квоты для рабочего набора в режиме ядра можно изменить с помощью экспортируемой но не документированной функции: NTOSKRNL MmAdjustWorkingSetSize( DWORD MinimumWorkingSet OPTIONAL, // if both == -1 DWORD MaximumWorkingSet OPTIONAL, // empty working set PVM Vm OPTIONAL); Для работы с WorkingSet менеджер использует множество внутренних функций, их знание позволяет понять принципы его работы. 06.Вытеснение страниц в страничные файлы ======================================== Фрейм может быть свободен - тогда его RefCounter равен 0 и он находится в одном из списков. Фрейм может принадлежать рабочему набору. При нехватке свободных фреймов или при достижении критических (treshhold) значений происходит вытеснение фреймов. Высокоуровневая информация на эту тему имеется. Задача этого пункта - подтвердить ее псевдокодом. В NT поддерживается до 16 файлов откачки. Создание страничного файла происходит при старте модуля SMSS.EXE. При этом открывается файл и его дескриптор копируется в таблицу дескрипторов процесса PsInitialSystemProcess. Приведу прототип недокументированной системной функции создания страничного файла. (Необходимо право на создание таких файлов - если вызов не из ядра.) NTSTATUS NTAPI NtCreatePagingFile( PUNICODE_STRING FileName, PLARGE_INTEGER MinLen, // старшее двойное слово должно быть 0 PLARGE_INTEGER MaxLen, // minlen должно быть больше мегабайта DWORD Reserved // игнорируется ); С каждым из страничных файлов связана структура PAGING_FILE. typedef struct _PAGING_FILE{ DWORD MinPagesNumber; //0 DWORD MaxPagesNumber; //4 DWORD MaxPagesForFlushing; //8 (максимум страниц на вытеснение) DWORD FreePages; //c(Free pages in PageFile) DWORD UsedPages; //10 занятых страниц DWORD MaxUsedPages; //14 DWORD CurFlushingPosition; //18 -??? DWORD Reserved1; //1c PPAGEFILE_MDL Mdl1; // 20 0x61 - empty ??? PPAGEFILE_MDL Mdl2; // 24 0x61 - empty ??? PRTL_BITMAP PagefileMap; // 28 0 - свободно, 1 - содержит вытесненную стр. PFILE_OBJECT FileObject; //2c DWORD NumberOfPageFile; //30 UNICODE_STRING FileName; //34 DWORD Lock; //3d }PAGING_FILE *PPAGING_FILE; DWORD MmNumberOfActiveMdlEntries; DWORD MmNumberOfPagingFiles; #define MAX_NUM_OF_PAGE_FILES 16 PPAGING_FILE MmPagingFile[MAX_NUM_OF_PAGE_FILES]; При старте подсистемы памяти (MmInitSystem(...)) запускается поток MiModifiedPageWriter, который производит следующие действия: Инициализирует MiPaging и MiMappedFileHeader. Создает и инициализирует в неоткачиваемом пуле MmMappedFileMdl Устанавливает приоритет LOW_REALTIME_PRIORITY+1 Ждет у события MmPagingFileCreated, после чего освобождает память от этого KEVENT. Инициирует MmMappedPageWriterEvent и список MmMappedPageWriterList Запускает поток MiMappedPageWriter Запускает процедуру MiModifiedPageWriterWorker. В задачу MiModifiedPageWriterWorker входит ожидание у события MmModifiedPageWriterEvent, работа с списками MmModifiedNoWritePageList и MmModifiedPageList и подготовка к реальному вытеснению страниц в отображаемый файл или страничный файл (происходит вызов одной из функций MiGatherMappedPages или MiGatherPagefilePages. В MiGatherPagefilePages происходит реальное вытеснение фреймов с помощью функции IoAsynchronousPageWrite( ). Причем происходит запись не одного фрейма, а кластера. (совокупности страниц числом MmModifiedWriteClasterSize. ) Вытесненные страницы в страничном файле отслеживаются с помощью битового массива PagefileMap в структуре PAGING_FILE. Псевдокоды рассмотренных функций приводятся в appendix.txt. Бессмысленно полностью описывать логику работы псевдокода - достаточно просто на него взглянуть. 07.Обработка страничных ошибок ============================== Теперь есть вся необходимая информация, чтобы перейти к рассмотрению обработчика страничных ошибок. При обращении по линейному адресу, при трансляции которого (страничный механизм включен) идет обращение к PDE/PTE со сброшенным флагом P (present) или если происходит нарушение защиты, в процессорах +i386 порождается исключение 14. При этом, в стек заносится код ошибки, содержащий в себе следующую информацию: бит ошибки пользователя/супервизора (возникло исключение на кольце 3 или 0?), бит ошибки записи/чтение (попытка чтения или записи?), бит присутствия страницы. Кроме того в регистр CR2 заносится 32-разрядный линейный адрес при преобразовании которого возникло исключение. Обработчик 14 прерывания в ядре это _KiTrap0E. При обращении к странице, которой физически нет, менеджер памяти выполняет определенную работу, для "исправления" ситуации. Этим занимается более высокоуровневая функция MmAccessFault (Wr,Addr,P); вызов которой производится из обработчика. Прежде чем анализировать псевдокод, полезно напомнить в каких случаях (с точки зрения VMM) возникает страничная ошибка. Самые очевидные - это ошибка доступа, когда код кольца 3 пытается писать в страницу со сброшенным битом U в PTE/PDE и запись в страницу, разрешенную только на чтение (бит W в PTE/PDE сброшен.) Затем, страницы могут вытесняться в страничные файлы, и соответствующий этой странице PTE помимо сброшенного бита P имеет информацию в каком страничном файле искать фрейм и по какому смещению. Еще одна похожая ситуация - фрейм принадлежит отображаемому файлу. Кроме того, возможно обращение к странице, которая принадлежит только что выделенному участку памяти (с помощью NtAllocateMemory ) и к которой еще не было обращений, VMM выделяет в этом случае нулевой фрейм (это одно из требований к системам с классом C2 ). И , наконец, исключение может возникнуть при защите copy on write, и при обращении к общей памяти. Выше перечислены только основные ситуации. В результате обработки, обычно, соответствующий фрейм добавляется в Working Set текущего процесса. С каждой из этих ситуаций связаны соответствующие внутренние структуры, с которыми работает VMM. Эти структуры достаточно сложные и для их полного описания требуют дизассемблирования большого числа функций. На данный момент нет полной информации по большинству сопутствующих структур , но для понимания обработчика этого и не требуется. Опишу в общих чертах такие понятия как VAD и PPTE, для того чтобы можно было рассматривать псевдокод обработчика. VAD Для манипуляции виртуальными адресами используются структуры VAD (Virtual Address Descriptor). Хорошо известная (она вызывается из почти одноименной Win32 функции) недокументированная функция NtAllocateVirtualMemory (или ZwAllocateVirtualMemory для кольца 0) манипулирует этими структурами. Каждый VAD описывает диапазон виртуального адресного пространства, и кроме, собственно, стартового и конечного адреса диапазона содержит информацию по защите. (См. параметры функции ZwAllocateVirualMemory.) А также некоторую другую специфическую информацию (на данный момент нет полной информации по структуре VAD, кроме его заголовка). Структуры VAD имеют смысл только для адресов пользователя (обычно младшие 2Гб), с помощью этих структур VMM может отследить в какую именно из областей было обращение при исключении. Структуры VAD образуют сбалансированное (есть внутренняя функция, которая при необходимости перестраивает дерево) двоичное дерево, оптимизированное для поиска. В VAD имеются два указателя на следующие элементы - левая и правая ветвь. Корень дерева находится в структуре EPROCESS, поле VadRoot (смещение 0x170 для ядра ОС NT 4.0). Конечно, для каждого процесса имеется свое дерево VAD. Заголовок VAD выглядит следующим образом. typedef struct vad_header { void *StartingAddress; void *EndingAddress; struct vad *ParentLink; struct vad *LeftLink; struct vad *RightLink; ULONG Flags; }VAD_HEADER, *PVAD; PPTE Prototype Pte это еще один уровень трансляции линейного адреса и используется при разделяемой памяти. Допустим, какой-либо файл проецируется в адресные пространства нескольких (3-х) процессов. Таблица PPTE содержит PPTE, которые описывают физические страницы файла загруженного в память. Какие-то PPTE могут иметь установленный бит P (то же местоположение и значение, что и в PTE/PDE), какие-то нет, и в этом случае есть информация, которая позволит загрузить фрейм из страничного или отображаемого файла. У всех трех процессов файл отображается на разные адреса, PTE соответствующие этим страницам изначально имеют сброшенный бит P и содержат ссылки на PPTE описывающие страницы файла. Таким образом, при трансляции линейного адреса, отображаемого на файл, в процессе #1 происходит исключение 14 , VMM находит PTE, получает ссылку на PPTE и может теперь непосредственно "исправить" соответствующий PTE так, чтобы он указывал на фрейм принадлежащий файлу, при необходимости загрузив фрейм из файла. Приведу формат PTE со сброшенным битом P, в таблице страниц, указывающего на prototype PTE. PTE points to PPTE +-----------------------------------------+-+---+-------------+-+ |3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1|1|0 0|0 0 0 0 0 0 0|0| |1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1|0|9 8|7 6 5 4 3 2 1|0| +-----------------------------------------+-+---+-------------+-+ | Address [7:27] |1|Un | Address |0| | | |use| [0:6] | | | | |d | | | +-----------------------------------------+-+---+-------------+-+ *MmAccessFault Начнем рассмотрение псевдокода MmAccessFault. Ее прототип: NTSTATUS MmAccessFault (BOOL Wr,DWORD Addr, BOOL P) Параметры очевидны: это признак записи, адрес при обращению к которому возникло исключение и бит присутствия страницы, к которой произошло обращение. Этой информации обработчику достаточно, для того, чтобы определить причину исключения. Обработчик выбирает одну из двух веток выполнения, в зависимости от того, принадлежит ли Addr пространству ядра или пространству пользователя. В первом случае обработка тривиальна - производится отслеживание одной из ситуаций: ACCESS VIOLATION или необходимость возврата страницы в Working Set (MiDispatchFault) В случае с пространством пользователя - ситуация несколько сложнее. Сначала выполняется обработка исключения для PDE если PDE не присутствует в памяти. Затем происходит ветвление в алгоритме. Первая ветвь - страница присутствует. Это означает либо ошибку доступа (ACCESS VIOLATION), либо отработку механизма copy on write. Вторая ветвь - отработка требования нулевой страницы, ACCESS VIOLATION, пограничной (GUARD) страницы (расширение стека), необходимость возврата страницы в working set. Интересно то, что в случае частого возникновения страничных ошибок система увеличивает размер working set. В случае с нулевым PTE для определения ситуации обработчик вынужден работать с деревом VAD, чтобы определить атрибуты области, к которой была попытка доступа. Этим занимается служебная функция MiAccessCheck, которая возвращает статус доступа. В общем случае, основную "черновую" работу по обработке исключения выполняет функция MiDispatchFault. Она уточняет ситуацию и определяет дальнейшие действия. MiDispatchFault в свою очередь, в основном опирается на более низкоуровневые рутины. Это MiResolveTransitionFault, MiResolveDemandZeroFault, MiResolveProtoPteFault, MiResolvePageFileFault. Из имен этих функций ясно, что это решение конкретных ситуаций: фрейм в состоянии 'transition' (может быть быстро возвращен в Working Set), нужен чистый фрейм, PTE ссылается на PPTE и фрейм вытеснен в страничный файл, соответственно. В случае ситуации со страничным файлом и в некоторых случаях с PPTE возможно потребуется последующее чтение фрейма из файла, тогда рутинами возвращается значение 0xc0033333, означающее (помимо хорошей фантазии разработчиков ядра) необходимость чтения страницы из файла. Что и делается с помощью вызова IoPageRead в MiDispatchFault. Посмотрим более внимательно на упомянутые рутины. Начнем с MiResolveDemandZeroFault. Если посмотреть на псевдокод этой функции, то легко понять логику ее работы. Требуется нулевой фрейм и процесс его получает. При этом используются функции MiRemoveZeroPage или MiRemoveAnyPage. Первая из них забирает страницу из списка обнуленных страниц. Если это не удается с помощью второй функции выбирается любая страница. При этом страница чистится с помощью MiZeroPhysicalPage. И, наконец в MiAddValidPageToWorkingSet чистая страница добавляется в рабочий набор. (Кстати, это фактическое подтверждение того, что процесс не может при выделении памяти получить доступ к неочищенным страницам.) Теперь рассмотрим более сложную ситуацию - страница находится в страничном файле. Прежде чем рассматривать псевдокод надо описать одну структуру. При подготовке к чтению страницы из файла, заполняется структура PAGE_SUPPORT_BLOCK. Затем, для всех PFN которые будут участвовать в операции проделывается следующее: устанавливается флаг read in progress и в поле Misc записывается адрес PAGE_SUPPORT_BLOCK (функция MiInitializeReadInProgressPfn). Наконец, функция возвращает магическое 0xc0033333, что означает (помимо хорошей фантазии разработчиков ядра) последующее использование структуры при вызове IoPageRead. (Кстати, IoPageRead экспортируется, но не документирована. Можно легко выделить ее прототип из псевдокода.) typedef struct _PAGE_SUPPORT_BLOCK{ // size: 0x98 DISPATCHER_HEADER DispHeader; // 0 FastMutex IO_STATUS_BLOCK IoStatusBlock; // 0x10 LARGE_INTEGER AddrInPageFile; // 0x18 (file offset) DWORD RefCounter; // 0x20 (0|1) ??? KTHREAD Thread; // 0x24 PFILE_OBJECT FileObject; // 0x28 DWORD AddrPte; // 0x2c PPFN pPfn; // 0x30 MDL Mdl; // 0x34 DWORD MdlFrameBuffer[0x10]; // 0x50 LIST_ENTRY PageSupportList; // 0x90 список связан с MmInPageSupportList }PAGE_SUPPORT_BLOCK *PAGE_SUPPORT_BLOCK; struct _MmInPageSupportList{ LIST_ENTRY PageSupportList; DWORD Count; }MmInPageSupportList; Сама функция MiResolvePageFileFault совсем простая, она ничего не делает кроме заполнения соответствующих структур и возврата кода 0xc0033333. Все остальное выполняет MiDispatchFault. Это логично, если вспомнить о принципе повторного использования кода. Еще одна несложная функция MiResolveTransitionFault. Про состояние фрейма transition было сказано выше. Из этого состояния фрейм может быть быстро возвращен в Working Set процесса. Итак, осталась последняя ситуация - PROTO PTE. Как оказывается, обработчик этой ситуации тоже не слишком сложный и опирается на вещи, которые уже были описаны. На самом деле с обработкой этой ситуации связана еще одна функция MiCompleteProtoPteFault, она вызывается позже - из MiDispatchFault. Чтобы понять логику работы этих функций достаточно посмотреть псевдокод. 07.Объект секция ================ Объект секция в ОС NT представляет блок памяти который разделяют один или несколько процессов. В Win32 подсистеме секция выглядит как отображаемый файл (file mapping object). Посмотрим, что представляет собой объект секция. Секция является очень часто используемым объектом в ОС NT, исполнительная система использует секции для загрузки исполняемого образа в память и в менеджере кэша. Секция также используется при отображении файла в адресное пространство процесса. При этом доступ к файлу выглядит как доступ к массиву. Объект секция, как и любой другой объект создается менеджером объектов. Имеющаяся высокоуровневая информация сообщает нам, что тело объекта содержит в себе информацию следующего рода: максимальный размер секции, атрибуты защиты, другие атрибуты. Что такое максимально допустимый размер секции - понятно. Атрибуты защиты - это атрибуты для страниц секции. Другие атрибуты секции это признак представляет ли секция файл или создана пустой (отображается в страничный файл), и признак того, является ли секция базовой. Базовые секции отображаются в адресных пространствах всех процессов по одинаковому виртуальному адресу. Для того чтобы получить реальную информацию по структуре этого объекта были продизассемблированы некоторые функции менеджера памяти для работы с секцией. Описываемую ниже информацию я не встречал нигде. Итак - начнем рассматривать структуры. Каждый файл в системе представлен объектом (который описан в NTDDK.H) FILE_OBJECT. В этой структуре имеется блок указателей SectionObjectPointer. Его структура также представлена в NTDDK.H. // : PSECTION_OBJECT_POINTERS SectionObjectPointer; : // typedef struct _SECTION_OBJECT_POINTERS { PVOID DataSectionObject; PVOID SharedCacheMap; PVOID ImageSectionObject; } SECTION_OBJECT_POINTERS; В структуре имеются два указателя - DataSectionObject и ImageSectionObject. В структуре NTDDK.H они описаны как PVOID, так как ссылаются на недокументированные структуры. DataSectionObject используется тогда, когда файл открыт как файл данных. ImageSectionObject - когда это исполняемый образ. Тип этих указателей одинаков и его можно назвать PCONTROL_AREA. Все последующие структуры будут приводиться для Windows 2K. Они немного изменились, по сравнению с ОС NT 4.0. typedef struct _CONTROL_AREA { // for NT 5.0, size = 0x38 PSEGMENT pSegment; //00 PCONTROL_AREA Flink; //04 PCONTROL_AREA Blink; //08 DWORD SectionRef; //0c DWORD PfnRef; //10 DWORD MappedViews; //14 WORD Subsections; //18 WORD FlushCount; //1a DWORD UserRef; //1c DWORD Flags; //20 PFILE_OBJECT FileObject; //24 DWORD Unknown; //28 WORD ModWriteCount; //2c WORD SystemViews; //2e DWORD PagedPoolUsage; //30 DWORD NonPagedPoolUsage; //34 } CONTROL_AREA, *PCONTROL_AREA; Как видим, CONTROL_AREA является связующим звеном между другими структурами (о которых речь пойдет дальше), а также содержит статистику и флаги. Чтобы было понятно какого рода информация закодирована во флагах - приведу их значения (для NT5.0) /******************** nt5.0 ******************/ #define BeingDeleted 0x1 #define BeingCreated 0x2 #define BeingPurged 0x4 #define NoModifiedWriting 0x8 #define FailAllIo 0x10 #define Image 0x20 #define Based 0x40 #define File 0x80 #define Networked 0x100 #define NoCache 0x200 #define PhysicalMemory 0x400 #define CopyOnWrite 0x800 #define Reserve 0x1000 #define Commit 0x2000 #define FloppyMedia 0x4000 #define WasPurged 0x8000 #define UserReference 0x10000 #define GlobalMemory 0x20000 #define DeleteOnClose 0x40000 #define FilePointerNull 0x80000 #define DebugSymbolsLoaded 0x100000 #define SetMappedFileIoComplete 0x200000 #define CollidedFlush 0x400000 #define NoChange 0x800000 #define HadUserReference 0x1000000 #define ImageMappedInSystemSpace 0x2000000 Непосредственно за структурой CONTROL_AREA следуют Subsections число подсекции. Каждая подсекция описывает информацию о конкретной секции проецируемого файла. Например, read-only, read-write, copy-on-write секция и т.д. Структура SUBSECTION для NT5.0 typedef struct _SUBSECTION { // size=0x20 nt5.0 // +0x10 if GlobalOnlyPerSession PCONTROL_AREA ControlArea; //38, 00 DWORD Flags; //3c, 04 DWORD StartingSector;//40, 08 DWORD NumberOfSectors; //44, 0c PVOID BasePte; //48, 10 pointer to start pte DWORD UnusedPtes; //4c, 14 DWORD PtesInSubsect; //50, 18 PSUBSECTION pNext; //54, 1c }SUBSECTION, *PSUBSECTION; В подсекции есть указатель на контрольную область, флаги, указатель на базовый Proto PTE описывающий область, число Proto PTE. StartingSector - это номер 4K блока, с которого начинается секция в файле. Дополнительная информация закодирована в флагах: #define SS_PROTECTION_MASK 0x1f0 #define SS_SECTOR_OFFSET_MASK 0xfff00000 // (low 12 bits) #define SS_STARTING_SECTOR_HIGH_MASK 0x000ffc00 // (nt5 only) (in pages) //other 5 bit(s) #define ReadOnly 1 #define ReadWrite 2 #define CopyOnWrite 4 #define GlobalMemory 8 #define LargePages 0x200 Итак, осталось рассмотреть последнюю структуру SEGMENT, описывающую все отображение а также Proto PTE которые служат для отображения секций. Память под SEGMENT выделяется из откачиваемого пула. Привожу структуру для SEGMENT (NT 5.0) typedef struct _SEGMENT { PCONTROL_AREA ControlArea; //00 DWORD BaseAddr; //04 DWORD TotalPtes; //08 DWORD NonExtendedPtes;//0c LARGE_INTEGER SizeOfsegemnt; //10 DWORD ImageCommit; //18 DWORD ImageInfo; //1c DWORD ImageBase; //20 DWORD Commited; //24 PTE PteTemplate; //28 or 64 bits if pae enabled DWORD BasedAddr; //2c DWORD BaseAddrPae; //30 if PAE enabled DWORD ProtoPtes; //34 DWORD ProtoPtesPae; //38 if PAE enabled }SEGMENT,*PSEGMENT; Как и ожидалось, структура содержит ссылку на CONTROL_AREA, указатель на пул с Proto PTE и информацию о всем отображении. Остановимся на одной вещи, требующей пояснения. Вид структуры зависит от того, поддерживается ли PAE или нет. PAE - Physical Address Extenion. Начиная с версии 5 - Windows NT включает версию ядра Ntkrnlpa.exe которое поддерживает PAE. В общих словах -поддержка PAE означает возможность использования в NT не 4 а 64Гб виртуальной памяти. Трансляция адреса при включенном PAE происходит с использованием еще одного уровня - все виртуальное пространство делится на 4 банка. Размер PTE и PDE при включенном PAE составляет не 4 а 8 байт, что мы и видим в структуре SEGMENT. Сейчас не стоит задача описывать PAE более подробно, тем более, что он используется все-таки редко, поэтому на этом мы остановимся. При работе над дипломом ядро NTKRNLPA не дизассемблировалось. Все структуры, описывающие секцию описаны. Не описана структура самого объекта секция. Интуитивно понятно, что она должна ссылаться на SEGMENT или на CONTROL_AREA, так как обладая ссылкой на одну из этих структур можно получить всю оставшуюся информацию. Дизассемблирование позволило описать тело объекта секции следующим образом. typedef struct _SECTION_OBJECT { // size 0x28 VAD_HEADER VadHeader; // 0 PSEGMENT pSegment; //0x14 Segment LARGE_INTEGER SectionSize; //0x18 DWORD ControlFlags; //0x20 DWORD PgProtection; //0x24 } SECTION_OBJECT, *SECTION_OBJECT; #define PageFile 0x10000 #define MappingFile 0x8000000 #define Based 0x40 #define Unknown 0x800000 // not sure, in fact it's AllocAttrib&0x400000 Как видно, полученная структура полностью соответствует имеющейся высокоуровневой информации. Единственное, что может вызывать вопрос это заголовок VAD_HEADER. Он описывает расположение в адресном пространстве базовой секции. При этом VAD_HEADER помещен в VAD дереве с вершиной _MmSectionBasedRoot. В очередной раз убеждаемся, что понимание принципов работы ОС лежит через понимание ее внутренних структур. Для закрепления общей картины, ниже приводится схема связей структур, участвующих при описании секции. SECTION_OBJECT->SEGMENT<->CONTROL_AREA->FILE_OBJECT->SECTION_OBJECT_POINTERS+ ^ | +--------------------------------------------+ 08.Создание процесса с точки зрения менеджера памяти ==================================================== Было рассмотрено как создаются процессы с точки зрения подсистемы Win32, были описаны принципы работы менеджера памяти и менеджера объектов, и только что была описана структура объекта - секции. Наибольший интерес теперь, конечно, представляет собой участие менеджера памяти при создании процесса. Процесс создается с помощью недокументированного системного вызова NtCreateProcess(). Ниже приведен его псевдокод. /*****************************************************************/ /* -- Here it is, just wrapper -- */ NtCreateProcess( OUT Handle, IN ACCESS_MASK Access, IN POBJECT_ATTRIBUTES ObjectAttrib, IN HANDLE Parent, IN BOOLEAN InheritHandles, IN HANDLE SectionHandle, IN HANDLE DebugPort, IN HANDLE ExceptionPort ) { if(Parent) { ret=PspCreateProcess(Handle, Access, ObjectAttrib, Parent, InheritHandles, SectionHandle, DebugPort, ExceptionPort); } else ret=STATUS_INVALID_PARAMETER; return ret; } Как видим - NtCreateProcess является заглушкой в другую внутреннюю функцию с такими же параметрами PspCreateProcess. Единственная работа, которая происходит в NtCreateProcess - обязательность параметра Parent (дескриптора родительского процесса ). Следует, однако, заметить - для NT это не имеет особого значения, так как наследственность процессов в общем не несет в себе особого смысла, как ,например, в *UNIX. Теперь посмотрим на PspCreateProcess(). PspCreateProcess( OUT PHANDLE Handle, IN ACCESS_MASK Access, IN POBJECT_ATTRIBUTES ObjectAttrib, IN HANDLE Parent, IN BOOLEAN InheritHandles, IN HANDLE SectionHandle, IN HANDLE DebugPort, IN HANDLE ExceptionPort ); Сразу замечу, что параметр Parent к этой функции может принимать значение 0, это означает, что проверка этого параметра в NtCreateProcess играет роль ограничителя для пользовательского режима. Такие параметры к функции как ссылка на секцию, порты отладки и исключения, процесс - отец передаются через дескрипторы. С помощью функции ObReferenceObjectByHandle функция получает указатели на тела этих объектов. В качестве дескриптора родительского процесса обычно передают -1, что означает текущий процесс. Если Parent равняется 0, то процессорное сродство (на каких процессорах может выполняться процесс) берется не из тела родителя, а из системной переменной. if(Parent) { //Get pointer to father's body ObReferenceObjectByHandle(Parent,0x80,PsProcessType,PrevMode,&pFather,0); AffinityMask=pFather->Affinity; // on witch processors will be executed Prior=8; } else { pFather=0; AffinityMask=KeActiveProcessors; Prior=8; } Приоритет в любом случае ставится равным 8. Затем, происходит создание объекта - процесс. Для NT4.0 его размер равен 504 байта. // size of process body - 504 bytes // creating process object... (type object PsProcessType) ObCreateObject(PrevMode,PsProcessType,ObjectAttrib,PrevMode,0,504,&pProcess); // clear body memset(pProcess,0,504); Происходит инициализация некоторых полей и блока квот. (См. описание менеджера объектов.) pProcess->CreateProcessReported=0; pProcess->DebugPort=pDebugPort; pProcess->ExceptPort=pExceptPort; // Inherit Quota Block, if pFather==NULL, PspDefaultQuotaBlock PspInheritQuota(pProcess,pFather); if(pFather){ pProcess->DefaultHardErrorMode=pFather->DefaultHardErrorMode; pProcess->InheritedFromUniqueProcessId=pFather->UniqueProcessId; } else { pProcess->InheritedFromUniqueProcessId=0; pProcess->DefaultHardErrorMode=1; } Затем идет вызов функции MmCreateProcessAddressSpace. Создается адресный контекст. В качестве параметров функция получает указатель на процесс, размер рабочего набора и указатель на структуру результатов. Эта структура выглядит следующим образом. struct PROCESS_ADDRESS_SPACE_RESULT{ dword Dt; // dict. table phys. addr. dword HypSpace; // hyp space page phys. addr. dword WorkingSet; // working set page phys. addr. }CASResult; MmCreateProcessAddressSpace(PsMinimumWorkingSet,pProcess,&CASResult); Как видим, функция возвращает нам физический адрес директории таблиц страниц (содержимое CR3 для нового адресного пространства), адрес страницы Hyper Space, адрес рабочего набора. После этого происходит инициализация некоторых полей объекта процесса. pProcess->MinimumWorkingSet=MinWorkingSet; pProcess->MaximumWorkingSet=MaximumWorkingSet; KeInitializeProcess(pProcess,Prior,AffinityMask,&CASResult,pProcess-> DefaultHardErrorProcessing&0x4); pProcess->ForegroundQuantum=PspForegroundQuantum; В случае, если имеется родительский процесс и установлен флаг-параметр наследовать базу данных дескрипторов - происходит наследование базы данных дескрипторов от родительского процесса. if(pFather) // if there is father and inherithandle, so, inherit handle db { pFather2=0; if(bInheritHandle)pFather2=pFather; ObInitProcess(pFather2,pProcess); // see info about ObjectManager } Дальше происходит интересная вещь, доказывающая гибкость исполнительной системы NT, которая не видна на поверхности. Если в параметрах была задана секция, то происходит инициализация адресного пространства процесса из этой секции иначе выполняются действия, как при операции fork() в *UNIX системе. if(pSection) { MmInitializeProcessAddressSpace(pProcess,0,pSection); ObDereferenceObject(pSection); res=ObInitProcess2(pProcess); //work with unknown byte +0x22 in process if(res>=0)PspMapSystemDll(pProcess,0); Flag=1; //Created addr space } else { // if there is futher, but no section, so, do operation like fork() if(pFatherProcess){ if(PsInitialSystemProcess==pFather){ MmRes=MmInitializeProcessAddressSpace(pProcess,0,0); } else { pProcess->SectionBaseAddress=pFather->SectionBaseAddress; MmRes=MmInitializeProcessAddressSpace(pProcess,pFather,0); Flag=1; //created addr space } } } Далее происходит вставка процесса в список активных процессов системы PsActiveProcessHead, создание Peb и другие вспомогательные действия. Не будем на этом останавливается. И, наконец, когда все сделано, происходит работа с подсистемой безопасности. Функции менеджера безопасности были рассмотрены раньше (см. описание менеджера объекта) поэтом просто привожу псевдокод этих действий. Замечу только, что в случае если родитель процесса - система (дескриптор процесса равен PspInitialSystemProcessHandle), то проверок на безопасность не производится. // finally, security operations if(pFather&&PspInitialSystemProcessHandle!=Father) { ObGetObjectSecurity(pProcess,&SecurityDescriptor,&MemoryAllocated); pToken=PsReferencePrimaryToken(pProcess); AccessRes=SeAccessCheck(SecurityDescriptor,&SecurityContext, 0,0x2000000, 0,0,&PsProcessToken->GenericMapping, PrevMode,pProcess->GrantedAccess, &AccessStatus); ObDereferenceObject(pToken); ObReleaseObjectSecuryty(SecurityDescriptor,MemoryAllocated); if(!AccessRes)pProcess->GrantedAccess=0; pProcess->GrantedAccess|=0x6fb; } else{ pProcess->GrantedAccess=0x1f0fff; } if(SeDetailedAuditing)SeAuditProcessCreation(pProcess,pFather); Гораздо больший интерес представляют функции KeInitializeProcess и MmCreateProcessAddressSpace. В первой из них кроме инициализации других членов структуры объекта процесса происходит инициализация смещения к карте ввода/вывода в TSS. pProcess->IopmOffset=0x20ad; // IOMAP BASE!!! // You can patch kernel here and // got i/o port control ;) Смещение выбирается таким, что оно указывает за карту ввода-вывода, это запрещает процессам напрямую работать с портами ввода/вывода. В функции MmCreateProcessAddressSpace происходит создание адресного пространства процесса. Не буду приводить весь псевдокод, опишу кратко основные операции. Забираются страницы для Hyper Space, Working Set и Page Directory. Дизассемблирование подтвердило, что они берутся из списка нулевых фреймов или чистятся с помощью функции MiZeroPhysicalPage. Затем происходит инициализация вновь созданной Page Directory. pProcess->WorkingSetPage=Frame3; // WorkingSetPage (MmPfnDatabase+0x18*Frame)->Pte=0xc0300000; ValidPde_U=ValidPdePde&0xeff^Frame2; // HyperSpace /**************IMPORTANT!!!!!!!!!!!!!!************************/ /* ВАЖНО! Тут происходит инициализация PD */ /*************************************************************/ Va=MiMapPageInHyperSpace(Frame,&LastIrql); // no we got Va of our new Page Directory // Fill some fields *(Va+0xc04)=ValidPde_U; // HyperSpace ValidPde_U=ValidPde_U&0xfff^PhysAddr; // DT *(Va+0xc00)=ValidPde_U; // self-pde // copy from current process, kernel address mapping memcpy( (MmVirtualBias+0x80000000)>>0x14+Va, // it's like that we found, // what MmVirtualBias is it ;) (MmVirtualBias+0x80000000)>>0x14+0xc0300000, 0x80 // 32 pdes -> 4Mb*32=128Mb ); memcpy( // copy pdes, corresponding to NonPagedArea MmNonPagedSystemStart>>0x14+Va, MmNonPagedSystemStart>>0x14+0xc0300000, (0xc0300ffc-MmNonPagedSystemStart>>0x14+0xc0300000)&0xfffffffc+4); memcpy(Va+0xc0c, // cache, forgot about it now, it's another story ;) 0xc0300c0c, (MmSystemCacheEnd>>0x14)-0xc0c+4 ); Т.е. происходит копирование PDE описывающих адресное пространство ядра (оно не меняется для всех процессов, за исключением Hyper Space) и не откачиваемую область. А также пространство, принадлежащее системному кэшу. 09.Переключение контекстов ========================== Зная структуры ETHREAD, EPROCESS и принципы работы менеджера памяти, не трудно догадаться, что происходит при переключении контекстов. Планировщик Windows NT оперирует потоками, не заботясь о том, адресное пространство чьего процесса тот разделяет. Т.е. возможно два случая: поток принадлежит текущему процессу - необходимо переключиться на другой поток (позаботиться о стеке и изменить дескриптор GDT связанный с TEB), поток принадлежит другому процессу и необходимо переключиться на него (перегрузка CR3). Для того, чтобы подтвердить предположение, была дизассемблирована функция KeAttachProcess. Эта недокументированная, но всем известная функция позволяет переключиться на адресное пространство другого процесса. С помощью KeDetachProcess можно вернуться к предыдущему процессу. KeAttachProcess использует следующие внутренние функции: KiAttachProcess - логика (KeAttachProcess просто заглушка в эту функцию) KiSwapProcess - поменять адресное пространство. (по сути - перегрузка CR3) SwapContext - поменять контекст. В общем случае помимо замены адресного пространства, настраивает контекст потока. KiSwapThred - переключается на следующий поток в списке (вызывает SwapContext) Ниже приведен псевдокод для некоторых функций. ----------------------------------------------------------------------------- /************************ KeAttachProcess ***************************/ // just wrapper // KeAttachProcess(EPROCESS *Process) { KiAttachProcess(Process,KeRaiseIrqlToSynchLevel); } /************************ KiAttachProcess ***************************/ KiAttachProcess(EPROCESS *Process,Irql){ //CurThread=fs:124h //CurProcess=CurThread->ApcState.Process; if(CurProcess!=Process){ if(CurProcess->ApcStateIndex || KPCR->DpcRoutineActive)KeBugCheckEx... } //if we already in process's context if(CurProcess==Process){KiUnlockDispatcherDatabase(Irql);return;} Process->StackCount++; KiMoveApcState(&CurThread->ApcState,&CurThread->SavedApcState); // init lists CurThread->ApcState.ApcListHead[0].Blink=&CurThread->ApcState.ApcListHead[0]; CurThread->ApcState.ApcListHead[0].Flink=&CurThread->ApcState.ApcListHead[0]; CurThread->ApcState.ApcListHead[1].Blink=&CurThread->ApcState.ApcListHead[1]; CurThread->ApcState.ApcListHead[1].Flink=&CurThread->ApcState.ApcListHead[1];; //fill curtheads's fields CurThread->ApcState.Process=Process; CurThread->ApcState.KernelApcInProgress=0; CurThread->ApcState.KernelApcPending=0; CurThread->ApcState.UserApcPending=0; CurThread->ApcState.ApcStatePointer.SavedApcState=&CurThread->SavedApcState; CurThread->ApcState.ApcStatePointer.ApcState=&CurThread->ApcState; CurThread->ApcStateIndex=1; //if process ready, just swap it... if(!Process->State)//state==0, ready { KiSwapProcess(Process,CurThread->SavedApcState.Process); KiUnlockDispatcherDatabase(Irql); return; } CurThread->State=1; //ready? CurThread->ProcessReadyQueue=1; //put Process in Thread's waitlist CurThread->WaitListEntry.Flink=&Process->ReadyListHead.Flink; CurThread->WaitListEntry.Blink=Process->ReadyListHead.Blink; Process->ReadyListHead.Flink->Flink=&CurThread->WaitListEntry.Flink; Process->ReadyListHead.Blink=&CurThread->WaitListEntry.Flink; // else, move process to swap list and wait if(Process->State==1){//idle? Process->State=2; //trans Process->SwapListEntry.Flink=&KiProcessInSwapListHead.Flink; Process->SwapListEntry.Blink=KiProcessInSwapListHead.Blink; KiProcessInSwapListHead.Blink=&Process->SwapListEntry.Flink; KiSwapEvent.Header.SignalState=1; if(KiSwapEvent.Header.WaitListHead.Flink!=&KiSwapEvent.Header.WaitListHead. Flink) KiWaitTest(&KiSwapEvent,0xa); //fastcall } CurThread->WaitIrql=Irql; KiSwapThread(); return; } Из этой функции можно сделать следующие выводы. Процесс может находиться в одном из следующих состояний - 0 (Готов), 1(Idle), 2( Trans - переходное). Это соответствует высокоуровневой информации. Из KiAttachProcess используются две другие функции - KiSwapProcess и KiSwapThread. /************************* KiSwapProcess ****************************/ KiSwapProcess(EPROCESS* NewProcess, EPROCESS* OldProcess) { // just reload cr3 and small work with TSS // TSS=KPCR->TSS; // xor eax,eax // mov gs,ax TSS->CR3=NewProcess->DirectoryTableBase;//0x1c // mov cr3,NewProcess->DirectoryTableBase TSS->IopmOffset=NewProcess->IopmOffset;//0x66 if(WORD(NewProcess->LdtDescriptor)==0){lldt 0x00; return;//} //GDT=KPCR->GDT; (QWORD)GDT->0x48=(QWORD)NewProcess->LdtDescriptor; (QWORD)GDT->0x108=(QWORD)NewProcess->Int21Descriptor; lldt 0x48; return; } Переключение контекстов процессов. Как и ожидалось - это просто перегрузка регистра CR3. Плюс немного сопровождающих действий. Например, устанавливается смещение к карте ввода/вывода в TSS из поля IopmOffset. При необходимости загружается значение селектора в ldt. (Только для VDM сессии.) /************************* SwapContext ******************************/ SwapContext(NextThread,CurThread,WaitIrql) { NextThread.State=ThreadStateRunning; //2 KPCR.DebugActive=NextThread.DebugActive; cli(); //Save Stack CurThread.KernelStack=esp; //Set stack KPCR.StackLimit=NextThread.StackLimit; KPCR.StackBase=NextThread.InitialStack; tmp=NextThread.InitialStack-0x70; newcr0=cr0&0xfffffff1|NextThread.NpxState|*(tmp+0x6c); if(newcr0!=cr0)reloadcr0(); if(!*(tmp-0x1c)&0x20000)tmp-=0x10; TSS=KPCB.TSS; TSS->ESP0=tmp; //set pTeb KPCB.Self=NextThread.pTeb; esp=NextThread.KernelStack; sti(); //correct GDT GDT=KPCB.GDT; WORD(GDT->0x3a)=NextThread.pTeb; BYTE(GDT->0x3c)=NextThread.pTeb>>16; BYTE(GDT->0x3f)=NextThread.pTeb>>24; //if we must swap processes, do it (like KiSwapProcess) if(CurThread.ApcState.Process!=NextThread.ApcState.Process) { //******** like KiSwapProcess } NextThread->ContextSwitches++; KPCB->KeContextSwitches++; if(!NextThread->ApcState.KernelApcPending)return 0; //popf; // jnz HalRequestSoftwareInterrupt// return 0 return 1; } Переключается стек, корректируется GDT, так, чтобы регистр FS указывал на TEB. Если поток принадлежит текущему процессу, переключение контекстов не делается. Иначе производятся действия примерно такие же как и в KiSwapProcess (но без вызова последней, видимо, в целях повышения производительности.) Для общности приведу прототип KeDetachProcess. KeDetachProcess(EPROCESS *Process,Irql); Как видно - псевдокод этих функций практически полностью описывает действия ОС при переключении контекстов. В общем, анализ кода показывает, что основной путь к пониманию ядра ОС это знание ее внутренних структур. 0a.Некоторые недокументированные функции менеджера памяти. ========================================================== Из ntoskrnl.exe SP3 из менеджера памяти экспортируются следующие имена: 467 1D0 00051080 MmAdjustWorkingSetSize 468 1D1 0001EDFA+MmAllocateContiguousMemory 469 1D2 00051A14+MmAllocateNonCachedMemory 470 1D3 0001EAE8+MmBuildMdlForNonPagedPool 471 1D4 000206BC MmCanFileBeTruncated 472 1D5 0001EF5A+MmCreateMdl 473 1D6 0002095C MmCreateSection 474 1D7 00021224 MmDbgTranslatePhysicalAddress 475 1D8 000224AC MmDisableModifiedWriteOfSection 476 1D9 000230C8 MmFlushImageSection 477 1DA 0001FA9C MmForceSectionClosed 478 1DB 0001EEA0+MmFreeContiguousMemory 479 1DC 00051AFE+MmFreeNonCachedMemory 480 1DD 0001EEAC+MmGetPhysicalAddress 481 1DE 00024028 MmGrowKernelStack 482 1DF 0004E144 MmHighestUserAddress 483 1E0 0002645A+MmIsAddressValid 484 1E1 00026CD8+MmIsNonPagedSystemAddressValid 485 1E2 0001F5D8 MmIsRecursiveIoFault 486 1E3 00026D56+MmIsThisAnNtAsSystem 487 1E4 000766C8+MmLockPagableDataSection 488 1E5 000766C8 MmLockPagableImageSection 489 1E6 0001F160+MmLockPagableSectionByHandle 490 1E7 0001ED18+MmMapIoSpace 491 1E8 0001EB74+MmMapLockedPages 492 1E9 0001F5F6 MmMapMemoryDumpMdl 493 1EA 00076A14 MmMapVideoDisplay 494 1EB 0005206C MmMapViewInSystemSpace 495 1EC 00079B0E MmMapViewOfSection 496 1ED 0007A7EE+MmPageEntireDriver 497 1EE 0001E758+MmProbeAndLockPages 498 1EF 00026D50+MmQuerySystemSize 499 1F0 00052A8A+MmResetDriverPaging 500 1F1 0004E0A4 MmSectionObjectType 501 1F2 00079D28 MmSecureVirtualMemory 502 1F3 0001EFCE MmSetAddressRangeModified 503 1F4 0007684E MmSetBankedSection 504 1F5 0001EF2C+MmSizeOfMdl 505 1F6 0004E0A0 MmSystemRangeStart 506 1F7 0001F516+MmUnlockPagableImageSection 507 1F8 0001EA16+MmUnlockPages 508 1F9 0007669A+MmUnmapIoSpace 509 1FA 0001ECA8+MmUnmapLockedPages 510 1FB 00076A2E MmUnmapVideoDisplay 511 1FC 00052284 MmUnmapViewInSystemSpace 512 1FD 0007AFE4 MmUnmapViewOfSection 513 1FE 0007A00A MmUnsecureVirtualMemory 514 1FF 0004DDCC MmUserProbeAddress Здесь символом '+' обозначены документированные в DDK функции. Приведу прототипы некоторых недокументированных функций. // изменить квоты working set. NTOSKRNL NTSTATUS MmAdjustWorkingSetSize( DWORD MinimumWorkingSet OPTIONAL, // if both == -1 DWORD MaximumWorkingSet OPTIONAL, // empty working set PVM Vm OPTIONAL); //can file be truncated??? NTOSKRNL BOOLEAN MmCanFileBeTruncated( PSECTION_OBJECT_POINTERS SectionPointer, // see FILE_OBJECT PLARGE_INTEGER NewFileSize ); // create section. NtCreateSection call this function... NTOSKRNL NTSTATUS MmCreateSection ( OUT PVOID *SectionObject, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL, IN PLARGE_INTEGER MaximumSize, IN ULONG SectionPageProtection,//PAGE_XXXX IN ULONG AllocationAttributes,//SEC_XXX IN HANDLE FileHandle OPTIONAL, IN PFILE_OBJECT File OPTIONAL ); typedef enum _MMFLUSH_TYPE { MmFlushForDelete, MmFlushForWrite } MMFLUSH_TYPE; NTOSKRNL BOOLEAN MmFlushImageSection ( IN PSECTION_OBJECT_POINTERS SectionObjectPointer, IN MMFLUSH_TYPE FlushType ); NTOSKRNL DWORD MmHighestUserAddress; // обычно 0x7ffeffff NTOSKRNL BOOLEAN MmIsRecursiveIoFault(); //ее код #define _MmIsRecursiveIoFault() ( \ (PsGetCurrentThread()->DisablePageFaultClustering) | \ (PsGetCurrentThread()->ForwardClusterOnly) \ ) NTOSKRNL POBJECT_TYPE MmSectionObjectType; //типовой объект Section NTOSKRNL DWORD MmSystemRangeStart; //обычно 0x80000000 NTOSKRNL DWORD MmUserProbeAddress; //обычно 0x7fff0000 NTOSKRNL PVOID MmMapVideoDisplay( // для i386 враппер в MmMapIoSpace IN PHYSICAL_ADDRESS PhysicalAddress, IN ULONG NumberOfBytes, IN BOOLEAN CacheEnable ); NTOSKRNL VOID MmUnmapVideoDisplay ( // для i386 враппер в MmUnmapIoSpace IN PVOID BaseAddress, IN ULONG NumberOfBytes ); // помечает фрэймы диапазонп как измененные и производит сопуствуюзие действия NTOSKRNL VOID MmSetAddressRangeModified( PVOID StartAddress, DWORD Length ); // вызывается из NtMapViewOfSection typedef enum _SECTION_INHERIT { ViewShare=1; ViewUnmap=2; }SECTION_INHERIT; NTOSKRNL NTSTATUS MmMapViewOfSection( PVOID pSection, PEPROCESS pProcess, OUT PVOID *BaseAddress, DWORD ZeroBits, DWORD CommitSize, OUT PLARGE_INTEGER SectionOffset OPTIONAL, OUT PDWORD ViewSize, SECTION_INHERIT InheritDisposition, DWORD AllocationType, DWORD ProtectionType ); NTOSKRNL NTSTATUS MmUnmapViewOfSection( PEPROCESS Process, PVOID Address ); PVOID MmLockPagableImageSection( PVOID AddressWithinImageSection // same entry as MmLockPagableDataSection ); // уменьшить StackLimit (стек растет) NTSTATUS MmGrowKernelStack( PVOID CurESP //адрес около вершины стека ); I talk to the wind My words are all carried away I talk to the wind The wind does not hear The wind cannot hear. King Crimson'69 -I Talk to the Wind 0b.Заключение ============= Это все. Круг замкнут. Если посмотреть на все описанное в комплексе, получится более-менее общая картина менджера памяти. К сожалению, она далеко не полная. Менеджер памяти, наверное, самый сложный и важный компонент ядра, для того что бы его полностью описать, мне понадобилось бы перелопатить еще не один десяток функций. Однако основные _фундаментальные_ вещи описаны. Они будут полезны при дальнейшем дизассемблировании ядра. А там, кто знает... ;) Best Regards, Peter Kosy aka Gloomy. Melancholy Coding '2001. mailto:gl00my@mail.ru P.S. Я знаю - мой 'труд' содержит неизбежные ошибки. Буду рад услышать любые комментарии и замечания. ПРИЛОЖЕНИЕ 0c. Некоторые недокументированные системные вызовы ================================================== Здесь я опишу несколько полезных Zw/Nt функций, которые могут использоваться из USER-режима или из драйверов (как Zw). Почти все они взяты из книги Коберниченко "Недокументированные возмождности Windows NT". Плюс знание структуры Working Set позволило описать MEMORY_WORKING_SET_INFORMATION для NtQueryVirtualMemory. NTSYSAPI NTSTATUS NTAPI NtAllocateVirtualMemory( HANDLE Process, OUT PVOID *BaseAddr, DWORD ZeroBits, OUT PDWORD RegionSize, DWORD AllocationType,// MEM_RESERVE|MEM_COMMIT|MEM_TOP_DOWN DWORD Protect); // PAGE_XXXX... NTSYSAPI NTSTATUS NTAPI NtFreeVirtualMemory( HANDLE Process, OUT PVOID* BaseAddr, OUT PULONG RegionSize, DWORD FreeType //MEM_DECOMMIT|MEM_RELEASE ); NTSYSAPI NTSTATUS NTAPI NtCreateSection( OUT PHANDLE Section, ACCESS_MASK DesirdAccess, //SECTION_MAP_XXX... POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL, PLARGE_IBTEGER MaximumSize OPTIONAL, DWORD SectionPageProtection, //PAGE_... DWORD AllocationAttributes, //SEC_XXX HANDLE FileHandle OPTIONAL // NULL - pagefile ); typedef enum _SECTION_INHERIT { ViewShare=1; ViewUnmap=2; }SECTION_INHERIT; NTSYSAPI NTSTATUS NTAPI NtMapViewOfSection( HANDLE Section, HANDLE Process, OUT PVOID *BaseAddress, DWORD ZeroBits, DWORD CommitSize, OUT PLARGE_INTEGER SectionOffset OPTIONAL, OUT PDWORD ViewSize, SECTION_INHERIT InheritDisposition, DWORD AllocationType, //MEM_TOP_DOWN,MEM_LARGE_BAGE,MEM_AUTO_ALIGN=0x40000000 DWORD ProtectionType // PAGE_... ); #define UNLOCK_TYPE_NON_PRIVILEGED 0x00000001L #define UNLOCK_TYPE_PRIVILEGED 0x00000002L NTSYSAPI NTSTATUS NTAPI NtLockVirtualMemory( IN HANDLE ProcessHandle, IN OUT PVOID *RegionAddress, IN OUT PULONG RegionSize, IN ULONG UnlockTypeRequired ); NTSYSAPI NTSTATUS NTAPI NtUnlockVirtualMemory( IN HANDLE ProcessHandle, IN OUT PVOID *RegionAddress, IN OUT PULONG RegionSize, IN ULONG UnlockTypeRequiested ); NTSYSAPI NTSTATUS NTAPI NtReadVirtualMemory( IN HANDLE ProcessHandle, IN PVOID StartAddress, OUT PVOID Buffer, IN ULONG BytesToRead, OUT PULONG BytesReaded OPTIONAL ); NTSYSAPI NTSTATUS NTAPI NtWriteVirtualMemory( IN HANDLE ProcessHandle, IN PVOID StartAddress, IN PVOID Buffer, IN ULONG BytesToWrite, OUT PULONG BytesWritten OPTIONAL ); NTSYSAPI NTSTATUS NTAPI NtProtectVirtualMemory( IN HANDLE ProcessHandle, IN OUT PVOID *RegionAddress, IN OUT PULONG RegionSize, IN ULONG DesiredProtection, OUT PULONG OldProtection ); NTSYSAPI NTSTATUS NTAPI NtFlushVirtualMemory( IN HANDLE ProcessHandle, IN PVOID* StartAddress, IN PULONG BytesToFlush, OUT PIO_STATUS_BLOCK StatusBlock ); typedef enum _MEMORYINFOCLASS { MemoryBasicInformation, MemoryWorkingSetInformation, // еще есть класс 2 - это информация из VAD, я пока не знаю // структуру VAD полностью, что бы описать соотв. INFO структуру } MEMORYINFOCLASS; typedef struct _MEMORY_BASIC_INFORMATION { PVOID BaseAddress; PVOID AllocationBase; ULONG AllocationProtect; ULONG RegionSize; ULONG State; ULONG Protect; ULONG Type; } MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION; #define WSFRAMEINFO_SHARED_FRAME 0x100 #define WSFRAMEINFO_INTERNAL_USE 0x4 #define WSFRAMEINFO_UNKNOWN 0x3 typedef struct _MEMORY_WORKING_SET_INFORMATION { ULONG SizeOfWorkingSet; DWORD WsEntries[ANYSIZE_ARRAY]; // is Page VA | WSFRAMEINFO... } MEMORY_ENTRY_INFORMATION, *PMEMORY_ENTRY_INFORMATION; NTSYSAPI NTSTATUS NTAPI NtQueryVirtualMemory( IN HANDLE ProcessHandle, IN PVOID RegionAddress, IN MEMORYINFOCLASS MemoryInformationClass, IN PVOID VirtualMemoryInfo, IN ULONG Length, OUT PULONG ActualLength OPTIONAL ); 0d.Некоторые замечания и черновые наброски ========================================== **** К MmCreateProcessAddressSpace ... **** ============================================= __fastcall MiTotalCommitLimit(PVOID pProcess, DWORD NumOfPages); // edx:ecx есть статистика dd MmTotalCommitLimit dd MmTotalCommitedPages Если NumOfPages+MmTotalCommitedPages не превышает Limit - все ОК и просто корректируется статистика. Иначе начинаются межпотоковые взаимодействия. Выбирается значение тайм-аута (20 секунд если требуется >=10 страниц) Иначе - 1 секунда. Далее заполняется некоторая структура - что то вроде: typedef struct _REQUEST_FOR_COMMITED_MEMORY{ LIST_ENTRY ListEntry; DWORD PagesToCommit; DWORD Result; KSEMAPHORE Semaphore; }_REQUEST_FOR_COMMITED_MEMORY; Эта структура (или элемент списка) вставляется в глобальный список ListOfRequest в глобальной структуре: [Pre List Item]<->[Our List Item]<->[ListOfRequest] typedef struct _COMMIT_MEMORY_REQUEST_LIST{ KSEMAPHORE CommitMemorySemaphore; LIST_ENTRY ListOfRequest; }COMMIT_MEMORY_REQUEST_LIST; Затем KeReleaseSemaphore для CommitMemorySemaphore и ждем у "нашего" семафора в REQUEST_FOR_COMMITED_MEMORY с выбранным тайм-аутом. Если таймаут не вышел и в Result не 0 - то еще раз проверка на Limit и выход c OK(если с лимитом проблема - повторяется вся схема заново). Если результат 0 - на MiCouseOverCommitPopup.Если же произошел time out - пошел анализ: В случае - если ListOfReques.Flink==&ListOfReques.Flink, т.е. все запросы в хвосте очереди(???) То опять ждем у нашего семафора - причем уже без таймаута, так как проблемы не наши ;) В случае - если ListOfReques.Flink==&RequestForCommitedMemory.ListEntry т.е. следующий по очереди - наш запрос (???) То убираем наш запрос из очереди, так как это из за нас. Теперь, смотрим сколько страниц мы хотели. Если >=10 идем на MiCouseOverCommitPopup Иначе на MiChargeCommitmentCantExpand. После - на выход. Вся работа сопровождается когда надо cli sti, а также работой с FastMutex'ом (смещение 10сh в процессе) Которая не производится при вызове этой функции при создании процесса. Теперь, что делает MiCouseOverCommitPopup(PagesNum,CommitTotalLimitDelta); - если мы хотели более 128 страниц - то ExRaiseStatus(STATUS_COMMITMENT_LIMIT); если меньше, то IoRaiseInformationalHardError(STATUS_COMMITMENT_LIMIT,0,0); (эти функции документированы) Если вызов последней функции удачный - то накапливаем статистику: MiOverCommitCallCount++; MmTotalCommitLimit+=CommitTotalLimitDelta; MmExtendedCommit+=CommitTotalLimitDelta; MmTotalCommittedPages+=PagesNum; И корректируется MmPeakCommintment; Если не удачный но MiOverCommitCallCount==0 все равно на статистику иначе ExRaiseStatus(STATUS_COMMITMENT_LIMIT); Служебные функции: DWORD NTOSKRNL RtlRandom(PDWORD Seed); Как ни странно - не документирована. Использует при своей работе таблицу из 128 DWORD'ов. Таблица и Seed корректируются после работы. Видимо это дает максимальный период. Есть два event'а MmAvailablePagesEventHigh и MmAvailablePagesEventHigh. MiSectionInitialization: MmDereferenceSegmentHeader: это структура описанная выша с добавленным spinlock сверху. Создает поток MiDereferenceSegmentThread PsChargePoolQuota(PVOID Process,DWORD Type(NP/P),DWORD Charge); [TO DO] -->> MmInfoCounters!!!! Очень много полезной инфы можно из них получить используя соотв NtQueryInfo.... ПОСМОТРЕТЬ!!! --------------------------------------------------------------------------- (c)Gloomy aka Peter Kosyh, Melancholy Coding'2001 http://gloomy.cjb.net mailto:gl00my@mail.ru