- Версия(и) Minecraft
- Любая
Методичка которую я формировал для себя, когда учил ассемблер x86-32.
Предисловие.Скомпоновано и пояснено. Я не преподаватель, не профессионал, говнокодер и вообще ассемблер изучал впервые. Просто для меня наилучшим способом научится и разобраться является "составление учебника" для самого себя. Поскольку вышло, на мой взгляд, довольно неплохо, я подумал что могу поделится этим с другими. Она предназначена для общеобразовательных целей для программистов всех уровней, которые могут быть профи в написании кода, но не иметь понятия, как этот их код работает (хотя это маловероятно). Материал построен с целью помочь разобраться людям, как работает их код - что скрывается за абстракциями языковых операторов. Поэтому методичка не преследует цель рассказать вам о всех возможных и невозможных операторах ассемблера, крутых алгоритмах и не ставит целью сделать из вас асоциального системного программиста в протертом свитере.
Учитывая текущее стремительное развитие нового уровня абстракции в написании кода в виде нейросетей, необходимость писать код на языках программирования, по всей вероятности, тоже скоро отпадет, хотя многие ретрограды со мной не согласятся. Однако для того, чтобы умело управлять абстракцией, будь то язык программирования 3 уровня типа С или С++, 4 уровня типа Java или будь то английский язык для составления промпта в нейросети, вам все равно необходимо знать, как выполняется ваш код, чтобы делать его наиболее быстрым, компактным, поддерживаемым и эффективным.
Вся методичка построена на пошаговом изучении от простого к сложному и активно использует наглядные практические примеры. Так как человек это обычная обезьяна которая лучше всего учится через примеры, это наиболее эффективный способ обучения.
Author Max. A. Vavaev. 2026.
Ресурсы
Для проверки компиляции программ я использовал сайт: Asm-EditorПеревод чисел между системами: Конвертер числовых систем
Оглавление
Основы ассемблерного кодаВведение
Перемещение и базовая арифметика
Операции с оперативной памятью
Управление потоком
Побитовые операции
Основы ассемблерного кода
Введение
Что такое x86-ассемблер?
Ассемблер — это язык, где одна инструкция ≈ одна команда процессора. В отличие от C/Java, здесь нет абстракций типа if или for — управление регистрами и памятью осуществляется напрямую.Вся методичка будет построена на работе с 32-битными процессорами и ниже. Но во введении будут упомянуты и 64-битные регистры.
Итак. Процессор x86 работает с:
- Регистрами
- Оперативной памятью (RAM)
- Инструкциями
В методичке используются и перемешаны три системы счисления: двоичная, десятичная и шестнадцатиричная. Двоичная понятно почему - машинный код. Десятичная - удобное человеческое общепринятое счисление. Но причем тут шестнадцатеричная? Я думаю вы уже догадываетесь, что компьютеру переводить значения из двоичной в десятичную не в пример сложнее, чем из двоичной в шестнадцатеричную. В тоже время, человеку понимать двоичный код еще тяжелее, чем компьютеру десятичный. Так что по сути это компромисс между человеческой десятичной системой и машинным двоичным кодом, удобный как для компьютера, так и для человека.
Везде в коде я где я использую одиночные числа, например 7, 8, 10 - это числа в десятичной системе. Однако учитывайте, что компилятор либо автоматически преобразует их в шестнадцатиричные. Там где явно хочу указать шестнадцатеричное число, я пишу через 0х... и восьмизначные шестнадцатеричное число, включая нули, для наглядности. Там где я явно указываю двоичные числа, которые по числу меньше 32, это просто упрощение, мысленно держим всегда в голове что речь идет про 32-битные процессоры.
Что такое регистры?
Регистры — это сверхбыстрые ячейки памяти внутри самого процессора, с которыми он работает напрямую. Начиная с 4 бит они развивались к 64 битам.Про 4-битные первые процессоры упоминать детально не будем ввиду отсутствия практического смысла. Единственное что полезно знать - Carry Flag для переноса впервые появился там. Которому позже выделили отдельный системный регистр.
Регистры х86 процессора делятся на регистры общего назначения и специальные (или системные) регистры.
Регистры имеют обратную совместимость. Это можно назвать "матрешкой" регистров для выполнения программ написанных под 8-битный процессор, на 16/32/64 битном процессоре более старших моделей.
Регистры состоят из байтов - блоков по 8 бит - поэтому они все кратны степени двойки - просто исторически так сложилось, что они "наращивались" путем условного "соединения" двух в один.
Регистры общего назначения групп 1 и 2.
Также существуют еще B, C, D. В рамках общего образования полезно знать что эти буквы не просто соответствуют первым четырем буквам латинского алфавита, но и также имеют исторические названия.Группа 1 (A, B, C, D) используется для прямых операций.
- A - Accumulator - результат каких-либо арифметических операций.
- B - Base - указатель на данные.
- С - Counter - счетчик (например в цикле)
- D - Data - расширение A - тоже как результат.
- SI - Source Index - "регистр-источник". Указывает на адрес в памяти (в шестнадцатеричной форме), откуда надо начать копирование или откуда начать чтение.
- DI - Destination Index - "регистр-приемник". Указывает на адрес в памяти (в шестнадцатеричной форме), куда мы хотим записать данные.
- SP - Stack Pointer - указатель верхушки стека. Удобен для операций внутри функции.
- BP - Base Pointer - якорь функции. Он остается постоянным на протяжении всего выполнения функции.
Примеры "матрешки" регистров на примере А.
64-битный процессор
- RAX (64 бита): Полный регистр.
- EAX (32 бита): Младшая половина RAX.
- AX (16 бит): Младшая половина EAX.
- AL (8 бит): Младший байт AX.
- AH (8 бит): Старший байт AX.
Обращение к младшим регистрам этих регистров осуществляется путем добавления суффикса.
- D - DoubleWord - младшие 32 бита (напр. R8D - младшие 32 бита R8 регистра)
- W - Word - младшие 16 бит
- B - Byte - младшие 8 бит
32-битный процессор
- EAX (32 бита): Полный регистр.
- AX (16 бит): Младшая половина EAX.
- AL (8 бит): Младший байт AX.
- AH (8 бит): Старший байт AX.
16-битный процессор
- AX (16 бит): Полный регистр.
- AL (8 бит): Младший байт AX.
- AH (8 бит): Старший байт AX.
8-битный процессор
- A (8 бит): Единственный байт.
Системные регистры - или флаги, принимающие значение 0 либо 1.
- C (CF, Carry Flag) — Флаг переноса: Переключается, если результат операции не влез в разрядную сетку (например, сложили два очень больших числа) или если при вычитании пришлось «занимать» бит из несуществующего старшего разряда.
- Z (ZF, Zero Flag) — Флаг нуля: Один из самых важных. Если результат операции равен 0, этот флаг становится 1. Именно его проверяют команды типа JZ (Jump if Zero).
- S (SF, Sign Flag) — Флаг знака: Становится 1, если результат отрицательный (копирует самый старший бит результата).
- O (OF, Overflow Flag) — Флаг переполнения: Загорается, если произошла ошибка при работе с числами со знаком. Например, если ты сложил два положительных числа, а результат получился таким огромным, что "вылетел" в знаковый бит и число стало выглядеть как отрицательное.
Другие системные регистры (для общего ознакомления)
- A (AC, Auxiliary Carry Flag) — Вспомогательный перенос: Используется для BCD-арифметики (двоично-десятичный код). Почти не встречается в обычном коде.
- P (PF, Parity Flag) — Флаг четности: Устанавливается, если в младшем байте результата четное количество единиц. В современном программировании используется редко, это наследие древних протоколов передачи данных.
Перемещение и базовая арифметика
Инструкции MOV и XOR
Инструкция mov выполняет операцию копирования второго в первое.
Код:
mov eax, 5
Здесь второе значение (число 5) копируется в регистр eax и регистр eax принимает значение 5.
Однако операции можно проводить не только с числами. Например:
Код:
mov eax, ebx
Здесь второе значение из регистра ebx копируется в регистр eax. При этом, ebx по прежнему содержит значение.
Если же значение в регистре ebx нам более не потребуется в программе, мы можем его удалить, обнулив регистр инструкцией xor
Код:
xor ebx, ebx
Тогда ebx станет 0.
Инструкции ADD и SUB, INC и DEC
Инструкция add выполняет операцию сложения первого со вторым
Код:
add eax, 5
add eax, ebx
Код:
sub eax, 5
sub eax, ebx
Что произойдет по шагам?
- Результат математически равен -3.
- В компьютере отрицательные числа хранятся в дополнительном коде (two's complement).
- Для 32-битного EAX число будет выглядеть в шестнадцатеричном виде как 0xFFFFFFFD.
- SF = 1: Флаг знака. Он всегда копирует самый старший бит результата. Раз там 1 (в 0xF...), значит результат отрицательный (если мы считаем его числом со знаком).
- CF = 1: Флаг переноса/заема. Поскольку мы вычитали большее из меньшего, произошел «заем». Для беззнаковых чисел это означает переполнение вниз (underflow).
- ZF = 0: Результат не ноль.
- OF = 0: Флаг переполнения для чисел со знаком. Здесь он будет 0, так как прекрасно укладывается в диапазон 32-битного числа со знаком.
Код:
inc eax
Инструкция dec аналогично выполняет операцию отнимания единицы. Выполняется быстрее чем sub
Код:
dec eax
Очевидное напрашивающееся практическое применение этих инструкций - циклы.
Практика 1.
Опиши, что происходит при выполнении этого кода построчно - объясни себе по шагам, без проверки на компиляторе.
Код:
mov eax, 5
mov ebx, 4
add eax, ebx
sub eax, 2
xor eax
Практика 2.
Опиши, что происходит при выполнении этого кода построчно - объясни себе по шагам.
Код:
mov eax, 5
mov ebx, eax
add eax, 1
add ecx, 1
mov edx, ecx
add edx, ecx
Инструкции MUL, IMUL, SHL, DIV, IDIV, SHR, SAR
Знаковые и беззнаковые операции
Почему существует две версии инструкций mul, imul или div idiv? Потому что существуют знаковые и беззнаковые операции умножения и деления. Что это означает? Это просто интерпретация компьютером самого старшего бита. Беззнаковая операция всегда положительная: 1111 1111 = 255. Если представить это в упрощенной форме - мы имеем гирьки с весами, лежащие в коробке: 128, 64, 32, 16, 8, 4, 2, 1. А для IMUL/IDIV меняется интерпретация самого старшего (первого) бита. Он воспринимает 0 как + и 1 как -. Например, при знаковом IMUL если мы берем 1111 1111, то первое значение интерпретируется как знак -128, 64, 32, 16, 8, 4, 2, 1 Как же эта операция работает с точки зрения компьютера? Смотрите: если мы берем число 1111 1111 в знаковом виде, то самый левый бит будет равен -128. А давайте сложим остальные значения: 64+32+16+8+4+2+1... получается 127! Таким образом -128 + 127 получается -1 - как раз тот самый знак минус!Умножение
Операция mul является самой старой и примитивной: она умножает значение в текущем регистре на значение, которое было в предыдущем регистре. То есть, если вы пишете:
Код:
mov eax, 4
mov ecx, 3
mul eax
Пример использования MUL:
Код:
mov eax, 0xFFFFFFFF
mov ebx, 2
mul ebx
Пример использования IMUL с двумя операндами:
Код:
mov eax, 0xFFFF4e
mov ebx, 4
imul ebx, eax
Пример использования IMUL с тремя операндами:
Код:
mov eax, 0xFAFe
mov ebx, 3
imul edx, eax, ebx
Практика 3.
- Напиши программу вычисления математического выражения: x = (3 + 5) * (8 - 4).
- Напиши программу возведения числа A = 5 в квадрат.
- Напиши программы перемножения чисел 5, 4, 3 используя сперва только mul, затем imul с двумя операндами и затем imul с тремя операндами.
Операция SHL - побитовый сдвиг влево (умножение на 2^n)
Что такое операция shl? Это операция побитового сдвига влево (сокр. shift left). Часто используется как более быстрая альтернатива обычному умножению. При ее использовании все числа, заполняющие регистр, смещаются в сторону старшего разряда на заданное количество бит. Условно, представьте что у вас стоит пять кубиков на столе и они стоят на квадратиках где написано 1, 2, 3, 4, 5. И тут вы берете и рукой сдвигаете кубики влево, оставляя последний пустым. Так и работает побитовый сдвиг: допустим eax: 0101 1101 - после операции shl eax, 1 все числа сдвинутся на одну позицию влево. То есть, после выполнения eax будет равен 1010 1010. Математический смысл сдвига на n значений влево эквивалентен умножению на 2^n.
Например, число 00000101 в двоичном это 5. А сдвиг на 1 бит влево дает 00001010 - а это уже 10. Таким образом произошло умножение на 2. Сдвиг на два бита уже будет означать умножение на 4. (2^2 = 4)
Пример использования SHL:
Код:
mov eax, 0x101
shl eax, 2
Практика 4.
Напишите программу, который умножает значение в регистре eax на 10, используя только команды mov, add и логический сдвиг shlДеление
Забудьте про абстракцию в виде десятичных дробей - процессор делит в столбик. И как и в математике, делить на ноль нельзя - это приведет к аварийному завершению программы. С делением всё немного сложнее, чем с умножением. Если умножение — это просто «взяли два числа и получили одно побольше», то деление — это целая процедура, где процессор заранее ожидает данные в строго определённых местах.
Деление всегда происходит «большого на малое». Чтобы результат (частное) поместился в регистр, само делимое должно быть в два раза длиннее делителя. Если вы делите на 32-битный регистр (например, ebx), то делимое должно занимать сразу два регистра: edx (старшая часть) и eax (младшая часть). Это записывается как edx:eax
Обычное деление div просто делит числа как есть, записывая целое в eax, а остаток - в edx.
Пример DIV:
Код:
mov eax, 100
mov edx, edx
mov ecx, 7
div ecx
При знаковом делении idiv процессор должен понимать, что делимое имеет знак и что результат тоже должен будет иметь знак. То есть результат должен быть заполнен нулями (если положительное) и единицами (если отрицательное делимое или делитель). Для этого надлежит использовать команду-помощник cdq (Convert Doubleword to Quadword). Эта команда готовит регистры к знаковому делению, просто копируя самый старший бит делимого во все 32 бита выходного регистра (обычно edx). Кроме того, у этой команды существуют аналоги на других уровнях, например:
- cbw (Byte to Word): Расширяет AL в AX.
- cwd (Word to Doubleword): Расширяет AX в DX:AX.
- cqo (Quadword to Octoword): Расширяет RAX в RDX:RAX.
Код:
mov eax, -100
cdq
mov ecx, 7
idiv ecx
Практика 5.
Напиши программу, которая делит 50 на 12.
Практика 6.
Напиши программу, которая делит -47 на 13Операция SHR и SAR - побитовые сдвиги вправо
shr - логический побитовый сдвиг вправо (shift right). Биты аналогично shl сдвигаются, только вправо. Математический смысл - сдвиг на n значений вправо это деление на 2^n.
sar - арифметический побитовый сдвиг вправо (shift arithmetic right). Тоже самое, но для знаковых чисел. При сдвиге вправо знаковый бит сохраняется. Если число было отрицательным (начиналось с 1), то слева будут дописываться единицы. Если положительным — нули.
Пример обоих сразу - положительные числа:
Код:
mov eax, 40
shr eax, 1
Код:
mov ebx, 40
sar ebx, 1
Пример - отрицательные числа:
Возьмем число -8 (в двоичном виде 1111 1000)
Код:
mov ecx, -8
mov edx, -8
shr ecx, 1
Практика 7.
Есть число 16 (в двоичном виде это 00010000).- Что получится, если сдвинуть его вправо на 1 бит (shr eax, 1)?
- Что получится, если сдвинуть его вправо на 2 бита (shr eax, 2)?
- На сколько бит нужно сдвинуть число, чтобы поделить его на 8?
Операции с оперативной памятью
Ровно точно также как мы оперируем значениями в регистрах, мы можем оперировать значениями в оперативной памяти.Адрес — это просто число, указывающее на ячейку памяти.
Ячейки в оперативной памяти уже гораздо более многочисленны, поэтому процессор обращается к ним по численному адресу, который опять же представлен в шестнадцатеричном виде.
Обращение к памяти по адресу осуществляется также, как и обращение к регистру, только адрес памяти заключается в квадратные скобки. Если обращение к памяти может быть неоднозначным (например, вы хотите записать в память напрямую значение), то используются уточняющие системные директивы размера и указатели. Память для процессора - это бесконечная "лента" байтов. Чтобы он знал, брать ему один байт или несколько сразу, ему нужно явно указывать это. То есть, допустим, вы хотите напрямую в память записать число 0x0000433b. Вам очевидно, что оно должно занимать два байта, а вот процессору это совершенно неочевидно. Поэтому чтобы записать данные прямо в память, вам надо непосредственно указать процессору что вы перемещаете в память по адресу конкретное количество байт: mov word ptr [0x1050], 66. Если вы попробуете записать больше, чем влазит в указанный размер, в лучшем случае вы просто потеряете эти данные. Например, 0xfffff в word (2 байта) записать не выйдет. В случае с регистрами эти уточняющие директивы не нужны, так как процессору "известно" о размере регистров.
Чтение значения по адресу в памяти называется разыменованием.
Примечание: я использую адрес 0x00001030 потому что на при работе с онлайн-компилятором, о котором я вам сообщил в самом начале, память по первым адресам будет занята. Вы заметите это когда запустите выполнение кода.
Код:
mov eax, [0x00001030]
Практика 8.
Код:
mov eax, 5
mov [0x00001030], eax
mov ebx, [0x00001030]
add ebx, 3
Практика 9.
Код:
mov eax, 0xA
mov ebx, 2
mov [0x00001040], ebx
mov ebx, [0x00001040]
add ebx, 2
mov [0x00001040], ebx
xor ebx, ebx
mov ebx, [0x00001040]
add ebx, eax
mov [0x00001050], ebx
mov eax, [0x00001050]
mov [0x00001040], ebx
Стек. Инструкции работы со стеком - PUSH и POP
Теперь, когда мы уже понимаем разницу между памятью, адресом памяти, регистром и инструкциями, можно разобрать операции со стеком. Стек - это область памяти, которая работает по принципу "последний зашел - первый вышел". Я уже приводил пример с чашками в начале. Зачем она вообще нужна? Стек - это место хранения временных данных в отдельной функции. Поскольку код может ветвится, а не выполнятся монолитным листом, то это идеально решение. Условно говоря, у дерева есть ветка. О том, что на ветке есть листочки, основной ствол дерева не знает - это знает только ветка. Получается такой базовый принцип наследственности - ствол знает о ветке, ветка знает о листьях. А вот ствол о листьях не знает. Да, можно объявить глобальные листы, но тогда они будут относится к стволу, а не к ветке. Так вот чтобы когда процессор начинает выполнять код "ветки", он узнает о существовании листьев и сохраняет их в стек. После того, как все дела с веткой завершены, процессору больше не нужно знать ничего о листьях - и они удаляются из стека. Здесь мы вспоминаем про ESP - главный регистр стека, хранящий адрес последнего значения стека (верхней чашки) во время выполнения функции. А EBP мы объявляем для того, чтобы знать, где было начало ветки, чтобы вернутся к ней и продолжить выполнение кода по стволу дерева.
Код:
mov eax, 0xAAAA
mov ebx, 0xBBBB
push eax
push ebx
- Уменьшает значение ESP на 4 (в 32-bit режиме).
- Записывает значение eax по адресу [ESP]
- Уменьшает значение ESP еще на 4.
- Записывает значение ebx по адресу [ESP-4]
А теперь про pop - давайте улучшим наш предыдущий код.
Код:
mov eax, 0xAAAA
mov ebx, 0xBBBB
push eax
push ebx
pop eax
pop ebx
Ух ты! Мы поменяли значения местами, не прибегая к операции перезаписывания!
Так работает стек.
Практика 10.
Напиши программу "Сумма трех значений". Положи в стек числа 3, 5 и 4. Затем по очереди извлеки их и сложи так, чтобы итоговая сумма (0xC) оказалась в регистре eax.
Практика 11.
Напиши программу "Математическое выражение": Реализуй вычисление eax = (5 + A) * 3. Попробуй сделать это, используя только mov, add и imul.
Практика 12.
Напиши программу "Обмен". У тебя есть значение в eax и значение в ebx. Поменяй их местами, используя только команды push и pop (без использования дополнительных регистров или mov).Управление потоком
Команда сравнения CMP и операторы переходов JMP
CMP eax, ebx — это, по сути, вычитание (eax - ebx), но результат никуда не записывается. Вместо этого процессор меняет состояние специальных флагов в регистре EFLAGS.- Если числа равны, устанавливается флаг нуля (ZF = 1).
- Если eax < ebx, устанавливается флаг знака (SF = 1).
- Если eax > ebx, то результат сравнения (вычитания) изменен не будет.
Команды переходов обычно пишутся сразу после команды сравнения, так как в этот момент системные флаги наверняка будут иметь актуальное значение. На основании именно этих актуальных значений и работает логика команд переходов. Ниже представлена небольшая табличка. Эти команды смотрят на флаги и решают: прыгать на "метку" или идти дальше.
| Команда | Описание | Условие (после CMP) |
|---|---|---|
| je (Jump Equal) | Переход, если равно | eax == ebx |
| jne (Jump Not Equal) | Переход, если не равно | eax != ebx |
| jg (Jump Greater) | Переход, если больше | eax > ebx |
| jl (Jump Less) | Переход, если меньше | eax < ebx |
| jmp | Безусловный переход | Переходит всегда |
Код:
mov eax, 10
mov ebx, 10
cmp eax, ebx
je label
mov ecx, 6
label:
mov ecx, 8
Для беззнаковых чисел тоже существуют свои операторы перехода, однако они оперируют терминами "выше" ja (jump if above - переход, если выше (больше)) или "ниже" jb (jump if below - переход, если ниже (меньше)), а также jae (above or equal - выше или равно) и jbe (below or equal - ниже или равно).
Беззнаковые операторы переключают только флаги CF и ZF
Небольшая памятка по ним:
| Команда | Описание | Условие на языке флагов | Почему так? |
|---|---|---|---|
| ja (Above) | Больше | CF=0 и ZF=0 | Нет заема (значит a >= b) и результат не ноль (значит a != b). |
| jb (Below) | Меньше | CF=1 | Произошел заем, значит мы вычитали большее из меньшего. |
| jae (Above or Equal) | Больше или равно | CF=0 | Заема не было — либо числа равны, либо первое больше. |
| jbe (Below or Equal) | Меньше или равно | CF=1 или ZF=1 | Либо произошел заем, либо результат обнулился. |
Пусть в eax у нас будет -1... 0xffffffff
Код:
mov eax, 0xffffffff
mov ebx, 0xffffffff
cmp eax, ebx
ja label
mov ecx, 4
label:
mov ecx, 6
Что еще?
В современных версиях уже есть разные упрощенные команды типа loop - это готовый цикл, однако я намеренно перенес их в конец, так как они все являются производными от тех базовых команд, которые я показываю в методичке. Например инструкция loop это по сути inc (dec) и cmp в одной команде, то есть она складывает (вычитает), сравнивает - если еще не ноль, то продолжает. В конечном итоге эта команда уже является первой абстракцией над более простыми командами - она декодируется в более простые микрооперации перед выполнением.Пример: здесь инструкция loop выполняет цикл, вычитая единицу из счетчика ecx, пока счетчик не станет равным 0.
Код:
mov ecx, 10
sum_loop:
add eax, ecx
loop sum_loop
Код:
mov ecx, 10
sum_loop_manual:
add eax, ecx
dec ecx
cmp ecx, 0
jne sum_loop_manual
Если провести параллели с языками высокого уровня - loop это ближе к for, а ручной цикл к do-while.
Практика 13.
Напиши программу, которая увеличивает значение в eax на 30, используя только команду inc, команду сравнения и команды переходов.
Практика 14.
Напиши программу, которая должна прибавлять по 1 к значению в eax = 3 до тех пор, пока eax не станет 47.
Практика 15.
Напиши программу, которая будет завершаться, если значение eax превысит 100.Побитовые операции
В основе лежат четыре побитовые операции and, or, xor и not. С xor мы уже сталкивались раньше и я обещал рассказать, что это такое. Для начала вообще узнаем что это такое.Команда AND
and - побитовое "И" - выдает 1, если оба входных бита равны 1. Одно из оисновых применений этой команды - побитовые маски. Представьте, что есть байт данных, где каждый бит отвечает за какой-то параметр (например, состояние датчиков). Нужно проверить только 3-й бит, а остальные значения мешают.| Операция | Биты | Пояснение |
|---|---|---|
| Исходное число | 1011 0110 | Произвольные данные |
| Маска | 0000 1000 | Мы хотим только 3-й бит (считая с нуля справа) |
| Результат AND | 0000 0000 | Все обнулилось, так как 3-й бит был 0 |
Другой небольшой пример использования - проверка четности числа.
Код:
mov eax, 7
and eax, 1
jnz is_odd
jmp end_check
is_odd:
add eax, 2
mov [0x00001050], eax
end_check:
Практика 16.
Напиши программу, которая записывает произвольное число и проверяет 5-й бит (считая от 0). Что дпоказывает флаг ZF? Затем переведи результат в двоичный вид и проверь компьютер.Команда OR
or - это команда побитовое ИЛИ. Если and можно использовать как фильтр, отсекающий ненужные биты, то or - точный инструмент для побитовой правки значения или сравнения. Она сравнивает входящие значения и выдает 1, если хотя бы один из входящих значений равен 1. Как это применяется? Ну вот например, у нас есть некоторое число 01010011. И мы хотим превратить 3 бит из 0 в 1. Для этого мы применяем побитовую маску с ИЛИ: 00001000Сначала переведем числа в шестнадцатеричную чтобы написать код - 01010011 = 0x00000053 (или просто 0х53), а 00001000 = 0x00000008 (0х8), а затем выполним код:
Код:
mov eax, 0x53
or eax, 0x8
Мы должны получить результат 0x0000005b (0x5b), что соответствует двоичному 1011011. Теперь смотрим:
| Значения |
|---|
| 01010011 |
| 00001000 |
| 01011011 |
Вот та самая единичка, которую мы "интегрировали" в исходное значение. Все сходится.
Практика 17.
Напиши программу, которая принудительно изменяет значение 0 и 31 бита, сохраняя при этом все остальные.Команда XOR
xor - команда исключающего побитового ИЛИ. Если and можно применять как фильтр, сбрасывающий биты кроме указанных, or - устанавливающий биты в нужное положение, то xor - инвертор битов. Если сравниваемые значения одинаковые, она возвращает ноль. Если разные - единицу. Мы уже видели выше примеры использования команды в качестве обнулятора. Давайте теперь посмотрим еще на примере простейшего шифровальщика (или упаковщика) - двойного инвертирования. Допустим, хотим зашифровать число 0xb74a (в двоичной1011011101001010) ключом 0x774a3 (1110111010010100011)
Код:
mov eax, 0x0000b74a
mov ebx, 0x000774a3
xor eax, ebx
xor ebx, eax
Мы должны после первого xor получить 0x0000c3e9, а после второго - исходное значение eax. Причем что интересно, если мы поменяем их местами, то мы сможешь получить исходное значение ebx. Таким образом мы можем хранить два значения в одном (или больше). Я думаю вам здесь на ум невольно приходят аналогии - больно похоже на всякие хеш-мапы из высокоуровневых языков, да? И не зря приходят. Ведь именно так они и работают.
Практика 18.
Напиши программу, которая инвертирует только младший байт регистра EBX, а затем, сохраняет в память.Оператор NOT можно вставить в этот же раздел, так как он выполняет похожую функцию инвертирования, только безусловно - он просто инвертирует данное ему значение. Например
Код:
mov ebx, 0x0000004a ; 0100 1010 в двоичной (помним что ebx - 32 разрядный регистр, то есть по сути это `0000 0000 0000 0000 0000 0000 0100 1010`)
not ebx
О, видите в самом старшем разряде f? При использовании знака можно интерпретировать число как отрицательное! Кстати, процессор именно так и делает:
Код:
mov ebx, 0x00000005
not ebx
Код:
mov ebx, 0x00000005
not ebx ; получили отрицательное число
not ebx ; инвертировали его обратно
inc ebx ; получили число, к которому можем мысленно подставить знак `-`.
Код:
mov ebx, 0x00000005
not ebx ; получили отрицательное число
inc ebx ; добавили единицу к инвертированному числу (отняли у неинвертированного)
not ebx ; инвертировали его обратно
inc ebx ; добавили единицу и получили требуемое число, к которому можем мысленно подставить знак `-`.
Код:
mov ebx, 0x00000005
neg ebx ; получим 0xfffffffb (0xfffffffa + 1)
Практика 19.
Загрузи в EDX значение 0x0000FFFF. Выполни команду NOT. Объясни, почему результат не равен простому «минусу» в обычном понимании.Циклические сдвиги ROL, ROR, RCL, RCR
Циклические сдвиги напоминают собой операции сдвига вправо (shr) или влево (shl), упомянутых ранее в разделе про арифметические операции, только сдвигаемые старшие биты не исчезают, а возвращаются на позицию самого младшего.Операции rol и ror представляют собой простые циклические сдвиги. Например
| Сдвиг | Значения |
|---|---|
| rol -> | 001001001 |
| orig | 010010010 |
| ror <- | 100100100 |
Код:
mov ebx, 0x92
rol ebx, 1
Так как надо где то хранить сдвигаемый бит, чтобы потом записать его вначало, данные команды используют системный регистр CF - сдвигаемый старший бит заходит в него и как бы там "хранится" до востребования.
Практическое применение циклических сдвигов - хэширование и шифрование. Сдвигая биты на определенное значение, мы перемешиваем данные так, что структура исходных данных полностью теряется, но при этом сами данные - не терялись, как в случае обычных сдвигов. Более того, это позволяет хранить кучу информации в одном значении, поэтапно ее извлекая путем сдвига битов на заданный размер.
Например, мы можем в одном регистре хранить сразу несколько более малых чисел таким образом, компактизируя их (чем то напоминает стек, ага?). На практике это очень популярно в использовании при передаче данных в видеопамять. Например в движке на котором я делаю игру, есть строгое ограничение на передачу за раз только 48 байт. И допустим у меня есть три обычных float, которые я могу упаковать во float3 и таким образом передав больше информации за раз.
Код:
mov eax, 0xbb
mov ebx, 0x11
mov ecx, 0x22
mov dl, al
rol edx, 8
mov dl, bl
rol edx, 8
mov dl, cl
rol edx, 8
Код:
mov eax, 0xbb
mov ebx, 0x11
mov ecx, 0x22
mov dl, al
rol edx, 8
mov dl, bl
rol edx, 8
mov dl, cl
rol edx, 8
ror edx, 16
mov [0x1050], dl
xor dl, dl
rol edx, 16
Если эти вышеперечисленныех команды rol и ror просто перезаписывают значение во флаге CF по кругу начиная с самого начала операции (им неинтересно, что там хранилось до этого), то операции циклического сдвига через перенос rcl и rcr используют его значение сразу, добавляя его в свой "расширенный цикл" как добавочный бит. То есть, они сначала "смотрят" что там во флаге уже есть, и копируют этот бит в соответствующую ячейку (смотря куда крутим). Таким образом в циклах rcl и rcr крутится 9 бит, а не 8.
Практическо применение этих ротируемых расширенных циклов - межрегистровые операции. Например, если необходимо сдвинуть 64-битное число, хранящееся в двух 32-битных регистрах, то обычный сдвиг приведет к потере крайнего бита. Прокручивание позволяет "прогнать" бит который некуда деть, через системный регистр.
Код:
shl eax, 1 ; Сдвигаем младшую часть, старший бит уходит в CF
rcl edx, 1 ; Сдвигаем старшую часть, забирая бит из CF в начало