Ассемблеры для Linux: Сравнение GAS и NASM
Параллельное рассмотрение GNU Assembler (GAS) и Netwide Assembler (NASM)
Рэм Нараян, инженер-программист, IBM
Описание: В этой статье объясняются некоторые наиболее важные синтаксические и семантические различия двух самых популярных ассемблеров для Linux® - GNU Assembler (GAS) и Netwide Assembler (NASM), а также различия в базовом синтаксисе, переменных и доступе к памяти, обработке макросов, функциях и внешних подпрограммах, работе со стеком и методиках простого повторения блоков кода.
В отличие от других языков для программирования на ассемблере необходимо знание архитектуры процессора машины, для которой выполняется программирование. Программы на ассемблере совершенно непортируемы, часто очень трудоемки в поддержке и понимании и, как правило, содержат большое количество строк кода. Однако наряду с этими ограничениями они имеют преимущество в скорости и размере запускаемого на машине двоичного кода.
Хотя по программированию под Linux на уровне ассемблера доступно большое количество информации, эта статья подробно показывает разницу между синтаксисами двух вариантов ассемблера в таком виде, чтобы помочь вам проще выполнять преобразование из одного вида ассемблера в другой. Эта статья родилась на основе моих собственных попыток освоить такое преобразование.
В этой статье используется ряд примеров программ. Каждая программа иллюстрирует те или иные особенности и сопровождается обсуждением и сравнением синтаксиса. Несмотря на то, что охватить все различия NASM и GAS невозможно, я попытаюсь раскрыть основные моменты и заложить основу для дальнейшего изучения этого вопроса. Даже если вы знакомы и с NASM, и с GAS, вы можете найти здесь что-нибудь полезное, например, макросы.
В этой статье предполагается, что вы по меньшей мере знакомы с терминологией ассемблера и программировали на ассемблере с использованием синтаксиса Intel®, возможно, с помощью NASM под Linux или Windows. Из этой статьи вы не узнаете, как вводить код в редактор или как транслировать и компоновать код (однако во врезке приведено краткое напоминание). Вы должны быть знакомы с операционной системой Linux (подойдет любой дистрибутив Linux; я использовал Red Hat и Slackware) и основными инструментами GNU, такими как gcc и ld, а также вы должны программировать на машине x86.
Теперь я расскажу о том, что вы сможете найти в этой статье, а чего не сможете.
Трансляция:
GAS:
as –o program.o
program.s
NASM:
nasm –f elf –o
program.o program.asm
Компоновка
(общая для обоих типов ассемблера):
ld –o program
program.o
Компоновка при
использовании внешней библиотеки C:
ld
–-dynamic-linker /lib/ld-linux.so.2 –lc –o program program.o
В этой статье описываются:
В этой статье не рассматриваются:
Дополнительную информацию можно найти в официальных руководствах к ассемблерам (ссылки можно найти в разделе Ресурсы), так как они являются наиболее полными источниками информации.
В листинге 1 показана очень простая программа, которая просто завершается с кодом выхода 2. Эта простая программа описывает базовую структуру программ на ассемблерах GAS и NASM.
Листинг 1. Программа, которая выходит с кодом 2 |
|||||
Строка |
NASM |
GAS |
|||
|
|
|
Теперь немного пояснений.
Одним из основных различий между NASM и GAS является синтаксис. В GAS используется относительно старый синтаксис AT&T, характерный для GAS и некоторых старых ассемблеров, тогда как NASM использует синтаксис Intel, поддерживаемый большинством ассемблеров, в том числе TASM и MASM. (Современные версии GAS поддерживают директиву .intel_syntax, которая позволяет использовать синтаксис Intel в GAS.)
Ниже приведены некоторые из наиболее значимых отличий, изложенные в сжатом виде на основании руководства GAS:
В обоих ассемблерах названия регистров одинаковы, но различается синтаксис их использования, равно как и синтаксис режимов адресации. Кроме того, директивы ассемблера GAS начинаются с ".", а в NASM это не так.
Секция .text – это место, где начинается выполнение кода процессором. Для того чтобы символ был виден компоновщику и доступен для других компонуемых модулей, используется ключевое слово global (в GAS также .globl и .global) . На стороне NASM листинга 1 global _start отмечает символ _start как видимый идентификатор, поэтому компоновщик знает, где находится вход в программу и начинается исполнение. Так же, как и NASM, GAS ищет метку _start как точку входа в программу по умолчанию. И в GAS, и в NASM метки всегда заканчиваются двоеточием.
Прерывания - это способ сообщить ОС о том, что требуются ее услуги. Эту работу в нашей программе выполняет инструкция int в строке 16. Обозначения прерываний в GAS и в NASM одинаковы. Для обозначения шестнадцатеричного числа в GAS используется префикс 0x, а в NASM - суффикс h. Поскольку в GAS у непосредственных операндов указывается префикс $, для вызова Linux и запроса сервиса используется шестнадцатеричное число 80 - $0x80.
int $0x80 (или 80h в NASM). Код сервиса записывается в регистр EAX. Для запроса выхода из программы в регистр EAX записывается значение 1 (для системного вызова Linux "выход"). В регистр EBX записывается код выхода (в нашем случае, 2), число, которое будет возвращаться в ОС. (Вы можете отследить это число, введя в командной строке echo $?.)
И, наконец, немного о комментариях. GAS поддерживает комментарии в стиле C (/* */), в стиле C++ (//) и в стиле shell (#). NASM поддерживает комментарии в одну строку, начинающиеся с символа ";".
Этот раздел начнем с примера программы, которая находит максимальное из трех чисел.
Листинг 2. Программа, которая находит максимальное из трех чисел |
|||||
Строка |
NASM |
GAS |
|||
|
|
|
Вы можете увидеть несколько различий в приведенных выше объявлениях переменных памяти. В NASM для объявления 32-, 16- и 8-разрядных чисел используются директивы dd, dw и db соответственно, тогда как в GAS для той же цели используются .long, .int и .byte. В GAS также используются другие директивы, такие как .ascii, .asciz и .string. В GAS переменные объявляются так же, как и любые другие метки (с помощью двоеточия), а в NASM просто вводится название переменной (без двоеточия) перед директивой выделения памяти (dd, dw и т.д.), после чего указывается значение переменной.
В строке 18 листинга 2 показан пример режима косвенной адресации памяти. Для разыменования значения по указанному адресу в памяти, в NASM используются квадратные скобки: [var1]. Для разыменования значения в GAS используются круглые скобки: (var1). Использование других режимов адресации будет рассмотрено ниже в этой статье.
В листинге 3 показана модель для этого раздела; на вход она принимает имя пользователя и возвращает приветствие.
Листинг 3. Программа, считывающая строку и отображающая приветствие пользователя |
|||||
Строка |
NASM |
GAS |
|||
|
|
|
Заголовок этого раздела предполагает обсуждение макросов, и оба ассемблера, и NASM, и GAS поддерживают их. Однако прежде чем переходить к макросам, будет полезно сравнить несколько других особенностей.
В листинге 3 показана модель неинициализированной памяти, определенной с помощью директивы раздела .bss (строка 14). BSS означает "block storage segment" ("блочный сегмент хранения", изначально означал "block started by symbol" - "блок, начатый с символа"), и при запуске программы память, зарезервированная под раздел BSS, инициализируется нулями. У объектов в разделе BSS есть только название и размер, но нет значения. Переменные, объявленные в разделе BSS, фактически не занимают пространства, в отличие от сегментов данных.
Для выделения пространства байтов, слов или двойных слов в разделе BSS в NASM используются ключевые слова resb, resw и resd. С другой стороны, в GAS для определения байтового пространства используется ключевое слово .lcomm. Обратите внимание на способ объявления названия переменной в обеих версиях программы. В NASM перед названием переменной указывается ключевое слово resb (или resw, или resd), после чего указывается величина резервируемого пространства, тогда как в GAS название переменной следует за ключевым словом .lcomm, после чего через точку с запятой указывается величина резервируемого пространства. Разница показана ниже:
NASM: varname resb size
GAS: .lcomm varname, size
В листинге 2 также введено понятие счетчика адресов (строка 6). В NASM для управления счетчиком адресов предоставляется специальная переменная (переменные $ и $$). Методов управления счетчиком адресов в GAS нет, поэтому для расчета следующей ячейки хранения (данных, инструкций и т.п.) следует использовать метки.
Например, для расчета длины строки в NASM нужно использовать следующее выражение:
prompt_str db 'Enter your
name: '
STR_SIZE equ $ - prompt_str ; $ - счетчик адресов
$ возвращает текущее значение счетчика адресов, и вычитание значения метки (все названия переменных являются метками) из этого счетчика дает количество байт между объявлением метки и текущей ячейкой. Для определения значения переменной STR_SIZE для следующего выражения используется директива equ. Такое же выражение в GAS будет выглядеть следующим образом:
prompt_str:
.ascii "Enter Your Name: "
pstr_end:
.set STR_SIZE, pstr_end - prompt_str
Конечная метка (pstr_end) задает адрес следующей ячейки, и вычитание из него адреса начальной метки дает размер. Также стоит обратить внимание, что при использовании .set для инициализации значения переменной STR_SIZE выражением после названия переменной указывается запятая. Также может быть использована соответствующая директива .equ. В NASM нет директивы, аналогичной set в GAS.
Как я упоминал выше, в листинге 3 используются макросы (строка 21). В NASM и GAS существует несколько различных технологий макросов, в том числе макросы одной строкой и переопределение макросов, но здесь я рассматриваю только простейший тип. В основном макросы в ассемблере используются для ясности кода. Вместо того чтобы вводить один и тот же кусок кода раз за разом, вы можете создать макрос и использовать его, что исключает такое повторение и, убирая нагромождения, улучшает внешний вид и удобство чтения кода.
Пользователи NASM могут быть знакомы с объявлением макросов с помощью директивы %beginmacro и завершением их с помощью директивы %endmacro. После директивы %beginmacro указывается название макроса. После названия вводится число, обозначающее ожидаемое количество аргументов макроса. Аргументы макроса в NASM нумеруются последовательно, начиная с 1. Таким образом, первым аргументом макроса будет %1, вторым -- %2, третьим -- %3 и так далее. Например:
%beginmacro macroname 2
mov eax, %1
mov ebx, %2
%endmacro
Этот код создает макрос с двумя аргументами, первым из которых является %1, а вторым %2. Таким образом, вызов определенного выше макроса будет выполняться следующим образом:
macroname 5, 6
Макрос также может создаваться и без аргументов, в этом случае никаких чисел не указывается.
Теперь давайте рассмотрим, как макросы используются в GAS. Для определения макросов в GAS имеются директивы .macro и .endm. После директивы .macro указывается название макроса, у которого могут быть аргументы, а могут и не быть. Аргументы макроса в GAS обозначаются именами. Например:
.macro macroname arg1, arg2
movl \arg1, %eax
movl \arg2, %ebx
.endm
При использовании названия аргумента в коде макроса перед этим названием ставится обратная косая черта. Если этого не сделать, компоновщик будет трактовать названия как метки, а не как аргументы, вследствие чего будет возникать ошибка.
Функции, внешние подпрограммы и стек
Пример программы для этого раздела выполняет сортировку массива целых чисел методом выбора.
Листинг 4. Реализация сортировки массива целых чисел методом выбора |
|||||
Строка |
NASM |
GAS |
|||
|
|
|
На первый взгляд листинг 4 может показаться огромным, но на самом деле он очень прост. Листинг представляет понятие функций, различных схем адресации памяти, стека и работы с функциями внешних библиотек. Программа сортирует массив из 10 чисел и с помощью функций puts и printf из внешней библиотеки C выводит содержимое несортированного и отсортированного массива. Для обеспечения модульности и представления понятия функций подпрограмма сортировки реализована отдельной процедурой, равно как и подпрограмма печати массива. Давайте разбираться с ними по очереди.
Выполнение программы начинается с вызова puts (строка 31) после объявления данных. Функция puts выводит строку на консоль. Её единственным аргументом является адрес отображаемой строки, который передается путем добавления адреса строки в стек (строка 30).
Любая метка в NASM, которая не является частью программы и должна быть разрешена во время компоновки, должна быть определена заранее, для чего используется ключевое слово extern (строка 24). В GAS такого требования нет. После этого адрес строки usort_str отправляется в стек (строка 30). Переменные памяти в NASM, например, usort_str, представляют адрес ячейки памяти, и, таким образом, выполнение команды push usort_str фактически помещает адрес в вершину стека. С другой стороны, в GAS перед переменной usort_str должен указываться префикс $, после чего она трактуется как непосредственный адрес. Если префикс $ не указывается, то в стек вместо адреса помещаются фактические байты, представленные переменной памяти.
Поскольку помещение переменной в стек фактически смещает указатель стека на двойное слово, указатель стека корректируется путем добавления к нему числа 4 (величина двойного слова) (строка 32)
Теперь в стек помещены три аргумента и вызывается функция print_array10 (строка 37). Функции в NASM и GAS определяются одинаково. По существу, они являются обычными метками, которые вызываются с помощью инструкции call.
После вызова функции в регистре ESP содержится начало стека. Значение esp + 4 представляет адрес возврата, а значениеesp + 8 - первый аргумент функции. Доступ ко всем последующим аргументам выполняется путем добавления размера переменной двойного слова к указателю стека (то есть esp + 12, esp + 16 и так далее).
Внутри функции путем копирования esp в ebp (строка 62) создается локальный кадр стека. Вы также можете выделить пространство для локальных переменных, как это сделано в программе (строка 63). Это выполняется путем вычитания количества необходимых байт из esp. Значение esp – 4 представляет пространство из четырех байт, выделенное для локальной переменной, и такое выделение может продолжаться до тех пор, пока в стеке достаточно пространства для локальных переменных.
В листинге 4 показан базовый режим косвенной адресации (строка 64), названный так потому, что вы начинаете с базового адреса и добавляете к нему смещение для указания нужного адреса. На стороне листинга NASM примером такой адресации является [ebp + 8], а также [ebp – 4] (строка 71). Адресация в GAS чуть более лаконична: 4(%ebp) и -4(%ebp), соответственно.
В подпрограмме print_array10 вы можете увидеть еще один режим адресации, используемый после метки push_loop (строка 74). Эта строка представлена в NASM и GAS следующим образом:
NASM: mov al, byte [ebx + esi]
GAS: movb (%ebx, %esi, 1), %al
Такой режим адресации называется базово-индексной адресацией. В нем используются три сущности: первая -- это базовый адрес, вторая -- индексный регистр, а третья -- множитель. Поскольку определить количество байт, вызываемых из определенного места памяти, не представляется возможным, необходим способ определения объема адресуемой памяти. NASM использует байтовый оператор для того, чтобы сообщить ассемблеру о том, что этот байт данных должен быть перемещен. В GAS та же проблема решается с помощью множителя и использования в мнемонических командах суффикса b, w или l (например, movb). Синтаксис GAS на первый взгляд может показаться несколько сложным.
Общая форма базово-индексной адресации в GAS выглядит следующим образом:
%сегмент:АДРЕС (, индекс, множитель)
или
%сегмент:(смещение, индекс, множитель)
или
%сегмент:АДРЕС(база, индекс, множитель)
Конечный адрес рассчитывается по следующей формуле:
АДРЕС или смещение + база + индекс * множитель.
Таким образом, для того, чтобы обратиться к байту, используется множитель 1, для слова 2 и для двойного слова 4. Конечно же, синтаксис, используемый в NASM, проще. Таким образом, изложенная выше формула в NASM будет иметь следующий вид:
Сегмент:[АДРЕС или смещение + база + индекс * множитель]
Для доступа к 1, 2 или 4 байтам памяти перед адресом памяти указывается префикс byte, word или dword соответственно.
Программа, приведенная в листинге 5, считывает перечень аргументов командной строки, записывает их в память и выводит на экран.
Листинг 5. Программа, которая считывает перечень аргументов командной строки, записывает их в память и выводит на экран. |
|||||
Строка |
NASM |
GAS |
|||
|
|
|
В листинге 5 показана конструкция, повторяющая инструкции ассемблера. Достаточно естественным образом она называется конструкцией повторения. Конструкция повторения в GAS начинается с директивы .rept (строка 6). Эта директива закрывается с помощью директивы .endr (строка 8). После .rept в GAS указывается число раз, которое должна быть повторена конструкция, заключенная в .rept/.endr. Любые инструкции, размещенные в этой конструкции, эквивалентны написанию этих инструкций count раз в отдельных строках.
Например, если количество (count) равно 3:
.rept 3
movl $2, %eax
.endr
Это соответствует:
movl $2, %eax
movl $2, %eax
movl $2, %eax
В NASM похожая инструкция используется на этапе предварительной обработки. Она начинается с директивы %rep и заканчивается директивой %endrep. После директивы %rep указывается выражение (в отличие от GAS, где за директивой .reptследует количество):
%rep <expression>
nop
%endrep
В NASM также есть альтернативный вариант - директива times. Так же, как и %rep, она работает на уровне ассемблера и после нее также указывается выражение. Например, приведенная выше конструкция %rep соответствует следующему:
times <expression> nop
А эта:
%rep 3
mov eax, 2
%endrep
эквивалентна следующему:
times 3 mov eax, 2
и обе они эквивалентны:
mov eax, 2
mov eax, 2
mov eax, 2
Для создания в памяти области данных величиной 10 двойных слов в листинге 5 используется директива .rept (или %rep). После этого из стека поочередно извлекаются аргументы командной строки и сохраняются в области памяти, пока таблица команд не заполнится.
Доступ к аргументам командной строки в обоих ассемблерах осуществляется одинаковым образом. В ESP или на вершине стека хранится количество аргументов командной строки, переданных в программу, по умолчанию указывается 1 (если аргументов командной строки нет). esp + 4 содержит первый аргумент командной строки, которым всегда является название программы, вызванной в командной строке. esp + 8, esp + 12 и далее содержат остальные аргументы командной строки.
Кроме того, обратите внимание, как выполняется доступ к таблице команд в обеих сторонах листинга 5. Здесь для доступа к таблице команд используется режим косвенной адресации памяти (строка 33), вместе со смещением ESI (и EDI) и множителем. Таким образом, [cmd_tbl + esi * 4] в NASM эквивалентно cmd_tbl(, %esi, 4) в GAS.
Даже несмотря на то, что различия между двумя этими ассемблерами значительны, преобразовать код из одной формы в другую не так сложно. Сначала вам может показаться, что синтаксис AT&T очень сложен в понимании, но после освоения он будет так же прост, как и синтаксис Intel.
Рэм закончил постдипломный курс в области информатики и работает инженером-программистом в индийской лаборатории программного обеспечения IBM, в подразделении Rational, разрабатывая и добавляя новые возможности в Rational ClearCase. Он имеет опыт работы с различными версиями Linux, UNIX и Windows, а также операционных систем реального времени для мобильных устройств, в том числе Symbian и Windows Mobile. В свободное время он осваивает Linux и читает книги.