assembler : Intel , AT&T , Linux , GNU as


  • разделы программы
  • регистры общего назначения
  • операции
  • префиксы операндов
  • суффиксы операторов
  • директивы распределения памяти программы
  • типы адресации
  • счетчкики
  • флаги
  • стек
  • регистр инструкций
  • инструкции перехода
  • условные переходы
  • циклы
  • битовые операции
  • сложение
  • вычитание
  • умножение со знаком
  • деление со знаком
  • сложение с переносом
  • вычитание с заимствованием
  • строковые операции
  • процедуры
  • выход из программы
  • файловый ввод/вывод
  • параметры командной строки
  • отображение файла в память
  • GDB отладка
  • strace отладка
  • objdump

    разделы программы

    простейший шаблон программы:

    .code64
    .global _start
    
    
    .data
    
        #  some data will be placed here
    
    
    .text
    
      _start :
    
        #  code of instructions will be placed here
    
        mov $60 , %rax
        syscall

    первая директива указывает для какой архитектуры написан код. возможные варианты:

    сама программа состоит из разделов, начало которых указывается директивами

    любая программа должна иметь глобальную метку _start - точку входа. директива .global указывает загрузчику на эту точку

    комментарии начинаются со знака # и идут до конца строки

    последние две инструкции осуществляют корректный выход из программы

    компиляция с отладочной инофрмацией :

      $> as -o q.o -gstabs q.s
      $> ld -o q.out q.o
    

    регистры общего назначения

    забудьте о размышлениях в терминах переменных языков высокого уровная - с ассемблером вместо переменных вы используете регистры. каждый бит данных с которым вы манипулируете должен попасть в CPU и простейшая инструкция X - 4 превращается в три инструкции псевдокода:
        mov    [OSP] , TMP        #  load the variable into a register
        sub    $4 ,    TMP        #  do the actual computation
        mov    TMP , [OSP]        #  store the result to memory
      
    где TMP - это имя регистра, а OSP - имя базового регистра

    в 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

    а команда

        movw    mymem+4 ,  %eax 
    загрузит третье и четвертое слова в регистр EAX и его значение eax = 0x00050003 (третье слово попадет в AL а четвертое - в AH. перед загрузкой можно очистить регистр EAX целиком
          xor %eax, %eax 
    а потом читать только содержимое регистра AX

    другие возможности:

        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
    
    можно не указывать регистр со смещением или вообще какие-либо регистры, а только смещение. в результате выполнения этого кода в регистры AL и AH будет записан ASCII-код буквы h, или 0x48

    еще одна полезная инструкция 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 (хотя сам стек по размеру становится меньше)

    red zone

    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
    
    в результате: ZF = 0 , CF = 1
         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)
    
    сравнить RAX с заданным куском памяти. если равны, выставить ZF и сохранить по указанному адресу значение указанного регистра, в данном примере RCX. иначе очистить ZF и загрузить значение из памяти в RAX. также оба операнда могут быть регистрами

       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 на единицу, и если после этого
    rcx != 0
    осуществляет переход на заданную метку

    инструкции 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
    
    в регистры rsi и rdi кладутся адреса двух строк

    командой 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 или, если его размера не достаточно, записывается в структуру, адрес которой передается в качестве аргумента

    соглашений о вызовах существует множество:

    1. аргументы передаются через регистры
    2. аргументы передаются через стек и за очистку стека от аргументов отвечает процедура
    3. аргументы передаются через стек, а за очистку стека от аргументов отвечает вызывающая сторона
    4. множество вариантов посередине

    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
    
    because there is no stack frame (%rbp is used for other purposes, and its value is never pushed to stack), each function call pushes only the return address to the stack, which is an 8-byte quantity, so we need to subtract 8 from %rsp to keep it a multiple of 16 (in general, the value subtracted from and added to %rsp is an odd multiple of 8)

    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

    i686

          * 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
    

    x64

         * 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:

    %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
    


    параметры командной строки

    точка входа ELF (a.k.a. _start) на x86 Linux executable

    i686 :
    ESP points to argc
    ESP + 4 указывает на argv[0]
    ESP + 8 указывает на argv[1] и т.д

    x86-64 :
    заменить ESP на RSP
    заменить шаг с 4 на 8

    mov (%rsp), %rax
    при запуске программы RSP указывает на вершину стека, где хранится число аргументов, переданных программе, а потом - указатели на эти аргументы, а также указатели на переменные окружения и кое-какая другая информация. таким образом, в результате выполнения приведенной выше инструкции (разумеется, при условии, что перед ней не выполнялось каких-либо других инструкций ) в RAX будет записано количество аргументов, с которыми была запущена программа

    i686

    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

    x64

    TODO


    файловый ввод/вывод

    для двух архетектур i686 и x64 существуют разные таблицы системных вызовов!

    i686

    системный вызов через 0x80 производится так: регистру eax присваивается номер системного вызова. до шести аргументов помещаются в регистры ebx, ecx, edx, esi, edi, ebp. возвращаемое значение помещается в регистр eax. значения остальных регистров при возвращении из системного вызова остаются прежними

    sycall nameeaxebxecxedxesiedi
    sys_read3 unsigned intchar *size_t--
    sys_write 4unsigned intconst char *size_t--
    sys_open 5const char *intint--
    sys_close 6unsigned int----
    sys_lseek19unsigned intoff_tunsigned 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
    

    x64

    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, поддерживает общий механизм отображения файлов

    x64

    %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


    выход из программы

    i686

       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

    x64

      mov $683 ,  %rdi     # return value
      mov $60  ,  %rax     # sys_exit
      syscall
    

    GDB отладка

    компилируем командой:

     $> 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.out

    Breakpoint 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


    strace

    $ 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 +++
    
    в таком простом варианте мы видим код возврата программы aaa, а именно - 36. еще мы видим, что программа осуществила всего лишь один системный вызов, а именно - exit


    objdump

    .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 
    

    readelf

    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