Перейти к основному содержимому

Архитектура современных процессоров

Разработчику Архитектору Инженеру

Архитектура современных процессоров

Основы системного программирования

Если вы подзабыли, что такое процессор, память, диск — перечитайте, ведь сейчас мы погрузимся ещё ниже. Что нужно знать ещё о железе?

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

Ниже — словарь ключевых терминов, разложенный от общей картины к деталям — сначала иерархия памяти и адресация, затем стек и куча, потом процессор, инструкции, ООП на железе и потоки. В конце — сводные схемы, связывающие всё в один путь от obj.method() до RET.

Оглавление раздела:

БлокО чём
Иерархия памятиРегистры, кэши, RAM, диск; L1/L2/L3
Адреса и трансляцияВиртуальная/физическая память, MMU, TLB, смещения
Стек и кучаLIFO, heap, ссылки vs указатели
Стек вызововCall stack, frame, RSP/ESP, RBP/EBP
Процессор: такты и регистрыClock, frequency, RIP, регистровые окна
Машинные инструкцииMOV, CALL, RET, JMP, PUSH/POP и др.
Внутренняя работа CPUМикрооперации, адрес возврата
ООП на уровне железаVTable, this, инициализация
Потоки и контекстThread, execution context, context switch
Схемы выполненияСквозной путь от вызова до возврата

Play ITЗагрузка интерактивного демо…


Иерархия памяти

Начнём с памяти. Память — иерархия, а не плоское пространство. Доступ к данным идёт по иерархии:

Регистры CPU →
L1 кэш (3-4 такта) →
L2 →
L3 →
RAM (100-300 тактов) →
SSD/HDD (миллионы тактов).

И эти такты как раз и образуют собой работу процессора, скорость которого измеряется частотой.

Кэши процессора — быстрая память внутри CPU, которая хранит копии часто используемых данных и инструкций:

  • L1 — самый быстрый (~3 такта), разделён на данные и instruction;
  • L2 — медленнее (~10 тактов), общий для ядра;
  • L3 — ещё медленнее (~40 тактов), общий для всех ядер.

Если будет промах кэша (cache miss), то потеряем такты.

Выполнение, кстати, производится не только центральным процессором — также может выполнять код и GPU (графический процессор), TPU/NPU (для ИИ), DMA (прямой доступ к памяти без CPU). Но в это пока не погружаемся.

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


Адреса и трансляция

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

ТерминСуть
Базовый адресНачальный адрес области памяти (объекта, массива или сегмента). Все остальные адреса вычисляются относительно него.
Смещение (Offset)Разница между базовым адресом структуры (например, объекта) и адресом конкретного поля. Если объект начинается по адресу 0x1000, а поле value — восьмой байт, то смещение = 8. Доступ: *(obj_addr + 8).
Физический адресРеальный адрес в оперативной памяти, по которому данные хранятся на физической микросхеме.
Виртуальный адресАдрес, который использует программа. MMU преобразует его в физический.

MMU (Memory Management Unit) — аппаратный компонент CPU, который отвечает за преобразование виртуальных адресов в физические. Он использует таблицы страниц, поддерживает виртуальную память, защиту доступа, кэширование TLB и позволяет каждому процессу иметь изолированное адресное пространство.

TLB (Translation Lookaside Buffer) — кэш MMU, который хранит соответствия виртуальных и физических адресов.

Ссылки — указатели на адреса в памяти. Когда вы пишете obj.method(), на низком уровне obj — это указатель (тот самый адрес в куче), method() — вызов по смещению в VTable, а this — неявный первый аргумент (передаётся как this-указатель).

Ссылка (Reference) — абстракция указателя в высокоуровневых языках, хранящая адрес объекта в памяти. Она не позволяет прямых арифметических операций (в отличие от указателей в C++, именно поэтому ссылка это уже не указатель).


Стек и куча

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

Стек

Стек — сегмент памяти по принципу LIFO (Last In, First Out — последним пришёл, первым ушёл). В нём хранят локальные переменные, параметры функций, адреса возврата и сохранённые регистры.

АЛГОРИТМ СТЕК (операции)
ПОЛОЖИТЬ_НА_СТЕК(значение) // push — указатель стека сдвигается
СНЯТЬ_СО_СТЕКА() → значение // pop
КОНЕЦ

АЛГОРИТМ ВЫЗОВ_ФУНКЦИИ через стек
ПОЛОЖИТЬ_НА_СТЕК(адрес_возврата)
ПОЛОЖИТЬ_НА_СТЕК(аргументы...)
ПЕРЕЙТИ_К(тело_функции)
// после return:
СНЯТЬ_СО_СТЕКА() → адрес_возврата
ПЕРЕЙТИ_К(адрес_возврата)
КОНЕЦ

В стеке выделяется фиксированный размер на поток, доступ к нему очень быстрый (через операции push — толкать, pop — выталкивать). В нём используется автоматическое управление (он освобождается при выходе из функции). Стек расположен в верхней части виртуального адресного пространства (растёт вниз).

Куча

Куча — область памяти для динамического выделения объектов (те самые new, malloc). Управление кучей выполняется либо вручную, либо через сборщик мусора.

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

При выделении памяти в куче выделяется блок нужного размера. Блоки управляются через списки свободных/занятых областей.

Высокие адреса ──► [ Стек ] растёт вниз ↓
...
[ Куча ] растёт вверх ↑
Низкие адреса

Стек вызовов

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

ТерминОписание
Стек вызовов (Call Stack)Специализированное использование стека в памяти для отслеживания активных функций (методов). Тот же стек, просто используется для хранения контекста вызовов.
Фрейм стека (Stack Frame)Блок данных в стеке, выделенный для одного вызова функции. Содержит параметры, локальные переменные, адрес возврата, сохранённые регистры, указатель на предыдущий фрейм. При выходе из функции фрейм удаляется — это и будет команда pop.
Указатель стека (Stack Pointer)Критически важный инструмент управления стеком. При push он уменьшается, при pop — увеличивается (при этом стек растёт вниз).
ESP (x86, 32-bit)Указывает на верх стека (последний занесённый элемент).
RSP (x86-64)64-битная версия ESP.
Указатель фрейма (Frame Pointer, EBP/RBP)Указывает на начало текущего фрейма, используется для доступа к параметрам и локальным переменным по фиксированному смещению.

Обращение к стеку происходит через RSP/ESP, управляемое процессором.

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


Процессор: такты и регистры

Время процессора

Такт (Clock Cycle) — один импульс тактового генератора, базовая единица времени для CPU. Да, время у него измеряется иначе, и все операции синхронизированы тактом. Как сердцебиение — и поэтому процессор является сердцем, тем самым моторчиком.

Частота (Clock Frequency) определяет количество тактов в секунду (Гц), например, 3.5 GHz = 3.5 миллиарда тактов/сек. Да, у процессоров такая скорость. Это определяет максимальную скорость выполнения инструкций (но не всегда, из-за конвейера, простоя и т.д.).

Регистры

Регистры — очень быстрая память внутри CPU, используемая для хранения данных, адресов, состояния. К примеру:

РегистрНазначение
RAXАккумулятор — результаты операций
RIPСчётчик команд — адрес следующей инструкции
RSPУказатель стека
RDI, RSIАргументы функций (System V AMD64 ABI)

Счётчик команд (Program Counter, RIP) — регистр, хранящий адрес следующей инструкции для выполнения. При каждом шаге он увеличивается, а при jmp/call изменяется.

Регистровые окна — архитектурный приём (в SPARC): при вызове функции автоматически переключается набор регистров. Это для ускорения вызовов, избегая push/pop в стек. Современные CPU используют переименование регистров вместо этого.


Машинные инструкции

Инструкции — элементарные команды, понятные процессору (MOV, ADD, CALL, JMP). Они хранятся в памяти как машинный код, в байтах, и декодируются процессором в микрооперации.

Ассемблерные команды — самый низкий уровень управления процессором. Они напрямую соответствуют машинному коду, который выполняет процессор.

Компилятор автоматически генерирует эти команды из C++, Java, Rust, .NET. Оптимизатор может менять порядок, удалять, заменять команды.

Перемещение данных

КомандаНазначение
MOVКопирует данные из источника в приёмник. Просто перемещение — одна из самых частых операций: загрузка значений, параметров, адресов.
LEALoad Effective Address — вычисляет адрес, но не обращается к памяти. Используется для арифметики указателей, смещений.
PUSH / POPРабота со стеком. PUSH кладёт значение на стек, RSP уменьшается; POP забирает значение со стека, RSP увеличивается. Используются при вызовах, сохранении регистров, локальных данных.

Арифметика и сравнение

КомандаНазначение
ADD / SUBСложение и вычитание; изменяют флаги (Zero Flag, Carry Flag). Используются в арифметике, инкременте, адресации.
CMPСравнение: вычитает два значения, обновляет флаги, но не сохраняет результат. Нужен перед условными переходами.
TESTПобитовое И (для флагов); используется для проверки на ноль.

Управление потоком выполнения

КомандаНазначение
CALLВызов функции или метода. Помещает адрес возврата (следующую инструкцию) в стек (PUSH RIP), затем загружает в счётчик команд (RIP) адрес функции, переходя к телу функции.
RETВозврат из функции. Извлекает адрес возврата из стека и загружает его в RIP, после чего продолжает выполнение с того места, откуда была вызвана функция. Автоматический финал любого метода.
JMPБезусловный переход — мгновенно переходит на указанный адрес. Используется в циклах, GOTO, оптимизациях.
JE / JNE / JL / JGУсловные переходы; работают после CMP или арифметики. Реализуют if, while, for.

Побитовые операции и служебные

КомандаНазначение
XOR / AND / OR / NOTПобитовые операции; используются в манипуляциях с флагами, хэшах, оптимизациях.
NOPNo Operation — ничего не делает, 1 такт простоя. Используется для выравнивания, отладки, задержек.

Важное уточнение: то, что работает на x86-64, не сработает на ARM (там другие инструкции — MOV, BL, LDR, STR и пр.).

Интересная задумка — можете поиграться с нейросетями в интерактиве: вы пишете в чат код на каком-либо языке, а нейросеть в ответ будет писать ассемблер. Так вы можете изучать низкий уровень работы команд.

Внутренняя работа CPU

Микрооперации (Micro-ops, μops) — внутренние команды, на которые CPU разбивает сложные инструкции. Они позволяют выполнять инструкции вне порядка (out-of-order execution) и параллельно.

Адрес возврата (Return Address) — адрес в коде, куда нужно вернуться после завершения функции. Сохраняется в стеке при выполнении CALL.


ООП на уровне железа

Объектно-ориентированный код на высоком уровне опирается на несколько низкоуровневых механизмов.

ТерминОписание
VTable (Virtual Method Table)Таблица указателей на реализации виртуальных методов для конкретного класса. Каждый объект с виртуальными методами содержит скрытое поле — указатель на VTable. При вызове виртуального метода: obj->vtable[METHOD_INDEX]() — динамическое разрешение.
Виртуальный методМетод, поведение которого может быть переопределено в наследнике. Вызов разрешается во время выполнения (dynamic dispatch) через VTable. К примеру, @Override в Java (ещё вернёмся).
this (или self)Неявный параметр метода, указывающий на текущий экземпляр объекта. На низком уровне — первый аргумент метода (в RDI на x86-64).
ИнициализацияПроцесс установки начального состояния объекта или переменной. Включает присвоение значений полям, вызов конструктора, выполнение статических инициализаторов, загрузку класса. На низком уровне — запись байтов в память по нужным смещениям.

Потоки и контекст

ТерминОписание
Поток (Thread)Поток выполнения внутри процесса. Имеет свой стек, свои регистры, счётчики команд, но (!) общую память (кучу) с другими потоками. Позволяет выполнять несколько задач параллельно.
Контекст выполнения (Execution Context)Совокупность данных, необходимых для выполнения кода: стек вызовов, регистры, локальные переменные, объект this. У каждого потока свой контекст.
Контекст потока (Thread Context)Набор регистров, стека, состояния, сохранённых при переключении потоков. Сохраняется ядром ОС при переключении контекста (context switch).

Схема выполнения

Можно построить обобщённую схему — сквозной путь от вызова метода до возврата.

Текстом:

[Программа: obj.method()]

[Компилятор: генерирует вызов, использует VTable]

[CPU: RSP управляет стеком, RIP — счётчик команд]

[Вызов: push аргументов, CALL → RIP = адрес метода]

[Метод: фрейм в стеке, доступ к this, полям по смещению]

[Память: MMU преобразует виртуальный → физический адрес]

[Кэш: L1/L2/L3 ускоряют доступ к данным]

[Возврат: RET, RSP++, RIP = адрес возврата]

Схематично — верхний уровень:

Пример выше — верхнеуровневый: показывает сквозной путь от вызова до возврата с акцентом на взаимодействие программного и аппаратного уровней. Ниже — системный уровень с иерархией памяти, TLB, смещением, MMU:

Ещё ниже — уровень инструкций:

Сложнее, наверное, уже не получится — технически так и работает регистр, стек, вызов и возврат.