Як перейменувати файл в асемблері




Як перейменувати файл в асемблері



Асемблер. Заміна певного символу у файлі

Доброго дня. Необхідно реалізувати програму на асемблері, яка виконує:
1. Створення файлу з ім'ям XXXXXXXX.LAB, де XXXXXXXX - Ваше прізвище.
2. Запис у файл рядка: - прізвище, ім'я, номер групи.
3. Читання файлу та виведення вмісту на екран.
4. Зміна атрибутів файлу – зробити його прихованим.
5. Запис у режимі прямої адресації файл на місце елемента 5 цифри 8.
6. Читання з файлу елементів з номерами 3 – 10 та виведення їх на екран. Пункти 1-4 виконати вдалося, але ніяк не вдається виконати останні 2 пункти. Нижче наведено код програми, який, мій погляд, повинен виконувати пункти 1-5.

.model small data segment cr equ 0dh lf equ 0ah string db 'AndrewS-31',cr,lf ;рядок для запису strlnth equ $-string ;її довжина handle dw ? ;осередок для дескриптора fname db 'Solodovnikov.LAB',0 ;ім'я файлу у форматі asciiz buffin db 12 dup ('') ;буфер введення replace db '8' replength equ :data main: mov ax, data mov ds, ax ;створені файлу mov ah, 3ch ;функція створення файлу mov cx, 0 ;без атрибутів mov dx, offset fname ;адреса імені файлу int 21h mov handle, ax ;збережемо дескриптор файлу ;запис у файл рядкаs mov ah; mov bx, handle ;дескриптор mov cx, strlnth ;довжина рядка mov dx, offset string ;адреса рядка int 21h ;закриття файлу mov ah, 3eh ;функція закриття mov bx, handle ;дескриптор int 21h ;відкриття файлу mov ah, 3 , offset fname int 21h mov handle, ax ;читання з файлу рядка mov ah, 3fh mov bx, handle mov cx, strlnth mov dx, offset buffin int 21h ;виведення рядка на консоль mov ah, 40h mov bx, 1h mov dx, 1 offset buffin int 21h ;зміна атрибуту файлу mov dx, offset fname mov cl, 2h mov al, 1h mov ah, 43h int 21h mov ah, 42h ;функція переміщення покажчика mov al, 0 ;вибір усунення: щодо початку файлу mov cx, 0 ; mov dx, 5h ;зміщення на 5 позицій mov bx, handle int 21h mov ah, 40h ;функція запису mov bx, handle ;дескриптор mov cx, replength ;довжина рядка mov dx, offset replace ;адреса рядка int 21 21h code ends end main

Але в результаті останніх двох блоків коду нібито і немає. Що потрібно виправити, щоб програма виконувала потрібне?

Довідник програміста на персональному комп'ютері фірми IBM. Дискові накопичувачі
Сторінка 22. Перейменування файлу; зміна позиції файлу в каталозі

5.3.4 Перейменування файлу; зміна позиції файлу у каталозі.


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

Високий рівень.


У Бейсику файл перейменовується командою NAME. За допомогою цієї
команди він може бути також перенесений до іншого каталогу. Напишіть
спочатку існуюче ім'я, а потім нове ім'я файлу, обидва -
ні в лапки, наприклад NAME "OLDFILE.EXT" AS "NEWFILE.EXT". У
цьому випадку буде перейменовано файл у кореневому каталозі. Для
зміни імен файлів, розміщених у підкаталогах, можуть бути
використано шляхи до файлу. Наприклад, NAME "B:LEVEL1\OLDFILE.EXT"
AS "B:LEVEL1\NEWFILE.EXT" змінює ім'я файлу в підкаталозі LE-
VEL1.
Зазначимо, що для нового імені файлу має бути вказаний повний
шлях. Якщо Ви запишете NAME "B:LEVEL1\OLDFILE.EXT" AS "NEWFI-
LE.EXT", то файл буде не тільки перейменований, але й перенесений у
кореневий каталог. Для перенесення файлу з одного підкаталогу в
інший без зміни його імені напишіть команду NAME "A:SUBDIR1-
\OLDFILE.EXT" AS "A:SUBDIR2\OLDFILE.EXT". Таким методом не можна
перенести файл із диска на диск. Оскільки файли, розташовані в
різних каталогах можуть мати те саме ім'я, то можлива помилка
під час спроби перенесення файлів з однаковими іменами. У цьому випадку
буде повернено код помилки 58 [5.4.8].

Середній рівень.


MS DOS може перейменовувати файли, використовуючи як метод управ-
ляющего блоку файлу, і метод дескриптора файла. Перший з них
може застосовуватися тільки до файлів, розташованих у поточному каталозі.
логі.

Метод FCB:
Використовуйте функцію 17H переривання 21H. DS:DX повинні вказувати
на відкритий керуючий блок файлу. Помістіть нове ім'я файлу в
FCB, починаючи зі зміщення 11H (це резервна область блоку).
Нове ім'я може використовувати символ "?", у цьому випадку символи,
що перебувають у цих позиціях, не змінюватимуться. При поверненні,
якщо нове ім'я вже існувало в каталозі, то AL дорівнюватиме FF,
інакше AL = 0. У прикладі ім'я файлу ACCOUNTS.DAT змінюється на
DEBTS.DAT.

;---в сегменті даних
FCB DB 'FILENAMEEXT',25 DUP(0)
NEWNAME DB 'NEWNAME EXT', ;11 символів нового імені

;---поміщаємо нове ім'я файлу в змінну NEWNAME
MOV SI,OFFSET NEWNAME ;DS:SI вказують на нове ім'я
MOV AX,SEG FCB ;ES:DI вказують на FCB
MOV ES, AX;
MOV DI,OFFSET FCB;
ADD DI,11H ;починаємо зі зміщення 11H
MOV CX,11 ;ім'я файлу містить 11 символів

REP MOVSB; переносимо 11 байтів
LEA DX,FCB ;DS:DX вказують на FCB
MOV AH,17H ;функція зміни імені
INT 21H ;змінюємо ім'я
CMP AL,0FFH ;перевірка на помилку
JE RENAME_ERROR ;догляд на обробку помилки

Метод дескриптора файлу:

Функція 56H переривання 21H перейменовує та переміщає файли.
DS:DX повинні вказувати на рядок, що дає шлях та ім'я перейменовано-
го файлу (до 63 символів) і завершується символом ASCII 0.
ES:DI повинні вказувати на другий рядок, який дає нові ім'я та
шлях файлу. Імена накопичувачів, якщо вони присутні, мають
збігатися. Якщо шляхи різні, то файл переноситься в інший
підкаталог. Щоб перенести файл без перейменування треба у вто-
рій рядку вказати те саме ім'я, але інший шлях. При поверненні,
якщо сталася помилка, то встановлюється прапор перенесення, а AX
міститиме 3 - якщо один із шляхів не знайдено, 5 - при помилці
на диску та 17 - при спробі перенесення між різними накопичувачами.
У прикладі файл ACCOUNTS.DAT переноситься з підкаталогу GAINS
підкаталог LOSSES.

;---в сегменті даних
OLDPATH DB 'A:GAINS\ACCOUNTS.DAT',0
NEWPATH DB 'A:LOSSES\ACCOUNTS.DAT',0

;---зміна шляху файлу
LEA DX,OLDPATH ;DS:DX вказують на старий шлях
MOV AX,SEG NEWPATH ;ES:DI вказують на новий шлях
MOV ES, AX;
MOV DI,OFFSET NEWPATH;
MOV AH,56H ;номер функції
INT 21H; переносимо файл
JC ERROR_ROUTINE ;догляд на обробку помилки

Записки програміста

Шпаргалка за основними інструкціями асемблера x86/x64

Минулої статті ми написали наш перший hello world додаток на асмі, навчилися його компілювати та налагоджувати, а також дізналися, як робити системні виклики в Linux. Сьогодні ми познайомимося безпосередньо з асемблерними інструкціями, поняттям регістрів, стека і ось цього всього. Ассемблери для архітектур x86 (a.k.a i386) і x64 (a.k.a amd64) дуже схожі, тому немає сенсу розглядати в окремих статтях. Притому акцент я постараюся робити на x64, попутно відзначаючи відмінності від x86, якщо вони є. Далі передбачається, що ви вже знаєте, наприклад, чим стек відрізняється від купи, і пояснювати такі речі не потрібно.

Реєстри загального призначення

Регістр – це невеликий (зазвичай 4 або 8 байт) шматочок пам'яті у процесорі з надзвичайно великою швидкістю доступу. Регістри поділяються на регістри спеціального призначення та регістри загального призначення. Нас зараз цікавлять регістри загального призначення. Як можна здогадатися за назвою, програма може використовувати ці регістри під свої потреби, як їй заманеться.

На x86 є вісім 32-бітних регістрів загального призначення - eax, ebx, ecx, edx, esp, ebp, esi і edi. Регістри не мають заданого наперед типу, тобто вони можуть трактуватися як знакові чи беззнакові цілі числа, покажчики, булеві значення, ASCII-коди символів тощо. Незважаючи на те, що теоретично ці регістри можна використовувати як завгодно, на практиці зазвичай кожен регістр використовується певним чином. Так, esp вказує на вершину стека, ecx відіграє роль лічильника, а в eax записується результат виконання операції чи процедури. Існують 16-бітні регістри ax, bx, cx, dx, sp, bp, si і di, що являють собою 16 молодших біт відповідних 32-бітних регістрів.Також доступні і 8-бітові регістри ah, al, bh, bl, ch, cl, dh і dl, які являють собою старші і молодші байти регістрів ax, bx, cx і dx відповідно.

Розглянемо приклад. Допустимо, виконуються такі три інструкції:

(gdb) x/3i $pc
=> 0x8048074: mov $0xaabbccdd,%eax
0x8048079: mov $0xee,%al
0x804807b: mov $0x1234, %ax

Значення регістрів після запису в eax значення 0 x AABBCCDD:

(gdb) p/x $eax
$1 = 0xaabbccdd
(gdb) p/x $ax
$2 = 0xccdd
(gdb) p/x $ah
$3 = 0xcc
(gdb) p/x $al
$4 = 0xdd

Значення після запису в регістр значення 0 x EE:

(gdb) p/x $eax
$5 = 0xaabbccee
(gdb) p/x $ax
$6 = 0xccee
(gdb) p/x $ah
$7 = 0xcc
(gdb) p/x $al
$8 = 0xee

Значення регістрів після запису в ax числа 0 x 1234:

(gdb) p/x $eax
$9 = 0xaabb1234
(gdb) p/x $ax
$10 = 0x1234
(gdb) p/x $ah
$11 = 0x12
(gdb) p/x $al
$12 = 0x34

Як бачите, нічого складного.

Примітка: Синтаксис GAS дозволяє явно вказувати розміри операндів шляхом використання суфіксів b (байт), w (слово, 2 байти), l (довге слово, 4 байти), q (четверка, 8 байт) та деяких інших. Наприклад, замість команди mov $0xEE, % al можна написати movb $0xEE, % al, замість mov $0x1234, % ax — movw $0x1234, % ax, і так далі. У сучасному GAS ці суфікси опціональні і я особисто їх не використовую. Але не лякайтеся, якщо побачите їх у чужому коді.

На x64 розмір регістрів було збільшено до 64-х біт. Відповідні регістри отримали назву rax, rbx тощо. Крім того, регістрів загального призначення стало шістнадцять замість восьми. Додаткові регістри отримали назви r8, r9, … , R15. Відповідні їм регістри, які репрезентують молодші 32, 16 і 8 біт, отримали назву r8d, r8w, r8b, і за аналогією для регістрів r9-r15. Крім того, з'явилися регістри, що є молодшими 8 біт регістрів rsi, rdi, rbp і rsp - sil, dil, bpl і spl відповідно.

Про адресацію

Як зазначалося, регістри можуть трактуватися, як покажчики дані у пам'яті. Для розіменування таких покажчиків використовується спеціальний синтаксис:

Цей запис означає «прочитай 8 байт за адресою, записаною в регістрі rsp, і збережи їх у регістр rax». При запуску програми rsp вказує на вершину стека, де зберігається кількість аргументів, переданих програмі (argc), покажчики на ці аргументи, а також змінні оточення та інша інформація. Таким чином, в результаті виконання наведеної вище інструкції (зрозуміло, за умови, що перед нею не виконувалось жодних інших інструкцій) в rax буде записано кількість аргументів, з якими була запущена програма.

В одній команді можна вказувати адресу та змішання (як позитивне, так і негативне) щодо нього:

Цей запис означає «візьми rsp, додай до нього 8, прочитай 8 байт за адресою, що вийшла, і поклади їх в rax». Таким чином, в rax буде записана адреса рядка, що представляє собою перший аргумент програми, тобто ім'я файлу, що виконується.

Працюючи з масивами буває зручно звертатися до елементу з певним індексом. Відповідний синтаксис:

Читається так: «Порахуй rcx*8 + rsp + 16, і поміняй місцями 8 байт (розмір регістру) за адресою, що вийшла, і значення регістра rax». Іншими словами, rsp і 16 так само грають роль зміщення, rcx грає роль індексу в масиві, а 8 - це розмір елемента масиву. При використанні цього синтаксису допустимими розмірами елемента є лише 1, 2, 4 і 8. Якщо потрібно якийсь інший розмір, можна використовувати інструкції множення, бінарного зсуву та інші, які ми розглянемо далі.

Нарешті, наступний код також валідний:

.data
msg :
. ascii "Hello, world!\n"
. text

. globl _start
_start:
# Обнулення rcx
xor%rcx,%rcx
mov msg (% rcx, 8), % al
mov msg, % ah

У сенсі, що можна не вказувати регістр зі усуненням або взагалі будь-які регістри. В результаті виконання цього коду регістри al і ah буде записаний ASCII-код літери H, або 0 x 48.

У цьому контексті хотілося б згадати ще одну корисну асемблерну інструкцію:

Інструкція lea дуже зручна, тому що дозволяє відразу виконати множення та кілька додавань.

Fun fact! На x64 в байткоді інструкцій ніколи не використовуються 64-бітові зсуви. На відміну від x86, інструкції часто оперують не абсолютними адресами, а адресами щодо адреси самої інструкції, що дозволяє звертатися до найближчих +/- 2 Гб оперативної пам'яті. Відповідний синтаксис:

Порівняємо довжини опкодів «звичайного» та «відносного» mov (objdump -d):

4000b0: 8a 0c 25 e8 00 60 00 mov 0x6000e8,%cl
4000b7: 8a 05 2b 00 20 00 mov 0x20002b(%rip),%al # 0x6000e8

Як бачите, «відносний» mov ще й на один байт коротший! Що це за регістр такий rip ми дізнаємось трохи нижче.

Для запису повного 64-х бітового значення в регістр передбачена спеціальна інструкція:

Іншими словами, процесори x64 так само економно кодують інструкції, як і процесори x86, і в наш час немає особливо сенсу використовувати процесори x86 в системах, що мають пару гігабайт оперативної пам'яті або менше (мобільні пристрої, холодильники, мікрохвильові печі, і так далі). Швидше за все, процесори x64 будуть навіть більш ефективні за рахунок більшої кількості доступних регістрів і більшого розміру цих регістрів.

Арифметичні операції

Розглянемо основні арифметичні операції:

# Інціалізуємо значення регістрів
mov $ 123, % rax
mov $ 456, % rcx

# инкремент: rax = rax + 1 = 124
inc % rax

# Декремент: rax = rax - 1 = 123
dec % rax

# додавання: rax = rax + rcx = 579
add % rcx , % rax

# віднімання: rax = rax - rcx = 123
sub % rcx , % rax

# Зміна знака: rcx = - rcx = -456
neg%rcx

Тут і далі операндами можуть бути не тільки регістри, а й ділянки пам'яті чи константи. Але обидва операнди не можуть бути ділянками пам'яті. Це правило стосується всіх інструкцій асемблера x86/x64, принаймні, з розглянутих у цій статті.

У цьому прикладі інструкція mul множить al на cl і зберігає результат множення в пару регістрів al і ah. Таким чином, ax прийме значення 0 x 12C або 300 у десятковій нотації. У найгіршому разі збереження результату перемноження двох N-байтових значень може знадобитися до 2*N байт. Залежно від розміру операнда, результат зберігається в al:ah, ax:dx, eax:edx або rax:rdx. При цьому як множники завжди використовується перший з цих регістрів і передано інструкції аргумент.

Знакове множення виробляється так само за допомогою інструкції imul. Крім того, існують варіанти imul з двома та трьома аргументами:

mov $ 123, % rax
mov $ 456, % rcx

# rax = rax * rcx = 56088
imul % rcx , % rax

# rcx = rax * 10 = 560880
imul $ 10, % rax, % rcx

Інструкції div та idiv роблять дії, зворотні mul та imul. Наприклад:

mov $ 0% rdx
mov $ 456 , % rax
mov $ 123, % rcx

# rax = rdx: rax / rcx = 3
# rdx = rdx: rax % rcx = 87
div%rcx

Як бачите, був отриманий результат цілого чисельного поділу, а також залишок від поділу.

Це далеко не всі арифметичні вказівки. Наприклад, є ще adc (додавання з урахуванням прапора переносу), sbb (віднімання з урахуванням позики), а також відповідні їм інструкції, що виставляють та очищають відповідні прапори (ctc, clc), та багато інших. Але вони поширені набагато менше, і тому в рамках цієї статті не розглядаються.

Логічні та бітові операції

Як зазначалося, особливої ​​типізації в асемблері x86/x64 не передбачено. Тому не варто дивуватися, що в ньому немає окремих інструкцій для виконання булевих операцій та окремих для виконання бітових операцій.Натомість є один набір інструкцій, що працюють з бітами, а як інтерпретувати результат — вирішує конкретна програма.

Так, наприклад, виглядає обчислення найпростішого логічного виразу:

mov $ 0 , % rax # a = false
mov $ 1, % rbx # b = true
mov $ 0 % rcx # c = false

# rdx: = a | !(b && c)
mov % rcx , % rdx # rdx = c
and % rbx , % rdx # rdx &= b
not % rdx # rdx = ~ rdx
or % rax , % rdx # rdx | = a
and $ 1, % rdx # rdx &= 1

Зверніть увагу, що тут ми використовували по одному молодшому біту в кожному з 64-х бітових регістрів. Таким чином, у старших бітах утворюється сміття, яке ми обнулюємо останньою командою.

Ще одна корисна інструкція - це xor (що виключає або). У логічних виразах xor використовується нечасто, але з його допомогою часто відбувається обнулення регістрів. Якщо подивитися на опкоди інструкцій, стає зрозуміло, чому:

4000b3: 48 31 db xor %rbx,%rbx
4000b6: 48 ff c3 inc %rbx
4000b9: 48 c7 c3 01 00 00 00 mov $0x1,%rbx

Як бачите, інструкції xor та inc кодуються всього лише трьома байтами кожна, в той час, як та сама інструкція mov займає цілих сім байт. Кожен окремий випадок, звичайно, краще бенчмаркати окремо, але загальне евристичне правило таке - чим коротший код, тим більше його міститься в кеші процесора, тим швидше він працює.

У цьому контексті також слід згадати інструкції побітового зсуву, тестування бітів (bit test) та сканування бітів (bit scan):

# покладемо що-небудь у регістр
movabs $0xc0de1c0ffee2beef,% rax

# Зрушення вліво на 3 біти
# rax = 0x0de1c0ffee2beef0
shl $ 4 , % rax

# Зрушення вправо на 7 біт
# rax = 0x001bc381ffdc57dd
shr $ 7 , % rax

циклічний зсув вправо на 5 біт
# rax = 0xe800de1c0ffee2be
ror $ 5 , % rax

циклічний зсув вліво на 5 біт
# rax = 0x001bc381ffdc57dd
rol $ 5 , % rax

# покласти в CF (див. далі) значення 13-го біта
# CF = !! (0x1bc381ffdc57dd & (1 bt $ 13, % rax

# те саме + встановити біт (bit test and set)
# rax = 0x001bc381ffdc77dd, CF = 0
bts $ 13 , % rax

# те саме + скинути біт (bit test and reset)
# rax = 0x001bc381ffdc57dd, CF = 1
btr $ 13 , % rax

# те саме + інвертувати біт (bit test and complement)
# rax = 0x001bc381ffdc77dd, CF = 0
btc $ 13, % rax

знайти наймолодший ненульовий байт (bit scan forward)
# rcx = 0, ZF = 0
bsf % rax , % rcx

знайти найстарший ненульовий байт (bit scan reverse)
# rdx = 52, ZF = 0
bsr % rax , % rdx

якщо всі біти нульові, ZF = 1, значення rdx невизначено
xor % rax , % rax
bsf % rax , % rdx

Ще є бітові зрушення зі знаком (sal, sar), циклічні зрушення з прапором перенесення (rcl, rcr), і навіть зрушення подвійний точності (shld, shrd). Але використовуються вони не так часто, та й втомишся перераховувати взагалі всі інструкції. Тому їх вивчення я залишаю вам як домашнє завдання.

Умовні висловлювання та цикли

Вище кілька разів згадувалися якісь там прапори, наприклад, прапор перенесення. Під прапорами розуміються біти спеціального регістру eflags/rflags (назва на x86 та x64 відповідно). Безпосередньо звертатися до цього регістру за допомогою інструкцій mov, add і подібних не можна, але він змінюється і використовується різними інструкціями побічно. Наприклад, вже згаданий прапор переносу (carry flag, CF) зберігається в нульовому біті eflags/rflags і використовується, наприклад, у тій же інструкції bt. Ще з прапорів, що часто використовуються, можна назвати zero flag (ZF, 6-ий біт), sign flag (SF, 7-ий біт), direction flag (DF, 10-ий біт) і overflow flag (OF, 11-ий біт) .

Ще з таких неявних регістрів слід назвати eip/rip, що зберігає адресу поточної інструкції. До нього також не можна звертатися безпосередньо, але він видно в GDB разом з eflags/rflags, якщо сказати info registers, і опосередковано змінюється усіма інструкції.Більшість інструкцій просто збільшують eip/rip на довжину цієї інструкції, але є винятки з цього правила. Наприклад, інструкція jmp просто здійснює перехід за заданою адресою:

# обнулюємо rax
xor % rax , % rax
jmp next
ця інструкція буде пропущена
inc % rax
next :
inc % rax

В результаті значення rax дорівнюватиме одиниці, так як перша інструкція inс буде пропущена. Зауважте, що адреса переходу також може бути записана в регістрі:

Втім, на практиці такого коду краще уникати, оскільки він ламає прогноз переходів і тому менш ефективний.

Примітка: GAS дозволяє давати міткам цифрні імена типу 1: , 2: , і так далі, і переходити до найближчої попередньої або наступної мітки із заданим номером інструкціями на зразок jmp 1b і jmp 1f . Це досить зручно, тому що іноді буває важко придумати мітки осмислені імена. Подробиці можна знайти тут.

Умовні переходи зазвичай здійснюються за допомогою інструкції cmp, яка порівнює два своїх операнда та виставляє відповідні прапори, за якою слідує інструкція із сімейства je, jg ​​та подібних:

je 1f # перейти, якщо рівні (equal)
jl 1f # перейти, якщо знаково менше (less)
jb 1f # перейти, якщо беззнаково менше (below)
jg 1f # перейти, якщо знаково більше (greater)
ja 1f # перейти, якщо беззнаково більше (above)

Існує також інструкції jne (перейти, якщо не рівні), jle (перейти, якщо знаково менше або рівні), jna (перейти, якщо беззнаково не більше) та подібні. Принцип їхнього іменування, сподіваюся, очевидний. Замість je/jne часто пишуть jz/jnz, тому що інструкції je/jne просто перевіряють значення ZF. Також є інструкції, які перевіряють інші прапори - js, jo та jp, але на практиці вони використовуються рідко. Всі ці інструкції разом узяті зазвичай називають jcc. Тобто замість конкретних умов пишуться дві літери "c", від "condition".Тут можна знайти хорошу зведену таблицю за всіма інструкціями jcc та тому, які прапори вони перевіряють.

Крім cmp часто використовують інструкцію test:

test % rax , % rax
jz 1f # перейти, якщо rax == 0
js 2f # перейти, якщо rax < 0
1 :
# якийсь код
2 :
# якийсь ще код

Fun fact! Цікаво, що cmp і test у душі є тими самими sub і and, тільки змінюють своїх операндов. Це знання можна використовувати для одночасного виконання sub або and і умовного переходу без додаткових інструкцій cmp або test.

Ще з інструкцій, пов'язаних з умовними переходами, можна назвати такі.

Інструкція jrcxz здійснює перехід тільки у тому випадку, якщо значення регістра rcx дорівнює нулю.

Інструкції сімейства cmovcc (conditional move) працюють як mov, але при виконанні заданої умови, за аналогією з jcc.

Інструкції setcc надають однобайтовому регістру або байту у пам'яті значення 1, якщо задана умова виконується, та 0 інакше.

Порівняти rax із заданим шматком пам'яті. Якщо рівні, виставити ZF і зберегти за вказаною адресою значення вказаного регістра, у цьому прикладі rcx. Інакше очистити ZF і завантажити значення з пам'яті в rax. Також обидва операнди можуть бути регістрами.

Інструкція cmpxchg8b головним чином потрібна x86. Вона працює аналогічно cmpxchg, тільки виробляє compare and swap відразу 8 байт. Регістри edx:eax використовуються для порівняння, а регістри ecx:ebx зберігають те, що хочемо записати. Інструкція cmpxchg16b за тим же принципом виробляє compare and swap відразу 16 байт на x64.

Важливо! Зверніть увагу, що без префікса lock всі ці compare and swap інструкції не атомарні.

Інструкція loop зменшує значення регістра rcx на одиницю, і якщо після цього rcx != 0 здійснює перехід на задану мітку. Інструкції loopz і loopnz працюють аналогічно, тільки більш складні умови (rcx != 0) && (ZF == 1) і (rcx != 0) && (ZF == 0) відповідно.

Не потрібно бути семи п'ядей у ​​лобі, щоб зобразити за допомогою цих інструкцій конструкцію if-then-else або цикли for/while, тому рухаємось далі.

«Рядкові» операції

Розглянемо наступний шматок коду:

У регістри rsi та rdi кладуться адреси двох рядків. Командою cld очищається прапор напряму (DF). Інструкція, що виконує зворотну дію, називається std. Потім у справу входить інструкція cmpsb. Вона порівнює байти (%rsi) та (%rdi) і виставляє прапори відповідно до результату порівняння. Потім, якщо DF = 0, rsi та rdi збільшуються на одиницю (кількість байт у тому, що ми порівнювали), інакше зменшуються. Аналогічні інструкції cmpsw, cmpsl та cmpsq порівнюють слова, довгі слова та четверні слова відповідно.

Інструкції cmps цікаві тим, що можуть використовуватися з префіксом rep, repe (repz) та repne (repnz). Наприклад:

Префікс rep повторює інструкцію задану в регістрі rcx кількість разів. Префікси repz і repnz роблять те саме, але тільки після кожного виконання інструкції додатково перевіряється ZF. Цикл переривається, якщо ZF = 0 у разі repz і якщо ZF = 1 у випадку repnz. Таким чином, наведений вище код перевіряє рівність двох буферів однакового розміру.

Аналогічні інструкції movs перекладає дані з буфера, адреса якого вказана в rsi, в буфер, адреса якого вказана в rdi (легко запам'ятати - rsi означає source, rdi означає destination). Інструкції stos заповнює буфер за адресою з регістра rdi байтами з регістра rax (або eax, або ax, або al, залежно від конкретної інструкції). Інструкції lods роблять зворотну дію - копіюють байти за вказаною в rsi адресою в регістр rax. Нарешті, інструкції scas шукають байти з регістра rax (або відповідних регістрів меншого розміру) у буфері, адреса якого вказана у rdi. Як і cmps, всі ці інструкції працюють із префіксами rep, repz та repnz.

На базі цих інструкцій легко реалізуються процедури memcmp, memcpy, strcmp і подібні.

Робота зі стеком та процедури

Із стеком все дуже просто. Інструкція push кладе свій аргумент на стек, а інструкція pop витягує значення зі стека.

Існують інструкції, що поміщають на стек і витягують з нього регістр rflags / eflags:

pushf
# робимо щось, що змінює прапори
popf
# прапори відновлені, саме час зробити jcc

А так, наприклад, можна отримати значення прапора CF:

На x86 також існують інструкції pusha і popa, що зберігають на стеку і відновлюють з нього значення всіх регістрів. У x64 цих інструкцій більше немає.

Процедури, як правило, «створюються» за допомогою інструкцій call і ret.

someproc :
типовий пролог процедури
# Для прикладу виділяємо 0x10 байт на стеку під локальні змінні
# rbp - покажчик на фрейм стека
push % rbp
mov %rsp, %rbp
sub $ 0x10 , % rsp

# Тут типу якісь обчислення.
mov $ 1 , % rax

типовий епілог процедури
add $ 0x10 , % rsp
pop % rbp

вихід з процедури
ret

_start:
# як і у випадку з jmp, адреса переходу може бути в регістрі
call someproc
test % rax , % rax
jnz error

Примітка: Аналогічний пролог та епілог можна написати за допомогою інструкцій enter $0x10, $0 та leave. Але нині ці інструкції використовуються рідко, оскільки вони виконуються повільніше через додаткову підтримку вкладених процедур.

Як правило, значення, що повертається передається в регістрі rax або, якщо його розміру недостатньо, записується в структуру, адреса якої передається як аргумент. До питання передачі аргументів. Угод про виклики існує безліч. В одних всі аргументи завжди передаються через стек (окреме питання — в якому порядку) і за очищення стека від аргументів відповідає сама процедура, в інших частина аргументів передається через регістри, а частина через стек, і за очищення стека від аргументів відповідає сторона, що викликає, плюс безліч варіантів посередині, з окремими правилами щодо вирівнювання аргументів на стеку, передачі this, якщо це ООП мову, і так далі. У загальному випадку для довільно взятої архітектури, компілятора та мови програмування угода про виклики може бути взагалі будь-якою.

Наприклад розглянемо асемблерний код, згенерований CLang 3.8 для простий програми на мові C під x64. Так виглядає одна з процедур:

unsigned int
hash ( const unsigned char * data , const size_t data_len ) {
unsigned int hash = 0x4841434B;
for ( int i = 0 ; i < data_len ; i ++ ) {
hash = (( hash }
return hash;
}

Дизассемблерний лістинг (при компіляції з -O0, коментарі мої):

типовий пролог процедури
# Регістр rsp не змінюється, так як процедура не викликає жодних
# інших процедур
400950: 55% push %rbp
400951: 48 89 e5 mov %rsp,%rbp

# ініціалізація локальних змінних:
# -0x08(%rbp) - const unsigned char *data (8 байт)
# -0x10(%rbp) - const size_t data_len (8 байт)
# -0x14(%rbp) - unsigned int hash (4 байти)
# -0x18(%rbp) - int i (4 байти)
400954: 48 89 7d f8 mov %rdi,-0x8(%rbp)
400958: 48 89 75 f0 mov %rsi,-0x10(%rbp)
40095c: c7 45 ec 4b 43 41 48 movl $0x4841434b,-0x14(%rbp)
400963: c7 45 e8 00 00 00 00 movl $0x0,-0x18(%rbp)

# rax := i. якщо досягли data_len, виходимо із циклу
40096a: 48 63 45 e8 movslq -0x18(%rbp),%rax
40096e: 48 3b 45 f0 cmp -0x10(%rbp),%rax
400972: 0f 83 28 00 00 00 jae 4009a0

# eax := (hash 400978: 8b 45 ec mov -0x14(%rbp),%eax
40097b: c1 e0 05 shl $0x5,%eax
40097e: 03 45 ec add -0x14(%rbp),%eax

# eax += data[i]
400981: 48 63 4d e8 movslq -0x18(%rbp),%rcx
400985: 48 8b 55 f8 mov -0x8(%rbp),%rdx
400989: 0f b6 34 0a movzbl (%rdx,%rcx,1),%esi
40098d: 01 f0 add %esi,%eax

# hash := eax
40098f: 89 45 ec mov %eax,-0x14(%rbp)

# i++ і перейти до початку циклу
400992: 8b 45 e8 mov -0x18(%rbp),%eax
400995: 83 c0 01 add $0x1,%eax
400998: 89 45 e8 mov %eax,-0x18(%rbp)
40099b: e9 ca ff ff ff jmpq 40096a

# Повертається значення (hash) кладеться в регістр eax
4009a0: 8b 45 ec mov -0x14(%rbp),%eax

типовий епілог
4009a3: 5d pop %rbp
4009a4: c3 retq

Тут ми зустріли дві нові інструкції – movs та movz. Вони працюють так само, як mov, тільки розширюють один операнд до розміру другого, знаково і беззнаково відповідно. Наприклад, інструкція movzbl (%rdx,%rcx,1),%esi читайт байт (b) за адресою (%rdx,%rcx,1) , розширює його в довге слово (l) шляхом додавання на початок нулів (z) і кладе результат у регістр esi.

Як бачите, два аргументи були передані процедурі через регістри rdi та rsi. Очевидно, використовується конвенція під назвою System V AMD64 ABI. Стверджується, що це стандарт де-факто під x64 на *nix системах. Я не бачу сенсу переказувати опис цієї конвенції тут, зацікавлені читачі можуть ознайомитися з повним описом наведеного посилання.

Висновок

Само собою зрозуміло, в рамках однієї статті, описати весь асемблер x86/x64 неможливо (більше того, я не впевнений, що сам знаю його прямо таки весь).Як мінімум, за кадром залишилися такі теми, як операції над числами з плаваючою точкою, MMX-, SSE- та AVX-інструкції, а також будь-які екзотичні інструкції на кшталт lidt, lgdt, bswap, rdtsc, cpuid, movbe, xlatb, або prefetch.Я намагатимусь висвітлити їх у наступних статтях, але нічого не обіцяю. Слід зазначити, що у висновку objdump -d для більшості реальних програм ви дуже рідко побачите щось крім описаного вище.

Ще цікавий топік, що залишився за кадром, — це атомарні операції, бар'єри пам'яті, спинлоки і ось це все. Наприклад, compare and swap часто реалізується просто як інструкція cmpxchg із префіксом lock. За аналогією реалізується атомарний інкремент, декремент та інше. На жаль все це тягне на тему для окремої статті.

Як джерела додаткової інформації можна рекомендувати книгу Modern X86 Assembly Language Programming, і, звичайно ж, мануали від Intel. Також досить непогана книга Assembly x86 на wikibooks.org.

З онлайн-довідників з асемблерних інструкцій варто звернути увагу на:

Додаток: На продовження теми вас може зацікавити пост Учбовий мікропроцесорний комплект УМК-80. У ньому розповідається про асемблері 8080, 8-бітному предку асемблера x86/x64.

Ви можете надіслати коментар мені на пошту, або скористатися коментарями в Telegram-групі.

Схожі статті

  • Скільки ставити файл підкачування на 4 Гб оперативної пам'яті
  • Навіщо використовують файл
  • Як зробити файл підкачки на Windows 10
  • Що таке файл на комп'ютері
  • Як видалити файли на телефоні
  • Як створити файл XSL
  • Що буде якщо видалити файл 1cv8
  • Де можна зберігати великі файли в Інтернеті
  • Недавні статті

  • Як бродить зернова брага
  • Що робити якщо не засмагаєш на сонці чому засмага погано лягає на шкіру або перестає прилипати
  • Як швидко зняти гель лак без апарату
  • Як робиться Каті голови
  • Яка гребінець краще для об'єму
  • Чим роблять м'яку покрівлю
  • Чи можна залишати крем для обличчя на ніч
  • Де знаходиться датчик селектора
  • географія нашої діяльності
    вулиця Драгоманова, 27
    вул. Курчатова 1Б
    вул. Міцкевича 130
    вул. Лабунського, 1
    вул. Макарова-Пржевальського
    вул. Толстого 10
    вул. Грушевського 28
    вул. Перший промінь (Черняхівського)
    напишіть нам

    сообщение успешно отправлено
    x