Погружение в ассемблер. Работаем с большими числами и делаем сложные математические вычисления — «Хакер»

Найти:

- Взлом
- Приватность
- Трюки
- Кодинг
- Админ
- Geek
- Обучение
- Магазин

- Windows
- Linux
- Android
- Железо
- Мессенджеры
- Утечки
- Коронавирус
- Все выпуски «Хакера»

### Содержание статьи

- Другие статьи курса
- Простейшие операции над 32-битными числами
- Реализуем операцию умножения двух 32-битных чисел
- Знакомимся с графическим видеорежимом
- Рисуем множество Мандельброта: подготовительные шаги
- Краткий ликбез по работе со стеком
- Рисуем множество Мандельброта: делаем вычисления
- Усовершенствованная подпрограмма умножения 32-битных чисел
- Выводы

Как ты знаешь, регистры процессора 8088 — 16-битные. Однако при необходимости ты можешь работать через эти регистры не только с 16-битными числами, но и с числами большей разрядности: и с 32-битными, и даже более крупными. В этой статье я сначала расскажу как, а затем мы нарисуем знаменитый фрактал — множество Мандельброта.

### Другие статьи курса

- Зачем учить ассемблер в 2020 году
- Делаем первые шаги в освоении асма
- Осваиваем арифметические инструкции
- Как работают переменные, режимы адресации, инструкции условного перехода
- Учимся работать с памятью
- Сокращаем размер программы
- Пишем клон игры Flappy Bird, который уместится в бутсектор
- Пишем бейсик и умещаем его в 512 байт

## Простейшие операции над 32-битными числами

Сразу возникает вопрос: если регистры у нас 16-битные, то как с их помощью обрабатывать 32-битные числа? Ответ очевиден: мы просто будем задавать каждое число не одним регистром, а сразу двумя.

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

У 8088 есть инструкция `mul`, которая умножает `AX` на 16-битный регистр и кладет результат в `DX:AX`. Также у него есть инструкция `div`, которая делит `DX:AX` на 16-битный регистр; результат попадает в регистр `AX`, а остаток — в `DX`. Еще у 8088 есть инструкция `cwd`. Она конвертирует знаковое 16-битное число из регистра `AX` в 32-битное число `DX:AX`.

Давай и мы, по примеру этих трех инструкций, тоже будем хранить 32-битные числа в `DX:AX` (в `DX` старшее слово, в `AX` — младшее). Но чтобы выполнять арифметические операции, нам нужно еще одно 32-битное число. Его, по аналогии с первым, будем хранить в `CX:BX` (в `CX` старшее слово, в `BX` — младшее).

Ну вот, мы с тобой условились, где и как хранить 32-битные числа. Теперь давай реализуем для них операцию сложения и операцию вычитания. Для этого нам пригодятся инструкции `adc` и `sbb`. Вот так выглядит сложение.

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

Инструкция `adc dx, cx` выполняет вот такую операцию: `DX = DX + CX + перенос`, то есть прибавляет к итоговому результату то значение, которое хранится во флаге переноса.

Теперь давай реализуем вычитание 32-битных чисел по такому же принципу.

Что тут происходит? Инструкция `sub` вычитает из одного числа другое, а еще изменяет флаг переноса. Когда операция вычитания делает «заем» из соседнего разряда, флаг переноса устанавливается в единицу.

Инструкция `sbb dx, cx` выполняет вот такую операцию: `DX = DX – CX – перенос`, то есть вычитает из итогового результата то значение, которое хранится во флаге переноса.

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

Чтобы сделать логическое инвертирование 32-битного числа (`not`), нам надо просто переключить все биты числа на противоположные.

Если требуется выполнить арифметическое инвертирование (`neg`), то есть поменять знак числа, нужно сделать то же самое, но только прибавить единицу к результату.

## Реализуем операцию умножения двух 32-битных чисел

А теперь давай реализуем операцию умножения. Это уже будет посложнее. Здесь нужно вспомнить то, что ты изучал в начальной школе.

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

Поочередно, справа налево, умножаем каждую цифру множимого на вторую цифру множителя. Так у нас получается первая строчка промежуточного результата. Затем умножаем каждую цифру множимого на первую цифру множителя. Так у нас получается вторая строчка промежуточного результата. Затем сдвигаем вторую строчку на один разряд влево и суммируем два промежуточных результата.

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

Но это если «в роли цифры» у нас выступают цифры от `0` до `9`. Однако, зная, что у процессора 8088 есть инструкция для умножения 16-битных чисел, мы для удобства можем в своем алгоритме умножения «назначить на роль цифры» сдвоенный байт. То есть будем считать значения вроде 0x6725 и 0x1561 не числами, а цифрами!

Почему это удобнее? Потому что для умножения двух 32-битных чисел (по две 16-битные цифры на каждое) нам понадобится всего четыре инструкции умножения. Тогда умножение двух 32-битных чисел можно будет реализовать вот так.

Умножение, конечно, выглядит сложновато по сравнению со сложением и вычитанием. Но не переживай, сейчас все объясню. Здесь весь алгоритм разделен на четыре операции умножения: по одной на каждое 16-битное слово. Точно так же, как на рисунке с умножением в столбик.

Кстати, если такой же алгоритм реализовывать на 32-битном процессоре, его можно расширить до операций над 64-битными числами, а если на 64-битном процессоре, то над 128-битными числами.

Но давай вернемся к нашему 16-битному алгоритму. Обрати внимание, здесь под результат отводится только 48 бит. А это значит, что если умножить, допустим, `0xFFFFFFFF` на `0xFFFFFFFF`, то старшие два байта потеряются. Чтобы они не терялись, нужно 64 бита, а не 48. Можешь в качестве домашнего задания доделать функцию — чтобы она возвращала 64-битный результат.

## Знакомимся с графическим видеорежимом

Мы реализовали пять операций для работы с 32-битными числами: сложение, вычитание, логическую инверсию, арифметическую инверсию, умножение. Давай скорее применим их на какой-нибудь интересной задаче!

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

До сих пор мы работали только в текстовом режиме (80 × 25), а сейчас поработаем в графическом режиме, с разрешением 320 на 200 пикселей и с 256 цветами. В этом режиме видеопамять экрана расположена по адресу `0xA0000–0xAFFFF`. Как к ней получить доступ? Просто загрузи в регистры `DS` и `ES` значение `0xA000`.

А теперь зададим положение текущего пикселя на экране.

Дальше вычисляем адрес очередного пикселя для рисования.

Теперь у нас есть адрес текущего пикселя (`0–63 999`, или `0x0000–0xF9FF`).

Обрати внимание на инструкцию `xchg`. Она у нас здесь меняет значение двух регистров: `AX` и `DI`. Но то значение, которое теперь записано в регистре `DI`, нам не нужно. Нам надо только поместить в `DI` значение `AX`. Зачем же тогда нам `xchg`? Почему бы не написать простой `mov`? Потому что `xchg` позволяет сэкономить один байт. Инструкция `mov di, ax` использует два байта, а `xchg` — один. `xchg` всегда экономит нам байт, когда один из операндов — это `AX`, а другой — еще какой-то 16-битный регистр.

Теперь вычислим цвет текущего пикселя и выведем его на экран. Причем вычислим так, чтобы цвета выводились квадратиками 16 × 16 пикселей.

Что тут делаем? Берем по четыре бита от двух координат и на их основе высчитываем цвет в диапазоне от `0` до `255` (два раза по четыре бита — это восемь бит; вот и получается нужный диапазон). Вычислив текущий цвет, выводим пиксель на экран. Выводим по адресу, на который указывает регистр `DI`. Этот адрес мы вычислили на предыдущем шаге.

А дальше просто закручиваем двойной цикл, который пробегает по всем столбцам и строкам и рисует пиксели нужного цвета, чтобы в итоге получилась палитра.

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

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

Например, левый верхний черный квадратик нарисован цветом `0x00`, а верхний правый квадратик белого цвета — `0x0F`.

## Рисуем множество Мандельброта: подготовительные шаги

Теперь, когда ты узнал, как работать с большими 32-битными числами и как рисовать пиксели в графическом режиме, давай напишем программу посложнее — будем рисовать множество Мандельброта.

Вот алгоритм, который мы с тобой сейчас реализуем.

Или, если ты знаком с С, вот аналогичный сишный код.

Давай реализуем этот алгоритм на ассемблере.

Обрати внимание: `fractX` и `fractY` — это 32-битные дробные числа. В них первые 24 байта хранят целую часть числа, а оставшиеся восемь — дробную. Например, `1.0` сохраняется как `0x00000100` (или `256` в десятичной системе счисления).

Имей в виду, что произведение двух таких дробных чисел удваивает количество битов, отведенных под дробную часть, и поэтому результат надо обязательно поделить на `256`. Например, `0x0100 * 0x0100 = 0x010000`. Но после деления на `256` результат будет выглядеть корректно: `0x0100 (1.0 * 1.0 = 1.0)`.

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

Что мы тут делаем? Директивой `cpu` просим компилятор проконтролировать, что мы используем инструкции только из набора 8086 (или его собрата — 8088).

Затем задаем адреса для нужных нам переменных. Обрати внимание, что переменные `fractX` и `fractY` занимают по четыре байта, а не по два, как остальные. А промежуточная переменная `dest48bit` вообще занимает шесть. Она нам нужна для реализации алгоритма умножения 32-битных чисел.

Затем переходим в графический режим 320 × 200 × 256 и нацеливаем сегментные регистры на видеопамять экрана.

Снова, как и в случае с программой-палитрой, начинаем рисовать от правого нижнего пикселя экрана. Его координаты `(xScreen, yScreen)` такие: `(319, 199)`. Смотри не спутай `xScreen` и `yScreen` с `fractX` и `fractY`, которые нужны не для рисования графики, а для сопутствующих математических вычислений.

Теперь реализуем первый шаг алгоритма: зададим начальные значения для `fractX` и `fractY`, а также для счетчика итераций.

Обрати внимание: для инициализации переменных `fractX` и `fractY` мы используем по две инструкции `mov`, потому что эти переменные 32-битные.

## Краткий ликбез по работе со стеком

Начальные значения заданы, теперь проверим условие выхода.

Что мы тут делаем? Во-первых, активно используем стек. До сих пор я не рассказывал, в каком порядке данные хранятся в стеке, но теперь пришло время разобраться в этом.

Начну с объяснения на пальцах, а уже потом познакомлю тебя с формальным определением.

Представь, что ты кладешь тарелку на стопку тарелок. Как раз так и работает `push`. А теперь представь, что ты снимаешь верхнюю тарелку со стопки тарелок. Так работает инструкция `pop`.

Такой способ хранения данных называется LIFO (last in first out, пришел последним — уйдет первым). Как этот принцип отражается на нашей программе? Мы кладем на стек сначала `DX`, потом `AX` (результат операции `x^2`). Потом первая инструкция `pop bx` снимает сохраненное значение `AX` и кладет его в `BX`, а вторая инструкция `pop bx` снимает сохраненное значение `DX`.

Более сложное и точное объяснение звучит так. Инструкции `push`, `pop`, `call` и `ret` отталкиваются от регистра `SP` (stack pointer — указатель стека). `SP` указывает на текущую ячейку в сегменте стека. Адрес сегмента стека хранится в регистре `SS`.

Всякий раз, когда ты кладешь данные на стек, используя инструкцию `push`, регистр `SP` уменьшается на `2`, а данные (сдвоенный байт) записываются в память по адресу, на который указывает регистр `SP`.

Каждый раз, когда ты снимаешь данные со стека, используя функцию `pop`, данные (сдвоенный байт) считываются с адреса, который указан в регистре `SP`, а `SP` увеличивается на 2.

## Рисуем множество Мандельброта: делаем вычисления

Идем дальше. Пишем тело основного цикла. Сначала вычисляем значение `t = x^2 – y^2 + i * x` (смотри чуть выше картинку с алгоритмом).

Обрати внимание, как я тут прибавляю к `xScreen` 32-битное значение. Поскольку инструкция `add` работает с 16-битными числами, я добавляю инструкцию `adc dx, 0` — для завершения операции сложения.

Теперь вычислим значение `y = 2x * y + i * y` (смотри чуть выше картинку с алгоритмом).

Обрати внимание на комбинацию двух операций: `shl ax, 1` и `rcl dx, 1`. Вместе они реализуют 32-битное умножение на два (через сдвиг влево). Первая инструкция сдвигает `AX` на один бит влево и подставляет справа ноль. После выполнения этой инструкции тот бит, который вытеснился слева, попадает во флаг переноса. Вторая инструкция (`rcl`) сдвигает `DX` влево и подставляет на освободившееся справа место бит из флага переноса.

Дальше готовимся к следующей итерации цикла.

Что мы тут делаем? Вначале снимаем со стека какое-то значение. Помнишь, что там хранится? Там сейчас хранится новое значение для переменной `fractX`! Если не понимаешь, как оно там оказалось, вернись назад и найди строку `sub ax, 480`. После этой инструкции мы положили на стек 32-битное значение. Вот его мы сейчас и снимаем со стека.

Обрати внимание, мы не можем записать это значение сразу в `fractX`, потому что предыдущее значение `fractX` нам еще нужно — для вычисления координаты `fractX`.

После этого увеличиваем счетчик итераций (`CX`) и сравниваем его с числом 100. Когда он достигает ста, выходим из цикла, а иначе переходим к следующей итерации (прыгаем на `@@nextIter`).

А теперь самое интересное! До сих пор мы только делали математические расчеты и ничего не рисовали на экране. Пришло время порисовать. Рисуем!

Что мы тут делаем? Почти то же самое, что и в программе-палитре. За исключением того, что к значению `CL` (0–99) добавляем число 32 — чтобы сразу попасть на цвета радуги, а не на серые.

С множеством Мандельброта закончили! Но мы еще не написали подпрограмму для 32-битного умножения. По сравнению с тем вариантом, который мы разбирали вначале, здесь есть несколько доработок. Подпрограмма теперь принимает дробные числа со знаками.

## Усовершенствованная подпрограмма умножения 32-битных чисел

Вот ее листинг с комментариями.

Рекомендую поэкспериментировать со значениями смещения, которые мы задали для центровки изображения (480 и 300). И попробуй удалить дублированные инструкции add для `yScreen` и `xScreen`, а еще поменять количество итераций. Сейчас установлено значение 100. Можешь попробовать задать значение больше или меньше.

## Выводы

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

#### Оцени статью:

###### Что тебе понравилось больше всего?

Статья легкая и понятная Статья глубокая и подробная Интересуюсь этой темой Данные новые и актуальные Приятный стиль изложения

###### Что тебе не понравилось больше всего?

Статья поверхностная Статья слишком сложная Тема мне не интересна Данные устарели Неприятный стиль изложения
← Ранее Четыре пакета npm собирали данные пользователей
Далее → Эксперты CyberArk Labs выявили баги в популярных антивирусах
Открыть комментарии

#### Последние взломы

### Краденые миллиарды и плохой рэп. Как арестовали супругов, которые отмывали деньги Bitfinex и снимали тиктоки

### HTB EarlyAccess. Эксплуатируем веб-уязвимости и мисконфиги Docker

### HTB Horizontall. Захватываем веб-сервер через уязвимости в Strapi и Laravel

### Препарируем P-Code. Как реверсить старый софт на Visual Basic

#### Компьютерные трюки

### Терминальное счастье. Колонка главреда

### Анатомия терминала. Осваиваем текстуальную графику

### Змей-стражник. Сигнализация для велосипеда из старого смартфона на Android

### M1 для хакера. Тестируем Homebrew, виртуалки, Docker и Node на новой архитектуре Apple

#### Свежие новости

- 2 дня назад

  ### Для критической уязвимости в Magento появился эксплоит, а Adobe исправила второй аналогичный баг

- 2 дня назад

  ### Вредоносные письма могут вывести Cisco Email Security Appliance из строя

- 2 дня назад

  ### WordPress-плагин с 3 000 000 установок позволял всем желающим скачивать бэкапы сайтов

- 3 дня назад

  ### Француз случайно оставил без интернета целый город, пытаясь отключить интернет своим детям

- 3 дня назад

  ### Google представила «легкую» Chrome OS Flex, подходящую для старых ПК

Вопросы по материалам и подписке: support@glc.ru
Отдел рекламы и спецпроектов: yakovleva.a@glc.ru
Контент 18+
Сайт защищен Qrator —
самой забойной защитой от DDoS в мире
Подписка для физлиц
Подписка для юрлиц
Реклама на «Хакере»
Контакты
Posting....
wpDiscuz