...
...

Последняя серия "Санта-Барбары"...

Сегодня завершается затянувшаяся череда статей "сериала", посвященного введению в низкоуровневое программирование. Последние недели принесли несколько противоречивых читательских оценок. Кто-то звонил, кто-то писал в редакцию. Особенно запомнилась мимолетная встреча со знакомым программистом, который выразил сомнение, стоило ли так усердно "разжевывать" материал. Меня порадовало, когда он все же согласился, что это имело смысл.

Я взялся говорить об ассемблерных вставках, когда припомнил, что первый толчок к изучению и применению низкоуровневого программирования получил именно от публикаций в периодике, а не от перечитанных к тому времени книжек. Пришел и мой черед кому-нибудь помочь, объяснив, что не так страшен черт, как его малюют.

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

* Область данных.

0D84:0100 DB 0 0 0 0 0 * Точка входа в процедуру обработки int 08.

0D84:0105 PUSH BX 0D84:0106 CALL SHORT 0109 0D84:0109 POP BX * Определена база для адресации данных.

0D84:010A PUSHF 0D84:010B CS: 0D84:010C CALL FAR [BX-08] * Симулирован вызов предыдущей процедуры обработки int 08. Далее следует код, уменьшающий значение счетчика времени.

0D84:010F PUSH DS 0D84:0110 PUSH CS 0D84:0111 POP DS 0D84:0112 TEST BYTE PTR [BX-09],FF 0D84:0116 JZ 011B 0D84:0118 DEC BYTE PTR [BX-09] 0D84:011B POP DS 0D84:011C POP BX 0D84:011D IRET * Процедура обработки int 08 завершена.

* Точка входа в процедуру перехвата int 08.

0D84:011E PUSH ES 0D84:011F PUSH BX 0D84:0120 PUSH DS 0D84:0121 PUSH DX 0D84:0122 PUSH AX 0D84:0123 PUSH DI * Сохранены значения регистров, которые будут изменены.

0D84:0124 CALL SHORT 0127 0D84:0127 POP DI * Определена база для адресации данных.

0D84:0128 MOV AX,3508 0D84:012B INT 21 * В результате вызова функции DOS адрес текущей процедуры обработки int 08 в виде сегмент:смещение занесен в регистры ES:BX.

0D84:012D PUSH CS 0D84:012E POP DS 0D84:012F MOV [DI-26],BX 0D84:0132 MOV [DI-24],ES * Полученный адрес сохранен в области данных.

0D84:0135 MOV AX,2508 0D84:0138 MOV DX,DI 0D84:013A SUB DX,22 * Подготовлен вызов функции DOS, который назначит int 08 нашу процедуру обработки, адрес которой содержится в регистрах DS:DX.

0D84:013D INT 21 * int 08 перехвачено. С этого момента выполняется наша процедура обработки.

0D84:013F POP DI 0D84:0140 POP AX 0D84:0141 POP DX 0D84:0142 POP DS 0D84:0143 POP BX 0D84:0144 POP ES * Восстановлены измененные значения регистров.

0D84:0145 RETF * Произошел возврат в программу, вызывавшую код.

* Точка входа в процедуру освобождения int 08 от нашей программы обработки.

0D84:0146 PUSH AX 0D84:0147 PUSH BX 0D84:0148 PUSH DX 0D84:0149 PUSH DS * Сохранены значения регистров 0D84:014A CALL SHORT 014D 0D84:014D POP BX * Определена база для адресации данных.

0D84:014E PUSH CS 0D84:014F POP DS 0D84:0150 MOV DX,[BX-4C] 0D84:0153 MOV AX,[BX-4A] 0D84:0156 MOV DS,AX 0D84:0158 MOV AX,2508 * Адрес прежнего обработчика int 08, хранившийся в области данных, занесен в регистры DS:DX. Подготовлен вызов функции DOS, устанавливающей вектор прерывания.

0D84:015B INT 21 * В результате вызова функции DOS int 08 с этого момента обрабатывается прежней процедурой.

0D84:015D POP DS 0D84:015E POP DX 0D84:015F POP BX 0D84:0160 POP AX 0D84:0161 RETF * Измененные значения регистров восстановлены. Произошел возврат в вызывавшую программу.

Строго говоря, код приведенной вставки можно использовать в программах на любых языках программирования. В качестве примера был взят QBASIC, но это совсем необязательно. В QBASIC мы пользовались такими средствами языка, как возможность прочитать/записать байт по любому адресу памяти (PEEK/POKE), определить адрес переменной (VARSEG, VARPTR) и передать управление по любому адресу (CALL ABSOLUTE). Этого достаточно, чтобы работать с кодами, записанными в массив.

Как правило, средства низкого уровня не одинаковы и для разных языков, и для разных компиляторов одного языка. Дело в том, что они не входят в стандарт, и всякий разработчик компиляторов поступает по собственному разумению.

В каждом языке свои средства. Например, часто программисту разрешают применять так называемые псевдорегистровые переменные в операторах присваивания. В таких случаях после присваиваний вида RegAX=1 или i=RegBX в регистре AX оказывается единица или значение BX заносится в целочисленную переменную i. Наряду с псевдорегистровыми переменными обязательно есть служебная функция, такая как INT(номер_прерывания), программно вызывающая прерывание с заданным номером.

Кроме функции INT(n) часто встречается функция CODE(byte0, byte1, byte2...) с неограниченным числом аргументов. Каждый из аргументов - байт кода низкоуровневой вставки в виде положительного целого числа. Скажем, результат выполнения функции CODE(0CDH, 21H) такой же, как вызова INT(21H). Конечно, должна быть функция, возвращающая адреса переменных. Тогда указанных возможностей хватит, чтобы обращаться к прерываниям DOS и BIOS, а также писать короткие вставки кодов другого назначения. Обратите внимание, что пользоваться псевдорегистровыми переменными для обмена данными между программой и вставкой может оказаться удобнее, чем записывать значения в область данных вставки, хотя одно другого не исключает. Как вы понимаете, имена функций и переменных выбраны произвольно.

В некоторых компиляторах разрешается писать вставки не в кодах, а на языке ассемблера, то есть в виде мнемоник MOV, INT и так далее, прямо в тексте основной программы. Это, бесспорно, удобнее всего.

Бывают, правда, случаи, когда в языке вообще нет средств низкого уровня, но есть возможность многоязыковой разработки. Такое мне встречалось в некоторых СУБД. Чтобы вставить в программу какую-нибудь "красоту", скажем хранитель экрана, приходилось писать его отдельно на ассемблере, имитируя механизм передачи параметров, принятый для процедур СУБД. Потом объектный файл хранителя просто редактировался к основной программе на этапе сборки.

Способ записи кодов вставки в массив мне нравится больше других. Причин тому несколько. С одной стороны, разработать и отладить такой код можно при помощи DEBUG или компилятора ассемблера независимо от основной программы. Потом, вовсе не обязательно осуществлять инициализацию массива присваиваниями в тексте: можно просто загрузить код вставки в массив из файла, где он предварительно сохранен, скажем, отладчиком. Последнее открывает перед программистом широкие возможности по созданию библиотек низкоуровневых процедур и даже своего рода оверлеев. Наконец, этот способ универсален. Одними и теми же вставками можно пользоваться в разных программах на разных языках программирования. Изменения, связанные со спецификой того или иного языка или компилятора, коснутся не кода вставок, а лишь способов его вызова.

Словом, какой бы язык вы ни взяли, низкоуровневыми вставками можно пользоваться, если в нем есть соответствующие средства. Во-первых, для определения адреса переменной (как сегмента, так и смещения). Во-вторых, для организации передачи управления по желаемому адресу. В-третьих, для обмена данными между кодом вставки и основной программой.

Евгений Щербатюк

© Компьютерная газета