Записки программиста
Написание и отладка кода на ассемблере x86/x64 в Linux
17 августа 2016
Сегодня мы поговорим о программировании на ассемблере. Вопрос «зачем кому-то в третьем тысячелетии может прийти в голову писать что-то на ассемблере» раскрыт в заметке Зачем нужно знать всякие низкоуровневые вещи, поэтому здесь мы к нему возвращаться не будем. Отмечу, что в рамках поста мы сосредоточимся на вопросе компиляции и отладки программ на ассемблере. Сам же язык ассемблера заслуживает отдельного большого поста, а то и серии постов.
Если вы знаете ассемблер, то любая программа для вас — open source.
Введение
Существует два широко используемых ассемблерных синтаксиса — так называемые AT&T-синтаксис и Intel-синтаксис. Они не сильно друг от друга отличаются и легко переводятся один в другой. В мире Windows принято использовать синтаксис Intel. В мире *nix систем, наоборот, практически всегда используется синтаксис AT&T, а синтаксис Intel встречается крайне редко (например, он используется в утилите perf). Поскольку Windows, как известно, не существует, далее мы сосредоточимся на правильном AT&T-синтаксисе 🙂
Компиляторов ассемблера существует много. Мы будем использовать GNU Assembler (он же GAS, он же /usr/bin/as). Скорее всего, он уже есть вашей системе. К тому же, если вы пользуетесь GCC и собираетесь писать ассемблерные вставки в коде на C, то именно с этим ассемблером вам предстоит работать. Из достойных альтернатив GAS можно отметить NASM и FASM.
Наконец, язык ассемблера отличается в зависимости от архитектуры процессора. Пока что мы сосредоточимся на ассемблере для x86 (он же i386) и x64 (он же amd64), так как именно с этими архитектурами приходится чаще всего иметь дело. Впрочем, ARM тоже весьма распространен, главным образом на телефонах и планшетах. Еще из сравнительно популярного есть SPARC и PowerPC, но шансы столкнуться с ними весьма малы. Отмечу, что x86 и x64 можно было бы рассматривать отдельно, но эти архитектуры во многом похожи, поэтому я не вижу в этом большого смысла.
«Hello, world» на int 0 x80
Рассмотрим типичный «Hello, world» для архитектуры x86 и Linux:
.data
msg :
. ascii «Hello, world!\n»
. set len , . — msg
. globl _start
_start :
# write
mov $ 4 , % eax
mov $ 1 , % ebx
mov $msg , % ecx
mov $len , % edx
int $ 0x80
# exit
mov $ 1 , % eax
xor % ebx , % ebx
int $ 0x80
Коротко рассмотрим первые несколько действий, выполняемых программой: (1) программа начинает выполнение с метки _start, (2) в регистр eax кладется значение 4, (3) в регистр ebx помещается значение 1, (4) в регистр ecx кладется адрес строки, (5) в регистр edx кладется ее длина, (6) происходит прерывание 0 x80. Так в мире Linux традиционно происходит выполнение системных вызовов. Конкретно int 0 x80 считается устаревшим и медленным, но из соображений обратной совместимости он все еще работает. Далее мы рассмотрим и более новые механизмы.
Нетрудно догадаться, что eax — это номер системного вызова, а ebx, ecx и edx — его аргументы. Какой системный вызов имеет какой номер можно подсмотреть в файлах:
Следующая строчка из файла unistd_32.h:
… как бы намекает нам, что производится вызов write. В свою очередь, из man 2 write мы можем узнать, какие аргументы этот системный вызов принимает:
ssize_t write ( int fd , const void * buf , size_t count ) ;
То есть, рассмотренный код эквивалентен:
Затем аналогичным образом производится вызов:
Совсем не сложно!
В общем случае системный вызов через 0 x80 производится по следующим правилам. Регистру eax присваивается номер системного вызова из unistd_32.h. До шести аргументов помещаются в регистры ebx, ecx, edx, esi, edi и ebp. Возвращаемое значение помещается в регистр eax. Значения остальных регистров при возвращении из системного вызова остаются прежними.
Выполнение системного вызова через sysenter
Начиная с i586 появилась инструкция sysenter, специально предназначенная (чего нельзя сказать об инструкции int) для выполнения системных вызовов.
Рассмотрим пример использования ее на Linux:
.data
msg :
. ascii «Hello, world!\n»
len = . — msg
. text
. globl _start
_start :
# write
mov $ 4 , % eax
mov $ 1 , % ebx
mov $msg , % ecx
mov $len , % edx
push $write_ret
push % ecx
push % edx
push % ebp
mov % esp , % ebp
sysenter
write_ret :
# exit
mov $ 1 , % eax
xor % ebx , % ebx
push $exit_ret
push % ecx
push % edx
push % ebp
mov % esp , % ebp
sysenter
Сборка осуществляется аналогично сборке предыдущего примера.
Как видите, принцип тот же, что при использовании int 0 x80, только перед выполнением sysenter требуются поместить в стек адрес, по которому следует вернуть управление, а также совершить кое-какие дополнительные манипуляции с регистрами. Причины этого более подробно объясняются здесь.
Инструкция sysenter работает быстрее int 0 x80 и является предпочтительным способом совершения системных вызовов на x86.
Выполнение системного вызова через syscall
До сих пор речь шла о 32-х битных программах. На x64 выполнение системных вызовов осуществляется так:
.data
msg :
. ascii «Hello, world!\n»
. set len , . — msg
. globl _start
_start :
# write
mov $ 1 , % rax
mov $ 1 , % rdi
mov $msg , % rsi
mov $len , % rdx
syscall
# exit
mov $ 60 , % rax
xor % rdi , % rdi
syscall
Собирается программа таким образом:
Принцип все тот же, но есть важные отличия. Номера системных вызовов нужно брать из unistd_64.h, а не из unistd_32.h. Как видите, они совершенно другие. Так как это 64-х битный код, то и регистры мы используем 64-х битные. Номер системного вызова помещается в rax. До шести аргументов передается через регистры rdi, rsi, rdx, r10, r8 и r9. Возвращаемое значение помещается в регистр rax. Значения, сохраненные в остальных регистрах, при возвращении из системного вызова остаются прежними, за исключением регистров rcx и r11.
Интересно, что в программе под x64 можно одновременно использовать системные вызовы как через syscall, так и через int 0 x80.
Отладка ассемблерного кода в GDB
Статья была бы не полной, если бы мы не затронули вопрос отладки всего этого хозяйства. Так как мы все равно очень плотно сидим на GNU-стэке, в качестве отладчика воспользуемся GDB. По большому счету, отладка не сильно отличается от отладки обычного кода на C, но есть нюансы.
Например, вы не можете так просто взять и поставить брейкпоинт на процедуру main. Как минимум, у вас попросту нет отладочных символов с информацией о том, где эту main искать. Решение заключается в том, чтобы самостоятельно определить адрес точки входа в программу и поставить брейкпоинт на этот адрес:
Источник
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. Различия появляются только на этапе компоновки. Особенности компоновки будут рассмотрены далее.
Источник
Компиляция / запуск ассемблера в Linux?
Я довольно новичок в Linux (Ubuntu 10.04) и полный новичок в ассемблере. Я следил за некоторыми учебниками, и я не мог найти ничего конкретного для Linux. Итак, мой вопрос, Что такое хороший пакет для компиляции/запуска ассемблера и какие команды для компиляции/запуска этого пакета?
7 ответов:
ассемблер GNU (gas) и NASM являются хорошим выбором. Однако у них есть некоторые различия, большой из которых-порядок, в котором вы размещаете операции и их операнды.
газ использует синтаксис AT&T:
nasm использует стиль intel:
любой из них, вероятно, сделает то, что вам нужно.
ассемблер GNU, вероятно, уже установлен в вашей системе. Попробуй man as чтобы увидеть полную информацию об использовании. Вы можете использовать as для компиляции отдельных файлов и ЛД по ссылке, Если вы действительно, действительно хотите.
тем не менее, GCC делает отличный интерфейс. Он может собрать .s файлов для вас. Например:
код выше AMD64. Это было бы по-другому, если вы все еще находитесь на 32-разрядной машине.
вы также можете скомпилировать код C/C++ непосредственно в сборку, если вам интересно, как что-то работает:
Если вы используете NASM, командная строка просто
где файл’.asm ‘- это ваш файл сборки (код), а «outfile» — это исполняемый файл, который вы хотите.
вот еще немного информации:
вы можете установить NASM в Ubuntu с помощью следующей команды:
вот основной Привет Мир в сборке Linux, чтобы подогреть ваш аппетит:
Я надеюсь, это то, что вы просили.
существует также FASM для Linux.
мое предложение было бы получить книгу программирования с нуля:
Это очень хорошая отправная точка для входа в ассемблерное программирование под linux, и она объясняет много основ, которые вам нужно понять, чтобы начать работу.
ассемблер (GNU) — это а(1)
Источник