Исполнение кода внутри ядра
Если я правильно понимаю, для языка Си библиотеки типа "Windows.h" содержат функции ядра (различные systemcalls). А как написать код, который будет работать внутри ядра, т.е. иметь доступ к памяти сторонних процессов, без systemcall. И, соответственно, чтобы программа работала быстрее, чем через вызовы ядра. Как пишут драйверы? Скорее всего, тоже через специальные либы. Но ведь они когда-то тоже были написаны стандартным Си, они могли иметь доступ только к "Windows.h"?
Ответы (1 шт):
При написании драйверов, <Windows.h> не используется вообще. Вместо него используется <ntddk.h>. Он содержит список функций ядра. Некоторые из них похожи на функции из <Windows.h>, другие - уникальны для драйверов.
Но ведь они когда-то тоже были написаны стандартным Си, они могли иметь доступ только к "Windows.h"?
<Windows.h> - это не часть языка Си. Это сторонняя библиотека, написанная программистами Майкрософт.
<Windows.h> не содержит функций - только их заголовки. Этого достаточно, чтобы компилятор мог сгенерировать код вызова функции, но их содержимое находится в DLL-файлах (kernel32.dll
, ntdll.dll
, user32.dll
, и т.д.). Причём эти DLL загружаются при запуске EXE-файла, а не при его компиляции! Разные версии Windows содержат разный код в этих функциях - программы должны загружать код из текущей версии, а не приносить свой.
Компилятор не знает, где находятся эти функции. Он просто оставляет пустое место для адреса и передаёт код компоновщику.
Компоновщик пытается найти эти функции в других библиотеках - поэтому ему надо передать особые файлы: kernel32.lib
, ntdll.lib
, и т.д., иначе будет ошибка. (Если вы используете Visual Studio - вы можете увидеть список этих файлов в параметрах проекта) Из этих файлов, компоновщик узнаёт, что функции на самом деле находятся в соответствующих DLL. Он добавляет имена функций и имена DLL в таблицу импорта EXE-файла.
При запуске EXE-файла, Windows читает таблицу импорта, загружает DLL, и проставяет адреса функций в пустые места, оставленные компилятором.
Компиляция драйвера проходит похоже:
- Используется файл <ntddk.h>. Он также содержит только заголовки функций, но функции другие.
- Компилятор так же генерирует код вызова функций, оставляя пустое место для адреса.
- Но компоновщику передаются другие параметры: подаётся файл
ntoskrnl.lib
, который содержит список функций вntoskrnl.exe
- главном файле ядра Windows. Использовать особые "ядерные" DLL (и подавать соответствующие им LIB-файлы) тоже допускается. Функции добавляются в таблицу импорта. - При загрузке драйвера, Windows прописывает реальные адреса функций из
ntoskrnl.exe
в пустые места. Таким образом, код драйвера может вызывать функции ядра напрямую.
В процессе есть некоторые нюансы, которые я опустил - но общий принцип таков.
UPD:
А что же тогда позволяет получить доступ к общем адресному пространству?
То, что код исполняется в режиме ядра (kernel mode).
Разделение на user mode и kernel mode заложено в процессор на "железном" уровне. Процессор всегда помнит, в каком режиме выполняется текущий код. Процессор откажется выполнять некоторые инструкции или обращаться к некоторым адресам в user mode. Одна из инструкций, которая разрешена только в kernel mode - это инструкция замены адресного пространства.
Стоит отметить, что "общего адресного пространства" как такового, как правило, не существует. В 32-битной системе оно вообще невозможно, так как память может быть больше чем адресное пространство. В 64-битной системе такое пространство создать можно - но адреса будут другие. Чтобы обратиться к памяти другого процесса, придётся временно переключиться на его адресное пространство.
здесь также возникает вопрос, совпадает ли адрес ячейки памяти в user mode и адрес этой же ячейки в kernel mode, или в user mode происходит замещение адресов?
Замещение адресов происходит всегда. В момент переключения, адреса те же, что и в user mode - но некоторые ранее недоступные адреса становятся доступны. Однако, код в kernel mode может в любой момент поменять адресное пространство, или создать новое.
Получается, функции из "kernel32.dll, ntdll.dll, user32.dll, и т.д." могут вызывать косвенно функции ядра?
Да, но для этого требуется переключение в режим ядра с помощью специальной инструкции. У процессоров x86 есть несколько таких инструкций:
INT
- программное прерывание. Довольно медленная инструкция, которая позволяет вызвать одну из 256 функций из заранее подготовленной системой таблицы. Инструкция делает несколько обращений к памяти, что замедляет её работу.SYSCALL
- системный вызов. Работает в 64-битном режиме (а на процессорах AMD - ещё и в 32-битном). В отличие отINT
, к памяти не обращается вообще - все операции происходят в регистрах процессора. Всегда вызывает одну и ту же функцию, чей адрес процессор должен знать заранее.SYSENTER
- 32-битный аналогSYSCALL
для процессоров Intel. Особенность - не сохраняет адрес возврата, что делает её весьма неудобной в использовании.
Скорость достигается исключительно за счёт меньшего числа посреднических функций? Или за счёт отсутствия проверок со стороны ОС в этих промежуточных функциях?
И то, и другое, а также за счёт отсутствия переключения режимов (хотя с приходом SYSCALL
это стало меньшей проблемой). Ещё при переходе в kernel mode нужно обязательно менять стек - стеку из user mode доверять нельзя.
Стоит отметить, что многие задачи можно выполнить и без перехода в ядро:
- Обмениваться данными с другим процессом можно через общую память.
- Работать с устройствами можно, отобразив их адреса в адресное пространство процесса
- Некоторые данные системы (дата и время, доступная память, загрузка процессора) или текущего процесса (PID, приоритет, использованное время процессора) можно просто записать на отдельную страницу памяти, и разрешить к ней доступ для чтения.