Разное

Mov asm: Инструкция MOV

Инструкция MOV


Что такое JavaScript

Если вы интересуетесь программированием вообще, и сайтостроением в частности, то вы наверняка слышали слово JavaScript. И, если вы до сих пор не узнали толком, что же это такое, то пришло время сделать это. Подробнее…

Пожалуй, инструкция MOV в ассемблере самая простая. Синтаксис этой команды такой:

MOV ПРИЁМНИК, ИСТОЧНИК

С помощью этой команды можно переместить значение из ИСТОЧНИКА в ПРИЁМНИК. То есть по сути команда MOV копирует содержимое ИСТОЧНИКА и помещает это содержимое в ПРИЁМНИК.

Никакие флаги при этом НЕ изменяются.

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

  • Записывать данные в регистры CS и IP.
  • Копировать данные из одного сегментного регистра в другой сегментный регистр (сначала нужно скопировать данные в регистр общего назначения).
  • Копировать непосредственное значение в сегментный регистр (сначала нужно скопировать данные в регистр общего назначения).

ИСТОЧНИКОМ может быть один из следующих:

  • Область памяти (MEM)
  • Регистр общего назначения (REG)
  • Непосредственное значение (например, число) (IMM)
  • Сегментный регистр (SREG)

ПРИЁМНИКОМ может быть один из следующих:

  • Область памяти (MEM)
  • Регистр общего назначения (REG)
  • Сегментный регистр (SREG)

С учётом ограничений, которые были описаны выше, комбинации ПРИЁМНИК-ИСТОЧНИК могут быть следующими:


REG,  MEM
SREG, MEM
MEM,  REG
REG,  REG
SREG, REG
MEM,  IMM
REG,  IMM
MEM,  SREG
REG,  SREG

Пример использования инструкции MOV:


MOV AX, 0B800h    ; установить AX = B800h (память VGA).
MOV DS, AX        ; копировать значение из AX в DS.
MOV CL, 'A'       ; CL = 41h (ASCII-код).
MOV CH, 01001110b ; CH = атрибуты цвета (желтый текст на красном фоне).
MOV BX, 72eh      ; BX = позиция на экране = 2*(x + y*80).
MOV [BX], CX      ; [0B800h:015Eh] = CX.

ПРИМЕЧАНИЕ
Этот пример не будет работать в Windows 2000 и выше, так как эти операционные системы запрещают программам напрямую обращаться к “железу”, а в этом примере мы пытаемся записать данные непосредственно в видеопамять.

Ну и напоследок скажу, почему эта инструкция называется

MOV. Это сокращение от английского слова MOVE, которое можно перевести как “переместить, перенести, передвинуть”. И, как теперь вам уже понятно, эта команда соответствует своему названию — она перемещает значение из одного регистра в другой. Хотя с точки зрения русского языка это будет не совсем правильно, потому что перемещения не происходит — значение из ИСТОЧНИКА никуда не исчезает (не перемещается), по сути оно копируется и вставляется в ПРИЁМНИК.



Первые шаги в программирование

Главный вопрос начинающего программиста – с чего начать? Вроде бы есть желание, но иногда «не знаешь, как начать думать, чтобы до такого додуматься». У человека, который никогда не имел дело с информационными технологиями, даже простые вопросы могут вызвать большие трудности и отнять много времени на решение. Подробнее…


Регистры процессора

Главная / Ассемблер / Для чайников / Введение в Ассемблер /

Начиная с модели 80386 процессоры Intel предоставляют 16 основных регистров для пользовательских программ и ещё 11 регистров для работы с мультимедийными приложениями (MMX) и числами с плавающей точкой (FPU/NPX). Все команды так или иначе изменяют содержимое регистров. Как уже говорилось, обращаться к регистрам быстрее и удобнее, чем к памяти. Поэтому при программировании на языке Ассемблера регистры используются очень широко.

В этом разделе мы рассмотрим основные регистры процессоров Intel. Названия и состав/количество регистров для других процессоров могут отличаться. Итак, основные регистры процессоров Intel.

Таблица 2.1. Основные регистры процессора.

Название Разрядность Основное назначение
EAX 32 Аккумулятор
EBX 32 База
ECX 32 Счётчик
EDX 32 Регистр данных
EBP 32 Указатель базы
ESP 32 Указатель стека
ESI 32 Индекс источника
EDI 32 Индекс приёмника
EFLAGS 32 Регистр флагов
EIP 32 Указатель инструкции (команды)
CS 16 Сегментный регистр
DS
16
Сегментный регистр
ES 16 Сегментный регистр
FS 16 Сегментный регистр
GS 16 Сегментный регистр
SS 16 Сегментный регистр

Регистры EAX, EBX, ECX, EDX – это регистры общего назначения. Они имеют определённое назначение (так уж сложилось исторически), однако в них можно хранить любую информацию.

Регистры EBP, ESP, ESI, EDI – это также регистры общего назначения. Они имеют уже более конкретное назначение. В них также можно хранить пользовательские данные, но делать это нужно уже более осторожно, чтобы не получить «неожиданный» результат.

Регистр флагов и сегментные регистры требуют отдельного описания и будут более подробно рассмотрены далее.

Пока для вас здесь слишком много непонятных слов, но со временем всё прояснится)))

Когда-то процессоры были 16-разрядными, и, соответственно, все их регистры были также 16-разрядными. Для совместимости со старыми программами, а также для удобства программирования некоторые регистры разделены на 2 или 4 «маленьких» регистра, у каждого из которых есть свои имена. В таблице 2.2 перечислены такие регистры.

Вот пример такого регистра.

Из этого следует, что вы можете написать в своей программе, например, такие команды:


MOV	AX,  1
MOV	EAX, 1
Обе команды поместят в регистр AX число 1. Разница будет заключаться только в том, что вторая команда обнулит старшие разряды регистра EAX, то есть после выполнения второй команды в регистре EAX будет число 1. А первая команда оставит в старших разрядах регистра EAX старые данные. И если там были данные, отличные от нуля, то после выполнения первой команды в регистре EAX будет какое-то число, но не 1. А вот в регистре AX будет число 1. Сложно? Ну это пока… Со временем вы к таким вещам привыкните.

Мы пока не говорили о разрядах (битах). Эту тему мы обсудим в разделах, посвящённых системам счисления. А сейчас пока вам достаточно знать, что нулевой разряд (бит) – это младший бит. Он крайний справа. Старший бит – крайний слева. Номер старшего бита зависит от разрядности числа/регистра. Например, в 32-разрядном регистре старшим битом является 31-й бит (потому что отсчёт начинается с 0, а не с 1).

Ниже приведён список регистров общего назначения, которые можно поделить описанным выше способом и при этом к «половинкам» и «четвертинкам» этих регистров можно обращаться в программе как к отдельному регистру.

Таблица 2.2. «Делимые» регистры..

Регистр Старшие разряды Имена 16-ти и 8-ми битных регистров
31…16 15…8 7…0
EAX AX
AH AL
EBX BX
BH BL
ECX CX
CH CL
EDX DX
DH DL
ESI SI
EDI DI
EBP BP
ESP SP
EIP
IP

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


Инструкции процессоров Intel


Что такое JavaScript

Если вы интересуетесь программированием вообще, и сайтостроением в частности, то вы наверняка слышали слово JavaScript. И, если вы до сих пор не узнали толком, что же это такое, то пришло время сделать это. Подробнее…

Как известно, программирование на Ассемблере — это написание исходных текстов, которые представляют собой набор команд (инструкций) процессора. В этом разделе публикуются подробные описания инструкций процессоров Интел и совместимых.

Я уже не раз об этом говорил, но снова повторю — вдруг кто не слышал )))

А это важно:

Каждый процессор имеет свой набор команд (инструкций)!

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

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

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

Набор базовых команд процессора

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

Пример инструкций процессора Интел (8086): ADD (сложение), SUB (вычитание), MOV (перемещение) и т.п.

Базовым набором команд процессора Intel можно считать полный набор инструкций процессора 8086, у которого было 116 команд. У современных процессоров команд, конечно, намного больше (хотя это зависит от архитектуры — есть процессоры с сокращённым набором команд, где их всего несколько десятков).

Современные процессоры кроме основных команд имеют ещё и разные расширения, такие как набор команд MMX, которые предназначены для более быстрого выполнения определённых операций.

Вообще это тема очень объёмная и довольно сложная. Поэтому в очередной раз советую вам изучить (причём очень тщательно) какую-нибудь хорошую книгу по Ассемблеру (на этом сайте есть ссылки на такие книги).

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



Первые шаги в программирование

Главный вопрос начинающего программиста – с чего начать? Вроде бы есть желание, но иногда «не знаешь, как начать думать, чтобы до такого додуматься». У человека, который никогда не имел дело с информационными технологиями, даже простые вопросы могут вызвать большие трудности и отнять много времени на решение. Подробнее…


Первая команда в ассемблере MOV

Эта команда используется для копирования значения из приёмника в источник. Синтаксис команды:

Mov приемник, источник

 

Пример:

Mov edx, ecx ; правильно

 

Размер источника и приемника должны быть одинаковыми.

Mov al, ecx; не правильно

 

Этот код пытается поместить DWORD (32-битное) значение в байт (8 битов).

 

Правильные команды:

Mov al, bl

Mov cl, dl

Mov cx, dx

Mov ecx, ebx

 

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

смещение 3A 3B 3C 3D 3E 3F
данные 0D 0A 7A 5E EF 7D FF AD C7

 

Данные, которые имеют смещение 3A: 25, 7A, 5E, 72, EF, и т.д. Чтобы поместить значение со смещения 3A, например, в регистр, можно воспользоваться командой mov:

mov eax, dword ptr [0000003Ah] ; eax=725E7A25h

 

При работе с памятью самый младший байт сохраняется в наиболее значимом байте: порядок байтов задом на перед.

 

dword (32-бит) значение 10203040 шестнадцатиричное сохраняется в памяти как: 40, 30, 20, 10

word (16-бит) значение 4050 шестнадцатиричное сохраняется в памяти как: 50, 40

 

Пример2:

mov cl, byte ptr [34h] ; cl получит значение 0Dh

mov dx, word ptr [3Eh] ; dx получит значение 7DEFh

 

Размеры данных для префикса ptr:

Byte — 1 байт

Word — 2 байта

Dword — 4 байта

 

Размер можно не указывать:

mov eax, [00403045h]

 

 

Пример3:

mov eax, 403045h ; eax= 00403045h

mov cx, [eax] ; CX=значение (размера word) из памяти указанной в EAX (403045)

 


Стековые операции — PUSH, POP.

Стек это область в памяти, на которую указывает регистр стека ESP(SP). Есть две команды, для размещения значения в стеке и извлечения его из стека: PUSH и POP. Команда PUSH размещает значение в стеке, т.е. помещает значение в ячейку памяти, на которую указывает регистр ESP, после этого значение регистра ESP уменьшается на 4. Команда POP извлекает значение из стека, т.е. извлекает значение из ячейки памяти, на которую указывает регистр ESP, после этого увеличивается значение регистра ESP на 4. Значение, помещенное в стек последним, извлекается первым.

Пример:

(1) mov ecx, 100

(2) mov eax, 200

(3) push ecx

(4) push eax

(5) xor ecx, eax

(6) add ecx, 400

(7) mov edx, ecx

(8) pop ebx

(9) pop ecx

 

Анализ:

1: поместить 100 в ecx

2: поместить 200 в eax

3: разместить значение из ecx (=100) в стеке (размещается первым)

4: разместить значение из eax (=200) в стеке (размещается последним)

5/6/7: выполнение операций над ecx, значение в ecx изменяется

8: извлечение значения из стека в ebx: ebx=200.

9: извлечение значения из стека в ecx: ecx=100.

 

Пример: Работа со стеком.

Смещение 120A 120B
Значение
      ESP     

ESP стоит в том месте, на которое он указывает)

Mov ax, 4560h

Push ax

Смещение 120A 120B
Значение
    ESP       

Mov cx, FFFFh

Push cx

Смещение 120A 120B
Значение FF FF
  ESP         

Pop edx

Смещение 120A 120B
Значение FF FF
      ESP     

edx = 4560FFFFh.

Пересылка данных

Поддерживаются начиная с процессора 8086.
mov <операнд_назначения>, <операнд_источник>

xchg <операнд1>, <операнд2>

cmovcc <приёмник><источник>

bswap <регистр 32>

 

Особенности команды mov:

1) нельзя осуществлять пересылку из одной области памяти в другую. При такой необходимости нужно использовать в качестве промежуточного буфера любой доступный регистр общего назначения. Пример: переслать байты из ячейки памяти fls в ячейку fld:

Data

Fls dd 947503b3h

Fld dd ?

Code

Start

—-

Mov eax, fls

Mov fld,eax

—-

End start

 

2) нельзя загрузить в сегментный регистр значение непосредственно из памяти. Для выполнения такой загрузки нужно использовать промежуточный объект (регистр общего назначения или стек).

 

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

Пример: инициализировать регистр es значением регистра ds:

Mov ax,ds

Move es,ax

Можно также использовать стек и команды push и pop:

Push ds ; поместить значение регистра ds в стек

Pop es ; записать в es число из стека

 

Нельзя использовать сегментный регистр cs в качестве операнда назначения.

 

5) оператор ptr можно применять и когда требуется принудительно поменять размерность операндов. К примеру, требуется переслать значение 0ffh во второй байт поля flp:

Data

Flp dw ?

Code

start:

—-

mov byte ptr (flp+1),0ffh

—-

End start

 

Для двунаправленной пересылки данных применяют команду xchg. Эту же операцию можно выполнить применив последовательность из нескольких команд mov. Общий вид записи:

 

XCHG <операнд1>, <операнд2>

 

Содержимое операнда2 копируется в операнд1, а старое содержимое операнда1 — в операнд2. XCHG можно выполнять над двумя регистрами или над регистром и переменной.

Например:

Xchg eax,ebx ; обменять содержимое регистров eax и ebx.

То же, что три команды на языке С:

temp = eax;

eax = ebx;

ebx = temp;

 

Xchg al,al ; а эта команда не делает ничего

xchg ax, word ptr [si] ; обменять содержимое регистра ах и слова в памяти по адресу в [si].

 

Для условной пересылки данных используется команда:

CMOVcc <приёмник><источник>

 

Набор команд, которые копируют содержимое источника в приемник, если удовлетворяется то или иное условие.

Можно использовать команды CMOVcc сразу после команды СМР (сравнение) с теми же операндами, например:



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

 

ASSEMBLER&WIN32. КУРС МОЛОДОГО БОЙЦА. УРОК 3.mov — команда ассемблера

Когда вы пишете программу на ассемблере, вы просто пишете команды процессору. Команды процессору — это просто коды или коды операций или опкоды. Опкоды — фактически «читаемый текст»- версии шестнадцатеричных кодов. Из-за этого, ассемблер считается самым низкоуровневым языком программирования, все в ассемблере непосредственно преобразовывается в шестнадцатеричные коды. Другими словами, у вас нет компилятора, который преобразовывает язык высокого уровня в язык низкого уровня, ассемблер только преобразовывает коды ассемблера в данные.

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

Комментарии в ваших программах оставляются после точки с запятой. Точно также как в дельфи или си через //.
Числа в ассемблере могут представляться в двоичной, десятеричной или шестнадцатеричной системе. Для того, чтобы показать в какой системе использовано число надо поставить после числа букву. Для бинарной системы пишется буква b (пример: 0000010b, 001011010b), для десятеричной системы можно ничего не указывать после числа или указать букву d (примеры: 4589, 2356d), для шестнадцатеричной системы надо указывать букву h, шестнадцатеричное число надо обязательно писать с нулём в начале (примеры: 00889h, 0AC45h, 056Fh, неправильно F145Ch, С123h).

Самая первая команда будет хорошо всем известная MOV. Эта команда используется для копирования (не обращайте внимания на имя команды) значения из одного места в другое. Это ‘место’ может быть регистр, ячейка памяти или непосредственное значение (только как исходное значение). Синтаксис команды:

mov приемник, источник

Вы можете копировать значение из одного регистра в другой.

mov edx, ecx

Вышеприведенная команда копирует содержание ecx в edx. Размер источника и приемника должны быть одинаковыми, например: эта команда — НЕ допустима:

mov al, ecx ; не правильно

Этот опкод пытается поместить DWORD (32-битное) значение в байт (8 битов). Это не может быть сделано mov командой (для этого есть другие команды).

А эти команды правильные, потому что у них источник и приемник не отличаются по размеру:

mov al, bl
mov cl, dl
mov cx, dx
mov ecx, ebx

Вы также можете получить значение из памяти и поместить эго в регистр. Для примера возьмем следующую схему памяти:

 

смещение3435363738393A3B3C3D3E3F404142
данные0D0A50324457257A5E72EF7DFFADC7

(Каждый блок представляет байт)

Значение смещения обозначено здесь как байт, но на самом деле это это — 32-разрядное значение. Возьмем для примера 3A, это также — 32-разрядное значение: 0000003Ah. Только, чтобы с экономить пространство, некоторые используют маленькие смещения.

Посмотрите на смещение 3A в таблице выше. Данные на этом смещении — 25, 7A, 5E, 72, EF, и т.д. Чтобы поместить значение со смещения 3A, например, в регистр, вы также используете команду mov:

mov eax, dword ptr [0000003Ah]

Означает: поместить значение с размером DWORD (32-бита) из памяти со смещением 3Ah в регистр eax. После выполнения этой команды, eax будет содержать значение 725E7A25h. Возможно вы заметили, что это — инверсия того что находится в памяти: 25 7A 5E 72. Это потому, что значения сохраняются в памяти, используя формат little endian . Это означает, что самый младший байт сохраняется в наиболее значимом байте: порядок байтов задом на перед. Я думаю, что эти примеры покажут это:

dword (32-бит) значение 10203040 шестнадцатиричное сохраняется в памяти как: 40, 30, 20, 10
word (16-бит) значение 4050 шестнадцатиричное сохраняется в памяти как: 50, 40

Вернемся к примеру выше. Вы также можете это делать и с другими размерами:

mov cl, byte ptr [34h] ; cl получит значение 0Dh
mov dx, word ptr [3Eh] ; dx получит значение 7DEFh

Вы, наверное, уже поняли, что префикс ptr обозначает, что надо брать из памяти некоторый размер. А префикс перед ptr обозначает размер данных:
Byte — 1 байт
Word — 2 байта
Dword — 4 байта

Иногда размер можно не указывать:

mov eax, [00403045h]

Так как eax — 32-разрядный регистр, ассемблер понимает, что ему также требуется 32-разрядное значение, в данном случае из памяти со смещением 403045h.

Можно также непосредственные значения:

mov edx, 5006h

Эта команда просто запишет в регистр edx, значение 5006. Скобки, [ и ], используются, для получения значения из памяти (в скобках находится смещение), без скобок, это просто непосредственное значение.

Можно также использовать регистр как ячейку памяти (он должен быть 32-разрядным в 32-разрядных программах):

mov eax, 403045h ; пишет в eax значение 403045
mov cx, [eax] ; помещает в регистр CX значение (размера word) из памяти
; указанной в EAX (403045)

В mov cx, [eax], процессор сначала смотрит, какое значение (= ячейке памяти) содержит eax, затем какое значение находится в той ячейке памяти, и помещает это значение (word, 16 бит, потому что приемник, cx, является 16-разрядным регистром) в CX.

Стековые операции — PUSH, POP . Перед тем, как рассказать вам о стековых операциях, я уже объяснял вам, что такое стек. Стек это область в памяти, на которую указывает регистр стека ESP. Стек это место для хранения адресов возврата и временных значений. Есть две команды, для размещения значения в стеке и извлечения его из стека: PUSH и POP. Команда PUSH размещает значение в стеке, т.е. помещает значение в ячейку памяти, на которую указывает регистр ESP, после этого значение регистра ESP увеличивается на 4. Команда Pop извлекает значение из стека, т.е. извлекает значение из ячейки памяти, на которую указывает регистр ESP, после этого уменьшает значение регистра ESP на 4. Значение, помещенное в стек последним, извлекается первым. При помещении значения в стек, указатель стека уменьшается, а при извлечении — увеличивается. Рассмотрим пример:

(1) mov ecx, 100
(2) mov eax, 200
(3) push ecx ; сохранение ecx
(4) push eax
(5) xor ecx, eax
(6) add ecx, 400
(7) mov edx, ecx
(8) pop ebx
(9) pop ecx

Анализ:
1: поместить 100 в ecx
2: поместить 200 в eax
3: разместить значение из ecx (=100) в стеке (размещается первым)
4: разместить значение из eax (=200) в стеке (размещается последним)
5/6/7: выполнение операций над ecx, значение в ecx изменяется
8: извлечение значения из стека в ebx: ebx станет 200 (последнее размещение, первое извлечение)
9: извлечение значения из стека в ecx: ecx снова станет 100 (первое размещение, последнее извлечение)

Чтобы узнать, что происходит в памяти, при размещении и извлечении значений в стеке, см. на рисунок ниже:

Смещение1203120412051206120712081209120A120B
Значение000000000000000000
ESP

(стек здесь заполнен нулями, но в действительности это не так, как здесь). ESP стоит в том месте, на которое он указывает)

mov ax, 4560h
push ax

Смещение1203120412051206120712081209120A120B
Значение000060450000000000
ESP

mov cx, FFFFh
push cx

Смещение1203120412051206120712081209120A120B
ЗначениеFFFF60450000000000
ESP

pop edx

Смещение1203120412051206120712081209120A120B
ЗначениеFFFF60450000000000
ESP

edx теперь 4560FFFFh.

Вызов подпрограмм возврат из них — CALL, RET. Команда call передает управление ближней или дальней процедуре с запоминанием в стеке адреса точки возврата. Команда ret возвращает управление из процедуры вызывающей программе, адрес возврата получает из стека. Пример:

..code..
call 0455659
..more code..
Код с адреса 455659:
add eax, 500
mul eax, edx
ret

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

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

push значение_1
push значение_2
call procedure

Внутри процедуры, аргументы могут быть прочитаны из стека и использованы. Локальные переменные, т.е. данные, которые необходимы только внутри процедуры, также могут быть сохранены в стеке. Я не буду подробно рассказывать об этом, потому, что это может быть легко сделано в ассемблерах MASM и TASM. Просто запомните, что вы можете делать процедуры и что они могут использовать параметры.

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

Постигаем Си глубже, используя ассемблер / Хабр

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

От читающих потребуются хотя бы базовые знания в следующих вещах:
  • регистры процессора
  • стек
  • представление чисел в компьютере
  • синтаксис ассемблера и Си

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

Что будем использовать?


  1. Нам понадобится компилятор Си, который поддерживает современный стандарт. Можно воспользоваться онлайн компилятором на сайте ideone.com.
  2. Так же нам нужен декомпилятор, опять же, можно воспользоваться онлайн декомпилятором на сайте godbolt.org.
  3. Можно так же взять компилятор для ассемблера, который есть на ideone по ссылке выше.

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

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

Простейшая программа


Эта статья не стремится повторить ту, которую я приводил в самом начале. Но начинать нужно с азов, поэтому часть материала будет вынуждено пересекаться. Надеюсь на понимание.

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

int main(void) 
{
	5 + 3;
	return 0;
}

Ничем не будет отличаться от:
int main(void) 
{
	return 0;
}

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

Второе, нам нужны флаги компиляции. Достаточно двух: -O0 и -m32. Этим мы задаем нулевой уровень оптимизации и 32-битный режим. С оптимизаций должно быть очевидно: нам не хочется видеть интерпретацию нашего кода в asm, а не оптимизированного. С режимом тоже должно быть очевидно: меньше регистров — больше внимания к сути. Хотя эти флаги я буду периодически менять, чтобы углубляться в материал.

Таким образом, если вы пользуетесь gcc, то компиляция может выглядеть так:

gcc source.c -O0 -m32 -o source

Соответственно, если вы пользуетесь godbolt, то вам нужно указать эти флаги в строку ввода рядом с выбором компилятора. (Первые примеры я демонстрирую на gcc 4.4.7, потом поменяю на более поздний)

Теперь, можно посмотреть первый пример:

int main(void) 
{
	register int a = 1; //записываем в регистровую переменную 1
	return a; //возвращаем значение из регистровой переменной
}

Итак, следующий код соответствует этому:
push 		ebp
mov 		ebp, esp

push 		ebx
mov 		ebx, 1
mov 		eax, ebx

pop 		ebx
pop 		ebp
ret

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

Инструкции ассемблера имеют вид:

mnemonic dst, src
т. е.

инструкция получатель, источник

Тут нужно оговориться, что AT&T-синтаксис имеет другой порядок, и потом мы к нему еще вернемся, но сейчас нас интересует синтаксис схожий с NASM.

Начнем с инструкции mov. Эта инструкция перемещает из памяти в регистры или из регистров в память. В нашем случае она перемещает число 1 в регистр ebx.

Давайте кратко о регистрах: в архитектуре x86 восемь 32х битных регистров общего назначения, это значит, что эти регистры могут быть использованы программистом (в нашем случае компилятором) при написании программ. Регистры ebp, esp, esi и edi компилятор будет использовать в особых случаях, которые мы рассмотрим позже, а регистры eax, ebx, ecx и edx компилятор будет использовать для всех остальных нужд.

Таким образом mov ebx, 1, прямо соответствует строке register int a = 1;

И означает, что в регистр ebx было перемещено значение 1.

А строчка mov eax, ebx, будет означать, что в регистр eax будет перемещено значение из регистра ebx.

Есть еще две строчки push ebx и pop ebx. Если вы знакомы с понятием «стек», то догадываетесь, что сначала компилятор поместил ebx в стек, тем самым запомнил старое значение регистра, а после окончания работы программы, вернул из стека это значение обратно в регистр ebx.

Почему компилятор помещает значение 1 из регистра ebx в eax? Это связано с соглашением о вызовах функций языка Си. Там несколько пунктов, все они нас сейчас не интересуют. Важно то, что результат возвращается в eax, если это возможно. Таким образом понятно, почему единица в итоге оказывается в eax.

Но теперь логичный вопрос, а зачем понадобился ebx? Почему нельзя было написать сразу mov eax, 1? Все дело в уровне оптимизации. Я же говорил: компилятор не должен вырезать наш код, а мы написали не return 1, мы использовали регистровую переменную. Т. е. компилятор сначала поместил значение в регистр, а затем, следуя соглашению, вернул результат. Поменяйте уровень оптимизации на любой другой, и вы увидите, что регистр ebx, действительно, не нужен.

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

Стек


Усложним пример и перестанем пользоваться регистровыми переменными (Вы же их нечасто используете?). Посмотрим во что превратится такой код:
int main(void) 
{
	int a = 1; //записываем в переменную 1
	int b = a + 5; //прибавим к 'a' 5 и сораним в 'b'
	return b; //возвращаем значение из переменной
}

ASM:
push 		ebp
mov 		ebp, esp
sub 		esp, 16

mov 		DWORD PTR [ebp-8], 1
mov 		eax, DWORD PTR [ebp-8]
add 		eax, 5
mov 		DWORD PTR [ebp-4], eax
mov 		eax, DWORD PTR [ebp-4]

leave
ret

Опять же, пропустим верхние 3 строчки и нижние 2. Теперь у нас переменная а локальная, следовательно память ей выделяется на стеке. Поэтому мы видим следующую магию: DWORD PTR [ebp-8], что же она означает? DWORD PTR — это переменная типа двойного слова. Слово — это 16 бит. Термин получил распространение в эпоху 16-ти битных процессоров, тогда в регистр помещалось ровно 16 бит. Такой объем информации стали называть словом (word). Т. е. в нашем случае dword (double word) 2*16 = 32 бита = 4 байта (обычный int).

В регистре ebp содержится адрес на вершину стека для текущей функции (мы к этому еще вернемся, потом), поэтому он смещается на 4 байта, чтобы не затереть сам адрес и дописывает значение нашей переменной. Только, в нашем случае он смещается на 8 байт для переменной a. Но если вы посмотрите на код ниже, то увидите, что переменная b лежит со смещением в 4 байта. Квадратные скобки означают адрес. Т. е. это строка работает следующим образом: на основе адреса, хранящегося в ebp, компилятор помещает значение 1 по адресу ebp-8 размера 4 байта. Почему минус восемь, а не плюс. Потому что плюсу бы соответствовали параметры, переданные в эту функцию, но опять же, обсудим это позже.

Следующая строка перемещает значение 1 в регистр eax. Думаю, это не нуждается в подробных объяснениях.

Далее у нас новая инструкция add, которая осуществляет добавление (сложение). Т. е. к значению в eax (1) добавляется 5, теперь в eax находится значение 6.

После этого нужно переместить значение 6 в переменную b, что и делается следующей строкой (переменная b находится в стеке по смещению 4).

Наконец, нам нужно вернуть значение переменной b, следовательно нужно переместить
значение в регистр eax (mov eax, DWORD PTR [ebp-4]).

Если с предыдущим все понятно, то можно переходить, к более сложному.

Интересные и не очень очевидные вещи.


Что произойдет, если мы напишем следующее: int var = 2.5;

Каждый из вас, я думаю, ответит верно, что в var будет значение 2. Но что произойдет с дробной частью? Она отбросится, проигнорируется, будет ли преобразование типа? Давайте посмотрим:

ASM:

mov DWORD PTR [ebp-4], 2

Компилятор сам отбросил дробную часть за ненадобностью.

Что произойдет, если написать так: int var = 2 + 3;

ASM:

mov DWORD PTR [ebp-4], 5

И мы узнаем, что компилятор сам способен вычислять константы. А в данном случае: так как 2 и 3 являются константами, то их сумму можно вычислить на этапе компиляции. Поэтому можно не забивать себе голову вычислением таких констант, компилятор может сделать работу за вас. Например, перевод в секунды из часов можно записать, как hours * 60 * 60. Но скорее, в пример тут стоит поставить операции над константами, которые объявлены в коде.

Что произойдет, если напишем такой код:

int a = 1;
int b = a * 2;
mov 		DWORD PTR [ebp-8], 1
mov 		eax, DWORD PTR [ebp-8]
add 		eax, eax
mov 		DWORD PTR [ebp-4], eax

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

Вы могли слышать, что операция «умножение» выполняется дольше, чем операция «сложение». Именно по этим соображениям компилятор оптимизирует такие простые вещи.

Но усложним ему задачу и напишем так:

int a = 1;
int b = a * 3;

ASM
mov DWORD PTR [ebp-8], 1
mov edx, DWORD PTR [ebp-8]
mov eax, edx
add eax, eax
add eax, edx
mov DWORD PTR [ebp-4], eax

Пусть вас не вводит в заблуждение использование нового регистра edx, он ничем не хуже eax или ebx. Может понадобиться время, но вы должны увидеть, что единица попадает в регистр edx, затем в регистр eax, после чего значение eax складывается само с собой и после уже добавляется еще одна единица из edx. Таким образом, мы получили 1+1+1.

Знаете, бесконечно он так делать не будет, уже на *4, компилятор выдаст следующее:

mov DWORD PTR [ebp-8], 1
mov eax, DWORD PTR [ebp-8]
sal eax, 2
mov DWORD PTR [ebp-4], eax
mov eax, 0

Итак, у нас новая инструкция sal, что же она делает? Это двоичный сдвиг влево. Эквивалентно следующему коду в Си:
int a = 1;
int b = a << 2;

Для тех, кто не очень понимает, как работает этот оператор:

0001 сдвигаем влево (или добавляем справа) на два нуля: 0100 (т. е. 4 в 10ой системе счисления). По своей сути сдвиг влево на 2 разряда — это умножение на 4.

Забавно, что если вы умножите на 5, то компилятор сделает один sal и один add, можете сами потестировать разные числа.

На 22, компилятор на godbolt.org сдается и использует умножение, но до этого числа он пытается выкрутиться самыми разными способами. Даже вычитание использует и еще некоторые инструкции, которые мы еще не обсуждали.

Ладно, это были цветочки, а что вы думаете по поводу следующего кода:

int a = 2;
int b = a / 2;

Если вы ожидаете вычитания, то увы — нет. Компилятор будет выдавать более изощренные методы. Операция «деление» еще медленнее умножения, поэтому компилятор будет также выкручиваться:
mov 		DWORD PTR [ebp-4], 2
mov 		eax, DWORD PTR [ebp-4]
mov 		edx, eax
shr 		edx, 31
add 		eax, edx
sar 		eax
mov 		DWORD PTR [ebp-8], eax

Следует сказать, что для этого кода я выбрал компилятор существенно более поздней версии (gcc 7.2), до этого я приводил в пример gcc 4.4.7. Для ранних примеров существенных отличий не было, для этого примера они используют разные инструкции в 5ой строчке кода. И пример, сгенерированный 7.2, мне сейчас легче вам объяснить.

Стоит обратить внимание, что теперь переменная a находится в стеке по смещению 4, а не 8 и сразу же забыть об этом незначительном отличии. Ключевые моменты начинаются с mov edx, eax. Но пока пропустим значение этой строки. Инструкция shr осуществляет двоичный сдвиг вправо (т. е. деление на 2, если бы было shr edx, 1). И тут некоторые смогут подумать, а почему, действительно, не написать shr edx, 1, это же то, что делает код в Си? Но не все так просто.

Давайте проведем небольшую оптимизацию и посмотрим на что это повлияет. В действительности, мы нашим кодом выполняем целочисленное деление. Так как переменная «a» является целочисленным типом и 2 константа типа int, то результат никак не может получиться дробным по логике Си. И это хорошо, так как делить целочисленные числа быстрее и проще, но у нас знаковые числа, а это значит, что отрицательное число при делении инструкцией shr может отличаться на единицу от правильного ответа. (Это все из-за того, что 0 влезает по середине диапазона для знаковых типов). Если мы заменим знаковое деление на unsigned:

unsigned int a = 2;
unsigned int b = a / 2;

То получим ожидаемое. Стоит учесть, что godbolt опустит единицу в инструкции shr, и это не скомпилируется в NASM, но она там подразумевается. Измените 2 на 4, и вы увидите второй операнд в виде 2.

Теперь посмотрим на предыдущий код. В нем мы видим sar eax, это то же самое, что и shr, только для знаковых чисел. Остальной же код просто учитывает эту единицу, когда мы делим отрицательное число (или на отрицательное число, хотя код немного изменится). Если вы знаете, как представляются отрицательные числа в компьютере, вам будет не трудно догадаться, почему мы делаем сдвиг вправо на 31 разряд и добавляем это значение к исходному числу.

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

Заключение


Для первой статьи материала уже больше, чем достаточно. Пора закруглятся и подводить итоги. Мы ознакомились с базовым синтаксисом ассемблера, выяснили, что компилятор может брать на себя простейшие оптимизации при вычислениях. Увидели разницу между регистровыми и стековыми переменными. И некоторые другие вещи. Это была вводная статья, пришлось много времени уделять очевидным вещам, но они очевидны не для всех, в будущем мы постигнем больше тонкостей языка Си.

Часть 2

MOV,

:

:


«-» -,,,.. . «МОВ» 15 — -.


  1. : MOV A, Rn; п = 0-7
    :
    : 1
    : (А): = (Rn)
    :
              ; (А) = FAH, (R4) = 93H
    MOV A, R4; (A) = 93H, (R4) = 93H
                         

  2. : MOV A,
    :
    : 1
    : (A): = (прямой)
    :
               ; (A) = 93H, ([40]) = 10H, (R0) = 40H
    MOV A, 40H; (A) = 10H, ([40]) = 10H, (R0) = 40H
    

  3. : MOV A, @ Ri; я = 0,1
    :
    : 1
    : (А): = ((Ri))
    :
               ; (A) = 10H, (R0) = 41H, ([41]) = 0CAH
    MOV A, @ R0; (A) = CAH, (R0) = 41H, ([41]) = 0CAH
     

  4. : MOV A, # data
    :
    : 1
    : (A): = <# data8>
    :
                ; (А) = C9H (11001001B)
    MOV A, # 37H; (A) = 37H (00110111B)
                 
     

  5. : МОВ Рн, А; п = 0-7
    :
    : 1
    : (Rn): = (А)
    :
              ; (А) = 38Н, (R0) = 42Н
    MOV R0, A; (A) = 38H, (R0) = 38H
                         

  6. : МОВ р-н, <прямой>; п = 0-7
    :
    : 2 а
    : (Rn): = (прямой)
    :
               ; (R0) = 39H, (P2) = 0F2H
    MOV R0, P2; (R0) = F2H
    

  7. : MOV Rn, # data; п = 0-7
    :
    : 1
    : (Rn): = <# data8>
    :
                 ; (R0) = 0F5H
    MOV R0, # 49H; (R0) = 49H
                 
     

  8. : MOV , A
    :
    : 1
    : (прямой): = (A)
    :
               ; (P0) = FFH, (A) = 4BH
    MOV P0, A; (P0) = 4BH, (A) = 4BH
    

  9. : MOV , Rn; п = 0-7
    :
    : 2 а
    : (прямой): = (Rn)
    :
               ; (PSW) = C2H, (R7) = 57H
    MOV PSW, R7; (PSW) = 57H, (R7) = 57H
    

  10. : MOV <прямой>, <прямой>
    :
    : 2 а
    : (прямой): = (прямой)
    :
                ; ([45]) = 33H, ([48]) = 0DEH
    MOV 48H, 45H; ([45]) = 33H, ([45]) = 33H
    

  11. : MOV , @ Ri; я = 0,1
    :
    : 2 а
    : (прямой): = ((Ri))
    :
                ; (R1) = 49H, ([49]) = 0E3H
    MOV 51H, @ R1; ([51]) = 0E3H, ([49]) = 0E3H
    

  12. : MOV , #data
    :
    : 2 а
    : (прямой): = <# data8>
    :
                 ; ([5F]) = 9BH
    MOV 5FH, # 07H; ([5F]) = 07H
    

  13. : MOV @ Ri, A; я = 0-7
    :
    : 1
    : ((Ri)): = (А)
    :
              ; (R1) = 51H, ([48]) = 75H, (A) = 0BDH
    MOV @ R1, A; ([48]) = 0BDH
                         

  14. : MOV @Ri, , i = 0,1
    :
    : 2 а
    : ((Ri)): = (прямой)
    :
               ; (R0) = 51H, ([51]) = 0E3H, (P0) = 0ACH
    MOV @ R0, P0; (A) = 10H, ([51]) = 0ACH
    

  15. : MOV Ri, # data; я = 0,1
    :
    : 1
    : ((Ri)): = <# data8>
    :
                  ; ([7E]) = 67H, (R1) = 7EH
    MOV @ R1, # 0A9H; ([7E]) = 0A9H, (R1) = 7EH
                         



— — DOC — — — — — — —

1 | 2 1 -. (26.11.2001 — 32 Кб)
2 -. (26.11.2001 — 32 Кб)
3 — HelloWord. (26.11.2001 — 8 Кб)
4 -. (26.11.2001 — 2 Кб)
5 -. (26.11.2001 — 2 Кб)
6 -. (27.11.2001 — 4 Кб)
7 — МОВ. (27.11.2001 — 7 Кб)
8 — ДС. (27.11.2001 — 9 Кб)
9 -. (27.11.2001 — 13 Кб)
10 — Turbo Debugger DOS. (27.11.2001 — 35 Кб)
11 — JMP. (28.11.2001 — 12 Кб)
12 -. (28.11.2001 — 14 Кб)
13 -. (28.11.2001 — 7 Кб)
14 -. (28.11.2001 — 10 Кб)
15 — IOCTL. (28.11.2001 — 5 Кб)
16 — 09ч. (29.11.2001 — 6 Кб)
17 — CMP. (29.11.2001 — 11 Кб)
18 — JE. (29.11.2001 — 9 Кб)
19 — 44H 08H INT 21H. (29.11.2001 — 8 Кб)
20 -. (30.11.2001 — 7 Кб)
21 — 25ч. (30.11.2001 — 18 Кб)
22 — ТЕСТ. (05.12.2001 — 6 Кб)
23 — Турбо-отладчик. (05.12.2001 — 20 Кб)
24 — (INT 21h 01H). (05.12.2001 — 7 Кб)
25 -. (05.12.2001 — 12 Кб)
26 — Основная загрузочная запись. (11.12.2001 — 11 Кб)
27 -. (23.12.2001 — 5 Кб)
28 -. (23.12.2001 — 2 Кб)
29 — ASCII. (23.12.2001 — 2 Кб)
30 — ВКЛЮЧИТЬ. (27.12.2001 — 3 Кб)
31 -. (27.12.2001 — 19 Кб)
32 — -. (31.12.2001 — 6 Кб)
33 — XCHG. (31.12.2001 — 12 Кб)
34 — EXE ASM. (31.12.2001 — 25 Кб)
35 -. (02.01.2002 — 14 Кб)
36 — 02ч. (02.01.2002 — 8 Кб)
37 -. (02.01.2002 — 8 Кб)
38 -. (02.01.2002 — 5 Кб)
39 -. (02.01.2002 — 5 Кб)
40 — 3dh -. (05.02.2002 — 2 Кб)
41 -. (05.02.2002 — 6 Кб)
42 -. (05.02.2002 — 2 Кб)
43 — 3ех. (05.02.2002 — 2 Кб)
44 — 3fh. (05.02.2002 — 8 Кб)
45 -. (07.01.2002 — 8 Кб)
46 — LOOP. (07.01.2002 — 8 Кб)
47 — ЛОДСБ. (08.01.2002 — 8 Кб)
48 -. (08.01.2002 — 8 Кб)
49 -. (09.01.2002 — 24 Кб)
50 -. (09.01.2002 — 10 Кб)
1 | 2


ОСНОВЫ СБОРКИ РЫЧАГА — MOV AX, BX

# Псевдоним Цель
R0 Общего назначения
R1 Общего назначения
R2 Общего назначения
R3 Общего назначения
R4 Общего назначения
R5 Общего назначения
R6 Общего назначения
R7 Поддерживает номер системного вызова
R8 Общего назначения
R9 Общего назначения
R10 Общего назначения
R11 FP Указатель кадра
Регистры специального назначения
R12 IP Внутренний процедурный звонок
R13 SP Указатель стека
R14 LR Регистр ссылок
R15 ПК Счетчик программ
CPSR Регистр текущего состояния программы

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

АРМ Описание x86
R0 Общего назначения EAX
R1-R5 Общего назначения EBX, ECX, EDX, ESI, EDI
R6-R10 Общего назначения
R11 (FP) Указатель кадра EBP
R12 Внутренний процедурный звонок
R13 (SP) Указатель стека ESP
R14 (LR) Регистр ссылок
R15 (ПК) <- Счетчик программ / Указатель команд -> EIP
CPSR Текущий регистр состояния программы / флаги EFLAGS

R0-R12 : может использоваться во время общих операций для хранения временных значений, указателей (мест в памяти) и т. Д.R0, например, может называться аккумулятором во время арифметических операций или для хранения результата ранее вызванной функции. R7 становится полезным при работе с системными вызовами, поскольку он хранит номер системного вызова, а R11 помогает нам отслеживать границы в стеке, выступая в качестве указателя кадра (будет рассмотрено позже). Более того, соглашение о вызове функций в ARM указывает, что первые четыре аргумента функции хранятся в регистрах r0-r3.

R13: SP (указатель стека).Указатель стека указывает на верхнюю часть стека. Стек — это область памяти, используемая для хранения, специфичного для функции, которая освобождается при возврате функции. Таким образом, указатель стека используется для выделения места в стеке путем вычитания значения (в байтах), которое мы хотим выделить, из указателя стека. Другими словами, если мы хотим выделить 32-битное значение, мы вычитаем 4 из указателя стека.

R14: LR (регистр ссылок). Когда выполняется вызов функции, регистр связи обновляется адресом памяти, указывающим на следующую инструкцию, из которой была инициирована функция.Это позволяет программе вернуться к «родительской» функции, которая инициировала вызов «дочерней» функции после завершения «дочерней» функции.

R15: PC (счетчик программ). Программный счетчик автоматически увеличивается на размер выполняемой инструкции. Этот размер всегда составляет 4 байта в состоянии ARM и 2 байта в режиме THUMB. Когда выполняется инструкция ветвления, ПК сохраняет адрес назначения. Во время выполнения ПК сохраняет адрес текущей инструкции плюс 8 (две инструкции ARM) в состоянии ARM, а текущая инструкция плюс 4 (две инструкции Thumb) в состоянии Thumb (v1).Это отличается от x86, где ПК всегда указывает на следующую инструкцию, которую нужно выполнить.

Давайте посмотрим, как ПК ведет себя в отладчике. Мы используем следующую программу для сохранения адреса компьютера в r0 и включения двух случайных инструкций. Давай посмотрим что происходит.

. Раздел. Текст
.global _start

_Начало:
 mov r0, pc
 mov r1, # 2
 добавить r2, r1, r1
 bkpt 

В GDB мы устанавливаем точку останова на _start и запускаем ее:

  gef>  br _start
Точка останова 1 на 0x8054
  gef>  run 

Вот скриншот результата, который мы видим первым:

 $ r0 0x00000000 $ r1 0x00000000 $ r2 0x00000000 $ r3 0x00000000
$ r4 0x00000000 $ r5 0x00000000 $ r6 0x00000000 $ r7 0x00000000
$ r8 0x00000000 $ r9 0x00000000 $ r10 0x00000000 $ r11 0x00000000
$ r12 0x00000000 $ sp 0xbefff7e0 $ lr 0x00000000 $ pc 0x00008054
$ cpsr 0x00000010

0x8054 <_start> mov r0, pc <- $ pc
0x8058 <_start + 4> mov r0, # 2
0x805c <_start + 8> добавить r1, r0, r0
0x8060 <_start + 12> bkpt 0x0000
0x8064 и r1, r0, r1, asr # 10
0x8068 cmnvs r5, r0, lsl # 2
0x806c tsteq r0, r2, ror # 18
0x8070 и r0, r0, r11
0x8074 tsteq r8, r6, lsl # 6 

Мы видим, что ПК содержит адрес (0x8054) следующей инструкции (mov r0, pc), которая будет выполнена.Теперь давайте выполним следующую инструкцию, после которой R0 должен содержать адрес ПК (0x8054), верно?

  $ r0 0x0000805c  $ r1 0x00000000 $ r2 0x00000000 $ r3 0x00000000
$ r4 0x00000000 $ r5 0x00000000 $ r6 0x00000000 $ r7 0x00000000
$ r8 0x00000000 $ r9 0x00000000 $ r10 0x00000000 $ r11 0x00000000
$ r12 0x00000000 $ sp 0xbefff7e0 $ lr 0x00000000 $ pc 0x00008058
$ cpsr 0x00000010

0x8058 <_start + 4> mov r0, # 2 <- $ pc
  0x805c  <_start + 8> добавить r1, r0, r0
0x8060 <_start + 12> bkpt 0x0000
0x8064 и r1, r0, r1, asr # 10
0x8068 cmnvs r5, r0, lsl # 2
0x806c tsteq r0, r2, ror # 18
0x8070 и r0, r0, r11
0x8074 tsteq r8, r6, lsl # 6
0x8078 adfcssp f0, f0, # 4.0 

… верно? Неправильно. Посмотрите на адрес в R0. Хотя мы ожидали, что R0 будет содержать ранее прочитанное значение ПК (0x8054), вместо этого он содержит значение, которое на две инструкции опережает значение ПК, которое мы ранее считали (0x805c). Из этого примера вы можете видеть, что когда мы напрямую читаем PC, это следует из определения, что PC указывает на следующую инструкцию; но при отладке ПК указывает на две инструкции впереди текущего значения ПК (0x8054 + 8 = 0x805C). Это связано с тем, что старые процессоры ARM всегда выбирали на две инструкции раньше выполняемых в данный момент инструкций.ARM сохраняет это определение для обеспечения совместимости с более ранними процессорами.

Cortex-M3

ADD ADD R0, R1, Operand2
ADD R0, R1, 12 бит конст.

R0 = R1 + R2
N, Z, C, V S
АЦП АЦП R0, R1, R2
АЦП R0, R1, 8 бит, константа

R0 = R1 + R2 + C
N, Z, C, V S
ADDW ADD R0, R1, 12 бит конст. 12 бит N, Z, C, V
ПОД SUB R0, R1, R2
SUB R0, R1, 12 бит конст.

R0 = R1 — R2
N, Z, C, V S
SBC SBC R0, R1, R2
SBC R0, R1, 8 бит, конст.

R0 = R1 — R2 — C
N, Z, C, V S
SUBW SUB R0, R1, 12 бит, конст. 12 бит N, Z, C, V
RSB RSB R0, R1, R2
RSB R0, R1, 8 бит конст.
RSB R0, R1, R2, ASR № 23

.
R0 = R2 — R1 R0 = 8 бит const — R1
N, Z, C, V S
ADR ADR R0, этикетка +/- 12 бит
ADR.W R0, этикетка +/- 32 бит
.
LDR
STR
LDR R0, [R1, # 8bit const.]! —
LDR R0, [R1], # 8бит конст. —
LDRB R0, [R1] —
STRB R0, [R1], # 1
/
.
B =, SB = ()
H =, SH = ()
LDR
STR
LDR R0, [R1, R2, {LSL # 0..3}]
STR R0, [R1, R2, {LSL # 0..3}]
.
B, SB, H, SH
LDR
LDRD
LDR R0, этикетка
LDRD R0, R1, этикетка
.
B, SB, H, SH
STR / STRD.
LDRT
STRT
.
LDR / STR.
LDRD
STRD
LDRD R0, R1, [R2, # 10bit const.]! —
LDRD R0, R1, [R2], # 10бит конст. —
LDRD R0, R1, [R2] —
STRD R0, R1, [R2]
/
.
4.
LDM
STM
LDM R0, {R1-R3}
LDM R0 !, {R1-R3} — R0
IA, DB, FD, EA -.
/.
IA —
ДБ -.
PUSH
POP
НАЖАТЬ {R0, R2-R7, R12}
POP {R0, R2-R7, R12}
/
LDREX
STREX
LDREX R1, [R2, # 10бит конст.]
STREX R0, R1, [R2, # 10bit const.]
/.
B =, H =.
CLREX CLREX ().

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

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