Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Ассемблер в Unix


UnixLib.org.Ru

"Портал документации UNIX-систем"
Информационный портал по установке, настройке и использованию UNIX
Множество статей, электронных книг и online помощь MAN.

Здравствуй, уважаемый подписчик!

Это 23-ый выпуск рассылки Лёгкий переход на бесплатные UNIX.

В этом выпуске попробуем ответить на некоторые Ваши вопросы.

Не далее чем вчера мной было получено письмо следующего содержания:
Добрый день! Я долгое время разрабатывал программы для Виндоус (в основном на языке ассемблер), но недавно по работе столкнулся с Unix (NetBSD). Можно ли под Unix писать программы на асме? Заранее спасибо. Аркадий - arc1435@mail-box.uhdesign.ru
Ответить на этот вопрос однозначно трудно. Программировать конечно можно - свидетельство этому ниже - но не очень удобно. Unix написан на C, поэтому в нём есть все условия для программирования именно на этом языке (хотя на многих других). С ассемблером труднее, т.к. мало документации и инструментов. Но пробовать можно и нужно!
Привожу статью (не очень свежую, но зато в тему...) по программированию на асме в linux:

Ассемблер в Linux для начинающих (часть 1 - Hello-world)

Автор: Xeoniks

http://netedu.da.ru

Привет, линуксоид!

Надеюсь, ты слышал о таком языке как Ассемблер :) Слышать-то ты точно слышал, а вот как насчет того, чтобы на нем программировать? Чего, никак? Ну, ладно, я не буду тут писать всякую чушь о том, почему всякий уважающий себя юзер должен знать ассемблер :), раз ты читаешь эту статью, то наверное уже знаешь, что такое асм и хочешь уметь на нем программить. Но, как ни странно, все туториалы в Сети посвящены исключительно ассемблеру в ДОС и ВинДОС. Потому что программить на асме в линухе никому не нужно :) Но некоторым (мне, например) интересно. Моя статья для начинающих, т.е. для тех кто или совсем не умеет программить на асме, либо писал программу hello-world в дос/винДОС, поэтому тем, кто умеет что-то большее, просьба не орать, что все уже знаете :) Ну, хватит трепаться, поехали...

Первым делом тебе нужен транслятор (ассемблер) - nasm и компоновщик (линкер) - ld. Что такое транслятор и линкер, объяснять не буду, потому что в линукс это то же самое, что и в дос. Если ты еще ничего про асм не знаешь, то просто поверь, что эти две проги тебе понадобятся :). В линухе можно программить используя только syscall-ы или и использованием glibc. Сегодня остановимся на первом способе. Начинаем с традиционной программы "Hello-world". Код такой:

section .text ;после ';' следует комментарий
global _start
hello db 'Hello-world', 0xa
hellolen equ $ - hello

_start:
mov ecx, hello
mov ebx, 1
mov edx, hellolen
mov eax, 4
int 0x80

mov eax, 1
int 0x80

Компилируется так:
$ nasm -f elf tvoi_fail.asm
$ ld -o tvoi_fail tvoi_fail.o
Ну и запускается как ./tvoi_fail.

Для этого лучше написать простенький bash-скрипт. Код может быть примерно такой:

#! /bin/bash
echo Введи имя файла...
read FILE
echo Работаю...
nasm -f elf $FILE.asm
ld -o $FILE $FILE.o
echo Done

Если ты не умеешь программировать на bash, замечу, что код надо сохранять в файле file.asm, а когда скрипт попросит ввести имя файла, писать просто file. Разумеется, вместо file надо подставить имя твоего файла, например: код сохранен в hello.asm, если ты запустишь скрипт, то в ответ на запрос программы, надо напечатать hello и ты получишь объектный файл hello.o и исполняемый файл hello

Теперь объясню, что мы написали:

section .text

Программа подразделется на sections'ы. .text - здесь располагается твой код. Еще есть .data для данных и .bss, здесь объявляются переменные.

global _start. Тут мы сообщаем линкеру, где начинается программа.

hello db 'Hello-world!', 0xa. Это, надеюсь, ясно - строка по имени hello, ее содержание 'Hello-world!'. 0xa - символ перевода строки

hellolen equ $ - hello - это длина строки hello ($ - hello). Надеюсь, не надо объяснять, что такое equ?

_start: - это тоже, надеюсь, ясно: точка входа в программу.

mov ecx, hello. Синтаксис операции mov:

mov приемник, источник

В нашем случае приемник - регистр ecx, источник - содержимое строки hello. Т. е. после этой операции содержимое регистра eax равно содержимому строки hello ('Hello-world').

mov ebx, 1 В ebx помещается дескриптор файла: 1 - stdout, т.е. стандартный вывод.

mov edx, hellolen В edx - длина строки.

mov eax, 4. Вот здесь - самое интересное (если в этой проге вообще может быть что-то интересное :). В eax находится номер функции, int 0x80 а здесь мы вызываем ядро линуха (делаем system call, это - аналог сервисов дос, таких как 21h, 27h, etc. Короче, нечто типа прерывания. BTW, вызвать прерывания БИОСа у тебя не получится, т.к. линух работает в защищенном режиме. Останавливаться на этом не буду, если надо - поищи в google :), которое определяет что:

  • Нас интересует содержимое строки hello
  • Вывод направлять надо в stdout
  • Длина строки содержится в hellolen
  • Функция имеет номер 4, а это значит, что надо вывести (ecx) длиной (edx) на (ebx).
Что ядро и выполняет...

Далее, mov eax, 1, помещаем в eax номер syscall-а (1 - exit) и int 0x80 вызываем ядро системы, которое и выполняет выход.

Элементарно, правда?

Теперь пара замечаний о компиляции: если ты ставишь точку входа в твою программу, например, _begin, то надо сообщить об этом линкеру. Примерно так: ld -e _begin -o tvoi_fail tvoi_fail.o

Соответственно, наш bash-скрипт для компиляции будет выглядеть так:


#! /bin/sh
echo Имя файла?
read FILE
echo Точка входа?
read EPOINT
echo Создаю объектный файл...
nasm -f elf $FILE.asm
echo Линкую полученный объектный файл...
ld -e $EPOINT -o $FILE $FILE.o
echo Done!

Разумеется, это можно еще много чем дополнить, но bash-программинг выходит за рамки данного документа :) Если народ думает, что эту тему надо освещать, просьба мылить мне.

Потом, если ты программил на асме под дос, то хочу отметить, что делать system call можно не только как int 0x80, но и более привычным способом - как int 80h

В строке hello db 'Hello-world!', 0xa писать ah вместо 0xa нельзя.

Да, кстати, если ты еще не догадался, здесь мы использовали синтаксис intel, привычный дос-виндос кодерам. Есть еще ассемблер as, который использует синтаксис AT&T. Правда (цитирую LINUX ASSEMBLER TUTORIAL by Robin Miyagi) Experienced intel syntax assembler programmers find AT&T `really weird'. Мне он тоже показался 'really weird', к примеру, описанный выше Hello-world выглядит примерно так:

.data #а тут комментарии... ну, ты уже догадался :)
hello: 
       .string "Hello!\n"
len = . - hello
 .text 
 .global _start
_start:
movl $len, %edx
movl $hello, %ecx
movl $1, %ebx
movl $4, %eax
int $0x80

movl $0, %ebx
movl $1, %eax
int $0x80
IMHO, писать на этом - садомазохизм :) Хотя, такое можно сказать про любого асм-кодера. Теперь пример надо скомпилить. Делается это так:

as -o hello.o hello.S

Думаю, ты догадался, что код надо сохранить в файле hello.S :) С линкером - то же самое, что и в nasme.

В коде, думаю, ты без труда разберешься самостоятельно. Только одно замечание: сразу этого не заметно, но в AT&T синтаксисе при операции, требующей два операнда, сначала пишется то, что передаем, а потом то, куда передаем. Т.е. если в интеловском синтаксисе операция присваивания регистру eax значение регистра ebx выглядит как mov eax, ebx, то в синтаксисе AT&T это будет выглядеть как movl %ebx, %eax. А теперь обратимся к еще одному примеру.

.section .data
hello: 
 .ascii  "Hello!\n"
hellolen:
 .long  . - hello
 .section .text
 .global _begin
 
_begin:
 xorl %ebx, %ebx  
 movl $4, %eax
 incl %ebx  
 leal hello, %ecx 
 movl hellolen, %edx 
 int $0x80  
 

 xorl %eax, %eax  
 incl %eax  
 xorl %ebx, %ebx 
 int $0x80  

Есть больше чем один способ сделать это! Смысл этих двух программ совершенно одинаковый и в обоих случаях мы пользуемся синтаксисом AT&T, Но код абсолютно другой. Впечатляет? Начинаем разбор кода:
В предыдущем примере мы не писали слово section и ты, наверное уже подумал, что его вообще не пишут, при использовании as. Как видишь, это не так...

hello: .ascii "Hello!\n" оставляю в качестве домашнего задания :) Для особо одаренных поясню, что '\n' - символ новой строки. С длиной строки и .section .text кажется, тоже все понятно. Единственное, перед словами типа section и global ставится точка.

xorl %ebx, %ebx. вот это уже достаточно интересно: с такой операцией мы в первом примере не встречались, поэтому объясняю. Операция xor, в AT&T-евском синтаксисе записывается как xorl (l означает размер пересылаемых данных, но в этот раз останавливаться на этом не будем). Занимается побитным сравнением своих операндов. Вот допустим, у нас есть два числа: 10110b и 11000b. Как ты, надеюсь, понял, это числа в двоичной системе счисления :) Выполним над ними операцию xor

     1 0 1 1 0
xor
     1 1 0 0 0
     -------------
     0 1 1 1 0

Дошло? Что, нет? Ладно, вот тебе таблица, надеюсь, разберешься:

xоперацияyрезультат
1 xor 1 0
0 xor 0 0
1 xor 0 1
0 xor 1 1

Ну вот, будем считать, что ты понял :) А теперь сообрази, что будет если какое либо число за xor-ить само с собой. Правильно, ноль будет. Для тех, кто еще ничего не понял (все остальные могут этот абзац пропустить :), если операции xor дать два одинаковых числа (никто не спорит, что число равно само себе? :), то в случае побитного сравнения каждый бит будет равен либо нулю либо единице. Если нуль - нуль на нуль дает нуль. Если один - один на один дает нуль. В любом случае мы получаем нуль. Итак, xor одного регистра - быстрый и красивый способ его обнулить. Конечно, в интеловском синтаксисе это выглядело бы по-другому, а именно xor ebx, ebx.

movl $4, eax - это понятно, помещаем в eax число 4.

incl %ebx - это операция инкремента, т.е. увеличения на единицу. Как ты помнишь, ebx мы заксорили, стало быть он у нас теперь равен единице. А в ebx в данном случае лежит место вывода программы (1 - stdout).

lea hello, %ecx - команда lea помещает в регистр-приемник смещение источника. В нашем случае, эта команда является заменой досового mov cx, offset hello. На самом деле, lea имеет отличие от mov, примененного с offset. Оно заключается в том, что lea присваивает первому операнду значение смещения, тогда как offset в mov вычисляется во время выполнения, что делает невозможным использования индексов. Но это в принципе не важно, т.к. в nasm-е mov xx, yy и так помещает не сам операнд, а ссылку на его адрес в памяти. В as лучше всегда использовать lea Дальше заталкиваем в edx длину строки hello, и вызываем ядро системы, которое выполняет вывод на stdout.

Затем обнуляем и сразу инкрементируем eax, потом обнуляем ebx и вызываем ядро. Между прочим, хочу объяснить почему мы делаем
xor eax, eax
inc eax
вместо
mov eax, 1.
Причина достаточно банальна: операции xor и inc выполняются быстрее, чем mov, по крайней мере, до определенных пределов. Я имею ввиду, что быстрее будет написать mov yyy, 100, чем организовывать цикл, в котором регистр yyy будет сто раз инкрементироваться :) Впрочем, если у тебя четвертый пентиум, вряд ли ты заметишь разницу :)

Кажется, это все, что хотелось на сегодня сказать.

Если тема вызовет интерес, то в скором будущем появятся и другие статьи на эту тему.

Удачи!

Copyleft Xeoniks. Все под GNU Free Documentation License...

Полезные ссылки:

Продвинутый туториал по AT&T ассемблеру

P.S.
Присылайте свои вопросы и пожелания по адресу root@unixlib.org.ru Ждем!
Успехов Вам!

Copyright © ProUNIX.h12.Ru, Unix Library.


В избранное