Какая нотация вызовов функций принята в системных вызовах windows

Обновлено: 07.07.2024

В вычислительной технике, системный вызов является программным способом, в котором компьютерная программа запрашивает определенную операцию от ядра операционной системы. Иными словами, системный вызов возникает, когда пользовательский процесс требует некоторой службы реализуемой ядром и вызывает специальную функцию.

Сюда могут входить услуги, связанные с аппаратным обеспечением (например, доступ к жесткому диску), создание и выполнение новых процессов, связь с интегральными службами ядра, такими как планирование процессов. Системные вызовы обеспечивают необходимый интерфейс между процессом и операционной системой. В основном, системные вызовы могут быть сделаны только самим пользователем, однако в таких системах как OS/360 привилегированный код системы также может вызвать системный вызов. [Источник 1]

Содержание

Привилегии

Архитектура большинства современных процессов, за исключением некоторых встроенных систем, включает в себя модель безопасности с несколькими уровнями привилегий. Например, модель колец определяет несколько уровней привилегий, в соответствии с которыми может выполняться программное обеспечение: программа, как правило, ограничивается собственным адресным пространством, поэтому она не может получить доступ или изменить другие запущенные программы или саму операционную систему, а также не может получать доступ к системным ресурсам (например, к сетевым устройствам, жестким дискам и тд) .

Тем не менее, многим приложениям необходим доступ к этим компонентам, поэтому системные вызовы дают возможность операционной системе обеспечить безопасный доступ к ним. Операционная система работает на самом высоком уровне привилегий и позволяет приложениям запрашивать сервис через системные вызовы, которые часто инициируются через прерывания. Прерывание автоматически переводит процессор в некоторый повышенный уровень привилегий, а затем передает управление ядру, которое определяет, предоставить вызывающей программе запрашиваемую услугу. Если служба предоставлена, ядро выполняет определенный набор инструкций, по которым вызывающая программа не имеет прямого управления, возвращает уровень привилегий на уровень привилегии вызывающей программы, а затем возвращает управление вызывающей программе.

Промежуточная библиотека

Обычно, системы предоставляют библиотеку или API , которые находятся среди обычных программ и операционной системой. В Unix-подобных системах этот API обычно является частью реализации библиотеки С (libc), такой как glibc, которая обеспечивает функции –оболочки для системных вызовов, которые, в свою очередь, часто называются также, как и системные вызовы, которые они вызывают. В Windows NT этот API является частью Native API, в библиотеке ntdll.dll; Это недокументированный API, используемый реализациями обычного Windows API и непосредственно используется некоторыми системными программами в Windows. Функции-оболочки библиотеки предоставляют обычное соглашение о вызове функций (вызов подпрограммы на уровне сборки) для использования системного вызова, а также делают системный вызов более модульным. Здесь основной функцией-оболочки является помещение всех аргументов, которые должны быть переданы системному вызову в соответствующие регистры процессора (возможно, и в стек вызовов), а также установка уникального номера системного вызова для вызова ядра. Таким образом, библиотека, которая существует между ОС и приложением, увеличивает мобильность.

Вызов самой функции библиотеки не приводит к переключению в режим ядра (если исполнение уже не было в режиме ядра) и обычно является обычным вызовом подпрограммы. Фактический системный вызов передает управление ядру (и более зависит от конкретной реализации и платформы, чем библиотека вызова). Например, в Unix-подобных системах функции fork и execve являются функциями библиотеки С, которые, в свою очередь, выполняют инструкции, вызывающие системные вызовы fork и exec. Создание системного вызова непосредственно в коде приложения сложнее и, к тому же, может потребовать использования встроенного ассемблерного кода (на С и С++), а также знание низкоуровневого двоичного интерфейса для операции системного вызова, который может меняться с течением времени и, следовательно, не быть частью бинарного интерфейса приложения. В системах, основанных на exokernels, библиотека особенно важна как посредник. На exokernels библиотеки защищают пользовательские приложения от API ядра с очень низким уровнем и обеспечивают управление ресурсами.

Операционные системы IBM, происходящие от OS / 360 и DOS / 360, включая z / OS и z / VSE, реализуют системные вызовы через библиотеку макросов языка ассемблера. Это связано с их происхождение, поскольку в то время программирование на языке ассемблера было более распространено, чем использование языков высокого уровня. Таким образом, системные вызовы IBM не могут напрямую исполняться языковыми программами высокого уровня, но требуют подпрограмму вызываемую ассемблером.

Примеры и инструменты

В UNIX-подобных и других совместимых с POSIX операционных системах популярными системными вызовами являются open, read, write, close, wait, exec, fork, exit, and kill. Многие современные операционные системы имеют сотни системных вызовов. Например, в Linux и OpenBSD каждый из них имеет более 300 различных вызовов. У NetBSD около 500, у FreeBSD более 500, Microsoft Windows 7 имеет почти 700, пока Plan 9 имеет 51. [Источник 2]

Такие инструменты, как strace и truss , позволяют процессу запускаться с самого начала и сообщать обо всех системных вызовах, которые процесс вызывает, или могут присоединяться к уже запущенному процессу и перехватывать любой системный вызов, сделанный упомянутым процессом, если эта операция не нарушает права пользователя. Эта специальная способность программы, как правило, также реализуется с помощью системного вызова. Наприме, strace реализуется с помощью ptrace или системных вызовов файлов в procfs .

Типичные реализации

Реализация системных вызовов требует передачи управления из пользовательского пространства в пространство ядра, которая связана с некоторой особенностью архитектуры. Типичным способом реализации этого является использование программного прерывания или ловушки. Прерывания передает управление ядру операционной системы, так что программное обеспечение просто вынуждено создать некоторый регистр с номером системного вызова, и выполняет программное прерывание.

Это единственный метод, предусмотренный для многих RISC-процессоров, но архитектуры CISC, такие как x86, поддерживают дополнительные методы. Например, набор команд x86 содержит инструкции SYSCALL / SYSRET и SYSENTER / SYSEXIT (эти два механизма были независимо созданы AMD и Intel, соответственно, но по сути они делают то же самое). Это «быстрые» инструкции, которые предназначены для быстрой передачи управления ядру для системного вызова без накладных расходов прерывания. Linux 2.5 начал использовать это на x86; Ранее он использовал инструкцию INT, где номер системного вызова был помещен в регистр EAX до того, как было выполнено прерывание 0x80. Старые x86-механизмы – это некие ворота вызова. Он позволяет программе вызывать функцию ядра напрямую, используя безопасный механизм передачи управления, который операционная система настраивает заранее. Такой подход был непопулярен, по-видимому, из-за требования удаленного вызова (вызов процедуры, расположенной в другом сегменте, чем текущий сегмент кода), который использует сегментацию памяти x86 и, как следствие, отсутствие переносимости, которую он вызывает, и Существование более быстрых инструкций, упомянутых выше. [Источник 3]

Для архитектуры IA-64 используется инструкция EPC (ввод привилегированного кода). Первые восемь аргументов системного вызова передаются в регистрах, а остальные передаются в стек. В семействе мэйнфреймов IBM System / 360 команда Supervisor Call реализует системный вызов устаревших объектов; Инструкция Program Call (PC) используется для новых объектов. В частности, ПК используется, когда вызывающий абонент может находиться в режиме SRB.

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

Управление процессами
  • load
  • execute
  • end (exit), abort
  • создание процесса (fork в UNIX-подобных, NtCreateProcess в Windows NT Native API)
  • завершение процесса
  • get/set process attributes
  • wait время, события, signal события
  • allocate, free memory
Работа с файлами
  • create file, delete file
  • open, close
  • read, write, reposition
  • get/set file attributes
Управление устройствами
  • request device, release device
  • read, write, reposition
  • get/set device attributes
  • logically attach or detach devices
Работа с информацией
  • get/set time or date
  • get/set system data
  • get/set process, file, or device attributes
Связь, коммуникация
  • create, delete communication connection
  • send, receive messages
  • transfer status information
  • attach or detach remote devices

Режим процессора и переключение контекста

Системные вызовы в большинстве Unix-подобных систем обрабатываются в режиме ядра, что достигается путем изменения режима выполнения процессора на более привилегированный, но не требуется переключения контекста процесса, хотя переключение контекста имеет место . Аппаратные средства рассматривают мир с точки зрения режима выполнения в соответствии с регистром статуса процессора, а процессы являются абстракциями, предоставляемыми операционной системой. Системный вызов обычно не требует переключения контекста на другой процесс; Вместо этого он обрабатывается в контексте того процесса, который его вызывал. [Источник 4]

В многопоточном процессе системные вызовы могут выполняться из нескольких потоков. Обработка таких вызовов зависит от структуры ядра конкретной операционной системы и среды выполнения приложения. В следующем списке показаны типичные модели, за которыми следуют операционные системы:

Про системные вызовы уже много было сказано, например здесь или здесь. Наверняка вам уже известно, что системный вызов — это способ вызова функции ядра ОС. Мне же захотелось копнуть глубже и узнать, что особенного в этом системном вызове, какие существуют реализации и какова их производительность на примере архитектуры x86-64. Если вам также интересны ответы на данные вопросы, добро пожаловать под кат.

System call

Каждый раз, когда мы хотим что-то отобразить на мониторе, записать в устройство, считать с файла, нам приходится обращаться к ядру ОС. Именно ядро ОС отвечает за любое общение с железом, именно там происходит работа с прерываниями, режимами процессора, переключениями задач… Чтобы пользователь программой не смог завалить работу всей операционной системы, было решено разделить пространство памяти на пространство пользователя (область памяти, предназначенная для выполнения пользовательских программ) и пространство ядра, а также запретить пользователю доступ к памяти ядра ОС. Реализовано это разделение в x86-семействе аппаратно при помощи сегментной защиты памяти. Но пользовательской программе нужно каким-то образом общаться с ядром, для этого и была придумана концепция системных вызовов.

Системный вызов — способ обращения программы пользовательского пространства к пространству ядра. Со стороны это может выглядеть как вызов обычной функции со своим собственным calling convention, но на самом деле процессором выполняется чуть больше действий, чем при вызове функции инструкцией call. Например, в архитектуре x86 во время системного вызова как минимум происходит увеличение уровня привилегий, замена пользовательских сегментов на сегменты ядра и установка регистра IP на обработчик системного вызова.

Программист обычно не работает с системными вызовами напрямую, так как системные вызовы обернуты в функции и скрыты в различных библиотеках, например libc.so в Linux или же ntdll.dll в Windows, с которыми и взаимодействует прикладной разработчик.

Теоретически, реализовать системный вызов можно при помощи любого исключения, хоть при помощи деления на 0. Главное — это передача управления ядру. Рассмотрим реальные примеры реализаций исключений.

Способы реализации системных вызовов

Выполнение неверной инструкции.

Ранее, ещё на 80386 это был самый быстрый способ сделать системный вызов. Для этого обычно применялась бессмысленная и неверная инструкция LOCK NOP, после исполнения которой процессором вызывался обработчик неверной инструкции. Это было больше 20 лет назад и, говорят, этим приёмом обрабатывались системные вызовы в корпорации Microsoft. Обработчик неверной инструкции в наши дни используется по назначению.

Call gates

Для того, чтобы иметь доступ к сегментам кода с различным уровнем привилегий, в Intel был разработан специальный набор дескрипторов, называемый gate descriptors. Существует 4 вида таких дескрипторов:

  • Call gates
  • Trap gates (для исключений, вроде int 3, требующих выполнения участка кода)
  • Interrupt gates (аналогичен trap gates, но с некоторыми отличиями)
  • Task gates (полагалось, что будут использоваться для переключения задач)

Нам интересны только call gates, так как именно через них планировалось реализовывать системные вызовы в x86.

Call gate реализован при помощи инструкции call far или jmp far и принимает в качестве параметра call gate-дескриптор, который настраивается ядром ОС. Является достаточно гибким механизмом, так как возможен переход и на любой уровень защитного кольца, и на 16-битный код. Считается, что call gates производительней прерываний. Этот способ использовался в OS/2 и Windows 95. Из-за неудобства использования в Linux механизм так и не был реализован. Со временем совсем перестал использоваться, так как появились более производительные и простые в обращении реализации системных вызовов (sysenter/sysexit).

Системные вызовы, реализованные в Linux

В архитектуре x86-64 ОС Linux существует несколько различных способов системных вызовов:

  • int 80h
  • sysenter/sysexit
  • syscall/sysret
  • vsyscall
  • vDSO

В реализации каждого системного вызова есть свои особенности, но в общем, обработчик в Linux имеет примерно одинаковую структуру:

  • Включается защита от чтения/записи/исполнения кода пользовательского пространства.
  • Заменяется пользовательский стек на стек ядра, сохраняются callee-saved регистры.
  • Выполняется обработка системного вызова
  • Восстановление стека, регистров
  • Отключение защиты
  • Выход из системного вызова

Рассмотрим немного подробнее каждый системный вызов.

int 80h

Изначально, в архитектуре x86, Linux использовал программное прерывание 128 для совершения системного вызова. Для указания номера системного вызова, пользователь задаёт в eax номер системного вызова, а его параметры располагает по порядку в регистрах ebx, ecx, edx, esi, edi, ebp. Далее вызывается инструкция int 80h, которая программно вызывает прерывание. Процессором вызывается обработчик прерывания, установленный ядром Linux ещё во время инициализации ядра. В x86-64 вызов прерывания используется только во время эмуляции режима x32 для обратной совместимости.

В принципе, никто не запрещает пользоваться инструкцией в расширенном режиме. Но вы должны понимать, что используется 32-битная таблица вызовов и все используемые адреса должны помещаться в 32-битное адресное пространство. Согласно SYSTEM V ABI [4] §3.5.1, для программ, виртуальный адрес которых известен на этапе линковки и помещается в 2гб, по умолчанию используется малая модель памяти и все известные символы находятся в 32-битном адресном пространстве. Под это определение подходят статически скомпилированные программы, где и возможно использовать int 80h. Пошаговая работа прерывания подробно описана на stackoverflow.

В ядре обработчиком этого прерывания является функция entry_INT80_compat и находится в arch/x86/entry/entry_64_compat.S

Или в расширенном режиме (программа работает так как компилируется статически)

sysenter/sysexit

Спустя некоторое время, ещё когда не было x86-64, в Intel поняли, что можно ускорить системные вызовы, если создать специальную инструкцию системного вызова, тем самым минуя некоторые издержки прерывания. Так появилась пара инструкций sysenter/sysexit. Ускорение достигается за счёт того, что на аппаратном уровне при выполнении инструкции sysenter опускается множество проверок на валидность дескрипторов, а так же проверок, зависящих от уровня привилегий [3] §6.1. Также инструкция опирается на то, что вызывающая её программа использует плоскую модель памяти. В архитектуре Intel, инструкция валидна как для режима совместимости, так и для расширенного режима, но у AMD данная инструкция в расширенном режиме приводит к исключению неизвестного опкода [3]. Поэтому в настоящее время пара sysenter/sysexit используется только в режиме совместимости.

В ядре обработчиком этой инструкции является функция entry_SYSENTER_compat и находится в arch/x86/entry/entry_64_compat.S

Несмотря на то, что в реализации архитектуры от Intel инструкция валидна, в расширенном режиме скорее всего такой системный вызов никак не получится использовать. Это из-за того, что в регистре ebp сохраняется текущее значение стека, а адрес верхушки независимо от модели памяти находится вне 32-битного адресного пространства. Это всё потому, что Linux отображает стек на конец нижней половины каноничного адреса пространства.

Разработчики ядра Linux предостерегают пользователей от жесткого программирования sysenter из-за того, что ABI системного вызова может измениться. Из-за того, что Android не последовал этому совету, Linux пришлось откатить свой патч для сохранения обратной совместимости. Правильно реализовывать системный вызов нужно используя vDSO, речь о которой будет идти далее.

syscall/sysret

Так как именно AMD разработали x86-64 архитектуру, которая и называется AMD64, то они решили создать свой собственный системный вызов. Инструкция разрабатывалась AMD, как аналог sysenter/sysexit для архитектуры IA-32. В AMD позаботились о том, чтобы инструкция была реализована как в расширенном режиме, так и в режиме совместимости, но в Intel решили не поддерживать данную инструкцию в режиме совместимости. Несмотря на всё это, Linux имеет 2 обработчика для каждого из режимов: для x32 и x64. Обработчиками этой инструкции является функции entry_SYSCALL_64 для x64 и entry_SYSCALL_compat для x32 и находится в arch/x86/entry/entry_64.S и arch/x86/entry/entry_64_compat.S соответственно.

Кому интересно более подробно ознакомиться с инструкциями системных вызовов, в мануале Intel [0] (§4.3) приведён их псевдокод.

Для тестирования следующего примера потребуется ядро с конфигурацией CONFIG_IA32_EMULATION=y и компьютер AMD. Если же у вас компьютер фирмы Intel, то можно запустить пример на виртуалке. Linux может без предупреждения изменить ABI и этого системного вызова, поэтому в очередной раз напомню: системные вызовы в режиме совместимости правильнее исполнять через vDSO.

Непонятна причина, по которой AMD решили разработать свою инструкцию вместо того, чтобы расширить инструкцию Intel sysenter на архитектуру x86-64.

vsyscall

При переходе из пространства пользователя в пространство ядра происходит переключение контекста, что является не самой дешёвой операцией. Поэтому, для улучшения производительности системных вызовов, было решено их обрабатывать в пространстве пользователя. Для этого было зарезервировано 8 мб памяти для отображения пространства ядра в пространство пользователя. В эту память для архитектуры x86 поместили 3 реализации часто используемых read-only вызова: gettimeofday, time, getcpu.

Со временем стало понятно, что vsyscall имеет существенные недостатки. Фиксированное размещение в адресном пространстве является уязвимым местом с точки зрения безопасности, а отсутствие гибкости в размере выделяемой памяти может негативно сказаться на расширении отображаемой области ядра.

Для того, чтобы пример работал, необходимо, чтобы в ядре была включена поддержка vsyscall: CONFIG_X86_VSYSCALL_EMULATION=y

Linux не отображает vsyscall в режиме совместимости.

На данный момент, для сохранения обратной совместимости, ядро Linux предоставляет эмуляцию vsyscall. Эмуляция сделана для того, чтобы залатать дыры безопасности в ущерб производительности.

Эмуляция может быть реализована двумя способами.

Первый способ — при помощи замены адреса функции на системный вызов syscall. В таком случае виртуальный системный вызов функции gettimeofday на x86-64 выглядит следующим образом:

Где 0x60 — код системного вызова функции gettimeofday.

Второй же способ немного интереснее. При вызове функции vsyscall генерируется исключение Page fault, которое обрабатывается Linux. ОС видит, что ошибка произошла из-за исполнения инструкции по адресу vsyscall и передаёт управление обработчику виртуальных системных вызовов emulate_vsyscall (arch/x86/entry/vsyscall/vsyscall_64.c).

Реализацией vsyscall можно управлять при помощи параметра ядра vsyscall. Можно как отключить виртуальный системный вызов при помощи параметра vsyscall=none , задать реализацию как при помощи инструкции syscall syscall=native , так и через Page fault vsyscall=emulate .

vDSO (Virtual Dynamic Shared Object)

Также vDSO используется в качестве выбора наиболее производительного способа системного вызова, например в режиме совместимости.

Список разделяемых функций можно посмотреть в руководстве.

Для режима совместимости:

Правильнее всего искать функции vDSO при помощи извлечения адреса библиотеки из вспомогательного вектора AT_SYSINFO_EHDR и последующего парсинга разделяемого объекта. Пример парсинга vDSO из вспомогательного вектора можно найти в исходном коде ядра: tools/testing/selftests/vDSO/parse_vdso.c

Или если интересно, то можно покопаться и посмотреть, как парсится vDSO в glibc:

  1. Парсинг вспомогательных векторов: elf/dl-sysdep.c
  2. Парсинг разделяемой библиотеки: elf/setup-vdso.h
  3. Установка значений функций: sysdeps/unix/sysv/linux/x86_64/init-first.c, sysdeps/unix/sysv/linux/x86/gettimeofday.c, sysdeps/unix/sysv/linux/x86/time.c

Согласно System V ABI AMD64 [4] вызовы должны происходить при помощи инструкции syscall. На практике же к этой инструкции добавляются вызовы через vDSO. Поддержка системных вызовов в виде int 80h и vsyscall остались для обратной совместимости.

Сравнение производительности системных вызовов

С тестированием скорости системных вызовов всё неоднозначно. В архитектуре x86 на выполнение одной инструкции влияет множество факторов таких как наличие инструкции в кэше, загруженность конвейера, даже существует таблица задержек для данной архитектуры [2]. Поэтому достаточно сложно определить скорость выполнения участка кода. У Intel есть даже специальный гайд по замеру времени для участка кода [1]. Но проблема в том, что мы не можем замерить время согласно документу из-за того, что нам нужно вызывать объекты ядра из пользовательского пространства.

Поэтому было решено замерить время при помощи clock_gettime и тестировать производительность вызова gettimeofday, так как он есть во всех реализациях системных вызовов. На разных процессорах время может отличаться, но в целом, относительные результаты должны быть схожи.

Программа запускалась несколько раз и в итоге бралось минимальное время исполнения.
Тестирование int 80h, sysenter и vDSO-32 производилось в режиме совместимости.

О системе
cat /proc/cpuinfo | grep "model name" -m 1 — Intel® Core(TM) i7-5500U CPU @ 2.40GHz
uname -r — 4.14.13-1-ARCH

Таблица Результатов


Реализация время (нс)
int 80h 498
sysenter 338
syscall 278
vsyscall emulate 692
vsyscall native 278
vDSO 37
vDSO-32 51

Как можно увидеть, каждая новая реализация системного вызова является производительней предыдущей, не считая vsysvall, так как это эмуляция. Как вы наверное уже догадались, если бы vsyscall был таким, каким его задумывали, время вызова было бы аналогично vDSO.

Все текущие сравнения производительности были произведены с патчем KPTI, исправляющим уязвимость meltdown.

Бонус: Производительность системных вызовов без KPTI

Патч KPTI был разработан специально для исправления уязвимости meltdown. Как известно, данный патч замедляет производительность ОС. Проверим производительность с выключенным KPTI (pti=off).

Таблица результатов с выключенным патчем


Реализация Время (нс) Увеличение времени исполнения после патча (нс) Ухудшение производительности после патча (t1 - t0) / t0 * 100%
int 80h 317 181 57%
sysenter 150 188 125%
syscall 103 175 170%
vsyscall emulate 496 196 40%
vsyscall native 103 175 170%
vDSO 37 0 0%
vDSO-32 51 0 0%

Переход в режим ядра и обратно в среднем после патча стал занимать примерно на 180 нс. больше времени, видимо это и есть цена сброса TLB-кэша.

Производительность системного вызова через vDSO не ухудшилась по причине того, то в данном типе вызова нет перехода в режим ядра, и, следовательно, нет причин сбрасывать TLB-кэш.

Системные вызовы в операционных системах семейства MS Windows, начиная с версии Windows 95, реализованы на основе интерфейса прикладного программирования, получившего название «Win32 API». Под Win32 API понимают совокупность функций, предоставляющих программисту возможность создавать приложения для операционных систем Windows 95/98/ME/NT/2000/XP, базирующихся на использовании 32-х разрядных процессоров Intel, начиная с i386 (и его аналогов). При этом, несмотря на различия между версиями операционной системы, основное множество функций API для них одно и то же. Большинство функций API доступны для вызова из программ на любом исходном языке программирования (в том числе и на ассемблере).

Функции API хранятся в так называемых динамических библиотеках (Dynamic Link Library), которые размещаются в файлах с расширением dll, таких как kernel.dll, user32.dll, gdi32.dll и некоторых других. Эти файлы размещаются в системном каталоге Windows (обычно C:\WINDOWS\SYSTEM).

Фактически функции API для Windows играют ту же самую роль, что и программные прерывания для MS DOS, однако вызов функций API производится более простым и привычным для программиста способом - через символические имена. Например, функция удаления файла вызывается по имени DeleteFile, функция установки системного времени - SetSystemTime и т.д.

При программировании на ассемблере передача параметров функциям Win32 осуществляется не через регистры процессора, а через стек. Результат работы функции API помещается в регистр EAX. Более сложные типы данных возвращаются через адреса памяти (указатели), передаваемые функции в виде входных параметров.

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

Подробное описание функций WIN32 API и их параметров можно найти в литературе 4, в файле справки win32.hlp (обычно размешается в каталоге C:\Program Files\Borland Shared\MSHelp\win32.hlp), а также на многочисленных сайтах Интернет (например, /library).

3.2. Типы данных, применяемые в Win32 API

Помимо совокупности функций API Windows поддерживает целый ряд специальных типов данных (например, HINSTANCE, HWND, LPSTR и т.п.), не совпадающих со стандартными типами, определенными в основных языках программирования. Использование типов, специально «изобретенных» для Windows, упрощает написание программы, делает ее более ясной и читабельной. Некоторые простейшие типы данных Windows приведены в таблице 2.

Таблица 2. Основные типы данных Windows

Логическая переменная, принимающая значения TRUE (ИСТИНА) или FALSE (ЛОЖЬ).

Байтовое число без знака

32-разрядное целое число без знака.

32-разрядное целое число со знаком.

32-разрядное целое число без знака.

Дальний указатель на строку символов с завершающим нулевым символом

16-разрядное целое число без знака.

Дескриптор объекта (четырехбайтовое целое число)

Особую роль в Windows играют специальные переменные - дескрипторы (хэндлы). Дескрипторы – это уникальные целые четырехбайтовые числа, применяемые для идентификации объектов, которые создаются и используются в системе. Большинство дескрипторов являются значениями индексов внутренних таблиц, которые Windows использует для доступа и управления своими объектами. Прикладная программа может получить или изменить данные, связанные с каким-либо объектом, только с помощью вызова функции API с указанием дескриптора соответствующего объекта. Для каждого вида объектов используется специальный дескрипторный тип, например – HWND – дескриптор окна, HDC - дескриптор контекста устройства, HFILE – дескриптор открытого файла, HLOCAL - дескриптор локального блока памяти и т.д. Общим для всех дескрипторов является наличие в описании первого символа “H”.

typedef struct _SYSTEMTIME WORD wYear; //текущий год
WORD wMonth; //номер месяца (январь-1, и т.д.)
WORD wDayOfWeek; //день недели (вск-0, пн-1, …)
WORD wDay; //день месяца
WORD wHour; //час
WORD wMinute; //минуты
WORD wSecond; //секунды
WORD wMilliseconds; //миллисекунды

При написании программ на языке C/C++ типы данных Windows и прототипы функций API определяются во включаемых заголовочных файлах Win32, основным из которых является файл windows.h. Помимо типов данных в этом файле определено более 1000 констант. Имена констант стандартизированы: они пишутся заглавными буквами и имеют вид «префикс_пояснение». Например, IDC_RESOURCE,CS_HREDRAW, WM_QUIT, DRIVE_UNKNOWN и т.п. Константы также широко применяются при установке значений параметров вызова функций API и при проверке результатов их выполнения.

Одной из особенностей программ, написанных для Windows, является использование так называемой «венгерской нотации» при записи имен переменных. Суть этой системы можно определить следующими правилами:

каждое слово в имени переменной пишется с прописной буквы и слитно с другими словами. Например, идентификатор для обозначения какой-то переменной может выглядеть следующим образом - MyVariable, YourVariable, VariableForSavingAnotherVariable и т.п.;

каждый идентификатор предваряется несколькими строчными символами, определяющими его тип. Например, целочисленная переменная MyVariable будет выглядеть как nMyVariable (n – общепринятый префикс для целочисленных переменных), символьная (char) переменная YourVariable превращается в cYourVariable. Указатель на строку символов, заканчивающуюся нулевым байтом, следут записать lpszVariableForSavingAnotlierVariable (lpsz - сокращение от Long Point то String with Zero). Как видим, префикс указателя может комбинироваться с другими префиксами. Примеры подобных префиксов приведены в таблице 3.

В любой операционной системе существует набор базовых концепций и базовых механизмов, ставших неотъемлемой частью теории и практики ОС. Например, в "Создание ОС Windows. Структура ОС Windows" были приведены краткие описания процессов и потоков. Ниже будет подробно рассмотрена реализация ряда других важных концепций современных ОС.

Из теории ОС известно [ Карпов ] , [ Таненбаум ] , [ Столлингс ] , что современные ОС реализуют поддержку системных вызовов, обработку прерываний и исключительных ситуаций, которые относят к основным механизмам ОС.

Системные вызовы (system calls) - механизм, позволяющий пользовательским программам обращаться к услугам ядра ОС, то есть это интерфейс между операционной системой и пользовательской программой. Концептуально системный вызов похож на обычный вызов подпрограммы. Основное отличие состоит в том, что при системном вызове выполнение программы осуществляется в привилегированном режиме или режиме ядра. Поэтому системные вызовы иногда еще называют программными прерываниями, в отличие от аппаратных прерываний, которые чаще называют просто прерываниями. В большинстве операционных систем системный вызов является результатом выполнения команды программного прерывания ( INT ). Таким образом, системный вызов - это синхронное событие.

Прерывание (hardware interrupt) - это событие, генерируемое внешним (по отношению к процессору) устройством. Посредством аппаратных прерываний аппаратура либо информирует центральный процессор о том, что произошло событие, требующее немедленной реакции (например, пользователь нажал клавишу), либо сообщает о завершении операции ввода вывода (например, закончено чтение данных с диска в основную память ). Каждый тип аппаратных прерываний имеет собственный номер, однозначно определяющий источник прерывания. Аппаратное прерывание - это асинхронное событие, то есть оно возникает вне зависимости от того, какой код исполняется процессором в данный момент. Обработка аппаратного прерывания не должна учитывать, какой процесс или поток является текущим.

Исключительная ситуация (exception) - событие, возникающее в результате попытки выполнения программой команды, которая по каким-то причинам не может быть выполнена до конца. Примерами таких команд могут быть попытки доступа к ресурсу при отсутствии достаточных привилегий или обращение к отсутствующей странице памяти. Исключительные ситуации, как и системные вызовы, являются синхронными событиями, возникающими в контексте текущей задачи. Исключительные ситуации можно разделить на исправимые и неисправимые. К исправимым относятся такие исключительные ситуации, как отсутствие нужной информации в оперативной памяти. После устранения причины исправимой исключительной ситуации программа может выполняться дальше. Возникновение в процессе работы операционной системы исправимых исключительных ситуаций считается нормальным явлением. Неисправимые исключительные ситуации чаще всего возникают в результате ошибок в программах (например, деление на ноль). Обычно в таких случаях операционная система реагирует завершением программы, вызвавшей исключительную ситуацию.

Прогон программы реализующей структурную обработку исключений

В качестве упражнения рекомендуется выполнить прогон программы, в которой произведена обработка деления на 0. Особенности применения операторов try и except описаны в MSDN.

Реализация прерываний, системных вызовов и исключений в ОС Windows

Рассмотрим реализацию основных механизмов операционной системы в ОС Windows . Следует отметить, что терминология корпорации Microsoft несколько отличается от общепринятой. Например, системные вызовы называются системными сервисами, а под программным прерыванием (см. прерывания DPC и APC ) понимается выполнение специфичных функций ядра, требующих прерывания работы текущего процесса.

Ловушки

Общим для реализации рассматриваемых основных механизмов является необходимость сохранения состояния текущего потока с его последующим восстановлением. Для этого в ОС Windows используется механизм ловушек (trap). В случае возникновения требующего обработки события (прерывания, исключения или вызова системного сервиса ) процессор переходит в привилегированный режим и передает управление обработчику ловушек, входящему в состав ядра. Обработчик ловушек создает в стеке ядра (о стеке ядра см. "Реализация процессов и потоков" ) прерываемого потока фрейм ловушки, содержащий часть контекста потока для последующего восстановления его состояния, и в свою очередь передает управление определенной части ОС, отвечающей за первичную обработку произошедшего события.

В типичном случае сохраняются и впоследствии восстанавливаются:

  • программный счетчик;
  • регистр состояния процессора;
  • содержимое остальных регистров процессора;
  • указатели на стек ядра и пользовательский стек;
  • указатели на адресное пространство, в котором выполняется поток (каталог таблиц страниц процесса).

Эта информация специфицирована в структуре CONTEXT (файл winnt.h), и может быть получена пользователем с помощью функции GetThreadContext .

Адрес части ядра ОС, ответственной за обработку данного конкретного события определяется из вектора прерываний, который номеру события ставит в соответствие адрес процедуры его первичной обработки. Это оказывается возможным, поскольку все события типизированы и их число ограничено. Для асинхронных событий их номер определяется контроллером прерываний , а для синхронных - ядром. В [ Руссинович ] описана процедура просмотра вектора прерываний, который в терминологии корпорации Microsoft называется таблицей диспетчеризации прерываний ( interrupt dispatch table, IDT ), при помощи отладчика kd. Например, для x86 процессора прерыванию от клавиатуры соответствует номер 0x52 , системным сервисам - 0x2e , а исключительной ситуации, связанной со страничной ошибкой, - 0xE (см. рис. 3.1рс. 3.1).

Вектор прерываний (IDT)

После прохождения первичной обработки для каждого события предусмотрена процедура его последующей обработки другими частями ОС. Например, обработка системного сервиса (системного вызова) предполагает передачу управления по адресу 0x2e , где располагается диспетчер системных сервисов, которому через регистры EAX и EBX передаются номер запрошенного сервиса и список параметров, передаваемых этому системному сервису.

То же самое происходит в случае возникновения исключений и прерываний. Простые исключения могут быть обработаны диспетчером ловушек, а более сложные обрабатываются диспетчером исключений, который может в случае возникновения исключения вернуть управление вызвавшему это исключение приложению. Это делается с помощью упомянутого выше аппарата структурной обработки исключений. Вторичная обработка прерывания обеспечивается драйверами соответствующих устройств.

В качестве примера рассмотрим процедуру обработки создания файла. Вызов Win32 функции CreateFile() генерирует передачу управления функции NtCreateFile исполнительной системы, ассемблерный код которой содержит следующие операции:

Рисунок 3.2 иллюстрирует дальнейшую обработку данного сервиса.

Пример обработки системного вызова (системного сервиса).


Рис. 3.2. Пример обработки системного вызова (системного сервиса).

Приоритеты. IRQL

В большинстве операционных систем аппаратные прерывания имеют приоритеты, которые определяются контроллерами прерываний. Однако ОС Windows имеет свою аппаратно-независимую шкалу приоритетов, которые называются уровни запросов прерываний ( interrupt request levels, IRQL), и охватывает не только прерывания, а все события, требующие системной обработки. В таблице 3.1 приведены значения IRQL уровней для x86 систем.

Обрабатываемые события обслуживаются в порядке их приоритета, и события с более высоким приоритетом вытесняют обработку событий с меньшим приоритетом. При возникновении события с высоким приоритетом IRQL процессора повышается до уровня данного события. После его обработки могут проявить себя замаскированные менее приоритетные события, которые, в свою очередь, могут быть обработаны по обычной схеме. Текущий уровень приоритета хранится в данных, описывающих состояние процессора, и может быть определен системным отладчиком kd или посредством вызова функции KeGetCurrentIrql .

Значения IRQL для аппаратных прерываний расставляются диспетчером Plug and Play с помощью уровня абстрагирования от оборудования HAL , а для остальных событий - ядром. Таким образом, уровень IRQL определяется источником события , что имеет иной смысл, нежели приоритеты в стратегии планирования потоков. Разбиение на IRQL уровни является основным механизмом упорядочивания по приоритетам действий операционной системы.

Можно сказать, что в ОС Windows действует двухуровневая схема планирования. Приоритеты высшего уровня (в данном случае IRQLs) определяются аппаратными или программными прерываниями , а приоритеты низшего уровня (в своем диапазоне от 0 до 31) устанавливаются для пользовательских потоков, выполняемых на нулевом уровне IRQL, и контролируются планировщиком.

На нулевом ( PASSIVE LEVEL) уровне IRQL работают пользовательские процессы и часть кода операционной системы. Программа, работающая на этом уровне, может быть вытеснена почти любым событием, случившимся в системе. Большинство процедур режима ядра старается удерживать IRQL уровень процессора как можно более низким.

IRQL уровни 1 ( APC LEVEL) и 2 (DISPATCH LEVEL) предназначены для так называемых программных (в терминологии Microsoft) прерываний соответственно: асинхронный вызов процедуры - APC (asynchronous procedure call) и отложенный вызов процедуры - DPC (deferred procedure call). Если ядро принимает решение выполнить некоторую системную процедуру, но нет необходимости делать это немедленно, оно ставит ее в очередь DPC и генерирует DPC прерывание. Когда IRQL процессора станет достаточно низким, эта процедура выполняется. Характерный пример - отложенная операция планирования. Из этого следует, что код, выполняемый на IRQL уровне, выше или равном 2, не подвержен операции планирования. Асинхронный вызов процедур - механизм, аналогичный механизму DPC , но более общего назначения, в частности, доступный пользовательским процессам.

IRQL уровни 3-26 относятся к обычным прерываниям от устройств. Более подробное описание IRQL уровней имеется в [ Руссинович ] .

Заключение

В настоящей лекции описаны прерывания, системные вызовы и исключительные ситуации, которые являются фундаментальными механизмами операционных систем, и проанализированы особенности их реализации в ОС Windows . Обработка всех типов событий осуществляется единым образом и связана с сохранением/восстановлением состояния и эффективным поиском программы обработчика по системным таблицам. Важную роль для правильной организации имеет иерархия событий, реализованная в виде набора IRQL приоритетов.

Читайте также: