Чтобы лучше понимать о чем идет речь, рекомендуем ознакомится с материалом до обзора языка Assembly.
Чтобы программировать было проще, программисты написали программу - assembler (сборщик). Программа assembler читала заданный файл, где были написаны инструкции для процессора символами, и транслировала это в другой файл в бинарном виде.
Например, если в исходном файле была строчка mov al, 6, то в выходном файле вместо нее появлялась команда b0 06 (команда - поместить число 6 в регистр al).
А строчка mov bl, 6 транслировалось в b1 06(команда - поместить число 6 в регистр bl).
Таким образом программа, написанная в виде:
push rbp mov rbp, rsp xor eax, eax mov dword [rbp-4H], 0 mov dword [rbp-8H], 10 mov ecx, dword [rel _i] add ecx, 5 mov dword [rel _i], ecx pop rbp ret читается намного проще, чем в бинарном виде:
55 48 89 E5 C7 45 FC 00 00 00 00 C7 45 F8 00 00 00 0A 8B 45 F8 83 C0 05 89 45 F8 8B 45 F8 5D C3 16-ричный формат. Переносы строк исключительно для удобства восприятия Язык, на котором нужно было писать в исходном файле назвали assembly, а программу assembler - компилятором.
Стоит отметить, что язык assembly бывает разный: многие фирмы, или даже частные лица, пишут свой assembler. Из-за этого синтаксис, требуемый компилятором, может прилично разнится. Подробнее можно почитать в этой статье.
Некоторые базовые основы и команды assembly ; комментарий Начиная с символа ; и до конца строки компилятором игнорируются и не попадают в бинарный код
mov приемник, источник Команда mov (сокращение от move - перемещать) записывает значение из источника в приемник. Приемником может быть регистр или адрес в памяти. Источником может быть еще и число. Например:
mov bl, 10 ; поместить число 10 (байт) в регистр bl mov cl, bl ; поместить число хранимое в регистре bl в регист cl mov [10],cl ; поместить число (байт) хранимое в регистре cl в ячейку 10 mov al, [0x0A] ; поместить число (байт) хранимое в ячейке 10 в регистр al
mov 10, al ; ошибка: куда записывать результат? mov [1],[10] ; ошибка: приемник и источник не могут быть адресами одноверменно and приемник, источник Команда логического сложения add - побитовая конъюнкция приемника и источника с сохранением результата в приемник. Конъюнкция - это превосходство 1 над 0. Например:
mov al, 10 ; 10 в 16-ричном формате это 0x0A или 1010 в бинарном and al, 5 ; 5 - в бинарном виже это 0101 ; в результате в al будет 1111, т.е. 15, т.к. 1010 + 01010 = 1111
mov bl, 3 ; 3 в бинарном виде это 0011 mov al, 1 ; 1 в бинарном виде это 0001 and bl, al ; результат в bl = 0011, т.е. 3, т.к. 0011 + 0001 = 0011
mov [0x0A], 11 ; 11 в бинарном виде это 1011 and [10], 10 ; в ячейке 0x0A = 11 или 1011, т.к. 1011 + 1010 = 1011
and 123, [15] ; ошибка: куда записывать результат? and [10], [11]; ошибка: приемник и источник - адреса or приемник, источник Команда логического умножения or - побитовое дизъюнкция приемника и источника, с сохранением в приемник. Дизъюнкция - это превосходство 0 над 1. Например:
mov al, 10 ; or al, 5 ; в результате в al = 0, т.к. 1010 * 0101 = 0000
mov bl, 3 mov al, 1 or bl, al ; в bl = 1, т.к. 0011 * 0001 = 0001
mov al, 15 ; 15 в бинарном виде 1111 or al, 0 ; в al = 0, т.к. 1111 * 0000 = 0000
or 0, al ; ошибка: куда записывать результат? or [0],[1] ; ошибка: приемник и источник - адреса add аккумулятор, источник Команда add суммирует два значения и сохраняет результат в аккумулятор. Аккумулятором может быть регистр или адрес в памяти. Источником может быть число, регистр или адрес в памяти. Адрес в памяти не может быть одновременно и аккумулятором и источником. Например:
add al, bl ;просуммировать числа в регистрах al и bl и сохранить в al add bl, 10 ;просуммировать число в регистре bl и 10 и сохранить в bl add [0x08], bl ; просуммировать числа в bl и ячейке с адресом 8 add al, [0x09] ; просуммировать числа по адресу 9 с al, сохранив в al
add 10, al ; ошибка: куда записывать результат? add [0x08], [0x09] ; ошибка: аккумулятор и источник - адреса sub аккумулятор, источник Команда sub - разность между аккумулятором и источником. Все аналогично add.
jmp адрес Команда jmp записывает заданный адрес в регистр счетчика команд. Таким образом, следующая исполняемая процессором команда будет расположена по указанному адресу. Например:
add al, 1 ; первая команда, значит будет расположена в ячейке с адресом 0 jmp 0 ; записать в регистр счетчика команд 0, значит следующая для исполнения команда, расположена по этому адресу Простейшая программа бесконечного цикла: они никогда не завершится, и будет бесконечно прибавлять 1 к регистру al Еще один бесконечный цикл, но более запутанный:
mov al, 0 mov bl, 0
add al, 1 jmp 11
add bl, 1 jmp 4 Еще один бесконечный цикл, но более запутанный Этот код компилируется в:
B0 00 ; mov al, 0 B3 00 ; mov bl, 0
04 01 ; add al, 1 E9 0B 00 00 00 ; jmp 11
80 C3 01 ; add bl, 1 E9 04 00 00 00 ; jmp 4 не трудно посчитать, что в ячейке с адресом 4 расположена команда add al, 1, а в ячейке с адресом 11 - команда add bl, 1 je адрес, jne адрес, jg адрес, jge адрес, jl адрес, jle адрес и другие После каждой операции в АЛУ процессора, результат вычисления формирует регистр флагов. Регистр флагов состоит из различных бит, каждый из которых проверяем результат на какое-то значение. Например, в регистре флагов, есть zero-флаг проверяющий равен ли результат 0. Если равен, zero-флаг устанавливается в 1. В противном случае в 0. Еще один флаг - Sign-флаг. Он установлен в 1, если результат последней операции имеет отрицательное значение. Команда je - записать адрес в счетчик команд, только если zero-флаг был установлен. В ином случае выполняется следующая команда. je - это сокращение от jump if equal (прыжок, если равно). Чтобы понять к чему это сокращение, можно представить, что перед командой je мы вычисляли разность двух чисел. Если эти числа одинаковые, то результат будет равен 0, а флаг zero (ZF) установлен:
mov al, 5 mov bl, 4
sub al, bl je 1 ; прыжок не произойдет, т.к. 5 - 4 = 1, zero-флаг не установлен
mov al, 0 je 3 ; прыжок не произойдет, т.к. флаги меняются только при арифметических операциях
add al, 0 je 0 ; прыжок произойдет, т.к. 0 + 0 = 0 Команда jne - прыжок, если не равно (jump if not equal), т.е. если ZF = 0. Комадна jg (jump if greater, прыжок, если больше) - переход если sign-флаг (SF) не установлен, т.е. гипотетическая разность двух чисел перед jg, больше 0. Команда jge - больше либо равно, т.е. SF = 0 или ZF = 1. Команда jl - меньше, т.е. SF = 1 и ZF =0. Команда jle - меньше либо равно, т.е. SF = 1 или ZF = 1.
push источник, pop приемник Команды для работы со стеком push и pop. Упрощенно, стек - это название последовательности, к которому применим принцип "последний пришел, первый ушел". Бытовым примером стека, являются тарелки лежащие друг на друге. Помыв еще одну тарелку, мы кладем ее на самую верхнюю из уже лежащих. А если потребуется взять одну, то самую верхнюю и возьмем. Получается, последний пришел, первый ушел. Противоположностью стека, является очередь, где применим принцип "первый пришел, первый ушел". Т.к. стек очень часто применяется в написании программ, для него сделали регистр и команды в процессоре. SP (stack pointer) - так называется регистр в процессоре для работы со стеком. Команд push - поместить в стек. Команда pop - вытащить из стека. Например:
mov al, 8 mov bl, 15
mov sp, 10 ; указывает адрес в памяти, где начинается стек
push al ; помещаем в стек число 8, при этом в sp = 9 push bl ; в sp = 8, т.к. sp уменьшается после каждого push
pop al ; в al число 15, sp = 9 pop bl ; в bl число 8, sp = 10 так, с помощью стека, можно поменять местами значение регистров call адрес и ret Команды для работы с участками кода, функциями. Бывает ситуация, когда реализовав сложную операцию, нам чется ее использовать в совершенно разных местах программы для обработки разных данных. На пути, несколько проблем. Мы можем сделать прыжок к данному участку программы, сохранив данные для обработки в регистрах. Но после их обработки, как понять куда прыгать дальше?! Тут то и спасает стэк. В стэк мы можем поместить адрес, куда нужно прыгать после завершения участка кода, а сам участок кода имеющий определенное назначение, называют функцией.
; тут много кода
; начало функции add al, bl ; складываем shl al, 1 ; делим на 2
pop cl ; вытаскивает адрес возврата jmp cl ; конец функции
; тут много кода
mov al, 8 mov bl, 12 push 0xABC ; сохраняем в стэк адрес возврата jump 0x123 ; прыгаем в функцию mov [0x1F], al ; сохраняем результат, тут адрес 0xABC
; тут много кода
mov al, [0x1A] mov bl, 10 push 0xBCD ; сохраняем в стэк адрес возврата jump 0x123 ; прыгаем в функцию mov [0x1B], al ; сохраняем результат, тут адрес 0xBCD Вызов функций было на столько частой операцией, что были созданы специальные команды call для вызова функции, и ret для возврата из нее. Пример выше с использованием call и ret выглядит следующим образом:
; тут много кода
; начало функции add al, bl ; складываем shl al, 1 ; делим на 2
ret ; прыгаем назад ; конец функции
; тут много кода
mov al, 8 mov bl, 12 call 0x123 ; вызываем функцию mov [0x1F], al ; сохраняем результат, тут адрес 0xABC
; тут много кода
mov al, [0x1A] mov bl, 10 call 0x123 ; вызываем функцию mov [0x1B], al ; сохраняем результат, тут адрес 0xBCD Данные db и dw Запись конкретных значений в определенном месте в память происходит благодаря этим командам. db (data byte) - записать байт, dw (data word) - записать два байта. Например:
db 10 ; записать байт db 0x05, 5 ; через запятую можно указывать несколько значений dw 0x1234, 5 ; записать 2 числа, каждое занимает 2 байта В результате, после компиляции, в бинарном файле мы увидим:
0A
05 05
34 12 05 00
label:
Метка - это способ задать имя определенному адресу, чтобы потом можно было указывать нужный адрес меткой. Например, пример выше с запутанным бесконечным циклом будет выглядеть так:
mov al, 0
mov bl, 0
first_label: add al, 1 jmp second_label
second_label: add bl, 1 jmp first_label пробелы от левого края - для удобства восприятия или пример с функцией:
; тут много кода
start_function: ; начало функции add al, bl ; складываем shl al, 1 ; делим на 2
ret ; прыгаем назад
; конец функции
; тут много кода
mov al, 8
mov bl, 12
call start_function ; вызываем функцию
mov [0x1F], al ; сохраняем результат, тут адрес 0xABC
; тут много кода
mov al, [0x1A]
mov bl, 10
call start_function ; вызываем функцию
mov [0x1B], al ; сохраняем результат, тут адрес 0xBCD
startfunction - название метки start_function - это название метки, которую придумывает программист, и оно может состоять из латинских букв, и цифр. Ее можно вставлять везде, где в коде ожидается адрес. В нашем примере при вызове функции call start_function. При компиляции, компилятор запоминает адрес следующей после метки команды, и везде по коду заменяет имя метки на этот адрес.
Метки очень упрощают программирование, т.к. благодаря им не нужно высчитывать адреса для прыжков, достаточно определить метку.
На этом краткий обзор языка Assembly завершен. Если этого вам мало, можно почитать статью Assembly для начинающих.