Часть 1. Цель работы. Постановка задачи.
Признаюсь честно: мне нравится Ассемблер. Вернее, даже не сам Ассемблер, а стиль общения с компьютером через него.
В сети есть несколько примеров создания драйверов виртуальных устройств VxD на Ассемблере.
Но нет ни одного аналогичного примера для драйвера WDM.
Так исправим же эту досадную оплошность!
Создание несложного драйвера с использованием только лишь Ассемблера — довольно трудоёмкое занятие.
По двум причинам:
- Отсутствие ассемблерных заголовочных файлов для использования драйверного API.
- Методически трудная отладка драйверов в системе Windows.
Первая причина может быть некритичной. Были бы руки да голова. Ведь известно, что значительная часть заголовков Win32 API была переведена энтузиастами на Ассемблер. И работа эта немалая.
Вторая причина более серьёзна и именно она является сдерживающей. Практически, самым доступным способом отладки является отладочный вывод из самого драйвера
При этом код осторожно дописывается небольшими кусочками.
Однако, теперь у вас будет хороший кусок работающего кода. Мы его сейчас напишем! Изменяя и дополняя его, вы сможете создать свой собственный драйвер, довольно быстро и легко.
Сразу оговорюсь: я не собираюсь подробно объяснять принципы функционирования драйверной системы Windows и растолковывать специальные понятия. Для этого существует специальная литература.
Что нам потребуется? Вот что:
1) Текстовый редактор.
Notepad. Но лучше что-нибудь поудобнее, например, Патриот XP.
2) MS Windows DDK.
DDK содержит почти всё, что требуется для создания драйверов. Но нам важны: справка DDK, Ассемблер MASM 6.1, компоновщик Link, также оттуда мы возьмём библиотеки и заголовочные файлы для C (что с ними делать — см. далее).
3) Утилита для визуализации отладочного вывода. Я использую DbgView, который можно взять с сайта www.sysinternals.com
4) Delphi для компиляции тестовой программы.
Но вам необязательно набирать текст с нуля. К счастью, я сделал это до вас
Скачайте файл с исходниками проекта AsmDrv и распакуйте его в подкаталог \NTDDK\src\AsmDrv.
Вот, кажется, всё. Можно начинать!
3.2. Процедура инициализации
Каждый драйвер имеет процедуру инициализации. Эта процедура вызывается системой сразу после загрузки драйвера в память.
У нас такая процедура называется DriverEntry. Объявим её как
Driver Entry proc near public, DriverObject:PDRIVER_OBJECT, RegistryPath:PUNICODE_STRING
DriverObject — это указатель на служебную структуру, сопоставленную драйверу. Она используется системой для вызова процедур драйвера. Её-то и следует инициализировать — записать в эту структуру адреса соответствующих процедур нашего драйвера.
Наш драйвер довольно прост. Он будет отрабатывать только 4 стандартных запроса:
- IRP_MJ_CREATE — Вызов CreateFile() в приложении пользователя для установления связи с драйвером;
- IRP_MJ_CLOSE — Вызов CloseHandle() в приложении пользователя для разрыва связи с драйвером;
- IRP_MJ_DEVICE_CONTROL — Вызов DeviceIoControl() в приложении пользователя для запроса выполнения какой-либо функции в драйвере.
Все эти три запроса мы адресуем некоей диспетчерской функции OnDispatch. Мы узнаем о ней позже.
Четвёртый запрос — на выгрузку. Об этом пойдёт речь ниже.
А пока необходимо сделать ещё 2 важные вещи — создать логический объект устройства при помощи функции IoCreateDevice() и символическую связь, имя которой пользовательские приложения будут использовать для связи с драйвером при помощи функции CreateFile(). Символическая связь создаётся при помощи вызова IoCreateSymbolicLink():
; Инициализируем юникодовые строки с именами устройства и линка invoke RtlInitUnicodeString, offset NtDeviceName, offset wsNtDeviceName invoke RtlInitUnicodeString, offset Win32DeviceName, offset wsWin32DeviceName ; ; Создаём логический объект устройства invoke IoCreateDevice, DriverObject, 0, offset NtDeviceName, ; Проверим, не было ли ошибки. FILE_DEVICE_UNKNOWN,0,FALSE,offset DeviceObject; cmp eax,STATUS_SUCCESS jnz @F ; Создаём symbolic link ; в eax останется код результата invoke IoCreateSymbolicLink, offset Win32DeviceName, offset NtDeviceName @@: ret
Итак, только что мы завершили разбор процедуры инициализации.
Синтаксис
Общепринятого стандарта для синтаксиса языков ассемблера не существует. Однако большинство разработчиков языков ассемблера придерживаются общих традиционных подходов. Основные такие стандарты — Intel-синтаксис и AT&T-синтаксис.
Общий формат записи инструкций одинаков для обоих стандартов:
Опкод — это и есть собственно ассемблерная команда, мнемоника инструкции процессору. К ней могут быть добавлены префиксы (например, повторения, изменения типа адресации). В качестве операндов могут выступать константы, названия регистров, адреса в оперативной памяти и так далее. Различия между стандартами Intel и AT&T касаются в основном порядка перечисления операндов и их синтаксиса при разных методах адресации.
Используемые команды обычно одинаковы для всех процессоров одной архитектуры или семейства архитектур (среди широко известных — команды процессоров и контроллеров Motorola, ARM, x86). Они описываются в спецификации процессоров.
Например, процессор Zilog Z80 наследовал систему команд Intel i8080, расширил ее и поменял некоторые команды (и обозначения регистров) на свой лад. Например, сменил Intel-команду mov на ld. Процессоры Motorola Fireball наследовали систему команд Z80, несколько ее урезав. Вместе с тем Motorola официально вернулась к Intel-командам, и в данный момент половина ассемблеров для Fireball работает с Intel-командами, а половина — с командами Zilog.
О программе
В SASM Вы можете легко разрабатывать и выполнять программы, написанные на языках ассемблера NASM, MASM, GAS, FASM. Вводите код в форму и запускайте приложение. В Windows также возможен запуск приложения в отдельном окне. Входные данные указывайте в поле «Ввод». В поле «Вывод» Вы сможете увидеть результат работы программы. При этом все сообщения и ошибки компиляции будут выводиться в форму снизу. Вы можете сохранять исходный или скомпилированный (exe) код программы в файл, а также загружать свои программы из файла.
Программа поддерживает работу с несколькими проектами – новые файлы открываются и создаются в новых вкладках. При выходе из программы текущий набор открытых файлов сохраняется. При следующем запуске Вы сможете восстановить предыдущую сессию. В параметрах настраивается шрифт, цветовая схема и текст, отображающийся при создании нового файла. Интерфейс программы доступен на восьми языках (русский, английский, турецкий (спасибо Ali Goren), китайский (спасибо Ahmed Zetao Yang), немецкий (спасибо Sebastian Fischer), итальянский (спасибо Carlo Dapor), польский (спасибо Krzysztof Rossa), иврит (спасибо Elian Kamal), испанский (спасибо Mariano Cordoba)). Все окна в программе плавающие, с возможностью закрепления в одной из множества позиций. Имеется возможность переназначения горячих клавиш.
Стандартное меню «Правка» дополнено возможностью комментирования/раскомментирования выделенного куска кода и создания/удаления отступа в 4 пробела (Tab/Shift+Tab).
В SASM вы можете находить ошибки в своих программах с помощью интерфейса к отладчику gdb. В программе можно просматривать значения регистров и переменных, а также устанавливать точки останова и перемещаться по отлаживаемой программе. Дополнительно имеется возможность выполнять произвольные команды отладчика gdb, результаты которых будут отображаться в логе.
SASM полностью поддерживает работу с четырьмя ассемблерами NASM, MASM, GAS, FASM в двух режимах — x64 и x86, переключаться между которыми можно в настройках на вкладке «Построение». Там же можно изменить опции ассемблера и компоновщика и выбрать, какие программы будут использоваться для ассемблирования и компоновки.
Немного фактов и цифр
В качестве примера того, почему ассемблер — это хорошо, могу привести Дисплейный модуль 128х128,
на микроконтроллере ATMega328P, работающий на частоте 20 МГц. Модуль, вообщем, работает и не тормозит, но захотелось мне его переделать под ATMega8A
на 16 МГц. Преимущества последней очевидны — она штатно умеет работать на максимальной частоте при питании от 3.3В (ATMega328P по даташиту при
таком напряжении гарантированно будет работать только на 10 МГц), стоит в раза в 3-4 дешевле, да и достать её легче. Вообщем, захотелось мне
сделать для ZX-магнитофона дисплейный модуль с питанием от 3.3В на ATMega8A, возможно, с немного урезанным функционалом, но с максимально
компактной и быстрой прошивкой. Прошивка модуля на ATMega328P на тот момент имела размер в почти 20Кб (из которых ровно 7Кб занимали шрифты, 5х7
и 13х15). Шрифты были ужаты до 3.5 КБ путём оптимизации их формата. В результате под код осталось чуть более 4кб флеша (с учётом того,
что надо ещё оставить место для bootloader-а). В итоге, мне удалось впихнуть всю прошивку в примерно 3Кб, сохранив основной функционал, а
именно рисование точек и линий, рисование и заливка прямоугольников, окружностей и текста (шрифтами 5х7 и 13х15, латиница + кириллица + основные
символы). При этом был удалён код работы с клавиатурой, пищалкой и подсветкой (только потому, что в случае ZX-магнитофона он не нужен), но оставшихся
свободных полутора килобайт без проблем хватит на эти вещи. При этом производительность новой прошивки должна быть ощутимо выше, т.к., чем меньше
кода, тем быстрее он будет выполняться.
Использование программы
Этот раздел описывает
использование компилятора и
встроенного редактора
Открытие
файлов
В WAVRASM могут быть открыты как
новые так и существующие файлы.
Количество открытых файлов
ограничено размером памяти, однако
объём одного файла не может
превышать 28 килобайт (в связи с
ограничением MS-Windows). Компиляция
файлов большего размера возможна,
но они не могут быть редактируемы
встроенным редактором. Каждый файл
открывается в отдельном окне.
Сообщения об
ошибках
После компиляции программы
появляется окно сообщений. Все
обнаруженные компилятором ошибки
будут перечислены в этом окне. При
выборе строки с сообщением о
ошибке, строка исходного файла, в
которой найдена ошибка, становится
красной. Если же ошибка находится
во вложенном файле, то этого
подсвечивания не произойдёт.
Если по строке в окне сообщений
клацнуть дважды, то окно файла с
указанной ошибкой становится
активным, и курсор помещается в
начале строки содержащей ошибку.
Если же файл с ошибкой не открыт
(например это вложенный файл) то он
будет автоматически открыт.
Учтите, что если вы внесли
изменения в исходные тексты
(добавили или удалили строки), то
информация о номерах строк в окне
сообщений не является корректной.
Опции
Некоторые установки программы
могут быть изменены через пункт
меню «Options».
В поле ввода, озаглавленном
«List-file extension», вводится
расширение, используемое для файла
листинга, а в поле «Output-file
extension» находится расширение для
файлов с результатом компиляции
программы. В прямоугольнике «Output
file format» можно выбрать формат
выходного файла (как правило
используется интеловский). Однако
это не влияет на объектный файл
(используемый AVR Studio), который
всегда имеет один и тот же формат, и
расширение OBJ. Если в исходном файле
присутствует сегмент EEPROM то будет
также создан файл с расширением EEP.
Установки заданные в данном окне
запоминаются на постоянно, и при
следующем запуске программы, их нет
необходимости переустанавливать.
Опция «Wrap relative jumps» даёт
возможность «заворачивать»
адреса. Эта опция может быть
использована только на чипах с
объёмом программной памяти 4К слов
(8К байт), при этом становится
возможным делать относительные
переходы (rjmp) и вызовы подпрограмм
(rcall) по всей памяти.
Опция «Save before assemble» указывает
программе на необходимость
автоматического сохранения
активного окна (и только его) перед
компиляцией.
Если вы хотите, чтобы при закрытии
программы закрывались все
открытые окна, то поставьте галочку
в поле «Close all windows before exit».
Atmel, AVR являются
зарегистрированными товарными
знаками фирмы Atmel Corporation
Перевод выполнил Руслан
Шимкевич, [email protected]
Почему ассемблер для AVR ужасен
Цена эффективности ассемблера — его высокая сложность. Хотя, сама по себе система машинных команд МК достаточно проста. Но ассемблерные компиляторы
до безобразия примитивные (по кр.мере, AVRA и GCC). Программа состоит из функций, а функции используют регистры для обработки данных.
Всего доступно 32 8-битных регистра, которых обычно бывает более, чем достаточно, чтобы держать основные данные внутри функции. Но, при этом, нет
никакой возможности присвоить этим регистрам какие-то осмысленные имена внутри функции. А человеческий мозг, как известно, может более-менее
эффективно работать одновременно с 7±2 объектами. И, когда этих объектов (т.е., регистров) раза в два больше, то мозг взрывается, и читать код,
пытаясь держать в уме таблицу соответствия между регистрами и их назначением, становится категорически невозможно. Приходится писать комментарии.
А ещё сложнее такой код изменять.
Второе невыносимое неудобство — это передача аргументов функциям. Тут, аналогично, приходится держать в уме (или копипастить комментарии), что в
каком регистре передаётся. Что так же убивает всякую читаемость кода.
Одно только устранение этих двух неудобств делает код в разы более читаемым.
Вообще, тут мне вспоминается язык С—, который позволяет писать Си-подобный код и лёгкими ассемблерными вставками и доступом к регистрам как к
переменным. Достаточно забавная была штука, но, к сожалению, для AVR ничего такого не создано.
Реализация
Windows
В качестве ассемблера для NASM используется nasm 2.11.02, в качестве компоновщика — gcc 4.6.2 из MinGW (gcc 4.8.1 из MinGW64 в режиме x64) или ld 2.22 из MinGW (ld 2.23.2 из MinGW64 в режиме x64).
Версии ассемблеров и компоновщиков для NASM подобраны с учетом рекомендуемых программ для курса «Архитектура ЭВМ и язык ассемблера» ВМК МГУ 1-го потока.
Также в программу включен отладчик gdb 7.4 (7.6 для x64) из пакета MinGW и немного измененная для отладки библиотека макросов ввода-вывода.
Начиная с версии 3.0, в SASM включены fasm 1.71.39 и gas 2.23.1 из MinGW (gas 2.23.2 из MinGW64).
Ассемблер MASM невозможно было включить в сборку из-за его лицензии. Чтобы им воспользоваться, Вы должны установить MASM на Ваш компьютер с сайта https://www.masm32.com/ и указать пути до ассемблера (ml.exe, путь обычно «C:/masm32/bin/ml.exe») и до компоновщика (link.exe, путь обычно «C:/masm32/bin/link.exe») в соответствующих полях на вкладке «Построение».
Под Windows SASM после установки сразу готов к работе.
Linux
Для работы программы на Linux должны быть установлены: nasm или gas (если их планируется использовать, fasm уже включён в сборку), gcc, gdb (для отладки).
Больше информации о программе и её использовании можно получить в Wiki проекта на GitHub.
Почему ассемблер для AVR — это хорошо
Не смотря на все утверждения о том, что современные компиляторы научились отлично оптимизировать код лучше человека, это не так. По кр.мере,
применительно к AVR GCC. Практика показывает, что создаваемый им код может быть ощутимо улучшен по размеру (и, соответственно, скорости выполнения).
Взять, например, обработчики прерываний, которые должны сохранять все используемые регистры на старте и восстанавливать их при завершении.
Так вот тут AVR GCC часто использует в обработчике регистров больше, чем надо (т.е., использует несколько разных регистров там, где можно было
бы использовать один и тот же регистр повторно). И, соответственно, имеем лишнюю работу по сохранению/восстановлению из стека (это при том,
что операции со стеком занимают по два машинных цикла). Оптимизация обработчиков прерываний особенно важна если они вызываются десятки тысяч
раз в секунду, тогда даже устранение одной лишней пары PUSH/POP даст ощутимую экономию ресурсов CPU.
Другой пример — функции, работающие с десятком (примерно) переменных и аргументов. GCC может свободно использовать большую часть верхних регистров
(r18 — r31, подробнее см. тут) в коде Си-функции не заботясь о их сохранении. Если же этих
регистров ему немного не хватило, компилятор вызывает довольно “жирные” подпрограммы и
при входе в функцию и выходе из неё. Функции эти сохраняют на входе стеке и восстанавливают на
выходе все нижние регистры (все, без разбора). Это также даёт ощутимый оверхед. При том, что часто этой же функции, переписанной руками,
будет достаточно доступных регистров, и сохранять вообще ничего не придётся.
Третий пример — не все команды ассемблера имеют свои аналоги в Си. Например, проход по битам байтовой переменно на Си выльется в монструозный цикл,
внутри которого вычисляется битовая маска и делается логическое И по этой маске. Тогда как на ассемблере такой код может быть гораздо проще сделан
с использованием команды . Аналогичные моменты возникают при работе с массивами и строками, когда ассемблерный код может быть более лаконичен.
Итого, на ассемблере имеет смысл почти всегда писать обработчики прерываний и наиболее требовательные к производительности функции. Также на нём
можно писать совсем простые прошивки. И, да, если писать код целиком на ассемблере, то можно свободно использовать в нём дополнительные регистры
r0-r17, которые GCC использует по своему усмотрению (и при написании ассемблерных процедур, вызываемых из Си-кода, программист должен заботиться о
сохранении и восстановлении этих регистров).
Часть 5. Компиляция и сборка.
Для компиляции программы следует выполнить командный файл assemble.cmd.
Его содержимое:
..\..\bin\ml.exe -coff -Fl -c -Foasmdrv.obj main.asm
В результате мы получим листинг main.lst и объектный модуль asmdrv.obj.
Дальше мы должны собрать бинарник драйвера из объектного модуля. Для этой цели существует команда link.cmd:
..\..\bin\link.exe @linkcmd.rsp
в файле linkcmd.rsp размещены настройки линкера. Полный список выглядит так:
-MACHINE:IX86 -STACK:32768,4096 -OPT:REF -OPT:ICF -INCREMENTAL:NO -FORCE:MULTIPLE -RELEASE -DEFAULTLIB:wdm.lib -DRIVER -ALIGN:0x20 -SUBSYSTEM:NATIVE -BASE:0x10000 -ENTRY:DriverEntry@8 -OUT:disk1\asmdrv.sys asmdrv.obj
В результате сборки мы получаем файл AsmDrv.sys в подкаталоге Disk1.
Препроцессор для ассемблера
Осознав, что на чистом ассемблере писать что-то более-менее большое решительно невозможно, я решил сделать свой препроцессор, который будет расширять
синтаксис языка и сохранять файл для компиляции. Препроцессор написан на яве и дружит с Atmel assembler/AVRA и AVR GCC.
Сначала я добавил возможность понятие процедуры, имеющей аргументы, локальные псевдонимы для регистров и локальные метки. Затем, сделал циклы и
условные операции. Далее — возможность работать с группами регистров как единым целым (для организации переменных длиной более, чем 8 бит). По
мере развития проекта, а исходном, когда-то ассемблерном коде, становилось все меньше и меньше ассемблера..
Препроцессор позволяет как писать код на чистом ассемблере, деля минимальные вставки, так и почти полностью перейти на Си-подобный синтаксис.
Создаём исполняемый файл PRG.COM.
Для достижения нашей цели делаем следующее.
;Строка, после точки с запятой является комментарием
;и не обрабатывается ассемблером
; prg.asm — название файла.
.model tiny ; создаём программу типа СОМ
.code ; начало сегмента кода
org 100h ; начальное значение смещения программы в памяти — 100h
start:
mov ah,9 ; номер функции DOS — в АН
mov dx,offset message ; адрес строки — в DX
int 21h ; вызов т.н. «прерывания» — системной функции DOS
ret ; завершение СОМ-программы
message db «Hello, World!»,0Dh,0Ah,’$’ ; строка для вывода
end start ; конец программы.
1 |
;Строка, после точки с запятой является комментарием .modeltiny; создаём программу типа СОМ .code; начало сегмента кода org100h; начальное значение смещения программы в памяти — 100h start movah,9; номер функции DOS — в АН movdx,offsetmessage; адрес строки — в DX int21h; вызов т.н. «прерывания» — системной функции DOS ret; завершение СОМ-программы messagedb»Hello, World!»,0Dh,0Ah,’$’; строка для вывода endstart; конец программы. |
В папке D:\TASM.2_0\TASM\ находим «батник» ASM-COM.BAT со следующим текстом:
tasm.exe prg.asm
tlink.exe /t /x prg.obj
1 |
tasm.exeprg.asm tlink.exetxprg.obj |
Первая строка — запуск транслятора с названием нашего файла с кодом, расположенного в одной директории с транслятором.
Вторая строка — запуск компилятора с параметрами /t /x и название объектного файла — prg.obj, получившегося в результате выполнения первой команды.
Чтобы посмотреть список всех возможных параметров с пояснениями для файлов tasm.exe и tlink.exe необходимо запустить эти программы без параметров. Если вы сделаете это, не выходя из оболочки NC, то, чтобы просмотреть чистое окно DOS нажмите Ctrl+O, чтобы вернуться в NC, нажмите сочетание клавиш повторно.
После запуска ASM-COM.BAT в этой же директории появится файл prg.com. Запустив его мы увидим сообщение «Hello World!» в окне MS-DOS (при необходимости просмотра, снова применяем Ctrl+O).
Батник ASM-EXE.BAT предназначен для создания исполняемого файла формате *.EXE (предусматривает раздельную сегментацию для кода, данных и стека — наиболее распространённый формат исполняемых файлов DOS).
Батник COMPLEX.BAT предназначен для создания исполняемых файлов из двух файлов кода (названия обязательно должны быть prg.asm, prg1.asm).
Наша первая программа на ассемблере прекрасно работает!