How's that again?

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-битных сегментных регистров:

  1. CS: Code segment register - для обращения к инструкциям
  2. SS: Stack segment register - для обращения к стеку
  3. DS: Data segment register - для обращения памяти, не относящейся к стеку, то есть к куче
  4. 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
  1. Бэкапим EBP на стек
  2. Копируем указатель стека в EBP
  3. Выделяем место на стеке в размере 4 байта для локальной переменной
  4. Кладем значение 10 в область локальных переменных стека, то есть создаем переменную А со значением 10
  5. Загружаем адрес переменной A в регистр EAX
  6. Прибавляем 66 к EAX и кладем результат в EAX

1-3: типичный пролог