Ответ 1
TL: DR: int 0x80
работает при правильном использовании, если любые указатели умещаются в 32 бита (указатели стека не умещаются). Кроме того, strace
неправильно его декодирует, декодируя содержимое регистра, как если бы это был 64-битный syscall
ABI. (Там нет никакого простого/надежного способа для strace
сказать, пока.)
int 0x80
нули r8-r11 и сохраняет все остальное. Используйте его точно так же, как в 32-битном коде, с 32-битными номерами вызовов. (Или лучше, не используйте его!)
Не все системы поддерживают даже int 0x80
: подсистема Windows Ubuntu строго 64-разрядная: int 0x80
вообще не работает. Также возможно собрать ядра Linux без эмуляции IA-32. (Нет поддержки 32-битных исполняемых файлов, нет поддержки 32-битных системных вызовов).
Подробности: что сохранено/восстановлено, какие части каких рег использует ядро
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 и аргументов. Усечение этого указателя до 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
это определяет собственные точки входа для 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
входа 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
входа sysenter
и AMD syscall32
хранят нули для r8-r15.)
Я думаю, что обнуление r8-r11 соответствует историческому поведению. Перед установкой полного pt_regs для всех фиксаций системных вызовов, точка входа сохраняла только регистры с C-clobbered. Он отправляется непосредственно из ассемблера с call *ia32_sys_call_table(, %rax, 8)
, и эти функции следует соглашению о вызовах, поэтому они сохраняют rbx
, rbp
, rsp
и r12-r15
. Обнуление r8-r11
вместо того, чтобы оставлять их неопределенными, было, вероятно, способом избежать утечки информации из ядра. IDK, как он обрабатывает 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
таблице ia32
может быть напрямую реализовано в собственной реализации системного вызова.
Linux 4.12
arch/x86/entry/common.c
if (likely(nr < IA32_NR_syscalls)) { /* * It 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 сама помещает аргументы в нужные регистры с xchg
команд mov
и xchg
, используя 32-битные регистры. Он даже использует mov %edx,%edx
для расширения нуля EDX в RDX (поскольку в обоих соглашениях arg3 используется один и тот же регистр). код здесь. Этот код дублируется в sysenter
входа sysenter
и syscall32
.
Простой пример/тестовая программа:
Я написал простой Hello World (в синтаксисе NASM), в котором все регистры имеют ненулевую верхнюю половину, затем выполняет два системных вызова write()
с int 0x80
, один с указателем на строку в .rodata
(успешно), второй с указателем на стек (завершается с -EFAULT
).
Затем он использует собственный 64-разрядный ABI syscall
для write()
символов из стека (64-разрядный указатель) и снова для выхода.
Таким образом, все эти примеры используют ABI правильно, за исключением 2-го типа int 0x80
который пытается передать 64-битный указатель и урезает его.
Если вы построите его как независимый от позиции исполняемый файл, первый тоже не получится. (Чтобы получить адрес hello:
в регистр, вы должны использовать RIP-родственник lea
вместо mov
.)
Я использовал 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 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
запустите ~/.gdbinit
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, когда режим GDB TUI испортится. Это происходит легко, даже когда программы не печатают сами.