Блоґ одного кібера

Історія хвороби контуженого інформаційним вибухом

Асемблер x86 на Linux

with 2 comments

Давно хотів познайомитись, але все стримувало те що в дитинстві я почав з синтаксису Intel, і хотів програмувати тут теж з синтаксисом Intel. Пройшло багато років, я забув, тому давайте вивчимо AT & T синтаксис. Потім при потребі можна перевчитись.

Ну й крім того, я знайшов книжку Programming from the Ground Up, в якій все дуже зрозуміло (як для дітей) пояснюють.

Ну дууууже коротка теорія

Перше що потрібно знати – x86 – це архітектура Фон Неймана, тобто код програми і дані зберігаються в одній і тій же пам’яті, і для комп’ютера виглядають однаково. (Щоправда операційні системи можуть обмежувати виконання лише областями з кодом, для того щоб запобігти можливим помилкам, якими можуть скористатись хитрі хакери)

Пам’ять – це довгий список байтів (можуть містити цілі числа від 0 до 255), кожен з яких має свою адресу. Адреса – це просто номер байта. Щоправда ми працюватимемо переважно не з байтами, а зі словами (в 32-х розрядній архітектурі це 4 байти, які можуть приймати значення від 0 до 4294967295). Тому що регістри процесора якраз стільки вміщають. Адреси – це теж слова (4 байти).

Найпростіша програма

Надрукуємо “Привіт, світе!”? О, ні, на ассемблері це надто складно. Давайте напишемо програму яка завершує роботу. Ага. В Unix, програма яка завершує свою роботу має повернути статус код. Статус код останньої виконаної команди можна побачити у спеціальній змінній командної оболонки: echo $?

_start – в лінкера за замовчуванням позначає точку входу в програму. Тобто адресу з якої почнеться її виконання. Можна замінити її наприклад на main, але тоді лінкер, якщо йому не вказати опцію -e main скаже:

ld: warning: cannot find entry symbol _start; defaulting to 0000000008048054

Символ відсотка позначає регістр процесора (наприклад %eax), долар позначає константу (наприклад $1, $0x80 (= 128 в шістнадцятковому записі) ).

Переривання – це місце в коді де контроль передається в якесь інше місце (в ядро). Коли ядро виконає своє переривання – воно поверне контроль нам, і програма продовжить виконання (стан регістрів може змінитись). Щоправда після системного виклику exit контроль нам ніколи не повернуть, але так й було задумано.

Щоб перевірити нашу програму, потрібно виконати команди:

as exit.s -o exit.o
ld exit.o -o exit

Перша створює об’єктний файл. Об’єктний файл – це вже код машинною мовою, але ще не до кінця зібраний. Потрібно передати його лінкеру – програмі яка вміє збирати багато об’єктних файлів в одну програму, та додавати інформацію для того щоб ядро знало як працювати з програмою.

Якщо все пройшло без помилок – можна перевірити роботу:

./exit 
echo $?

Також можна спробувати замінити код повернення в %ebx на якісь інші і подивитись результат.

Опис інструкцій

mov – це інструкція переміщення, яка має два операнди – звідки й куди.

Суфікс l означає розмір даних (long = 4 байти). Інші можливі префікси: b byte = (8 біт); s short = (2 байти (16 біт).

Багато інших інструкцій мають два операнди, як наприклад addl (додавання), subl (віднімання). Але, наприклад, інструкція div приймає єдиний аргумент, бере вміст регістрів %edx та %eax як велике 64-х розрядне число (якщо треба менше, %edx має містити нуль) ділить на переданий аргумент а тоді результат поміщує в %eax, а остачу від ділення в %ebx.

Регістри загального призначення, які можна використовувати як аргументи команд переміщення та арифметичних – це %eax, %ebx, %ecx, %edx, %edi, %esi.

Контроль ходу виконання

Давайте напишемо наступну програму, яка не просто даватиме наперед заданий статус код, а даватиме максимальне число серед заданих. Нехай ці числа зберігатимуться за адресою, яку ми позначимо data_items, і останнім числом буде нуль, щоб ми знали коли припинити пошук.

Виконання має повернути 89 (найбільше в списку). Це вже набагато цікавіша програма.

По-перше, ми маємо дані. Окрім типу .long (4 байти), ми можемо використовувати .byte, .int (2 байти), та .ascii – рядок символів, наприклад .ascii "Hello, world!\n".

По-друге, цікава інструкція movl data_items(,%edi,4), %eax. Лівий її операнд означає взяти дані з адреси data_items, але зсунутись на %edi елементів розміру 4. Тобто це як доступ до елементу масиву. Синтаксис паскудний, в Intel здається писали б data_items + %edi * 4, що виглядає зрозуміліше, але маємо що маємо.

Також, якщо ми хочемо отримати значення за адресою – теж використовуємо дужки. Наприклад – %esp – містить адресу вершини стеку. movl %esp, %eax – збереже цю адресу в %eax, а от movl (%esp), %eax – значення з вершини стеку.

Наступна незнайома інструкція – cmpl. Вона порівнює два чотирибайтові числа (суфікс бачите?), а результат порівняння записує в певні біти регістру %eflags. Про нього я дуже давно написав непогану статтю на CybWiki а потім на вікіпедії, тому в деталі не вдаватимусь.

Після порівняння зазвичай йде інструкція зробити стрибок на певну адресу, якщо виконується умова порівняння. Існують такі інструкції стрибків з умовою: je – стрибати якщо рівні, jg – якщо друге значення було більше за перше, jge – більше або рівне, jl – менше, jle – менше рівне.

incl – збільшує аргумент на одиничку. incl %edi – це те саме що й addl $1, %edi, тільки коротше й швидше працює.

Далі буде

Вже скоро ранок, а hello world ми ще так і не написали. Треба розібратись з написанням і викликом функцій, викликом функцій роботи з файлами, щоб можна було писати в STDOUT. Але це вже завтра, бо це вже й так надто червоноока публікація.

Хоча в книжці там ще йде мова про рекурсію, обробку помилок, бібліотеки функцій, та їх створення, менеджер пам’яті і інші цікаві речі.

Advertisements

Written by bunyk

Вересень 6, 2014 at 03:34

Оприлюднено в Кодерство

Tagged with

Відповідей: 2

Subscribe to comments with RSS.

  1. Мені, до речі, чимось at&t синтаксис подобається навіть більше, ніж intel. Звичайно, в ньому є об’єктивно серйозні недоліки (те, що числові константи і мітки як значення треба писати із доларом, інакше це буде звернення до пам’яті за такою абсолютною адресою — це реально убиває час, коли долар все ж забувається; крім того, синтаксис адресації із базою та індексом менш наочний, 0xdeadbeef(,%ecx,4) замість [0xdeadbeef + ecx * 4], але до цього можна звикнути), але загалом він якийсь більш машинний, і це по-юніксовому: асемблер має бути асемблером, а не тягнути у себе фічі високорівневим мов, як цим страждав tasm, наприклад.

    Зауваження щодо .ascii: ця директива не приписує нульовий байт після рядка, на відміну від .asciz, тому краще не передавати дані, оголошені через .ascii туди, де чекають сішного рядка (якщо тільки явно не писати цей щоразу).

    Маленький лайфхак: на 64-бітних машинах так само можна експериментувати із 32-бітним кодом, достатньо збирати так:

    gcc -m32 -c exit.s -o exit.o
    gcc -m32 exit.o -o exit

    dmytrish

    Вересень 6, 2014 at 12:46


Залишити відповідь

Заповніть поля нижче або авторизуйтесь клікнувши по іконці

Лого WordPress.com

Ви коментуєте, використовуючи свій обліковий запис WordPress.com. Log Out / Змінити )

Twitter picture

Ви коментуєте, використовуючи свій обліковий запис Twitter. Log Out / Змінити )

Facebook photo

Ви коментуєте, використовуючи свій обліковий запис Facebook. Log Out / Змінити )

Google+ photo

Ви коментуєте, використовуючи свій обліковий запис Google+. Log Out / Змінити )

З’єднання з %s

%d блогерам подобається це: