Как перевести код в машинный код
Многие любители не испытывают серьезных трудностей в овладении БЕЙСИКом. Для этого достаточно немного практики. Но рано или поздно они приходят к барьеру «машинного кода». Как это ни печально, но некоторые так перед ним и останавливаются. Это ни в коей мере не связано с отсутствием желания или способностей, просто многие не знают, с чего начать. Если в БЕЙСИКе можно начинать с чего угодно (при ошибке компьютер сам Вас поправит), то здесь Вы оказываетесь с процессором один на один, и такой метод проб и ошибок не срабатывает.
Итак, давайте напишем первую программу в машинном коде. Прежде всего, выделим для нее область памяти. Если Вы читали нашу книгу «Большие возможности Вашего «ZX-Spectrum`а», то знаете, что для БЕЙСИКа в оперативной памяти компьютера отведена область памяти, начинающаяся с адреса, на который указывает системная переменная PROG и заканчивается адресом, на который указывает системная переменная RAMTOP. Предположим, что Вы хотите записать программу в машинных кодах, начиная с адреса 30000. Дайте команду CLEAR 29999. Эта команда установит RAMTOP в 29999 и Ваша программа будет защищена от возможной порчи из БЕЙСИКа. Даже если Вы дадите команду NEW, области памяти, находящиеся выше RAMTOP, не будут поражены.
Теперь дайте две прямые команды одну за другой:
Если все, что Вы здесь прочитали, Вам понятно, то Вы уже поняли, как составляются программы в машинных кодах. Можно, конечно, возразить, что пользы от такой программы не очень много, но сейчас не в этом суть. Важно, чтобы Вы поняли, что некая последовательность чисел может быть последовательностью команд для процессора Z-80.
Теперь давайте вернемся к нашей первой программе и попробуем ее несколько развить, чтобы она все же что-то делала. Процессор Z-80 имеет несколько регистров, у которых есть имена – «А», «В», «С» и т.д. Каждый из них может содержать одно какое-либо целое число от 0 до 255 (т.е. один байт).
Существуют десятки команд процессора, которые позволяют копировать содержимое регистров из одного в другой, а также выполнять связь с внешним миром, в т.ч. и с оперативной памятью.
Итак, мы уже готовы к тому, чтобы написать программу, которая будет перебрасывать какое-либо число из одного регистра процессора в другой.
Источник
Машинный код и компиляция в него — это как?
1 ответ 1
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). Из-за таких сложностей были придуманы языки ассемблера, которые вводят простые мнемоники для инструкций процессора.
Вот так может выглядет отрывок программы на языке ассемблера:
Вот так выглядит программа на машинном языке:
Очевидно, что асссемблерный код и читать, и писать проще.
Теперь у вас достаточно знаний, чтобы открыть справочник, как по словарю, написать программу в машинных кодах и исполнить ее на процессоре. Но, это не сработает в случае, если вы хотите написать программу, которая будет работать в какой-либо операционной системе.
Операционная система
Как я уже сказал, каждая программа процессора — это просто последовательность команд, однако каждая программа операционной системы — это особая последовательность байт, имеющая специальную структуру, в которую входят не только команды процессора.
Поэтому чтобы вручную написать программу в машинных кодах, которая будет запускаться в Windows 10, например, нам, по-мимо написания самой программы, потребуется привести ее к формату Portable Executable.
Но и этого будет не достаточно. Нам придется ознакомится с соглашениями, которые называются ABI и написать программу в машинных кодах, используя именно эти соглашения, а не какие-то другие.
Здесь необходимо, чтобы все части паззла подходили друг к другу по форме: программа должна быть валидной для процессора, формат бинарного файла должен быть понятен операционной системе, программа должна уметь корректно общаться с ОС и т. д. Это все очень сложно обеспечить, если писать программу в шестнадцатеричном редакторе.
Можете начать с написания программ на языке ассемблера (да, вам придется еще выучить синтаксис конкретного языка ассемблера и диалект Intel или AT&T). «Hello, World» на языке NASM будет выглядеть так:
А нужно ли вам это?
В наше время компьютеры стали очень сложными, с десятками слоями абстраций. Даже инструкции ISA современных процессоров — не атомарные сущности, и процессоры выполняет каждую такую инструкцию как набор еще более мелких инструкций — микрооперации (из таких мокроопераций складывается микрокод).
На самом деле, умение писать на языке ассемблера (а тем более, на машинном языке) довольно бесполезно. Умение просто читать и понимать ассемблерный листинг гораздо более практично и действительно может вам пригодится.
А непрактично это в первую очередь потому, что ничего сложнее «Hello, World!» в машинных кодах вы не напишете. На ассемблере — да, напишете, но потратите на это колоссальное количество времени, которое можно было бы потратить на более полезные вещи.
1. Что интересно, инструкция MOV в x86 является Тьюринг-полной, т. е. любая программа может быть написана с использованием одной только этой инструкции. Есть даже специальный компилятор, который использует только одну эту инструкцию.
2. Некоторые ассемблеры могут сразу формировать исполняемые файлы в нужном формате. В том числе и Portable Executable.
3. Я говорю о современных ОС типа Windows или Linux.
Источник
Как перевести код в машинный код
Вы читали «Хроники Амбера» Роджера Желязны? Там есть такой эпизод:
Главный герой находится в заточении. В абсолютной тьме. У него были выколоты глаза, но за год они регенерировали, и зрение постепенно к нему возвращается.
И однажды каким-то чудом в одной камере с ним оказывается загадочный Дворкин — создатель Лабиринта. Именно «чудом» — он просто появился неизвестно откуда. Он тоже находится «в заключении», но, в отличие от Корвина (главного героя), может спокойно ходить через каменные стены.
Удивленный Корвин спрашивает его:
— Как ты оказался в моей камере? Ведь здесь нет дверей.
Дворкин отвечает:
— Двери есть везде. Просто нужно знать, как в них войти.
Будем считать это эпиграфом.
1.1. Система счисления
#4. Очень наглядно это отображают обыкновенные счеты. Набранное на них число 35672 будет выглядеть. см. рисунок слева в общем.
Это (если сверху вниз считать) сколько на каждом «прутике» «костяшек» влево отодвинуто.
#5. Пальцев на руках у человека 10, поэтому и считать мы привыкли в системе счисления с основанием 10, то есть в десятичной. Если вы хорошо представляете себе счеты и немного поупражнялись в разложении чисел аналогично выражению 1, то перейти на систему счисления с основанием, отличным от привычной, особого труда для вас не составит. Нужно всего лишь представить себе счеты, на каждый прут которых нанизано не привычные 10 костяшек, а. скажем, 9 или 8, или 16, или 32, или 2 и. попробовать мысленно считать на них.
#6. Для обозначения десятичных чисел мы используем цифры от 0 до 9, для обозначения чисел в системах счисления с основанием менее 10 мы используем те же цифры:
Если же основание системы счисления больше десяти, то есть больше, чем десять привычных нам чисел, то начинают использоваться буквы английского алфавита. Например, для обозначения чисел в системе счисления с основанием 11 «как цифра» будет использоваться буква А:
Правда, при определенном основании (при каком?) буквы аглицкого алфавита закончатся.
Но нам это, пока что, глубоко фиолетово, так как работать мы будем только с тремя 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 из шестнадцатеричной в десятичную систему счисления:
Преобразования чисел в системы счисления с другим основанием проводятся аналогично. Счеты! Обыкновенные счеты, только с «плавающим» числом «костяшек» на каждом «прутике».
Правда, этим немножко затрудняется понимание происходящего? А ведь тоже десятичная система! И рисунок цифр как бы знакомый ))
Или вообще считать в 256-ричной системе счисления, используя в качестве «рисунка цифр» таблицу ASCII-символов! (По сравнению с вами, извращенцами, любой Биллгейтс будет девственником казаться!!).
#12. Теперь самая интересная часть Марлезонского балета.
Компьютер, как известно, считает только в двоичной системе счисления. Человеку привычна десятичная. Так нахрена еще и шестнадцатеричную какую-то знать нужно?
Все очень просто. В умных книжках пишут, что «шестнадцатеричная нотация является удобной формой представления двоичных чисел». Что это значит?
Переведите число A23F из шестнадцатеричной «нотации» в двоичную. (Один из возможных алгоритм приведен в п.10.). В результате длительных манипуляций у вас должно получиться 1010001000111111.
А теперь еще раз посмотрите на таблицу в п. 8. (которую вы как бы уже и выучили) и попробуйте то же самое сделать в уме :
#13. Кстати (наверняка вы это уже знаете):
#1. Наверняка вы имеете представление о том, что такое переменная. Наиболее продвинутые даже знают, что переменная имеет тип. Кажется вполне естественным, что любой высокоуровневый язык программирования позволяет создавать любое количество переменных того или иного типа.
О специализации нам пока что говорить рано, описание наподобие «регистр-указатель базы кадра стека» вам вряд ли о чем-то скажет. Поэтому для начала познакомимся только с так называемыми регистрами общего назначения (РОН), и то не со всеми, а только с четырьмя основными, которые являются своего рода «рабочими лошадками» микропроцессора.
А сейчас мы поближе посмотрим на эти «рабочие лошадки» микропроцессора.
Не правда ли, весьма похоже на то, что показывают в художественных фильмах про хакеров?
Природа не терпит пустоты. Писателей приводит в ужас чистый лист бумаги.
Весьма скоро и вы при виде «пустых» регистров будете испытывать непреодолимое наркотическое желание чем-нибудь их заполнить.
Однако прежде чем мы сделаем это в первый раз, давайте уточним тип этих «переменных».
В общем, в умных книжках рисуют вот такую вот «нездоровую» схемку 3 :
А означает она следующее.
Очевидно, что присвоить AX значение, например, 72F9h, мы можем следующими способами:
Точно так же присвоить значение 78h регистру AH можно двумя способами:
То же самое, но для регистра AL:
Тех, кого смущают числа с буквами, мы со зловредной ухмылкой отсылаем к 1.1. Система счисления :-]
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. «Принудительно» присвоить регистру значение можно при помощи той же команды «R», только с параметром «имя собственное регистра».
выбросит вам на монитор
Введите после двоеточия, например, число 123 и снова нажмите на Enter:
На дисплее опять появится приглашение «-«, на которое мы отвечаем командой «R» без параметров и таким образом вновь просматриваем значения наших регистров:
4). «Первый справа» бит мы будем называть «нулевым». Однако нам попадались руководства, в которых это же бит обозван как «первый». Можно долго обсуждать тонкости русского языка (которые, к сожалению, не всегда понимает переводчик), однако это выходит за рамки данной книги. Просто имейте это ввиду, что можете с этим столкнуться, и будьте бдительнее, читая документацию.
Например, команда (параметр L8 означает «вывести 8 байтов»):
покажет вам системную дату в правом столбце дампа.
Итак, у нас есть оперативная память, в которую загружается программа перед ее выполнением (сразу же по нажатию на Enter из Norton Commander). Операционная система, которая, собственно, и загружает программу, сообщает процессору, что надо начать обрабатывать команды, которые в памяти начинаются с такого-то адреса. И здесь первый подводный камень, вернее скала, которую трудно не заметить.
Каким образом? А очень легко! Компьютер «распознает» как выход из программы специальную последовательность байтов. Например, для исполнимых файлов типа com (именно с этим типом файлов мы будем работать на начальном этапе) достаточно последовательности CD и 20.
Пробуем-проверяем? Ну конечно же! Только для этого вам понадобится какой-нибудь шестнадцатеричный редактор, например, HexWorkshop.
Запускать это ваше первое творение лучше из Norton или Volcov Commander (все же это пока что DOS’овская программулька).
Последнее и является единственным, что она пока что может делать (корректно выгружаться из памяти).
Вот и давайте создадим еще одну программу типа com со следующим «шестнадцатеричным содержимым»:
На высоком уровне это делает операционная система. Например, она не пытается загрузить в память для выполнения файлы с расширениями, отличными от COM, EXE и BAT (последний вообще не из этой оперы, но принцип сохраняется).
Почти такой же эффект, но с потенциально большей разрушительной силой может получиться, если управление получит ИСПОРЧЕННЫЙ код, который вроде бы «в основном» правильный, но часть его вместо инициализации переменных и прочих подготовительных действий в лучшем случае ничего не делает, а в худшем портит другой код и данные.
#4. Еще немного идеологии. О программе, которая выполняется в памяти.
Соответственно, и программа состоит из трех частей (сегментов): сегмента данных (data), сегмента кода (code) и сегмента стека (stack).
Оставим пока что «гнилой базар» про смысл словосочетаний «реентерабельный/рекурсивный код» и «адрес возврата». Чтобы не затруднять себе понимание происходящего, мы попытаемся абстрагироваться от всех этих ужасающих вещей и для начала заняться только кодом.
Посмотрите на машинные коды, и «что они делают» в #2. Немножко дополним эту «простыню». Например, командой «внести значение» 1234 последовательно в каждый из «регистров общего пользования»:
В этом вы можете убедиться, загрузив вашу программу myprg_1.com в debug (например, командной строкой
А вот дальше начинается самое интересное ))
#7. Вот что вы должны увидеть:
Возвратившись к #2, перенесем сюда «описание» машинных команд.
Соответственно, вместо шестнадцатеричных кодов мы легко могли вводить эти команды при помощи команды «A» (однако этим мы займемся позже).
Итак, вводим «T» и жмем на Enter!
Вводим команду «T» снова:.
Вводим команду «T» снова:.
Вводим команду «T» снова:
«Прибавить содержимое AX к BX». Оно? А то!
Вводим команду «T» снова:
«Переслать содержимое BX в CX». Сделано!
Вводим команду «T» снова:
«Очистка AX»? И точно: AX=0000!
Вводим команду «T» снова. И ГРОМКО РУГАЕМСЯ!!
Для тех, кому лень продолжать жать на букву «T», введите для разнообразия команду «G» (от английского GO). На монитор должна вывалиться надпись «Нормальное завершение работы программы».
#9. Только непонятно вот, почему вдруг между int 20 (CD 20) и надписью «Нормальное завершение работы программы» куча всяких «левых» непонятных команд (в том случае, если вы и дальше производили тарассировку, а не воспользовались «халявной» командой «G»)?
А потому, дорогие наши, что вы имели счастье нарваться на прерывание (interrupt)!
Ну, посудите сами, должна же операционная система ну хоть что-нибудь делать!!
Итак, я достаю свой толстый талмуд с описанием прерываний и выбираю, каким бы это прерыванием вас занять на ближайшие 1/2 часа ;).
Ну, например, вот одно симпатичное, под названием «прокрутить вверх активную страницу».
Внимательно читаем описание (и наши комментарии):
Далее представим входные параметры в виде таблички: (_7)
AH | 06h | AL | Число строк |
BH | Атрибут | BL | Не имеет значения |
CH | Строка (верх) | CL | Столбец (верх) |
DH | Строка (низ) | DL | Столбец (низ) |
Плюс подробнейшее толкование, что подразумевается под словом «атрибут» (регистр BH):
Источник
Как перевести код в машинный код
Главный герой находится в заточении. В абсолютной тьме. У него были выколоты глаза, но за год они регенерировали, и зрение постепенно к нему возвращается.
И однажды каким-то чудом в одной камере с ним оказывается загадочный Дворкин — создатель Лабиринта. Именно «чудом» — он просто появился неизвестно откуда. Он тоже находится «в заключении», но, в отличие от Корвина (главного героя), может спокойно ходить через каменные стены.
Удивленный Корвин спрашивает его:
— Как ты оказался в моей камере? Ведь здесь нет дверей.
Дворкин отвечает:
— Двери есть везде. Просто нужно знать, как в них войти.
Будем считать это эпиграфом.
1.1. Система счисления
#4. Очень наглядно это отображают обыкновенные счеты. Набранное на них число 35672 будет выглядеть. см. рисунок слева в общем.
Это (если сверху вниз считать) сколько на каждом «прутике» «костяшек» влево отодвинуто.
#5. Пальцев на руках у человека 10, поэтому и считать мы привыкли в системе счисления с основанием 10, то есть в десятичной. Если вы хорошо представляете себе счеты и немного поупражнялись в разложении чисел аналогично выражению 1, то перейти на систему счисления с основанием, отличным от привычной, особого труда для вас не составит. Нужно всего лишь представить себе счеты, на каждый прут которых нанизано не привычные 10 костяшек, а. скажем, 9 или 8, или 16, или 32, или 2 и. попробовать мысленно считать на них.
#6. Для обозначения десятичных чисел мы используем цифры от 0 до 9, для обозначения чисел в системах счисления с основанием менее 10 мы используем те же цифры:
Если же основание системы счисления больше десяти, то есть больше, чем десять привычных нам чисел, то начинают использоваться буквы английского алфавита. Например, для обозначения чисел в системе счисления с основанием 11 «как цифра» будет использоваться буква А:
Правда, при определенном основании (при каком?) буквы аглицкого алфавита закончатся.
Но нам это, пока что, глубоко фиолетово, так как работать мы будем только с тремя 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 из шестнадцатеричной в десятичную систему счисления:
Преобразования чисел в системы счисления с другим основанием проводятся аналогично. Счеты! Обыкновенные счеты, только с «плавающим» числом «костяшек» на каждом «прутике».
Правда, этим немножко затрудняется понимание происходящего? А ведь тоже десятичная система! И рисунок цифр как бы знакомый :-)))
Или вообще считать в 256-ричной системе счисления, используя в качестве «рисунка цифр» таблицу ASCII-символов! (По сравнению с вами, извращенцами, любой Биллгейтс будет девственником казаться!!).
#12. Теперь самая интересная часть Марлезонского балета.
Компьютер, как известно, считает только в двоичной системе счисления. Человеку привычна десятичная. Так нахрена еще и шестнадцатеричную какую-то знать нужно?
Все очень просто. В умных книжках пишут, что «шестнадцатеричная нотация является удобной формой представления двоичных чисел». Что это значит?
Переведите число A23F из шестнадцатеричной «нотации» в двоичную. (Один из возможных алгоритм приведен в п.10.). В результате длительных манипуляций у вас должно получиться 1010001000111111.
А теперь еще раз посмотрите на таблицу в п. 8. (которую вы как бы уже и выучили) и попробуйте то же самое сделать в уме :
#13. Кстати (наверняка вы это уже знаете):
#1. Наверняка вы имеете представление о том, что такое переменная. Наиболее продвинутые даже знают, что переменная имеет тип. Кажется вполне естественным, что любой высокоуровневый язык программирования позволяет создавать любое количество переменных того или иного типа.
О специализации нам пока что говорить рано, описание наподобие «регистр-указатель базы кадра стека» вам вряд ли о чем-то скажет. Поэтому для начала познакомимся только с так называемыми регистрами общего назначения (РОН), и то не со всеми, а только с четырьмя основными, которые являются своего рода «рабочими лошадками» микропроцессора.
А сейчас мы поближе посмотрим на эти «рабочие лошадки» микропроцессора.
Не правда ли, весьма похоже на то, что показывают в художественных фильмах про хакеров?
Природа не терпит пустоты. Писателей приводит в ужас чистый лист бумаги.
Весьма скоро и вы при виде «пустых» регистров будете испытывать непреодолимое наркотическое желание чем-нибудь их заполнить.
Однако прежде чем мы сделаем это в первый раз, давайте уточним тип этих «переменных».
В общем, в умных книжках рисуют вот такую вот «нездоровую» схемку 3 :
А означает она следующее.
Очевидно, что присвоить AX значение, например, 72F9h, мы можем следующими способами:
Точно так же присвоить значение 78h регистру AH можно двумя способами:
То же самое, но для регистра AL:
Тех, кого смущают числа с буквами, мы со зловредной ухмылкой отсылаем к 1.1. Система счисления :-]
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. «Принудительно» присвоить регистру значение можно при помощи той же команды «R», только с параметром «имя собственное регистра».
выбросит вам на монитор
Введите после двоеточия, например, число 123 и снова нажмите на Enter:
На дисплее опять появится приглашение «-«, на которое мы отвечаем командой «R» без параметров и таким образом вновь просматриваем значения наших регистров:
4). «Первый справа» бит мы будем называть «нулевым». Однако нам попадались руководства, в которых это же бит обозван как «первый». Можно долго обсуждать тонкости русского языка (которые, к сожалению, не всегда понимает переводчик), однако это выходит за рамки данной книги. Просто имейте это ввиду, что можете с этим столкнуться, и будьте бдительнее, читая документацию.
Например, команда (параметр L8 означает «вывести 8 байтов»):
покажет вам системную дату в правом столбце дампа.
Итак, у нас есть оперативная память, в которую загружается программа перед ее выполнением (сразу же по нажатию на Enter из Norton Commander). Операционная система, которая, собственно, и загружает программу, сообщает процессору, что надо начать обрабатывать команды, которые в памяти начинаются с такого-то адреса. И здесь первый подводный камень, вернее скала, которую трудно не заметить.
Каким образом? А очень легко! Компьютер «распознает» как выход из программы специальную последовательность байтов. Например, для исполнимых файлов типа com (именно с этим типом файлов мы будем работать на начальном этапе) достаточно последовательности CD и 20.
Пробуем-проверяем? Ну конечно же! Только для этого вам понадобится какой-нибудь шестнадцатеричный редактор, например, HexWorkshop.
Запускать это ваше первое творение лучше из Norton или Volcov Commander (все же это пока что DOS’овская программулька).
Что же она делает, эта 2-байтовая малышка? А ничего, просто этот файл обладает двумя важными свойствами:
Последнее и является единственным, что она пока что может делать (корректно выгружаться из памяти).
Вот и давайте создадим еще одну программу типа com со следующим «шестнадцатеричным содержимым»:
На высоком уровне это делает операционная система. Например, она не пытается загрузить в память для выполнения файлы с расширениями, отличными от COM, EXE и BAT (последний вообще не из этой оперы, но принцип сохраняется).
Хотя. вы всегда можете поэкспериментировать! Смените, например, у какого-нибудь текстового файла тип с TXT на COM и попробуйте его запустить на выполнение (хотя мы это делать настоятельно не рекомендуем!). В большинстве случаев ваш компьютер зависнет! Потому что:
Почти такой же эффект, но с потенциально большей разрушительной силой может получиться, если управление получит ИСПОРЧЕННЫЙ код, который вроде бы «в основном» правильный, но часть его вместо инициализации переменных и прочих подготовительных действий в лучшем случае ничего не делает, а в худшем портит другой код и данные.
#4. Еще немного идеологии. О программе, которая выполняется в памяти.
#5. Как уже говорилось в #3, одна и та же последовательность битов в памяти может быть:
Соответственно, и программа состоит из трех частей (сегментов): сегмента данных (data), сегмента кода (code) и сегмента стека (stack).
Оставим пока что «гнилой базар» про смысл словосочетаний «реентерабельный/рекурсивный код» и «адрес возврата». Чтобы не затруднять себе понимание происходящего, мы попытаемся абстрагироваться от всех этих ужасающих вещей и для начала заняться только кодом.
Посмотрите на машинные коды, и «что они делают» в #2. Немножко дополним эту «простыню». Например, командой «внести значение» 1234 последовательно в каждый из «регистров общего пользования»:
В этом вы можете убедиться, загрузив вашу программу myprg_1.com в debug (например, командной строкой
А вот дальше начинается самое интересное :)))
#7. Вот что вы должны увидеть:
Возвратившись к #2, перенесем сюда «описание» машинных команд.
Соответственно, вместо шестнадцатеричных кодов мы легко могли вводить эти команды при помощи команды «A» (однако этим мы займемся позже).
Итак, вводим «T» и жмем на Enter!
Вводим команду «T» снова:.
Вводим команду «T» снова:.
Вводим команду «T» снова:
«Прибавить содержимое AX к BX». Оно? А то!
Вводим команду «T» снова:
«Переслать содержимое BX в CX». Сделано!
Вводим команду «T» снова:
«Очистка AX»? И точно: AX=0000!
Вводим команду «T» снова. И ГРОМКО РУГАЕМСЯ!!
Для тех, кому лень продолжать жать на букву «T», введите для разнообразия команду «G» (от английского GO). На монитор должна вывалиться надпись «Нормальное завершение работы программы».
#9. Только непонятно вот, почему вдруг между int 20 (CD 20) и надписью «Нормальное завершение работы программы» куча всяких «левых» непонятных команд (в том случае, если вы и дальше производили тарассировку, а не воспользовались «халявной» командой «G»)?
А потому, дорогие наши, что вы имели счастье нарваться на прерывание (interrupt)!
Ну, посудите сами, должна же операционная система ну хоть что-нибудь делать!!
Когда же и «кем» генерируются эти «сигналы» (в смысле «прерывания»)?
Итак, я достаю свой толстый талмуд с описанием прерываний и выбираю, каким бы это прерыванием вас занять на ближайшие 1/2 часа ;).
Ну, например, вот одно симпатичное, под названием «прокрутить вверх активную страницу».
Внимательно читаем описание (и наши комментарии):
ВХОДНЫЕ ПАРАМЕТРЫ: (_2)
Далее представим входные параметры в виде таблички: (_7)
AH | 06h | AL | Число строк |
BH | Атрибут | BL | Не имеет значения |
CH | Строка (верх) | CL | Столбец (верх) |
DH | Строка (низ) | DL | Столбец (низ) |
Плюс подробнейшее толкование, что подразумевается под словом «атрибут» (регистр BH):
ВЫХОДНЫЕ ПАРАМЕТРЫ: отсутствуют (т.е. ни один регистр не меняется). (_8)
Входные строки гасятся в нижней части окна. (_9)
Совсем недавно, если бы вам показали подобное «описание», вы бы ничего в нем не поняли и ужаснулись. Теперь же, после прочтения предыдущих глав курса, в эти «таблицы» вы, более или менее, но «въехать» должны! Тем более, что сейчас я сделаю комментарии для. хм. «отстающих» учеников (внимательно смотрим на циферки в скобках):
_1. Черным по белому, в толстом талмуде, описывающем функции прерываний, написано: «Драйвер видео вызывается по команде INT 10h и выполняет все функции, относящиеся к управлению дисплеем».
И далее. «…ДЕЙСТВИЕ: после входа управление передается одной из 18 программ в соответствии с кодом функции в регистре AH. При использовании запрещенного кода функции, управление возвращается вызывающей программе.
НАЗНАЧЕНИЕ: прикладная программа может использовать INT 10h для прямого выполнения функций видео. «
Вот что из этого следует:
Нижеследующая табличка называется «Функции, реализуемые драйвером видео»:
Соответственно, если перед выполнением INT 10 в регистре AH будет значение 06h, то выполнится именно «прокрутить вверх активную страницу», а не что-то другое из «простыни» функций десятого прерывания.
Теперь читаем описание дальше (смотрим на циферки в скобках):
_2. Входные параметры? Что тут может быть непонятного? Даже запуск ракеты с атомной боеголовкой требует прежде всего указать координаты цели. Чего уж тут говорить об обыкновенной функции?
_6. Как известно из школьного курса геометрии, прямоугольник можно построить по двум точкам. Это утверждение справедливо и для окна, в котором мы желаем проскроллировать наш текст.
_7. Резюме того, что было написано выше.
_8. К примеру, попала ли наша ракета в цель или нет ;).
_9. Если бы мы использовали функцию 07h, то было бы глубокомысленно написано, что «строки гасятся в верхней части окна».
_10. Это то самое, которое в DOS по умолчанию. Т.е. белыми буквами на черном фоне. Правда, это 07h лучше все же рассматривать как 00000111b 🙂 но это уже совсем другая проблема.
#4. А теперь мы напишем программу. Ручками, без использования компилятора. Запускаем наш любимый debug.exe, вводим команду «а» и судорожно стучим по клавиатуре:
Сначала запускаем из-под Norton Commander. Затем запускаем из-под debug. Трассируем. Открываем в HEX-редакторе. Смотрим на «бессмыслицу» шестнадцатеричных циферек. Медитируем, медитируем и еще раз медитируем.
1.6. Немножко программируем и немножко отлаживаем
Давайте немножко видоизменим программу, которую мы писали в прошлый раз. Сделаем так, чтобы наше «окошко скроллинга» располагалось более или менее посередине экрана.
#2. Вот к каким умозаключениям вы должны были придти, пораскинув мозгами. За атрибут (то бишь цвет) у нас отвечает регистр BH. Он был равен 10h, а нужно на 10h больше. это, значит, 20h будет.
А еще можно одним махом в CX записать число 0510h той же командой mov.
Правда, красивые циферки-буковки? Набиваем-набиваем! Если сейчас к вам подойдут недZенствующие приятели/коллеги и посмотрят, что вы тут колупаете, то ни черта не поймут и покрутят пальцем у виска. Привыкайте к этому. Только не говорите им, что пытаетесь сейчас получить Матрицу, ПОТОМУ ЧТО это неправда. А неправда это потому, что сейчас Матрица в очередной раз обманула вас!
#3. ВСЕ ПОТОМУ, ЧТО МОЗГАМИ ДУМАТЬ НАДО, А НЕ ТОЛЬКО СЛЕПО СЛЕДОВАТЬ РУКОВОДСТВУ!
1. Что вам мешает после команды «a» указать адрес, который вы желаете переассемблировать? И благополучно заменить старую команду на новую!
— А что делать, если не переассемблировать нужно, а вообще удалить?
Кстати, если процессор встретит команду NOP, то он просто побездельничает некоторое очень короткое время.
ПРОБУЙТЕ!! В конце концов, ваша прога должна принять такой вот вид:
Мы просто приведем вам последовательность действий, а вывод «зачем это нужно» и прочие возможные выводы вы уже сами делать будете. Ок?
Состояние: обнулился регистр AX (первую команду MOV AL,AL мы не видим). Процессор готовится выполнить команду MOV BH,10. Дадим ему это сделать!
Вот теперь-то мы и делаем это «на лету»:
А действительно ли сделали? Проверим!
Состояние? BH теперь равно 40h! Мы «вклинились» между строчками:
И изменили текущую цепь событий, заставив программу делать ТО, ЧТО НАМ НУЖНО! Поздравляю!
Что такое тяжелая атлетика вы наверняка знаете. Видели по телевизору, когда выходит на помост этакий здоровяк, и рвет собственное здоровье, поднимая штангу.
Это вам не очередь времен социализма. Это очередь «загрузки-разгрузки» стека!
#2. Для работы со стеком вам пока что необходимо знать только две команды: push и pop. Так как в качестве «блинов» у нас регистры, то, соответственно, необходимо после этих команд указывать и «имена собственные» помещаемых в стек значений регистров.
Ну а как делать то же самое с остальными регистрами вы, наверняка, уже и сами догадались.
С очередностью заполнения стека, наверное, все понятно :). Я много про абстрактные «блины» загружал. А вот с адреса 114 начинается извлечение из стека. В какой последовательности это делается, вы можете увидеть сами, произведя трассировку этой небольшой проги.
Анализируем. Прога еще не начала работать, готовится выполниться команда по адресу 100. Делаем ШАГ!
Проехали до адреса 114.
А вот теперь снова анализируем :). При следующем шаге выполнится команда, извлекающая некогда «запомненное» значение AX из стека.
Обратите внимание, регистр IP указывает на адрес (114) выполняемой команды. Мы с вами это уже проходили, не так ли?
AX=1 🙂 То есть нашлись-таки наши 1, 2, 3, 4, 5 :). Восстановились из стека. Теперь поверили? А то!
Медитируйте над этой темой до полного просветления! Иначе потом придется туго!
Еще более очевидно, что простая команда POP AX (с 114 по 119) повторяется у нас тоже 5 раз.
Мне почему-то сразу вспомнился анекдот о том, как два мента едут в машине, и один спрашивает у другого: «Глянь, работает ли у нас мигалка на крыше». Тот высунул в голову в форточку и говорит: «Работает-не работает-работает-не работает-работает-не работает. «
Так вот, не будем уподобляться этим нехорошим людям и сделаем нашу прогу более нормальной.
Реализуется же он (цикл), например, при помощи регистра CX и команды LOOP следующим образом.
Число циклов заносится в регистр CX. После этого следует «простыня» из команд, которые вы хотите «зациклить», т. е. выполнить энное количество раз. Заканчиваться все это должно LOOP’ом с указанием адреса «строки», с которой необходимо начать цикл (обычно это «строка», следующая сразу же после mov СХ.
Давайте мы сначала «набьем» нелинейный вариант нашей проги, а потом разберемся, что там к чему. Набиваем:
Протрассируйте эту программу! Искренне надеюсь, что вы поняли, чем это я вас тут загружал.
#2. А теперь вопрос на засыпку ;). Сколько раз выполнится следующий цикл:
Самые подозрительные могут сразу же посмотреть на этот цикл под отладчиком, и с удивлением обнаружат, что LOOP сначала уменьшает значение CX (0-1=FFFF), а потом уже проверяет, не равен ли он нулю. И с гордостью за задний ум своей головы воскликнут: FFFFh раз!
Так вот: этот ответ близок к истине, но тоже неправильный 😉
Но только вы и мне не верьте! Истинно только то утверждение, которое вы сами проверили на практике. Медитируйте!
1.9. Немножко оптимизации
Попытайтесь оттрассировать «зацикленную». Не правда ли, она трассируется намного дольше своего линейного аналога?
Угу, всё поняли? Сам знаю, что ни черта. 🙁
Объясняю: в «зацикленной» программе «компутеру» приходится выполнять БОЛЬШЕ команд, нежели в «незацикленной».
Аргументирую это голословное утверждение следующей таблицей (построенной на основе трассировки):
Ну и как по-вашему, какую из двух простыней процессор быстрее обработает? Сказать вам по секрету? А вот ничего я вам не скажу! Сами думайте! :]
Как сейчас помню, был в моем Турбо-Си в преференсах к компилятору такой радиобуттон: «оптимайзить» по размеру или по скорости выполнения. Угадайте, на чем основан принцип этой оптимизации?
1.10. Разборка с процедурами
Если вы так подумали, то оказались совершенно правы! Программу из #4 запросто можно было представить в таком вот виде:
И несмотря на то, что размер ее оказался несколько большим, она тоже будет работать правильно :).
Внимательно всмотритесь в полный текст программы и в этот выделенный кусок. И помедитируйте над ним до полного просветления текущей «обстановки».
Все более чем просто. Когда в программе встречается CALL с указанием АДРЕСА-НАЧАЛА-ПРОЦЕДУРЫ (в нашем случае это 011E), то компьютер «идет» по этому адресу и выполняет все команды, расположенные между «точкой входа» (включительно) и командой RET, то есть так называемое «тело» процедуры.
Не ругайтесь. Мы знаем, что вы ни черта не поняли. А по сему набьем в debug’е эту прогу и посмотрим, что она делает.
Те, кто читал внимательно, могут отметить, что инструкция CALL по своему действию очень похожа на инструкцию генерации прерывания INT, с той лишь разницей, что аргументом CALL является адрес процедуры, а не индекс в таблице «векторов прерываний», где и хранится адрес обработчика прерывания (той же процедуры). А для особо продвинутых отметим, что в ранних моделях процессоров от Intel при подаче запроса на обработку от внешнего устройства контроллер прерываний, помимо собственно сигнала прерывания, посылал в процессор инструкцию CALL.
#3. Кстати, вы уже поняли, почему мы называем debug «до боли любимой программой»? Нет? Неужели вы еще не полюбили это произведение программерского гения всеми фибрами своей души? Еще нет? М-да. мы в вас разочаровались.
Не правда ли, красиво получилось?
Итак, до адреса 0110 вам все должно быть понятно, мы это рассматривали. Трассируем дальше.
Мысленно мы ругаем вас нехорошими словами (ну сколько раз повторять-то можно!), а вслух скажем: Команда «T» и Enter. Команда «T» и Enter. Команда «T» и Enter…
Команда CALL 011E по адресу 0110 говорит процессору: «Дальше мы не пойдем, пока не выполним простыню, начинающуюся по адресу 011E». И далее, естественно, следует переход на этот адрес.
Входим в тело процедуры, начиная с 011E, и выполняем команды до 012D включительно.
А теперь внимательно смотрим, на какой адрес нас «перекинет» команда RET.
На 113-й? И это правильно! По 113-му адресу у нас какая команда? Да вот опять CALL 011E!
Опять процедура с адреса 011E, опять RET[URN] на строку ниже, то есть на 116.
Короче, куда бы вас не посылали всяческие «столбы с указателями», конец вашего пути только один. А плутать вокруг да около этого конца вы можете сколько вам заблагорассудится.
Кстати, именно это и является одной из многочисленных тайн программинга.
#4. Те, кто внимательно ознакомились с циклами, они и на этом не остановятся! Посмотрев на адреса 110. 119, они вообще возьмут и возомнят себя воистину крутыми парнями! Знаете, что они напишут? А вот что (предвидим!):
То бишь еще и CALL в цикл при помощи MOV CX,4 и LOOP’а «закрутят». И что? А попробуйте!
Что, не «пашет»? А что надо делать, если «не пашет, а должно бы»? Правильно! Смотреть из-под отладчика!
А теперь попробуйте выкрутиться из этой нехорошей ситуации с использованием стека :). Кстати, весьма «мозгопрочищающая» задачка :).
Соответствено, для успешного перехода необходимо указать: ПРИ КАКОМ УСЛОВИИ выполнить переход, КУДА ПЕРЕЙТИ, ну и, наконец, сам пинок под зад нужно СДЕЛАТЬ, чтобы переход «гостя» в заданном направлении все-таки «состоялся».
Например, на первом шаге можно использовать как «аптекарские весы» инструкцию CMP, которой обязательно нужно указать, ЧТО и С ЧЕМ она будет сравнивать.
В зависимости от значений регистров у нас возможны следующие состояния: «наклон влево» (AX &tt; BX), «наклон вправо» (AX > BX) и «равновесие» (AX = BX). Таким образом ВЫЧИСЛЕНИЕ УСЛОВИЯ у нас уже организовано :). Только условие не бинарное, а есть еще и «серединный вариант» (и даже несколько других!). Это нормально. Это для того сделано, чтобы мы могли выражения типа «больше-или-равно», «меньше-или-равно» да и просто «равно» в своих программах использовать.
Итак, УСЛОВИЕ есть. Теперь решаем, что нам делать при том или ином условии. Вот далеко не полный список возможных «прыг-скоков»:
Естественно, что после мнемоники («прыгнуть, если») должен стоять АДРЕС, куда нужно «прыгнуть», если условие соблюдено. Если же условие не соблюдено, то прыжок не происходит, и выполняется нижеследующая строка программы.
А о чем это мы? Ах да, переходы.
#3. Продолжим программировать, что ли? Напишем что-нибудь красивое и неизменно тупое? С использованием условных и безусловных переходов?
Поехали! Слабаем мы сейчас что-то наподобие графического редактора :)). Не верите?
Как работает последний кусок кода, обязательно проверьте под отладчиком, это полезно :).
#4. И лезем, лезем в наш горячо любимый DZEBUG, дабы набить там драгоценные строчки машинного мнемонического никому-кроме-вас-непонятного кода!
Тут один из автору вставили шпильку:
#5. А сейчас мы это все дело прокомментируем:
Правда, здорово получилось?
#1. Работать с кодом мы с вами научились. Сейчас поучимся заставить наш код обрабатывать данные.
Итак, запускаем DZEBUG и вводим следующую команду:
Которая означает: «набиваем память всяким дерьмом начиная со смещения 115».
В ответ он вам выплюнет:
Если вы вознамеритесь последовательно ввести 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.
Задание простое. Есть у нас табличка типа:
Для начала мы наберем «исходные данные» и зарезервируем место (например, забьем нулями) под третий столбец, в который собираемся помещать результат.
Набиваем блок данных, начиная с адреса, например, 115:
Вот так это у меня в DZEBUG’е выглядело :). Только я еще дамп посмотрел, правильно ли я ввел:
Вроде правильно :)). Ну а программу я вот какую придумал:
Команда по адресу 106 забирает в AL цифирь из первого столбца.
Ну и ADD BX,3 для перехода на следующую строчку :).
Сделайте трассировку (внутрь INT 20 залезать не надо) и посмотрите на дамп нашего блока данных 🙂
Я и говорю: ПРОЩЕ ПАРЕНОЙ РЕПЫ!! 😉
#5. Видите? В качестве переменных «в компьютере» можно использовать не только регистры, но и «куски» памяти! А уж там вы можете клепать свои переменные в почти неограниченном количестве! Единственное, что нужно иметь ввиду: с переменными-регистрами компьютер работает намного быстрее, чем с переменными-в-памяти :).
Кстати, если вы хотите сохранить плод своих сегодняшних трудов на веник, то имейте ввиду, что вы и сегмент данных тоже должны сохранить! То есть: вам нужно сохранить весь «диапазон» от адреса 100 до 123 включительно :).
Ну и, само собой, при попытке дизассемблирования с адреса 115 у вас абракадабра пойдет. мы об этом уже говорили и упоминали один из принципов фон Неймана.
Полагаю, вы уже поняли, что значит «выучить язык ассемблера» 🙂 и теперь с удовольствием кинете грязью в того, кто скажет вам, что это сложно 😉
Да о чем это я, в общем-то? (Утомлен кофием, поэтому речь несвязна)? Просто хотел сообщить вам, что первая часть курса закончилась. Вооружившись справочником команд и прерываний, вы уже можете программировать под дос. Если вы внимательно штудировали предыдущие главы, то идеология этого дела (под дос) вам уже должна быть понятна как 2х2=100b.
Источник