- Разработка на ассемблере в Linux
- Системные вызовы
- Hello, World!
- Посмотрим, что внутри
- Записки программиста
- Написание и отладка кода на ассемблере x86/x64 в Linux
- Введение
- «Hello, world» на int 0 x80
- Выполнение системного вызова через sysenter
- Выполнение системного вызова через syscall
- Отладка ассемблерного кода в GDB
Разработка на ассемблере в Linux
В папке у нас будет бинарный файл fasm, который мы можем использовать для компиляции. Для удобства вы можете создать симлинк на него:
sudo ln -s /home/username/fasm/fasm /usr/local/bin
ald и shed устанавливаются не сложнее:
$ ./configure
$ make
# make install
В итоге у нас будет 3 полезных инструмента для программирования на ассемблере.
Системные вызовы
Как и большинство других операционных систем, Linux предоставляет т.н. API — набор полезных для программиста функций. В большинстве случаев вызов системной функции производится с помощью прерывания 80h. Следует отметить, что Linux используется fastcall-конвенция передачи параметров. Согласно ей параметры передаются через регистры (в windows, например, используется stdcall, где параметры передаются через стек). Номер вызываемой функции кладется в eax, а параметры в регистры:
1 / ebx
2 / ecx
3 / edx
4 / esi
5 / edi
6 / ebp
Как видите все не так сложно. Узнать номер системной функции, ее описание и параметры можно, хотя бы здесь. Возьмем, к примеру sys_exit . Как можно увидеть на той странице у нее есть один параметр — код возврата и она имеет порядковый номер 1. Таким образом мы можем вызвать ее следующим кодом:
mov eax, 1 ; 1 — номер системной функции
sub ebx, ebx ; Обнуляем регистр (можно было записать mov ebx, 0)
int 80h ; Вызываем прерывание 80h
Hello, World!
Ну что же. Писать мы ничего не будем, т.к. за нас все написано 🙂 В папке fasm/examples/elfexe есть файл hello.asm, в котором находится следующий код:
; fasm demonstration of writing simple ELF executable
format ELF executable 3
entry start
segment readable executable
mov eax,4
mov ebx,1
mov ecx,msg
mov edx,msg_size
int 0x80
mov eax,1
xor ebx,ebx
int 0x80
segment readable writeable
msg db ‘Hello world!’,0xA
msg_size = $-msg
Как видите здесь вызываются 2 системных функции — sys_write (с порядковым номером 4) и sys_exit . sys_write принимает 3 параметра — дескриптор потока вывода (1 — stdout), указатель на строку и размер строки. Сам номер функции, как уже говорилось, мы должны положить в eax. Функцию sys_exit мы уже разобрали. Скомпилировать это чудо можно так: fasm hello.asm (но не обязательно, т.к. там же, где лежит исходник, есть и бинарник).
Посмотрим, что внутри
Думаю, что самое время заглянуть в наш бинарник. Для начала воспользуемся шестнадцатеричным редактором, чтобы посмотреть что у нас получилось. Выполним команду:
Мы видим всю нашу программу, данные, elf-заголовок. Неплохо? Теперь мы посмотрим на нашу программу в отладчике. Наберем в консоли:
Нас должна поприветствовать строка с предложением ввести команду. Список команд вы можете узнать, набрав help или получить помощь по отдельной команде, набрав help command . Дизассемблировать нашу программу можно командой disassemble (или ее алиас — » d «). Вы увидете дизассемблированный листинг вашей программы. Слева — адрес, справа — сама команда, а посередине — опкод команды.
Получить дамп можно командой dump (странно, но ее нет в выводе команды help ).
Теперь попробуем поработать с командой next . Выполните ее и в ответ вам покажут значения регистров, установленные флаги, а так же адрес, опкод и дизассемблированную команду, которая должна выполниться следующей. Попробуйте выполнять команды и следите за изменением флагов и регистров. После вызова первого прерывания у вас на экране должна появиться надпись «Hello world!».
Целью данной статьи было показать основы программирования на ассемблере в linux, а не программирования на ассемблере в общем. Надеюсь, что вы подчерпнули для себя что-то полезное от сюда.
PS. Первая статья на хабре.
Записки программиста
Написание и отладка кода на ассемблере x86/x64 в Linux
Сегодня мы поговорим о программировании на ассемблере. Вопрос «зачем кому-то в третьем тысячелетии может прийти в голову писать что-то на ассемблере» раскрыт в заметке Зачем нужно знать всякие низкоуровневые вещи, поэтому здесь мы к нему возвращаться не будем. Отмечу, что в рамках поста мы сосредоточимся на вопросе компиляции и отладки программ на ассемблере. Сам же язык ассемблера заслуживает отдельного большого поста, а то и серии постов.
Если вы знаете ассемблер, то любая программа для вас — 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
# Или: gcc -m32 -c hello-int80.s
as —32 hello-int80.s -o hello-int80.o
ld -melf_i386 -s hello-int80.o -o hello-int80
Коротко рассмотрим первые несколько действий, выполняемых программой: (1) программа начинает выполнение с метки _start, (2) в регистр eax кладется значение 4, (3) в регистр ebx помещается значение 1, (4) в регистр ecx кладется адрес строки, (5) в регистр edx кладется ее длина, (6) происходит прерывание 0 x80. Так в мире Linux традиционно происходит выполнение системных вызовов. Конкретно int 0 x80 считается устаревшим и медленным, но из соображений обратной совместимости он все еще работает. Далее мы рассмотрим и более новые механизмы.
Нетрудно догадаться, что eax — это номер системного вызова, а ebx, ecx и edx — его аргументы. Какой системный вызов имеет какой номер можно подсмотреть в файлах:
# для x86
/ usr / include / x86_64-linux-gnu / asm / unistd_32.h
# для x64
/ usr / include / x86_64-linux-gnu / asm / unistd_64.h
Следующая строчка из файла 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
_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 искать. Решение заключается в том, чтобы самостоятельно определить адрес точки входа в программу и поставить брейкпоинт на этот адрес: