В каком виде хранятся машинные команды в памяти компьютера

Обновлено: 17.07.2024

1. Представление данных. Форматы команд и способы адресации. Программирование в машинных кодах и на языке АССЕМБЛЕРа. Рабочий цикл и состояние микропроцессора. Байт-состояние, назначение разрядов в коде слова состояния процессора

Представление данных. Форматы команд и способы адресации. Программирование в машинных кодах и на языке АССЕМБЛЕРа. Рабочий цикл и состояние микропроцессора. Байт-состояние, назначение разрядов в коде слова состояния процессора.

Любая программа для ЭВМ - системная или приклад­ная — воспринимается (распознается) процессором толь­ко в том случае, если она состоит из специальных ко­манд, коды которых известны процессору определенного типа. Команды записаны в памяти компьютера в специ­альном формате. Каждая команда состоит из операционной и адресной частей. В первой из них находится позиционный двоичный код, определяющий требуемое от процессора действие (сложение, вычитание и т.д.). Во второй – адресной части команды, также в виде двоичного позиционного кода, находятся адреса данных (операндов), над которыми это действие необходи­мо выполнить, либо сами операнды. В вольном переводе на русский язык не­которую команду можно, например, интерпретировать так: сложить два числа, находящиеся в памяти по ад­ресам 100 и 120.

Разные типы ЭВМ имеют отличные друг от друга спосо­бы кодировки команд. Так, на персональных IВМ-совместимых компьютерах некоторая команда сложения в двоичном коде может иметь вид:0000001111000011D или в шестнадцатиричном коде 03С3H . А на «древних» компью­терах типа М-220 команда сложения двух чисел могла выглядеть так:

001 00000001100100 00000001111000 00000001111011.

Поэтому программа в кодах компьютера (машинных кодах) является машинно-зависимой и непереносимой, т.е. подготовленная для компьютера одного типа, она не сможет выполняться на других. Этот факт определяет основной недостаток про­граммирования в машинных кодах.

Вторым недостатком программирования в кодах явля­ется сильное дробление программы. Дело в том, что логи­чески команды процессора достаточно примитивны и обу­славливают выполнение простейших операций. Так, про­граммирование несложной формулы x=(a+b)(c+d) требо­вало задания серии команд типа:

- сложить а и b, промежуточный результат записать в ,

- сложить c и d, промежуточный результат записать в ,

- умножить на , результат записать в х.

При программировании в кодах ви­зуально каждая программа состояла из большого количе­ства команд-строк, похожих на приведенные выше двоич­ные коды. Это определяло третий недостаток программи­рования в кодах - затрудненную читаемость программы и, как следствие, сложность исправления (отладки) или доработки программы.

Однако программированию в кодах присущи и значи­тельные плюсы. Программист управляет всеми ресурсами компьютера, полностью контролирует текущее состояние ЭВМ, выбирает наиболее оптимальный код команды. Са­мые короткие по объему и наиболее быстрые по выполнению программы или их фрагменты разрабатываются и се­годня в кодах. Для облегчения наглядности программы в кодах разработаны специальные символические языки — ассемблеры. В них каждой команде компьютера сопостав­ляется определенный символьный код, являющийся со­кращением «родных» для человека слов. Специальная программа (она также называется ассемблером) переводит (транслирует)«непонятную» для компьютера (но более понятную для человека) символьную строку в коды ком­пьютера. Так, приведенные выше коды команд сложения на ассемблере могли выглядеть так: ADD АХ, BX (сло­жить числа из регистров АХ и ВХ и результат запомнить в АХ). При программировании на ассемблере программист мо­жет оперировать не с адресами памяти, в которых хра­нятся данные, а с их символическим представлением. На­пример, вначале ассемблеру специальной инструкцией со­общается, что по такому-то адресу хранится число, на­званное для программиста . Далее программист не заду­мывается над тем, по какому адресу находится соответ­ствующее число, но просто использует его имя.

Ассемблер является машинно-зависимым языком про­граммирования, так как его инструкции соответствуют кодам команд компьютера. Поэтому ассемблерная програм­ма может выполняться только на тех ЭВМ, для которых она разрабатывалась. Кроме того, для работы на ассемб­лере требуется детальное знание особенностей конкретной ЭВМ.

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

Команды, как и любая информация в ЭВМ, кодируются двоичными словами, которые должны содержать в себе следующие виды информации:

· тип операции, которую следует реализовать в данной команде (КОП);

· место в памяти, откуда следует взять первый операнд (А1);

· место в памяти, откуда следует взять второй операнд (А2);

· место в памяти, куда следует поместить результат (A3).

Каждому из этих видов информации соответствует своя часть двоичного сло­ва — поле, а совокупность полей (их длины, расположение в командном слове, способ кодирования информации) называется форматом команды. В свою очередь, некоторые поля команды могут делиться на подполя. Формат команды, поля которого перечислены выше, называется трехадресным . Команды трехадресного формата занимают много места в памяти, в то же время далеко не всегда поля адресов используются в командах эффективно. [Действительно, наряду с двухместными операциями (сложение, деление, Конъюнкция и др.) встречаются и одноместные (инверсия, сдвиг, инкремент др.), для которых третий адрес не нужен. При выполнении цепочки вычис-ний часто результат предыдущей операции используется в качестве опе-да для следующей. Более того, нередко встречаются команды, для кото-операнды не определены (СТОП) или подразумеваются самим кодом раций (DAA, десятичная коррекция аккумулятора).

Система адресации обеспечивает в МП устройствах доступ к операндам, хранящимся в устройствах памяти, внутренних программно- доступных регистрах и внешних устройствах через порты ввода/вывода. Длина формата команды (общее количество бит в коде команды) определяет скорость выполнения команды и зависит от способа адресации операндов. Способы адресации играют большую роль в организации управления ходом выполнения программы, во многом определяют скорость обработки информации. Существуют следующие основные способы адресации:

Прямая адресация – исполнительный адрес размещается в адресном поле команды. В этом способе непосредственно используется счетчик команд и аккумулятор МП. При прямой адресации содержимое счетчика команд передается на ША и из адресуемой ячейки выбирается очередная команда, поступающая в устройство управления МП. С помощью прямой адресации в АЛУ может вызываться и операнд. Для этого на ША передается адрес ячейки памяти, содержащийся в команде. При необходимости изменения последовательного порядка выполнения программы адресуется ячейка памяти, содержащая адрес условного или безусловного перехода. Команда, содержащая прямой адрес, занимает 2-3 ячейки памяти, что соответственно требует и 2-3 обращений к памяти. Еще одно обращение может потребоваться для непосредственного вызова операнда.

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

Косвенная адресация– адресное поле команды указывает не адрес операнда, а лишь адрес ячейки, в которой хранится косвенный адрес, по которому можно получить операнд. В МП косвенная адресация обычно является неявной регистровой. В этом случае команда предписывает выполнить операцию над операндом, адрес которого содержится в регистрах общего назначения (обычно для этого выделяются определенные регистры блока РОН). Так, команда MOV REG,M загружает любой регистр из ячейки памяти, адрес которой содержится в регистровой паре H,L. Команда LDAX загружает аккумулятор с использованием адреса, содержащегося в регистровой паре B или D (используя регистры B или D можно осуществить косвенную загрузку только аккумулятора).

Относительная индексная адресация (автоинкрементная и автодекрементная) – базовый исполнительный адрес вычисляется так же, как и при регистровой косвенной адресации, а следующие исполнительные адреса получаются увеличением или уменьшением текущего адреса операнда на определенную величину после каждой обработки адреса. Обычно такая адресация используется при обработке массивов данных. В этом случае список данных представляется как совокупность индексных операндов. Операнды с индексами помещаются в соседних ячейках памяти. Для организации доступа к этим ячейкам памяти используются индексные регистры аналогичные в данном случае счетчику команд. Для двумерного массива истинный адрес

ИА = [базовый адрес] + (содержимое индексных регистров)

Для перебора операндов с индексами осуществляется автоиндексация, т.е. добавление к содержимому регистров плюс или минус единицы. Разновидностью автоиндексации является стековая адресация. В этом случае автоиндексный регистр является указателем стека, а область памяти им адресуемая, – стеком. Принцип стековой адресации заключается в следующем: после каждой засылки в стек, содержимое указателя стека увеличивается, а после каждого изъятия из стека - уменьшается на единицу.

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

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

Функционирование процессора состоит из повторяющихся рабочих циклов, каждый из которых соответствует выполнению либо целой команды, либо её части. Завершив рабочий цикл процессор переходит к выполнению следующего рабочего цикла. Предположим, что процессор может выполнять четыре типа команд:

- основные (арифметические, логические, пересылочные операции);

- системные (установка маски прерываний, состояния процессора и др.).

Рассмотрим рабочий цикл, выполняющийся по командно (существуют рабочие циклы, выполняющиеся по машинным циклам).

Рабочий цикл начинается (см. рисунок 4.6) с определения состояния процессора - СЧЕТ или ОЖИДАНИЕ. Процессор никаких действий в состоянии ожидания не выполняет и может выйти из него только при активизации некоторых внешних сигналов, например, запроса на прерывание ЗПр.

В состоянии СЧЕТ (в этом режиме происходит последовательная выборка и выполнение команд), если сигнал ЗПр отсутствует, то последовательно выполняются этапы рабочего цикла: формирование исполнительных адресов операндов, выборка операндов, выполнение операций и запоминание результата. После этого процессор переходит к выборке следующей команды и цикл повторяется. Если поступил запрос прерывания, процессор сбрасывает триггер прерывания ТПр, запоминает свое текущее состояние (например, путем записи адреса следующей команды в стек) и переходит к выполнению подпрограммы обработки прерывания путем передачи адреса подпрограммы в СчК.

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

При выполнении команд передачи управления проверяется условие перехода по вышеуказанным признакам для команд условных переходов. Если условие перехода не выполняется, то выбирается следующая по порядку команда по подвинутому адресу, хранящемуся в СчК. Если условие выполняется, то в СчК заносится адрес перехода.

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

Микропроцессор может находиться во включенном и выключенном режиме. Во включенном режиме различают четыре состояния микропроцессора:

1. Состояние захвата шин.

2. Состояние прерывания.

3. Состояние ожидания.

4. Состояние останова.

1) Состояние захвата шин инициируется внешним сигналом HLD, поступающим на одноименный вход микропроцессора. Указанный сигнал посылает специализированный контроллер КР580ВТ57 – контроллер прямого доступа к памяти – это четырехканальный программируемый контроллер, берущий на себя функцию управления шинами адреса и данных. Такое управление реализуется контроллером после получения разрешающего сигнала HLDА. С помощью контроллера осуществляется двунаправленный обмен массивами данных до 16 килобайт по одному из каналов. К каналу с одной стороны подключено внешнее устройство, с другой – память. При работе в этом режиме микропроцессор переводит свои магистрали (шина адреса и шина данных) в третье состояние (отключенное). Состояние захвата шин длится целое число машинных тактов. Микропроцессор при этом на каждом машинном такте синхроимпульсов С2 проверяет наличие сигнала HLD. Если сигнал снимается, то по следующему тактовому импульсу С2 режим захвата шин прекращается, и микропроцессор переходит к выполнению очередного машинного цикла, начиная с такта Т1.

2) Состояние обслуживания прерывания инициируется сигналом INT на входе микропроцессора. Данный сигнал может поступить либо от внешнего устройства, либо от контроллера прерываний КР580ВН59.

Микропроцессор может не обслуживать прерывания, если внутренний триггер разрешения прерываний сброшен. Если триггер установлен в единицу (сигнал INTA), то микропроцессор после получения запроса на прерывание сообщает другим модулям системы о выполняемом машинном цикле записью байта состояния (D0D1D5, равные «1») в регистр слова состояния или одноименный регистр внутри системного контроллера. После перехода в режим обслуживания микропроцессор осуществляет процедуру вызова подпрограммы прерывания.

Существует два пути перехода микропроцессора на подпрограмму обслуживания прерываний (указания адреса первой команды подпрограммы прерываний):

1. Формируется сигнал разрешения прерывания INTA на выходе микропроцессора. По этому сигналу возможна запись адреса первой команды подпрограммы обслуживания прерываний. (Существует однобайтная инструкция RST <AAA> для указания адреса перехода на ячейку памяти, где <AAA> - трехразрядный код).

2. Трехбайтная команда вызова CALL <A1><A2>, где A1 – младший байт, A2 – старший байт. CALL - код команды вызова. При наличии в системе контроллера прерывания и системного контроллера по первому разрешающему сигналу INTA, который формирует системный контроллер, контроллер прерываний выдает на шину данных код команды CALL. По двум последующим сигналам INTA системного контроллера на шину данных поступают младший и старший байты адреса первой команды подпрограммы обслуживания прерываний. До считывания указанного адреса перехода на подпрограмму прерывания, микропроцессор сохраняет содержимое программного счетчика в стеке, а также всю необходимую информацию, а в программный счетчик загружает поступивший адрес перехода. Далее выполняется подпрограмма обслуживания прерываний.

3) Состояние ожидания предполагает отключение микропроцессора от шины адреса и шины данных, формирование на его выходе сигнала ожидания WT, на входе осуществляется проверка сигнала готовности RDY. Сигнал RDY может отсутствовать из-за низкого быстродействия периферийного устройства. Данный режим позволяет синхронизировать работу более быстрого микропроцессора с более медленным внешним устройством.

4) Состояние останова предполагает также формирование сигнала ожидания WT на выходе микропроцессора, отключение его от шины адреса и шины данных. Переход в состояние останова осуществляется программно, по инструкции (сигнал HLT). Выход из этого состояния возможен только после перезагрузки (начальной установки).

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

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

Как известно программист, когда пишет программы работает не с физическим адресом, а только с логическим. И то если он программирует на ассемблере. В том же Си ячейки памяти от программиста уже скрыты указателями, для его же удобства, но если грубо говорить указатель это другое представление логического адреса памяти, а в Java и указателей нет, совсем плохой язык. Однако грамотному программисту не помешают знания о том как организована память хотя бы на общем уровне. Меня вообще очень огорчают программисты, которые не знают как работает машина, обычно это программисты Java и прочие php-парни, с квалификацией ниже плинтуса.

Так ладно, хватит о печальном, переходим к делу.
Рассмотрим адресное пространство программного режима 32 битного процессора (для 64 бит все по аналогии)
Адресное пространство этого режима будет состоять из 2^32 ячеек памяти пронумерованных от 0 и до 2^32-1.
Программист работает с этой памятью, если ему нужно определить переменную, он просто говорит ячейка памяти с адресом таким-то будет содержать такой-то тип данных, при этом сам програмист может и не знать какой номер у этой ячейки он просто напишет что-то вроде:
int data = 10;
компьютер поймет это так: нужно взять какую-то ячейку с номером стопицот и поместить в нее цело число 10. При том про адрес ячейки 18894 вы и не узнаете, он от вас будет скрыт.

Все бы хорошо, но возникает вопрос, а как компьютер ищет эту ячейку памяти, ведь память у нас может быть разная:
3 уровень кэша
2 уровень кэша
1 уровень кэша
основная память
жесткий диск

Это все разные памяти, но компьютер легко находит в какой из них лежит наша переменная int data.
Этот вопрос решается операционной системой совместно с процессором.
Вся дальнейшая статья будет посвящена разбору этого метода.

Архитектура х86 поддерживает стек.

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

push operand
помещает операнд в стек

pop operand
изымает из вершины стека значение и помещает его в свой операнд

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

Теперь кратко рассмотрим что такое регистры.
Это ячейки памяти в самом процессоре. Это самый быстрый и самый дорогой тип памяти, когда процессор совершает какие-то операции со значением или с памятью, он берет эти значения непосредственно из регистров.
В процессоре есть несколько наборов логик, каждая из которых имеет свои машинные коды и свои наборы регистров.
Basic program registers (Основные программные регистры) Эти регистры используются всеми программами с их помощью выполняется обработка целочисленных данных.
Floating Point Unit registers (FPU) Эти регистры работают с данными представленными в формате с плавающей точкой.
Еще есть MMX и XMM registers эти регистры используются тогда, когда вам надо выполнить одну инструкцию над большим количеством операндов.

Рассмотрим подробнее основные программные регистры. К ним относятся восемь 32 битных регистров общего назначения: EAX, EBX, ECX, EDX, EBP, ESI, EDI, ESP
Для того чтобы поместить в регистр данные, или для того чтобы изъять из регистра в ячейку памяти данные используется команда mov:

mov eax, 10
загружает число 10 в регистр eax.

mov data, ebx
копирует число, содержащееся в регистре ebx в ячейку памяти data.

Регистр ESP содержит адрес вершины стека.
Кроме регистров общего назначения, к основным программным регистрам относят шесть 16битных сегментных регистров: CS, DS, SS, ES, FS, GS, EFLAGS, EIP
EFLAGS показывает биты, так называемые флаги, которые отражают состояние процессора или характеризуют ход выполнения предыдущих команд.
В регистре EIP содержится адрес следующей команды, которая будет выполнятся процессором.
Я не буду расписывать регистры FPU, так как они нам не понадобятся. Итак наше небольшое отступление про регистры и стек закончилось переходим обратно к организации памяти.

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

Логический адрес --> Линейный (виртуальный)--> Физический

image


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

Линейный адрес вычисляется по формуле:
линейный адрес=Базовый адрес сегмента(на картинке это начало сегмента) + смещение
Сегмент кода

Базовый адрес сегмента кода берется из регистра CS. Значение смещения для сегмента кода берется из регистра EIP, в котором хранится адрес инструкции, после исполнения которой, значение EIP увеличивается на размер этой команды. Если команда занимает 4 байта, то значение EIP увеличивается на 4 байта и будет указывать уже на следующую инструкцию. Все это делается автоматически без участия программиста.
Сегментов кода может быть несколько в нашей памяти. В нашем случае он один.
Сегмент данных

Данные загружаются в регистры DS, ES, FS, GS
Это значит что сегментов данных может быть до 4х. На нашей картинке он один.
Смещение внутри сегмента данных задается как операнд команды. По дефолту используется сегмент на который указывает регистр DS. Для того чтобы войти в другой сегмент надо это непосредственно указать в команде префикса замены сегмента.
Сегмент стека

Используемый сегмент стека задается значением регистра SS.
Смещение внутри этого сегмента представлено регистром ESP, который указывает на вершину стека, как вы помните.
Сегменты в памяти могут друг друга перекрывать, мало того базовый адрес всех сегментов может совпадать например в нуле. Такой вырожденный случай называется линейным представлением памяти. В современных системах, память как правило так организована.

Теперь рассмотрим определение базовых адресов сегмента, я писал что они содержаться в регистрах SS, DS, CS, но это не совсем так, в них содержится некий 16 битный селектор, который указывает на некий дескриптор сегментов, в котором уже хранится необходимый адрес.

Так выглядит селектор, в тринадцати его битах содержится индекс дескриптора в таблице дескрипторов. Не хитро посчитать будет что 2^13 = 8192 это максимальное количество дескрипторов в таблице.
Вообще дескрипторных таблиц бывает два вида GDT и LDT Первая называется глобальная таблица дескрипторов, она в системе всегда только одна, ее начальный адрес, точнее адрес ее нулевого дескриптора хранится в 48 битном системном регистре GDTR. И с момента старта системы не меняется и в свопе не принимает участия.
А вот значения дескрипторов могут меняться. Если в селекторе бит TI равен нулю, тогда процессор просто идет в GDT ищет по индексу нужный дескриптор с помощью которого осуществляет доступ к этому сегменту.
Пока все просто было, но если TI равен 1 тогда это означает что использоваться будет LDT. Таблиц этих много, но использоваться в данный момент будет та селектор которой загружен в системный регистр LDTR, который в отличии от GDTR может меняться.
Индекс селектора указывает на дескриптор, который указывает уже не на базовый адрес сегмента, а на память в котором хранится локальная таблица дескрипторов, точнее ее нулевой элемент. Ну а дальше все так же как и с GDT. Таким образом во время работы локальные таблицы могут создаваться и уничтожаться по мере необходимости. LDT не могут содержать дескрипторы на другие LDT.
Итак мы знаем как процессор добирается до дескриптора, а что содержится в этом дескрипторе посмотрим на картинке:
Дескрипторы состоит из 8 байт.
Биты с 15-39 и 56-63 содержат линейный базовый адрес описываемым данным дескриптором сегмента. Напомню нашу формулу для нахождения линейного адреса:

линейный адрес = базовый адрес + смещение
[база; база+предел)

В зависимости от 55 G-бита(гранулярити), предел может измеряться в байтах при нулевом значении бита и тогда максимальный предел составит 1 мб, или в значении 1, предел измеряется страницами, каждая из которых равна 4кб. и максимальный размер такого сегмента будет 4Гб.
Для сегмента стека предел будет в интервале:
(база+предел; вершина]

Кстати интересно почему база и предел так рвано располагаются в дескрипторе. Дело в том что процессоры х86 развивались эволюционно и во времена 286х дескрипторы были по 8 бит всего, при этом старшие 2 байта были зарезервированы, ну а в последующих моделях процессоров с увеличением разрядности дескрипторы тоже выросли, но для сохранения обратной совместимости пришлось оставить структуру как есть.
Значение адреса «вершина» зависит от 54го D бита, если он равен 0, тогда вершина равна 0xFFF(64кб-1), если D бит равен 1, тогда вершина равна 0xFFFFFFFF (4Гб-1)
С 41-43 бит кодируется тип сегмента.
000 — сегмент данных, только считывание
001 — сегмент данных, считывание и запись
010 — сегмент стека, только считывание
011 — сегмент стека, считывание и запись
100 — сегмент кода, только выполнение
101- сегмент кода, считывание и выполнение
110 — подчиненный сегмент кода, только выполнение
111 — подчиненный сегмент кода, только выполнение и считывание

44 S бит если равен 1 тогда дескриптор описывает реальный сегмент оперативной памяти, иначе значение S бита равно 0.

Самым важным битом является 47-й P бит присутствия. Если бит равен 1 значит, что сегмент или локальная таблица дескрипторов загружена в оперативку, если этот бит равен 0, тогда это означает что данного сегмента в оперативке нет, он находится на жестком диске, случается прерывание, особый случай работы процессора запускается обработчик особого случая, который загружает нужный сегмент с жесткого диска в память, если P бит равен 0, тогда все поля дескриптора теряют смысл, и становятся свободными для сохранения в них служебной информации. После завершения работы обработчика, P бит устанавливается в значение 1, и производится повторное обращение к дескриптору, сегмент которого находится уже в памяти.

На этом заканчивается преобразование логического адреса в линейный, и я думаю на этом стоит прерваться. В следующий раз я расскажу вторую часть преобразования из линейного в физический.
А так же думаю стоит немного поговорить о передачи аргументов функции, и о размещении переменных в памяти, чтобы была какая-то связь с реальностью, потому размещение переменных в памяти это уже непосредственно, то с чем вам приходится сталкиваться в работе, а не просто какие-то теоретические измышления для системного программиста. Но без понимания, как устроена память невозможно понять как эти самые переменные хранятся в памяти.
В общем надеюсь было интересно и до новых встреч.

Машинный код или машинный язык представляет собой набор инструкций, выполняемых непосредственно центральным процессором компьютера (CPU). Каждая команда выполняет очень конкретную задачу, например, загрузки (load), перехода (jump) или элементарной арифметической или логической операции для единицы данных в регистре процессора или памяти. Каждая программа выполняется непосредственно процессором и состоит из ряда таких инструкций.

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

Почти все практические программы сегодня написаны на языках более высокого уровня или ассемблере. Исходный код затем транслируется в исполняемый машинный код с помощью таких утилит, как интерпретаторы, компиляторы, ассемблеры, и/или линкеры. [Источник 1]

Содержание

Инструкции машинного кода (ISA)

Каждый процессор или семейство процессоров имеет свой собственный набор инструкций машинного кода. Инструкции являются паттернами битов, которые в силу физического устройства соответствуют различным командам машины. Говорят, что процессор A совместим с процессором B, если процессор A полностью «понимает» машинный код процессора B. Если процессоры A и B имеют некоторое подмножество инструкций, по которым они взаимно совместимы, то говорят, что они одной архитектуры. Таким образом, набор команд является специфическим для одного класса процессоров. Новые процессоры одной архитектуры часто включают в себя все инструкции предшественника и могут включать дополнительные. Иногда новые процессоры прекращают поддержку или изменяют значение какого-либо кода команды (как правило, потому, что это необходимо для новых целей), влияя на совместимость кода до некоторой степени; даже почти полностью совместимые процессоры могут показать различное поведение для некоторых команд, но это редко является проблемой.

Системы также могут отличаться в других деталях, таких как расположение памяти, операционные системы или периферийные устройства. Поскольку программа обычно зависит от таких факторов, различные системы, как правило, не запустят один и тот же машинный код, даже если используется тот же тип процессора. [Источник 2]

Виды ISA

x86 всегда был архитектурой с инструкциями переменной длины, так что когда пришла 64-битная эра, расширения x64 не очень сильно повлияли на ISA. ARM это RISC-процессор разработанный с учетом инструкций одинаковой длины, что было некоторым преимуществом в прошлом. Так что в самом начале все инструкции ARM кодировались 4-мя байтами. Это то, что сейчас называется «режим ARM».

На самом деле, самые используемые инструкции процессора на практике могут быть закодированы c использованием меньшего количества информации. Так что была добавлена ISA с названием Thumb, где каждая инструкция кодируется всего лишь 2-мя байтами. Теперь это называется «режим Thumb». Но не все инструкции ARM могут быть закодированы в двух байтах, так что набор инструкций Thumb ограниченный. Код, скомпилированный для режима ARM и Thumb может сосуществовать в одной программе. Затем создатели ARM решили, что Thumb можно расширить: так появился Thumb-2 (в ARMv7). Thumb-2 это всё ещё двухбайтные инструкции, но некоторые новые инструкции имеют длину 4 байта. Распространено заблуждение, что Thumb-2 — это смесь ARM и Thumb. Это неверно. Режим Thumb-2 был дополнен до более полной поддержки возможностей процессора и теперь может легко конкурировать с режимом ARM. Основное количество приложений для iPod/iPhone/iPad скомпилировано для набора инструкций Thumb-2, потому что Xcode делает так по умолчанию. Потом появился 64-битный ARM. Это ISA снова с 4-байтными инструкциями, без дополнительного режима Thumb. Но 64-битные требования повлияли на ISA, так что теперь у нас 3 набора инструкций ARM: режим ARM, режим Thumb (включая Thumb-2) и ARM64. Эти наборы инструкций частично пересекаются, но можно сказать, это скорее разные наборы, нежели вариации одного. Существует ещё много RISC ISA с инструкциями фиксированной 32-битной длины — это как минимум MIPS, PowerPC и Alpha AXP. [Источник 3]

Выполнение инструкций

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

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

Абсолютный и позиционно-независимый код

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

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

Хранение в памяти

Гарвардская архитектура представляет собой компьютерную архитектуру с физически разделенным хранением сигнальных путей для инструкций и данных. На сегодняшний день, в большинстве процессоров реализованы отдельные сигнальные пути для повышения производительности. Модифицированная Гарвардская архитектура поддерживает такие задачи, как загрузка исполняемой программы из дисковой памяти в качестве данных, а затем её выполнение. Гарвардская архитектура контрастирует с архитектурой фон Неймана, где данные и код хранятся в памяти вместе, и считываются процессором, позволяя компьютеру выполнять команды.

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

Связь с языками программирования

Ассемблерные языки

Гораздо более читаемым представлением машинного языка называется язык ассемблера, использующий мнемонические коды для обозначения инструкций машинного кода, а не с помощью числовых значений. Например, на процессоре Zilog Z80, машинный код 00000101, который дает указание процессору декрементировать регистр процессора B, будет представлен на языке ассемблера как DEC B.

Связь с микрокодом

В некоторых компьютерных архитектурах, машинный код реализуется с помощью более фундаментального базового слоя программ, называемых микропрограммами, обеспечивающими общий интерфейс машинного языка для линейки различных моделей компьютеров с самыми различными базовыми потоками данных. Это делается для облегчения портирования программ на машинном языке между различными моделями. Примером такого использования являются компьютеры IBM System/360 и их наследники. Несмотря на то, что ширина потоков данных разнится от 8 до 64 бит и более, тем не менее они представляют общую архитектуру на уровне машинного языка по всей линейке.

Использование микрокода для реализации эмулятора позволяет компьютеру симулировать совершенно другую архитектуру. Семейство System / 360 использовало это для портирования программ с более ранних машин IBM на новые семейства компьютеров, например на IBM 1401/1440/1460.

Связь с байткодом

Машинный код, как правило, отличается от байт-кода (также известного как р-код), который либо выполняется интерпретатором, или сам компилируется в машинный код для более быстрого исполнения. Исключением является ситуация, когда процессор предназначен для использования конкретного байт-кода как машинного, например, как в случае с процессорами Java. Машинный и ассемблерный код иногда называют собственным (внутренним) кодом ЭВМ, когда ссылаются на платформо-зависимые части свойств или библиотек языка. [Источник 4]

Примеры

Пример MIPS 32-bit инструкции

Набор инструкций MIPS – пример машинного кода с инструкциями фиксированной длины – 32 бита. Тип инструкции содержится в поле op (поле операции) – первые 6 бит. Например типы инструкций перехода или немедленных операций полностью определяются этим полем. Инструкции регистров включают дополнительное поле funct, для определения конкретной операции. Все поля, использущиеся в данных типах инструкций:

Rs,rt и rd – индикаторы задействования регистров, shamt – параметр сдвига,а поле address/immediate явно содержит операнд.

Пример: сложение значений в регистрах 1 и 2 и запись результата в регистр 6:

Пример: загрузка значения в регистр 8, взятое из ячейки памяти, находящейся на 68 ячеек дальше, чем адрес, находящийся в регистре 3:

Пример: переход к адресу 1024:

Пример для x86 (MS DOS) – “Hello, World!”

Программа «Hello, world!» для процессора архитектуры x86 (ОС MS-DOS, вывод при помощи BIOS прерывания int 10h) выглядит следующим образом (в шестнадцатеричном представлении):

BB 11 01 B9 0D 00 B4 0E 8A 07 43 CD 10 E2 F9 CD 20 48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21


Данная программа работает при её размещении по смещению 10016. Отдельные инструкции выделены цветом:

2.1. Процессор.

Самый основной элемент компьютера, это, конечно, процессор. Давайте подробней его рассмотрим. Упрощённая структура процессора (рис. 4):


Рис. 4. Упрощённая структура процессора

Основные элементы процессора:

· Регистры – это специальные ячейки памяти, физически расположенные внутри процессора. В отличие от ОЗУ, где для обращения к данным требуется использовать шину адреса, к регистрам процессор может обращаться напрямую. Это существенно ускорят работу с данными.

· Арифметико-логическое устройство выполняет арифметические операции, такие как сложение, вычитание, а также логические операции.

· Блок управления определяет последовательность микрокоманд, выполняемых при обработке машинных кодов (команд).

· Тактовый генератор , или генератор тактовых импульсов, задаёт рабочую частоту процессора.

2.2. Режимы работы процессора.

Процессор архитектуры x86 может работать в одном из пяти режимов и переключаться между ними очень быстро:

1. Реальный (незащищенный) режим (real address mode) — режим, в котором работал процессор 8086. В современных процессорах этот режим поддерживается в основном для совместимости с древним программным обеспечением (DOS-программами).

2. Защищенный режим (protected mode) — режим, который впервые был реализован в 80286 процессоре. Все современные операционные системы (Windows, Linux и пр.) работают в защищенном режиме. Программы реального режима не могут функционировать в защищенном режиме.

3. Режим виртуального процессора 8086 (virtual-8086 mode, V86) — в этот режим можно перейти только из защищенного режима. Служит для обеспечения функционирования программ реального режима, причем дает возможность одновременной работы нескольких таких программ, что в реальном режиме невозможно. Режим V86 предоставляет аппаратные средства для формирования виртуальной машины, эмулирующей процессор8086. Виртуальная машина формируется программными средствами операционной системы. В Windows такая виртуальная машина называется VDM (Virtual DOS Machine — виртуальная машина DOS). VDM перехватывает и обрабатывает системные вызовы от работающих DOS-приложений.

4. Нереальный режим (unreal mode, он же big real mode) — аналогичен реальному режиму, только позволяет получать доступ ко всей физической памяти, что невозможно в реальном режиме.

5. Режим системного управления System Management Mode (SMM) используется в служебных и отладочных целях.

При загрузке компьютера процессор всегда находится в реальном режиме, в этом режиме работали первые операционные системы, например MS-DOS, однако современные операционные системы, такие как Windows и Linux переводят процессор в защищенный режим. Вам, наверное, интересно, что защищает процессор в защищенном режиме? В защищенном режиме процессор защищает выполняемые программы в памяти от взаимного влияния (умышленно или по ошибке) друг на друга, что легко может произойти в реальном режиме. Поэтому защищенный режим и назвали защищенным.

2.3. Регистры процессора (программная модель процессора).

Для понимания работы команд ассемблера необходимо четко представлять, как выполняется адресация данных, какие регистры процессора и как могут использоваться при выполнении инструкций. Рассмотрим базовую программную модель процессоров Intel 80386, в которую входят:

· 8 регистров общего назначения, служащих для хранения данных и указателей;

· регистры сегментов — они хранят 6 селекторов сегментов;

· регистр управления и контроля EFLAGS, который позволяет управлять состоянием выполнения программы и состоянием (на уровне приложения) процессора;

· регистр-указатель EIP выполняемой следующей инструкции процессора;

· система команд (инструкций) процессора;

· режимы адресации данных в командах процессора.

Начнем с описания базовых регистров процессора Intel 80386.

Базовые регистры процессора Intel 80386 являются основой для разработки программ и позволяют решать основные задачи по обработке данных. Все они показаны на рис. 5.


Рис. 5. Базовые регистры процессора Intel 80386

Среди базового набора регистров выделим отдельные группы и рассмотрим их назначение.

2.4. Регистры общего назначения.

Остальные четыре регистра – ESI (индекс источника), EDI (индекс приемника), ЕВР (указатель базы), ESP (указатель стека) – имеют более конкретное назначение и применяются для хранения всевозможных временных переменных. Регистры ESI и EDI необходимы в строковых операциях, ЕВР и ESP – при работе со стеком. Так же как и в случае с регистрами ЕАХ - EDX, младшие половины этих четырех регистров называются SI, DI, BP и SP соответственно, и в процессорах до 80386 только они и присутствовали.

2.5. Сегментные регистры.

При использовании сегментированных моделей памяти для формирования любого адреса нужны два числа – адрес начала сегмента и смещение искомого байта относительно этого начала (в бессегментной модели памяти flat адреса начал всех сегментов равны). Операционные системы (кроме DOS) могут размещать сегменты, с которыми работает программа пользователя, в разных местах памяти и даже временно записывать их на диск, если памяти не хватает. Так как сегменты способны оказаться где угодно, программа обращается к ним, применяя вместо настоящего адреса начала сегмента 16-битное число, называемое селектором. В процессорах Intel предусмотрено шесть 16-битных регистров - CS, DS, ES, FS, GS, SS , где хранятся селекторы. (Регистры FS и GS отсутствовали в 8086, но появились уже в 80286.) Это означает, что в любой момент можно изменить параметры, записанные в этих регистрах.

В отличие от DS, ES, GS, FS, которые называются регистрами сегментов данных, CS и SS отвечают за сегменты двух особенных типов – сегмент кода и сегмент стека. Первый содержит программу, исполняющуюся в данный момент, следовательно, запись нового селектора в этот регистр приводит к тому, что далее будет исполнена не следующая по тексту программы команда, а команда из кода, находящегося в другом сегменте, с тем же смещением. Смещение очередной выполняемой команды всегда хранится в специальном регистре EIP (указатель инструкции, 16-битная форма IP), запись в который так же приведет к тому, что далее будет исполнена какая-нибудь другая команда. На самом деле все команды передачи управления – перехода, условного перехода, цикла, вызова подпрограммы и т.п. – и осуществляют эту самую запись в CS и EIP.

2.6. Регистр флагов.

Еще один важный регистр, использующийся при выполнении большинства команд, - регистр флагов. Как и раньше, его младшие 16 бит, представлявшие собой весь этот регистр до процессора 80386, называются FLAGS. В EFLAGS каждый бит является флагом, то есть устанавливается в 1 при определенных условиях или установка его в 1 изменяет поведение процессора. Все флаги, расположенные в старшем слове регистра, имеют отношение к управлению защищенным режимом, поэтому здесь рассмотрен только регистр FLAGS (см. рис. 6):


Рис. 6. Регистр флагов FLAGS.

CF – флаг переноса. Устанавливается в 1, если результат предыдущей операции не уместился в приемнике и произошел перенос из старшего бита или если требуется заем (при вычитании), в противном случае – в 0. Например, после сложения слова 0 FFFFh и 1, если регистр, в который надо поместить результат, – слово, в него будет записано 0000 h и флаг CF = 1.

PF – флаг четности. Устанавливается в 1, если младший байт результата предыдущей команды содержит четное число битов, равных 1, и в 0, если нечетное. Это не то же самое, что делимость на два. Число делится на два без остатка, если его самый младший бит равен нулю, и не делится, когда он равен 1.

AF – флаг полупереноса или вспомогательного переноса. Устанавливается в 1, если в результате предыдущей операции произошел перенос (или заем) из третьего бита в четвертый. Этот флаг используется автоматически командами двоично-десятичной коррекции.

ZF – флаг нуля. Устанавливается в 1, если результат предыдущей команды – ноль.

SF – флаг знака. Он всегда равен старшему биту результата.

TF – флаг ловушки. Он был предусмотрен для работы отладчиков, не использующих защищенный режим. Установка его в 1 приводит к тому, что после выполнения каждой программной команды управление временно передается отладчику.

IF – флаг прерываний. Сброс этого флага в 0 приводит к тому, что процессор перестает обрабатывать прерывания от внешних устройств. Обычно его сбрасывают на короткое время для выполнения критических участков кода.

DF – флаг направления. Он контролирует поведение команд обработки строк: когда он установлен в 1, строки обрабатываются в сторону уменьшения адресов, когда DF =0 – наоборот.

OF – флаг переполнения. Он устанавливается в 1, если результат предыдущей арифметической операции над числами со знаком выходит за допустимые для них пределы. Например, если при сложении двух положительных чисел получается число со старшим битом, равным единице, то есть отрицательное, и наоборот.

Флаги IOPL (уровень привилегий ввода-вывода) и NT (вложенная задача) применяются в защищенном режиме.

2.7. Цикл выполнения команды

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

Для того чтобы процессор знал, какую команду нужно выполнять в определённый момент, существует счётчик команд – специальный регистр, в котором хранится адрес команды, которая должна быть выполнена после выполнения текущей команды. То есть при запуске программы в этом регистре хранится адрес первой команды. В процессорах Intel в качестве счётчика команд (его ещё называют указатель команды) используется регистр EIP (или IP в 16-разрядных программах).

Счётчик команд работает со сверхоперативной памятью, которая находится внутри процессора. Эта память носит название очередь команд, куда помещается одна или несколько команд непосредственно перед их выполнением. То есть в счётчике команд хранится адрес команды в очереди команд, а не адрес оперативной памяти.

Цикл выполнения команды – это последовательность действий, которая совершается процессором при выполнении одной машинной команды. При выполнении каждой машинной команды процессор должен выполнить как минимум три действия: выборку, декодирование и выполнение. Если в команде используется операнд, расположенный в оперативной памяти, то процессору придётся выполнить ещё две операции: выборку операнда из памяти и запись результата в память. Ниже описаны эти пять операций.

  • Выборка команды . Блок управления извлекает команду из памяти (из очереди команд), копирует её во внутреннюю память процессора и увеличивает значение счётчика команд на длину этой команды (разные команды могут иметь разный размер).
  • Декодирование команды . Блок управления определяет тип выполняемой команды, пересылает указанные в ней операнды в АЛУ и генерирует электрические сигналы управления АЛУ, которые соответствуют типу выполняемой операции.
  • Выборка операндов . Если в команде используется операнд, расположенный в оперативной памяти, то блок управления начинает операцию по его выборке из памяти.
  • Выполнение команды . АЛУ выполняет указанную в команде операцию, сохраняет полученный результат в заданном месте и обновляет состояние флагов, по значению которых программа может судить о результате выполнения команды.
  • Запись результата в память . Если результат выполнения команды должен быть сохранён в памяти, блок управления начинает операцию сохранения данных в памяти.

Суммируем полученные знания и составим цикл выполнения команды:

  1. Выбрать из очереди команд команду, на которую указывает счётчик команд.
  2. Определить адрес следующей команды в очереди команд и записать адрес следующей команды в счётчик команд.
  3. Декодировать команду.
  4. Если в команде есть операнды, находящиеся в памяти, то выбрать операнды.
  5. Выполнить команду и установить флаги.
  6. Записать результат в память (по необходимости).
  7. Начать выполнение следующей команды с п.1.

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

Читайте также: