Ассемблер для блондинок (и блондинов). И блондинов
Скачать 0.75 Mb.
|
mov и оператора offser. Аргументом оператора является адрес (переменная, а результатом значение этого адреса. Первым операндом такой команды должен быть 32- разрядный регистр (в другой регистр адрес не поместится) – с помощью команды lea – загрузка исполнительного адреса в регистр. У этой команды 2 операнда первый – разрядный регистр, второй – адрес. Команды mov ebx,offset X lea ebx,X Делают одно и тоже (флаги обе не меняют, первая на 1 байт короче. Заметим интересную особенность команды lea (лайфхак!!!). В качестве второго аргумента может фигурировать непросто адреса адресное выражение, в том числе с модификаторами и множителями (как только что было описано выше. Таким образом, команда lea eax,[ecx][4*ebx]+5 фактически выполняет присваивание eax:=ecx+4*ebx+5, то есть в некоторых случаях несколько команд сложения-умножения можно заменить одной командой. Примеры программ 1. Пусть в памяти хранится текст, некоторое количество слов. Задается номер начального символа и количество символов в слове. Вывести слово. Слова в строке запишем с пробелами, чтобы легче было читать, на самом деле пробелы ненужны. Нужный фрагмент на Ассемблере T db 'Собака Слон Кошка Крокодил' mov число загрузка номера начала слова в регистр (нумерацию начинаем с 0) mov число загрузка длины слова в регистр PRINT: outchar T[ebx] inc ebx loop PRINT Для печати слова «Cлон» надо задать числа 7 и 4. Можно перед циклом воспользоваться загрузкой адреса в регистр ив цикле имя массива не использовать (это нам пригодится при программировании процедур. mov ebx,offset T; загрузка адреса начала текста в регистр add число добавление к адресу номера начала слова mov ecx,4; загрузка длины слова в регистр PRINT: outchar byte ptr [ebx] inc ebx loop PRINT Можно адрес начала массива загрузить в другой регистр ив макрокоманде использовать два модификатора 2. Вести в массив числа в формате dd (последнее число 0, чисел на входе не больше 30). Напечатать введенные числа в обратном порядке. include console.inc .data mas dd 30 dup (?) .code BEGIN: mov esi,-4 mov ecx,0; счетчик количества введенных чисел nnn: add esi,4 ; регистр увеличиваем на 4, таков тип элементов массива inint Введите число" inc ecx cmp mas[esi],0 jne nnn; если не 0, продолжаем цикл newline Comment @ Теперь вывод. Так удобно оказалось, что к началу этого цикла все готово – в регистре ecx количество введѐнных чисел, в esi – смещение от начала массива до последнего числа @ kkk: outint mas[esi],10 sub esi,4 loop kkk newline exit end BEGIN Отметим, что при работе с массивами надо очень внимательно следить, чтобы регистр модификатор изменялся не на 1, как мы привыкли в Паскале, а на величину, соответствующую типу элементов 1 для байтов, 2 для слов, 4 для двойных слов, 8 для восьми байтовых чисел. Иначе мы рискуем получить данные, которые никто не вводил. Скажем, в случае только что рассмотренного массива, если по ошибке изменять регистр ESI на единичку, возникнут числа, состоящие из байтов соседних элементов, например, в качестве второго числа будут использованы 3 байта первого числа и один байт второго. Если вспомнить, что числа хранятся в перевернутом виде, даже сложно представить, какой хаос получается. 3. Циклический сдвиг массива на 2 элемента вправо include console.inc N equ 5 .data X dd N dup( ?) .code BEGIN: mov ebx,-type X mov ecx,N VVOD: add ebx,type X inint X[eBX],'?' loop VVOD mov esi,X[ebx]; сохранили последний mov edi,X[ebx-type X]; сохранили предпоследний mov ecx,N-2; оставшиеся N-2 будем двигать CIKL: mov eax,X[ebx-2*type X] mov X[ebx],eax sub ebx,type X ; уменьшили индекс loop CIKL; в цикле передвинем mov X,edi последние 2 элемента надо поставить на 1 место mov X+type X,esi; и на второе ; печать массива mov ecx,N; количество элементов mov ebx,0; начнем с нулевого VIV: outint X[ebx],10 add ebx,type X loop VIV newline exit end BEGIN К сожалению, несмотря на использование в работе с массивом оператора type, заменить в описании массива тип (чтобы программка при этом осталась работающей) просто так не получится. Элементы массива сохраняются в регистрах, регистр указывается явно. Работа с матрицей В программе ниже показаны разные приемы работы с матрицей. К сожалению, при изменении типа компонент матрицы программа тоже работать не будет, так как для вычислений используются регистры. Обратите внимание, что с матрицей можно работать как с линейным массивом (чем она и является) обычным образом переходя от одного элемента к другому и только при печати "вспомнить, что надо элементы вывести на разных строках. Для такой работы достаточно одного регистра-модификатора. Можно использовать два модификатора один отвечает за смещение по строке, другой – по столбцу. При этом надо внимательно следить за правильностью изменения значений регистров при переходе от одной строки к другой. Модификатор надо увеличить не на единичку, (как в Паскале, и даже не на тип элемента массива (как при переходе к соседнему элементу, а на количество байт, которое занимает строка, то есть надо количество элементов в строке умножить на тип элемента. include console.inc comment * Работа с матрицей. Матрица не вводится, а задается константами - в переменной matr считается сумма всех элементов матрицы * .const d equ 5; количество столбцов матрицы h equ 3; количество строк .data matr dw 1, 2, 3, 4, 5 dw 0, 0, 9, 8, 7 dw 6,-5,-4,-3,-2; можно задать всѐ водной строке .code Start: ClrScr ConsoleTitle " Матрица" ; Работа со всеми элементами матрицы. Матрица как ; линейный массив, использование одного индекса xor eax,eax; здесь будет сумма xor ebx,ebx; обнуляется индексный регистр,чтобы ; начать работу с первым элементом mov ecx,d*h; количество элементов в матрице sum: add eax,matr[ebx*type matr] inc ebx loop sum outintln Сумма элементов матрицы' ; Работа со всеми элементами матрицы, ; использование двух индексов outstrln 'печать матрицы' xor ebp,ebp; обнуление для движения вниз ( индекс строки) r0: xor ebx,ebx ; обнуление для движения по строке вправо (индекс столбца) r1: outint matr[ebp][ebx],8 add ebx,type matr; индекс столбца увеличивается на размер элемента cmp ebx,d*type matr jne r1 newline add ebp,d*type matr; индекс строки увеличивается на размер строки cmp ebp,d*h*type matr jne r0 ; Вычисление адреса элемента по его позиции (вводится позиция, ; печатается элемент, который на ней стоит) inint номер строки' dec eax mov edx,d*type matr mul edx inint номер столбца' dec ebx outintln matr[eax][ebx*type на этом месте стоит' ; Работа с элементами столбца. Загрузка адреса в регистр inint введите номер столбца, который надо напечатать' lea ebx,matr[ebx*type matr-type matr] ; адрес первого элемента заданного столбца mov ecx,h xor edx,edx sta: outint dword ptr [ebx+edx*type matr],8 add edx,d; перехд к элементу следующей строки loop sta newline exit end Start 6. Подсчет. Входные данные в массиве не хранятся. Массив используется для хранения данных, полученных при обработке входной последовательности. Условие задачи. Ввести непустую последовательность неотрицательных чисел, не превышающих 15, последовательность заканчивается –1. Общее количество чисел не превышает 256. Определить, сколько раз каждое число входит в эту последовательность. Поясним ограничения число от 0 до 15, чтобы использовать массив размером 16 элементов. Соответственно, при изменении диапазона чисел надо просто изменить длину массива. Количество чисел не превышает 256, чтобы при любом раскладе это количество поместилось в компоненту массива длиной в байт. Соответственно, если количество чисел будет больше, надо взять массив слов, для индексации массива надо будет использовать модификатор с множителем. include console.inc .data DOP db 16 dup(0); дополнительный массив для подсчета ; количества вхождений каждого числа ; вначале массив заполнен нулями .code start: VVOD: outstr 'Введите число не превышающее 15 признак конца -1 ' inint ebx cmp ebx,-1 je Fin cmp ebx,0 jl Neprav cmp ebx,16 jge Neprav ; сюда попали, если число от 0 до 15 inc DOP[ebx]; увеличиваем соответствующий элемент массива jmp VVOD Neprav: outstrln 'Неправильное число' jmp VVOD Fin: newline mov ebx,0; загрузка индекса, распечатываем массив сначала в массиве 16 чисел mov eax,0 Print: outword ebx,8; печатаем число outwordln DOP[ebx],8,'-' ; печатаем, сколько раз оно входит в последовательность inc ebx dec ecx jne Print exit end start 12. Запись операндов в командах Мы узнали, что в команде операнд может быть не только числом или просто адресом, а ещѐ и адресным выражением. Посмотрим, как правильно записывать операнды разных видов. Про команды, в которых 2 операнда Если типы обоих операндов известны (регистры, память, то они должны быть одинаковы. A dd ? cmp A,eax; верно cmp A,al; неверно Для изменения типа адресного выражения перед адресом можно использовать оператор ptr cmp byte ptr A,al; возьмет байт по адресу А, то есть младший байт Писать ptr перед регистром нельзя – ошибка. По правилам писать ptr можно перед любым адресом и указывать любой тип. Практически имеет смысл только "уменьшение" типа – как в примере, от двойного слова взяли один байт. Наоборот, если переменная байта ее расширяем до слова – не ошибка, работать будет, но получается неизвестно что, так как захватывается соседняя, следующая ячейка. Если известен тип одного операнда (например, регистра тип второго – не определен (косвенная адресация, квадратные скобочки, в этом случае тип второго операнда определяется по типу регистра. Оператор здесь ненужен (будет лишним. Здесь надо быть особенно внимательным, можно сделать ошибку, которую транслятор не показывает. Например, используется байтовый массив X и есть такие команды lea ebx,X mov [ebx],eax; в байтовый массив помещается двойное слово, ; то есть изменяется не один байта сразу 4 Если известен тип одного операнда (регистр, адреса второй – непосредственный операнд (число. В этом случае число должно помещаться в размер операнда. Если оно больше – ошибка mov eax,12345678; верно mov eax,'F'; верно mov al,300; ошибка Перед первым операндом (адресом) может стоять ptr с указанием типа. Синтаксис языка не запрещает поставить ptr с указанием типа и перед числом, но делать этого не стоит, потому что по определению оператор указания типа переменной в памяти. Логично, что зато есть, за ссылкой (pointer), стоит адрес переменной. Тем более, что ассемблер работает в этом случае "странно, число уменьшается только в случае необходимости (если первый операнд маленького типа, иначе указатель типа игнорируется mov al,byte ptr 300; al:=44 (???) mov ax,byte ptr 300; ax:=300, ptr игнорирует Если тип первого операнда не определѐн (косвенная адресация, а второй – непосредственный операнд (число. Это ошибка, каким бы ни было число. mov [ebx],12345 Ошибка. Обязательно нужно писать ptr c указанием типа. Логичнее это делать для первого операнда, потому что именно он является адресом mov dword ptr[ebx],12345 – так правильно. mov [eax],byte ptr 300 Так тоже синтаксически правильно, запишет 44 в байт, но лучше так не делать, нелогично. Если в команде один операнд, то, как правило, должен быть известен его тип div [ebx]; ошибка, надо использовать ptr 13. Стек Теорию читаем [1. глава 6] Стек – выделенная область памяти, с которой можно работать как обычными, так специальными стековыми) командами. Обычно память в стеке не именуется, доступ к памяти осуществляется с помощью адресов в регистрах. Главным, отвечающим за адресацию в стеке регистром, является регистр ESP вспомним, это тот самый регистр, который нельзя использовать как регистр-модификатор с множителем. Вначале работы программы ему автоматически присваивается некоторое значение – адрес начала стека, его "дно" (точнее, вначале он указывает не на само "дно, а стоит за ним, на свободном месте за концом стека. При записи информации в стек ESP уменьшается, при чтении – увеличивается. Вовремя работы программы в ESP хранится адрес вершины стека, то есть адрес последнего записанного в стек элемента. Стековые команды Команды для работы со стеком push и pop имеют один операнд размера двойное слово (на самом деле еще и слово, номы будем работать со стеком только с двойными словами. Команда push уменьшает значение ESP и заносит в стек по получившемуся адресу свой операнд (он может быть из памяти или из регистра. Команда pop считывает двойное слово из стека (по адресу ESP) в свой операнд (адрес или регистр) и увеличивает значение ESP. Примеры push 3 pop X push eax Заметим, что, если операндом стековой команды является адрес, команда выполняет действие типа "память-память". Мы при этом говорили, что команд формата "память-память" не бывает. В чем противоречие Запрещены только команды, у которых два явных операнда из памяти. Формату этой команды – один операнд из памяти, второй в явно команде не указывается, содержится в регистре ESP. Можно записать и прочитать из стека регистр флагов EFLAFS, это делают команды без операндов pushfd и popfd. Можно записать в стеки прочитать из стека все 8 регистров общего назначения командами и popad. Чтобы освоить работу со стеком, полезно провести следующие "эксперименты. Посмотрим, чему равно значение ESP вначале программы (это же обычный регистр, его можно распечатать. Потом запишем в стек несколько чисел, посмотрим, как изменится ESP. outwordln esp push 111 push 222 outwordln esp Чему равно значение ESP вначале неважно (оно может быть разным на разных компьютерах, важно, что оно уменьшилось на 8, так как в стек записали 2 двойных слова. Если теперрь эти значения вытащить из стека двумя командами pop, значение ESP станет таким же, каким было вначале. Именно так можно отслеживать, не стал ли стек пустым сохранить начальное значение ESP и проверять, не достигнуто ли оно. Это бывает нужно для предотвращения ошибок в программе, чтобы не было попыток чтения из пустого стека (при этом, конечно, программа аварийно завершается. Второй способ контроля пустоты стека – считать, сколько чисел в него поместили, сколько удалили. Описанный выше прием можно использовать и для быстрой очистки стека. Понятно, что для очистки стека надо можно команду pop столько раз, сколько в нем на данный момент чисел. Однако можно просто вначале работы программы запомнить значение ESP и для очистки стека присвоить ESP это запомненное значение. Аналогичным образом можно не полностью очищать стека только от какой-то порции информации. С информацией, находящейся в стеке, можно работать не только с помощью команд push и pop. Числа, находящиеся в стеке, можно считывать и изменять, используя адресацию с помощью регистров, те. работая со стеком, как с обычной памятью. При этом нам известен адрес вершины стека (в каждый конкретный момент) он находится в регистре ESP и тип компонент – 4 байта. Поместим в стек 4 числа и выведем их на экран, не убирая из стека (те. не пользуясь командой pop) push 111 push 222 push 333 push 444 ; забросили числа в стек mov ebp,esp; запомнили адрес начала массива mov ecx,4; счетчик для работы цикла cykl1: outint dword ptr [ebp],5; ; выводим число, находящееся по адресу [EBP] add ebp,4; числа – двойные слова, изменяем регистр на 4 loop cykl1 Заметьте мы в качестве регистра-модификатора использовали EBP, а не ESP. Это важно Использовать в качестве модификатора можно, но тогда, изменяя ESP, мы будем фактически "доставать" число из стека. То есть, если в данном фрагменте в качестве модификатора использовать ESP, работа фрагмента будет аналогична использованию 4 раза команды pop, стек станет пустым. Работу со стеком с помощью обоих приемов (стековые команды и адресация) можно посмотреть на примере следующей программы. Задача. Вести последовательность не менее х знаковых чисел формата dd, заканчивающуюся нулем. Напечатать числа, стоящие в этой последовательности после максимального (последнего из них, если их несколько. Сначала за максимальное мы вынуждены принять первое число (никакого другого нет. Все вводимые числа будем класть в стек, пока они меньше максимального. Если вдруг встретилось число, большее максимального, придется очистить стеки начать в него складывать числа заново, те, которые стоят уже после нового максимального. |