простейший шаблон программы:
.code64 .global _start .data # some data will be placed here .text _start : # code of instructions will be placed here mov $60 , %rax syscall
первая директива указывает для какой архитектуры написан код. возможные варианты:
.code16
.code32
.code64
сама программа состоит из разделов, начало которых указывается директивами
.bss
- для раздела неинициализированных данных .data
- для раздела данных .text
- для раздела инструкций любая программа должна иметь глобальную метку _start
- точку входа. директива .global
указывает загрузчику на эту точку
комментарии начинаются со знака #
и идут до конца строки
последние две инструкции осуществляют корректный выход из программы
компиляция с отладочной инофрмацией :
$> as -o q.o -gstabs q.s $> ld -o q.out q.o
забудьте о размышлениях в терминах переменных языков высокого уровная - с ассемблером вместо переменных вы используете регистры. каждый бит данных с которым вы манипулируете должен попасть в CPU и простейшая инструкция X - 4 превращается в три инструкции псевдокода:где TMP - это имя регистра, а OSP - имя базового регистраmov [OSP] , TMP # load the variable into a register sub $4 , TMP # do the actual computation mov TMP , [OSP] # store the result to memory
в 16-разрядной архитектуре 8086 были следующие регистры общего назначения:
первые четыре регистра делятся на две однобайтовых части: AH, BH, CH, DH для старших байтов и AL, BL, CL, DL для младших
в 80386 разрядность регистров была удвоена и составила 32 бита. 32-разрядные версии получили имена EAX, EBX, ECX, EDX, ESI, EDI, EBP и ESP, а их младшие слова сохранили прежние обозначения, причём только у первых четырёх регистров сохранилась возможность раздельного обращения к двум младшим байтам (AH, AL; BL, BH; CL, CH; DL, DH). компоненты 32-разрядных адресов стало можно хранить в любом регистре. появилась возможность масштабирования — использования содержимого регистра в качестве индекса, при вычислении адреса умножаемого на фактор, равный 2, 4 или 8
появление 64-разрядных микропроцессоров повлекло изменения в наборе регистров общего назначения
при запуске программы регистры общего назначения (кроме регистра стека SP и регистра счетчика команд IP) обнуляются:
(gdb) starti ... (gdb) info registers rax 0x0 0x0 rbx 0x0 0x0 rcx 0x0 0x0 rdx 0x0 0x0 rsi 0x0 0x0 rdi 0x0 0x0 rbp 0x0 0x0
код любой операции состоит из мнемоники операции и (может быть) одного или двух операндов
коды могут быть без операндов, с одним операндом, с двумя операнадми. в последнем случае один из операндов обязательно будет регистром
имя регистра всегда предваряется префиксом %
числовые константы в операциях всегда предваряются префиксом $
если суффикс не используется и в инструкции только регистровые переменные, то по умолчанию размеры операндов полагаются равными размеру второго операнда (destination register operand)
.byte
.word
.long
.quad
директива
будет ассемблирована в :
mymem: .word 7,2,3,5
07 00 02 00 03 00 05 00
а команда
загрузит третье и четвертое слова в регистр EAX и его значение eax = 0x00050003 (третье слово попадет в AL а четвертое - в AH. перед загрузкой можно очистить регистр EAX целиком
movw mymem+4 , %eax
а потом читать только содержимое регистра AX
xor %eax, %eax
другие возможности:
это будет транслировано в:
mymem: .quad 7,2,3,5
07 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 03 00 00 00 00 00 00 00 05 00 00 00 00 00 00 00
а это будет транслировано в:
mymem: .long 7,2,3,5
07 00 00 00 02 00 00 00 03 00 00 00 05 00 00 00
и вот это будет транслировано в:
mymem: .byte 7,2,3,5
07 02 03 05
адресация памяти может быть прямой или косвенной
при прямой адресации в код операции помещается метка области данных
при косвенной адресации используются данные регистров и, может быть, фактор-константа и константа смещения
итак, у операнда-адреса памяти может быть до четырех параметров:
segment:displacement(base register , index register , scale factor)
все они, кроме базы, могут быть опущены. если не используется база, то ее отсутсвие обозначатеся запятой ,
movl -8(%ebp, %edx, 4) , %eax # (EBP + (EDX * 4) - 8) into EAX movl -4(%ebp), %eax # load a stack variable (EBP - 4) into EAX movl (%ecx), %edx # copy the target of a pointer into EAX leal 8(, %eax, 4), %eax # multiply EAX by 4 and add 8 leal (%edx, %eax, 2), %eax # multiply EAX by 2 and add EDX
-4(%ebp)
база %ebp , смещение -4. имя секции пропущено и определяется по умолчанию (%ss при адресации с использованием %ebp, %ds при адресации с использованием %edx). также пропущены индекс и фактор
foo(,%eax,4)
индекс %eax (скалированный фактором 4); смещение foo. все другие поля не указаны. по умолчанию регистром секции будет %ds
%gs:foo
выбирается содержимое памяти по адресу foo и явно используется регистр секции %gs
регистры могут трактоваться, как указатели на данные в памяти. для разыменования таких указателей используется специальный синтаксис:
mov (%rsp), %rax
«прочитай 8 байт по адресу, записанному в регистре RSP, и сохрани их в регистр RAX». при запуске программы RSP указывает на вершину стека, где хранится число аргументов, переданных программе, указатели на эти аргументы, а также переменные окружения и кое-какая другая информация. таким образом, в результате выполнения приведенной выше инструкции (разумеется, при условии, что перед ней не выполнялось каких-либо других инструкций) в RAX будет записано количество аргументов, с которыми была запущена программа
в одной команде можно указывать адрес и смешение (как положительное, так и отрицательное) относительно него:
mov 8(%rsp), %rax
«возьми RSP, прибавь к нему 8, прочитай 8 байт по получившемуся адресу и положи их в RAX». таким образом, в RAX будет записан адрес строки, представляющей собой первый аргумент программы, то есть, имя исполняемого файла (в i686 - смещение должно быть равно 4)
при работе с массивами бывает удобно обращаться к элементу с определенным индексом. соответствующий синтаксис:
xchg 16(%rsp,%rcx,8), %rax
«посчитай RCX*8 + RSP + 16, и поменяй местами 8 байт (размер регистра RAX) по получившемуся адресу и значение регистра RAX». RSP и 16 все так же играют роль смещения, RCX играет роль индекса в массиве, а 8 — это размер элемента массива. при использовании данного синтаксиса допустимыми размерами элемента являются только 1, 2, 4 и 8
следующий код тоже валиден:
.globl _start .data msg: .ascii "hello, world!\n" .text _start: xor %rcx, %rcx mov msg(,%rcx,8), %al mov msg, %ah mov $60, %rax syscall
еще одна полезная инструкция lea
грузит в регистр не данные, а их адрес:
# rax := rcx * 8 + rax + 123
lea 123(%rax,%rcx,8) , %rax
если вам требуется адрес в памяти BP+16, а не значение, хранящееся по этому адресу, то используйте
lea 16(%ebp) , %eax
instead of
mov
Load Effective Address выполняет адресную арифметику и заносит получившиейся адрес в регистр
NB: 10(%rip) значит 10 байтов после текущей инструкции
NB: label_bbb(%rip) означает смещение до метки label_bbb, а не RIP + symbol_value
две команды - инкрементирования и декрементирования содержимого регистров:
# инкремент: rax = rax + 1 inc %rax # декремент: rcx = rcx - 1 dec %rcx
биты специального регистра eflags/rflags (i686 и x64 соответственно). напрямую обращаться к этому регистру нельзя, но он изменяется и используется различными инструкциями косвенно
carry flag (CF, 0-ой бит)
parity flag (PF, 2-бит)
zero flag (ZF, 6-ой бит)
auxiliary carry flag (AF, 4-бит)
sign flag (SF, 7-ой бит)
direction flag (DF, 10-ый бит)
overflow flag (OF, 11-ый бит)
trap flag (TF, 8-й бит)
auxiliary flag используется только при BCD вычислениях (на младших четырех битах регистра)
инструкции сохранения флагов в аккумуляторе lahf
и загрузки флагов из аккумулятора sahf
первая загружает флаги в аккумулятор,
7 6 5 4 3 2 1 0 ------------------------------------- AH := SF ZF x AF x PF x CF
вторая - выгружает их оттуда в регистр флагов
инструкции сохранения флагов в стеке
pushf
и загрузки флагов из стека
popf
первая загружает флаги в стек, вторая - выгружает их оттуда в регистр флагов
к примеру, вот так можно получить в регистре AL значение флага CF:
pushf pop %rax and $1, %rax
интелевский стек растет в сторону младших адресов. это значит, что когда вы пушите что нибуть на стек, то указатель RSP сначала уменьшается на 8 (хотя сам стек по размеру становится больше), и потом по этому адресу помещается запушеное вами значение. а когда вы попите из стека, то получаете значение по текущему адресу RSP, а потом этот адрес увеличивается на 8 (хотя сам стек по размеру становится меньше)
in Linux the 128-byte area beyond the location pointed to by RSP (i.e. towards low addresses) is considered to be reserved and shall not be modified by signal or interrupt handlers. therefore, functions may use this area for temporary data. in particular, leaf functions may use this area for their entire stack frame, rather than adjusting the stack pointer in the prologue and epilogue. this area is known as the "red zone"
"red zone" begins directly under the current value of the stack pointer RSP (-8(%rsp), -16(%rsp) and so on)
the "red zone" is an optimization. code can assume that the 128 bytes below RSP will not be asynchronously clobbered by signals or interrupt handlers, and thus can use it for scratch data, without explicitly moving the stack pointer. the last sentence is where the optimization lays - decrementing RSP and restoring it are two instructions that can be saved when using the "red zone" for data
keep in mind that the "red zone" will be clobbered by function calls, so it's usually most useful in leaf functions (functions that call no other functions)
пример вывода на stdout содержимого ячейки стека:
.code64 .globl _start _start: push $67 mov $1 , %rax # sys_write mov $1 , %rdi # to stdout mov %rsp , %rsi # buffer address - stack cell mov $1 , %rdx # byte count syscall sub $8, %rsp # restore stack mov $0, %rdi mov $60, %rax syscall
регистр eip/rip, хранит адрес текущей инструкции. к нему нельзя обращаться напрямую, но он виден в GDB вместе с eflags/rflags, если сказать info registers
. большинство инструкций просто увеличивают eip/rip на длину этой инструкции, но есть и исключения из этого правила
стандартный способ положить статический адрес в регистр - "RIP-relative LEA":
lea _start(%rip) , %r10
NB: 10(%rip) значит 10 байтов после текущей инструкции
NB: label_bbb(%rip) означает смещение до метки label_bbb, а не RIP + symbol_value
инструкция jmp осуществляет переход по заданному адресу:
# обнуляем rax xor %rax, %rax jmp next # эта инструкция будет пропущена inc %rax next: inc %rax
адрес перехода может быть записан в регистре:
xor %rax, %rax mov $next, %rcx jmp *%rcx inc %rax next: inc %rax
GAS позволяет давать меткам цифирные имена типа 1:, 2:, и так далее, и переходить к ближайшей предыдущей или следующей метке с заданным номером инструкциями вроде jmp 1b (back) и jmp 1f (forward). это удобно
условные переходы обычно осуществляются при помощи инструкции cmp
, которая сравнивает два своих операнда и выставляет соответствующие флаги, за которой следует инструкция из семейства je
, jg
и подобных
выполнение инструкции
cmp
изменяет флаги ZF и CF точно таким же образом, как команда
sub
. например:
mov $8, %ax mov $5, %bx cmp %ax, %bx
cmp %rax, %rcx je 1f # перейти, если равны (equal) jl 1f # перейти, если знаково меньше (less) jg 1f # перейти, если знаково больше (greater) jb 1f # перейти, если беззнаково меньше (below) ja 1f # перейти, если беззнаково больше (above) # кусок какого-то кода 1: # какой-то код здесь
существует также инструкции
jne
(перейти, если не равны),
jle
(перейти, если знаково меньше или равны),
jna
(перейти, если беззнаково не больше)
и подобные
вместо je/jne часто пишут jz/jnz, так как инструкции je/jne просто проверяют значение ZF
есть инструкции, проверяющие другие флаги — js
, jo
и jp
все эти инструкции вместе взятые обычно называют jcc
помимо cmp
также часто используют инструкцию test
:
test %rax, %rax jz 1f # перейти, если rax == 0 js 2f # перейти, если rax < 0 1: # какой-то код 2: # какой-то еще код
jrcxz 1f # какой-то код 1: # другой код
jrcxz
осуществляет переход только в том случае, если значение регистра RCX равно нулю
cmovge %rcx, %rax
cmovcc
(conditional move) работают как mov, но только при выполнении
заданного условия, по аналогии с jcc.
setnz %al
setcc
присваивают однобайтовому регистру или байту в памяти значение 1, если
заданное условие выполняется, и 0 иначе
cmpxchg %rcx, (%rdx)
cmpxchg8b (%rsi) cmpxchg16b (%rsi)
cmpxchg8b
работает аналогично cmpxchg, только производит compare and swap сразу 8-и байт. регистры EDX:EAX используются для сравнения, а регистры ECX:EBX хранят то, что мы хотим записать. инструкция cmpxchg16b
по тому же принципу производит compare and swap сразу для 16 байт на x64
NB! без префикса lock все эти compare and swap инструкции не атомарны
mov $10, %rcx 1: # какой-то код loop 1b # loopz 1b # loopnz 1b
loop
уменьшает значение регистра rcx на единицу, и если после этого инструкции loopz
и loopnz
работают аналогично:
(rcx != 0) && (ZF == 1) и
(rcx != 0) && (ZF == 0)
соответственно
and %rbx, %rdx # rdx &= b not %rdx # rdx = ~ rdx or %rax, %rdx # rdx |= a and $1, %rdx # rdx &= 1 shl $4, %rax # сдвиг влево на 3 бита shr $7, %rax # сдвиг вправо на 7 бит ror $5, %rax # циклический сдвиг вправо на 5 бит rol $5, %rax # циклический сдвиг влево на 5 бит
инструкции установки битов и флага:
# положить в CF значение 13-го бита bt $13, %rax # то же самое + установить бит (bit test and set) bts $13, %rax # то же самое + сбросить бит (bit test and reset) btr $13, %rax # то же самое + инвертировать бит (bit test and complement) btc $13, %rax # найти младший ненулевой байт (bit scan forward) bsf %rax, %rcx # если все биты нулевые, ZF = 1, значение RDX неопределено xor %rax, %rax bsf %rax, %rdx # найти старший ненулевой байт (bit scan reverse) bsr %rax, %rdx
пример вычисления суммы массива чисел из памяти:
.global _start .data mydata : .byte 10, 20, 30, 40, 50, 60 .text _start : # summation of "mydata" array members mov $5 , %cl mov mydata(%ecx) , %al do : dec %cl add mydata(%ecx) , %al cmp $0, %cl jne do mov %al , %bl mov %rax, %rdi mov $60 , %rax syscall
.code64 .global _start .data num : .word 12 , 24 .text _start : mov $num , %edx add 0(%edx) , %ebx add 2(%edx) , %ebx mov %rbx, %rdi # return value in RDI mov $60 , %rax syscall
.code64 .global _start .data num : .word 12 , 27 .text _start : mov num + 2 , %rax sub num , %rax mov %rax , %rdi mov $60 , %rax syscall
есть некие тонкости с операциями imul
и idiv
всегда используйте xor %rdx , %rdx
непосредственно перед операцией
div
, чтобы заполнить нулями расширение аккумулятора _AX в _DX:_AX потому что
div
and
idiv
выдадут мусор если результат не поместится в регистр (AL,AX,EAX,RAX при условиии, что делимое находится в регистре такой же длины), а в регистре _DX будет что-то отличное от нуля
imul %ebx
возьмет второй множитель из регистра EAX и выдаст 64-битовый результат в регистры EDX:EAX. если регистр EDX не был инициализирован битами знака числа из EAX, но вы можете получить мусор в ответе
idiv
divides (signed) the value in the AX, DX:AX, or EDX:EAX registers (dividend) by the source operand (divisor) and stores the result in the AX (AH:AL), DX:AX, or EDX:EAX registers
источник может быть регистром или адресом в памяти
действие этой инструкции зависят от размеров операндов (dividend/divisor):
например, если вы делите на содержимое регистра EBX, то делимое будет браться автоматом из EDX:EAX. и если вы не инициализировали EDX знаком числа из регистра EAX, то в ответе вы можете получить мусор
поэтому всегда перед выполнением операции IDIVx или IMULx выполняйте одну из операций : cbw, cwd, cdq
(для 8, 16 и 32-битовых чисел соответственно). все эти операции заполняют биты регистров DX битом знака числа из регистра AX
пример деления со знаком:
.code64 .global _start .data mydata: .quad -3 .text _start : movq mydata , %rbx movq $28 , %rax cdq idivq %rbx , %rax mov %rax, %rdi mov $60 , %rax syscall
в арифметических операциях используются флаги CF и SF
CF Carry Flag. устанавливается в 1, если результат предыдущей операции не уместился в приёмнике и произошёл перенос из старшего бита или если требуется заём (при вычитании), иначе установлен в 0. команда CLC очищает флаг переноса, а команда STC - устанавливает его. команда CMC инвертирует флаг переноса
SF Sign Flag. этот флаг всегда равен старшему биту результата
ADC - Add With Carry adc src , dest изменяет флаги : AF CF OF SF PF ZF складывает src и dest и помещает результат в dest и если CF устанавлен, то добавляет 1 к dest
SBB - Subtract with Borrow sbb src , dest изменяет флаги : AF CF OF PF SF ZF вычитает src из dest и помещает результат в dest и вычитает еще и 1 если CF установлен
разница между sub
и sbb
:
clc xor %eax, %eax mov $6, %ax # AH=0000 AL=0101 SF=0 CF=0 sub $9, %al # AH=0000 AL=1101 SF=1 CF=1 clc xor %eax, %eax mov $6, %ax # AH=0000 AL=0101 SF=0 CF=0 sbb $9, %al # AH=1111 AL=1101 SF=1 CF=1
sbb
распространит знак минус на верхнюю часть результата. т.е. команда
sub
заимствует как бы "из воздуха", а команда
sbb
"честно" заимствует из старших разрядов
DF Direction Flag. контролирует поведение команд обработки строк. если установлен в 1, то строки обрабатываются в сторону уменьшения адресов, если сброшен в 0, то наоборот. команда CLD очищает флаг направления, а команда STD - устанавливает его
OF Overflow Flag. устанавливается в 1, если результат предыдущей арифметической операции над числами со знаком выходит за допустимые для них пределы. например, если при сложении двух положительных чисел получается число со старшим битом, равным единице, то есть отрицательное
mov $str1, %rsi mov $str2, %rdi cld cmpsb
командой cld
очищается флаг направления DF (выставить флаг - std
)
инструкция cmpsb
сравнивает байты (%rsi) и (%rdi) и выставляет флаги в соответствии с результатом сравнения
если DF = 0, RSI и RDI увеличиваются на единицу, иначе — уменьшаются
инструкции cmpsw
, cmpsl
и cmpsq
сравнивают слова, длинные слова и четверные слова соответственно
инструкции cmps
могут использоваться с префиксом rep, repe (repz)
и repne (repnz)
префикс rep
повторяет инструкцию заданное в регистре RCX количество раз
префиксы repz
и repnz
делают то же самое, но только после каждого выполнения инструкции дополнительно проверяется ZF
цикл прерывается, если ZF = 0 в случае c repz
и если ZF = 1 в случае с repnz
например:
mov $buf1, %rsi mov $buf2, %rdi mov $len, %rcx cld repe cmpsb jne not_equal
приведенный код проверяет равенство двух буферов одинакового размера
инструкция movs
перекладывает данные из буфера, адрес которого указан в rsi, в
буфер, адрес которого указан в RDI (RSI - source, RDI - destination)
инструкция stos
заполняет буфер по адресу из регистра RDI байтами из регистра RAX (или EAX, или AX, или AL - в зависимости от конкретной инструкции)
инструкция lods
делает обратное — копируют байты по указанному в RSI адресу в регистр RAX
инструкция scas
ищет байты из регистра RAX (или соответствующих регистров меньшего размера) в буфере, адрес которого указан в RDI
как и cmps
, все эти инструкции работают с префиксами rep, repz, repnz
процедуры вызываются при помощи инструкции
call
, которая кладет на стек адрес следующей инструкции и передает управление по указанному в аргументе адресу
возврат из процедуры осуществляет инструкция
ret
, которая читает со стека адрес возврата и передает по нему управление
например передачи параметров через стек (плохой стиль на самом деле):
.code64 .globl _start .text _start: push $5 # param 3 push $3 # param 2 push $2 # param 1 call myproc add $24 , %rsp # clear stack mov %rax, %rdi mov $60 , %rax syscall myproc: mov 8 (%rsp) , %rbx # access to param 1 mov 16 (%rsp) , %rax # access to param 2 add %rax , %rbx mov 24 (%rsp) , %rax # access to param 3 add %rbx , %rax ret
как правило, возвращаемое значение передается в регистре RAX или, если его размера не достаточно, записывается в структуру, адрес которой передается в качестве аргумента
соглашений о вызовах существует множество:
Linux uses the System V ABI for x86-64 (AMD64) architecture. this means the stack grows down - smaller addresses are "higher up" in the stack. typical C functions are compiled to
pushq %rbp ; save address of previous stack frame movq %rsp, %rbp ; address of current stack frame subq $16, %rsp ; reserve 16 bytes for local variables ; ... function ... movq %rbp, %rsp ; \ equivalent to the popq %rbp ; / 'leave' instruction ret
the amount of memory reserved for the local variables is always a multiple of 16 bytes, to keep the stack aligned to 16 bytes. if no stack space is needed for local variables, there is no subq $16, %rsp or similar instruction. (note that the return address and the previous %rbp pushed to the stack are both 8 bytes in size, 16 bytes in total). while %rbp points to the current stack frame, %rsp points to the top of the stack. because the compiler knows the difference between %rbp and %rsp at any point within the function, it is free to use either one as the base for the local variables
a stack frame is just the local function's playground: the region of stack the current function uses current versions of GCC disable the stack frame whenever optimizations are used. this makes sense, because for programs written in C, the stack frames are most useful for debugging, but not much else. (you can use e.g. -O2 -fno-omit-frame-pointer to keep stack frames while enabling optimizations otherwise, however). although the same ABI applies to all binaries, no matter what language they are written in, certain other languages do need stack frames for "unwinding" (for example, to "throw exceptions" to an ancestor caller of the current function); i.e. to "unwind" stack frames that one or more functions can be aborted and control passed to some ancestor function, without leaving unneeded stuff on the stack
when stack frames are omitted -- -fomit-frame-pointer for GCC --, the function implementation changes essentially to:
subq $8, %rsp ; re-align stack frame, and ; reserve memory for local variables ; ... function ... addq $8, %rsp ret
function parameters are typically passed in registers. in short, integral types and pointers are passed in registers %rdi, %rsi, %rdx, %rcx, %r8, and %r9, with floating-point arguments in the %xmm0 to %xmm7 registers
in some cases you'll see rep ret instead of rep. don't be confused: the rep ret means the exact same thing as ret; the rep prefix, although normally used with string instructions (repeated instructions), does nothing when applied to the ret instruction. it's just that certain AMD processors' branch predictors don't like jumping to a ret instruction, and the recommended workaround is to use a rep ret there instead
the red zone above the top of the stack (the 128 bytes at addresses less than %rsp) is not really useful for typical functions: in the normal have-stack-frame case, you'll want your local stuff to be within the stack frame, to make debugging possible. in the omit-stack-frame case, stack alignment requirements already mean we need to subtract 8 from %rsp, so including the memory needed by the local variables in that subtraction costs nothing
the standard calling convention - code passes the first 6 integer args in registers, only using the stack for the 7th and later
* 32bit SYSENTER instruction entry
* Arguments:
* %eax System call number.
* %ebx Arg1
* %ecx Arg2
* %edx Arg3
* %esi Arg4
* %edi Arg5
* %ebp user stack
* 0(%ebp) Arg6
* 64-bit SYSCALL instruction entry.
* Registers on entry:
* rax system call number
* rcx return address
* r11 saved rflags (note: r11 is callee-clobbered register in C ABI)
* rdi arg0
* rsi arg1
* rdx arg2
* r10 arg3 (needs to be moved to rcx to conform to C ABI)
* r8 arg4
* r9 arg5
* (note: r12-r15, rbp, rbx are callee-preserved in C ABI)
/* The Linux/x86-64 kernel expects the system call parameters in
registers according to the following table:
syscall number rax
arg 1 rdi
arg 2 rsi
arg 3 rdx
arg 4 r10
arg 5 r8
arg 6 r9
The Linux kernel uses and destroys internally these registers:
return address from
syscall rcx
eflags from syscall r11
Normal function call, including calls to the system call stub
functions in the libc, get the first six parameters passed in
registers and the seventh parameter and later on the stack. The
register use is as follows:
system call number in the DO_CALL macro
arg 1 rdi
arg 2 rsi
arg 3 rdx
arg 4 rcx
arg 5 r8
arg 6 r9
We have to take care that the stack is aligned to 16 bytes. When
called the stack is not aligned since the return address has just
been pushed.
x86-64 ABI in section A.2.1:
syscall
instruction. the kernel destroys registers %rcx and %r11 %rdi, %rsi, %rdx, %rcx, %r8 and %r9 are the registers in order used to pass integer/pointer parameters
note that the Windows x64 function calling convention has multiple significant differences from x86-64 System V, like shadow space that must be reserved by the caller (instead of a red-zone), and call-preserved xmm6-xmm15. and very different rules for which arg goes in which register
calling conventions defines how parameters are passed in the registers when calling or being called by other program. and the best source of these convention is in the form of ABI standards defined for each these hardware. for ease of compilation, the same ABI is also used by userspace and kernel program. Linux/FreeBSD follow the same ABI for x86-64 and another set for 32-bit. but x86-64 ABI for Windows is different from Linux/FreeBSD. and generally ABI does not differentiate system call vs normal "functions calls". i.e., here is a particular example of x86_64 calling conventions and it is the same for both Linux userspace and kernel
note the sequence a,b,c,d,e,f of parameters
the "cc" clobber is unnecessary: Linux syscalls save/restore RFLAGS (the syscall/sysret instructions do that using R11, and the kernel doesn't modify the saved R11/RFLAGS other than via ptrace debugger system calls). not that it ever matters, because a "cc" clobber is implicit for x86/x86-64 in GNU C Extended asm, so you can't gain anything by leaving it out
пример передачи параметров в процедуру через регистры и создание локальных переменных в "red zone":
.code64 .globl _start .text _start: mov $5, %rdx # param 3 mov $3, %rsi # param 2 mov $2, %rdi # param 1 call myproc mov %rax, %rdi mov $60, %rax syscall myproc: # local variables in "red zone" mov $30, %rbx # mov %rbx , -8 (%rsp) # local variable 1 mov $20, %rbx # mov %rbx , -16 (%rsp) # local variable 2 mov %rdx , %rbx # access to param 3 add -16 (%rsp) , %rbx # access to local variable 2 add %rsi , %rbx # access to param 2 add -8 (%rsp) , %rbx # access to local variable 1 add %rdi , %rbx # access to param 1 mov %rbx , %rax # return value in RAX ret
i686 :
ESP points to argc
ESP + 4 указывает на argv[0]
ESP + 8 указывает на argv[1] и т.д
x86-64 :
заменить ESP на RSP
заменить шаг с 4 на 8
mov (%rsp), %rax
при запуске программы RSP указывает на вершину стека, где хранится число аргументов, переданных программе, а потом - указатели
на эти аргументы, а также указатели на переменные окружения и кое-какая другая информация. таким образом, в результате выполнения приведенной выше инструкции (разумеется, при условии, что перед ней не выполнялось каких-либо других инструкций
) в RAX будет записано количество аргументов, с которыми была запущена программа
as long as all arguments (including pointers) fit in the low 32 of a register (all symbols are known to be located in the virtual addresses in the range 0x00000000 to 0x7effffff), you can do stuff like mov $hello, %edi to get a pointer into a register. but this is not the case for position-independent executables, which many Linux distros now configure gcc to make by default (and they enable ASLR for executables)
GDB disables ASLR by default, so you always see the same address from run to run, if you run from within GDB. Linux puts the stack near the "gap" between the upper and lower ranges of canonical addresses, i.e. with the top of the stack at 2^48-1 (or somewhere random, with ASLR enabled). so RSP on entry to _start in a typical statically-linked executable is something like 0x7fffffffe550, depending on size of env vars and args. truncating this pointer to ESP does not point to any valid memory, so system calls with pointer inputs will typically return -EFAULT if you try to pass a truncated stack pointer and your program will crash if you truncate RSP to ESP and then do anything with the stack, e.g. if you built 32-bit asm source as a 64-bit executable
TODO
для двух архетектур i686 и x64 существуют разные таблицы системных вызовов!
системный вызов через 0x80 производится так: регистру eax присваивается номер системного вызова. до шести аргументов помещаются в регистры ebx, ecx, edx, esi, edi, ebp. возвращаемое значение помещается в регистр eax. значения остальных регистров при возвращении из системного вызова остаются прежними
sycall name | eax | ebx | ecx | edx | esi | edi |
---|---|---|---|---|---|---|
sys_read | 3 | unsigned int | char * | size_t | - | - |
sys_write | 4 | unsigned int | const char * | size_t | - | - |
sys_open | 5 | const char * | int | int | - | - |
sys_close | 6 | unsigned int | - | - | - | - |
sys_lseek | 19 | unsigned int | off_t | unsigned int | - | - |
------------------------------------------------------------------------- sys_open returns the file descriptor > 0 or an error number < 0 %eax 5 %ebx nullterm. file name %ecx option list %edx permission mode ------------------------------------------------------------------------- sys_read reads from the file into the buffer %eax 3 %ebx file descriptor %ecx buffer start %edx buffer size ------------------------------------------------------------------------- sys_write writes from the buffer to the file %eax 4 %ebx file descriptor %ecx buffer start %edx buffer size ------------------------------------------------------------------------- sys_lseek repositions in the given file %eax 19 %ebx file descriptor %ecx offset %edx 0 - for absolute positioning, 1 - for relative positioning ------------------------------------------------------------------------- sys_close returns the result or error number %eax 6 %ebx file descriptor -------------------------------------------------------------------------
пример копирования терминального ввода на терминальный вывод:
.bss .lcomm buffer 12 .text .global _start _start: mov $3, %eax # read mov $0, %ebx # stdin mov $buffer , %ecx # buffer address mov $12, %edx # buffer lenght int $0x80 mov $4, %eax # write mov $1, %ebx # stdout mov $buffer, %ecx # buffer address mov $12, %edx # buffer length int $0x80 mov %rax, %rdi mov $60 , %rax syscall
здесь не требуется открывать и закрывать файлы, так как это уже сделала (сделает) сама операционная система, загрузив нашу программу
другой пример - копирование из файла в файл:
.global _start .equ O_READ , 0 .equ O_WRITE , 1 .equ READ , 3 .equ WRITE , 4 .equ OPEN , 5 .equ CLOSE , 6 .bss .lcomm buffer 12 .data input_name: .ascii "foo\0" # null-terminated file name output_name: .ascii "bar\0" # null-terminated file name .text _start: # input mov $OPEN , %eax lea input_name , %ebx mov $O_READ , %ecx mov $777 , %edx # permissions int $0x80 xor %ebx , %ebx cmp %eax , %ebx # success? ja exit mov %eax , %ebx # file descriptor lea buffer , %ecx # buffer address mov $8 , %edx # buffer lenght mov $READ , %eax # read int $0x80 mov $CLOSE , %eax int $0x80 # output mov $OPEN , %eax lea output_name , %ebx mov $O_WRITE , %ecx mov $777 , %edx # permissions int $0x80 xor %ebx , %ebx cmp %eax , %ebx # success? ja exit mov %eax , %ebx # file descriptor mov $WRITE , %eax lea buffer , %ecx # buffer address mov $8 , %edx # buffer length int $0x80 mov $CLOSE , %eax int $0x80 exit: mov %rax, %rdi mov $60 , %rax syscall
parameters go into %rdi, %rsi, %rdx
----------------------------------------------------------------------------------------- %rax system call %rdi %rsi %rdx ----------------------------------------------------------------------------------------- 0 sys_read uint fd char *buf size_t count 1 sys_write uint fd const char *buf size_t count 2 sys_open const char *filename int flags int mode 3 sys_close uint fd 8 sys_lseek uint fd off_t offset uint origin -----------------------------------------------------------------------------------------
пример чтения из файла (cmd-line arg1) в другой файл (cmd-line arg2):
.code64 .global _start .data buf: .rept 31 .byte 0 .endr fd1: .long 0 fd2: .long 0 .text _start: mov (%rsp) , %rcx # num of args cmp $3 , %rcx jne exit mov 16(%rsp), %rdi # file name from command-line - the first arg mov $0, %rsi # access flag: O_READ mov $755, %rdx # permissions: doesn't matter if the file exists mov $2, %rax # sys_open syscall cmp $-4096, %rax # success? ja exit mov %rax , fd1 mov $0, %rax # sys_read mov fd1 , %rdi mov $buf , %rsi mov $18 , %rdx # count syscall mov $3, %rax # syc_close mov fd1, %rdi # file descriptor syscall mov 24(%rsp), %rdi # file name from command-line - the second arg mov $1, %rsi # access flag: O_WRITE mov $755, %rdx # permissions: doesn't matter if the file exists mov $2, %rax # sys_open syscall cmp $-4096, %rax # success? ja exit mov %rax , fd2 mov $1, %rax # sys_write mov fd2 , %rdi mov $buf , %rsi mov $18 , %rdx # count syscall mov $3, %rax # syc_close mov fd2, %rdi # file descriptor syscall exit: mov $0, %rdi # return value mov $60, %rax syscall
параметры доступа одинаковы для i686 и для x64:
отображение файла в память - это способ работы с файлами, при котором всему файлу или некоторой непрерывной его части ставится в соответствие определённый диапазон адресов оперативной памяти. и при этом чтение данных из этих адресов фактически приводит к чтению данных из отображенного файла, а запись данных по этим адресам приводит к записи этих данных в файл
преимуществом использования отображения является меньшая, по сравнению с чтением/записью, нагрузка на операционную систему — дело в том, что при использовании отображений операционная система не загружает в память сразу весь файл, а делает это по мере необходимости, блоками размером со страницу памяти (как правило, 4 килобайта). таким образом, даже имея небольшое количество физической памяти (например, 32 мегабайта), можно легко отобразить файл размером больше 32 мегабайт, и при этом для системы это не приведет к большим накладным расходам. также выигрыш происходит и при записи из памяти на диск: если вы обновили большое количество данных в памяти, они могут быть одновременно (за один проход головки над диском) записаны на диск
использование отображений чревато замедлениями из-за страничных ошибок доступа. допустим, страница, относящаяся к нужному файлу, уже лежит в кэше, но не ассоциирована с данным отображением. если она была изменена другим процессом, то попытка ассоциировать её с отображением может закончиться неудачей и привести к необходимости повторно зачитывать данные с диска либо сохранять данные на диск. таким образом, хотя программа и делает меньше операций для доступа через отображение, в реальности операция записи данных в какое-то место файла может занять больше времени, чем с использованием операций файлового ввода-вывода (при том, что в среднем использование отображений даёт выигрыш)
большинство современных операционных систем или оболочек поддерживает те или иные формы работы с файлами, отображенными на память. например, функция mmap(), создающая отображение для файла с данным дескриптором, начиная с некоторого места в файле и с некоторой длиной, является частью спецификации POSIX. таким образом, огромное количество POSIX-совместимых систем, таких как UNIX, Linux, FreeBSD, MacOS или OpenVMS, поддерживает общий механизм отображения файлов
%rax System call %rdi %rsi %rdx %r10 %r8 %r9 ----------------------------------------------------------------------------------------- 9 sys_mmap ulong addr ulong len ulong prot ulong flags ulong fd ulong off 11 sys_munmap ulong addr size_t len -----------------------------------------------------------------------------------------
syscalls:
mmap - allocate memory, or map files or devices into memory
munmap - remove a mapping for the specified address range
mov $345 , %ebx # return value in EBX mov $1 , %eax # sys_exit int $0x80
int 0x80 works when used correctly, as long as any pointers fit in 32 bits (stack pointers don't fit). also, strace decodes it wrong, decoding register contents as if it was the 64-bit syscall ABI.
int 0x80 zeros R8-R11, and preserves everything else. use it exactly like you would in 32-bit code, with the 32-bit call numbers. not all systems even support int 0x80 : Windows Subsystem for Linux (WSL) is strictly 64-bit only and int 0x80 doesn't work at all. it's also possible to build Linux kernels without IA-32 emulation either (no support for 32-bit executables, no support for 32-bit system calls)
only the low 32 bits of arg registers are passed. the upper halves of rbx-rbp are preserved, but ignored by int 0x80 system calls. note that passing a bad pointer to a system call doesn't result in SIGSEGV; instead the system call returns -EFAULT. if you don't check error return values (with a debugger or tracing tool), it will appear to silently fail
the return value is sign-extended to fill 64-bit RAX (Linux declares 32-bit sys_ functions as returning signed long). this means that pointer return values need to be zero-extended before use in 64-bit addressing modes
mov $683 , %rdi # return value mov $60 , %rax # sys_exit syscall
компилируем командой:
$> as -gstabs myprog.s -o myprog.o
-g флаг отладки
“stabs” формат используемый GDB
собираем исполняемый файл командой:
$> ld -o myprogname myprog.o
запускаем отладчик командой:
$> gdb ./myprogname
q - quit
l - вывод листинга
i register - отобразить регистры
r - выполнить программу
r arg1 arg2 - запустить программу с аргументами командной строки args arg1, arg2
set args arg1 arg2 - установить аргументы командной строки arg1, arg2
show args - показать текущие аргументы командной строки
s - выполнить одну инструкцию “step into”
n - “step over” (не входить в тела функций)
c - продолжить исполнение
k - завершить исполнение
fin - “step out” (идет в точку после вызова данной функции
disp $eax - отобразить регистр EAX
p/f $reg - показать содержимое регистра по формату f: t(bin), x(hex), u(udecimal), o(oct), a(address), c(char), f(float)
p/h $eax - отобразить в шестнадцатеричной
p/d $eax - отобразить в десятичной
p/t $eax - отобразить в двоичной
wa $eax - отобразить лишь при изменении
i wat - показать список вотчей
i b - показать список бряков
b - установка бряков
b linenum - поставить бряк на строке листинга с номером
b *addr - поставить бряк на адресе в памяти
b fan - поставить бряк на входе функции fan
condition bpnum expr - условный бряк (if expression expr is non-zero)
d b1 b2 - удалить бряки b1, b2 (или все - если нет спецификации)
clear *addr - очистить бряк на памяти
clear fn - очистить бряк на функции fn (или - текущей, если нет спецификации)
clear linenum - очистить бряк на строке листинга
disable b1 b2 - блокировать бряки b1, b2 (или все - если нет спецификации)
enable b1 b2 - разблокировать бряки b1, b2 (или все - если нет спецификации)
x/rsf addr - показать содержимое памяти ; repeat count r, size s, format f
size is b (byte), h (halfword), w (word), g (double word)
format is the same as for print, with the additions of s (string) and i (instruction)
i disp - показать список выражений для просмотра
display/f $reg - показать регистр при каждом бряке : format f
display/si addr - показать память при каждом бряке размера i
display/sn addr - показать память как строку размера n
undisplay num - удалить из списка дисплеев
where - показать стек вызовов
backtrace - показать стек вызовов
frame - показать текущий фрейм стека
up - move the context toward the bottom of the call stack
down - move the context toward the top of the call stack
(gdb) b _start set output-radix 16 set output-radix 10 set disassembly intel set language asm disassemble
---
список бряков: info b
список вотчей: info wa
дамп памяти:
x/16xb some_var
x/32uh some_var
x/64dw some_var
выводится дамп (1) 16-и байт с выводом в hex, (2) 32-х полуслов, которые выводятся, как числа без знака и (3) 64-х слов, которые выводятся, как числа со знаком
прочитать память по адресу 0x30(%rsp,%rdx,4)
x/d $rsp + $rdx * 4 + 0x30
p/x &memorylabel
где memorylable - это метка в .data секции
---
(gdb) r Starting program: /home/user/asm/src/a.outBreakpoint 1, _start () at a.s:8 8 movb nums , %al # multiply two bytes (gdb) x/16ux &nums 0x402000: 0x000064c8 0x00000001 0x000b0000 0x00000005 0x402010: 0x00000001 0x00000064 0x00401000 0x00000000 0x402020: 0x00080044 0x00401000 0x00000000 0x00090044 0x402030: 0x00401007 0x00000000 0x000a0044 0x0040100e (gdb) x/2bx &nums 0x402000: 0xc8 0x64
посмотреть следующие пять ассемблерных инструкций: x/5i $pc
$ as -o a.o a.s $ ld -o aaa a.o $ strace ./aaa execve("./aaa", ["./aaa"], 0x7ffeb0fdfa50 /* 69 vars */) = 0 exit(1572900) = ? +++ exited with 36 +++
.global _start .text _start: asm_main_after_prologue: /* write */ mov $1, %rax /* syscall number */ mov $1, %rdi /* stdout */ mov $msg, %rsi /* buffer */ mov $len, %rdx /* len */ syscall /* exit */ mov $60, %rax /* syscall number */ mov $0, %rdi /* exit status */ syscall .data msg: .ascii "hello\n" len = . - msg
$> as -o a.o hello_99.s $> ld -o a.out a.o $> objdump -d a.o a.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 <_start>: 0: 48 c7 c0 01 00 00 00 mov $0x1,%rax 7: 48 c7 c7 01 00 00 00 mov $0x1,%rdi e: 48 c7 c6 00 00 00 00 mov $0x0,%rsi 15: 48 c7 c2 06 00 00 00 mov $0x6,%rdx 1c: 0f 05 syscall 1e: 48 c7 c0 3c 00 00 00 mov $0x3c,%rax 25: 48 c7 c7 00 00 00 00 mov $0x0,%rdi 2c: 0f 05 syscall
this program performs a similar function to objdump but it goes into more detail and it exists independently of the BFD library
-h displays the information contained in the ELF header at the start of the file -l displays the information contained in the file's segment headers, if it has any