Исполнение кода внутри ядра

Если я правильно понимаю, для языка Си библиотеки типа "Windows.h" содержат функции ядра (различные systemcalls). А как написать код, который будет работать внутри ядра, т.е. иметь доступ к памяти сторонних процессов, без systemcall. И, соответственно, чтобы программа работала быстрее, чем через вызовы ядра. Как пишут драйверы? Скорее всего, тоже через специальные либы. Но ведь они когда-то тоже были написаны стандартным Си, они могли иметь доступ только к "Windows.h"?


Ответы (1 шт):

Автор решения: user22405329

При написании драйверов, <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, приоритет, использованное время процессора) можно просто записать на отдельную страницу памяти, и разрешить к ней доступ для чтения.
→ Ссылка