Введение
Я делала много вещей с компьютерами, но в моих знаниях всегда был пробел: что конкретно происходит при запуске программы на компьютере? Я думала об этом пробеле - у меня было много низкоуровневых знаний, но не было цельной картины. Программы действительно выполняются прямо в центральном процессоре (central processing unit, CPU)? Я использовала системные вызовы (syscalls), но как они работают? Чем они являются на самом деле? Как несколько программ выполняются одновременно?
Наконец, я сломалась и начала это выяснять. Мне пришлось перелопатить тонны ресурсов разного качества и иногда противоречащих друг другу. Несколько недель исследований и почти 40 страниц заметок спустя я решила, что гораздо лучше понимаю, как работают компьютеры от запуска до выполнения программы. Я бы убила за статью, в которой объясняется все, что я узнала, поэтому я решила написать эту статью.
И, как говорится, ты по-настоящему знаешь что-то, только если можешь объяснить это другому.
1. Основы
Одной вещью, которая удивляла меня снова и снова, является то, насколько компьютеры просты. Эта простота очень красива, но порой превращается в кошмар.
Архитектура компьютера
ЦП - это место, в котором происходят все вычисления. Он начинает "пыхтеть" как только вы включаете компьютер, выполняя инструкцию за инструкцией.
Первым массовым ЦП был Intel 4004, спроектированный в конце 60-х итальянским физиком и инженером Федерико Фарджином. Это была 4-битная архитектура, а не 64-битная, которая используется сегодня. 4-битная архитектура была гораздо менее сложной, чем современные процессоры, но многое осталось почти неизменным.
"Инструкции", выполняемые ЦП - всего лишь двоичные (binary) данные: байт или два для представления запускаемой инструкции (код операции - opcode), за которыми следуют данные, необходимые для вы полнения инструкции. То, что мы называем машинным кодом (machine code) - всего лишь серия этих бинарных инструкций в виде строки. Assemble - это полезный синтаксис, облегчающий чтение и запись сырых (raw) битов людьми. Он всегда компилируется в двоичные данные, понятные ЦП.
Ремарка: инструкции в машинном коде не всегда представлены 1:1, как в приведенном примере. Например,
add eax, 512
преобразуется в05 00 02 00 00
.Первый байт (
05
) - это код операции, представляющий добавление регистра EAX к 32-битному числу. Остальные байты - это 512 (0x200
) с порядком байтов little-endian.Defuse Security разработали полезный инструмент для преобразования языка ассемблера в машинный код.
Оперативная память или оперативное запоминающее устройство (Random Access Memory, RAM) - это основное хранилище компьютера, большое многоцелевое пространство, где хранятся все данные, используемые программами, запущенными на компьютере. Это включает код самих программ, а также код ядра (kernel) операционной системы. ЦП всегда читает машинный код прямо из ОП. Код, не загруженный в ОП, не может быть выполнен.
ЦП хранит указатель инструкции (instruction pointer), который указывает на место в ОП, где находится следующая инструкция. После выполнения всех инструкций, ЦП возвращает указатель в начало, и процесс повторяется. Это называется циклом выборки-исполнения (fetch-execute cycle).
После выполнения инструкции указатель передвигается за нее в ОП и таким образом указывает на следующую инструкцию. Указатель инструкции двигается вперед, и машинный код выполняется в порядке его хранения в памяти. Некоторые инструкции могут заставить указатель переместиться (перепрыгнуть - jump) в другое место (выполнить другую инструкцию вместо текущей или выполнить одну из инструкций в зависимости от определенного условия). Это делает возможным повторное и условное выполнение кода.
Указатель инструкции хранится в регистре (registry). Регистры - это маленькие хранилища, которые являются очень производительными для чтения и записи ЦП. Каждая архитектура ЦП имеет фиксированный набор регистров, используемых для всего: от хранения временных значений в процессе вычислений до настройки процессора.
Некоторые регистры доступны из машинного кода напрямую, например, ebx
на приведенной выше диаграмме.
Другие регистры предназначены только для внутреннего использования ЦП, но часто могут обновляться или читаться с помощью специальных инструкций. Одним из примеров такого регистра является указатель инструкции, который не может читаться напрямую, но может обновляться, например, для выполнения другой инструкции вместо текущей.
Процессоры наивны
Вернемся к вопросу о том, что происходит при запуске программы на компьютере. Сначала происходит некоторая магия - мы поговорим об этом позже - и мы получаем файл с машинным кодом, который где-то хранится. ОС загружает его в ОП и указывает ЦП переместить указатель на определенную позицию в ОП. Начинается цикл выборки-исполнения, и программа запускается!
Ваш ЦП получает последовательные инструкции браузера из ОП и выполняет их, что приводит к рендерингу этой статьи.
ЦП имеют очень ограниченный кругозор. Они видят только указатель текущей инструкции и немного внутреннего состояния. Процессы - это абстракции ОС, а не нечто, что понимают или отслеживают ЦП.
У меня это вызывает больше вопросов, чем ответов:
- Если ЦП не знает о многозадачности (multiprocessing) и выполняет инструкции последовательно, почему он не застревает в выполняемой программе? Как могут одновременно выполняться несколько программ?
- Если программа выполняется в ЦП, и ЦП имеет прямой доступ к ОП, почему код из других процессов или, прости господи, ядра не имеет доступа к памяти?
- Каков механизм, предотвращающий выполнение любой инструкции любым процессом? И что такое системный вызов?
Вопрос о памяти заслуживает отдельного раздела (см. раздел 5). Если коротко, то бо льшая часть обращений к памяти проходит через слой неправильного направления (misdirection), который переназначает все адресное пространство. Пока мы будем исходить из предположения, что программы имеют прямой доступ ко всей ОП, и компьютеры могут запускать только один процесс за раз. Со временем мы избавимся от обоих этих предположений.
Пришло время прыгнуть в нашу первую кроличью нору – в страну, полную системных вызовов и колец безопасности (security rings).
Ремарка: что такое ядро?
ОС вашего компьютера, такая как macOS, Windows или Linux - это коллекция программного обеспечения, выполняющая всю основную работу. "Основная работа" - это общее понятие, и, в зависимости от ОС, может включать такие вещи, как приложения, шрифты, иконки, которые поставляются с компьютером по умолчанию.
Каждая ОС имеет ядро. Когда вы включаете компьютер, указатель инструкции запускает какую-то программу. Этой программой является ядро. Ядро имеет почти полный доступ к памяти, периферийным устройствам и другим ресурсам и отвечает за запуск ПО, установленного на компьютере (пользовательских программ).
Linux - это всего лиш ь ядро, которому для полноценной работы требуется множество пользовательских программ, таких как оболочки (shells) и серверы отображения (display servers). Ядро macOS называется XNU, а современное ядро Windows - NT Kernel.
Два кольца, чтобы править всеми
Режим (mode) (иногда именуемый уровнем привилегий (privilege level) или кольцом (ring)), в котором находится процессор, управляет тем, что разрешено делать. В современных архитектурах имеется, как минимум, два варианта: режим ядра/администратора (kernel/supervisor mode) и режим пользователя (user mode). Несмотря на то, что архитектура может поддерживать более двух режимов, как правило, используются только режимы ядра и пользователя.
В режиме ядра разрешено все: ЦП может выполнять любую поддерживаемую инструкцию и обращаться к любой памяти. В режиме пользователя разрешен только определенный набор инструкций, ввод/вывод (input/output, I/O) и доступ к памяти ограничены, многие настройки ЦП заблокированы. Как правило, ядро и драйверы запускаются в режиме ядра, а приложения - в режиме пользователя.
Процессоры запускаются в режиме ядра. Перед выполнением программы ядро инициализирует переключение в пользовательский режим.
Пример того, как режимы процессора проявляются в реальной архитектуре: в x86-64 текущий уровень привилегий (current privilege level, CPL) может читаться из регистра cs
(code segment - сегмент кода). В частности, CPL содержится в 2 наименьших битах регистра cs
. Эти 2 бита могут хранить 4 возможных кольца x86-64: кольцо 0 - это режим ядра, а кольцо 3 - режим пользователя. Кольца 1 и 2 предназначены для запуска драйверов, но используются только несколькими нишевыми ОС. Если, например, битами CPL являются 11
, ЦП запускается в режиме пользователя.