Que se passe-t-il si vous utilisez le 32-bit int 0x80 Linux ABI en code 64 bits?
int 0x80
sur Linux invoque toujours L'ABI 32 bits, quel que soit le mode d'où il est appelé: args dans ebx
, ecx
,... et les numéros de syscall de /usr/include/asm/unistd_32.h
. (Ou s'écrase sur des noyaux 64 bits compilés sans CONFIG_IA32_EMULATION
).
le code 64 bits doit utiliser syscall
, avec les numéros d'appel de /usr/include/asm/unistd_64.h
, et args dans rdi
, rsi
, etc. Voir quelles sont les conventions d'appel pour Le système UNIX & Linux fait appel à i386 et x86-64 . Si votre question a été marquée un duplicata de cela, voir ce lien pour des détails sur la façon dont vous devrait faire des appels système en code 32 ou 64 bits. Si vous voulez comprendre ce qui s'est exactement passé, continuez à lire.
syscall
les appels système sont plus rapides que les appels système int 0x80
, donc utilisez natif 64 bits syscall
à moins que vous n'écriviez code machine polyglotte qui fonctionne de la même façon lorsqu'il est exécuté en 32 ou 64 bits. ( sysenter
retourne toujours en mode 32 bits, donc ce n'est pas utile dans l'espace utilisateur 64 bits, bien qu'il s'agisse d'une instruction x86-64 valide.)
Related: The Definitive Guide to Linux Appels Système (sur x86) comment faire int 0x80
ou sysenter
système 32 bits appels, ou syscall
64 bits du système d'appels, ou en appelant le vDSO de "virtuel" appels système comme gettimeofday
. Plus le Contexte sur ce que les appels système sont tout au sujet.
L'utilisation de int 0x80
permet d'écrire quelque chose qui assemblera en mode 32 ou 64 bits, il est donc pratique pour un exit_group()
à la fin d'un microbenchmark ou quelque chose.
PDFs actuels du système officiel i386 et x86-64 psABI documents qui normalisent la fonction et les conventions d'appel syscall sont liés à partir de https://github.com/hjl-tools/x86-psABI/wiki/X86-psABI .
voir le x86 wiki d'étiquette pour les guides de débutant, les manuels x86, la documentation officielle et les guides / ressources d'optimisation de performance.
mais depuis que les gens postent des questions avec le code qui utilise int 0x80
dans le code 64 bits , ou accidentellement bâtiment binaires 64 bits d'après une source écrite pour 32 bits, je me demande que exactement se passe-t-il sur Linux actuel?
Ne int 0x80
sauver/restaurer tous les registres 64 bits? Est-ce qu'il tronque tous les registres à 32 bits? Que se passe-t-il si vous passez args pointer qui ont la moitié supérieure non-zéro?
Ne fonctionnera pas si vous passer des pointeurs 32 bits?
1 réponses
TL:DR : int 0x80
fonctionne lorsqu'il est utilisé correctement, aussi longtemps que n'importe quels pointeurs s'adaptent en 32 bits ( les pointeurs de pile ne s'adaptent pas ). En outre, strace
le décode mal , en décodant le contenu du Registre comme si c'était le 64-bit syscall
ABI.
int 0x80
zéros r8-r11, et préserve tout le reste. Utilisez-le exactement comme vous le feriez en code 32 bits, avec les numéros d'appel 32 bits. (Ou mieux, ne l'utilisez pas!)
tous les systèmes ne prennent même pas en charge int 0x80
: le sous-système Ubuntu de Windows est strictement 64 bits seulement: int 0x80
ne fonctionne pas du tout . Il est également possible de construire des noyaux Linux sans émulation IA-32 non plus. (Aucun support pour les exécutables 32 bits, aucun support pour les appels système 32 bits).
les détails: ce qui est sauvé/restauré, quelles parties du noyau regs utilise
int 0x80
utilise eax
(pas le entier rax
) comme numéro d'appel système, expédiant à la même table de fonction-pointeurs que l'utilisateur-espace de 32 bits int 0x80
utilise. (Ces pointeurs sont des implémentations ou enveloppements sys_whatever
pour l'implémentation 64 bits à l'intérieur du noyau. Les appels système sont en fait des appels de fonction à travers la frontière utilisateur/noyau.)
Seule la bas 32 bits de registres arg sont passés. les moitiés supérieures de rbx
- rbp
sont préservées, mais ignorées par les appels système int 0x80
. notez que le fait de passer un mauvais pointeur à un appel système ne produit pas de SIGSEGV; l'appel système renvoie plutôt -EFAULT
. Si vous ne Vérifiez pas les valeurs de retour d'erreur (avec un débogueur ou un outil de traçage), cela semblera échouer silencieusement.
tous les registres (sauf eax bien sûr) sont sauvegardés / restaurés (y compris RFLAGS, et le 32 supérieur de integer regs), sauf que r8-r11 sont mis à zéro . r12-r15
sont des call-preserved dans la convention d'appel de fonction SysV ABI x86-64, de sorte que les registres qui sont mis à zéro par int 0x80
en 64 bits sont le sous-ensemble call-clobbered des" nouveaux " registres qu'AMD64 a ajouté.
ce comportement a été préservé lors de certains changements internes à la façon dont la sauvegarde des registres a été implémentée dans le noyau, et commentaires dans le noyau, mentionnez qu'il est utilisable à partir de 64 bits, donc cet ABI est probablement stable. (C'est-à-dire: vous pouvez compter sur r8-r11 mis à zéro, et tout le reste étant préservé.)
la valeur de retour est étendue pour remplir 64 bits rax
. (Linux déclare des fonctions sys_ 32 bits comme retournant signé long
.) Cela signifie que les valeurs de retour du pointeur (comme dans void *mmap()
) doivent être étendues à zéro avant d'être utilisées dans les modes d'adressage à 64 bits.
contrairement à sysenter
, il conserve la valeur originale de cs
, donc il retourne à l'espace utilisateur dans le même mode qu'il a été appelé. (L'utilisation de sysenter
se traduit par le paramétrage du noyau cs
à $__USER32_CS
, qui sélectionne un descripteur pour un segment de code 32 bits.)
strace
décoder int 0x80
incorrectement pour les procédés à 64 bits. Il décode comme si le process avait utilisé syscall
au lieu de int 0x80
. cela peut prêter à confusion . par exemple depuis strace
imprime write(0, NULL, 12 <unfinished ... exit status 1>
pour eax=1
/ int "1519370920"x80
, qui est en fait _exit(ebx)
, pas write(rdi, rsi, rdx)
.
int 0x80
fonctionne aussi longtemps que tous les arguments (y compris les pointeurs) s'inscrivent dans le bas 32 d'un registre . C'est le cas pour le code et les données statiques par défaut le code du modèle ("petit") dans le x86-64 SysV ABI . (Section 3.5.1
: tous les symboles sont connus pour être situés dans les adresses virtuelles dans la gamme 0x00000000
à 0x7effffff
, de sorte que vous pouvez faire des choses comme mov edi, hello
(AT&T mov $hello, %edi
) pour obtenir un pointeur dans un registre avec une instruction de 5 octets).
mais c'est pas le cas de les exécutables indépendants de la position , que beaucoup de distros Linux configurent maintenant gcc
pour faire par défaut (et ils activent ASLR pour les exécutables). Par exemple, j'ai compilé un hello.c
sur Arch Linux, et j'ai défini un point de rupture au début de main. La constante de chaîne passée à puts
était à 0x555555554724
, de sorte qu'un appel système de 32 bits ABI write
ne fonctionnerait pas. (GDB désactive ASLR par défaut, donc vous voyez toujours la même adresse de l'exécution à l'exécution, si vous exécutez depuis GDB.)
Linux place la pile près de le "gap" entre les plages supérieure et inférieure des adresses canoniques , c.-à-d. avec le haut de la pile à 2^48-1. (Ou quelque part à l'aléatoire, à l'ASLR activé). Ainsi rsp
à l'entrée de _start
dans un exécutable typique lié statiquement est quelque chose comme 0x7fffffffe550
, selon la taille de l'env VAR et args. Tronçonner ce pointeur à esp
ne pointe pas vers une mémoire valide, donc les appels système avec des entrées de pointeur retourneront typiquement -EFAULT
si vous essayez de passer un pointeur de pile tronqué. (Et votre programme se plantera si vous tronquez rsp
en esp
et puis faire n'importe quoi avec la pile, par exemple si vous avez construit 32-bit ASM source comme un 64-bit exécutable.)
Comment cela fonctionne dans le noyau:
dans le code source de Linux, arch/x86/entry/entry_64_compat.S
définit
ENTRY(entry_INT80_compat)
. Les processus 32 et 64 bits utilisent le même point d'entrée lorsqu'ils exécutent int 0x80
.
entry_64.S
définit les points d'entrée natifs pour un noyau 64 bits, qui inclut les gestionnaires d'interruptions / pannes et les appels système natifs syscall
de long mode (alias 64-bit mode) processus.
entry_64_compat.S
définit les points d'entrée des appels système du mode compat dans un noyau 64 bits, plus le cas particulier de int 0x80
dans un processus 64 bits. ( sysenter
dans un processus 64 bits peut aller à ce point d'entrée, mais il pousse $__USER32_CS
, donc il va toujours revenir en mode 32 bits.) Il existe une version 32 bits de l'instruction syscall
, supportée sur les processeurs AMD, et Linux la supporte aussi pour les appels système 32 bits rapides à partir de processus 32 bits.
je suppose qu'un cas d'utilisation possible pour int 0x80
en mode 64 bits est si vous vouliez pour utiliser a code personnalisé-segment descripteur que vous avez installé avec modify_ldt
. int 0x80
segment push s'enregistre pour être utilisé avec iret
, et Linux revient toujours de int 0x80
appels système via iret
. Le 64-bit syscall
points d'Entrée fixe pt_regs->cs
et ->ss
aux constantes, __USER_CS
et __USER_DS
. (Il est normal que SS et DS utilisent le même segment descripteur. Les différences de Permission sont faites avec la pagination, pas la segmentation.)
entry_32.S
définit des points d'entrée dans un noyau 32 bits, et n'est pas impliqué.
le
int 0x80
point d'entrée dans Linux 4.12'sentry_64_compat.S
:/* * 32-bit legacy system call entry. * * 32-bit x86 Linux system calls traditionally used the INT "151900920"x80 * instruction. INT "151900920"x80 lands here. * * This entry point can be used by 32-bit and 64-bit programs to perform * 32-bit system calls. Instances of INT "151900920"x80 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 * "151900920"x80 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)
le code zéro-étend eax dans rax, puis pousse tous les registres sur la pile de noyau pour former un struct pt_regs
. C'est là qu'il restaurera à partir du moment où l'appel système reviendra. Il s'agit d'une disposition standard pour les registres d'espace utilisateur enregistrés (pour n'importe quel point d'entrée), de sorte que ptrace
d'un autre processus (comme gdb ou strace
) lira et/ou écrira cette mémoire s'ils utilisent ptrace
pendant que ce processus est à l'intérieur d'un appel système. ( ptrace
modification des registres est une chose qui rend les chemins de retour compliqués pour les autres points d'entrée. Voir les commentaires.)
mais il pousse "1519860920"
au lieu de r8/r9/r10/r11. ( sysenter
et AMD syscall32
points d'entrée de magasin de zéros pour r8-r15.)
je pense que cette réduction à zéro de r8-r11 doit correspondre au comportement historique. Avant que le configure pt_regs complet pour tous les appels compat commit, le point d'entrée a seulement sauvegardé les registres c appel-clobbered. Il a envoyé directement de asm avec call *ia32_sys_call_table(, %rax, 8)
, et ces fonctions suivent le appelant la convention, pour qu'ils préservent rbx
, rbp
, rsp
, et r12-r15
. La mise à zéro de r8-r11
au lieu de les laisser indéterminés était probablement un moyen d'éviter les fuites d'information du noyau. IDK comment il a traité ptrace
si la seule copie de l'utilisateur de l'espace à l'appel de conservés les registres était sur la pile du noyau où une fonction C sauvés. Je doute qu'il ait utilisé des métadonnées pour les trouver.
l'implémentation actuelle (Linux 4.12) dispatches appels système 32 bits-ABI de C, rechargement de la sauvegarde ebx
, ecx
, etc. de pt_regs
. (64 bits natif du système d'appels d'expédition directement à partir de l'asm, avec seulement mov %r10, %rcx
nécessaire pour prendre en compte le peu de différence dans la convention d'appel entre les fonctions et les syscall
. Malheureusement, il ne peut pas toujours utiliser sysret
, car les bogues CPU le rendent dangereux avec des adresses non-canoniques. Il essaie, donc la voie rapide est plutôt sacrément rapide, bien que syscall
lui-même prenne encore des dizaines de cycles.)
quoi qu'il en soit, sous Linux actuel, les appels de 32 bits (y compris int 0x80
à partir de 64 bits) finissent par do_syscall_32_irqs_on(struct pt_regs *regs)
. Elle distribue à un pointeur de fonction ia32_sys_call_table
, avec 6 zéro étendu args. Cela évite peut-être d'avoir besoin d'un enveloppeur autour de la fonction syscall natif 64 bits dans plus de cas pour préserver ce comportement, donc plus de la table ia32
les entrées peuvent être l'implémentation d'appel système natif directement.
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);
dans les versions plus anciennes de Linux qui envoient des appels système 32 bits depuis asm (comme le fait encore 64 bits), le point d'entrée int80 place lui-même args dans les registres de droite avec les instructions mov
et xchg
, en utilisant des registres 32 bits. Il utilise même mov %edx,%edx
pour zéroter EDX dans RDX (parce que arg3 utilise le même registre dans les deux conventions). code ici . Ce code est reproduit aux points d'entrée sysenter
et syscall32
.
exemple Simple / programme d'essai:
j'ai écrit un monde Hello simple (en syntaxe NASM) qui définit tous les registres pour avoir les moitiés supérieures non-zéro, puis fait deux write()
appels système avec int 0x80
, l'un avec un pointeur vers une chaîne dans .rodata
(succède), le second avec un pointeur vers la pile (échoue avec -EFAULT
).
puis il utilise le 64-bit natif syscall
ABI à write()
les caractères de la pile (64-bit pointer), et encore pour sortir.
donc tous ces exemples utilisent l'ABIs correctement, à l'exception du 2ème int 0x80
qui essaie de passer un pointeur 64 bits et l'a tronqué.
si vous le construisez comme un exécutable indépendant de la position, le premier échouera aussi. (Vous devez utiliser un RIP-relatif lea
au lieu de mov
pour obtenir l'adresse de hello:
dans un registre.)
j'ai utilisé gdb, mais utilisez le débogueur que vous préférez. Utilisez un qui met en évidence les registres changés depuis la dernière étape. gdbgui
fonctionne bien pour le débogage asm source, mais n'est pas idéal pour démontage. Pourtant, il a un volet de Registre qui fonctionne bien pour des règles entières au moins, et il a fonctionné très bien sur cet exemple.
voir la ligne ;;;
commentaires décrivant comment les enregistrements sont modifiés par les appels de système
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
Construire en 64 bits binaire statique avec
yasm -felf64 -Worphan-labels -gdwarf2 abi32-from-64.asm
ld -o abi32-from-64 abi32-from-64.o
Exécuter gdb ./abi32-from-64
. Dans gdb
, exécutez set disassembly-flavor intel
et layout reg
si vous ne l'avez pas déjà dans votre ~/.gdbinit
. (GAS .intel_syntax
est comme MASM, pas NASM, mais ils sont assez proches pour qu'il soit facile à lire si vous aimez la syntaxe 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
Press control-L quand le mode TUI de gdb est perturbé. Cela se produit facilement, même lorsque les programmes ne s'impriment pas eux-mêmes.