Программирование на языке ассемблера в ОС Linux.Часть 1

Авторы:
dbsh, nops

Эта статья не претендует на звание учебника, охватывающего все стороны программирования на ассемблере в Linux. Я лишь хочу помочь людям, которые начинают делать первые шаги. Всё, что здесь будет описано, я буду описывать так, как понял это я. Надеюсь, что люди, которые найдут ошибку в моём описании, меня поправят.
Зачем все это? Литературы по ассемблеру под Linux как таковой нет. Руководства по разработке драйверов я не учитываю — до этого еще далеко. В интернете можно встретить лишь несколько статей, но там всё рассматривается поверхностно и заканчивается на примере вывода надписи «Hello, world!». На www.wasm.ru встречал интересное руководство по опкодам (опкоды – машинное представление команд ассемблера), но чтобы эти опкоды правильно писать — нужно что то знать :) Да, и перепись всего понятого — мне поможет лучше запомнить. Надеюсь, у читающего есть хотя бы туманное представление какого-либо языка программирования высокого уровня.

Инструментарий.
Чтобы писать программы на ассемблере не нужны тяжёлые среды разработки (вроде Eclipse). Если вы занимаетесь программированием, то, наверно, большинство необходимого у вас уже установлено:
  • NASM — свободный (GNU LGPL) кроссплатформенный Intel x86 ассемблер;
  • LD – GNU линкер – объединяет несколько объектных файлов в один исполняемый файл;
  • KHexEdit – редактор бинарных файлов;
  • strace — утилита, помогающая узнать какие системные вызовы использует процесс;
  • GCC – набор GNU компиляторов. Полезно дизассемблировать программы и изучать, какой код на ассемблере был сгенерирован в соответствии с исходным кодом на языке С;
  • GDB — Отладчик. Как им пользоваться, поясню позже;
  • VIM+GDB plugin — Текстовый редактор (или любой другой).
  • и, собственно SlackWare 10.2 и ядро 2.6.22

Операционная система не важна — во всех линуксах ядро одно и тоже. Принципиальная разница отсутствует.
Про синтаксис компилятора и описание архитектуры компьютера писать не буду. Много времени займет. Про архитектуру понаписано достаточно учебников. А про синтаксис можно посмотреть в приложении.
Стоит, наверно, начать с того, с чего начинают в любой книжке. Введите следующий код в файл, и сохраните под именем hello.asm
BITS32 ; Говорим компилятору, что код 32-битный

; В исполняемом файле может быть различное количество секций.
; Секции обычно выделяются по содержимому:
; .text — для кода,
; .data — для данных,
; .bss — для неинициализированных данных.

section .text ; Начало секции кода

global _start ; Метка _start должна быть глобальной,
; чтобы линкер смог её найти и сделать точкой входа в программу.

_start:

mov eax,4 ; системный вызов «write»
mov ebx,1 ; стандартный вывод
mov ecx,msg ; адрес сообщения
mov edx,[msg_size] ; длина
int 0x80 ; вызов прерывания

mov eax,1 ; системный вызов «exit»
xor ebx,ebx ; код выхода
int 0x80 ; вызов прерывания

section .data ; Начало секции данных

; Объявление переменной с сообщением
msg db 'Hello, YarLUG',0xA
; Объявление переменной с длинной сообщения
msg_size dd $-msg

Это первая наша программа. Она должна выводить на экран приветствие «Hello, YarLUG».
Для того, чтобы создать работающую программу нужно откомпилировать текст программы в объектный файл, а затем объектный файл скомпоновать в исполняемый.
$ nasm -f elf hello.asm
$ ld -o hello hello.o

Зачем так сложно? Представьте, у нас есть очень большой проект, состоящий из нескольких подпроектов. Каждый подпроект компилируется в отдельный объектный файл и затем все они компонуются в один большой исполняемый файл. Если вы изменяете один подпроект, то вам не нужно перекомпилировать все подпроекты, нужно всего лишь откомпилировать изменённый подпроект, а потом скомпоновать с остальными.
Давайте проверим работоспособность нашей первой программы:
$ ./example1

Если выводится строчка, то всё работает. Если нет, то проверьте сообщения компилятора и компоновщика при создании исполняемого файла.
Давайте разберём текст программы.
Вероятно, самое непонятное — это откуда я взял эти циферки:
mov eax,4
mov ebx,1
mov ecx,msg
mov edx,msg_size
int 0x80

По пунктам:
1) mov eax,4

Помещаю в регистр еах номер системного вызова. Номера системных вызовов можно найти исходных текстах ядра. В файле /usr/src/linux/include/asm-i386/unistd.h описаны константы номеров системных вызовов. Номер 4 описан так:
#define __NR_write 4
2) mov ebx,1

Записываем файловый дескриптор стандартного вывода в еbх. Всегда существует 3 стандартных дескриптора:
0 – стандартный ввод 1 – стандартный вывод 2 – вывод ошибкок.
3) mov ecx,msg

Помещаем адрес сообщения в регистр есх.
4) mov edx,[msg_size]

Длина сообщения передаётся в регистре edx. При описании переменной msg_size мы использовали символ $. Он означает текущее смещение в программе, из него мы вычитаем смещение переменной msg и получается длина сообщения в байтах. Длину можно указать и константой, но ошибившись в числе, можно придти к нежелательным последствиям. К примеру, замените в коде msg_size на какое нибудь число, например, 4, и заново пересоберите программу. В итоге на екране получим строку из четырёх символов: Hell. 5) int 0×80 Системный вызов операционной системы. Это аналог DOS'овскому int 21h.
А теперь, самое важное — откуда я вообще узнал, что и как заносится в регистры? Наберите в консоли
$ man 2 write

Второй раздел справочника (man 2) посвящён системным вызовам ОС. Строчка, которая сразу же бросается в глаза:
ssize_t write(int fd, const void *buf, size_t count);

Это описание вызова на языке C. Чтобы понять, куда, что помещать нужно знать способ вызова (call convention). В регистре eax передаётся номер системного вызова, параметры передаются в регистрах, в порядке очерёдности, ebx, ecx, edx.
Следующий кусок кода:
mov eax,1 
xor ebx,ebx
int 0x80

По номеру системного вызова в регистре eax понимаем, что это выход из программы – системный вызов exit. XOR – операция «исключающее ИЛИ». Этой операцией в ebx заносится 0. Для простоты понимания можно записать эту строчку так:
mov ebx,0
или так:
sub ebx,ebx

По описанию функции (man 2 exit) видно, что она принимает один параметр – код выхода из программы.
В описании значения переменной msg в конце написано 0xA – это переход на следующую строку. Попробуйте это убрать и посмотреть, что получиться.
Здесь можно закончить, и направить вас штудировать учебники, техническую документацию, как это делают некоторые авторы. Но я продолжу
Сейчас попробуем записать строчку в файл. Что для этого нужно? Нужен сам файл, но, его отдельно создавать не будем — заставим код это сделать. Потом его нужно открыть, записать, закрыть, нельзя так же забывать выйти из программы. Используемые функции:
int open(const char *pathname, int flags, mode_t mode);

Открыть файл
ssize_t write(int fd, const void *buf, size_t count);

Записать в файл
int close(int fd);

Закрыть файл
void _exit(int status);

Выход из программы.
BITS32
section .text

global _start
_start:

; open

mov eax,5
mov ebx,path_file
mov ecx,0001002
mov edx,7666
int 0x80
mov [fd],eax

; write

mov eax,4
mov ebx,[fd]
mov ecx,msg
mov edx,[msg_size]
int 0x80

; close

mov eax,6
mov ebx,[fd]
int 0x80

; exit

mov eax,1
xor ebx,ebx
int 0x80

section .data
msg db 'Hello, YarLUG'
msg_size dd $-msg

path_file db 'file.txt',0

section .bss
fd resd 1


Не забываем, что после знака точка с запятой идет строка комментария и компилятор её пропускает.
С начала, вызывается функция open:
mov eax,5

Системный вызов open под номером 5.
mov ebx,path_file

Передаем адрес пути файла.
mov ecx,0001002

В ecx передаются битовые флаги. Здесь взведены два флага: 0001000 – если файла не существует, то создать новый; 0000002 – открыть файл в режиме чтение/запись.
mov edx,7666

В регистре edx передаются права для создаваемого файла. Подробнее можно узнать в станице man chmod.
mov dword [fd],eax

В регистре eax системный вызов open возвращает файловый дескриптор. Поскольку еах нам еще потребуется, то дескриптор необходимо сохранить в какую-нибудь промежуточную переменную. В программе для этого используется переменная fd. Дальше код достаточно простой, а местами знакомый.
Программа компилируется и компонуется аналогично предыдущей. Стоит лишь сказать о мощной утилите make, которая очень облегчит нам жизнь. Если в каталоге, где вы сохранили программу (я назвал её hellofile.asm), создать файл с именем Makefile и следующим содержимым:
build:
nasm -f elf hellofile.asm
ld -o hellofile hellofile.o
clean:
rm -f hellofile hellofile.o

То после любого изменения исходного текста нам остаётся ввести
$ make build

и получим готовый исполняемый файл, а утилита make вызовет всё за нас. Если ввести
$ make clean

то в каталоге останется только файл hellofile.asm, а всё лишнее будет удалено.
За кадром осталось ещё много всего интересного. Но об этом я постараюсь изложить в следующих своих записках.

Полезные ссылки:
nasm.sourceforge.net/ — ассемблер NASM;
home.online.no/~espensa/khexedit/ – KhexEdit;
sourceforge.net/projects/strace/ — strace;
gcc.gnu.org/ – коллекция GNU компиляторов
www.gnu.org/software/gdb/ — отладчик GDB;
mitya.pp.ru/gdb/ — русское руководство по GDB;
wasm.ru/ — много статей по программированию на ассемблепре.
  • 0
  • 23 декабря 2008, 16:42
  • admin

Комментарии (0)

RSS свернуть / развернуть

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.