X86 assembler tutorial
Ассемблер в UNIX использует особенный синтакс, в котором source и destination перепутаны местами:
opcode source, dest
movl %edx, %eax		# перемещает содержимое регистра edx в регистр eax
addl %edx, %eax		# складывает содержимое регистров edx и eax, результат кладет в eaxТакже, к отличиям относится то, что все назнания регистров должны начинаться со знака %, а инструкции кончаются на l, w или b, означающие размер операнда: long (32 бита), word (16 бит), или byte (8 бит), соответственно.
Регистры
Регистры 32-битные.
Для разных частей одного и того же регистра есть разные имена, например, младшие 8 бит (0-7) регистра %eax носят имя %al, а следующие после них (8-15) - %ah. Первые 16 бит %eax (0-15) носят имя %ax. А %eax используется, когда нужно обратиться ко всем 32 битам регистра eax. Форма имени регистра должна совпадать с суффиксом инструкции, то есть для инструкций, кончающихся на b используются %al и %ah, для w - %ax, а для l - %eax.
Вот основные регистры процессора:
| Название | Описание | 
|---|---|
| EAX, EBX, ECX, EDX | регистры общего назначения, EAX обычено используется для вохвращаемых значений (stdcall) | 
| EBP | Базовый указатель для текущего фрейма стека. | 
| ESI, EDI | Индексные регистры, относятся к DS и ES соответственно | 
| SS, DS, CS, ES, FS, GS | Сегментные регистры. Содержат селектор начала сегмента данных. Все содержат по 16 бит. | 
| EIP | program counter / instruction pointer, относителен к CS (code segment) | 
| ESP | stack pointer, относителен к SS (stack segment). Автоматически изменяется при push/pop | 
| EFLAGS | Флаги | 
Разница между EBP и ESP
ESP - указатель на вершину стека (т.к. стек растет сверху вниз, то вершина стека будет внизу стека). После пролога все локальные переменные, адрес возврата и аргументы функции оказываются над ним.
EBP - указатель на начало текущего фрейма стека (будет наверху той части стека, которая предназначается для текущей процедуры). Над ним - адрес возврата и аргументы функции, под ним - локальные переменные.
Обращение к локальным переменным обычно идет через EBP.
Пролог обычно выглядит так:
pushl %ebp
movl %esp, %ebp
subl  <some_number>, %espТаким образом, EBP всегда означает начало текущего фрейма, а ESP - инструкция, следующая за его концом, то есть место, куда будет сунуто значение при следующей команде push. Когда мы вызовем еще одну функцию, то она в своем прологе запушит на стек наш EBP и присвоит ему ESP, то есть инструкция, следующая за концом нашего фрейма, станет началом ее фрейма.
Обращение в коде к локальным переменным может выглядеть так:
movl eax, -4(%ebp)
movl -8(%ebp), ebxОбращение к аргументам (аргументы пушатся на стек в обратном порядке, то ест сначала самый правый, в конце самый левый):
movl 4(%ebp), %eax # прочесть **первый** аргумент в EAX
movl 8(%ebp), %ebx # прочесть **второй** аргумент в EBXА выход из функции (эпилог) выглядит так:
mov esp, ebp ;
pop ebp ;
retДля перечисленных ниже соглашений (кроме [cdecl] перед возвратом значений из функции подпрограмма обязана восстановить значения сегментных регистров, регистров esp и ebp. Значения остальных регистров могут не восстанавливаться.
Если размер возвращаемого значения функции не больше размера регистра eax, возвращаемое значение сохраняется в регистре eax. Иначе, возвращаемое значение сохраняется на вершине стека, а указатель на вершину стека сохраняется в регистре eax.
(в X64 везде используется fastcall, то есть при передаче аргументов в функцию первые несколько аргументов хранятся в регистрах (rcx-rdx-r8-r9 для Windows и rdi-rsi-rdx-rcx-r8-r9 для линукса, остальные - в стеке)
Сегментация
Все адреса формируются из адреса начала сегмента и сдвига. Чтобы вычислить адрес начала сегмента, процессор определяет, какой регистр сегмента используется, берет его значение и использует его в качестве индекса для GDT (global descriptor table), откуда получает абсолютный физический адрес начала сегмента. Затем процессор складывает этот адрес с указанным в инструкции сдвигом и получает финальный физический адрес.
У i486 есть 6 16-битных сегментных регистров:
- CS: Code segment register - для обращения к инструкциям
 - SS: Stack segment register - для обращения к стеку
 - DS: Data segment register - для обращения памяти, не относящейся к стеку, то есть к куче
 - ES, FS, GS: Extra segment registers - хз зачем, вроде могут использоваться в каких-то специальных инструкциях
 
НЕЛЬЗЯ копировать из сегментного регистра в сегментный регистр, то есть следующая операция запрещена:
movw seg-reg, seg-regЗато ничто не запрещает использовать в качестве промежуточного хранилища регистр или область памяти:
movw seg-reg,memory
movw memory,seg-reg
movw seg-reg,reg
movw reg,seg-regЧастые/полезные инструкции
- pushl/popl - положить/снять 32-битное значение на стек
 - pushal/popal - положить/снять со стека EAX, EBX, ECX, EDX, ESP, EBP, ESI, EDI (аналога в x64 нет)
 - call - положить адрес возврата на стек и перейти к указанной метке в коде
 - int - вызвать программное прерывание
 - ret - вернуться из куска кода, в который перешли инструкцией 
call, то есть использовать лежащий на стеке адрес возврата и передать по нему управление. Адрес возврата ищется по адресуRBP + 4, поэтому регистр RBP обязательно нужно сохранять в прологе и восстанавливать в эпилоге. - iretl - вернуться из куска кода, в который перешли благодаря прерыванию
 - sti/cli - установить/очистить бит прерывания, чтобы включить/выключить все прерывания
 - lea - Load Effective Address, похож на MOV, см. далее
 
Адресация
Использование круглых скобок позволяет получить значение по указанному адресу.
Пример:
mov (%rsp), %rax		# прочитай 8 байт по адресу, указанному в регистре RSP и сохрани их в регистр RAXМожно сразу указывать смещение (положительное либо отрицательное) относительно адреса:
mov 8(%rsp), %rax		# возьми rsp, прибавь к нему 8, прочитай 8 байт по получившемуся адресу и положи их в raxКоманда LEA позволяет выполнить умножение и несколько сложений сразу:
# rax := rcx*8 + rax + 123
lea 123(%rax,%rcx,8), %raxПример
void function1() {
	int A = 10;
	A += 66;
}компилится в:
function1:
1	pushl %ebp #
2	movl %esp, %ebp #,
3	subl $4, %esp #,
4	movl $10, -4(%ebp) #, A
5	leal -4(%ebp), %eax #, 
6	addl $66, (%eax) #, A
7	leave
8	ret- Бэкапим EBP на стек
 - Копируем указатель стека в EBP
 - Выделяем место на стеке в размере 4 байта для локальной переменной
 - Кладем значение 10 в область локальных переменных стека, то есть создаем переменную А со значением 10
 - Загружаем адрес переменной A в регистр EAX
 - Прибавляем 66 к EAX и кладем результат в EAX
 
1-3: типичный пролог