Как читать машинный код компьютера

Обновлено: 06.07.2024

Машинный код или машинный язык представляет собой набор инструкций, выполняемых непосредственно центральным процессором компьютера (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. Отдельные инструкции выделены цветом:

35672 = 3*10<sup>4</sup> + 5*10<sup>3</sup> + 6*10<sup>2</sup> + 7*10<sup>1</sup> + 2*10<sup>0</sup> (1)

Чтобы набрать число 35672 мы должны передвинуть влево две "костяшки" на первом "прутике", 7 на втором, 6 на третьем, 5 на четвертом и 3 на пятом. (У нас ведь 1 "костяшка" на втором - это то же самое, что и 10 "костяшек" на первом, а одна на третьем равна десяти на втором, и так далее. ) Пронумеруем наши "прутики" снизу вверх - да так, чтобы номером первого был "0". И снова посмотрим на наши выражения:

Это (если сверху вниз считать) сколько на каждом "прутике" "костяшек" влево отодвинуто.

Это номер прутика (самый нижний - 0), на котором отодвинуто определенное число костяшек.

Это на каждом прутике - по 10 костяшек нанизано, не все влево отодвинуты, но всего-то их - 10!
Кстати, красненькое 10 в последнем выражении соответствует основанию (radix) системы счисления (number system).

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

В системе счисления с основанием 16 - буквы от A до F:

Правда, при определенном основании (при каком?) буквы аглицкого алфавита закончатся.
Но нам это, пока что, глубоко фиолетово, так как работать мы будем только с тремя radix-ами: 10 (ну естественно), 16 и 2. Правда, если кто на ДВК поизучать это дело собирается, тому еще и radix 8 понадобится.

Если же мы используем символы 123 для представления, например, шестнадцатеричного числа, то подразумеваем следующее:

Короче - полный беспредел. Говорим одно, а подразумеваем другое. И последнее не для красного словца сказано. А потому, что так оно и есть.

Истина где-то рядом.

radix 10 0123456789101112131415
radix 16 0123456789ABCDEF
radix 2 0 1 10 11 100 101 110 111 1000 1001 1010 1011 1100 1101 1110 1111

Следуя этой таблице, число 5BC в шестнадцатеричном формате "строится" так:

+ 11 раз 16 (10 - потому что по таблице B как бы равно 11)

А теперь, если пораскинуть мозгами, с легкостью переведем 5BC из шестнадцатеричной в десятичную систему счисления:

Вот и объединили цифры с буквами. Пространство со временем поучимся объединять немного позже - если не испугаетесь сложностей низкоуровневого программирования.
В общем-то решать вам. В Delphi тоже много чего объединять можно.

А вот и обратный процесс - перевод из HEX в DEC числа A7B8h:

Или использовать родные цифры в десятичной системе счисления, но по другому "вектору упорядоченных цифр" - 1324890576:

Правда, этим немножко затрудняется понимание происходящего? А ведь тоже десятичная система! И рисунок цифр как бы знакомый ))
Или вообще считать в 256-ричной системе счисления, используя в качестве "рисунка цифр" таблицу ASCII-символов! (По сравнению с вами, извращенцами, любой Биллгейтс будет девственником казаться!!).

Каждой шестнадцатеричной цифре соответствует тетрада (4 штуки) ноликов и единичек. Все, что потом нужно сделать - "состыковать" эти тетрады. Круто? Вас еще не то ждет!

. но это так. кстати.

AX=0000 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 DS=18B2 ES=18B2 SS=18B2 CS=18B2 IP=0100 NV UP EI PL NZ NA PO NC

Точно так же присвоить значение 78h регистру AH можно двумя способами:

То же самое, но для регистра AL:

выбросит вам на монитор

Введите после двоеточия, например, число 123 и снова нажмите на Enter:

На дисплее опять появится приглашение "-", на которое мы отвечаем командой "R" без параметров и таким образом вновь просматриваем значения наших регистров:

AX=0123 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 DS=18B2 ES=18B2 SS=18B2 CS=18B2 IP=0100 NV UP EI PL NZ NA PO NC 18B2:0100 6A 00 68 4B 01 66 83 7E-E0 00 74 05 B8 4C 01 EB j.hK.f. 18B2:0110 03 B8 4A 01 2B D2 52 50-57 FF 36 C4 34 00 A1 18 ..J.+.RPW.6.4. 18B2:0120 F7 7F 83 C4 12 56 9A 16-44 F7 7F FF 76 FE 9A 59 . V..D. v..Y 18B2:0130 04 8F 17 B8 FE FF 1F 5E-5F C9 CA 06 00 90 C8 54 . ^_. T 18B2:0140 04 00 57 56 8B 76 04 33-C0 89 46 D6 B9 0B 00 8D ..WV.v.3..F. 18B2:0150 7E D8 16 07 F3 AB 89 46-BC B9 0C 00 8D 7E BE F3 18B2:0160 AB 9A 21 9C 8F 17 89 46-FA A1 08 01 8E 46 06 26 . F. F.& 18B2:0170 39 44 02 0F 84 55 01 C7-46 BC 1A 00 C4 5E 0C 26 9D. U..F. ^.&
  • 0:417 - два байта разрядов состояния клавиатуры. Они активно используются ROM-BIOS для управления интерпретаций действий клавиатуры. Изменение этих байтов изменяет значение нажатых клавиш (например, верхний или нижний регистр).
  • 0:41A - cлово по этому адресу указывает на начало буфера BIOS для ввода с клавиатуры, расположенного начиная с адреса 41E. В этом буфере хранятся и ждут обработки результаты нажатия на клавиши. Конец буфера - слово по адресу 41C.
  • 0:43E - байт указывает, необходима ли проверка дискеты перед подводом головки на дорожку. Разряды 0. 3 соответствуют дисководам 0. 3. Если разряд установлен в 0, то необходима проверка дискеты. Как правило, вы можете обнаружить, что разряд установлен в 0, если при предыдущем обращении к дисководу имели место какие-либо проблемы. Например, разряд проверки будет равен 0, если вы попытаетесь запросить каталог на дисководе, на котором нет дискеты, и затем на запрос, появившийся на экране дисплея: "Not ready reading drive B: Abort, Retry, Ignore?" вы ответите: "A".
  • 0:44C (2 байта) - длина регенерации экрана. Это число байтов, используемых для страницы экрана. Зависит от режима.
  • 0:44E (2 байта) - смещение для адреса начала текущей страницы в памяти дисплея. Этот адрес указывает, какая страница в данный момент используется (маленькая, но неприятная подробность - это смещение внутри текущего сегмента видеопамяти, без учета самого сегмента. Например, для нулевой страницы смещение всегда будет равно нулю.)
  • 0:460 (2 байта) - размер курсора, представленный в виде диапазона строк развертки. Первый байт задает конечную, а второй - начальную строку развертки.
  • 0:449 - значение этого байта определяет текущий видеорежим. Для расшифровки требуется целая таблица. Например, 3 - 80-колонный текст, 16 цветов; 13h (19) - 256-цветный графический режим 320x200 и т. д.
  • это - программа;
  • эта программа - программа с корректным выходом.
  • B82301 - внести значение 0123h в AX;
  • 052500 - прибавить значение 0025h к AX;
  • 8BD8 - переслать содержимое AX в BX;
  • 03D8 - прибавить содержимое AX к BX;
  • 8BCB - переслать содержимое BX в CX;
  • 31C0 - очистка AX;
  • CD20 - конец программы. Передача управления операционной системе.
  1. Он пытается интерпретировать данные как код. Соответственно, в процессор "попадает" всякая ерунда.
  2. Вряд ли он натолкнется на последовательность CD 20 вашем тексте . Даже в том случае, если этот код выполнится "успешно" - ваша программа не возвратит управление операционной системе, а пойдет выполняться хлам, содержащийся в оперативной памяти. Как-то - остатки ранее выполненных программ, куски чьих-то данных, интерпретированные как код. и прочая многочисленная ерунда.
  • кодом (т.е. что компьютеру нужно делать) - последовательностью инструкций;
  • данными (т.е. с чем компьютеру нужно выполнять ту или иную работу). Именно данные являются исходной "задачей" и конечным результатом работы процессора.
  • стеком - это область памяти, позволяющая писать реентерабельный/рекурсивный код и служащая для хранения адресов возврата и локальных данных и передачи параметров.
11B7:0100 B82301 MOV AX,0123 ; Внести значение 0123h в AX 11B7:0103 052500 ADD AX,0025 ; Прибавить значение 0025h к AX 11B7:0106 8BD8 MOV BX,AX ; Переслать содержимое AX в BX 11B7:0108 03D8 ADD BX,AX ; Прибавить содержимое AX к BX 11B7:010A 8BCB MOV CX,BX ; Переслать содержимое BX в CX AX=0123 BX=0000 CX=0010 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=0103 NV UP EI PL NZ NA PO NC

Смотрим на значение AX и вспоминаем предыдущую инструкцию - "внести значение 0123h в AX". Внесли? И правда! А в самом низу - код и мнемоника команды, которая будет выполняться следующей.

Вводим команду "T" снова:.

AX=0148 BX=0000 CX=0010 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=0106 NV UP EI PL NZ NA PE NC

AX=0148 - "прибавить значение 0025h к AX". Сделали? Сделали!!

Вводим команду "T" снова:.

AX=0148 BX=0148 CX=0010 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=0108 NV UP EI PL NZ NA PE NC

AX=0148=BX - "переслать содержимое AX в BX". Сделали? Сделали!!

Вводим команду "T" снова:

AX=0148 BX=0290 CX=0010 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=010A NV UP EI PL NZ AC PE NC

"Прибавить содержимое AX к BX". Оно? А то!

Вводим команду "T" снова:

AX=0148 BX=0290 CX=0290 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=010C NV UP EI PL NZ AC PE NC

"Переслать содержимое BX в CX". Сделано!

Вводим команду "T" снова:

AX=0000 BX=0290 CX=0290 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=010E NV UP EI PL ZR NA PE NC
  1. Многочисленными "схемами" компьютера, его устройствами. Например, соответствующее прерывание генерируется при нажатии клавиши на клавиатуре.
  2. Также прерывания генерируются как "побочный продукт" при некоторых "необычных" ситуациях (например, при делении на "букву О"), из которых компьютеру хочешь, не хочешь, но приходится как-то выкручиваться.
  3. Наконец, прерывания могут преднамеренно генерироваться программой, для того чтобы произвести то или иное "низкоуровневое" действие.
  • AH=06h; (_3)
  • AL - число строк прокрутки (0…25) (AL=0 означает гашение всего окна); (_4)
  • BH - атрибут, использованный в пустых строках (00h…FFh); (_5)
  • CH - строка прокрутки - верхний левый угол; (_6)
  • CL - столбец прокрутки - верхний левый угол;
  • DH - строка прокрутки - нижний правый угол;
  • DL - столбец прокрутки - нижний правый угол;
  • выполнив команду INT 10h, мы ВЫПОЛНЯЕМ одну из "функций видео";
  • так как функций видео - много, необходимо УКАЗАТЬ, КАКУЮ именно ФУНКЦИЮ из МНОЖЕСТВА мы хотим ВЫПОЛНИТЬ.
119A:0104 mov ch,10 ;четыре координаты прямоугольника :0100 XOR AL,AL ;ПРИМЕЧАНИЕ: с целью экономии пространства и :0102 MOV BH,10 ;времени мы немножко сократили наш DEBUG-й :0106 MOV CL,10 ;машинные коды, соответствующие мнемоническим :010A MOV DL,3E ;А так, конечно, в оригинале первая строка вот <p>&nbsp; Правда, красивые циферки-буковки? Набиваем-набиваем! Если сейчас к вам подойдут недZенствующие приятели/коллеги и посмотрят, что вы тут колупаете, то ни черта не поймут и покрутят пальцем у виска. Привыкайте к этому. Только не говорите им, что пытаетесь сейчас получить Матрицу, ПОТОМУ ЧТО это неправда. А неправда это потому, что сейчас Матрица в очередной раз обманула вас! <p>&nbsp; У вас только первое окно прорисуется, сразу же после чего программа натолкнется на INT 20h и благополучно завершится! А следовательно, и все, что после первого CD 20 написано будет - останется проигнорированным! Исправляйте! (Т.е. уберите все INT 20 КРОМЕ ПОСЛЕДНЕГО). <p>&nbsp; Второй момент. ВОЗВРАЩАЕТ ЛИ это прерывание ЧТО-НИБУДЬ В РЕГИСТР AX? Смотрите описание. Ничего? Ну так какого черта тогда по новой вводить XOR AL,AL и MOV AH,06 и переприсваивать AH значение 6h, если и без того AH = 6h? Один раз ввести - более чем достаточно! <p>&nbsp; Скажите, какая мелочь- байтом больше, байтом меньше! А я скажу вот что - на то он и assembler, <p>&nbsp; - <i>По-новому?</i> - возмутимся мы в свою очередь! - <i>Зачем по-новому? Вы что, с ума сошли?</i> <p>&nbsp; 1. Что вам мешает после команды "a" указать адрес, который вы желаете переассемблировать? И <p>&nbsp; - <i>А что делать, если не переассемблировать нужно, а вообще удалить?</i> <p>&nbsp; 2. Существует куча способов, что вы в самом-то деле! Например, в HEX Workshop с блоками шестнадцатеричных цифр запросто можно работать. Да и в других программах это можно делать - например, в <p>&nbsp; Кстати, если процессор встретит команду NOP, то он просто побездельничает некоторое очень <p>&nbsp; ПРОБУЙТЕ!! В конце концов, ваша прога должна принять такой вот вид: AX=0000 BX=0000 CX=0043 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=0102 NV UP EI PL ZR NA PE NC

Состояние: обнулился регистр AX (первую команду MOV AL,AL мы не видим). Процессор готовится выполнить команду MOV BH,10. Дадим ему это сделать!

AX=0000 BX=1000 CX=0043 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=0104 NV UP EI PL ZR NA PE NC

Состояние - в BX уже внесен код синего цвета, который нам по условию необходимо заменить на красный (т. е. заменить значение регистра BX с 1000h на 4000h).

Вот теперь-то мы и делаем это "на лету":

А действительно ли сделали? Проверим!

AX=0000 BX=4000 CX=0043 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=0104 NV UP EI PL ZR NA PE NC

Состояние? BH теперь равно 40h! Мы "вклинились" между строчками:

Ну а как делать то же самое с остальными регистрами вы, наверняка, уже и сами догадались.

Очень важно помнить, каким "нездоровым" образом в стеке реализована ОЧЕРЕДЬ -поместить/извлечь. Помните, мы вас предупреждали, что нам нельзя верить на слово? Не верьте! А посему - обязательно убедитесь в истинности/ложности нашего голословного утверждения при помощи следующей программульки:

С очередностью заполнения стека, наверное, все понятно . Я много про абстрактные "блины" загружал. А вот с адреса 114 начинается извлечение из стека. В какой последовательности это делается, вы можете увидеть сами, произведя трассировку этой небольшой проги.

AX=0000 BX=0000 CX=001B DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=14DC ES=14DC SS=14DC CS=14DC IP=0100 NV UP EI PL NZ NA PO NC

Анализируем. Прога еще не начала работать, готовится выполниться команда по адресу 100. Делаем ШАГ!

AX=0001 BX=0000 CX=001B DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=14DC ES=14DC SS=14DC CS=14DC IP=0103 NV UP EI PL NZ NA PO NC

Анализируем. AX=0001 - значит, команда выполнилась правильно . Следующая команда, по идее, должна поместить 1 в стек.

AX=0001 BX=0000 CX=001B DX=0000 SP=FFFC BP=0000 SI=0000 DI=0000 DS=14DC ES=14DC SS=14DC CS=14DC IP=0104 NV UP EI PL NZ NA PO NC

И что? Команда выполнилась, но где мы можем увидеть, что в стек действительно "ушла" единица? Увы, но здесь это не отображается . Проверим потом. Ведь логично предположить, что если эти значения действительно сохранились в стеке, то мы их потом без проблем оттуда извлечем, т.е. если найдем "там" наши 1, 2, 3, 4, 5 - значит все Ок.

А поэтому - дадим программе работать дальше до адреса 114 (не включительно), не вдаваясь в подробный анализ. Что тут анализировать? Если значение регистра AX последовательно меняется от 1 до 5 - значит, команда mov работает. А стек (команда push) проверим потом, как и договорились.

Проехали до адреса 114.

AX=0005 BX=0000 CX=001B DX=0000 SP=FFF4 BP=0000 SI=0000 DI=0000 DS=14DC ES=14DC SS=14DC CS=14DC IP=0114 NV UP EI PL NZ NA PO NC

А вот теперь снова анализируем . При следующем шаге выполнится команда, извлекающая некогда "запомненное" значение AX из стека.

Обратите внимание, регистр IP указывает на адрес (114) выполняемой команды. Мы с вами это уже проходили, не так ли?

Поехали дальше!!

AX=0005 BX=0000 CX=001B DX=0000 SP=FFF6 BP=0000 SI=0000 DI=0000 DS=14DC ES=14DC SS=14DC CS=14DC IP=0115 NV UP EI PL NZ NA PO NC

Выполнился первый POP. Готовится выполниться второй. AX=5. Т.е., по сравнению с предыдущим шагом, вроде ничего не изменилось. Но на самом деле это не так. AX=5 - эта пятерка "загрузилась" из стека ). В этом вы легко убедитесь, сделав следующий шаг трассировки.

AX=0004 BX=0000 CX=001B DX=0000 SP=FFF8 BP=0000 SI=0000 DI=0000 DS=14DC ES=14DC SS=14DC CS=14DC IP=0116 NV UP EI PL NZ NA PO NC

Ууупс. AX=4 . А команда, вроде, та же - POP AX

AX=0003 BX=0000 CX=001B DX=0000 SP=FFFA BP=0000 SI=0000 DI=0000 DS=14DC ES=14DC SS=14DC CS=14DC IP=0117 NV UP EI PL NZ NA PO NC AX=0002 BX=0000 CX=001B DX=0000 SP=FFFC BP=0000 SI=0000 DI=0000 DS=14DC ES=14DC SS=14DC CS=14DC IP=0118 NV UP EI PL NZ NA PO NC AX=0001 BX=0000 CX=001B DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=14DC ES=14DC SS=14DC CS=14DC IP=0119 NV UP EI PL NZ NA PO NC :0102 MOV CX,0005 ; нижеследующий до команды LOOP кусок повторить CX раз :0105 ADD AX,0001 ; AX=AX+1 (у нас же значение AX на 1 увеличивается. ) :0109 LOOP 0105 ; конец цикла; инициируем повторение; CX уменьшается на 1

Многие любители не испытывают серьезных трудностей в овладении БЕЙСИКом. Для этого достаточно немного практики. Но рано или поздно они приходят к барьеру «машинного кода». Как это ни печально, но некоторые так перед ним и останавливаются. Это ни в коей мере не связано с отсутствием желания или способностей, просто многие не знают, с чего начать. Если в БЕЙСИКе можно начинать с чего угодно (при ошибке компьютер сам Вас поправит), то здесь Вы оказываетесь с процессором один на один, и такой метод проб и ошибок не срабатывает.

Одним словом, есть некий психологический барьер, который бывает трудно преодолеть в одиночку. Известно, что для того, чтобы научиться программировать, надо взять и начать программировать. «ИНФОРКОМ» предлагает Вам следующий компромиссный подход - сначала в рамках этой главы мы, беря «быка за рога», просто начнем программировать, а затем посвятим оставшуюся часть книги систематическому изложению материала.

Итак, давайте напишем первую программу в машинном коде. Прежде всего, выделим для нее область памяти. Если Вы читали нашу книгу "Большие возможности Вашего «ZX-Spectrum`а», то знаете, что для БЕЙСИКа в оперативной памяти компьютера отведена область памяти, начинающаяся с адреса, на который указывает системная переменная PROG и заканчивается адресом, на который указывает системная переменная RAMTOP. Предположим, что Вы хотите записать программу в машинных кодах, начиная с адреса 30000. Дайте команду CLEAR 29999. Эта команда установит RAMTOP в 29999 и Ваша программа будет защищена от возможной порчи из БЕЙСИКа. Даже если Вы дадите команду NEW, области памяти, находящиеся выше RAMTOP, не будут поражены.

Теперь дайте две прямые команды одну за другой:

Мы сейчас записали два числа в нужные нам адреса. Они образуют простейшую программу. Выполнить ее можно командой RANDOMIZE USR 30000. Попробуйте сами. Вам покажется, что ничего не произошло, но это не так. Сначала процессор обратился по адресу 30000 и нашел там число 0, которое обозначает машинный код операции NOP. Операция NOP ( no operation - нет операции) дает команду процессору, что ничего делать не надо. В течение 0,0000014 сек. он действительно ничего не делает, а затем переходит к следующему адресу, где находит число 201.

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

К сожалению, для нас мало, что говорит простая последовательность чисел вроде таких, как 0 и 201. Держать в памяти коды всех команд процессора (а их около семисот) непросто, но дело упрощается тем, что есть промежуточный язык между процессором и человеком - язык Ассемблера. У каждого кода есть своя мнемоника Ассемблера. Мнемоника - это набор букв, являющихся сокращением английских слов. Для нашего примера программа на Ассемблере выглядит так:

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

И тех программ и других достаточно много. Часто они объединяются в пакеты. Широко распространены пакеты GENS3/MONS3 фирмы HISOFT и EDITAS / MONITOR 16/48 фирмы PICTURESQUE . Здесь GENS 3 и EDITAS - Ассемблеры, а MONS 3, MONITOR 16 и MONITOR 48 - Дизассемблеры.

Теперь давайте вернемся к нашей первой программе и попробуем ее несколько развить, чтобы она все же что-то делала. Процессор Z-80 имеет несколько регистров, у которых есть имена – «А», «В», «С» и т.д. Каждый из них может содержать одно какое-либо целое число от 0 до 255 (т.е. один байт).

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

Так, например, команда Ассемблера LD B,A (машинный код - 71) означает «загрузить содержимое регистра А в регистр В». LD - это сокращение от LOAD (загрузка).

Точно так же LD C,B (машинный код 72) означает «загрузить в регистр С содержимое регистра В». Можно загружать в регистры и целые числа. Например, LD A, n - означает «загрузить в регистр А целое число n », где n может быть числом от 0 до 255. До этого все команды были однобайтными. Эта же команда - двухбайтная. Сначала идет машинный код - 62, а за ним само число - n . Так, например, LD A, 77 (загрузить в регистр А число 77) будет выглядеть так: 62,77. Здесь 62 - код операции, - он сообщает процессору, что надо сделать, а 77 - это операнд. Заметим здесь же, что бывают операции и трехбайтные и четырехбайтные. Первый байт, как правило, - код операции, а следующие за ним - операнды. Мы говорим «как правило» потому, что есть некоторые операции, код которых записывается двумя байтами [прим.1].

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

как работает процессор и языки программирования

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

Все описанное ниже как всегда упрощено для лучшего понимания.

Процессор и оперативная память

процессор управляет всеми устройствами и процессами в компьютере

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

Процессор знает много команд и у каждой из них есть свой числовой код, например:


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


Понимаете прикол? Это значит, что вам нужно писать код для каждой архитектуры процессора. Жуть.

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

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

как работает процессор и оперативная память

Ассемблер

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

Для того, чтобы заставить процессор выполнить какую-то программу, например решить уравнение 2 + 2 * 2, нам нужно написать цепочку простых числовых команд.

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

Согласитесь, что писать такой код очень сложно и легко запутаться. И это мы всего лишь написали код для решения простого уравнения. А теперь представьте, как написать ВКонтактик или Инстаграм.

Для упрощения жизни люди придумали инструмент Ассемблер и язык программирования на ассемблере.

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

Помните примеры кодов команд, которые были указаны выше? Теперь они выглядят так:

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

пример кода на языке assembler

Что-то слишком много непонятного кода для такой пустяковой задачи, не правда ли?

Языки программирования высшего уровня

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

Это усложняет портативность. Добавим сюда сложность в написании больших программ и получим необходимость в создании новых инструментов.

Так стали появляться языки программирования высокого уровня.

Компилируемые языки

Первыми появились компилируемые языки программирования. К ним относится С, С++, Java и другие.

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

процессор не понимает код высшего порядка напрямую

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

как работает компилятор

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

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

компилятор языка C собрал исполняемый файл

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

Интерпретируемые языки

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

Вот тут в ход идут интерпретируемые языки программирования такие как: Python, PHP, Perl, Pascal и другие.

Это тоже языки высшего порядка, которые также упрощают написание кода. Но у них есть как минимум два преимущества перед компилируемыми языками:

как работает интерпретатор

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

Подытожим

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

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