Как писать на машинном коде
Перейти к содержимому

Как писать на машинном коде

  • автор:

1.7. Машинный код

Машинный кодилимашинный язык– это язык, на котором записываются программы для данной машины или данного семейства машин. Этот код определяется набором кодов операций и форматом команд процессора, типом адресации ячеек, форматом представления данных в памяти и пр.

Для примера рассмотрим программу, которая выводит на консоль текст «Hello,world!», для процессора архитектуры x86 фирмыIntelc16-разрядной адресацией. В этой архитектуре адресуемой ячейкой памяти является каждый 8‑разрядный байт. Поле КОП команды занимает первый байт (т.е. допускается не более 2 8 = 256 различных команд), поле адреса – 2 байта (т.е. можно адресовать 2 16 байтов = 65536 байтов = 64 КБ – длинный адрес) или 1 байт (можно адресовать только 256 байт – короткий адрес).

Первая команда этой программы занимает 3 байта и выглядит в двоичном коде следующим образом:

Для удобства чтения программ программисты записывают их в шестнадцатеричном(16-ричном) коде, заменяя каждую четвёрку битов одной 16-ричной цифрой (всего этих цифр 16), как показано в следующей таблице:

16-ричная цифра

Двоичное число

Десятичное число

В любой системе счисления число, следующее за последней цифрой, равно 10, т.е. изображается двумя цифрами (см. в таблице 102, 1010и 1016).

Таким образом, эта команда в 16-ричной системе имеет вид ВВ 11 01. Байты 11 01 образуют здесь двухбайтовое поле адреса. Адрес, указанный в этом поле, – 011116, а не 110116. Чтобы записать правильный адрес, нужно поменять местами байты адресного поля! В десятичной системе этот адрес равен 273.

Вся программа в 16-ричном коде выглядит следующим образом (за программой последует её комментарий):

Адрес первого байта

Команды и данные 25

8A 07

CD 10

E2 F9

48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21

Программа размещена, начиная с ячейки с адресом 010016, и состоит из 8 команд, занимающих первые 17 байтов. После команд в неё включена выдаваемая строка текста «Hello,world!» – 13 символов (включая пробел). Каждый символ представлен своим 8‑разряднымASCII-кодом (см. любую таблицу символов). Вся строка занимает 1310=D16байтов:

Таким образом, вся программа с данными занимает 30 байтов.

Команды с номерами 4 – 7 (выделены жирным шрифтом) повторяются столько раз, сколько символов в выводимой строке (13 раз). Это – цикл.

Комментарии к программе

В программе для передачи данных используются регистры: 8-разряд­ные – AL,AH(это байты 16-разрядного регистра АХ), 16‑разрядные –BX,CX. РегистрыCXиIP(адрес следующей команды) используются для организации цикла. Команды «знают», какие регистры они используют.

Вывод символа на консоль осуществляется операционной системой (BIOS) с помощью прерывания с номером 1016. Останов программы (возврат управления операционной системеMSDOSпосле завершения программы) осуществляется с помощью прерывания 2016. Команда останова необходима, иначе процессор начнёт следующий байт (4816– символ «Н») рассматривать как код операции, и это может привести к тяжёлым последствиям.

Команды и данные

Комментарии

Поместить в BX адрес строки (байт с символом «Н» становится текущим)

Поместить в CX длину строки (000D)

Поместить в AH номер функции 0E16 прерывания 1016

8A 07

Поместить в AL значение ячейки памяти, адрес которой находится в BX (т.е. текущий символ)

CD 10

Вызов прерывания 1016, которое с помощью регистра АХ выведет текущий символ

Увеличить значение ВХ на 1 (сделать текущим следующий байт)

E2 F9

Если CX≠0, то уменьшить CX на 1 и увеличить IP (равный 010F) на F9 (равносильно уменьшению на 7, т.к. F9 – дополнительный код числа -7), т.е. перейти по адресу 010816 – на начало цикла

Вызвать прерывание 2016: завершить программу

48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21

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

Поэтому уже в начале 1950-х годов стали появляться языки программирования, позволявшие записывать машинные команды и данные не в цифровом виде, в символическом, используя мнемонические имена команд, регистров, ячеек памяти. Эти языки называют языками символического кодирования.Для них создавались программы-трансляторы, преобразующие команды из символической записи в цифровую. Эти трансляторы, а вместе с ними и языки символического кодирования, назывались сначалаавтокодами, а позже ­ассемблерами(по-русски – «сборщиками»). Появление таких языков и трансляторов стало возможным лишь на машинах с архитектурой фон Неймана благодаря принципу хранимой программы (они и были созданы для первых таких машин 26 ).

Приведём код той же программы на языке ассемблера:

mov bx, HW ; move — переместить

L: mov al, [bx]

int 10h ; interruption — прерывание

inc bx ; increment – увеличить на 1

loop L ; loop — цикл

HW: db ‘Hello, World!’

Кроме расшифровки смысла команд, здесь происходит автоматическое определение адресов команд и данных с помощью меток (LиHW). В целом, использование языков символического кодирования увеличило производительность программистов на порядок, сохраняя возможность использования всех особенностей архитектуры вычислительной системы. Таким образом, языки символического кодирования и программы-трансляторы автокоды и ассемблеры стали первым шагомавтоматизации программирования– одной из важнейших составляющих технологии программирования.

Однако, языки ассемблера и написанные на них программы привязаны к конкретной архитектуре вычислительной системы и не годятся для других систем. Кроме того, производительность программистов на ассемблере слишком низкая для написания того объёма программного кода, который требуется в современных условиях. Для удовлетворения этой потребности разрабатываются языки программирования высокого и сверхвысокого уровня – следующие шаги автоматизации программирования. На таких языках предыдущая программа превращается в один простой оператор:

Машинный код и компиляция в него — это как?

Хочу поработать с этим, потому что всегда мечтал попробовать собрать .exe самостоятельно, без помощи готовых компиляторов.

eanmos's user avatar

Baremetal

Каждый конкретный процессор (например, Intel Core i3-4160 или ARM Cortex-A9) имеет свою микроархитектуру и реализует архитектуру уровня набора команд (англ. instruction set architecture).

Микроархитектура определяет структуру процессора на уровне электронных компонентов и логических вентилей.

Архитектура уровня набора команд (ISA), грубо говоря, определяет то, какие команды может выполнять процессор. Эта архитектура абстрагированна от микроархитектуры. Процессоры разных комнаний могут реализовывать одну и ту же архитектуру (например, многие процессоры Intel и AMD реализует одно и то же семейство архитектур x86).

Если два процессора реализуют одну и ту же ISA, то они могут исполнять одни и те же программы. ISA определяет, какие команды доступны программисту, какие регистры он может использовать, как он может использовать страничную адресацию, виртуальную память и т. д. Кроме того, она определяет формат команд, которые понимает процессор.

Каждая программа процессора — это просто набор подряд идущих команд. При своем запуске процессор выбирает команду из память по адресу, называемому вектором сброса (англ. reset vector) и начинает исполнять эту программу, пока питание не будет отключено.

Написать программу в машинных кодах достаточно просто — нужно лишь взять справочник по ISA (например, Intel 64 and IA-32 Architectures Software Developer Manuals), которую реализует ваш процессор и написать нужные команды байт за байтом.

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

Например, одна инструкция ассемблера x86 MOV может кодировать около 20 различных инструкций процессора MOV 1 . Ассемблер читает вашу программу на языке ассемблера и переводит ее в бинарный файл 2 , который, опять же, является просто последовательность байт, кодирующих подряд идущие инструкции процессора.

Вот так может выглядет отрывок программы на языке ассемблера:

Вот так выглядит программа на машинном языке:

Очевидно, что асссемблерный код и читать, и писать проще.

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

Операционная система

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

Как я уже сказал, каждая программа процессора — это просто последовательность команд, однако каждая программа операционной системы — это особая последовательность байт, имеющая специальную структуру, в которую входят не только команды процессора.

Если брать в пример ОС Windows 10, она работает с исполняемыми файлами .exe , которые имеют специальный формат, называемый Portable Executable. Он имеет довольно сложную структуру. Помимо собственно набора машинных команд он содержит в себе информацию необходимую для определения адреса и размера секций, таблиц импорта и экспорта, специальную сигнатуру и т. д.

Поэтому чтобы вручную написать программу в машинных кодах, которая будет запускаться в Windows 10, например, нам, по-мимо написания самой программы, потребуется привести ее к формату Portable Executable.

Но и этого будет не достаточно. Нам придется ознакомится с соглашениями, которые называются ABI и написать программу в машинных кодах, используя именно эти соглашения, а не какие-то другие.

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

Можете начать с написания программ на языке ассемблера (да, вам придется еще выучить синтаксис конкретного языка ассемблера и диалект Intel или AT&T). «Hello, World» на языке NASM будет выглядеть так:

А нужно ли вам это?

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

На самом деле, умение писать на языке ассемблера (а тем более, на машинном языке) довольно бесполезно. Умение просто читать и понимать ассемблерный листинг гораздо более практично и действительно может вам пригодится.

А непрактично это в первую очередь потому, что ничего сложнее «Hello, World!» в машинных кодах вы не напишете. На ассемблере — да, напишете, но потратите на это колоссальное количество времени, которое можно было бы потратить на более полезные вещи.

1. Что интересно, инструкция MOV в x86 является Тьюринг-полной, т. е. любая программа может быть написана с использованием одной только этой инструкции. Есть даже специальный компилятор, который использует только одну эту инструкцию.

2. Некоторые ассемблеры могут сразу формировать исполняемые файлы в нужном формате. В том числе и Portable Executable.

Знакомимся с программированием на ассемблере x86

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

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

В этом руководстве я помогу вам сформировать устойчивое понимание x86 ISA на основе базовых принципов. Здесь мы сосредоточимся на построении понятной ментальной модели происходящего, не акцентируясь на деталях (что заняло бы много времени и оказалось скучным чтивом). Если вы хотите в итоге применять эти знания, то лучше будет заиметь под рукой список инструкций ЦПУ, а также параллельно изучать какое-нибудь другое руководство, которое научит писать и компилировать простые функции. В отличие от другой документации, которая обычно вываливает на вас всю информацию скопом, свой урок я начну с уже знакомой всем территории и буду постепенно повышать его сложность.

Для понимания содержания статьи вам потребуется навык работы с двоичными числами, некоторый опыт в программировании на императивном языке (С/С++/Java/Python/ и т.д.), а также понимание принципа работы указателей памяти (С/С++). При этом знать внутреннее устройство ЦПУ или иметь опыт работы с ассемблером не обязательно.

Содержание

  1. Инструменты и тестирование
  2. Базовая среда выполнения
  3. Базовые арифметические инструкции
  4. Регистр флагов и операции сравнения
  5. Работа с памятью
  6. Переходы, метки и машинный код
  7. Стек
  8. Соглашение о вызовах
  9. Повторяемые строковые инструкции
  10. Плавающая точка и SIMD
  11. Виртуальная память
  12. 64-битный режим
  13. Сравнение с другими архитектурами
  14. Обобщение
  15. Дополнительные материалы

1. Инструменты и тестирование

Параллельно с чтением будет полезно также писать и тестировать ваши собственные программы ассемблера. Проще всего это делать под Linux (менее удобно под Windows). Вот образец функции на ассемблере:

Сохраните ее в файле my-asm.s и скомпилируйте командой gcc -m32 -c -o my-asm.o my-asm.s . Пока что выполнить этот код у нас возможности нет, потому что для этого потребуется либо взаимодействие с программой Си, либо написание шаблонного кода для взаимодействия с ОС для обработки начала/вывода/остановки/и т.д. По меньшей мере, возможность скомпилировать код дает нам способ убедиться в синтаксической верности наших программ ассемблера.

Имейте ввиду, что в моем руководстве используется синтаксис AT&T, а не Intel. Отличаются они только нотацией, внутренние же принципы работы остаются одинаковыми. При этом всегда можно механически перевести программу из одного синтаксиса в другой, так что беспокоиться особо не о чем.

2. Базовая среда выполнения

В ЦПУ х86 есть восемь 32-битных универсальных регистров. По историческим причинам они имеют следующие названия: . (В других архитектурах ЦПУ они называются просто r0, r1, …, r7 ). Каждый из них может содержать любое 32-битное целочисленное значение. Вообще, в архитектуре x86 есть более сотни регистров, но мы разберем только необходимые нам.

Если говорить в общих чертах, то ЦПУ последовательно выполняет набор инструкций в порядке, указанном в исходном коде. Чуть позже мы увидим, как код может сойти с линейного маршрута, когда будем разбирать такие принципы, как if-then , циклы и вызовы функций.

По факту мы имеем восемь 16-битных и восемь 8-битных регистров, являющихся частью восьми 32-битных универсальных регистров. Эти элементы происходят из 16-битной эпохи процессоров x86, но все еще иногда применяются в 32-битном режиме. 16-битные регистры называются и представляют младшие 16 бит соответствующих 32-битных регистров (префикс e означает «расширенный»). 8-битные регистры именуются и представляют младшие и старшие 8 бит регистров . Когда значение 16-битного или 8-битного регистра изменяется, старшие биты, принадлежащие полному 32-битному регистру остаются неизменными.

3. Базовые арифметические инструкции

Основные арифметические инструкции x86 оперируют с 32-битными регистрами. Первый операнд выступает в качестве источника, а второй в качестве источника и точки назначения. Например: addl %ecx, %eax – в нотации Си означает: eax = eax + ecx; , где eax и ecx имеют тип uint_32 .

Этой важной схеме следуют многие инструкции, например:

  • xorl %esi, %ebp означает ebp = ebp ^ esi; .
  • subl %edx, %ebx означает ebx = ebx — edx; .
  • andl %esp, %eax означает eax = eax & esp; .

    notl %eax означает eax =

Например: shll %cl, %ebx означает ebx = ebx << cl; .

Многие арифметические инструкции могут получать в качестве первого операнда непосредственное значение. Это значение является фиксированным (не переменным) и кодируется в саму инструкцию.

Непосредственные значения сопровождаются приставкой $ . Вот примеры:

  • movl $0xFF, %esi означает esi = 0xFF; .
  • addl $-2, %edi означает edi = edi + (-2); .
  • shrl $3, %edx означает edx = edx >> 3; .

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

  • Нельзя складывать две непосредственные константы, хотя в Си это допускается. В ассемблере мы либо вычисляем значение во время компиляции, либо выражаем его как последовательность инструкций.
  • В одной инструкции можно сложить содержимое двух 32-битных регистров, но нельзя сложить значения трех – потребуется разбить такую инструкцию на две.
  • Нельзя прибавлять содержимое 16-битного регистра к содержимому 32-битного. Нужно будет написать одну инструкцию для выполнения преобразования из 16 в 32 бита, и еще одну для выполнения сложения.
  • При выполнении битового сдвига количество сдвигов должно быть либо жестко прописанным непосредственным значением, либо задаваться регистром cl . Если количество сдвигов находилось в другом регистре, тогда это значение нужно сначала скопировать в cl .

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

4. Регистр флагов и операции сравнения

Среди прочих, нам доступен 32-битный регистр eflags , который неявно считывается или записывается во многих инструкциях. Другими словами, его значение играет роль в выполнении инструкции, но сам этот регистр в коде ассемблера не упоминается.

Арифметические инструкции вроде addl обычно обновляют eflags на основе результата вычислений. Инструкция устанавливает или снимает флаги вроде переноса ( CF ), переполнения ( OF ), знаковый ( SF ), четности ( PF ), нуля ( ZF ) и т.д. Некоторые инструкции считывают эти флаги – например, adcl складывает два числа и использует флаг переноса в качестве третьего операнда: adcl %ebx, %eax означает eax = eax + ebx + cf; . Некоторые инструкции устанавливают на основе флага регистр – например, setz %al устанавливает 8-битный регистр al на 0 , если ZF неактивен, или на 1, если ZF установлен. Некоторые инструкции напрямую влияют на один бит флага, например cld , очищающая флаг направления ( DF ).

Инструкции сравнения влияют на eflags , не меняя никаких универсальных регистров. Например, cmpl %eax, %ebx выполнит сравнение значений двух регистров путем их вычитания в неименованной временной области и установит флаги согласно результату, что позволит вам в беззнаковом или знаковом режиме понять, является ли eax < ebx либо eax == ebx , либо eax > ebx . Аналогичным образом, testl %eax, %ebx вычисляет eax & ebx во временной области и устанавливает соответствующие флаги. В большинстве случаев инструкция, следующая за сравнением, является условным переходом (рассмотрим позже).

Пока что нам известно, что некоторые биты флагов относятся к арифметическим операциям. Другие биты флагов связаны с поведением процессора – принятием аппаратных прерываний, виртуальным режимом 8086 и прочими элементами управления системой, которые уже касаются разработчиков ОС, а не создателей приложений. По большей части, регистр eflags можно игнорировать. Системные флаги вполне допустимо опускать, как и арифметические флаги, за исключением сравнений и арифметических операций bigint .

5. Работа с памятью

Одного только процессора для эффективной работы компьютера будет недостаточно. Наличие всего 8 регистров данных сильно ограничивает объем вычислений ввиду невозможности хранения большого количества информации. Для увеличения вычислительного потенциала процессора у нас есть ОЗУ, представляющее обширную системную память. По сути, ОЗУ представляет собой огромный массив байт – например, 128МиБ ОЗУ – это 134,217,728 байт, в которых можно хранить значения.

При сохранении значения размером больше байта оно кодируется в прямом порядке байтов. Например, если 32-битный регистр содержит значение 0xDEADBEEF , и этот регистр нужно сохранить в памяти по адресу 10, тогда значение байта 0xEF отправляется в адрес ОЗУ 10 , 0xBE в адрес 11 , 0xAD в адрес 12 , а 0xDE в адрес 13 . То же правило работает и при считывании значений из памяти – байты из нижних адресов памяти загружаются в нижние части регистра.

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

  • movb (%ecx), %al означает al = *ecx; . (считывает байт по адресу памяти ecx в 8-битный регистр al ).
  • movb %bl, (%edx) означает *edx = bl; . (записывает байт из bl в байт по адресу памяти edx ).
  • (в типичном коде Си регистры al и bl имеют тип uint8_t , а ecx и edx приводятся из uint32_t в uint8_t* .)
  • addl (%ecx), %eax означает eax = eax + (*ecx); . (считывает из памяти 32 бита).
  • addl %ebx, (%edx) означает *edx = (*edx) + ebx; . (считывает и записывает 32 в память).

Режимы адресации

Когда мы пишем код с циклами, то нередко один регистр содержит базовый адрес массива, а другой текущий обрабатываемый индекс. Несмотря на то, что адрес обрабатываемого элемента можно вычислить вручную, x86 ISA предоставляет более элегантное решение – у нас есть режимы адресации памяти, которые позволяют складывать и перемножать содержимое определенных регистров.

Это будет проще показать, чем объяснять:

  • movb (%eax,%ecx), %bh означает bh = *(eax + ecx); .
  • movb -10(%eax,%ecx,4), %bh означает bh = *(eax + (ecx * 4) — 10); .

Режимы адресации памяти допустимы везде, где разрешен операнд памяти. Таким образом, если вы можете написать sbbl %eax, (%eax) , и вам нужна возможность индексации, то вы определенно можете написать sbbl %eax, (%eax,%ecx,2) . Также имейте ввиду, что вычисляемый адрес является временным значением, которое не сохраняется ни в каком регистре. Это хорошо, потому что если вы захотите вычислить этот адрес явно, то нужно будет выделить под него регистр, а наличие всего 8 универсальных регистров не позволяет особо разгуляться, когда вам нужно сохранять и другие переменные.

Есть одна специальная инструкция, которая использует адресацию памяти, но по факту к ней не обращается. Инструкция leal (загрузка действительного адреса) вычисляет заключительный адрес памяти согласно режиму адресации и сохраняет результат в регистре. К примеру, leal 5(%eax,%ebx,8), %ecx означает ecx = eax + ebx*8 + 5; . Заметьте, что это полностью арифметическая операция, которая не включает разыменовывание адреса памяти.

6. Переходы, метки и машинный код

Каждую инструкцию в ассемблере можно предварить нужным числом меток. Эти метки пригодятся, когда потребуется перейти к определенной инструкции. Вот несколько примеров:

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

Несмотря на то, что jmp условия не имеет, у нее есть родственные инструкции, которые смотрят на состояние eflags и переходят либо к метке (при выполнении условия), либо к очередной инструкции ниже. К инструкциям с условным переходом относятся: ja (перейти, если больше), jle (перейти, если меньше либо равно), jo (перейти, если переполнение), jnz (перейти, если не нуль) и так далее.

Всего таких инструкций 16, и у некоторых есть синонимы – например jz (перейти, если нуль) равнозначна je (перейти, если равно), ja (перейти, если больше) равнозначна jnbe (перейти, если не меньше или равно).

Вот пример использования условного перехода:

Адреса меток фиксируются в коде при его компиляции, но также можно переходить к произвольному адресу памяти, вычисляемому в среде выполнения. В частности, можно перейти к значению регистра: jmp *%ecx , по сути, означает «скопировать значение ecx в eip ».

Теперь самое время обсудить принцип, касающийся инструкций и выполнения, о котором я заикнулся еще в первом разделе. В ассемблере каждая инструкция в конечном итоге преобразуется в 1-15 байт машинного кода, после чего эти машинные инструкции компонуются вместе, образуя исполняемый файл. У процессора есть 32-битный регистр eip (расширенный указатель инструкции), который во время выполнения программы хранит адрес памяти текущей обрабатываемой инструкции. Имейте ввиду, что есть очень мало способов для считывания/записи регистра eip , в связи с чем он работает не так, как 8 основных универсальных регистров. При каждом выполнении инструкции процессор знает ее длину в байтах и продвигает eip на это число, чтобы он указывал на следующую инструкцию.

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

7. Стек

Стек – это область памяти, адресуемая регистром esp . В x86 ISA есть ряд инструкций для управления стеком. Несмотря на то, что всю эту функциональность можно реализовать посредством movl, addl, … и т.д. с помощью других регистров, подход с использованием инструкций стека оказывается более идиоматичным и кратким.

В архитектуре x86 стек растет вниз, от больших адресов памяти в направлении меньших. К примеру, добавление 32-битного значения в стек подразумевает уменьшение esp на 4 с последующим помещением этого 4-байтового значения в область памяти, начиная с адреса esp . Извлечение значения подразумевает обратные операции – загрузку 4 байтов, начинающихся с адреса esp (либо в заданный регистр, либо отбрасывание), и увеличение esp на 4.

Стек очень важен для вызовов функций. Инструкция call подобна jmp , за исключением того, что перед переходом она сначала помещает в стек адрес следующей инструкции. Таким образом, можно вернуться к выполнению инструкции retl , которая извлекает адреса в eip . Кроме того, стандартное соглашение о вызовах в Си помещает некоторые или все аргументы функций в стек.

Имейте ввиду, что память стека можно использовать для чтения/записи регистра eflags и считывания регистра eip . Обращаться к этим двум регистрам неудобно, поскольку они не могут быть использованы в типичной movl или в арифметических инструкциях.

8. Соглашение о вызовах

Когда мы компилируем код Си, он переводится в код ассемблера и в последствии в машинный код. Соглашение о вызовах определяет то, как функции Си получают аргументы и возвращают значения, помещая значения в стек и/или в регистры. Это соглашение применяется к функции Си, вызывающей другую функцию Си, фрагменту кода ассемблера, вызывающему функцию Си, либо функции Си, вызывающей функцию ассемблера. (Оно не применяется к фрагменту кода ассемблера, вызывающему произвольный фрагмент кода ассемблера; в этом случае ограничения отсутствуют).

В 32-битной системе x86 под Linux соглашение о вызовах называется cdecl . Вызывающий функцию компонент справа налево помещает аргументы в стек, вызывает целевую функцию, получает возвращаемое значение в eax и извлекает аргументы из стека.

9. Повторяемые строковые инструкции

Есть ряд инструкций, которые упрощают обработку длинных последовательностей байтов/слов и неформально относятся к разряду «строковых» инструкций. Каждая такая инструкция использует в качестве адресов памяти регистры esi и edi и автоматически инкрементирует/декрементирует их после выполнения инструкции. Например, movsb %esi, %edi означает *edi = *esi; esi++; edi++; (копирует один байт). (По факту esi и edi инкрементируются, если DF равен 0; если же он равен 1, то они декрементируются). К примерам других строковых инструкций относятся cmpsb , scasb , stosb .

Строковую инструкцию можно изменить с помощью приставки rep (сюда же относятся repe и repne ), чтобы она выполнялась ecx раз (при автоматическом уменьшении ecx ). К примеру, rep movsb %esi, %edi означает:

Эти строковые инструкции и приставки rep привносят в ассемблер некоторые итерируемые составные операции. Они отражают часть парадигмы дизайна CISC, где для программистов считается нормальным писать код прямо на ассемблере, и предоставляют более высокоуровневые возможности для упрощения работы. (Однако современным решением считается писать код на Си или даже более высокоуровневом языке, а генерацию муторного кода ассемблера поручать компилятору).

10. Плавающая точка и SIMD

Математический сопроцессор x87 имеет восемь 80-битных регистров с плавающей точкой (но вся функциональность x87 сейчас уже встроена в основной ЦПУ x86), и у процессора x86 также есть восемь 128-битных регистров xmm для инструкций SSE. У меня мало опыта работы с FP/x87, так что по этой теме вам стоит обратиться к другим руководствам. Стек в x87 FP работает несколько странным образом, и сегодня удобнее выполнять арифметику с плавающей точкой, используя вместо этого регистры xmm и инструкции SSE/SSE2.

Регистр xmm можно интерпретировать по-разному, в зависимости от выполняемой инструкции: как 16-байтовые значения, как 16-битные слова, как четыре 32-битных двойных слова или числа одинарной точности с плавающей точкой. Например, одна инструкция SSE копирует 16 байтов (128 бит) из памяти в регистр xmm , а другая инструкция SSE складывает содержимое двух регистров xmm , рассматривая каждый как восемь параллельных 16-битных слов. Идея SIMD состоит в выполнении одной инструкции для одновременной обработки большого количества данных, что оказывается быстрее, чем обработка каждого значения по-отдельности, поскольку запрос и выполнение каждой инструкции вносит определенную нагрузку.

Отмечу, что все операции SSE/SIMD можно эмулировать с меньшей скоростью, используя базовые скалярные операции (например, 32-битную арифметику, рассмотренную в разделе 3). Осторожный программист может создать прототип программы с использованием скалярных операций, оценить ее корректность и постепенно преобразовать под использование более скоростных инструкций SSE, обеспечив получение тех же результатов.

11. Виртуальная память

До этого момента мы предполагали, что когда инструкция запрашивает считывание из/запись в адрес памяти, то это будет адрес, обрабатываемый ОЗУ. Но, если мы добавим в промежутке переводящий слой, то сможем выполнять интересные действия. Этот принцип известен как виртуальная память, пейджинг и под другими именами.

Основная идея в том, что у нас есть таблица страниц, которая описывает, с чем сопоставлена каждая страница (блок) из 4096 байтов 32-битного виртуального адресного пространства. Например, если страница ни с чем не сопоставлена, то попытка считать/записать адрес памяти на эту страницу вызовет прерывание/исключение/ловушку. Либо, к примеру, тот же виртуальный адрес 0x08000000 можно сопоставить с другой страницей физической ОЗУ в каждом запущенном процессе приложения. Кроме того, каждый процесс может иметь собственный набор страниц и никогда не видеть содержимое других процессов или ядра операционной системы. Принцип пейджинга, по большому счету, относится к сфере написания ОС, но его поведение иногда затрагивает и разработчиков приложений, поэтому им стоит о нем знать.

Имейте ввиду, что отображение адресов не обязательно должно происходить по схеме 32 бита в 32 бита. Например, 32 бита виртуального адресного пространства можно сопоставить с 36 битами области физической памяти (PAE). Либо 64-битное виртуальное адресное пространство можно сопоставить с 32 битами области физической памяти на компьютере, имеющем всего 1ГиБ ОЗУ.

12. 64-битный режим

Здесь я только немного расскажу о режиме x86-64 и примерно обрисую, какие изменения он собой привнес. При желании в сети можно найти множество статей и справочных материалов, которые поясняют все отличия детально.

Из наиболее очевидного — 8 универсальных регистров были расширены до 64 бит. Новые регистры получили имена , а старые 32-битные теперь занимают младшие 32 бита вышеупомянутых 64-битных регистров.

Также появилось восемь новых 64-битных регистров , и общее число универсальных регистров дошло до 16. Это существенно снижает нагрузку при работе с большим числом переменных. У новых регистров также есть подрегистры – например, 64-битный r9 содержит 32-битный r9d , 16-битный r9w и 8-битный r9l . Кроме того, нижний байт теперь адресуется как .

Арифметические инструкции могут оперировать с 8-, 16-, 32- или 64-битными регистрами. При работе с 32-битными верхние 32 бита очищаются на нуль, но при меньшей ширине операнда все старшие биты остаются неизменными. Многие нишевые инструкции из 64-битного набора были удалены – например, связанные с BCD, большинство инструкций, задействующих 16-битные сегментные регистры, а также добавляющие/извлекающие 32-битные значения из стека.

Не так уж много отличий x86-64 от старой x86-32 касаются конкретно разработчиков приложений. Если говорить в общем, то работать стало легче ввиду доступности большего числа регистров и удаления ненужного функционала. Все указатели памяти должны быть 64-битными (к этому нужно привыкать) в то время, как значения данных могут быть 32-, 64-, 8-битными и так далее, в зависимости от ситуации (не обязательно использовать для данных именно 64 бита).

Рассмотренное соглашение о вызовах существенно упрощает извлечение аргументов функций в коде ассемблера, потому что первые

6 аргументов помещаются не в стек, а в регистры. В остальном принцип работы остался прежним. (Хотя для программистов систем архитектура x86-64 представляет новые режимы, возможности, новые проблемы и новые кейсы для обработки).

13. Сравнение с другими архитектурами

Принцип работы архитектур ЦПУ RISC в некоторых аспектах отличен от x86. Память затрагивают только явные инструкции загрузки/сохранения, обычные арифметические этого не делают. Инструкции имеют фиксированную длину, а именно 2 или 4 байта каждая. Операции с памятью обычно нужно объединять, например загрузка 4-байтового слова должна содержать адрес памяти, кратный 4.

Для сравнения, в x86 ISA операции с памятью встраиваются в арифметические инструкции, инструкции кодируются как последовательности байтов переменной длины, и почти всегда допускается невыравненное обращение к памяти. Кроме того, если в x86 есть полный набор 8-, 16- и 32-битных арифметических операций ввиду обратной совместимости, то архитектуры RISC обычно являются просто 32-битными. Для работы с более короткими значениями они загружают байт или слово из памяти, расширяют его значение на полный 32-битный регистр, выполняют арифметические операции в 32 битах и в завершении сохраняют нижние 8 или 16 бит в памяти. К популярным RISC ISA относятся ARM, MIPS и RISC-V.

Архитектуры VLIW позволяют явно выполнять несколько параллельных подинструкций. К примеру, можно написать add a, b; sub c, d на одной строке, потому что у процессора есть два независимых арифметических блока, работающих одновременно. Процессоры x86 тоже могут выполнять несколько инструкций параллельно (суперскалярная обработка), но инструкции в этом случае не прописываются явно – ЦПУ внутренне анализирует параллелизм в потоке инструкций и распределяет допустимые инструкции по нескольким блокам выполнения.

14. Обобщение

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

Надеюсь, этого руководства было достаточно, чтобы вы сориентировались в общем принципе устройства архитектуры x86. В эту ознакомительную статью мне не удалось вместить очень много деталей – полноценное написание простой функции, отладку распространенных ошибок, эффективное использование SSE/AVX, работу с сегментацией, знакомство с системными структурами данных вроде таблиц страниц и дескрипторов прерываний, да и многое другое. Тем не менее теперь у вас есть устойчивое представление о работе процессора x86, и вы можете приступить к изучению более продвинутых уроков, попробовать написать код с пониманием происходящего внутри и даже решиться полистать чрезвычайно подробные руководства Intel по ЦПУ.

Как писать на машинном коде

Главный герой находится в заточении. В абсолютной тьме. У него были выколоты глаза, но за год они регенерировали, и зрение постепенно к нему возвращается.
И однажды каким-то чудом в одной камере с ним оказывается загадочный Дворкин — создатель Лабиринта. Именно «чудом» — он просто появился неизвестно откуда. Он тоже находится «в заключении», но, в отличие от Корвина (главного героя), может спокойно ходить через каменные стены.
Удивленный Корвин спрашивает его:
— Как ты оказался в моей камере? Ведь здесь нет дверей.
Дворкин отвечает:
— Двери есть везде. Просто нужно знать, как в них войти.
Будем считать это эпиграфом.

1.1. Система счисления

#1. Наверняка среди ваших знакомых есть «крутые» программисты, или люди, таковыми себя считающие ;). Попробуйте как-нибудь проверить их «на вшивость». Предложите им в уме перевести число 12 из шестнадцатеричной в двоичную систему счисления. Если над подобным вопросом «крутой программист» будет думать дольше 10 секунд — значит он вовсе не так крут, как говорит.

#2. Система счисления (сие не подвластное человеческой логике определение взято из математической энциклопедии) — это совокупность приемов представления обозначения натуральных чисел. Этих «совокупностей приемов представления» существует очень много, но самая совершенная из всех — та, которая подчиняется позиционному принципу. А согласно этому принципу один и тот же цифровой знак имеет различные значения в зависимости от того места, где он расположен. Такая система счисления основывается на том, что некоторое число n единиц (radix) объединяются в единицу второго разряда, n единиц второго разряда объединяются в единицу третьего разряда и т. д.

#3. «Разрядам» нас учили еще в начальных классах школы. Например, у числа 35672 цифра «2» имеет первый разряд, «7» — второй, «6» — третий, «5» — четвертый и «3» — пятый. А «различные значения» цифрового знака «в зависимости от того места, где он расположен» и «объединение в единицу старшего разряда» на тех же уроках арифметики «объяснялось» следующим образом:

#4. Очень наглядно это отображают обыкновенные счеты. Набранное на них число 35672 будет выглядеть. см. рисунок слева в общем.

Чтобы набрать число 35672 мы должны передвинуть влево две «костяшки» на первом «прутике», 7 на втором, 6 на третьем, 5 на четвертом и 3 на пятом. (У нас ведь 1 «костяшка» на втором — это то же самое, что и 10 «костяшек» на первом, а одна на третьем равна десяти на втором, и так далее. ) Пронумеруем наши «прутики» снизу вверх — да так, чтобы номером первого был «0». И снова посмотрим на наши выражения:

Это (если сверху вниз считать) сколько на каждом «прутике» «костяшек» влево отодвинуто.

Это номер прутика (самый нижний — 0), на котором отодвинуто определенное число костяшек.

Это на каждом прутике — по 10 костяшек нанизано, не все влево отодвинуты, но всего-то их — 10!
Кстати, красненькое 10 в последнем выражении соответствует основанию (radix) системы счисления (number system).

#5. Пальцев на руках у человека 10, поэтому и считать мы привыкли в системе счисления с основанием 10, то есть в десятичной. Если вы хорошо представляете себе счеты и немного поупражнялись в разложении чисел аналогично выражению 1, то перейти на систему счисления с основанием, отличным от привычной, особого труда для вас не составит. Нужно всего лишь представить себе счеты, на каждый прут которых нанизано не привычные 10 костяшек, а. скажем, 9 или 8, или 16, или 32, или 2 и. попробовать мысленно считать на них.

#6. Для обозначения десятичных чисел мы используем цифры от 0 до 9, для обозначения чисел в системах счисления с основанием менее 10 мы используем те же цифры:

Если же основание системы счисления больше десяти, то есть больше, чем десять привычных нам чисел, то начинают использоваться буквы английского алфавита. Например, для обозначения чисел в системе счисления с основанием 11 «как цифра» будет использоваться буква А:

В системе счисления с основанием 16 — буквы от A до F:

Правда, при определенном основании (при каком?) буквы аглицкого алфавита закончатся.
Но нам это, пока что, глубоко фиолетово, так как работать мы будем только с тремя radix-ами: 10 (ну естественно), 16 и 2. Правда, если кто на ДВК поизучать это дело собирается, тому еще и radix 8 понадобится.

#7. Числа в любой системе счисления строятся аналогично десятичной. Только на «счетах» не с 10, а с другим количеством костяшек.
Например, когда мы пишем десятичное число 123, то имеем в виду следующее:

Если же мы используем символы 123 для представления, например, шестнадцатеричного числа, то подразумеваем следующее:

Короче — полный беспредел. Говорим одно, а подразумеваем другое. И последнее не для красного словца сказано. А потому, что так оно и есть.

Истина где-то рядом.

#8. Трудность у вас может возникнуть при использовании символов A, B, C и т. д. Чтобы решить эту проблему раз и навсегда, необходимо назубок вызубрить ма-а-аленькую табличку «соответствия» между употребляемыми в «компьютерном деле» систем счисления:

radix 10 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
radix 16 0 1 2 3 4 5 6 7 8 9 A B C D E F
radix 2 0 1 10 11 100 101 110 111 1000 1001 1010 1011 1100 1101 1110 1111

Следуя этой таблице, число 5BC в шестнадцатеричном формате «строится» так:

А теперь, если пораскинуть мозгами, с легкостью переведем 5BC из шестнадцатеричной в десятичную систему счисления:

Вот и объединили цифры с буквами. Пространство со временем поучимся объединять немного позже — если не испугаетесь сложностей низкоуровневого программирования.
В общем-то решать вам. В Delphi тоже много чего объединять можно.

#9. Двоичная система по-компьютерному обзывается «bin», «родная» десятичная — «dec», а шестнадцатеричная — «hex». Это так компьютерщики обозвали те системы счисления, с которыми имеют дело. А обозвали потому, что у них ведь полный бардак в голове, оказывается!
Например, 10 — что это за число? Да это вообще не число! Палка и барабан — и только. А вот 10d или же 10_10 — уже понятно, что это — число, соответствующее количеству пальцев на обеих руках. И именно на обеих, а не на двух. Почему не на двух? — А потому что на двух в какой системе? Ежели в двоичной, так это на десяти! То бишь 100, если в десятичной.
Вот и придумали программисты после числа буковку писать — b, d или h. А самые ленивые еще и директиву специальную придумали: напишут в самом начале программы какой-нибудь .radix 16 и будут автоматически все числа, которые без этих букв, за шестнадцатеричные приниматься.

#10. Еще немного про перевод между «радиксами». (Вообще-то это плевое дело, конечно, если представляешь себе, что такое «совокупность приемов представления обозначения натуральных чисел»).
Например, преобразование числа 42936 из десятичного в шестнадцатеричный формат проводится следующим образом (в скобках — остаток):

А вот и обратный процесс — перевод из HEX в DEC числа A7B8h:

Преобразования чисел в системы счисления с другим основанием проводятся аналогично. Счеты! Обыкновенные счеты, только с «плавающим» числом «костяшек» на каждом «прутике».

#11. Если честно, то конкретный «рисунок» цифр — единица там палкой обозначается, двойка — лебедем — это все лишь историческая случайность. Мы запросто можем считать в «троичной» системе счисления с цифрами %, *, _ (где запятая — это знак препинания, а вовсе не число):

Или использовать родные цифры в десятичной системе счисления, но по другому «вектору упорядоченных цифр» — 1324890576:

Правда, этим немножко затрудняется понимание происходящего? А ведь тоже десятичная система! И рисунок цифр как бы знакомый :-)))
Или вообще считать в 256-ричной системе счисления, используя в качестве «рисунка цифр» таблицу ASCII-символов! (По сравнению с вами, извращенцами, любой Биллгейтс будет девственником казаться!!).

#12. Теперь самая интересная часть Марлезонского балета.
Компьютер, как известно, считает только в двоичной системе счисления. Человеку привычна десятичная. Так нахрена еще и шестнадцатеричную какую-то знать нужно?
Все очень просто. В умных книжках пишут, что «шестнадцатеричная нотация является удобной формой представления двоичных чисел». Что это значит?
Переведите число A23F из шестнадцатеричной «нотации» в двоичную. (Один из возможных алгоритм приведен в п.10.). В результате длительных манипуляций у вас должно получиться 1010001000111111.
А теперь еще раз посмотрите на таблицу в п. 8. (которую вы как бы уже и выучили) и попробуйте то же самое сделать в уме :

Каждой шестнадцатеричной цифре соответствует тетрада (4 штуки) ноликов и единичек. Все, что потом нужно сделать — «состыковать» эти тетрады. Круто? Вас еще не то ждет!

#13. Кстати (наверняка вы это уже знаете):

. но это так. кстати.

#14. И, напоследок, еще несколько слов про HEX и BIN :). Зайдите в Norton Commander, наведите указатель на какой-нить файл и нажмите там F3. А когда он откроется — на F4. Там какие-то нездоровые циферки попарно сгруппированы. Это и есть «нолики и единички» (которыми в компьютере все-все-все описывается), но в шестнадцатеричном формате.

Следует основательно разобраться с системой счисления. Минимум, что должен вынести из этой главы юзвер, вступивший на скользкий путь низкоуровневого программирования — это научиться переводить числа между DEC, HEX и BIN. хе-хе. В УМЕ!

1.2. Регистры

#1. Наверняка вы имеете представление о том, что такое переменная. Наиболее продвинутые даже знают, что переменная имеет тип. Кажется вполне естественным, что любой высокоуровневый язык программирования позволяет создавать любое количество переменных того или иного типа.

Так вот, господа — при программировании на ассемблере вас ждет большая неожиданность. Потому что для всех ваших навороченных вычислений разрешается использовать только несколько переменных с фиксированными «именами собственными» и имеющих фиксированную «длину». Эти «предопределенные переменные» называются регистрами, и каждая из них имеет свою специализацию.

О специализации нам пока что говорить рано, описание наподобие «регистр-указатель базы кадра стека» вам вряд ли о чем-то скажет. Поэтому для начала познакомимся только с так называемыми регистрами общего назначения (РОН), и то не со всеми, а только с четырьмя основными, которые являются своего рода «рабочими лошадками» микропроцессора.

Вот их «имена собственные» — AX, CX, DX, BX (именно в такой последовательности они «упорядочены» в Intel’овских микропроцессорах).

А сейчас мы поближе посмотрим на эти «рабочие лошадки» микропроцессора.

1. Запустите программу DEBUG.EXE 1 .

2. Когда появится приглашение в виде «минусика», введите букву «R» (можно и «r» — регистр символов значения не имеет) и нажмите на «Enter».

Не правда ли, весьма похоже на то, что показывают в художественных фильмах про хакеров?

Ну и что это такое? — скептически спросите вы.
А черт его знает! Будем разбираться!

#2. То, что у вас должно появиться — это список доступных регистров и текущее значение каждого из них. Как видите, значения регистров AX, BX, CX, DX равны 0. Не правда ли, создается впечатление, что они просто-напросто ждут того, чтобы в них внесли какое-либо значение?

Природа не терпит пустоты. Писателей приводит в ужас чистый лист бумаги.

Весьма скоро и вы при виде «пустых» регистров будете испытывать непреодолимое наркотическое желание чем-нибудь их заполнить.

Однако прежде чем мы сделаем это в первый раз, давайте уточним тип этих «переменных».

А он очень простой, этот тип — шестнадцатеричное число в диапазоне 0. FFFF. Или, если в BIN, то — от 0 до 1111 1111 1111 1111.

Маловато будет? А вот создатели первых «IBM-совместимых» 🙂 компьютеров посчитали, что и этого много 2 ! 16-битная переменная еще и на две части дробится — для совместимости с языками ассемблера для предыдущих моделей процессора Intel, работавших только с 8-битными регистрами; да и просто ради удобства.

В общем, в умных книжках рисуют вот такую вот «нездоровую» схемку 3 :

AX
AH AH

А означает она следующее.

Физически существует один регистр — AX, а вот логически он делится на два — на старшую (AH) и младшую (AL) части (от английского — high и low).

Очевидно, что присвоить AX значение, например, 72F9h, мы можем следующими способами:

Точно так же присвоить значение 78h регистру AH можно двумя способами:

То же самое, но для регистра AL:

Тех, кого смущают числа с буквами, мы со зловредной ухмылкой отсылаем к 1.1. Система счисления :-]

#3. Если рассматривать регистр «целиком», то каждый из них имеет «длину» 16 бит, которые принято нумеровать справа налево 4 . Так, для числа 2F4Dh, внесенного, например, в регистр AX, мы можем нарисовать такую вот «навороченную» табличку:

AX 2F4D
AH AL 2F 4D
Значение бита 0 0 1 0 1 1 1 1 0 1 0 0 1 1 0 1
Номер бита 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
Тетрады Старшая AH Младшая AH Старшая AL Младшая AL

Внимательно смотрим на таблицу: одной шестнадцатеричной цифре соответствует тетрада двоичных цифр (4 шт., они же — 4 бита). «Емкость» регистров AH и AL — две тетрады, т. е. 8 бит. Точно такую «длину» имеют: коды символов, скан-коды клавиш, номера функций прерываний и куча всего прочего, чего вы пока еще не знаете.

Емкость AX (состоящего из двух половинок) — 4 тетрады, т. е. 16 бит; они же (эти 16 бит) иначе еще называются «словом».

#4. «Принудительно» присвоить регистру значение можно при помощи той же команды «R», только с параметром «имя собственное регистра».

выбросит вам на монитор

Введите после двоеточия, например, число 123 и снова нажмите на Enter:

На дисплее опять появится приглашение «-«, на которое мы отвечаем командой «R» без параметров и таким образом вновь просматриваем значения наших регистров:

Смотрим внимательно — AX=0123, что и требовалось доказать.

1). В W9X она находится в папке WINDOWS\COMMAND\. В Y2K и XP — WINDOWS\SYSTEM32\. В обоих случаях достаточно набрать в командной строке «debug», чтобы она запустилась.

2). Не забудьте, что при тогдашней технологической базе и это было большим прорывом. А экстенсивное расширение, например, разрядности, во-первых, нужно правильно предвидеть (вспомните, сколько в том же MS-DOS закладок на будущее, которые никуда не пошли за ненадобностью), а во-вторых, правильно оценить (в буквальном смысле). Неужели вы думаете, что, например, производители памяти не могут легким мановением руки увеличить ширину шины, соединяющую память с процессором? Могут, но во-первых — это резко повысит стоимость памяти, а во-вторых — не гарантирует повышения производительности.

3). Впоследствии мы немного усложним эту схемку — регистры современных процессоров 32-разрядные и называются немного иначе 😉

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

#1. Первым видом памяти, с которым мы войдем (придется!) в тесный физический контакт, будет оперативная, она же — RAM (от английского — Random Access Memory). Оперативная память — это своего рода «рабочая площадка», по которой суетится этакий шустрый многорукий дядька-процессор — чего-то там собирает, от кучи к куче бегает, всех ругает. 🙂

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

Короче — пришло время испробовать еще одну команду из скромного арсенала DEBUG’a! Запустите debug и введите команду D (от английского — DUMP).

«Картинка», которую вы увидели, называется «дамп памяти» (что в переводе с английского означает «свалка») и она насыщена не только важной информацией, но и специальной низкоуровневой энергетикой. Да чего уж там греха таить — каждый ассемблерщик знает, что рассматривание дампа памяти поднимает настроение, жизненный тонус и другие, не менее важные вещи 😉

Слева — это адрес памяти. В центре — 16 столбцов из спаренных цифр.

А здесь и повториться лишний раз не грех. Каждая пара шестнадцатеричных цифр — это байт. Смотрите внимательно на дамп! Байт по адресу 100 имеет значение 6A, байт по адресу 101 — 00, байт по адресу 102 — 68. Эти «сладкие парочки» — и есть неделимая «единица адресации» оперативной памяти.

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

И, наконец, столбец справа — это символы, соответствующие шестнадцатеричным кодам центрального столбца (например, коду 6A соответствует символ J). Большинству кодов не соответствует никакой из «печатных символов» — таким в колонке справа соответствуют точки.

#2. А теперь потренируем наши пальчики дампировать память — пройдемся по некоторым «историческим местам» нашей оперативной памяти. Для этого мы будем вводить команду D с параметром.

Например, команда (параметр L8 означает «вывести 8 байтов»):

покажет вам системную дату в правом столбце дампа.

Короче, искателям приключений выдаем «простыню» самых интересных адресов (большинство слов в описании вам пока должны быть непонятны, но вы не пугайтесь — понимание придет!).

  • 0:417 — два байта разрядов состояния клавиатуры. Они активно используются ROM-BIOS для управления интерпретаций действий клавиатуры. Изменение этих байтов изменяет значение нажатых клавиш (например, верхний или нижний регистр).
  • 0:41A — cлово по этому адресу указывает на начало буфера BIOS для ввода с клавиатуры, расположенного начиная с адреса 41E. В этом буфере хранятся и ждут обработки результаты нажатия на клавиши. Конец буфера — слово по адресу 41C.
  • 0:43E — байт указывает, необходима ли проверка дискеты перед подводом головки на дорожку. Разряды 0. 3 соответствуют дисководам 0. 3. Если разряд установлен в 0, то необходима проверка дискеты. Как правило, вы можете обнаружить, что разряд установлен в 0, если при предыдущем обращении к дисководу имели место какие-либо проблемы. Например, разряд проверки будет равен 0, если вы попытаетесь запросить каталог на дисководе, на котором нет дискеты, и затем на запрос, появившийся на экране дисплея: «Not ready reading drive B: Abort, Retry, Ignore?» вы ответите: «A».
  • 0:44C (2 байта) — длина регенерации экрана. Это число байтов, используемых для страницы экрана. Зависит от режима.
  • 0:44E (2 байта) — смещение для адреса начала текущей страницы в памяти дисплея. Этот адрес указывает, какая страница в данный момент используется (маленькая, но неприятная подробность — это смещение внутри текущего сегмента видеопамяти, без учета самого сегмента. Например, для нулевой страницы смещение всегда будет равно нулю.)
  • 0:460 (2 байта) — размер курсора, представленный в виде диапазона строк развертки. Первый байт задает конечную, а второй — начальную строку развертки.
  • 0:449 — значение этого байта определяет текущий видеорежим. Для расшифровки требуется целая таблица. Например, 3 — 80-колонный текст, 16 цветов; 13h (19) — 256-цветный графический режим 320×200 и т. д.

Ну и хватит для первого раза. Кому мало — ищите дополнительную документацию :-p «

1.4. Программа

#1. Любая программа выполняется последовательно (мы ведь пока обсуждаем «простой» IBM PC, а не какой-нибудь крутой векторный параллельный суперкомпьютер). То есть пока не выполнилась текущая «строка» (инструкция) программы, следующая не выполнится. Совсем другой вопрос, какая «строка» будет выполнена после «текущей» (здесь мы имеем дело со всевозможными логическими «ветвлениями», «циклами» и т. д.), или же строчку из какой программы процессор выполнит следующей, а какая — будет ждать своей очереди (так называемая «многозадачность», которую пока трогать не будем — в большинстве случаев мы можем прекрасно прожить и без нее, поскольку все заботы об этом все равно берут на себя операционные системы).

Итак, у нас есть оперативная память, в которую загружается программа перед ее выполнением (сразу же по нажатию на Enter из Norton Commander). Операционная система, которая, собственно, и загружает программу, сообщает процессору, что надо начать обрабатывать команды, которые в памяти начинаются с такого-то адреса. И здесь первый подводный камень, вернее скала, которую трудно не заметить.

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

Каким образом? А очень легко! Компьютер «распознает» как выход из программы специальную последовательность байтов. Например, для исполнимых файлов типа com (именно с этим типом файлов мы будем работать на начальном этапе) достаточно последовательности CD и 20.

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

Все очень просто — создаем новый файл, единственным содержимым которого является последовательность CD 20, и сохраняем его как, например, myprg_1.com. Если вы позаботились о том, чтобы после CD 20 не было никаких прочих символов, то исполнимая программа будет «весить» только 2 байта.

Запускать это ваше первое творение лучше из Norton или Volcov Commander (все же это пока что DOS’овская программулька).

Что же она делает, эта 2-байтовая малышка? А ничего, просто этот файл обладает двумя важными свойствами:

  • это — программа;
  • эта программа — программа с корректным выходом.

Последнее и является единственным, что она пока что может делать (корректно выгружаться из памяти).

Еще к вопросу о выгружаемости — если после CD 20 вы напишите еще что-нибудь (чепуху), она все равно будет проигнорирована. Дело до ее выполнения просто-напросто не дойдет. Другое дело — если вы напишите чепуху до.

#2. Честно говоря, опасно при низкоуровневом программировании чепуху писать. Можно невзначай и винт отформатировать :))). Поэтому лабуду писать не будем, вернее — будем, но не лабуду.

Итак, продолжим наше извращение. Познакомимся еще с некоторыми «машинными командами» (в нашем случае — последовательностями шестнадцатеричных циферек).

  • B82301 — внести значение 0123h в AX;
  • 052500 — прибавить значение 0025h к AX;
  • 8BD8 — переслать содержимое AX в BX;
  • 03D8 — прибавить содержимое AX к BX;
  • 8BCB — переслать содержимое BX в CX;
  • 31C0 — очистка AX;
  • CD20 — конец программы. Передача управления операционной системе.

Вот и давайте создадим еще одну программу типа com со следующим «шестнадцатеричным содержимым»:

Если вы все ввели правильно, то прога у вас без проблем запустится, а операционная система не будет ругаться. Правда, визуально (в смысле на мониторе) вы ее работу так и не заметите, но поверьте на слово — она работает! В этом вы еще убедитесь, когда посмотрите на ее работу изнутри — не различающими цветов глазами компьютера. Только сначала еще немного теории.

#3. Теперь поговорим о втором подводном камне :). Один из принципов фон Неймана звучит приблизительно так: машине безразлично целевое назначение данных. Одна и та же цепочка битов может быть и машинными командами, и данными (например, символами, выраженными в виде кодов — есть такая «таблица символов ASCII», наверняка вы ее знаете).

Что из этого следует? А то, что компьютеру нужно указывать, что подразумевается под той или иной «простыней» из битов — данные или код.

На высоком уровне это делает операционная система. Например, она не пытается загрузить в память для выполнения файлы с расширениями, отличными от COM, EXE и BAT (последний вообще не из этой оперы, но принцип сохраняется).

Хотя. вы всегда можете поэкспериментировать! Смените, например, у какого-нибудь текстового файла тип с TXT на COM и попробуйте его запустить на выполнение (хотя мы это делать настоятельно не рекомендуем!). В большинстве случаев ваш компьютер зависнет! Потому что:

  1. Он пытается интерпретировать данные как код. Соответственно, в процессор «попадает» всякая ерунда.
  2. Вряд ли он натолкнется на последовательность CD 20 вашем тексте :). Даже в том случае, если этот код выполнится «успешно» — ваша программа не возвратит управление операционной системе, а пойдет выполняться хлам, содержащийся в оперативной памяти. Как-то — остатки ранее выполненных программ, куски чьих-то данных, интерпретированные как код. и прочая многочисленная ерунда.

Почти такой же эффект, но с потенциально большей разрушительной силой может получиться, если управление получит ИСПОРЧЕННЫЙ код, который вроде бы «в основном» правильный, но часть его вместо инициализации переменных и прочих подготовительных действий в лучшем случае ничего не делает, а в худшем портит другой код и данные.

Как вам тяжело «въехать» в смысл повествования, состоящего из кусков различных книг, так и компьютеру тяжело понять подобную «мешанину». С той лишь разницей, что любую «неинтересную книгу» вы можете использовать в качестве туалетной бумаги, а вот «компутер» подобного права выбора лишен — он должен в это «въезжать», его процессор начинает перегреваться, а мозги кипят и вытекают через низкоуровневые порты ввода-вывода (командами IN и OUT соответственно).

#4. Еще немного идеологии. О программе, которая выполняется в памяти.

Сколько бы ни было «мозгов» в вашей навороченной тачке, любая программа выполняется в 640 килобайтах «нижней» (или основной) памяти. Если отнять от этой цифры «резидентную часть» операционной системы, многочисленные драйвера и т.д., то оставшееся и есть объем памяти, в котором выполняется ваша программа. А остальные мегабайты — это место для кэширования диска, хранения промежуточных данных и т.п.

#5. Как уже говорилось в #3, одна и та же последовательность битов в памяти может быть:

  • кодом (т.е. что компьютеру нужно делать) — последовательностью инструкций;
  • данными (т.е. с чем компьютеру нужно выполнять ту или иную работу). Именно данные являются исходной «задачей» и конечным результатом работы процессора.
  • стеком — это область памяти, позволяющая писать реентерабельный/рекурсивный код и служащая для хранения адресов возврата и локальных данных и передачи параметров.

Соответственно, и программа состоит из трех частей (сегментов): сегмента данных (data), сегмента кода (code) и сегмента стека (stack).

Оставим пока что «гнилой базар» про смысл словосочетаний «реентерабельный/рекурсивный код» и «адрес возврата». Чтобы не затруднять себе понимание происходящего, мы попытаемся абстрагироваться от всех этих ужасающих вещей и для начала заняться только кодом.

#6. Помните, как в конце фильма «Matrix» Нео в конце концов увидел ее — черно-зеленую «матрицу»? Сейчас с вами произойдет нечто подобное! 😉

Посмотрите на машинные коды, и «что они делают» в #2. Немножко дополним эту «простыню». Например, командой «внести значение» 1234 последовательно в каждый из «регистров общего пользования»:

Наиболее наблюдательные должны для себя отметить, что первый байт — это команда «переместить в регистр», а второй и третий — само число, только байты почему-то «наоборот».

Однако никто не пишет программы в шестнадцатеричных редакторах! Никто! Это большая глупость! Единственное, зачем мы вам про это рассказываем — это чтобы вы поняли, что могут означать загадочные пары шестнадцатеричных цифр в дампе.

Нет необходимости заучивать, что B8 — это «переместить в регистр AX», BB — «переместить в регистр BX» и так далее. Когда-нибудь это может пригодиться тому, кто будет писать компилятор, умеющий генерировать исполняемый код, упаковщики исполняемых файлов, самомодифицирующийся код или, на худой конец, конструкторы полиморфных самошифрующихся вирусов. Но это мы оставим на будущее.

Все намного проще!

В этом вы можете убедиться, загрузив вашу программу myprg_1.com в debug (например, командной строкой

и введя команду «u».

А вот дальше начинается самое интересное :)))

#7. Вот что вы должны увидеть:

Возвратившись к #2, перенесем сюда «описание» машинных команд.

Эти mov, add, xor, int — так называемые «мнемонические команды» (более или менее понятные человеку), на основе которых формируется (это debug делает) «машинный код». Не правда ли, так намного легче?

Соответственно, вместо шестнадцатеричных кодов мы легко могли вводить эти команды при помощи команды «A» (однако этим мы займемся позже).

#8. А теперь мы выполним нашу программу пошагово — произведем так называемую «трассировку» при помощи команды «T».

Итак, вводим «T» и жмем на Enter!

Вот что мы видим:

Смотрим на значение AX и вспоминаем предыдущую инструкцию — «внести значение 0123h в AX». Внесли? И правда! А в самом низу — код и мнемоника команды, которая будет выполняться следующей.

Вводим команду «T» снова:.

AX=0148 — «прибавить значение 0025h к AX». Сделали? Сделали!!

Вводим команду «T» снова:.

AX=0148=BX — «переслать содержимое AX в BX». Сделали? Сделали!!

Вводим команду «T» снова:

«Прибавить содержимое AX к BX». Оно? А то!

Вводим команду «T» снова:

«Переслать содержимое BX в CX». Сделано!

Вводим команду «T» снова:

«Очистка AX»? И точно: AX=0000!

Вводим команду «T» снова. И ГРОМКО РУГАЕМСЯ!!

Потому что, по идее, сейчас наша программа должна была завершиться — у нас же там код выхода прописан, а она куда лезет? NOPы какие-то (если продолжать команду «T» вводить), CALL 1085 (да вы продолжайте «трассировку», продолжайте!)

Для тех, кому лень продолжать жать на букву «T», введите для разнообразия команду «G» (от английского GO). На монитор должна вывалиться надпись «Нормальное завершение работы программы».

Уф, — должны сказать вы — Работает!

#9. Только непонятно вот, почему вдруг между int 20 (CD 20) и надписью «Нормальное завершение работы программы» куча всяких «левых» непонятных команд (в том случае, если вы и дальше производили тарассировку, а не воспользовались «халявной» командой «G»)?

А потому, дорогие наши, что вы имели счастье нарваться на прерывание (interrupt)!

Понимаете ли, завершить программу — дело непростое :). Нужно восстановить первоначальное значение регистров, восстановить переменные среды и кучу всего другого! Знаете, как это сложно?

Однако эта процедура насколько сложная, настолько и типичная для исполняемых программ. А по сему разработчики операционной системы решили избавить программистов от необходимости делать это вручную, и включили эту стандартную процедуру в ядро операционной системы. И сказали: «да будешь ты (процедура обработки прерывания) вызываться как int 20, и будешь ты обеспечивать корректную передачу управления из выполняемой программы — назад в ядро». И стало так.

Ну, посудите сами, должна же операционная система ну хоть что-нибудь делать!!

1.5. Прерывания

#1. Прерывание — это СИГНАЛ процессору, что одно из устройств в компьютере НУЖДАЕТСЯ в обслуживании со стороны программного обеспечения. В развитие этой же идеи, программам позволили самим посылать запросы на обслуживание через механизм прерываний. Получив этот сигнал, процессор временно переключается на выполнение другой программы («обработчика прерывания») с последующим ВОЗОБНОВЛЕНИЕМ выполнения ПРЕРВАННОЙ программы.

Когда же и «кем» генерируются эти «сигналы» (в смысле «прерывания»)?

  1. Многочисленными «схемами» компьютера, его устройствами. Например, соответствующее прерывание генерируется при нажатии клавиши на клавиатуре.
  2. Также прерывания генерируются как «побочный продукт» при некоторых «необычных» ситуациях (например, при делении на «букву О»), из которых компьютеру хочешь, не хочешь, но приходится как-то выкручиваться.
  3. Наконец, прерывания могут преднамеренно генерироваться программой, для того чтобы произвести то или иное «низкоуровневое» действие.

Когда процессор получает сигнал прерывания, он останавливает работу приложения и активизирует «программу обработки прерывания», соответствующую «номеру прерывания» (т.е. разных сигналов прерываний больше одного — точнее, их 256). После того как обработчик свое отработает, снова продолжает выполняться основная программа.

Для тех, кто не понял. Представьте себе, что вы сидите за компом и выполняете какую-либо работу. И вдруг ловите себя на мысли, что вам СРОЧНО НУЖНО сходить в туалет (терпеть вы больше уже не можете). Вот это СРОЧНО НУЖНО и есть сигнал-прерывание, по которому вы начинаете выполнять определенную СТАНДАРТНУЮ последовательность инструкций (программу обработки прерывания), как-то: встать, пойти туда-то, включить свет . вернуться, сесть за комп и ПРОДОЛЖИТЬ РАБОТУ с того же самого места, на котором вы остановились перед выполнением программы «поход в туалет». В данном случае наш мозг выполняет роль процессора, наши внутренние органы сигнализируют мозгу о потребности в обслуживании, а само обслуживание проводится «программой-навыком», заложенным в процессе нашего развития и (хм!) воспитания.

#2. Программы обработки прерывания располагаются в оперативной памяти (ну а где же еще им располагаться?!) и, следовательно, имеют свой АДРЕС. Однако генератору прерывания этот адрес знать не обязательно :). Есть такая замечательная штука (спросите у тех, кто пишет вирусы) — таблица векторов прерываний. Это таблица соответствия номеров и адресов памяти, по которым находятся программы их обработки.

Почему «спросите у вирмейкеров?». А потому, что поменять адрес «программы обработки прерывания» на другой — проще пареной репы (мы этим еще займемся), в результате чего при запуске классической программы «HELLO, WORLD» может получиться еще более классический format c.

Программы обработки прерывания автоматически сохраняют значения регистра флагов, регистра кодового сегмента CS и указателя инструкции IP, чтобы по завершении «обработки прерывания», к нашей безумной радости, снова возвратиться к выполняемой программе (просто программе). Остальные регистры, содержимое которых меняется в обработчике, должен сохранять сам обработчик — и если он этого делать не будет, то нарушится выполнение основной программы. Ведь она даже не знает, что ее «ненадолго» прервали!

Однако на самом деле все намного сложнее :)). Но ведь это только «первое погружение» в прерывания, верно? А посему — пока что без особых «наворотов».

#3. Одно прерывание мы с вами уже знаем. Это 20-е прерывание, обеспечившее «выход» из нашей COM-программы. Сегодня мы пойдем немножко дальше — помимо «выхода» попробуем поработать еще с одним прерыванием.

Итак, я достаю свой толстый талмуд с описанием прерываний и выбираю, каким бы это прерыванием вас занять на ближайшие 1/2 часа ;).

Ну, например, вот одно симпатичное, под названием «прокрутить вверх активную страницу».

Внимательно читаем описание (и наши комментарии):

INT 10h, AH=06h (_1) — прокручивает вверх произвольное окно на дисплее на указанное количество строк.

ВХОДНЫЕ ПАРАМЕТРЫ: (_2)

  • AH=06h; (_3)
  • AL — число строк прокрутки (0…25) (AL=0 означает гашение всего окна); (_4)
  • BH — атрибут, использованный в пустых строках (00h…FFh); (_5)
  • CH — строка прокрутки — верхний левый угол; (_6)
  • CL — столбец прокрутки — верхний левый угол;
  • DH — строка прокрутки — нижний правый угол;
  • DL — столбец прокрутки — нижний правый угол;

Далее представим входные параметры в виде таблички: (_7)

AH 06h AL Число строк
BH Атрибут BL Не имеет значения
CH Строка (верх) CL Столбец (верх)
DH Строка (низ) DL Столбец (низ)

Плюс подробнейшее толкование, что подразумевается под словом «атрибут» (регистр BH):

ВЫХОДНЫЕ ПАРАМЕТРЫ: отсутствуют (т.е. ни один регистр не меняется). (_8)

Входные строки гасятся в нижней части окна. (_9)

Нормальное значение байта атрибута — 07h. (_10)

Совсем недавно, если бы вам показали подобное «описание», вы бы ничего в нем не поняли и ужаснулись. Теперь же, после прочтения предыдущих глав курса, в эти «таблицы» вы, более или менее, но «въехать» должны! Тем более, что сейчас я сделаю комментарии для. хм. «отстающих» учеников (внимательно смотрим на циферки в скобках):

_1. Черным по белому, в толстом талмуде, описывающем функции прерываний, написано: «Драйвер видео вызывается по команде INT 10h и выполняет все функции, относящиеся к управлению дисплеем».

И далее. «…ДЕЙСТВИЕ: после входа управление передается одной из 18 программ в соответствии с кодом функции в регистре AH. При использовании запрещенного кода функции, управление возвращается вызывающей программе.

НАЗНАЧЕНИЕ: прикладная программа может использовать INT 10h для прямого выполнения функций видео. «

Вот что из этого следует:

  • выполнив команду INT 10h, мы ВЫПОЛНЯЕМ одну из «функций видео»;
  • так как функций видео — много, необходимо УКАЗАТЬ, КАКУЮ именно ФУНКЦИЮ из МНОЖЕСТВА мы хотим ВЫПОЛНИТЬ.

Дело в том, что «прерывание номер десять» — это не только «прокрутка окна», но и, например, «установка режима видео», «установка типа курсора», «установка палитры» и многое другое. Нас же интересует именно первое, поэтому из списка возможных значений (он приведен ниже) мы выбираем именно AH=06h.

Нижеследующая табличка называется «Функции, реализуемые драйвером видео»:

Соответственно, если перед выполнением INT 10 в регистре AH будет значение 06h, то выполнится именно «прокрутить вверх активную страницу», а не что-то другое из «простыни» функций десятого прерывания.

Теперь читаем описание дальше (смотрим на циферки в скобках):

_2. Входные параметры? Что тут может быть непонятного? Даже запуск ракеты с атомной боеголовкой требует прежде всего указать координаты цели. Чего уж тут говорить об обыкновенной функции?

_3. То, о чем мы уже говорили — номер функции из «простыни».

_4. Т.е. на СКОЛЬКО строчек прокручивать. Вспомните так называемый «скроллинг» в любой прикладной программе. На кнопки Up, Down подвешен скроллинг на одну строчку (не путать с координатами курсора), а вот на PgUp и PgDown — штук на 18 строк (AL=01h и AL=12h соответственно). А вот AL=0, вместо того чтобы вообще не скроллировать (по идее), поступает наоборот — «скроллирует» все, что может.

_5. Скажем так — какого цвета будет окно и символы в нем после скроллирования.

_6. Как известно из школьного курса геометрии, прямоугольник можно построить по двум точкам. Это утверждение справедливо и для окна, в котором мы желаем проскроллировать наш текст.

_7. Резюме того, что было написано выше.

_8. К примеру, попала ли наша ракета в цель или нет ;).

_9. Если бы мы использовали функцию 07h, то было бы глубокомысленно написано, что «строки гасятся в верхней части окна».

_10. Это то самое, которое в DOS по умолчанию. Т.е. белыми буквами на черном фоне. Правда, это 07h лучше все же рассматривать как 00000111b 🙂 но это уже совсем другая проблема.

#4. А теперь мы напишем программу. Ручками, без использования компилятора. Запускаем наш любимый debug.exe, вводим команду «а» и судорожно стучим по клавиатуре:

Сначала запускаем из-под Norton Commander. Затем запускаем из-под debug. Трассируем. Открываем в HEX-редакторе. Смотрим на «бессмыслицу» шестнадцатеричных циферек. Медитируем, медитируем и еще раз медитируем.

1.6. Немножко программируем и немножко отлаживаем

#1. Тем, кто не в курсе — НАСТОЯТЕЛЬНО рекомендую проштудировать предыдущие части курса, иначе «въехать» будет сложно. Тем же, кто внимательно читал предыдущие главы, нижеследующие упражнения для ума и пальцев покажутся детским лепетом.

Давайте немножко видоизменим программу, которую мы писали в прошлый раз. Сделаем так, чтобы наше «окошко скроллинга» располагалось более или менее посередине экрана.

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

Если мы будем программировать ЭТО линейно (именно так для начала), то очевидно, что все, что мы должны сделать — это заданное количество раз (5) заставить машину выполнить вышеуказанные операции, изменяя перед «запуском прерывания» значения регистров AL, BH, CX, DX (полное описание 6-й функции 10-го прерывания ищите в прошлых главах).

#2. Вот к каким умозаключениям вы должны были придти, пораскинув мозгами. За атрибут (то бишь цвет) у нас отвечает регистр BH. Он был равен 10h, а нужно на 10h больше. это, значит, 20h будет.

Ладно. CX (он же CH и CL, как известно) — это ТОЖЕ ШЕСТНАДЦАТЕРИЧНЫЕ координаты левого верхнего угла нашего окошка. Чтобы «окно в окне» получилось, все это нужно на строчку больше сделать и на колонку больше тоже, и все считать в HEX’e. Получается, что в регистр СH нужно вместо значения 05h внести 06h, а в регистр CL вместо 10h — 11h.

А еще можно одним махом в CX записать число 0510h той же командой mov.

Ладно. DX (DH и DL соответственно) — это координаты правого нижнего угла прямоугольника. DH=10h-1h=Fh и DL=3Eh-1h=3Dh.

Ну, а AL=0 и AH=6 — это уже и ежу понятно из описания данной функции (mov AH,6) данного прерывания (INT 10h).

Все, что осталось — это набить в debug’е после команды «a» эти мнемоники энное количество раз. (кажется 5). Набиваем!!

Правда, красивые циферки-буковки? Набиваем-набиваем! Если сейчас к вам подойдут недZенствующие приятели/коллеги и посмотрят, что вы тут колупаете, то ни черта не поймут и покрутят пальцем у виска. Привыкайте к этому. Только не говорите им, что пытаетесь сейчас получить Матрицу, ПОТОМУ ЧТО это неправда. А неправда это потому, что сейчас Матрица в очередной раз обманула вас!

#3. ВСЕ ПОТОМУ, ЧТО МОЗГАМИ ДУМАТЬ НАДО, А НЕ ТОЛЬКО СЛЕПО СЛЕДОВАТЬ РУКОВОДСТВУ!

У вас только первое окно прорисуется, сразу же после чего программа натолкнется на INT 20h и благополучно завершится! А следовательно, и все, что после первого CD 20 написано будет — останется проигнорированным! Исправляйте! (Т.е. уберите все INT 20 КРОМЕ ПОСЛЕДНЕГО).

Второй момент. ВОЗВРАЩАЕТ ЛИ это прерывание ЧТО-НИБУДЬ В РЕГИСТР AX? Смотрите описание. Ничего? Ну так какого черта тогда по новой вводить XOR AL,AL и MOV AH,06 и переприсваивать AH значение 6h, если и без того AH = 6h? Один раз ввести — более чем достаточно!

Скажите, какая мелочь- байтом больше, байтом меньше! А я скажу вот что — на то он и assembler, чтобы «байтом меньше».

#4.Исправляйте? — возмутитесь вы — Да это же по-новому все вводить нужно!

По-новому? — возмутимся мы в свою очередь! — Зачем по-новому? Вы что, с ума сошли?

1. Что вам мешает после команды «a» указать адрес, который вы желаете переассемблировать? И благополучно заменить старую команду на новую!

А что делать, если не переассемблировать нужно, а вообще удалить?

2. Существует куча способов, что вы в самом-то деле! Например, в HEX Workshop с блоками шестнадцатеричных цифр запросто можно работать. Да и в других программах это можно делать — например, в HIEW или даже в Volcov Commander.

Кстати, если процессор встретит команду NOP, то он просто побездельничает некоторое очень короткое время.

ПРОБУЙТЕ!! В конце концов, ваша прога должна принять такой вот вид:

О, да! Получившаяся у вас программа написана долго и бездарно! Имейте это в виду :)). Мы же торжественно обещаем, что в последующих главах обязательно ее усовершенствуем. Да, вот еще что — особенно извращенные могут попытаться заменить INT 20 на JMP 100. Получится, конечно, не ахти, но все же — «анимация» 😉

#5. А теперь мы попробуем ОТЛАДОЧНЫЙ прием! Все кракеры его знают и пользуются им для взлома софта. Имейте в виду, пока вы будете использовать его для своих исполняемых программ — вы программер, исправляющий ошибки, а как только попытаетесь использовать это для отвязки чужой программы от какого-нибудь серийного номера — ваша деятельность станет считаться неэтичной или незаконной. Так что думайте сами, что лучше — флаг в руки или барабан вместе с петлей на шею.

Итак, вводим приблизительно такую командную строку — debug имя_проги.com или же подгружаем прогу в отладчик командой «l» (от слова load) и трассируем, как вы уже неоднократно это делали.

Цель — «на лету» (без изменения кода) заставить первое окошко «рисоваться» не синим (BH=10h), а красным (BH=40h) цветом.

Мы просто приведем вам последовательность действий, а вывод «зачем это нужно» и прочие возможные выводы вы уже сами делать будете. Ок?

Состояние: обнулился регистр AX (первую команду MOV AL,AL мы не видим). Процессор готовится выполнить команду MOV BH,10. Дадим ему это сделать!

Состояние — в BX уже внесен код синего цвета, который нам по условию необходимо заменить на красный (т. е. заменить значение регистра BX с 1000h на 4000h).

Вот теперь-то мы и делаем это «на лету»:

А действительно ли сделали? Проверим!

Состояние? BH теперь равно 40h! Мы «вклинились» между строчками:

И изменили текущую цепь событий, заставив программу делать ТО, ЧТО НАМ НУЖНО! Поздравляю!

А дальше — вводим команду «g» и даем нашей тупорылой программе исполниться. 1:0 не в пользу Матрицы!

#1. В средней школе, где когда-то учился автор, учителем физкультуры был настоящий зверь. Помимо того, что он заставлял нас школьников бегать-прыгать-подтягиваться, у него еще любимое наказание было — проходило оно в так называемом «зале тяжелой атлетики».

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

Штанга — это такая «палка», по бокам которой навешиваются так называемые «блины» — круглые плоские с дыркой посередине диски невероятной тяжести.

Хранятся же эти диски на штырях, которые представляют собой те же «палки», но вторкнутые вертикально в пол. На них и хранились диски — один на другой положенные.

«Наказание» заключалось вот в чем — тот «педагог» заставлял нерадивого ученика комплектовать штангу! Это элементарно сделать, если диски просто валяются на полу. Но когда они аккуратно сложены на штырь — это намного сложнее :(.

Садист! Чтобы достать со штыря диск заданной тяжести (который обычно находился внизу) необходимо было снять со штыря все «вышележащие» диски, достать самый нижний, а остальные снова надеть на штырь. А потом точно так же достать диск другой «тяжести» со второго штыря. А он как бы случайно тоже в самом низу. И так далее — до полной победы идиотизма над здравым рассудком. Не правда ли, изощренная пытка?

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

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

Правила работы с ним те же — вы можете снять только верхний «блин». Чтобы получить самый нижний «блин» — вам нужно прежде снять все те, которые НАД ним.

Очевидно, что из десяти «блинов», которые вы надели на «штырь», первым будет сниматься последний из надетых (верхний), а последним — первый, то есть самый нижний.

Все очень просто: «первый пришел — последним уйдешь» и наоборот «пришел последним — уйдешь первым».

Это вам не очередь времен социализма. Это очередь «загрузки-разгрузки» стека!

#2. Для работы со стеком вам пока что необходимо знать только две команды: push и pop. Так как в качестве «блинов» у нас регистры, то, соответственно, необходимо после этих команд указывать и «имена собственные» помещаемых в стек значений регистров.

Ну а как делать то же самое с остальными регистрами вы, наверняка, уже и сами догадались.

Очень важно помнить, каким «нездоровым» образом в стеке реализована ОЧЕРЕДЬ -поместить/извлечь. Помните, мы вас предупреждали, что нам нельзя верить на слово? Не верьте! А посему — обязательно убедитесь в истинности/ложности нашего голословного утверждения при помощи следующей программульки:

С очередностью заполнения стека, наверное, все понятно :). Я много про абстрактные «блины» загружал. А вот с адреса 114 начинается извлечение из стека. В какой последовательности это делается, вы можете увидеть сами, произведя трассировку этой небольшой проги.

Анализируем. Прога еще не начала работать, готовится выполниться команда по адресу 100. Делаем ШАГ!

Анализируем. AX=0001 — значит, команда выполнилась правильно :). Следующая команда, по идее, должна поместить 1 в стек.

И что? Команда выполнилась, но где мы можем увидеть, что в стек действительно «ушла» единица? Увы, но здесь это не отображается :). Проверим потом. Ведь логично предположить, что если эти значения действительно сохранились в стеке, то мы их потом без проблем оттуда извлечем, т.е. если найдем «там» наши 1, 2, 3, 4, 5 — значит все Ок.

А поэтому — дадим программе работать дальше до адреса 114 (не включительно), не вдаваясь в подробный анализ. Что тут анализировать? Если значение регистра AX последовательно меняется от 1 до 5 — значит, команда mov работает. А стек (команда push) проверим потом, как и договорились.

Проехали до адреса 114.

А вот теперь снова анализируем :). При следующем шаге выполнится команда, извлекающая некогда «запомненное» значение AX из стека.

Обратите внимание, регистр IP указывает на адрес (114) выполняемой команды. Мы с вами это уже проходили, не так ли?

Выполнился первый POP. Готовится выполниться второй. AX=5. Т.е., по сравнению с предыдущим шагом, вроде ничего не изменилось. Но на самом деле это не так. AX=5 — эта пятерка «загрузилась» из стека :)). В этом вы легко убедитесь, сделав следующий шаг трассировки.

Ууупс. AX=4 :). А команда, вроде, та же 🙂 — POP AX 🙂

AX=1 🙂 То есть нашлись-таки наши 1, 2, 3, 4, 5 :). Восстановились из стека. Теперь поверили? А то!

Еще раз обращаю ваше внимание на то, что последовательность записи (четыре PUSH’а) была — 1, 2, 3, 4, 5, а вот последовательность извлечения (четыре POP’а) — 5, 4, 3, 2, 1. Т.е. «последний пришел — первый ушел». Зарубите это себе на носу! (Как сделал это на своем перебитом носе наш школьный учитель физкультуры).

Медитируйте над этой темой до полного просветления! Иначе потом придется туго!

#1. Наша программа для работы со стеком линейна. А линейное программирование — это плохо. Хотя и не всегда 🙂

Итак, давайте еще раз посмотрим на нашу программу для работы со стеком. С 100-го до 113-го адреса у нас имеется пять почти идентичных блоков. Изменяется только значение AX, но на одно и то же число — на единицу в большую сторону. То есть AX = предыдущее значение + 1. Это очевидно.

Еще более очевидно, что простая команда POP AX (с 114 по 119) повторяется у нас тоже 5 раз.

Мне почему-то сразу вспомнился анекдот о том, как два мента едут в машине, и один спрашивает у другого: «Глянь, работает ли у нас мигалка на крыше». Тот высунул в голову в форточку и говорит: «Работает-не работает-работает-не работает-работает-не работает. «

Так вот, не будем уподобляться этим нехорошим людям и сделаем нашу прогу более нормальной.

Добьемся мы этого с помощью так называемого «цикла». Цикл — это. Не буду давать общепринятые определения; кто хочет — поищите в книжках, благо, их навалом.

Скажу только: «сесть-встать, сесть-встать, сесть-встать» — это не цикл, а вот «сесть-встать и так три раза» — уже можно считать циклом.

Реализуется же он (цикл), например, при помощи регистра CX и команды LOOP следующим образом.

Число циклов заносится в регистр CX. После этого следует «простыня» из команд, которые вы хотите «зациклить», т. е. выполнить энное количество раз. Заканчиваться все это должно LOOP’ом с указанием адреса «строки», с которой необходимо начать цикл (обычно это «строка», следующая сразу же после mov СХ.

Давайте мы сначала «набьем» нелинейный вариант нашей проги, а потом разберемся, что там к чему. Набиваем:

Наверное, вы уже поняли, что цикл повторяется до тех пор, пока CX не станет равен 0. Несмотря на то что CX — он как бы регистр общего назначения, для «зацикливания» используется именно он :). С остальными такой фокус не проходит. Это и есть так называемая «специализация регистров», о которой мы уже вскользь упоминали.

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

#2. А теперь вопрос на засыпку ;). Сколько раз выполнится следующий цикл:

Очевидный ответ — 0 раз. В CX же у нас занесен 0. Так вот — ответ неправильный.

Менее очевидный ответ — 1 раз! Ведь перед LOOP’ом сложение один раз все-таки выполнится. Так вот, этот ответ тоже неправильный.

Самые подозрительные могут сразу же посмотреть на этот цикл под отладчиком, и с удивлением обнаружат, что LOOP сначала уменьшает значение CX (0-1=FFFF), а потом уже проверяет, не равен ли он нулю. И с гордостью за задний ум своей головы воскликнут: FFFFh раз!

Так вот: этот ответ близок к истине, но тоже неправильный 😉

Правильный ответ — цикл выполнится 10000h (65536d) раз.

Но только вы и мне не верьте! Истинно только то утверждение, которое вы сами проверили на практике. Медитируйте!

1.9. Немножко оптимизации

Как мы уже говорили, линейное программирование — это плохо, но не всегда. Сравните размеры ваших линейной и нелинейной программ. Не знаю, как у вас, но у нас линейная «весит» 27, а нелинейная — 19 байтов. Как по-вашему, какая быстрее работать будет?

Ну, естественно, нелинейная, потому что она меньше! — скажете вы и будете неправы.

Попытайтесь оттрассировать «зацикленную». Не правда ли, она трассируется намного дольше своего линейного аналога?

Угу, всё поняли? Сам знаю, что ни черта. 🙁

Объясняю: в «зацикленной» программе «компутеру» приходится выполнять БОЛЬШЕ команд, нежели в «незацикленной».

Аргументирую это голословное утверждение следующей таблицей (построенной на основе трассировки):

Что делает линейная Что делает нелинейная
AX=1 AX=0
Помещаем в стек 1 CX=5
AX=2 AX=AX+1=1
Помещаем в стек 2 Помещаем в стек 1
AX=3 Конец цикла — переход
Помещаем в стек 3 AX=AX+1=2
AX=4 Помещаем в стек 2
Помещаем в стек 4 Конец цикла — переход
AX=5 AX=AX+1=3
Помещаем в стек 5 Помещаем в стек 3
Достаем из стека 5 Конец цикла — переход
Достаем из стека 4 AX=AX+1=4
Достаем из стека 3 Помещаем в стек 4
Достаем из стека 2 AX=AX+1=5
Достаем из стека 1 Помещаем в стек 5
Выход CX=5
Достаем из стека 5
Конец цикла — переход
Достаем из стека 4
Конец цикла — переход
Достаем из стека 3
Конец цикла — переход
Достаем из стека 1
Выход

Ну и как по-вашему, какую из двух простыней процессор быстрее обработает? Сказать вам по секрету? А вот ничего я вам не скажу! Сами думайте! :]

Как сейчас помню, был в моем Турбо-Си в преференсах к компилятору такой радиобуттон: «оптимайзить» по размеру или по скорости выполнения. Угадайте, на чем основан принцип этой оптимизации?

Только не вздумайте писать линейные проги! Пишите «нелинейные»! Нелинейную в линейную «переоптимайзить» — как два пальца намочить! А вот наоборот — :((

Резюме — бесплатный сыр бывает только в мышеловке, и за все надо платить. Компактность и скорость — обычно параметры конфликтующие, поэтому в каждом конкретном случае нужно выбирать, что предпочтительнее. Исследования, проведенные в свое время еще Кнутом, показали, что 80% времени затрачивается на выполнение 20% программы. Соответственно, рекомендуется тратить время на оптимизацию скорости именно тех 20% программы, а остальные можно оптимизировать по размеру (в частности, за счет циклов). Так и получается баланс между компактностью и скоростью программы ;).

1.10. Разборка с процедурами

#1. В разделе 1.7. #4 мы сделали глупую линейную программульку, выводящую окошки. Обещали, что в следующей главе сделаем ее менее «тупой», да отвлеклись почему-то на циклы и стек. То есть я-то знаю, ПОЧЕМУ, но вот вам об этом — не скажу! Догадайтесь сами. Итак, поехали.

Шаг первый. Внимательно посмотрев на «линейную» прогу из 1.7. #4 и прочитав «условие задачи» из главы 1.7. #1, вы обязаны возмутиться — зачем мы использовали команду MOV, если и ежу понятно, что отличия следующего окошка от предыдущего можно выразить более лаконично: BH=BH+10, CH=CH+1, CL=CL+1, DH=DH-1, DL=DL-1? И не нужно напрягать мозги, подсчитывая новое значение регистра вручную.

Если вы так подумали, то оказались совершенно правы! Программу из #4 запросто можно было представить в таком вот виде:

И несмотря на то, что размер ее оказался несколько большим, она тоже будет работать правильно :).

Но тут любой более или менее наблюдательный программер возмутится повторно — да что это за программа такая? В ней целых четыре раза повторяется один и тот же кусок:

И знаете что? Этот наблюдательный программер будет прав! А если он еще и нехорошо выразится по поводу такого «неправильного» стиля программирования — будет прав. или почти прав.

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

Внимательно всмотритесь в полный текст программы и в этот выделенный кусок. И помедитируйте над ним до полного просветления текущей «обстановки».

#2. Итак, у нас есть ПОВТОРЯЮЩАЯСЯ ЧАСТЬ программы. А еще у нас есть пальцы, которым, как правило, лень набивать длинные «простыни» программного кода. Это одна из многочисленных причин, по которым и придумали такого «зверя» как ПОДПРОГРАММУ (она же — ПРОЦЕДУРА, она же — ФУНКЦИЯ). Остальные причины мы рассмотрим попозже, а вот на счет «лени» поговорим прямо сейчас:

Если мы возьмем наш «часто повторяющийся» кусок программы и допишем в конец команду RET, то получится у нас именно ПРОЦЕДУРА — во всей своей красе:

Красота ее вот в чем заключается — процедуру можно «вызвать» командой CALL :)))

Все более чем просто. Когда в программе встречается CALL с указанием АДРЕСА-НАЧАЛА-ПРОЦЕДУРЫ (в нашем случае это 011E), то компьютер «идет» по этому адресу и выполняет все команды, расположенные между «точкой входа» (включительно) и командой RET, то есть так называемое «тело» процедуры.

RET — это тоже команда, но к «телу» (адреса 11E. 12D) она не относится. Она является «ОРГАНИЗАТОРОМ» этого «тела». Процессор, встретив команду RET, возвращает управление обратно после последнего CALL (т.е. «перепрыгивает» на строчку ниже «вызвавшего» данную процедуру CALL’а).

Короче, CALL XXXX означает — «выполнить процедуру, начинающуюся по адресу XXXX». А RET означает — «конец процедуры» и, соответственно, переход на строчку ниже вызвавшего его CALL’а.

Если же говорить более формально и строго, то процедура — это средство обобщения, когда некоторая общая последовательность действий получает «имя», и потом при необходимости ПОВТОРНОГО ИСПОЛЬЗОВАНИЯ данного кода к нему просто идет обращение по «имени» (напомним, что в отличие от языков высокого уровня и даже ассемблера, в машинном коде от имен остаются одни только адреса, а язык, принимаемый DEBUG, является промежуточным между машинным кодом и ассемблером). Более того, следующим логическим шагом после обобщения является параметризация, когда некоторые части общего кода зависят от передаваемой извне информации (параметров), с чем очень хорошо знакомы программисты на языках высокого уровня. Но о параметризации и ее применении в ассемблере мы поговорим в другой раз.

Не ругайтесь. Мы знаем, что вы ни черта не поняли. А по сему набьем в debug’е эту прогу и посмотрим, что она делает.

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

#3. Кстати, вы уже поняли, почему мы называем debug «до боли любимой программой»? Нет? Неужели вы еще не полюбили это произведение программерского гения всеми фибрами своей души? Еще нет? М-да. мы в вас разочаровались.

А по сему: — НАБИВАЕМ! — злобно кричим, брызгая слюной на эргономичный коврик:

Не правда ли, красиво получилось?

Первое, что вас может смутить — это то, что команда выхода (INT 20) расположена не там, где вы привыкли, то есть не в конце программы.

Ну что я вам могу на это ответить? Концы — они-то разные бывают! Последняя строчка в листинге вовсе не означает, что последней будет выполняться именно она. И это не должно вас смущать! А если все же смущает — смотрим, как работает эта прога из-под отладчика.

Итак, до адреса 0110 вам все должно быть понятно, мы это рассматривали. Трассируем дальше.

Что значит «трассируем»? — попросите вы напомнить.

Мысленно мы ругаем вас нехорошими словами (ну сколько раз повторять-то можно!), а вслух скажем: Команда «T» и Enter. Команда «T» и Enter. Команда «T» и Enter…

Команда CALL 011E по адресу 0110 говорит процессору: «Дальше мы не пойдем, пока не выполним простыню, начинающуюся по адресу 011E». И далее, естественно, следует переход на этот адрес.

Входим в тело процедуры, начиная с 011E, и выполняем команды до 012D включительно.

А теперь внимательно смотрим, на какой адрес нас «перекинет» команда RET.

На 113-й? И это правильно! По 113-му адресу у нас какая команда? Да вот опять CALL 011E!

Опять процедура с адреса 011E, опять RET[URN] на строку ниже, то есть на 116.

И так далее, до того момента, пока следующей строчкой не окажется INT 20 — собственно, на этом и программе конец.

Ну оно и ежу понятно, что, несмотря на то что INT 20 — не в конце программы, последним выполнится именно он.

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

Кстати, именно это и является одной из многочисленных тайн программинга.

Кто после этого скажет, что программисты — недZенствующие люди?

#4. Те, кто внимательно ознакомились с циклами, они и на этом не остановятся! Посмотрев на адреса 110. 119, они вообще возьмут и возомнят себя воистину крутыми парнями! Знаете, что они напишут? А вот что (предвидим!):

То бишь еще и CALL в цикл при помощи MOV CX,4 и LOOP’а «закрутят». И что? А попробуйте!

Что, не «пашет»? А что надо делать, если «не пашет, а должно бы»? Правильно! Смотреть из-под отладчика!

Смотрим? Если посмотрите, то сразу же и «загвоздку» увидите — CX, использованный в качестве «счетчика» циклов, «перебивает» тот же CX, но используемый как «координаты верхнего левого угла окна». И что с этим делать прикажете?

Вот вы и столкнулись с одной из самых больших проблем. В процессорах фирмы Intel есть только 4 регистра общего назначения (и то в большинстве случаев — специализированных). Помните, мы вам говорили об этом?

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

1.11. Переходы

#1. «Переходы» бывают разные. Если вы пришли в гости, а вас просто послали к черту — такой переход называется «безусловный». А нежели вам сказали: «Если без пива — то иди к черту, а если с пивом — тогда проходи», — то это уже «условный» переход.

Соответствено, для успешного перехода необходимо указать: ПРИ КАКОМ УСЛОВИИ выполнить переход, КУДА ПЕРЕЙТИ, ну и, наконец, сам пинок под зад нужно СДЕЛАТЬ, чтобы переход «гостя» в заданном направлении все-таки «состоялся».

Безусловный переход у нас «делает» мнемоническая команда JMP, после которой следует указать адрес, на который «компьютер» должен пойти «на» ;). В данном случае УСЛОВИЕМ у нас будет «при любых обстоятельствах»: хоть пустой, хоть с пивом, хоть с . все равно. Когда рисовали окошки, вы уже использовали эту команду для создания «спецдефекта». Если кто еще не понял, что делает эта команда — к нему (см. 1.7 #4) и отсылаю. Сделайте «спецдефект» и посмотрите на него под отладчиком. Когда до вас дойдет, почему мы там не предусмотрели выхода (INT 20h) — можете переходить к п.2 текущей главы.

#2. Условный переход у нас организуется в два шага. На первом шаге мы вычисляем условие («принес ли пиво?»), на втором шаге «посылаем» или не «посылаем» — в зависимости от результатов вычислений. Можно привести такую аналогию — на первом шаге два груза кладутся на весы, сравнивающие их массу. Соответственно, возможны только три их положения: наклон влево (груз в левой чашке тяжелее), наклон вправо (груз в правой чашке тяжелее) и равновесие. На втором шаге мы предпринимаем действия в зависимости от положения весов.

Например, на первом шаге можно использовать как «аптекарские весы» инструкцию CMP, которой обязательно нужно указать, ЧТО и С ЧЕМ она будет сравнивать.

В зависимости от значений регистров у нас возможны следующие состояния: «наклон влево» (AX &tt; BX), «наклон вправо» (AX > BX) и «равновесие» (AX = BX). Таким образом ВЫЧИСЛЕНИЕ УСЛОВИЯ у нас уже организовано :). Только условие не бинарное, а есть еще и «серединный вариант» (и даже несколько других!). Это нормально. Это для того сделано, чтобы мы могли выражения типа «больше-или-равно», «меньше-или-равно» да и просто «равно» в своих программах использовать.

Итак, УСЛОВИЕ есть. Теперь решаем, что нам делать при том или ином условии. Вот далеко не полный список возможных «прыг-скоков»:

  • JE — переход если равно;
  • JNE — переход если не равно;
  • JA — переход, если больше;
  • JAE — переход, если больше или равно;
  • JB — переход, если меньше;
  • JBE — переход, если меньше или равно.
  • и т.д.

Естественно, что после мнемоники («прыгнуть, если») должен стоять АДРЕС, куда нужно «прыгнуть», если условие соблюдено. Если же условие не соблюдено, то прыжок не происходит, и выполняется нижеследующая строка программы.

Задание на медитирование — зрительно представьте себе «весы правосудия». И побросайте на их чашки разную шестнадцатеричную дрянь в различных «пропорциях» и «комбинациях». Просветлиться вы должны следующим образом — в какую бы сторону эти ваши «весы» ни склонялись, вы все равно заставите систему работать так, как ЭТО вам угодно! «Весы» — они только констатируют факт. А вот «приговор» выносят судьи. Хе. и пусть после этого только кто-нибудь скажет, что программерам чужда политика — дело, как известно, весьма грязное.

А о чем это мы? Ах да, переходы.

#3. Продолжим программировать, что ли? Напишем что-нибудь красивое и неизменно тупое? С использованием условных и безусловных переходов?

Поехали! Слабаем мы сейчас что-то наподобие графического редактора :)). Не верите?

У-у-у. Сложная задачка! Если въедете, что да как — значит, молодцы! Значит, разобрались-таки с дzебагом! Значит, подключились-таки к программерскому эгрегору и более или менее привели в порядок свои мозги :). А это сложная штука, мы вам скажем — мозги в порядок приводить! Особенно когда есть куча инструментов, которые «порядок в коде» сами как бы наводят :).

Думаете, вы по нашим текстам программировать учитесь? По-настоящему программировать мы еще не начали! Все, чем мы пока занимаемся — это приводим в порядок свои мозги и тренируемся на кнопки клавиатуры нажимать :)). А вот ско-о-оро НАЧНЕМ. тогда «прощай, здоровье» будет настоящее!

Итак, сначала рассмотрим прерывания, которые в нашем «графическом редакторе» будут использоваться. Их три штуки, и все — BIOS’овские:

Опять-таки — подробности о координатах и «кодах» цвета ищите сами!! Благо, знаете, где искать.

Эта функция считывает код сканирования и код символа (клавиши на клавиатуре и соответствующий ей ASCII-код) из буфера клавиатуры (есть такой). Если в буфере ничего нет — она ждет, пока там что-нибуть появится. То есть ЖДЕТ, чтобы вы нажали на какую-нибудь клавишу, код которой будет занесен в регистр AX. Причем в AL — «символ», а вот в AH — так называемый «код сканирования».

Кодами вы пока голову не забивайте. Достаточно знать, что после нажатия клавиши Up в AH «попадет» значение 48h, Down — 50h, Left — 4Bh, Right — 4Dh.

Как работает последний кусок кода, обязательно проверьте под отладчиком, это полезно :).

#4. И лезем, лезем в наш горячо любимый DZEBUG, дабы набить там драгоценные строчки машинного мнемонического никому-кроме-вас-непонятного кода!

Тут один из автору вставили шпильку:

«Не хватает проверки и выхода (со сбросом видеорежима!) по Esc — такие действия должны быть обязательным атрибутом, а не домашним заданием.»

Совершенно верная шпилька, товарищи! Но все равно — пусть это будет домашним заданием.

Если вы все ввели правильно — должно заработать! Полюбуйтесь плодами своей медитации. Красиво?

#5. А сейчас мы это все дело прокомментируем:

  • адреса 100. 200 — устанавливаем графический режим, указываем функцию (100), указываем номер режима (102) и вызываем прерывание (104);
  • адреса 106. 109 — инициализируем координаты первой точки. Координаты последующих точек будут определяться «динамически» — в зависимости от нажатой клавиши;
  • адреса 10С. 110 — рисуем точку. Первый раз — в координатах, инициализированных командами по адресам 106 и 109. Все последующие разы — по координатам, «инкрементированным» или «декрементированным» (во словеса!) по адресам: 12A, 12D, 130 и 133;
  • адреса 112. 114 — ждем нажатия на клавишу;
  • адреса 116. 128 — «щемим» нужные нам клавиши. «Взвешиваем». На каждую из курсорных клавиш по адресам 12A. 134 приготовлены «обработчики». Если найдена «нужная клавиша», то делаем прыг на «обработчик» этой клавиши;
  • aдреса 12A. 134 — в этом блоке определяется, что делать с координатами следующей точки. После чего — прыжок на «рисуем точку» :).

Правда, здорово получилось?

#1. Работать с кодом мы с вами научились. Сейчас поучимся заставить наш код обрабатывать данные.

Итак, запускаем DZEBUG и вводим следующую команду:

Которая означает: «набиваем память всяким дерьмом начиная со смещения 115».

В ответ он вам выплюнет:

Что означает: байт по смещению 115 равно 00. И точка. Но это не простая точка — это приглашение ввести НОВОЕ ЗНАЧЕНИЕ этого байта. Когда вы его ввели, нужно нажать на пробел.

Если вы вознамеритесь последовательно ввести 1,2,3,4,5, то это будет выглядеть приблизительно так:

А теперь делаем дамп памяти и смотрим, что за дрянь у нас получилась.

А ведь получилoсь же!!

#2. Мы запросто умеем «присваивать» регистру любое значение (mov AL,1C какой-нить), запросто можем «копировать» содержимое одного регистра в другой (mov AL,BL например). А сейчас мы с вами научимся при помощи той же команды MOV еще и с данными из памяти работать.

Все проще пареной репы. Если мы напишем

то в результате выполнения этой команды в регистр AL «внесутся» две шестнадцатеричные циферки (байт), которые по адресу 115 находятся. То есть в нашем случае AL станет равным 1.

А теперь посмотрите, что делает «обратная» команда:

В первой строчке мы присвоили AL значение 55, а второй строчкой «скопировали» значения регистра в байт по адресу 115. Правда, проще некуда?

Обязательно посмотрите на этот процесс под отладчиком!

#3. А еще вот какой изврат с этим можно делать:

Сие присваивает регистру AL значение байта по адресу 115 :). Ну. через посредника «BX» присваивает! Который у нас «переменная», как известно :).

А этот кусок кода у нас «записал» 1C в сегмент данных по адресу 115 :). Ну, и извращения наподобие:

Тоже весьма и весьма полезны в программерском деле :).

Короче: все, что в квадратных скобках, — это адрес в памяти, с которым вы собираетесь «работать». Другой вопрос, что этот адрес может быть «составным».

#4. Низкоуровневый Paint мы с вами уже писали. Сегодня напишем низкоуровневый дZенский EXCEL.

Задание простое. Есть у нас табличка типа:

в которой данные в формате HEX. И все, что нам нужно с ними сделать — это просуммировать каждую «строчку», а сумму занести в третий «столбец». В EXCEL’е это делается элементарно. А на машинном уровне, в общем-то, не намного сложней!!

Для начала мы наберем «исходные данные» и зарезервируем место (например, забьем нулями) под третий столбец, в который собираемся помещать результат.

Набиваем блок данных, начиная с адреса, например, 115:

Вот так это у меня в DZEBUG’е выглядело :). Только я еще дамп посмотрел, правильно ли я ввел:

Вроде правильно :)). Ну а программу я вот какую придумал:

В BX я занес адрес начала блока данных (он же — верхний левый угол нашей таблицы). В CX внес 5, чтобы столько раз цикл выполнился (LOOP по адресу 111). А тело цикла вообще простое:

Команда по адресу 106 забирает в AL цифирь из первого столбца.

108 — суммирует «цифирь из первого столбца с цифирью из второго столбца» (сумма, само собой, в AL’е остается).

10B — записывает сумму в третий столбец :).

Ну и ADD BX,3 для перехода на следующую строчку :).

Сделайте трассировку (внутрь INT 20 залезать не надо) и посмотрите на дамп нашего блока данных 🙂

Я и говорю: ПРОЩЕ ПАРЕНОЙ РЕПЫ!! 😉

#5. Видите? В качестве переменных «в компьютере» можно использовать не только регистры, но и «куски» памяти! А уж там вы можете клепать свои переменные в почти неограниченном количестве! Единственное, что нужно иметь ввиду: с переменными-регистрами компьютер работает намного быстрее, чем с переменными-в-памяти :).

Кстати, если вы хотите сохранить плод своих сегодняшних трудов на веник, то имейте ввиду, что вы и сегмент данных тоже должны сохранить! То есть: вам нужно сохранить весь «диапазон» от адреса 100 до 123 включительно :).

Ну и, само собой, при попытке дизассемблирования с адреса 115 у вас абракадабра пойдет. мы об этом уже говорили и упоминали один из принципов фон Неймана.

Полагаю, вы уже поняли, что значит «выучить язык ассемблера» 🙂 и теперь с удовольствием кинете грязью в того, кто скажет вам, что это сложно 😉

Что значит «выучить язык», и что значит «программировать»? А проводите сами границы между этими понятиями! Только имейте в виду, кто скажет «выучить — значит все команды запомнить — тот дурак :((.

Слова и понятия извращать можно по-всякому. Переопределите собственный тип и носите свежеобмоченные пеленки в полной уверенности, что это носки. В моем понимании «знать ассемблер» и «изучить на ассемблере» — синонимы (хотя лингвисты могут придраться, но мне это пофиг). Согласно границам, которые провел для себя (гы. ну и для вас немножко) автор курса, ассемблер вы уже знаете, а вот программировать на нем пока еще не умеете.

Да о чем это я, в общем-то? (Утомлен кофием, поэтому речь несвязна)? Просто хотел сообщить вам, что первая часть курса закончилась. Вооружившись справочником команд и прерываний, вы уже можете программировать под дос. Если вы внимательно штудировали предыдущие главы, то идеология этого дела (под дос) вам уже должна быть понятна как 2х2=100b.

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

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