Esi assembler что это
Регистры процессора
Начиная с модели 80386 процессоры Intel предоставляют 16 основных регистров для пользовательских программ и ещё 11 регистров для работы с мультимедийными приложениями (MMX) и числами с плавающей точкой (FPU/NPX). Все команды так или иначе изменяют содержимое регистров. Как уже говорилось, обращаться к регистрам быстрее и удобнее, чем к памяти. Поэтому при программировании на языке Ассемблера регистры используются очень широко.
В этом разделе мы рассмотрим основные регистры процессоров Intel. Названия и состав/количество регистров для других процессоров могут отличаться. Итак, основные регистры процессоров Intel.
Таблица 2.1. Основные регистры процессора.
Название | Разрядность | Основное назначение |
EAX | 32 | Аккумулятор |
EBX | 32 | База |
ECX | 32 | Счётчик |
EDX | 32 | Регистр данных |
EBP | 32 | Указатель базы |
ESP | 32 | Указатель стека |
ESI | 32 | Индекс источника |
EDI | 32 | Индекс приёмника |
EFLAGS | 32 | Регистр флагов |
EIP | 32 | Указатель инструкции (команды) |
CS | 16 | Сегментный регистр |
DS | 16 | Сегментный регистр |
ES | 16 | Сегментный регистр |
FS | 16 | Сегментный регистр |
GS | 16 | Сегментный регистр |
SS | 16 | Сегментный регистр |
Регистры EAX, EBX, ECX, EDX – это регистры общего назначения. Они имеют определённое назначение (так уж сложилось исторически), однако в них можно хранить любую информацию.
Регистры EBP, ESP, ESI, EDI – это также регистры общего назначения. Они имеют уже более конкретное назначение. В них также можно хранить пользовательские данные, но делать это нужно уже более осторожно, чтобы не получить «неожиданный» результат.
Регистр флагов и сегментные регистры требуют отдельного описания и будут более подробно рассмотрены далее.
Пока для вас здесь слишком много непонятных слов, но со временем всё прояснится)))
Когда-то процессоры были 16-разрядными, и, соответственно, все их регистры были также 16-разрядными. Для совместимости со старыми программами, а также для удобства программирования некоторые регистры разделены на 2 или 4 «маленьких» регистра, у каждого из которых есть свои имена. В таблице 2.2 перечислены такие регистры.
Вот пример такого регистра.
Из этого следует, что вы можете написать в своей программе, например, такие команды: Обе команды поместят в регистр AX число 1. Разница будет заключаться только в том, что вторая команда обнулит старшие разряды регистра EAX, то есть после выполнения второй команды в регистре EAX будет число 1. А первая команда оставит в старших разрядах регистра EAX старые данные. И если там были данные, отличные от нуля, то после выполнения первой команды в регистре EAX будет какое-то число, но не 1. А вот в регистре AX будет число 1. Сложно? Ну это пока… Со временем вы к таким вещам привыкните.
Мы пока не говорили о разрядах (битах). Эту тему мы обсудим в разделах, посвящённых системам счисления. А сейчас пока вам достаточно знать, что нулевой разряд (бит) – это младший бит. Он крайний справа. Старший бит – крайний слева. Номер старшего бита зависит от разрядности числа/регистра. Например, в 32-разрядном регистре старшим битом является 31-й бит (потому что отсчёт начинается с 0, а не с 1).
Ниже приведён список регистров общего назначения, которые можно поделить описанным выше способом и при этом к «половинкам» и «четвертинкам» этих регистров можно обращаться в программе как к отдельному регистру.
Таблица 2.2. «Делимые» регистры..
Регистр | Старшие разряды | Имена 16-ти и 8-ми битных регистров | |
31…16 | 15…8 | 7…0 | |
EAX | . | AX | |
AH | AL | ||
EBX | . | BX | |
BH | BL | ||
ECX | . | CX | |
CH | CL | ||
EDX | . | DX | |
DH | DL | ||
ESI | . | SI | |
EDI | . | DI | |
EBP | . | BP | |
ESP | . | SP | |
EIP | . | IP |
На этом мы закончим наше краткое знакомство с регистрами. Если вам пока не всё понятно – просто прочитайте этот раздел, чтобы более-менее представлять себе, что такое регистры. По мере приобретения новых знаний вы можете вернуться к этому разделу и уже на новом уровне воспринять эту информацию. А в следующем разделе мы коротко опишем процесс выполнения команды.
Цель регистров ESI & EDI?
какова фактическая цель и использование регистров EDI & ESI в ассемблере?
Я знаю, что они используются для строковых операций для одного.
может кто-нибудь также привести пример?
5 ответов
есть несколько операций, которые вы можете только do с DI / SI (или их расширенными аналогами, если вы не изучили ASM в 1985 году). Среди них
которые являются, соответственно, операциями для повторного (=массового) хранения, загрузки и сканирования. Что вы делаете, так это настраиваете SI и / или DI, чтобы указать на один или оба операнда, возможно, подсчитайте в CX, а затем пусть ‘er rip. Это операции, которые работают с кучей байтов за раз, и они как бы помещают процессор в автоматический. Поскольку вы явно не кодируете циклы, они делают свою работу более эффективно (обычно), чем цикл с ручным кодом.
на всякий случай вам интересно: В зависимости от того, как вы настраиваете операцию, повторное хранение может быть чем-то простым, например, пробивая значение 0 в большой непрерывный блок памяти; MOVSB используется, Я думаю, для копирования данных из одного буфера (ну, любая куча байтов) в другой; и SCASB используется для поиска байта, который соответствует некоторому критерию поиска (я не конечно, если это только поиск по равенству, или что – вы можете посмотреть его:))
это большая часть того, для чего предназначены эти правила.
SI = Исходный Индекс
DI = Целевой Показатель
как указывали другие, они имеют специальное использование со строковыми инструкциями. Для программирования в реальном режиме, ES сегментный регистр должен использоваться с DI и DS С SI а в
SI и DI можно также использовать как общецелевые регистры индекса. Например, C исходный код
Хотя я не могу вспомнить, где я его видел, но этой подтверждает большую часть этого, и этой (слайд 17) другие:
AX = аккумулятор
DX = двойное слово аккумулятор
CX = счетчик
BX = базовый регистр
они похожи на регистры общего назначения, но есть ряд инструкций, которые (неожиданно?) используйте один из них-но какой?-неявно.
опкоды, такие как MOVSB и MOVSW, которые эффективно копируют данные из памяти, на которую указывает ESI, в память, на которую указывает EDI. Таким образом,
в дополнение к строковым операциям (MOVS/INS/STOS/CMPS/SCASB/W/D / Q и т. д.) упомянутый в других ответах, я хотел добавить, что есть также более «современные» инструкции по сборке x86, которые неявно используют по крайней мере EDI/RDI:
С поддержкой SSE2 MASKMOVDQU (и предстоящий AVX VMASKMOVDQU ) инструкция выборочно записывает байты из регистра XMM в память, на которую указывает EDI / RDI.
в дополнение к регистрам, используемым для массовых операций, они полезны для их свойства сохранения через вызов функции (call-preserved) в 32-битном соглашении о вызове. Институт, ЕОИ, регистры ebx, ЭБП ВСД называют сохранившиеся в то время как в eax, ecx и edX имеют не называют сохранившиеся. Регистры, сохраненные при вызове, уважаются библиотечной функцией C, и их значения сохраняются при вызовах библиотечной функции C.
Джефф Duntemann в его ассемблере книга пример код сборки для печати аргументов командной строки. Код использует ESI и edi для хранения счетчиков, поскольку они будут неизменны функцией библиотеки C printf. Для других регистров, таких как eax, ecx, edx, нет никакой гарантии, что они не будут использоваться функциями библиотеки C.
см. раздел 12.8 как C видит аргументы командной строки.
обратите внимание, что 64-битный вызов соглашения отличаются от 32-битных соглашений о вызовах, и я не уверен, что эти регистры сохраняются при вызове или нет.
Ассемблер. Сегменты памяти и регистры
Обновл. 27 Сен 2021 |
Результат выполнения программы:
Сегменты памяти
Модель сегментированной памяти разбивает системную память на группы независимых сегментов, на которые указывают указатели, расположенные в регистрах сегментов. Каждый сегмент используется для хранения данных определенного типа. Первый сегмент используется для хранения кода инструкций, второй — для хранения элементов данных, а третий — для программного стека.
Сегменты памяти:
Сегмент данных (data segment) — представлен секциями .data и .bss. Секция .data используется для объявления области памяти, где хранятся элементы данных для программы. Эта секция не может быть расширена после объявления элементов данных, и она остается статической во всей программе. Секция .bss также является секцией статической памяти, содержащей буферы для данных, которые будут объявлены в программе позже. Эта буферная память заполнена нулями.
Сегмент кода (code segment) — представлен секцией .text. Он определяет область в памяти, в которой хранятся коды инструкций. Это также фиксированная область.
Стек (stack) — это сегмент, который содержит значения данных, передаваемые в функции и процедуры в программе.
Регистры
Обычно операции с процессором включают в себя обработку данных. Эти данные могут быть как сохранены в памяти, так и извлечены оттуда. Однако процесс чтения данных из памяти и хранения данных в памяти замедляет работу процессора, так как это предполагает сложный процесс отправки запроса данных в блок памяти и получение данных обратно из блока по одному и тому же каналу — через шину управления.
Чтобы ускорить свою работу, процессор подключает определенные внутренние места хранения памяти, которые называются регистрами. Регистры хранят элементы данных для обработки без необходимости получать доступ к памяти. Ограниченное количество регистров встроено в чип процессора.
Регистры процессора
В архитектуре IA-32 есть десять 32-битных и шесть 16-битных процессорных регистров. Регистры делятся на три категории:
Общие регистры (General Registers);
Регистры управления (Control Registers);
Сегментные регистры (Segment Registers).
В свою очередь, общие регистры делятся на следующие:
Регистры данных (Data Registers);
Регистры-указатели (Pointer Registers);
Индексные регистры (Index Registers).
Регистры данных
Регистры данных — это четыре 32-битных регистра, которые используются для арифметических, логических и других операций. Эти 32-битные регистры могут быть использованы следующими тремя способами:
как полные 32-битные регистры данных: EAX, EBX, ECX, EDX;
нижние половины 32-битных регистров могут использоваться как четыре 16-битных регистра данных: AX, BX, CX и DX;
нижняя и верхняя половины вышеупомянутых четырех 16-битных регистров могут использоваться как восемь 8-битных регистров данных: AH, AL, BH, BL, CH, CL, DH и DL.
Некоторые из этих регистров данных имеют специфическое применение в арифметических операциях:
AX (primary accumulator) — используется для ввода/вывода и в большинстве арифметических операций. Например, в операции умножения один операнд сохраняется в регистре EAX/AX/AL в соответствии с размером операнда.
BX (base register) — используется при индексированной адресации.
CX (count register) — хранит количество циклов в повторяющихся операциях (также, как и регистры ECX и CX).
DX (data register) — используется в операциях ввода/вывода, а также с регистрами AX и DX для выполнения операций умножения и деления, связанных с большими значениями.
Регистры-указатели
Регистрами-указателями являются 32-битные регистры EIP, ESP и EBP и соответствующие им 16-битные регистры IP, SP и BP. Есть три категории регистров-указателей:
Указатель на инструкцию или команду (Instruction Pointer или IP) — 16-битный регистр IP хранит смещение адреса следующей команды, которая должна быть выполнена. IP в сочетании с регистром CS (как CS:IP) предоставляет полный адрес текущей инструкции в сегменте кода.
Указатель на стек (Stack Pointer или SP) — 16-битный регистр SP обеспечивает значение смещения в программном стеке. SP в сочетании с регистром SS (SS:SP) означает текущее положение данных или адреса в программном стеке.
Базовый указатель (Base Pointer или BP) — 16-битный регистр BP используется в основном при передаче параметров в подпрограммы. Адрес в регистре SS объединяется со смещением в BP, чтобы получить местоположение параметра. BP также можно комбинировать с DI и SI в качестве базового регистра для специальной адресации.
Индексные регистры
В процессоре существуют 32-битные индексные регистры ESI и EDI и их 16-битные версии: SI и DI. Все они используются в индексированной адресации, и, иногда, в операциях сложения/вычитания. Есть два типа индексных указателей:
Исходный индекс (Source Index или SI) — используется в качестве исходного индекса в строковых операциях.
Индекс назначения (Destination Index или DI) — используется в качестве индекса назначения в строковых операциях.
Регистры управления
Регистром управления является объединенный 32-битный регистр инструкций и 32-битный регистр флагов (регистр процессора, отражающий его текущее состояние). Многие инструкции включают в себя операции сравнения и математические вычисления, которые способны изменить состояния флагов, а некоторые другие условные инструкции проверяют значения флагов состояния, чтобы перенести поток управления в другое место.
Распространенные битовые флаги:
Флаг переполнения (Overflow Flag или OF) — указывает на переполнение старшего бита данных (крайнего левого бита) после signed арифметической операции.
Флаг ловушка (Trap Flag или TF) — позволяет настроить работу процессора в одношаговом режиме.
Вспомогательный флаг переноса (Auxiliary Carry Flag или AF) — после выполнения арифметической операции содержит перенос от бита 3 до бита 4. Используется для специализированной арифметики. AF устанавливается, когда 1-байтовая арифметическая операция вызывает перенос из бита 3 в бит 4.
Флаг переноса (Carry Flag или CF) — после выполнения арифметической операции содержит перенос 0 или 1 из старшего бита (крайнего слева). Кроме того, хранит содержимое последнего бита операции сдвига или поворота.
В следующей таблице указано положение битовых флагов в 16-битном регистре флагов:
Флаг: | O | D | I | T | S | Z | A | P | C | |||||||
Бит №: | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Сегментные регистры
Сегменты — это специфические части программы, которые содержат данные, код и стек. Есть три основных сегмента:
Сегмент кода (Code Segment или CS) — содержит все команды и инструкции, которые должны быть выполнены. 16-битный регистр сегмента кода или регистр CS хранит начальный адрес сегмента кода.
Сегмент данных (Data Segment или DS) — содержит данные, константы и рабочие области. 16-битный регистр сегмента данных или регистр DS хранит начальный адрес сегмента данных.
Сегмент стека (Stack Segment или SS) — содержит данные и возвращаемые адреса процедур или подпрограмм. Он представлен в виде структуры данных «Стек». Регистр сегмента стека или регистр SS хранит начальный адрес стека.
Кроме регистров CS, DS и SS существуют и другие регистры дополнительных сегментов — ES (Extra Segment), FS и GS, которые предоставляют дополнительные сегменты для хранения данных.
Сегментные регистры хранят начальные адреса сегмента. Чтобы получить точное местоположение данных или команды в сегменте, требуется значение смещения. Чтобы сослаться на любую ячейку памяти в сегменте, процессор объединяет адрес сегмента в сегментном регистре со значением смещения местоположения.
Пример на практике
Посмотрите на следующую простую программу, чтобы понять, как используются регистры в программировании на ассемблере. Эта программа выводит 9 звёздочек с простым сообщением:
Руководство по ассемблеру x86 для начинающих
В наше время редко возникает необходимость писать на чистом ассемблере, но я определённо рекомендую это всем, кто интересуется программированием. Вы увидите вещи под иным углом, а навыки пригодятся при отладке кода на других языках.
В этой статье мы напишем с нуля калькулятор обратной польской записи (RPN) на чистом ассемблере x86. Когда закончим, то сможем использовать его так:
Весь код для статьи здесь. Он обильно закомментирован и может служить учебным материалом для тех, кто уже знает ассемблер.
Начнём с написания базовой программы Hello world! для проверки настроек среды. Затем перейдём к системным вызовам, стеку вызовов, стековым кадрам и соглашению о вызовах x86. Потом для практики напишем некоторые базовые функции на ассемблере x86 — и начнём писать калькулятор RPN.
Предполагается, что у читателя есть некоторый опыт программирования на C и базовые знания компьютерной архитектуры (например, что такое регистр процессора). Поскольку мы будем использовать Linux, вы также должны уметь использовать командную строку Linux.
Настройка среды
Как уже сказано, мы используем Linux (64- или 32-битный). Приведённый код не работает в Windows или Mac OS X.
Я бы также рекомендовал держать под рукой таблицу ASCII.
Hello, world!
Для проверки среды сохраните следующий код в файле calc.asm :
Комментарии объясняют общую структуру. Список регистров и общих инструкций можете изучить в «Руководстве по ассемблеру x86 университета Вирджинии». При дальнейшем обсуждении системных вызовов это тем более понадобится.
Следующие команды собирают файл ассемблера в объектный файл, а затем компонует исполняемый файл:
После запуска вы должны увидеть:
Makefile
Затем вместо вышеприведённых инструкций просто запускаем make.
Системные вызовы
Системные вызовы Linux указывают ОС выполнить для нас какие-то действия. В этой статье мы используем только два системных вызова: write() для записи строки в файл или поток (в нашем случае это стандартное устройство вывода и стандартная ошибка) и exit() для выхода из программы:
eax | ebx | ecx | edx |
---|---|---|---|
Номер системного вызова | arg1 | arg2 | arg3 |
Стек вызовов
Стек вызовов — структура данных, в которой хранится информация о каждом обращении к функции. У каждого вызова собственный раздел в стеке — «фрейм». Он хранит некоторую информацию о текущем вызове: локальные переменные этой функции и адрес возврата (куда программа должна перейти после выполнения функции).
Сразу отмечу одну неочевидную вещь: стек увеличивается вниз по памяти. Когда вы добавляете что-то на верх стека, оно вставляется по адресу памяти ниже, чем предыдущий элемент. Другими словами, по мере роста стека адрес памяти в верхней части стека уменьшается. Чтобы избежать путаницы, я буду всё время напоминать об этом факте.
Соглашение о вызовах для архитектуры x86
В х86 нет встроенного понятия функции как в высокоуровневых языках. Инструкция call — это по сути просто jmp ( goto ) в другой адрес памяти. Чтобы использовать подпрограммы как функции в других языках (которые могут принимать аргументы и возвращать данные обратно), нужно следовать соглашению о вызовах (существует много конвенций, но мы используем CDECL, самое популярное соглашение для x86 среди компиляторов С и программистов на ассемблере). Это также гарантирует, что регистры подпрограммы не перепутаются при вызове другой функции.
Правила вызывающей стороны
Перед вызовом функции вызывающая сторона должна:
Правила вызываемой подпрограммы
Перед вызовом подпрограмма должна:
Стек вызовов после шага 2:
Стек вызовов после шага 4:
На последней диаграмме также можно заметить, что локальные переменные функции всегда начинается на 4 байта выше ebp с адреса ebp-4 (здесь вычитание, потому что мы двигаемся вверх по стеку), а аргументы функции всегда начинается на 8 байт ниже ebp с адреса ebp+8 (сложение, потому что мы двигаемся вниз по стеку). Если следовать правилам из этой конвенции, так будет c переменными и аргументами любой функции.
Когда функция выполнена и вы хотите вернуться, нужно сначала установить eax на возвращаемое значение функции, если это необходимо. Кроме того, нужно:
Вход и выход
Написание некоторых основных функций
Здесь понадобится ещё одна функция _strlen для подсчёта длины строки. На C она может выглядеть так:
Другими словами, с самого начала строки мы добавляем 1 к возвращаемым значением для каждого символа, кроме нуля. Как только замечен нулевой символ, возвращаем накопленное в цикле значение. В ассемблере это тоже довольно просто: можно использовать как базу ранее написанную функцию _subtract :
И посмотрим плоды нашей тяжёлой работы, используя эту функцию в полной программе “Hello, world!”.
Хотите верьте, хотите нет, но мы рассмотрели все основные темы, которые нужны для написания базовых программ на ассемблере x86! Теперь у нас есть весь вводный материал и теория, так что полностью сосредоточимся на коде и применим полученные знания для написания нашего калькулятора RPN. Функции будут намного длиннее и даже станут использовать некоторые локальные переменные. Если хотите сразу увидеть готовую программу, вот она.
Создание стека
Теперь можно реализовать функции _push и _pop :
Вывод чисел
На C программа будет выглядеть примерно так:
Теперь вы понимаете, зачем нам эти три функции. Давайте реализуем это на ассемблере:
Теперь у нас есть все необходимые функции, осталось реализовать основную логику в _start — и на этом всё!
Вычисление обратной польской записи
Как мы уже говорили, обратная польская запись вычисляется с помощью стека. При чтении число заносится на стек, а при чтении оператор применяется к двум объектам наверху стека.
Например, если мы хотим вычислить 84/3+6* (это выражение также можно записать в виде 6384/+* ), процесс выглядит следующим образом:
Шаг | Символ | Стек перед | Стек после |
---|---|---|---|
1 | 8 | [] | [8] |
2 | 4 | [8] | [8, 4] |
3 | / | [8, 4] | [2] |
4 | 3 | [2] | [2, 3] |
5 | + | [2, 3] | [5] |
6 | 6 | [5] | [5, 6] |
7 | * | [5, 6] | [30] |
Если на входе допустимое постфиксное выражение, то в конце вычислений на стеке остаётся лишь один элемент — это и есть ответ, результат вычислений. В нашем случае число равно 30.
В ассемблере нужно реализовать нечто вроде такого кода на C:
Теперь у нас имеются все функции, необходимые для реализации этого, давайте начнём.
И мы закончили! Удивите всех своих друзей, если они у вас есть. Надеюсь, теперь вы с большей теплотой отнесётесь к языкам высокого уровня, особенно если вспомнить, что многие старые программы писали полностью или почти полностью на ассемблере, например, оригинальный RollerCoaster Tycoon!
Весь код здесь. Спасибо за чтение! Могу продолжить, если вам интересно.
Дальнейшие действия
Можете попрактиковаться, реализовав несколько дополнительных функций: