→ Пошук по сайту       Увійти / Зареєструватися
Знання Асемблер Команди і алгоритми

Організація додатків MS-DOS

Як вже наголошувалося вище, мова асемблера є віддзеркаленням архітектури процесора, і вивчення мови по суті означає вивчення системи команд і способів адресації, що реалізовуються процесором. Одна до будь-яка мова програмування корисна лише постільки, поскільки на нім можна написати якісь працездатні програми. В той же час важкий уявити собі реальну програму, яка виконує чисто логічні або обчислювальні дії, нічого не вводячи і не виводячи і не взаємодіючи з іншими програмами. Проте такі питання, як організація здійснимої програми, її запуск, взаємодія з різноманітними апаратними і програмними об'єктами обчислювальної системи (клавіатурою, дисками, таймером, пам'яттю, системними драй вірою і інш.) і, нарешті, завершення є прерогативою операційної системи. Тому в програмах на мові асемблера завжди широко використовуються системні засоби, наприклад, для виведення па екран або введення з клавіатури, читання або запису файлів, управління пам'яттю і інш. Більш того, сама внутрішня організація програми, її структура і, певною мірою, алгоритми поведінки в сильному ступені визначаються правилами організації обчислювального процесу, закладеними в DOS. Вивчення мови асемблера у відриві від конкретної операційної системи вироджується в схоластичне заняття, результатом якого буде знання формальних правил написання програмних пропозицій без можливості застосувати ці правила для створення працездатних програм.

В той же час можливості навіть такий відносно простий операційної системи, як MS-DOS, вельми великі і багатообразні, і їх вивчення складає самостійний розділ програмування. У справжній книзі засобу DOS розглядаються лише в тому мінімальному об'ємі, який необхідний для створення простих, але працездатних програм на мові асемблера, а також для демонстрації основних алгоритмів і прийомів програмування.
Охочі отримати глибше уявлення про можливості MS-DOS і використанні функцій DOS в прикладному програмуванні, можуть звернутися до книги: К.Г.Фіногенов "Самовчитель по системних функціях MS-DOS", M., Радіо і зв'язок, Ентроп, 1995.

До найважливіших питань, що вимагають хоч би мінімального розгляду, слід віднести вимоги, MS-DOS, що пред'являються, до структури прикладних програм, а також до особливостей їх взаємодії з самою DOS і з іншими програмами.

Програми, призначені для виконання під управлінням MS-DOS, можна класифікувати по різних ознаках. По внутрішній організації всі програми належать до одного з двох типів, яким відповідають розширення імен програмних файлів .ехе і .сом. По взаємодії з самою DOS програми підрозділяються на транзитних і резидентних. Нарешті, слід виділити найважливіший клас програм, службовців для обробки апаратних або програмних переривань, і званих зазвичай обробниками переривань. Ми не стосуємося тут таких специфічних програм, як встановлювані драйвери пристроїв, командні процесори (до їх числа належить COMMAND.COM) або оболонки DOS (наприклад, широко поширена програма Norton Commander), які можна виділити в самостійні класи.

Перший приклад закінченої програми, розглянутий нами в гл. 2, відносився до найбільш поширеного типу .ехе-пріложеній. Для такої програми характерна наявність окремих сегментів команд, даних і стека; для адресації до полів кожного сегменту використовується свій сегментний регістр. Зручність .ехе-программи полягає в тому, що її можна майже необмежено розширювати за рахунок збільшення числа сегментів. У разі великого об'єму обчислень в програму можна включити декілька сегментів команд, забезпечивши, зрозуміло, переходи з сегменту в сегмент за допомогою команд дальніх переходів або дальніх викликів підпрограм. Якщо ж програма повинна обробляти великі об'єми даних, в ній можна передбачити декілька сегментів даних. Кожен сегмент не може мати розміру більше 64 Кбайт, проте в сумі їх об'єм обмежується тільки наявною оперативною пам'яттю. Правда, в реальному режимі скрутно звернутися до пам'яті за межами 1 Мбайт адресного простору, так що максимальний розмір програми, якщо не передбачати в ній якісь спеціальні засоби почергового завантаження сегментів, обмежений величиною 550 ... 600 Кбайт. Наявність в МП 86 лише двох сегментних регістрів даних (DS і ES) декілька ускладнює алгоритми обробки великих об'ємів даних, оскільки доводиться постійно перемикати ці регістри з одного сегменту на іншій. Проте реально в сучасних процесорах є не два, а чотири сегментні регістри даних (DS, ES, FS і GS), які цілком можна використовувати в додатках DOS, спростивши тим самим процедури звернення до даних і прискоривши виконання програм. Пізніше всі ці можливості будуть розглянуті детальніше.

У багатьох випадках об'єм програми виявляється невеликий - менше, а часто і багато менше, ніж 64 Кбайт. Таку програму немає ніякої необхідності складати з декількох сегментів: і команди, і дані, і стек можна розмістити в єдиному сегменті, набудувавши на його початок все 4 сегментних регістра. Для односегментних програм в MS-DOS існує спеціальний формат і спеціальні правила їх складання. Програмні файли з програмами, складеними по цих правилах, мають розширення .сом. У форматі .сом зазвичай пишуться резидентні програми і драйвери, хоча будь-яку прикладну програму невеликого об'єму можна оформити у вигляді .сом-пріложенія. Якщо подивитися список системних програм, що входять в DOS, і що реалізовують, зокрема, зовнішні команди DOS, то можна відмітити, що приблизно третина цих програм написана у форматі .COM (COMMAND.COM, FORMAT.COM, SYS.COM і ін.), а дві третини - у форматі .EXE (FC.EXE: PRINT.EXE, XCOPY.EXE і так далі). Нижче ми розглянемо правила складання і особливості виконання як .ехе-, так і .сом-программ.

Інший критерій класифікації програм визначає спосіб взаємодії прикладної програми з іншими програмами і самою DOS. По цьому критерію програми діляться на два види: транзитні і резидентні.

Хід виконання транзитної програми (а до транзитних відносяться переважна більшість додатків DOS) виглядає таким чином. Користувач запускає програму, вводячи з клавіатури її ім'я, що завершується натисненням клавіші Enter. Відповідні програми-компоненти DOS відшукують на диску файл з вказаним ім'ям, завантажують його в пам'ять і передають управління на вхідну крапку цієї програми. Далі програма виконується, фактично монополізуючи ресурси комп'ютера. Поки вона не завершилася, користувач не має доступу до DOS і, відповідно, позбавлений можливості запустити іншу програму або виконати яку-небудь команду DOS. Введення з клавіатури можливе тільки у відповідь на запит поточної програми, якщо в ній передбачено звернення до клавіатури за отриманням яких-небудь даних.

Зовсім по-іншому функціонує резидентна програма. Користувач запускає її точно так, як і транзитну, вводячи з клавіатури її ім'я. Програми DOS завантажують програмний файл в пам'ять і передають управління на точку входу. Проте далі обчислювальний процес розвивається поїному. Програма виконує тільки свій початковий, такий, що ініціалізував фрагмент, після чого викликає спеціальну функцію DOS (з номером 31h). Ця функція завершує програму і повертає управління в DOS, але не звільняє пам'ять від програми, що завершилася, а залишає цю програму в пам'яті, роблячи її резидентною. Програма залишається в пам'яті і, можна сказати, нічого не робить. Оскільки управління передане DOS, користувач може вводити з клавіатури будь-які команди і, зокрема, запускати інші транзитні (або резидентні) програми. Коли буде активізована резидентна програма, що знаходиться в пам'яті? Як правило, резидентні програми включають обробники апаратних або програмних переривань. Якщо, наприклад, в резидентній програмі є обробник переривань від системного таймера, який, як відомо, видає сигнали переривань приблизно 18 разів в секунду, то кожне таке переривання зраджуватиме управління резидентній програмі, яка може, наприклад, періодично виводити на екран поточний час або якусь іншу інформацію. Робота резидентної програми протікатиме незалежно від інших програм і паралельно з ними. Іншим класичним прикладом резидентної програми є русифікатор клавіатури, який отримує управління при натисненні будь-якої клавіші, незалежно від того, яка програма зараз виконується. Завдання русифікатора - визначити по наявному в нім прапору, на якій мові працює користувач, і в необхідних випадках сформувати відповідний натиснутій клавіші код ASCII російської букви.

Слід відмітити, що необхідність в резидентних програмах виникла лише тому, що MS-DOS є істотно однозадачною системою. У багатозадачній операційній системі Windows поняття резидентної програми в принципі відсутнє.

Зрозуміло, своїми особливостями складання і функціонування володіють і обробники переривань - надзвичайно важливий клас програм, обслуговуючих численні зовнішні пристрої комп'ютера - клавіатуру, мишу, магнітні диски і інш., а також нестандартну апаратуру, якщо комп'ютер використовується для управління науковою установкою або технологічним процесом.

Розглянемо основні правила складання і функціонування перерахованих типів програм, щоб надалі можна було використовувати їх в прикладах, що ілюструють ті або інші засоби мови асемблера.

Програма типу .ехе Характерні особливості програм типу .ехе детально розглядалися в попередніх розділах. Приведемо ще декілька узагальнювальних міркувань. Структура типової програми на мові асемблера виглядає таким чином.

.586                         ; Розміщення трансляції всіх

                               ; команд (386-486-pentium)

code segment usee16 ; Початок сегменту команд

                                ; 16-розрядне застосування

assume Cs:code, DS: data

 main proc                 ; Початок головної процедури

mov  AX,  data           ; Ініціалізація

mov DS, AX               ;сегментного регістра DS

...                             ;Текст головної процедури

mov  Ax,4c00h           ;Вызов функції DOS

int 2 In                      ; Завершення програми

main endp                  ; Кінець головної процедури

code ends                  ; Кінець сегменту команди

data segments use16  ; Початок сегменту даних

...                               ; Визначення даних

data ends                    ; Кінець сегменту даних

stk segment stack         ; Початок сегменту даних

  db  256 dup(0)           ; Стік

stk ends                       ; Кінець сегменту стека

end main                      ; Кінець програми і точка входу 

Програма починається з директиви асемблера .586, програми, що дозволяє використовувати в тексті, весь набір команд процесора Pentium (окрім привілейованих). Якщо програма використовуватиме тільки базовий набір команд МП 86, вказівка цієї директиви не обов'язкова.

З іншого боку, її вказівка не зобов'язує нас обов'язково використовувати команди Pentium. Якщо в програмі передбачається використовувати лише додаткові команди процесорів 486 або 386, то замість .586 можна написати .486 або .386.

Вказівка будь-якого номера 32-розрядного процесора приведе до того, що за умовчанням програма транслюватиметься, як 32-розрядне застосування, тоді як нам потрібно створити звичайне 16-розрядне застосування. Для того, щоб всі адреси в програмі розглядалися, як 16-бітові, необхідно додати сегментам команд і даних описувачі use16. Для сегменту стека цей описувач не потрібний, оскільки в стеку немає пойменованих осередків.

Програма складається з трьох сегментів - команд, даних і стека. Імена сегментів вибрані довільно. Власне програма зазвичай складається з процедур. Ділення на процедури не обов'язково, але підвищує її наочність і полегшує передачу управління на підпрограми. У даному прикладі сегмент команд містить єдину процедуру main, що відкривається оператором ргос (від procedure, процедура) і що закривається оператором endp (end procedure, кінець процедури). Перед обома операторами указується ім'я процедури, яке надалі може використовуватися як відносна адреса процедури (по суті, відносної адреси першої здійснимої пропозиції цієї процедури). У нас це ім'я виступає як параметр директиви end, що завершує програму. Імена процедур, так само, як і імена сегментів, вибираються довільно.

Якщо програма має сегмент даних з якими-небудь даними, то для того, щоб до цих даних можна було звернутися, необхідно занести сегментну адресу сегменту даних в один з сегментних регістрів. Зазвичай як такий регістр вибирають DS. Таким чином, пропозиції, з яких почався текст головної процедури

mov Ax,data ;Инициализация

mov Ds,ах ;сегментного регістра DS 

де data - ім'я, дане сегменту даних, практично є обов'язковими для будь-якої програми.

Точно також обов'язковими є і завершуючі пропозиції

mov Ax,4c00h ;Вызов функції DOS

int 21h ;завершения програми 

у яких викликається функція DOS з номером 4ch. Ця функція, як вже наголошувалося, завершує програму, звільняє займану нею пам'ять і передає управління командному процесору COMMAND.COM. Ще два зауваження слід зробити щодо процедури трансляції і компоновки програми. Якщо сегмент даних розташувати після сегменту команд, як це зроблено в нашому прикладі, то у транслятора виникнуть складнощі при обробці імен полів даних, що зустрічаються в програмних пропозиціях, оскільки ці імена ще невідомі транслятору. Для того, щоб такі, як то кажуть, "посилання вперед" могли правильно оброблятися, слід в команді виклику транслятора TASM замовити два проходи. Це робиться вказівкою ключа /m2.

З іншою неприємністю ми зіткнемося, якщо спробуємо включити в програму операції з 32-розрядними операндами (навіть і з командами МП 86). Компонувальник TASM за умовчанням забороняє такого роду операції. Щоб подолати цю заборону, слід в команді виклику компонувальника вказати ключ /3.
Таким чином, приведений в гл. 1 командний файл повинне виглядати (для підготовки програми P.ASM) таким чином:

tasm /z /zi /n /m2 p,p,p

tlink /x /v /3 p,p 

Включення вказаних описувачів і ключів не зобов'язує нас використовувати нові команди або 32-розрядні операнди, так що приведені вище тексти командного файлу і самої програми можна використовувати як зразок для підготовки всіх приведених в цій книзі програмних прикладів, навіть якщо вони використовують тільки засоби МП 86. У подальших прикладах програм, в основному присвячених системі команд МП 86, ці описувачі опускатимуться.

Приведемо як ще один приклад просту закінчену програму типу .ехе, яка з'ясовує букву - позначення поточного диска і виводить її на екран з пояснюючим написом.

Приклад 3-1. Отримання поточного диска

; Опишемо сегмент команд

assume Cs:code,ds:data

code segment

main proc

move AX, data   ;Настроим DS

mov Ds,ax    ; на сегмент даних

mov Ah,19h    ; Функція DOS отримання

int 21h             ; поточного диска

add disk,al   ; Перетворимо номер в код

                        ; ASCII

mov Ah,09h  ; Функція DOS виводу на екран

mov Dx,offset msg  ; Адреса рядка

int 21h                       ; Виклик DOS

mov  Ah,01h            ; Функція DOS введення символу

int 2 In                       ; Виклик DOS

mov Ax,4c00h        ; Функція DOS завершення

int 21h                       ; програми

code ends

;Опишемо сегмент даних

data segment use16

msg db "Поточний диск"   ; Текст, що виводиться на екран

disk db " A:",13,10,"$"     ; Продовження тексту

data ends

; Опишемо сегмент стека

stk segment stack

 db 256 dup(U)        ; Стік

stk ends

end main 

Розглянемо текст приведеного прикладу. Після настроювання сегментного регістра DS на сегмент даних, викликається функція DOS з номером 19h, яка дозволяє отримати код поточного диска. У цієї функції немає ніяких параметрів, а результат своєї роботи вона повертає в регістрі AL у вигляді умовної коди. 0 позначає диск А:, 1 диск В:, 2 диск З: і так далі Якщо, наприклад, користувач працює на диску F, то функція 19h поверне в AL код 5.

Для перетворення коди диска в його буквене позначення, ми скористалися широко поширеним прийомом. У полях даних визначений символьний рядок, який виводитиметься на екран. Для зручності роботи вона розділена на дві частини, кожна з яких має своє ім'я. Початку рядка привласнено ім'я msg, а тій її частині, яка починається з позначення диска А:, ім'я disk (зрозуміло, імена вибрані довільно). Якщо подивитися на таблицю код ASCII, то можна відмітити, що код кожної наступної букви алфавіту на 1больше попередньою. Таким чином, якщо до коду латинської букви A (41h) додати 1, вийде код букви В, а якщо додати, наприклад, 5, вийде код букви F. Саме ця операція і виконується в пропозиції

add disk,al ;Преобразуем номер в код ASCII 

де до байта з адресою disk додається код, повернений функцією DOS.

Виконавши модифікацію рядка, ми виводимо її на екран вже знайомій нам функцією DOS 09h. Вона виводить всі символи рядка, поки не зустрінеться з символом $, яким наш рядок і завершується. Перед знаком S в рядку є два числа: 13 і 10. При виведенні текстового рядка на екран будь-якою функцією DOS код 13 трактується DOS, як команда повернути курсор почато рядки ("повернення каретки"), а код 10 - як команда на переклад рядка. Два ці коди переводять курсор в початок наступного рядка екрану. В даному випадку, коли на екран нічого більше не виводиться, можна було обійтися і без цих код, які включені лише в пізнавальних цілях.
Між іншим, правильна робота програми заснована на тому припущенні (безумовно правильному), що асемблер розташує наші дані в пам'яті в точності в тому ж порядку, як вони описані в програмі. Саме це обставина і дозволяє дробити єдиний рядок на частини, не побоюючись, що в пам'ять вони потраплять в різні місця, що привело б, зрозуміло, до непередбачуваного результату. Після виводу на екран повідомлення про поточний диск в програмі викликається функція DOS з номером 01h. Ця функція вводить з клавіатури один символ. Якщо символів немає (ми після запуску програми не натискали на клавіші), функція 01h чекає натиснення фактично будь-якої клавіші (точніше - будь-якої алфавітно-цифрової або функціональної клавіші). Такий вельми поширений прийом дозволяє зупинити виконання програми до натиснення клавіші, що дає можливість програмістові подивитися, що вивела програма на екран, і оцінити правильність її роботи.

Нарешті, остання дія носить, як вже наголошувалося, сакраментальний характер. Викликом функції DOS 4ch програма завершується з передачею управління DOS.
Поглянемо ще раз на текст програми 3-1. Якщо не рахувати перших пропозицій ініціалізації регістра DS, то в програмі є лише один рядок, що носить, можна сказати, обчислювальний характер, - це збільшення отриманої коди диска до вмісту байта пам'яті. Решта всіх рядків служить для виклику тих або інших функцій DOS - отримання інформації про поточний диск, виведення рядка на екран, зупинки програми і, нарешті, її завершення. Це підтверджує висловлене вище твердження про важливість вивчення системних засобів і широке використання їх в програмах на мові асемблера. Зрозуміло, в програмі можуть бути і скільки завгодно складні і протяжні ділянки обробки даних і інших обчислень, але такі операції, як введення з клавіатури, вивід на екран, робота з файлами, отримання, як в нашому прикладі, системній інформації і багато що інше виконується виключно за допомогою виклику тих або інших функцій DOS (або BIOS). Програму на мові асемблера просто неможливо написати без використання системних засобів.

Структура і образ пам'яті програми .сом

Як вже наголошувалося, програма типу .сом відрізняється від програми типу .ехе тим, що містить лише один сегмент, що включає всі компоненти програми: PSP, програмний код (тобто програмні рядки, що відтранслюють в машинні коди), дані і стек. Структура типової програми типу .сом на мові асемблера виглядає таким чином:

code segment:

assume Cs:text,ds:text

org 100h ;Место для PSP

main proc

...             ; Текст програми

main endp

...           ; Визначення даних

code ends

end main 

Програма містить єдиний сегмент code. У операторові ASSUME вказане, що сегментні регістри CS і DS указуватимуть на цей єдиний сегмент. Оператора ORG 100h резервує 256 байт для PSP. Заповнювати PSP буде як і раніше система, але місце під нього на початку сегменту повинен відвести програміст. У програмі немає необхідності ініціалізувати сегментний регістр DS, оскільки його, як і решта сегментних регістрів, ініціалізувала система. Дані можна розмістити після програмної процедури (як це показано в приведеному прикладі), або усередині ніс, або навіть перед нею. Слід тільки мати на увазі, що при завантаженні програми типу .сом регістр IP завжди ініціалізувався числом 100h, тому відразу услід за оператором ORG 100h повинна стояти перша здійснима команда програми. Якщо дані бажано розташувати на початку програми, перед ними слід помістити оператора переходу на фактичну точку входу, наприклад jmp entry.

Образ пам'яті програми типу .сом показаний на рис.3.1. Після завантаження програми всі сегментні регістри указують на почато єдиного сегменту, тобто фактично на почате PSP. Покажчик стека автоматично ініціалізувався числом Fffeh. Таким чином, незалежно від фактичного розміру програми, їй виділяється 64 Кбайт адресного простору, всю нижню частину якого займає стек. Оскільки верхня межа стека не визначена і залежить від інтенсивності і способу використання стека програмою, слід побоюватися затирання стеком нижньої частини програми. Втім, така небезпека існує і в програмах типу .ехе, оскільки в реальному режимі немає ніяких механізмів захисту, і при збереженні в стеку більшого об'єму даних, чим може так поміститися, дані почнуть затирати поля того сегменту, який розташований за стеком (якщо такий сегмент існує).

Програми типу .сом відрізняються від .ехе-программ не тільки відсутністю сегментів даних і стека. У гл. 2 було показано, що при вирівнюванні сегментів на байт, що робиться за допомогою описувача byte

data segment byte 

системні програми розташовують сегменти завантажуваної програми з деяким перекриттям, що дозволяє уникнути порожніх проміжків між сегментами в пам'яті, що виникають через те, що розміри сегментів можуть бути не кратні величині параграфа - 16 байт. Таке розташування сегментів вимагає зміни значень посилань на адреси елементів пам'яті. До складу програмного файлу з розширенням .ехе входить таблиця з переліком байтів програми, вміст яких може піддатися зміні в процесі завантаження програми в пам'ять. Тому, до речі, розмір файлу з розширенням .ехе може перевищувати дійсний розмір програми в пам'яті.

Програма типу .сом складається з єдиного сегменту, і проблема налаштування посилань не виникає. Файл з розширенням .сом майже в точності відображає вміст пам'яті після завантаження програми. Відмінність полягає тільки в тому, що в програмному файлі відсутній префікс програми PSP, який система вставляє в програму в процесі її завантаження. Таким чином, файл з розширенням .сом зазвичай виявляється на 256 байт коротше за свій образ в пам'яті.

Якщо відтранслювати і ськомпоновать програму, написану у форматі .сом, звичайним способом, утворюється програмний файл з розширенням .ехе. Цей файл можна запустити на виконання, проте працювати він буде невірний. Річ у тому, що система, завантажуючи файл типу .ехе в пам'ять, пристроює перед завантаженою програмою префікс і настроює на нього регістри DS і ES. В результаті значення DS опиниться на 10h менше, ніж сегментна адреса сегменту з командами і даними, що приведе до неправильної адресації при зверненні до полів даних. Програму, написану у форматі .сом, можна запускати тільки у вигляді файлу з розширенням .сом, для якого в DOS передбачений CBI алгоритм завантаження і запуску. Для того, щоб компонувальник створив файл з розширенням .сом, в рядку запуску компонувальника необхідно передбачити ключ /t (при використанні компонувальника TLINK.EXE):

tlink /x /v /3 /t p,p 

Для того, щоб уникнути помилок при підготовці програм, целєє образно підготувати два командні файли для трансляції і компоновки програмних прикладів - один для програм типу .ехе, і інший для програм типу .сом. Зрозуміло, файлам треба призначити розрізняючі імена.
Розглянемо приклад закінченої програми типу .сом, яка виводить на екран рядок тексту.

Приклад 3-2. Проста .COM- програма

assume Cs:code,ds:code

code segment

org 256      ; Місце під PSP

main proc

mov AH, 09h    ; Функція виводу на екран

mov Dx,offset msg

int 21h

mov Ax,4c00h     ; Функція завершення

int 21h                   ; програми

main endp

msg db 16,16,16 ' Програма типу .COM'17,17,17,'$'

code ends

end main 

На початку програми відведене 256 байт під PSP; у програмі відсутня ініціалізація регістра DS; поле даних розміщене в програмному сегменті безпосередньо після останньої команди. Для різноманітності в рядок, що виводиться на екран, включені коди 16 і 17, які відображаються на екрані у вигляді залитих трикутників (рис. 3.2). Як видно з цього малюнка, програма мала ім'я Р. Сом і запускалася з каталога F:\current.

Розглянемо важливе в принциповому плані питання про місце розміщення даних в .сом-программе. У нашому прикладі дані описані в кінці програмного сегменту услід за процедурою main, яка, як і в попередніх прикладах, введена швидше для порядку, ніж з потреби.

Рис. 3.2. Виведення програми 3.2.

З таким же успіхом можна було пропозиція з ім'ям msg помістити після виклику int21h, усередині процедури main. Третій можливий варіант, з яким ми ще зіткнемося в прикладах резидентних програм, приведений нижче.

assume Cs:code,ds:code

code segment

org 256         ; Місце під PSP

main proc

jmp start      ; Перша здійснима команда

msg db  16,16,16,'программа типу .COM',17,17,17,'$'

start:  mov Ah,09h   ; Функція виводу на екран

mov Dx,offset msg

int 21h

...          ;Продовження програми 

Таким чином, дані можуть бути розміщені як після програми, так і серед здійснимих пропозицій програми. Важливо тільки дотримати обов'язкову умову: ні за яких обставин на дані не має бути передане управління. У першому випадку (приклад 3-2) дані поміщені за викликом функції DOS, що завершує програму. Ясно, що після виконання цієї функції управління вже не повернеться в нашу програму, а буде передане командному процесору, тому розміщення тут даних цілком можливо. У останньому фрагменті дані описані, можна сказати, в середині програми. Проте перед ними коштує команда безумовного переходу jmp, яка приводить при виконанні програми до обходу даних.

А ось чого не можна було зробити, так це розмістити дані після закриття сегменту, як це зроблено в приведеному нижче (неправильному!) фрагменті:

...

main endp   ; Кінець процедури

code ends   ; Кінець сегменту

msg db  16,16,16' Програма типу .COM',17,17,17,'$'

end main  


Це друга обов'язкова умова: з чого б не складалася програма, всі її компоненти повинні входити в ті або інші сегменти. Поза сегментами допускаються тільки нетрансльовані директиви асемблера типу .586 або assume.

Нарешті, третя умова, про яку вже мовилося, відноситься тільки до програм типу .COM. DOS, завантаживши програму в пам'ять, ініціалізував покажчик команд числом 100h, тобто адресою першої команди услід за оператором org 100h. Тому головна процедура .сом-программи (якщо в ній є декілька процедур) обов'язково має бути першою, причому перша пропозиція цієї процедури має бути здійснимою командою (наприклад, командою jmp, як це показано вище).

Обробники апаратних переривань

Обробники переривань є найважливішою складовою частиною багатьох програмних продуктів. Як було показано в гл. 1, переривання розділяються на внутрішні, виникаючі в самому мікропроцесорі у разі певних збоїв (спроба ділення на 0, неіснуюча команда), зовнішні, такі, що приходять з периферійного устаткування (клавіатура, миша, диски, нестандартні пристрої, підключені до комп'ютера) і програмні, такі, що є реакцією процесора на команду int з тим або іншим номером. У прикладних програмах доводиться обробляти, головним чином, зовнішні і програмні переривання. Загальні принципи обслуговування тих і інших переривань однакові, проте умови функціонування обробників апаратних переривань мають значну специфіку, зв'язану, головним чином, з тим, що переривання від апаратури приходять в довільні моменти часу і можуть перервати поточну програму в будь-якій її крапці. Обробник переривання має бути написаний так, щоб його виконання ні в якому ступені не відбилося на правильному функціонуванні поточної (що переривається) програми.

Розглянемо схематично структуру і функціонування програмного комплексу, що включає власний обробник якого-небудь апаратного переривання (мал. 3.3).

Рис. 3.3. Функціонування програмного комплексу з обробником прериваній.

Обробник переривань може входити до складу програми у вигляді процедури, або просто бути частиною програми, що починається з деякої мітки (вхідної точки обробника) і командою виходу, що завершується, з переривання iret. Поки ми не розглядатимемо складніший випадок, коли обробник є самостійною резидентною програмою.

Програма, починаючи свою роботу, перш за все повинна виконати дії, що ініціалізували, з установки обробника переривань. У простому випадку ці дії полягають в занесенні у відповідний вектор повної адреси (сегменту і зсуву) обробника. Оскільки обробник входить до складу програми, його відносна адреса відома; це ім'я його процедури або мітка вхідної крапки. Що ж до сегментної адреси, то обробник може входити в сегмент основної частини програми, якщо вона невелика за об'ємом і займає один сегмент, але може утворювати і самостійний сегмент. У будь-якому випадку як сегментна адреса можна використовувати ім'я відповідного сегменту.
Часто ініціалізація обробника, окрім установки вектора, припускає і інші дії: збереження початкового вмісту вектора переривання, размаськированіє відповідного рівня переривань в контроллері переривань, посилка в пристрій команди дозволу переривань і інш.

Встановивши обробник, програма може приступити до подальшої роботи. У разі приходу переривання, процесор зберігає в стеку прапори і поточну адресу програми, витягує з вектора адресу обробника і передає управління на його вхідну крапку. Весь час, поки виконується програма обробника, основна програма, природно, стоїть. Що завершує обробник команда irct витягує із стека збережені там дані і повертає управління в перервану програму, яка може продовжити свою роботу. Подальші переривання обслуговуються так само.

Функції обробника переривань залежать від вирішуваного завдання і призначення того пристрою, від якого поступають сигнали переривань. У разі переривань від клавіатури завдання обробника переривань - прийняти і зберегти код натиснутої клавіші. Переривання від миші свідчать про її переміщення, що вимагає оновлення положення курсора на екрані. Якщо обслуговуваним пристроєм є фізична установка, то сигнал переривання може говорити про те, що в установці накопичений певний об'єм даних, які треба перенести з пам'яті установки в пам'ять комп'ютера. У будь-якому випадку обробник переривань має бути програмою нескладною, для виконання якої не потрібний багато машинного часу.

Розглянемо структуру програми з обробкою апаратних переривань. Найбільш зручним апаратним перериванням, яке можна використовувати в дослідницьких цілях, є переривання від системного таймера, яке генерується 18.2 разу в секунду і служить джерелом сигналів для ходу системного годинника, що відлічує час, минулий після включення машини. Заміна системного обробника на прикладній не приводить до яких-небудь неприємностей, окрім, можливо, зупинки на деякий час системного годинника.

Вважатимемо, що наш програмний комплекс є програмою типу .ехе і що обробник переривань входить в загальний з основною програмою програмний сегмент. Для визначеності використовуватимемо вектор 08h, хоча, зрозуміло, для будь-якого іншого апаратного вектора структура програми залишиться тій же. Спочатку приведемо текст програми з деякими купюрами.

Приклад 3-3. Обробник переривань від таймера

code segment

assume Cs:code,ds:data

;Головна процедура

main proc

mov Ax,data   ; Ініціалізація сегментного 

mov Ds,ax     ; регістра DS

;Збережемо початковий вектор

mov Ah,35h    ; Функція отримання вектора

mov Al,08h    ; Номер вектора

int 21h

mov word ptr old_08,bx ; Зсув початкового обробника

mov word ptr old_08+2,es ; Сегмент початкового обробника

;Встановимо наш обробник

mov Ah,25h    ;Функция заповнення вектора

mov Al,08h    ; Номер вектора

mov Dx,offset new_08 ; Зсув нашого обробника

push DS                    ; Збережемо Ds=data

push CS                    ; Перепишемо CS в DS

pop DS                      ; через стек. Ds:dx->new_08

int 21h

pop DS                         ; Відновлений Ds=data

...                                   ; Продовження основної програми

; Перед завершенням програми відновимо початковий вектор

Ids DX,old_08      ; Заповнимо Ds:dx з old_08

mov Ah,25h                     ; Функція заповнення вектора

move Al,08h           ; Номер вектора

int 21h

mov Ax,4c00h         ;Функция завершення програми

int 21h

main endp

;Процедура обробника переривань від таймера

new_08 proc

...                ; Дії. виконувані 18 разів в секунду

mov Al,20h     ;Разблокировка переривань

out 20h,al      ; у контроллері переривань

iret                   ; Повернення в перервану програму

new_08   endp

code ends

data segment

old_08 db 0   ; Осередок для зберігання початкового вектора

data ends

stk segment stack

db 256 dup(U)

stk ends

 end main 

У приведеному прикладі обробник переривань розташований в кінці програми, після головної процедури main. Взаємне розташування процедур не має анінайменшого значення; з таким же успіхом обробник можна було помістити на початку програми. Не має також значення, чи виділений обробник в окрему процедуру або просто починається з мітки.

Для того, щоб прикладний обробник отримував управління в результаті переривань, його адресу слід помістити у відповідний вектор переривання. При цьому початковий вміст вектора буде затертий, і якщо переривання поступатимуть і після завершення програми, виникне вельми неприємна ситуація, коли управління передаватиметься за адресою, по якій в пам'яті може розташовуватися що завгодно. Тому стандартною методикою є збереження в пам'яті початкового вмісту вектора і відновлення цього вмісту перед завершенням програми.

Хоча і читання, і заповнення вектора переривань можна виконати за допомогою простих команд mov, проте переважно використовувати спеціально передбачені для цього функції DOS. Для читання вектора використовується функція з номером 35h. У регістр AL поміщається номер вектора. Функція повертає початковий вміст вектора в парс регістрів Es:bx (легко здогадатися, що в ES сегментна адреса, а у ВХ зсув). Для зберігання початкового вмісту вектора в сегменті даних передбачений двухсловная осередок old_08. У молодшому слові цього осередку (з фактичною адресою old_08) буде зберігається зсув, в старшому (з фактичною адресою old_08+2) - сегментна адреса. Для того, щоб звернутися до слів, складовим цей осередок, доводиться використовувати описувач word ptr, який як би примушує транслятор на якийсь час забути про початкове оголошення осередки і дозволяє розглядати її, як два окремі слова. Якби ми відвели для початкового вектора два 16-бітові осередки, наприклад

old_08_offs dw 0 ; Для зсуву

old_08_seg dw 0 ;Для сегментної адреси 

то до них можна було б звертатися без всяких описувачів.

Зберігши початковий вектор, можна встановити в нім адресу нашого обробника. Для установки вектора в DOS передбачена функція 25h. Вона вимагає вказівки номера встановлюваного вектора в регістрі AL, а його повної адреси - в парс регістрів Ds:dx. Тут нас підстерігає неприємність. Занести в регістр DX зсув нашого обробника new_08 не складає труднощів, це робиться командою

mov Dx,offset new_08 ;Смещение нашого обробника 

Проте регістр DS у нас зайнятий - в нім зберігається сегментна адреса сегменту даних. Доведеться на якийсь час зберегти цю адресу, для чого найзручніше скористатися стеком. Звідки узяти сегментну адресу обробника? Між іншим, в мові асемблера існує спеціальна конструкція, що дозволяє визначити сегментну адресу будь-якого поля. У нашому випадку вона виглядала б таким чином:

mov Ax,seg new_08  ; Отримаємо сегмент з процедурою new_08

mov Ds,ax                 ;  Перепишемо його в DS 

У прикладі 3-3 використаний інший прийом - вміст CS відправляється в стек і тут же витягується звідти в регістр DS:

push CS pop DS 

Після повернення з DOS треба не забути відновити початковий вміст DS, збережений в стеку. Ініціалізація обробника переривань закінчена. Починаючи з цього моменту, кожен сигнал таймера приводитиме до переривання основної програми, що продовжується, і передачі управління на процедуру new_08.

Перед завершенням програми необхідно помістити у вектор 8 адреса початкового, системного обробника, який був збережений в двухсловном полі old_08. Перед викликом функції 25h установки вектора в регістри Ds:dx треба занести вміст цього двухсловного поля. Цю операцію можна виконати однією командою Ids, якщо вказати як її перший операнд регістр DX, а як другий - адреса двухсловной осередку, в нашому випадку old_08. Саме маючи на увазі використання цієї команди, ми і оголосили поле для зберігання вектора двухсловним, чому виникли деякі труднощі при його заповненні командами mov. Якби ми використовували другий запропонований вище варіант і відвели для зберігання вектора два однослівні осередки (old_08_offs і old_08_seg), то команду Ids довелося б забезпечити описувачем зміни розміру осередку:

Ids Dx,dword ptr old_08_offs 

Між іншим, тут так само руйнується вміст DS, але оскільки відразу ж услід за функцією 25h викликається функція 4ch завершення програми, це не має значення.
Останнє, що нам залишилося розглянути - це стандартні дії із завершення самого обробника переривань. Вище вже мовилося, що останньою командою обробника має бути команда iret, що повертає управління в перервану програму. Проте перед нею необхідно виконати ще одну обов'язкову дію - послати в контроллер
переривань команду кінця переривань. Річ у тому, що контроллер переривань, передавши в процесор сигнал переривання INT, блокує усередині себе лінії переривань, починаючи з тієї, яка викликала дане переривання, і до останньої в порядку зростання номерів IRQ. Таким чином, переривання, що прийшло, наприклад, по лінії IRQ 6 (гнучкий диск) заблокує подальшу обробку переривань по лініях 6 і 7, а переривання від таймера (Irq0) блокує взагалі всі переривання (Irq0...IRQ7, а також і Irq8...IRQ15, оскільки всі вони є розгалуженням рівня Irq2, см, гл. 1, мал. 1.11). Будь-який обробник апаратного переривання зобов'язаний перед своїм завершенням зняти блокування в контроллері переривань, інакше вся система переривань вийде з ладу. Зняття блокування здійснюється посилкою команди з кодом 20h в один з двох портів, закріплених за контроллером переривань. Для провідного контроллера ця команда посилається в порт 20h, для веденого - в порт A0h. Таким чином, якби ми обробляли переривання від годинника реального часу (лінія переривань Irq8, вектор 70h, ведений контроллер), то команда кінця переривання виглядала б так:

mov Al,20h  ;Команда кінця переривання

out A0h,al   ; Пошлемо її в порт веденого контроллера 

Вказану послідовність команд іноді називають наказом, або командою EOI (від end of interrupt, кінець переривання).

Знається на цих загальних питаннях, розглянемо приклад реальної програми, що включає обробник переривань від таймера. Для того, щоб приведену вище фрагментарну програму перетворити в ту, що діє, треба написати змістовну частину самого обробника, а також придумати, що робитиме основна програма після ініціалізації переривань. Все це зробити дуже просто.

Хай наш обробник у відповідь на кожне переривання від таймера виводить на екран який-небудь символ. Для цього можна скористатися функцією 0eh переривання BIOS 10h. Це переривання обслуговує велика кількість різних функцій, що забезпечують управління екраном. Сюди входять функції виведення символів і рядків, налаштування режимів відеосистеми, завантаження нестандартних таблиць символів і багато інших. Функція 0eh призначена для виводу на екран окремих символів. Вона вимагає вказівки в регістрі AL коди символу, що виводиться. Процедура new_08 виглядатиме в цьому випадку таким чином:

; Обробник переривань для прикладу 3-3

new_08 proc

push AX      ;Сохраним початкове значення AX

mov Ah,0eh   ; Функція виведення символу

mov Al,'@'     ; Символ, що виводиться

int 10 h            ; Перехід в BIOS

mov Al,20h    ; Розблокування переривань

out 20h,al      ; у контроллері переривань

pop AX           ; Відновлений AX

iret                   ; Повернення в перервану програму

new_08  endp 

Що ж до основної програми, те саме просте включити в неї (після завершення дій з ініціалізації обробника переривань) функцію DOS 0 Hi очікування введення з клавіатури:

mov Ah,01h

int 21h 

В результаті програма, дійшовши до цих рядків, зупиниться (фактично виконуватиметься цикл опиту клавіатури в очікуванні натиснення клавіші, включений до складу програми реалізації функції 01h DOS), а на екран безперервною низкою виводитимуться символи комерційного at (рис. 3.4). Після натиснення на будь-яку клавішу програма завершиться.

Рис. 3.4. Виведення програми 3-3

Приведений приклад дозволяє обговорити надзвичайно важливе питання про взаємодію обробників апаратних переривань з програмою, що переривається, і операційною системою. Особливістю апаратних переривань є те, що вони можуть виникнути у будь-який момент часу і, відповідно, перервати виконувану програму в будь-якій крапці. Поточна програма, зрозуміло, використовує регістри, як загального призначення, так і сегментні. Якщо в обробнику переривання ми зруйнуємо вміст хоч би один з регістрів процесора, перервана програма щонайменше продовжить своє виконання неправильним чином, а швидше за все відбудеться зависання системи. Тому в будь-якому обробнику апаратних переривань необхідно в самому його початку зберегти всі регістри, які використовуватимуться в програмі обробника, а перед завершенням обробника (перед командою iret) відновити їх. Регістри, які обробником не використовуються, зберігати не обов'язково.
У нашому простому обробнику використовується тільки один регістр АХ. Його ми і зберігаємо в стеку першою ж командою push AX, відновлюючи в самому кінці обробника командою pop AX.

Друга неприємність може виникнути через те, що в обробнику апаратного переривання ми скористалися для виводу на екран функцією BIOS. Взагалі кажучи, вважається, що в обробниках апаратних переривань не можна використовувати ніякі системні засоби. Причина такої заборони полягає в тому, що апаратне переривання може прідті у будь-який момент, зокрема тоді, коли що переривається програми сама виконує яку-небудь функцію DOS або BIOS. Проте, якщо ми перервемо виконання системної функції на півдорозі, і почнемо виконувати ту ж саму або навіть іншу функцію з початку, відбудеться руйнування системи, яка не передбачає таке "вкладене" виконання своїх програм. В даний час розроблені програмні прийоми, що дозволяють ефективно обійти вказану заборону, проте використання їх в програмі драматично збільшує її складність і об'єм, і розглядати ці прийоми ми тут не будемо.

У нашому випадку справа посилюється тим, що переривання від таймера не тільки можуть прідті в той момент, коли виконується функція DOS, але і неминуче приходять тільки в такі моменти, оскільки ми зупинили програму за допомогою виклику функції 01h переривання 2hi і, отже, весь час, поки наша програма чекає натиснення клавіші, насправді виконуються внутрішні програми DOS. Саме тому ми відмовилися від використання в обробнику переривання функції DOS і виводимо на екран символи за допомогою переривання BIOS. Виконання функції BIOS "на фоні" DOS не так небезпечно. Треба тільки стежити за тим, щоб наше переривання BIOS в обробнику не виявилося вкладеним в таке ж переривання BIOS в програмі, що переривалася.

Розглянутий приклад має істотний недолік. Записавши у вектор переривань 8 адреса нашого обробника, ми затерли початковий вміст вектора і тим самим ліквідовували (у логічному плані) початковий, системний обробник. Практично це приведе до того, що на час роботи нашої програми зупиниться системний годинник, і якщо в системі є якісь інші програми, що використовують переривання від таймера, вони перестануть працювати належним чином. Ліквідовувати вказаний недолік дуже просто: треба "зчепити" наш обробник з системним так, щоб у відповідь на кожен сигнал переривання активізувалися послідовно обидва обробники. Розглянемо методику зчеплення обробників.

При ініціалізації прикладного обробника, що зчіплюється з системним, слідує точно так, як і раніше, зберегти в програмі адресу системного обробника і помістити у вектор переривання адресу прикладного обробника. Змінам піддасться тільки програма самого обробника, почало якій повинно виглядати таким чином:

;Зчеплення прикладного обробника з системним

; для програми 3-3

new_08 proc

pushf            ;Отправляем у стек слово флигов

call Cs:old 08   ; У системний обробник

...      ;Продовження програми обробника

iret

new_08 endp    

Як працюватиме така програма? Після того, як процесор виконає процедуру переривання, в стеку перерваного процесу опиняються три слова: слово прапорів, а також двухсловний адреса повернення в перервану програму (ріс.3.5, нижні три слова стека).

Рис. 3.5. Стек перерваної програми в процесі виконання прикладного обробника переривань.

Cs1 - сегментна адреса перерваного процесу;
Ip1 - зсув точки повернення в перервану програму;
Cs2 - сегментна адреса прикладного обробника;
Ip2 - зсув точки повернення в прикладний обробник. 

Саме така структура даних має бути на верху стека, щоб команда iret, якою завершується будь-який обробник переривань, могла повернути управління в перервану програму.

Перша команда нашого обробника pushf засилає в стек ще раз слово прапорів, а команда дальнього виклику процедури call cs:old_08 (де осередок old_08 оголошений за допомогою оператора dd подвійним словом) в процесі передачі вправи системному обробникові поміщає в стек двухсловний адресу повернення на наступну команду прикладного обробника. В результаті в стеку формується трехсловная структура, яка потрібна команді iret.

Системний обробник, закінчивши обробку даного переривання, завершується командою iret. Ця команда забирає із стека три верхні слова і здійснює перехід за адресою Cs2:ip2, тобто на продовження прикладного обробника.

Завершуюча команда нашого обробника iret знімає із стека три верхні слова і передає вправу за адресою Cs1:ip1.
Описана методика зчеплення прикладного обробника з системним використовується надзвичайно широко і носить спеціальну назву перехоплення переривання.

Обробники програмних переривань

Програмні переривання викликаються командою int, операндом якої служить номер вектора з адресою обробника даного переривання. Команда int використовується перш за все, як стандартний механізм виклику системних засобів. Так, команда int 2 Hi дозволяє звернутися до численних функцій DOS, а команди int 10h, int 13h або int 16h - до груп функцій BIOS, що відповідають за управління тими або іншими апаратними засобами комп'ютера. У цих випадках обробники переривань є готовими системними програмами, і в завдання програміста входить тільки виклик необхідного програмного засобу за допомогою команди int з відповідним номером.

У деяких спеціальних випадках, проте, програмістові доводиться писати власний обробник переривання, яке вже обслуговується системою. Таким чином, наприклад, здійснюється управління резидентними програмами, які для зв'язку із зовнішнім світом зазвичай використовують переривання 2fh. У кожній резидентній програмі є власний обробник цього переривання, який, виконавши свою частку дій, передає управління "попередньому", адреса якого знаходилася раніше у векторі 2fh, і був збережений обробником в своїх полях даних. Інший приклад - перехоплення переривань BIOS в обробниках апаратних переривань з метою виявлення моментів часу, коли жодна з наявних програм не використовує дане переривання і, отже, сам обробник може їм скористатися.

Нарешті, прикладний програміст може скористатися одним з вільних векторів, написати власний обробник відповідного переривання і залишити його резидентним в пам'яті. Після цього будь-які програми можуть за допомогою команди int викликати цей обробник, який, таким чином, стає резидентною програмою загального користування.

Резидентні програми

Великий клас програм, що забезпечують функціонування обчислювальної системи (драйвери пристроїв, оболонки DOS, русифікатори, інтерактивні довідники і ін.), повинні постійно знаходитися в пам'яті і миттєво реагувати на запити користувача, або на якісь події, що відбуваються в обчислювальній системі. Такі програми носять назви програм, резидентних в пам'яті (Terminate and Stay Resident, TSR), або просто резидентних програм. Зробити резидентною можна як програму типу .сом, так і програму типу .ехе, проте оскільки резидентна програма має бути максимально компактною, найчастіше як резидентні використовують програми типу .сом.
Програми, призначені для завантаження і залишення в пам'яті, зазвичай складаються з двох частин (секцій) - що ініціалізувала і робочою (резидентною). У тексті програми резидентна секція розміщується на початку, що ініціалізувала - за нею.

При першому виклику програма завантажується в пам'ять цілком і управління передається секції ініціалізації, яка заповнює або модифікує вектори переривань, настроює програму на конкретні умови роботи (можливо, виходячи з параметрів, переданих програмі при її виклику) і за допомогою переривання DOS Int 21h з функцією 31h завершує програму, залишаючи в пам'яті її резидентну частину. Розмір резидентної частини програми (у параграфах) передається DOS в регістрі DX. Указувати при цьому сегментну адресу програми немає необхідності, оскільки він відомий DOS. Для визначення розміру резидентної секції її можна завершити пропозицією вигляду

ressize=$-main 

де main - зсув почала програми, а при виклику функції ЗШ в регістр DX заслати результат обчислення виразу (rcsslze+10fh) /16.

Різниця S - main є розміром головної процедури. Проте перед головною процедурою розміщується префікс програми, що має розмір 100h байт, який теж треба залишити в пам'яті. Далі, при цілочисельному діленні відкидається залишок, тобто відбувається округлення результату у бік зменшення. Для компенсації цього дефекту можна додати до ділимого число 15 = Fh. Ділення всього цього виразу на 16 дасть необхідний розмір резидентної частини програми в параграфах (можливо, з невеликим шматочком секції ініціалізації величиною до 15 байт).

Функція 31h, закріпивши за резидентною програмою необхідну для її функціонування пам'ять, передає управління командному процесору COMMAND.СОМ, і обчислювальна система переходить, таким чином, в початковий стан. Наявність програми, резидентної в пам'яті, ніяк не відбивається на ході обчислювального процесу за винятком того, що зменшується об'єм вільної пам'яті. Одночасно може бути завантажене декілька резидентних програм.

Для того, щоб активізувати резидентну програму, їй треба якось передати управління і, можливо, параметри. Як правило, активізація резидентної програми здійснюється за допомогою механізму переривань.

Крім того, спеціально для взаємодії з резидентними програмами в DOS передбачено мультиплексне переривання 2fh.

Розглянемо типову структуру резидентної програми і системні засоби залишення її в пам'яті. Як вже наголошувалося, резидентні програми найчастіше пишуться у форматі .сом:

code segment

assume Cs:text,ds:text

org 100h

main proc

jmp init    ;Переход на секцію ініціалізації

...              ; Дані резидентної секції програми

entry:        ; Точка входу при активізації

...              ; Текст резидентної секції програми

iret

main endp

ressize=$-myproc    ; Розмір (у байтах) резидентної секції

init proc                     ; Секція ініціалізації

...

mov DX,(ressize+1ofh) /16  ;Размер у параграфах

mov Ax,3100h                        ;Функция "завершити і

int 21h                                      ; залишити в пам'яті"

init endp

code ends

end main 

При першому запуску програми з клавіатури управління передається на почато процедури main (перший байт після префікса програми). Командою jmp здійснюється перехід на секцію ініціалізації, в якій, зокрема, готуються умови для подальшої активізації програми вже в резидентному стані. Останніми рядками секції ініціалізації викликається функція ЗШ, яка виконує завершення програми із залишенням в пам'яті вказаної її частини. З метою економії пам'яті секція ініціалізації розташовується в кінці програми і відкидається при її завершенні.

Змістовна частина резидентної програми, що починається з мітки entry, активізується, як вже наголошуюся вище, за допомогою апаратного або програмного переривання і закінчується командою iret. На мал. 3.6 приведена типова структура резидентної програми.

Рис. З.6. Структура резидентної програми.

Як видно з мал. 3.7, резидентна програма має принаймні дві точки входу. Після завантаження програми в пам'ять командою оператора, що вводиться на командному рядку, управління передається в крапку, вказану в полі що завершує текст програми оператора end (на малюнку - початок процедури main). Для програм типу .сом ця точка входу повинна відповідати найпершому рядку програми, що йде услід за префіксом програми. Оскільки при завантаженні програми повинна виконатися її установка в пам'яті, першою командою програми завжди є команда переходу на секцію ініціалізації і установки (jmp init на малюнку).

Після установки в пам'яті резидентна програма залишається пасивною і ніяк не проявляє свого існування, поки не буде активізована передбаченим в ній для цього способом. Ета, друга точка виклику позначена на малюнку міткою entry.

На жаль, резидентні програми, що виконують корисну роботу, виявляються досить складними. Ми ж як приклад можемо розглянути тільки зовсім просту резидентну програму, в принципі правильну і працездатну, але не претендуючу на практичну цінність. Програма активізується перериванням від клавіші Print Screen і виводить на екран вміст сегментного регістра CS, що дозволяє визначити її положення в пам'яті.

Як відомо, клавіша Print Screen в DOS виконує друк вмісту екрану на принтері. Який механізм цієї операції? При натисненні на будь-яку клавішу клавіатури виникає сигнал переривання, що ініціює активізацію обробника переривань від клавіатури, BIOS, що знаходиться в ПЗП. При натисненні на алфавітно-цифрових і деякі інші клавіші (наприклад, функціональні клавіші <F1>...F<12>) обробник зберігає у визначеному місці пам'яті код натиснутої клавіші і завершується. Поточна програма може за допомогою відповідних функцій DOS або BIOS витягувати цей код і використовувати його в своїх цілях. Якщо ж користувач натискає на клавішу Print Screen, то обробник переривань, в числі інших дій, виконує команду hit 5, передаючи управління через вектор 5 на обробник цього програмного переривання, який теж розташовується в ПЗП BIOS. Завдання обробника переривання 5 полягає в читанні вмісту відеобуфера і виведенні його на пристрій друку.

Таким чином, якщо ми напишемо власний обробник переривання і помістимо його адресу у вектор з номером 5, він активізуватиметься натисненням клавіші Print Screen. Звернете увагу на ту обставину, що переривання 5 є перериванням програмним; воно збуджується командою int 5 і не має відношення до контроллера переривань. Проте активізується це переривання не командою int в прикладній програмі, а натисненням клавіші, тобто, фактично, апаратним перериванням.
Перехоплення переривання 5 здійснюється значно простішим, через перехоплення "дійсного" апаратного переривання від клавіш клавіатури, із-за чого ми і скористалися ним в нашому прикладі.

code segment

assume Cs:text

org 100h

main proc

jmp init      ; Перехід на секцію ініціалізації

new_05: push AX    ; Збережемо регістри AX і BX

push BX                    ; використовувані далі

mov Bx,cs             ; Bx= сегментна адреса програми

mov Ah,0eh            ; Функція виводу на екран символу

mov Al,bh              ; Виведемо старшу половину

                                  ; сегментної адреси

int 10h                      ; Виклик BIOS

pop BX                    ; Відновимо

pop AX                    ; регістри

iret                            ; Завершення обробника

main endp

init proc                    ; Секція ініціалізації

mov Ax,2505h        ; Функція установки вектора

mov Dx,offset new_05  ;Смещение обробника

int 21h                                 ; Виклик DOS

mov DX,(init-main+10fh) /16  ; Розмір в параграфах

mov Ax3100h              ;Функция " завершити і

int 21h                            ; залишити в пам'яті"

init endp

code ends

end main 

Структура програми відповідає описаною раніше. У секції ініціалізації виконується установка обробника переривання 05h, при цьому початковий вміст вектора 5 не зберігається. Це, зрозуміло, дуже погано, оскільки позбавляє нас можливості цей вектор відновити. З іншого боку, відновлювати перехоплені вектори належить при завершенні програми, а стосовно резидентної програми - при її вивантаженні з пам'яті. Проте в нашій простий програмі не передбачено засобів вивантаження (процедура вивантаження досить складна), і програмі доведеться знаходитися в пам'яті до перезавантаження машини.

Встановивши вектор, програма завершується із залишенням в пам'яті її резидентної частини за допомогою функції 31h.

Резидентна частина програми є класичним обробником програмного переривання. По-перше ж пропозиціях зберігаються регістри АХ і ВХ, використовувані далі в програмі, а потім вміст сегментного регістра CS переноситься в регістр ВХ. З таким же успіхом можна було скопіювати вміст будь-якого з регістрів DS, ES або SS, оскільки в програмі типу .сом всі регістри налаштовані на одну і ту ж сегментну адресу (див. рис. 3.1). Копіювання з сегментного регістра в регістр загального призначення знадобився тому, що надалі нам доведеться працювати з окремими половинками сегментної адреси, а у сегментних регістрів половинок немає.
Далі старша половина сегментної адреси заноситься в регістр AL, і викликом вже знайомої нам функції BIOS 0 Eh цей код виводиться на екран. Потім таким же чином виводиться молодша половина сегментної адреси. Нарешті, після відновлення регістрів ВХ і АХ (у зворотному порядку по відношенню до їх збереження) командою iret управління повертається в перервану програму, якій в даному випадку є COMMAND.COM.

Виведення програми (їй для наочності було дано ім'я TSR.COM) для конкретного прогону показане на рис. 3.7.

Рис. 3.7. Виведення програми 3-4.

Отриманий результат далекий від наочності. Дійсно, розділивши сегментну адресу на дві половини завдовжки в байт кожна, ми просто записали у відеобуфер ці числа. Кожне число розміром в байт можна трактувати, як код ASCII якогось символу. При виведенні числа на екран ці символи і відображаються. Зображення пікового туза відповідає коду 06, а знак рівності має код 3dh (див. таблицю код ACSII на рис. 3.1). Таким чином, сегментна адреса резидентної програми, що знаходиться в пам'яті, опинилася рівний 063dh, що відповідає приблизно 25 Кбайт. Так і повинно бути, оскільки конфігурація комп'ютера, використаного для підготовки прикладів, передбачала зберігання більшої частини DOS в розширеній пам'яті, в області НМА. У основній пам'яті в цьому випадку розташовується шматочок DOS разом з драйверами обслуговування розширеної пам'яті і частиною програми COMMAND.COM загальним об'ємом близько 25 Кбайт.

Для того, щоб отримати на екрані сегментну адресу в звичній нам формі, його двійкове машинне уявлення необхідно перетворити в коди ASCII, що відображають шістнадцятиричне (або, якщо завгодно, десяткове) представлення цього числа. У нашому прикладі, щоб отримати на екрані зображення числа 063dh, треба було сформувати такий ланцюжок код ASCII (у шістнадцятиричному уявленні):

30 36 33 44 68 

Розглянутий метод виводу на екран чисел у вигляді зображень символів, звичайно, далекий від досконалості, проте підкуповує свій винятковою простотою і цілком може бути використаний в процесі відладки резидентних програм і обробників переривань, включення в яких досить громіздких програм тієї, що перекодувала може виявитися небажаними або навіть неможливими.

Читач може, підготувавши розглянутий приклад, завантажити декілька екземплярів програми і подивитися, як змінюються в цьому випадку їх початкові адреси.

загрузка...
Сторінки, близькі за змістом