Assembler Linux
Компиляторы ассемблера в Linux
В Linux традиционно используется компилятор ассемблера GNU Assembler (GAS, вызываемый командой as), входящий в состав пакета GCC. Этот компилятор является кроссплатформенным, т. е. может компилировать программы, написанные на различных языках ассемблера для разных процессоров. Однако GAS использует синтаксис AT&T, а не Intel, поэтому его использование программистами, привыкшими к синтаксису Intel, вызывает некоторый дискомфорт.
Например программа, выводящая на экран сообщение «Hello, world!» (далее будем называть ее hello) выглядит следующим образом:
.section .data
msg:
.ascii "Hello, world!\n"
len = . - msg # символу len присваевается длина строки
.section .text
.global _start # точка входа в программу
_start:
movl $4, %eax # системный вызов № 4 — sys_write
movl $1, %ebx # поток № 1 — stdout
movl $msg, %ecx # указатель на выводимую строку
movl $len, %edx # длина строки
int $0x80 # вызов ядра
movl $1, %eax # системный вызов № 1 — sys_exit
xorl %ebx, %ebx # выход с кодом 0
int $0x80 # вызов ядра
Как видно из примера, различия видны как в синтаксисе команд, так и в синтаксисе директив ассемблера и комментариях.
В последних версиях GAS появилась возможность использования синтаксиса Intel для команд, но синтаксис директив и комментариев остается традиционным. Включение синтаксиса Intel осуществляется директивой .intel_syntax с параметром noprefix. При этом программа, приведенная выше изменится следующим образом:
.intel_syntax noprefix
.section .data
msg:
.ascii "Hello, world!\n"
len = . - msg # символу len присваевается длина строки
.section .text
.global _start # точка входа в программу
_start:
mov eax, 4 # системный вызов № 4 — sys_write
mov ebx, 1 # поток № 1 — stdout
mov ecx, OFFSET FLAT:msg # указатель на выводимую строку
# OFFSET FLAT означает использовать тот адрес,
# который msg будет иметь во время загрузки
mov edx, len # длина строки
int 0x80 # вызов ядра
mov eax, 1 # системный вызов № 1 — sys_exit
xor ebx, ebx # выход с кодом 0
int 0x80 # вызов ядра
Другим широко распространенным компилятором ассемблера для Linux является Netwide Assembler (NASM, вызываемый командой nasm). NASM использует синтаксис Intel. Кроме того, синтаксис директив ассемблера NASM частично совпадает с синтаксисом MASM. Пример приведенной выше программы для ассемблера NASM выглядит следующим образом:
section .data
msg db "Hello, world!\n"
len equ $-msg ; символу len присваевается длина строки
section .text
global _start ; точка входа в программу
_start:
mov eax, 4 ; системный вызов № 4 — sys_write
mov ebx, 1 ; поток № 1 — stdout
mov ecx, msg ; указатель на выводимую строку
mov edx, len ; длина строки
int 80h ; вызов ядра
mov eax, 1 ; системный вызов № 1 — sys_exit
xor ebx, ebx ; выход с кодом 0
int 80h ; вызов ядра
Кроме перечисленных ассемблеров в среде Linux можно использовать ассемблеры FASM и YASM. Оба поддерживают синтаксис Intel, но FASM имеет свой синтаксис директив, а YASM синтаксически полностью аналогичен NASM и отличается от него только типом пользовательской лицензии. В дальнейшем изложении материала все примеры будут даваться применительно к синтаксису, используемому NASM. Желающим использовать GAS можно порекомендовать статью о сравнении этих двух ассемблеров. Кроме того, при использовании в GAS директивы .intel_syntax noprefix различия между ними будут не столь значительными. Тексты программ, подготовленные для NASM, как правило, без проблем компилируются и YASM.
Структура программы
Программы в Linux состоят из секций, каждая из которых имеет свое назначение [6]. Секция .text содержит код программы. Секции .data и .bss содержат данные. Причем первая содержит инициализированные данные, а вторая — не инициализированные. Секция .data всегда включается при компиляции в исполняемый файл, а .bss в исполняемый файл не включается и создается только при загрузке процесса в оперативную память. Начало секции объявляется директивой SECTION имя_секции. Вместо директивы SECTION можно использовать директиву SEGMENT. Для указания конца секции директив не существует — секция автоматически заканчивается при
объявлении новой секции или в конце программы. Порядок следования секций в программе не имеет значения. В программе обязательно должна быть объявлена метка с именем _start – это точка входа в программу. Кроме того, метка точки входа должна быть объявлена как глобальный идентификатор директивой GLOBAL _start. Так как имя точки входа предопределено, то необходимость в директиве конца программы END отпадает: в NASM данная директива не поддерживается.
При создании многомодульных программ все метки (идентификаторы переменных и функций), которые предполагается использовать в других модулях, необходимо объявить как глобальные с помощью директивы GLOBAL. Наоборот, все идентификаторы, реализованные в других модулях и объявленные там, как глобальные, необходимо объявить как внешние директивой EXTERN. Функция сложения двух чисел sum, рассмотренная в предыдущей лабораторной работе, в NASM будет выглядеть так:
SECTION .text
global sum
sum:
push ebp
mov ebp, esp
mov eax, [ebp+8]
add eax, [ebp+12]
pop ebp
ret
Использование библиотечных функций
В программах на ассемблере можно использовать функции библиотеки Си. Для использования функции ее надо предварительно объявить директивой EXTERN. Например, для того. чтобы использовать функцию printf необходимо предварительно указать выполнить следующую директиву:
EXTERN printf
Программу hello можно модифицировать так, чтобы она использовала для вывода информации не функцию API Linux, а функцию printf библиотеки Си. Код программы, назовем ее hello-c, будет выглядеть так:
SECTION .data
msg db "Hello, world!",0
fmt db "%s",0Ah
SECTION .text
GLOBAL _start ; точка входа в программу
EXTERN printf ; внешняя функция библиотеки Си
_start:
push msg ; второй параметр - указатель на строку
push fmt ; первый параметр - указатель на формат
22
call printf ; вызов функции
add esp, 4*2 ; очистка стека от параметров
mov eax, 1 ; системный вызов № 1 — sys_exit
xor ebx, ebx ; выход с кодом 0
int 80h ; вызов ядра
Компиляция программ, использующих библиотечные функции ничем не отличается от компиляции программ, использующих только функции API. Различия появляются только на этапе компоновки. Особенности компоновки будут рассмотрены далее.
Отличия NASM от MASM
- NASM чувствителен к регистру символов
- NASM требует квадратные скобки для ссылок на память
- NASM не хранит типы переменных
- NASM не поддерживает ASSUME
- NASM не поддерживает модели памяти
- Обозначения операций в NASM совпадают с языком СИ
Компиляция и запуск
nasm -f elf hello.asm
gcc hello.o
chmod +x a.out
./a.out