МІНІСТЕРСТВО
ОСВІТИ УКРАЇНИ
Житомирський інженерно-технологічний інститут
Данильченко
О.М.
Власенко О.В.
Защипас С.М.
СИСТЕМНЕ
ПРОГРАМУВАННЯ
КУРС
ЛЕКЦІЙ
(
Частина ІІ )
Житомир 2001
Курс лекцій з дисципліни «Системне програмування» для
студентів денної та заочної форм навчання за професійним напрямком
«Інформаційно-комп‘ютерні технології».
Курс
Лекцій / Укл. Данильченко О.М., Власенко О.В., Защипас C.М. –
Житомир: ЖІТІ, 2001. – 138 c.
Викладено основи системного програмування на прикладі
операційної системи MS-DOS. Посібник складається з трьох частин. Перша та друга
частини – теоретичні, містять курс лекцій для дисципліни “Системне
програмування”; третя частина – практична, містить завдання та приклади їх
розв‘язування, а також рекомендації для виконання курсових та лабораторних робіт.
Житомирський
інженерно-технологічний інститут, 2001.
Укладачі:
Данильченко О.М., Власенко О.В. Защипас С.М.
Відповідальний
редактор: Данильченко Олександр Михайлович
Рецензент:
Затверджено на
засіданні кафедри ПЗОТ
Протокол № від 2001 р.
ЗМІСТ
1 Динамічне керування пам’яттю....................................................... 7
1.1 Керування пам’яттю в MS-DOS................................................................ 7
1.1.1 Функціональні виклики MS-DOS для керування пам'яттю................... 7
1.1.2 Блок керування пам’яттю та його структура......................................... 7
1.1.3 Робота MS-DOS з блоками пам’яті........................................................... 9
1.2 Програма-завантажник системи програмування на Сі........................ 11
1.3 Моделі пам’яті Turbo С.............................................................................. 12
1.3.1 Загальна характеристика моделей пам’яті.............................................. 12
1.3.2 Модель пам’яті TINY.............................................................................. 15
1.3.3 Модель пам’яті SMALL.......................................................................... 16
1.3.4 Модель пам’яті MEDIUM...................................................................... 17
1.3.5 Модель пам’яті COMPACT................................................................... 19
1.3.6 Модель пам’яті LARGE.......................................................................... 19
1.3.7 Модель пам’яті HUGE............................................................................ 20
1.4 Динамічне керування пам'яттю в "купі".............................................. 22
2 ВВЕДЕННЯ ІНФОРМАЦІЇ З КЛАВІАТУРИ.................................................. 33
2.1 Загальні положення.................................................................................... 33
2.2 Апаратні засоби комп'ютера для введення інформації з клавіатури 33
2.2.1 Аналіз і перетворення скен-коду............................................................ 34
2.2.2 Буфер клавіатури.................................................................................... 36
2.3 Введення інформації з клавіатури засобами MS-DOS........................ 37
2.3.1 Функції переривання 21h MS-DOS для введення інформації з клавіатури 37
2.3.2 Функції бібліотеки Turbo С..................................................................... 39
2.4 Введення інформації з клавіатури засобами BIOS.............................. 42
2.4.1 Використання переривання 16h BIOS.................................................... 42
2.4.2 Функції бібліотеки Turbo С..................................................................... 43
3 РОЗРОБКА РЕЗИДЕНТНИХ ПРОГРАМ....................................................... 45
3.1 Введення в резидентні програми.............................................................. 45
3.1.1 Нерезидентне та резидентне завершення програми. Резидентна програма як обробник переривання 45
3.1.2 Вимоги до програми-обробника переривання. Модифікатор типу функції interrupt 46
3.1.3 Передача і повернення значень в обробниках переривань.................. 48
3.1.4 Загальний дизайн резидентної програми. Каскадне включення обробників переривань 50
3.2 Дії, які виконуються ініціалізуючою частиною TSR-програми........ 53
3.2.1 Перехоплення переривання..................................................................... 53
3.2.2 Резидентне завершення............................................................................ 61
3.2.3 Запобігання повторного завантаження................................................... 63
3.3 Побудова резидентної частини TSR......................................................... 69
3.3.1 Переключення стека................................................................................ 69
3.3.2 Переключення PSP.................................................................................. 72
3.3.3 Запобігання повторного входження в MS-DOS.................................... 73
3.3.4 Захищений обмін з диском. Свопінг........................................................ 76
3.3.5 Перехоплення переривання критичної помилки................................... 76
3.3.6 Перехоплення переривання по натисканню клавіш Ctrl-Break........... 76
3.3.7 Використання переривання таймера...................................................... 76
3.4 Видалення резидентної програми з пам'яті........................................... 76
3.5 Резидентна програма запису текстового вікна у файл......................... 76
4 КЕРУВАННЯ ВІДОБРАЖЕНОЮ ТА РОЗШИРЕНОЮ ПАМ'ЯТТЮ..... 76
4.1 Відображена пам'ять EMS......................................................................... 76
4.1.1 Елементи механізму відображеної пам'яті.............................................. 76
4.1.2 Основні функції інтерфейсу з відображеною пам'яттю........................ 76
4.2 Керування розширеною пам'яттю............................................................ 76
4.2.1 Загальні положення.................................................................................. 76
4.2.2 Функції керування розширеною пам'яттю............................................ 76
4.2.3 Особливості використання НМА........................................................... 76
MS-DOS є однопрограмною операційною системою, здатною оперувати адресним простором не більш 1 Мб.
У склад MS-DOS включені три функції переривання 21h для керування пам'яттю:
AH = 48h - розподіл пам'яті (Allocate Memory);
AH = 49h - звільнення пам'яті (Deallocate Memory);
АН = 4Ah - зміна розміру блоку пам'яті (Setblock Memory).
Про виникнення помилки при виконанні функції керування пам'яттю повідомляють встановленням прапора переносу CF. У випадку, якщо при виході з функції СF=1, регістр АХ містить код помилки. Для функцій керування пам'яттю MS-DOS повідомляє наступні коди помилок:
АХ=7 - зруйнований блок керування пам’яттю МСВ (Memory Control Block) (зустрічається для усіх трьох функцій);
АХ=8 - недостатньо вільної пам’яті (зустрічається для функцій 48h і 4Ah при запиті на збільшення розміру блоку);
АХ=9 - неприпустима адреса блоку пам’яті (зустрічається для функцій 49h і 4Ah).
Для того щоб відслідковувати розподіл пам’яті, MS-DOS веде спеціальний зв'язний список керуючих блоків - блоків керування пам’яттю, або МСВ (Memory Control Block), кожний МСВ займає цілий параграф і безпосередньо передує блоку пам’яті - безперервної області пам’яті, що починається на 16-байтовій границі. Спеціальна внутрішня змінна MS-DOS містить покажчик на перший МСВ ланцюжка. Значними є перші 5 байт МСВ:
байт 0 - ASCII-символ 5А ('Z'), якщо даний блок останній у ланцюжку МСВ - блоків; ASCII-символ 4Dh ('M') у протилежному випадку;
байти 1-2 - рівні нулю, якщо блок вільний; більші нуля, якщо блок зайнятий. У останньому випадку байти 1-2 МСВ містять PID програми, для якої MS-DOS розподіляла блок;
байти 3-4 - розмір блоку в 16-байтових параграфах.
Рис. 1.1 Ланцюжок блоків керування пам’яттю
Параграф, в якому розташовується перший МСВ, зберігається MS-DOS в спеціальній внутрішній змінній. Рис. 1.1 ілюструє використання МСВ.
Номер параграфу (сегмент адреси) seg_mcb, по якому розташовується наступний МСВ - блок в ланцюжку, визначається по формулі
seg_mcb = seg_mcb_old + length + 1,
де seg_mcb_old - сегмент, по якому розташовується попередній МСВ - блок ланцюжка блоків; length - довжина попереднього блоку в параграфах (байти 3-4 попереднього МСВ - блоку ланцюжка блоків). Фізична адреса, з якої розташовується в пам’яті наступний МСВ - блок, дорівнює seg_mcb:0. Обчислення мають зміст, якщо seg_mcb_old відповідає не останньому МСВ - блоку в ланцюжку. Для останнього МСВ - блоку буде отримано значення, що задає сегмент границі оперативної пам’яті комп'ютера.
Нові блоки пам’яті (тобто, МСВ - блоки в ланцюжку) створюються або модифікуються MS-DOS в декількох випадках:
1) при розподілі нового блоку пам’яті;
2) при зміні розміру існуючого блоку пам’яті;
3) при завантаженні та запуску програми на виконання;
4) при резидентному завершенні програми.
При розподілі нового блоку пам’яті (функція АН=48h переривання 21h) в регістрі ВХ специфікується тільки довжина створюваного блоку в параграфах. MS-DOS відшукує перший МСВ-блок в ланцюжку, позначений як вільний (частіше всього, це останній МСВ-блок ланцюжка блоків пам’яті), розмір якого перевищує запитане значення. Якщо два та більш підряд розташованих блоків позначені як вільні, при розподілі пам’яті вони розглядаються як єдиний блок. МСВ обраного блоку (першого з підряд розташованих вільних блоків) коректується:
1) в байти 1 і 2 записується номер параграфа поточного PSP (PID активної програми);
2) в байти 3-4 записується розмір створеного блоку. Якщо розмір запитаної пам’яті менше розміру вільного блоку (підряд розташованих вільних блоків), на границі "залишку" вільної пам’яті створюється МСВ; цей знову створений МСВ включається в ланцюжок МСВ - блоків. Новий МСВ не створюється, якщо розмір запитаної пам’яті точно дорівнює розміру вільного блоку (підряд розташованих вільних блоків).
Таким чином, при розподілі блоку пам’яті MS-DOS робить "злиття" декількох підряд розташованих вільних блоків. Ці вільні блоки виникають, якщо програма видає запити розподілу та вивільнення, й існують до тих пір, поки не буде зроблений запит на розподіл блоку. В цей момент MS-DOS проводить "ревізію" ланцюжка МСВ - блоків.
При завантаженні й запуску програми на виконання MS-DOS розподіляє під програму всю вільну пам'ять, через це будь - який запит ALLOCATE MEMORY, виданий з меж запущеної програми, зіткнеться з проблемою відсутності вільної пам’яті. При програмуванні на асемблері звільнення невикористаної програмою оперативної пам’яті - обов'язок програміста. При використанні Turbo С в склад завантажувального модуля "непомітно" для програміста вставляється спеціальна програма-завантажник. Вона, крім багатьох інших дій, звільняє незайняту пам’ять.
При виконанні функції SETBLOCK MEMORY (функція АН=49h переривання 21h) в регістрі ES задається сегмент початку модифікованого блоку, а в регістрі ВХ - новий розмір блоку в параграфах. Якщо розмір блоку зменшується, на границі "залишку" створюється новий вільний блок. Якщо здійснюється спроба збільшення розміру існуючого блоку, вона буде успішною в випадку, якщо наступний в ланцюжку блок - вільний.
MS-DOS має спеціальний засіб створення резидентних програм - так зване ТSR-завершення (Terminate but Stale Resident). Резидентне завершення програми виконує функція MS-DOS AH=31h переривання 21h (.ЕХЕ- і .СОМ-файли) та переривання 27h (тільки .СОМ-файли). При цьому в регістрі DX вказується розмір блоку пам’яті від початку PSP, оголошеного резидентним (кількість параграфів для DOS-функції 31h, кількість байтів для int 27h). Початок блоку MS-DOS визначає по PID активної програми. Дії MS-DOS у випадку резидентного завершення подібні тим, які виконуються при зміні розміру блоку. Блок, з якого починається PSP, усікається до запитаного розміру, але залишається зайнятим після завершення програми. Тому збережені там дані та код програми не будуть перевизначатись при завантаженні нових програм й (або) створенні нових блоків пам’яті. Адже ця пам’ять, з точки зору MS-DOS, залишається "зайнятою".
Звільнення блоку пам’яті (функція АН=4Ah переривання 21h) вимагає завдання в регістрі ES номеру параграфа початку блоку. Цей функціональний виклик може надаватися або самою програмою, або MS-DOS при завершенні програми. В байти 1 та 2 МСВ блоку, що звільняється, записуються нулі.
В реальному режимі процесора 80х86 відсутні апаратні засоби захисту пам’яті від несанкціонованого доступу. Тому неправильна робота програми при записі інформації в пам’ять може призвести до знищення або небажаної модифікації МСВ - блоку. В результаті ланцюжок блоків розривається, й у MS-DOS виникають нездоланні проблеми керування пам’яттю.
Інша "неприємність" - фрагментація оперативної пам’яті, яка виникає при резидентному завершенні програм. Фрагментація - це стан пам’яті, при якому чергуються вільні й зайняті блоки. Нефрагментована пам’ять - це пам’ять, в якій є єдиний вільний блок, що є останнім в ланцюжку МСВ; нефрагментованою також є пам’ять, в якій декілька вільних блоків, але всі вони в ланцюжку блоків розташовані підряд і в самому кінці.
Багато професійно написаних програм використовують безпосередньо МСВ - блоки для визначення наявності в пам’яті резидентних програм, їх "зняття", виявлення та знищення програм-вірусів і т.д.
До всіх скомпільованих та скомпонованих завантажувальних модулів включається спеціальний модуль COx.OBJ. Він включає дві основні секції: завантажник, або стартер (Startup Code); функції завершення (Exit Code). Структура і алгоритм роботи завантажника залежать від обраної моделі пам’яті, в склад Turbo С включені варіанти для кожної з них. Головна задача цієї програми - сформувати завантажувальний модуль точно в відповідності з представленнями програміста про його структуру, передати керування функції main(), а після її завершення виконати заключну секцію.
Програма-завантажник, одержавши керування, виконує наступні дії:
1) зберігає в кодовому сегменті сегмент адреси, з якого починається секція даних DGROUP;
2) ініціалізує в сегменті даних ряд зовнішніх змінних: номер версії MS-DOS, сегмент адреси початку PSP програми, сегмент адреси середовища програми, сегмент адреси границі оперативної пам’яті та ін.;
3) зберігає в сегменті даних деякі вектори переривань (вектори 0, 4, 5, 6 і ін.) та встановлює власний обробник переривання 0. Встановлений обробник буде друкувати повідомлення 'Divide error' та аварійно завершувати програму при виникненні виняткової ситуації - ділення на нуль;
4) формує середовище програми і аргументи, передані в точку входу main(); ці аргументи доступні програмі, якщо точка входу описується як main(int argc, char **argv, char **env);
5) звільняє не використану програмою оперативну пам’ять;
6) виконує налагодження програми на апаратуру, якщо в програм існує код для математики з плаваючою крапкою і компіляція виконується з опцією Emulation (див. першу книгу комплексу);
7) налагоджує регістри процесора у відповідності з моделлю пам’яті (див. далі);
8) викликає процедуру main();
Після завершення виконання main() керування знову одержує модуль COx.OBJ, який виконує дії по завершенню програми:
9) відновлює вектори переривань 0, 4,5, б та ін.;
10) закриває файли, відкриті в ході виконання програми;
11) виконує одну з функцій MS-DOS завершення програм.
Розміри завантажника достатньо великі; це особливо помітно для невеликих програм. Завдяки роботі програми-завантажника, оперативна пам’ять перед входом в main() знаходиться в стані, зображеному нижче.
Програма розташовується в двох блоках пам’яті: перший займає середовище програми, а другий - завантажувальний модуль. Перед передачею керування в main() оперативна пам’ять, не використана програмою, вже звільнена завантажником. В наслідок ця пам’ять використовується під far-«кучу».
|
0 |
+1 |
+3 +Fh |
||
MCB
SEG-1:0000 |
‘M’ |
S_PSP |
L_ENV ... |
||
SEG:0000 |
ASCIIZ-рядки
середовища, специфікація файлу завантажувального модуля програми |
||||
MCB
S_PSP-1:0000 |
‘M’ |
S_PSP |
L_PRJ ... |
||
PSP |
S_PSP:0000 |
CD
20... (INT 20h) |
|||
|
+Ch +Eh +Fh |
||||
S_PSP:0020 |
|
|
SEG |
||
|
|
||||
S_PSP+10:0000 |
Завантажувальний
модуль (структура залежить від моделі пам'яті) |
||||
MCB |
‘Z’ |
00
00 |
L_END |
||
|
Вільний |
||||
Стан пам’яті перед запуском функції main()
Turbo С підтримує 6 різних моделей пам’яті, кожна з яких має свої особливості адресації. Нижче в таблиці приведена зведена характеристика моделей пам’яті Turbo С.
Моделі пам’яті Turbo С
Модель пам’яті |
Тип покажчика (по замовченню) |
Число 64К-байтових сегментів |
||||
Дані |
Функція |
Дані |
Код |
Усього |
Один об'єкт |
|
TINY |
near |
near |
Розділяють |
1 |
1 |
|
SMALL |
near |
near |
1 |
1 |
2 |
1 |
MEDIUM |
near |
far |
1 |
>1 |
>1 |
1 |
COMPACT |
far |
near |
>1 |
1 |
>1 |
1 |
LARGE |
far |
far |
>1 |
>1 |
>1 |
1 |
HUGE |
far |
far |
>1 |
>1 |
>1 |
>1 |
Turbo С будує завантажувальний модуль з
сегментів з зарезервованими іменами. Весь код програми (один або більше
сегментів) компонується в секцію коду
(підряд розташовані сегменти). Всі дані розташовуються підряд в секцію даних. Посиланням на початок
секції є ім'я групи DGROUP. Секція
коду в завантажувальному модулі і, отже, в оперативній пам’яті передує секції
даних. Крім того, програма обов'язково має стек,
який включається в склад завантажувального модуля. В залежності від моделі пам’яті
завантажник створює доступний для динамічного розподілу простір пам’яті,
іменований "купою" (heap).
В сегмент _ТЕХТ (name_TEXT для моделей MEDIUM, LARGE та HUGE) записується код усієї програми (моделі пам'яті TINY, SMALL, COMPACT) або окремої функції name (моделі пам’яті MEDIUM, LARGE, HUGE). Усі сегменти _ТЕХТ в пам’яті будуть розташовуватися один за одним в тому порядку, в якому вони зустрічаються в тексті Сі-програми або в окремих файлах, якщо виконується багатофайлова компіляція. При виконанні функції сегмент початку _ТЕХТ (name_TEXT) зберігається в регістрі CS, значення якого доступно програмі через псевдозмінну _CS. Зміщення усередині _ТЕХТ (name_TEXT) задає регістр IP, значення якого доступно програмі через псевдозмінну _ІР. Якщо функція має тип near, її виклик зв'язаний зі зміною тільки IP. Якщо функція має тип far, при її виклику буде змінюватися як CS, так і IP. Навіть якщо загальний об'єм програмного коду не перевищує 64Кбайти (тобто може весь адресуватись без зміни CS), але (або) по замовченню, або відкрито функція має тип far, при її виклику будуть змінюватися і СS, і ІР.
В сегмент _DATA (name_DATA для моделі HUGE) записано ініціалізовані зовнішні та статичні дані усієї програми (або конкретної функції name для моделі HUGE). В пам’яті усі сегменти name_DATA розташовуються один за одним в тому порядку, в якому вони зустрілися компонувальнику при його роботі. В сегменті _BSS містяться не ініціалізовані зовнішні й статичні дані усіх модулів програми. Звідси випливає, що незалежно від моделі пам’яті загальний об’єм таких даних не може перевищувати б4Кбайти. В моделі пам’яті HUGE сегмент _BSS відсутній. Сегменти _DATA й _BSS об'єднуються в групу по імені DGROUP (для HUGE не виконується). Це об'єднання дозволяє при обчисленні зміщення в пам’яті до об’єктів, віднесених до різних сегментів, відраховувати його від однієї границі, заданої іменем групи. Розмір однієї групи не перевищує 64Кбайти. По замовченню об’єкти в DGROUP адресуються сегментним регістром DS й зміщенням. Якщо в програмі використовується зміщення відносно DGROUP, можна не перевизначати значення сегментного регістра DS. Тому безпосередньо перед передачею керування функції main() завантажник системи програмування COх.OBJ записує в DS константу DGROUP, після чого значення в DS, як правило, не змінюється в ході всього часу виконання програми. Для функцій, скомпільованих з моделлю пам’яті HUGE, перед початком їх виконання в DS записується значення name_DATA. Після завершення функції значення в DS відновлюється. Тому кажуть, що такі функції мають власні сегменти статичних даних і їх загальний об’єм може перевищувати 64Кбайти.
Сегменти програми та їх типи, встановлені
по замовченню для моделей пам’яті Turbo С
Модель пам’яті |
Ім’я |
Вирівнювання |
Тип |
Клас сегменту |
Група |
TINY |
_TEXT _DATA _BSS |
BYTE PARA WORD |
PUBLIC PUBLIC PUBLIC |
'CODE' 'DATA' 'BSS' |
- DGROUP DGROUP |
SMALL |
_TEXT _DATA _BSS _STACK |
BYTE PARA WORD PARA |
PUBLIC PUBLIC PUBLIC STACK |
'CODE' 'DATA' 'BSS' 'STACK' |
- DGROUP DGROUP - |
MEDIUM |
name _TEXT _DATA _BSS _STACK |
BYTE PARA WORD PARA |
PUBLIC PUBLIC PUBLIC STACK |
'CODE' 'DATA' 'BSS' 'STACK' |
- DGROUP DGROUP - |
COMPACT |
_TEXT _DATA _ BSS _STACK |
BYTE PARA WORD PARA |
PUBLIC PUBLIC PUBLIC STACK |
'CODE' 'DATA' 'BSS' 'STACK' |
- DGROUP DGROUP - |
LARGE |
name _TEXT _DATA _BSS _STACK |
BYTE PARA WORD PARA |
PUBLIC PUBLIC PUBLIC STACK |
'CODE' 'DATA' 'BSS' 'STACK' |
- DGROUP DGROUP - |
HUGE |
Name_TEXT Name_DATA _STACK |
BYTE PARA PARA |
PUBLIC PUBLIC STACK |
'CODE' 'DATA' 'STACK' |
— — — |
Зміщення для доступу до даних міститься або в самій машинній команді (пряма адресація), або в одному з регістрів (непряма адресація). Джерело, з якого береться значення сегмента (DS, ES, SS, CS), при обчисленні фізичного адреса задається або по замовченню, або відверто префіксом перевизначення сегмента. При використанні для доступу до пам’яті far-покажчиків Turbo С встановлює сегмент покажчика в ES, а зміщення покажчика - в один з регістрів загального призначення і використовує префікс перевизначення сегмента ES.
Сегмент стека використовується при виконанні програми під дані, що мають клас збереження auto, при виконанні машинних інструкцій виклику процедур, перериваннях, в інших випадках. Сегмент початку стека в пам’яті поміщається в сегментний регістр SS та доступний програмі через псевдозмінну _SS. Регістр SP задає поточну вершину стека. Відразу після завантаження значення SP встановлюється рівним "дну" стека. Це значення, близьке до FFFFh.
Модель пам’яті вказується при компіляції програми (опція IDE Main Menu-Options-Compile-Memory Model; опції -mt, -ms, -mm, -me, -ml, -mh компілятора командного рядка; по замовченню приймається -ms). Крім задання моделі пам’яті, можливе задання імен сегментів, що відрізняються від наведених вище в таблиці.
Значення сегмента DGROUP записується завантажником в кодовий сегмент в два байта, розміщений безпосередньо перед початком кодової секції Сі-програми.
Код та дані програми, скомпільованої з моделлю пам’яті TINY, розташовуються в єдиному сегменті і, отже, не можуть перевищувати в сумі 64Кб. Після завантаження такої програми сегментні регістри CS, DS, ES та SS мають однакове значення. Для стека та "купи" використовуються ті ж 64Кб пам’яті, але заповнення стека йде від верхньої границі стека до молодших адрес, а заповнення "купи" - навпаки. Тому завжди існує імовірність "перекриття" пам’яті, що використовується під стек, з пам’яттю, яка надається динамічно з "купи". Основне призначення моделі пам’яті TINY - генерування .ЕХЕ-файлів, придатних для перетворення в .СОМ-файли.
Рис. 1.1. Розташування завантажувального модуля
в пам’яті (модель пам’яті TINY)
Програми, скомпільовані з моделлю пам’яті SMALL, мають два окремі сегменти максимум по 64Кб кожний. В кодовий сегмент компонуються кодові сегменти усіх функцій Сі-програми. При передачі керування функції main() CS встановлюється так, що зберігає сегмент _ТЕХТ. Регістри DS, ES, SS містять сегмент початку групи DGROUP. По замовченню усі покажчики, як на дані, так й на функції – near-покажчики. Особливістю даної моделі пам’яті є наявність двох "куп":
near -"купа" займає 64Кб мінус розмір сегментів STACK, _DATA _BSS та розташовується відразу ж після сегмента _BSS;
far- "купа" займає весь простір оперативної пам’яті, що залишився після завантаження програми.
Рис. 1.1. Розташування завантажувального модуля
в пам’яті (модель пам’яті SMALL)
Область застосування моделі SMALL - Сі-програми, розмір коду яких не перевищує 64Кб та усі дані (зовнішні, статичні, автоматичні, динамічно розподілена пам’ять в near -"купі") в сумі не перевищують 64Кб.
Turbo С при використанні моделі пам’яті MEDIUM будує завантажувальний модуль програми з єдиним для усіх Сі-функцій сегментом даних та будь-яким числом сегментів для програмного коду. Кожна функція програми-завантажника, Сі-програми, бібліотечна функція розглядається як far-процедура і для її виклику використовується машинна інструкція CALL FAR.
Об’єм програмного коду, таким чином, обмежується тільки об’ємом оперативної пам’яті. Загальний об’єм даних програми (зовнішні, статичні, автоматичні, пам’ять, динамічно розподілена функціями malloc(), calloc()) в сумі не перевищує 64Кб. Модель пам’яті MEDIUM представляє собою хороший баланс між продуктивністю та простором пам’яті, займаним програмою. Маючи на увазі, що більшість програм частіше звертаються до даних, а не викликають процедури, висока швидкість звертання до даних через near - покажчики дає більший виграш порівняно з уповільненням виклику функцій через CALL FAR.
Рис. 1.1 Розташування завантажувального модуля
в пам’яті (модель пам’яті MEDIUM)
Як і для моделі пам’яті SMALL, програма може використовувати дві "купи": near й far.
Модель пам’яті COMPACT використовується при
побудові програм, що мають багато даних й відносно невелику кількість операторів.
Загальний об’єм даних для програми COMPACT обмежується тільки розміром
оперативної пам’яті. По замовченню функції Сі-програми розглядаються як near –
процедури,
Рис. 1.1Розташування завантажувального модуля
в пам’яті (модель пам’яті COMPACT)
і для їх виклику компілятор генерує машинну інструкцію CALL NEAR. Усі покажчики на дані по замовченню - far-покажчики, тобто, мають сегмент та зміщення відносно значення сегмента. Зазвичай, можна перевизначити значення по замовченню, використовуючи ключові слова near для покажчиків на дані та far для опису функцій. Для даної моделі пам’яті існує тільки одна "купа".
Модель пам’яті LARGE використовується при побудові дуже великих Сі-програм, що мають багато даних. По замовченню доступ, як до функцій, так і до даних, здійснюється через far-покажчики. Для даної моделі пам’яті існує тільки одна "купа".
Рис. 1.1 Розташування завантажувального модуля
в пам’яті (модель пам’яті LARGE)
Модель пам’яті HUGE дає можливість використовувати в програмі статичні дані загальним об’ємом більше 64Кб. Завдяки механізму нормалізації покажчиків на дані можна, використовуючи покажчик або індексацію, здійснювати доступ до блоків пам’яті як до масивів, розмір яких більше 64Кбайт, (проте це не означає, що окрема функція може описати більше 64Кбайт даних). Покажчик зберігається в пам’яті та вступає в операції адресної арифметики в такому вигляді, що максимально можливе значення записується в сегментну частину покажчика, а мінімально можливе - в зміщення. Усі операції адресної арифметики виконуються за спеціальними внутрішніми процедурами, автоматично нормалізуючими покажчик. Наприклад, фізична адреса пам’яті 00418h має в моделі HUGE єдине представлення - 0041:0008h. В моделі LARGE така однозначність відсутня, а при виконанні арифметичних операцій з покажчиками їх нормалізація не відбувається. В цьому криється причина багатьох важкопомітних помилок програмування. Одна з них полягає в некоректному виконанні операцій порівняння покажчиків. Наприклад, нехай описано й ініціалізовано три покажчики:
Рис. 1.1. Розташування завантажувального модуля
в пам’яті (модель пам’яті HUGE)
char far * ptr1 = 0xb8000000L;
char far * pfr2 = 0xb4004000L;
char far * ptr3 = 0xb0008000L;
Вони відповідають одній і тій же фізичній адресі 0хb8000. Проте операція == порівняння покажчиків завжди дасть значення НЕПРАВДА. При виконанні порівняння far-покажчиків операціями >=, >, <=, < використовуються тільки зміщення, тобто молодші слова покажчиків у форматі unsigned. Тому, наприклад, ptr1<ptr2 дає значення ПРАВДА, a ptr2>=ptr3 - НЕПРАВДА. Порівняння ж far-покажчиків операціями != та == виконується як порівняння чисел long unsigned, а не значень фізичних адрес.
Інша розповсюджена помилка використання покажчиків, в тому числі й far-покажчиків, відома в програмуванні як "перескакування сегмента" (segment round wrapping). Якщо до покажчика додається якесь число, змінюється тільки значення зміщення. Якщо при цьому отримане значення зміщення повинно перевищити FFFFh, "перенесення" в значенні сегмента не відбувається, а зміщення "перестрибує" на границю 0000. Представимо ситуацію збільшення на 17h значення покажчика типу char far*. Якщо значення покажчика було 0FFF:0006h, буде отримано коректне значення нової адреси - 0FFF:001Eh. Проте якщо перед додаванням значення покажчика було представлене у вигляді 0000:FFF6h, що відповідає тій самій фізичній адресі, додавання покажчика з 17h дає некоректний результат - 0000:001Eh. Нормалізовані покажчики позбавляють програму від можливості таких важковловимих помилок, оскільки покажчик завжди буде нормалізуватися, що гарантує коректність операції адресної арифметики. Тому, послідовно нарощуючи far-покажчик, програма може здійснювати доступ до даних, розмір яких перевищує 64Кбайт. Проте використання процедур нормалізації в багато разів уповільнює програму.
Для того, щоб відслідковувати розподіл пам'яті "купи", функції динамічного керування пам'яттю Turbo С ведуть внутрішні зв'язні списки. Елементи таких списків передують виділеній ділянці пам'яті і займають для near-“купи” 4 байти, для far-“купи” – 8 байт. Початок списку задають зовнішні змінні сегмента даних, ініційованих завантажувачем COx.OBJ. Розподілена пам'ять захищена від повторного виділення при наступних запитах на виділення пам'яті. Тип покажчика, що повертається, відповідає типу покажчика по-замовченню обраної при компіляції моделі пам'яті. Елементи списку не слід плутати з МСВ-блоками, що веде MS-DOS. Крім модифікації списків, функції виконують і деяку додаткову роботу для запобігання повторного виділення пам'яті, займаною "купою" у моделях пам'яті COMPACT, LARGE і HUGE.
Програма, її дані і стек поєднуються в один блок пам'яті в термінах MS-DOS, тобто один МСВ-блок розташовується перед PSP завантаженої програми. Його поле довжини вказує на наступний МСВ, з якого починається пам'ять, звільнена завантажником. Для моделей пам'яті TINY, SMALL і MEDIUM у МСВ-блок програми включена і near-"купа", тому пам'ять "купи" не може бути повторно виділена засобами MS-DOS. Однак для інших моделей вважається, що "купа" продовжується аж до границі наявної оперативної пам'яті. Якщо "купа" буде включена в МСВ-блок програми, то фактично, буде відсутня вільна оперативна пам'ять, необхідна, наприклад, для запуску програм-нащадків. Але, з іншого боку, якщо не виконати додаткових заходів по захисту уже виділеної з "купи" пам'яті і, можливо, перенесених туди програмою даних, ці дані будуть зруйновані при запуску нащадка. Тому для far-"купи" реалізовано наступний підхід. Завантажник COx.OBJ створює для far-"купи" свій МСВ-блок. Як тільки запитується пам'ять, виконується його "подовження" на розмір виділеної ділянки пам'яті плюс розмір внутрішнього керуючого блоку (8 байт). MS-DOS ніби "прикриває" своїм МСВ-блоком пам'ять, виділену в far-"купі".
Якщо виділена ділянка пам'яті більше не потрібна, вона може бути звільнена відповідною функцією. При цьому внутрішній керуючий блок позначається як вільний, але зберігається в списку блоків. Тому при високій активності по динамічному розподілі пам'яті "купа" фрагментується. Для зм'якшення негативних наслідків фрагментації служать функції повторного розподілу пам'яті. При звертанні до таких функцій робиться спроба або розширити, або зменшити розмір раніше виділеного блоку пам'яті. За рахунок знищення внутрішніх керуючих блоків і корекції списку вдається "злити" трохи вільних блоків в більш великий.
Функції malloc(), calloc(), realloc() і free() для моделей пам'яті TINY, SMALL, MEDIUM використовують near-"купу", а для інших моделей - far-"купу". Функції з префіксом far у назві працюють тільки з far-"купою". Їхня поведінка для моделей пам'яті COMPACT і старше не відрізняється від поведінки функцій malloc(), calloc() і free(): і ті, і інші функції "черпають" пам'ять з однієї і тієї ж "купи". Функції для роботи з far-“купою” не застосовуються в моделі пам'яті TINY. При використанні моделей SMALL і MEDIUM доступ до виділеної в far-"купі" пам'яті вимагає явного опису far-покажчика.
Далі приводяться специфікації функцій бібліотеки Turbo С для роботи з пам'яттю.
#include <alloc.h>
int brk(void *endds);
Встановлює нове "брейк-значення"
(break value) програми, що задається коміркою пам'яті, на яку вказує endds.
"Брейк-значення" визначає число байтів, яке використане
обчислювальним процесом у сегменті даних за замовченням на поточний момент
часу. Іншими словами, це адреса останнього байта в сегменті даних. Зміна цієї
адреси, фактично змінює розмір програми в пам'яті. У випадку успіху функція
повертає 0. У противному випадку повертається (-1) і в зовнішню змінну еrrnо
записується код помилки ENOMEM. Функція введена в бібліотеку Turbo C для сумісності
з реалізаціями Сі, розрахованими на ОС UNIX. Коректне
використання функції вимагає визначення розташування попереднього кінця сегмента
даних. Шлях, що рекомендується - використання функції sbrk().
#include <stdlib.h>
#include <alloc.h>
void *calloc(size_t nelem, size_t elsize);
Виділяє з "купи" місце для nelem елементів розміром elsize
кожен, тобто усього nelem*elsize байт, і обнуляє виділену пам'ять. Для моделей
пам'яті TINY, SMALL, MEDIUM виділяє пам'ять з near-"купи". Для інших
моделей пам'яті використовує far-"купу". Тип size_t визначений у даний
момент як unsigned і, отже, загальний обсяг виділеної пам'яті не може
перевищувати 64Кбайт. Повертає покажчик типу void* на початок виділеного блоку
пам'яті. При відсутності вільної пам'яті запитаного обсягу або рівності size
нулю повертається NULL-покажчик.
#include <alloc.h>
unsigned coreleft(void) - моделі пам'яті TINY, SMALL, MEDIUM;
unsigned long coreleft(void) - моделі пам'яті старші за COMPACT.
Повертає число вільних для динамічного розподілу байтів пам'яті в
"купі". Для моделей пам'яті TINY, SMALL, MEDIUM повертається число
вільних для розподілу байтів у nеаг-"купі", розмір якої менше 64Кбайт. Для моделей пам'яті старше COMPACT повертає число вільних
для розподілу байтів у far-"купі".
#include <alloc.h>
void far *farcalloc( unsigned long nelem, unsigned long elsize)
Виділяє з far-"купи" місце для nelem елементів розміром
elsize кожен, тобто усього nelem*elsize байт, і обнуляє виділену пам'ять.
Загальний обсяг виділеної пам'яті може перевищувати 64Кбайт. Повертає покажчики типу void far* на початок виділеного
блоку пам'яті. При відсутності вільної пам'яті запитаного обсягу або рівності
size нулю повертається NULL. Для моделей пам'яті старше COMPACT функція
аналогічна calloc().
#include <alloc.h>
unsigned long farcoreleft(void)
Повертає число невикористаних байтів пам'яті в far-"купі".
Для моделей пам'яті старше COMPACT функція аналогічна coreleft().
#include <alloc.h>
void farfree(void far *block)
Звільняє в far-"купі" блок пам'яті, на початок якого вказує
block. Функція не має значення, що повертається. Варто бути дуже уважним, задаючи
значення block. Для моделей пам'яті TINY, SMALL і MEDIUM це значення повинне
бути повернуте раніше функціями farcalloc(), farmalloc(), farrealloc(), а для моделей
пам'яті COMPACT, LARGE і HUGE - функціями calloc(), malloc(), realloc(). Для
моделей пам'яті COMPACT, LARGE і HUGE функція farfree() аналогічна функції
free(). Для інших моделей пам'яті це різні функції, що використовують різні
"купи".
#include <alloc.h>
void far* farmalloc(unsigned long nbytes)
Виділяє з far-"купи" блок у nbytes. Загальний обсяг виділеної
пам'яті може перевищувати 64Кбайт. Повертає покажчик типу void far* на початок
виділеного блоку пам'яті. При відсутності вільної пам'яті запитаного обсягу і
рівності size нулю повертається NULL. Для моделей пам'яті COMPACT, LARGE і HUGE
функція аналогічна malloc().
#include <alloc.h>
void far *farrealloc(void far *block, unsigned long size);
Робить спробу встановити новий розмір size блоку в
far-"купі". На початок блоку вказує block. Якщо виконується запит на
розширення блоку, збільшення існуючого блоку відбувається за рахунок вільної
пам'яті, пов'язаною з ним чи ліворуч чи праворуч. Якщо неможливо задовольнити
запит без зміни адреси блоку, проглядається список блоків для пошуку одного чи
групи підряд розташованих вільних блоків розміром не менш size байт. У випадку
успіху функція повертає покажчик на новий виділений блок. Значення, що
повертається, може не збігатися з заданим параметром block. У цьому випадку
виконується копіювання "старого" блоку в нове місце. Якщо
"старий" блок пам'яті не може бути розширений до запитаного розміру,
функція повертає NULL. Для моделей пам'яті COMPACT, LARGE і HUGE функція
аналогічна realloc().
#include <stdlib.h>
#include <alloc.h>
void free(void *btock);
Звільняє блок пам'яті в "купі", на початок якого вказує
block. Для моделей TINY, SMALL, MEDIUM працює з near-"купою", для
інших моделей пам'яті - з far-"купою". Функція не має значення, що
повертається. Варто бути дуже уважним, задаючи значення block. Для моделей
пам'яті TINY, SMALL і MEDIUM це значення повинне бути повернуте раніше
функціями calloc(), malloc(), realloc(), а для моделей пам'яті COMPACT, LARGE і
HUGE - функціями farcalloc(), farmalloc(), farrealloc(). Для моделей пам'яті
COMPACT, LARGE і HUGE функція free() аналогічна функції farfree(). Для інших
моделей пам'яті - це різні функції, що використовують різні "купи".
#include <stdlib.h>
#include <alloc.h>
void* malloc(size_t size);
Виділяє з "купи" size байт. Для моделей пам'яті TINY, SMALL,
MEDIUM виділяє пам'ять з near-"купи". Для інших моделей пам'яті
використовує fаr-"купу" і аналогічні функції farmalloc(). Тип size_t
визначений у даний момент як unsigned і, отже, загальний обсяг виділеної
пам'яті не може перевищувати 64Kбайт. Є мобільною функцією, працюючою для всіх
моделей пам'яті. Повертає покажчик типу void* на початок виділеного блоку
пам'яті. При відсутності вільної пам'яті запитаного обсягу або рівності size
нулю повертається NULL-покажчик.
#include <stdlib.h>
#include <alloc.h>
void *realloc(void *block, size_t size);
Робить спробу встановити новий розмір size блоку, на початок якого вказує block. Якщо виконується запит на
розширення блоку, збільшення існуючого блоку відбувається за рахунок вільної
пам'яті, пов'язаної з ним чи ліворуч чи праворуч. Якщо неможливо задовольнити
запит без зміни адреси блоку, проглядається список блоків для пошуку одного чи
групи підряд розташованих вільних блоків розміром не менш size байт. У випадку
успіху функція повертає покажчик на новий виділений блок. Повернене значення
може не збігатися з заданим параметром block. У цьому випадку виконується
копіювання "старого" блоку в нове місце. Якщо "старий" блок
пам'яті не може бути розширений, до запитаного розміру, функція повертає NULL.
Для моделей пам'яті COMPACT, LARGE і HUGE функція аналогічна farrealloc().
#include <alloc.h>
void *sbrk(int incr);
Додає incr байт до "брейк-значення" програми.
"Брейк-значення" вказує число байтів, використане обчислювальним
процесом у сегменті даних за замовчуванням на поточний момент часу. Іншими
словами, це адреса останнього використаного байта в сегменті даних. Для Turbo С
це зсув у "купі", з якого може починатися новий блок, що
розподіляється динамічно. Завдання від‘ємного incr приводить до зменшення
розміру програми. У випадку успіху функція повертає попереднє
"брейк-значення". У протилежному випадку повертається (-1) і в
зовнішню змінну еrrno записується код помилки ENOMEM. Функція введена в
бібліотеку Turbo C для сумісності з реалізаціями Сі, розрахованими на ОС UNIX,
і являє собою альтернативний спосіб резервування пам'яті. Мобільний і тому
рекомендований спосіб-використання функцій mа11ос(), calloc(), геа11ос() чи
їхніх far-аналогів.
Наведемо приклади використання розглянутих функцій. Програма демонструє використання sbrk для виділення 128 байт пам'яті під введений з клавіатури рядок:
#include <stdio.h>
#include <alloc.h>
unsigned char*buffer;
int main(void) {
/* Спроба зарезервувати, місце для
рядка встановленням нового"брейк-значення" і обробка можливої помилки.
*/
if(buffer= (char*)sbrk(128)) == (char*)
- 1) {
perror( "\а Помилка встановлення
\”брейк-значення\"");
return 1;
}
puts("Бyфep створений. Введіть
рядок символів");
gets(buffer);
printf(“Введений рядок - %s",
buffer);
sbrk(-128); /* звільнення
зарезервованої пам'яті */
}
Класичним прикладом використання динамічного керування пам'яттю є прийом зі стандартного введення довільного числа рядків, їх максимально компактне розміщення в пам'яті і видача рядків, які було прийнято та запам’ятовано на екран, наприклад у порядку їхнього надходження:
#include
<stdio.h>
#include <alloc.h>
#include <stdlib.h>
#include <string.h>
#define STOP_STRING ""
#define BUFFER_SIZE 129
#define STRING_NUMBER 1000
void main( ){
char buffer[BUFFER_SIZE], *pointers[
STRING_NUMBER], *ptr;
unsigned n, index;
for( index=0; index < STRING_NUMBER
&& strcmp(gets(buffer), STOP_STRING); pointers[index++]=ptr) {
/* Спроба розподілу пам'яті й обробка можливої помилки. Запитується число
байтів, більше на 1 довжини введеного рядка, тому що символ '\0’ також зберігається.
*/
if((
Ptr=(char*)malloc(strlen(buffer)+1))==NULL) {
printf(“Відсутня вільна
пам'ять\п");
break;
}
else
strcpy(ptr, buffer);
}
n=index;
printf("Прийом рядків завершено\nПринято
%d рядків:\n", index);
/*Цикл виведення рядків, розмішених в
"купі" */
for(index=0; n>0; n--)
puts(pointers[index++]);
}
Число рядків, що може прийняти програма, обмежено розміром масиву покажчиків pointers. Щоб зняти таке обмеження, можна виконати динамічний розподіл пам'яті і під масив покажчиків, наприклад після введення STRING_NUMBER рядків, запитувати блок для нових STRING_NUMBER покажчиків. При цьому, однак, ускладнюється доступ до рядків через те, що покажчики на них розташовуються в декількох блоках пам'яті по STRING_NUMBER елементів у кожнім блоці. Блоки покажчиків зв'язуються в список. Далі наводиться текст програми, що реалізує розглянутий підхід. Програма приймає з клавіатури довільне число рядків і розміщує їх в оперативній пам'яті. Введення припиняється, якщо вводиться порожній рядок. Пам'ять для збереження покажчиків на рядки в "купі" виділяється динамічно. Масив покажчиків розташовується в зв'язному списку блоків з STRING_NUMBER покажчиків в кожному блоці. Останній елемент блоку використовується як покажчик на наступний блок покажчиків.
#include <stdio.h>
#include <alloc.h>
#include <stdlib.h>
# include <string.h>
#define STOP_STRING ""
#define BUFFER_SIZE 129
#define STRING_NUMBER 128
void main( ) {
char buffer[BUFFER_SIZE], **pointers,
**pointers_start, *ptr;
unsigned n=0, index=0;
/* Розподіл пам‘яті для першого блоку покажчиків
і обробка можливої помилки, розподілу пам'яті. */
pointers_start=(char*)calloc(
STRING_NUMBER+1, sizeof(char*) );
if(pointers_start==NULL) exit( 4 );
pointers=pointers_start; /* зберігає
покажчик на початок списку блоків покажчиків */
while( strcmp( gets( buffer ),
STOP_STRІNG ) ) { /* цикл прийому рядків */
/* Спроба розподілу пам'яті і обробка
можливої помилки. Запитується число байтів, більше на 1 довжини введеного
рядка, тому що символ '\0' також зберігається. */
if((ptr= malloc(strlen( buffer) + 1))
==NULL) {
printf("Відсутня вільна
пам'ять\п”);
break;
}
strcpy(ptr, buffer); /* перенос у
"купу" введеного рядка */
if( index==STRING_NUMBER) /* якщо всі
покажчики блоку, крім одного, використані */
{ /*Розподіл пам”яті для наступного блоку покажчиків і запис покажчика на нього
в останній елемент попереднього блоку покажчиків */
pointers
[STRING_NUMBER]=calloc(STRlNC_NUMBER + 1, sizeof( char* ) );
if(pointers[STRІNG_NUMBER]==NULL) {
printf("Відсутня вільна
пам‘ять\п");
break;
}
index=0;
pointers = (char**)
pointers[STRING_NUMBER];
}
pointers[index++]=ptr;
n++;
}
printf("Прийом рядків завершений. Принято %d рядків \п”, п);
/*Вивід на екран рядків по покажчиках на
них в “купі” */
for(index=0, pointers= pointers_start; n
> 0; n--) {
puts(poіnters[indeх++]);
if( іndex == STRІNG_NUMBER) /*
використано блок покажчиків ?*/
{ index=0;
pointers = (char**) pointers[STRING_NUMBER];
}
}
}
Можлива альтернатива зв'язному списку блоків покажчиків - використання зв'язного списку рядків. Разом з рядком, прийнятим зі стандартного введення, у "купі" міститься покажчик на наступний рядок. Цей покажчик записується, наприклад, безпосередньо перед рядком. Спеціальна змінна зберігає покажчик на початок списку. Перевага методу - простота реалізації алгоритмів сканування списку. Недоліками методу є збільшення числа операцій по динамічному розподілу пам'яті; це сповільнює програму і приводить до нераціонального використання пам'яті "купи", тому що кожна операція зв'язана зі створенням внутрішнього керуючого блоку в ній.
При динамічному розподілі пам'яті випливає, що по можливості, потрібно запитувати пам'ять якомога більшими блоками, що значно скорочує втрати пам'яті на внутрішніх керуючих блоках. Причому джерелом втрат є не тільки місце, займане самим внутрішнім керуючим блоком, але і втрати через вирівнювання кожного нового керуючого блоку на границі або 4 байти (near-"купа"), або 8 байт (far-"купа").
Далі розглянемо невелику демонстраційну програму, яка тестує загальний обсяг "купи" і оцінює корисний її обсяг. Програма виконує нескінченний цикл динамічного розподілу блоків введеного з клавіатури розміру. У залежності від розміру блоку і використаної моделі пам'яті корисний обсяг "купи" може розрізнятися більш ніж у 10 разів.
#include <stdio.h>
#include <alloc.h>
#include <stdlib.h>
void main(void) {
unsigned size;
unsigned long block_number = 0, total;
рrіntf(“Введіть розмір блоку (1…65535):
");
scanf("%u", &size);
total = coreleft(); /* загальний обсяг
"купи" */
/* Цикл виділення блоків пам'яті */
while( ( malloc( size ) ) != NULL )
block_number++;
printf("Функцією malloc() виділено
%lu блоків по %u байт\п"
"Корисний обсяг
\"купи\" - %lu байт\п"
"Загальний обсяг
\"купи\" -%lu\n",
block_number,
size,block_number * size, total);
}
Наступна програма демонструє використання обох "куп" для моделей пам'яті молодше COMPACT. Ця програма приймає довільне число рядків із клавіатури; для припинення введення вводиться порожній рядок. Парні рядки записуються в near-"купу", непарні - у far-"купу". Після цього збережені в "купах" рядки виводяться на екран. Для визначення моделі пам'яті в програмі використовується умовна компіляція.
#include <stdio.h>
#include <alloc.h>
#include <stdlib.h>
#include <string.h>
# include <dos.h>
#define STOP_STRING “”
#define BUFFER_SIZE 129
#define STRING_NUMBER 500
void main(void) {
char buffer[BUFFER_SIZE], *
near_pointers[STRING_NUMBER],
far *far_pointers[STRING_NUMBER];
unsigned src_seg, src_offset;
unsigned index, near_index= 0,
far_index= 0, flag = 1;
/* Умовна компіляція для визначення
заданої моделі пам'яті.*/
#ifdef _ _TINY_ _
рuts(“Модель пам'яті TINY має тільки
пеаr-\"купу\". Завершення");
exit(3);
#endif
#if defined _ _COMPACT_ _ || defined_ _LARGE_ _ || defined _ _HUGE_ _
puts(“Моделі пом'яті COMPACT, LARGE,
HUGE мають тільки "
"far-купу\”."Завершення" );
exit(4);
#endif
printf( “Об‘єм доступної
памяті:\п"
" пеаr-\"купа\”-%u\п"
" fаr-\"купа\" - %lu\n”,
coreleft(), farcoreleft());
src_seg =_DS;
src_offset = FP_OFF(buffer);
puts("Введіть рядок. Завершення -
введення порожнього рядка");
for(index=0;
index < (STRING_NUMBER * 2)
&& strcmp(gets(buffer),
STOP_STRING);
flag^=1, index++) { /* Розподіл пам'яті, обробка можливої, помилки. */
if(flag) /* використовується
пеаr-"купа" */
if((near_pointers[near_index]=
(char *)malloc(strlen(buffer)+1))==NULL)
{
printf( Відсутня вільна
пам'ять у пеаr-\"купі\”\п");
break;
}
else
strcpy(near_pomters[near_index++], buffer);
else /* використовується
fаr-"купа" */
іf((far_pointers[far_index]=
(char
far*)farmalloc(strlen(buffer)+1))==NULL) {
printf("Відсутня вільна
пам'ять в far-\"купі\”\п");
break;
}
else {
movedata(src_seg,
src_offset,
FP_SEG(far_pointers[far_index]),
FP_OFF(far_pointers[far_index]),
strlen(buffer) + 1); /* перенесення рядка */
far_index++;
}
}
printf(“Прийом рядків
завершено.\пПринято %d рядків:\n", index);
/* Цикл виведення рядків, розміщених у
\"купі\". */
for(flag= 1, near_index= 0, far_index=
0; index > 0; index--, flag^=1)
if(flag)
puts(near_pointers[near_index++]);
else {
printf(“%Fs\n",
far_pointers[far index]);
far_index++;
}
}
Відзначимо, що в far-"купу" рядок переноситься не функцією strcpy, що працює для моделей пам'яті молодше COMPACT з near - покажчиками, а функцією movedata(), що використовує far-покажчики.
Ще одна важковловима помилка при динамічному розподілі пам'яті в Turbo С виникає, якщо в програму не включається файл <alloc.h>. У результаті при компіляції зі старшими моделями пам'яті відбувається додаткове перетворення значення покажчика, що повертається, наприклад функцією malloc(), типу void far* до типу void*. Як результат, far-покажчик перетвориться в ціле число (тип, що повертається функцією за замовченням), що часто перетворюється далі в far-покажчик із сегментом 0. Наприклад,
/* Файл <alloc.h> не підключений,
використовується LARGE-модель */
struct WINDOW *wptr;
if(wptr= (struct WINDOW*) malloc(sizeof( struct WINDOW))) !=NUUL)
/* wptr
буде дорівнювати, наприклад, 0000:0008 */
…
Більшість програм виконують введення інформації з клавіатури. Введення інформації в комп'ютер може бути виконане на трьох рівнях: зверненням до функцій MS-DOS; зверненням до функцій BIOS; фізичним доступом до апаратних засобів.
Введення інформації на рівні MS-DOS дозволяє "минути" клавіатурне введення через драйвери, що встановлюються, і забезпечує контроль натиску комбінації клавіш Ctrl-C (Ctrl-Break), стандартну для MS-DOS обробку помилок. На цьому рівні працюють функції Turbo C для:
1. введення інформації з файлу stdin або через потік, або через префікс 0;
2. консольного введення ( getch(), getche() та ін.), прототипи яких наведені в файлі <conio.h>.
Доступ до клавіатури на рівні BIOS дозволяє програмі відслідковувати натиск всіх, а не тільки символьних клавіш, виконувати управління апаратурою клавіатури та ін. Інтерфейсом Turbo С з BIOS є функція bioskey().
Клавіатура персонального комп'ютера містить спеціальний вбудований мікропроцесор. Він при кожному натиску і відпусканні клавіші визначає її порядковий номер і розміщує його в порт 60h спеціальної електронної схеми ‑ периферійного інтерфейсу, що програмується. Далі цей код будемо називати скен-кодом. Скен-код в перших 7 бітах містить порядковий номер нажатої клавіші, а восьмий біт рівний 0, якщо клавіша була натиснута (прямий скен-код), рівний 1, якщо клавіша була відпущена (зворотний скен-код). Коли скэн-код записаний в порт 60h, схема PPI видає сигнал "підтвердження", повідомляючи мікропроцесор клавіатури про прийняття коду. Якщо клавіша залишається натиснутою довше деякого часу затримки (delay value), мікропроцесор клавіатури починає генерувати з заданою частотою (typematic rate) прямий скен-код нажатої клавіші. Значення затримки і частоти повторення можуть встановлюватися в потрібні значення або через порт клавіатури, або через функцію АН=03h переривання 16h BIOS. Коли скен-код прийнятий схемою PPI, апаратура комп'ютера генерує переривання 9h.
Стандартний обробник переривання 9h - це програма, що входить до складу BIOS (BIOS ISR). BIOS ISR аналізує скен-код і по спеціальним правилам перетворює його. Відзначимо, що по скен-коду все ще можна встановити, внаслідок чого ISR отримала управління: через натиск або через відпускання клавіші.
Дії BIOS ISR при натисканні та відпусканні однієї і тієї ж клавіші розрізняються. Клавіші в залежності від алгоритму обробки їх скен-коду поділяються на наступні групи:
1. шифт-клавіші(Right-Shift, Left-Shift, Alt, Ctrl);
2. тригерні клавіші (NumLock, ScrollLock, CapsLock);
3. клавіші з буферизацією розширеного коду
4. спеціальні клавіші (PrnScr, Ctrl-Alt-Del,Ctrl-C…)
За кожною шифт- або тригерною клавішею закріплений свій байт за адресою 40:17h та 40:18h. При кожному натисненні або відпусканні шифт-клавіші, BIOS ISR інвертує відповідний біт. Таким чином стан біту шифт-клавіші говорить про те, натиснута клавіша чи ні. За тригерними клавішами закріплені два біти: один з них інвертується тільки при натисненні клавіші (фіксує положення Вкл/Викл), другий - при натисканні та відпусканні, відслідковуючи поточний стан клавіші.
Поточний стан шифт- та тригерних клавіш використовується BIOS-обробником переривання при визначенні правил обробки скен-кодів від інших клавіш. Більшість клавіш та їх комбінацій з шифт- клавішами ‑ це клавіші з буферизацією розширеного коду: при їх натисненні у спеціальний буфер пам’яті заноситься двобайтовий код, що називається BIOS-кодом клавіші. Молодший байт цього коду містить ASCII-код символу, або нуль. Старший байт містить скен-код клавіатури, або так званий розширений скен-код. Комбінація ASCII-код/скен-код генерується в наступних випадках:
1) якщо натиснута клавіша клавіатури, що помічена ASCII символом. Так як великі та малі літери мають різні ASCII-коди, при генеруванні BIOS-коду враховується стан клавіш CapsLock та Shift.
Стан шифт- та тригерних клавіш
Біт |
Стан шифт- та
тригерних клавіш |
|
Байт 40:17h |
0 |
Натиснута і не
відпущена клавіша Right-Shift |
1 |
Натиснута і не
відпущена клавіша Left-Shift |
2 |
Натиснута і не
відпущена клавіша Ctrl |
3 |
Натиснута і не
відпущена клавіша Alt |
4 |
Зафіксована
клавіша ScrollLock |
5 |
Зафіксована
клавіша NumLock |
6 |
Зафіксована
клавіша CapsLock |
7 |
Включений режим
вставки (код клавіші також розміщується в буфері клавіатури) |
|
Байт 40:18h |
0 |
Натиснута і не
відпущена клавіша Left Ctrl |
1 |
Натиснута і не відпущена
клавіша Left Alt |
2 |
Натиснута клавіша
System Request (System) |
3 |
Включений режим
Pause (Ctrl + NumLock) |
4 |
Натиснута і не
відпущена клавіша ScrollLock |
5 |
Натиснута і не
відпущена клавіша NumLock |
6 |
Натиснута і не
відпущена клавіша CapsLock |
7 |
Натиснута і не відпущена клавіша Ins |
2) Якщо натиснути деякі з ASCII-клавіш в комбінації з натиснутою та не відпущеною клавішею Ctrl, а також при натисненні клавіш BackSpase, Enter, Tab і Esc. В цьому випадку молодший байт BIOS-коду клавіші дорівнює одному з керуючих ASCII-кодів. Це ASCII-коди зі значенням 00 – 31, які не входять до числа друкованих символів, а використовуються для управління периферійними пристроями. Наприклад, натиснення клавіші Enter породжує керуючий символ Carriage Return (повернення каретки), натиснення клавіші Tab - символ горизонтальної табуляції, Ctrl-B - сигнал (Bell) та ін.
Двобайтовий код у вигляді "0/розширений скен-код" записується у буфер клавіатури при натисканні клавіші F1 - F12, Ins, Del, клавіші керування курсором, PgDn, PgUp, End, та їх комбінації з клавішами Alt, Ctrl, Shift, а також при натисканні комбінації Alt+ASCII. Розширений скен-код та скен-код клавіатури не співпадають. Деякі клавіші обробляються BIOSом особливим чином:
PrnScr викликає переривання BIOS 5h.
Ctrl+Alt+Del - викликає програму початкового завантаження комп’ютера, яка входить до складу BIOS.
Ctrl+C - ISR BIOS записує за адресою 00471h значення 80h. Воно є прапором того, що користувач бажає завершити поточну програму. Значення цього прапору перевіряють усі ДОС-програми, які працюють з файлами stdin, stdout, stdprn, stdaux. Якщо прапор встановлений, то генерується переривання 23h.
Особливим чином обробляються Alt-введення. Якщо натискається і утримується клавіша Alt і на цифровій клавіатурі набираються цифри, то після відпускання клавіші Alt у буфер клавіатури буде поміщений двобайтовий код, старший байт якого рівний 0, а молодший - набраний цифрами код. Якщо набраний код більше 256, то молодший байт буде містити остачу від ділення на 256.
Буфер BIOS для запису кодів клавіш займає 32 байти з адреси 40:1Еh по 40:3Еh. Запис інформації у буфер виконує ISR BIOS int 9h, читання - ISR BIOS int 16h. Буфер клавіатури розрахований на 15 натиснень клавіш, що генерують двобайтові коди, і тому мають 30 байт для кодів клавіш і 2 "холостих" додаткових байти, що резервуються для двобайтового коду клавіші Enter.
Буфер організовується як кільцева черга, доступ до якої здійснюється за допомогою покажчика “голови”, адреса якого 40:1Ah, і покажчика “хвоста”, адреса якого 40:1Сh. Значення, що записані у покажчиках відповідають зміщенню відносно сегмента 40h. Покажчик "хвоста" задає зміщення до слова, де буде записаний обробником переривання 9h код клавіші, що буферизується. Покажчик "голови" задає зміщення слова, яке буде повернене запиту буферизованого введення з клавіатури, що було зроблено операційною системою або BIOSом.
При кожному натисненні клавіш, для яких генерується двобайтовий код, ISR BIOS переривання 9h, використовуючи поточне значення покажчика "голови", записує у пам’ять згенерований двобайтовий код. Після цього покажчик "голови" збільшується на 2. Якщо покажчик голови перед доступом до буфера вказує на верхню границю буфера( на слово 40:3Eh), то покажчик після запису у буфер "переходить" на початок буферу, йому присвоюється значення 40:1Eh. Тому значення покажчика "хвоста" може бути менше значення покажчика "голови". Це означає, що покажчик "голови" "перескочив" назад до нижньої границі буфера. Коли покажчик "голови" дожене покажчик "хвоста", наступить переповнення буфера. У цьому випадку покажчик хвоста задає зміщення до “холостої” позиції. Кожне нове натиснення клавіші ігнорується BIOS-обробником; код клавіші не поміщується у буфер, і звучить звуковий сигнал.
Покажчик “голови” використовується BIOS-обробником переривання 16h, яке викликається безпосередньо з прикладної програми або функціями MS-DOS введення з клавіатури. Якщо виконується читання буфера з руйнуванням інформації(фун. АН=0, переривання 16h), ISR читає два байти пам’яті за адресою, зміщення якої задає значення "голови", а сегмент рівний 40h. Потім ISR переривання 16h збільшує значення покажчика “голови” на 2. Якщо значення покажчика “голови” та “хвоста” рівні між собою, то це означає, що буфер порожній. У цьому випадку ISR переривання 16h виконує нескінченний цикл очікування введення із клавіатури, умовою виходу із якого буде нерівність покажчиків "голови" та "хвоста". При натисненні клавіші, що генерує двобайтовий код, цей цикл буде перерваний BIOS-обробником переривання 9h. В результаті покажчик “голови” буде збільшено на 2. Після завершення роботи обробника переривання 9h, керування повернеться до ISR переривання 16h. Так як буфер вже не порожній, виконається вихід із циклу, і у точку виклику буде передано прочитаний у буфері двобайтовий код.
Якщо виконується читання буферу без руйнування інформації (фун.АН=1, переривання 16h), збільшення покажчика голови не виконується, але прочитаний за цією адресою код передається у точку виклику, якщо тільки буфер не порожній. Якщо буфер порожній, то ISR не виконує циклу очікування. Замість цього прапор переносу CF встановлюється в 1, і обробник завершує свою роботу.
Буфер клавіатури – це класичний приклад використання кільцевого буферу для організації асинхронної взаємодії двох програм по схемі виробник-споживач. Одна з програм (ISR 9h) "виробляє" інформацію, а друга (16h) її споживає.
MS-DOS має цілу групу функцій переривання 21h для виконання введення інформації з клавіатури. Послідовність дій системи при введенні з клавіатури така. Функція MS-DOS викликає драйвер клавіатури, передаючи йому запит на введення одного символу з буферу клавіатури. Драйвер, виконуючи запит, звертається до потрібної функції переривання 16h BIOS. ISR BIOS переривання 16h читає з буферу клавіатури потрібне слово і передає в драйвер. Драйвер повертає байт (звичайно молодший) в MS-DOS. Таким чином, функції MS-DOS та функції бібліотеки Turbo C, що спираються на них слабко залежать від особливостей апаратури, оскільки система від неї ізольована двома шарами програмного забезпечення – драйверами і BIOSом.
Далі приводиться характеристика функцій MS-DOS, що використовуються для введення з клавіатури.
AH=01h – введення з очікуванням зі стандартного пристрою (клавіатури). Виконується відображення на екран введених символів. Якщо виявляється натиск комбінації клавіш Ctrl-Break, викликається переривання 23h. ASCII-код прочитаного символу розміщується в AL. Якщо натискається спеціальна клавіша, в AL повертається 0, а друге звернення до функції повертає розширений скен-код клавіші.
AH=06h – введення/виведення з консолі. Якщо DL=FFh, виконується введення зі стандартного пристрою введення без очікування. Якщо буфер порожній, функція повідомляє про це встановленим в 1 прапором нуля (ZF). В протилежному випадку в регістрі AL повертається ASCII-код прочитаного символу. Не виконує перевірку натиску комбінації клавіш Ctrl-Break.
AH=07h – введення з консолі з очікуванням без відображення на екран. ASCII-код прочитаного символу повертається в AL. Якщо натискається спеціальна клавіша, в AL передається значення рівне нулю, а друге звернення до функції повертає розширений скен-код клавіші. Функція не виконує "фільтрування" введення з клавіатури. Це означає, що натиск клавіші BackSpace не стирає символ на екрані, а тільки зсуває курсор. Натиск ENTER не переводить рядок, а тільки переміщає курсор на початок рядка. Реакція на натиск комбінації клавіш Ctrl-Break буде відсутня.
AH=08h – подібна AH=07h, за винятком того, що якщо виявляється натиск комбінації клавіш Ctrl-Break, викликається переривання 23h.
АН=0Ah – буферизоване введення рядка з консолі. DS:DX вказують на початок буферу для прийому рядка.
ASCII-коди символів зі стандартного введення розміщуються в буфер до отримання символу повернення каретки CR (ASCII-код 0Dh) або до досягнення умови кінця рядку. В цьому випадку при кожному новому введенні символу звучить сигнал динаміка і символ не поміщується в буфер. Так триває до тих пір, доки не буде введений символ CR, після чого функція повертається в точку виклику. Таким чином, останнім символом буферу завжди є CR. MS-DOS використовує другий байт буферу як лічильник числа введених символів. Символи, вже поміщені в буфер, використовуються як "шаблон" (template), і діють клавіші редагування MS-DOS. Якщо натискається клавіша Esc, на екран виводиться символ ‘\’ і введення рядка починається спочатку; натиск клавіші F5 виводить символ ‘@’ і поточний рядок запам‘ятовується як поточний шаблон; натиск клавіші F3 виводить на екран частину шаблону, що залишилася. Всі інші спеціальні клавіші ігноруються. Якщо виявляється натиск комбінації клавіш Ctrl-Break, викликається переривання 23h.
AH=0Bh – перевірка стану стандартного введення. Повертає в регістрі AL значення FFh, якщо буфер клавіатури не порожній, і 0 в протилежному випадку. Якщо виявляється натиснення комбінації клавіш Ctrl-Break, викликається переривання 23h. Функцію слід використати перед виконанням функцій АН=01, 07h та 08h для того, щоб уникнути очікування введення, якщо воно буде відсутнє. Крім того, функція використовується як засіб перевірки того, чи натиснута комбінація клавіш Ctrl-Break, якщо програма довгий час виконує роботу, не зв'язану з зверненням до функцій MS-DOS. Періодичне виконання функції дозволяє аварійно завершити програму, наприклад, у випадку її зациклювання.
AH=0Ch – введення з клавіатури з очисткою буферу. Значення в регістрі AL містить номер функції, що виконується: 01, 06, 07, 08 або 0Ah. Поведінка функції і значення, що повертаються, описані раніше в специфікації функцій АН= 01, 06, 07, 08 або 0Ah.
Окрім перерахованих функцій MS-DOS, введення з клавіатури виконує і функція MS-DOS префіксного читання файлу або пристрою АН=3Fh, якщо в регістрі ВХ заданий префікс 0, закріплений за стандартним введенням.
Розглянуті функції MS-DOS для введення з клавіатури можуть викликатися напряму з Сі-програми через функції Turbo С geninterrupt(), int86(), intr() і т.п. або неявно функціями введення Turbo С.
Велика група функцій Turbo С для введення з клавіатури – це функції потокового і префіксного введення. Відзначимо, що вони використовують функцію AH=3Fh MS-DOS з префіксом файлу стандартного введення 0. Бо стандартний драйвер консолі підтримує MS-DOS-клавіші редагування (див. раніше опис функції 0Ah), при використанні цих функцій особливим чином обробляються клавіші Esc, F3 і F5. Однак всі інші спеціальні клавіші ігноруються. Отже, використовуючи функції введення, прототипи яких поміщені в файлі <stdio.h>, неможливо виконати, наприклад, введення символів, що не відображаються на екрані, "обминути" обробку реакції на натиск комбінації клавіш Ctrl-Break, визначити натиск спеціальних клавіш. Для виконання таких дій використовуються функції Turbo С, прототипи яких поміщені в файлі <conio.h>. Вони не є частиною ANSI-стандарту мови і входять в розширення Turbo С. Функції, передусім, розраховані на побудову найпростішого віконного інтерфейсу і тому при будь-якому виведенні на екран додатково корегуються змінні, координати, що зберігають поточні позиції курсору активного вікна. Крім того, є можливість управління кольором виведення. Далі надамо опис функцій файлу <conio.h>, призначених для введення символів з клавіатури, і приклади використання цих функцій.
#include <conio.h>
char *cgets(char *str);
Розташовує в буфері, на початок якого вказує str, рядок символів зі стандартного
введення. Запис символів починається з str[2]; str[0] повинен містити
максимальне число символів, що має бути прочитано і записано в рядок. Використовує
MS-DOS-функцію 0Ah, але символ CR перетворює в символ '\0', утворюючи коректний
ASCIIZ-рядок. Функція
повертає покажчик на початок буферу str. Перерахуємо переваги цієї функції у
порівнянні з gets():
ü
можливість визначення при введенні довжини рядка;
ü
захист при введенні від "зайвих" символів, для яких
компілятором не зарезервовано місце;
ü
можливість введення за одне звернення до функції рядків, довжина яких
перевищує встановлений за замовченням буфер для стандартного введення в 128
символів.
Наведемо приклад використання функції cgets() введення рядка до 254 символів за одне звернення до MS-DOS:
#include <conio.h>
#include <stdio.h>
void main (void){
char str[256]; str[0]=254;
cgets( str );
printf("Введено %d символів рядка
%s\n", str[1], &str[2]);
}
#include <conio.h>
int cscanf(char *format [, argument,..]);
Виконує форматоване введення з клавіатури. На відміну від функції
scanf() не виконує буферизацію введення: всі символи, введені з клавіатури,
доступні програмі негайно. Введення пробілу розглядається як завершення
введення. Працює через функцію AH=07h MS-DOS. Відображення введення на екрані
виконує Turbo С.
#include <conio. h>
int getch(void);
Виконує введення з клавіатури через функцію MS-DOS AH=07h. Turbo С не
виконує виведення того, що вводиться. Корисна для організації інтерфейсу з
користувачем, при якому натиск тої або іншої клавіші викликає негайну реакцію
програми без відображення введеного символу на екрані.
#include <conio.h>
int getche( void );
Виконує небуферизоване введення з клавіатури через функцію MS-DOS
AH=07h. Turbo С виводить введення на екран. Переведення рядка відбувається при
досягненні правої вертикальної границі поточного активного вікна.
Наведемо приклад програми, що ілюструє застосування функцій getch() і getche() для визначення натиснень не тільки ASCII-клавіш, але і спеціальних клавіш:
#include <conio.h>
#include <stdio.h>
void main(void) {
int ch;
do {
puts( " Натисніть любу
клавішу...");
if( !(ch=getch()) ) {
ch=getch();
printf("Спеціальна клавіша”
"Розширений скен-код
%#u\n", ch);
}
else
printf( "Символьна клавіша
%c"
" (Код %#u)\n",
ch, ch);
puts ( " Продовжуєте?
(y/n) " );
} while( (ch=getch() )=='Y' || ch=='y');
#include <conio.h>
int kbhit(void);
Перевіряє, чи порожній буфер клавіатури. Якщо в буфері є символи, функція
повертає ненульове значення, в протилежному випадку вона повертає 0.
Використовує функцію 0Bh MS-DOS. Є зручним засобом запобігання "зациклення"
при очікуванні неможливої в даний момент події. Крім того, при виконанні фікції
0Bh здійснюється перевірка натиску комбінації Ctrl-Break, що дозволяє
виконати аварійне завершення програми.
#include <conio.h>
int ungetch(int ch);
Записує безпосередньо в буфер клавіатури символ ch. Він буде доступний
при виконанні наступної операції читання з консолі (функціями файлу
<conio.h>). Дозволяє розміщувати тільки один символ, що не повинен співпадати
з константою EOF, описаною в файлі <stdio.h>. У випадку успіху функція
повертає ch; в протилежному випадку повертається (–1).
Наведемо приклад програми, що розміщує рядок символ за символом в буфер клавіатури. Рядок після цього читається і виводиться на екран.
#include <conio.h>
#include <stdio.h>
void main (void){
char string "Тестовий рядок для виведення
на екран";
while( *string ){
ungetch( *string++); putchar(
getch() );
}
}
Інтерфейсом програм в персональному комп'ютері з клавіатурою є переривання 16h BIOS. Далі приводиться опис його функцій.
АН=00h – читання з очікуванням двобайтового коду з буферу клавіатури. Прочитаний код повертається в регістрі АХ: молодший байт - в регістрі AL, старший - в АН. Якщо натиснута ASCII-клавіша, в AL розміщується ASCII-код символу, в АН - скен-код. При натиску спеціальних клавіш AL рівний 0, а в АН повертається розширений скен-код.
АН=01h – читання без очікування двобайтового коду з буферу клавіатури. Якщо буфер порожній, то в 1 виставляється прапор нуля ZF. В протилежному випадку в АХ повертається двобайтовий код з буферу клавіатури, але просування покажчика "голови" буферу не виконується, т.ч. код "залишається" в буфері.
АН=02h – визначення стану шифт- і тригерних клавіш. В регістрі AL повертається вміст байту за адресою 40:17h.
Описані далі функції переривання 16h підтримуються тільки BIOS, дата якого ‑ 15.11.85 і пізніше.
АН=03h – встановлення затримки і частоти повторення клавіатури. Якщо клавіша залишається в натиснутому стані на протязі часу, що перевищує встановлену затримку (delay), апаратура комп'ютера починає повторну передачу прямого скен-коду клавіші з частотою, що називається частотою повторення (typematic rate). При виконанні функції AL=5 в ВН задається затримка (0 відповідає 250мс, 1 - 500мс, 2 - 750мс, 3 - 1с), в BL - частота повторення (0 відповідає частоті 30раз/с, 2 - 26раз/с,..., 1Eh - 6раз/с, 1Fh - 2рази/с.)
АН=05h – запис в буфер клавіатури двобайтового коду клавіші, що повинен бути записаний в регістр СХ: в CL молодший байт, в СН - старший. Після виходу з функції регістр AL=0, якщо код клавіші записаний в буфер. Якщо ж буфер порожній, AL=1.
Функція АН=05h не має аналогів в бібліотеці Turbo С і може використовуватися для імітації натиснень клавіш в демонстраційних програмах, програмах переносу тексту і т.д.
Функції АН =10 - 12h є аналогами функцій 00 - 02h, але перевизначені для використання в комп'ютерах з клавіатурою 101/102 клавіші.
Наведемо приклад функції enter_kb_BIOS(), призначеної для запису двобайтового коду клавіші key_code в буфер клавіатури. Працює для версій BIOS, подібних BIOS IBM PC AT, що датувалися не раніше 15.11.85, і IBM PC XT, що датувалися не раніше 10.01.86. Функція повертає 0 у випадку успіху і -1, якщо буфер порожній.
#include <dos.h>
int enter_kb_BIOS( unsigned key_code){
struct REGРАСK r;
r.r_cx=key_code;
r.r_ax=0х0500;
intr( 0х16, &r );
return r.r_ax & 0x00ff;
}
Функції АН=00 - 02h переривання 16h BIOS покладені в основу функції bioskey() бібліотеки Turbo С.
#include <bios.h>
int bioskey( int cmd );
Звертається в залежності від значення в cmd до функцій АН=00 - 02h переривання
16h. Значення, що повертається функцією, повторює значення регістру АХ при
виході з переривання.
Наведемо приклад використання bioskey(2) для визначення стану шифт- і тригерних клавіш. Вихід з програми відбувається при натиску символьної клавіші ‘q’.
#include <bios.h>
#include <conio.h>
#include <stdio.h>
void main( void ) {
char BIOS_key_cur, BIOS_key_old,
BIOS_key;
char* icons[ ] = {
"Right
Shift:", ”LeftShift:", "Ctrl
:",”Alt :",
"ScrollLock:",
”NumLock:", "CapsLock:”, "Ins
:", NULL };
int index = 0, x, y;
// Початкове формування екрану
clrscr();
while( icons[ index ] ) puts( icons[
index++ ] );
BIOS_key_old = bioskey(2);
BIOS_key_old ~= BIOS_key_old;
//інверсія бітів
//Цикл очікування натиснення будь-якої
клавіші
while( !( ( bioskey(1) & 0xff00 )
== 0х1000 ) ) {
y=1;
BIOS_key_cur = BIOS_key = bioskey(2);
//цикл інтерпритації бітів байту
стану
for( index=0; index<8; index++) {
x=14; gotoxy( x, y );
if( ( BIOS_key & 0x01 ) != (
BIOS_key_old & 0x01 ) )
if( BIOS_key & 0x01 ) {
textattr( BLACK | ( LIGHTGRAY
<< 4 ) );
cputs( “включена” );
}
else {
textattr( LIGHTGRAY | ( BLACK << 4
) );
cputs( “виключена” );
}
BIOS_key >>= 1; BIOS_key_old
>>= 1; y++;
}
BIOS_key_old = BIOS_key_cur;
}
bioskey(0);
}
В першій книзі комплексу докладно описаний механізм переривань, реалізований персональними комп'ютерами. Розглянемо основні прийоми побудови резидентних програм. Відмінна властивість цих програм полягає в тому, що вони після свого завершення лишаються в пам'яті комп'ютера, а операційна система "захищає" зайняту ними пам'ять від повторного використання. У літературі такі програми найчастіше називають TSR (Terminate-but-Stay-Resident), а іноді — "спливаючими" (Pop-Up) програмами. Використання TSR дозволяє розширити можливості системи по обслуговуванню зовнішніх пристроїв або реалізувати так зване "пасивне" мультипрограмування. MS-DOS є однопрограмною системою, але активізація TSR викликає "переключення" комп'ютера на резидентну програму. Якщо активізація TSR виконується періодично, з'являється можливість виконання програм на фоні інших програм — виконання музики, друк файлів, обслуговування зв'язку між комп'ютерами і т.д. Непристосованість MS-DOS до багатопрограмної роботи обумовлює особливі вимоги до TSR, докладно розглянутого далі.
MS-DOS має дві можливості для резидентного завершення програм:
1) переривання 27h (тільки для. Сом-файлів);
2) функція AH=31h переривання 21h (для .ЕХЕ- і .Сом-файлів).
Невелика різниця між ними обумовлена способом, що використовує MS-DOS для визначення розміру блоку пам'яті, що оголошується резидентним. Виконуючи переривання 27h, MS-DOS приймає за початок блоку значення в регістрі CS, а довжина блоку в байтах задається в регістрі DX. При виконанні MS-DOS-функції 31h значення в DX задає довжину блоку в параграфах, а за початок блоку приймається ідентифікатор активної програми (PID) — параграф, по якому в пам'яті розташовується префікс сегмента програми, що виконується в даний момент. У регістрі AL задається код повернення, що передається програмі-предку при завершенні TSR.
Дії MS-DOS у випадку резидентного завершення подібні тим, що виконуються при зміні розміру блоку пам‘яті. Блок, з якого починається PSP, усікається до запитаного розміру, але залишається зайнятим після завершення програми. Збережені там дані і код програми не будуть перевизначатися при завантаженні нових програм і (або) створенні нових блоків пам'яті. Адже ця пам'ять з погляду MS-DOS залишається "зайнятою". Отже, TSR може бути запущена в будь-який момент після завершення без повторного завантаження в пам'ять.
Сама TSR-програма складається з двох функціонально різних секцій: резидентної і ініціалізуючої. Ініціалізуюча частина виконується тільки один раз — при запуску програми на виконання. Резидентна частина TSR найчастіше складається з двох секцій:
складених з дотриманням спеціальних правил одного або декількох обробників переривання, або ISR (Interrupt Service Routine), і звичайних Сі-функцій, які викликаються з ISR. Обробники одержують керування при виникненні яких-небудь зовнішніх умов, що генерують апаратне переривання, або при виконанні поточною програмою інструкції INT (як говорять, при виникненні програмного переривання). Місце входу в ISR записується в таблицю векторів переривань. У найпростішому випадку вся резидентна частина TSR являє собою єдиний обробник переривання. Найбільший інтерес при проектуванні TSR представляють ISR, основи дизайну яких докладно розглядаються в наступних параграфах.
Функція, побудована для використання в якості ISR, повинна бути абсолютно "непомітною" для тих програм, що вона перериває або, як часто говорять, на фоні яких виконується. Це можливо тільки в тому випадку, якщо стан системних та апаратних засобів комп'ютера перед активізацією ISR і після її завершення буде тим самим. Крім того, для активізації ISR процесор виконує інструкцію INT, що зв'язано зі збереженням у стеку не тільки CS:IP точки повернення, але і слова прапорів. Тому ISR повинна задовольняти наступним обов'язковим вимогам:
1) ISR повинна починатися секцією коду, що зберігає регістри процесора, які змінює в процесі роботи;
2) ISR повинна завершуватися секцією відновлення усіх регістрів, які змінила у ході своєї роботи;
3) повернення з ISR повинно виконуватися не звичайною машинною командою RET, а спеціальною інструкцією IRET.
Дотримання даних вимог звичайними засобами Turbo С (псевдозмінні, бібліотечні функції і т.п.) неможливо. Однак Turbo С надає програмісту зручну можливість для цього. Якщо звичайна Сі-функція з'являється з модифікатором interrupt, компілятор автоматично додає секції збереження всіх регістрів на початку коду функції, та їхнього відновлення наприкінці цього коду і замість RET підставляє IRET, забезпечуючи правильне повернення з переривання. Наприклад, якщо планується використовувати функцію my_isr() як обробник переривань, її варто описати так:
void interrupt my_isr( unsigned bp, unsigned di,
unsigned si, unsigned ds, unsigned es,
unsigned dx,
unsigned ex, unsigned bx, unsigned ax,
unsigned ip,
unsigned cs, unsigned flags);
Параметри в круглих дужках задають значення відповідних регістрів, переданих обробнику переривання my_isr(). Якщо регістри для передачі параметрів не використовуються, специфікувати їх не потрібно. Тому можливо наступний опис my_isr():
void interrupt my_isr();
Всі функції типу interrupt вважаються far-функціями, але викликаються інакше. Якщо при виклику звичайної far-функції компілятор використовує машинну команду CALL FAR, а функція завершується командою RET FAR, то для interrupt-функцій використання тільки CALL FAR приведе до "зависання". Адже така функція завершується командою IRET, по якій зі стека витягаються значення для CS:IP точки повернення, а також слово прапорів. Але зі стека не можна витягати те, чого там не було і немає! От чому компілятор при звертанні до interrupt-функції перед CALL FAR поміщає інструкцію PUSHF, записуючи в стек слово прапорів. Це сильно полегшує побудову ланцюжків обробників переривань.
Крім збереження регістрів, при вході в interrupt-функцію компілятор генерує код для установки регістру DS рівним сегменту DGROUP. Практично це означає, що ISR має доступ до статичних і зовнішніх даних програми. Використання автоматичних змінних, збережених у стеці, для функцій типу interrupt можливо у випадку, коли прийняті додаткові заходи по переключенню стека. Компілятор не генерує автоматично код по переключенню стека на ті значення, що відповідають interrupt-функції.
Крім розглянутих вимог, грамотно побудована ISR повинна дотримувати інші умови, що випливають з очевидного: завершивши роботу, ISR чисто "прибирає за собою". Інакше перервана програма, знову почавши своє виконання, зіштовхнеться з неочікуваними змінами. Наслідок цього при роботі під керуванням MS-DOS непередбачений. Найбільш ймовірний результат — необхідність перезавантаження комп'ютера з вимиканням живлення.
У ряді випадків виникає необхідність у передачі параметрів interrupt-функціям і поверненні значень. Так, як основне призначення таких функцій — це їх використання в якості ISR, природно, що обмін значеннями варто виконувати через внутрішні регістри процесора.
Компілятор додає в код interrupt-функції
секцію запису в стек усіх внутрішніх регістрів процесора, за винятком SS і SP.
Тому що interrupt-функція викликається як обробник переривання (або явно інструкцією
INT, або її аналогом PUSHF, CALL DWORD PTR), у стеці будуть збережені прапори й
адреса точки повернення. У
результаті після виконання початкової (недоступної програмісту) секції
interrupt-функції, стек матиме, те що ілюструє мал. 3.1.
Рис. 3.1. Стан стека після виклику interrupt-функції
Досить просто вирішується задача передачі в interrupt-функцію параметрів у регістрах. Перед викликом функції потрібно записати передані значення в регістри і викликати функцію. Для цього зручно використовувати псевдозмінні _ВР, _DI, _SI і т.п. Якщо точка входу у interrupt-функцію записана в таблиці векторів перериваннь, передачу керування функції виконує geninterrupt. Інша можливість — це використання бібліотечної функції int86() і подібних їй. В останньому випадку вирішується відразу і задача установки потрібних значень у регістри процесора. Але в ряді випадків interrupt-функція викликається не через таблицю векторів переривання, а непрямим викликом (CALL DWORD PTR). Це не дозволяє передавати параметри в interrupt-функцію через регістр DS: поточне значення DS використовується для задання в сегменті даних адреси точки входу у функцію.
Повернення значень у регістрах з interrupt-функції можливе, але вимагає деяких додаткових зусиль. Установка значення регістра, що повертається, у межах interrupt-функції, наприклад, через псевдозмінні _АХ, _ВХ, _СХ і тому подібні не зберігається при завершенні функції. Компілятор, зустрівши модифікатор типу функції interrupt, вставляє в початок коду функції секцію збереження всіх регістрів, а в кінець — секцію відновлення значень зі стека. Тому всі зміни значень регістрів усередині interrupt-функції не будуть позначатися на значеннях регістрів після її завершення. Єдина можливість для повернення значень через регістри — це запис значень, що повертаються, у стек. У результаті при відновленні регістрів буде встановлюватися потрібне значення, а не те, що було в регістрі при вході в interrupt-функцію. Turbo С для полегшення доступу до копій регістрів у стеці використовує формальні параметри. Перший параметр списку — це посилання на копію в стеці регістра BP, другий — регістра DI, третій — регістра SI і т.д. Саме в цьому і полягає зміст формальних параметрів, що перераховані при описі функції. Якщо interrupt-функція передає в регістрах кілька значень, потрібно описати всі попередні формальні параметри. Наприклад, якщо планується передача параметрів у регістрі ES, то список буде складатися з п'яти елементів, де перші чотири параметри не використовуються.
Як приклад передачі параметрів у interrupt-функцію і повернення значень з неї через регістри далі наводиться тестова програма, що складається з main()-функції і interrupt-функції my_isr(). У функцію my_isr() передаються константи в регістрах процесора АХ, ES і BX. Функція my_isr() повертає встановленими в одиницю прапори нуля ZF (6-й у слові прапорів) і переносу CF (0-й у слові прапорів), а також встановлює в регістр АХ значення F0FFh, якщо при виклику my_isr() АХ = F000h, ES = BX = FFFFh і my_isr0 не змінює значення регістра АХ і прапорів процесора в протилежному випадку. Для більшої "переконливості" my_isr() "чіпляється" за невикористаний вектор переривання F1h і викликається як обробник переривання функцією intr().
#include <stdio.h>
#include <dos.h>
#include <conio.h>
void interrupt my_isr(unsigned bp, unsigned di, unsigned si, unsigned ds,
unsigned in_es, unsigned dx, unsigned ex, unsigned in_bx, unsigned in_ax,
unsigned ip, unsigned cs, unsigned out_flags) {
if(in_ax == 0xF000 && in_es ==
0xFFFF && in_bx == 0xFFFF) {
/* встановлення прапорів ZF і CF */
out_flags = 0х0041;
in_ax = 0xF0FF; /* повернення AX =
F0FFh */
}
}
void main( void ) {
struct REGPACK r;
int ch, cs, ip;
void interrupt( *old_f1 )( void
); /* тут зберігається
"старий" вектор переривання f1h */
old_f1 = getvect(0xf1);
setvect(0xf1, my_isr); /* за переривання f1h "чіпляється"
my_isr()*/
do {
printf( "AX = ");
scanf("%x", &r.r_ax);
fflush( stdin );
printf( "BX = ");
scanf("%x", &r.r_bx);
fflush( stdin );
printf( "ES = ");
scanf("%x", &r.r_es);
fflush( stdin );
/*
активізація my_isr через переривання */
intr(0xf1, &r);
рrіntf(“АХ = %x, ZF = %ld, CF = %ld
\n",
r.r_ax, (r.r_flags &
0х40) >> 6, r.r_flags & 0х01);
puts( "Завершуєте? (Y/N)"
);
} whi1e( !((ch = getch()) == ‘y’ || ch
== 'Y'));
setvect(0xf1, old_f1); /*відновлюється
вектор f1h*/
}
Можливість установки потрібних значень у регістрах на виході з ISR дозволяє використовувати interrupt-функції як розширення BIOSa і MS-DOS, запобігти повторній установці TSR і ін.
TSR-програма складається з двох частин: ініціалізуючої і резидентної. Ініціалізуюча частина TSR-програми виконується тільки один раз, при інсталюванні резидентної частини. Як відзначалося раніше, резидентна частина TSR складається з двох секцій: одного або декількох обробників перериваннь, або ISR, і однієї або декількох звичайних Сі-функцій, які викликаються з ISR. Обробник переривання (ISR) — це функція, що одержує керування при виконанні апаратних або програмних переривань. У найпростішому випадку резидентна частина TSR являє собою єдиний обробник переривання. У більшості ж випадків до складу TSR входить не один, а декілька ISR, що працюють паралельно і взаємодіють один з одним через зовнішню змінну, виконуючи роль бінарних семафорів. Такий семафор приймає тільки два значення: YES (1) і NO (0).
Ініціалізуюча частина виконує обов'язкові і додаткові (факультативні) дії. До числа обов'язкових відносяться:
1) встановлення зв'язку резидентного обробника переривання з тим або іншим перериванням системи (перехоплення переривання);
2) резидентне завершення TSR.
Факультативні дії покликані підвищити "професіоналізм" TSR і включають:
1) запобігання повторної установки резидентної частини TSR;
2) звільнення пам'яті, зайнятої TSR;
3) перехоплення інших переривань системи для підвищення надійності роботи TSR: переривання по натисканню клавіш Ctrl-Break, переривання критичної помилки MS-DOS, звертання до диска, від таймера й ін.
Надалі будуть розглядатися дві основні схеми включення обробників переривань, що входять до складу резидентної частини TSR:
1) каскадне включення;
2) перевизначення "старого" обробника.
Каскадне включення — це таке встановлення в систему "нового" обробника переривання, при якому останній одержує керування у випадку виникнення апаратного або програмного переривання, а потім викликає "старий" обробник переривання (мал. 2.2). "Старий" обробник — це ISR, що одержувала керування при генеруванні даного переривання до запуску TSR. "Новий" обробник — це ISR, що входить до складу резидентної програми. Він може виконувати додаткові дії або до виклику "старого" обробника, або після повернення керування з "старого" обробника.
На мал. 2.2 показане каскадне включення "нової" ISR my_іsr(), що одержує керування при натисканні (відпусканні) будь-якої клавіші клавіатури і виконуючої свою функцію після "старого" обробника. Вважається, що вихідний ("старий") обробник переривання 9 — це ISR BIOS. На практиці для деяких переривань каскад включає не два, а багато більше ISR. Адже замість ISR BIOS переривання 9 цілком може бути ISR, що викликає свій "старий" обробник, що, у свою чергу, викликає ще одну ISR. Саме для переривання 9 звичайно вибудовуються такі довгі каскади, функція my_isr() може бути побудована так, що "нові" дії будуть виконуватися перед "старими" функціями. Для цього виклик "старого" обробника виконується в самому кінці "нового".
int 9
void interrupt my_isr()
Збереження регістрів
old_9(); /•
виклик "старого" оброблювача
*/|
Функції, що доповнюють "старий" оброблювач
переривання 9
Збереження регістрів;
обробка переривання 9:
- читання порту 60h;
- формування і запис у пам'ять коду натиснутої клавіші
або зміна слова стану шифт- і триггер-ных клавіш;
відновлення регістрів
IRET
Відновлення регістрів IRET
"Новий" оброблювач переривання 9
"Старий" оброблювач переривання 9
Рис. 2.2. Каскадне підключення оброблювача переривання 9 ("новиною" функції виконуються після "старих")
Перевизначення — це спосіб підключення ISR, при якому "новий" обробник не викликає зі своїх меж "старий", тобто цілком підмінює "стару" ISR.
Розглянемо приклад TSR-програми, що зберігає у файлі на диску копію довільного вікна екрана в текстовому режимі. Резидентна частина TSR активізується натисканням клавіші PrnScr. Ім'я файлу для збереження копії вікна екрана задається в діалозі. Перелічимо клавіші, на які буде реагувати резидентна частина TSR:
PrnScr — на екрані з'являється прямокутне вікно, відмічуване контрастним кольором. Розміри вікна можуть змінюватися з клавіатури. Координати є статичними, тобто при наступній активізації TSR вікно має координати, задані в попередньому сеансі роботи з нею;
клавіші зі стрілками (Left, Right, Up, Down) — керують координатами лівого верхнього вікна екрана;
клавіші Ctrl-Left, Ctrl-Right, Ctrl-PgUp, Ctrl-PgDn — керують координатами правого нижнього кута екрана;
S (Save) — на екрані з'являється вікно діалогу, у якому задається ім'я файлу призначення. Вікно дозволяє редагувати ім'я файлу. Рядок імені є статичний, зберігаючи значення, задане в попередньому сеансі роботи з програмою;
Esc — TSR повертає керування в перервану програму, не виконуючи ніяких дій;
Q (Quit) — "зняття" резидентної частини TSR зі звільненням займаної пам'яті.
В міру розгляду матеріалу TSR буде ставати все "розумніше" і надійніше. Приклади Сі-функцій, що приводяться далі, розраховані на багатофайлову компіляцію з включенням у файл проекту ініціалізуючої функції main(), власної ISR і всіх необхідних функцій — резидентного завершення, виведення рядків, рамок і т.д. Орієнтація на багатофайлову компіляцію змушує повторювати у файлах включення файлів і явно описувати багато об'єктів як зовнішні.
Розумна технологія розробки TSR на Turbo С базується на наступному підході: резидентні Сі-функції розробляються як звичайні з використанням усіх доступних у Turbo С засобів. Однак варто враховувати, що ці функції можуть викликатися на фоні будь-яких програм при найнесприятливішому сполученні умов. Усю додаткову роботу з визначення моментів безпечного виклику резидентних функцій варто виконувати в межах ISR. У результаті ISR являють собою невеликі interrupt-функції, що забезпечують умови безпечного виклику звичайних Сі-функцій.
Перехоплення переривання — це запис адреси точки входу "нового" обробника переривання в таблицю векторів переривання. Після того, як ця дія виконана, кожне переривання даного номера буде активізувати "нову" ISR. Використання "старого" вектора залежить від опцій поводження інсталятора. Як правило, "старий" вектор записується в сегменті даних TSR і використовується для відновлення таблиці векторів переривання у вихідний стан при видаленні TSR з пам'яті і (або) при каскадному включенні "нового" обробника як адреса для виклику "старого" обробника.
Існує кілька можливостей для запису в таблицю векторів переривання "нового" обробника TSR-програми:
1) безпосередній доступ до векторів переривання з використанням механізму far-покажчиків;
2) застосування спеціальних бібліотечних функцій Turbo С;
3) використання функцій MS-DOS.
Безпосередній доступ до таблиці векторів переривання — найбільш швидкий спосіб. Однак маніпуляції з векторами вимагають особливої обережності. Тому що апаратне переривання може виникнути в будь-який момент при виконанні програми, ймовірна ситуація неповної зміни вектора переривання, що, звичайно ж, неприпустимо. Для того, щоб виключити таку можливість при маніпуляції з таблицею векторів, варто установити в 0 прапор IF, блокуючи апаратні переривання, а після завершення роботи з таблицею — знову дозволити апаратні переривання, установивши IF у 1. Для керування прапором IF можна використовувати функції бібліотеки Turbo С enable() та disable().
Наприклад, так міг би виглядати найпростіший інсталятор TSR, що зв'язує interrupt-функцію ISR_int5() з вектором переривання 5, виконуючи перевизначення "старого" обробника переривання 5. У дужках дана нумерація точок покрокового коментарю виконуваних програмою дій.
/* L2_2.C */
Інсталятор резидентної програми, що використовує
переривання 5. "Старий" обробник переривання викликається
перед завершенням "нового". */
#include <dos.h>
(1) struct address {
void interrupt ( *p )(void);
};
(8) void interrupt ( *old_int5 )( void );
void interrupt ISR_int5( void ); /* власна ISR переривання 5;
найпростіший варіант - див. L2_3.C */
void tsr(char status, unsigned size); /* прототип
функції резидентного завершення - L2_6.C */
int main(void) {
(2) struct address far *int5 = (struct
address far * )(5 << 2);
(3) old_int5 = int5 -> p;
(4) disable();
(5) int5 -> p = ISR_int5;
(6) enable();
(7) tsr(0, 2000);
Найпростіша ISR для розглянутого приклада програми інсталяції поміщена у файл L2_3.C. У приведеному варіанті — це просто "заглушка", що викликає функцію TSR_activate(). Ніякі спеціальні дії по визначенню безпечного моменту активізації резидентної функції не виконуються, що робить TSR потенційною загрозою системі. Надалі будуть внесені всі необхідні удосконалення для того, щоб звести до мінімуму цю загрозу.
/* L2_3.C Найпростіший варіант ISR переривання 5.
*/
#include <dos.h>
extern void interrupt ( *old_int5 )( void );
void TSR_activate( void ); /* прототип функції, яка викликається
обробником резидентної функції */
void interrupt ISR_int5(void) {
TSR_activate(); /* активізація
резидентної функції */
old_int5(); /* виклик
"старого" обробника переривання 5*/
}
TSR_activate() просто позначає факт свого виклику:
друкує, починаючи з центра екрана, слово "Центр". Вона розрахована на коректну роботу у всіх текстових режимах EGA- і VGA-адаптерів: 25х40, 25х80, 28х50, 43х80, 50х80. Починається обробник з визначення поточних параметрів відеосистеми і запису їх у зовнішньою змінну, яку використовує функція виведення рядка hor_pm(). Специфіка TSR така, що для виведення інформації не слід використовувати функції файлу <stdio.h> або <conio.h>, якщо те що активізує ISR не аналізує стан MS-DOS. Тому виведення рядка виконує hor_prn(), яка виводить інформацію безпосередньо у відеопам'ять).
/* L2_4.C */
/* Найпростіший варіант TSR-функції, що виводить в центрі екрана слово
"Центр". Розрахована на EGA- і VGA-адаптери. Для CGA зовнішня змінна
max_str жорстко прирівнюється 24. Для монохроматичного адаптера vid_memory
повинна встановлюватися рівною 0хb000. */
#include <dos.h>
#include "screen.h"
unsigned vid_memory = 0xb800, start_adr, max_str, max_stolb;
void TSR_activate(void) {
/* При кожнім вході у функцію визначаємо поточні значення параметрів
відеосистеми, що служать зовнішніми змінними функції hor_prn(). */
max_str = *(char far *) MK_FP(0x40,
0х84);
max_stolb = *(char far *)
MK_FP(0x40, 0x4a);
start_adr = *(unsigned far *)
MK_FP(0x40, 0x4e);
/* Виведення тестового рядка безпосередньо у відеобуфер. */
hor_prn( max_str / 2, max_stolb / 2, "Центр", 0х1е);
return;
Для одержання завантажувального модуля TSR-програми варто створити файл проекту, що включає файли L2_2.C, L2_3.C, L2_4.C, L2_6.C і файл з функцією hor_prn(). Після завершення компіляції і компонування буде отримана програма, при запуску якої на виконання в пам'яті комп'ютера залишиться резидентна порція, яка активізується при натисканні клавіші PrnScr. Компіляція резидентних програм вимагає ретельного настроювання опцій. Зокрема, повинні виключатися опції контролю переповнення стека і використання регістрових змінних. Для розглянутого приклада (точніше, для функції hor_prn()) критичною буде й опція, що визначає символьні змінні як беззнакові (Unsigned Chars). При розробці TSR-програм рекомендується використовувати модель пам'яті SMALL, тому що резидентні програми не повинні бути занадто великими (більш 64 Кбайт).
Коротко пояснимо зміст окремих позицій функції main() програми L2_2.C, що виконує інсталяцію резидентної частини TSR:
(1) — описується зовнішній шаблон address для структури, що складається з єдиного поля р. Поле містить покажчик на функцію типу void interrupt name(void). Відзначимо ще раз, що interrupt-функції вважаються компілятором far-функціями і тому р — це far-покажчик, що займає 4 байти. Фактично — це вектор переривання. Розміщення вектора в пам'яті комп'ютера в точності відповідає структурі far-покажчика: молодші два байти займає зсув, старші два байти — сегмент. Для спрощення доступу до таблиці векторів далі будуть використовуватися покажчики на структурні змінні по шаблону address;
(2) — описується far-покажчик int5 на структурну змінну по шаблону address, і цей покажчик ініціалізується значенням 20 (точніше, значенням 00000020). Зазначене в дужках явне приведення типу гарантує коректність ініціалізації. Фізична адреса 20 (0000:0020) — це адреса, по якій в пам'яті розташовується вектор переривання 5 (номер переривання, помножений на 4, тому що кожен вектор має довжину 4 байти);
(3) — "старий" вектор переривання 5 записується в зовнішню змінну old_int5. Вона описана як покажчик на функцію типу void interrupt (див. (8)). Надалі це значення використовується "новим" обробником переривання для виклику з поверненням "старого" обробника, тобто для утворення каскаду (або ланцюжка) обробників переривання 5;
(4) — маскуються апаратні переривання, тому що всі дії з таблицею векторів переривання варто виконувати "атомарно", тобто без переривань. При написанні TSR-програм завжди варто виходити з можливості виникнення самої неприємної події. У даному випадку — з можливості того, що клавіша PrnScr буде натиснута саме в той момент, коли вектор переривання 5 змінився тільки наполовину;
(5) — copy_scr — ім'я програми, що встановлюється резидентною. Тому що вона описана як функція типу void interrupt, типи операндів ліворуч і праворуч від оператора присвоювання збігаються;
(6) — після завершення критичної секції коду необхідно знову дозволити апаратні переривання. У противному випадку комп'ютер не буде реагувати на натискання клавіш, модифікувати лічильник реального часу дня і т.п. Час, протягом якого прапор IF залишається скинутим у 0, повинен бути завжди мінімально можливим;
(7) — після модифікації таблиці векторів переривання виконується резидентне завершення всієї програми. Параметр, переданий у функцію tsr(), визначає число 16-байтових параграфів, що залишаються резидентно. Якщо TSR "захоплює" вектори переривань 0, 4, 5 або 6, вона повинна завершуватися безпосереднім звертанням до функції MS-DOS.
Інший спосіб роботи з таблицею векторів переривання — використання бібліотечних функцій Turbo С getvect() і setvect() відповідно для одержання й установки значення потрібного вектора переривання. Далі приводиться специфікація цих функцій.
#include <dos.h>
void interrupt( *getvect(int іntr_num) )();
Функція звертається до функції АН = 35h MS-DOS для одержання адреси
точки входу поточного обробника для переривання, номер якого задає intr_num.
Функція повертає far-покажчик на точку входу обробника (вектор переривання), що
вважається функцією типу void interrupt name(void).
#include <dos.h>
void setvect( іnt іntr_num, void interrupt (*іsr)());
Функція звертається до функції АН = 25h MS-DOS для запису в таблицю на
місце вектора intr_num far-покажчика isr (вектора переривання) на точку входу в
"новий" обробник переривання. "Новий" обробник переривання
— це функція, описана як void interrupt isr(void).
От як виглядає программа-ініціалізатор, еквівалентна L2_2.C, але вектори, що використовує для роботи з таблицею, повертають бібліотечні функції Turbo С:
/* L2_5.C Інсталятор резидентної програми, що
використовує
переривання 5. */
#include <dos.h>
void interrupt (* old_int5)(void);
void interrupt ISR_int5(void); /* власна ISR переривання 5;
найпростіший варіант -див. L2_3.C */
void tsr(char status, unsigned size); /* функція резидентного завершення -
L2_6.C */
int main(void) {
old_int5 = getvect(5); /* зберігаємо
"старий" вектор 5 */
setvect( 5, ISR_int5 ); /* записуємо
новий вектор 5 */
tsr( 0, 2000 );
Використання бібліотечних функцій getvect() і setvect() робить програму наочніше і рятує користувача від необхідності піклуватися про скидання (встановлення) прапора переривань IF. Плата за це — збільшення розміру завантажувального модуля програми й уповільнення її роботи. Наприклад, для приведеного фрагмента з бібліотечними функціями .Ехе-файл виходить більшим приблизно на 50 байт. Далі у всіх прикладах даної глави буде використаний саме такий прийом роботи з таблицею векторів переривання. Справа в тім, що розмір інсталятора TSR і його швидкість не грають принципової ролі, тому що ця частина TSR виконується тільки один раз, а при грамотній установці інсталятор виключається з резидентной частини програми.
Безпосереднє звертання до функцій MS-DOS для роботи з таблицею векторів переривання з програми мовою Сі не дає ніяких додаткових переваг і далі не розглядається.
У наведеному прикладі обробника переривання ISR_int5() використовується каскадна побудова — "новий" обробник виконує "нові" функції, а потім викликає "старий" обробник. Така побудова дозволяє доповнити "старі" обробники, у ролі яких, найчастіше, виступають стандартні ISR BIOSa і MS-DOS, "новими" функціями, роблячи систему відкритою для розширень.
Виклик "старого" обробника може виконуватися на самому початку "нового" обробника. У цьому випадку "нові" функції виконуються після повернення керування. Як окремий випадок, "новий" обробник може не утворювати каскад ISR. Однак у цьому випадку всі "старі" обробники, у тому числі можливо вхідні в BIOS і MS-DOS, відключаються або як, іноді говорять, "топляться".
Повне перевизначення ISR можливе тоді, коли "нова" ISR виконує всі необхідні системні операції, наприклад обробку введення з клавіатури, або "чіпляється" за невикористаний BIOS або MS-DOS вектор переривання. Встановлений компанією Microsoft розподіл векторів переривання залишає для користувальницьких TSR вектори переривання з номерами F1h — FFh. Однак які-небудь угоди по розподілу цих векторів іншими резидентними програмами відсутні. Тому завжди існує імовірність "утопити" раніше встановлені резидентні програми, можливо активізуємі по невикористаному вектору. Іноді такий прийом бажаний або для економії пам'яті, або для захисту програмного продукту від копіювання. Наприклад, усі професійні ігрові програми використовують власні ISR для введення з клавіатури (переривання 9), від таймера (переривання 1Ch), друку екрана (переривання 5) і інших апаратних переривань, підвищуючи швидкість програми в кілька разів і захищаючи її від "зайвих" апаратних переривань. Повне перевизначення апаратних переривань зменшує також ризик несанкціонованого копіювання текстової інформації в різних інформаційних базах і "розшифровку" способу захисту програми від копіювання за допомогою раніше встановлених резидентних програм типу розглянутого в даній главі copy_scr(). "Каскадна" побудова — найбільш часто застосовуваний на практиці прийом.
У розглянутому прикладі "старий" обробник викликається з поверненням у точку виклику і при цьому використовується машинна інструкція CALL DWORD PTR з непрямою адресацією, що випереджається машинною інструкцією запису в стек прапорів. Саме так компілюється рядок коду
old_int5();
у файлі L2_3.C.
Існують більш витончені рішення по утворенню каскадів обробників, доступні, щоправда, тільки при використанні асемблера. Ідея цих методів полягає в тому, що код обробника переривання, що включається в каскад, є "недобудованим", а його завершальну побудову виконує програма інсталяції. Наприклад, фрагмент асемблерного коду показує, як можна організувати каскадне включення ISR:
у кодовому
сегменті "нової" ISR
еквівалент
машинної команди INT: pushf + call far з;
"недобудованою" машинною командою CALL FAR pushf
db 9A
old int dd ?
iret ;повернення з "нового" обробника
викликається обробник,
завершується командою IRET
код машинної команди CALL FAR
с прямою адресацією
4 байти для точки входу в "старий"
обробник
Програма інсталяції, прочитавши вектор
"старого" обробника, записує його в кодовий сегмент
"нового" обробника зі зсувом, що задається ім'ям old_int. Після цього
5-байтова машинна команда CALL FAR здобуває закінчений вид: перший байт (9Ah)
задає код операції, а чотири наступних — адресу, по якій потрібно передати
керування. Труднощі використання наведеного методу полягають в неможливості
визначити засобами Turbo Сі значення зсуву імені old_int на етапі компіляції.
Тому приходиться використовувати відладник для визначення значення зсуву
old_int від адреси точки входу в "новий" обробник. Розглянутий
технічний прийом дозволяє для виклику "старої" ISR обійтися без
зовнішньої змінної (наприклад, old_int5() у лістингу L2_3.C), доступ до якої
неминуче вимагає установки в регістр DS значення DGROUP. Крім того, виклик із
прямою адресацією CALL FAR виконується набагато швидше, ніж виклик з непрямою
адресацією CALL DWORD PTR. Якщо "нова" ISR будується за каскадною схемою,
нові функції виконуються перед "старою" ISR і не потрібно повернення
керування з "старої" ISR у "нову", то найбільш раціональним
буде виклик "старої" ISR машинною командою безумовного переходу JMP
FAR. У цьому випадку каскад ISR
виглядає так, як це показано на мал. 2.3.
"Нова" ISR
INT 2Fh
ISR_int2F PROC FAR ; будь-які функції, ; дополняющие
"стару" ISR
"Стара" ISR
; виклик наступний ISR ; каскаду OF4F: OlAOh JMP FAR OF4F
: OlAOh —і—і—і—<—і—і—і—,
ISR_int2F ENDP
..——————————————————————————— IRET
Рис. 2.3. Каскад
ISR переривання 2Fh, утворений інструкцією JMP FAR (лістинг L2_8.ASM)
Докладний приклад використання подібної техніки приведений у лістингу L2_11.C для "динамічної" побудови машинної команди JMP FAR, використаної в ISR переривання 2Fh (див. лістинг L2_8.ASM).
Наступна після перехоплення переривання обов'язкова дія ініціалізуючої частини TSR — резидентне завершення. Для цього можна:
1) використовувати спеціальну функцію keep() з бібліотеки Turbo С;
2) звернутися безпосередньо до функції АН = 31h MS-DOS.
Далі приводиться специфікація функції keep().
#include <dos.h>
void keep(unsіgned char status, unsigned
size);
Звертаючись до MS-DOS, функція АН=31h, залишає поточну програму в
пам'яті резидентно. Резидентний блок пам'яті починається з адреси _PSP:0 та
займає size параграфів. У предок передається код повернення, що задається
змінною status.
Вибір методу залежить від переривання, за яке "чіпляється" "новий" обробник переривання. Якщо це переривання 0, 4, 5 або 6, функція keep() непридатна, тому що перед звертанням до функції АН=31h MS-DOS резидентного завершення програми, keep() виконує відновлення перерахованих векторів у ті значення, що вони мали перед входом у main(). Коли так, то всі зміни цих векторів, зроблені ініціалізуючою частиною TSR, не будуть діяти після завершення програми. У цьому випадку приходиться звертатися безпосередньо до MS-DOS. Далі приводиться текст функції tsr(), параметри якої збігаються з параметрами keep(), але для резидентного завершення використовується звертання до операційній системі.
/* L2_6.C */
/* Резидентне завершення програми. Розмір резидентного блоку - size параграфів,
у MS-DOS передається код завершення status. Використовується тоді, коли
потрібно "зачепити" TSR за переривання 0, 4, 5 і 6. В інших випадках
варто використовувати keep(). */
#include <dos.h>
void tsr(char status, unsigned size) {
_AH = 0x31;
_AL = status;
_DX = size;
geninterrupt(0x21);
}
Основна задача при резидентному завершенні програм — запит достатнього числа параграфів пам'яті. Якщо в резидентний блок не уміститься весь код і дані, необхідні для роботи TSR, завантаження наступних програм неодмінно "зіпсують" або обробник переривання, або його дані. Занадто великий "запас" також неприпустимий — резидентний блок зменшує розмір доступної для MS-DOS пам'яті. Найпростіше рішення — вибрати розмір блоку "на око". Звичайно неприємностей не буває, якщо розділити розмір. Ехе-файлу, що містить TSR, на 8. Фактично — це запит блоку, розмір якого вдвічі перевищує весь код TSR, включаючи і програму ініціалізації, зберігати яку резидентно не потрібно. На жаль, прийнятий спосіб формування завантажувального модуля в Turbo С не дозволяє досить просто виключити пам'ять, займану програмою інсталяції.
Більш точним буде метод визначення розміру резидентного блоку пам'яті виходячи зі структури завантажувального модуля відповідно до моделі пам'яті. Для всіх моделей пам'яті стек розташовується після секції коду і статичних даних. У момент звертання до функції keep(), якщо вона розташована в останньому рядку ініціалізуючого коду, поточна вершина стеку SS:SP приблизно відповідає границі програми. Якщо перевести цю фізичну адресу в параграфи і відняти від неї сегмент, по якому починається PSP, вийде досить точний розмір резидентної програми. Правда, для моделей пам'яті TINY, SMALL і MEDIUM у цей розмір включається і near-"купа", що дуже сильно завищує розмір резидентного блоку. Але Turbo С дозволяє встановити граничний розмір пам'яті, займаний near-"купою". Для цього використовується зовнішня змінна _heaplen: її значення встановлює граничний розмір "купи". Значення 0 відповідає "купі" максимально можливої довжини. Для інших моделей пам'яті, що не мають near-"купи", _heaplen не використовується. І, нарешті, зовнішня змінна _stklen дозволяє установити розмір стека, якій використовується програмою в процесі виконання. З врахуванням сказаного будуть зрозумілі зміни, що варто було б зробити в програмі L2_2.C:
/* L2_7.C */
/* Інсталятор резидентної програми,
що використовує переривання 5. */
#include <dos.h>
void interrupt (* old_int5) (void);
void interrupt ISR_int5(void); /* власна ISR переривання 5; найпростіший варіант
- див. L2_3.C */
void tsr(char status, unsigned size); /* прототип функції резидентного
завершення - L2_5.C */
extern unsigned _heaplen = 1024; /* завдання розміру-купи" */
extern unsigned _stklen = 512; /* завдання розміру стека */
int main(void) {
unsigned size;
old_int5 = gеtvесt(5); /* зберігаємо
"старий" вектор 5 */
setvect(5, ISR_int5); /* записуємо
новий вектор 5 */
size = _SS * ( _SP + 15 ) / 16 - _psp;
tsr(0, size);
}
Як відзначено раніше, однією зі специфічних задач програми ініціалізації є визначення того, чи залишена в пам'яті резидентна частина TSR-програми. Якщо не виконувати таку перевірку, при повторному запуску, у пам'яті з'явиться другий екземпляр резидентної частини TSR, потім третій, четвертий і т.д., поки не буде вичерпана вся доступна пам'ять. Рішення задачі складається у виконанні ініціалізуючою частиною TSR перевірки того, чи була встановлена раніше резидентна програма і чи працездатна вона. Можливі три основних методи реалізації сформульованої задачі:
1) сканування ланцюжка МСВ і визначення того, чи є в пам'яті блок, "хазяїном" якого є TSR з відомим ім'ям і (або) маршрутом розташування у файловій ієрархії;
2) використання спеціальних сигнатур, поміщених у ISR;
3) використання "луни" (еха) у ланцюжку ISR.
Перший з методів дозволяє знайти наявність у пам'яті резидентної частини TSR, але не дає відповіді на питання, чи працездатна вона.
Ідея другого методу полягає в наступному. У ISR у заздалегідь відомому місці міститься ідентифікуючий код. Зсув sign_offs до сигнатури визначається переглядом машинного коду в відладнику. Точкою відліку для зсуву буде служити вектор переривання, за який "чіпляється" ISR. Процедура перевірки полягає в наступному:
1) з таблиці зчитується вектор переривання ISR, яка виконується;
2) до зсуву додається sign_offs;
3) формується far-покажчик, сегмент якого дорівнює сегменту вектора переривання, а зсув обчислений на кроці 2;
4) перевіряється, чи збігаються байти в пам'яті по обчисленому покажчику з байтами рядка сигнатури. Якщо збіг повний, це означає, що в даний момент за вектор переривання "зачеплена" ISR і повторну установку виконувати не слід. Розбіжність свідчить про те, що за вектор переривання "зачеплена" "стара" ISR або "нова" ISR була "перечеплена". У неоднозначності і криється основний недолік методу— він не здатний визначити, чи встановлена вже власна ISR, якщо вона не перша в каскаді.
Третій метод — метод "луни" — врятований від цього недоліку. Його ідея проста. В одну з власних ISR при розробці включається додаткова секція — так званий "фільтр". Ця ISR підключається обов'язково за каскадною схемою. Одержавши керування, "фільтр" порівнює поточні значення внутрішніх регістрів процесора з "луно"-константами. Якщо збіг знайдений, "фільтр" змінює на заздалегідь відомі значення деяких регістрів або прапорів (так званий "луно"-повернення). Для перевірки наявності в пам'яті "резидента" програма ініціалізації активізує обробник переривання, передаючи йому "луно"-константи. "Фільтр" встановлює в регістри і(або) прапори "луно"-повернення і викликає "стару" ISR каскаду обробників. Якщо всі правильно побудовані ISR зберігають незмінними регістри, то "луно"-повернення прийде в ініціалізатор незміненим всіма обробниками ланцюжка. Якщо ISR відсутня у ланцюжку обробників, програма ініціалізації не знайде в регістрах "луно"-повернення і буде думати, що власна ISR ще не встановлена. Метод працює для власних ISR, "перечеплених" іншими резидентними програмами як завгодно далеко від початку каскаду. Єдиний недолік методу — імовірність того, що значення "луно"-констант можуть збігатися зі значеннями регістрів, що означають для інших ISR виконання тих або інших дій. Однак, вибравши "екзотичну" комбінацію для "луно"-констант, цю імовірність можна практично звести до нуля. При виборі "луно"-повернення не слід змінювати значення апаратних регістрів, за винятком АХ, тому що це чревате непередбаченими наслідками.
Метод "луни" знайшов найбільше поширення серед професійних резидентних програм. Для розміщення "фільтра" найбільш часто використовуються:
1) BIOS ISR керування апаратурою, наприклад переривань 10h, 13h, 17h і ін. У якості "луно"-константи вибирається значення, що не збігається з жодним з номерів функцій BIOSa даного переривання. Наприклад, для переривання 13h (Керування диском) будь-яке значення АН, більше 1Bh, може надійно використовуватися для цих цілей. Недолік методу складається в значному зменшенні системної продуктивності, тому що каскади часто викликуваних обробників переривань подовжуються;
2) ISR переривання 2Fh, названого MS-DOS-мультиплексним перериванням. Каскад обробників цього переривання є стандартним засобом, що рекомендується MS-DOS для взаємодії з резидентними програмами, у тому числі і тими, що входять до складу MS-DOS: APPEND, PRINT, ASSIGN, SHARE. Значення, встановлене в регістрі АН, використовується MS-DOS для індикації резидентної програми, якій адресується запит. Значення, встановлене в AL, задає код виконуваної команди.
Обробник переривання 2Fh будується по каскаднії схемі і при одержанні керування аналізує значення в регістрі АН. Якщо воно дорівнює закріпленому за користувальницькою ISR значенню, виконується запитана команда. Для визначення того, чи інстальована вже резидентна порція TSR, використовується команда AL=0. Правила побудови "фільтрів" переривання 2Fh такі, що, одержавши таку команду, "фільтр" повідомляє про стан резидентної програми. Звідси випливає алгоритм визначення наявності в пам'яті власної резидентної програми:
1) ініціалізуюча частина TSR встановлює АН рівним номеру, обраному для ISR (у діапазоні C0h— FFh), a AL — нулю і видає переривання 2Fh;
2) на виході з переривання 2Fh аналізується значення регістра AL: якщо воно не змінилося, TSR не встановлене; якщо AL=FFh, резидентна програма вже встановлена і повторна установка не потрібна.
Табл. 2.1. Мультиплексне
переривання (INT 2Fh)
Значення |
Опис |
На вході в переривання |
|
АН |
Індикатор викликаної резидентної програми |
01h 02h 03h 0Fh 10h 11h 42h 43h 44h B6h B7h B8h BFh COh FFh |
Резидентна частина команди PRINT MS-DOS Резидентна частина команди ASSIGN MS-DOS Зарезервоване MS-DOS (недоступно для використання) Зарезервовано MS-DOS (недоступно для використання)
Резидентна частина команди SHARE MS-DOS Зарезервоване MS-DOS (недоступно для
використання) Зарезервовано MS-DOS (недоступно для використання) Драйвер
розширеної пам'яті (XMS-драйвер) Зарезервоване MS-DOS (недоступно для використання) Зарезервовано MS-DOS (недоступно для використання)
Резидентна частина команди APPEND MS-DOS Зарезервоване MS-DOS (недоступно для
використання) Зарезервовано MS-DOS (недоступно для використання)
Доступно для використання резидентної програмою Доступно для використання резидентної програмою |
AL |
Команда, передана резидентній програмі |
00h 10h |
Стан резидентної програми Запит адреси точки входу у функції XMS-драйвера (якщо
AH-43h) |
На виході з переривання |
|
AX |
Код помилки для резидентних програм MS-DOS, якщо встановлено
прапор переносу CF |
AL |
Стан резидентної програми |
OOh 0lh FFh |
Програма не встановлена резидентно і її можна встановлювати. Програма не встановлена резидентно і її не можна встановлювати. Програма встановлена резидентно |
Перевагою розглянутого методу є те, що не задіюється додатковий вектор переривання для використання "фільтром". Інша перевага методу полягає в тому, що MS-DOS генерує переривання 2Fh при виконанні багатьох внутрішніх функцій, наприклад при зміні букви поточного накопичувача, і, якщо резидентні програми MS-DOS не встановлені, власна резидентна програма буде одержувати керування.
У даній главі використовується метод "луни", а програма "фільтр" включається в каскад обробників переривання 2Fh. Програмі привласнений номер F0h і, отже, "луно"-константі — значення в регістрі AX = F000h. "Луно"-поверненням буде значення в регістрі АХ=F0FFh. Обробник, що включається в каскад переривання 2Fh, будується за особливими правилами, дотримання яких неможливо при програмуванні на Сі. Тому далі приводиться приклад ISR, що включається в каскад переривання 2Fh, написаний на асемблері:
; L2_8.ASM
; ISR, що включається в каскад обробників переривання 2Fh.
; Використовується для запобігання повторної установки
; резидентної програми в пам'яті.
_ТЕХТ
SEGMENT BYTE PUBLIC 'CODE' DGROUP GROUP _DATA _TEXT ENDS
PUBLIC
_ISR_int2F ; робить ідентифікатор видимим із Сі. MODEL SMALL
.CODE
_ISR_int2F PROC FAR
cmp ax, F000h чи викликана резидентна програма?
jne chain_2F немає; перехід на "старий" обробник
xor а1, 0FFh установка "луно"-повернення
іret вихід
; виклик наступний ISR каскаду машинною командою ; безумовного переходу JMP
FAR. Сама команда "дост-; раивается" програмою інсталяції. chain_2F:
db OEAh ; код команди JMP FAR
_old_vector2F
dd ? ; місце для адреси "старої" ISR
; зсув до
цієї крапки
; дорівнює
9 байт _ISRJnt2F ENOP END
Занесення "старого" вектора переривання 2Fh old_vector2F у кодовий сегмент виконує програма інсталяції, використовуючи функцію movedata(). Для цього в відладнику необхідно визначити зсув до мітки _old_vector2F. При компіляції приведеного приклада з використанням моделі пам'яті SMALL отримане значення 9.
Далі приводиться функція isInstalled(), що реалізує розглянутий метод. Функція повертає YES (1), якщо отримано "луно"-повернення і NO (0) у протилежному випадку.
/* L2_9.C */
/* Визначення того, чи є присутнім в каскаді ISR переривання 2Fh
"фільтр", що реагує на значення в AX=0xF000 установкою AL=0xFF. */
#include <dos.h>
#define YES 1
#define NO 0
int is_installed() {
union REGS r;
/* Установка
"луно"-константи. */
r.x.ax = 0xF000;
int86(0x2F, &r, &r);
/* Аналіз "луно"-повернення
*/
if( r.h.al == 0xFF)
return YES; /* резидентна частина
знаходиться в пам'яті */
else
return NO; /* резидентна частина
відсутня */
}
Тепер можна написати цілком професійну програму інсталяції. "Правила гарного тону" вимагають виведення повідомлення про установку резидентної програми і дати коротку інструкцію про використання. Якщо інсталятор виявляє, що TSR уже встановлено, видається повідомлення про це і повторюється виведення інструкції про використання. Для розглянутої програми копіювання вікна екрана виведення на екран інструкції про використання буде виконувати функція print_help().
/* L2_10.C */
/* Виводить інструкцію про використання резидентної програми. */
#include <io.h>
void print_help(void) {
int
index;
char *icons[ ] = { "PrnScr",
"Left, Right, Up, Down",
"Ctrl+Left,
+Right, +PgUp, +PgDn", "S (Save)",
"Esc",
"Q (Quit)", 0};
char *text[ ] = { " - активізація
програми; \n",
" - керування лівим
верхнім кутом вікна, яке копіюється; \n",
" - керування правим
нижнім кутом вікна, яке копіюється; \n",
" - запис тексту вікна у
файл на диску; \n",
" - вихід без збереження
копії вікна; \n",
" - видалення програми з
пам'яті \n"};
printf("Резидентна програма
копіювання текстового вікна екрана. \n\n" );
for( index = 0; icons[ index ];
index++) {
write(1, icons[ index ], strlen( icons[
index ] ) );
write(1, text[ index ], strlen( text[
index ] ) );
}
}
Далі приводяться текст інсталятора, що перевіряє методом "луни" наявність у пам'яті резидентної порції TSR. "Фільтр" встановлюється в каскад переривання 2Fh. Приклад, що приводиться, є доробкою лістинга L2_7.C: доданий виклик функції is_installed(), а також установка в каскад обробників переривання 2Fh "фільтру", що виконує "луно"-повернення.
/* L2_11.C */
/* Інсталятор резидентної програми, що використовує переривання 5. */
#include <dos.h>
#include <stdio.h>
#define YES 1
#define NO 0
void interrupt (* old_int5)(void);
void interrupt (* old_int2F)(void);
void interrupt ISR_int5(void); /* власна ISR переривання 5, найпростіший варіант
- див. L2_3.C */
void interrupt ISR_int2F(void); /* власна ISR переривання
void tsr(char status, unsigned size); /* функція резидентного завершення - див.
L2_6.C */
int is_installed(void); /* перевірка, чи є в каскаді ISR переривання 2Fh власна
ISR, - див. L2_9.C */
void print_help(void); /* виведення інструкції про TSR -див. L2_10.C */
/* завдання розміру "купи" */
extern unsigned _heaplen = 1024;
/* завдання розміру стека */
extern unsigned _stklen= 512;
int main( int argc, char **argv ) {
unsigned size;
/*
Запобігання повторної установки TSR */
if(is_installed() == YES) {
printf( "\n\a Програма %s вже
встановлена \n\n", argv[ 0 ]);
print_help();
return 1;
}
/* Включення в каскад ISR переривання 5
ISR_int5() */
old_int5 = getvect(5);
setvect(5, ISR_int5);
/* Збереження "старого"
вектора 2Fh у сегменті даних */
old_int2F = getvect(0x2F);
/* Перенесення "старого" вектора 2Fh у кодовий сегмент ISR_int2F() */
movedata( _DS, &old_int2F, _CS,
FP_OFF( ISR_int2F ) * 9, 4);
/* Включення в каскад ISR переривання 2Fh ISR_int2F() */
setvect(Ox2F, ISR_int2F);
/* Резидентне завершення програми */
size = _SS * (_SP + 15) / 16 - _psp;
print_help();
tsr(0, size);
Для одержання завантажувального модуля TSR-програми варто сформувати файл проекту і виконати багатофайлову компіляцію. Файл проекту повинний включати наступні файли: L2_3.C, L2_4.C, L2_6.C, L2_8.ASM, L2_9.C, L2_10.C, L2_11.C і файл L9_.9.C, що містить текст функції hor_prn(). Після завершення компіляції і компонування буде отримана програма, при запуску якої на виконання в пам'яті комп'ютера залишається резидентна порція, активізуєма при натисканні клавіші PrnScr. Спроба повторної установки приведе до видачі на екран повідомлення "Програма... вже встановлена", супроводжуваного звуковим сигналом і повторенням інструкції про використання програми. Ця інструкція приведена "на виріст" для вже завершеного приклада, а поки TSR просто позначає свою присутність у пам'яті виведенням рядка тексту "Центр" в центрі екрана у всіх текстових режимах.
Резидентна частина TSR — це одна або кілька функцій і взаємодіючих обробників переривань. Далі для економії місця абревіатура TSR використовується для позначення тільки резидентної частини TSR. Природне бажання програміста для розробки резидентних функцій і ISR використовувати мову Сі і всі звичні засоби бібліотеки. Реалізація такого підходу на практиці можлива, але вимагає дотримання при побудові TSR ряду додаткових умов, докладно розглянутих далі. Найбільш доцільно, на думку автора, використовувати таку побудову TSR:
1) обробники переривань одержують керування й аналізують необхідність і можливість активізації TSR;
2) якщо активізація можлива, викликається спеціальна функція, наприклад TSR_prepare_activate(), що виконує підготовчі дії, обумовлені непристосованістю функцій бібліотеки Turbo С до резидентної роботи;
3) після завершення всіх підготовчих операцій викликається звичайна Сі-функція, написана без дотримання яких-небудь особливих умов і, можливо, використана раніше в інших програмах. Назвемо її умовно TSR_body().
В ідеалі, одержавши керування, TSR повинна зробити усе для того, щоб MS-DOS сприйняла її як звичайну поточну програму. Перед своїм завершенням TSR повинна чисто "прибрати" за собою. Далі програму, що перериває TSR, будемо називати фоновою. Додаткові дії TSR після одержання керування у великій мірі залежать від того, які функції виконуються резидентно: чи використовується доступ до файлів, чи виконується виведення на екран і введення інформації з клавіатури і т.д.
Програма при виконанні обов'язково має потребу в стеці для збереження адрес точок повернення, передачі параметрів і ін. Коли ISR одержує керування, регістри SS і SP процесора вказують на поточну вершину стека фонової програми. Якщо залишити їх без зміни, TSR буде "паразитувати" на стеці фонової програми. Це небезпечно, тому що перервана програма розраховує розмір стека тільки для власних потреб. Оскільки розглянута в даній главі TSR складається мовою Сі, компілятор якого генерує код з активним використанням стека, досить ймовірна ситуація перевантаження стека перерваної програми. От чому першою обов'язковою вимогою до TSR_prepare_activate() є переключення стека. Сі-подібний код, що приводиться далі, ілюструє дії по переключенню стека. Як і колись, передбачається багатофайлова компіляція програми; інсталююча частина TSR розміщається у файлі MAIN.С, а текст функції TSR_prepare_activate() — у файлі TSR_ACT.C.
/* Файл MAIN.С */
unsigned TSR_ss, TSR_sp; /* значення SS, SP для роботи TSR */
unsigned TSR_stack[512]; /* використовується під стек TSR */
unsigned old_ss, old_sp; /* "старі" SS, SP при вході в TSR */
main() {
...
TSR_ss = FP_SEG( (unsigned far* ) TSR_stack
);
TSR_sp = FP_OFF( TSR_stack ) * 1024; /*
стек росте
...
}
/* Файл TSR_ACT.C */
extern unsigned TSR_ss, TSR_sp, old_ss, old_sp;
void TSR_prepare_activate(void) {
/* Переключення стека. Це перше, що варто зробити */
old_ss = _SS;
old_sp = _SP;
disable(); /* установку нових
SS, SP не переривати */
_SS = TSR_ss;
_SP = TSR_sp;
enable();
TSR_body(); /* звичайна Сі-функція */
/*Зворотне переключення стека */
disable(); /* установку нових
SS,SP не переривати */
_SS = old_ss;
_SP = old_sp;
enable();
}
Установку нових значень SS і SP обов'язково виконують з маскованими зовнішніми перериваннями. У протилежному випадку не виключена імовірність того, що TSR буде перервана іншою резидентною програмою, що, можливо, користується чужим стеком. Але вона застане стек на "півшляху" до переключення. Місце під стек може бути зарезервоване в сегменті даних у виді статичного масиву, а може використовуватися той стек, що входить у завантажувальний модуль Сі-програми. Тому що при обчисленні розміру резидентного блоку ми виключили стек Turbo С, у прикладі під стек TSR-програми використовується масив у 512 слів ( unsigned TSR_stack[ 512 ] ).
Якщо виникає повторне входження в TSR до її завершення (тобто, якщо TSR може перериватися запуском самої себе), переключення стека на той самий масив неминуче приведе до помилки. При повторному входженні в стеці будуть "затерті" дані, що відносяться до першого, ще не завершеного входження. Для того, щоб уникнути такої помилки, використовується блокування повторного входження в TSR. Найпростіше рішення при організації блокування — використання бінарного семафора (спеціальні зовнішні змінні, приймаючі два значення: YES і NO).
Якщо interrupt-функція повертає значення в регістрах, вона описується як така, що має формальні параметри. У цьому випадку всі операції по доступу до них повинні виконуватися на непереключеному ("старому") стеці. Це випливає з того, що формальні параметри є посиланнями на копії регістрів, записані в поточний стек ще до того, як може бути виконане переключення стека в interrupt-функції.
Ідентифікатором активної програми для MS-DOS служить PID—сегмент, по якому в пам'яті розташовується PSP програми. У PSP зосереджена вся інформація, необхідна MS-DOS для керування пам'яттю і програмами. Для TSR, що використовує доступ до файлів, особливо важливою є таблиця відкритих файлів PSP. Зокрема, для розглянутого в даній главі приклада TSR потрібно виконувати запис текстового вікна у файл, а отже, файл повинний бути відкритий. Його префікс буде поміщений у PSP. Навіть якщо відкриття файлу виконає програма ініціалізації і залишить його відкритим, а префікс помістить у зовнішню змінну, це все рівно не дозволить ISR використовувати його! Адже в момент, коли TSR одержує керування, активною є вже інша програма, зі своїм PSP і своєю таблицею відкритих файлів. Використання того ж префікса викликає доступ до зовсім іншого файлу і може його просто зруйнувати. Альтернативне рішення — відкрити потрібний файл на вході в TSR, потім виконати перенесення даних, після чого знову закрити файл. Однак може статися так, що у фонової програми, на PSP якої TSR "паразитує", вже вичерпаний ліміт на префікси. Значення ліміту за замовчуванням дорівнює 20, але може бути збільшено звертанням до MS-DOS. Однак TSR не має права робити це без відома перерваної програми. Таким чином, надійність роботи TSR знижується.
Схожа проблема виникає і при вивантаженні резидентної програми з пам'яті, перехопленні переривань від натискання клавіш Ctrl-Break і критичної помилки MS-DOS (ці проблеми докладно обговорюються далі). Радикальне рішення — виконання переключення PSP. MS-DOS підтримує спеціальні функції, що дозволяють виконати переключення: функція АН=62h (GetPID) повертає в регістрі ВХ поточне значення PID; функція АН=50h (SetPID) встановлює в якості поточного новий PID, що задається в регістрі ВХ. Функція АН=50h не документована, але у версіях MS-DOS 3.0 і старше працює надійно. Відзначимо, що функції GetPID і SetPID можна виконувати тільки тоді, коли фонова програма — не MS-DOS. Спосіб визначення цієї обставини розглядається в п. 2.3.3.
Сі-подібний код, що приводиться далі, ілюструє дії, виконувані по переключенню PSP.
/* Файл MAIN.С */
unsigned old_PSP; /* зберігає PID фонової програми */
...
main() {
...
}
/* Файл TSR_ACT.C */
extern unsigned _psp, o1d_PSP;
void TSR_prepare_activate(void) {
/*
Переключення поточного PSP на PSP резидентної програми */
_АН = 0х62;
geninterrupt(0x21);
old_PSP = _BX;
_AH = 0х50;
_BX = _psp;
geninterrupt(0x21);
TSR_body(); /* звичайна Сі-функція */
/* Зворотне переключення PSP на PSP фонової програми */
_АН = 0х50;
_ВХ = old_PSP;
geninterrupt(0x21);
}
Найпростіше блокувати повторне входження, використовуючи зовнішню змінну-семафор (busy), що приймає всього два значення, наприклад YES (0) і NO (1). Перед можливим входженням у TSR перевіряється прапор busy, і, якщо він дорівнює, наприклад, YES, входження в резидентну програму не виконується. У протилежному випадку прапор busy встановлюється в YES і викликається TSR.
Наступна обов'язкова вимога до надійно працюючої TSR — запобігання повторного входження в MS-DOS. Однопрограмна MS-DOS не є реентерабельною. Реентерабельна програма — це програма, що дозволяє, у силу особливостей своєї побудови, починати її виконання кілька разів, не чекаючи завершення виконання (виходу) програми, початого раніше. Реентерабельна програма не змінює ні однієї константи або змінної, котрі можуть вплинути на повторне виконання програми. Більшість програм, що утворюють у сукупності ядро MS-DOS, не є реентерабельними (принаймні, для версій MS-DOS молодше 5.0). У цьому зв'язку не є реентерабельними і програми, що звертаються до функцій MS-DOS безпосередньо або через функції бібліотеки Turbo С. Для TSR, написаної мовою Сі з використанням бібліотечних функцій, завжди існує імовірність повторного входження в MS-DOS, тому що TSR може одержати керування в будь-який момент, у тому числі і тоді, коли MS-DOS виконує нереентерабельну секцію свого коду. Звідси випливає вимога до ISR активізувати TSR тільки тоді, коли MS-DOS дозволяє повторне входження. Для цього існують дві основні можливості:
1) використання переривання 28h;
2) використання прапору повторного входження в MS-DOS.
Механізм переривання 28h, що часто називають "таємницею переривання 28h", такий. Як відомо, MS-DOS має утиліти "фонового" або псевдопаралельного виконання. Наприклад, PRINT.COM дозволяє друкувати файли і виконувати іншу програму. Утиліти рівнобіжного виконання перехоплюють "переривання" 28h. Для того щоб рівнобіжна програма періодично одержувала керування, MS-DOS у ті моменти, коли вона виконує цикл чекання введення з клавіатури і перед виконанням функцій з номерами від 0h до 0Сh, видає переривання 28h.
Якщо включити власну ISR у каскад обробників переривання 28h, вона буде періодично одержувати керування й аналізувати необхідність активізації TSR, наприклад, по стану прапора, яким керує ще одна ISR. Цінність такого підходу до побудови TSR саме в тому, що якщо викликається переривання 28h, гарантується, що MS-DOS знаходиться в безпечному для переривання (а отже, і для можливого повторного входження) стані. Можна впевнено активізувати TSR, якщо в цьому виникає необхідність. Єдине обмеження, що накладається на TSR, що одержує керування з переривання 28h, складається в забороні на використання функцій MS-DOS від 00h до 0Ch. Це викликає повторне входження в ISR переривання 28h, і, якщо код ISR нереентерабельний, система "висне". Однак бінарний семафор на самому початку ISR (див. далі) легко вирішує цю проблему.
Інший спосіб запобігання повторного входження в MS-DOS побудований на використанні прапора повторного входу MS-DOS. Адресу цього прапора повертає в ES:BX недокументована функція АН=34h MS-DOS. Якщо MS-DOS знаходиться в нереентерабельній секції свого коду, прапор повторного входу дорівнює 1. Якщо ж MS-DOS виконує реентерабельну секцію свого коду, прапор дорівнює 0 і резидентна програма може безпечно активізуватися і використовувати у своїй роботі усі функції операційної системи. Далі приводиться фрагмент коду, що пояснює визначення і використання прапора повторного входу в MS-DOS.
/* Файл MAIN.С */
char far *inside_DOS_ptr; /* Адреса прапору повторного входу MS-DOS */
maіn() {
_АН = 0х34;
geninterrupt(0x21);
inside_DOS_ptr = MK_FP(_ES, _BX);
/* У файлах ISR, що виконують перевірку прапора
повторного входу в MS-DOS */
if( *inside_DOS_ptr == 1) {
/* Дії у випадку, коли заборонена
активізація TSR */
…
}
else {
/* Дії у випадку, коли можлива
активізація TSR, функції, що використовує у своїй роботі, MS-DOS */
…
}
…
Недолік цього підходу полягає в тім, що рішення не активізувати TSR приймається незалежно від того, яку зі своїх функцій виконує MS-DOS. Якщо прапор повторного входу дорівнює 1, але MS-DOS зайнята виконанням функцій консольного виводу 01-0Ch, інші функції, зв'язані з доступом до файлів, можуть використовуватися абсолютно безпечно. Тому даний підхід можна підсилити, якщо поставити ISR-фільтр на переривання 21h. Його основна задача— відбивати в зовнішніх змінних-прапорах поточний стан MS-DOS. Наприклад, працювати з прапорами inside_01_0C і insude_0D_FF, що показують, чи виконує MS-DOS у момент апаратного переривання функції 01-0С, зв'язані з консольним виводом, або які-небудь інші функції. Якщо виконується консольний вивід, можна безпечно здійснювати доступ до файлів, і навпаки. Детальний аналіз цього прапора дозволяє зменшити затримку між настанням моменту активізації TSR і її фактичною активізацією. Якщо покладатися тільки на значення прапора повторного входу в MS-DOS (*inside_DOS_ptr), час чекання може стати неприпустимо великим. Крім того, тому що прапор повторного входу— недокументована можливість MS-DOS, фірма Microsoft при розробці чергової версії операційної системи може змінити спосіб звертання до нього або навіть значення самого прапора. Відзначимо, що складання програми-фільтра переривання 21h мовою Сі неефективно. Ряд специфічних труднощів роботи з перериванням (можливе руйнування регістрів SS, SP, DS, висока імовірність повторного входу в код і ін.) змушують вибрати асемблер. Прапори в цьому випадку варто поміщати в кодовому сегменті "нового" обробника, а це ускладнює редагування зв'язків з іншими модулями програми і робить тексти важкими для сприйняття.
У випадку, коли обробник переривання виявляє необхідність активізації TSR, але MS-DOS зайнята, виникає "відкладений" запит активізації. Такий запит при побудові багатьох TSR варто обробити якнайшвидше, інакше наступний запит "затре" його. Універсальне рішення— використовувати фільтри часто викликуваних переривань: таймера, звертання до диска й інших для того, щоб збільшити число можливостей для активізації TSR. У розглянутому прикладі резидентної програми буде використовуватися фільтр переривання 1Ch. В результаті TSR будується як декілька рівнобіжних ISR, взаємодіючих через зовнішні змінні: ISR переривання 5, ISR переривання 28h, ISR переривання 1Ch і ін. Найбільш проста і надійна схема побудови TSR приведена на мал. 2.4.
ISR, що активізується (у розглянутому прикладі це ISR переривання 5), виконує тільки установку прапора, що керує активізацією TSR. Наприклад, нехай прапор має ім'я request. Всю іншу роботу, покладену на TSR, виконує обробник переривання 28h. Він перевіряє значення прапора і, якщо request = YES, ISR аналізує можливість активізації. Перешкодами для цього можуть бути:
1) та обставина, що MS-DOS знаходиться в критичній секції свого коду;
2) раніше почате і незавершене виконання функцій TSR (незавершене входження в TSR).
Рис. 2.4.
Спрощена схема побудови TSR (активізуюче переривання 5, робоче переривання
28h)
Якщо MS-DOS активізує переривання 28h, це свідчить про те, що ядро операційної системи в реентерабельному стані і додаткової перевірки вмісту байта пам'яті за адресою inside_DOS_ptr не потрібно, функція TSR_prepare_activate виконує всі підготовчі дії і викликає основну функцію. Нею може бути практично будь-яка Сі-функція, у тому числі і функції доступу до файлів на диску.
Будемо вважати далі, що ця функція має ім'я TSR_body(). (Відзначимо однак, що розглянуті раніше дії по переключенню стека і PSP ще не гарантують безпечного використання будь-яких Сі-функцій, а вимагають інших дій, описаних далі: переключення області переносу диска, перехоплення переривань критичної помилки MS-DOS і натискання клавіш Ctrl-Break). Резидентна робота функції вимагає "ретельної" роботи з екраном: якщо параметри відеосистеми (режим, позиція курсору, активна відеосторінка) будуть змінюватися, то потрібно їхнє відновлення перед завершенням функції TSR_body(). Якщо виконується виведення на екран, у тому числі і "луна" введення з клавіатури, що змінить вміст відеобуфера, повинний зберігатися всередині TSR, а перед завершенням TSR_body() відновлюватися.
За подібною схемою взаємодії "активізуючого" і "робітника" переривань будуються популярні TSR. У якості активізаторів можуть виступати апаратні переривання (найбільш часто—переривання 9 від клавіатури), або програмні переривання. В останньому випадку фоновою буде програма, що виконала інструкцію INT. Побудова таких TSR набагато простіша, тому що фонова програма сама може виконати необхідні підготовчі дії по активізації TSR, а поводження TSR не є для фонової програми несподіваним.
Далі приводиться текст ISR переривання 28h, що працює за схемою, приведеною на мал. 2.4.
/* L2_12.C
Фільтр переривання 28h. Якщо є
"відкладена" спроба активізації TSR, він викликає TSR_prepare_activate() */
#define NO 0
#define YES 1
#define OK 0
# include <dos.h>
extern void interrupt (*old_int28)(void);
extern unsigned busy, request;
int TSR_prepare_activate(void);
void interrupt ISR_int28(void) {
old_int28(); /* виклик
"старої" ISR переривання 28h */
disable(); /* маскування апаратних
переривань */
if( busy = = NO && request = =
YES) {
/* активізація TSR*/
busy = YES;
if( TSR_prepare_activate() = = OK)
request = NO;
busy = NO;
}
return;
}
Якщо резидентній програмі потрібен обмін з диском, необхідно почати додаткові заходи безпеки. Вони пов'язані, по-перше, з визначенням моменту, коли безпечно можуть виконуватися функції файлової системи MS-DOS і бібліотечні функції, що спираються на них, Turbo С, по-друге, зі створенням "звичного" для файлових функцій середовища.
Звертання до диска варто виконувати в тих випадках, коли прапор повторного входження в MS-DOS не дорівнює 0 та не виконується яка-небудь з функцій переривання 13h. Функції цього переривання керують роботою дискової підсистеми на фізичному рівні. Нажаль, відсутній який-небудь системний засіб визначення того, чи активне в даний момент переривання 13h. Єдина можливість — використати фільтр переривання 13h. Основна задача фільтра — відображати в зовнішній змінний inside_BIOS поточний стан: при вході в каскад ISR переривання 13h встановлювати прапор у стан YES, на виході—NO. Значення прапора варто перевіряти на додаток до прапора повторного входу в MS-DOS і не активізувати TSR до завершення переривання 13h. Встановлений фільтр повинний "прозоро пропускати" можливі значення, що повертаються "старим" обробником 13h. Значення, що повертаються, завжди містяться в регістрі АХ і прапорах. Далі приводиться приклад ISR переривання 13h.
/* L2_13.C
Фільтр переривання 13h BIOS - керування диском. Встановлює в значення YES
зовнішню змінну inside_BIOS, коли активне переривання 13h. */
#define NO 0
#define YES 1
#include <dos.h>
extern unsigned inside_BIOS;
extern void interrupt (*old_intl3)(void);
void interrupt ISR_intl3(unsigned bp, unsigned di, unsigned sі, unsigned ds,
unsigned es, unsigned dx, unsigned ex, unsigned bx, unsigned out_ax, unsigned
ip, unsigned cs, unsigned out_flags) {
inside_BIOS = YES;
old_intl3();
disable(); /*при виході з ISR прапори
відновлюються зі стека */
out_ax = АХ; /* передача інформації надходить з BIOS у
внутрішніх регістрах */
asm {
pushf;
pop outflags;
}
inside_BIOS = NO;
}
У випадку, коли запис даних на диск із резидентної програми не може початися негайно, дані поміщають у внутрішній буфер. Тому що резидентна програма має досить обмежений обсяг пам'яті, приходиться використовувати додаткові засоби "прискорення" розвантаження буфера. Найбільш загальне рішення— використовувати часто викликувані прикладними програмами переривання, наприклад переривання від таймера.
Інша проблема — створити при доступі до файлів умови роботи MS-DOS і функцій Turbo С, цілком ідентичних тим, що мають місце при звичайній, нерезидентній роботі. Для функцій префіксного файлового доступу обов'язкове переключення PSP. Якщо резидентна програма використовує безпосередньо або через функції Turbo С FCB-функції MS-DOS, додатково потрібно переключення області перенесення даних (DTA—Data Transfer Area). З DTA працюють функції перегляду директорія findfirst(), findnext() і деякі інші. MS-DOS підтримує дві спеціальні функції для спрощення роботи з переключення DTA: MS-DOS-функція АН = 2Fh повертає в ES:BX far-покажчик на початок встановленої в поточний момент DTA; MS-DOS-функція АН=21h встановлює нову адресу DTA, що задається парою регістрів DS:DX. На цих MS-DOS-функціях базуються функції бібліотеки Turbo С – getdta() і setdta().
Фрагмент Сі-подібного тексту, що приводиться далі, ілюструє послідовність дій по переключенню DTA, яка виконуються у випадку, коли TSR працює з дисками:
/* Файл MAIN.С */
char far *old_DTA; /* адреса "старої" DTA */
char far *TC_DTA; /* адреса DТА, яка використовується Turbo С */
main() {
ТС_DТА = getdta(); /* зберігаємо адресу
DТА Turbo С */
...
}
/* Файл TSR_ACT.C */
extern char far * old_DTA;
extern char far * TC_DTA;
void TSR_prepare_activate(void) {
old_DTA = getdta(); /*
збереження поточного DТА */
setdta(TC_DTA); /* переключення на DTA
Turbo С */
TSR_body(); /* звичайна Сі-функція */
setdta(old_DTA) /* зворотне
переключення DTA */
}
Одним з ефективних рішень при побудові TSR є свопінг. Свопінг — це перенос програми з диска в пам'ять, коли програма одержує керування, і зворотне перенесення даних і коду, коли минає час, відведений для роботи програми. Свопінг широко використовується в мультипрограмних операційних системах і дозволяє за рахунок активного використання диска організувати рівнобіжне виконання багатьох програм навіть при невеликих обсягах пам'яті. Резидентні програми — це форма пасивного паралелізму. Стосовно до TSR свопінг означає наступне. У пам'яті розміщається не вся TSR, а лише її мінімальне ядро — обробники переривань. Визначивши необхідність і можливість активізації, ядро "довантажує" у пам'ять відсутню частину коду з диска. У випадку, коли для коду, що завантажується, не вистачає пам'яті, будь-яка частина її вмісту копіюється на диск і на її місці міститься код TSR. Завжди існує імовірність того, що відвантажена на диск пам'ять — це фрагмент якого-небудь обробника переривань, код якого може стати потрібним системі. Для того, щоб цього не відбувалося, перед свопінгом TSR з диска маскуються апаратні переривання. Після завершення TSR відновлює пам'ять.
При виникненні помилки, зареєстрованої засобами MS-DOS, остання передає керування ISR переривання 24h. Правильно спроектована TSR повинна підмінювати перед активізацією цей вектор на адресу власної функції і відновлювати вектор перед виходом з TSR. Іншими словами, коли TSR активна, обов'язково діє власний обробник критичної помилки, навіть якщо він не виконує ніяких дій.
При побудові власного обробника переривання 24h каскадне включення не використовується. Вся обробка відповіді користувача (наприклад, на помилку неготовності диска — Abort, Retry, Ignore, Fail) повинна виконуватися TSR-програмою.
Сі-подібний код, що приводиться далі, ілюструє роботу з перехоплення переривання критичної помилки:
/* Файл MAIN.С */
void interrupt ( *old_int24 )( void ); /* "стара" ISR 24h */
main() {
...
}
/* Файл TSR_ACT.C */
extern void interrupt (* old_int24)(void);
extern void interrupt (* ISR_int24)(void);
void TSR_prepare_activate(void) {
old_int24 = getvect(0x24);
/* збереження поточного вектора 24h */
setvect(0x24, ISR_int24); /* установка
власної ISR */
TSR_body(); /* звичайна Сі-функція */
setvect(0x24, old_int24); /* зворотне
переключення */
}
У найпростішому випадку ISR_24() просто ігнорує помилку або без якої-небудь додаткової обробки дає команду MS-DOS повернутися в прикладну програму. Нижче приводиться відповідний цьому текст функції:
/* L2_14.C
"Заглушка" переривання 24h - Критична помилка MS-DOS.
Передає АХ = 3 - Повернення в прикладну програму з індикацією помилки MS-DOS */
void interrupt ISR_int24(unsigned bp, unsigned di, unsigned si, unsigned ds,
unsigned es, unsigned dx, unsigned ex, unsigned bx, unsigned out_ax, unsigned
ip, unsigned cs, unsigned out_flags){
out_ax =3; /* передача інформації в
MS-DOS */
}
При необхідності ISR_int24 може сама проінтерпретувати код помилки (регістр AL) і можливі відповіді користувача на неї (АН), вивести відповідне запрошення і прийняти відповідь. Безпечними для TSR будуть відповіді Fail (out_ax = 3) і Retry (out_ax = 1). Передача в MS-DOS відповіді Abort (out_ax = 2) забороняється. У цьому випадку MS-DOS завершить TSR, що до цього не готова, тому що не відновлена таблиця векторів переривань.
При виявленні натискання комбінації клавіш Ctrl-Break під час виконання своїх функцій MS-DOS видає переривання 23h. За замовченням обробник цього переривання негайно завершує виконання активної програми. Тому натискання клавіш Ctrl-Break у той час, коли TSR активна, може викликати її завершення з невідновленою таблицею векторів переривання. Для того щоб уникнути цього, можна, як це було показано в попередньому пункті, перехоплювати переривання 23h на час активності TSR. Існує більш простий спосіб, що не вимагає перехоплення переривання. Перед активізацією резидентної програми реакція на натискання клавіш Ctrl-Break встановлюється в стан OFF. У результаті MS-DOS буде перевіряти наявність натискання клавіш Ctrl-Break тільки при виконанні функцій виводу, зв'язаних з файлами stdin, stdout і stdaux. Але якщо TSR не використовує ці операції, то і не відчувається натискання клавіш Ctrl-Break. Перед завершенням TSR реакція на натискання клавіш Ctrl-Break відновлюється. Для збереження (відновлення) реакції на натискання клавіш Ctrl-Break використовується функція АН=33h. Підфункція AL=0 запитує поточну реакцію, що повертається в DL (0 — OFF, 1 — ON); підфункція AL=1 встановлює реакцію, задану в DL. Ці підфункції мають аналоги в бібліотеці Turbo С getcbrk() і setcbrk().
Сі-подібний код, що приводиться далі, ілюструє установку і відновлення реакції на натискання клавіш Ctrl-Break.
/* Файл TSR_ACT.C */
int old_cbrk;
void TSR_prepare_activate(void) {
old_cbrk = getcbrk(); /*
збереження поточної реакції */
setcbrk(0); /* установка CTRL-BREAK =
OFF */
TSR_body(); /* звичайна Сі-функція */
setcbrk(old_cbrk); /* відновлення
реакції */
}
Персональний комп'ютер має службу реального часу, що утворює апаратний таймер, що генерує запит апаратного переривання в середньому 18,2 рази в секунду. Системний таймер підключається до лінії запитів IRQ0. Контролер переривань при одержанні запиту передає в процесор номер переривання 8. Це переривання обробляється ISR BIOS. Крім різного роду системної роботи, обробник виконує й інструкцію INT 1Ch, тобто комп'ютер 18,2 рази в секунду викликає ISR переривання 1Ch. Системний обробник цього переривання складається з єдиної інструкції повернення з переривання IRET. Іншими словами, це програмна "заглушка" для підключення власних ISR. Крім звичайних дій, прийнятих для обробників переривань (збереження (відновлення) регістрів, визначення необхідності і можливості активізації TSR, активізація TSR), ISR переривання 1Ch повинна видавати в програмувальний контролер переривань команду EOI. Найбільш загальне рішення цього питання складається в записі в порт 20h байта 20h (так зване загальне завершення).
Переривання від таймера при побудові резидентних програм використовується, в основному, у наступних випадках:
1) побудова TSR, активізуємих при настанні якої-небудь події в реальному часі. Окремий випадок таких програм — різного роду "годинник" на екрані або "записні книжки" з попередженням про настання тієї або іншої події. У багатьох випадках допомоги Сі (Help C) приводяться подібні приклади;
2) TSR, що реалізують "відкладені" по розуміннях безпеки запити активізації. Наприклад, приведена на мал. 2.4 схема побудови резидентної частини має один, не очевидний на перший погляд, недолік: переривання 28h генерується у багатьох додатках недостатньо часто для нормального функціонування резидентної програми.
У багатьох випадках зручно оснастити резидентну програму засобами "чистого" завершення з відновленням усіх використовуваних системних ресурсів і звільненням займаної пам'яті. Можливі дві різні схеми, що відрізняються способами інформування TSR про необхідність завершити своє існування. Перший з них вимагає повторного запуску TSR-програми на виконання зі спеціальним ключем у командному рядку. Функція main() перевіряє, чи встановлена вже TSR у пам'яті, і, якщо вона там є, викликає ISR, призначену для запобігання повторної установки, наприклад ISR переривання 2Fh (див. п. 2.2.3), зі спеціальним значенням у регістрі AL. "Фільтр" переривання 2Fh розпізнає команду на відвантаження "резидента" з пам'яті і виконує всі необхідні для цього дії. Інший спосіб — передача з клавіатури команди на відвантаження в момент, коли TSR активізована. В обох випадках дії TSR по відвантаженню з пам'яті ті самі.
Функція відвантаження TSR з пам'яті повинна обов'язково виконати:
1) відновлення таблиці векторів переривання, що усуває надалі можливість активізації TSR;
2) закриття усіх файлів програми;
3) звільнення пам'яті, займаної TSR;
4) повернення керування в перервану програму.
Коротко прокоментуємо особливості виконуваних кроків. При відновленні таблиці векторів використовуються "старі" вектори, збережені при запуску TSR. Але після цього могли бути запущені й інші резидентні програми ("загарбники"), що установили свої вектори і перемістили ISR з початку каскаду обробників. Відновлення вектора в такій ситуації вдало "втопить" всі резидентні програми, включені в каскад програми, яка раніше завершується. Але це тільки одна проблема. Якщо після цього "загарбник" сам завершиться, він може установити в таблицю векторів вектори вже відсутньої в пам'яті ISR. У результаті неминуче "зависання" системи. Тому, перш ніж відновлювати таблицю векторів, що вивантажується, TSR повинна перевірити, чи є вона першої в каскаді ISR, або вже "перечеплена" глибше по ланцюжку. Критерієм є збіг адреси точки входу в ISR зі значенням вектора переривання, записаним в поточний момент в таблиці. При розбіжності значень відновлення векторів і видалення TSR не виконується. Як варіант, у такій ситуації можна виконати "приспання" TSR — запис у додаткову зовнішню змінну заздалегідь встановленого значення. "Приспання" вимагає доробки ISR — введення додаткової перевірки зовнішнього прапора. Якщо він, наприклад, YES, ISR відразу викликає наступний обробник у каскаді, не виконуючи своїх звичайних функцій по визначенню моменту активізації TSR.
Якщо TSR є першою в ланцюжку обробників, проводиться очищення блоків пам'яті, зайнятих TSR. При цьому можливо виникнення фрагментації пам'яті, якщо після цього відвантажується TSR, встановлювалися інші TSR. Для звільнення пам'яті використовується MS-DOS-функція АН=49h або її Turbo С аналог freemem(). Нагадаємо, що Сі-програма володіє двома блоками пам'яті: блоком, у якому зберігаються резидентні код і дані, і блоком, у якому зберігаються рядки середовища програми.
Перший блок починається з параграфа _psp, другий — з параграфа, записаного по зсуві 2Ch у PSP.
Звичайно відкриті файли закриваються автоматично програмою-завантажником системи програмування на Сі при виконанні функцій завершення. Через те що відвантаження TSR виконується нестандартно, закриття файлів повинне бути виконане явно. У протилежному випадку не будуть звільнені елементи системної таблиці описів відкритих файлів. Не буде зайвим закрити не тільки відкриті регулярні файли на диску, але і файли стандартного введення-виведення з префіксами 0-4. Нагадаємо, що закриття файлів вимагає переключення PSP резидентної програми. Якщо це зроблено, можна використовувати функції бібліотеки Turbo С, зокрема close або _close().
Після того, як усі дії по відвантаженню виконані, TSR повертає керування у фонову програму. Це безпечний спосіб завершення. Не можна завершувати TSR функцією exit() навіть на переключеному PSP.
Ще одна рекомендація з відвантаження резидентної програми з пам'яті. Функція, що відвантажує TSR, викликається після виконання всіх кроків по підготовці звичної для Сі-функцій середовища: переключення PSP, області переносу DTA, встановленої реакції на натискання клавіш Ctrl-Break і т.д. Таким чином, функцію відвантаження варто викликати з меж функції TSR_prepare_activate(). Для сигналізації про необхідність відвантаження використовується зовнішній прапор, наприклад unload. Його установку виконує TSR, знайшовши запит на відвантаження.
Далі приводиться текст функції TSR_exit(), що виконує завершення резидентної програми, при інсталяції якої перехоплювалися вектори переривань 5h, 13h, 1Ch, 28h і 2Fh. функція викликається з меж обробника переривання 28h.
/* L2_15.C
Виконує відвантаження резидентной програми.
Повернення:
NO (0) - відвантаження TSR у даний
момент неможливі;
YES(l) - відвантаження TSR виконане успішно */
#include <dos.h>
#include <io.h>
#define NO 0
#define YES 1
/* "Старі" вектори переривань, збережені інсталятором */
extern void interrupt ( *old_int5 )(void);
extern void interrupt ( *old_intlC )(void);
extern void interrupt ( *old_intl3 )(void);
extern void interrupt ( *old_int28 )(void);
extern void interrupt ( *old_int2F )(void);
/* Прототипи встановлюваних ISR */
void interrupt ISR_int5 (void);
void interrupt ISR_intlC(void);
void interrupt ISR_intl3(void);
void interrupt ISR_int28(void);
void interrupt ISR_int2F(void);
extern unsigned psp;
int TSR_exit(void) {
register env_seg, handle = 0;
disable();
/* Перевірка можливості безпечного
відвантаження TSR */
if(getvect(5) != ISR_int5 ||
getvect(0xl3) != ISR_intl3 ||
getvect(0xlC) != ISR_intlC ||
getvect(0x28) != ISR_int28 ||
getvect(0x2F) != ISR_int2F )
return NO; /* відвантаження в даний
момент небезпечна для системи */
setvect( 0x5, old_int5 );
setvect( 0xl3, old_intl3 );
setvect( 0x1C, old_intlC );
setvect( 0x28, old_int28 );
setvect(0x2F, old_int2F );
/* Звільнення пам'яті, зайнятої TSR */
freemem( _psp );
env_seg = *( unsigned far* )MK_FP(
_psp, 0x2C);
freeniem( env_seg );
/* Закриття відкритих файлів */
for( handle = 0; handle < 5; handle++)
close( handle );
enable();
return YES;
}
У даному параграфі розглядається приклад використання приведених у 2.2 — 2.4 теоретичних зведень про побудову резидентної програми. Специфікація TSR копіювання текстового вікна у файл дана в п. 2.1.4. Основна ідея побудови TSR така. Сама TSR виконує дві несхожі задачі: оцінку текстового вікна на екрані і запис вмісту вікна в файл. Уся робота з екраном і клавіатурою виконується функцією TSR_screen_copy(). Виклик цієї функції відбувається "негайно" з меж ISR переривання 5h без виконання яких-небудь перевірок. Єдина вимога полягає в необхідності переключення стека перед викликом TSR_screen_copy(). Функція TSR_screen_copy() — це звичайна Сі-функція, що може використовувати всі можливості Turbo C Єдине обмеження зв'язане з забороною на використання для введення-виведення MS-DOS-функцій. Практично це змушує для введення-виведення використовувати звертання до BIOS і безпосередній доступ до відеобуферу (функції консольного введення-виведення Turbo C або власні функції).
Завершивши оцінку, копіюємо вміст текстового вікна й, одержавши команду на його збереження у файлі, функція TSR_screen_copy() приймає рядок специфікації файлу. Потім вміст вікна записується у внутрішній буфер пам'яті й в YES виставляється зовнішня змінна request, що сигналізує про необхідність відвантаження буфера. Якщо ISR переривання 5 знову одержує керування, а змінна request ще не скинута в NO (внутрішній буфер зайнятий), виклик TSR_screen_copy() не виконується і генерується звуковий сигнал. Звуковий сигнал генерується й у випадку, коли TSR_screen_copy() застає екран у графічному режимі, на роботу з яким варіант функції, що приводиться, не розрахований.
Якщо з клавіатури прийнята команда на відвантаження TSR з пам'яті, TSR_screen_copy() встановлює в YES зовнішню змінну unload. Відвантаження TSR з пам'яті виконує функція TSR_exit(), яка викликається з функції TSR_prepare_activate().
Запис вмісту внутрішнього буфера виконує функція TSR_body(). Тому що робота з диском вимагає дотримання багатьох попередніх умов, її виклику передує виклик TSR_prepare_activate(). Функція TSR_prepare_activate() викликається з ISR переривання 28h або 1Ch. Переривання від таймера використовується для того, щоб максимально прискорити відвантаження внутрішнього буфера на диск або вивантаження TSR з пам'яті. Таким чином, розглянута TSR копіювання текстового вікна будується за схемою, приведеною на мал. 2.5.
Зробимо деякі пояснення до малюнка.
Внутрішня статична змінна inside блокує повторне входження у функцію
TSR_screen_copy(). Зовнішня змінна busy використовується для запобігання
повторного входження у функцію TSR_prepare_activate(), виклик якої можливий як
з ISR переривань 28h, так і ISR переривання 1Ch. На мал. 2.5 не показані "фільтри"
переривань 2Fh і 13h. Перший з них використовується для запобігання повторної
установки TSR, а другий керує прапором inside_BIOS (див. L2_13.C). Установку
прапорів request і unload виконує функція TSR_screen_copy(). Для більшої
надійності в розглянутій програмі використовуються різні стеки: один
застосовується при активізації функції TSR_screen_copy() ("старі"
значення регістрів SS і SP запам'ятовуються в змінних old_screen_ss і
old_screen_sp, "нові" значення зберігаються в зовнішніх змінних TSR_screen_ss
і TSR_screen_sp, а під стек
Рис. 2.5.
Спрощена схем взаємодії обробників переривань 1Ch (таймер). 28h, 5
використовується масив TSR_screen_stack[512]), а другий використовується функцією TSR_body ("старі" значення регістрів SS і SP запам'ятовуються в змінних old_ss і old_sp, "нові" значення зберігаються в зовнішніх змінних TSR_ss і TSR_sp, а під стек використовується масив TSR_stack[512]).
Далі приводиться текст функції TSR_screen_copy():
/* L2_16.C
Задає інверсним атрибутом текстове вікно екрана. Після натискання клавіші 'S'
відкриває вікно, в якому задається ім'я файлу. При натисканні ENTER встановлює
зовнішню змінну request=YES, а вміст відзначеного текстового вікна поміщає у
внутрішній буфер data[]. При натисканні клавіші 'Q' після активізації програми
встановлює зовнішню змінну unload=YES. Розрахована на роботу у всіх кольорових
текстових режимах. */
#include <dos.h>
#include "screen.h"
#include <bios.h>
#include <ctype.h>
#include <string.h>
#define ESC
27
#define SAVE 13
#define QUIT 51
#define OK 0
#define BAD_PARAMS 1
#define NO 0
#define YES 1
extern unsigned request, unload; /*прапори для ISR_int28() і ISR_intl() */
/* Зовнішні змінні, використовувані тільки у файлі L2_16.C */
unsigned str0 = 0, str1=10, col0=0, col1=10;
unsigned vid_memory = 0х0800, start_adr, max_str, max_stolb;
char filename[ 40 ], data[ 4100 ];
unsigned buf[ 120 ];
/* Прототипи функцій. */
int TSR_window(void); /* відзначає вікно і приймає команду */
void inv_window(void); /* інвертує атрибути вікна */
void get_window(void); /* записує текст вікна в буфер data */
void vert_inv(unsigned col); /* інвертує атрибути рядка вертикально */
void hor_inv(unsigned str); /* інвертує атрибути рядка горизонтально */
int video_params(void); /* визначає поточні параметри відеосистеми */
int get_string(unsigned str, unsigned col, char *ptr); /* прийом рядка з клавіатури
з "луною" в потрібному місті екрана */
void get_text(int, int, int, int, unsigned* ); /* аналог gettext() Turbo С */
void put_text(int, int, int, int, unsigned* ); /* аналог puttext() Turbo С */
void beep(int tone, int duration); /* сигнал частоти tone тривалістю duration
*/
void TSR_screen_copy(void) {
register ret;
ret = TSR_window(); /* виконує
основну роботу */
switch( ret ) {
case SAVE:
get_window(); /* запис вікна в
буфер */
disable(); /* переривання, заборонені
до виходу з ISR_int5() */
request = YES;
return;
case QUIT:
unload = YES;
return;
case ESC:
return;
case BAD_PARAMS:
beep(800, 500); /*сигнал 800 Гц
тривалістю 0.5 с */
}
}
/* TSR_WINDOW()
Відзначає інверсним атрибутом текстове вікно екрана; реагує на клавіші Left,
Right, Up, Down, Ctrl-Left, Ctrl-Right, Ctrl-Up, Ctrl-Down. При натисканні ESC
і 'Q' відновлює атрибути вікна і завершується. При натисканні клавіші 'S'
відкриває вікно, у якому задається ім'я файлу. Повернення :
ESC (27) - натиснута клавіша ESC;
SAVE (13) - прийняте ім'я файлу і натиснута клавіша ENTER;
QUIT (51) - натиснута клавіша 'Q';
BAD_PARAMS (1) - невідповідний відеорежим. */
int TSR_window(void) {
register ret;
union KEY {
int i;
char c[2];
} key;
static char *ftitle = "Ім'я файлу
призначення";
static char *fkey = "ESC <Cancel>, ENTER
<OK>";
static char style[8] = { '\xC9', '\xCD',
'\xBB', '\xBA', '\xBA', '\xCB', '\xCD', '\xBC' };
if( video_params() == BAD_PARAMS )
return BAD_PARAMS; /* функція, не
розрахована на ці випадки */
cursor(OFF); /* виключаємо курсор з
текстового екрана */
/*Корекція статичних координат вікна, якщо вони перевершують поточні можливості
екрана. */
if( str1 > max_str ) str1 = max_str;
if(col1 > max_stolb - 1) col1 =
max_stolb - 1;
if(str0 > max_str) str0 = max_str;
if(col0 > max_stolb - 1) col0 = max_stolb - 1;
inv_window(); /* координати вікна –
статичні зовнішні */
/* Нескінченний цикл прийому і інтерпретації введення з клавіатури */
for( ; ; ) {
key.і=bioskey(0);
switch( key.c[ 0 ] ) {
case 0: /* натиснута спеціальна
клавіша */
switch( key.c[ l ] ){
case 75: /* Left */
if( col0 ) { /*
"перефарбування" вертикальної смуги */
col0--;
vert_inv(col0);
}
break;
case 77: /* Right */
if(col0 < col1) { /* відновлення
вертикальної смуги */
vert_inv(col0);
col0++;
}
break;
case 72: /* Up */
if(str0) {
/*"перефарбування" горизонтальної смуги */
str;
hor_inv(str0);
}
break;
case 80: /* Down */
if( str0 <
str1){ /*відновлення горизонтальної смуги */
hor_inv(str0);
str0++;
}
break;
Відображена пам'ять (Expanded Memory) реалізується спеціальною апаратною платою. Для побудови таких плат і організації інтерфейсу прикладної програми по керуванню відображеною пам'яттю вироблені спеціальні стандарти (специфікації), які називаються EMS (Expanded Memory Specification). Найбільш відомим з них є стандарт, запропонований спільно компаніями Lotus, Intel і Microsoft, — специфікація LIM EMS. Специфікації постійно розширюються й удосконалюються. Найбільш раннім є EMS версії 3.2 (опублікований у вересні 1985 р.). У серпні 1987р. був опублікований стандарт EMS версії 4.0, що і розглядається далі.
Ідея EMS-пам'яті полягає в наступному. Адресний простір у 1Мб розподіляється в IBM PC при роботі під керуванням MS-DOS так, як показано на малюнку.
Рис 6.1 Розподіл адрес пам’яті в IBM PC
Таким чином, прикладні програми можуть використовувати не більш 640Кбайт оперативної пам'яті. Однак, якщо в системі встановлена EMS-плата, з'являється можливість використовувати до 32Мбайт додаткової пам'яті плати (так званої EMS-пам'яті). Область адрес, що зарезервована для BIOSa, зайнята цілком програмами ПЗУ тільки в PS/2, а для IBM PC XT і IBM PC AT у цьому діапазоні є вікно розміром не менш 64Кбайт. Його і закриває EMS-плата, реалізуючи механізм віртуального відображення сторінок (мал. 6.2).
Рис 6.2 Елементи механізму відображеної (expanded) пам'яті
Початкова границя вікна (сегмент адреси) задається регістрами-конфігураторами плати, а в деяких реалізаціях EMS може встановлюватися прикладною програмою.
Відображена пам'ять має до 8Мбайт (EMS версії 3.2) чи до 32Мбайт (EMS версії 4.0 і старше) на одній чи декількох платах адаптера відображеної пам'яті. Сукупність чотирьох сторінок пам'яті в адресному просторі 1Мбайт називають фреймом сторінок. Ці 64Кбайта пам'яті належать EMS-платі. Запит читання чи запису до адрес фрейму сторінок апаратно переадресується на логічну сторінку. Закріплення фізичних сторінок за логічними називають картою відображення. Карта може змінюватися програмно. Фізичні сторінки мають номери 0, 1, 2 і 3. Кожна з чотирьох фізичних сторінок може бути відображена на кожну з логічних сторінок.
Логічні сторінки перед використанням повинні бути розподілені. У ході операції розподілу EMS-пам'яті програма запитує потрібне їй число логічних сторінок. Надалі для посилання на виділені сторінки використовується ціле число, яке називається ЕММ-префіксом (EMM-handle). Логічні сторінки, закріплені за префіксом, нумеруються від 0 до N-1, де N—число сторінок, закріплених операцією розподілу за даним префіксом. Іншими словами, логічна сторінка в групі закріплених за префіксом сторінок має відносний номер, а не абсолютний.
EMS-плата керується рядом внутрішніх портів, доступ до яких здійснюють спеціальні драйвери відображеної пам'яті, наприклад, EMM.SYS, XMS2EMS.SYS і ін. Далі такі драйвери будемо називати ЕММ-драйверами. Для взаємодії прикладної програми з ЕММ-драйвером використовуються функції переривання 67h.
Перш ніж програма почне використовувати будь-яку функцію EMS, необхідно визначити наявність EMM-драйвера в системі. Якщо драйвер у ході початкового завантаження системи був інстальований, виклик переривання 67h буде безпечним. У протилежному випадку ймовірна ситуація звертання до переривання 67h, для якого відсутній вектор у таблиці векторів переривань, і, як результат, — "зависання" системи. Один із двох рекомендованих LIM EMS способів визначення типу драйвера, що обслуговує відображену пам'ять, полягає в наступному.
За EMS версії 3.2 вектор переривання 67h після встановлення ЕММ-драйвера повинний дорівнювати покажчику на заголовок інсталюємого драйвера. Через те що, драйвери встановлюються в пам'яті з вирівнюванням на границі параграфу, поле зсуву у векторі 67h буде дорівнює нулю. У заголовку драйвера, як це визначено технічною документацією MS-DOS, по зміщенню 0Ah розташовується ASCIIZ-рядок імені драйвера (не більш 8 символів). Встановлене для EMS ім'я повинне бути "ЕММХХХХ\0". Розглянутий спосіб покладений в основу функції, що далі наводиться, EmsInstalled, що повертає символьну константу NOTINSTALLED (1), якщо ЕММ-драйвер не виявлений. У протилежному випадку функція повертає символьну константу NOERROR (0).
/* L6_1.C
Визначає наявність драйвера, що керує EMS-пам’яттю.
Повертає:
NOTINSTALLED (1) - якщо не інстальований ЕММ-драйвер;
NOERROR (0) - якщо виявлений драйвер EMS-пам'яті*/
#define NOTINSTALLED 1
#define NOERROR 0
#include <dos.h>
int Emslnstalled(void) {
static char *EmmName =
"EMMXXXX0";
char far *EmmPtr;
/* Формування покажчика на ім'я
драйвера в заголовку */
_АХ = 0x3567; /* визначення вектору переривання
67h */
geninterrupt(0x21);
EmmPtr = ( char far* )MK_FP( _ES,
0x0a);
/* Перевірка імені драйверу на збіг зі
стандартним. */
while( *EmmName )
if( *EmmName++ != *EmmPtr++ )
return NOTINSTALLED;
return NOERROR;
}
Після того, як виявлена наявність ЕММ-драйвера, можна звертатися до різних функцій переривання 67h. Усі ці функції в регістрі АН повертають код помилки (табл. 6.1).
Табл. 6.1. Деякі коди помилок з ЕММ-драйвера
Значення в регістрі АН |
Коментар |
0 |
Відсутність помилок: запитана функція успішно виконана |
81h |
Внутрішня помилка в ЕММ-драйвері |
82h |
ЕММ-драйвер зайнятий |
83h |
Неприпустимий ЕММ-префікс |
84h |
Запит неописаної в стандарті функції |
85h |
Вичерпано ліміт на ЕММ-префікси |
86h |
Помилка збереження чи відновлення карти відображення |
87h |
Запитане число сторінок перевищує обсяг EMS-пам’яті |
88h |
Запитане число сторінок більше числа вільних сторінок |
89h |
Запит закріплення нуля сторінок |
8Ah |
При створенні карти сторінок зазначений номер, що перевищує максимально допустимий для даного ЕММ-префіксу |
8Bh |
Номер фізичної сторінки більше 3 |
8Ch |
Область збереження контексту карти відображення порожня |
8Dh |
Спроба зберегти контекст карти відображення вдруге |
8Eh |
Спроба відновлення контексту карти без попереднього його збереження |
8Fh |
Неприпустимий номер підфункції в регістрі AL |
Далі наводиться специфікація найбільш поширених функцій EMS, визначених версіями 4.0 і старше:
АН = 40h — визначення стану EMS. Функція використовується після того, як визначена наявність ЕММ-драйвера. Повернення в АН нульового значення свідчить про готовність EMS-плати до роботи;
АН = 41h — визначення сегмента, починаючи з якого у пам'яті розташовується фрейм фізичних сторінок (значення, яке повертається у ВХ, якщо АН=0);
АН = 42h — визначення загального об’єму EMS-пам’яті і кількості вільних для розподілу сторінок. На виході з функції регістр ВХ повідомляє загальну кількість логічних сторінок розміром по 16Кбайт кожна; регістр DX повідомляє загальну кількість ще не розподілених сторінок;
АН = 43h — розподіл зазначеного в регістрі ВХ числа логічних сторінок У випадку успіху (АН=0) запитані підряд розташовані логічні сторінки вважаються в наступних запитах розподілу зайнятими і не зв'язуються з іншим EMM-префіксом. Для посилань на розподілені сторінки повинний використовуватися ЕММ-префікс, що повертається в регістрі DX;
АН = 44h — побудова карти відображення. Зв'язує фізичну сторінку, номер якої задається в регістрі AL (від 0 до 3), з логічною сторінкою, номер якої задається в регістрі ВХ Логічна сторінка закріплена за ЕММ-префіксом, що задається в регістрі DX. Неважко здогадатися, що максимальне значення номеру логічної сторінки не повинне перевищувати загального числа запитаних при розподілі логічних сторінок. Для побудови карти, що відображає весь фрейм фізичних сторінок, потрібно виконати дану функцію окремо для кожної з фізичних сторінок (тобто чотири рази підряд);
АН = 45h — закриття ЕММ-префікса, заданого в регістрі DX, і звільнення закріплених за ним логічних сторінок;
АН = 46h — визначення версії EMS. Значення, яке повертається в регістрі AL інтерпретується так: біти 7-4 задають "старший" номер, біти 3-0 — "молодший". Наприклад, версії EMS 4.0 відповідає значення в AL, рівне 40h.
Перераховані функції підтримуються усіма версіями EMS, починаючи з 3.2. У версії 4.0 передбачений ряд удосконалень. Зокрема, обсяг керованої EMS-пам'яті збільшений до 32Мбайт. Розмір фрейму фізичних сторінок може дорівнювати 128Кбайт, дозволяючи не 4, а 8 фізичних сторінок. Допускається відображення фізичних сторінок і на пам'ять у межах основної пам'яті 1Мбайт, передбачена можливість запобігання від руйнування при "гарячому" перезавантаженні обраних сторінок пам'яті. Значно збільшене число функцій, призначених, насамперед, для спрощення швидкого переключення системи з задачі на задачу, переносу великих блоків даних з основної пам'яті в EMS і назад, переміщення даних у межах EMS-пам'яті і ін. Далі розглядаються тільки деякі з функцій розширеного версією 4.0 набору керування EMS-пам'яттю:
АН = 47h — збереження поточної карти відображення між усіма фізичними і відповідними їм логічними сторінками. У регістрі DX задається ЕММ-префікс логічних сторінок поточного процесу.
АН = 48h — відновлення карти відображення в стан, у якому вона знаходилася в момент виклику функції АН = 47h з тим самим ЕММ-префіксом, що задається в регістрі DX, Використовуючи функції AH=47h і АН=48h з тим самим префіксом ,ТSR- програма може зберегти, а потім відновити карту відображення фонової програми.
Далі наводяться приклади Сі-функцій для керування EMS. Вони утворюють базовий набір операцій і можуть послужити відправною точкою для створення власної більш повної бібліотеки. Функція EmsVersion() повертає код версії EMS. У випадку виникнення помилки повертається 0 і в зовнішню змінну EmsError записується код помилки ЕММ-драйвера (див. табл. 6.1).
/* L6_2.С
Визначає версію драйвера, що керує EMS-пам'яттю.
Повернення:
unsigned int - версія EMS:
біти 7 - 4 - "старший" номер;
біти 3 - 0 - "молодший"
номер;
0- при виникненні помилки. */
#define
NOTINSALLED 1
#include <dos.h>
int EmsInstalled(void);
int EmsVersion(void) {
extern int EmsError;
/* Перевірка того, чи встановлений
ЕММ-драйвер. */
if( EmsInstalled() == NOTINSTALLED) {
EmsError = NOTINSTALLED;
return( 0 );
}
/* Визначення номера версії */
_АН = 0x46;
geninterrupt( 0x67 );
if(_AH) {
EmsError = _AH;
return( 0 );
}
EmsError = 0;
return( _AL );
}
Функція EmsPagesAvail() повертає кількість сторінок логічної пам'яті,
доступних для розподілу. У випадку виникнення помилки повертається (-1) і в
зовнішню змінну EmsError записується код помилки ЕММ-драйвера (див. табл. 6.1).
/*L6_3.C
Визначає кількість вільних логічних сторінок EMS-пам'яті
Повернення:
0 - число доступних для розподілу
сторінок,
-1 - виникнення помилки */
#define NOTINSTALLED 1
#include <dos.h>
int EmsInstalled(void);
int EmsPagesAvail(void) {
extern іnt EmsError;
/* Перевірка того, чи встановлений ЕММ-драйвер.
*/
if( EmsInstalled() == NOTINSTALLED) {
EmsError = NOTINSTALLED;
return( -1 );
}
/* Визначення числа доступних сторінок
*/
_АН = 0x42;
geninterrupt( 0x67 );
if( _AH ) {
EmsError = _AH;
return ( -1 );
}
EmsError = 0;
return( _BX );
}
Функція EmsAlloc()
розподіляє Pages сторінок EMS-пам'яті логічної пам'яті, доступних для
розподілу. У випадку виникнення помилки повертається (-1) і в зовнішню змінну
EmsError записується код помилки ЕММ-драйвера (див. табл. 6.1). У протилежному
випадку повертається ЕММ-префікс для посилання на виділені сторінки.
/* L6_4.C
Розподіляє Pages логічних сторінок EMS-пам'яті
Повернення:
0 - ЕММ-префікс виділених сторінок;
-1 - виникнення помилки */
#define NOTINSTALLED 1
#include <dos.h>
int EmsІnstalled( void );
int EmsAlloc( Pages ) {
extern int EmsError;
/* Перевірка того, чи встановлений
ЕММ-драйвер. */
if( EmsInstalled() == NOTINSTALLED) {
EmsError = NOTINSTALLED;
return( -1 );
}
/* Запит Pages доступних сторінок. */
_BX = Pages;
_AH = 0x43;
geninterrupt( 0x67 );
if( _AH ) {
EmsError = _AH;
return( -1 );
}
EmsError = 0; /* немає помилки */
return (_DX);
}
Точна адреса
пам'яті, з якої в межах першого мегабайта починається фрейм фізичних сторінок, повертає
функція EmsFrameAdr(). При виникненні помилки повертається NULL-покажчик і в
зовнішню змінну EmsError записується код помилки ЕММ-драйвера (див. табл. б.1).
/* L6_5.C
Визначає адресу фрейму фізичних сторінок.
Повернення:
far-покажчик на перший байт фрейму
NULL
- виникнення помилки */
#define NOTINSTALLED 1
#include <dos.h>
int EmsІnstalled( void );
char far *EmsFrameAdr( void ) {
extern int ЕmsЕrror;
/* Перевірка того, чи встановлено EММ-драйвер */
if( EmsІnstalled() == NOTINSTALLED) {
EmsError = NOTINSTALLED;
return ( -1 );
}
/* Формування адреси фрейму фізичних
сторінок */
_АН = 0x41;
geninterrupt( 0x67 );
if( _AH ) {
EmsError = _АН;
return( ( char far* ) 0);
}
EmsError = 0;
return( ( char far * )MK_FP( _BX, 0 )
);
}
Функція EmsMap()
будує карту відображення для чотирьох логічних сторінок, переданих як
параметри. У випадку успіху функція повертає 0. При виникненні помилки
повертається (-1) і в зовнішню змінну EmsError записується код помилки
ЕММ-драйвера (див. табл. 6.1).
/* L6_6.C
Задає відображення чотирьох логічних
сторінок, номери яких поміщені в масиві Pages[ ], і чотирьох фізичних сторінок.
Логічні сторінки розподілено раніше, і для посилань на них використовується
ЕММ-префікс Handle.
Повернення:
0 - у випадку успіху;
-1 - у
випадку помилки. */
#define NOTINSTALLED 1
#include <dos.h>
int EmsІnstalled( void );
int EmsMap( int Handle, int Pages[ ] ) {
extern int EmsError;
register int i;
/* Перевірка того, чи встановлений
ЕММ-драйвер */
if( EmsInstalled() == NOTINSTALLED) {
EmsError = NOTINSTALLED;
return( -1 );
}
/* Цикл побудови карти відображення для
чотирьох сторінок */
for( i=0; i<4; i++) {
_ВХ = Pages[ і ];
_DX = Handle;
_AH = 0x44;
_AL = ( char ) і;
geninterrupt( 0x67 );
/* Аналіз стану виконаної операції.
*/
if( _AH ) {
EmsError = _AH;
return( -l );
}
}
EmsError = 0;
return( 0 );
}
Функція EmsFree()
звільняє всі логічні сторінки EMS-пам'яті, закріплені за Емм-префіксом Handle.
У випадку успіху функція повертає 0. При виникненні помилки повертається (-1) і
в зовнішню змінну EmsError записується код помилки ЕММ-драйвера (див. табл.
6.1).
/* L6_7.С
Звільняє всі логічні сторінки
EMS-пам'яті, закріплені за ЕMM-префіксом Handle
Повернення:
0 – у випадку успіху;
-1- у випадку помилки. */
#define NOTINSTALLED 1
#include <dos.h>
int Emslnstalled( void );
int EmsFree( int Handle ) {
extern int EmsError;
/* Перевірка того, чи встановлений
ЕMM-драйвер. */
if( EmsInstalled() == NOTINSTALLED ) {
EmsError = NOTINSTALLED;
return( -1 );
}
/* Звільнення логічних сторінок. */
_DX = Handle;
_AH = 0x45;
geninterrupt( 0x67 );
/* Аналіз стану виконаної операції. */
if( _AH ) {
EmsError = _AH;
return( -l );
}
EmsError = 0;
return( 0 );
}
Функція EmsSave()
зберігає поточну карту відображення для заданого ЕMM-префікса Handle у
внутрішній області збереження EMS-плати. У випадку успіху функція повертає 0.
При виникненні помилки повертається (-1) і в зовнішню змінну EmsError
записується код помилки ЕММ-драйвера (див. табл. 6.1).
/* L6_8.C
Зберігає поточну карту відображення для
заданого ЕMM - префікса Handle у внутрішній області збереження EMS-плати.
Повернення:
0- у випадку успіху;
-1 - у випадку помилки. */
#define NOTINSTALLED 1
#include <dos.h>
int EmsInstalled( void );
int EmsSave( int Handle ) {
extern int EmsError;
/* Перевірка того, чи встановлений
EMM-драйвер. */
if( EmsInstalled() == NOTINSTALLED ) {
EmsError = NOTINSTALLED;
return( -1 );
}
/* Збереження карти відображення. */
_DX = Handle;
_AH = 0x47;
geninterrupt( 0x67 );
/* Аналіз стану виконаної операції. */
if( _AH ) {
EmsЕrror = _АН;
return( -l );
}
EmsError = 0;
return( 0 );
}
Функція EmsRestore()
відновлює з внутрішньої пам'яті EMS-адаптера збережену раніше карту
відображення для ЕММ-префікса Handle.
/* L6_9.C
Відновлює карту відображення для заданого ЕMM-префиікса Handle з
внутрішньої області збереження EMS-плати.
Повернення:
0 - у випадку успіху;
-1 - у випадку помилки. */
#define NOTINSTALLED 1
#include <dos.h>
int EmsInstalled( void );
int EmsRestore( int Handle ) {
extern int EmsError;
/* Перевірка того, чи встановлений
ЕММ-драйвер. */
if( EmsInstalled() == NOTINSTALLED) {
EmsError = NOTINSTALLED;
return( -1 );
}
/*
Відновлення карти відображення. */
_DX = Handle;
_AH = 0x48;
geninterrupt( 0x67 );
/* Аналіз стану виконаної операції. */
if( _AH ) {
EmsError = _AH;
return( -1 );
}
EmsError = 0;
return( 0 );
}
Наведемо невелику демонстраційну програму для ілюстрації керування EMS-пам'яттю й організації доступу до неї. Програма визначає наявність ЕММ-драйвера, потім збирає відомості про EMS-пам'ять: номер версії, загальне число сторінок і доступну для розподілу кількість логічних сторінок. Якщо останніх не менш 4, демонстраційна програма продовжує свою роботу. Розподіляється 4 логічні сторінки і для них будується карта наступного виду:
фізична сторінка 0 відображається в логічну сторінку 0,
фізична сторінка 1 - у логічну сторінку 1 і т.д.
В початок кожної з логічних сторінок записується рядок "Логічна сторінка номер х", де х = 0; 1; 2; 3. Побудована програмою карта відображення запам'ятовується. Потім встановлюється інверсна карта: фізична сторінка 0 відображається в логічну сторінку 3, фізична сторінка 1 — у логічну сторінку 2 і т.д. Після цього виводиться зміст рядків, записаних на початку кожної фізичної сторінки. І, нарешті, демонстраційна програма відновлює раніше збережену карту відображення і повторює операцію виведення рядків.
Останньою дією демонстраційна програма звільняє розподілені логічні сторінки.
/* L6-
Демонструє керування EMS-пам'яттю і доступ до неї. Компілюється з використанням
моделі пам'яті COMPACT, LARGE, HUGE*/
#include
<stdio.h>
#include <dos.h>
#define NOTINSTALLED 1
#define NOERROR 0
int EmsInstalled(void);
int EmsVersion(void);
int EmsPagesAvai1(void);
int EmsAlloc(int Pages);
int EmsFree(int Handle);
char far *EmsFrameAdr(void);
int EmsMap(int Handle, int Pages[ ]);
int EmsSave(int Handle);
int EmsRestore(int Handle);
int EmsError;
int main(void) {
unsigned PagesOffset[ 4 ] = { 0, /* зсуви */
0x4000, /* до початку
*/
0x8000, /* фізичних
*/
0xC000 }; /* сторінок */
char far *PageFrame; /* покажчик на фрейм фізичних сторінок */
int AvailPages, Version, Handle,
Pages[4], i;
/* Перевірка того, чи встановлений
ЕММ-драйвер. */
if( EmsInstalled() == NOTINSTALLED ) {
puts( "Відображена (Expanded)
пам'ять відсутня" );
return( 1 );
}
/* Збір
інформації про EMS-пам'ять. */
if( !( Version = EmsVersion() ) ) {
printf( "Помилка при визначенні
версії EMS. Код-%х \n", EmsError );
return( 2 );
}
puts( "B системі встановлена
відображена (Expanded) пам'ять" );
printf( “Версія EMS: %d.%d \n",
Version >> 4, Version & 0x000F );
if( ( AvailPages = EmsPagesAvail() ) ==
-1 ) {
printf( "Помилка при визначенні
числа вільних сторінок EMS. Код-%х\n", EmsError);
return( 3 );
}
printf( "Вільних логічних сторінок
EMS: %d \n", AvailPages);
/* Розподіл чотирьох логічних сторінок і визначення початкової адреси
фрейму фізичних сторінок. */
if( ( Handle = EmsAlloc( 4 ) ) == -1 )
{
printf( "Помилка при розподілі
вільних сторінок EMS Код-%х\n", EmsError );
return( 4 );
}
if( ( PageFrame = EmsFrameAdr() ) ==
NULL ) {
printf("Помилка при визначенні
адреси фрейму фізичних сторінок. Код-%х\n", EmsError);
return( 5 );
}
printf( "Розподілені 4 логічні
сторінки EMS-пам’яті. \n Префікс для посилань: %d. \n Адреса фрейму фізичних сторінок: %р
\n", Handle, PageFrame );
/* Побудова карти відображення і її
збереження. */
Pages[ 0 ] = 0; Pages[ l ] = 1;
Pages[ 2 ] = 2; Pages[ 3 ] = 3;
if( EmsMap( Handle, Pages ) == -1) {
printf( "Помилка при побудові
карти відображення, Код-%х\n, EmsError );
return( 5 );
}
if( EmsSave( Handle ) == -1) {
printf( "Помилка при збереженні
карти відображення. Код-%х\n", EmsError );
return ( 6 );
}
/* Доступ до виділених логічних
сторінок: запис на самому початку кожної сторінки тестового ASCIIZ-рядка. */
for( i = 0; i < 4; i++ )
sprintf( PageFrame + PagesOffset[ і
], “Логічна сторінка %d \n”, і );
/* Установка інверсної карти. */
Pages[ 0 ] = 3; Pages[ 1 ] = 2;
Pages[ 2 ] = 1; Pages[ 3 ] = 0;
if( ( EmsMap( Handle, Pages ) ) == -1 )
{
printf( "Помилка при побудові
карти відображення. Код-%х \n", EmsError );
return( 5 );
}
/* Доступ до виділених логічних
сторінок виведення на екран записаної на
початку сторінки тестового ASCIIZ-рядка */
for(i = 0; i < 4; i++)
printf( PageFrame + PagesOffset[ і ]
);
/* Відновлення раніше збереженої карти
*/
if( EmsRestore( Hand1e ) == -1 ) {
printf( "Помилка при відновленні
карти відображення. Код-%х \n", EmsError );
return( 7 );
}
/* Доступ до виділених логічних
сторінок: виведення записаної на початку сторінки ASCIIZ-рядка індикації. */
for( i =0; i < 4, i++ )
printf( PageFrame + PagesOffset[ i ]
);
/* Звільнення розподіленої EMS-пам'яті.
*/
if( EmsFree( Handle ) == -1 ) {
printf( "Помилка при звільненні
сторінок EMS-пам'яті. Код-%х \n", EmsError);
return (8);
}
return( 0 ); /* тест успішно завершений
*/
}
Для одержання .ЕХЕ-файлу використовується файл проекту (наприклад, L6_10.PRJ), у який включені файли L6_1.C — L6_10.C. При своєму виконанні демонстраційна програма видає на екран наступну інформацію:
С:\>L6_10.EXE
У системі установлена відображувана (Expanded) пам'ять
Версія EMS. 4.0
Вільних логічних сторінок EMS: 169
Розподілені 4 логічні сторінки EMS-пам'яті.
Префікс для посилань: 3.
Адреса фрейму фізичних сторінок: E000:0000
Логічна сторінка 3 - використовується інверсна карта
Логічна сторінка 2
Логічна сторінка 1
Логічна сторінка 0
Логічна сторінка 0 - використовується вихідна карта
Логічна сторінка 1
Логічна сторінка 2
Логічна сторінка 3
Розглянута демонстраційна програма ілюструє використання EMS-пам'яті в звичайних нерезидентних програмах, Робота TSR-програми з відображеною пам'яттю має деякі особливості:
1) розподіл EMS-пам'яті, необхідної для роботи резидентної програми, повинен виконуватися програмою інсталяції, а не при активізації резидентної частини TSR;
2) відразу ж після активізації резидентна частина TSR повинна зберегти поточну карту відображення (наприклад, використовуючи наведену раніше функцію EmsSave), потім побудувати карту відображення для власних логічних сторінок EMS-пам'яті, а перед виходом у фонову програму виконати відновлення карти (наприклад, використовуючи наведену раніше функцію EmsRestore). Функції EmsSave і EmsRestore використовують той самий ЕММ-префікс, отриманий інсталюючою частиною TSR при розподілі сторінок;
3) при "вивантаженні" резидентної програми з пам'яті необхідно звільнити всі логічні сторінки EMS-пам'яті, виділеної для використання резидентною програмою.
Повний опис усіх функцій інтерфейсу прикладної програми з EMS можна знайти в електронних довідниках (TechHelp 6.0). Відзначимо, що EMS версії 4.0 і старше підтримує механізм іменованих ЕMM-префіксів, посилання на який здійснюється по імені префікса. Спеціальні функції призначені для одержання повної інформації про використання EMS: число сторінок, закріплених за кожним ЕММ-префіксом, специфічні атрибути сторінок і ін. Група функцій полегшує роботу з картою відображення фізичних сторінок на логічні, а також спрощує свопінг між основною і EMS-пам'яттю.
EMS-пам'ять може використовуватися не тільки для збереження даних, але і розміщення коду програми, що виконується. Обмеження полягає в тому, що обсяг фрагмента, що безупинно виконується, не повинний перевищувати 64Кбайти, а сам фрагмент повинний завершуватися передачею керування на програму-диспетчер. Диспетчер записує код першого фрагмента програми як звичайний блок даних в підряд розташовані логічні сторінки. Будується карта відображення, наприклад, для перших чотирьох логічних сторінок, і керування передається на точку входу у фрагмент. Після того, як керування досягає останньої інструкції фрагмента і повертається в диспетчер, будується нова карта для наступних сторінок EMS-пам'яті і т.д. EMS версії 4.0 включає спеціальні функції інтерфейсу, що спрощують побудову програм, що розташовуються при виконанні в EMS-пам'яті.
Відзначимо ще раз, що відображена пам'ять вимагає установки в комп'ютер спеціальної плати пам'яті. EMS-пам'ять доступна тільки через фрейм фізичного вікна. Найбільш сучасні EMS-плати можуть програмно змінювати конфігурацію, "перетворюючи" у так звану розширену (Extended) пам'ять, керування якої докладно розглядається в наступному параграфі. Спеціальні драйвери, наприклад XMS2EMS.SYS, підтримують таке "перетворення". Можлива ситуація і зворотного "перетворення", коли відображена пам'ять використовується як розширена пам'ять і спеціальні драйвери програмно імітують наявність EMS-плати (наприклад, EMM386.EXE, QEMM.SYS і ін.).
Перевагами EMS-пам'яті є:
1) можливість розширення доступної пам'яті в комп'ютерах на базі процесора 8086;
2) висока швидкість доступу, забезпечувана апаратною переадресацією.
Розширена пам'ять (Extended Memory) — це пам'ять понад адресного простору в 1Мбайт у комп'ютерах, заснованих на процесорах Intel 80286, 80386, 80486. Для керування такою пам'яттю використовуються апаратні і програмні засоби, що підтримують специфікацію розширеної пам'яті, чи XMS (Extended Memory Specification), Існують різні її версії. Далі розглядається специфікація версії 2.0 і старше, розроблена спільно фірмами Microsoft, Lotus, Intel і AST Research. На мал. 6.3 наводиться розподіл адресного простору з погляду XMS. Використання XMS припускає установку в системі спеціального драйвера, що надалі буде називатися XMS-драйвером, що забезпечує інтерфейс прикладної програми з розширеною пам'яттю. Звичайно це вхідний до складу MS-DOS інсталюючий драйвер HIMEM.SYS. Деякі функції інтерфейсу розглядаються в наступних параграфах.
Рис.6.3. Розподіл пам'яті в IBM PC на основі процесорів 80286/386
Коротко прокоментуємо окремі елементи, показані на мал. 6.3. Перші 64Кбайт розширеної пам'яті (пам'яті, фізичні адреси якої перевищують 1Мбайт) утворюють НМА (High Memory Area). Доступ до НМА вимагає керування спеціальним апаратним вузлом — мікросхемою 8042 у IBM PC AT. Робота мікросхеми може бути блокована чи активізована. Виходом мікросхеми є 21-й розряд фізичної адреси (так називана лінія А20). Якщо лінія А20 деблокована, НМА доступна для будь-якої програми, що працює в реальному режимі. У цьому випадку перенос, що виникає при формуванні 20-розрядної фізичної адреси, не буде ігноруватися, як це має місце в процесорі 8086. З цієї причини адреса пам'яті, сегментна частина якої містить значення 0FFFFh, а зсув перевищує 10h, буде відповідати 21-розрядна фізична адреса, що перевищує границю в 1Мбайт. Однак якщо лінія А20 блокована, перенос буде ігноруватися й у 80286/386. Тому НМА починається з адреси FFFF:0010h і закінчується адресою FFFF:FFFFh, а загальна довжина області дорівнює (64К – 16) байт.
Починаючи з версії 4.0 MS-DOS, НМА може використовуватися для розміщення ядра операційної системи прикладних програм. У цьому випадку у файл конфігурації системи розміщується рядок
DOS=HIGH
Обробляючи дану команду, MS-DOS цілком захоплює всю НМА, і надалі ця область розширеної пам'яті керується менеджером пам'яті MS-DOS. Сюди переноситься резидентна частина ядра і при необхідності інші резидентні програми. При цьому передбачається, що в системі вже інстальований драйвер HIMEM.SYS, деблокуючий лінію А20.
Блок пам'яті UMB (Upper Memory Block) доступний на деяких комп'ютерах, оснащених спеціальною платою так званої системної чи "тіньової" пам'яті (System чи Shadow Memory). Загальний розмір цієї пам'яті звичайно складає 256Кбайт. Один чи кілька UMB-блоків розташовуються в не використовуваному ВIOSом і відеопам'яттю адресному просторі вище 640Кбайт до границі в 1Мбайт. Основне призначення плати "тіньової" пам'яті складається в підвищенні продуктивності за рахунок прискорення доступу до BІOSy. В сучасних комп'ютерах швидкість фізичного доступу до оперативної пам'яті набагато вище швидкості доступу до ПЗУ. Тому в ході початкового завантаження “основний” BIOS системи і його розширення на окремих платах копіюються в “тіньову” пам'ять і в процесі роботи комп'ютера використовується копія в оперативній пам'яті, а не в ПЗУ. "Тіньова" пам'ять, що залишилася вільною, може включатися в адресний простір MS-DOS і використовуватися нею як основна і НМА-пам’ять. Таким чином, UMB розширює загальний обсяг пам'яті, доступної MS-DOS для збереження ядра і резидентних програм, залишаючи в розпорядженні прикладних програм практично всю основну пам'ять у 640К байт. Для створення UMB і організації інтерфейсу прикладної програми використовуються спеціальні інсталюючі драйвери, наприклад EMM386.SYS. MS-DOS версії 5.0 здатна ефективно використовувати UMB. Наприклад, рядок файлу конфігурації
DOS = HIGH, UMB
дає вказівку розмістити ядро операційної системи в UMB, а не в НМА.
Пам'ять ЕМВ (Extended Memory Block) утворює один чи кілька блоків, розташованих в адресному просторі вище НМА. Для доступу до них XMS-драйвер переключає процесор у захищений режим. Тому програми реального режиму можуть використовувати ЕМВ тільки для збереження даних.
Керування розширеною пам'яттю може виконуватися безпосереднім звертанням до XMS-драйвера або через функції спеціальних бібліотек. Наприклад, компанією Microsoft поширюється Сі-бібліотека EMM.LIB. Вона розрахована на спільне використання з Microsoft С. Далі розглядаються Сі-функції керування XMS-пaм’ятю, побудовані на безпосереднім звертанні до інстальованого драйвера.
XMS-драйвери включають у каскад переривання 2Fh власну "ехо-секцію". Вона, одержавши керування, перевіряє значення регістра АН: якщо АН == 43h, драйвер повідомляє про свою присутність (AL=00) або передає в регістрах ES:BX адресу точки входу у функції керування (AL=10h). Адреса повернутої точки, використовується надалі для безпосереднього звертання до XMS-драйвера. Номер функції в цьому зверненні задається драйверу в регістрі АН. У випадку успіху XMS-драйвер повертає АХ=0. При виникненні помилки АХ=1, а код помилки передається в регістрі BL. Можливі коди помилок наведені в табл. 6.2.
Табл. 6.2. Коди помилок
керування розширеною пам'яттю
Значення в регістрі BL |
Коментар |
80h |
Функція не підтримується
драйвером |
81h |
НМА використана драйвером
віртуального диска VDISK |
82h |
Помилка керування лінією
A20 |
8Eh |
Спільна помилка XMS –
драйвера |
8Fh |
Невідновлювальна помилка
XMS - драйвера |
90h |
НМА відсутня |
91h |
НМА зайнята |
92h |
Об'єм запрошеної у НМА
пам'яті меньше межі, встановленого параметром /HMAMIN= при інсталяції XMS-драйвера HIMEM.SYS |
93h |
HMA не
росподілена |
94h |
Лінія A20 все ще
деблокована |
A0h |
Відсутня вільна розширена
пам’ять |
A1h |
Відсутні вільні XMS-
префікси |
A2h |
Недоступний XMS- префікс |
A3h |
Недоступний XMS- префікс
джерела |
A4h |
Недоступне зміщення адреси
джерела |
A5h |
Недоступний XMS- префікс
призначення |
A6h |
Недоступне зміщення адреси
призначення |
A7h |
Недоступна довжина блоку |
A8h |
Перенос блоку з
неприпустимим перекриттям |
A9h |
Помилка паритету |
AAh |
Блок не “закритий” |
ABh |
Блок “закритий” |
ACh |
Переповнення лічильника
числа операцій “закривання” блоку |
ADh |
Помилка операції
“закривання” |
B0h |
Може бути розподілений UMB
меншого розміру |
B1h |
Нема доступних UMB |
B2h |
Недопустиме значення
сегменту адреси UMB |
Відзначимо, що драйвер HIMEM.SYS несумісний із драйвером VDISK.SYS, що використовує розширену пам'ять по зовсім іншій схемі. Тому, якщо виявлений VDISK.SYS, багато функцій керування розширеною пам'яттю не виконуються. Наприклад, відсутня можливість керування НМА, блокування чи деблокування лінії А20 і ін.
Перша функція, що повинна виконуватися програмою, при використанні розширеної пам'яті, — визначення присутності в системі XMS-драйвера. Спосіб, що рекомендується стандартом, складається з наступних кроків:
1) у регістр АН записується 43h, у AL записується 00h і видається переривання 2Fh. На відміну від розглянутого раніше методу визначення наявності EMS немає потреби попередньо визначати, чи є хоча б "заглушка" для переривання 2Fh: MS-DOS при завантаженні завжди ініціалізує вектор 2Fh;
2) аналізується повернення в регістрі AL: якщо XMS-драйвер інстальований, то AL=80h. Це, однак, не означає, що розширена пам'ять доступна для прикладної програми;
3) визначається адреса точки входу у функції XMS-драйвера. Для цього в регістр АН записується 43h, у AL записується 10h і видається переривання 2Fh;
4) адреса точки входу у функції XMS-драйвера повертається в регістрах ES:BX. Розумно зберегти її в зовнішній змінній, доступній для всіх інших Сі-функцій інтерфейсу з XMS.
Тут і далі для спрощення текстів програм, що наводяться, використовується власний заголовний файл "xms.h", у якому поміщені всі необхідні символічні константи і прототипи функцій керування розширеною пам'яттю:
/* XMS.H
Заголовний файл для власних функцій
керування розширеною пам'яттю */
#if !defined ( DEF_XMS) /*запобігає повторному опису */
enum XmsErrorCodes
(NOERROR, NOTINSTALLED);
#define NOTINSTALLED 1
#define NOERROR 0
struct XmsMove {
unsigned long Length;
unsigned short SourceHandle;
unsigned long SourceOffset;
unsigned short DestHandle;
unsigned long DestOffset;
};
/*Прототипи функцій по керуванню
розширеною пам'яттю */
int XmsInstalled( void );
long XmsVersion( void );
int XmsRequestHMA( unsigned Space );
int XmsReleaseHMA( void );
int XmsGlobalEnableA20( void );
int XmsGlobalDisableA20( void );
int
XmsLocalEnableA20( void );
int
XmsLocalDisableA20( void );
int XmsQueryA20( void );
int
XMSQueryLargestFree( void );
int
XMSQueryTotalFree( vold );
int XmsAllocateEMS( unsigned SizeK );
int XmsFreeEMB( int Handle );
int XmsMoveBlock( struct XmsMove
*pMoveDescription );
long XmsLockEMB( int Handle );
int XmsUnLockEMB( int Handle );
long XmsGetHandleInfo( int Handle );
long XmsReallocateEMB( int Handle, unsigned NewSizeK);
long XmsRequestUMB( unsigned SizeP );
int XmsReleaseUMB( unsigned Segment );
#define DEF_XMS 1
/* описи використовуються тільки один раз */
#endif
Наведемо приклад функції XmsInstalled, що повертає константу NOTINSTALLED, якщо XMS-драйвер не виявлено, або константу NOERROR у протилежному випадку. Якщо драйвер виявлений, функція визначає адресу точки входу у функції XMS-драйвера і записує її в зовнішню змінну XmsControl. Усі наступні функції перед звертанням до XMS-драйвера перевіряють на рівність нулю значення XmsControl. Якщо XmsControl дорівнює нулю, це означає, що XMS-драйвер не був виявлений. Ще одна зовнішня змінна, описана у файлі L6_11.C, використовується для установки коду помилки, що виникає при виконанні функцій керування розширеною пам'яттю.
/*L L6_11. C
Визначає наявність драйвера, що керує XМS-пам’яттю, і адресу точки входу у функції
XMS-драйвера.
Повернення:
NOTINSTALLED (1) - якщо не
інстальований XMS-драйвер;
NOERROR (0) - якщо виявлений драйвер
XMS-пам’яті.
Зовнішні змінні:
XmsControl - на виході містить адресу
входу у функції XMS-драйвера;
XmsError - код помилки
функцій керування XMS-пам’яттю. */
#include <dos.h>
#include "xms.h"
void far (*XmsControl) ( void ); /*точка в XMS-драйвер */
unsigned XmsError; /* код помилки */
int XmsІnstalled( void ) {
/* Визначення наявності XMS-драйвера.
*/
_AX = 0x4300;
geninterrupt( 0x2F );
if( _AL != 0x80 ) {
XmsError = NOTINSTALLED;
return( NOTINSTALLED );
}
/*Визначення адреси точки входу у
функції XMS-драйвера */
_АХ = 0x4310;
geninterrupt( 0x2F );
XmsControl = ( void far (*) ( void
) )MK_FP( _ES, _BX );
XmsError = NOERROR;
return( NOERROR );
}
Після того, як виявлений XMS-драйвер і визначена точка входу в його функції керування, прикладна програма може керувати розширеною пам'яттю. Номер функції задається в регістрі АН. Далі даються специфікації функцій керування розширеною пам'яттю:
АН = 00h — визначення номера версії XMS. Функція в регістрі АХ повідомляє номер версії, у регістрі ВХ — внутрішній номер версії драйвера. Регістр DX вказує на наявність чи відсутність НМА: DX=0001h, якщо виявлена область НМА; DX=0h у протилежному випадку. Біти 15-8 регістра АХ задають "старший" номер версії, біти 7-0 — "молодший" номер. Наприклад, якщо АХ=0200h, XMS-драйвер підтримує стандарт 2.00. Регістр DX повідомляє тільки про існування, але не про присутність НМА;
АН = 01h — запит розподілу НМА. Якщо його виконує інсталятор TSR-програми чи секція установки інсталюючого драйвера, на вході у функцію в регістрі DX задається розмір потрібного простору НМА у байтах. Якщо запит НМА виконує звичайна програма, то DX=FFFFh. Якщо запит НМА виконаний, у регістрі АХ повертається 0001h. У протилежному випадку АХ=0000h, а значення в регістрі BL уточнює причину помилки (див. табл. 6.2). Існують деякі обмеження на використання НМА, які обговорюються далі;
АН = 02h — звільнення пам'яті в НМА. Якщо запит звільнення НМА виконано, у регістрі АХ повертається 0001h. У протилежному випадку АХ=0000h, а значення в регістрі BL уточнює причину помилки (див. табл. 6.2);
АН = 03h — глобальне деблокування лінії А20. Виконується, якщо програма здійснює доступ до НМА, попередньо розподіленого для власних потреб, і керує НМА самостійно. Глобальне керування лінією — це установка її стану "на постійно". Після того, як доступ до НМА закінчений, програма повинна виконати глобальне блокування лінії А20. Якщо запит деблокування лінії А20 виконано, у регістрі АХ повертається 0001h. У протилежному випадку АХ=0000h, а значення в регістрі BL уточнює причину помилки (див, табл. 6.2);
АН = 04h — виконує глобальне блокування лінії А20. Це необхідно, якщо програма здійснює доступ до НМА, попередньо розподіленого для власних потреб. Після того, як доступ до НМА закінчений, програма повинна виконати глобальне блокування лінії А20. Якщо запит блокування лінії А20 виконано, у регістр АХ повертається 0001h. У протилежному випадку АХ=0000h, а значення в регістрі BL уточнює причину помилки (див. табл. 6.2);
АН = 05h — локальне деблокування лінії А20. Виконується, якщо програма здійснює безпосередній доступ до розширеної пам'яті. Після того, як доступ до розширеної пам'яті закінчений, програма повинна виконати локальне деблокування лінії А20. Якщо запит деблокування лінії А20 виконано, у регістр АХ повертається 0001h. У протилежному випадку АХ=0000h, а значення в регістрі BL уточнює причину помилки (див табл 6. 2);
АН = 06h — локальне блокування лінії А20. Виконується, якщо програма здійснює безпосередній доступ до розширеної пам'яті Після того, як доступ до розширеної пам'яті закінчений, програма повинна виконати локальне блокування лінії А20. Якщо запит блокування лінії А20 виконано, у регістр АХ повертається 0001h. У протилежному випадку АХ=0000h, а значення в регістрі BL уточнює причину помилки (див табл 6.2);
АН = 07h — запит стану лінії А20 Якщо лінія фізично деблокована, функція повертає АХ=0001h. Якщо лінія блокована, то АХ=0000h. Якщо функція виконана без помилок, регістр BL дорівнює 0. Інше значення в BL свідчить про виникнення помилки і дорівнює її коду (див. табл 6.2). Функція визначає стан лінії, перевіряючи, чи виникає "перескок" адреси через границю в 1Мбайт;
АН = 08h — запит стану розширеної пам'яті. У регістрі АХ функція повертає розмір найбільшого вільного блоку розширеної пам'яті (ЕМВ). Регістр DX вказує загальний об’єм у кілобайтах вільної розширеної пам'яті, розмір НМА не включається в значення, що повертається, навіть якщо вона цілком вільна. Якщо функція виконана без помилок, регістр BL дорівнює 0 Інше значення в BL свідчить про виникнення помилки і дорівнює її коду (див табл 6 .2);
АН = 09h — розподіл блоку розширеної пам'яті (ЕМВ), розмір якого вказується в регістрі DX. У випадку успіху значення в регістрі АХ дорівнює 0001h, а в DX повідомляється ціле позитивне число — так званий XMS-префікс, який використовуємо для наступних посилань на блок. У випадку виникнення помилки АХ=0000h, а значення в регістрі BL уточнює причину помилки (див табл 6.2). Число доступних префіксів за замовченням дорівнює 32, але може бути збільшене за допомогою параметра /NUMHANDLES у рядку DEVICE=HIMEM.SYS файлу конфігурації. Якщо всі XMS-префікси використані, розширена пам’ять, що залишилася вільною, недоступна системі. Тому блоки розширеної пам'яті повинні бути досить великого розміру, а XMS-префікси, які не використовуються, слід якомога швидше звільняти;
АН = 0Ah — звільнення раніше розподіленого блоку в розширеній пам'яті, на який посилається префікс, що вказується в регістрі DX. У випадку успіху значення в регістрі АХ дорівнює 0001h. У випадку виникнення помилки АХ=0000h, а значення в регістрі BL уточнює причину помилки (див. табл. б 2) Будь-яка програма, що розподіляла блоки розширеної пам'яті, повинна перед своїм завершенням звільнити їх. У протилежному випадку розподілена пам'ять не може використовуватися системою. Блоки розширеної пам'яті можуть бути "замкнені", в цьому випадку запит звільнення не виправить ситуацію;
АН = 0Bh — переміщає блок інформації в розширеній пам'яті. Пара регістрів DS:SI задає far-покажчик на блок параметрів, що містить адреси джерела і призначення (табл. 6.3). У випадку успіху значення в регістрі АХ дорівнює 0001h. У випадку виникнення помилки АХ=0000h, а значення в регістрі BL уточнює причину помилки (див табл 6.2). Найчастіше функція використовується для перенесення даних між основною і розширеною пам'яттю, але може використовуватися для переносів і в межах тільки розширеної чи тільки основної пам'яті У випадку, коли поле префікса в блоці параметрів (див табл. 6.3) дорівнює нулю, вважається, що поле зсуву задає звичайний far-покажчик у форматі Сегмент (старше слово):Зсув (молодше слово) у межах першого мегабайта адресного простору. Значення довжини блоку повинне бути парним. Для підвищення швидкості переносу рекомендується задавати зміщення адрес на границі слова, а для 80386 — на границі подвійного слова. Якщо блок-джерело перекривається блоком-призначенням, гарантується коректний перенос тільки "уперед", тобто коли зсув блоку-джерела менше зміщення блоку-призначення;
Табл. 6.3 Блок параметрів для переносу інформації в розширеній пам'яті
Зміщення поля блоку параметрів |
Розмір поля |
Використання |
0 |
4 байти |
Розмір блоку, який
переноситься. Для комп'ютерів на базі процесора 80286 використовується
молодше слово (розмір блоку не перевищує 64К байт). Для процесора 80386
розмір блоку може бути до 1М байта |
4 |
2 байти |
XMS-префікс блоку-джерела.
Якщо він дорівнює 0, наступне подвійне слово розглядається як far-покажчик в
адресному просторі до 1М байта |
6 |
4 байти |
Зсув від початку
блоку-джерела |
10 |
2 байти |
XMS-префікс блоку
–призначення. Якщо він дорівнює 0, наступне подвійне слово розглядається як
far-покажчик в адресному просторі до 1М байта |
12 |
4 байти |
Зсув від початку блоку –
призначення |
АН = 0Сh — "замикає" блок у розширеній пам'яті, на який посилається XMS-префікс, що задається в регістрі DX. Механізм блокувань охороняє блок від видалення з пам'яті в мультизадачних середовищах, що використовують поділ блоків пам'яті між задачами. Для кожного з ЕМВ ведеться лічильник числа операцій "запирання". Блок може бути вилучений з пам'яті тільки у випадку, коли він "відімкнутий" усіма задачами, які його використовують. Кожна операція "запирання" збільшує, а кожна операція "відмикання" зменшує лічильник на одиницю. У випадку успіху на виході з функції регістр АХ дорівнює 0001h і в регістрах DX:BX повертається 32-розрядна базова фізична адреса "замкненого" блоку. У випадку помилки АХ=0000h, а значення в регістрі BL уточнює причину помилки (див. табл. 6.2). Значення адреси дозволяє, поки блок "замкнений", здійснювати доступ до нього безпосередньо, використовуючи захищений режим роботи процесора. "Замкнені" блоки повинні "відмикатися" якомога швидше, що дає можливість менеджерам пам'яті мультизадачних операційних систем використовувати пам'ять раціональніше;
АН = 0Dh — "відмикає" блок розширеної пам'яті, на який посилається XMS-префікс, що задається в регістрі DX. У випадку успіху на виході з функції регістр АХ дорівнює 0001h. У випадку помилки АХ=0000h, а значення в регістрі BL уточнює причину помилки (див. табл. 6.2). Після того, як блок "відмикається", 32-розрядний покажчик на нього, повернутий функцією АН =0Ch, стає недійсним і не повинний використовуватися для безпосереднього доступу до блоку;
АН = 0Eh — повертає інформацію про XMS-префікс, що задається в регістрі DX. У випадку успіху регістр АХ на виході з функції дорівнює 0001h, BH повідомляє значення лічильника операцій "запирання", BL — число вільних XMS-префіксів, DX — довжину блоку в кілобайтах. При виникненні помилки АХ=0000h, а значення в регістрі BL уточнює причину помилки (див. табл. 6.2);
АН = 0Fh — перерозподіляє в розширеній пам'яті блок, на який посилається XMS-префікс, заданий у регістрі DX. У регістрі ВХ вказується новий розмір блоку. Блок повинний бути попередньо "відімкнутий". У випадку успіху регістр АХ на виході з функції дорівнює 0001h. При виникненні помилки АХ=0000h, а значення в регістрі BL уточнює причину помилки (див. табл. 6.2),
АН = 10h — запитує UMB. В регістрі DX задається розмір запитуваного блоку в параграфах. Якщо запит виконано, на виході з функції регістр АХ=0001h, у регістрі ВХ повідомляється сегмент адреси наданого блоку, а в DX — його істинний розмір у параграфах. UMB вирівнюється на границі параграфа. У випадку неможливості виконання запиту АХ=0000h, а в DX вказується розмір максимального доступного UMB у параграфах. Регістр BL містить код помилки (див. табл. 6.2);
АН = 11h — звільняє виділений раніше UMB. Регістр DX задає сегмент адреси початку UMB. Якщо запит виконано, на виході з функції регістр АХ=0001h. У випадку неможливості виконання запиту АХ=0000h, а в DX вказується розмір максимального доступного UMB у параграфах. Регістр BL містить код помилки (див. табл. 6.2).
Далі розглядаються Сі-функції керування XMS-пам'яттю. Функція XmsVersion() повідомляє в значенні типу long інформацію про версію XMS: молодше слово дорівнює номеру версії, а старше слово кодує наявність НМА (0, якщо НМА не виявлена; 1 - в протилежному випадку). Таким чином, повернене функцією значення, більше за 0x0000FFFF, свідчить про наявність НМА. Функція повертає (-1), якщо точка входу в XMS-драйвер не ініціалізована попереднім звертанням до функції XmsInstalled().
/* L6_12.C
Визначає версію драйвера, що керує XMS-пам'яттю.
Повернення:
long Int - молодше слово: версія XMS:
біти 15 - 8 - "старший" номер;
біти 7 - 0 - "молодший" номер;
старше слово: 0 - якщо не виявлена НМА;
1 - у протилежному випадку;
(-1) - якщо попередньо не виконана
XMSInstalled.
Зовнішні змінні:
XmsControl - адреса входу у функції
XMS-драйвера;
встановлюється
функцією XmsInstalled;
XmsError - код помилки функцій керування
XMS-пам'яттю. */
#include
"xms.h"
long XmsVersion(void) {
extern void far (*XmsControl)( void ); /* точка входу */
extern unsigned XmsError;
/* Чи виконана ініціалізація адреси
входу в XMS-драйвер? */
if( ( long )XmsControl == 0L) {
XmsError = NOTINSTALLED;
return( -1 );
}
/* Визначення номера версії XMS */
_АХ = 0x0000;
XmsControl();
return; /* long повертається через АХ і DХ */
}
Функція XmsRequestHMA() розподіляє Space байтів із НМА. У випадку
успіху вона повертає 0. При виникненні помилки функція повертає (-1) і записує
в зовнішню змінну XmsError код помилки (див. табл. 6.2).
/* L6_13.C
Розподіляє Space байтів із НМА
Повернення:
0
у випадку успіху;
(-1) - при виникненні помилки.
Зовнішні змінні:
XmsControl - адреса входу у функції
XMS-драйверу; установлюється функцією XmsІnstalled();
XmsError - код помилки функцій керування XMS-пам'яттю
*/
#іnclude "xms.h"
int XmsRequestHMA( unsigned Space ) {
extern void far (*XmsControl)( void
);. /*точка входу у XMS-драйвер*/
extern unsigned XmsError; /* код помилки функції */
/* Чи виконана ініціалізація адреси входу
в XMS-драйвер? */
if( ( long ) XmsControl == 0L ) {
XmsError = NOTINSTALLED;
return( -1 );
}
/* Спроба розподілу пам'яті в НМА */
_АН = 0x01;
_DХ = Space;
XmsControl();
/* Аналіз коду повернення */
if( _AX == 0 ) {
XmsError = 0x00FF & _BL;
return( -1 );
}
XmsError = 0;
return( 0 );
}
Функція XmsReleaseHMA() звільняє НМА. У випадку успіху вона повертає 0.
При виникненні помилки функція повертає (-1) і записує в зовнішню змінну XmsError
код помилки (див. табл. 6.2).
/* L6_14.C
Звільняє НМА
Повернення:
0 - у випадку успіху;
(-1) - при виникненні помилки.
Зовнішні змінні:
XmsControl - адреса входу у функції
XMS-драйвера; установлюється функцією XmsІnstalled();
XmsError - код помилки функцій керування XMS-пам'яттю.
*/
#include "xms.h"
int XmsReleaseHMA( void ) {
extern void far ( *XmsControl )( void
); /*точка входу у XMS-драйвер */
extern unsigned XmsError; /* код
помилки функції */
/* Чи виконана ініціалізація адреси
входу в XMS-драйвер? */
if( ( long ) XmsControl == 0L) {
XmsError = NOTINSTALLED;
return( -1 );
}
/* Спроба звільнення НМА */
_AH = 0x02;
XmsControl();
/* Аналіз коду повернення. */
if( _AX == 0) {
XmsError = 0x00FF & _BL;
return( -1 );
}
XmsError = 0;
return( 0 );
}
Функція XmsGlobalEnable20() виконує глобальне деблокування лінії А20. У
випадку успіху вона повертає 0. При виникненні помилки функція повертає (-1) і
записує в зовнішню змінну XmsError код помилки (див. табл. 6.2).
/* L6_15.С
Виконує глобальне деблокування лінії
А20.
Повернення:
0 - у випадку успіху;
(-1) - при виникненні помилки.
Зовнішні змінні:
XmsControl - адреса входу у
функції XMS-драйвера; встановлюється функцією XmsІnstalled();
XmsError - код помилки функцій керування XMS-пам’ятю,
*/
#include "xms.h"
int XmsGlobalEnableA20( void ) {
extern void far ( *XmsControl ) ( void
); /*точка входу у XMS-драйвер*/
extern unsigned XmsError; /* код помилки функції */
/* Чи виконана ініціалізація адреси
входу в XMS-драйвер?*/
if( ( long ) XmsControl == 0L) {
XmsError = NOTINSTALLED;
return ( -1 );
}
/* Спроба деблокування лінії А20. */
_АН = 0x03;
XmsControl();
/* Аналіз коду повернення. */
if( _AX == 0) {
XmsError = 0x00FF & _BL;
return( -1 );
}
XmsError = 0;
return( 0 );
}
Функція XmsGlobalDisable20()
виконує глобальне блокування лінії А20. У випадку успіху вона повертає 0. При
виникненні помилки функція повертає -1 і записує в зовнішню змінну XmsError код
помилки (див. табл. 6.2).
/* L6_16.С
Виконує глобальне блокування лінії A20.
Повернення:
0-у випадку успіху;
-1 - при виникненні помилки.
Зовнішні змінні:
XmsControl - адреса входу у
функції XMS-драйвера; встановлюється функцією XmsІnstalled();
XmsError - код помилки функцій
керування XMS-пам'яттю. */
#include "xms.h"
int XmsGlobalDisable20( void ) {
extern void far ( *XmsControl )( void
); /*точка входу у XMS-драйвер */
extern unsigned XmsError; /* код помилки
функції */
/* Чи виконана ініціалізація адреси входу в XMS-драйвер? */
if( ( long ) XmsControl == 0L) {
XmsError =
NOTINSTALLED;
return( -1 );
}
/* Спроба блокування лінії А20. */
_AH = 0x04;
XmsControl( );
/* Аналіз коду повернення */
if( _AX ==0 ) {
XmsError = 0x00FF & _BL;
return( -1 );
}
XmsError = 0;
return( 0 );
}
Функції XmsLocalEnabIeА20() і XmsLocalDisableА20() виконують відповідно
локальне деблокування і блокування лінії А20. У випадку успіху функції
повертають 0. При виникненні помилки вони повертають -1 і записують у зовнішню
змінну XmsError код помилки (див. табл. 6.2).
/*L6_17.C
Виконує локальне деблокування лінії А20.
Повернення:
0-у випадку успіху;
-1 - при виникненні помилки.
Зовнішні змінні:
XmsControl - адреса
входу у функції XMS-драйвера;
встановлюється
функцією XmsІnstalIed();
XmsError - код помилки функцій керування
XMS-пам’яттю. */
#include "xms.h"
int XmslocalEnableA20(void) {
extern void far (*XmsControl)(void); /*точка
входу у XMS-драйвер*/
extern unsigned XmsError; /* код помилки функції */
/* Чи виконана ініціалізація адреси входу в
XMS-драйвер? */
if( ( long ) XmsControl == 0L) {
XmsError = NOTINSTALLED;
return( -1 );
}
/*
Спроба деблокування лінії A20.
*/
_AH = 0x05;
XmsControl();
/* Аналіз коду повернення. */
if( _AX == 0 ) {
XmsError = 0x00FF & _BL;
return( -1 );
}
XmsError = 0;
return( 0 );
}
/*L6_18.C
Виконує локальне блокування лінії А20.
Повернення:
0-у випадку успіху;
-1 - при виникненні помилки.
Зовнішні змінні:
XmsControl - адреса
входу у функції XMS-драйвера;
встановлюється
функцією XmsІnstalIed();
XmsError - код помилки функцій керування
XMS-пам’яттю. */
#include "xms.h"
int XmslocalEnableA20(void) {
extern void far (*XmsControl)(void); /*точка
входу у XMS-драйвер*/
extern unsigned XmsError; /* код помилки функції */
/* Чи виконана ініціалізація адреси входу в
XMS-драйвер? */
if( ( long ) XmsControl == 0L) {
XmsError = NOTINSTALLED;
return( -1 );
}
/*
Спроба деблокування лінії A20.
*/
_AH = 0x06;
XmsControl();
/* Аналіз коду повернення. */
if( _AX == 0 ) {
XmsError = 0x00FF & _BL;
return( -1 );
}
XmsError = 0;
return( 0 );
}
Функція XmsQuery20() визначає стан лінії А20 і повертає 1, якщо лінія деблокована,
0 — якщо лінія А20 блокована. При виникненні помилки функція повертає -1 і записує
в зовнішню змінну XmsError код помилки (див. табл. 6.2).
/* L6_19 .С
Визначає поточний стан лінії А20.
Повернення:
1 - лінія деблокована;
0 - лінія блокована;
-1
- при виникненні помилки.
Зовнішні змінні:
XmsControl - адреса входу у функції
XMS-драйвера; установлюється функцією XmslnstalIed();
XmsError - код помилки функцій керування XMS-пам’ятю. */
#include
"xms.h"
int XmsQueryA20( void ) {
extern void far
(*XmsControl)(void); /*точка входу у
XMS-драйвер */
extern unsigned XmsError; /* код помилки функції */
/* Чи виконана ініціалізація адреси
входу в XMS-драйвер? */
if((long)XmsControl == 0L) {
XmsError =
NOTINSTALLED;
return( -1 );
}
/* Спроба виконання
функції. */
_АН = 0x07;
XmsControl();
/* Аналіз коду повернення.
*/
if( _BL != 0) {
XmsError = 0x00FF & _BL;
return( -1 );
}
XmsError = 0;
return( _AX );
}
Функції, що далі наводяться, XMSQueryLargestFree() і XMSQueryTotalFree()
повертають відповідно розмір найбільшого вільного блоку і загальний обсяг
вільної розширеної пам'яті. При виникненні помилки функції повертають (-1) і
записують у зовнішню змінну XmsError код помилки (див. табл. 6.2).
/* L6_20.C
Визначає розмір найбільшого вільного
блоку розширеної пам'яті.
Повернення:
>= 0 - розмір найбільшого
блоку в кілобайтах;
-1 - при виникненні помилки.
Зовнішні змінні:
XmsControl - адреса входу у функції XMS-драйвера;
установлюється
функцією Xmslnstalled( ) ;
XmsError
- код помилки функцій керування XMS-пам'яттю. */
#include "xms.h"
int XMSQueryLargestFree(void) {
extern void far (*XmsControl )( void ) ; /*точка входу у XMS-драйвер */
extern unsigned XmsError; /* код
помилки функції */
/* Чи виконана ініціалізація адреси
входу у XMS-драйвер? */
if((long)XmsControl == 0L) {
XmsError =
NOTINSTALLED;
return ( -1 );
}
/* Спроба виконання функції. */
_АН = 0x08;
XmsControl();
/* Аналіз коду повернення */
if( _BL != 0) {
XmsError = 0x00FF & _BL;
return( -1 );
}
XmsError = 0;
return( _AX );
}
/* L6_21.C
Визначає загальний обсяг вільної розширеної пам'яті
Повернення:
>= 0 - обсяг вільної пам'яті в
кілобайтах;
-1 - при виникненні помилки.
Зовнішні змінні:
XmsControl - адреса входу у
функції XMS-драйвера;
установлюється функцією XmsInstalled();
XmsError - код помилки функцій керування XMS-пам'яттю.
*/
#include
"xms.h"
int XMSQueryTotalFree( void ) {
extern void far ( *XmsControl )( void
); /*точка входу у XMS-драйвер */
extern unsigned XmsError; /* код помилки функції */
/* Чи виконана ініціалізація адреси
входу в XMS-драйвер? */
if( ( long ) XmsControl == 0L)
XmsError = NOTINSTALLED;
return( -1 );
}
/* Спроба виконання функції */
_AH = 0x08;
XmsControl();
/* Аналіз коду повернення */
if( _BL != 0) {
XmsError = 0x00FF & _BL;
return( -1 );
}
XmsError = 0;
return( _DX );
}
Функція XmsAIlocateEMB() розподіляє в розширеній пам'яті блок розміром
SizeК кілобайтів. У випадку успіху повертає XMS-префікс. При виникненні помилки
функція повертає (-1) і записує в зовнішню змінну XmsError код помилки (див.
табл. 6.2).
/* L6_22.C
Розподіляє в розширеній пам'яті блок розміром SizeК кілобайтів .
Повернення:
>0 - XMS-префікс для посилань
на виділений блок;
-1 - при виникненні помилки.
Зовнішні
змінні:
XmsControl - адреса входу у
функції XMS-драйвера;
встановлюється функцією XmsInstalIed();
XmsError - код помилки функцій керування XMS-пам’ятю
*/
#include
"xms.h"
int XmsAllocateEMB (unsigned SizeK ) {
extern void far (*XmsControl)( void
); /*точка входу у XMS-драйвер */
extern unsigned XmsError; /* код помилки функції */
/* Чи виконана ініціалізація адреси
входу в XMS-драйвер? */
if( ( long ) XmsControl == 0L) {
XmsError = NOTINSTALLED;
return( -1 );
}
/* Спроба виконання функції */
_AH = 0x09;
_DX = SizeК;
XmsControl();
/* Аналіз коду повернення */
if( _AX == 0 ) {
XmsError = 0x00FF & _BL;
return( -1 );
}
XmsError = 0;
return( _DX );
}
Функція XmsFreeEMB( Handle ) звільняє блок розширеної пам'яті, на який
посилається XMS-префікс Handle. У випадку успіху вона повертає 0. При виникненні
помилки функція повертає (-1) і записує в зовнішню змінну XmsError код помилки
(див. табл. 6.2).
/* L6_23.C
Звільняє в розширеній пам'яті блок, на який посилається Handle.
Повернення:
0 - блок звільнений;
-1 - при виникненні помилки.
Зовнішні змінні:
XmsControl - адреса входу у функції XMS-драйвера;
установлюється функцією
XmsInstalled();
XmsError - код помилки функцій керування XMS-пам'яттю. */
#include "xms.h"
int XmsFreeEMB( int Handle ) {
extern void far ( *XmsControl )( void
); /*точка входу у XMS-драйвер */
extern unsigned XmsError; /* код помилки функції */
/* Чи виконана ініціалізація адреси входу в XMS-драйвер? */
if( ( long ) XmsControl == 0L) {
XmsError = NOTINSTALLED;
return( -1 );
}
/* Спроба виконання функції. */
_AH = 0х0А;
_DX = Handle;
XmsControl();
/* Аналіз коду повернення. */
if( _AX = 0 ) {
XmsError = 0x00FF & _BL;
return( -1 );
}
XmsError = 0;
return( 0 );
}
Функція XmsMoveBlock() переміщає блок з одного місця пам'яті в інше.
Адреси джерела і призначення, а також розмір блоку задаються структурній
змінний по шаблоні XmsMove, на яку вказує pMoveDescription. Шаблон структури
XmsMove приведений у заголовному файлі XMS.Він відповідає структурі блоку
параметрів табл. 6.3. При виникненні помилки функція повертає (-1) і записує в
зовнішню змінну XmsError код помилки (див. табл. 6.2).
/*L6_24.C
Переміщає блок, адресу джерела, призначення і розмір задає структура, на яку
вказує pMoveDescription. Не використовується в HUGE-моделі.
Повернення:
0 – блок перенесений;
-1 - при виникненні помилки.
Зовнішні змінні:
XmsControl - адреса входу у
функції XMS-драйвера;
встановлюється
функцією Xmslnstalled();
XmsError - код помилки функцій керування XMS-пам’ятю
*/
# include "xms.h"
#include <dos.h>
int XmsMovelock( struct XmsMove *pMoveDescription ) {
extern void far ( *XmsControl )( void ); /*точка входу у XMS-драйвер */
extern
unsigned XmsError; /* код
помилки функції */
/* Чи
виконана ініціалізація адреси
входу в XMS-драйвер? */
if( ( long ) XmsControl == 0L ) {
XmsError =
NOTINSTALLED;
return( -1 );
}
/* Спроба виконання
функції. */
_АН = 0х0В;
_SI = FP_OFF( pMoveDescription );
_АН = 0х0В;
XmsControl( );
/* Аналіз коду повернення */
if( _AX == 0 ) {
XmsError = 0x00FF & _BL;
return ( -1 );
}
XmsError = 0;
return( 0 );
}
Функція XmsLockEMB()
"замикає" у розширеній пам'яті блок, на який посилається XMS-префікс
Handle. У випадку успіху вона повертає базову фізичну адресу початку блоку
(біти 0 - 30). Знаковий (31-й) біт значення, що повертається, є індикатором
правильності роботи функції. При виникненні помилки функція повертає (-1) і записує в зовнішню змінну
XmsError код помилки (див. табл. 6.2).
/* L6_25.C
"Замикає" у розширеній пам'яті
блок, на який посилається XMS-префікс Handle.
Повернення:
long - базова фізична адреса початку блоку;
-1 - при виникненні помилки.
Зовнішні змінні:
XmsControl - адреса входу у функції XMS-драйвера;
встановлюється функцією XmsInstalled();
Xms Error - код помилки функцій керування
XMS-пам'яттю. */
#include "xms.h"
long XmsLockEMB( int Handle ) {
extern void far (*XmsControl
)(void); /*точка входу у XMS-драйвер */
extern
unsigned XmsError; /* код
помилки функції */
unsigned dx, bx;
/* Чи виконана ініціалізація адреси входу в XMS-драйвер? */
if ( ( long ) XmsControl == 0L) {
XmsError = NOTINSTALLED;
return( -1 );
}
/* Спроба виконання
функції. */
_AH = 0x0C;
_DX = Handle;
XmsControl( );
/* Аналіз коду повернення. */
dx = _DX;
bx = _BX;
if( _AX == 0 ) {
XmsError = 0x00FF & _BL;
return( -1 );
}
XmsError = 0;
return( ( ( long )( dx & 0x7F )
<< 16 ) | ( unsigned ) bx );
}
Функція XmsUnLockEMB() "відмикає" у розширеній пам'яті блок,
на який посилається XMS-префікс Handle. У випадку успіху вона повертає 0. При виникненні
помилки функція повертає (-1) і записує в зовнішню змінну XmsError код помилки
(див. табл. 6.2).
/*L6_26.C
"Відмикає" у розширеній пам'яті блок, на який посилається XMS-префікс
Handle.
Повернення:
0 - блок
"відімкнутий";
-1 - при виникненні помилки.
Зовнішні змінні:
XmsControl - адреса входу у функції
XMS-драйвера;
встановлюється
функцією XmsInstalled();
XmsError - код помилки функцій керування XMS-пам'яттю.
*/
#include "xms.h"
int XmsUnLockEMB( int Handle ) {
extern void far (*XmsControl )( void );
/*точка входу в XMS-драйвер */
extern unsigned XmsError; /* код помилки функції */
/* Чи виконана ініціалізація адреси
входу в XMS-драйвер? */
if( ( long ) XmsControl == 0L ) {
XmsError = NOTINSTALLED;
return( -1 );
}
/* Спроба виконання функції */
_АН = 0x0D;
_DX = Handle;
XmsControl( );
/* Аналіз коду повернення */
if( _AX == 0 ) {
XmsError = 0x00FF & _BL;
return( -1 );
}
XmsError = 0;
return( 0 );
}
Функція XmsGetHandleInfo() повертає в long
інформацію про блок розширеної пам'яті, на який посилається XMS-префікс Handle.
Біти 30- 16 задають розмір блоку в кілобайтах, біти 15-8 — значення лічильника
операцій "запирання", біти 7-0 містять число вільних XMS-префіксів у
системі. При виникненні помилки функція повертає (-1) і записує в зовнішню
змінну XmsError код помилки (див. табл. 6.2).
/* L6_27.C
Повертає інформацію про блок у розширеній пам'яті, на який посилається
XMS-префікс Handle.
Повернення:
long
: біти 30 - 16 - довжина блоку;
15 - 8 - лічильник
операцій "запирання";
7 - 0 - число вільних
префіксів;
(-1) - при виникненні
помилки.
Зовнішні змінні:
XmsControl - адреса входу у функції
XNS-драйвера;
встановлюється функцією XmsInstalled();
XmsError - код помилки функцій керування XMS-пам'яттю.
*/
#include "xms.h"
long XmsGetHandleInfo( int Handle ) {
extern void far (*XmsControl)(void); /*точка входу у XMS-драйвер*/
extern unsigned XmsError; /* код помилки функції */
unsigned bx, dx;
/* Чи виконана ініціалізація адреси
входу в XMS-драйвер? */
if( ( long ) XmsControl == 0L ) {
XmsError =
NOTINSTALLED;
return ( -1 );
}
/* Спроба виконання функції. */
_АН = 0х0Е;
_DX = Handle;
XmsControl();
bx = _ВХ;
dx = _DX;
/* Аналіз коду повернення. */
if( _AX == 0 ) {
XmsError = 0x00FF & _BL;
return( -1 );
}
XmsError = 0;
return( ( ( long ) dx << 16) |
( unsigned ) bx );
}
Функція XmsReallocateEMB() повторно розподіляє EMB, на який посилається
XMS-префікс Handle, встановлюючи новий розмір блоку NewSize. У випадку успіху
вона повертає 0. При виникненні помилки функція повертає (-1) і записує в
зовнішню змінну XmsError код помилки (див. табл. 6.2).
/*L6_28.C
Перерозподіляє в розширеній пам'яті блок, на який посилається XMS-префікс
Handle, встановлюючи новий розмір блоку NewSize.
Повернення:
0 - блоку заданий новий розмір;
-1 - при виникненні помилки;
Зовнішні
змінні:
XmsControl - адреса входу у
функції XMS-драйвера;
встановлюється
функцією XmsInstalled();
XmsError - код помилки функцій керування XMS-пам’яттю
*/
#include "xms.h"
long XmsReallocateEMB( int Handle, unsigned NewSize) {
extern void far (*XmsControl)(void); /*точка входу в XMS-драйвер */
extern unsigned XmsError; /* код помилки функції */
/* Чи виконана ініціалізація адреси входу в XMS-драйвер? */
if( ( long ) XmsControl ==
0L) {
XmsError = NOTINSTALLED;
return( -1 );
}
/* Спроба виконання функції. */
_АН = 0x0F;
_DX = Handle;
_BХ = NewSize;
XmsContro();
/* Аналіз коду повернення. */
if( _AX == 0 ) {
XmsError = 0x00FF & _BL;
return ( -1 );
}
XmsError = 0;
return( 0 );
}
Функція XmsRequestUMB() намагається розподілити UMB розміром SizeP
параграфів. У випадку успіху повертає позитивне число (long), молодше слово
якого містить сегмент початку виділеного UMB, а старше слово — фактичний розмір
UMB. При неможливості задоволення запиту функція повертає негативне число,
молодше слово якого вказує розмір найбільшого доступного UMB. Код помилки
записується в зовнішню змінну XmsError.
/* L6_29.C
Запитує UMB розміром SizeP.
Повернення:
long >0: біти 31-16 - фактичний
розмір виділеного UMB;
біти 15- 0 - сегмент початку виділеного
UMB;
<0: біти 15- 0 - розмір найбільшого доступного
UMB.
Зовнішні
змінні:
XmsControl - адреса входу
у функції XMS-драйвера встановлюється функцією XmsInstalled();
XmsError - код помилки функцій керування XMS-пам'яттю.
*/
#include "xms.h"
long XmsRequestUMB( unsigned SizeP ) {
extern void far
(*XmsControl)(void); /* точка входу у
XMS-драйвер */
extern unsigned XmsError; /* код помилки функції */
/* Чи виконана ініціалізація адреси
входу в XMS-драйвер? */
if( ( long ) XmsControl == 0L ) {
XmsError = NOTINSTALLED;
return( -1 );
}
/* Спроба виконання
функції. */
_AH = 0x10;
_DX = SizeP;
XmsControl();
/* Аналіз коду повернення. */
if( _ax
== 0) {
XmsError = 0x00FF & _BL;
return( 0x0000FFFF | ( long ) _DX );
}
XmsError = 0;
return( ( ( ( long) _DX ) << 16 )
| _BX );
}
Функція XmsReleaseUMB() звільняє UMB,
сегмент початок якого задає параметр Segment. У випадку успіху функція повертає
0. При виникненні помилки функція повертає (-1) і записує в зовнішню змінну
XmsError код помилки (див. табл. 6.2).
/*L6_30.C
Звільняє UMB, що починається з параграфа Segment.
Повернення:
0 - UMB звільнений;
-1 - при виникненні помилки.
Зовнішні змінні:
XmsControl - адреса входу у функції
XMS-драйвера; встановлюється функцією XmsInstalled();
XmsError - код помилки функцій керування XMS-пам'яттю.
*/
#include "xms.h"
int XmsReleaseUMB( unsigned Segment ) {
extern void far ( *XmsControl )( void
); /*точка входу у XMS-драйвер */
extern unsigned XmsError; /* код помилки функції */
/* Чи виконана ініціалізація адреси
входу в XMS-драйвер? */
if( ( long ) XmsControl == 0L) {
XmsError = NOTINSTALLED;
return( -1 );
/* Спроба виконання функції */
_АН = 0x11;
_DX = Segment;
XmsControl( );
/* Аналіз коду повернення. */
if( _AX == 0 ) {
XmsError = 0x00FF & _BL;
return( -1 );
}
XmsError = 0;
return( 0 );
}
Наведемо демонстраційну програму для тестування розроблених функцій керування розширеною пам'яттю й ілюстрації основних прийомів роботи з нею. Насамперед програма визначає наявність XMS-драйвера і версію XMS, ним реалізовану. Потім виконується розподіл блоку пам'яті розміром 100Кбайт і визначаються параметри виділеного блоку. Використовуючи ЕМВ, програма переносить 80 байт з основної пам'яті в розширену, а потім з розширеної в основну. Перенесена з розширеної пам'яті інформація (DestStr) виводиться на екран. Програма завершується звільненням виділеного блоку. Якщо цього не зробити, блок залишається зайнятим і після завершення програми.
/* L6_31. С
Демонструє керування XMS-пам’яттю і доступ до неї. */
#include<stdio.h>
#include<dos.h>
#include "xms.h"
int main( void ) {
extern unsigned XmsError;
int XmsHandle, TotalFreeK,
LargestFreeK, UmbSize;
long Ret, BaseAdr;
unsigned Version;
char SourceStr[80] = "Тестовий
рядок для переносу через ЕМВ";
char DestStr[80];
struct XmsMove Parameters;
/* Перевірка того, чи встановлений XМS-драйвер */
if( XmsInstalled( ) == NOTINSTALLED ) {
рuts( “Відсутній драйвер для
керування розширеною пам'яттю" );
return( 1 );
}
/* Збір зведень про XMS-пам'ять */
Ret = XmsVersion( );
Version = ( int )Ret;
puts("B системі встановлена
розширена (Extended) пам'ять");
printf( “Версія XMS: %d.%d \n",
Version >> 8, Version & 0x00FF );
if( Ret > 0x0000FFFFL)
puts( "Виявлена HMA (High Memory
Area ) " );
else
puts( "He виявлена НМА ( High
Memory Area ) " );
if( ( TotalFree = XMSQueryTotalFre() )
== -1)
printf( "Помилка при визначенні
загального об’єму вільної розширеної пам'яті.
Код-%х \n", XmsЕrror );
else
printf( "Загальний
обсяг вільної розширеної пам'яті: %dК. \n ", TotalFree);
if( ( LargestFreeK =
XMSQueryLargestFree() ) == -1 )
printf("Помилка при визначенні
максимального вільного блоку розширеної пам'яті. Код-%х \n ", XmsError );
else
printf( "Максимальний вільний
блок розширеної пам'яті %dK \n", LargestFreeK );
/* Розподіл блоку 100K байт у
розширеній пам'яті, визначення його базової адреси, перенос через нього
тестового рядка SourceStr у масив DestStr[ ]. */
if( ( XmsHandle = XmsAllocateEMB(100) )
== -1 )
printf( "Помилка при розподілі
ЕМВ. Код-%х\n", XmsError );
else {
/* Одержання інформації про блок в
розширеній пам'яті */
if( ( BaseAdr = XmsLockEMB( XmsHandle
) ) == -1)
printf( "Помилка при
\"запиранні\" ЕМВ. Код-%х\n", ХmsError);
else
printf( "Базова адреса блоку
ЕМВ: %#010lх \n", BaseAdr );
if( ( Ret = XmsGetHandleInfo(
XmsHandle ) ) == -1 )
printf( "Помилка при одержанні
інформації про ЕМВ. Код-%х\nп, XmsError);
else
printf( "Довжина блоку: %dK.
Лічильник операцій \"запирання\" %d\n"
"Число вільних
XMS-префіксів %d\n",
( int )( ( Ret & 0xFFFF0000) >> 16),
( int )( Ret &
0x0000FF00) >> 8,
( int )Ret &
0x000000FF);
/* Перенос блоку з основної в розширену пам'ять */
Parameters.Length = 80;
Parameters.SourceHandle = 0;
Parameters.SourceOffset = (unsigned
long(SourceStr);
Parameters.DestHandle = XmsHandle;
Parameters.DestOffset = 0L;
if( XmsMoveBlock( &Parameters ) )
printf( "Помилка при переносі
блоку пам'яті. Код-%х\п", XmsError );
/*Перенос
блоку з розширеної в основну пам'ять. */
Parameters.Length =
80;
Parameters.SourceHandle
= XmsHandle;
Parameters.SourceOffset
= 0L;
Parameters.DestHandle
= 0L;
Parameters.DestOffset =
( unsigned
long )( DestStr );
if( XmsMoveBloc( &Parameters ) )
printf( "Помилка при
переносі блоку пам'яті. Код-%х\n", XmsError );
/* Виведення рядка, перенесеного через EMS. */
puts(DestStr);
/* Повторний розподіл блоку з новим
розміром 200К байт.*/
if( XmsUnLockEMB( XmsHandle ) == -1 )
printf( "Помилка при
\"відмиканні\" ЕМВ. "Код-%х\n", XmsError);
else {
puts( "Перерозподіл блоку
розширеної пам'яті." );
if( XmsReallocateEMB( XmsHandle,
200 ) == -1 )
printf("Помилка при перерозподілі
ЕMВ. Код-%х\п", XmsError );
if( ( BaseAdr =
XmsLockEMB(XmsHandle ) ) == -1 )
printf( "Помилка при
\"запиранні\" ЕМВ. Код-%х\n", XmsError );
else
printf( "Нова базова адреса
блоку ЕМВ: %#010Lx\n", BaseAdr );
}
/ *Звільнення виділеного блоку пам'яті.
Вимагає обов'язкового
"відмикання" блоку. */
if( XmsUnLockEMB( XmsHandle ) == -1 )
printf( "Помилка при
\"відмиканні\" ЕМВ. Код-%х\n", XmsError );
if( XmsFreeEMB( XmsHandle ) )
printf ( "Помилка при
звільненні блоку пам'яті. Код-%х\n”, XmsError );
else
puts( "Розподілений блок ЕМВ
звільнений" );
}
return( 0 ); /* тест
завершений */
}
Для одержання .ЕХЕ-файлу використовується файл проекту (наприклад, L6_31.PRJ), у який включені файли L6_31.C, L6_12.C, L6_20.C - L6_28.C і L6_31.C. Далі наведено приклад виконання програми при одному з варіантів її запуску на виконання:
С:\>L6.31.EXE
У системі встановлена розширена (Extended) пам'ять
Версія XMS: 2.0
Виявлена НМA (High Memory Area)
Загальний обсяг вільної розширеної пам'яті: 1563К.
Максимальний вільний блок розширеної пам'яті: 1563К.
Базова адреса блоку ЕМВ: 0x00279400
Довжина блоку: 100К. Лічильник операцій "запирання" : 1
Число вільних XMS-префіксів : 28
Тестовий рядок для переносу через ЕМВ - рядок
DestStr[]
Перерозподіл блоку розширеної пам'яті.
Нова базова адреса блоку ЕМВ: 0x00279400
Розподілений блок ЕМВ звільнений
Блок пам'яті НМА може використовуватися для збереження ядра MS-DOS версії 4.0 і старше. Для цього необхідно встановити в системі через файл конфігурації CONFIG.SYS драйвер HIMEM.SYS і інформувати MS-DOS про необхідність використання НМА. Робиться це включенням у файл конфігурації двох рядків:
DEVICE=С:\HIMEM.SYS
DOS = HIGH
Операційна система при завантаженні займає всю НМА. Якщо ядро МS-DOS розміщається в основній пам'яті чи UMB-блоці (див. далі), НМА може використовуватися для збереження інших або резидентних програм. При цьому слід враховувати деякі особливості НМА:
1) у MS-DOS не можуть бути передані far-покажчики на дані. збережені в НМА, тому що MS-DOS нормалізує переданні їй покажчики. Зокрема, неможливий перенос даних на диск безпосередньо з НМА; по цій же причині на НМА не повинні вказувати вектори переривання встановлених резидентних програм;
2) драйвери і резидентні програми повинні використовувати більшу частину НМА; використана один раз НМА залишається недоступною для інших програм доти, поки не буде звільнена;
3) використання НМА вимагає, щоб лінія А20 була деблокована. Тому оброблювачі переривань не повинні розміщуватися в НМА, тому що можлива ситуація блокування лінії фоновою програмою. Найбільш типова побудова TSR, що використовують НМА, така: оброблювачі переривань розміщуються в основній пам'яті; у НМА розміщуються дані резидентної програми і резидентні функції. Одержавши керування, оброблювачі запам'ятовують стан лінії А20, деблокують її і передають управління на код, збережений у НМА. Перед завершенням виконується відновлення стану лінії А20. Загрузку в НМА повинна виконувати інсталююча частина TSR.
Далі приводиться приклад тестової програми, що розподіляє для власних потреб НМА, що записує туди дані і виводять їх потім на екран.
/*L6_32.C
Демонструє використання НМА */
#include <stdio.h>
#include <dos.h>
#include <mem.h>
#include "xms.h"
int main( void ) {
extern unsigned XmsError;
int oldA20;
long Ret;
unsigned Version;
char SourceStr[80] = "Тестовий
рядок для переносу через НМA";
char DestStr[80];
/* Перевірка того, чи встановлений
XMS-драйвер. */
if( XmsInstalled() == NOTINSTALLED ) {
puts(" Відсутній драйвер для
керування розширеною пам'яттю" );
return( 1 );
}
/* Перевірка наявності НМА. */
Ret = XmsVersion();
Version = (int) Ret;
puts( " B системі встановлена
розширена (Extended) пам'ять " );
printf( "Версія XMS: %d.%d
\n", Version >> 8, Version & 0x00FF);
if( Ret > 0x0000FFFFL)
puts( "Виявленна НМА (High
Memory Area)");
else {
puts("He виявлена НМА (High
Memory Area). Завершення. ");
return (2);
}
/* Запит стану лінії А20 і її деблокування у разі
потреби. */
if( !(oldА20 = XmsQueryА20() ) ) {
puts( "Лінія A20 блокована.
Спроба деблокування ");
if( XmsGlobalEnableA20() ) {
рuts( “закінчилась невдачею
Завершення тесту.");
return(3);
}
}
/* Розподіл 20К байт з НМА і перенос
через неї тестового рядка SourceStr у
масив DestStr[]. */
if( XmsRequestHMA( 20*1024 ) ) {
printf("Помилка при розподілі
НМА. Код-%х\n", XmsError );
if( !oldA20 )
XmsGlaba1Disable20(); /*
відновлення стану А20 */
return( 4 );
}
/* Перенос блоку інформації з основної
пам'яті в НМA. */
movedata( FP_SEG( SourceStr ),
FP_OFF( SourceStr ),
/* адреса джерела */
0xFFFF, 0x0010, /*
адреса початку НМA */
80 ); /* всього 80 байт */
/* Перенос блоку інформації з НМА в
основну пам'ять і його виведення. */
movedata( 0xFFFF, 0x0010, /* адреса початку НМА*/
FP_SEG( DestStr
), /* адреса джерела */
FP_OFF( DestStr ),
80 );
/* всього 80 байт */
puts(DestStr);
/* Демонстрація того, що НМА розподіляється тільки один раз: спроба
виділити ще 20К байт завершується невдачею, хоча використано тільки 20К байт із
загального розміру 64К байт. */
if( XmsRequestHMA( 20*1024
) )
printf( "Помилка при розподілі
20К байт НМД. Код-%х.\n"
"Причина в тім, що
НМA розподіляється один раз", XmsError );
/* Звільнення НМA. */
if( XmsReleaseHMA( ) ) {
printf( "Помилка при звільненні
НМA. Код-%х\n", XmsError);
if( !oldA20 ) XmsGlobalDisable20();
/* відновлення стану лінії А20 */
return(5);
}
/* Відновлення стану лінії А20. */
if( !oldA20 )
XmsGlobalDisableA20();
return( 0 ); /* тест
завершений */
}
Для одержання завантажувального модуля використовується файл проекту (наприклад, L6_32.PRJ), у який включені файли L6.11.C - L6_16.C, L6_19.C і L6_32.C. Далі наводиться приклад виконання програми у випадку, коли в системі мається НМА, що не була використана MS-DOS чи іншими резидентними програмами:
D:\SOURCE\XMS>L6_32.EXE
У системі встановлена розширена (Extended) пам'ять
Версія XMS: 3.0
Виявлена НМА (High Memory Area)
Лінія А20 блокована. Спроба деблокування
Тестовий рядок для переносу через НМА
Помилка при розподілі 20К байт НМА. Код-91.
Причина в тім, що НМА розподіляється один раз.
В параграфі даний мінімальний набір функцій інтерфейсу прикладної програми по керуванню розширеною пам'яттю. Використовуючи приведені приклади Сі-функцій, засобами TurboC легко побудувати власну бібліотеку підтримки керування відображуваною і розширеною пам'яттю.