web-dev-qa-db-fra.com

Que se passe-t-il si vous utilisez l'ABI Linux 32 bits int 0x80 en code 64 bits?

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?

31
Peter Cordes

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).


Les détails: ce qui est enregistré/restauré, quelles parties de regs le noyau utilise

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.)


Comment ça marche dans le noyau:

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 Dans entry_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.


Exemple simple/programme de test:

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.

36
Peter Cordes