Стоит упомянуть классификацию архитектуры компьютеров

По типу применяемого процессора

  • CISC (англ. complex instruction set computing) - архитектура с полным набором команд. Такие процессоры выполняют все команды, простые и сложные, за большое количество тактов. Команд в таких процессорах много, и бывает, что компиляторы верхнего уровня очень редко используют все команды.

  • RISC (англ. reduced instruction set computing) - архитектура с сокращенным набором команд. Такие процессоры работают быстрее, чем CISC архитектура, за счет упрощения архитектуры и сокращения количества команд, но для выполнения сложной команды, она составляется из набора простых, что увеличивает время выполнения команды (за большее количество тактов).

  • MISC (англ. minimal instruction set computing) - архитектура с минимальным набором команд. Такие процессоры имеют минимальное количество команд, все команды простые и требуют небольшого количества тактов на выполнение, но если выполняются сложные вычисления, например с числами с плавающей запятой, то такие команды выполняются за большое количество тактов, превышающее CISC и RISC архитектуры.

По принципу разделения памяти

Системы команд и микропроцессорных архитектур:

 x86, MIPS, SPARC, PowerPC 

В дальнейшем мы рассмотрим более популярную архитектуру x86 это по типу применяемого процессора это CISC а по типу разделения памяти гарвардская архитектура

Из чего вообще состоит процессор, цифровые логические узлы

Вообще этих узлов много мы создадим и рассмотрим простейшую архитектуру основанной на гарвардской архитектуре разделения памяти

Алу, матрицы памяти (ROM, RAM), регистры и генератор тактовых сигналов

Арифметико-логическое устройство (АЛУ, Arithmetic/Logical Unit (ALU) объединяет различные арифметические и логические операции в одном узле. Например, типичное АЛУ может выполнять сложение, вычитание, сравнение величин, операции «И» и «ИЛИ». АЛУ входит в ядро большинства компьютерных систем.

В АЛУ поступает управляющий сигнал F, который определяет, какую функцию нужно выполнить. 

Регистры

Подавляющее большинство современных коммерческих систем построено на регистрах, использующих срабатывающие по переднему фронту тактового импульса D-триггеры.

Матрицы памяти

Матрица памяти грубо говоря это сетка объединенных между собой регистров

Разделяют:

  • Динамические ОЗУ (DRAM)
  • Статические ОЗУ (SRAM)
  • Постоянное запоминающее устройство (ROM)

Запоминающие устройства классифицируются по способу хранения битов.

Запоминающие устройства делятся на два больших класса оперативные запоминающие устройства (ОЗУ) (RAM, память с произвольным доступом) и постоянные запоминающие устройства (ПЗУ) (ROM, память только для чтения). ОЗУ является энергозависимым, то есть, при отключении питания информация, которая хранилась в ОЗУ, утрачивается. ПЗУ энергонезависимо, оно сохраняет свои данные даже при отсутствии питания. 

В настоящее время ситуация не совсем такая. В ROM  тоже можно записывать - называется это “прошивка”

Главное отличие, на которое следует обратить внимание, состоит в том, что ОЗУ энергозависимо, а ПЗУ энергонезависимо.

Язык ассемблера, архитектура 8086 -> x86

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

Ассемблер в Linux для программистов C

Регистр — это небольшой объем очень быстрой памяти, размещённой на процессоре. Он предназначен для хранения результатов промежуточных вычислений, а также некоторой информации для управления работой процессора. Так как регистры размещены непосредственно на процессоре, доступ к данным, хранящимся в них, намного быстрее доступа к данным в оперативной памяти.

Регистры 16 битного процессора intel 8086 это древний потомок архитектуры x86

Регистры общего назначения (РОН, англ. General Purpose Registers, сокращённо GPR). Размер — 32 бита.

  • %eax: Accumulator register — аккумулятор, применяется для хранения результатов промежуточных вычислений.
  • %ebx: Base register — базовый регистр, применяется для хранения адреса (указателя) на некоторый объект в памяти.
  • %ecx: Counter register — счетчик, его неявно используют некоторые команды для организации циклов (см. loop).
  • %edx: Data register — регистр данных, используется для хранения результатов промежуточных вычислений и ввода-вывода.
  • %esp: Stack pointer register — указатель стека. Содержит адрес вершины стека.
  • %ebp: Base pointer register — указатель базы кадра стека (англ. stack frame). Предназначен для организации произвольного доступа к данным внутри стека.
  • %esi: Source index register — индекс источника, в цепочечных операциях содержит указатель на текущий элемент-источник.
  • %edi: Destination index register — индекс приёмника, в цепочечных операциях содержит указатель на текущий элемент-приёмник.

 можно обратиться как %ax. А %ax, в свою очередь, содержит две однобайтовых половинки, которые могут использоваться как самостоятельные регистры: старший %ah и младший %al. Аналогично можно обращаться к %ebx/%bx/%bh/%bl, %ecx/%cx/%ch/%cl, %edx/%dx/%dh/%dl, %esi/%si, %edi/%di.

Не следует бояться такого жёсткого закрепления назначения использования регистров. Большая их часть может использоваться для хранения совершенно произвольных данных. Единственный случай, когда нужно учитывать, в какой регистр помещать данные — использование неявно обращающихся к регистрам команд. Такое поведение всегда чётко документировано.

Сегментные регистры:

  • %cs: Code segment — описывает текущий сегмент кода.
  • %ds: Data segment — описывает текущий сегмент данных.
  • %ss: Stack segment — описывает текущий сегмент стека.
  • %es: Extra segment — дополнительный сегмент, используется неявно в строковых командах как сегмент-получатель.
  • %fs: F segment — дополнительный сегментный регистр без специального назначения.
  • %gs: G segment — дополнительный сегментный регистр без специального назначения.

Стек

Мы полагаем, что читатель имеет опыт программирования на Си и знаком со структурами данных типа стек. В микропроцессоре стек работает похожим образом: это область памяти, у которой определена вершина (на неё указывает %esp). Поместить новый элемент можно только на вершину стека, при этом новый элемент становится вершиной. Достать из стека можно только верхний элемент, при этом вершиной становится следующий элемент. У вас наверняка была в детстве игрушка-пирамидка, где нужно было разноцветные кольца надевать на общий стержень. Так вот, эта пирамидка — отличный пример стека. Также можно провести аналогию с составленными стопкой тарелками. На разных архитектурах стек может "расти" как в сторону младших адресов (принцип описан ниже, подходит для x86), так и старших.

Содержимое стека  Адреса в памяти
.                .
.                .
.                .
+----------------+ 0x0000F040
|                |
+----------------+ 0x0000F044 <-- вершина стека (на неё указывает %esp)
|     данные     |
+----------------+ 0x0000F048
|     данные     |
+----------------+ 0x0000F04C
.                .
.                .
.                .
+----------------+ 0x0000FFF8
|     данные     |
+----------------+ 0x0000FFFC
|     данные     |
+----------------+ 0x00010000 <-- дно стека

Стек растёт в сторону младших адресов. Это значит, что последний записанный в стек элемент будет расположен по адресу младше остальных элементов стека.

При помещении нового элемента в стек происходит следующее (принцип работы команды push):

  • значение %esp уменьшается на размер элемента в байтах (4 или 2);
  • новый элемент записывается по адресу, на который указывает %esp.
Содержимое стека  Адреса в памяти
.                .
.                .
.                .
+----------------+ 0x0000F040 <-- новая вершина стека (%esp)
|  новый элемент |
+----------------+ 0x0000F044 <-- старая вершина стека
|     данные     |
+----------------+ 0x0000F048
.                .
.                .
.                .
+----------------+ 0x0000FFFC
|     данные     |
+----------------+ 0x00010000 <-- дно стека

При выталкивании элемента из стека эти действия совершаются в обратном порядке(принцип работы команды pop):

  • содержимое памяти по адресу, который записан в %esp, записывается в регистр;
  • а значение адреса в %esp увеличивается на размер элемента в байтах (4 или 2).
Содержимое стека  Адреса в памяти
.                 .
.                 .
.                 .
+-----------------+ 0x0000F040 <-- старая вершина стека
| верхний элемент | -------------> записывается в регистр
+-----------------+ 0x0000F044 <-- новая вершина стека (%esp)
|      данные     |
+-----------------+ 0x0000F048
.                 .
.                 .
.                 .
+-----------------+ 0x0000FFFC
|      данные     |
+-----------------+ 0x00010000 <-- дно стека

Циклы команда loop

Для организации цикла предназначена команда LOOP. У этой команды один операнд — имя метки, на которую осуществляется переход. В качестве счётчика цикла используется регистр CX. Команда LOOPвыполняет декремент CX, а затем проверяет его значение. Если содержимое CX не равно нулю, то осуществляется переход на метку, иначе управление переходит к следующей после LOOP команде.

Условные и безусловные  переходы

Безусловный переход — это переход, который выполняется всегда. Безусловный переход осуществляется с помощью команды JMP. У этой команды один операнд, который может быть непосредственным адресом (меткой), регистром или ячейкой памяти, содержащей адрес. Существуют также «дальние» переходы — между сегментами, однако здесь мы их рассматривать не будем.

Условный переход осуществляется, если выполняется определённое условие, заданное флагами процессора (кроме одной команды, которая проверяет CX на равенство нулю). Как вы помните, состояние флагов изменяется после выполнения арифметических, логических и некоторых других команд. Если условие не выполняется, то управление переходит к следующей команде.

Существует много команд для различных условных переходов. Также для некоторых команд есть синонимы (например, JZ и JE — это одно и то же). Для наглядности все команды условных переходов приведены в таблице:

Команда Переход, если Условие перехода
JZ/JE нуль или равно ZF=1
JNZ/JNE не нуль или не равно ZF=0
JC/JNAE/JB есть переполнение/не выше и не равно/ниже CF=1
JNC/JAE/JNB нет переполнения/выше или равно/не ниже CF=0
JP число единичных бит чётное PF=1
JNP число единичных бит нечётное PF=0
JS знак равен 1 SF=1
JNS знак равен 0 SF=0
JO есть переполнение OF=1
JNO нет переполнения OF=0
JA/JNBE выше/не ниже и не равно CF=0 и ZF=0
JNA/JBE не выше/ниже или равно CF=1 или ZF=1
JG/JNLE больше/не меньше и не равно ZF=0 и SF=OF
JGE/JNL больше или равно/не меньше SF=OF
JL/JNGE меньше/не больше и не равно SF≠OF
JLE/JNG меньше или равно/не больше ZF=1 или SF≠OF
JCXZ содержимое CX равно нулю CX=0

Вызов процедур

Для работы с процедурами предназначены команды CALL и RET. С помощью команды CALL выполняется вызов процедуры. Эта команда работает почти также, как команда безусловного перехода (JMP), но с одним отличием — одновременно в стек сохраняется текущее значение регистра IP. Это позволяет потом вернуться к тому месту в коде, откуда была вызвана процедура. В качестве операнда указывается адрес перехода, который может быть непосредственным значением (меткой), 16-разрядным регистром (кроме сегментных) или ячейкой памяти, содержащей адрес.

Возврат из процедуры выполняется командой RET. Эта команда восстанавливает значение из вершины стека в регистр IP. Таким образом, выполнение программы продолжается с команды, следующей сразу после команды CALL. Обычно код процедуры заканчивается этой командой.

Как передавать аргументы в процедуру? конечно же через стек

push ax
pop ax

но в x86 все немного сложней

Правила вызывающей стороны

Перед вызовом функции вызывающая сторона должна:

  1. Сохранить в стек регистры, которые обязан сохранять вызывающий. Вызываемая функция может изменить некоторые регистры: чтобы не потерять данные, вызывающая сторона должна сохранить их в памяти до помещения в стек. Речь идёт о регистрах eax, ecx и edx. Если вы не используете какие-то из них, то их можно не сохранять.
  2. Записать аргументы функции на стек в обратном порядке (сначала последний аргумент, в конце первый аргумент). Такой порядок гарантирует, что вызываемая функция получит из стека свои аргументы в правильном порядке.
  3. Вызвать подпрограмму.

По возможности функция сохранит результат в eax. Сразу после call вызывающая сторона должна:

  1. Удалить из стека аргументы функции. Обычно это делается путём простого добавления числа байтов в esp. Не забывайте, что стек растёт вниз, поэтому для удаления из стека необходимо добавить байты.
  2. Восстановить сохранённые регистры, забрав их из стека в обратном порядке инструкцией pop. Вызываемая функция не изменит никакие другие регистры.

Следующий пример демонстрирует, как применяются эти правила. Предположим, что функция _subtract принимает два целочисленных (4-байтовых) аргумента и возвращает первый аргумент за вычетом второго. В подпрограмме _mysubroutine вызываем _subtract с аргументами 10 и 2:

_mysubroutine:

    ; ...
    ; здесь какой-то код
    ; ...
    push ecx       ; сохраняем регистры (я решил не сохранять eax)
    push edx
    push 2         ; второе правило, пушим аргументы в обратном порядке
    push 10
    call _subtract ; eax теперь равен 10-2=8
    add esp, 8     ; удаляем 8 байт со стека (два аргумента по 4 байта)
    pop edx        ; восстанавливаем сохранённые регистры
    pop ecx
    ; ...
    ; ещё какой-то код, где я использую удивительно полезное значение из eax
    ; ...

Что-то уже дофига и многое осталось за кадром

Упомянуть что такое операционная система, рассказать как устроены процессы в windows, рассказать немного о PE формате и способе загрузки программы в память

Архитектура компьютера
Очень простая схема описания операционной системы
Архитектура UNIX

Процессы и потоки

Процесс обычно определяют как экземпляр выполняемой программы, и он состоит из двух компонентов:

  • объекта ядра, через который операционная система управляет процессом. Там же хранится статистическая информация о процессе;
  • адресного пространства, в котором содержится код и данные всех EXE и DLL модулей. Именно в нем находятся области памяти, динамически распределяемой для стеков потоков и других нужд.

Процессы инертны. Чтобы процесс что нибудь выполнил, в нем нужно создать поток. Именно потоки отвечают за исполнение кода, содержащегося в адресном пространстве процесса. В принципе, один процесс может владеть несколькими потоками, и тогда они «одновременно» исполняют код в адресном пространстве процесса.

Для этого каждый поток должен располагать собственным набором регистров процессора и собственным стеком. В каждом процессе есть минимум один поток. Если бы у процесса не было ни одного потока, ему нечего было бы делать на этом свете, и система автоматически уничтожила бы его вместе с выделенным ему адресным пространством.

Чтобы все эти потоки работали, операционная система отводит каждому из них определенное процессорное время. Выделяя потокам отрезки времени (называемые квантами) по принципу карусели, она создает тем самым иллюзию одновременного выполнения потоков.

При создании процесса первый (точнее, первичный) поток создается системой автоматически. Далее этот поток может породить другие потоки, те в свою очередь — новые и т. д.

Источники