Регистры процессора
Главная / Ассемблер / Для чайников / Введение в Ассемблер /
Начиная с модели 80386 процессоры Intel предоставляют 16 основных регистров для пользовательских программ и ещё 11 регистров для работы с мультимедийными приложениями (MMX) и числами с плавающей точкой (FPU/NPX). Все команды так или иначе изменяют содержимое регистров. Как уже говорилось, обращаться к регистрам быстрее и удобнее, чем к памяти. Поэтому при программировании на языке Ассемблера регистры используются очень широко.
В этом разделе мы рассмотрим основные регистры процессоров Intel. Названия и состав/количество регистров для других процессоров могут отличаться. Итак, основные регистры процессоров Intel.
Таблица 2.1. Основные регистры процессора.
Название | Разрядность | |
Аккумулятор | ||
База | ||
Счётчик | ||
Регистр данных | ||
Указатель базы | ||
Указатель стека | ||
Индекс источника | ||
Индекс приёмника | ||
Регистр флагов | ||
Указатель инструкции (команды) | ||
Сегментный регистр | ||
Сегментный регистр | ||
Сегментный регистр | ||
Сегментный регистр | ||
Сегментный регистр | ||
Сегментный регистр |
Регистры EAX, EBX, ECX, EDX – это регистры общего назначения. Они имеют определённое назначение (так уж сложилось исторически), однако в них можно хранить любую информацию.
Регистры EBP, ESP, ESI, EDI – это также регистры общего назначения. Они имеют уже более конкретное назначение. В них также можно хранить пользовательские данные, но делать это нужно уже более осторожно, чтобы не получить «неожиданный» результат.
Регистр флагов и сегментные регистры требуют отдельного описания и будут более подробно рассмотрены далее.
Пока для вас здесь слишком много непонятных слов, но со временем всё прояснится)))
Когда-то процессоры были 16-разрядными, и, соответственно, все их регистры были также 16-разрядными. Для совместимости со старыми программами, а также для удобства программирования некоторые регистры разделены на 2 или 4 «маленьких» регистра, у каждого из которых есть свои имена. В таблице 2.2 перечислены такие регистры.
Вот пример такого регистра.
Из этого следует, что вы можете написать в своей программе, например, такие команды:
MOV AX, 1 MOV EAX, 1
Обе команды поместят в регистр AX число 1. Разница будет заключаться только в том, что вторая команда обнулит старшие разряды регистра EAX, то есть после выполнения второй команды в регистре EAX будет число 1. А первая команда оставит в старших разрядах регистра EAX старые данные. И если там были данные, отличные от нуля, то после выполнения первой команды в регистре EAX будет какое-то число, но не 1. А вот в регистре AX будет число 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 |
На этом мы закончим наше краткое знакомство с регистрами. Если вам пока не всё понятно – просто прочитайте этот раздел, чтобы более-менее представлять себе, что такое регистры. По мере приобретения новых знаний вы можете вернуться к этому разделу и уже на новом уровне воспринять эту информацию. А в следующем разделе мы коротко опишем процесс выполнения команды.
виды, назначение и особенности команд — ABC IMPORT
28-01-2019 16:04
Содержание статьи:
- Виды регистров
- Особенности использования регистров
- Регистры общего назначения
- Регистры указатели
- Регистры-индексы
- Сегментные регистры
- Регистр указателя команд
- Регистр флагов ассемблера
Ячейки процессора, которые также называются регистры ассемблера, из-за управляющего ими низкоуровневого языка программирования представляют собой некий блок свободных элементов в памяти.
Напрямую обратиться к регистру невозможно. Кроме того, имеется ряд доступных блоков памяти, однако обратиться к ним возможно только из оболочки операционной системы. К таковым относят управляющие сегментные регистры, а также теневые системы дескрипторов. Применяют в своей работе данные регистры исключительно девелоперы ОС.
Вам будет интересно:Как записать гитару в FL Studio: простейшие методы
Виды регистров
Для различных нужд во время программирования применяются разные регистры Assembler. Используют их в зависимости от целей. К примеру, регистр счетчика применяется для организации как простых, так и вложенных циклов. Ниже перечислены основные типы регистров ассемблера:
- Регистры общего назначения.
- Индексные регистры.
- Регистры-указатели.
- Сегментные блоки памяти.
- Регистры флагов.
Вам будет интересно:Как сделать визитку в иллюстраторе своими силами
Фактически все регистры занимают в памяти 32 бита. То есть могут содержать числа от нуля до 4294967295. Некоторые из регистров разделены на несколько частей по 16 и 8 бит. Это позволяет управлять либо частью блока памяти, либо ячейкой целиком, записывая в нее только часть данных.
Регистры ассемблера получили название согласно выполняемым функциям:
- ЕАХ – Accumulator регистра аккумулятора;
- EBX – Base – база;
- ECX – Counter – отвечает за счет;
- EDX – Data – блок ячеек данных;
- ESI – Source Index – регистр источника;
- EDI – Destination Index – регистр приемника;
- ESP – Pointer of stack – указатель на стек;
- EBP – Base Pointer — указатель основания стека.
Особенности использования регистров
Стоит учитывать то, что каждый из обозначенных регистров может применяться не только для внесения данных. К примеру, в регистр базы можно внести любое десятичное число и пользоваться им как счетчиком. Однако при этом нежелательно применять в этих целях блоки памяти, которые соответствуют указателям – ESP и EBP, так как при этом могут возникать проблемы с доступом к ячейкам абстрактного типа данных. Назначение регистров ассемблера — хранение некой информации, для чего может использоваться любой их вид.
Регистры общего назначения
Данный вид регистров создан для сохранения данных после вычислительных операций. Фактически в них можно при помощи команды mov внести информацию в любом виде и системе исчисления: двоичной, восьмеричной, десятичной или шестнадцатеричной. Другое название — регистры данных ассемблера. К перечню регистров общего назначения assembler относят:
- ЕАХ (accumulator) регистр аккумуляции. Состоит из трех младших блоков по 8 бит: АХ, АН, AL. При необходимости можно обращаться к двум младшим блокам.
- ЕВХ (base) – блок данных, который отвечает за базу. Так же, как и все регистры общего назначения, состоит из двух младших разделов по 8 бит и одного шестнадцатибитного. Таким образом в один регистр можно поместить сразу несколько числовых значений.
- ЕСХ (counter) – отвечает за счетчик. Используется во время выполнения циклов. Без него не работает команда loop. Состоит из двух частей, одна из которых включает два восьмибитных блока СН и CL.
- EDX – Data – требуется для операций точного определения адреса в оперативной памяти для функций ввода и вывода. Кроме того, в этот регистр можно помещать данные для переадресации на использование в процедурах и шаблонах.
Регистры указатели
Для работы со стеком в assembler разработчиками предусмотрено два вида регистра. Для доступа к ним осуществляется операция прибавления к указателю вершины абстрактного типа значений битности определенного типа данных, который был помещен в стек. Все расчеты проводятся вручную. Таким образом сохраняется большое количество данных и передается в подпрограммы – процедуры и массивы. Среди регистров указателей в ассемблере выделяют:
- Регистр ESP – указатель на вершину стека. Всегда содержит адрес первого элемента, который был помещен в стек в кэш-памяти процессора. При необходимости может быть заполнен другими данными. Включает младший регистр SP, состоящий из 16 бит.
- Регистр ЕВР (Base Pointer) – представляет собой блок ячеек памяти, требующийся для адресации данных, которые содержатся в стеке. При этом значительно облегчает доступ ко всем данным и переменным. Включает младший разряд ВР.
Регистры-индексы
Индексные блоки памяти требуются для расширенной индексации. Кроме того, они участвуют в работе некоторых арифметических операций и обработки байтовых строк – последовательности байт, содержащих произвольное значение. В assembler включено два регистра, которые отвечают за индексирование ESI и EDI. Опишем их:
- ESI (Source index) включает индекс источника (место, откуда берутся данные) и нужен для части действий над байтовыми строками;
- EDI (Destination index) требуется для записи результатов вычислений. Также применяется для части строковых действий. Частично связан с регистром сегментов ES.
Сегментные регистры
Являются первыми блоками в памяти. Называются текущими сегментами. Программному обеспечению разрешается распределять более четырех блоков памяти. Однако при этом обязательно занести адреса блоков в ячейки памяти между сегментными регистрами. Данный вид блоков памяти является строго специфичным, благодаря чему невозможно заполнять их отдельным видом данных. Порядок блоков регистров в памяти может меняться. Хранение сегментных регистров производится в произвольном порядке в случайных местах памяти.
- Регистр кода CS в обычном виде содержит адрес начала сегмента кода программного обеспечения (начала машинного представления кода). Таким образом осуществляется переход по командам за счет командного указателя IP.
- Регистр данных содержит адрес данных, которые обрабатывает программа в момент начала запуска. Передвижение по данным осуществляется за счет смещения, которое записано в регистр EIP.
- Stack Segment (ESS) нужен для сохранения начала сегмента абстрактного типа данных.
- Extra segment – вспомогательный регистр, который содержит пустую область для записи данных во время некоторых действий над байтовыми строками. Может содержать информацию, аналогичную регистру данных.
Регистр указателя команд
Данный вид относится к командным. С помощью данного указателя осуществляется вывод регистра ассемблера в листинг. Включает данные по поводу смещения на следующую команду относительно предыдущей. При разработке программного обеспечения практически не используется, однако требуется для просмотра листинга выполнения кода. Таким образом отслеживают ошибки.
Регистр флагов ассемблера
Отвечает за текущее состояние центрального процессора. Состоит из 16 бит, из которых могут быть заняты только 9. Заполнение данного блока памяти осуществляется после выполнения, пропуска или кода ошибки в результате предыдущей команды. Кроме того, часть битов используется процессором и может инициализироваться и удаляться посредством определенной системы команд. Таким образом осуществляется управление системой команд.
Источник
Автор: Макар Беляев
Похожие статьи
Как подключить «Яндекс.Диск» как сетевой в стационарных Windows-системах и мобильных ОС Android?
Как с «Гугл.Диска» скачивать целые папки или отдельные файлы: несколько простых способов
Браузер «Тор» не запускается: нюансы установки, использования и устранения проблем
Драйвер SPTD: что это такое и как его удалить?
Как подключить «Яндекс.Диск» как сетевой в стационарных Windows-системах и мобильных ОС Android?
Как посмотреть жесткий диск в БИОСе на компьютере: инструкция проверки
Как настроить эквалайзер на Windows 7 и выше: нюансы выбора опций и программного обеспечения
Ошибка статуса VPN в «Хамачи»: способы быстрого решения проблемы
Ошибка статуса VPN в «Хамачи»: способы быстрого решения проблемы
Как с «Гугл. Диска» скачивать целые папки или отдельные файлы: несколько простых способов
Шпаргалка по основным инструкциям ассемблера x86/x64
В прошлой статье мы написали наше первое hello world приложение на асме, научились его компилировать и отлаживать, а также узнали, как делать системные вызовы в Linux. Сегодня же мы познакомимся непосредственно с ассемблерными инструкциями, понятием регистров, стека и вот этого всего. Ассемблеры для архитектур x86 (a.k.a i386) и x64 (a.k.a amd64) очень похожи, в связи с чем нет смысла рассматривать их в отдельных статьях. Притом акцент я постараюсь делать на x64, попутно отмечая отличия от x86, если они есть. Далее предполагается, что вы уже знаете, например, чем стек отличается от кучи, и объяснять такие вещи не требуется.
Регистры общего назначения
Регистр — это небольшой (обычно 4 или 8 байт) кусочек памяти в процессоре с чрезвычайно большой скоростью доступа. Регистры делятся на регистры специального назначения и регистры общего назначения. Нас сейчас интересуют регистры общего назначения. Как можно догадаться по названию, программа может использовать эти регистры под свои нужды, как ей вздумается.
На x86 доступно восемь 32-х битных регистров общего назначения — eax, ebx, ecx, edx, esp, ebp, esi и edi. Регистры не имеют заданного наперед типа, то есть, они могут трактоваться как знаковые или беззнаковые целые числа, указатели, булевы значения, ASCII-коды символов, и так далее. Несмотря на то, что в теории эти регистры можно использовать как угодно, на практике обычно каждый регистр используется определенным образом. Так, esp указывает на вершину стека, ecx играет роль счетчика, а в eax записывается результат выполнения операции или процедуры. Существуют 16-и битные регистры ax, bx, cx, dx, sp, bp, si и di, представляющие собой 16 младших бит соответствующих 32-х битных регистров. Также доступны и 8-и битовые регистры ah, al, bh, bl, ch, cl, dh и dl, которые представляют собой старшие и младшие байты регистров ax, bx, cx и dx соответственно.
Рассмотрим пример. Допустим, выполняются следующие три инструкции:
(gdb) x/3i $pc
=> 0x8048074: mov $0xaabbccdd,%eax
0x8048079: mov $0xee,%al
0x804807b: mov $0x1234,%ax
Значения регистров после записи в eax значения 0xAABBCCDD:
(gdb) p/x $eax
$1 = 0xaabbccdd
(gdb) p/x $ax
$2 = 0xccdd
(gdb) p/x $ah
$3 = 0xcc
(gdb) p/x $al
$4 = 0xdd
Значения после записи в регистр al значения 0xEE:
(gdb) p/x $eax
$5 = 0xaabbccee
(gdb) p/x $ax
$6 = 0xccee
(gdb) p/x $ah
$7 = 0xcc
(gdb) p/x $al
$8 = 0xee
Значения регистров после записи в ax числа 0x1234:
(gdb) p/x $eax
$9 = 0xaabb1234
(gdb) p/x $ax
$10 = 0x1234
(gdb) p/x $ah
$11 = 0x12
(gdb) p/x $al
$12 = 0x34
Как видите, ничего сложного.
Примечание: Синтаксис GAS позволяет явно указывать размеры операндов путем использования суффиксов b (байт), w (слово, 2 байта), l (длинное слово, 4 байта), q (четверное слово, 8 байт) и некоторых других. Например, вместо команды mov $0xEE, %al
можно написать movb $0xEE, %al
, вместо mov $0x1234, %ax
— movw $0x1234, %ax
, и так далее. В современном GAS эти суффиксы являются опциональными и я лично их не использую. Но не пугайтесь, если увидите их в чужом коде.
На x64 размер регистров был увеличен до 64-х бит. Соответствующие регистры получили название rax, rbx, и так далее. Кроме того, регистров общего назначения стало шестнадцать вместо восьми. Дополнительные регистры получили названия r8, r9, …, r15. Соответствующие им регистры, которые представляют младшие 32, 16 и 8 бит, получили название r8d, r8w, r8b, и по аналогии для регистров r9-r15. Кроме того, появились регистры, представляющие собой младшие 8 бит регистров rsi, rdi, rbp и rsp — sil, dil, bpl и spl соответственно.
Про адресацию
Как уже отмечалось, регистры могут трактоваться, как указатели на данные в памяти. Для разыменования таких указателей используется специальный синтаксис:
mov (%rsp), %rax
Эта запись означает «прочитай 8 байт по адресу, записанному в регистре rsp, и сохрани их в регистр rax». При запуске программы rsp указывает на вершину стека, где хранится число аргументов, переданных программе (argc), указатели на эти аргументы, а также переменные окружения и кое-какая другая информация. Таким образом, в результате выполнения приведенной выше инструкции (разумеется, при условии, что перед ней не выполнялось каких-либо других инструкций) в rax будет записано количество аргументов, с которыми была запущена программа.
В одной команде можно указывать адрес и смешение (как положительное, так и отрицательное) относительно него:
mov 8(%rsp), %rax
Эта запись означает «возьми rsp, прибавь к нему 8, прочитай 8 байт по получившемуся адресу и положи их в rax». Таким образом, в rax будет записан адрес строки, представляющей собой первый аргумент программы, то есть, имя исполняемого файла.
При работе с массивами бывает удобно обращаться к элементу с определенным индексом. Соответствующий синтаксис:
# инструкция xchg меняет значения местами
xchg 16(%rsp,%rcx,8), %rax
Читается так: «посчитай rcx*8 + rsp + 16, и поменяй местами 8 байт (размер регистра) по получившемуся адресу и значение регистра rax». Другими словами, rsp и 16 все так же играют роль смещения, rcx играет роль индекса в массиве, а 8 — это размер элемента массива. При использовании данного синтаксиса допустимыми размерами элемента являются только 1, 2, 4 и 8. Если требуется какой-то другой размер, можно использовать инструкции умножения, бинарного сдвига и прочие, которые мы рассмотрим далее.
Наконец, следующий код тоже валиден:
.data
msg:
.ascii «Hello, world!\n»
.text
.globl _start
_start:
# обнуление rcx
xor %rcx, %rcx
mov msg(,%rcx,8), %al
mov msg, %ah
В смысле, что можно не указывать регистр со смещением или вообще какие-либо регистры. В результате выполнения этого кода в регистры al и ah будет записан ASCII-код буквы H, или 0x48.
В этом контексте хотелось бы упомянуть еще одну полезную ассемблерную инструкцию:
# rax := rcx*8 + rax + 123
lea 123(%rax,%rcx,8), %rax
Инструкция lea очень удобна, так как позволяет сразу выполнить умножение и несколько сложений.
Fun fact! На x64 в байткоде инструкций никогда не используются 64-х битовые смещения. В отличие от x86, инструкции часто оперируют не абсолютными адресами, а адресами относительно адреса самой инструкции, что позволяет обращаться к ближайшим +/- 2 Гб оперативной памяти. Соответствующий синтаксис:
movb msg(%rip), %al
Сравним длины опкодов «обычного» и «относительного» mov (objdump -d
):
4000b0: 8a 0c 25 e8 00 60 00 mov 0x6000e8,%cl
4000b7: 8a 05 2b 00 20 00 mov 0x20002b(%rip),%al # 0x6000e8
Как видите, «относительный» mov еще и на один байт короче! Что это за регистр такой rip мы узнаем чуть ниже.
Для записи же полного 64-х битового значения в регистр предусмотрена специальная инструкция:
movabs $0x1122334455667788, %rax
Другими словами, процессоры x64 так же экономно кодируют инструкции, как и процессоры x86, и в наше время нет особо смысла использовать процессоры x86 в системах, имеющих пару гигабайт оперативной памяти или меньше (мобильные устройства, холодильники, микроволновки, и так далее). Скорее всего, процессоры x64 будут даже более эффективны за счет большего числа доступных регистров и большего размера этих регистров.
Арифметические операции
Рассмотрим основные арифметические операции:
# инциализируем значения регистров
mov $123, %rax
mov $456, %rcx
# инкремент: rax = rax + 1 = 124
inc %rax
# декремент: rax = rax — 1 = 123
dec %rax
# сложение: rax = rax + rcx = 579
add %rcx, %rax
# вычитание: rax = rax — rcx = 123
sub %rcx, %rax
# изменение знака: rcx = — rcx = -456
neg %rcx
Здесь и далее операндами могут быть не только регистры, но и участки памяти или константы. Но оба операнда не могут быть участками памяти. Это правило применимо ко всем инструкциям ассемблера x86/x64, по крайней мере, из рассмотренных в данной статье.
Пример умножения:
mov $100, %al
mov $3, %cl
mul %cl
В данном примере инструкция mul умножает al на cl, и сохраняет результат умножения в пару регистров al и ah. Таким образом, ax примет значение 0x12C или 300 в десятичной нотации. В худшем случае для сохранения результата перемножения двух N-байтовых значений может потребоваться до 2*N байт. В зависимости от размера операнда результат сохраняется в al:ah, ax:dx, eax:edx или rax:rdx. Притом в качестве множителей всегда используется первый из этих регистров и переданный инструкции аргумент.
Знаковое умножение производится точно так же при помощи инструкции imul. Кроме того, существуют варианты imul с двумя и тремя аргументами:
mov $123, %rax
mov $456, %rcx
# rax = rax * rcx = 56088
imul %rcx, %rax
# rcx = rax * 10 = 560880
imul $10, %rax, %rcx
Инструкции div и idiv производят действия, обратные mul и imul. Например:
mov $0, %rdx
mov $456, %rax
mov $123, %rcx
# rax = rdx:rax / rcx = 3
# rdx = rdx:rax % rcx = 87
div %rcx
Как видите, был получен результат целочисленного деления, а также остаток от деления.
Это далеко не все арифметические инструкции. Например, есть еще adc (сложение с учетом флага переноса), sbb (вычитание с учетом займа), а также соответствующие им инструкции, выставляющие и очищающие соответствующие флаги (ctc, clc), и многие другие. Но они распространены намного меньше, и потому в рамках данной статьи не рассматриваются.
Логические и битовые операции
Как уже отмечалось, особой типизации в ассемблере x86/x64 не предусмотрено. Поэтому не стоит удивляться, что в нем нет отдельных инструкций для выполнения булевых операций и отдельных для выполнения битовых операций. Вместо этого есть один набор инструкций, работающих с битами, а уж как интерпретировать результат — решает конкретная программа.
Так, например, выглядит вычисление простейшего логического выражения:
mov $0, %rax # a = false
mov $1, %rbx # b = true
mov $0, %rcx # c = false
# rdx := a || !(b && c)
mov %rcx, %rdx # rdx = c
and %rbx, %rdx # rdx &= b
not %rdx # rdx = ~ rdx
or %rax, %rdx # rdx |= a
and $1, %rdx # rdx &= 1
Заметьте, что здесь мы использовали по одному младшему биту в каждом из 64-х битовых регистров. Таким образом, в старших битах образуется мусор, который мы обнуляем последней командой.
Еще одна полезная инструкция — это xor (исключающее или). В логических выражениях xor используется нечасто, однако с его помощью часто происходит обнуление регистров. Если посмотреть на опкоды инструкций, то становится понятно, почему:
4000b3: 48 31 db xor %rbx,%rbx
4000b6: 48 ff c3 inc %rbx
4000b9: 48 c7 c3 01 00 00 00 mov $0x1,%rbx
Как видите, инструкции xor и inc кодируются всего лишь тремя байтами каждая, в то время, как делающая то же самое инструкция mov занимает целых семь байт. Каждый отдельный случай, конечно, лучше бенчмаркать отдельно, но общее эвристическое правило такое — чем короче код, тем больше его помещается в кэши процессора, тем быстрее он работает.
В данном контексте также следует вспомнить инструкции побитового сдвига, тестирования битов (bit test) и сканирования битов (bit scan):
# положим что-нибудь в регистр
movabs $0xc0de1c0ffee2beef, %rax
# сдвиг влево на 3 бита
# rax = 0x0de1c0ffee2beef0
shl $4, %rax
# сдвиг вправо на 7 бит
# rax = 0x001bc381ffdc57dd
shr $7, %rax
# циклический сдвиг вправо на 5 бит
# rax = 0xe800de1c0ffee2be
ror $5, %rax
# циклический сдвиг влево на 5 бит
# rax = 0x001bc381ffdc57dd
rol $5, %rax
# положить в CF (см далее) значение 13-го бита
# CF = !!(0x1bc381ffdc57dd & (1 << 13)) = 0
bt $13, %rax
# то же самое + установить бит (bit test and set)
# rax = 0x001bc381ffdc77dd, CF = 0
bts $13, %rax
# то же самое + сбросить бит (bit test and reset)
# rax = 0x001bc381ffdc57dd, CF = 1
btr $13, %rax
# то же самое + инвертировать бит (bit test and complement)
# rax = 0x001bc381ffdc77dd, CF = 0
btc $13, %rax
# найти самый младший ненулевой байт (bit scan forward)
# rcx = 0, ZF = 0
bsf %rax, %rcx
# найти самый старший ненулевой байт (bit scan reverse)
# rdx = 52, ZF = 0
bsr %rax, %rdx
# если все биты нулевые, ZF = 1, значение rdx неопределено
xor %rax, %rax
bsf %rax, %rdx
Еще есть битовые сдвиги со знаком (sal, sar), циклические сдвиги с флагом переноса (rcl, rcr), а также сдвиги двойной точности (shld, shrd). Но используются они не так уж часто, да и утомишься перечислять вообще все инструкции. Поэтому их изучение я оставляю вам в качестве домашнего задания.
Условные выражения и циклы
Выше несколько раз упоминались какие-то там флаги, например, флаг переноса. Под флагами понимаются биты специального регистра eflags / rflags (название на x86 и x64 соответственно). Напрямую обращаться к этому регистру при помощи инструкций mov, add и подобных нельзя, но он изменяется и используется различными инструкциями косвенно. Например, уже упомянутый флаг переноса (carry flag, CF) хранится в нулевом бите eflags / rflags и используется, например, в той же инструкции bt. Еще из часто используемых флагов можно назвать zero flag (ZF, 6-ой бит), sign flag (SF, 7-ой бит), direction flag (DF, 10-ый бит) и overflow flag (OF, 11-ый бит).
Еще из таких неявных регистров следует назвать eip / rip, хранящий адрес текущей инструкции. К нему также нельзя обращаться напрямую, но он виден в GDB вместе с eflags / rflags, если сказать info registers
, и косвенно изменяется всеми инструкциям. Большинство инструкций просто увеличивают eip / rip на длину этой инструкции, но есть и исключения из этого правила. Например, инструкция jmp просто осуществляет переход по заданному адресу:
# обнуляем rax
xor %rax, %rax
jmp next
# эта инструкция будет пропущена
inc %rax
next:
inc %rax
В результате значение rax будет равно единице, так как первая инструкция inс будет пропущена. Заметьте, что адрес перехода также может быть записан в регистре:
xor %rax, %rax
mov $next, %rcx
jmp *%rcx
inc %rax
next:
inc %rax
Впрочем, на практике такого кода лучше избегать, так как он ломает предсказание переходов и потому менее эффективен.
Примечание: GAS позволяет давать меткам цифирные имена типа 1:
, 2:
, и так далее, и переходить к ближайшей предыдущей или следующей метке с заданным номером инструкциями вроде jmp 1b
и jmp 1f
. Это довольно удобно, так как иногда бывает трудно придумать меткам осмысленные имена. Подробности можно найти здесь.
Условные переходы обычно осуществляются при помощи инструкции cmp, которая сравнивает два своих операнда и выставляет соответствующие флаги, за которой следует инструкция из семейства je, jg и подобных:
cmp %rax, %rcx
je 1f # перейти, если равны (equal)
jl 1f # перейти, если знаково меньше (less)
jb 1f # перейти, если беззнаково меньше (below)
jg 1f # перейти, если знаково больше (greater)
ja 1f # перейти, если беззнаково больше (above)
1:
Существует также инструкции jne (перейти, если не равны), jle (перейти, если знаково меньше или равны), jna (перейти, если беззнаково не больше) и подобные. Принцип их именования, надеюсь, очевиден. Вместо je / jne часто пишут jz / jnz, так как инструкции je / jne просто проверяют значение ZF. Также есть инструкции, проверяющие другие флаги — js, jo и jp, но на практике они используются редко. Все эти инструкции вместе взятые обычно называют jcc. То есть, вместо конкретных условий пишутся две буквы «c», от «condition». Здесь можно найти хорошую сводную таблицу по всем инструкциям jcc и тому, какие флаги они проверяют.
Помимо cmp также часто используют инструкцию test:
test %rax, %rax
jz 1f # перейти, если rax == 0
js 2f # перейти, если rax < 0
1:
# какой-то код
2:
# какой-то еще код
Fun fact! Интересно, что cmp и test в душе являются теми же sub и and, только не изменяют своих операндов. Это знание можно использовать для одновременного выполнения sub или and и условного перехода, без дополнительных инструкций cmp или test.
Еще из инструкций, связанных с условными переходами, можно отметить следующие.
jrcxz 1f
# какой-то код
1:
Инструкция jrcxz осуществляет переход только в том случае, если значение регистра rcx равно нулю.
cmovge %rcx, %rax
Инструкции семейства cmovcc (conditional move) работают как mov, но только при выполнении заданного условия, по аналогии с jcc.
setnz %al
Инструкции setcc присваивают однобайтовому регистру или байту в памяти значение 1, если заданное условие выполняется, и 0 иначе.
cmpxchg %rcx, (%rdx)
Сравнить rax с заданным куском памяти. Если равны, выставить ZF и сохранить по указанному адресу значение указанного регистра, в данном примере rcx. Иначе очистить ZF и загрузить значение из памяти в rax. Также оба операнда могут быть регистрами.
cmpxchg8b (%rsi)
cmpxchg16b (%rsi)
Инструкция cmpxchg8b главным образом нужна в x86. Она работает аналогично cmpxchg, только производит compare and swap сразу 8-и байт. Регистры edx:eax используются для сравнения, а регистры ecx:ebx хранят то, что мы хотим записать. Инструкция cmpxchg16b по тому же принципу производит compare and swap сразу 16-и байт на x64.
Важно! Примите во внимание, что без префикса lock все эти compare and swap инструкции не атомарны.
mov $10, %rcx
1:
# какой-то код
loop 1b
# loopz 1b
# loopnz 1b
Инструкция loop уменьшает значение регистра rcx на единицу, и если после этого rcx != 0
, осуществляет переход на заданную метку. Инструкции loopz и loopnz работают аналогично, только условия более сложные — (rcx != 0) && (ZF == 1)
и (rcx != 0) && (ZF == 0)
соответственно.
Не нужно быть семи пядей во лбу, чтобы изобразить при помощи этих инструкций конструкцию if-then-else или циклы for / while, поэтому двигаемся дальше.
«Строковые» операции
Рассмотрим следующий кусок кода:
mov $str1, %rsi
mov $str2, %edi
cld
cmpsb
В регистры rsi и rdi кладутся адреса двух строк. Командой cld очищается флаг направления (DF). Инструкция, выполняющая обратное действие, называется std. Затем в дело вступает инструкция cmpsb. Она сравнивает байты (%rsi)
и (%rdi)
и выставляет флаги в соответствии с результатом сравнения. Затем, если DF = 0, rsi и rdi увеличиваются на единицу (количество байт в том, что мы сравнивали), иначе — уменьшаются. Аналогичные инструкции cmpsw, cmpsl и cmpsq сравнивают слова, длинные слова и четверные слова соответственно.
Инструкции cmps интересны тем, что могут использоваться с префиксом rep, repe (repz) и repne (repnz). Например:
mov $str1, %rsi
mov $str2, %edi
mov $len, %rcx
cld
repe cmpsb
jne not_equal
Префикс rep повторяет инструкцию заданное в регистре rcx количество раз. Префиксы repz и repnz делают то же самое, но только после каждого выполнения инструкции дополнительно проверяется ZF. Цикл прерывается, если ZF = 0
в случае c repz и если ZF = 1
в случае с repnz. Таким образом, приведенный выше код проверяет равенство двух буферов одинакового размера.
Аналогичные инструкции movs перекладывает данные из буфера, адрес которого указан в rsi, в буфер, адрес которого указан в rdi (легко запомнить — rsi значит source, rdi значит destination). Инструкции stos заполняет буфер по адресу из регистра rdi байтами из регистра rax (или eax, или ax, или al, в зависимости от конкретной инструкции). Инструкции lods делают обратное действие — копируют байты по указанному в rsi адресу в регистр rax. Наконец, инструкции scas ищут байты из регистра rax (или соответствующих регистров меньшего размера) в буфере, адрес которого указан в rdi. Как и cmps, все эти инструкции работают с префиксами rep, repz и repnz.
На базе этих инструкций легко реализуются процедуры memcmp, memcpy, strcmp и подобные. Интересно, что, например, для обнуления памяти инженеры Intel рекомендуют использовать на современных процессорах rep stosb
, то есть, обнулять побайтово, а не, скажем, четверными словами.
Работа со стеком и процедуры
Со стеком все очень просто. Инструкция push кладет свой аргумент на стек, а инструкция pop извлекает значение со стека. Например, если временно забыть про инструкцию xchg, то поменять местами значение двух регистров можно так:
push %rax
mov %rcx, %rax
pop %rcx
Существуют инструкции, помещающие на стек и извлекающие с него регистр rflags / eflags:
pushf
# делаем что-то, что меняет флаги
popf
# флаги восстановлены, самое время сделать jcc
А так, к примеру, можно получить значение флага CF:
pushf
pop %rax
and $1, %rax
На x86 также существуют инструкции pusha и popa, сохраняющие на стеке и восстанавливающие с него значения всех регистров. В x64 этих инструкций больше нет. Видимо, потому что регистров стало больше и сами регистры теперь длиннее — сохранять и восстанавливать их все стало сильно дороже.
Процедуры, как правило, «создаются» при помощи инструкций call и ret. Инструкция call кладет на стек адрес следующей инструкции и передает управление по указанному в аргументе адресу. Инструкция ret читает со стека адрес возврата и передает по нему управление. Например:
someproc:
# типичный пролог процедуры
# для примера выделяем 0x10 байт на стеке под локальные переменные
# rbp — указатель на фрейм стека
push %rbp
mov %rsp, %rbp
sub $0x10, %rsp
# тут типа какие-то вычисления …
mov $1, %rax
# типичный эпилог процедуры
add $0x10, %rsp
pop %rbp
# выход из процедуры
ret
_start:
# как и в случае с jmp, адрес перехода может быть в регистре
call someproc
test %rax, %rax
jnz error
Примечание: Аналогичный пролог и эпилог можно написать при помощи инструкций enter $0x10, $0
и leave
. Но в наше время эти инструкции используются редко, так как они выполняются медленнее из-за дополнительной поддержки вложенных процедур.
Как правило, возвращаемое значение передается в регистре rax или, если его размера не достаточно, записывается в структуру, адрес которой передается в качестве аргумента. К вопросу о передаче аргументов. Соглашений о вызовах существует великое множество. В одних все аргументы всегда передаются через стек (отдельный вопрос — в каком порядке) и за очистку стека от аргументов отвечает сама процедура, в других часть аргументов передается через регистры, а часть через стек, и за очистку стека от аргументов отвечает вызывающая сторона, плюс множество вариантов посередине, с отдельными правилами касательно выравнивания аргументов на стеке, передачи this, если это ООП язык, и так далее. В общем случае для произвольно взятой архитектуры, компилятора и языка программирования соглашение о вызовах может быть вообще каким угодно.
Для примера рассмотрим ассемблерный код, сгенерированный CLang 3. 8 для простой программки на языке C под x64. Так выглядит одна из процедур:
unsigned int
hash(const unsigned char *data, const size_t data_len) {
unsigned int hash = 0x4841434B;
for(int i = 0; i < data_len; i++) {
hash = ((hash << 5) + hash) + data[i];
}
return hash;
}
Дизассемблерный листинг (при компиляции с -O0
, комментарии мои):
# типичный пролог процедуры
# регистр rsp не изменяется, так как процедура не вызывает никаких
# других процедур
400950: 55 push %rbp
400951: 48 89 e5 mov %rsp,%rbp
# инициализация локальных переменных:
# -0x08(%rbp) — const unsigned char *data (8 байт)
# -0x10(%rbp) — const size_t data_len (8 байт)
# -0x14(%rbp) — unsigned int hash (4 байта)
# -0x18(%rbp) — int i (4 байта)
400954: 48 89 7d f8 mov %rdi,-0x8(%rbp)
400958: 48 89 75 f0 mov %rsi,-0x10(%rbp)
40095c: c7 45 ec 4b 43 41 48 movl $0x4841434b,-0x14(%rbp)
400963: c7 45 e8 00 00 00 00 movl $0x0,-0x18(%rbp)
# rax := i. если достигли data_len, выходим из цикла
40096a: 48 63 45 e8 movslq -0x18(%rbp),%rax
40096e: 48 3b 45 f0 cmp -0x10(%rbp),%rax
400972: 0f 83 28 00 00 00 jae 4009a0 <hash+0x50>
# eax := (hash << 5) + hash
400978: 8b 45 ec mov -0x14(%rbp),%eax
40097b: c1 e0 05 shl $0x5,%eax
40097e: 03 45 ec add -0x14(%rbp),%eax
# eax += data[i]
400981: 48 63 4d e8 movslq -0x18(%rbp),%rcx
400985: 48 8b 55 f8 mov -0x8(%rbp),%rdx
400989: 0f b6 34 0a movzbl (%rdx,%rcx,1),%esi
40098d: 01 f0 add %esi,%eax
# hash := eax
40098f: 89 45 ec mov %eax,-0x14(%rbp)
# i++ и перейти к началу цикла
400992: 8b 45 e8 mov -0x18(%rbp),%eax
400995: 83 c0 01 add $0x1,%eax
400998: 89 45 e8 mov %eax,-0x18(%rbp)
40099b: e9 ca ff ff ff jmpq 40096a <hash+0x1a>
# возвращаемое значение (hash) кладется в регистр eax
4009a0: 8b 45 ec mov -0x14(%rbp),%eax
# типичный эпилог
4009a3: 5d pop %rbp
4009a4: c3 retq
Здесь мы встретили две новые инструкции — movs и movz. Они работают точно так же, как mov, только расширяют один операнд до размера второго, знаково и беззнаково соответственно. Например, инструкция movzbl (%rdx,%rcx,1),%esi
читайт байт (b) по адресу (%rdx,%rcx,1)
, расширяет его в длинное слово (l) путем добавления в начало нулей (z) и кладет результат в регистр esi.
Как видите, два аргумента были переданы процедуре через регистры rdi и rsi. По всей видимости, используется конвенция под названием System V AMD64 ABI. Утверждается, что это стандарт де-факто под x64 на *nix системах. Я не вижу смысла пересказывать описание этой конвенции здесь, заинтересованные читатели могут ознакомиться с полным описанием по приведенной ссылке.
Заключение
Само собой разумеется, в рамках одной статьи, описать весь ассемблер x86/x64 не представляется возможным (более того, я не уверен, что сам знаю его прямо таки весь). Как минимум, за кадром остались такие темы, как операции над числами с плавающей точкой, MMX-, SSE- и AVX-инструкции, а также всякие экзотические инструкции вроде lidt, lgdt, bswap, rdtsc, cpuid, movbe, xlatb, или prefetch. Я постараюсь осветить их в следующих статьях, но ничего не обещаю. Следует также отметить, что в выводе objdump -d
для большинства реальных программ вы очень редко увидите что-то помимо описанного выше.
Еще интересный топик, оставшийся за кадром — это атомарные операции, барьеры памяти, спинлоки и вот это все. Например, compare and swap часто реализуется просто как инструкция cmpxchg с префиксом lock. По аналогии реализуется атомарный инкремент, декремент, и прочее. Увы, все это тянет на тему для отдельной статьи.
В качестве источников дополнительной информации можно рекомендовать книгу Modern X86 Assembly Language Programming, и, конечно же, мануалы от Intel. Также довольно неплоха книга x86 Assembly на wikibooks.org.
Из онлайн-справочников по ассемблерным инструкциям стоит обратить внимание на следующие:
- http://ref.x86asm.net/;
- http://www.felixcloutier.com/x86/;
- http://x86.renejeschke.de/;
- https://en.wikipedia.org/wiki/X86_instruction_listings;
А знаете ли вы ассемблер, и если да, то находите ли это знание полезным?
Метки: Ассемблер.
Глава 6. Использование регистров общего назначения для адресации.
Smart ASM: Защищённый режим: Глава 6. Использование регистров общего назначения для адресации.Глава 6. Использование регистров общего назначения для адресации.
Для обращения к памяти через регистры обычно используются четыре 16-разрядных регистра общего назначения (РОН): BX, SI, DI и BP. В защищённом режиме для адресации можно использовать все 8 регистров общего назначения.
В 32-разрядных процессорах обычные регистры расширены до 32-х разрядов. Вот так регистр AX расширен до регистра EAX:
Рисунок 6-1. Формат регистра EAX.
Как видите, регистр AX является составной частью регистра EAX (так же, как регистры AL и AH являются составными частями регистра AX), т.е. если вы обращаетесь к регистру AX, то вы меняете содержимое регистра EAX.
Подобным образом расширены все 8 регистров общего назначения:
Рисунок 6-2. Регистры общего назначения.
Также, расширен 16-разрядный регистр FLAGS — теперь это 32-разрядный EFLAGS, младшая половина которого представляет собой FLAGS:
Рисунок 6-3. Формат регистра EFLAGS.
Регистр IP (Instruction Pointer) был расширен до 32-разрядного EIP:
Рисунок 6-4. Формат регистра EIP.
Регистр EIP непосредственно использовать нельзя, но теперь следует учитывать, что при 32-разрядной адресации памяти в качестве адреса перехода можно указывать 32-разрядную величину.
Хотя указатель стека (регистр ESP) также относится к регистрам общего назначения и может использоваться в командах, настоятельно рекомендуется никогда не привлекать его к использованию вне стека. Особенно это важно при работе в защищённом режиме, когда процессор автоматически использует текущее значение стека, чтобы поместить в него значения, например, при обработке исключений.
В 32-разрядном процессоре вы по-прежнему можете адресовать память через четыре 16-разрядных регистра BX, SI, DI и BP, но дополнительно к этому можно использовать каждый из 32-разрядных регистров общего назначения, причём в любом режиме (не только защищённом). Например:
mov ax,[ ebx ] ; Поместить в AX значение из памяти ; по адресу DS:EBX mov dx,[ ecx ] ; Поместить в DX значение из памяти ; по адресу DS:ECX mov cx,es:[ eax ] ; Поместить в CX значение из памяти ; по адресу ES:EAX
Дополнительно к этой возможности введены следующие:
- Использование константы и регистра:
mov eax,[ ecx + 1 ] mov bl,[ edx + 12345678h ]
- Сумма двух регистров:
mov ebp,[ ebx + edi ] mov eax,[ ecx + edx ]
- Сумма двух регистров и константы:
mov bl,[ edx + eax + 12345678h ]
- Масштаб — автоматическое умножение на 2, 4 или 8 одного из регистров, участвующих в образовании адреса:
mov ax,[ ebx * 2 ] mov cl,[ edx + ebp * 4 ] mov esi,[ edi + eax * 8 + 12345678h ]
Очевидно, что возможности, которые нам предоставляла XT, 32-разрядные процессоры значительно расширили. Следует, однако, учитывать, что в ранних процессорах (i386 и i486) на вычисление эффективного адреса процессор расходует дополнительное время, но благодаря универсальности этой системы адресации всё равно имеет место итоговый выигрыш в производительности.
При использовании 32-разрядных регистров для адресации в режиме реальных адресов, следует учитывать, что размер сегмента фиксирован и равен 64 Кб. Если процессор сформирует адрес, больший 64 Кб, то процессор зависнет, т.е. он не будет производить заворачивание адресов. Например:
mov eax,1234h mov bl,[ eax ] ; В регистр BL будет произведена загрузка ; значения с адреса DS:EAX, равного DS:1234h. mov edx,ffffh mov bl,[ eax + edx ] ; Эффективный адрес будет равен ; 1234h + ffffh = 11233h (это больше, ; чем 64 Кб. Процессор зависнет.
Использование 32-разрядных регистров для адресации памяти в защищённом режиме очень распространено, в основном, из-за того, что размер сегментов может достигать 4 Гб, да и просто потому, что это удобно.
Следующая глава | Оглавление | Вопросы? Замечания? Пишите: [email protected] |
Copyright © Александр Семенко. |
Регистры процессора изучаем с помощью Turbo Debugger.
Регистры процессора.
Процессор работает с данными. Обрабатываемые данные содержаться в оперативной памяти, куда считываются с жёсткого диска. Для того, чтобы ими манипулировать, а также верно организовывать свою работу по их обработке — выполнению программного кода, у процессора имеется система, построенная на небольших, но очень быстрых блоках памяти — регистров. Регистры процессора — это входящие непосредственно в процессор блоки памяти.
Изучать регистры процессора мы будем с использованием отладчика TD (Turbo Debugger) и нашей первой программы «Hello, world!» (prg. com). Так что запускаем предустановленный DOSBox и поехали (всё необходимое имеется в архиве DOS-1.rar, который можно скачать с нашего сайта).
Изучаем регистры процессора с помощью TD и нашей первой программы.Для удобства работы с Turbo Debugger целесообразно увеличить рабочую площадь программы на всё окно.
Используем F5 для увеличения рабочей площади TD.Регистры процессора Intel и их аналоги.
Количество регистров и их размер зависит от разрядности и сложности процессора.
Практически при написании кода регистры нужно понимать как постоянно имеющиеся под рукой программиста переменные.
Начиная с 80386, процессоры Intel и их аналоги имеют 16 основных регистров и 11 регистров для работы с числами с плавающей запятой (FPU/NPX) и мультимедийными приложениями (MMX). Для машины всегда быстрее и удобнее обращаться к регистру процессора, чем к памяти.
Помимо основных регистров существуют регистры управления памятью (GDTR, IDTR, TR, LDTR), регистры управления (CR0, CR1 – CR4), отладочные регистры (DR0 – DR7) и машинно-зависимые регистры, которые практически не используются в прикладном программировании.
Регистры процессора 8086.
Так как мы имеем дело с 16 битной операционной системой MS-DOS, то вернёмся во времена процессоров 8086 и 80286. В процессорах следующих поколений указанные регистры являются частями соответствующих 32 и 64 битных регистров с сохранением названия и функционала.
Например, регистр ax (16 бит) является составляющей частью регистра eax (32 бита). Благодаря указанному подходу достигается совместимость работы старых программ на новых компьютерах. Также сохраняются команды и директивы, переходя с 16 битного в 32 битный код.
Итак, процессор (8086 и 80286) содержит 12 16-ти разрядных программно-адресуемых регистров. Регистры процессора принято объединять в три группы:
- Регистры данных.
- Регистры указатели.
- Сегментные регистры.
Кроме этого, в состав процессора входят:
- Счётчик команд.
- Регистр флагов.
16-ти разрядность обозначает, что в каждом регистре может содержаться 0FFFFh бит информации, то есть два байта: 0FFh-0FFh информации.
Регистры данных (регистры общего назначения).
Регистры данных допускают независимое обращение к старшим (High — обозначается литерой H) и младшим (Lough — обозначается литерой L) половинам.
AX (AH+AL) — Аккумулятор (Accumulator). BX (BH+BL) — Базовый регистр (Base). CX (CH+CL) — Счетчик (Counter). DX (DH+DL) — Регистр данных (Data).
AX (AH+AL) — Аккумулятор (Accumulator). BX (BH+BL) — Базовый регистр (Base). CX (CH+CL) — Счетчик (Counter). DX (DH+DL) — Регистр данных (Data). |
Регистры данных эквивалентны в использовании. Во времена старых процессоров, рекомендовалась использовать в первую очередь AX, так как многие команды, если их операндом (составной частью команды) является AX (AH, AL) занимают в памяти меньше места и выполняются быстрее.
Регистры указатели.
Регистры указатели в отличие от регистров общего назначения не допускают побайтовую адресацию. SI и DI называют индексными регистрами.
SI — Индекс имточника (Source Index). DI — Индекс приёсемка (Destination Index). BP — Указатель базы (Base Pointer). SP — Указатель стека (Stack Pointer).
SI — Индекс имточника (Source Index). DI — Индекс приёсемка (Destination Index). BP — Указатель базы (Base Pointer). SP — Указатель стека (Stack Pointer). |
Регистры указатели в принципе также могут использоваться в любых целях, как и регистры общего назначения. В некоторых командах (работы со строками) используется конкретный установленный перечень регистров. Например, команда movs — «пересылка строк» предполагает использование исключительно регистров SI и DI для хранения адресов источника и приемника строки байт, слов и двойных слов. В случае использования префикса rep, дополнительно задействуется сегмент CX.
Сегментные регистры.
Сегментные регистры хранят начальные адреса сегментов программы, что даёт возможность обращаться к этим сегментам.
CS — Регистр сегмента команд (Commands Segment). DS — Регистр сегмента данных(Data Segment). ES — Регистр дополнительного сегмента данных (Extantion Segment). SS — Регистр сенмента стека (Stack Segment).
CS — Регистр сегмента команд (Commands Segment). DS — Регистр сегмента данных(Data Segment). ES — Регистр дополнительного сегмента данных (Extantion Segment). SS — Регистр сенмента стека (Stack Segment). |
Счётчик команд.
IP — Указатель команд (Index Pointer).
IP — Указатель команд (Index Pointer). |
На каждом шаге выполнения программы указывает на адрес команды, следующей за исполняемой. Регистр IP программно не доступен. Можно только получить его значение (и то, не на прямую), программно влиять на IP — не возможно.
Регистр флагов.
CF — Регистр флагов (Flags).
CF — Регистр флагов (Flags). |
Побитно содержит флаги : OF, DF, IF, TF, SF, ZF, AF,PF, CF. Флаги устанавливаются процессором и на них можно влиять программно. Определенные команды влияют на изменение значения флага. Например, флаг нуля (ZF) устанавливается в 1, если результат операции равен нулю. Так, если в AX содержится число 1, то после команды
dec ax
dec ax |
содержимое AX станет равным 0, а в регистре флагов установится бит ZF.
Используем отладчик для усвоения материала.
С помощью Turbo Debugger самостоятельно изучите рассмотренные регистры процессора, отлаживая нашу первую программу prg.com.
Усвоим материал, изучив регистры процессора с помощью TD.Flat Assembler — 2.1.1 Инструкции перемещения данных
Инструкция mov передает байт, слово или двойное слово из операнда- источника в операнд-назначение. Она может передавать данные между регистрами общего назначения, из регистра общего назначения в память или из памяти в этот регистр, но она не может перемещать данные из памяти в память. Также она может передать непосредственное значение в регистр общего назначения или память, сегментный регистр в регистр общего назначения или память, регистр общего назначения или память в сегментный регистр, управляющий или отладочный регистр в регистр общего назначения и регистр общего назначения в управляющий или отладочный регистр. Инструкция mov может быть скомпилирована только в случае, если размер операнда-источника совпадает с размером операнда-назначения. Ниже приведены примеры для каждой допустимой комбинации:
mov bx,ax ; из регистра общего назначения в регистр общего назначения
mov [char],al ; из регистра общего назначения в память
mov bl,[char] ; из памяти в регистр общего назначения
mov dl,32 ; непосредственное значение в регистр общего назначения
mov [char],32 ; непосредственное значение в память
mov ax,ds ; из сегментного регистра в регистр общего назначения
mov [bx],ds ; из сегментного регистра в память
mov ds,ax ; из регистра общего назначения в сегментный регистр
mov ds,[bx] ; из памяти в сегментный регистр
mov eax,cr0 ; из управляющего регистра в регистр общего назначения
mov cr3,ebx ; из регистра общего назначения в управляющий регистр
Инструкция xchg обменивает содержимое двух операндов. Она может поменять значения операндов с одинаковым размером: два байта, два слова или два двойных слова. Порядок операндов не важен. Операндами могут выступать два регистра общего назначения или регистр общего назначения и память. Например:
xchg ax,bx ; обменивает значения двух регистров общего назначения
xchg al,[char] ; обменивает значения регистра общего назначения и памяти
Инструкция push уменьшает значение регистра-указателя стека (регистр sp/esp в зависимости от разрядности режима 16 или 32-битный) на 2 или 4 (размер адреса), а затем передает операнд на вершину стека, на который указывает регистр sp/esp. Операндом может быть память, регистр общего назначения, сегментный регистр или непосредственное значение размером в слово или двойное слово. Если операндом является непосредственное значение и его размер не определен, то по умолчанию в 16-битном режиме он обрабатывается как слово, а в 32-битном режиме как двойное слово. Инструкции pushw и pushd ‑ мнемоники вариантов этой инструкции, в которых сохраняют соответственно значения в размере слова или двойного слова. Если в одной строке содержится несколько операндов (разделенных только пробелами, а не запятыми), то компилятор будет компилировать последовательность инструкций push с этими операндами. Примеры этой инструкции с одним операндом:
push ax ; сохраняет регистр общего назначения
push es ; сохраняет сегментный регистр
pushw [bx] ; сохраняет память
push 1000h ; сохраняет непосредственное значение
Инструкция pusha сохраняет содержимое восьми регистров общего назначения в стек. Соответственно эта инструкция уменьшает значение указателя стека sp/esp на 16/32. Эта команда не имеет операндов. Существуют две версии этой инструкции, одна 16-битная, а вторая 32-битная, ассемблер автоматически генерирует нужную версию для текущего режима, но это можно изменить с помощью инструкций pushaw или pushad, позволяющих всегда получать 16-битные или 32-битных версии. 16-битная версия этой инструкции помещает регистры общего назначения в стек в следующем порядке: ax, cx, dx, bx, sp, bp, si и di. В стек помещается содержимое регистра sp на момент до выполнения команды. 32-битная версия помещает в стек эквивалентные 32-битные регистры общего назначения в том же порядке.
Инструкция pop перемещает слово или двойное слово из текущей вершины стека в операнд-назначение, а после увеличивает регистр esp, так чтобы он указывал на новую вершину стека. Операндом может быть память, регистр общего назначения или сегментный регистр. Мнемоники popw и popd ‑ это варианты этой инструкции, восстанавливающие соответственно слова и двойные слова. Если на одной строке находится несколько операндов, разделенных пробелами, ассемблер компилирует последовательность инструкций с этими операндами.
pop bx ; восстанавливает регистр общего назначения
pop ds ; восстанавливает сегментный регистр
popw [si] ; восстанавливает память
Инструкция popa восстанавливает регистры, сохраненные в стек инструкцией pusha, кроме сохраненного значения sp/esp, который игнорируется. У этой инструкции нет операндов. Чтобы компилировать 16-битную или 32-битную версию этой инструкции, используйте мнемоники popaw или popad.
регистров x86
регистров x86Основными инструментами для написания программ на ассемблере x86 являются регистры процессора. Регистры похожи на переменные, встроенные в процессор. Вместо этого используйте регистры памяти для хранения значений делает процесс быстрее и чище. Проблема с серия процессоров x86 заключается в том, что для использования используется мало регистров. Эта секция описывает основное использование каждого регистра и способы их использования. Это в заметку, что описанные здесь правила являются скорее рекомендациями, чем строгими правилами. Некоторые операции Нужны абсолютно какие-то регистры, но большинство из них можно использовать любые из свободных.
Вот список доступных регистров на процессорах 386 и выше. В этом списке показаны 32-битные регистры. Большинство из них можно разбить на 16 или даже 8 бит. регистр.
Общие регистры EAX EBX ECX EDX Сегментные регистры CS DS ES FS GS SS Индекс и указатели ESI EDI EBP EIP ESP Индикатор ЭФЛАГСОбщие регистры
Как следует из названия, общий регистр — это тот, который мы используем большую часть времени. Большинство инструкций выполняются в этих регистрах. Все они могут быть разбиты в 16- и 8-битные регистры.
32 бита: EAX EBX ECX EDX 16 бит: AX BX CX DX 8 бит: AH AL BH BL CH CL DH DLСуффиксы «H» и «L» в 8-битных регистрах обозначают старший и младший байты. С этим покончено, давайте посмотрим на их индивидуальное основное использование.
EAX,AX,AH,AL : Вызывается регистром аккумулятора. Он используется для доступа к порту ввода/вывода, арифметических операций, вызовов прерываний, так далее... EBX,BX,BH,BL : Вызывается базовым регистром Он используется в качестве базового указателя для доступа к памяти. Получает некоторые возвращаемые значения прерывания ECX,CX,CH,CL : вызывает регистр счетчика. Используется как счетчик циклов и для смен Получает некоторые значения прерывания EDX,DX,DH,DL: Вызывается регистром данных. Он используется для доступа к порту ввода-вывода, арифметических операций, некоторых прерываний. звонки.Сегментные регистры
Регистры сегментов содержат адреса сегментов различных элементов. Они доступны только в 16 значениях. Они могут быть установлены только общим реестром или специальными инструкциями. Некоторые из них имеют решающее значение для хорошего выполнения программы, и вы можете хотите поиграть с ними, когда будете готовы к многосегментному программированию
CS: Содержит сегмент кода, в котором работает ваша программа. Изменение его значения может привести к зависанию компьютера. DS: содержит сегмент данных, к которому обращается ваша программа. Изменение его значения может привести к ошибочным данным. ES,FS,GS: это дополнительные сегментные регистры, доступные для дальняя адресация указателя, такая как видеопамять и тому подобное. SS: Содержит сегмент стека, который использует ваша программа. Иногда имеет то же значение, что и DS. Изменение его значения может дать непредсказуемые результаты, в основном связанные с данными.Индексы и указатели
Индексы и указатель и смещенная часть и адрес. Они имеют различное использование но каждый регистр имеет определенную функцию. Они некоторое время использовались с сегментным регистром чтобы указать на дальний адрес (в диапазоне 1 Мб). Регистр с префиксом «Е» может только использоваться в защищенном режиме.
ES:EDI EDI DI : Регистр индекса назначения Используется для копирования и установки строки, массива памяти и для дальней адресации указателя с ES DS:ESI EDI SI : Регистр исходного индекса Используется для копирования строк и массивов памяти SS:EBP EBP BP : Регистр указателя базы стека Содержит базовый адрес стека SS:ESP ESP SP : Регистр указателя стека Содержит верхний адрес стека CS:EIP EIP IP: индексный указатель Содержит смещение следующей инструкции Его можно только прочитатьРегистр EFLAGS
Регистр EFLAGS хранит состояние процессора. Модифицируется многими инструкциями и используется для сравнения некоторых параметров, условных циклов и условных переходов. Каждый бит содержит состояние определенного параметра последней инструкции. Вот список:
Метка бита Описание --------------------------- 0 CF Перенести флаг 2 PF Флаг четности 4 Флажок вспомогательного переноса AF 6 Флаг нуля ZF 7 Флажок знака SF 8 Флаг ловушки TF 9IF флаг разрешения прерывания 10 DF Флаг направления 11 OF Флаг переполнения 12-13 Уровень привилегий ввода/вывода IOPL 14 NT Флаг вложенной задачи 16 Флажок возобновления РФ 17 Флаг режима виртуальной машины 8086 18 Флаг проверки выравнивания AC (486+) 19 Флаг виртуального прерывания VIF 20 Флаг ожидания виртуального прерывания VIP 21 ID Идентификационный флаг Те, которые не перечислены, зарезервированы Intel.Недокументированные регистры
На процессорах 80386 и выше имеются регистры, которые недостаточно документированы. от Intel. Они делятся на регистры управления, регистры отладки, регистры тестирования и регистры сегментации защищенного режима. Насколько я знаю, контрольные регистры, наряду с регистрами сегментации, используются в программировании защищенного режима, все эти регистры доступны на процессорах 80386 и выше, за исключением тестовых регистров, которые были удалены на пентиуме. Регистры управления — от CR0 до CR4, регистры отладки — от DR0 до DR7, тестовые регистры — от TR3 до TR7, а регистры сегментации защищенного режима — GDTR (глобальный дескриптор). регистр таблицы), IDTR (регистр таблицы дескрипторов прерываний), LDTR (локальный DTR) и TR.
Регистры в сборке x86 — ресурсы Infosec
Безопасное кодирование
19 сентября 2019 г. по Ричард Азу
Поделиться:
Введение
В этой статье будут определены три основных типа регистров в архитектуре x86, а затем после выполнения определенных инструкций будет показано моделирование содержимого регистров.
Эта статья предназначена для начинающих, студентов и профессионалов, которые хотят получить подробное представление об аппаратной архитектуре x86, включая ее внутреннюю архитектуру с упором на регистры. Эта статья поможет вам получить более подробные сведения и лучше понять проблемы архитектуры, особенно в регистрах процессора.
Рисунок 1. Внутренняя архитектура компьютера с аппаратным обеспечением x86
Основные компоненты компьютерной системы включает в себя блок управления, арифметико-логическое устройство (АЛУ) и регистры
Центральный процессор (ЦП)
В статье для Digital Trends Джон Мартиндейл определил ЦП следующим образом: «Центральный процессор или ЦП, возможно, является наиболее важным компонентом любого вычислительного устройства. Он обрабатывает основные инструкции и распределяет более сложные задачи по другим конкретным чипам, чтобы заставить их делать то, что у них получается лучше всего».
CPU Registers
TechDifferences определили регистры следующим образом: «Регистры — это наименьшие элементы хранения данных, которые встроены в само аппаратное обеспечение процессора. Регистры — это места временного хранения, к которым процессор имеет прямой доступ. Регистры содержат инструкции или операнды, к которым в данный момент обращается ЦП».
Регистры общего назначения (GPR)
Архитектура x86 содержит восемь 32-разрядных регистров общего назначения (GPR). Эти регистры в основном используются для выполнения адресных вычислений, арифметических и логических вычислений. Четыре GPR можно рассматривать как 32-битную величину, 16-битную величину или как две 8-битные величины. Это EAX, EBX, ECX и EDX, как показано на рисунке 2.
Таблица 1. Обычное использование регистров общего назначения
Рисунок 2. Регистры общего назначения
Ниже приведен список инструкций, описывающих использование GPR:
При работе с регистрами в x86 их имена не чувствительны к регистру. Например, имена EBX и ebx относятся к одному и тому же регистру. Инструкции 1 и 2 скопируют шестнадцатеричное значение f7 в регистр EBX, который совпадает с ebx. Начальный 0x указывает, что следующее за ним шестнадцатеричное значение.
- 3. MOV EBX, 0x0000
- 4. MOV ebx, 0x7fff ffff
Инструкция 3 копирует шестнадцатеричное значение 0 в регистр EBX, а инструкция 4 копирует шестнадцатеричное значение 7fff ffff в тот же регистр.
Как показано на рис. 2, все GPR EAX, EBX, ECX, EDX, ESI, EDI, ESP и EBP являются 32-битными. Это означает, что они могут содержать двоичные значения от 00000000 00000000 00000000 00000000 до 01111111 11111111 11111111 11111111, десятичные значения от 0 до 2147483647 и шестнадцатеричные значения от 0 до 7fff ffff.
Предположим, что функция требуется для выполнения инструкции путем копирования двоичного значения 00000000 00000000 00000000 00000111 b в регистр общего назначения.
Вместо использования, например, регистра ECX или любого из 32-битных GPR, лучше всего использовать 8-битный GPR младшего разряда. Два младших байта регистра ECX можно рассматривать как 16-битный регистр CX.
Младший значащий байт регистра CX также можно использовать как один 8-битный регистр CL, тогда как старший значащий байт регистра CX можно использовать как один 8-битный регистр CH. Важно отметить, что эти имена относятся к одному и тому же физическому регистру ECX.
- 5. МОВ КЛ, 111 б
- 6. MOV CH, 1 000 0111 б
Рисунок 3: Эмулятор x86, показывающий содержимое 16-битного регистра CX после выполнения инструкций 5 и 6
реестр ЕКХ.
Инструкция 6 копирует двоичное значение 1 000 0111 b в CH, старший 8-битный регистр ECX.
Точно так же регистры DX, CX, BX и AX могут использоваться для выполнения 16-битных вычислений. Важно отметить, что таким образом можно использовать только георадары EAX, EBX, ECX и EDX.
- 7. MOV BX, 0xff00
- 8. МОВ ЧД, 0xff
- 9. МОВ БЛ, 0x00
Рисунок 4: Результат копирования шестнадцатеричного значения ff00 в регистр BX — Инструкция 7
0008
Рисунок 6: Результат копирования шестнадцатеричного значения 00 в регистр BL — Инструкция 9
Из инструкций 7, 8 и 9, а также рис. что поместить 0xff00 в BX — это то же самое, что поместить 0xff в BH и поместить 0x00 в BL. Это подтверждает, что мы можем сослаться:
- BX с точки зрения BH, BL
- AX в условиях AH, AL
- CX в пересчете на CH, CL
- DX в терминах DH, DL
Рисунок 7: отладчик, показывающий регистры общего назначения со значениями
EFLAGS или Регистры управления
Рисунок 8: EFFAGS
EFFLAGS . от выполнения арифметических инструкций, а затем выполнять конкретные задачи на основе отчета о состоянии. Рисунок 8 выше показывает сводку функций для каждого из этих регистров.Сегментные регистры
Рис. 9. Сегментные регистры
действия начинаются:
- Хранение данных
- Выполнение кода
Заключение
В этой статье кратко объясняется внутренняя архитектура аппаратной системы x86. Он представил три ключевых типа регистров, в частности регистры общего процессора (GPR) уникальным способом, демонстрируя симуляции содержимого регистров после выполнения определенных инструкций. Эти симуляции поясняют, когда использовать 16-битные и 8-битные подсекции 32-битных регистров процессора.
Источники
- Что такое ЦП?, Digital Trends
- Разница между регистром и памятью, TechDifferences
Registers — SkullSecurity
Этот раздел является первым разделом, относящимся к сборке. Так что, если вы читаете полное руководство, приготовьтесь к реальному обучению!
Регистр похож на переменную, за исключением того, что существует фиксированное количество регистров. Каждый регистр — это особое место в ЦП, где хранится одно значение. Регистр — единственное место, где можно выполнять математические операции (сложение, вычитание и т. д.). Регистры часто содержат указатели, которые ссылаются на память. Перемещение значений между регистрами и памятью очень распространено.
Сборка Intel имеет 8 32-битных регистров общего назначения: eax, ebx, ecx, edx, esi, edi, ebp, esp. Хотя между любыми из этих регистров можно перемещать любые данные, компиляторы обычно используют одни и те же регистры для одних и тех же целей, а некоторые инструкции (например, умножение и деление) могут использовать только те регистры, для которых они предназначены.
Различные компиляторы могут иметь совершенно разные соглашения об использовании различных регистров. Для целей этого документа я расскажу о наиболее распространенном компиляторе Microsoft.
Содержание
- 1 Летучесть
- 2 регистра общего назначения
- 2,1 шт.
- 2.2 ebx
- 2.3 ЕСХ
- 2.4 edx
- 2,5 еси
- 2.6 Эди
- 2,7 э.п.н.
- 2.8 особенно
- 3 регистра специального назначения
- 3.1 ЭИП
- 3.2 флаги
- 4 16-битных и 8-битных регистров
- 5 64-битных регистров
- 6 Вопросы
Изменчивость
Некоторые регистры обычно изменчивы для разных функций, а другие остаются неизменными. Это особенность стандартов компилятора, за которой нужно следить в коде, регистры не сохраняются автоматически (хотя в некоторых языках ассемблера они сохраняются, но не в x86). Это означает, что при вызове функции нет гарантии, что энергозависимые регистры сохранят свое значение, когда функция вернется, и функция несет ответственность за сохранение энергонезависимых регистров.
Компилятор Microsoft использует следующие соглашения:
- Изменчивый : ecx, edx
- Энергонезависимый : ebx, esi, edi, ebp
- Special : eax, esp (обсуждается позже)
Регистры общего назначения
В этом разделе рассматриваются 8 регистров общего назначения в архитектуре x86.
eax
eax — это 32-битный регистр общего назначения с двумя распространенными применениями: для хранения возвращаемого значения функции и в качестве специального регистра для определенных вычислений. Технически это изменчивый регистр, так как значение не сохраняется. Вместо этого его значение устанавливается равным возвращаемому значению функции перед возвратом функции. Помимо esp, это, вероятно, самый важный регистр, который следует помнить по этой причине. eax также используется специально в некоторых вычислениях, таких как умножение и деление, в качестве специального регистра. Это использование будет рассмотрено в разделе инструкций.
Вот пример возврата функции на C:
возврат 3; // Возвращаем значение 3
Вот тот же код на ассемблере:
mov eax, 3 ; Установите eax (возвращаемое значение) на 3 возврат; Возвращаться
ebx
ebx — энергонезависимый регистр общего назначения. Он не имеет конкретного применения, но часто устанавливается в обычно используемое значение (например, 0) во всей функции для ускорения вычислений.
ecx
ecx — это энергозависимый регистр общего назначения, который иногда используется как параметр функции или как счетчик циклов.
Функции соглашения «__fastcall» передают первые два параметра в функцию, используя ecx и edx. Кроме того, при вызове функции-члена класса указатель на этот класс часто передается в ecx независимо от соглашения о вызовах.
Кроме того, ecx часто используется как счетчик циклов. для циклов обычно, хотя и не всегда, задайте для переменной аккумулятора значение ecx. Инструкции rep- также используют ecx в качестве счетчика, автоматически уменьшая его до тех пор, пока он не достигнет 0. Этот класс функций будет обсуждаться в следующем разделе.
edx
edx — это энергозависимый регистр общего назначения, который иногда используется в качестве параметра функции. Как и ecx, edx используется для функций «__fastcall».
Помимо fastcall, edx обычно используется для хранения краткосрочных переменных внутри функции.
esi
esi — это энергонезависимый регистр общего назначения, который часто используется в качестве указателя. В частности, для инструкций класса «rep-», которые требуют источника и назначения для данных, esi указывает на «источник». esi часто хранит данные, которые используются во всей функции, потому что они не изменяются.
edi
edi — это энергонезависимый регистр общего назначения, который часто используется в качестве указателя. Он похож на esi, за исключением того, что обычно используется как место назначения для данных.
ebp
ebp — это энергонезависимый регистр общего назначения, который имеет два разных применения в зависимости от настроек компиляции: это либо указатель кадра, либо регистр общего назначения.
Если компиляция не оптимизирована или код написан вручную, ebp отслеживает, где находится стек в начале функции (стек будет подробно описан в следующем разделе). Поскольку стек изменяется на протяжении всей функции, установка исходного значения ebp позволяет легко обращаться к переменным, хранящимся в стеке. Это будет подробно рассмотрено при объяснении стека.
Если компиляция оптимизирована, ebp используется как общий регистр для хранения любых данных, в то время как вычисления для указателя стека выполняются на основе перемещения указателя стека (что сбивает с толку — к счастью, IDA автоматически обнаруживает и исправляет перемещение указатель стека!)
esp
esp — это специальный регистр, в котором хранится указатель на вершину стека (на самом деле вершина имеет меньший виртуальный адрес, чем низ, поскольку стек растет в памяти вниз по направлению к куче). Математические операции редко выполняются непосредственно над esp, и значение esp должно быть одинаковым в начале и в конце каждой функции. esp будет рассмотрен более подробно в следующем разделе.
Регистры специального назначения
Регистры специального назначения и регистры с плавающей запятой, не перечисленные здесь, можно найти в статье Википедии или на других справочных сайтах.
eip
eip или указатель инструкции — это специальный регистр, в котором хранится указатель на адрес выполняемой в данный момент инструкции. Совершение перехода похоже на добавление или вычитание из указателя инструкции.
После каждой инструкции к eip добавляется значение, равное размеру инструкции, что означает, что eip указывает на машинный код следующей инструкции. Этот простой пример показывает автоматическое добавление в eip на каждом шаге:
eip+1 53 нажать ebx eip+4 8B 54 24 08 mov edx, [esp+arg_0] eip+2 31 БД xor ebx, ebx eip+2 89 D3 mov ebx, edx eip+3 8D 42 07 lea eax, [edx+7] . ....
флаги
В регистре флагов каждый бит имеет определенное значение и используется для хранения метаинформации о результатах предыдущих операций. Например, было ли последнее вычисление переполнено регистром или были ли операнды равными. Наш интерес к реестру флагов обычно составляет около cmp и проверяют операции , которые обычно устанавливают или сбрасывают флаги нуля, переноса и переполнения. Затем эти флаги будут проверены условным переходом, который может управлять ходом программы или циклом.
16-битные и 8-битные регистры
В дополнение к 8 32-битным регистрам имеется также ряд 16-битных и 8-битных регистров. В этих регистрах сбивает с толку то, что они используют то же пространство для хранения, что и 32-битные регистры. Другими словами, каждый 16-разрядный регистр является половиной одного из 32-разрядных регистров, поэтому изменение 16-разрядного также приводит к изменению 32-разрядного. Кроме того, 8-битные регистры являются частью 16-битных регистров.
Например, eax является 32-битным регистром. Младшая половина eax — это ax, 16-битный регистр. ax разделен на два 8-битных регистра, ah и al (a-high и a-low).
- Имеется 8 32-битных регистров: eax, ebx, ecx, edx, esi, edi, ebp, esp.
- Имеется 8 16-битных регистров: ax, bx, cx, dx, si, di, bp, sp.
- Имеется 8 8-битных регистров: ah, al, bh, bl, ch, cl, dh, dl.
Взаимосвязь этих регистров показана в таблице ниже:
32-битный | эакс | ebx | ЕСХ | эдкс | |||||||||||||||
16-битный | топор | бх | сх | дх | |||||||||||||||
8-битный | ах | и | бх | бл | ч | кл | дх | дл | |||||||||||
32-разрядный | ЕСИ | эди | н. э.п. | особенно | |||||||||||||||
16-битный | и | и | п.н. | сп |
Вот два примера:
eax | 0x12345678 |
топор | 0x5678 |
ах | 0x56 |
и | 0x78 |
ebx | 0x00000025 |
бх | 0x0025 |
бх | 0x00 |
бл | 0x25 |
64-битные регистры
64-битный регистр создается путем объединения пары 32-битных регистров. Это показано путем помещения двоеточия между ними.
Самый распространенный 64-битный регистр (используемый для таких операций, как деление и умножение) — edx:eax. Это означает, что 32-битный регистр edx помещается перед 32-битным регистром eax, создавая, так сказать, регистр двойной длины.
Вот простой пример:
едкс | 0x11223344 |
Еакс. | 0xaabbccdd |
edx:eax | 0x11223344aabbccdd |
Вопросы
Не стесняйтесь редактировать этот раздел и задавать вопросы, я сделаю все возможное, чтобы ответить на них. Но вам может потребоваться связаться со мной, чтобы сообщить мне, что вопрос существует. Сборка
— Как узнать, является ли регистр «регистром общего назначения»?
Термин Регистр общего назначения (GPR) отличается от Регистр специального назначения . Последнее не может быть использовано во всех контекстах.
Исторически сложилось так, что старая архитектура 8086 ввела эту разницу для целочисленных регистров, присутствующих в их именах до сегодняшнего дня:
- AX = Регистр-накопитель : накапливает результат (**)
- BX = B регистр ase : базовое смещение для определенной инструкции, например. XLAT
- CX = C встречный регистр : количество циклов, например. JCXZ.
- DX = D регистр ata : расширяет диапазон данных, т.е. результат MUL в DX:AX
- SI = S индекс источника : источник строковых инструкций, например. ЛОДСБ
- DI = D индекс назначения : место назначения для строковых инструкций, например. СТОСБ
- SP = S указатель прихвата : указывает на текущий элемент стека
- BP = B указатель ase : указывает на базу текущей подпрограммы (кадр стека)
(**) AX/AL тоже своего рода регистр специального назначения. Многие инструкции имеют специальные кодировки для AX/AL в качестве операндов, например. загрузка сегмента регистрируется с помощью MOV.
Другие Регистры специального назначения были
- Сегментные регистры (CS,DS,ES,SS) Регистр флагов
- (FLAGS) и
- Указатель инструкций (IP)
Некоторые из этих ограничений до сих пор используются в режиме адресации для 16-битных инструкций в реальном режиме (см. Архитектуры Intel® 64 и IA-32). Руководство разработчика программного обеспечения , том 2, раздел 2.1.5, таблица 2-1. «16-битные формы адресации с байтом ModR/M»). (отсюда общее назначение). Это также отражается в кодировании режима адресации инструкций, см. Руководство Intel, том 2, раздел 2.1.5, таблица 2.2 . (Сравните Таблицу 2.1 с Таблицей 2.2, чтобы получить представление о разнице)
Имена имеют префикс E и R до EAX и RAX соответственно, а их исторические имена, указывающие на использование теперь просто обычный.
Со многими новыми архитектурами были добавлены новые регистры специального назначения. Полный обзор приведен в руководстве Intel , том 1, раздел 3.7.2. :
- 32-разрядные регистры общего назначения (EAX, EBX, ECX, EDX, ESI, EDI, ESP или EBP)
- 16-битные регистры общего назначения (AX, BX, CX, DX, SI, DI, SP или BP)
- 8-битные регистры общего назначения (AH, BH, CH, DH, AL, BL, CL или DL)
- сегментных регистра (CS, DS, SS, ES, FS и GS)
- Регистр EFLAGS
- регистра x87 FPU (от ST0 до ST7, слово состояния, слово управления, слово тега, указатель операнда данных и инструкция указатель)
- Регистры MMX (от MM0 до MM7)
Регистры XMM- (от XMM0 до XMM7) и регистр MXCSR
- управляющих регистра (CR0, CR2, CR3 и CR4) и регистры указателя системной таблицы (GDTR, LDTR, IDTR и задача регистрация)
- регистры отладки (DR0, DR1, DR2, DR3, DR6 и DR7)
- MSR регистрирует
Регистр общего назначения может использоваться более чем для одной цели. Эти цели
- значение
- адресация
- индексация
- (считая)
- (базовый)
Сегментный регистр , например, может содержать только значение сегмента, но не может использоваться для сложения. А регистр FPU может содержать только значение с плавающей запятой, но не может использоваться для адресации.
В IA-32 регистр ESP ближе к регистру общего назначения , поскольку его можно использовать (почти) для всех перечисленных выше целей:
- как значение:
mov eax, esp
- в адресации:
mov eax, [esp+4]
, но не как (масштабированный) индекс вродеmov eax, [4+esp*2]
- в качестве базы:
mov eax, [esp + eax]
- as count:
inc esp
до того, как прыжок станет действительным
Единственным исключением для ESP
является то, что адресация (масштабированного) индекса не может быть закодирована. Его можно использовать только как базовый регистр , который в исключительных случаях кодируется с помощью SIB-байта (см. руководство Intel, том 2, раздел 2.1.5, таблица 2.3 — см. нижний колонтитул).
Чтобы проиллюстрировать разницу в кодировании между ESP и другими регистрами (например, ECX ):
8b 01 mov eax, [ecx] ; MOV + ModRM (обычный) 8b 04 24 mov eax, [esp] ; MOV + ModRM + SIB байт 8b 41 04 mov eax, [ecx+4] ; MOV + ModRM + дисп8 8b 44 24 04 mov eax, [esp+4] ; MOV + ModRM + SIB + disp8
Думаю, несмотря на одно исключение ESP
все еще может считать себя GPR . Сборка
. Почему rbp и rsp называются регистрами общего назначения?
Если регистр может быть операндом для добавить
или использоваться в режиме адресации, это «общее назначение» , в отличие от таких регистров, как сегментный регистр FS
или RIP. Регистры GP также называются «целочисленными регистрами», хотя другие типы регистров также могут содержать целые числа.
В компьютерной архитектуре процессоры обычно обрабатывают целочисленные регистры/инструкции отдельно от регистров/инструкций FP/SIMD. например ЦП семейства Intel Sandybridge имеют отдельные файлы физических регистров для переименования целочисленных регистров GP и регистров FP/векторов. Они просто называются файлами регистров целочисленных и FP-файлов. (Где FP — это сокращение для всего, что ядру не нужно сохранять/восстанавливать, чтобы использовать регистры GP, оставляя нетронутым состояние FPU/SIMD пользовательского пространства.) Каждая запись в файле регистров FP имеет ширину 256 бит (чтобы содержат вектор AVX ymm), но записи файла целочисленного регистра должны иметь ширину только 64 бита.
На процессорах, которые переименовывают регистры сегментов (Skylake этого не делает), я предполагаю, что это будет частью целочисленного состояния, а также RFLAGS + RIP. Но когда мы говорим «целочисленный регистр», мы обычно имеем в виду регистр общего назначения.
«Общее назначение» в этом использовании означает «данные или адрес», в отличие от ISA, такой как m68k, где у вас есть регистры данных d0. .7 и регистры адреса a0..7, все 16 из которых являются целочисленными регистрами. Независимо от того, как обычно используется регистр , регистр общего назначения примерно таков, как он можно использовать .
Каждый регистр имеет некоторые особенности для некоторых инструкций, за исключением некоторых совершенно новых регистров, добавленных с x86-64: R8-R15. Это не лишает их права называться общего назначения. Исходные 8 (младшие 16 из них) восходят к 8086, и каждое из них неявно использовалось даже в исходном 8086.
Для RSP это специально для push/pop. /call/ret, поэтому большая часть кода никогда не использует его ни для чего другого. (И в режиме ядра используется асинхронно для прерываний, поэтому вы действительно не можете спрятать его где-нибудь, чтобы получить дополнительный регистр GP, как вы можете сделать это в коде пользовательского пространства: ESP так же универсален, как EAX?)
Но в контролируемых условиях (например, без обработчиков сигналов) вам не нужно использовать RSP для указателя стека. например вы можете использовать его для чтения массива в цикле с помощью pop, как в этом ответе на код-гольф. (На самом деле я использовал esp
в 32-битном коде, но разница та же: pop
быстрее, чем lodsd
на Skylake, хотя оба имеют размер 1 байт.)
См. также x86 Assembly — Почему [e]bx сохранены в соглашениях о вызовах? для неполного списка.
Я в основном ограничиваю это инструкциями пользовательского пространства, особенно теми, которые современный компилятор может фактически выдать из кода C или C++. Я не пытаюсь быть исчерпывающим для регуляров, которые имеют много неявных применений.
rax
: однооперандный [i]mul/[i]div/cdq/cdqe, строковые инструкции (stos),cmpxchg
и т.д. и т.п. А также специальные более короткие кодировки для многих непосредственных инструкций типа 2-байтовыйcmp al, 1
или 5-байтовыйadd eax, 12345
(без байта ModRM). См. также codegolf. SE Советы по игре в гольф в машинном коде x86/x64.Также есть
xchg
-with-eax, откуда взялась0x90 nop
(до того, какnop
стала отдельно документированной инструкцией в x86-64, потому чтоxchg eax,eax
ноль-расширяет eax в RAX и, таким образом, не может использовать кодировку0x90
. Ноxchg rax,rax
может по-прежнему собираться в REX.W=1 0x90.)rcx
: количество смен,rep
— количество строк, медленнаяпетля
инструкцияrdx
:rdx:rax
используется делением и расширением-умножением (формы с одним операндом), аcwd
/cdq
/cqo
для настройки наidiv
. Такжеrdtsc
и BMI2mulx
.rbx
: 8086xlatb
.cpuid
используют все четыре EAX. .EDX. 486смpxchg8b
, x86-64смpxchg16b
. Большинство 32-битных компиляторов будут выдаватьcmpxchg8
вместоstd::atomic
. (Тем не менее, чистая загрузка / чистое хранилище могут использовать SSE MOVQ или x87 fild/fistp, если они нацелены на Pentium или более позднюю версию.) 64-разрядные компиляторы будут использовать 64-разрядную версию 9.0800 блокирует cmpxchg , а не cmpxchg8b.::compare_exchange_weak Некоторые 64-битные компиляторы выдают
cmpxchg16b
дляatomic
. RBX имеет наименьшее количество неявных применений исходных 8, но блокировкаcmpxchg16b
— одна из немногих компиляторов, которые действительно будут использоваться.rsi
/rdi
: строковые операции, включаяrep movsb
, которые некоторые компиляторы иногда встраивают. (gcc также встраиваетrep cmpsb
для строковых литералов в некоторых случаях, но это, вероятно, не оптимально).rbp
:оставить
(всего на 1 мкп медленнее, чемmov rsp, rbp
/pop rbp
. gcc фактически использует его в функциях с указателем кадра, когда он не может просто90 90 rbp 01rbp pop). Также ужасно медленный
входит в
, который никто никогда не использует.rsp
: операции стека: push/pop/call/ret иleave
. (Ивведите
). И в режиме ядра (не пользовательского пространства) асинхронное использование аппаратным обеспечением для сохранения контекста прерывания. Вот почему в коде ядра не может быть красной зоны.r11
:syscall
/sysret
используйте его для сохранения/восстановления RFLAGS пользовательского пространства. (Вместе с RCX для сохранения/восстановления RIP пользовательского пространства).
Особые случаи кодирования режима адресации:
(см. Также rbp не допускается в качестве базы SIB? Это касается только режимов адресации, где я скопировал эту часть этого ответа.)
rbp
/ r13
can не быть базовым регистром без смещения: эта кодировка вместо этого означает: (в ModRM) rel32
(относительно RIP) или (в SIB) disp32
без базового регистра. ( r13
использует те же 3 бита в ModRM/SIB, поэтому этот выбор упрощает декодирование, поскольку декодер длины инструкции не обращается к биту REX.B, чтобы получить 4-й бит базового регистра). [r13]
собирается в [r13 + disp8=0]
. [r13+rdx]
собирается в [rdx+r13]
(чтобы избежать проблемы, поменяв местами базу/индекс, если это возможно).
рсп
/ r12
в качестве базового регистра всегда нуждается в байте SIB. (Кодировка ModR/M base=RSP является управляющим кодом для передачи байта SIB, и опять же, большему количеству декодера пришлось бы заботиться о префиксе REX, если бы r12
обрабатывались по-другому).
rsp
не может быть индексным регистром . Это позволяет кодировать [rsp]
, что более полезно, чем [rsp + rsp]
. (Intel могла бы разработать кодировки ModRM/SIB для 32-битных режимов адресации (новый в версии 386), поэтому SIB без индекса был возможен только с base=ESP. Это сделало бы [eax + esp*4]
возможно и исключить только [esp + esp*1/2/4/8]
. Но это бесполезно, поэтому они упростили аппаратное обеспечение, сделав index=ESP кодом без индекса независимо от базы. Это позволяет использовать два избыточных способа кодирования любого режима адресации base или base+disp: с SIB или без него.)
r12
может быть индексным регистром . В отличие от других случаев, это не влияет на декодирование длины инструкции. Кроме того, его нельзя обойти с помощью более длинной кодировки, как в других случаях. AMD хотела, чтобы набор регистров AMD64 был как можно более ортогональным, поэтому имеет смысл потратить несколько дополнительных транзисторов на проверку REX. X как часть индексного/безиндексного декодирования. Например, [rsp + r12*4]
требует index=r12, поэтому наличие r12
не совсем общего назначения сделало бы AMD64 худшей целью компилятора.
0: 41 8b 03 mov eax,DWORD PTR [r11] 3: 41 8b 04 24 mov eax,DWORD PTR [r12] # требуется SIB, например RSP 7: 41 8b 45 00 mov eax,DWORD PTR [r13+0x0] # требуется disp8 как RBP b: 41 8b 06 mov eax,DWORD PTR [r14] e: 41 8b 07 mov eax,DWORD PTR [r15] 11: 43 8b 04 e3 mov eax,DWORD PTR [r11+r12*8] # *может* быть индексом
Компиляторам нравится, когда все регистры могут использоваться для чего угодно, только ограничивая выделение регистров для нескольких особых операций. Это то, что подразумевается под ортогональностью регистров.
Сборка— Управление регистрами общего назначения AVR
Когда следует использовать GPR или SRAM?
Представьте свой стол, за которым вы сидите, когда работаете над чем-то. Это может быть электронная схема, головоломка или набор LEGO, который вы собираете. Но вы работаете над чем-то, состоящим из частей, и у вас есть ряд операций, которые вам нужно выполнить, чтобы закончить свою работу.
Хотя организация рабочего стола часто сводится к личным предпочтениям, я считаю, что один из разумных способов сделать это — разбросать все детали и инструменты по всему столу, оставив рядом с собой пустое место для выполнения реальных операций. Инструменты и детали можно сгруппировать по функции, размеру, сходству или другому признаку — это хорошая идея. Но важно, чтобы они не мешали вам работать с другими элементами, когда они не нужны. Таким образом, вы оставляете пустое место рядом и убираете их подальше на стол. Пока вы можете дотянуться и схватить их, когда они понадобятся, все в порядке, хотя для того, чтобы принести что-то подобное, требуется некоторая работа и время — вам нужно наклониться вперед, протянуть руку, может быть, даже немного встать.
Случайная картинка, которую я нашел в Интернете, которая объясняет, что я имею в виду:
Теперь, если вам нужно выполнить операцию и вам нужны две детали со своего стола, вы можете просто взять их и положить в пустую рабочую зону. . Поскольку он пуст и находится рядом с вами, вы можете легко соединить два блока LEGO, припаять резистор к печатной плате или соединить две части головоломки вместе, что бы вы ни делали. Такая организация вашего рабочего стола позволяет вам быть эффективным и облегчает вашу жизнь.
В этом сценарии рабочая область — это ваше АЛУ вместе с регистрами, а область, где вы размещаете все свои инструменты и компоненты, — это SRAM. Вы можете легко добраться и взять что-нибудь из SRAM в любое время, и она очень просторная, поэтому вы можете разместить здесь много вещей, но было бы очень сложно или совершенно невозможно выполнять какую-либо работу прямо здесь. Кроме того, каждая выборка из памяти занимает некоторое время.
С другой стороны, ваши регистры очень близки к вам, и здесь очень легко выполнять работу, а данные очень близки к АЛУ (ваши руки, которые выполняют работу). В то же время это также гораздо меньшая площадь, поэтому вы не можете поместить туда много компонентов, не нарушая всей ее цели.
Здесь применяется тот же рабочий процесс. У вас есть все данные, которые нужны вашей программе, сохраненные в SRAM. Всякий раз, когда вам нужно выполнить какие-то операции, вы выбираете только те части данных, которые вам нужны, и выполняете операции непосредственно в регистрах. Впоследствии результат становится другим фрагментом данных, который вы можете сохранить в одном из регистров или обратно в память, в зависимости от ваших обстоятельств.
Если вы работаете с небольшим алгоритмом, вы можете поместить все в регистры. Следуя настольной аллегории: вы паяете 10-элементную электронную схему, которая мигает светодиодами с интервалом? Вы, вероятно, можете разместить все на своем рабочем месте и работать свободно, несмотря на это. Вы паяете печатную плату с отдельным микроконтроллером и различными компонентами, состоящими из еще большего количества компонентов, например, 50 штук? Да, на рабочем месте все не уместишь, не мешая работе, лучше используй полки на столе.
То же самое относится и к процессору: вы вычисляете сумму массива, состоящего из 4 элементов? Вы можете поместить все элементы в георадар, без проблем. Что делать, если ваш массив состоит из 10 элементов? 30? 100? В какой-то момент вы не можете поместить все это в регистры, поэтому лучше просто хранить массив в памяти и извлекать только те части массива, которые вам нужны в данный момент. При этом аккумулятор, в котором хранится частичная сумма всех ранее обработанных элементов, всегда нужен для следующей операции, поэтому лучше хранить его в регистре.
В конце концов, то, как вы используете свои регистры и SRAM, полностью зависит от вас и работы, которую вы выполняете с данными. Но ваше решение не совсем случайное, это компромисс. Одни данные важнее в конкретный момент времени, чем другие, и только вам решать, какие из них оставить в журнале, а какие положить дальше на стол, на потом, а какие, может быть, и вовсе выбросить. . Регистры могут хранить меньший объем данных, но они могут выполнять реальную работу с этими данными, поэтому храните в них то, что вам нужно в данный момент времени. Память может хранить все остальное, но вам нужно некоторое время, чтобы получить данные из памяти в регистры для выполнения работы.
Теперь все это верно и для других архитектур, не только для AVR. В архитектурах CISC, таких как инструкции x86, которые выполняют работу, например ADD, могут принимать операнды памяти. Означает ли это, что он выполняет сложение в памяти? Нет, это означает только то, что и выборка, и добавление кодируются в одной инструкции. Сложение по-прежнему должно выполняться в АЛУ, поэтому выборка по-прежнему должна происходить — просто она явно не прописана программистом в ассемблерном коде в отдельной инструкции. С другой стороны, версия ADD AVR (которую вы можете найти в этом pdf, раздел 6) принимает только регистровые операнды. Это часто имеет место в архитектуре RISC. Вам необходимо явно загрузить свои данные в регистры перед выполнением работы с ними (например, ADDition) с помощью специальной инструкции (например, инструкции LD на AVR). И в архитектуре CISC, и в RISC выборка все еще происходит со всеми теми же недостатками.
Я знаю, вы, вероятно, ожидали более конкретного ответа, но правда в том, что только от вас как программиста зависит, как вы будете реализовывать алгоритм, над которым работаете. Однако в своих решениях вы руководствуетесь ограничениями вашей платформы, а также самим алгоритмом. В конце концов, вы хотите использовать регистры в полной мере и выполнять как можно больше работы с данными, которые вы уже извлекли, при этом сводя к минимуму общее количество загрузок и сохранений из и в SRAM. Надеюсь, что объяснение и аллегория стола помогут вам, и удачи в обучении.
Как избежать уничтожения GRP?
Когда функция вызывается и использует некоторые GRP, может произойти прерывание, вызывающее вектор прерывания. Теперь для вектора прерывания могут также потребоваться те же GPR, как избежать «выбрасывания» предыдущих значений? Если у вас есть работающий стек (вы должны это сделать! Это очень полезно и ничего не стоит, если вы его не используете), вы можете начать свой обработчик прерываний, помещая все регистры, которые он планирует использовать, в стек, а затем выталкивая их обратно. в регистры до возврата обработчика прерывания.
Аналогичный метод часто используется другими платформами при вызове функции - обычно есть регистры, сохраненные вызывающим абонентом, и регистры, сохраненные вызываемым пользователем (соответствующий ответ SO здесь). Разница с прерываниями заключается в том, что нет «вызывающего», так как функция прерывается и не имеет контроля над тем фактом, что вызывается обработчик прерывания, поэтому все регистры сохраняются вызываемым — вызываемый является обработчиком прерывания. И самый удобный способ сохранить их — это запихнуть их в стек, а потом вытолкнуть из него.
Недостатком здесь является то, что обработчик прерывания должен быть быстрым, а стек, в котором вы сохраняете регистры, имеет ограниченное пространство. Быстрые прерывания могут начать заполнять стек слишком быстро и перекрываться по времени, если каждое из них занимает слишком много времени для помещения и извлечения значений. Из-за этого вы можете подумать о назначении и сохранении только части GPR для использования в обработчиках прерываний, чтобы им ВООБЩЕ не разрешалось изменять другие регистры. Еще один разумный шаг — сохранить только те регистры, которые прерывание фактически изменяет. Если обработчику прерывания требуется только 2-3 GPR, то их нажатие и выталкивание не должно занимать много места и времени.