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: типичный пролог