пятница, 23 ноября 2012 г.

Основы Assembler начало и вновь конец.

В современных операционных система задания по ВССиТ выполнять, мягко говоря проблематично. Для написания контрольных, да и просто освоения начальных навыков необходимо разобраться с компилятором MASM или TASM которые работают из под DOS.
Сами компиляторы, линковщики и все другие приблуды можно найти, например, на http://kalashnikoff.ru/ или http://www.wasm.ru/. А тут будет рассказываться о том, что ближе к телу.

Необходимые инструменты и их настройка.

И так, что нам нужно. Нам нужен эмулятор DOS-терминала и компилятор. Компилятор я выбрал MASM 6.11, так как им до этого пользовался, и кое что в моей памяти уже было.Как DOS-эмулятор я советую DOSBox. Версии есть и под Windows и под Linux. Под Linux его можно установить из стандартного репозитория. Для Ubuntu:
$sudo apt-get install dosbox
или из Центра приложений.
После запуска вы увидите экран с приглашением :
Z:\>
Смонтируйте диск C:\ для этого наберите:
Z:\>mount c /home/user/folder_prodject
И перейдите в неё:
Z:\>c:
командой dir вы сможете посмотреть содержание директории, а командой cd имя_директории перейти в другую папку находящуюся в точке монтирования или в её подпапках.
Пример на моем проекте:
Окно DOSBox с приглашением в Ubuntu 12.10

Если вы работаете много в DOS-эмуляторе, то такой способ может оказаться не очень удобным. Но моно настроить автоматическое монтирование диска C:\
Открываем /home/user/.dosbox/dosbox-0.74.conf в редакторе, и ищем строки:
[autoexec]
# Lines in this section will be run at startup.
# Yju can put your MOUNT lines here.
и пишем тут что-то типа:
mount C /home/user/asm
PATH=%PATH%;C:\masm611\bin\
C:
При старте будет монтироваться C:\ диск в /home/user/asm, при этом эта директория должна существовать.  PATH назначает пути поиска программ, это нужно что бы нам не мусорить в одной директории, а была возможность разнести компилятор и проект в разные. В конце стоит C: что бы DOSBox самостоятельно переводил нас на диск C:\ и берег наши телодвижения.

На последнем рисунке показана команда cd и dir, обратите внимание на то как отражаются имена файлов написанных русскими буквами. Возможно это и лечиться, но нужно ли это нам? И не забывайте, что MS-DOS использовал название файлов в формате 8.3. То есть, на имя отводилось 8 символов, потом шла точка (.), а затем расширение файла. При других названиях тоже могут возникнуть проблемы.

Как использовать MASM 6.11.

Теперь разберем как нам компилировать код, который мы напишем.
C:\>ml /с имя_файла.asm
получим файл имя_файла.obj Ключ /c говорит компилятору не проводить линковку.
C:\>link имя_файла.obj
получим исполняемый файл.
Но можно сделать всё быстрее:
C:\>ml имя_файла.asm /AT
При этом мы получим и .obj файл и исполняемый .com. Флаг /AT говорит компилятору вызвать линковщик и передает ему флаг /T который обозначает модель TINY.
Ещё часто бывает нужен файл листинга программы, для этого используйте ключ /Fl:
C:\>ml имя_файла.asm /Fl
Файл листинга будет записан в имя_файла.lst

Пишем программку.

Всё, на этом самое сложное закончилось. Теперь начинаем писать программу на ассемблере.
Так как в задачах часто просят вывести решение формул на экран, то и будем разбирать подобный пример. Сумма делений с x= от 1 до 10:

Основные команды которые будем использовать

mov приёмник,источник - на просто языке это "=", значение приёмника становиться равно значению источника.
пример:
mov ax,bx ;значит, ax=bx
mov ax,2  ;значит, ax=2
add приёмник,источник - складывает приёмник и источник, и помещает результат в в приёмник.
пример:
mov ax,2
mov bx,5
add ax,bx  ; в ax теперь сумма ax и bx, то есть 7
add ax,2   ; в ax теперь сумма ax+2, то есть 9
sub приёмник,источник - приёмник=приёмник-источник
пример:
mov ax,5
mov bx,2
sub ax,bx  ; в ax теперь разность ax и bx, то есть 3
sub ax,2   ; в  ax теперь разность ax-2, то есть 1
mul источник - умножение без знака. Первый множитель находиться в al, ax либо eax, он же является приёмником, второй множитель нужно задать. Только нужно уточнить, что это без знаковое умножение.
пример:
mov ax,2
mov bx,2
mul bx   ; в ax теперь 4
div источник - деление без знака. Делимое должно находиться в ax, eax или eax:edx. Приёмником целого является al, ax, eax соответственно. А остаток будет записывается в ah, dx или edx соответственно.
пример:
mov ax,5
mov bx,2
div bx   ; в al теперь 2, а в ah теперь 1.
ещё две команды: push и pop. Первая сохраняет регистр в стеке, вторая его восстанавливает. Самое главное запомнить: первый вошёл, последний вышел. Пример:
push ax
push cx
push bx
pop bx
pop cx
pop ax
Иначе вы можете получить не предсказуемый результат.
Если вы хотите подробнее узнать о командах, то можно обратиться к Справочнику команд.

Регистры, ох уж эти регистры. Краткий экскурс.

Сейчас, что бы начать читать код, достаточно знать регистры общего назначения:
Они могут быть 8 битные (например, al, ah), 16 битные (например, ax) и 32 битные (например, eax).
Все 16 битные регистры делятся на на младший и старший, например, ax делиться на al и ah. Делиться значит состоит, а не математическое действие.
eax, ax, al - очень часто выступают как приёмник значений, например, при умножении и делении.
ebx, bx. bl, bh - свободный регистр, который можно использовать в хвост и гриву.
edx, dx - часто используется для пересылки дополнительной информации или остатка от деления.
ecx, cx - называется счетчик, используется в циклах.
Всё остальное в комментариях программы. Кстати точка с запятой (;) используется как команда начать комментарий, и всё что написано за ней в строке не интерпретируется компилятором

Тренируемся

Первое что нам нужно - это разложить наш пример на простые составляющие, что бы составить алгоритм. К примеру: x в кубе - это x*x*x. Вспоминаем: умножение это mul, то есть мы можем это записать:
mov ax,1   ; присвоили значение
mul ax     ; умножили первый раз, то есть возвели в квадрат
mul ax     ; умножили второй раз, то есть возвели в куб
Вот, так пошагово это и происходит.

А теперь сам листинг решения примера:

.MODEL tiny              ; задаём модель в данном случае .com
.CODE
.486                     ; указываем модель процессора
org 100h                 ; выделяем память

start:
 mov ecx,10              ; задаём количество циклов. 10 потому, что у нас x от 1 до 10
 
Lb1:                     ; устанавливаем метку для цикла
 mov eax,11              ; находим х
 sub eax,ecx             ; x=11-число циклов (1, 2, 3... 11-1=10)
 push ecx                ; сохраним в стеке сх для использования в цикле
 push eax                ; сохраним в стеке ax что бы больше не высчитывать его
 mul eax                 ; x^2 (^ - будет означать "в степени")
 mov ecx,eax             ; сохраним eax в ecx потом отнимать нужно будет
 mov ebx,3               ; ebx = 3
 mul ebx  ; 3x^2
 sub eax,1               ; 3x^2-1
 mov ebx,eax             ; сохраняем eax в  ebx потом понадобиться
                
 pop eax                 ; восстанавливаем eax так как уже нужен eax опять = x
 push ebx                ; и сохраняем ebx так как потом понадобиться
 mov ebx,eax             ; ebx = eax
 mov eax,ecx             ; помните ecx=x^2, теперь eax = ecx, но можно было просто перемножать
 mul ebx                 ; eax = x^3
 mov ebx,4               ; ebx = 4
 mul ebx                 ; eax = 4x^3
 sub eax,ecx             ; eax = 4x^3-x^2
 add eax,2               ; eax = 4x^3-x^2+2
 pop ebx                 ; восстанавливаем ebx
 mov a,eax               ; делимое (a - это переменная)
 mov z,ebx               ; делитель (z  - это тоже переменная) описаны в конце программы

 finit                   ; инициализируем сопроцессор
 fld  a                  ; делимое в сопроцессор
 fdiv z                  ; проводим деление в сопроцессоре

 call tEnter             ; это вызов функции которая переводит строку
 call pFloat             ; выводим результат деления в консоль
 
 @4:
 fadd  t                 ; добавляем предыдущий результат деления, для получения суммы
                                        ; значений функции ( t - это переменная)
 fst   t                 ; запоминаем сумму в переменной

 pop ecx                 ; вынимаем ecx из памяти для корректной работы цикла
 
 loop Lb1                ; переходим на метку Lb1 пока ecx не станет равным нулю

 call pEnter             ; это вызов функции которая переводит строку
 call pFloat             ; выводим sum((4x^3-x^2+2)/(3x^2-1)) при x от 1 до 10 с шагом 1

 int     20h             ; Завершение работы программы (это называется
                         ; вызвать прерывание 20h)

 ret                     ; конец блока
 
; дальше начинается настоящее колдовство
;выводит целое число
pInt:
 pushad                         ; сохраняем все регистры
 mov  bx,sp                     ; bx = sp
 mov  byte ptr ss:[bx-1],'$'    ; символ конца строки
 @1:cdq                         ; метка цикла. и команда cdq копирует знаковый бит регистра eax на
                                ; все биты регистра edx
 div  ecx                       ; делим число на основание edx - остаток, eax - частное
 add  dl,'0'                    ; преобразование в ASCII
 dec  bx                        ; уменьшаем bx на 1
 mov  ss:[bx-1],dl              ; добавляем в стоку перед предыдущим
 test eax,eax                   ; проверяем есть ли в eax  ещё что-нибудь
 jne @1                         ; если есть повторяем цикл

 mov  ax,ss                     ; выводим строку на экран
 mov  ds,ax                     ; ds = ax
 dec  bx                        ; уменьшаем bx на 1
 mov  dx,bx                     ; dx = bx
 xchg sp,bx                     ; если не обменять вылезет мусор
 mov  ah,9                      ; номер функции прерывания
 int  21h                       ; вызываем прерывание
 xchg sp,bx                     ; если не обменять программа завершиться
 popad                          ; восстанавливаем данные из стека
 ret

;выводит число
pFloat:
 pushad                         ; сохраняем все регистры
 mov  ecx,10                    ; задаём основание системы счисления
 push ecx                       ; сохраняем основание
 mov  bp,sp                     ; bp = sp
 fst  dword ptr ss:[bp-4]       ; выводим из сопроцессора как двойное слово в регистр ss
 xor  eax,eax                   ; обнуляем eax (xor бинарное или)
 mov  edx,ss:[bp-4]             ; edx = ss:[bp-4]
  shl  edx,1                    ; сдвигаем в edx все биты на 1 влево
 mov  ecx,edx                   ; ecx = edx
 shr  ecx,24                    ; сдвигаем в ecx все биты в право на 24
 sub  cx,126                    ; вычитаем их cx 126 - Порядок (экспонента)
 shl  edx,7                     ; сдвигаем edx влево на 7
 or   edx,80000000h             ; значащие биты (мантисса)
 shld eax,edx,cl                ; eax - целая часть
 shl  edx,cl                    ; edx - дробная часть
 pop  ecx                       ; сохраняем ecx
 call pInt                      ; выводим на экран целую часть

;выводим на экран дробную часть
 xchg eax,edx                   ; обмениваемся значениями
 cdq                            ; преобразуем двойное слово в четверное
 sub  bp,28                                     ; вычитаем из bp 28
 mov  byte ptr ss:[bp],'.'      ; добавляем разделитель десятичной дроби
 @2:mul  ecx                    ; умножаем на основание 10
 add  dl,'0'                    ; преобразование в ASCII
 inc  bp                        ; увеличиваем bp на единицу
 mov  ss:[bp],dl                ; добавляем в строку
 test eax,eax                   ; проверяем есть ли ещё знаки после запятой
 jne @2                         ; да? повторяем цикл
 mov  byte ptr ss:[bp+1],'$'    ; нет? добавляем знак конца строки, иначе выведет мусор
 sub  sp,32                     ; приводим строку в нормальное состояние
 push ss
 pop  ds
 mov  dx,sp                     ; помещаем в dx откуда её и будем печатать
 mov  ah,9                      ; функция прерывания прерывания
 int  21h                       ; вызов прерывания
 call nEnter                    ; вызов функции перевода строки
 add  sp,32                     ; восстанавливаем sp
 popad                          ; восстанавливаем регистры
 ret

;вспомогательная функция - переводит строку
nEnter:
 pushad
 mov  dx,offset str0
 mov  ah,9
 int  21h
 popad
 ret

;вспомогательная функция, печатает строку "Temp rezult:"
tEnter:
 pushad
 mov  dx,offset str1
 mov  ah,9
 int  21h
 popad
 ret

;вспомогательная функция, печатает строку "Result:"
pEnter:
 pushad
 mov  dx,offset strE
 mov  ah,9
 int  21h
 popad
 ret

;переменные
a dd ?
z dd ?
t dd 0
str0 db 10,13,'$'
str1 db 'Temp result: ','$'
strE db 10,13,'Result: ','$'

end start     ; конец программы
Вывод приложения

И вновь начало

Это был очень краткий экскурс в язык ассемблер. Я не ставил перед собой цели детального описания. Всё что тут написано служит только помощью в написании контрольной работы. Если кому-то (например, как мне) интересен язык, и хочется не только получить оценку по ВССиТ, но и что-нибудь начать понимать, воспользуйтесь дополнительной литературой.