Pourquoi ce programme ptrace dit que syscall a renvoyé -38?

C’est le même que celui-ci sauf que execl("/bin/ls", "ls", NULL); .

Le résultat est évidemment faux car chaque appel syscall est renvoyé avec -38 :

 [user@ test]# ./test_trace syscall 59 called with rdi(0), rsi(0), rdx(0) syscall 12 returned with -38 syscall 12 called with rdi(0), rsi(0), rdx(140737288485480) syscall 9 returned with -38 syscall 9 called with rdi(0), rsi(4096), rdx(3) syscall 9 returned with -38 syscall 9 called with rdi(0), rsi(4096), rdx(3) syscall 21 returned with -38 syscall 21 called with rdi(233257948048), rsi(4), rdx(233257828696) ... 

Quelqu’un sait la raison?

METTRE À JOUR

Maintenant, le problème est le suivant:

 execve called with rdi(4203214), rsi(140733315680464), rdx(140733315681192) execve returned with 0 execve returned with 0 ... 

execve retourné deux fois , pourquoi?

Le code ne tient pas compte de la notification de l’ exec de l’enfant, et finit donc par gérer l’entrée syscall en tant que syscall exit et syscall exit en tant qu’entrée syscall. C’est pourquoi vous voyez ” syscall 12 returnedavantsyscall 12 called “, etc. ( -38 est ENOSYS qui est mis dans RAX comme valeur de retour par défaut par le code d’entrée du kernel syscall.)

Comme l’indique la page de manuel de ptrace(2) :

PTRACE_TRACEME

Indique que ce processus doit être tracé par son parent. Tout signal (à l’exception de SIGKILL) fourni à ce processus provoquera son arrêt et la notification de son parent via wait (). De même, tous les appels ultérieurs à exec () par ce processus provoqueront l’envoi d’un SIGTRAP , ce qui donnera au parent une chance de prendre le contrôle avant que le nouveau programme ne commence à s’exécuter. […]

Vous avez dit que le code original que vous execl("/bin/ls", "ls", NULL); était “le même que celui-ci sauf que execl("/bin/ls", "ls", NULL); “. Eh bien, ce n’est clairement pas le cas, car vous travaillez avec x86_64 plutôt que 32 bits et avez au moins changé les messages.

Mais, en supposant que vous n’ayez pas trop changé, la première fois que wait() réveille le parent, ce n’est pas pour syscall entry ou exit – le parent n’a pas encore exécuté ptrace(PTRACE_SYSCALL,...) . Au lieu de cela, vous voyez cette notification que l’enfant a effectué un exec (sur x86_64, syscall 59 est execve ).

Le code interprète de manière incorrecte cela comme entrée syscall. Ensuite, il appelle ptrace(PTRACE_SYSCALL,...) , et la prochaine fois que le parent est réveillé, c’est pour une entrée syscall (syscall 12), mais le code le signale en tant que syscall exit.

Notez que dans ce cas original, vous ne voyez jamais l’entrée / sortie execve execve – uniquement la notification supplémentaire – car le parent n’exécute pas ptrace(PTRACE_SYSCALL,...) avant qu’il ne se produise.

Si vous organisez le code de manière à ce que l’entrée / la sortie de l’ execve syscall soit interceptée, vous verrez le nouveau comportement que vous observez. Le parent sera réveillé trois fois: une fois pour execve entrée syscall (en raison de l’utilisation de ptrace(PTRACE_SYSCALL,...) , une fois pour execve syscall exit (également dû à l’utilisation de ptrace(PTRACE_SYSCALL,...) , et un troisième temps pour la notification exec (qui arrive quand même).


Voici un exemple complet (pour x86 ou x86_64) qui prend soin de montrer le comportement de l’ exec lui-même en arrêtant d’abord l’enfant:

 #include  #include  #include  #include  #include  #include  #include  #include  #ifdef __x86_64__ #define SC_NUMBER (8 * ORIG_RAX) #define SC_RETCODE (8 * RAX) #else #define SC_NUMBER (4 * ORIG_EAX) #define SC_RETCODE (4 * EAX) #endif static void child(void) { /* Request tracing by parent: */ ptrace(PTRACE_TRACEME, 0, NULL, NULL); /* Stop before doing anything, giving parent a chance to catch the exec: */ kill(getpid(), SIGSTOP); /* Now exec: */ execl("/bin/ls", "ls", NULL); } static void parent(pid_t child_pid) { int status; long sc_number, sc_retcode; while (1) { /* Wait for child status to change: */ wait(&status); if (WIFEXITED(status)) { printf("Child exit with status %d\n", WEXITSTATUS(status)); exit(0); } if (WIFSIGNALED(status)) { printf("Child exit due to signal %d\n", WTERMSIG(status)); exit(0); } if (!WIFSTOPPED(status)) { printf("wait() returned unhandled status 0x%x\n", status); exit(0); } if (WSTOPSIG(status) == SIGTRAP) { /* Note that there are *three* reasons why the child might stop * with SIGTRAP: * 1) syscall entry * 2) syscall exit * 3) child calls exec */ sc_number = ptrace(PTRACE_PEEKUSER, child_pid, SC_NUMBER, NULL); sc_retcode = ptrace(PTRACE_PEEKUSER, child_pid, SC_RETCODE, NULL); printf("SIGTRAP: syscall %ld, rc = %ld\n", sc_number, sc_retcode); } else { printf("Child stopped due to signal %d\n", WSTOPSIG(status)); } fflush(stdout); /* Resume child, requesting that it stops again on syscall enter/exit * (in addition to any other reason why it might stop): */ ptrace(PTRACE_SYSCALL, child_pid, NULL, NULL); } } int main(void) { pid_t pid = fork(); if (pid == 0) child(); else parent(pid); return 0; } 

ce qui donne quelque chose comme ceci (ceci est pour 64 bits – les numéros d’appel système sont différents pour 32 bits, en particulier execve est 11, plutôt que 59):

  Enfant arrêté en raison du signal 19
 SIGTRAP: syscall 59, rc = -38
 SIGTRAP: syscall 59, rc = 0
 SIGTRAP: syscall 59, rc = 0
 SIGTRAP: syscall 63, rc = -38
 SIGTRAP: syscall 63, rc = 0
 SIGTRAP: syscall 12, rc = -38
 SIGTRAP: syscall 12, rc = 5324800
 ... 

Le signal 19 est le SIGSTOP explicite; l’enfant s’arrête trois fois pour l’ execve comme décrit ci-dessus; puis deux fois (entrée et sortie) pour les autres appels système.

Si vous êtes vraiment intéressant dans tous les détails ptrace() de ptrace() , la meilleure documentation que je connaisse est le README-linux-ptrace dans la source de strace . Comme il est dit, “l’API est complexe et a des subtilités subtiles” ….

Vous pouvez imprimer une description lisible par l’homme de la dernière erreur système avec perror ou strerror. Cette description d’erreur vous aidera beaucoup plus.

Sur un rax je dirais que vous examinez eax , ou son équivalent 64 bits (probablement rax ) pour le code retour d’un appel système. Il existe un emplacement supplémentaire pour enregistrer ce registre nommé orig_eax , utilisé pour redémarrer les appels système.

J’ai beaucoup fouillé dans ce genre de choses mais je ne peux pas pour la vie trouver mes découvertes. Voici quelques questions connexes:

  • Sous Linux, à l’entrée d’un appel sys, quelle est la valeur de% eax? (pas orig_eax)
  • Pourquoi orig_eax est-il fourni en plus de eax?

Mise à jour0

Il semble que ma mémoire soit correcte. Vous trouverez tout ce dont vous avez besoin ici dans la source du kernel (le site principal est en panne, heureusement torvalds reflète maintenant Linux à github).