Что произойдет, если вы используете 32-битный int 0x80 Linux ABI в 64-битном коде?

int 0x80 в Linux всегда вызывает 32-битный ABI, независимо от того, из какого режима он вызывается: args in ebx, ecx,... и системные номера из /usr/include/asm/unistd_32.h, (Или вылетает на 64-битных ядрах, скомпилированных без CONFIG_IA32_EMULATION).

64-битный код должен использовать syscall, с номерами звонков от /usr/include/asm/unistd_64.h и аргументы в rdi, rsi и т.д. См. Каковы соглашения о вызовах для системных вызовов UNIX и Linux на i386 и x86-64. Если ваш вопрос был помечен как дубликат этого, просмотрите эту ссылку, чтобы узнать, как выполнять системные вызовы в 32- или 64-битном коде. Если вы хотите понять, что именно произошло, продолжайте читать.


syscall системные вызовы быстрее, чем int 0x80 системные вызовы, поэтому используйте родной 64-битный syscall если вы не пишете машинный код полиглота, который запускается одинаково при выполнении как 32- или 64-битный. (sysenter всегда возвращается в 32-битном режиме, так что это бесполезно из 64-битного пространства пользователя, хотя это действительная инструкция x86-64.)

Связанный: Полное руководство по системным вызовам Linux (на x86) о том, как сделать int 0x80 или же sysenter 32-разрядные системные вызовы или syscall 64-битные системные вызовы или вызов vDSO для "виртуальных" системных вызовов, таких как gettimeofday, Плюс справочная информация о том, что системные вызовы все о.


С помощью int 0x80 позволяет писать что-то, что будет собираться в 32- или 64-битном режиме, так что это удобно для exit_group() в конце микробенчмарка или что-то.

Текущие PDF-файлы официальных документов psABI для i386 и x86-64 System V, которые стандартизируют соглашения о вызовах функций и системных вызовов, приведены по https://github.com/hjl-tools/x86-psABI/wiki/X86-psABI.

См. Вики-теги x86 для начинающих, руководств по x86, официальной документации и руководств / ресурсов по оптимизации производительности.


Но так как люди продолжают публиковать вопросы с кодом, который использует int 0x80 в 64-битном коде или в случайном создании 64-битных двоичных файлов из исходного кода, написанного для 32-битных, мне интересно, что именно происходит в нынешнем Linux?

Есть ли int 0x80 сохранить / восстановить все 64-битные регистры? Обрезает ли он какие-либо регистры до 32-битного? Что произойдет, если вы передадите аргументы-указатели с ненулевыми верхними половинами?

Работает ли это, если вы передаете 32-битные указатели?

1 ответ

Решение

TL: DR: int 0x80 работает при правильном использовании до тех пор, пока любые указатели помещаются в 32 бита (указатели стека не подходят). Также, strace декодирует его неправильно, декодируя содержимое регистра, как если бы оно было 64-битным syscall ABI.

int 0x80 нули R8-R11, и сохраняет все остальное. Используйте его точно так же, как в 32-битном коде, с 32-битными номерами вызовов. (Или лучше, не используйте его!)

Не все системы даже поддерживают int 0x80: Подсистема Windows Ubuntu строго 64-битная: int 0x80 не работает вообще. Также возможно собирать ядра Linux без эмуляции IA-32. (Нет поддержки 32-битных исполняемых файлов, нет поддержки 32-битных системных вызовов).


Подробности: что сохранено / восстановлено, какие части которого использует ядро ​​regs

int 0x80 использования eax (не полный rax) в качестве номера системного вызова, отправляя в ту же таблицу указателей функций, что и 32-разрядное пространство пользователя int 0x80 использует. (Эти указатели должны sys_whatever реализации или оболочки для встроенной 64-битной реализации внутри ядра. Системные вызовы - это действительно вызовы функций через границу пользователя / ядра.)

Передаются только младшие 32 бита регистров arg. Верхние половины rbx - rbp сохраняются, но игнорируются int 0x80 системные вызовы. Обратите внимание, что передача неверного указателя на системный вызов не приводит к SIGSEGV; вместо этого системный вызов возвращает -EFAULT, Если вы не проверяете возвращаемые значения ошибок (с помощью отладчика или инструмента трассировки), он будет автоматически завершаться с ошибкой.

Все регистры (кроме, конечно, eax) сохраняются / восстанавливаются (включая RFLAGS и верхние 32 целочисленных регистров), за исключением того, что r8-r11 обнуляются. r12-r15 сохраняются в вызове в соглашении о вызовах функций SysV ABI x86-64, поэтому регистры, которые обнуляются int 0x80 в 64-битном - подмножество вызовов "новых" регистров, добавленных AMD64.

Это поведение было сохранено после некоторых внутренних изменений в том, как сохранение регистров было реализовано внутри ядра, и в комментариях к ядру упоминается, что его можно использовать из 64-битной версии, поэтому этот ABI, вероятно, стабилен. (То есть вы можете рассчитывать на обнуление r8-r11 и сохранение всего остального.)

Возвращаемое значение расширяется до 64-битного rax, (Linux объявляет 32-битные функции sys_ возвращаемыми со знаком long.) Это означает, что указатель возвращает значения (например, из void *mmap()) необходимо расширить до нуля перед использованием в 64-битных режимах адресации

В отличие от sysenter сохраняет первоначальное значение cs, поэтому он возвращается в пользовательское пространство в том же режиме, в котором был вызван. (Использование sysenter результаты в настройках ядра cs в $__USER32_CS, который выбирает дескриптор для 32-битного сегмента кода.)


strace декодирует int 0x80 неправильно для 64-битных процессов. Он декодирует, как если бы процесс использовал syscall вместо int 0x80, Это может быть очень запутанным. например, с strace печать write(0, NULL, 12 <unfinished ... exit status 1> за eax=1 / int $0x80 что на самом деле _exit(ebx) не write(rdi, rsi, rdx),


int 0x80 работает до тех пор, пока все аргументы (включая указатели) помещаются в младшие 32 регистра. Это касается статического кода и данных в модели кода по умолчанию ("small") в x86-64 SysV ABI. (Раздел 3.5.1: известно, что все символы расположены в виртуальных адресах в диапазоне 0x00000000 в 0x7effffff так что вы можете делать такие вещи, как mov edi, hello (AT & T mov $hello, %edi) чтобы получить указатель в регистр с 5-байтовой инструкцией).

Но это не относится к позиционно-независимым исполняемым файлам, которые сейчас настраивают многие дистрибутивы Linux. gcc сделать по умолчанию (и они включают ASLR для исполняемых файлов). Например, я составил hello.c на Arch Linux и установите точку останова в начале main. Строковая константа, переданная puts был в 0x555555554724 Итак, 32-битный ABI write Системный вызов не будет работать. (GDB по умолчанию отключает ASLR, поэтому вы всегда видите один и тот же адрес от запуска к запуску, если вы запускаете его из GDB.)

Linux помещает стек около "промежутка" между верхним и нижним диапазоном канонических адресов, то есть с вершиной стека в 2^48-1. (Или где-то случайно, с включенным ASLR). Так rsp при въезде в _start в типичном статически связанном исполняемом файле что-то вроде 0x7fffffffe550 в зависимости от размера env vars и args. Усечение этого указателя до esp не указывает на допустимую память, поэтому системные вызовы с указателями ввода обычно возвращают -EFAULT если вы попытаетесь передать усеченный указатель стека. (И ваша программа потерпит крах, если вы урежете rsp в esp и затем делать что-либо со стеком, например, если вы создали 32-битный источник asm как 64-битный исполняемый файл.)


Как это работает в ядре:

В исходном коде Linux arch/x86/entry/entry_64_compat.S определяет ENTRY(entry_INT80_compat), И 32-разрядные, и 64-разрядные процессы используют одну и ту же точку входа при выполнении. int 0x80,

entry_64.S is определяет собственные точки входа для 64-битного ядра, которое включает в себя обработчики прерываний / сбоев и syscall родные системные вызовы из процессов длинного режима (он же 64-битный режим).

entry_64_compat.S определяет точки входа системного вызова из режима Compat в 64-битное ядро, а также особый случай int 0x80 в 64-битном процессе. (sysenter в 64-битном процессе может также перейти к этой точке входа, но это толкает $__USER32_CS, поэтому он всегда будет возвращаться в 32-битном режиме.) Существует 32-битная версия syscall инструкция, поддерживаемая на процессорах AMD, и Linux также поддерживает ее для быстрых 32-битных системных вызовов из 32-битных процессов.

Я предполагаю возможный вариант использования int 0x80 в 64-битном режиме, если вы хотите использовать пользовательский дескриптор сегмента кода, который вы установили с modify_ldt, int 0x80 толкает сегментные регистры для использования с iret и Linux всегда возвращается из int 0x80 системные звонки через iret, 64-битный syscall наборы точек входа pt_regs->cs а также ->ss постоянным, __USER_CS а также __USER_DS, (Это нормально, что SS и DS используют одни и те же дескрипторы сегмента. Различия в разрешениях выполняются с помощью подкачки, а не сегментации.)

entry_32.S определяет точки входа в 32-битное ядро ​​и не участвует вообще.

int 0x80 точка входа в Linux 4.12 entry_64_compat.S:

/*
 * 32-bit legacy system call entry.
 *
 * 32-bit x86 Linux system calls traditionally used the INT $0x80
 * instruction.  INT $0x80 lands here.
 *
 * This entry point can be used by 32-bit and 64-bit programs to perform
 * 32-bit system calls.  Instances of INT $0x80 can be found inline in
 * various programs and libraries.  It is also used by the vDSO's
 * __kernel_vsyscall fallback for hardware that doesn't support a faster
 * entry method.  Restarted 32-bit system calls also fall back to INT
 * $0x80 regardless of what instruction was originally used to do the
 * system call.
 *
 * This is considered a slow path.  It is not used by most libc
 * implementations on modern hardware except during process startup.
 ...
 */
 ENTRY(entry_INT80_compat)
 ...  (see the github URL for the full source)

Код ноль расширяет eax в rax, затем помещает все регистры в стек ядра, чтобы сформировать struct pt_regs, Это где он будет восстанавливать, когда системный вызов возвращается. Это стандартная схема для сохраненных регистров пространства пользователя (для любой точки входа), поэтому ptrace из другого процесса (например, GDB или strace) будет читать и / или записывать эту память, если они используют ptrace пока этот процесс находится внутри системного вызова. (ptrace модификация регистров - это одна вещь, которая усложняет пути возврата для других точек входа. Смотрите комментарии.)

Но это толкает $0 вместо r8/r9/r10/r11. (sysenter и AMD syscall32 точки входа в магазин нули для r8-r15.)

Я думаю, что обнуление r8-r11 соответствует историческому поведению. Перед установкой полного pt_regs для всех фиксаций системных вызовов, точка входа сохраняла только регистры с C-clobbered. Отправляется прямо из asm с call *ia32_sys_call_table(, %rax, 8) и эти функции следуют соглашению о вызовах, поэтому они сохраняют rbx, rbp, rsp, а также r12-r15, обнуление r8-r11 вместо того, чтобы оставлять их неопределенными, был, вероятно, способ избежать утечки информации из ядра. ИДК, как это обрабатывается ptrace если единственная копия сохраненных вызовом регистров пользовательского пространства была в стеке ядра, где функция C сохранила их. Я сомневаюсь, что он использовал метаданные для размотки стека, чтобы найти их там.

Текущая реализация (Linux 4.12) отправляет системные вызовы 32-битного ABI из C, перезагружая сохраненные ebx, ecx и т. д. из pt_regs, (64-битные системные системные вызовы отправляются непосредственно из asm, только с mov %r10, %rcx необходимо учитывать небольшую разницу в соглашении о вызовах между функциями и syscall, К сожалению, это не всегда можно использовать sysret, потому что ошибки процессора делают его небезопасным с неканоническими адресами. Он пытается, так что быстрый путь чертовски быстр, хотя syscall Сам по-прежнему занимает десятки циклов.)

Во всяком случае, в нынешнем Linux 32-битные системные вызовы (включая int 0x80 из 64-битной) в конечном итоге в конечном итоге do_syscall_32_irqs_on(struct pt_regs *regs), Он отправляет указатель на функцию ia32_sys_call_table, с 6 расширенными нулями аргументами. Это, возможно, позволяет избежать необходимости в обертке вокруг 64-битной собственной функции syscall в большем числе случаев, чтобы сохранить это поведение, поэтому большая часть ia32 Записи таблицы могут быть непосредственно реализацией системного вызова.

Linux 4.12 arch/x86/entry/common.c

if (likely(nr < IA32_NR_syscalls)) {
  /*
   * It's possible that a 32-bit syscall implementation
   * takes a 64-bit parameter but nonetheless assumes that
   * the high bits are zero.  Make sure we zero-extend all
   * of the args.
   */
  regs->ax = ia32_sys_call_table[nr](
      (unsigned int)regs->bx, (unsigned int)regs->cx,
      (unsigned int)regs->dx, (unsigned int)regs->si,
      (unsigned int)regs->di, (unsigned int)regs->bp);
}

syscall_return_slowpath(regs);

В более старых версиях Linux, которые отправляют 32-битные системные вызовы из asm (как это делает 64-битная версия), точка входа int80 сама помещает аргументы в правильные регистры с помощью mov а также xchg инструкции, использующие 32-битные регистры. Он даже использует mov %edx,%edx обнулить EDX в RDX (потому что в обоих соглашениях arg3 используется один и тот же регистр). код здесь. Этот код дублируется в sysenter а также syscall32 точки входа


Простой пример / тестовая программа:

Я написал простой Hello World (в синтаксисе NASM), в котором все регистры имеют ненулевые верхние половины, а затем два write() системные вызовы с int 0x80 один с указателем на строку в .rodata (успешно), второй с указателем на стек (не с -EFAULT).

Тогда он использует родной 64-битный syscall ABI для write() символы из стека (64-битный указатель) и снова для выхода.

Таким образом, все эти примеры используют ABI правильно, за исключением 2-го int 0x80 который пытается передать 64-битный указатель и урезает его.

Если вы построите его как независимый от позиции исполняемый файл, первый тоже не получится. (Вы должны использовать RIP-родственник lea вместо mov получить адрес hello: в реестр.)

Я использовал GDB, но использую любой отладчик, который вы предпочитаете. Используйте тот, который выделяет измененные регистры с момента последнего пошагового выполнения. gdbgui хорошо работает для отладки исходного кода asm, но не подходит для разборки. Тем не менее, у него есть панель регистров, которая хорошо работает, по крайней мере, для целочисленных регистров, и она прекрасно работала в этом примере.

Смотрите в строке ;;; комментарии, описывающие, как регистр изменяется системными вызовами

global _start
_start:
    mov  rax, 0x123456789abcdef
    mov  rbx, rax
    mov  rcx, rax
    mov  rdx, rax
    mov  rsi, rax
    mov  rdi, rax
    mov  rbp, rax
    mov  r8, rax
    mov  r9, rax
    mov  r10, rax
    mov  r11, rax
    mov  r12, rax
    mov  r13, rax
    mov  r14, rax
    mov  r15, rax

    ;; 32-bit ABI
    mov  rax, 0xffffffff00000004          ; high garbage + __NR_write (unistd_32.h)
    mov  rbx, 0xffffffff00000001          ; high garbage + fd=1
    mov  rcx, 0xffffffff00000000 + .hello
    mov  rdx, 0xffffffff00000000 + .hellolen
    ;std
after_setup:       ; set a breakpoint here
    int  0x80                   ; write(1, hello, hellolen);   32-bit ABI
    ;; succeeds, writing to stdout
;;; changes to registers:   r8-r11 = 0.  rax=14 = return value

    ; ebx still = 1 = STDOUT_FILENO
    push 'bye' + (0xa<<(3*8))
    mov  rcx, rsp               ; rcx = 64-bit pointer that won't work if truncated
    mov  edx, 4
    mov  eax, 4                 ; __NR_write (unistd_32.h)
    int  0x80                   ; write(ebx=1, ecx=truncated pointer,  edx=4);  32-bit
    ;; fails, nothing printed
;;; changes to registers: rax=-14 = -EFAULT  (from /usr/include/asm-generic/errno-base.h)

    mov  r10, rax               ; save return value as exit status
    mov  r8, r15
    mov  r9, r15
    mov  r11, r15               ; make these regs non-zero again

    ;; 64-bit ABI
    mov  eax, 1                 ; __NR_write (unistd_64.h)
    mov  edi, 1
    mov  rsi, rsp
    mov  edx, 4
    syscall                     ; write(edi=1, rsi='bye\n' on the stack,  rdx=4);  64-bit
    ;; succeeds: writes to stdout and returns 4 in rax
;;; changes to registers: rax=4 = length return value
;;; rcx = 0x400112 = RIP.   r11 = 0x302 = eflags with an extra bit set.
;;; (This is not a coincidence, it's how sysret works.  But don't depend on it, since iret could leave something else)

    mov  edi, r10d
    ;xor  edi,edi
    mov  eax, 60                ; __NR_exit (unistd_64.h)
    syscall                     ; _exit(edi = first int 0x80 result);  64-bit
    ;; succeeds, exit status = low byte of first int 0x80 result = 14

section .rodata
_start.hello:    db "Hello World!", 0xa, 0
_start.hellolen  equ   $ - _start.hello

Соберите его в 64-битный статический двоичный файл с

yasm -felf64 -Worphan-labels -gdwarf2 abi32-from-64.asm
ld -o abi32-from-64 abi32-from-64.o

Бежать gdb ./abi32-from-64, В gdb, бежать set disassembly-flavor intel а также layout reg если у вас нет этого в вашем ~/.gdbinit уже. (GAS .intel_syntax это как MASM, а не NASM, но они достаточно близки, чтобы их было легко прочитать, если вам нравится синтаксис NASM.)

(gdb)  set disassembly-flavor intel
(gdb)  layout reg
(gdb)  b  after_setup
(gdb)  r
(gdb)  si                     # step instruction
    press return to repeat the last command, keep stepping

Нажмите control-L, когда режим TUI GDB испортится. Это происходит легко, даже когда программы не печатают на себя.

Другие вопросы по тегам