Изучение языка ассемблера | Кодементор

Язык ассемблера — это удобочитаемый эквивалент самого низкого программного уровня компьютерного программирования — машинного кода.

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

Изучите язык программирования Си. Если вы знаете C (или C++), сборка будет проще.

C# и Java — хорошие языки, но инструментов, показывающих, как выглядит код C# и Java на языке ассемблера, практически нет. (На самом деле Java и C# имеют свой собственный язык ассемблера байт-кода, который сильно отличается и далек от реальной архитектуры набора инструкций аппаратного процессора.)

Из кода C (и C++) мы можем использовать компиляторы, чтобы показать нам, как выглядит язык ассемблера. C — это язык низкого уровня, и если вы знаете, как каждая конструкция C переводится на язык ассемблера, вы будете знать язык ассемблера! Примеры конструкций C, которые мы хотим знать для языка ассемблера:

C ПостроитьЭквивалент языка ассемблера
локальная переменнаяРасположение регистра ЦП или стека в памяти
глобальная переменнаядоступ к глобальной или константной ячейке памяти
ссылка на массившаблон сложения и доступа к памяти
перейти кинструкция безусловного перехода
если операторшаблон в if-goto: условный тест и инструкции ветвления
пока циклшаблон в if-goto
назначениемодификация регистра или памяти
арифметикаарифметические инструкции
передача параметровмодификация регистровой или стековой памяти
получение параметровдоступ к регистру или стеку памяти
вызов функцииинструкция вызова
запись функциипролог шаблон
выход из функцииэпилог шаблон
возвращаемое значениезарегистрировать модификацию и эпилог

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

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

  • В языке ассемблера нет структурированных операторов, поэтому все конструкции управления потоком должны быть запрограммированы в стиле «If-Goto» (в других языках компилятор переводит структурированные операторы в If-Goto). Стиль if-goto использует метки в качестве целей. (Хотя C поддерживает использование goto и меток, программисты большую часть времени предпочитают структурированные операторы, поэтому мы не часто видим goto и метки в C.)

Язык ассемблера предназначен для чтения и записи людьми.

Этикетки

В языке ассемблера мы используем множество меток. Использование меток преобразуется в числа в машинном коде, а метки исчезают в машинном коде. Метки (в отличие от прямого использования чисел) позволяют вносить небольшие изменения в программы — без них даже небольшое изменение на самом деле было бы огромным изменением.

Процессору необходимо знать, где находится операнд, возможно, операнд данных или, возможно, место кода. Например, для местоположений кода процессор обычно хочет знать, насколько далеко эти элементы от места кода, которое он выполняет в данный момент, — это называется адресацией относительно компьютера. Таким образом, чтобы выполнить оператор if-then, мы можем сказать процессору: при определенных условиях пропустить вперед N инструкций, где N представляет собой размер или количество инструкций, которые нужно пропустить (или нет), т. е. N — размер инструкции. then-part — это будет машинный код. Если мы добавим инструкцию в часть then, количество пропускаемых инструкций увеличится (а если мы удалим инструкцию из части then, уменьшится).

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

Псевдоинструкции

Большинство инструкций на языке ассемблера переводятся 1:1 в инструкции машинного кода. Однако иногда нам приходится сообщать ассемблеру что-то, что напрямую не транслируется в машинный код. Типичные примеры сообщают ассемблеру, что следующие инструкции являются кодом (обычно через .text директива) по сравнению с данными (обычно через .data директива). Как и обычные инструкции, эти инструкции обычно записываются на отдельной строке, однако они информируют ассемблер, а не генерируют инструкции машинного кода напрямую.

Сходства с C

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

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

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

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

Выбор регистра

Чтобы выбрать регистр, нам нужна ментальная модель того, какие регистры заняты, а какие свободны. Зная это, мы можем выбрать свободный регистр для хранения временного (кратковременного) результата. Этот регистр будет оставаться занятым до тех пор, пока мы не воспользуемся им в последний раз, а затем он снова станет свободным — однажды освободившись, его можно переназначить для чего-то совершенно другого. (В некотором смысле процессор на самом деле не знает, что мы делаем это постоянное перепрофилирование, поскольку он не знает намерения программы.)

Выбор инструкции

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

Например, MIPS и RISC V являются похожими архитектурами, которые предоставляют инструкции сравнения и ветвления, но они могут сравнивать только два регистра процессора. Итак, если мы хотим увидеть, является ли байт в строке определенным символом (например, новой строкой), то мы должны предоставить (загрузить) это значение новой строки (числовую константу) в регистре процессора, прежде чем мы сможем использовать сравнение и инструкция филиала.

Программа состоит из тысячи функций! Хотя эти функции должны совместно использовать память, память огромна, и поэтому они могут работать вместе. Однако с одним процессором ограниченное и фиксированное количество регистров (например, 32 или 16), и все функции должны использовать эти регистры!!

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

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

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

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

Выделенные регистры

Некоторые регистры ЦП предназначены для определенных целей. Например, большинство процессоров в наши дни имеют некоторую форму указателя стека. В MIPS и RISC V указатель стека представляет собой обычный регистр, не отличающийся от других, за исключением определения программного соглашения — выбор регистра является произвольным, но он действительно выбран, и этот выбор публикуется, чтобы мы все знали, какой регистр это. является. На x86, с другой стороны, указатель стека поддерживается выделенным push & pop инструкции (а также инструкции вызова и возврата), поэтому для регистра указателя стека есть только один логический выбор.

Регистрация

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

  • Регистры для передачи параметров
  • Регистры для получения возвращаемых значений
  • Регистры, которые являются «изменчивыми»
  • Регистры, которые являются «энергонезависимыми»

Регистры параметров и возврата

Регистры параметров используются для передачи параметра! Придерживаясь соглашения о вызовах, вызываемый объект знает, где найти свои аргументы, а вызывающий знает, где размещать значения аргументов. Регистры параметров используются для передачи параметров по правилам, изложенным в соглашении о вызовах, и, когда передается больше параметров, чем позволяет соглашение для ЦП, остальные параметры передаются с использованием памяти стека.

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

Энергонезависимые и энергонезависимые регистры

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

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

Статьи Википедии
Соглашение о вызовах
Бинарный интерфейс приложения
Архитектура набора инструкций

Кодементор Статьи
Преобразование структурированного кода в стиль If-Goto

Copyright (c) 2019 Эрик Л. Эйдт

Похожие записи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *