В программировании машинный код — это компьютерный код , состоящий из инструкций машинного языка , которые используются для управления центральным процессором компьютера (ЦП). Для обычных двоичных компьютеров машинный код — это двоичное представление компьютерной программы, которое фактически считывается и интерпретируется компьютером. Программа в машинном коде состоит из последовательности машинных инструкций (возможно, перемежаемых данными). [1]
Каждая инструкция машинного кода заставляет ЦП выполнять определенную задачу. Примеры таких задач включают:
В целом, каждое семейство архитектур (например, x86 , ARM ) имеет собственную архитектуру набора инструкций (ISA) и, следовательно, свой собственный язык машинного кода. Существуют исключения, такие как архитектура VAX , которая включает дополнительную поддержку набора инструкций PDP-11 ; архитектура IA-64 , которая включает дополнительную поддержку набора инструкций IA-32 ; и микропроцессор PowerPC 615 , который может изначально обрабатывать как наборы инструкций PowerPC , так и x86.
Машинный код — это строго числовой язык, и это интерфейс самого низкого уровня к ЦП, предназначенный для программиста. Язык ассемблера обеспечивает прямое отображение между числовым машинным кодом и понятным человеку мнемоническим кодом. В ассемблере числовые коды операций и операнды заменяются мнемоническими кодами и метками. Например, архитектура x86 имеет доступный код операции 0x90; он представлен как NOP в исходном коде ассемблера . Хотя можно писать программы непосредственно в машинном коде, управление отдельными битами и вычисление числовых адресов утомительно и подвержено ошибкам. Поэтому программы редко пишутся непосредственно в машинном коде. Однако существующую программу в машинном коде можно редактировать, если исходный код ассемблера недоступен.
Большинство программ сегодня написаны на языке высокого уровня . Программа высокого уровня может быть переведена в машинный код компилятором .
Каждый процессор или семейство процессоров имеет свой собственный набор инструкций . Инструкции — это шаблоны битов , цифр или символов, которые соответствуют машинным командам. Таким образом, набор инструкций специфичен для класса процессоров, использующих (в основном) одну и ту же архитектуру . Последующие или производные конструкции процессоров часто включают инструкции предшественника и могут добавлять новые дополнительные инструкции. Иногда последующая конструкция прекращает или изменяет значение некоторого кода инструкции (обычно потому, что это необходимо для новых целей), влияя на совместимость кода в некоторой степени; даже совместимые процессоры могут показывать немного различное поведение для некоторых инструкций, но это редко является проблемой. Системы также могут отличаться в других деталях, таких как организация памяти, операционные системы или периферийные устройства . Поскольку программа обычно полагается на такие факторы, разные системы, как правило, не будут запускать один и тот же машинный код, даже если используется один и тот же тип процессора.
Набор инструкций процессора может иметь инструкции фиксированной или переменной длины. То, как организованы шаблоны, зависит от конкретной архитектуры и типа инструкции. Большинство инструкций имеют одно или несколько полей кода операции , которые определяют базовый тип инструкции (например, арифметический, логический, переход и т. д.), операцию (например, сложение или сравнение) и другие поля, которые могут давать тип операнда ( ов), режим(ы) адресации , смещение(я) адресации или индекс или само значение операнда (такие постоянные операнды, содержащиеся в инструкции, называются немедленными ). [2]
Не все машины или отдельные инструкции имеют явные операнды. На машине с одним аккумулятором аккумулятор неявно является как левым операндом, так и результатом большинства арифметических инструкций. Некоторые другие архитектуры, такие как архитектура x86 , имеют версии аккумуляторов общих инструкций, при этом аккумулятор рассматривается как один из общих регистров более длинными инструкциями. Стековая машина имеет большинство или все свои операнды в неявном стеке. Инструкции специального назначения также часто не имеют явных операндов; например, CPUID в архитектуре x86 записывает значения в четыре неявных регистра назначения. Это различие между явными и неявными операндами важно в генераторах кода, особенно в частях распределения регистров и отслеживания диапазона в реальном времени. Хороший оптимизатор кода может отслеживать неявные и явные операнды, что может позволить более частое распространение констант , свертывание констант регистров (регистр, назначенный результат константного выражения, освобожденный путем замены его этой константой) и другие улучшения кода.
Гораздо более удобная для человека версия машинного языка, называемая языком ассемблера , использует мнемонические коды для обозначения инструкций машинного кода, а не непосредственно числовые значения инструкций, а также использует символические имена для обозначения мест хранения и иногда регистров . [3] Например, на процессоре Zilog Z80 машинный код 00000101
, который заставляет ЦП уменьшать B
регистр общего назначения , будет представлен на языке ассемблера как DEC B
. [4]
IBM 704, 709, 704x и 709x хранят одну инструкцию в каждом командном слове; IBM нумерует бит слева направо как S, 1, ..., 35. Большинство инструкций имеют один из двух форматов:
Для всех, кроме IBM 7094 и 7094 II, есть три индексных регистра, обозначенных как A, B и C; индексация с несколькими битами 1 в теге вычитает логическое ИЛИ выбранных индексных регистров, а загрузка с несколькими битами 1 в теге загружает все выбранные индексные регистры. 7094 и 7094 II имеют семь индексных регистров, но при включении они находятся в режиме множественных тегов , в котором они используют только три индексных регистра способом, совместимым с более ранними машинами, и требуют инструкции Leave Multiple Tag Mode ( LMTM ) для доступа к остальным четырем индексным регистрам.
Эффективный адрес обычно YC(T), где C(T) — это либо 0 для тега 0, либо логический или выбранных индексных регистров в режиме множественных тегов или выбранный индексный регистр, если не в режиме множественных тегов. Однако эффективный адрес для инструкций управления индексным регистром — это просто Y.
Флаг с обоими битами 1 выбирает косвенную адресацию; слово косвенного адреса имеет как тег, так и поле Y.
Помимо инструкций переноса (перехода), эти машины имеют инструкции пропуска, которые условно пропускают одно или два слова, например, сравнение аккумулятора с хранилищем (CAS) выполняет трехстороннее сравнение и условно пропускает NSI, NSI+1 или NSI+2 в зависимости от результата.
Архитектура MIPS представляет собой конкретный пример машинного кода, инструкции которого всегда имеют длину 32 бита. [5] : 299 Общий тип инструкции задается полем op (операция), старшими 6 битами. Инструкции J-типа (переход) и I-типа (немедленный) полностью определяются op . Инструкции R-типа (регистр) включают дополнительное поле funct для определения точной операции. Поля, используемые в этих типах:
6 5 5 5 5 6 бит[ op | rs | rt | rd |shamt| funct] R-тип[ op | rs | rt | адрес/немедленный] I-тип[ op | целевой адрес ] J-тип
rs , rt и rd обозначают операнды регистра; shamt задает величину сдвига; а поля адреса или непосредственного адреса содержат непосредственно операнд. [5] : 299–301
Например, сложение регистров 1 и 2 и помещение результата в регистр 6 кодируется: [5] : 554
[ оп | рс | рт | рд | шамт | фунц] 0 1 2 6 0 32 десятичный 000000 00001 00010 00110 00000 100000 двоичный
Загрузите значение в регистр 8, взятое из ячейки памяти, расположенной на 68 ячеек после ячейки, указанной в регистре 3: [5] : 552
[ op | rs | rt | адрес/немедленно] 35 3 8 68 десятичная 100011 00011 01000 00000 00001 000100 двоичный
Переходим по адресу 1024: [5] : 552
[ оп | целевой адрес ] 2 1024 десятичная 000010 00000 00000 00000 10000 000000 двоичный
На архитектурах процессоров с наборами инструкций переменной длины [6] (таких как семейство процессоров Intel x86 ) в пределах феномена повторной синхронизации потока управления, известного как счет Крускала , [7] [6] [8] [9] [10] иногда возможно посредством программирования на уровне опкода намеренно организовать результирующий код таким образом, чтобы два пути кода совместно использовали общий фрагмент последовательностей опкодов. [nb 1] Это называется перекрывающимися инструкциями , перекрывающимися опкодами , перекрывающимся кодом , перекрывающимся кодом , разрывом инструкции или переходом в середину инструкции . [11] [12] [13]
В 1970-х и 1980-х годах перекрывающиеся инструкции иногда использовались для сохранения пространства памяти. Одним из примеров была реализация таблиц ошибок в Altair BASIC от Microsoft , где чередующиеся инструкции взаимно делили свои байты инструкций. [14] [6] [11] Сегодня эта техника используется редко, но к ней все еще может потребоваться прибегнуть в областях, где необходима экстремальная оптимизация размера на уровне байтов, например, при реализации загрузчиков , которые должны вписываться в загрузочные секторы . [nb 2]
Иногда его также используют как метод запутывания кода , как меру против разборки и подделки. [6] [9]
Этот принцип также используется в общих последовательностях кода толстых двоичных файлов , которые должны работать на нескольких процессорных платформах, несовместимых по набору инструкций. [примечание 1]
Это свойство также используется для поиска непреднамеренных инструкций, называемых гаджетами, в существующих репозиториях кода и используется в возвратно-ориентированном программировании как альтернатива внедрению кода для таких эксплойтов, как атаки возврата в libc . [15] [6]
В некоторых компьютерах машинный код архитектуры реализуется еще более фундаментальным базовым слоем, называемым микрокодом , который обеспечивает общий интерфейс машинного языка для линейки или семейства различных моделей компьютеров с существенно различающимися базовыми потоками данных . Это делается для облегчения переноса программ машинного языка между различными моделями. Примером такого использования является семейство компьютеров IBM System/360 и их последователи.
Машинный код, как правило, отличается от байт-кода (также известного как p-код), который либо выполняется интерпретатором, либо сам компилируется в машинный код для более быстрого (прямого) выполнения. Исключением является случай, когда процессор предназначен для использования определенного байт-кода непосредственно в качестве своего машинного кода, как в случае с процессорами Java .
Машинный код и ассемблерный код иногда называют собственным кодом , когда речь идет о платформенно-зависимых частях языковых функций или библиотек. [16]
С точки зрения ЦП машинный код хранится в оперативной памяти, но обычно также хранится в наборе кэшей по соображениям производительности. Для инструкций и данных могут быть разные кэши, в зависимости от архитектуры.
Процессор знает, какой машинный код выполнять, на основе своего внутреннего счетчика программ. Счетчик программ указывает на адрес памяти и изменяется на основе специальных инструкций, которые могут вызывать программные переходы. Счетчик программ обычно устанавливается на жестко закодированное значение при первом включении процессора, и, следовательно, будет выполнять любой машинный код, который окажется по этому адресу.
Аналогично, счетчик программ может быть настроен на выполнение любого машинного кода, находящегося по произвольному адресу, даже если это недействительный машинный код. Обычно это вызывает ошибку защиты, специфичную для архитектуры.
Процессору часто сообщают, с помощью разрешений на страницы в системе на основе страничного обмена, что текущая страница фактически содержит машинный код с помощью бита выполнения — страницы имеют несколько таких битов разрешения (чтение, запись и т. д.) для различных служебных функций. Например, в Unix-подобных системах страницы памяти можно переключить на исполнение с помощью mprotect()
системного вызова, а в Windows VirtualProtect()
можно использовать для достижения аналогичного результата. Если делается попытка выполнить машинный код на неисполняемой странице, обычно возникает ошибка, специфичная для архитектуры. Обработка данных как машинного кода или поиск новых способов использования существующего машинного кода с помощью различных методов является основой некоторых уязвимостей безопасности.
Аналогично, в системе, основанной на сегментах, дескрипторы сегментов могут указывать, может ли сегмент содержать исполняемый код и в каких кольцах этот код может выполняться.
С точки зрения процесса , пространство кода является частью его адресного пространства , где хранится исполняемый код. В многозадачных системах это включает сегмент кода программы и обычно общие библиотеки . В многопоточной среде различные потоки одного процесса совместно используют пространство кода вместе с пространством данных, что значительно снижает накладные расходы на переключение контекста по сравнению с переключением процессов.
Существуют различные инструменты и методы для декодирования машинного кода обратно в соответствующий ему исходный код .
Машинный код можно легко декодировать обратно в соответствующий ему исходный код на языке ассемблера , поскольку язык ассемблера образует однозначное соответствие с машинным кодом. [17] Метод декодирования языка ассемблера называется дизассемблированием .
Машинный код может быть декодирован обратно в соответствующий ему язык высокого уровня при выполнении двух условий:
Первое условие — принять запутанное прочтение исходного кода. Запутанная версия исходного кода отображается, если машинный код отправляется декомпилятору исходного языка.
Второе условие требует, чтобы машинный код имел информацию об исходном коде, закодированном внутри. Информация включает в себя таблицу символов , которая содержит отладочные символы . Таблица символов может храниться в исполняемом файле или может существовать в отдельных файлах. Затем отладчик может прочитать таблицу символов, чтобы помочь программисту интерактивно отлаживать машинный код при выполнении .
.pdb
). [22].dSYM
файле.