В этой главе мы расскажем вам о том, как создавать резидентные программы . Такие программы еще называют TSR-программами (Terminate and Stay Resident ).
Что это такое резидентная программа?
Обычно после завершения очередной программы MS-DOS освобождает место в памяти, которое занимала программа, чтобы загрузить на это место новую. Однако есть способ оставить программу в памяти и после ее завершения. Такая программа и будет резидентной, т. е. постоянно присутствующей в памяти.
Для чего используются резидентные программы ?
Резидентные программы могут переключать на себя обработку прерываний, например, связанных с выводом на печать или с обращением к клавиатуре и т. д.
Вы можете встретить множество резидентных программ, предназначенных для загрузки русских шрифтов в память видеоконтроллера , для печати текста, содержащего символы кириллицы, на принтере в графическом режиме, для русификации клавиатуры и т. п. Все эти программы обычно запускаются один раз при загрузке компьютера через файл autoexec.bat .
Другой пример использования резидентных программ: резидентные калькуляторы, справочные базы данных или интегрированные системы, наподобие Borland SideKick.
Такие программы тоже обычно запускаются через файл autoexec.bat или при необходимости. Они перехватывают прерывания, предназначенные для работы с клавиатурой. Как только пользователь нажимает заранее определенную комбинацию клавиш, резидентная программа активизируется. Поверх имеющегося на экране изображения выводится диалоговое окно резидентной программы.
Иногда резидентные программы используют вместо загружаемых драйверов для обслуживания нестандартной аппаратуры. В этом случае резидентная программа может встроить свой обработчик, через который все прикладные программы смогут обращаются к аппаратуре.
Аналогично работают резидентные модули некоторых систем управления базами данных (СУБД). Прикладная программа посылает запросы к базе данных через прерывание, устанавливаемое при запуске такой СУБД.
Для обслуживания нестандартной аппаратуры больше подходят загружаемые драйверы, о которых мы еще будем подробно говорить в следующей главе. Драйверы предоставляют намного более гибкие и богатые средства общения с устройствами, чем резидентные программы . Однако если вам требуется всего лишь обработать несколько прерываний или создать программу управления каким-нибудь несложным устройством, то резидентные программы - это то, что вам нужно.
Вы, наверное, слышали, что составить правильно работающую резидентную программу не так просто. Мы полностью с этим согласны. Причин много, главная заключается в том, что фирма Microsoft не предоставила в распоряжение программистов всю необходимую для этого информацию.
На резидентные программы накладываются многочисленные ограничения, затрудняющие работу программиста.
Например, резидентным программам не разрешается использовать прерывания MS-DOS, когда вздумается. Это связано с тем, что MS-DOS с самого начала проектировалась как однозадачная операционная система, поэтому функции прерываний MS-DOS не обладают свойством реентерабельности (повторной входимости).
Представьте себе такую ситуацию.
Пусть обычная программа вызвала какую-либо функцию прерывания MS-DOS, на выполнение которой требуется относительно много времени (например, запись на диск).
Так как пользователь может активизировать резидентную программу в любой момент, то если не принять специальных мер предосторожности, возможен повторный вызов той же самой функции, обработка которой еще не завершена. В этом случае мы получим повторный вызов функции MS-DOS, который недопустим из-за того, что функции MS-DOS не реентерабельны.
Функции BIOS также далеко не все реентерабельны. Резидентная программа может смело вызывать разве лишь прерывание INT 16h (которое предназначено для работы с клавиатурой). Если резидентной программе нужно вывести что-нибудь на экран, то вместо прерывания INT 10h следует выполнить непосредственную запись символов и их атрибутов в видеопамять.
Без принятия специальных мер предосторожности резидентная программа не может вызывать многие функции библиотеки транслятора, так как последние вызывают прерывания MS-DOS. Например, функция malloc вызывает прерывание MS-DOS для определения размера свободной памяти в системе.
Могут возникнуть трудности и с использованием арифметических действий с числами в формате плавающей запятой. Это происходит потому, что функция _dos_keep восстанавливает прерывания, использовавшиеся для эмуляции арифметики с плавающей запятой и для работы с арифметическим сопроцессором.
Как оставить программу резидентной в памяти?
У программы есть две возможности остаться резидентной в памяти - использовать прерывание INT 27h или функцию 31h прерывания INT 21h .
Для использования прерывания INT 27h сегментный регистр CS должен указывать на PSP программы. При этом в регистр DX следует записать смещение последнего байта программы плюс один байт.
Нетрудно заметить, что этот способ больше всего подходит для com-программ, так как с помощью прерывания INT 27h невозможно оставить в памяти резидентной программу длиннее 64 Кбайт.
Другой, более удобный, способ заключается в вызове функции 31h прерывания INT 21h . В регистре AL вы можете указать код завершения программы, регистр DX должен содержать длину резидентной части программы в параграфах. Здесь уже нет указанного выше ограничения на размер программы.
Для того чтобы оставить резидентной в памяти программу, размер которой превышает 64 Кбайт, вы можете использовать только последний метод. Но не стоит увлекаться большими резидентными программами, так как занимаемая ими память нужна другим программам.
Библиотека функций системы разработки Borland C++ содержит в своем составе специальную функцию, предназначенную для оставления программы резидентной в памяти.
Эта функция использует прерывание INT 21h (функция 31h) и называется _dos_keep . Первый параметр функции - код завершения (то, что записывается в регистр AL), а второй - размер резидентной части программы в параграфах.
Нужно внимательно следить за размером оставляемой резидентной части программы. Если указать функции _dos_keep неправильный размер, то резидентная программа может оказаться разрушенной программой, загруженной следом за ней. Все это, в конечном счете, приведет к разрушению операционной системы и к необходимости ее перезапуска.
В этом разделе мы приведем исходные тексты программы TSRDEMO, которую вы можете использовать в качестве заготовки для вашей собственной разработки.
Программа TSRDEMO может работать в MS-DOS версии 4.0 и более старших версий. Мы проверили ее в среде MS-DOS версии 6.22.
Наша резидентная программа выполняет все необходимые действия, включая защиту от повторного запуска и удаление себя из оперативной памяти. Функция, которая получает управление при активизации резидентной программы, может использовать функции прерываний MS-DOS и стандартные функции библиотеки транслятора. Все это возможно благодаря правильному выбору момента для активизации программы, предварительному сохранению некоторых системных областей данных и резервированию памяти для областей стека и кучи.
Если программу TSRDEMO запустить без параметров в первый раз, она выведет сообщение и останется резидентной в памяти. При повторной попытке запуска программа выведет сообщение о том, что она уже активна. Для того чтобы деинсталлировать программу (выгрузить ее из памяти), достаточно указать при запуске параметр u.
В ответ на любой другой параметр на экран выводится инструкция по запуску программы и список комбинаций клавиш, которые используются для ее активизации.
Вы можете активизировать программу при помощи комбинации клавиш <Ctrl+R>. При этом раздается звуковой сигнал и содержимое текстовой части видеопамяти записывается в файл (байты атрибутов игнорируются). Этот файл создается в текущем каталоге и называется !grab<xxx>.scr, где xxx - последовательный номер копии экрана. Если нажать комбинацию клавиш <Ctrl+R> в первый раз, номер будет равен 0, во второй раз - 1 и т. д.
Когда программа TSRDEMO загружена в память, в правом верхнем углу экрана мигают символы "*" и "+". Соответствующие функции демонстрируют способ непосредственной записи кодов символов и атрибутов в видеопамять.
Полные листинги программы TSRDEMO занимают много места, поэтому мы привели их ниже в разделе, который так и называется - "Листинги программы TSRDEMO". Мы будем изучать их последовательно, рассматривая все этапы работы резидентной программы.
При запуске программы TSRDEMO управление получает функция main, исходный текст которой приведен ниже:
void main(int argc, char *argv[]) { int tsrsize, i; printf("\n\nРезидентная программа TSRDEMO," " v1.1, (C) Фролов А.В., 1995\n"); if(_osmajor < 4) { printf("\nИзвините, вы пользуетесь слишком" "старой версией MS-DOS"); return; } if(argc > 1) { if(argv[1][0] == 'u') unload(); else printf("\nЗапуск TSRDEMO:\n" "tsrdemo : загрузка в память\n" "tsrdemo u : выгрузка из памяти\n" "<Ctrl+R> : запись содержимого экрана" " в файл !grabXXX.scr"); return; } if(tsrloaded()) { printf("Программа TSRDEMO уже загружена\n" "Введите 'tsrdemo u' для выгрузки"); return; } if(!tsrinit()) { printf("Мало памяти для загрузки TSR"); return; } tsrsize = (_DS - _CS) + (_SP / 16); _dos_keep (0, tsrsize + 1); }
Прежде всего, функция main проверяет параметры, переданные программе через командную строку.
Если указан параметр u, вызывается функция unload, которая удаляет резидентную программу из памяти или отключает ее (при невозможности удаления). Если же указан любой другой параметр, функция main выводит на экран инструкцию по запуску программы. В обоих случаях вслед за этим работа программы завершается.
В том случае, когда при запуске программы TSRDEMO пользователь не указал никаких параметров, функция main предпринимает попытку инициализировать программу и оставить ее резидентной в памяти. Перед этим она вызывает функцию tsrloaded, проверяющую, нет ли в памяти копии программы TSRDEMO, загруженной ранее. Попытки повторной загрузки блокируются с выводом соответствующего сообщения на экран.
Если программа запускается впервые, функция main вызывает функцию tsrinit. Последняя выполняет все необходимые инициализирующие действия, такие, например, как встраивание обработчиков аппаратных и программных прерываний.
Для завершения программы используется функция _dos_keep , которой передается размер программы. Способ вычисления этого размера зависит от модели памяти и заслуживает отдельного рассмотрения.
Отложим на время описание способа, с помощью которого резидентная программа может выгрузить себя из памяти, и займемся более детально процессом инициализации.
На первом этапе инициализации наша резидентная программа резервирует память для области кучи (heap) и стека. Это необходимо, если функция, получающая управление при активизации программы, работает со стандартными функциями из библиотеки транслятора (например, открывает файлы или потоки ввода/вывода, заказывает области памяти функцией malloc и т. п.).
Для того чтобы вам была понятна процедура резервирования памяти, а также способ, при помощи которого мы вычисляем размер области памяти, передаваемой в качестве параметра функции _dos_keep , рассмотрим расположение различных сегментов, составляющих программу в модели памяти small . Эта модель используется в нашей программе TSRDEMO. Описание расположения сегментов для других моделей памяти вы сможете найти в руководстве программиста, которое поставляется вместе с Borland C++ (или другим аналогичным средством разработки).
В модели памяти small программе выделяются две области памяти размером не более 64 Кбайт. Одна область содержит сегмент кода с названием _TEXT и содержит исполнимый код программы, в другой расположены сегмент инициализированных данных с названием _DATA , сегмент неинициализированных данных _BSS , область кучи и сегмент стека. Перед сегментом кода расположен сегмент PSP (рис. 5.1).
Рис. 5.1. Расположение сегментов в модели памяти small
Из этого рисунка видно, что вслед за сегментом PSP в памяти располагается сегмент кода, а затем - сегмент данных.
Какие из этих сегментов потребуются программе, после того как она останется резидентной в памяти?
Блок памяти, выделенный в сегменте PSP для хранения переменных среды, очевидно, не нужен, так как его содержимое может быть прочитано на этапе инициализации и в последующем останется неизменным. С сегментом кода тоже все ясно - его нужно оставить в памяти.
Если резидентная программа составлена на языке программирования С, то наибольшие трудности возникают при определении необходимого размера сегмента данных.
Программа может иметь статические глобальные переменные, имеющие или не имеющие начальные значения. Они располагаются, соответственно, в сегментах _DATA и _BSS , доступ к которым возможен через сегментный регистр DS. Размер указанных сегментов можно определить только приблизительно, зная общий объем памяти, выделенной программой для статических и глобальных переменных.
Вслед за сегментом _BSS в памяти расположена область HEAP, которая иногда называется просто кучей. Из этой области (доступ к которой возможен с помощью регистра DS) происходит динамическое выделение памяти по явному или неявному запросу программы. Заметим, что вызов многих функций стандартной библиотеки С приводит к неявному выделению памяти из области HEAP.
Механизм выделения памяти из области HEAP имеет одну особенность: если программа заказала для себя блок памяти из этой области (например, функцией malloc), а затем освободила его, этот блок памяти не будет возвращен MS-DOS до завершения работы программы. Когда программе опять потребуется блок памяти, по возможности он будет выделен из области HEAP без обращения к соответствующей функции прерывания MS-DOS.
Что же касается стека, то, как известно, он адресуется с помощью регистров SS:SP. Содержимое регистра SS после запуска программы в модели памяти small равно содержимому регистра DS, так как для этой модели памяти стек расположен в сегменте данных.
В отличие от области данных HEAP, которая растет в направлении к концу памяти (в сторону больших адресов), стек растет в направлении к началу памяти. На рис. 5.1 это показано при помощи стрелок.
Вслед за областью стека в памяти расположена область FAR HEAP. Блоки памяти из этой области выделяются в тех случаях, когда программа заказывает их такой функцией, как farmalloc.
Для того чтобы при активизации резидентная программа могла вызывать стандартные функции библиотеки транслятора С, необходимо зарезервировать достаточное количество памяти для областей HEAP и стека. Кроме того, при активизации необходимо правильно загрузить регистры SS:SP, чтобы они указывали на нижнюю границу блока памяти, выделенного резидентной программой для стека.
Как мы уже говорили, программа, составленная на языке программирования С, остается резидентной в памяти с помощью функции _dos_keep . Через первый параметр этой функции следует передать код завершения (для анализа в пакетных файлах), а через второй - объем памяти, который должен быть зарезервирован для программы после ее завершения. Этот объем нужно указать в параграфах.
Взгляните на последние две строки нашей программы TSRDEMO:
tsrsize = (_DS - _CS) + (_SP / 16); _dos_keep (0, tsrsize + 1);
Размер области памяти, выделенной для сегмента кода, можно вычислить как _DS - _SS, так как сегмент данных расположен сразу вслед за сегментом кода. Нижняя граница сегмента данных определяется содержимым регистра SP. Дополнительно мы резервируем еще один параграф памяти, прибавляя к переменной tsrsize число 1.
Функция tsrinit выполняет все действия, необходимые для инициализации резидентной программы.
Прежде всего, эта функция резервирует область памяти для стека, который будет использован в момент активизации резидентной программы.
Вот соответствующий фрагмент кода:
wk_ptr = malloc(HEAP_RESERVED); if(wk_ptr == NULL) return 0; tsr_stack = malloc(STACK_SIZE); if(tsr_stack == NULL) return 0; tsr_stack += STACK_SIZE; free(wk_ptr);
На первый взгляд может показаться, что здесь мы выполняем странные действия - вначале заказываем блок памяти, имеющий размер HEAP_RESERVED, а потом сразу же отдаем его, так и не использовав ни разу. Тем не менее, такие действия имеют определенный смысл.
Вернувшись к рис. 5.1, заметим, что перед стеком должна располагаться область памяти HEAP. Она нужна для работы функций динамического выделения памяти (и не только для этих функций). Поэтому область стека резидентной программы должна быть расположена ниже, как это показано на рис. 5.1.
На самом деле вызывая функцию malloc в первый раз, мы резервируем память для области HEAP. После второго вызова функции malloc будет выделена область памяти размером STACK_SIZE, расположенная сразу после области HEAP. Такая область подходит для стека резидентной программы.
Так как стек растет в направлении к началу памяти, мы запоминаем в переменной tsr_stack адрес нижней границы полученной области (точнее говоря, компоненту смещения адреса стека, пригодную для загрузки в регистр SP).
Когда резидентная программа активизируется, регистры SS:SP будут загружены так, чтобы указывать на выделенную нами область стека. При этом перед стеком будет расположена область памяти, имеющая размер HEAP_RESERVED. Она будет выполнять роль области HEAP, обеспечивая возможность использования стандартных функций библиотеки С.
После всех этих манипуляций функция tsrinit получает и сохраняет указатель на так называемый флаг InDos , вызывая для этого функцию getInDosFlag. Этот флаг устанавливается перед началом работы любых функций прерываний MS-DOS и сбрасывается после завершения выполнения указанных функций. Наша резидентная программа использует флаг InDos для определения возможности активизации.
Далее функция tsrinit сохраняет старые значения ряда векторов прерываний и устанавливает новые обработчики для этих прерываний.
В завершение функция освобождает блок памяти, выделенный для переменных среды в сегменте PSP :
FP_SEG (fp) = _psp; FP_OFF (fp) = 0x2c; _dos_freemem(*fp);
Для этого полный адрес освобождаемого блока памяти формируется в указателе fp. Напомним, что сегментная компонента адреса блока переменных среды находится в PSP . Смещение соответствующего слова равно 0x2c. Сегментный адрес блока PSP находится в глобальной переменной с именем _psp.
Функция tsrinit вызывает функцию getInDosFlag, получающую и сохраняющую указатель на флаг InDos , а также на флаг обработчика критических ошибок. Последний устанавливается в том случае, когда MS-DOS обрабатывает критическую ошибку ввода/вывода.
Резидентная программа не должна активизироваться, если установлены указанные выше флаги. Однако есть одно исключение - если вызвано прерывание INT 28h (о котором мы еще будем говорить), можно выполнять активизацию резидентной программы.
Получить указатель на флаг InDos несложно - для этого достаточно воспользоваться недокументированной функцией 34h прерывания INT 21h :
regs.h.ah = 0x34; intdosx (®s, ®s, &sregs); FP_SEG (indos_ptr) = sregs.es; FP_OFF (indos_ptr) = regs.x.bx;
Искомый адрес возвращается в регистрах ES:BX.
В MS-DOS ранних версий (до версии 3.0) флаг обработчика критических ошибок был расположен непосредственно перед флагом InDos. В версии 3.0 его перенесли на новое место - сразу после флага InDos. В обоих случаях расположение этих флагов не было указано в документации.
Для выяснения расположения флага обработчика критических ошибок в MS-DOS новых версий вы должны использовать недокументированную функцию 5Dh прерывания INT 21h:
regs.x.ax = 0x5D06; intdosx (®s,®s,&sregs); FP_SEG (crit_err_ptr) = sregs.ds; FP_OFF (crit_err_ptr) = regs.x.si;
Эта функция вернет указатель на флаг обработчика критических ошибок в регистрах DS:SI.
Последнее, что делает функция tsrinit перед возвращением управления - встраивание обработчиков некоторых прерываний. При этом для обеспечения возможности выгрузки резидентной программы из памяти сохраняются старые значения изменяемых векторов прерываний.
Перед тем как приступить к изменению векторов прерываний, функция tsrinit запрещает аппаратные прерывания, вызывая функцию _disable . После того как изменение векторов прерываний завершено, вызывается функция _enable , разрешающая обработку аппаратных прерываний:
_disable(); old_int1c = _dos_getvect (0x1C); old_int2f = _dos_getvect (0x2F); old_int8 = _dos_getvect (8); old_int9 = _dos_getvect (9); old_int13 = _dos_getvect (0x13); old_int28 = _dos_getvect (0x28); get_int_13(); _dos_setvect (0x1C, (fptr)new_int1c); _dos_setvect (0x2F, (fptr)new_int2f); _dos_setvect (8, (fptr)new_int8); _dos_setvect (9, (fptr)new_int9); _dos_setvect (0x13, (fptr)new_int13); _dos_setvect (0x28, (fptr)new_int28); _enable();
Обработчики всех прерываний, за исключением прерывания INT 13h, составлены на языке С. Так как прерывание INT 13h возвращает результат выполнения операций в регистре флагов, соответствующий обработчик удобнее составить на языке ассемблера.
Расскажем подробно о том, какие вектора прерываний мы переопределяем и зачем.
Прерывание INT 1Ch является программным и вызывается обработчиком аппаратного прерывания от таймера INT 08h приблизительно 18,2 раза в секунду.
Наша программа использует это прерывание, для того чтобы выводить в правом верхнем углу экрана мигающие символы "*" и "+". Такое мигание нужно только для индикации активности резидентной программы. Если индикация не требуется, вы можете не изменять соответствующий вектор прерывания и не предусматривать обработчик для этого прерывания.
Наш обработчик прерывания INT 1Ch называется new_int1c и имеет следующий вид:
void interrupt new_int1c() { s_arrayptr screen[80]; // видеопамять static int count; // счетчик screen[0] = (s_arrayptr) MK_FP (0xB800,0); count++; count %= 6; screen[0][79] = ((count > 2) ? '*' : '+') + ATTR; _chain_intr (old_int1c); }
Массив screen содержит дальние указатели на массив целых чисел, определенные следующим образом:
typedef unsigned int (far *s_arrayptr);
В первый элемент этого массива мы записываем указатель на область видеопамяти , соответствующую самой верхней строке экрана. Для цветного видеоконтроллера этот адрес равен B800h:0000h.
Для того чтобы символы не мигали слишком часто, обработчик прерывания INT 1Ch подсчитывает прерывания в статическом счетчике count. Если значение этого счетчика больше 2, выводится символ '*", если меньше - символ "+".
Перед тем как возвратить управление, функция new_int1c вызывает старый обработчик прерывания INT 1Ch , адрес которого хранится в переменной old_int1c. Для этого используется функция _chain_intr .
Обработчик прерывания INT 2Fh , который встраивает наша программа, выполняет две функции.
Во-первых, этот обработчик используется для предотвращения повторной загрузки резидентной программы в память.
Во-вторых, он нужен для выгрузки резидентной программы из памяти.
Исходный текст обработчика прерывания INT 2Fh представлен ниже:
void interrupt far new_int2f( unsigned bp, unsigned di, unsigned si, unsigned ds, unsigned es, unsigned dx, unsigned cx, unsigned bx, unsigned ax) { if(ax == 0xff00) ax = 0x00ff; else if(ax == 0xff01) { ExitAddress = ((long)bx << 16) + dx; if(!tsr_already_active) { _enable(); tsr_exit(); ax = 0xFFFF; tsr_already_active = -ax; } } else _chain_intr (old_int2f); }
Вызов прерывания INT 2Fh происходит при запуске нашей программы, а также и при запуске других резидентных программ. Регистр AH при этом должен содержать идентификатор резидентной программы. Для программы TSRDEMO мы выбрали идентификатор FFh, хотя никто не сможет гарантировать, что такой идентификатор уже не используется другой программой. Для надежности вы можете указать дополнительный идентификатор, например, в виде текстовой строки, адрес которой передается через другие регистры. Мы не стали этого делать для сокращения объема листинга программы.
Итак, если при вызове прерывания INT 2Fh регистр AH содержит значение FFh, наш обработчик прерывания считает, что вызов выполняет программа TSRDEMO.
В этом случае он дополнительно проверяет содержимое регистра AL, в котором находится код выполняемой функции.
Если этот код равен 0, прерывание INT 2Fh вызвано для проверки наличия программы TSRDEMO в памяти. В ответ обработчик возвращает в регистре AX значение 00FFh. Это и есть признак того, что программа TSRDEMO уже загружена в память и ее повторная загрузка невозможна.
Если же код равен 1, программа TSRDEMO была запущена с параметром u для выгрузки своей копии из памяти.
В этом случае обработчик прерывания INT 2Fh сохраняет в глобальной переменной ExitAddress адрес завершения, переданный обработчику в регистрах BX:DX. Затем проверяется флаг tsr_already_active, который установлен, если резидентная программа активна и ее в данный момент нельзя выгружать из памяти.
Далее обработчик разрешает аппаратные прерывания, которые были запрещены перед вызовом прерывания INT 2Fh для выгрузки, и предпринимает попытку выгрузить резидентную программу, вызывая функцию tsr_exit.
Заметим, что после выгрузки резидентной программы функция tsr_exit передает управление MS-DOS, а не возвращает его обратно в программу.
Попытка выгрузки программы может окончиться неудачно, если пользователь загрузил после программы TSRDEMO другую резидентную программу, изменившую вектора прерываний, для которых программа TSRDEMO установила свои обработчики. В этом случае наша программа "не знает", как восстановить вектора прерываний и не выгружается из памяти. Тем не менее, она блокирует свою работу, записывая в переменную tsr_already_active значение -FFFFh.
Подробнее процесс выгрузки резидентной программы будет рассмотрен позже.
Заметим, что если содержимое регистра AX при вызове прерывания INT 2Fh не равно FF00h или FF01h, наш обработчик прерывания передает управление по цепочке, вызывая для этого функцию _chain_intr .
Аппаратное прерывание от клавиатуры INT 09h обрабатывается для того чтобы обнаружить комбинацию клавиш, предназначенную для активизации резидентной программы:
void interrupt far new_int9( unsigned bp, unsigned di, unsigned si, unsigned ds, unsigned es, unsigned dx, unsigned cx, unsigned bx, unsigned ax) { if(tsr_already_active) _chain_intr (old_int9); keycode = inp(KEYBOARD_PORT); if((_bios_keybrd(_KEYBRD_SHIFTSTATUS) & ShiftKey) != ShiftKey) _chain_intr (old_int9); if(!(keycode == HotKeyRecording)) _chain_intr (old_int9); popup_while_dos_busy = 1; asm cli asm in al, 61h asm mov ah, al asm or al, 80h asm out 61h, al asm xchg ah, al asm out 61h, al asm mov al, 20h asm out 20h, al asm sti }
Когда программа уже активна, установлен флаг tsr_already_active. В этом случае наш обработчик прерывания передает управление по цепочке, вызывая старый обработчик.
Если программа TSRDEMO не активна, наш обработчик прерывания вводит код нажатой клавиши из порта клавиатуры и проверяет, была ли нажата клавиша <Ctrl>. Затем проверяется код нажатой клавиши. В том случае, если пользователь нажал комбинацию клавиш <Ctrl+R>, нужно активизировать резидентную программу.
Однако активизация во время обработки аппаратного прерывания от клавиатуры - не самое лучшее решение. Поэтому наш обработчик просто устанавливает флаг popup_while_dos_busy, который служит запросом на активизацию.
Активизация резидентной программы будет выполнена позднее, при обработке аппаратного прерывания INT 08h или прерывания INT 28h .
Для корректной обработки аппаратного прерывания необходимо выдать в контроллер прерывания соответствующую команду, разрешающую обработку других прерываний. Эту задачу решают несколько команд, оформленных как asm-вставки в исходный текст обработчика.
Активизация резидентной программы TSRDEMO не выполняется сразу после того, как пользователь нажал комбинацию клавиш <Ctrl+R>. Для активизации необходимо выбрать подходящий момент, когда прерываемая программа не вызывает функцию MS-DOS или прерывание BIOS , предназначенное для работы с диском.
Наша программа использует несколько возможностей для своей активизации. В частности, обработчик аппаратного прерывания таймера INT 08h периодически проверяет возможность активизации:
void interrupt far new_int8( unsigned bp, unsigned di, unsigned si, unsigned ds, unsigned es, unsigned dx, unsigned cx, unsigned bx, unsigned ax) { if(!tsr_already_active && popup_while_dos_busy && !DosBusy() && !unsafe_flag) { popup_while_dos_busy = 0; tsr_already_active = 1; (*old_int8)(); _enable(); // разрешаем прерывания activate_tsr(); // активизируем TSR tsr_already_active = 0; } else (*old_int8)(); }
Прежде всего, проверяется флаг tsr_already_active. Этот флаг устанавливается, когда резидентная программа уже активизирована. Проверка флага активизации позволяет избежать повторной активизации, которая может привести к катастрофе.
Активизация не выполняется также и в том случае, если установлен флаг InDos или unsafe_flag (последний устанавливается, если вызван обработчик прерывания INT 13h ).
Флаг popup_while_dos_busy установлен в том случае, когда был запрос на активизацию. Он устанавливается обработчиком аппаратного прерывания от клавиатуры INT 08h . Если запроса на активизацию нет, этот флаг не установлен и, следовательно, активизацию выполнять не нужно.
Если активизация возможна, сбрасываются флаги popup_while_dos_busy и устанавливается флаг tsr_already_active. Затем вызывается старый обработчик прерывания INT 08h и разрешаются прерывания.
Далее вызывается функция activate_tsr, которая выполняет все действия, необходимые для активизации. Она будет описана отдельно. После возвращения управления из этой функции флаг tsr_already_active сбрасывается.
В том случае, когда активизация резидентной программы невозможна, наш обработчик прерывания INT 08h вызывает старый обработчик и возвращает управление прерванной программе.
Прерывание INT 28h вызывается MS-DOS, когда она ожидает ввод данных от клавиатуры или, иными словами, ничем особенным не занята. Поэтому обработчик прерывания INT 28h может активизировать резидентную программу, если на это есть запрос от пользователя.
Приведем исходный текст обработчика прерывания INT 28h :
void interrupt far new_int28( unsigned bp, unsigned di, unsigned si, unsigned ds, unsigned es, unsigned dx, unsigned cx, unsigned bx, unsigned ax) { int_28_in_progress++; if(popup_while_dos_busy && (!Int28DosBusy()) && !tsr_already_active && !unsafe_flag) { tsr_already_active = 1; activate_tsr(); // активизируем TSR tsr_already_active = 0; } int_28_in_progress--; _chain_intr (old_int28); }
Обработчик прерывания INT 28h содержит счетчик рекурсивных вызовов int_28_in_progress, который анализируется при активизации резидентной программы функцией activate_tsr.
Если есть запрос на активизацию, проверяются флаги tsr_already_active и unsafe_flag. Дополнительно проверяется, не выполняется ли попытка активизировать резидентную программу во время обработки прерывания MS-DOS, отличного от INT 28h . Для этого вызывается функция Int28DosBusy.
Обратите внимание на различие в способе проверки возможности активизации при обработке прерывания INT 08h и INT 28h .
В первом случае вызывается функция DosBusy:
int DosBusy(void) { if(indos_ptr && crit_err_ptr) return(*crit_err_ptr || *indos_ptr); else return 0xFFFF; }
Эта функция проверяет содержимое флагов InDos и флага обработки критической ошибки. Если MS-DOS не выполняет обработку критической ошибки и если прерываемая программа не вызывает функцию MS-DOS, можно выполнять активизацию. В этом случае функция DosBusy возвращает значение 0.
Однако при вызове прерывания INT 28h флаг InDos установлен всегда, так как указанное прерывание - это тоже прерывание MS-DOS. В данном случае для проверки возможности активизации используется другая функция, которая называется Int28DosBusy.
Эта функция допускает однократный (не рекурсивный) вызов функции INT 28h , проверяя значение, которое записывается в байт памяти, отведенный для флага InDos:
int Int28DosBusy(void) { if(indos_ptr && crit_err_ptr) return (*crit_err_ptr || (*indos_ptr > 1)); else return 0xFFFF; }
При рекурсивных вызовах функций MS-DOS значение, хранящееся в этом байте, увеличивается на 1. Активизация резидентной программы допускается только в том случае, когда в этом байте находится значение 1, т. е. когда нет рекурсии.
Обработчик прерывания INT 28h активизирует резидентную программу точно таким же способом, что и обработчик прерывания INT 08h , а именно, вызывая специально предназначенную для активизации функцию activate_tsr, определенную в нашей программе.
Обработчик прерывания INT 13h составлен на языке ассемблера. Его единственное назначение - увеличение значения флага unsafe_flag при каждом вызове прерывания INT 13h и уменьшение при возврате из этого прерывания. Когда программа TSRDEMO будет предпринимать попытку активизации во время обработки прерывания INT 13h, она проверит значение флага unsafe_flag. Если оно не будет равно 0, активизация невозможна, так как в данный момент времени выполняется обработка прерывания INT 13h.
Исходный текст обработчика прерывания INT 13h представлен ниже:
_new_int13 proc far push ax push ds mov ax, DGROUP mov ds, ax inc word ptr _unsafe_flag pop ds pop ax pushf call cs:old_int13 push ax push ds mov ax, DGROUP mov ds, ax dec word ptr _unsafe_flag pop ds pop ax ret 2 _new_int13 endp
Для обеспечения возможности адресации глобальной переменной unsafe_flag регистр DS устанавливается на сегмент данных программы TSRDEMO.
Адрес старого обработчика прерывания INT 13h находится в переменной old_int13, куда он записывается функцией get_int_13. Эта функция вызывается из функции tsrinit. Ее исходный текст вы найдете в листинге 5.3 (см. ниже).
Активизация резидентной программы - непростая задача, состоящая из многих шагов.
Когда обработчик прерываний INT 08h или INT 28h обнаруживает, что есть запрос на активизацию и, кроме того, возможность активизации, вызывается функция activate_tsr. Она и выполняет всю необходимую работу.
Прежде всего, функция activate_tsr переключает стек при помощи функции set_stack. Эта функция составлена на языке ассемблера и приведена в листинге 5.3.
Резидентная программа во время активизации должна пользоваться собственным стеком, память для которого была отведена на этапе инициализации, а не стеком прерванной программы. Размер стека прерванной программы в общем случае неизвестен, поэтому резидентная программа может разрушить важные структуры, записывая в него свои данные.
Далее функция activate_tsr сохраняет адреса обработчиков прерываний <Ctrl+Break>, <Ctrl+C> и прерывания по критической ошибке ввода/вывода, а затем устанавливает для этих прерываний свои собственные обработчики. Все это необходимо потому, что резидентная программа может обрабатывать перечисленные выше прерывания не так, как это делает прерванная программа.
На следующем этапе активизации резидентная программа сохраняет PSP прерванной программы и устанавливает свой PSP. Если этого не сделать, текущий PSP не будет соответствовать текущей выполняемой программе (в данном случае программе TSRDEMO).
Для получения адреса PSP прерванной программы вызывается функция GetPSP.
Если используется MS-DOS версии 2.х, адрес PSP определяется с помощью функции 51h прерывания INT 21h . Для более новых версий можно вызвать функцию 62h того же прерывания.
Установка PSP текущей исполняемой программы выполняется функцией SetPSP с помощью функции 50h прерывания INT 21h .
Далее резидентная программа, выполняющая активизацию, должна сохранить область DTA (Disk Transfer Area ) прерванной программы, которая используется при работе с дисковой памятью. Это можно сделать при помощи функции 2Fh прерывания INT 21h .
После этого устанавливается новая область DTA , для которой используется часть сегмента PSP . Установка новой области DTA выполняется функцией 1Ah прерывания INT 21h .
Если при выполнении какой-либо функции прерывания INT 21h возникает ошибка, в соответствующую структуру памяти записывается расширенная информация, позволяющая определить причины ее возникновения.
Перед активизацией резидентная программа должна сохранить содержимое указанной структуры, а перед возвращением управления прерванной программе - восстановить его.
Для сохранения информации об ошибке программа TSRDEMO вызывает функцию GetExtErr, которая получает указанную информацию с помощью функции 59h прерывания INT 21h .
Функция SetExtErr восстанавливает расширенную информацию об ошибке при помощи функции 5D0Ah того же самого прерывания.
На последнем этапе активизации наша программа очищает буфер клавиатуры . Это необходимо для того, чтобы коды клавиш, предназначенные для прерванной программы, не были переданы активизированной резидентной программе.
Для удаления содержимого буфера клавиатуры используется функция _bios_keybrd. Она вызывается в цикле до тех пор, пока буфер клавиатуры не окажется пуст:
while(_bios_keybrd(_KEYBRD_READY)) _bios_keybrd(_KEYBRD_READ);
Теперь можно делать то, для чего предназначена резидентная программа. В данном случае, программа TSRDEMO вызывает функцию application, которая копирует содержимое видеопамяти в создаваемый текстовый файл.
Перед вызовом функции application мы выполнили все необходимые действия по переключению активной программы, сохранению структур данных. Кроме того, мы зарезервировали память для области HEAP и стека. Поэтому функция application может вызывать практически любые функции стандартной библиотеки, в том числе предназначенные для динамического выделения памяти и для работы с потоками данных.
Сохранив содержимое видеопамяти в файле, функция application возвращает управление функции activate_tsr. Последняя должна очистить буфер клавиатуры , восстановить расширенную информацию об ошибках, область DTA , PSP , адреса прерываний <Ctrl+Break>, <Ctrl+C> и прерывания по критической ошибке ввода/вывода и выполнить обратное переключение стека.
Напомним, что для выгрузки программы TSRDEMO из памяти ее нужно запустить с параметром u.
Что происходит в этом случае?
Функция main вызывает функцию unload. Та, в свою очередь, пытается выполнить удаление копии программы TSRDEMO из памяти, вызывая для этого функцию tsrunload. В зависимости от успеха этой операции функция unload выводит на экран соответствующее сообщение.
Функция tsrunload составлена на языке ассемблера. Ее исходный текст вы сможете найти в листинге 5.3.
Кратко о том, что делает эта функция.
Прежде всего, она сохраняет содержимое регистров SS:SP и DS в глобальных переменных. Затем она загружает адрес точки входа ExitTSR в регистры BX:DX и вызывает прерывание INT 2Fh , передавая в регистре AX команду выгрузки программы из памяти - значение FF01h.
Теперь в дело включается обработчик прерывания INT 2Fh , установленный загруженной резидентной программой TSRDEMO. Вот соответствующий фрагмент этого обработчика:
else if(ax == 0xff01) { ExitAddress = ((long)bx << 16) + dx; if(!tsr_already_active) { _enable(); tsr_exit(); ax = 0xFFFF; tsr_already_active = -ax; } }
Полученный адрес сохраняется в глобальной переменной ExitAddress, вслед за чем вызывается функция tsr_exit.
Эта функция устанавливает стек, зарезервированный для работы при активизации программы, вслед за чем пытается восстановить переназначенные при установке векторы прерываний.
Для восстановления используется функция RestoreIntVect, которая проверяет, не были ли переназначены векторы прерывания еще раз, например, другой резидентной программой, запущенной после программы TSRDEMO.
Если все векторы прерываний удалось восстановить, резидентную программу можно удалить из памяти. Для этого используется простой способ: резидентная программа делается текущей, после чего ее работа завершается при помощи функции 4Ch прерывания INT 21h . Это стандартный способ завершения работы программы, при котором MS-DOS удаляет все связанные с ней блоки памяти.
При невозможности восстановления векторов прерывания резидентная программа остается в памяти, но активизации не поддается из-за того что слово памяти, отведенное для флага tsr_already_active, записывается значение -FFFFh.
Листинг 5.1. Файл tsrdemo\tsrdemo.c
#include <ctype.h> #include <stdlib.h> #include <stdio.h> #include <conio.h> #include <dos.h> #include <bios.h> #include <process.h> #include "tsrdemo.h" // Структуры для работы с функциями прерываний union REGS regs; struct SREGS sregs; // Идентификатор для мультиплексного прерывания unsigned char tsrid = 0xff; // Указатель на стек, который будет использован // после активизации TSR char far *tsr_stack; // Флаг активности TSR. Равен 1, если TSR активна int tsr_already_active = 0; // Флаг активизации TSR. Равен 1, // если нажали клавишу активизации int popup_while_dos_busy = 0; // Флаг обработки прерывания INT 28h . // Равен 1, когда работает INT 28 int int_28_in_progress = 0; // Флаг обработки прерывания INT 13h. // Равен 1, когда работает INT 13 int unsafe_flag = 0; // Код клавиши, при помощи которой была // активизирована TSR unsigned keycode; // Сегмент PSP прерванного процесса unsigned foreground_psp; // Сегмент и смещение блока DTA // прерванного процесса unsigned foreground_dta_seg; unsigned foreground_dta_off; // Адрес, по которому передается управление // при деинсталляции TSR unsigned long ExitAddress; // Область сохранения расширенной // информации об ошибках struct ExtErr ErrInfo; // Указатели на флаги MS-DOS char far *indos_ptr = 0; char far *crit_err_ptr=0; // Область сохранения для старых прерываний INTADDR old_int8, old_int9, old_int10, old_int13, old_int1b, old_int23; INTADDR old_int24, old_int28, old_int2f, old_int1c; typedef unsigned int (far *s_arrayptr); typedef void interrupt (*fptr)(void); // Определение размера кучи и стека // с учетом области статических данных extern unsigned _heaplen = STACK_SIZE + HEAP_RESERVED + STATIC_SIZE; extern unsigned _stklen = STACK_SYSTEM; // ======================================== // Вход в программу - функция main() // ======================================== void main(int argc, char *argv[]) { int tsrsize, i; printf("\n\nРезидентная программа TSRDEMO," " v1.1, (C) Фролов А.В., 1995\n"); // Проверяем версию MS-DOS if(_osmajor < 4) { printf("\nИзвините, вы пользуетесь слишком" "старой версией MS-DOS"); return; } // Если есть параметр, проверяем его if(argc > 1) { // Выгрузка из памяти if(argv[1][0] == 'u') unload(); // Вывод инструкции на экран else printf("\nЗапуск TSRDEMO:\n" "tsrdemo : загрузка в память\n" "tsrdemo u : выгрузка из памяти\n" "<Ctrl+R> : запись содержимого экрана" " в файл !grabXXX.scr"); return; } // Выход, если уже загружена if(tsrloaded()) { printf("Программа TSRDEMO уже загружена\n" "Введите 'tsrdemo u' для выгрузки"); return; } // Инициализация TSR if(!tsrinit()) { printf("Мало памяти для загрузки TSR"); return; } // Оставляем резидентно в памяти tsrsize = (_DS - _CS) + (_SP / 16); _dos_keep (0, tsrsize + 1); } // ======================================== // activate_tsr // Функция вызывается при активизации TSR // ======================================== void activate_tsr() { // Для работы в активном режиме // устанавливаем свой стек set_stack(); // Определяем, когда TSR активизировалась - // во время работы MS-DOS или нет if(DosBusy() && !int_28_in_progress) // Если активизировалась, когда // работает функция MS-DOS, просто // устанавливаем флаг. Активизация будет // отложена до вызова INT 8h или INT 28h popup_while_dos_busy = 1; else { // Если никакая функция прерывания MS-DOS // не вызвана, сбрасываем флаг и начинаем // активизацию TSR popup_while_dos_busy = 0; // Сохраняем адреса прерываний // <Ctrl+Break>, <Ctrl+C> и прерывания по // критической ошибке ввода/вывода old_int1b = _dos_getvect (0x1b); old_int23 = _dos_getvect (0x23); old_int24 = _dos_getvect (0x24); // Устанавливаем свои обработчики прерываний _dos_setvect (0x1b, new_int1b); _dos_setvect (0x23, new_int23); _dos_setvect (0x24, new_int24); // Сохраняем текущий PSP и устанавливаем свой PSP foreground_psp = GetPSP(); SetPSP(_psp); // Сохраняем текущую область DTA regs.h.ah = 0x2f; intdosx (®s, ®s, &sregs); foreground_dta_seg = sregs.es; foreground_dta_off = regs.x.bx; // Устанавливаем свою область DTA . // Используем DTA в своем PSP regs.h.ah = 0x1a; regs.x.dx = 0x80; sregs.ds = _psp; intdosx (®s, ®s, &sregs); // Сохраняем расширенную информацию // об ошибках GetExtErr(&ErrInfo); // Очищаем буфер клавиатуры while(_bios_keybrd(_KEYBRD_READY)) _bios_keybrd(_KEYBRD_READ); // Вызываем функцию, выполняющую все то, // что должно быть сделано // при активизации TSR application(); // Очищаем буфер клавиатуры while(_bios_keybrd(_KEYBRD_READY)) _bios_keybrd(_KEYBRD_READ); // Восстанавливаем информацию об ошибках SetExtErr(&ErrInfo); // Восстанавливаем DTA regs.h.ah = 0x1a; regs.x.dx = foreground_dta_off; sregs.ds = foreground_dta_seg; intdosx (®s, ®s, &sregs); // Восстанавливаем PSP SetPSP(foreground_psp); // Восстанавливаем адреса прерываний // <Ctrl+Break>, <Ctrl+C> и прерывания по // критической ошибке ввода/вывода _dos_setvect (0x1b, old_int1b); _dos_setvect (0x23, old_int23); _dos_setvect (0x24, old_int24); } // Восстанавливаем стек и переводим TSR // в неактивное систояние restore_stack(); } // ======================================== // new_int1c // Обработчик прерывания 1Ch. // Выводит в правом верхнем углу мигающие // символы "*" и "+", сигнализирующие // о нормальной работе TSR // ======================================== void interrupt new_int1c() { s_arrayptr screen[80]; // видеопамять static int count; // счетчик // Для цветного дисплея адрес сегмента // видеобуфера равен B800h, для ч/б - B000. // Устанавливаем указатель на видеобуфер screen[0] = (s_arrayptr) MK_FP (0xB800,0); // Увеличиваем содержимое счетчика // с ограничением count++; count %= 6; // Выводим символ, соответствующий значению // счетчика, в правый верхний угол экрана screen[0][79] = ((count > 2) ? '*' : '+') + ATTR; // Передаем управление по цепочке прерываний _chain_intr (old_int1c); } // ======================================== // new_int2f // Новый обработчик мультиплексного // прерывания INT 2Fh // ======================================== #pragma argsused void interrupt far new_int2f( unsigned bp, unsigned di, unsigned si, unsigned ds, unsigned es, unsigned dx, unsigned cx, unsigned bx, unsigned ax) { // Если ax == 0xff00, выполняется проверка, // была ли данная TSR загружена ранее // Возвращаем 0x00ff. Это означает, что // программа уже загружена if(ax == 0xff00) ax = 0x00ff; // Если ax == 0xff01, была запрошена // выгрузка программы из памяти. // Пытаемся ее выполнить else if(ax == 0xff01) { // Записываем переданный адрес завершения ExitAddress = ((long)bx << 16) + dx; // Выполняем выгрузку TSR из памяти, // если она неактивна if(!tsr_already_active) { _enable(); // разрешаем прерывания tsr_exit(); // пытаемся выгрузить TSR // Если попали управление передано в это // место программы, значит деинсталляция // не удалась,так как какая-то программа // уже перехватила наши прерывания. // В этом случае устанавливаем признак // неудачной выгрузки и блокируем // работу TSR ax = 0xFFFF; tsr_already_active = -ax; } } // Если TSR активна, передаем // управление по цепочке else _chain_intr (old_int2f); } // ======================================== // new_int28 // Новый обработчик прерывания INT 28h // Вызывается MS-DOS, когда она неактивна. // Используется для активизации TSR // ======================================== #pragma argsused void interrupt far new_int28( unsigned bp, unsigned di, unsigned si, unsigned ds, unsigned es, unsigned dx, unsigned cx, unsigned bx, unsigned ax) { // Увеличиваем счетчик рекурсивных // вызовов прерывания INT 28h int_28_in_progress++; // Если это безопасно, активизируем TSR. // Учитываем возможность рекурсивного вызова // прерывания INT 28h . // Не запускаемся, если TSR уже активизирована, // если вызвано прерывание INT 13h, // предназначенное для работы с диском, или // если есть рекурсия при вызове INT 28h if(popup_while_dos_busy && (!Int28DosBusy()) && !tsr_already_active && !unsafe_flag) { // Устанавливаем флаг активизации tsr_already_active = 1; activate_tsr(); // активизируем TSR // Сбрасываем флаг активизации tsr_already_active = 0; } // Уменьшаем счетчик рекурсивных вызовов INT 28 int_28_in_progress--; // Передаем управление по цепочке прерываний _chain_intr (old_int28); } // ======================================== // new_int9 // Новый обработчик аппаратного прерывания // от клавиатуры INT 9h // ======================================== #pragma argsused void interrupt far new_int9( unsigned bp, unsigned di, unsigned si, unsigned ds, unsigned es, unsigned dx, unsigned cx, unsigned bx, unsigned ax) { // Если TSR активна, ничего не делаем, // передавая управление по цепочке if(tsr_already_active) _chain_intr (old_int9); // Проверяем, была ли нажата клавиша <Ctrl>. // Если нет, передаем управление по цепочке. keycode = inp(KEYBOARD_PORT); if((_bios_keybrd(_KEYBRD_SHIFTSTATUS) & ShiftKey) != ShiftKey) _chain_intr (old_int9); // Проверяем коды активизации. Если не наши коды, // передаем управление по цепочке. if(!(keycode == HotKeyRecording)) _chain_intr (old_int9); // Если немедленная активизация невозможна, // устанавливаем флаг запроса на активизацию // при занятой MS-DOS. Активизация будет // выполнена при первой же возможности // обработчиками INT 8 или INT 28h popup_while_dos_busy = 1; // Завершаем обработку аппаратного прерывания asm cli asm in al, 61h asm mov ah, al asm or al, 80h asm out 61h, al asm xchg ah, al asm out 61h, al asm mov al, 20h asm out 20h, al asm sti } // ======================================== // new_int8 // Новый обработчик прерывания INT 8h // Вызывается по прерываниям таймера // Используется для активизации TSR // ======================================== #pragma argsused void interrupt far new_int8( unsigned bp, unsigned di, unsigned si, unsigned ds, unsigned es, unsigned dx, unsigned cx, unsigned bx, unsigned ax) { // Если это безопасно, активизируем TSR // Не запускаемся, если TSR уже активизирована, // если вызвано прерывание INT 13h для // работы с диском, если MS-DOS занята if(!tsr_already_active && popup_while_dos_busy && !DosBusy() && !unsafe_flag) { // Активизация TSR popup_while_dos_busy = 0; tsr_already_active = 1; // Вызываем старый обработчик INT 8h #pragma warn -pro (*old_int8)(); #pragma warn +pro _enable(); // разрешаем прерывания activate_tsr(); // активизируем TSR tsr_already_active = 0; } // Если активизация невозможна, передаем // управление по цепочке #pragma warn -pro else (*old_int8)(); #pragma warn +pro } // ======================================== // new_int1b // Обработчик прерывания <Ctrl+Break> // ======================================== #pragma argsused void interrupt far new_int1b( unsigned bp, unsigned di, unsigned si, unsigned ds, unsigned es, unsigned dx, unsigned cx, unsigned bx, unsigned ax) { // Игнорируем попытку прервать TSR } // ======================================== // new_int23 // Обработчик прерывания <Ctrl+C> // ======================================== #pragma argsused void interrupt far new_int23( unsigned bp, unsigned di, unsigned si, unsigned ds, unsigned es, unsigned dx, unsigned cx, unsigned bx, unsigned ax) { // Игнорируем попытку прервать TSR } // ======================================== // new_int24 // Обработка критической ошибки // ======================================== #pragma argsused void interrupt far new_int24( unsigned bp, unsigned di, unsigned si, unsigned ds, unsigned es, unsigned dx, unsigned cx, unsigned bx, unsigned ax) { ax = 3; // возвращаем код ошибки 3 } // ======================================== // RestoreIntVect // Восстановление векторов прерываний. // Возможно только в том случае, если // они не были еще раз переустановлены // уже после запуска TSR // ======================================== int RestoreIntVect(int Vect, INTADDR NewInt, INTADDR OldInt) { // Сравниваем текущий вектор прерывания с тем // значением, которое установил для себя TSR if(NewInt == _dos_getvect (Vect)) { _dos_setvect (Vect, OldInt); return 0; // восстановили } return 1; // не смогли восстановить } // ======================================== // tsr_exit // Выгрузка резидентной программы // ======================================== void tsr_exit(void) { set_stack(); // используем стек TSR // Восстанавливаем обработчики прерываний, // если они не были переназначены // после запуска TSR _disable(); if(!( RestoreIntVect(0x1c, new_int1c, old_int1c) | RestoreIntVect(8, new_int8, old_int8) | RestoreIntVect(9, new_int9, old_int9) | RestoreIntVect(0x13, new_int13, old_int13) | RestoreIntVect(0x28, new_int28, old_int28) | RestoreIntVect(0x2f, new_int2f, old_int2f))) { // Устанавливаем родительский PSP , // записанный в нашем PSP *(int far *)(((long)_psp << 16) + PSP _PARENT_PSP) = GetPSP(); // Устанавливаем в нашем PSP адрес завершения *(long far *)(((long)_psp << 16) + PSP _TERMINATE) = ExitAddress; // Устанавливаем наш PSP SetPSP(_psp); _enable(); bdos(DOS_EXIT, 0, 0); // завершаем работу TSR } // Если прерывания отсоединить не удалось, // восстанавливаем стек и возвращаем // управление restore_stack(); _enable(); } // ======================================== // tsrinit // Инициализация резидентной программы // ======================================== int tsrinit(void) { char * wk_ptr; unsigned far *fp; // Резервируем место для области, // которая будет использована при // активизации TSR для динамического // выделения памяти wk_ptr = malloc(HEAP_RESERVED); if(wk_ptr == NULL) return 0; // Заказываем стек для резидентной программы tsr_stack = malloc(STACK_SIZE); if(tsr_stack == NULL) return 0; // Устанавливаем указатель стека, // который будет использоваться при // активизации TSR tsr_stack += STACK_SIZE; // Возвращаем зарезервированную память free(wk_ptr); // Инициализируем указатель на флаг InDos getInDosFlag(); // Запрещаем все прерывания _disable(); // Получаем старое значение векторов прерываний old_int1c = _dos_getvect (0x1C); old_int2f = _dos_getvect (0x2F); old_int8 = _dos_getvect (8); old_int9 = _dos_getvect (9); old_int13 = _dos_getvect (0x13); old_int28 = _dos_getvect (0x28); // Сохраняем адрес обработчика INT 13h get_int_13(); // Устанавливаем новые обработчики прерываний _dos_setvect (0x1C, (fptr)new_int1c); _dos_setvect (0x2F, (fptr)new_int2f); _dos_setvect (8, (fptr)new_int8); _dos_setvect (9, (fptr)new_int9); _dos_setvect (0x13, (fptr)new_int13); _dos_setvect (0x28, (fptr)new_int28); // Разрешаем прерывания _enable(); // Освобождаем блок среды MS-DOS FP_SEG (fp) = _psp; FP_OFF (fp) = 0x2c; _dos_freemem(*fp); return 1; } // ======================================== // tsrloaded // Проверяем, была ли уже загружена программа. // Если была, завершаемся без установки // ======================================== int tsrloaded(void) { // Проверяем, была ли уже запущена программа // Для этого вызываем мультиплексное прерывание // со специальным кодом 0xff00 regs.h.ah = tsrid; regs.h.al = 0x00; int86 (0x2f, ®s, ®s); if(regs.x.ax == 0x00ff) return 1; else return 0; } // ======================================== // unload // Выгрузка из памяти // ======================================== void unload(void) { // Вызываем программу выгрузки TSR из // оперативной памяти switch (tsrunload()) { case 1: printf("TSRDEMO не была запущена"); break; case 2: printf("TSRDEMO успешно выгружена из памяти"); break; default: printf("TSRDEMO отключена, но не выгружена"); break; } } // ======================================== // getInDosFlag // Инициализация указателей на флаг InDos // ======================================== void getInDosFlag(void) { regs.h.ah = 0x34; intdosx (®s, ®s, &sregs); // Указатель на флаг InDos возвращается в ES:BX FP_SEG (indos_ptr) = sregs.es; FP_OFF (indos_ptr) = regs.x.bx; // Находим флаг критических ошибок regs.x.ax = 0x5D06; intdosx (®s,®s,&sregs); // Указатель на флаг находится в DS:SI FP_SEG (crit_err_ptr) = sregs.ds; FP_OFF (crit_err_ptr) = regs.x.si; } // ======================================== // DosBusy // Возвращает 0xFFFF, если MS-DOS занята // (если флаги не установлены) // ======================================== int DosBusy(void) { if(indos_ptr && crit_err_ptr) return(*crit_err_ptr || *indos_ptr); else return 0xFFFF; } // ======================================== // Int28DosBusy // Функция возвращает ненулевое значение, // если значение флага InDOS > 1 или // установлен флаг критической ошибки. // В этом случае MS-DOS занята // ======================================== int Int28DosBusy(void) { if(indos_ptr && crit_err_ptr) return (*crit_err_ptr || (*indos_ptr > 1)); else return 0xFFFF; } // ======================================== // GetExtErr // Получение расширенной информации об ошибках // ======================================== void GetExtErr(struct ExtErr * ErrInfo) { union REGS regs; regs.h.ah = 0x59; regs.x.bx = 0; intdos (®s,®s); ErrInfo->errax = regs.x.ax; ErrInfo->errbx = regs.x.bx; ErrInfo->errcx = regs.x.cx; } // ======================================== // SetExtErr // Установка расширенной информации об ошибках // ======================================== void SetExtErr(struct ExtErr near * ErrInfo) { union REGS regs; struct SREGS segregs; regs.x.ax = 0x5d0a; regs.x.bx = 0; // Запись адреса информации об ошибке в DS:DX segread(&segregs); regs.x.dx = (int) ErrInfo; intdosx (®s,®s,&segregs); } // ======================================== // GetPSP // Получить адрес текущего блока PSP // ======================================== unsigned GetPSP(void) { regs.h.ah = 0x62; intdos (®s,®s); return regs.x.bx; } // ======================================== // SetPSP // Установить текущий PSP // ======================================== void SetPSP(unsigned segPSP) { if(!crit_err_ptr) // Нельзя вызывать InitInDos return; *crit_err_ptr = 0xFF; regs.h.ah = 0x50; regs.x.bx = segPSP; intdos (®s,®s); *crit_err_ptr = 0; }
Листинг 5.2. Файл tsrdemo\tsrdemo.h
// Коды клавиш #define HotKeyRecording 0x13 // "R" #define ShiftKey 0x04 // <Ctrl> // Экранный атрибут - голубой на сером #define ATTR 0x7900 // Стек TSR при запуске #define STACK_SYSTEM 512 // Размер кучи, доступный для использования // в момент активизации #define HEAP_RESERVED 2048 // Размер стека, доступного для использования // в момент активизации #define STACK_SIZE 1024 // Размер области статических данных #define STATIC_SIZE 2000 // Адрес завершения в нашем PSP #define PSP _TERMINATE 0x0A // Родительский PSP из нашего PSP #define PSP _PARENT_PSP 0x16 // Функция MS-DOS для завершения программы #define DOS_EXIT 0x4C // Порт данных клавиатуры #define KEYBOARD_PORT 0x60 struct ExtErr { unsigned int errax; unsigned int errbx; unsigned int errcx; }; void interrupt far new_int2f( unsigned bp, unsigned di, unsigned si, unsigned ds, unsigned es, unsigned dx, unsigned cx, unsigned bx, unsigned ax); void interrupt far new_int28( unsigned bp, unsigned di, unsigned si, unsigned ds, unsigned es, unsigned dx, unsigned cx, unsigned bx, unsigned ax); void interrupt far new_int9( unsigned bp, unsigned di, unsigned si, unsigned ds, unsigned es, unsigned dx, unsigned cx, unsigned bx, unsigned ax); void interrupt far new_int8( unsigned bp, unsigned di, unsigned si, unsigned ds, unsigned es, unsigned dx, unsigned cx, unsigned bx, unsigned ax); void interrupt far new_int1b( unsigned bp, unsigned di, unsigned si, unsigned ds, unsigned es, unsigned dx, unsigned cx, unsigned bx, unsigned ax); void interrupt far new_int23( unsigned bp, unsigned di, unsigned si, unsigned ds, unsigned es, unsigned dx, unsigned cx, unsigned bx, unsigned ax); void interrupt far new_int24( unsigned bp, unsigned di, unsigned si, unsigned ds, unsigned es, unsigned dx, unsigned cx, unsigned bx, unsigned ax); typedef void (interrupt far *INTADDR)(); void far idle_int_chain(void); void interrupt far new_int10(void); void interrupt far new_int13(void); void interrupt far new_int25(void); void interrupt far new_int26(void); void far timer_int_chain(void); extern char far * indos_ptr; extern char far * crit_err_ptr; void main(int argc,char *argv[]); void Beep(void); void application(void); void unload(void); int tsrloaded(void); int tsrinit(void); void getInDosFlag(void); void get_int_13(void); int DosBusy(void); int Int28DosBusy(void); void GetExtErr(struct ExtErr * ErrInfo); void SetExtErr(struct ExtErr near * ErrInfo); unsigned GetPSP(void); void SetPSP(unsigned segPSP); void activate_tsr(void); void tsr_exit(void); void usage(char *); int RestoreIntVect(int Vect, INTADDR NewInt, INTADDR OldInt); void set_stack(void); void restore_stack(void); int tsrunload(void);
Листинг 5.3. Файл tsrdemo\tsrlib.asm
;// TSRLIB.ASM - дополнительные функции .MODEL small .DATA public _new_int13, _get_int_13 public _tsrunload public _set_stack, _restore_stack ;// Стек для TSR extrn _tsr_stack:near ;// Флаг обработки прерывания INT 13h. ;// Равен 1, когда работает INT 13 extrn _unsafe_flag:near ;// Старый вектор прерывания INT 13h extrn _old_int13:near ;// Идентификатор программы для ;// мультиплексного прерывания INT 2Fh extrn _tsrid:near ;// Область сохранения old_int13 dd 0 _ds_old dw 0 _ss_old dw 0 _sp_old dw 0 .CODE ;//------------------------------------------- ;// void tsrunload(void) ;// Функция выгрузки TSR из памяти ;//------------------------------------------- _tsrunload proc push di push si push bp ;// Сохраняем указатель на стек mov word ptr _ss_old, ss mov word ptr _sp_old, sp ;// Сохраняем регистр DS mov cs:_ds_old, ds ;// Устанавливаем BX:DX на адрес завершения mov bx, cs mov dx, offset ExitTSR ;// Вызываем мультиплексное прерывание, ;// передавая ему команду завершения mov ah, byte ptr _tsrid mov al, 01h int 2fh ;// Если TSR завершилась, мы не попадаем на ;// команду jmp, а переходим ;// по адресу ExitTSR jmp short NotExit ExitTSR: ;// Восстанавливаем DS и стек mov ax,cs:_ds_old mov ds,ax ;// Признак успешной деинсталляции mov al,2 ;// Восстанавливаем стек mov ss, word ptr _ss_old mov sp, word ptr _sp_old NotExit: ;// Преобразуем возвращенное значение в ;// двойное слово cbw pop bp pop si pop di ret _tsrunload endp ;//------------------------------------------- ;// void get_int_13(void) ;// Пересылка указателя на INT 13 ;// в наш сегмент данных ;//------------------------------------------- _get_int_13 proc push es push bx ;// Указатель на INT 13h les bx, dword ptr _old_int13 mov word ptr cs:old_int13, bx mov word ptr cs:old_int13 + 2, es pop bx pop es ret _get_int_13 endp ;//------------------------------------------- ;// void set_stack(void) ;// Сохранить старый стек и установить ;// стек, который будет использован для TSR ;//------------------------------------------- _set_stack proc ;// Получаем смещение и сегмент ;// для возврата pop ax pop bx ;// Сохраняем стек mov word ptr _ss_old, ss mov word ptr _sp_old, sp ;// Устанавливаем стек для TSR mov ss, word ptr _tsr_stack + 2 mov sp, word ptr _tsr_stack ;// Загружаем стек для возврата push bx push ax ret _set_stack endp ;//------------------------------------------- ;// void restore_stack(void) - ;// Восстанавливаем стек ;//------------------------------------------- _restore_stack proc ;// Получаем смещение и сегмент ;// для возврата pop cx pop bx ;// Сохраняем старый стек mov word ptr _tsr_stack + 2, ss mov word ptr _tsr_stack, sp ;// Восстанавливаем стек mov ss,word ptr _ss_old mov sp,word ptr _sp_old ;// Загружаем стек для возврата push bx push cx ret _restore_stack endp ;//------------------------------------------- ;// void far new_int13(void) ;// Новый обработчик INT 13h ;//------------------------------------------- _new_int13 proc far push ax push ds ;// Устанавливаем DS на сегмент данных TSR mov ax, DGROUP mov ds, ax ;// Увеличиваем флаг _unsafe_flag inc word ptr _unsafe_flag ;// Восстанавливаем DS и AX pop ds pop ax ;// Вызываем прерывание pushf call cs:old_int13 push ax push ds mov ax, DGROUP mov ds, ax ;// Уменьшаем флаг _unsafe_flag dec word ptr _unsafe_flag pop ds pop ax ret 2 _new_int13 endp end
Листинг 5.4. Файл tsrdemo\applicat.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <dos.h> #include "tsrdemo.h" int write_buf(void); extern unsigned keycode; int name_counter = 0; // ======================================== // application(void) // Вызывается при активизации TSR. Из этой // функции можно вызывать прерывания MS-DOS // ======================================== void application(void) { if(keycode == HotKeyRecording) { write_buf(); Beep(); } } // ======================================== // Beep // Выдача звукового сигнала // ======================================== void Beep(void) { union REGS regs; regs.x.ax = 0x0e07; regs.x.bx = 0x0000; int86 (0x10, ®s, ®s); } // ======================================== // get_vmode // Функция возвращает номер // текущего видеорежима // ======================================== int get_vmode(void) { char far *ptr; // Получаем указатель на байт, содержащий // номер текущего видеорежима ptr = (char far*)MK_FP (0x40, 0x49); return(*ptr); } // ======================================== // get_vbuf // Функция возвращает сегментный адрес // видеопамяти. Учитывается содержимое // регистров смещения адреса видеобуфера // ======================================== int get_vbuf(int vmode) { unsigned vbase; unsigned adr_6845; unsigned high; unsigned low; unsigned offs; // В зависимости от видеорежима базовый адрес // видеопамяти может быть 0xb000 или 0xb800 vbase = (vmode == 7) ? 0xb000 : 0xb800; // Получаем адрес порта видеоконтроллера adr_6845 = *(unsigned far*)(MK_FP (0x40, 0x63)); // Считываем содержимое регистров 12 и 13 // видеоконтроллера outp(adr_6845, 0xc); high = inp(adr_6845 + 1); outp(adr_6845, 0xd); low = inp(adr_6845 + 1); offs = ((high << 8) + low) >> 4; // Добавляем к базовому адресу видеопамяти // смещение, взятое из регистров видеоконтроллера vbase += offs; return(vbase); } // ======================================== // get_column // Функция возвращает количество символов в строке // для текущего видеорежима // ======================================== int get_column(void) { return(*(int _far *)(MK_FP (0x40, 0x4a))); } // ======================================== // get_row // Функция возвращает количество строк // для текущего видеорежима // ======================================== int get_row(void) { unsigned char ega_info; ega_info = *(unsigned char far*)(MK_FP (0x40, 0x87)); // Если тип контроллера не EGA , то размер // экрана по вертикали составляет 25 строк. // Если установлен контроллер EGA , число // строк находится в области данных // BIOS по адресу 0040:0084. if(ega_info == 0 || ( (ega_info & 8) != 0) ) { return(25); } else { return((*(unsigned char far *) (MK_FP (0x40, 0x84))) + 1); } } // ======================================== // write_buf // Функция записи содержимого видеобуфера в // файл // ======================================== int write_buf(void) { // Видеопамять состоит из байтов символов и байтов // атрибутов. Нам нужны байты символов chr. typedef struct _VIDEOBUF_ { unsigned char chr; unsigned char attr; } VIDEOBUF; VIDEOBUF far *vbuf; int i, j, k, max_col, max_row; FILE *out_file; char fname[20], ext[8]; // Определяем видеорежим i = get_vmode(); // Для графического режима ничего не записываем if(i > 3 && i != 7) return(-1); // Устанавливаем указатель vbuf на видеобуфер vbuf = (VIDEOBUF far *)MK_FP (get_vbuf(i), 0); // Определяем размеры экрана max_col = get_column(); max_row = get_row(); // Формируем имя файла для записи образа экрана itoa(name_counter++, ext, 10); strcpy(fname,"!grab"); strcat(fname, ext); strcat(fname,".scr"); out_file = fopen(fname,"wb+"); // Записываем содержимое видеобуфера в файл for(i = 0; i < max_row; i++) { for(j = 0; j < max_col; j++) { fputc(vbuf->chr, out_file); vbuf++; } // В конце каждой строки добавляем // символы перевода строки и // возврата каретки fputc(0xd, out_file); fputc(0xa, out_file); } fclose(out_file); return(0); }