Програмирование на fasm под Win64 часть 2 «Системы счисления, память, регистры»

В данной статье мы коснёмся основ без которых программирование на ассемблере невозможно, я расскажу кое-что о системах счисления, коснусь организации памяти и рассмотрю пустую программу.

Системы счисления

Прежде всего я хотел бы остановиться на способах записи целых чисел. Начнём с примера: запись 32167 означает следующее: 32167=3*10^4 + 2*10^3 + 1*10^2 + 6*10^1 + 7*10^0 \quad (1)

То есть любое число можно представить в виде суммы:
\sum\limits_{k=0}^N a_k*10^k

Где a k – цифра от 0 до 9, N – количество цифр в записи числа. Такая запись числа называется записью в десятичной системе счисления. В принципе, в вышеупомянутой сумме 10 можно заменить на любое другое натуральное число, это число будет называться основанием системы счисления. Например в восьмеричной системе счисления число представляется в виде суммы:
\sum\limits_{k=0}^N a_k*8^k

В данном случае требуется не 10 цифр, а 8, от 0 до 7. Обычно, если требуется указать систему счисления, её основание записывается как нижний индекс справа от числа(основание записывается в десятичной системе счисления), например:
76647_{8}=7*8^4 + 6*8^3 + 6*8^2 + 4*8^1 + 7*8^0 \quad (2)

в свою очередь:
76647_{8}=32167_{10}

то есть сумма (1) равна суме (2).
В системе счисления с основанием n существует n цифр, например в десятичной – десять, в восьмеричной – восемь. Если же нужно использовать систему счисления с основанием большим десяти, то придётся добавлять новые цифры, например в шестнадцатеричной системе счисления 16 цифр: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F. Цифры A-F, равны 10-15 соответственно. Чтобы лучше понять рассмотрим несколько примеров:
A_{16}=10_{10} \\ F_{16}=15_{10} \\ A3_{16}=10*16^1 + 3*16^0=163_{10} \\ F2E_{16}=15*16^2 + 2*16^1 + 14*16^0=3886_{10}

Подробнее почитать о системах счисления можно в википедии

Что касается fasm-а, то в нём числа можно записывать в 4 разных системах счисления: в двоичной, в восьмеричной, в десятичной и в шестнадцатеричной. Чтобы записать число в десятичной системе счисления можно либо не указывать систему счисления вообще, либо дописать к числу справа букву “d”, например: 32167 или 32167d. Чтобы записать в восьмеричной нужно дописать к числу справа букву “o”, например: 32167o равно числу 13431. Чтобы записать в двоичной нужно дописать справа букву “b”, например: 100110b равно числу 38. Записать число в шестнадцатеричной системе счисления можно несколькими способами: можно дописать слева последовательность “0x”, можно дописать слева “$” и можно дописать справа букву “h”, но в последнем случае число должно начинаться с одной из десятичных цифр. Пример: 0xFE=$FE=0FEh=254, запись FEh приведёт к ошибке.

Компьютер работает с числами в двоичной системе счисления. Один двоичный знак называется битом, соответственно число состоящее из n двоичных знаков называется n-битным (или n-разрядным). Например число 11010010b – 8-ми битное. Группа из 8 бит называется байтом(byte), из 2-х байт – словом(word), из 4-х байт – двойным словом(double word), из 8-и байт – счетверённым словом(quad word), и т.д.

Организация памяти

Каждая программа (её код и данные) располагается в оперативной памяти. Оперативная память состоит из ячеек размером в 1 байт. Для того чтобы обращаться к этим ячейкам, каждой из них сопоставлено некоторое число, называемое адресом (в нашем случае — 64-битное) (см. рис. 1).
Адреса принято записывать в 16-ричной системе счисления. Множество адресов вместе с сопоставленными им ячейками памяти называется адресным пространством. В адресном пространстве не каждому адресу сопоставлена какая-то ячейка памяти. Существует физическое адресное пространство, в нём каждой ячейке оперативной памяти сопоставлен единственный адрес. В целях безопасности сделано так, что у каждого процесса адресное пространство своё, т.е. процессу доступна не вся оперативная память компьютера, а только некоторая её часть. Адресное пространство процесса называется виртуальным. Одной и той же ячейке памяти в разных адресных пространствах могут быть сопоставлены разные адреса.
У виртуальной памяти есть атрибуты доступа: запись, чтение, исполнение. Например, если память не доступна для записи, то попытка записать в неё приведёт к ошибке, если она не доступна для исполнения, то попытка исполнения кода, находящегося там приведёт к ошибке, и т.д.
Ещё хотелось бы добавить, что когда говорят “указатель на <что-то>”, то имеется ввиду виртуальный адрес, с которого начинается <что-то>.

Общий вид исходника

Начну с того, что коснусь структуры исполняемого файла.
Исполняемый файл Windows состоит из нескольких частей:
1)Заголовок – обязателен
2)Секции, их может быть любое количество
У каждой секции есть имя (состоит из 8 ANSI символов) и атрибуты, в них входят атрибуты доступа к памяти.

Теперь рассмотрим HelloWorld простейшую программу на fasm-е:

format PE64 console;формат файла, подсистема
entry start;точка входа

include 'win64w.inc';заголовки

section '.code' readable executable code;секция кода

proc start;процедура

	nop
	nop
	nop

	ret
endp ;конец процедуры

section '.data' readable writeable data; секция данных
dd ?;данные

Первой строчкой указывается формат файла(в нашем случае PE64 – это 64-битный исполняемый файл Windows PE-Portable Executable) и подсистема console(это значит что при запуске нашей программы у неё будет создана консоль).
Вторая строчка entry start указывает точку входа, это — место с которого начинает исполняться код нашей программы.
include 'win64w.inc' – подключение некоторых заголовочных файлов.
Слово section обозначает начало новой секции, после него идёт имя в кавычках, а затем атрибуты.
Секцию кода мы пометили как доступную для чтения и исполнения, но не для записи, атрибут “code” указывает на то, что в данной секции находится код, он не обязателен, без него всё будет нормально работать.
Секция данных доступна для чтения и записи, атрибут “data” указывает на то, что в данной секции находятся данные, он, опять же, не обязателен. В принципе можно создавать любое количество секций(даже с одинаковыми именами) и класть код и данные в любые из них, главное, чтобы то, что предназначено для исполнения находилось в исполняемой памяти, а то, что для записи находилось в памяти доступной для записи.

В секции данных находится загадочная строка dd ?, сейчас расскажу, что она означает.

Объявление данных

Для объявления данных существует ряд директив, изображённых в следующей таблице:

Размер (в байтах) Объявление данных
1 db,file
2 dw,du
4 dd
6 dp,df
8 dq
10 dt

Чтобы объявить данные размером в байт нужно написать db <значение>, например для объявления байта со значением 0xAB служит строчка db 0xAB, остальные директивы предназначены для объявления данных большего размера. Если вместо значения после директивы стоит “?”, это означает, что вам безразлично, какое там будет значение. Следует так-же отметить, что директива dt не предполагает указания целых чисел, а предполагает числа с плавающей точкой. С помощью этих директив можно объявить несколько блоков данных подряд, например: db 12o,10,? объявляет 3 байта подряд: сначала со значением 12(в восьмеричной системе счисления), далее 10, далее – безразлично. Строка dd 32167,12объявляет 2 двойных слова со значениями 32167 и 12 соответственно. Чтобы заполнить участок памяти одним значением используется слово “dup”, например строка dd 12 dup (10) объявляет 12 двойных слов со значением 10, строка dd 5 dup (3,6) объявляет 5 пар двойных слов со значениями 3 и 6 соответственно.
Что касается директив db и du, они в качестве значения могут принимать строки (в кавычках), например: строка db 'Hello world!',0 объявляет строку в формате ANSI с нулём на конце, а du 'Hello world!',0 объявляет такую-же строку только в формате юникод (UTF-16).
Директива file принимает в качестве значения имя файла (file <имя файла>), после компиляции в этом месте программы будет находиться содержимое файла <имя файла>. Чтобы в коде можно было обращаться к определённым данным нужно дать им какое-нибудь имя. Данные с именем мы будем называть переменными. Имя может состоять из латинских букв, цифр и знака подчёркивания (хотя буквы могут быть не только латинские, но и любые другие, но я настоятельно не рекомендую их использовать). Имя пишется перед директивой объявления данных, например так: qwe dd 32167, в данном случае мы объявили переменную с именем “qwe”, размером 4 байта, у которой начальное значение равно 32167.

Регистры

Перед тем, как подходить к писанию кода необходимо обсудить регистры. Всего есть 16 регистров общего назначения (вообще регистров больше, но общего назначения – 16). Имена 8-и из них: rax,rcx,rdx,rbx,rsp,rbp,rsi,rdi, все остальные называются r8-r15, нижние 32 бита регистров называются: eax,ecx,edx,ebx,esp,ebp,esi,edi, остальные r8d-r15d. Нижние 16 бит называются ax,cx,dx,bx,sp,bp,si,di, остальные r8w-r15w. Нижние 8 бит называются al,cl,dl,bl,spl,bpl,sil,dil, остальные r8b-r15b. Так же можно обращаться верхним 8-и битам нижних 16 бит первых 4-х регистров по именам: ah,ch,dh,bh. Так же есть регстр – указатель инструкций rip, и регистр флагов rflags. rip всегда указывает на следующую инструкцию (которая идёт после той, которая выполняется), регистры изображены на рис. 2

Собственно программа

Если вы скомпилируете программу приведённую выше, то обнаружите при запуске, что она ничего не делает, потому что она действительно ничего не делает! Выше я говорил про точку входа, она начиналась со строки proc start, и закончилась на endp, что представляют собой эти макросы (proc и endp), я пока говорить не буду, скажу лишь что объявив таким образом процедуру к ней можно обращаться из любого места программы. Внутри процедуры “start” трижды написана команда “nop”, так вот:
nop — команда, которая ничего не делает.
Далее я хотел бы ввести ещё одну команду:
int3 (без пробела) — команда точки останова. Если программа находится под отладчиком, то как только исполнение доходит до этой инструкции программа останавливается, что будит видно в отладчике. Если же программа не находится под отладчиком, то выполнение этой инструкции немедленно приводит к ошибке.
Попробуйте добавить int3 перед 3-мя nop-ами в приведённой в статье программе, скомпилируйте и откройте отладчиком, вы увидите что-то вроде того, что изображено на рис. 3.
Это — точка останова внутри системного кода. Продолжите выполнение программы (в WinDbg — F5), после чего вы увидите в окне дизассемблера что-то вроде этого:

push rbp
mov rbp,rsp
int3
nop
nop
nop
leave
ret

“push rbp\mov rbp,rsp” и “leave\ret” — это пролог и эпилог процедуры соответственно, позже я объясню, что это и зачем.
Нажмите несколько раз “step over” (в WinDbg – F10) или “step into” (в WinDbg – F8) и вы увидите как отладчик переходит от одной инструкции к другой.

Программы для развития.

На этом – всё, в следующей статье я рассмотрю основные машинные инструкции и расскажу несколько подробнее про содержимое этих окон в WinDbg, если вы используете другой отладчик, то разница, в общем, небольшая.