#1C Язык Assembler

Вступление

Завиты пеплом щели паркета,
В форточку лезут запахи лета.
А по эту сторону стекла для просмотра затмений
Летает запах патологической лени.

Лень неизличима, лень непобедима,
Лень просто необходима тем, кто выпил пива.
Здравствуйте!
Думаю эпиграф сегодня всё скажет за меня :) Да и ещё мне тут подумалось, что мы несколько рановато взялись за графику. Будем счтитать, что это было коротким забеганием вперёд. Через пару выпусков мы вернёмся к этой теме, а пока у вас есть возможность подумать над тем, как строить график. Сегодняшняя тема довольно сложна и не может целиком влезть в нашу рассылку. Так как рассмотререние языка Ассемблера требует отдельной рассылки. С моей стороны я рекомендую для изчения рассылку "Низкоуровневое программирование для дZенствующих" с сайта http://www.wasm.ru. и рассылку "Ассемблер? Это просто! Учимся программировать" http://subscribe.ru/archive/comp.prog.assembler. Обе этих рассылки уже закончились и поэтому надо качать архив. Однако они уже стали если так можно сказать классикой жанра.

Ну и ещё освоив эту тему, вы сможете покорить всех заумными словами типа: дамп, пуш, поп, прерывание, регистры и т.п.

Теория

Сегодня мы поговорим о связи этих двух языков - Паскаля и Ассемблера. Начну как всегда с неких общих понятий. Наши программы пишутся не только под определённую ОС, но и под определённый процессор. В нашем случае это Intel и совместимые сним. У каждого процессора есть своя система команд. Он не понимает чего либо другого. На компьютере с процессором не совместимым с Intel (т.е. имеющим другую систему команд) наши программы работать не будут. В exe файле помимо заголовка, который определяет, что это за программа, и содержаться комманды процессору. Это и есть занятие транслятора - перевести с языка высокого уровня (в нашем случае Паскаль) на машинный язык, понятный процесссору. Поэтому мы можем писать программы прямо на понятном процессору языке машинных кодов. Давайте ради прикола напишем такую програмку. Создайте пустой файл например с именем my_low_level_prog.com (именно com, так как в нём нет заголовка, характерного для ехе файла) И запишите в него последовательность символов с номерами CD-20h. Для этого откройте её блокнотом и впишите два символа: "Н ". Русская буква Н и пробел (в кодировке Win разумеется). После этого можете сохранить файл и запустить программу на исполнение. Правда результата работы вы не увидете. Эта программа ничего не делает (если это понятие можно применить к программе). Единственная команда процессору в этой программе закодирована двумя байтами CD-20h. Это команда выхода из программы :) Своебразный аналог end. в Паскале. Продолжая далее можно попробовать написать что-то большее, однако думаю что делать этого не стоит.

Для того что бы не писать команды процессору таким вот простым, но мало понятным способом и был придуман язык низкоуровнего программирования ассемблер. Он переводит команды процессора в понятный и приятный для глаза вид. Например CD-20h на языке ассемблер выглядит как int 20h - согласитесь глаз радуется, душа поёт :) Однако что бы ещё более облегчить жизнь программистам придумали более сложные языки, которые назвали языками высокого уровня. В этих языках программирования вы не обращаетсь непосредственно к процессору - вы пишите программы на языке более приближенном к человеческому. Это имеет свои плюсы - вам сразу понятна программа. И свои минусы - при вам неизвестно в какие машинные коды переводит вашу программу компилятор. Программируя на языке ассемблер вы можете получить существенный выйгрыш в скорости выполнения и в объёме выполняемого файла. На данный момент очень мало программ пишутся целиком на ассемблере - в основном это вирусы и трояны. А сам язык применяется совместно с языком высокого уровня в критических местах программ, требующих очень большой скорости выполнения. Мне например думается, что часть таких программ как MS Office или Adobe Photoshop была написана именно на языке ассемблер. Сегодня я и собираюсь рассказать как встраивать куски кода на ассемблере в ваши программы.

BP 7 подерживает набор инструкций 2-х процессоров - 8086 и 80286. По умолчанию используется именно 8086. Что бы включить набор 286 надо влезть в Options -> Compiler и поставить галочку 286 instructions. Для вставки команд в тело программы надо использовать следующий оператор:

asm
текст на языке ассемблер
end
Например напишем простую программку выводящую на экрана строку Hello world!:
program test;

label
 Next, Msg;

begin
  asm
    jmp Next
    Msg: db 'Hello world!$'
    Next:
    push ds
    push cs
    pop  ds
    mov ah, 9
    mov dx, offset  Msg
    int 21h
    pop ds
  end
end.
Вот такая вот программка. Теперь давайте разберём что к чему и зачем. Думаю все догадались, что ассемблерный код вставится непосредственно в сегмент кода. А что это значит? Это значит, что процессор попытается исполнить все инструкции, даже те которые мы ему не давали. Строка db 'Hello world!$' запишет в ехе файл строку Hello world!$ (можете убедится в этом сами, открыв его в текстовом редакторе). А помните как процессор выполнил сочетание буквы Н и пробела? Правильно надо не дать ему выполнить инструкции, которые имеют коды 'Hello world!$'. Поэтому надо обойти эту строку. Поэтому мы используем команду jmp Next. Синтаксис её таков:
jmp МЕТКА
переходит на метку. Аналог уже знакомого нам goto. Думаю что с этой командой всё понятно jmp Next - переходит к строчке, помеченной Next.

Дальше мы ставим метку Msg. Это нам понадобится, что бы как-то пометить строку db 'Hello world!$', что бы к ней вернуться. Следубщие строки пока пропускаем и переходим сразу к mov ah, 9.

Тут мы сделаем долгую остановку. Вы уже привыкли пользоваться переменными, создавать их столько сколько нужно и давать им любые имена. Однако сюрприз. При программировании на ассемблере для навороченных вычислений разрешается использовать только несколько переменных с фиксированными именами собственными и имеющих фиксированный размер. Эти переменные называются регистрами. Например процессор 8086 имеет 14 регистров (посмотреть на них можно выбрав Debug -> Register). На все них мы не будем останавливаться подробно. Сейчас я раскажу о регистрах общего назначения. К ним относятся регистры с именами AX, CX, DX, BX. Каждый регистр имеет размер в 1 слово (2 байта). Обычно в этот момент рисуют вот такую схемку:

AX BX CX DX
AH AL BH BL CH CL DH DL

О чем она нам говорит? О том, что каждый дву байтовый регист состоит из 2-х однобайтовых (логично не правдо ли :). Левый байт называют старшей частью, а правый младшей. Соответственно и буква после имени H (High - старший) и L (Low - младший). Поэтому команда mov ah, 9 делает что-то со старшей частью ax, а mov dx, offset Msg с регистром dx.

Очевидно, что присвоить значение регистру можно 2-мя способами:

  1. присвоить сразу значение: AX = 32D2h
  2. присвоить значения старшей и младшей части отдельно: AH = 32h, AL=D2h
Точно также значение старшей (младшей) части можно присвоить двумя способами:
  1. присвоить непосредственно: AH = 32h
  2. или через AX = 3200h
Кстати так как язык ассемблера представляет собой команды процессора, то команды равно ( = ) нет. Для присвоения регистру какого либо значения надо пользоваться командой mov (от англ. move - движение, хотя в данном контексте переводится, как загружать). Т.е. когда мы пишем mov ah, 9 - мы загружаем в AH 9. По русски это так AH = 9.

Пропустим пока mov dx, offset Msg и перейдём сразу к строчке int 21h. Команда int - имеет такой синтаксис:

int [номер]
при вызове этой команды происходит прерывание (от англ. Interruption - прерывание). Что такое прерывание? Прерывание - это своего рода подпрограмма, которая находится постоянно в памяти и может вызываться в любое время из любой программы. Т.е. выполнение нашей программы прерывается и осуществляется переход на обработку прерывания. Прервание хотя и записывается одной командой на самом деле состоит из нескольких. После обработки (выполнения) прерывания программа продолжит работу.

Если вы не поняли привожу замечательный пример из рассылки "Низкоуровневое программирование для дZенствующих":

Для тех, кто не понял. Представьте себе, что вы сидите за компом и выполняете какую-либо работу. И вдруг ловите себя на мысли, что вам СРОЧНО НУЖНО сходить в туалет (терпеть вы больше уже не можете). Вот это СРОЧНО НУЖНО и есть сигнал-прерывание, по которому вы начинаете выполнять определенную СТАНДАРТНУЮ последовательность инструкций (программу обработки прерывания), как-то: встать, пойти туда-то, включить свет ... вернуться, сесть за комп и ПРОДОЛЖИТЬ РАБОТУ с того же самого места, на котором вы остановились перед выполнением программы "поход в туалет". В данном случае наш мозг выполняет роль процессора, наши внутренние органы сигнализируют мозгу о потребности в обслуживании, а само обслуживание проводится "программой-навыком", заложенным в процессе нашего развития и (хм!) воспитания.
В нашей программе мы вызываем int 21h - прерывание 21h (читается двадцать первое прерывание). Это прерывание DOS. Каждое прерывание имеет несколько функции. Т.е. совершает различные действия в зависимости от номера функции. Номер функции должен быть помещён перед вызовом прерывания в ah (что мы и делаем mov ah, 9). Функция 9 прерывания 21h - это вывод строки на экран. Так же в зависимости от нужд прерывание может получать дополнительные параметры через другие регистры. Вполне логично, что для функции вывода строки на экран нам надо передать эту строку в прерывание.

Этим и занимается mov dx, offset Msg. Так как dx может содержать всего 2 байта (а строка наверно будет длинее :), поэтому в dx должен быть адресс строки. Так вот offset Msg - это смещение до метки Msg (помните мы ей пометили Hello world). Однако нужно вспомнить, что в адресе помимо смещения есть ещё и сегмент. Откуда он берётся?

Для этого существуют специальные сегментные регистры. Их имена - CS DS SS ES. Эти регистры уже не делятся на младшую и старшую часть, так они несут адрес сегмента. При этом подразумеваются следующее их назначение:

  • Регистр CS служит для хранения сегмента кода программы (Code Segment - сегмент кода);
  • Регистр DS - для хранения сегмента данных (Data Segment - сегмент данных);
  • Регистр SS - для хранения сегмента стека (Stack Segment - сегмент стека);
  • Регистр ES - дополнительный сегментный регистр, который может хранить любой другой сегмент (например, сегмент видеобуфера).
Сразу скажу, что загрузка напрямую в сегментный регистр запрещена! Делать это можно например так:
mov ax, 0B800h
mov es, ax
Так вот, возвращаясь к 9 функции 21h прерывания - по умолчанию предпологается, что строка у нас находится в сегменте данных. Т.е. адресс строки на входе выглядит так - DS:DX. Однако наша то строка находится в сегменте кода. Т.е. нам надо присвоить DS значение CS. Делаем мы это через стек. Стек - область памяти, используемая программой для временного хранения данных.

Работа со стеком заключается в простом правиле - первый пришёл, последний ушёл :) Для работы со стеком нам понадобятся всего 2 команды: push и pop. Синтаксис у них такой:

PUSH приемник
POP приемник
PUSH - записывает в стек, POP - извлекает из стека. Соответственно:
push AX - ПОМЕЩАЕТ В СТЕК значение регистра AX
pop AX - ИЗВЛЕКАЕТ ИЗ СТЕКА значение регистра AX
Вернёмся к нашей программе:
push ds
push cs
pop ds
........
pop ds
Итак первым push'eм мы сохраняем в стеке значение регистра ds (который сейчас указывает на сегмент данных). Вторым push'eм - значение cs (который указывает на сегмент кода). Что же мы извлекаем первым pop'ом? Давайте вспомним правило - первый пришёл, последний ушёл или же если сказать нооборот последний пришёл - первый ушёл. Значит первым pop'ом мы извлечём из стека значение cs. Т.е. после pop ds регистр DS = CS (что нам и было нужно). Второй pop нам нужен, что бы во первых вернуть стек в его первоначальное состояние (т.е. состояние до выполнения нашего кода). Во вторых что бы востановить значение DS. Здесь нам были не нужны никакие переменные, а если они будут, то программа будет лезть в сегмент данных (т.е. в DS), а он у нас указывает на код.... вообщем получится полная лабуда.

Несмотря на кажащуюся простоту (всё так просто не правда ли :) со стеком надо обращаться аккратно.Ведь помимо нас со стеком работают и другие фукции программы. Например вы никода не задумывались как после выполнения процедуры программа знает куда ей вернуться? Оказывается адресс возврата предварительно записывается в стек и после выполнения процедуры извлекается от туда, и осуществлляется переход на него. А если внутри процедуры мы положили в стек число 3 и забыли его извлечь? Программа то не разберётся и решит, что это адрес возврата :) и перейдёт на него... что произойдёт дальше я боюсь предсказывать.

По этотму не забывайте извлекать из стека, то что в него занесли. И что бы укрепить пройденное рассмотрим несколько примеров. Допустим, мы помещаем в стек следующие регистры: AX, BX, CX:

push ax
push bx
push cx
Восстанавливать со стека нужно в обратном порядке:
pop cx
pop bx
pop ax
Если вы поменяете местами регистры при восстановлении, то ничего страшного не произойдет, только содержать они будут другие числа. Например:
mov ax,1234h
mov bx,5678h
push ax
push bx
pop ax
pop bx
В итоге AX будет равен 5678h, а BX - 1234h.

И тепрь ещё раз в кратце прокоментирую каждую строчку программы:

   jmp Next  -  переходим на метку Next
    Msg: db 'Hello world!$'  - наша строка, содержащаяся в сегменте кода!
    Next:
    push ds  - сохраняем в стеке адрес сегмента данных
    push cs  - сохраняем в стеке адрес сегмента кода
    pop  ds - извлекаем его из стека и записываем в ds, т.е. DS = CS
    mov ah, 9 - записываем в ah - номер функции, AH = 9
    mov dx, offset  Msg - в dx смещение до строки 
    int 21h - вызываем 21h прерывание
    pop ds - восстанавливаем ds в первоначальное значение

Чуть не забыл :) Вы обратили внимание на строку: Hello world!$. Зачем тут $ ? Просто 9-ая функция 21h прерывания считает $ признаком конца строки. Т.е. она будет выводить на экран символы начиная с адреса DS:DX пока не встретит $. Например уберите его из строки и вы увидите как редко $ встречается в памяти :)

Итак теперь вы знаете 8 регистров. Назову ещё несколько:

  • Регистр командного указателя IP
    Он содержит смещение на команду, которая должна быть выполнена. Обычно этот регистр в программе не применяется.
  • Индексные указатели SI и DI
    Регистры SI (Source Index register - индекс источника) и DI (Destination Index register - индекс приемника) применяются для расширенной адресации и для использования в операциях сложения, вычитания и в некоторых строковых. Например адрес видеобуффера занесём в es, а смещение в di... ну и изменим первый символ:
    	mov ax,0B800h
    	mov es,ax - записываем в es адресс начала видеобуффера
    	mov di,0 - в di смещение относительно начала
    
    	mov ah,31 - байт аттрибут
    	mov al,1 - код символа
    	mov es:[di],ax
    Придётся опять вспомнить организацию видеопамяти. Квадратные скобки ( [ ] ) в команде mov es:[di],ax указывают на то, что надо загрузить число не в регистр, а по адресу, который содержится в этом регистре (в данном случае - это 0B800:0000).
  • Регистры-указатели SP и BP
    о них я раскажу позже.
  • Флаговый регистр (или регист флажков).
    Очередной сюрприз - этот регист не имеет имени. Так как нас интересует не сам регист, а состояние некоторых его бит, которые имеют свои имена. Если посмотреть окно Registers, то они занимают 2 нижние строчки. О них я тоже раскажу в следующий раз.

Это должен посетить каждый!

С сегодняшнего дня я начну публиковать в рассылке ссылки на интересные ресурсы, посвященные программированию. Итак не говоря о том, что http://www.web-pascal.narod.ru должен был посеть каждый, сегодня я преподношу первую порцию ссылок:

    pascal-central.com (eng)
    Достаточно интересный англоязычный ресурс по программированию на Pascal'е.
    pascal.dax.ru
    Все о программировании на Pascal'е. Статьи, докумнетация......
    pascal.sources.ru
    Куча исходников на Pascal.
    borlpasc.narod.ru
    Много хорошей документации и исходников.
    algolist.manual.ru
    Библиотека алгоритмов.
    codenet.ru
    Все для программистов.
    rusfaq.ru
    Российский специализированный ресурс по различного рода FAQ.
    prog.agava.ru
    Бибилиотека программиста (Не обновляется).
Если у вас есть хорошие ссылки, не обязательно по программированию на Паскалю, а о компьютерах вообще, то присылайте их. если они заслуживают внимания, то я их опубликую и возможно они войдут в раздел ссылки на сайте http://www.web-pascal.narod.ru.

Послесловие

В следующий раз мы продолжим изучать связь ассемблера и паскаля. Узнаем как на самом деле передаются параметры в процедуру и почувствуем себя настоящими кул-][ацкерами :) Ещё раз призываю писать мне, если что-то не понятно.


[Назад] [Содержание] [Дальше]
Hosted by uCoz