![]() |
1. Заголовок или название темы должно быть информативным
2. Все тексты программ должны помещаться в теги [CODE=asm] [/CODE]
3. Прежде чем задавать вопрос, см. "FAQ",если там не нашли ответа, воспользуйтесь ПОИСКОМ, возможно, такую задачу уже решали!
4. Не предлагайте свои решения на других языках, кроме Ассемблера. Исключение только с согласия модератора.
5. НЕ используйте форум для личного общения! Все, что не относиться к обсуждению темы - на PM!
6. Проверяйте программы перед тем, как выложить их на форум!!
![]() |
BlackShadow |
![]() ![]()
Сообщение
#1
|
Гость ![]() |
От нечего делать опишу основные моменты при программировании на встроенном ассемблере в Паскале.
Есть 2 способа задействовать эту замечательную возможность:
Ну, на самом деле применений целое море, о чём можно убедиться полистав этот раздел форума. Рассмотрим вопрос адресации в реальном режиме (именно он используется "by def" при компиляции в BP). Адрес состоит из двух частей: сегментной части и смещения. Обе части являются 16-ти разрядными двоичными числами или, что на практике и применяется, 4-х разрядными шестнадцатиричными. Рассмотрим пример и на нём разберёмся, что и какая часть значит: Пусть сегментная часть (далее Seg) = $ABCD, а смещение (далее Ofs) = $1234. Это означает, что эта пара Seg:Ofs хранит следующий адрес Seg * $10 + Ofs = $ABCD * $10 + $1234 = $ABCD0 + $1234 = $ACF04. Как легко заметить, пользуясь таким способом адресации мы можем указать адрес любой ячейки памяти в пределах первого мегабайта ($00000..$FFFFF) и даже чуть-чуть больше, но это не имеет значения, т.к. процессор в реальном режиме даёт доступ только к первому МБ. Возникает естественный вопрос: а зачем нужен этот геморрой, и почему нельзя просто указывать полный адрес? А вот нельзя. А потому, что процессор при работе с памятью опирается на информацию, которая хранится в его регистрах, а т.к. мы используем 16-битный вариант команд, то, соответственно, в 1 регистр более 16 бит (4 16-ричные цифры) не впихнуть. Поэтому и приходится использовать 2 регистра: сегментный и какой-нибудь, который можно использовать для адресации. Рассмотрим предназначение регистров процессора:
Теперь давайте рассмотри примитивный набор команд. mov dst, src копирует значение src в dst. Есть один важный момент: командой mov нельзя скопировать значение одной переменной в другую за один приём. Примеры: (Показать/Скрыть)
inc p увеличивает на 1 значение операнда. После компиляции эта команда занимает меньше места чем команда прибавления единицы. Примеры: (Показать/Скрыть)
dec p соответственно уменьшает операнд на 1. add dst, src прибавляет к src значение dst. Результат сохраняется в dst, так что просто число там написать нельзя. Примеры: (Показать/Скрыть)
sub dst, src вычитает из dst значение src. mul n умножает значение регистра AL (AX) на n. Если n размером в 1 байт, то происходит следующее: AX = AL * n, если же слово, то старшие 16 бит произведения сохраняются в DX, а младшие в AX, т. е., умножив AX=$1010 на $100 получим в DX $0010 и в AX $1000. Примеры: (Показать/Скрыть)
div n делит значение в AX (DX:AX, как в команде mul) на n. При этом остаток сохраняется в AH (DX), а целая часть от деления в AL (AX). Например: (Показать/Скрыть)
cmp a, b - сравнивает значения a и b и устанавливает флаги процессора в соответствии с результатом сравнения. Например: (Показать/Скрыть)
jmp L - команда безусловного перехода на метку L. То что в BP называется GoTo. Например: (Показать/Скрыть)
j<cc> L - серия команд условного перехода. Тут <cc> определяет условия перехода:
Например: (Показать/Скрыть)
Рассмотрим ещё команду loop L она сравнивает CX с 0 и, если он отличен, то уменьшает его на 1 и делает переход на указанную метку. Пример: (Показать/Скрыть)
Теперь для закрепления сказанного рассмотрим реализацию вычисления факториала:
Function Factorial(n: Integer): Integer; Assembler;
Asm
mov CX, [n] {Проверим, может n<0?}
cmp CX, 0 {Сравним с 0}
jl @@1 {Если меньше, то считать не будем}
mov AX, 1 {Начальное произведение}
@@2: {В CX мы уже загрузили кол-вл итераций, так что к циклу готовы}
mul CX {Домножим текущее произведение на значение счётчика}
loop @@2 {И продолжим цикл}
jmp @@3 {Произведение вычислено - можно выходить из функции}
@@1: {А, если попросили вычислить факториал отрицательного числа}
mov AX, 0 {То вернём 0}
End
Var n: Integer;
Begin
ReadLn(n);
WriteLn(Factorial(n))
End.
Стоит объяснить ещё и то, как возвращаются значения функций. Это всё зависит от типа результата: Byte, Char - через AL Word, Integer - через AX LongInt - старшая часть в DX, а младшие 16 бит в AX. Pointer - сегментная часть в DX, смещение - AX. Остальные типы возвращаются более извращённым способом... Так же отмечу, что убрав проверку на <0 можно переписать эту функцию так: Function Factorial(n: Integer): Integer; Assembler;
Asm
mov CX, [n]
mov AX, 1
@@1:
mul CX
loop @@1
End
Правда проблема переполнения остаётся, но зато покажите мне компилятор, который стандартное Function Factorial(n: Integer): Integer;
Var
i, Res: Integer;
Begin
Res:=1;
For i:=2 To n Do
Res:=Res*n
Factorial:=Res
End;
скомпилирует вот так вот красиво... |
![]() ![]() |
BlackShadow |
![]()
Сообщение
#2
|
Гость ![]() |
Продолжим ликбез
![]() Стек Сейчас я вот возьму и расскажу про стек. Положение стека в памяти определяется парой регистров SS:SP. Причём эта комбинация указывает именно на вершину стека. Что с ним можно делать? В него можно чего-то сохранять и чего-то восстанавливать. Особенностью является то, что элементы стека - 16-битные слова. Для работы с ним используются 2 команды: PUSH <Src>, которая производит следующие действия:
PUSH AX
PUSH [WORD PTR a]
PUSH 5 ; только я не уверен, что Pascal даёт возможность такой вот записи.
И естественно POP <Dst>, которая
POP AX
POP [WORD PTR b]
Так же есть команды PUSHA и POPA, которые сохраняют регистры AX, BX, CX, DX, SI, DI, BP в стеке (PUSHA) или восстанавливают их оттуда (POPA). По своей сути эти команды аналогичны цепочке команд PUSH или POP. Как правило PUSHA и POPA "обрамляют" код на Assembler'е, в котором происходит недопустимое изменение нескольких регистров. Например: написали обработчик прерывания. Допустим от таймера. И в этом обработчике мы совсем не позаботились о сохранении регистров. Выполняется какая-нибудь чудная программа, которая занята какими-нибудь чудными вычислениями, а тут происходит плановое прерывание от таймера, вызывается наш обработчик, сбивает все регистры и возвращает управление к программе. В итоге в штате Колорадо банкомат на мостовую выплюнул $700 ![]() Стоит вспомнить и про команды PUSHF и POPF, которые сохраняют/восстанавливают регистр флагов в/из стека. Например: CMP AX, BX
PUSHF
ADD BX, 1234h
POPF
JE TudaTo
Тут сравниваются значения регистров AX и BX, сохраняются флаги, затем производятся какие-то операции, способные изменить состояние флагов, затем они восстанавливаются вместе с результатом сравнения. Зачем это надо? Как уже можно понять, стек часто используется для сохранения значения регистров. Например, рассмотрим реализацию вложенного цикла:
MOV CX, 5 ;Заносим в CX кол-во итераций внешнего цикла
Outter:
PUSH CX ;Сохраним значение счётчика в стеке
MOV CX, 3 ;Заносим в CX кол-во итераций внутреннего цикла
Inner:
INC AX ;Чего-нибудь вытворяем}
LOOP Inner ;Продолжаем внутренний цикл
POP CX ;Восстанавливаем значение счётчика внешнего цикла
LOOP Outter ;Продолжаем внешний цикл
А теперь следует отметить, что некоторые команды процессора так же используют стек для сохранения регистров. А именно команда CALL <Proc> вызывает процедуру <Proc>. И вот как это происходит:
Есть ещё одно ОЧЕНЬ важное применение стека - это передача параметров в функцию. Возьмём пример: procedure DrawWindow(x1, y1, width, height: Integer;
Caption: PChar; Flags: Integer; SomethingAlso: Integer;
OneMoreParameter: Integer; NuIEschoChtoNibud: LongInt);
Как передать параметры в такую процедуру? Через регистры, естественно не получится - их просто не хватит. Есть вариант "от MicroSoft'а" написать функцию типа Drawwindow(p: DrawWindowParameters), но это тоже не всегда удобно. В таком случае можно воспользоваться стеком. Так как местоположение стека одинаково для всех функций, то и DrawWindow сможет их оттуда извлечь. Только не командами POP, а при помощи нехитрого трюка. Скопируем в BP значение SP и будем обращаться к параметрам, как[BP+...]. Возникает серия вопросов.
Как закрепление, перепишем функцию Factorial с использованием рекурсии: Function Factorial(n: Integer): Integer; Assembler;
Asm
MOV AX, [n]
CMP AX, 0
JE @@0
CMP AX, 1
JE @@1
DEC AX
PUSH AX
CALL Factorial
MOV BX, [n]
MUL BX
JMP @@2
@@0:
@@1:
MOV AX,1
@@2:
End
Следует отметить, что функция написанная на Паскале автоматически очищает стек от параметров (это достигается путём вставки команды RET n, которая освобождает стек от указанного объёма ненужной инфы). А так же, что, если функция описана типа f(p1, p2, p3), то и при её вызове в стек надо сохранять сначала p1, затем p2 и в последнюю очередь p3. Зачем я это говорю? А потому что в C это всё совсем наоборот: стек чистит не функция, а тот кто её вызвал и параметры сохраняются в обратном порядке. |
![]() ![]() |
![]() |
Текстовая версия | 31.07.2025 3:07 |