Расширенный ассемблер: NASM

Следующая глава | Предыдущая глава | Содержание | Указатель

Глава 3: Язык NASM

Перевод: AsmOS group, © 2001

3.1 Обзор ассемблерной строки NASM

Как и в большинстве ассемблеров, каждая строка NASM содержит (если это не макрос, препроцессорная или ассемблерная директива √ см. главу 4 и главу 5) комбинацию четырех полей:

метка: инструкция операнды ; комментарий

Как обычно, большинство этих полей необязательны; допускается присутствие или отсутствие любой комбинации метки, инструкции и комментария. Конечно, необходимость поля операндов определяется инструкцией процессора.

NASM не накладывает ограничений на количество пробелов в строке: метки могут иметь пробелы вначале, а инструкции могут не иметь никаких пробелов и т.п. Двоеточие после метки также необязательно. (Это означает, что если вы хотите поместить в строку инструкцию lodsb, а введете lodab, строка останется корректной, но вместо инструкции будет объявлена метка. Выявить данные опечатки отчасти можно, введя в строке запуска NASM ключ -w+orphan-labels — в этом случае при обнаружении метки без заключительного двоеточия будет выдаваться предупреждение).

Допустимыми символами в метках являются буквы, цифры, знаки _, $, #, @, ~, . и ?. Допустимые символы в начале метки (первый символ метки) — только буквы, точка (.) (со специальным значением, см. параграф 3.8), знак подчеркивания (_) и вопросительный знак (?). В идентификаторе может также присутствовать префикс $ для указания того, что это действительно идентификатор, а не зарезервированное слово; таким образом, если некоторый компонуемый вами модуль описывает символ eax, вы можете в коде NASM (для указания того, что это не регистр) сослаться на него так: $eax.

Поле инструкций может содержать любые процессорные инструкции: поддерживаются инструкции Pentium и P6, FPU, MMX, а также некоторые недокументированные инструкции. Перед инструкциями могут присутствовать префиксы LOCK, REP, REPE/REPZ или REPNE/REPNZ, используемые по их обычному предназначению. Поддерживаются префиксы размера адреса и операнда A16, A32, O16 и O32 — пример их использования приведен в главе 9. В качестве префикса инструкции вы можете использовать также обозначение сегментного регистра: код mov [bx],ax эквивалентен коду mov [es:bx],ax. Мы рекомендуем использовать последний синтаксис, т.к. он согласуется с другими синтаксическими особенностями языка, однако для инструкций, не имеющих операндов (например, LODSB) и требующих в некоторых случаях замены сегмента, на данный момент не существует никакого синтаксического способа обойти конструкцию es lodsb.

Префиксы, такие как CS, A32, LOCK или REPE могут присутствовать в строке самостоятельно и при этом NASM будет генерировать соответствующие префикс-байты.

В дополнение к инструкциям процессора, NASM поддерживает также несколько псевдо-инструкций, описанных в параграфе 3.2.

Операнды инструкций могут принимать несколько форм: они могут быть регистрами (например ax, bp, ebx, cr0: NASM не использует синтаксис стиля а-ля-gas, где имена регистров должны предваряться знаком %), эффективными адресами (см. параграф 3.3), константами (параграф 3.4) или выражениями (параграф 3.5).

Для инструкций сопроцессора NASM допускает различные формы синтаксиса: вы можете использовать двух-операндную форму, поддерживаемую MASMом, а также чисто NASMовскую одно-операндную форму. Подробности о форме каждой поддерживаемой инструкции приведены в приложении A. Например, вы можете написать:

fadd st1 ; это значит st0 := st0 + st1 fadd st0,st1 ; это то же самое fadd st1,st0 ; это значит st1 := st1 + st0 fadd to st1 ; это то же самое

Почти любая инструкция сопроцессора, ссылающаяся на содержимое памяти, должна использовать один из префиксов DWORD, QWORD или TWORD для указания на то, операнд какого размера должен участвовать в команде.

3.2 Псевдо-инструкции

Псевдо-инструкции не являются реальными инструкциями х86 процессора, но все равно помещаются в поле инструкций, т.к. это наиболее подходящее место для них. Текущими псевдо-инструкциями являются DB, DW, DD, DQ и DT, их копии для работы с неинициализированной памятью RESB, RESW, RESD, RESQ и REST, команды INCBIN, EQU и префикс TIMES.

3.2.1 DB и ее друзья: Объявление инициализированных данных

Как и в MASM, DB, DW, DD, DQ и DT используются для объявления инициализированных данных в выходном файле. Они могут использоваться достаточно многими способами:

db 0x55 ; просто байт 0x55 db 0x55,0x56,0x57 ; последовательно 3 байта db 'a',0x55 ; символьная константа db 'hello',13,10,'$' ; это строковая константа dw 0x1234 ; 0x34 0x12 dw 'a' ; 0x41 0x00 (это просто число) dw 'ab' ; 0x41 0x42 (символьная константа) dw 'abc' ; 0x41 0x42 0x43 0x00 (строка) dd 0x12345678 ; 0x78 0x56 0x34 0x12 dd 1.234567e20 ; константа с плавающей точкой dq 1.234567e20 ; двойной точности dt 1.234567e20 ; расширенной точности

DQ и DT не допускают в качестве операндов числовые или строковые константы.

3.2.2 RESB и ее друзья: Объявление неинициализированных данных

RESB, RESW, RESD, RESQ и REST разработаны для использования в BSS-секции модуля: они объявляют не инициализированное пространство для хранения данных. Каждая принимает один операнд, являющийся числом резервируемых байт, слов, двойных слов и т.д. Как было указано в параграфе 2.2.7, NASM не поддерживает синтаксис резервирования неинициализированного пространства, реализованный в MASM/TASM, где можно делать DW ? или подобные вещи: это заменено полностью. Операнд псевдо-инструкций класса RESB является критическим выражением: см. параграф 3.7.

Например:

buffer: resb 64 ; резервирование 64 байт wordvar: resw 1 ; резервирование слова realarray resq 10 ; массив из 10 чисел с плавающей точкой

3.2.3 INCBIN: Включение внешних бинарных файлов

INCBIN заимствована из старого ассемблера DevPac, работавшего на Amigе: она включает бинарный файл в выходной файл, оставляя его (бинарный файл) неизменным. Это может быть полезно (например) для включения картинок и музыки непосредственно исполняемый файл игрушки. Эта псевдо-инструкция может быть вызвана тремя разными способами:

incbin "file.dat" ; включение файла целиком incbin "file.dat",1024 ; пропуск первых 1024 байт incbin "file.dat",1024,512 ; пропуск первых 1024 и ; включение следующих 512 байт

3.2.4 EQU: Определение констант

EQU вводит символ для указанного константного значения: если используется EQU, в этой строке кода должна присутствовать метка. Смысл EQU — связать имя метки со значением ее (только) операнда. Данное определение абсолютно и не может быть позднее изменено. Например,

message db 'Привет, фуфел!' msglen equ $-message

определяет msglen как константу 12. msglen не может быть позднее переопределено. Это не определение препроцессора: значение msglen обрабатывается здесь только один раз при помощи значения $ (что такое $ √ см. параграф 3.5) в месте определения. Имейте в виду, что операнд EQU также является критическим выражением (параграф 3.7).

3.2.5 TIMES: Повторение инструкций или данных

Префикс TIMES заставляет инструкцию ассемблироваться несколько раз. Данная псевдо-инструкция отчасти представляет NASM-эквивалент синтаксиса DUP, поддерживающегося MASM-совместимыми ассемблерами. Вы можете написать, например

zerobuf: times 64 db 0

или что-то подобное; однако TIMES более разносторонняя инструкция. Аргумент TIMES — не просто числовая константа, а числовое выражение, поэтому вы можете писать следующие вещи:

buffer: db 'Привет, фуфел!' times 64-$+buffer db ' '

При этом будет резервироваться строго определенное пространство, начиная от метки buffer и длиной 64 байта. Наконец, TIMES может использоваться в обычных инструкциях, так что вы можете писать тривиальные развернутые циклы:

times 100 movsb

Заметим, что нет никакой принципиальной разницы между times 100 resb 1 и resb 100 за исключением того, что последняя инструкция будет обрабатываться примерно в 100 раз быстрее из-за внутренней структуры ассемблера.

Операнд псевдо-инструкции TIMES, подобно EQU и RESB, является критическим выражением (параграф 3.7).

Имейте также в виду, что TIMES не применима в макросах: причиной служит то, что TIMES обрабатывается после макро-фазы, позволяющей аргументу TIMES содержать выражение, подобное 64-$+buffer. Для повторения более одной строки кода или в сложных макросах используйте директиву препроцессора %rep.

3.3 Эффективные адреса

Эффективный адрес — это любой операнд инструкции со ссылкой на память. Эффективные адреса в NASM имеют очень простой синтаксис: они содержат выражение (в результате вычислений которого получается нужный адрес), обрамленное квадратными скобками. Например:

wordvar dw 123 mov ax,[wordvar] mov ax,[wordvar+1] mov ax,[es:wordvar+bx]

Любая другая ссылка, не соответствующая этой простой системе, для NASM недействительна, например es:wordvar[bx].

Более сложные эффективные адреса, когда вовлечено более одного регистра, работают точно также:

mov eax,[ebx*2+ecx+offset] mov ax,[bp+di+8]

NASM способен воспринимать алгебру таких выражений, поэтому он правильно транслирует вещи, выглядящие на первый взгляд недопустимыми:

mov eax,[ebx*5] ; ассемблируется как [ebx*4+ebx] mov eax,[label1*2-label2] ; то есть [label1+(label1-label2)]

Некоторые варианты эффективных адресов имеют более одной ассемблерной формы; в большинстве таких ситуаций NASM будет генерировать самую короткую из них. Например, у нас имеются простые ассемблерные инструкции [eax*2+0] и [eax+eax]. NASM будет генерировать последнюю из них, т.к. первый вариант требует дополнительно 4 байта для хранения нулевого смещения.

NASM имеет механизм подсказок, позволяющий создавать из [eax+ebx] и [ebx+eax] разные инструкции; это порой полезно, т.к. например [esi+ebp] и [ebp+esi] по умолчанию имеют разные сегментные регистры.

Несмотря на это, вы можете заставить NASM генерировать требуемые формы эффективных адресов при помощи ключевых слов BYTE, WORD, DWORD и NOSPLIT. Если вам нужно, чтобы [eax+3] ассемблировалась со смещением в двойное слово, вместо одного байта по умолчанию, вы можете написать [dword eax+3]. Точно также при помощи [byte eax+offset] вы можете заставить NASM использовать байтовые смещения для небольших значений, не определяемых при первом проходе (см. пример такого кода в параграфе 3.7). В особых случаях, [byte eax] будет кодироваться как [eax+0] с нулевым байтовым смещением, а [dword eax] будет кодироваться с нулевым смещением в двойное слово. Обычная форма, [eax], будет оставлена без смещения.

NASM будет разделять [eax*2] на [eax+eax], т.к. это позволяет избежать использования поля смещения и сэкономить некоторое пространство; соответственно, [eax*2+offset] будет разделено на [eax+eax+offset]. При помощи ключевого слова NOSPLIT вы можете запретить такое поведение NASM: [nosplit eax*2] будет буквально оттранслировано в [eax*2+0].

3.4 Константы

NASM знает четыре различных типа констант: числовые, символьные, строковые и с плавающей точкой.

3.4.1 Числовые константы

Числовая константа — это просто число. NASM позволят определять числа в различных системах счисления и различными способами: вы можете использовать суффиксы H, Q и B для шестнадцатеричных, восьмеричных и двоичных чисел соответственно; можете использовать для шестнадцатеричных чисел префикс в стиле С, а также префикс $ в стиле Borland Pascal. Однако имейте в виду, что префикс $ может быть также префиксом идентификаторов (см. параграф 3.1), поэтому первой цифрой шестнадцатеричного числа при использовании этого префикса должна быть обязательно цифра, а не буква.

Некоторые примеры числовых констант:

mov ax,100 ; десятичная mov ax,0a2h ; шестнадцатеричная mov ax,$0a2 ; снова hex: нужен 0 mov ax,0xa2 ; опять hex mov ax,777q ; восьмеричная mov ax,10010011b ; двоичная

3.4.2 Символьные константы

Символьная константа содержит от одного до четырех символов, заключенных в одиночные или двойные кавычки. Тип кавычек для NASM несущественен, поэтому если используются одинарные кавычки, двойные могут выступать в роли символа и, соответственно, наоборот.

Символьная константа, содержащая более одного символа, будет загружаться в обратном порядке следования байт: если вы пишете

mov eax,'abcd'

сгенерированной константой будет не 0x61626364, а 0x64636261, поэтому если сохранить эту константу в память, а затем прочитать, получится снова abcd, но никак не dcba. Это также влияет на инструкцию CPUID Пентиумов (см. section A.29).

3.4.3 Строковые константы

Строковые константы допустимы только в некоторых псевдо-инструкциях, а именно в семействе DB и инструкции INCBIN. Строковые константы похожи на символьные, только длиннее. Они обрабатываются как сцепленные друг с другом символьные константы. Так, например, следующие строки кода эквивалентны.

db 'hello' ; строковая константа db 'h','e','l','l','o' ; эквивалент из символьных констант

Следующие строки также эквивалентны:

dd 'ninechars' ; строковая константа в двойное слово dd 'nine','char','s' ; три двойных слова db 'ninechars',0,0,0 ; и действительно похоже

Обратите внимание, что когда используется db, константа типа 'ab' обрабатывается как строковая, хотя и достаточно коротка, чтобы быть символьной, потому что иначе db 'ab' имело бы тот же смысл, какой и db 'a', что глупо. Соответственно, трех- или четырехсимвольные константы, являющиеся операндами инструкции dw, обрабатываются также как строки.

3.4.4 Константы с плавающей точкой

Константы с плавающей точкой допустимы только в качестве аргументов DD, DQ и DT. Выражаются они традиционно: цифры, затем точка, затем возможно цифры после точки, и наконец, необязательная Е с последующей степенью. Точка обязательна, т.к. dd 1 NASM воспримет как объявление целой константы, в то время как dd 1.0 будет воспринята им правильно.

Несколько примеров:

dd 1.2 ; "простое" число dq 1.e10 ; 10,000,000,000 dq 1.e+10 ; синоним 1.e10 dq 1.e-10 ; 0.000 000 000 1 dt 3.141592653589793238462 ; число pi

В процессе компиляции NASM не может проводить вычисления над константами с плавающей точкой (это сделано с целью переносимости). Несмотря на то, что NASM генерирует код для х86 процессоров, сам по себе ассемблер может работать на любой системе с ANCI C компилятором. Само собой, ассемблер не может гарантировать присутствия устройства, обрабатывающего числа с плавающей точкой в формате Intel, поэтому стало бы необходимо включить собственный полный набор подпрограмм для работы с такими числами, что неизбежно привело бы к значительному увеличению размера самого ассемблера, хотя польза от этого была бы минимальна.

3.5 Выражения

Синтаксис выражений NASM подобен синтаксису выражений языка C.

NASM не гарантирует размер целых чисел, используемых для вычисления выражений при компиляции: с тех пор как NASM может вполне успешно компилировать и выполняться на 64-разрядных платформах, не будьте так уверены, что выражения вычисляются в 32-битных регистрах и что можно попробовать умышленно сделать переполнение. Это сработает не всегда. NASM гарантирует только то, что и ANSI C: вы всегда имеете дело как минимум с 32-битными регистрами.

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

Ниже перечислены арифметические операции NASM в порядке возрастания приоритета.

3.5.1 |: Побитовый оператор ИЛИ

Оператор | производит побитовую операцию ИЛИ, соответствующую процессорной инструкции OR. Побитовое ИЛИ имеет самый низкий приоритет среди арифметических операторов, поддерживаемых NASMом.

3.5.2 ^: Побитовый оператор ИСКЛЮЧАЮЩЕЕ ИЛИ

Оператор ^ обеспечивает выполнение побитовой операции ИСКЛЮЧАЮЩЕЕ ИЛИ.

3.5.3 &: Побитовый оператор И

Оператор & обеспечивает выполнение побитовой операции И.

3.5.4 << и >>: Операторы сдвига бит

<< производит сдвиг бит влево точно так, как это делается в С. Так, 5<<3 обрабатывается как 5 умножить на 8, или 40. >> производит сдвиг бит вправо; в NASM этот сдвиг всегда беззнаковый, поэтому биты, освобождаемые слева в результате сдвига, заполняются нулями, а не старшим знаковым разрядом.

3.5.5 + и : Операторы сложения и вычитания

Операторы + и выполняют обычное сложение и вычитание.

3.5.6 *, /, //, % и %%: Умножение и деление

* является оператором умножения. Операторы / и // обозначают деление: / соответствует беззнаковому делению, а // — знаковому. Подобно этому, операторы % и %% обеспечивают соответственно беззнаковое и знаковое получение остатка от деления (взятие по модулю).

NASM, также как и ANSI C, не дает никаких гарантий о физическом смысле знакового оператора взятия по модулю.

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

3.5.7 Унарные операторы: +, -, ~ и SEG

Наивысший приоритет в грамматике выражений NASM имеют операторы, применяемые к одному аргументу: оператор "минус" () изменяет знак своего операнда, оператор "плюс" (+) ничего не делает (введен для симметричности с минусом), оператор "тильда" (~) вычисляет дополнение операнда, а оператор SEG извлекает сегментный адрес операнда (более подробно описывается в параграфе 3.6).

3.6 SEG и WRT

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

Оператор SEG возвращает базу предопределенного сегмента символа, относительно которой вычисляется смещение последнего. Так следующий код

mov ax,seg symbol mov es,ax mov bx,symbol

будет загружать в пару ES:BX корректный указатель на символ symbol.

Бывают и более сложные случаи: т.к. 16-битные сегменты и группы способны перекрываться, вы возможно захотите иногда сослаться на некоторый символ при помощи базы сегмента, отличного от предопределенного. NASM позволяет это сделать при помощи ключевого слова WRT (With Reference To). Например, код

mov ax,weird_seg ; weird_seg является базой сегмента mov es,ax mov bx,symbol wrt weird_seg

загрузит в ES:BX другой, но функционально эквивалентный указатель на символ symbol.

NASM поддерживает дальние (межсегментные) вызовы подпрограмм и передачи управления при помощи синтаксиса call segment:offset, где segment и offset являются непосредственными значениями, поэтому для вызова дальней процедуры вы можете использовать следующий синтаксис:

call (seg procedure):procedure call weird_seg:(procedure wrt weird_seg)

(Круглые скобки включены для большей ясности приведенных инструкций. На практике они не нужны).

 

NASM также поддерживает синтаксис call far, являющийся аналогом первой из выше приведенных инструкций. В этих примерах инструкция JMP будет работать также, как CALL.

Для объявления дальнего указателя на сегмент данных, вы можете писать:

dw symbol, seg symbol

NASM не поддерживает более удобных аналогов этому объявлению, однако при помощи макропроцессора вы всегда можете их придумать.

3.7 Критические выражения

В отличие от TASM и других, NASM является двухпроходным ассемблером; он всегда делает только два прохода. Из-за этого он не способен "справиться" со сложными исходными файлами, требующими три и более проходов.

Первый проход используется для определения размера всех ассемблируемых инструкций и данных, поэтому на втором проходе (где генерируется код) известны адреса всех символов, на которые имеются ссылки. Таким образом, NASM не сможет обработать код, в котором размер зависит от значения символа, объявленного позднее, например:

times (label-$) db 0 label: db 'Где это я?'

Аргумент TIMES в этом случае должен точно рассчитываться для всех меток; NASM воспримет этот пример ошибочным, т.к. он не сможет узнать размер строки с TIMES. Для него это будет то же, что и заведомо ошибочный код

times (label-$+1) db 0 label: db 'А теперь я где?'

где любое значение аргумента TIMES по определению неверно!

NASM отклоняет такой код при помощи концепции т.н. критического выражения, определяемого как выражение, значение которого должно быть рассчитано на первом проходе и которое, следовательно, должно зависеть только от символов, описанных перед ним. Аргумент префикса TIMES является критическим выражением; по некоторым причинам аргументы псевдо-инструкций семейства RESB также являются критическими выражениями.

Критическое выражение может неожиданно возникнуть в следующем контексте:

mov ax,symbol1 symbol1 equ symbol2 symbol2:

На первом проходе NASM не может определить значение symbol1, т.к. он объявлен равным symbol2, который, в свою очередь, NASM еще "не видит". Соответственно на втором проходе, при обработке строки mov ax,symbol1 он не способен сгенерировать правильный код, потому что значение symbol1 остается неизвестным. На следующей строке, увидев EQU, NASM сможет определить значение symbol1, однако будет уже поздно.

NASM предотвращает возникновение данных проблем, вводя для критических выражений правосторонний оператор EQU, при котором объявление symbol1 будет отбраковано на первом проходе.

Еще одна похожая проблема, связанная с опережающими ссылками: рассмотрите следующий фрагмент кода.

mov eax,[ebx+offset] offset equ 10

На первом проходе NASM должен вычислить длину инструкции mov eax,[ebx+offset], не зная значение offset. Он никак не сможет узнать, что смещение offset представляет собой малую величину, вписывающуюся в однобайтное поле смещения и что можно "безбоязненно" сгенерировать более короткую форму эффективного адреса. Однако на первом проходе еще не известно, что такое offset — это может быть символ в сегменте кода и для него возможно нужна полная четырехбайтовая форма инструкции. Таким образом, размер инструкции рассчитывается исходя из четырехбайтовой адресной части. Сделав это предположение, на втором проходе NASM вынужден оставлять длину инструкции как есть, генерируя при этом не совсем оптимальный код. Данная проблема может быть разрешена путем объявления offset перед ее первым использованием или явным указанием на байтовый размер смещения: [byte ebx+offset].

3.8 Локальные метки

NASM дает специальную трактовку символов, начинающихся с точки. Метка, начинающаяся с точки, обрабатывается как локальная. Это означает, что она неразрывно связана с предыдущей нелокальной меткой. Например:

label1 ; некоторый код .loop ; еще какой-то код jne .loop ret label2 ; некоторый код .loop ; еще какой-то код jne .loop ret

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

Данный способ обработки локальных меток позаимствован из ассемблера DevPac (Amiga); однако NASM делает шаг вперед — он позволяет обращаться к локальным меткам из другой части кода. Это достигается путем описания локальной метки на основе предыдущей нелокальной. Описания .loop в примере выше в действительности описывают два разных символа: label1.loop и label2.loop, поэтому если вам это действительно надо, то можете написать:

label3 ; некоторый код ; и т.д. jmp label1.loop

Иногда бывает полезно, например, в макросах — определить метку, на которую можно ссылаться отовсюду, но которая не пересекается с обычным механизмом локальных меток. Такая метка не может быть нелокальной, так как существует последующее описание и ссылки на локальные метки; она также не может быть и локальной, вследствие того, что описывающий ее макрос не будет знать полное имя метки. Для разрешения этой проблемы в NASM введен третий тип меток, которые обычно используются только в описаниях макросов: если метка начинается со специального префикса ..@, она ничего не делает по отношению к механизму локальных меток. Таким образом, вы можете написать:

label1: ; нелокальная метка .local: ; это label1.local ..@foo: ; это специальный символ label2: ; другая нелокальная метка .local: ; это label2.local jmp ..@foo ; переход на три строки вверх

NASM имеет возможность определять другие специальные символы, начинающиеся с двух точек: например, ..start используется для указания точки входа в объектном формате obj (см. параграф 6.2.6).

Следующая глава | Предыдущая глава | Содержание | Указатель