Что произойдет, если вы используете 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.12entry_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 испортится. Это происходит легко, даже когда программы не печатают на себя.