int 0x80
Sous Linux invoque toujours l'ABI 32 bits, quel que soit le mode à partir duquel il est appelé: arguments dans ebx
, ecx
, ... et numéros d'appel système à partir de /usr/include/asm/unistd_32.h
. (Ou se bloque sur les 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 les arguments dans rdi
, rsi
, etc. Voir Quelles sont les conventions d'appel pour les appels système UNIX et Linux sur i386 et x86-64 . Si votre question a été marquée en double, consultez ce lien pour savoir comment vous devriez effectuer des appels système en code 32 ou 64 bits. Si vous voulez comprendre ce qui s'est exactement passé, continuez à lire.
(Pour un exemple de 32 bits contre 64 bits sys_write
, Voir tilisation de l'interruption 0x80 sur Linux 64 bits )
syscall
les appels système sont plus rapides que les appels système int 0x80
, utilisez donc les 64 bits natifs syscall
sauf si vous écrivez du code machine polyglotte qui s'exécute de la même manière lorsqu'il est exécuté en 32 ou 64 bit. (sysenter
renvoie toujours en mode 32 bits, il n'est donc pas utile à partir de l'espace utilisateur 64 bits, bien qu'il s'agisse d'une instruction x86-64 valide.)
Connexes: Le guide définitif des appels système Linux (sur x86) pour savoir comment créer int 0x80
Ou sysenter
appels système 32 bits, ou syscall
appels système 64 bits, ou appel du vDSO pour des appels système "virtuels" comme gettimeofday
. Plus d'informations sur les appels système.
L'utilisation de int 0x80
Permet d'écrire quelque chose qui s'assemblera en mode 32 ou 64 bits, donc c'est pratique pour une exit_group()
à la fin d'un microbenchmark ou quelque chose.
Les PDF actuels des documents officiels i386 et x86-64 System V psABI qui standardisent la fonction et les conventions d'appel syscall sont liés à partir de https://github.com/hjl-tools/x86-psABI/wiki/X86-psABI .
Voir la balise x86wiki pour les guides pour débutants, les manuels x86, la documentation officielle et les guides/ressources d'optimisation des performances.
Mais comme les gens continuent de publier des questions avec du code qui utilise int 0x80
En code 64 bits , ou accidentellement création de binaires 64 bits à partir de la source écrite pour 32 bits , Je me demande ce qui exactement se passe-t-il sur Linux actuel?
Est-ce que int 0x80
Enregistre/restaure tous les registres 64 bits? Tronque-t-il des registres à 32 bits? Que se passe-t-il si vous passez des arguments de pointeur qui ont des moitiés supérieures non nulles?
Cela fonctionne-t-il si vous lui passez des pointeurs 32 bits?
TL: DR : int 0x80
Fonctionne lorsqu'il est utilisé correctement, tant que les pointeurs tiennent sur 32 bits ( les pointeurs de pile ne conviennent pas ). De plus, strace
le décode mal , décodant le contenu du registre comme s'il s'agissait de l'ABI 64 $ syscall
. (Il n'y a pas encore de moyen simple/fiable pour dire strace
.)
int 0x80
Zéros r8-r11 et conserve tout le reste. Utilisez-le exactement comme vous le feriez dans du 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 Windows Ubuntu est strictement 64 bits uniquement: int 0x80
Ne fonctionne pas du tout . Il est également possible de construire des noyaux Linux sans émulation IA-32 non plus. (Pas de prise en charge des exécutables 32 bits, pas de prise en charge des appels système 32 bits).
int 0x80
Utilise eax
(pas le rax
complet) comme numéro d'appel système, répartissant dans la même table de pointeurs de fonction que l'espace utilisateur 32 bits int 0x80
Utilise. (Ces pointeurs sont vers des implémentations ou des wrappers sys_whatever
Pour l'implémentation 64 bits native à l'intérieur du noyau. Les appels système sont en réalité des appels de fonction à travers la frontière utilisateur/noyau.)
Seuls les 32 bits les plus faibles des registres arg sont passés. Les moitiés supérieures de rbx
- rbp
sont conservées, mais ignorées par les appels système de int 0x80
. Notez que la transmission d'un mauvais pointeur à un appel système n'entraîne pas SIGSEGV; à la place, l'appel système renvoie -EFAULT
. Si vous ne vérifiez pas les valeurs de retour d'erreur (avec un débogueur ou un outil de traçage), il semblera échouer en silence.
Tous les registres (sauf eax bien sûr) sont sauvegardés/restaurés (y compris RFLAGS et les 32 premiers des regs entiers), sauf que r8-r11 sont mis à zéro . r12-r15
Sont préservés dans la convention d'appel de fonction du SysV ABI x86-64, donc les registres qui sont mis à zéro par int 0x80
En 64 bits sont le sous-ensemble clobé des "nouveaux" registres que AMD64 a ajouté.
Ce comportement a été préservé malgré certaines modifications internes de la façon dont la sauvegarde des registres a été implémentée dans le noyau, et les commentaires dans le noyau mentionnent qu'il est utilisable à partir de 64 bits, donc cet ABI est probablement stable. (C'est-à-dire que vous pouvez compter sur r8-r11 étant mis à zéro et tout le reste étant préservé.)
La valeur de retour est étendue par signe pour remplir 64 bits rax
. (Linux déclare que les fonctions sys_ 32 bits retournent des signés long
.) Cela signifie que les valeurs de retour du pointeur (comme de void *mmap()
) doivent être étendues à zéro avant utilisation en modes d'adressage 64 bits
Contrairement à sysenter
, il conserve la valeur d'origine de cs
, il revient donc à l'espace utilisateur dans le même mode qu'il a été appelé. (L'utilisation de sysenter
entraîne le noyau définir cs
sur $__USER32_CS
, qui sélectionne un descripteur pour un segment de code 32 bits.)
strace
décode int 0x80
Incorrectement pour les processus 64 bits. Il décode comme si le processus avait utilisé syscall
au lieu de int 0x80
. Cela peut être très déroutant . par exemple. puisque strace
imprime write(0, NULL, 12 <unfinished ... exit status 1>
pour eax=1
/int $0x80
, qui est en fait _exit(ebx)
, pas write(rdi, rsi, rdx)
.
int 0x80
Fonctionne tant que tous les arguments (y compris les pointeurs) tiennent dans le bas 32 d'un registre . C'est le cas pour le code statique et les données dans le modèle de code par défaut ("petit") dans le x86-64 SysV ABI . (Section 3.5.1: tous les symboles sont connus pour se trouver dans les adresses virtuelles dans la plage 0x00000000
À 0x7effffff
, vous pouvez donc 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 pour exécutables indépendants de la position , que de nombreuses distributions Linux configurent maintenant gcc
pour make par défaut (et ils activer ASLR pour les exécutables). Par exemple, j'ai compilé un hello.c
Sur Arch Linux et défini un point d'arrêt au début de main. La constante de chaîne passée à puts
était à 0x555555554724
, Donc un appel système ABI write
32 bits ne fonctionnerait pas. (GDB désactive ASLR par défaut, donc vous voyez toujours la même adresse d'une exécution à l'autre, si vous exécutez à partir de GDB.)
Linux place la pile près "l'écart" entre les plages supérieure et inférieure des adresses canoniques , c'est-à-dire avec le haut de la pile à 2 ^ 48-1. (Ou quelque part au hasard, avec ASLR activé). Ainsi, rsp
lors de l'entrée dans _start
Dans un exécutable lié statiquement typique est quelque chose comme 0x7fffffffe550
, Selon la taille des vars et des arguments env. Tronquer ce pointeur à esp
ne pointe vers aucune mémoire valide, donc les appels système avec des entrées de pointeur renverront généralement -EFAULT
Si vous essayez de passer un pointeur de pile tronqué. (Et votre programme se bloquera si vous tronquez rsp
en esp
, puis faites quoi que ce soit avec la pile, par exemple si vous avez créé une source asm 32 bits en tant qu'exécutable 64 bits.)
Dans le code source 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 mode long (également appelé mode 64 bits) = processus.
entry_64_compat.S
Définit les points d'entrée d'appel système du mode compat dans un noyau 64 bits, plus le cas spécial de int 0x80
Dans un processus 64 bits. (sysenter
dans un processus 64 bits peut également aller à ce point d'entrée, mais il pousse $__USER32_CS
, Il reviendra donc toujours en mode 32 bits.) Il existe une version 32 bits de l'instruction syscall
, prise en charge sur les processeurs AMD, et Linux la prend également en charge 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 utiliser a descripteur de segment de code personnalisé que vous avez installé avec modify_ldt
. int 0x80
Pousse le segment à s'inscrire pour être utilisé avec iret
, et Linux revient toujours des appels système int 0x80
Via iret
. Le point d'entrée 64 $ syscall
définit pt_regs->cs
Et ->ss
Sur des constantes, __USER_CS
Et __USER_DS
. (Il est normal que SS et DS utilisent les mêmes descripteurs de segment. Les différences de permission se font 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 du tout impliqué.
Le point d'entrée
int 0x80
Dansentry_64_compat.S
De Linux 4.12 :/* * 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)
Le code étend zéro eax en rax, puis pousse tous les registres sur la pile du noyau pour former un struct pt_regs
. C'est là qu'il sera restauré à partir du retour de l'appel système. C'est dans une disposition standard pour les registres de l'espace utilisateur enregistrés (pour tout point d'entrée), donc ptrace
d'un autre processus (comme gdb ou strace
) lira et/ou écrit cette mémoire s'ils utilisent ptrace
pendant que ce processus se trouve dans un appel système. (ptrace
la modification des registres est une chose qui complique les chemins de retour pour les autres points d'entrée. Voir commentaires.)
Mais il pousse $0
Au lieu de r8/r9/r10/r11. (Les points d'entrée sysenter
et AMD syscall32
Stockent des zéros pour r8-r15.)
Je pense que cette réduction à zéro de r8-r11 doit correspondre au comportement historique. Avant la validation Set up full pt_regs for all compat syscalls , le point d'entrée ne sauvegardait que les registres C clobbered. Il a été envoyé directement depuis asm avec call *ia32_sys_call_table(, %rax, 8)
, et ces fonctions suivent la convention d'appel, donc elles préservent rbx
, rbp
, rsp
et r12-r15
. Mettre à zéro r8-r11
Au lieu de les laisser indéfinis était probablement un moyen d'éviter les fuites d'informations du noyau. IDK comment il a géré ptrace
si la seule copie des registres préservés des appels de l'espace utilisateur était sur la pile du noyau où une fonction C les a enregistrés. Je doute qu'il ait utilisé des métadonnées de déroulement de pile pour les y trouver.
L'implémentation actuelle (Linux 4.12) distribue les appels système ABI 32 bits à partir de C, rechargeant les ebx
, ecx
, etc. enregistrés à partir de pt_regs
. (Les appels système natifs 64 bits sont envoyés directement depuis asm, avec seulement un mov %r10, %rcx
nécessaire pour tenir compte de la petite différence dans la convention d'appel entre les fonctions et syscall
. Malheureusement, il ne peut pas toujours utiliser sysret
, car les bogues CPU le rendent dangereux avec des adresses non canoniques. Il essaie de le faire, donc le chemin rapide est sacrément rapide, bien que syscall
lui-même prenne encore des dizaines de cycles.)
Quoi qu'il en soit, dans Linux actuel, les appels système 32 bits (y compris int 0x80
À partir de 64 bits) finissent finalement par do_syscall_32_irqs_on(struct pt_regs *regs)
. Il distribue à un pointeur de fonction ia32_sys_call_table
, Avec 6 arguments étendus zéro. Cela évite peut-être d'avoir besoin d'un wrapper autour de la fonction syscall native 64 bits dans plus de cas pour préserver ce comportement, de sorte qu'une plus grande partie des entrées de la table ia32
Peut ê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 anciennes versions de Linux qui envoient des appels système 32 bits depuis asm (comme le fait toujours 64 bits), le point d'entrée int80 lui-même place les arguments dans les bons registres avec les instructions mov
et xchg
, en utilisant des registres 32 bits. Il utilise même mov %edx,%edx
Pour étendre zéro EDX en RDX (car arg3 utilise le même registre dans les deux conventions). code ici . Ce code est dupliqué dans les points d'entrée sysenter
et syscall32
.
J'ai écrit un simple Hello World (dans la syntaxe NASM) qui définit tous les registres pour avoir des moitiés supérieures non nulles, puis effectue deux appels système write()
avec int 0x80
, Un avec un pointeur sur une chaîne dans .rodata
(Réussit), le second avec un pointeur vers la pile (échoue avec -EFAULT
).
Ensuite, il utilise l'ABI 64 bits natif syscall
pour write()
les caractères de la pile (pointeur 64 bits), puis de nouveau pour quitter.
Donc, tous ces exemples utilisent correctement les ABI, à l'exception du 2e int 0x80
Qui essaie de passer un pointeur 64 bits et le fait tronquer.
Si vous le construisiez comme un exécutable indépendant de la position, le premier échouerait également. (Vous devez utiliser un lea
relatif au RIP 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-en un qui met en évidence les registres modifiés depuis la dernière étape unique. gdbgui
fonctionne bien pour le débogage d'une source asm, mais n'est pas idéal pour le démontage. Pourtant, il a un volet de registre qui fonctionne bien pour les regs entiers au moins, et cela a très bien fonctionné sur cet exemple.
Voir les commentaires ;;;
En ligne décrivant comment les registres sont modifiés par les appels 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
Build it dans un binaire statique 64 bits avec
yasm -felf64 -Worphan-labels -gdwarf2 abi32-from-64.asm
ld -o abi32-from-64 abi32-from-64.o
Exécutez gdb ./abi32-from-64
. Dans gdb
, exécutez set disassembly-flavor intel
Et layout reg
Si vous n'en avez pas déjà dans votre ~/.gdbinit
. (GAS .intel_syntax
Est comme MASM, pas NASM, mais ils sont suffisamment proches pour être faciles à 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
Appuyez sur control-L lorsque le mode TUI de gdb est perturbé. Cela se produit facilement, même lorsque les programmes n'impriment pas pour sortir d'eux-mêmes.