- Ассемблер для Linux. Для начала Hello, world! (книга)
- Пишем первую программу Hello, world!
- Записки программиста
- Написание и отладка кода на ассемблере x86/x64 в Linux
- Введение
- «Hello, world» на int 0 x80
- Выполнение системного вызова через sysenter
- Выполнение системного вызова через syscall
- Отладка ассемблерного кода в GDB
- Saved searches
- Use saved searches to filter your results more quickly
- KyleNehman/Linux-ASM-Hello-World
- Name already in use
- Sign In Required
- Launching GitHub Desktop
- Launching GitHub Desktop
- Launching Xcode
- Launching Visual Studio Code
- Latest commit
- Git stats
- Files
- README.md
- About
Ассемблер для Linux. Для начала Hello, world! (книга)
Приветствую всех на моем канале Old Programmer . Продолжаем публиковать материалы из книги об ассемблере GAS .
Пишем первую программу Hello, world!
Ну вот настало время, когда нужно и можно написать программу«Hello, world!». Это не сложно ведь у нас имеется шаблон программы. И мы просто должны его правильно заполнить. Но это еще не все мы должны знать как вывести информацию на консоль. Как уже понятно из предыдущего параграфа одним из инструментов, который широко используется при программировании на ассемблере в операционной системе Linux являются системные вызовы. Мы вызываем функцию ядра, которая выполняет то или иное системное действие. В предыдущем параграфе мы вызывали функцию, которая заканчивает выполнение текущей программы (процесса). И как легко догадаться мы должны теперь использовать системный вызов, который выводит строку текста на консоль. В дальнейшем мы дадим список системных вызовов операционной системы Linux, а пока будем знакомить с ними последовательно, по мере их поступления. В данном параграфе это системный вызов с номером 1 — вывод на консоль.
Как и говорили, мы возьмем программу из листинга 3 и дополним ее. Мы дополним ее еще одним системным вызовом. Кроме этого добавим в программу еще одну секцию для хранения там данных, а точнее строки, которую будем выводить. Эта программа представлена в листинге 4.
Давайте подробно разберем программу из листинга 4, с учетом того, что уже было объяснено для листинга 3.
1. Как мы видим у нас появился еще один системный вызов с номером 1 (число, помещаемое в регистр rax ). Но у этого системного вызова есть еще и другие параметры, которые прокомментированы в программе. rdi — стандартный поток вывода, rsi — адрес (метка) начала выводимой строки, rdx — длина строки в байтах.
2. В программе имеются две секции: .data — для данных (глобальных переменных), .text — для программного кода. В большинстве случаев нам будет вполне хватать именно двух секций.
3. В программе имеются две метки. С одной, глобальной меткой _start мы уже знакомы. Метка msg указывает на начало строки. Для нас смысл обеих меток равнозначен — обе указывают на некоторый адрес.
4. Давайте посмотрим еще на один важный элемент. Это len . Это не команда процессора. Ее можно назвать макропеременной или переменной времени трансляции. Ее значение определяется до того, как кодируются команда процессора. Точка в ассемблере GAS принимает значение текущего адреса, поэтому . — msg это просто длина строки. Конечно команду процессора mov $len, %rdx можно заменить другой: mov $n, %rdx, где n количество байтов в строке. Но с использованием переменной len мы реализовали более общий подход. Можно брать строки разной длины и не считать, сколько там на самом деле байтов. Подчеркнем лишний раз, что считать нужно количество байтов, при кодировке UTF-8, которую мы используем, а это может быть проблематично.
5. Обратим внимание еще на один элемент программы, точнее выводимой на консоль строки. Это символ ‘ \n ‘. Конечно, вы с ним знакомы из языка C. Это символ перевода строки. Для вывода на консоль с помощью системного вызова с номером 1 этот символ работает ровно также.
Трансляции программы производится уже известным на способом:
as —64 l4.s -o l4.o
ld -s l4.o -o l4
Подписываемся на мой канал Old Programmer и пишем свои комментарии.
Записки программиста
Написание и отладка кода на ассемблере 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 искать. Решение заключается в том, чтобы самостоятельно определить адрес точки входа в программу и поставить брейкпоинт на этот адрес:
Saved searches
Use saved searches to filter your results more quickly
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session. You switched accounts on another tab or window. Reload to refresh your session.
A simplistic Hello World for x64 nasm w/ intel syntax
KyleNehman/Linux-ASM-Hello-World
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Sign In Required
Please sign in to use Codespaces.
Launching GitHub Desktop
If nothing happens, download GitHub Desktop and try again.
Launching GitHub Desktop
If nothing happens, download GitHub Desktop and try again.
Launching Xcode
If nothing happens, download Xcode and try again.
Launching Visual Studio Code
Your codespace will open once ready.
There was a problem preparing your codespace, please try again.
Latest commit
Git stats
Files
Failed to load latest commit information.
README.md
A simplistic Hello World for nasm w/ intel syntax
The makefile assumes you’ve got nasm and ld installed, and uses the ‘-m elf_i386’ linker flag for my own pc, you’ll probably have to adjust this.
git clone https://github.com/KyleNehman/Linux-ASM-Hello-World.git cd Linux-ASM-Hello-World/ make make run
What’s this crazy code do? (Breakdown)
This program is more or less the equivalent of the following python:
Let’s go line by line, since this is for total beginners.
section .data msg: db "Hello, world!", 0x0a msgLen: equ $-msg
section .text global _start _start: mov ecx, msg mov edx, msgLen call print call exit
print: push eax push ebx mov eax, 4 mov ebx, 1 int 0x80 pop ebx pop eax ret
exit: mov eax, 1 mov ebx, 0 int 0x80
About
A simplistic Hello World for x64 nasm w/ intel syntax