Смешивание 16- и 32-битного кода в ассемблере NASM
Эта глава повествует о некоторых проблемах, связанных с использованием необычных способов адресации и инструкций перехода, возникающих при написании кода операционных систем, таких мест как инициализация защищенного режима, которые требуют, чтобы код, который оперирует с сегментами смешанных адресаций, например, код из 16-битного сегмента пытается модифицировать данные в 32-битном, или инструкции перехода между сегментами разной разрядности.9.1 Переходы между сегментами смешанной разрядности
Наиболее распространенные формы инструкций смешанной разрядности встречаются при написании 32-битной ОС: как только закончился ваш код настройки в 16-битном режиме, например, загрузка ядра, вам придется запускать его, переключаясь в защищенный режим и прыгая в 32-битный сегмент на точку входя ядра. В полностью 32-биной ОС, есть потребность в использовании только инструкций смешанной разрядности, потому что любой код до ее создания может быть выполнен как чистый 16-битный, и все написанное для нее после, может быть чистым 32-битным.
Пусть переход должен быть осуществлен по 48-битному дальнему адресу, так как сегмент, в который происходит переход 32-битный. Однако, он должен быть ассемблирован в 16-битном сегменте, поэтому, если мы для примера напишем,
jmp 0x1234:0x56789ABC ; ошибка!
то это будет неправильно работать, так как смещение будет обрезано до 0x9ABC и переход окажется обычным 16-битным.
В коде инициализации ядра Линукса, из-за неспособности as86 генерировать необходимые инструкции, их кодируют вручную, используя директиву DB. NASM способен сделать это лучше, он действительно самостоятельно генерирует правильный код. Вот как это делается правильно:
jmp dword 0x1234:0x56789ABC ; правильноПрефикс DWORD (строго говоря, он должен следовать после двоеточия, так как это объявление размера двойного слова для смещения, но NASM поддерживает и такую форму записи, потому что обе они не двусмысленны) сообщает, что смещение должно рассматриваться как дальнее, в предположении, что вы умышленно совершаете переход их 16-битного сегмента в 32-битный.
Вы можете сделать обратное действие, делая переход из 32-битного сегмента в 16-битный, указав префикс WORD:
jmp word 0x8765:0x4321 ; 32 to 16 bit
Если префикс WORD указан в 16-битном режиме, или префикс DWORD в 32-битном, они будут проигнорированы, потому что они переводят NASM в режим, в котором он и так находится.
9.2 Адресация между сегментами различной разрядности
Если ваша ОС является смесью 16 и 32-битной, или если вы пишете расширитель ДОС, наверняка захотите использовать несколько 16-битных и несколько 32-битных сегментов. Но с другой стороны, вам придется писать код в 16-битном сегменте, который обращается к данным в 32-битном сегменте, или наоборот.
Если данные в 32-битном сегменте расположены в передлах первых 64К сегмента, то к ним можно обращаться, используя обычные 16-битные операции, но рано или поздно, вам понадобится совершать 32-битную адресацию из 16-битного сегмента.
Самый легкий путь сделать это, убедится, что вы используете регистр для адреса, с этих пор каждый эффективный адрес, содержащийся в 32-битном регистре принудительно будет 32-битным адресом. Поэтому вы можете сделать так
mov eax,offset_into_32_bit_segment_specified_by_fs mov dword [fs:eax],0x11223344
Это хорошо, но немного громоздко (потому что мы проигрываем инструкцию и регистр) если вам уже известно точное смещение. Архитектура x86 поддерживает 32-битную эффективную адресацию, чтобы указать только 4-байтное смещение, поэтому почему NASM не мог бы генерировать инструкцию лучше для этих целей?
Он может. Как описано в параграфе 9.1, понадобится только предварить адрес ключевым словом DWORD, и это будет 32-битным адресом:
mov dword [fs:dword my_offset],0x11223344
Еще, как описано в параграфе 9.1, NASM не придирчив к использованию префикса DWORD, он может идти до или после замещения сегмента, поэтому неплохо будет выглядеть код с этой инструкцией вот так:
mov dword [dword fs:my_offset],0x11223344
Не удивляйтесь, что префикс DWORD вне квадратных скобок, он контролирует размер данных, сохраняемых по этому адресу, а один внутри квадратных скобок, который указывает на длину адреса. Последнее можно очень просто продемонстрировать:
mov word [dword 0x12345678],0x9ABC
Это объявление помещает 16 бит данных по адресу, указанному по 32-битному смещению.
Вы также можете указать префикс WORD или DWORD месте с префиксом FAR для косвенных дальних переходов или вызовов. Например:
call dword far [fs:word 0x4321]
Эта инструкция содержит адрес, указанный 16 битным смещением; она загружает 48-битный дальний указатель из него (16-битного сегмента и 32-битного смещения), и вызывает этот адрес.
9.3 Другие инструкции смешанного размера
Другой способ, который вы можете использовать для доступа к данным, является применение инструкций для работы со строками (LODSx, STOSx и так далее) или инструкции XLATB. Поскольку эти инструкции не имеют параметров, может показаться, что нет простого способа заставить их работать с 32-битными адресами из 16-битного сегмента.
Как раз для этого и предназначены префиксы a16 и a32 NASM-а. Если вы пишите LODSB в 16-битном сегменте, но предполагаете обращаться к строке из 32-битного сегмента, загрузите адрес в ESI и затем напишите
a32 lodsb
Этот префикс даставит использовать 32-битную адресацию, это означает, что LODSB загружает из [DS:ESI] вместо [DS:SI]. Чтобы получить доступ к строке в 16-битном сегменте из 32-битного, можно использовать соответствующий префикс a16.
Префиксы a16 и a32 могут быть применимы к любой инструкции из таблицы инструкций NASM-а, но для большинства из них создаются необходимые формы адресации и без них. Эти префиксы необходимы только для инструкций с неявной адресацией: CMPSx (параграф A.24), SCASx (параграф A.229), LODSx (параграф A.117), STOSx (параграф A.243), MOVSx (параграф A.137), INSx (параграф A.98), OUTSx (параграф A.149) и XLATB (параграф A.269). Также, различные инструкции push и pop (PUSHA и POPF также как и более частоиспользуемые PUSH и POP) могут использоваться с префиксами a16 и a32 для выбора SP или ESP для использования в качестве указателя стека, в случае, если размер сегмента стека имеет разрядность, отличную от кодового сегмента.
PUSH и POP, когда они используются с сегментными регистрами в 32-битном режиме, также имеют немного необычное поведение, когда они помещают в стек и выталкивают из него 4 байта за один раз, из них два верхних игнорируются и нижние два используются как значения сегментых регистров, с которыми вызывались инструкции. Чтобы зафиксировать 16-битное поведение инструкций push и pop для операций с сегментными регистрами, вы можете использовать префикс размера операнда o16:
o16 push ss o16 push ds
Этот код сохраняет двойные слово стекового пространства, запихивая два сегментных регистра в пространство, которое при нормальных обстоятельствах используется только под одну операцию помещения в стек.
(Вы также можете использовать префикс o32 для
указания 32-битного поведения, когда вы в 16-битном режиме, но это оказывается
менее полезно.)