Источник: John Gulbrandsen. "How Do Windows NT System Calls REALLY Work?" // http://www.codeguru.com. - 2004.
http://www.codeguru.com/Cpp/W-P/system/devicedriverdevelopment/article.php/c8035/

Перевод: Похилец Н.В.


Как на самом деле работают системные вызовы Windows?

Окружение:  Windows NT, 2K, XP, 2003

Большинство материалов, которые описывают системные вызовы Windows NT, оставлют многие важные детали за кадром. Это приводит к затруднениям при попытке понять, что же именно происходит, когда код пользовательского режима вызывает код ядра. Данная статья призвана пролить свет на действительный механизм, который Windows NT использует для переключения в режим ядра для исполнения системных операций. Описание приводится для x86-совместимого процессора в защищенном режиме. Другие платформы, поддерживаемые Windows NT имеют схожий механизм для переключения в режим ядра.

Что такое режим ядра?

Вопреки мнению многих программистов (и в том числе системных) у x86-процессора не существует так называемого "режима ядра". Другие процессоры, такие, как Motorola 68000, имеют два процессорных режима "встроенных" в процессор. Т.е. в статусном регистре содержится флаг, который определяет происходит ли сейчас исполнение кода в пользовательском режиме или в режиме супервизора. У процессоров Intel x86 такого флага нет. Вместо этого, уровень привелегий исполняемой программы определяется уровнем привилегий текущего сегмента кода. Каждый сегмент кода в приложении пользовательского режима на процессоре x86 описывается 8-байтой структурой данных, называемой сегментым дескриптором. Сегментный дескриптор (среди прочего) содержит начальный адресс описываемого сегмента кода, его длину и уровень привилегий, на котором будет исполняться код, содержащийся в этом сегменте. Код исполняемый с уровнем привилегий равным 3 считается кодом пользовательского режима, а код с уровнем привилегий 0 – кодом режима ядра. Другими словами, режим ядна (уровень привилегий 0) и пользовательский режим (уровень привилегий 3) являеются атрибутами кода, а не процессора. В терминологии Intel уровень привилегий 0 называется "Ring 0", а уровень привилегий 3 – "Ring 3". У x86-процессора есть еще два уровня привилегий, которые не используются Windows NT ("Ring 1" и "Ring 2"). Причина этого в том, что Windows NT спроектирована с учетом возможности работы и на других аппаратных платформах, которые могут иметь, а могут и не иметь четырех уровней привилегий подобно Intel x86.

Процессор не позволит коду с более низким (численно большим) уровнем привилегий непосредственно сделать вызов в код с более высоким (численно меньшим) уровнем привилегий. Если такая попытка будет предпринята, то процессором автоматически будет сгенерировано исключение общей защиты (general protection, GP). Будет вызван обработчик этого исключения, предоставленный операционной системой, и будут предприняты соответсвующие действия (выдача сообщения пользователю, принудительное завершение приложения и др.). Следует отметить что весь вышеописанный механизм защиты является свойством x86-процессора, а не операционной системы. Без надлежащей поддержки со стороны процессора, реализовать данный механизм средствами ОС было бы невозможно.

Где располагаются сегментные дескрипторы?

Поскольку каждый сегмент кода, который существует в системе, описывается сегментным дескриптором, а их количество может быть достаточно велико (каждая программа может иметь несколько), то сегментные дескрипторы должны где-то храниться, чтобы процессор мог прочитать их и разрешить либо запретить программе доступ к сегменту. Разработчики Intel приняли решение хранить всю эту информацию не в самом чипе процессора, а в основной памяти. Есть два таблицы в основной памяти, которые хранят сегментные дескрипторы: глобальная таблица дескрипторов (Global Descriptor Table, GDT) и локальная таблица дескрипторов (Local Descriptor Table, LDT). Адреса и размеры этих таблиц хранятся в соответсвующих регистрах процесоора. Этими регистрами являются Global Descriptor Table Register (GDTR) и Local Descriptor Table Register (LDTR). Заполнение таблиц и установка значений этих регистров является обязанностью операционной системы. И это должно быть сделано на самых ранних этапах процесса загрузки, еще до переключения в защищенный режим, потому, что без таблиц дескрипторов в защищенном режиме невозможно обратиться ни к одному сегменту памяти. Рисунок 1 иллюстрирует отношения между GDTR, LDTR, GDT и LDT.

GDTR и LDTR хранят адреса и размеры таблиц дескрипторов
(Увеличить изображение)

Поскольку существуют две таблицы дескрипторов, то для однозначной идентификации сегментного дескриптора необходимо знать не только его индекс в таблице, но и в какой таблице он находится. Для этих целей введен бит, который определяет в какой из двух таблиц находится дескриптор. Индекс в таблице объединенный сместе с этим битом называется селектором сегмента. Формат селектора сегмента изображен ниже.

Формат 16-битного селектора сегмента
(Увеличить изображение)

Как можно видеть из рисунка 2, селектор сегмента также содерижт 2-битное поле, которое называется Requestor Privilege Level (RPL). Эти биты используются для определения того, может ли определенный кусок кода обращаться к дескриптору, заданному данным селектором. К примеру, если код с уровнем привилегий 3 (режим пользователя) пытается сделать переход или вызвать код из другого сегмента, через селектор с RPL=0, то произойдет исключение общей защиты. Таким образом x86-процессор гарантирует что никакой код режима пользователя не получит доступ к коду уровня ядра. На самом деле, истинное положение вещей куда сложнее, чем изложено здесь. Получить детальную информацию по этой Рекомендуется For the information-eager please see the further reading list, "Protected Mode Software Architecture" for the details of the RPL field. For our purposes it is enough to know that the RPL field is used for privilege checks of the code trying to use the segment selector to read a segment descriptor.

Шлюзы прерываний

Итак, если приложение, исполняющееся в пользовательском режиме (с уровнем привилегий 3), не может вызвать кода ядра (с уровнем привилегий 0), то как же тогда работаю системные вызовы в Windows NT? Ответ опять лежит в использовании возможностей процессора. Для того чтобы контролировать переходы между кодом с разным уровнем привилегий, Windows NT использует возмножность x86-процессора, называемую шлюз прерывания. Для того, чтобы понять суть шлюза прерывания, необходимо сначала разобраться с тем, как используются прерывания в x86-процессоре в защищенном режиме.

Подобно многим другим процессорам, x86-процессор имеет таблицу векторов прерывания, которая содержит информацию о том, как следует обрабатывать каждое прерывание. В реальном режиме, таблица векторов прерываний x86-процессора, просто содержит указатели (4 байта каждый) на процедуры обслуживания прерываний (Interrupt Service Routines, ISR). А в защищенном режиме таблица векторов прерываний содержит декскрипторы шлюзов прерываний, которые являются 8-байтовыми структурами, которые описывают как данное прерывание должно быть обработано. Дескриптор шлюза прерывания содержит информацию о том, какой сегмент кода содержит процедуру обслуживания прерываний и ее адресс внутри сегмента. Причина, по которой в таблице используются декскрипторы шлюзов прерываний вместо простых указателей, заключается в требовании того, что код пользовательского режима не может вызвать произвольный код ядра, в том числе и путем генерации программных прерываний. Проверяя уровень привилегий в дескрипторе шлюза прерывания процессор гарантирует, что вызывающему приложению разрешено вызывать код ядра только в специально отведенных для этого точках (в этом и заключается суть термина "шлюз прерывания", т.е. это четко определенный шлюз, который может передать управление от кода уровня пользователя к коду уровня ядра).

Дескриптор шлюза прерырвания содержит селектор сегмента, который однозначно определяет дексриптор сегмента кода, который описывает сегмент кода, в котором располагается процедура обслуживания прерывания. В случае системных вызовов Windows NT, селектор сегмента указывает на дескриптор в GDT. В GDT хранятся декскрипторы всех сегментов, которые являются общими для все системы, и не связаны с конкретным процессом (т.е.это сегменты кода и данных ядра самой операционной системы). На рисунке 3 изображены связи между записью в IDT, закрепленной за командой 'int 2e', записью в GDT и процедурой обслуживания прерывания в целевом сегменте кода.

Как процессор находит процедуру обслуживания прерывания для программного прерывания 2E
(Увеличить изображение)

Возвращаясь к системным вызовам Windows NT

Теперь, после рассмотрения базового материала, можно описать как же именно системный вызов Windows NT находит свой путь из режима пользователя в режим ядра. Системные вызовы в Windows NT инициируются исполнением инструкции "int 2e". Комаднда 'int' заставляет процессор сгенерировать программное прерывание, т.е. проследовать в IDT по индексу 2e и считать находящийся там дескриптор шлюза прерывания. Использую селектор из декскриптора шлюза, процессор загрузит дескриптор сегмента кода и установит значение EIP на смещение точки входа в процедуру обслуживания прерывания, взятое из дескриптора шлюза. На этом шаге процессор уже почти готов начать исполнение кода ISR в сегменте режима ядра.

Процессор автоматически переключается к стеку режима ядра

Прежде чем процессор начнет исполнять ISR в кодовом сегмента режима ядра, он должен переключиться на стек ядра. Причина этого заключается в том, что код режима ядра не может полагаться на то что в стеке пользовательского режима достаточно свободного места для работы в режиме ядра. К примеру, вредоносный код пользовательского режима может модифицировать собственный указатель стека и установить его на несуществующий адрес, выполнить инструкцию 'int 2e' и таким образом добиться краха системы когда код ядра обратится по недействительному указателю стека. Каждый уровень привилегий в защищенном режиме x86-процессора имеет свой собственный стрек. Когда происходит вызов функции более привелигированного уровня через шлюз прерывания, как описано выше, процессор автоматически сохраняет значения регистров SS, ESP, EFLAGS, CS и EIP из пользовательского режима в стеке режима ядра. В нашем случай системного вызова Windows NT функции диспетчера сервисов (KiSystemService) требуются доступ к параметрам системного вызова, которые код пользовательского режима поместил в стек перед выполнением 'int 2e'. По соглашению, код пользовательского режима должен поместить в региср EBX указатель на блок параметров в стеке пользовательского режима. Тогда функция KiSystemService может просто скопировать требуемое число байтов из стека пользовательского режима в стек режима ядра перед вызовом системной функции. Рисунок 4 иллюстрирует это.

Регистры пользовательского режима автоматически записываются в стек режима ядра, но параметры системного вызова должны быть скопированы
(Увеличить изображение)

Как системный вызов вызывается?

Поскольку все системные вызовы Windows NT используют одно и тоже программное прерывание 'int 2e' для переключения в режим ядра, как код пользовательского режима информирует код ядра какую именно системную функцию нужно выполнить? Ответ заключается в том, что индекс помещается в регистр EAX перед вызовом инструкции int 2e. В режиме ядра ISR считывает значение регистра EAX и вызывает указанную системную функцию, если все переданные параметры проходят предварительную проверку. Параметры системного вызова (к примеру, те что были переданы в функцию OpenFile) передаются функции ядра в теле ISR.

Возврат из системного вызова

Когда работа системного вызова завершается, исполняется инструкция iret. Исполняя эту инструкцию, процессор восстанавливает из стека значения сохраненные значения регистров пользовательского режима и продолжает исполнение с инструкции следующей за инструкцией 'int 2e'.

Эксперимент

Изучая дескриптор шлюза прерывания 2e, можно убедиться что процессор действительно находит процедуру диспетчера системных функций как описано в этой статье. Приложенный пример кода, содержит расширение для отладчика WinDbg, которое в режиме отладки ядра выводит содержимое дескрипторов в GDT, LDT или IDT.

Скачать пример кода: ProtMode.zip

Приведенное расширение отладчика является DLL с именем 'protmode.dll' (Protected Mode). Она загружается в WinDbg командной ".load protmode.dll", предварительно скопировав DLL в каталог, содержащий файл kdextx86.dll для целевой платформы. Остановиnt выполнение в отладчике WinDbg (CTRL-C) подключившись к целевой платформе. Синтаксис для вывода дескриптора в IDT для прерывания 'int 2e' следующий: "!descriptor IDT 2e". Это выводит следующую информацию:

kd>!descriptor IDT 2e
------------------- Interrupt Gate Descriptor --------------------
IDT base = 0x80036400, Index =    0x2e, Descriptor @ 0x80036570
80036570 c0 62 08 00 00 ee 46 80
Segment is present, DPL = 3, System segment, 32-bit descriptor
Target code segment selector =    0x0008 (GDT Index = 1, RPL = 0)
Target code segment offset =      0x804662c0
------------------- Code Segment Descriptor --------------------
GDT base = 0x80036000, Index =    0x01, Descriptor @ 0x80036008
80036008 ff ff 00 00 00 9b cf 00
Segment size is in 4KB pages, 32-bit default operand and data size
Segment is present, DPL =         0, Not system segment, Code segment
Segment is not conforming, Segment is readable, Segment is accessed
Target code segment base address =     0x00000000
Target code segment size = 0x000fffff

Комманда 'descriptor' показывает следующее:

Комманда "!descriptor IDT 2e" также выводит выводит дескриптор целевого сегмента кода по индексу 1 в GDT. Вот объяснение данным извлеченным из дескриптора в GDT:

Для сброки файла ProtMode.dll, откройте проект в Visual Studio 6.0 и нажмите кнопку "build". Для вводной информации по созданию расширений отладчика вроде ProtMode.dll, смотрите сопутствующее SDK для "Debugging Tools for Windows", которое можно бесплатно скачать с сайта Microsoft.

Дальнейшее чтение

Вот два отличных источника по защищенному режиму процессоров Intel x86:

  1. "Intel Architecture Software Developers Manual". Доступно на сайте Intel-а в PDF формате.
  2. "Protected Mode Software Architecture" by Tom Shanley. Доступно на Amazon.com (издательство Addison Wesley).

Загрузки

  • ProtMode.zip - ProtMode.zip