Системное программирование

Тема 1. Прикладной программный интерфейс

Системное программирование

План лекции

Основные вопросы

  • Архитектурные уровни вычислительной системы
  • Аппаратная изоляция и кольца защиты ЦПУ
  • Прерывания и механизм системного вызова
  • Нативный API: Linux против Windows
  • Стратегии обработки системных ошибок
  • Инструментарий системного программиста

Цели лекции

  • Освоить архитектуру взаимодействия приложений с ядром ОС
  • Изучить аппаратный механизм перехода User/Kernel Mode
  • Сравнить подходы POSIX (syscalls) и Windows (WinAPI)
  • Научиться диагностировать системные ошибки
  • Овладеть средствами отладки и трассировки (GDB, strace)
Прикладной программный интерфейс
Системное программирование

Архитектурные уровни вычислительной системы

Иерархия абстракций

  • Прикладной уровень: пользовательские процессы и утилиты
  • Библиотечный уровень: стандарты Си (glibc), подсистемы WinAPI
  • Интерфейс ядра (SCI): граница привилегий процессора
  • Ядро ОС: память, планировщик, драйверы, VFS
  • Аппаратный уровень: ЦПУ, MMU, шины, память

Разделение ответственности

  • Прикладной код изолирован от аппаратных ресурсов
  • Ядро монопольно управляет физическими устройствами
  • Переход между слоями строго регламентирован аппаратным интерфейсом
Прикладной программный интерфейс
Системное программирование

Архитектура вычислительной системы

center

Прикладной программный интерфейс
Системное программирование

Понятие прикладного программного интерфейса (API)

Контракт API

  • Задокументированный интерфейс вызова функций
  • Спецификация типов данных, структур и констант
  • Соглашения о возвращаемых значениях и ошибках
  • Обеспечивает переносимость исходного кода (Source Compatibility)

Спецификация ABI

  • Бинарный интерфейс целевой платформы (Application Binary Interface)
  • Порядок передачи параметров через регистры процессора
  • Выравнивание структур данных в памяти
  • Обеспечивает совместимость скомпилированных модулей
Прикладной программный интерфейс
Системное программирование

Аппаратные кольца защиты процессора

Уровни привилегий x86/x64

  • Ring 0 (Supervisor): максимальные привилегии, прямой доступ к памяти, портам I/O и регистрам
  • Ring 1 и Ring 2: промежуточные уровни (в современных ОС не используются)
  • Ring 3 (User Mode): минимальные привилегии, изолированное исполнение прикладного кода

Контроль привилегий (CPL)

  • Биты CPL в селекторе сегмента кода CS
  • CPL = 00b — режим ядра (Kernel Mode)
  • CPL = 11b — пользовательский режим (User Mode)
  • Попытка запрещенной операции вызывает отказ General Protection (#GP)
Прикладной программный интерфейс
Системное программирование

Привилегированные инструкции и защита памяти

Привилегированные команды

  • cli / sti — управление прерываниями
  • hlt — остановка процессора до прерывания
  • mov cr3, reg — смена каталога страниц памяти
  • wrmsr / rdmsr — запись и чтение MSR-регистров
  • lgdt / lidt — загрузка таблиц дескрипторов

Аппаратная защита MMU

  • Бит U/S (User/Supervisor) в таблицах страниц
    • Запрет чтения/записи памяти ядра из Ring 3
  • Бит R/W — контроль модификации страниц
  • Бит NX/XD (No-Execute)
    • Запрет исполнения кода из стека и кучи
  • Нарушение прав вызывает отказ страницы (#PF)
Прикладной программный интерфейс
Системное программирование

Механизм прерываний в вычислительных системах

Аппаратные прерывания и исключения

  • Аппаратные (Hardware): асинхронные сигналы от контроллеров (таймер, диск, сеть)
  • Исключения (Exceptions): синхронные события выполнения команд процессора
  • Типы исключений: Faults (сбои, #PF), Traps (ловушки, int 3), Aborts (аварии)

Программные прерывания

  • Инструкция процессора int N
  • Вызывается прикладным кодом намеренно
  • Векторы 0x80 (Linux) и 0x2e (Windows)
  • Исторический шлюз перехода в режим ядра
Прикладной программный интерфейс
Системное программирование

Таблица дескрипторов прерываний (IDT)

Структура IDT

  • Массив из 256 шлюзовых дескрипторов
  • Адрес базовой таблицы хранится в регистре IDTR
  • Загрузка адреса выполняется инструкцией lidt

Типы дескрипторов

  • Interrupt Gate (Шлюз прерывания):
    • Автоматически сбрасывает флаг IF (запрет прерываний)
  • Trap Gate (Шлюз ловушки):
    • Сохраняет текущее значение флага IF
  • Поле DPL (Descriptor Privilege Level):
    • Задает минимальный уровень привилегий для вызова
Прикладной программный интерфейс
Системное программирование

Эволюция системных вызовов: int 0x80 против syscall

Устаревший механизм (int 0x80)

  • Чтение дескриптора из IDT в оперативной памяти
  • Проверка прав доступа DPL и CPL
  • Сохранение SS, ESP, EFLAGS, CS, EIP в стек ядра
  • Загрузка стека ядра из сегмента TSS
  • Накладные расходы: 100–150 тактов ЦПУ

Быстрый вызов (syscall / sysret)

  • Прямой переход без обращения к IDT и памяти
  • Адрес входа считывается из MSR IA32_LSTAR
  • Сохранение RIP в RCX, флагов RFLAGS в R11
  • Маскирование флагов через MSR IA32_FMASK
  • Накладные расходы: 10–20 тактов ЦПУ
Прикладной программный интерфейс
Системное программирование

Переход между User Mode и Kernel Mode

center

Прикладной программный интерфейс
Системное программирование

Системные вызовы в Linux (System V AMD64 ABI)

Соглашение о передаче параметров

  • Номер системного вызова: регистр RAX
  • Аргумент 1: RDI
  • Аргумент 2: RSI
  • Аргумент 3: RDX
  • Аргумент 4: R10 (вместо RCX)
  • Аргумент 5: R8
  • Аргумент 6: R9
  • Результат: регистр RAX

Особенности ABI Linux

  • Стабильные номера системных вызовов
  • Гарантия обратной бинарной совместимости
  • Тонкие обертки в библиотеке GNU C Library (glibc)
  • Прямой доступ через системную функцию syscall(SYS_nr, ...)
Прикладной программный интерфейс
Системное программирование

Прямой системный вызов на ассемблере NASM

; Пример прямого вызова write(1, msg, len) и exit(0) под Linux x86-64
global _start

section .data
    msg db "Hello, System Programming!", 0x0A
    len equ $ - msg

section .text
_start:
    mov rax, 1          ; номер sys_write
    mov rdi, 1          ; дескриптор stdout
    mov rsi, msg        ; адрес буфера
    mov rdx, len        ; длина строки
    syscall             ; вызов ядра

    mov rax, 60         ; номер sys_exit
    xor rdi, rdi        ; код возврата 0
    syscall             ; завершение процесса
Прикладной программный интерфейс
Системное программирование

Архитектура Windows: подсистемы и Native API

Уровни подсистемы Win32

  • kernel32.dll / KernelBase.dll
    • Процессы, память, файлы, синхронизация
  • user32.dll — окна, диалоги, сообщения
  • gdi32.dll — графические примитивы
  • advapi32.dll — безопасность, службы, реестр

Уровень Native API (ntdll.dll)

  • Недокументированный слой ядра Windows
  • Функции NtCreateFile, NtAllocateVirtualMemory
  • Выполняют инструкцию syscall
  • Номера системных вызовов меняются между сборками ОС
Прикладной программный интерфейс
Системное программирование

Сравнение стеков вызовов: Linux против Windows

center

Прикладной программный интерфейс
Системное программирование

Концепции WinAPI: дескрипторы HANDLE и Unicode

Дескрипторы HANDLE

  • Непрозрачный идентификатор объекта ядра
  • Индекс во внутренней таблице дескрипторов процесса
  • Управляет файлами, потоками, процессами, мьютексами
  • Обязательное освобождение: CloseHandle(h)
  • Утечка дескриптора удерживает объект в ядре

Поддержка кодировок (A vs W)

  • Суффикс A (ANSI): однобайтовые строки char*
    • Зависимость от локали и кодовой страницы
  • Суффикс W (Wide): двухбайтовые строки UTF-16LE
    • Тип wchar_t*, нативный формат ядра
  • Функции A перекодируют строки и вызывают функции W
Прикладной программный интерфейс
Системное программирование

Обработка системных ошибок в Linux: переменная errno

Механизм фиксации ошибки

  • Ядро возвращает отрицательное число [-4095..-1]
  • Обертка libc инвертирует знак и пишет в errno
  • Функция API возвращает -1 или NULL
  • Коды POSIX: ENOENT (2), EACCES (13), EBUSY (16)

Безопасность в многопоточности

  • errno — потоко-локальная переменная (TLS)
  • Раскрывается через *__errno_location()
  • Текстовое описание: strerror(errno)
  • Потокобезопасная версия: strerror_r()
  • Вывод в stderr: функция perror("описание")
Прикладной программный интерфейс
Системное программирование

Обработка системных ошибок в Windows: GetLastError

Коды NTSTATUS и Win32 Error

  • Ядро возвращает статус NTSTATUS (0xC0000034)
  • KernelBase транслирует статус через RtlNtStatusToDosError
  • Код сохраняется в структуре TEB текущего потока
  • Функция возвращает FALSE или INVALID_HANDLE_VALUE

Чтение и форматирование

  • Чтение кода: DWORD err = GetLastError();
  • Получение текста: системная функция FormatMessageW
  • Флаг FORMAT_MESSAGE_ALLOCATE_BUFFER
  • Освобождение буфера: LocalFree(msgBuffer)
Прикладной программный интерфейс
Системное программирование

Поток обработки ошибок системных вызовов

center

Прикладной программный интерфейс
Системное программирование

Сборочные цепочки и компиляторы

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

  • GCC (GNU Compiler Collection):
    • Поддержка ассемблерных вставок и атрибутов ELF
  • Clang / LLVM:
    • Санитайзеры памяти (ASan, TSan, MSan)
  • MSVC (Microsoft Visual C++):
    • Встроенные интринсики процессора, разработка под Windows

Рекомендуемые флаги (GCC/Clang)

  • -Wall -Wextra -Wpedantic — строгая диагностика
  • -O2 — оптимизация без искажения обращений к памяти
  • -fno-omit-frame-pointer — надежная трассировка стека
  • -g3 — подробные отладочные символы DWARF
Прикладной программный интерфейс
Системное программирование

Системные отладчики: GDB и WinDbg

GDB (GNU Debugger)

  • Отладка на уровне ассемблера: layout asm, layout regs
  • Анализ дампов памяти: gdb ./app core.dump
  • Просмотр содержимого памяти: x/16xg $rsp
  • Точки останова по системным вызовам: catch syscall open

WinDbg (Windows Debugger)

  • Сервер отладочных символов Microsoft
  • Анализ структур ядра: !peb, !teb, !gle
  • Дизассемблирование инструкций: u rip L10
  • Исследование дампов падения ядра и приложений
Прикладной программный интерфейс
Системное программирование

Трассировка системных вызовов: strace и ProcMon

Утилита strace в Linux

  • Перехват вызовов через механизм ptrace
  • Просмотр аргументов, строк и кодов возврата:
    strace -e trace=openat,read,write ./app
  • Измерение накладных расходов: strace -c ./app
  • Подключение к работающему процессу: strace -p <PID>

Process Monitor (ProcMon)

  • Драйвер-фильтр ядра Windows (Sysinternals)
  • Мониторинг файловой системы, реестра, потоков
  • Фильтрация по процессам, путям и статусам
  • Детальный стек вызовов для каждого события
Прикладной программный интерфейс
Системное программирование

Резюме лекции

Теоретические основы

  • Уровни абстракции: прикладной софт изолирован от аппаратуры через барьер ядра ОС.
  • Кольца защиты процессора: User Mode (Ring 3) и Kernel Mode (Ring 0) контролируются битами CPL и таблицами MMU.
  • Эволюция вызова: переход от медленного прерывания int 0x80 к аппаратным инструкциям syscall/sysret через MSR IA32_LSTAR.

Архитектура и практика

  • Linux: стабильный контракт прямых системных вызовов (System V ABI), фиксация ошибок в TLS через errno.
  • Windows: подсистема WinAPI скрывает Native API (ntdll), ресурсы адресуются через HANDLE, ошибки опрашиваются функцией GetLastError().
  • Диагностика: трассировщики strace и ProcMon позволяют анализировать системные вызовы без исходного кода.
Прикладной программный интерфейс
Системное программирование

Вопросы для самопроверки

Вопросы 1–4

  1. В чем заключается фундаментальное отличие между спецификациями API и ABI?
  2. Какие аппаратные механизмы процессора x86-64 предотвращают доступ кода Ring 3 к страницам Ring 0?
  3. За счет каких архитектурных оптимизаций инструкция syscall работает быстрее int 0x80?
  4. Какие регистры центрального процессора используются для передачи аргументов системного вызова в Linux?

Вопросы 5–8

  1. Почему номера системных вызовов в Windows меняются между версиями, а в Linux остаются неизменными?
  2. Что представляет собой дескриптор HANDLE и к каким последствиям приводит его утечка?
  3. Как в многопоточном приложении обеспечивается независимость переменной errno для каждого потока?
  4. В каких случаях системный инженер применяет утилиты strace и Process Monitor?
Прикладной программный интерфейс
Системное программирование

Полезные ресурсы

Учебные издания

  • Современные операционные системы (4-е изд.) — Таненбаум Э., Бос Х. СПб.: Питер, 2021. 1119 с.
  • Операционные системы (2-е изд.) — Гордеев А. В. СПб.: Питер, 2009. 415 с.
  • Устройство и функционирование OC Windows — Коньков К. А. М.: Бином, 2008. 208 с.
  • Методические указания к лабораторным работам — Бизюк А. Н., Соколова А. С. ВГТУ, 2024.

Документация и справочники

Прикладной программный интерфейс

Лекция №1. Цель: сформировать у слушателей системное понимание концепции нативного прикладного программного интерфейса операционных систем, аппаратных механизмов изоляции (кольца защиты, MMU) и архитектуры системных вызовов. Связь с предыдущими темами: первая вводная лекция курса, предварительных дисциплинарных зависимостей нет. Связь с практической работой: формирует теоретический фундамент для лабораторных работ по низкоуровневой разработке и вызовам системного API.

Акцентировать внимание на том, что системный программист должен понимать полный путь вызова от прикладного кода на Си до машинных инструкций процессора.

Пояснить слушателям, что изоляция создана для обеспечения стабильности и безопасности всей системы.

Обратить внимание на красную пунктирную линию — это аппаратный барьер переключения привилегий процессора.

Подчеркнуть: соответствие стандарту POSIX API позволяет перекомпилировать код на FreeBSD, Linux или macOS без правок.

Объяснить, почему архитектуры ОС общего назначения перешли на двухуровневую модель Ring 0 / Ring 3.

Отметить роль бита NX как базового аппаратного барьера против классических атак с переполнением буфера.

Пояснить разницу между асинхронным аппаратным сигналом и синхронным отказом процессора.

Указать, что вектор 0x80 в 32-битном Linux имел DPL=3, что позволяло вызывать его из User Mode.

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

Проследить по схеме пошаговый маршрут: от подготовки аргументов до возврата результата через RAX.

Обратить внимание студентов: регистр R10 используется вместо RCX, так как RCX затирается инструкцией syscall.

Пояснить, что компоновка выполняется командой ld без подключения libc, получая минимальный бинарный файл.

Подчеркнуть: прикладной разработчик Windows никогда не должен вызывать ntdll напрямую из-за нестабильности номеров.

Сравнить: открытый интерфейс системных вызовов в Linux против скрытого шлюза Native API в Windows.

Рекомендовать слушателям всегда использовать явные вызовы с суффиксом W и строковые литералы L"...".

Напомнить: errno сохраняет старое значение при успешных системных вызовах, проверять ее нужно только при возврате -1.

Отметить, что структура TEB (Thread Environment Block) доступна потоку напрямую через сегментный регистр GS на x64.

Обратить внимание на симметрию: обе ОС используют потоко-локальное хранилище (TLS / TEB) для изоляции ошибок.

Подчеркнуть важность флага -fno-omit-frame-pointer для инструментов профилирования ядра (perf, eBPF).

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

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

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

Предложить слушателям ответить на вопросы устно для закрепления терминологии.

Завершить презентацию, ответить на вопросы аудитории.