Команды ЭВМ. Машинные коды и команды ассемблера. Функциональные группы команд.
Важной составной частью архитектуры ЭВМ является система команд. Несмотря на большое число разновидностей ЭВМ, на самом низком (“машинном”) уровне они имеют много общего.
Под командой понимают совокупность сведений, представленных в виде двоичных кодов, необходимых процессору для выполнения очередного шага. В ходе команды для сведений о типе операции, адресной информации о нахождении обрабатываемых данных, а также для информации о месте хранения результатов выделяются определенные разряды (поля).
Система команд любой ЭВМ обязательно содержит следующие группы команд обработки информации:
1. Команды передачи данных (перепись), копирующие информациюиз одного места в другое.
2. Арифметические операции, которым фактически обязана своим названием вычислительная техника. Конечно, доля вычислительных действий в современном компьютере заметно уменьшилась, но они по-прежнему играют в программах важную роль. Отметим, что к основным арифметическим действиям обычно относятся сложение и вычитание (последнее в конечном счете чаще всего тем или иным способом также сводится к сложению). Что касается умножения и деления, то они во многих ЭВМ выполняются по специальным программам.
3. Логические операции, позволяющие компьютеру анализировать обрабатываемую информацию. Простейшими примерами могут служить сравнение, а также известные логические операции И, ИЛИ, НЕ (инверсия). Кроме того к ним часто добавляются анализ отдельных битов кода, их сброс и установка.
4. Сдвиги двоичного кода влево и вправо. Для доказательства важности этой группы команд достаточно вспомнить правило умножения столбиком: каждое последующее произведение записывается в такой схеме со сдвигом на одну цифру влево. В некоторых частных случаях умножение и деление вообще может быть заменено сдвигом (вспомните, что дописав или убрав ноль справа, т.е. фактически осуществляя сдвиг десятичного числа, можно увеличить или уменьшить его в 10 раз).
5. Команды ввода и вывода информации для обмена с внешними устройствами. В некоторых ЭВМ внешние устройства являются специальными служебными адресами памяти, поэтому ввод и вывод осуществляется с помощью команд переписи.
6. Команды управления, реализующие нелинейные алгоритмы. Сюда прежде всего следует отнести условный и безусловный переход, а также команды обращения к подпрограмме (переход с возвратом). Некоторые ЭВМ имеют специальные команды для организации циклов, но это не обязательно: цикл может быть сведен к той или иной комбинации условного и безусловного переходов. Часто к этой же группе команд относят немногочисленные операции по управлению процессором -типа “останов” или НОП (“нет операции”). Иногда их выделяют в особую группу.
С ростом сложности устройства процессора увеличивается и число команд, воздействующих на них. Здесь для примера можно назвать биты режима работы процессора и биты управления механизмами прерываний от внешних устройств.
В последнее время все большую роль в наборе команд играют команды для преобразования из одного формата данных в другой (например, из 8-битного в 16-битный и т.п.), которые заметно упрощают обработку данных разного типа, но в принципе могут быть заменены последовательностью из нескольких более простых команд.
Рассматриваясистему команд, нельзя не упомянуть о двух современных взаимно конкурирующих направлениях в ее построении: компьютер с полным набором команд CISC (Complex Instruction Set Computer) и с ограниченным набором – RISC (Reduced Instruction Set Computer). Разделение возникло из-за того, что основную часть времени компьютеру приходится выполнять небольшую часть из своего набора команд, остальные же используются эпизодически. Таким образом, если существенно ограничить набор операций до наиболее простых и коротких, зато тщательно оптимизировать их, получится достаточно эффективная и быстродействующая RISC-машина. Правда за скорость придется платить необходимостью программной реализации “отброшенных” команд, но часто эта плата бывает оправданной: например, для научных расчетов или машинной графики быстродействие существенно важнее проблем программирования.
Как уже отмечалось в п.2, важной составной частью фон-неймановской архитектуры является счетчик адреса команд. Этот специальный внутренний регистр процессора всегда указывает на ячейку памяти, в которой хранится следующая команда программы. При включении питания или при нажатии на кнопку сброса (начальной установки) в счетчик аппаратно заносится стартовый адрес находящейся в ПЗУ программы инициализации всех устройств и начальной загрузки. Дальнейшее функционирование компьютера определяется программой. Таким образом, вся деятельность ЭВМ – это непрерывное выполнение тех или иных программ, причем программы эти могут в свою очередь загружать новые программы и т.д.
Каждая программа состоит из отдельных машинных команд. Каждая машинная команда, в свою очередь, делится на ряд элементарных унифицированных составных частей, которые принято называть тактами. В зависимости от сложности команды она может быть реализована за разное число тактов. Например, пересылка информации из одного внутреннего регистра процессора в другой выполняется за несколько тактов, а для перемножения двух целых чисел их требуется на порядок больше. Существенное удлинение команды происходит, если обрабатываемые данные еще не находятся внутри процессора и их приходится считывать из ОЗУ.
При выполнении каждой команды ЭВМ проделывает определенные стандартные действия:
1) согласно содержимому счетчика адреса команд, считывается очередная команда программы (ее код обычно заносится на хранение в специальный регистр УУ, который носит название регистра команд);
2) счетчик команд автоматически изменяется так, чтобы в нем содержался адрес следующей команды (в простейшем случае для этой цели достаточно к текущему значению счетчика прибавить некоторую константу, определяющуюся длиной команды);
3) считанная в регистр команд операция расшифровывается, извлекаются необходимые данные и над ними выполняются требуемые действия.
Затем во всех случаях, за исключением команды останова или наступления прерывания (см. ниже в п. 3.5), все описанные действия циклически повторяются.
После выборки команды останова ЭВМ прекращает обработку программы. Для выхода из этого состояния требуется либо запрос от внешних устройств, либо перезапуск машины.
Рассмотренный основной алгоритм работы ЭВМ позволяет шаг за шагом выполнить хранящуюся в ОЗУ линейную программу. Если же требуется изменить порядок вычислений для реализации развилки или цикла, достаточно в счетчик команд занести требуемый адрес (именно так происходит условный или безусловный переход).
В компьютерах на базе микропроцессоров INTEL 80286 и более поздних моделей для ускорения основного цикла выполнения команды используется метод конвейеризации (иногда применяется термин “опережающая выборка”). Идея состоит в том, что несколько внутренних устройств процессора работают параллельно: одно считывает команду, другое дешифрует операцию, третье вычисляет адреса используемых операндов и т.д. В результате по окончании команды чаще всего оказывается, что следующая уже выбрана из ОЗУ, дешифрована и подготовлена к исполнению. Отметим, что в случае нарушения естественного порядка выполнения команд в программе (например, при безусловном переходе) опережающая выборка оказывается напрасной и конвейер очищается. Следующая за переходом команда выполняется дольше, так как, чтобы конвейер “заработал на полную мощность”, необходимо его предварительно заполнить. Иными словами, в конвейерной машине время выполнения программы может зависеть не только от составляющих ее команд, но и от их взаимного расположения.
Cпособы указания адреса расположения информации Команда ЭВМ обычно состоит из двух частей – операционной и адресной. Операционная часть (иначе она еще называется кодом операции – КОП) указывает, какое действие необходимо выполнить с информацией. Адресная часть описывает, где используемая информация хранится. У нескольких немногочисленных команд управления работой машины адресная часть может отсутствовать, например, в команде останова; операционная часть имеется, всегда.
Код операции можно представить себе как некоторый условный номер в общем списке системы команд. В основном этот список построен в соответствии с определенными внутренними закономерностями, хотя они не всегда очевидны.
Адресная часть обладает значительно большим разнообразием и ее следует рассмотреть подробнее.
Прежде всего отметим, что команды могут быть одно-, двух- и трехадресные в зависимости от числа участвующих в них операндов.
Первые ЭВМ имели наиболее простую и наглядную трехадресную систему команд. Например: взять числа из адресов памяти А1 и А2, сложить их и сумму поместить в адрес A3. Если для операции требовалось меньшее число адресов, то лишние просто не использовались. Скажем, в операции переписи указывались лишь ячейки источника и приемника информации А1 и A3, а содержимое А2 не имело никакого значения.
Трехадресная команда легко расшифровывалась и была удобна в использовании, но с ростом объемов ОЗУ ее длина становилась непомерно большой. Действительно, длина команды складывается из длины трех адресов и кода операции. Отсюда следует, например, что для скромного ОЗУ из 1024 ячеек только для записи адресной части одной команды требуется 3*10 = 30 двоичных разрядов, что для технической реализации не очень удобно. Поэтому появились двухадресные машины, длина команды в которых сокращалась за счет исключения адреса записи результата. В таких ЭВМ результат операции оставался в специальном регистре (сумматоре) и был пригоден для использования в последующих вычислениях. В некоторых машинах результат записывался вместо одного из операндов. Дальнейшее упрощение команды привело к созданию одноадресных машин.
Машинный код (также употребляются термины собственные коды, или платформенно-ориентированные коды, или родной код, или нативный код — от англ. native code) — система команд (язык) конкретной вычислительной машины (машинный язык), который интерпретируется непосредственно микропроцессором или микропрограммами данной вычислительной машины.
Каждая модель процессора имеет свой собственный машинный язык, хотя во многих моделях эти наборы команд сильно перекрываются. Говорят, что процессор A совместим с процессором B, если процессор A полностью «понимает» машинный код процессора B. Если процессор A знает несколько команд, которых не понимает процессор B, то B несовместим с A.
«Слова» машинного языка называются машинными инструкциями. Каждая из них описывает элементарное действие, выполняемое процессором, такое как «переслать байт из памяти в регистр». Программа — это просто длинный список инструкций, выполняемых процессором. Раньше процессоры просто выполняли инструкции одну за другой, но новые суперскалярные процессоры способны выполнять несколько инструкций за раз. Прямой поток выполнения команд может быть изменён инструкцией перехода, которая переносит выполнение на инструкцию с заданным адресом. Инструкция перехода может быть условной, выполняющей переход только при соблюдении некоторого условия.
Также инструкции бывают постоянной длины (у RISC, MISC-архитектур) и диапазонной (у CISC-архитектур; например, для архитектуры x86 команда имеет длину от 8 до 120 битов).
Язык ассемблера (автокод) — язык программирования «низкого уровня». В отличие от языка машинных кодов позволяет использовать более удобные для человека мнемонические (символьные) обозначения команд. При этом для перевода с языка ассемблера в понимаемый процессором машинный код требуется специальная программа, также называемая ассемблером. (Ассе́мблер (от англ. assembler — рабочий-сборщик) — компьютерная программа, компилятор исходного текста программы написанной на языке ассемблера, в программу на машинном коде. Как и сам язык (ассемблера), ассемблеры, как правило, специфичны конкретной архитектуре, операционной системе и варианту синтаксиса языка. Вместе с тем существуют, мультиплатформенные или вовсе универсальные (точнее, ограниченно-универсальные, потому что на языке низкого уровня нельзя написать аппаратно-независимые программы) ассемблеры, которые могут работать на разных платформах и операционных системах. Среди последних можно также выделить группу кросс-ассемблеров, способных собирать машинный код и исполняемые модули (файлы) для других архитектур и ОС).
Команды языка ассемблера один к одному соответствуют командам процессора, фактически, они представляют собой более удобную для человека символьную форму записи (мнемокод) команд и их аргументов. Кроме того, язык ассемблера обеспечивает использование символических меток вместо адресов ячеек памяти, которые при ассемблировании заменяются на автоматически рассчитываемые абсолютные или относительные адреса, а также так называемых директив [1] .
Директивы ассемблера позволяют, в частности, включать блоки данных, задать ассемблирование фрагмента программы по условию, задать значения меток, использовать макроопределения с параметрами.
Каждая модель (или семейство) процессоров имеет свой набор команд и соответствующий ему язык ассемблера (автокод).
Существуют ЭВМ, реализующие в качестве машинного язык программирования высокого уровня (Forth, Lisp,Эль-76), фактически в них он является «ассемблером».
Машинный код пример
Машинный код — это ни что иное, как набор инструкций, написанных на машинном языке и понимаемых компьютером. Для большинства программистов, особенно для начинающих, машинный код может показаться сложным и запутанным. Однако, понимание машинного кода является важным навыком для любого разработчика программного обеспечения. В этой статье мы рассмотрим несколько примеров машинного кода, которые помогут вам лучше понять эту технологию.
Один из примеров машинного кода, который можно использовать для изучения, — это код, который складывает два числа. Например, если у нас есть два числа — 5 и 7, то мы можем написать машинный код, который сложит эти два числа и выведет результат. Код может выглядеть примерно так:
00110001 00000100 00001000
Другой интересный пример машинного кода — это код, который осуществляет простую арифметическую операцию — умножение. Если мы хотим умножить два числа — 3 и 4, то мы можем использовать следующий машинный код:
00000011 00000101 00000100
Это только небольшая часть того, что можно сделать с машинным кодом. Как программист, вы можете использовать машинный код для создания сложных алгоритмов, обработки данных и многого другого. Понимание машинного кода может не только помочь вам лучше понять внутреннее устройство компьютера, но и дать вам больше контроля над вашим программным обеспечением.
Что такое машинный код?
Машинный код обычно представлен в виде последовательности байтов (цифр в двоичной системе счисления), которые кодируют определенные операции процессора. Каждая команда машинного кода может выполнять основные операции, такие как сложение, вычитание, перемещение данных и управление выполнением программы.
Машинный код полностью зависит от конкретного процессора и его архитектуры. Каждый процессор имеет свой набор инструкций и формат данных, поэтому машинный код, предназначенный для одного процессора, может быть непонятен или неисполним на другом.
Программистам обычно не требуется писать программы на машинном коде, так как это очень низкоуровневый и сложный язык. Однако понимание машинного кода может быть полезным для оптимизации и отладки программ, а также для создания компиляторов и интерпретаторов.
Несмотря на то, что машинный код может показаться громоздким и трудным для чтения, с определенным опытом программисты могут научиться интерпретировать и анализировать его. Исходный код программы, написанной на высокоуровневом языке программирования, может быть скомпилирован в машинный код для исполнения компьютером.
Понятие и основные принципы
Основные принципы машинного кода:
- Низкоуровневость — машинный код понимает и выполняет процессор компьютера без каких-либо преобразований. Каждая инструкция машинного кода соответствует определенному микрооперации, выполняемой процессором.
- Зависимость от архитектуры компьютера — каждый процессор имеет свою собственную архитектуру и набор инструкций машинного кода. Поэтому программы, написанные для одной архитектуры, не будут работать на другой.
Для удобства восприятия машинного кода программистами разработаны высокоуровневые языки программирования, которые позволяют писать код на более понятном для человека уровне. Однако для понимания работы компьютеров и оптимизации программы важно понимать основные принципы и уметь работать с машинным кодом.
Смотрите также: Установка последней версии Python на Ubuntu
Примеры машинного кода для программистов
Программистам может быть полезно изучать машинный код, чтобы лучше понимать, как работает компьютер и как выполняются их программы на более высокоуровневых языках программирования. Ниже приведены некоторые примеры машинного кода для программистов:
- MOV AX, 5 Эта инструкция перемещает значение 5 в регистр AX. В языке программирования x86, AX — это 16-битный регистр общего назначения, который может использоваться для хранения целочисленных значений.
- ADD BX, AX Эта инструкция складывает значения, хранящиеся в регистрах BX и AX, и сохраняет результат в регистре BX. Подобные инструкции позволяют программисту выполнять арифметические операции, такие как сложение, вычитание и умножение.
- CMP CX, 10 Эта инструкция сравнивает значение, хранящееся в регистре CX, с числом 10. Результат сравнения сохраняется во флагах процессора и может быть использован для принятия решения о переходе к другой части программы.
- JNZ LOOP_START Эта инструкция отвечает за условный переход в программе. Если во флагах процессора результат предыдущего сравнения не равен нулю, то выполнение программы будет перенесено к метке LOOP_START, которая определена в другом месте программы.
Это только несколько примеров машинного кода, которые могут быть полезны программистам. Изучение машинного кода помогает лучше понять работу компьютера, оптимизировать программы и владеть различными низкоуровневыми техниками программирования.
Простые примеры для новичков
Машинный код может показаться сложным и непонятным для новичков в программировании. Однако, с помощью простых примеров можно легко понять основы и начать разбираться в этой теме.
- Пример простого машинного кода для сложения двух чисел:
- mov ax, 5
- add ax, 3
В данном примере мы загружаем значение 5 в регистр ax с помощью команды mov, а затем прибавляем к нему значение 3 с помощью команды add. Результат будет сохранен в том же регистре ax.
- mov ah, 0x09
- mov dx, offset message
- int 0x21
- message db ‘Hello, world!’,0
В данном примере мы используем прерывание 0x21 для вызова функции, которая будет отображать текст на экране. С помощью команд mov мы загружаем значения регистров ah и dx, указывающие на соответствующие данные, а затем вызываем прерывание int 0x21. В переменной message хранится сам текст, который будет выведен.
- mov ax, 5
- cmp ax, 10
- jg greater
- less:
- mov bx, 0
- jmp end
- greater:
- mov bx, 1
- end:
В данном примере мы сравниваем значение регистра ax с числом 10 с помощью команды cmp. Затем с помощью команды jg мы проверяем условие и переходим к метке greater, если регистр ax больше 10. В противном случае, мы переходим к метке less. В зависимости от условия, мы выполняем различные инструкции, в данном случае, просто сохраняем значение 0 или 1 в регистр bx.
Таким образом, посредством этих простых примеров можно начать понимать и использовать машинный код при программировании.
Примеры сложного машинного кода
Машинный код представляет собой набор инструкций, которые процессор может прочитать и выполнить. Чем сложнее вычисления в программе, тем сложнее будет машинный код, соответствующий этим вычислениям. Рассмотрим несколько примеров сложного машинного кода:
1. Пример сортировки массива
Сортировка массива – это одна из основных операций в программировании. Пример сложного машинного кода для сортировки массива может выглядеть следующим образом:
- MOV CX, [Size] – загрузка значения переменной Size в регистр CX
- MOV SI, OFFSET Array – загрузка адреса массива Array в регистр SI
- Loop1:
- MOV BX, [SI] – загрузка значения из памяти по адресу, хранящемуся в регистре SI, в регистр BX
- CMP [SI+2], BX – сравнение значения в ячейке памяти, находящейся по адресу SI+2, с значением в регистре BX
- JGE SkipSwap – переход к метке SkipSwap, если предыдущее сравнение было неравно или меньше
- MOV AX, [SI+2] – загрузка значения в ячейке памяти, находящейся по адресу SI+2, в регистр AX
- MOV [SI], AX – сохранение значения из регистра AX в ячейку памяти, находящуюся по адресу, хранящемуся в регистре SI
- MOV [SI+2], BX – сохранение значения из регистра BX в ячейку памяти, находящуюся по адресу SI+2
- SkipSwap:
- ADD SI, 2 – прибавление значения 2 к значению в регистре SI
- LOOP Loop1 – уменьшение значения в регистре CX на 1 и переход к метке Loop1, если значение в регистре CX не равно 0
2. Пример вычисления факториала
Вычисление факториала – это еще одна распространенная операция в программировании. Пример сложного машинного кода для вычисления факториала может быть следующим:
- MOV CX, [Number] – загрузка значения переменной Number в регистр CX
- MOV AX, 1 – загрузка значения 1 в регистр AX
- Loop1:
- MUL CX – умножение значения в регистре AX на значение в регистре CX и сохранение результата в регистре AX
- SUB CX, 1 – вычитание 1 из значения в регистре CX
- JNZ Loop1 – переход к метке Loop1, если значение в регистре CX не равно 0
Это лишь два примера сложного машинного кода, которые могут использоваться в программировании. Машинный код может быть гораздо более сложным, в зависимости от требуемых операций и алгоритмов.
Описание работы программиста с машинным кодом
Работа программиста с машинным кодом может включать в себя несколько шагов:
- Анализ и понимание задачи. Программист должен внимательно изучить требования и спецификации, определить, какие операции должны быть выполнены процессором, и какие данные должны быть обработаны. Это поможет ему составить алгоритм и определить необходимые инструкции в машинном коде.
- Написание кода на языке ассемблера. Машинный код может быть представлен в более удобной форме с помощью языка ассемблера. Язык ассемблера предлагает набор символов и инструкций, которые упрощают написание кода на машинном уровне.
- Трансляция ассемблерного кода в машинный код. Ассемблерный код не может быть непосредственно исполнен процессором, поэтому необходима его трансляция в машинный код с помощью специальных программ, называемых ассемблерами. Ассемблер преобразует каждую инструкцию ассемблерного кода в соответствующий машинный код.
- Тестирование и отладка. После получения машинного кода программист должен его протестировать и проверить его работоспособность. В случае ошибок, необходимо выполнить отладку и найти и исправить ошибки в коде.
Работа с машинным кодом требует от программиста глубокого понимания аппаратной архитектуры компьютера, а также владения языком ассемблера и умения анализировать и понимать код на машинном уровне. Это очень важные навыки, которые позволяют программисту создавать эффективный и оптимизированный код, ближе к железу компьютера.
Вопрос-ответ:
Какие языки программирования используют машинный код?
Машинный код используется во всех языках программирования, так как компьютеру понятным является только машинный код. Однако, программисты обычно пишут на высокоуровневых языках программирования, которые компилируются в машинный код.
Можно ли программировать на машинном коде напрямую?
Теоретически, можно программировать на машинном коде напрямую, но это крайне трудно и неудобно. Машинный код представляет собой набор инструкций и данных, записанных в виде двоичного кода, который понятен только компьютеру. Поэтому обычно программисты пишут на высокоуровневых языках программирования, которые затем компилируются в машинный код.
Как выглядит машинный код?
Машинный код представляет собой последовательность инструкций и операндов, записанную в виде двоичного кода. Каждая инструкция имеет свой определенный формат, состоящий из опкода (операционного кода) и операндов (данных или адресов). Например, машинный код может выглядеть так: 10110010 11001000, где 10110010 — опкод, а 11001000 — операнд.
Почему нужно знать машинный код?
Знание машинного кода может быть полезно программистам в некоторых случаях. Например, при оптимизации кода или дебаггинге, знание машинного кода позволяет лучше понять, как работает программа на низком уровне и какие именно инструкции выполняются. Кроме того, зная машинный код, программист может более эффективно использовать аппаратные возможности компьютера.
Какие есть примеры машинного кода для программистов?
Примеры машинного кода могут быть разными, в зависимости от архитектуры процессора. Например, для процессора x86 ассемблерные команды могут выглядеть так: mov eax, 5 (переместить значение 5 в регистр eax), add ebx, eax (прибавить значение в регистре eax к значению в регистре ebx). Это лишь простые примеры, а машинный код, как правило, гораздо сложнее и содержит множество инструкций и операндов.
Какие примеры машинного кода считаются понятными для программистов?
Для программистов, чтобы код был понятен, он должен быть представлен в виде команд, которые они могут легко прочитать и понять. Примеры машинного кода, которые считаются понятными для программистов, включают команды, такие как «MOV» (переместить значение из одного места в другое), «ADD» (сложить два значения) и «CMP» (сравнить два значения).
Машинный код и компиляция в него — это как?
Хочу поработать с этим, потому что всегда мечтал попробовать собрать .exe самостоятельно, без помощи готовых компиляторов.
Отслеживать
6,651 6 6 золотых знаков 30 30 серебряных знаков 53 53 бронзовых знака
задан 21 апр 2020 в 8:02
Krutos VIP Krutos VIP
69 2 2 серебряных знака 9 9 бронзовых знаковЕсли идти от сложного: изучить мануалы intel по платформе x86 — чтобы знать как писать машинный код, перед этим еще изучить ассемблер, хотя бы на минимальном уровне; изучить формат исполняемых файлов для вашей системы (portable executable для Windows или ELF для Linux); изучить как вообще создаются компиляторы — тут «Книга Дракона» в помощь.
21 апр 2020 в 8:22
Если от более простого — отказаться от идеи самому создавать бинарник, а генерировать например промежуточный Си код, который уже будет компилироваться в бинарный (или воспользоваться инструментарием LLVM). Но опять же в этом случае все равно нужно будет изучить «Книгу Дракона».
21 апр 2020 в 8:25
Связанный вопрос: Генерация exe файла
21 апр 2020 в 8:25
Не совсем по теме, но, возможно, вдохновит — история одного байта, fermi paradox
– user249284
21 апр 2020 в 9:541 ответ 1
Сортировка: Сброс на вариант по умолчанию
Baremetal
Каждый конкретный процессор (например, Intel Core i3-4160 или ARM Cortex-A9) имеет свою микроархитектуру и реализует архитектуру уровня набора команд (англ. instruction set architecture).
- Микроархитектура определяет структуру процессора на уровне электронных компонентов и логических вентилей.
- Архитектура уровня набора команд (ISA), грубо говоря, определяет то, какие команды может выполнять процессор. Эта архитектура абстрагированна от микроархитектуры. Процессоры разных комнаний могут реализовывать одну и ту же архитектуру (например, многие процессоры Intel и AMD реализует одно и то же семейство архитектур x86).
Если два процессора реализуют одну и ту же ISA, то они могут исполнять одни и те же программы. ISA определяет, какие команды доступны программисту, какие регистры он может использовать, как он может использовать страничную адресацию, виртуальную память и т. д. Кроме того, она определяет формат команд, которые понимает процессор.
Каждая программа процессора — это просто набор подряд идущих команд. При своем запуске процессор выбирает команду из память по адресу, называемому вектором сброса (англ. reset vector) и начинает исполнять эту программу, пока питание не будет отключено.
Написать программу в машинных кодах достаточно просто — нужно лишь взять справочник по ISA (например, Intel 64 and IA-32 Architectures Software Developer Manuals), которую реализует ваш процессор и написать нужные команды байт за байтом.
Конечно, в наше время никто в машинных кодах не пишет, потому что человеку тяжело работать с большим объемом чисел и сложными форматами команд (особенно в x86). Из-за таких сложностей были придуманы языки ассемблера, которые вводят простые мнемоники для инструкций процессора.
Например, одна инструкция ассемблера x86 MOV может кодировать около 20 различных инструкций процессора MOV 1 . Ассемблер читает вашу программу на языке ассемблера и переводит ее в бинарный файл 2 , который, опять же, является просто последовательность байт, кодирующих подряд идущие инструкции процессора.
Вот так может выглядет отрывок программы на языке ассемблера:
cli lgdt (gdtr) mov %cr0, %eax or $0x1, %eax mov %eax, %cr0
Вот так выглядит программа на машинном языке:
0000000 05ea 007c 3100 8ec0 8ed8 bcd0 7c00 1688 0000010 7cdb c031 c08e 00bb 8a80 db16 b67c b100 0000020 b502 b000 e830 0053 59e8 8400 75c0 fa30 0000030 010f f416 0f7c c020 8366 01c8 220f eac0 0000040 7c44 0008 b866 0010 d88e c08e e08e e88e 0000050 d08e 00bc 07c0 e800 03a4 0000 ebf4 befd 0000060 7cbc 03e8 f400 fdeb 5350 30fc b4ff ac0e 0000070 c084 0474 10cd f7eb 585b b4c3 cd02 7213 0000080 3102 c3c0 1e9c 0657 fa56 c031 d88e 10bf 0000090 f705 8ed0 bec0 0500 058a 2650 048a 2650 00000a0 04c6 c600 be05 8026 be3c 2658 0488 8858 00000b0 3105 74c0 4001 075e 1f5f c39d 3241 2030 00000c0 7369 6420 7369 6261 656c 2e64 4820 6c61 00000d0 2074 6874 2065 5043 2e55 0000 0000 0000 00000e0 0000 0000 ffff 0000 9a00 00cf ffff 0000 00000f0 9200 00cf 0017 7cdc 0000 0000 0000 0000
Очевидно, что асссемблерный код и читать, и писать проще.
Теперь у вас достаточно знаний, чтобы открыть справочник, как по словарю, написать программу в машинных кодах и исполнить ее на процессоре. Но, это не сработает в случае, если вы хотите написать программу, которая будет работать в какой-либо операционной системе.
Операционная система
Операционная система — это еще один уровень абстрации, который полностью лишает нас возможности неограниченно пользоваться нашим процессором, заставляя его исполнять любые наши команды 3 . ОС делает очень много различных вещей, но остановимся только на одной — запуск исполняемых файлов.
Как я уже сказал, каждая программа процессора — это просто последовательность команд, однако каждая программа операционной системы — это особая последовательность байт, имеющая специальную структуру, в которую входят не только команды процессора.
Если брать в пример ОС Windows 10, она работает с исполняемыми файлами .exe , которые имеют специальный формат, называемый Portable Executable. Он имеет довольно сложную структуру. Помимо собственно набора машинных команд он содержит в себе информацию необходимую для определения адреса и размера секций, таблиц импорта и экспорта, специальную сигнатуру и т. д.
Поэтому чтобы вручную написать программу в машинных кодах, которая будет запускаться в Windows 10, например, нам, по-мимо написания самой программы, потребуется привести ее к формату Portable Executable.
Но и этого будет не достаточно. Нам придется ознакомится с соглашениями, которые называются ABI и написать программу в машинных кодах, используя именно эти соглашения, а не какие-то другие.
Здесь необходимо, чтобы все части паззла подходили друг к другу по форме: программа должна быть валидной для процессора, формат бинарного файла должен быть понятен операционной системе, программа должна уметь корректно общаться с ОС и т. д. Это все очень сложно обеспечить, если писать программу в шестнадцатеричном редакторе.
Можете начать с написания программ на языке ассемблера (да, вам придется еще выучить синтаксис конкретного языка ассемблера и диалект Intel или AT&T). «Hello, World» на языке NASM будет выглядеть так:
; ---------------------------------------------------------------------------- ; helloworld.asm ; ; This is a Win32 console program that writes "Hello, World" on one line and ; then exits. It needs to be linked with a C library. ; ---------------------------------------------------------------------------- global _main extern _printf section .text _main: push message call _printf add esp, 4 ret message: db 'Hello, World', 10, 0
А нужно ли вам это?
В наше время компьютеры стали очень сложными, с десятками слоями абстраций. Даже инструкции ISA современных процессоров — не атомарные сущности, и процессоры выполняет каждую такую инструкцию как набор еще более мелких инструкций — микрооперации (из таких мокроопераций складывается микрокод).
На самом деле, умение писать на языке ассемблера (а тем более, на машинном языке) довольно бесполезно. Умение просто читать и понимать ассемблерный листинг гораздо более практично и действительно может вам пригодится.
А непрактично это в первую очередь потому, что ничего сложнее «Hello, World!» в машинных кодах вы не напишете. На ассемблере — да, напишете, но потратите на это колоссальное количество времени, которое можно было бы потратить на более полезные вещи.
1. Что интересно, инструкция MOV в x86 является Тьюринг-полной, т. е. любая программа может быть написана с использованием одной только этой инструкции. Есть даже специальный компилятор, который использует только одну эту инструкцию.
2. Некоторые ассемблеры могут сразу формировать исполняемые файлы в нужном формате. В том числе и Portable Executable.
3. Я говорю о современных ОС типа Windows или Linux.
Машинный код
Машинный код (также употребляются термины собственный код, или платформенно-ориентированный код, или родной код, или нативный код — от англ.native code) — система команд (язык) конкретной вычислительной машины (машинный язык), который интерпретируется непосредственно микропроцессором или микропрограммами данной вычислительной машины.
Машинный язык — набор команд конкретной вычислительной машины, который интерпретируется на аппаратном уровне или с помощью микропрограмм самой машины.
Каждая модель процессора имеет свой собственный машинный язык, хотя во многих моделях эти наборы команд сильно перекрываются. Говорят, что процессор A совместим с процессором B, если процессор A полностью «понимает» машинный код процессора B. Если процессор A знает несколько команд, которых не понимает процессор B, то B несовместим с A.
«Слова» машинного языка называются машинными инструкциями. Каждая из них описывает элементарное действие, выполняемое процессором, такое как «переслать байт из памяти в регистр». Программа — это просто длинный список инструкций, выполняемых процессором. Раньше процессоры просто выполняли инструкции одну за другой, но новые суперскалярные процессоры способны выполнять несколько инструкций за раз. Прямой поток выполнения команд может быть изменён инструкцией перехода, которая переносит выполнение на инструкцию с заданным адресом. Инструкция перехода может быть условной, выполняющей переход только при соблюдении некоторого условия.
Также инструкции бывают постоянной длины (у RISC, MISC-архитектур) и диапазонной (у CISC-архитектур; например, для архитектуры x86 команда имеет длину от 8 до 120 битов).