Ассемблер для блондинок (и блондинов). И блондинов
Скачать 0.75 Mb.
|
4. Резервирование памяти. Вводи вывод данных Напишем программу, которая позволит нам убедиться, что числа в памяти компьютера хранятся именно так, как об этом говорится в [1]. В секции .data зарезервируем память для хранения нескольких чисел, то есть, в терминах Паскаля, опишем несколько переменных. Какие имена можно дать переменным Имя должно начинаться с буквы латинской, может состоять из букв, цифр и символов из набора @ _ $ ? . Напомним, что большие и маленькие буквы в именах различаются. Имена не должно совпадать со служебными словами и названиями макрокоманд. В частности, нельзя использовать имена Proc, End, Inc, DB, DD, AX, Al, Ah – всего не перечислить, но потихоньку вы все эти ограничения освоите. Главное в директиве резервирования памяти не имя переменной, а размер выделяемой памяти. Он как раз директивой и определяется. ВОТ ГЛАВНЫЕ ДИРЕКТИВЫ db, dw, dd, dq. Они резервируют место в памяти размером соответственно в 1, 2, 4, 8 байт. Отметим, что это служебные слова, их можно писать любыми буквами, и большими и маленькими. Запомнить названия директив несложно первая буква от слова define, вторая – от byte, word, double word итак далее [1, разд. 6.4]. В общем виде директива резервирования памяти выглядит так имя переменной директива операнды [; комментарий Имя переменной и комментарий необязательны. Операндов может быть один или несколько, если операндов несколько, они отделяются друг от друга запятыми. Что может быть операндом директивы резервирования памяти 1. Операнд ?. Пример A db ?; описывается переменная A размером в байт. Переменная не получает начального значения, то есть в ней будет неизвестно что, мусор. 2. Операнд число. Пример B dw 500; АНАЛОГ НА Free Pascal var B: integer=500. Число должно умещаться в отведенную для него память (иначе будет ошибка в байт можно поместить число от -128 до 255, в слово – от -32768 до 65535, дальше посчитайте сами. Число может быть записано в разных системах счисления двоичной, восьмеричной, десятичной и шестнадцатеричной. Для определения системы счисления к числу в конце приписывается соответственно b (binary), o (octal), d (decimal) и h (hex). Букву d в конце десятичного числа можно не указывать, а в восьмеричном числе вместо o можно писать q (чтобы не путалось с нулем. Буквы могут быть большие и маленькие, нос маленькими буквами число смотрится понятнее. Небольшое замечание о шестнадцатеричных числах. Как вы помните, в таких числах могут встречаться латинские буквы A–F (можно использовать и большие, и маленькие буквы, но большие смотрятся красивее. Если число начинается с буквы, возникает путаница понимать запись как число или как имя переменной (например, A2h). Чтобы ликвидировать эту неоднозначность, договорились в таком случае к числам добавлять впереди ноль A2h – переменная, 0A2h – число. 3. Операнд – символ (в кавычках или апострофах. Пример Letter db 'a'; это var Letter: char='a'. Этот операнд отличается от операнда число только формой записи. Ассемблер понимает символ в кавычках как его код, то есть символ. Для переменной размером в N байт можно в кавычках записать N символов, например, c4 dd 'КАША, но обычно так не делают, так как символ хранится в байте и для работы с несколькими символами удобнее использовать массив байтов. В директиве резервирования памяти может быть несколько операндов, но об этом позже. Подробнее про директивы db и dw полезно почитать в [3 стр. Заведем несколько переменных разного размера, придадим им начальные значения в виде чисел в разных системах счисления, не забудем про символы в конце не десятичных чисел. Итак, обещанная программа include console.inc COMMENT * Вывод чисел разного вида * .data B db 200 W1 dw 200 W2 dw 32769 Q db 0ah F dw 11b L db 'a' .code Numbers: ClrScr ConsoleTitle " Числа разного вида" outint B,8 outwordln B,8 outint W1,12 outwordln W1,12 outint W2,12 outwordln W2,12 outwordln Q outwordln F outchar L outwordln L,5 exit end Numbers Смотрим, что получилось переменная B – байтовая, число 200, представленное в ней, трактуется по- разному в знаковом ив беззнаковом виде. А переменная W1 имеет тоже значение, но размер ее – слово, поэтому в знаковом и беззнаковом виде она выглядит одинаково. Подробнее о знаковых и беззнаковых числах [1, разд. 5.5]. Также видим, что числа в разных системах счисления напечатались в десятичном виде, а символ можно вывести как в виде символа, таки в виде кода. Если любопытно, можно посмотреть макрокоманду outnum, которая умеет выводить числа в ной иной системе счисления. 5. Распределение памяти Когда мы описываем переменные в программе на Паскале, компилятор сам как-то располагает их в памяти. На Ассемблере все четко определено. То есть конкретные адреса мы предугадать не можем, но взаимное расположение данных известно точно они находятся в памяти подряд друг за другом без всяких промежутков точно в том порядке, в каком описаны. В нашей программе адрес переменной W1 будет на 1 больше адреса B (так как B занимает один байта адрес W2 на 2 больше адреса W1 (так как W1 занимает байта. Убедиться в этом можно, напечатав значение адреса, например outwordln offset W1; offset W1 – это адрес W1 Можно также загрузить адрес в регистр (например, EAX): mov eax,offset W1 или lea eax,W1; адрес W1> Об этой команде, о регистрах – позже, сейчас просто можно этими командами посмотреть, чему равны адреса наших переменных. Теперь можно печатать значение адресов и из регистров, например lea eax,B ; записывает в регистр eax адрес B outwordln eax ; печатает содержимое регистра итак далее. Так как вместо имени переменной Ассемблера подставляет ее адрес, то есть число, то можно обращаться к переменной (то есть к месту в памяти с определенным адресом) не по еѐ имени, а по имени соседних переменных со смещением. Так, по адресу B+1 располагается переменная W1, а по адресу W1+2 – переменная W2. Учитывая взаимные расположения переменных в памяти, можно печатать их значения, указывая их адреса, например, выполнив макрокоманду outwordln W2-2 получим число 200. А что находится по адресу W1-1? Напечатаем outwordln W1-1 Получается совсем не 200. Почему так Дело в том, что, выполняя резервирование памяти, Ассемблер не только отводит место в памяти и запоминает адрес. С именем переменной также связывается тип данных. Переменная B у нас типа byte, а переменная W1 – типа word. Поэтому макрокоманда видит имя W1 и берет по адресу W1-1 слово, то есть 2 байта Посмотрим, что находится в этих байтах. В первом 200 и во втором тоже 200. Что получается В позиционной системе счисления по основанию 256 это 200*256+200=51400. Именно это число и будет напечатано. А можно представить число 200 в двоичной системе, выписать содержимое слова 11001000 11001000 и перевести это беззнаковое число в десятичную систему, опять получится 51400. Сами разберитесь, что напечатает макрокоманда outintln W1-1 Таким образом, имя переменной определяет не только еѐ адрес (те. "откуда брать, но и тип, размер числа (те. "сколько байт брать. Кстати, тип переменной можно узнать с помощью оператора type. Можно напечатать OutwordLn type A OutwordLn type F Получим 1 итак как тип считается в байтах. Кстати, здесь уместно вспомнить, что компьютер хранит числа в "перевернутом" виде сначала располагаются младшие байты, потом старшие [1, пс. Посчитаем, что находится в байтах с адресами Q-1 и Q-2, а потом проверим себя, напечатав outwordln Q-1 outwordln Q-2 Q-1=W2=32769=32768+1=2 15 +1 , в двоичной системе это 10000000 00000001 – число специально разделено на две части, это содержимое байтов. Вспомним, что в ЭВМ числа хранятся в перевернутом виде, сначала располагаются младшие байты, а потом старшие. Поэтому по адресу Q-1 находится старший байт W2 (это число 128), а по адресу Q-2 – младший байт W2 (число 1). Байты числа W2 можно напечатать и без помощи переменной Q, можно использовать и адрес W2, вот только тип надо изменить. Для этого используется оператор ptr. Перед этим словом ставится название типа outwordln byte ptr W2 outwordln byte ptr W2+1 Напечатается, естественно, тоже самое. Посмотрим теперь, как хранится в памяти "длинное" число размером в 4 байта. Опишем его Q dq 01020304h и распечатаем содержимое байтов OutWordln byte ptr Q OutWordln byte ptr Q+1 итак далее Увидим, что по адресу Q находится число 4, по адресу Q+1 – число 3, а далее числа 2 и 1. При переносе числа из памяти в регистр (при выполнении арифметических операций) компьютер сам "переворачивает" число, так что о "перевернутом" представлении чисел надо помнить только если зачем-то хочется работать счастью числа или посмотреть значение числа в листинге программы. Таким образом, следует усвоить, что в памяти компьютера лежат не числа, которые мы указали в директивах Ассемблера, там лежат нолики и единички. Когда мы хотим работать с содержимым памяти, мы может просто указать адрес и количество байтов, которое нам нужно, начиная с этого адреса. Про директивы с несколькими операндами. В директиве может быть несколько операндов, они перечисляются через запятую. Это следует понимать как отведение памяти для нескольких переменных, причем имя имеет только первая из них. К остальным можно обращаться, прибавляя к адресу смещение. Например, одни и те же байтовые переменные можно описать по-разному. Z db 'a','b','c','d' Z db 'abcd' Z db 'a' db 'b' db 'c' db 'd' Во всех этих фрагментах резервируется 4 байта памяти, первый из них имеет имя Z, остальные безымянные, к ним можно обращаться Z+1, Z+2, Z+3. Директива Y dd 1,2,3 резервирует место для трех переменных, каждая размером в двойное слово (4 байта. По адресу Y находится число 1, число 2 находится по адресу Y+4, число 3 по адресу Y+8. Несколько значений одного типа, располагающихся в памяти подряд, можно считать массивом. В Ассемблере нет никакого специального служебного слова для описания массива, нет и типа с таким названием. Тип переменной, которая дает массиву имя, определяется директивой резервирования памяти и никак не зависит от длины массива. Собственно никакой длины массива нет, ассемблер ее никаким образом "узнать" не может, никаких запретов для обращения к любому участку памяти по любому существующему имени. Для компактного задания массива из, например, 100 элементов используется директива X dd 100 dup (?) Она предписывает зарезервировать в памяти (подряд) 100 переменных размером в двойное слово, первая из этих переменных будет иметь имя X. 6. Регистры Для работы доступны 8 32-битных регистров со служебными именами EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP. При желании можно использовать только младшие 16 бит каждого регистра, при этом надо писать названия без буквы E, например AX, SI. Младшие части первых четырех регистров (тех, которые с буквой X) еще разделятся пополам, давая нам возможность работать с байтами. Названия байтовых регистров формируются так первая буква от названия регистра (A, B, C или D), а вторая L или H. L означает low (нижний, так как это самая младшая часть регистра, младшие разряды с ого по ой. H – это hight (верхний, старшая половинка слова, разряды с ого по 16-ый. Например, AL – это один байт из четырехбайтового регистра EAX, а DH – кусочек регистра EDX. При работе с регистрами надо понимать, что, меняя "кусочек, мы меняем и весь регистр и наоборот. Hапример, если после действий AL:=1 EAX:=0 , тов регистре AL будет 0, а после действий EAX:=0; AH:=2; AL:=1 в регистре EAX будет 0000 0000 0000 0000 0000 0010 0000 0001, это число 513 7. Команда Ассемблера Каждая команда пишется на отдельной строчке (допускается и приветствуется смещение некоторых команд вправо. Команда может быть помечена, после метки ставится двоеточие. Команда состоит из мнемокода (несколько латинских букв, регистр букв значения не имеет) и операндов (обычно 1 или 2, бывают команды без операндов. После команды на той же строке может быть комментарий (через точку с запятой. 7.1. Команда MOV Это команда присваивания. У нее два операнда. Общий вид mov op1,op2 перед командой может стоять метка, в конце – комментарий. Команда выполняет действие op1:=op2. Исходя из ее смысла, на операнды накладываются следующие ограничения Оба операнда не могут быть из памяти (в ассемблере вообще нет команд формата память-память, у которых два явных операнда из памяти. Операнды должны быть одного типа (для работы с операндами разных типов существуют другие команды. Первый операнд не может быть числом. Ассемблер работает с байтами, словами и двойными словами, то есть четверным словом (DQ) никакой операнд быть не может. Примеры mov ebx,eax; ebx:=eax mov PER,al: команда правильная, если описано PER db ? mov eax,bl; неверная команда, разный тип операндов Отдельно скажем про команду mov с непосредственным операндом (те. второй операнд – число, ведь у числа типа нет. Число должно быть таким, чтобы помещаться в первый операнд. Примеры mov eax,500; eax:=500 mov И eax:=200 (код буквы 'И) mov al,300; ошибка, 300 в байт не помещается Команда mov флаги не меняет. 7.2. Команда XCHG xchg op1,op2 Меняет значения операндов местами. Они должны быть одного типа, ни один не может быть числом, нельзя, чтобы оба операнда одновременно были из памяти. Флаги не меняет. Пример mov al,1 mov ah,2 outwordln ax; ax состоит из байтов 00000010 и 00000001 – число 513 xchg al,ah outwordln ax; байты поменялись местами, стало число 258 8. Отладка программы. Листинг А что, если программа она не работает Увы, такое случается часто. Если программа не запустилась насчет, в окне консоли появляется сообщение и информация об ошибке. Эту информацию следует прочитать, хоть она и по-английски. Если написано, что не найден файл с расширением .asm, значит в bat- файле неправильно указано имя программы на ассемблере, или программа находится не в той папке. Если не найден еще какой-то файл, скорее всего что-то напутано с размещением программы. Более частые ошибки – синтаксические. Давайте специально сделаем такую ошибку и посмотрим реакцию компьютера. Напишем неправильную директиву A db 300 ; число 300 в байт не помещается Запустим программу. Увидим, что производилось "ассемблирование" нашего файла (assembling). А дальше выписано, имя файла ив какой строке замечена ошибка a.asm(xxx), номер ошибки и ее расшифровка. Номер ошибки нам ненужен (вообще-то есть список всех ошибок, в котором можно, зная номер, про каждую из них прочитать, обычно из описания ошибки можно понять, в чем дело. Для нашего случая будет сообщение a.asm(xxx) : error A2070: invalid instruction operand В нашем случае сообщается, что значение слишком большое для данного типа. Если программа небольшая, можно сразу искать неправильную строку в тексте. Место строки с ошибкой можно посмотреть в листинге – это файл стем же именем, что и исходный файл на ассемблере, с расширением lst. Он создается ассемблером при трансляции программы, в нем программа представлена как в исходном виде, таки в "переводе на машинный язык" – в виде чисел в 16-ричной системе счисления. Посмотрим на строчки кода, соответствующие нашим директивам. Самая левая колонка – адрес ячейки. Правда, это не абсолютный адрес, по которому данная строка располагается в памяти компьютера, это смещение от начала секции (в нашем случае секции .code). Все остальное – совершенно "честно. Мы можем видеть, что адреса соседних переменных отличаются ровно на размер переменной (в байтах. После адреса в строке листинга записано содержимое байтов – перевод строки кода. Для директив это представление числа в 16-ричной системе (числа в компьютере, конечно же, представляются в двоичной системе, листинге все переведено в 16-ричную, так как такой текст удобнее читать. В нашей программе пока нет никаких команд (макрокоманды в нашем листинге не отображаются, но как только выбудете писать программы с командами – посмотрите и убедитесь, что в переводе на машинный язык они ничем не отличаются отданных. Давайте теперь сделаем ошибку в макрокоманде, напишем в ней операнд – неописанную переменную. Ассемблер нам показывает сразу несколько ошибок "неописанная переменная. В листинге видим, что после неправильной макрокоманды появилось несколько строчек с этой переменной, ив них всех ассемблер отмечает ошибку. Дело в том, что в макрокоманде переменная используется несколько раз, вот ассемблер и выписывает все неправильные строки из макрокоманды. Исправлять надо, конечно же, c саму макрокоманду. Итак, увидев сообщение об ошибке, смотрим в листинге, в какой строке она обнаружена, исправляем ошибку (исправлять надо, не в листинге, а в своем файле с расширением .asm). И не забудьте сохранить исправления 9. Арифметические команды 9.1. Сложение и вычитание Команды сложения (add) и вычитания (sub) в чѐм то похожи на команду mov: два операнда, результат кладется в первый. Работает с байтами, словами и двойными словами, то есть четверным словом (DQ) никакой операнд быть не может al,5; AL:=AL+5 sub ebx,ecx; ebx:=ebx-ecx Операнды должны быть одного типа, но результат при этом не обязан помещаться в первый операнд. Если в регистре AL лежит число 250, то при выполнении команды add al,10 в регистре AL окажется число 4. О том, как выполняется сложение и вычитание, как вырабатываются флаги, читайте [1, п. 5.5]. Команды inc и dec с одним операндом служат для прибавления или вычитания единицы. Они работают аналогично командам сложения и вычитания со вторым операндом 1 (первый операнд – регистр или память, ноу них есть некоторое отличие они не меняют флаг переноса CF. Команда изменения знака |