Вступление
|
||||||||||||||||
ТеорияСегодня мы поговорим о связи этих двух языков - Паскаля и Ассемблера. Начну как всегда с неких общих понятий. Наши программы пишутся не только под определённую ОС, но и под определённый процессор. В нашем случае это 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Например напишем простую программку выводящую на экрана строку Hello world!:
Вот такая вот программка. Теперь давайте разберём что к чему и зачем. Думаю все догадались, что ассемблерный код вставится непосредственно в сегмент кода. А что это значит? Это значит, что процессор попытается исполнить все инструкции, даже те которые мы ему не давали. Строка 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 байта). Обычно в этот момент рисуют вот такую схемку:
О чем она нам говорит? О том, что каждый дву байтовый регист состоит из 2-х однобайтовых (логично не правдо ли :). Левый байт называют старшей частью, а правый младшей. Соответственно и буква после имени H (High - старший) и L (Low - младший). Поэтому команда mov ah, 9 делает что-то со старшей частью ax, а mov dx, offset Msg с регистром dx. Очевидно, что присвоить значение регистру можно 2-мя способами:
Пропустим пока 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. Эти регистры уже не делятся на младшую и старшую часть, так они несут адрес сегмента. При этом подразумеваются следующее их назначение:
mov ax, 0B800hТак вот, возвращаясь к 9 функции 21h прерывания - по умолчанию предпологается, что строка у нас находится в сегменте данных. Т.е. адресс строки на входе выглядит так - DS:DX. Однако наша то строка находится в сегменте кода. Т.е. нам надо присвоить DS значение CS. Делаем мы это через стек. Стек - область памяти, используемая программой для временного хранения данных. Работа со стеком заключается в простом правиле - первый пришёл, последний ушёл :) Для работы со стеком нам понадобятся всего 2 команды: push и pop. Синтаксис у них такой: PUSH приемникPUSH - записывает в стек, POP - извлекает из стека. Соответственно: push AX - ПОМЕЩАЕТ В СТЕК значение регистра AXВернёмся к нашей программе: push dsИтак первым push'eм мы сохраняем в стеке значение регистра ds (который сейчас указывает на сегмент данных). Вторым push'eм - значение cs (который указывает на сегмент кода). Что же мы извлекаем первым pop'ом? Давайте вспомним правило - первый пришёл, последний ушёл или же если сказать нооборот последний пришёл - первый ушёл. Значит первым pop'ом мы извлечём из стека значение cs. Т.е. после pop ds регистр DS = CS (что нам и было нужно). Второй pop нам нужен, что бы во первых вернуть стек в его первоначальное состояние (т.е. состояние до выполнения нашего кода). Во вторых что бы востановить значение DS. Здесь нам были не нужны никакие переменные, а если они будут, то программа будет лезть в сегмент данных (т.е. в DS), а он у нас указывает на код.... вообщем получится полная лабуда. Несмотря на кажащуюся простоту (всё так просто не правда ли :) со стеком надо обращаться аккратно.Ведь помимо нас со стеком работают и другие фукции программы. Например вы никода не задумывались как после выполнения процедуры программа знает куда ей вернуться? Оказывается адресс возврата предварительно записывается в стек и после выполнения процедуры извлекается от туда, и осуществлляется переход на него. А если внутри процедуры мы положили в стек число 3 и забыли его извлечь? Программа то не разберётся и решит, что это адрес возврата :) и перейдёт на него... что произойдёт дальше я боюсь предсказывать. По этотму не забывайте извлекать из стека, то что в него занесли. И что бы укрепить пройденное рассмотрим несколько примеров. Допустим, мы помещаем в стек следующие регистры: AX, BX, CX: push axВосстанавливать со стека нужно в обратном порядке: pop cxЕсли вы поменяете местами регистры при восстановлении, то ничего страшного не произойдет, только содержать они будут другие числа. Например: mov ax,1234hВ итоге AX будет равен 5678h, а BX - 1234h.
И тепрь ещё раз в кратце прокоментирую каждую строчку программы:
Чуть не забыл :) Вы обратили внимание на строку: Hello world!$. Зачем тут $ ? Просто 9-ая функция 21h прерывания считает $ признаком конца строки. Т.е. она будет выводить на экран символы начиная с адреса DS:DX пока не встретит $. Например уберите его из строки и вы увидите как редко $ встречается в памяти :) Итак теперь вы знаете 8 регистров. Назову ещё несколько:
|
||||||||||||||||
Это должен посетить каждый!С сегодняшнего дня я начну публиковать в рассылке ссылки на интересные ресурсы, посвященные программированию. Итак не говоря о том, что http://www.web-pascal.narod.ru должен был посеть каждый, сегодня я преподношу первую порцию ссылок:
| ||||||||||||||||
ПослесловиеВ следующий раз мы продолжим изучать связь ассемблера и паскаля. Узнаем как на самом деле передаются параметры в процедуру и почувствуем себя настоящими кул-][ацкерами :) Ещё раз призываю писать мне, если что-то не понятно. |