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 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?

22
demandé sur Peter Cordes 2017-09-07 07:20:40

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's entry_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.

30
répondu Peter Cordes 2018-03-19 07:43:43