Ανάπτυξη λογισμικού εντοπισμού σφαλμάτων με strace

Ανάπτυξη λογισμικού εντοπισμού σφαλμάτων με strace

Η καθημερινή μου δουλειά είναι κυρίως ανάπτυξη λογισμικού, πράγμα που σημαίνει ότι ξοδεύω πολύ χρόνο προσπαθώντας να απαντήσω σε ερωτήσεις όπως:

  • Αυτό το λογισμικό λειτουργεί για τον προγραμματιστή, αλλά όχι για μένα. Γιατί;
  • Χθες αυτό το λογισμικό δούλευε για μένα, αλλά σήμερα δεν λειτουργεί. Γιατί;

Αυτό είναι ένα είδος εντοπισμού σφαλμάτων που είναι ελαφρώς διαφορετικό από τον κανονικό εντοπισμό σφαλμάτων λογισμικού. Ο τακτικός εντοπισμός σφαλμάτων αφορά τη λογική του κώδικα, αλλά ο εντοπισμός σφαλμάτων ανάπτυξης αφορά την αλληλεπίδραση μεταξύ του κώδικα και του περιβάλλοντος. Ακόμα κι αν η ρίζα του προβλήματος είναι ένα λογικό σφάλμα, το γεγονός ότι όλα λειτουργούν σε ένα μηχάνημα και όχι σε άλλο σημαίνει ότι το πρόβλημα είναι κατά κάποιο τρόπο στο περιβάλλον.

Έτσι, αντί για τα συνηθισμένα εργαλεία εντοπισμού σφαλμάτων όπως gdb Έχω ένα διαφορετικό σύνολο εργαλείων για την ανάπτυξη εντοπισμού σφαλμάτων. Και το αγαπημένο μου εργαλείο για την αντιμετώπιση του προβλήματος όπως "Γιατί αυτό το λογισμικό δεν λειτουργεί για μένα;" που ονομάζεται σκελετό.

Τι είναι το strace;

σκελετό είναι ένα εργαλείο για την «ανίχνευση κλήσεων συστήματος». Αρχικά δημιουργήθηκε για Linux, αλλά τα ίδια κόλπα εντοπισμού σφαλμάτων μπορούν να γίνουν με εργαλεία για άλλα συστήματα (DTrace ή ktrace).

Η βασική εφαρμογή είναι πολύ απλή. Απλώς πρέπει να εκτελέσετε το strace με οποιαδήποτε εντολή και θα απορρίψει όλες τις κλήσεις συστήματος (αν και πρώτα θα πρέπει πιθανώς να το εγκαταστήσετε μόνοι σας σκελετό):

$ strace echo Hello
...Snip lots of stuff...
write(1, "Hellon", 6)                  = 6
close(1)                                = 0
close(2)                                = 0
exit_group(0)                           = ?
+++ exited with 0 +++

Τι είναι αυτές οι κλήσεις συστήματος; Αυτό είναι κάτι σαν ένα API για τον πυρήνα του λειτουργικού συστήματος. Μια φορά κι έναν καιρό, το λογισμικό είχε άμεση πρόσβαση στο υλικό στο οποίο λειτουργούσε. Αν, για παράδειγμα, χρειαζόταν να εμφανίσει κάτι στην οθόνη, έπαιζε με θύρες ή καταχωρητές με αντιστοίχιση μνήμης για συσκευές βίντεο. Όταν τα συστήματα υπολογιστών πολλαπλών εργασιών έγιναν δημοφιλή, επικράτησε χάος καθώς διάφορες εφαρμογές μάχονταν για το υλικό. Τα σφάλματα σε μία εφαρμογή θα μπορούσαν να καταστρέψουν άλλες, αν όχι ολόκληρο το σύστημα. Στη συνέχεια εμφανίστηκαν λειτουργίες προνομίου (ή «προστασία δακτυλίου») στην CPU. Ο πυρήνας έγινε ο πιο προνομιακός: έλαβε πλήρη πρόσβαση στο υλικό, δημιουργώντας λιγότερο προνομιούχες εφαρμογές που έπρεπε ήδη να ζητήσουν πρόσβαση από τον πυρήνα για να αλληλεπιδράσουν με το υλικό μέσω κλήσεων συστήματος.

Σε δυαδικό επίπεδο, μια κλήση συστήματος είναι ελαφρώς διαφορετική από μια απλή κλήση συνάρτησης, αλλά τα περισσότερα προγράμματα χρησιμοποιούν ένα περιτύλιγμα στην τυπική βιβλιοθήκη. Εκείνοι. η τυπική βιβλιοθήκη POSIX C περιέχει μια κλήση συνάρτησης γράφω(), το οποίο περιέχει όλο τον κώδικα που αφορά την αρχιτεκτονική για την κλήση συστήματος γράφω.

Ανάπτυξη λογισμικού εντοπισμού σφαλμάτων με strace

Εν ολίγοις, οποιαδήποτε αλληλεπίδραση μεταξύ μιας εφαρμογής και του περιβάλλοντος της (συστήματα υπολογιστών) πραγματοποιείται μέσω κλήσεων συστήματος. Επομένως, όταν το λογισμικό λειτουργεί σε ένα μηχάνημα αλλά όχι σε άλλο, θα ήταν καλό να κοιτάξετε τα αποτελέσματα ανίχνευσης κλήσεων συστήματος. Πιο συγκεκριμένα, εδώ είναι μια λίστα τυπικών σημείων που μπορούν να αναλυθούν χρησιμοποιώντας ένα ίχνος κλήσης συστήματος:

  • Κονσόλα I/O
  • Δίκτυο I/O
  • Πρόσβαση στο σύστημα αρχείων και I/O αρχείων
  • Διαχείριση της διάρκειας ζωής ενός νήματος διεργασίας
  • Διαχείριση μνήμης χαμηλού επιπέδου
  • Πρόσβαση σε συγκεκριμένα προγράμματα οδήγησης συσκευών

Πότε να χρησιμοποιήσετε το strace;

Θεωρητικά, σκελετό χρησιμοποιείται με οποιοδήποτε πρόγραμμα στο χώρο χρήστη, επειδή οποιοδήποτε πρόγραμμα στο χώρο χρήστη πρέπει να πραγματοποιεί κλήσεις συστήματος. Λειτουργεί πιο αποτελεσματικά με μεταγλωττισμένα προγράμματα χαμηλού επιπέδου, αλλά λειτουργεί επίσης με γλώσσες υψηλού επιπέδου όπως η Python, εάν μπορείτε να μειώσετε τον πρόσθετο θόρυβο από το χρόνο εκτέλεσης και τον διερμηνέα.

Σε όλο το μεγαλείο σκελετό εκδηλώνεται κατά τον εντοπισμό σφαλμάτων ενός λογισμικού που λειτουργεί καλά σε ένα μηχάνημα, αλλά ξαφνικά σταματά να λειτουργεί σε άλλο, παράγοντας ασαφή μηνύματα σχετικά με αρχεία, δικαιώματα ή ανεπιτυχείς προσπάθειες εκτέλεσης κάποιων εντολών ή κάτι άλλο... Είναι κρίμα, αλλά δεν το κάνει συνδυάζονται τόσο καλά με προβλήματα υψηλού επιπέδου, όπως σφάλματα επαλήθευσης πιστοποιητικών. Συνήθως αυτό απαιτεί συνδυασμό σκελετόμερικές φορές ίχνος και εργαλεία υψηλότερου επιπέδου (όπως το εργαλείο γραμμής εντολών OpenSSL για τον εντοπισμό σφαλμάτων του πιστοποιητικού).

Θα χρησιμοποιήσουμε έναν αυτόνομο διακομιστή ως παράδειγμα, αλλά η ανίχνευση κλήσεων συστήματος μπορεί συχνά να γίνει σε πιο σύνθετες πλατφόρμες ανάπτυξης. Απλά πρέπει να επιλέξετε τα σωστά εργαλεία.

Απλό παράδειγμα αποσφαλμάτωσης

Ας υποθέσουμε ότι θέλετε να εκτελέσετε την καταπληκτική εφαρμογή διακομιστή foo, και καταλήγετε σε αυτό:

$ foo
Error opening configuration file: No such file or directory

Προφανώς δεν μπορούσε να βρει το αρχείο ρυθμίσεων που γράψατε. Αυτό συμβαίνει επειδή μερικές φορές όταν οι διαχειριστές πακέτων μεταγλωττίζουν μια εφαρμογή, παρακάμπτουν τις αναμενόμενες θέσεις αρχείων. Και αν ακολουθήσετε τον οδηγό εγκατάστασης για μια διανομή, σε μια άλλη θα βρείτε αρχεία εντελώς διαφορετικά από εκείνα που περιμένατε. Το πρόβλημα θα μπορούσε να λυθεί σε λίγα δευτερόλεπτα εάν το μήνυμα σφάλματος έλεγε πού να αναζητήσετε το αρχείο διαμόρφωσης, αλλά δεν το κάνει. Πού να κοιτάξουμε λοιπόν;

Εάν έχετε πρόσβαση στον πηγαίο κώδικα, μπορείτε να τον διαβάσετε και να μάθετε τα πάντα. Ένα καλό εφεδρικό σχέδιο, αλλά όχι η πιο γρήγορη λύση. Μπορείτε να καταφύγετε σε ένα πρόγραμμα εντοπισμού σφαλμάτων βήμα προς βήμα όπως gdb και δείτε τι κάνει το πρόγραμμα, αλλά είναι πολύ πιο αποτελεσματικό να χρησιμοποιήσετε ένα εργαλείο που έχει σχεδιαστεί ειδικά για να δείχνει αλληλεπίδραση με το περιβάλλον: σκελετό.

Παραγωγή σκελετό μπορεί να φαίνεται περιττό, αλλά τα καλά νέα είναι ότι τα περισσότερα από αυτά μπορούν να αγνοηθούν με ασφάλεια. Συχνά είναι χρήσιμο να χρησιμοποιείτε τον τελεστή -o για να αποθηκεύσετε τα αποτελέσματα ιχνών σε ένα ξεχωριστό αρχείο:

$ strace -o /tmp/trace foo
Error opening configuration file: No such file or directory
$ cat /tmp/trace
execve("foo", ["foo"], 0x7ffce98dc010 /* 16 vars */) = 0
brk(NULL)                               = 0x56363b3fb000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=25186, ...}) = 0
mmap(NULL, 25186, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f2f12cf1000
close(3)                                = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF2113 3 > 1 260A2 "..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1824496, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f2f12cef000
mmap(NULL, 1837056, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f2f12b2e000
mprotect(0x7f2f12b50000, 1658880, PROT_NONE) = 0
mmap(0x7f2f12b50000, 1343488, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x22000) = 0x7f2f12b50000
mmap(0x7f2f12c98000, 311296, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x16a000) = 0x7f2f12c98000
mmap(0x7f2f12ce5000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1b6000) = 0x7f2f12ce5000
mmap(0x7f2f12ceb000, 14336, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f2f12ceb000
close(3)                                = 0
arch_prctl(ARCH_SET_FS, 0x7f2f12cf0500) = 0
mprotect(0x7f2f12ce5000, 16384, PROT_READ) = 0
mprotect(0x56363b08b000, 4096, PROT_READ) = 0
mprotect(0x7f2f12d1f000, 4096, PROT_READ) = 0
munmap(0x7f2f12cf1000, 25186)           = 0
openat(AT_FDCWD, "/etc/foo/config.json", O_RDONLY) = -1 ENOENT (No such file or directory)
dup(2)                                  = 3
fcntl(3, F_GETFL)                       = 0x2 (flags O_RDWR)
brk(NULL)                               = 0x56363b3fb000
brk(0x56363b41c000)                     = 0x56363b41c000
fstat(3, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x8), ...}) = 0
write(3, "Error opening configuration file"..., 60) = 60
close(3)                                = 0
exit_group(1)                           = ?
+++ exited with 1 +++

Περίπου ολόκληρη η πρώτη σελίδα εξόδου σκελετό - Πρόκειται συνήθως για προετοιμασία χαμηλού επιπέδου για εκτόξευση. (Πολλές κλήσεις χιλ, mprotect, πέννα για πράγματα όπως η ανίχνευση μνήμης χαμηλού επιπέδου και η εμφάνιση δυναμικών βιβλιοθηκών.) Στην πραγματικότητα, κατά τον εντοπισμό σφαλμάτων της εξόδου σκελετό Είναι καλύτερα να το διαβάσετε από το τέλος. Θα υπάρξει μια πρόκληση παρακάτω γράφω, το οποίο εμφανίζει ένα μήνυμα σφάλματος. Κοιτάμε παραπάνω και βλέπουμε την πρώτη λανθασμένη κλήση συστήματος - την κλήση ανοιχτό, το οποίο κάνει λάθος ENOENT ("αρχείο ή κατάλογος δεν βρέθηκε") προσπαθεί να ανοίξει /etc/foo/config.json. Εδώ πρέπει να βρίσκεται το αρχείο ρυθμίσεων.

Αυτό ήταν απλώς ένα παράδειγμα, αλλά θα έλεγα ότι το 90% του χρόνου που χρησιμοποιώ σκελετό, δεν υπάρχει τίποτα πιο δύσκολο να γίνει από αυτό. Παρακάτω είναι ένας πλήρης οδηγός εντοπισμού σφαλμάτων βήμα προς βήμα:

  • Αναστατωθείτε εξαιτίας ενός αόριστου μηνύματος σχετικά με ένα σφάλμα συστήματος από ένα πρόγραμμα
  • Επανεκκινήστε το πρόγραμμα με σκελετό
  • Βρείτε το μήνυμα σφάλματος στα αποτελέσματα ανίχνευσης
  • Πηγαίνετε ψηλότερα μέχρι να πραγματοποιήσετε την πρώτη αποτυχημένη κλήση συστήματος

Είναι πολύ πιθανό η κλήση συστήματος στο βήμα 4 να αποκαλύψει τι πήγε στραβά.

συμβουλές

Πριν σας δείξω ένα παράδειγμα πιο περίπλοκης αποσφαλμάτωσης, θα σας δείξω μερικά κόλπα για αποτελεσματική χρήση σκελετό:

ο άνθρωπος είναι φίλος σου

Σε πολλά συστήματα *nix, μια πλήρης λίστα κλήσεων συστήματος προς τον πυρήνα μπορεί να ληφθεί εκτελώντας άντρας συσκαλές. Θα δείτε πράγματα όπως brk(2), πράγμα που σημαίνει ότι μπορείτε να λάβετε περισσότερες πληροφορίες εκτελώντας άνδρας 2 βρκ.

Μικρή γκανιότα: άνθρωπος 2 πιρούνι μου δείχνει τη σελίδα για το κέλυφος πιρούνι() в GNU libc, το οποίο, όπως αποδεικνύεται, υλοποιείται με κλήση κλώνος (). Κλήση σημασιολογίας πιρούνι παραμένει το ίδιο εάν γράψετε ένα πρόγραμμα χρησιμοποιώντας πιρούνι(), και εκτελέστε ένα ίχνος - Δεν θα βρω καμία κλήση πιρούνι, αντί αυτών θα υπάρχουν κλώνος (). Τέτοιες τσουγκράνες σας μπερδεύουν μόνο αν αρχίσετε να συγκρίνετε την πηγή με την έξοδο σκελετό.

Χρησιμοποιήστε το -o για να αποθηκεύσετε την έξοδο σε ένα αρχείο

σκελετό μπορεί να δημιουργήσει εκτεταμένα αποτελέσματα, επομένως είναι συχνά χρήσιμο να αποθηκεύονται τα αποτελέσματα ιχνών σε ξεχωριστά αρχεία (όπως στο παραπάνω παράδειγμα). Αυτό βοηθά επίσης να αποφευχθεί η σύγχυση της εξόδου του προγράμματος με την έξοδο σκελετό στην κονσόλα.

Χρησιμοποιήστε -s για να δείτε περισσότερα δεδομένα ορίσματος

Ίσως έχετε παρατηρήσει ότι το δεύτερο μισό του μηνύματος σφάλματος δεν εμφανίζεται στο παραπάνω παράδειγμα ίχνους. Είναι επειδή σκελετό Η προεπιλογή δείχνει μόνο τα πρώτα 32 byte του ορίσματος συμβολοσειράς. Αν θέλετε να δείτε περισσότερα, προσθέστε κάτι σαν -s 128 στην κλήση σκελετό.

-y διευκολύνει την παρακολούθηση αρχείων, υποδοχών κ.λπ.

"Όλα είναι αρχείο" σημαίνει ότι τα συστήματα *nix κάνουν όλες τις εισόδους/εξόδους χρησιμοποιώντας περιγραφείς αρχείων, είτε αυτό ισχύει για ένα αρχείο είτε για σωλήνες δικτύου ή διαδιεργασιών. Αυτό είναι βολικό για προγραμματισμό, αλλά δυσκολεύει την παρακολούθηση του τι πραγματικά συμβαίνει όταν βλέπετε κοινά ανάγνωση и γράφω στο σύστημα κλήσης αποτελεσμάτων ιχνών.

Με την προσθήκη ενός τελεστή Ναι, θα αναγκάσεις σκελετό Σημειώστε κάθε περιγραφικό αρχείου στην έξοδο με μια σημείωση για το τι δείχνει.

Επισύναψη σε μια διαδικασία που ήδη εκτελείται με -p**

Όπως θα δείτε από το παρακάτω παράδειγμα, μερικές φορές χρειάζεται να εντοπίσετε ένα πρόγραμμα που εκτελείται ήδη. Εάν είναι γνωστό ότι εκτελείται ως διεργασία 1337 (ας πούμε, από την έξοδο ps), τότε μπορείτε να το εντοπίσετε ως εξής:

$ strace -p 1337
...system call trace output...

Μπορεί να χρειαστείτε δικαιώματα root.

Χρησιμοποιήστε το -f για την παρακολούθηση των θυγατρικών διεργασιών

σκελετό Από προεπιλογή, παρακολουθεί μόνο μία διαδικασία. Εάν αυτή η διαδικασία δημιουργεί θυγατρικές διεργασίες, τότε μπορεί να φανεί η κλήση συστήματος για δημιουργία θυγατρικής διαδικασίας, αλλά οι κλήσεις συστήματος της θυγατρικής διεργασίας δεν θα εμφανίζονται.

Εάν πιστεύετε ότι το σφάλμα βρίσκεται σε θυγατρική διαδικασία, χρησιμοποιήστε τη δήλωση -f, αυτό θα επιτρέψει την ιχνηλάτησή του. Το μειονέκτημα αυτού είναι ότι η έξοδος θα σας μπερδέψει ακόμα περισσότερο. Οταν σκελετό ανιχνεύει μια διαδικασία ή ένα νήμα, εμφανίζει μια ενιαία ροή συμβάντων κλήσης. Όταν ανιχνεύει πολλές διεργασίες ταυτόχρονα, μπορεί να δείτε την έναρξη μιας κλήσης που διακόπτεται από ένα μήνυμα , τότε - ένα σωρό κλήσεις για άλλους κλάδους εκτέλεσης και μόνο τότε - το τέλος του πρώτου <…συνέχισε η κλήση εστίασης>. Ή διαχωρίστε όλα τα αποτελέσματα ιχνών σε διαφορετικά αρχεία, χρησιμοποιώντας επίσης τον τελεστή -ff (λεπτομέρειες στο οδηγός επί σκελετό).

Φιλτράρετε τα ίχνη χρησιμοποιώντας -e

Όπως μπορείτε να δείτε, το αποτέλεσμα του ίχνους είναι ένας πραγματικός σωρός όλων των πιθανών κλήσεων συστήματος. Σημαία -e Μπορείτε να φιλτράρετε το ίχνος (βλ οδηγός επί σκελετό). Το κύριο πλεονέκτημα είναι ότι είναι πιο γρήγορο να εκτελέσετε ένα φιλτραρισμένο ίχνος από το να κάνετε ένα πλήρες ίχνος και μετά grep`στο. Για να είμαι ειλικρινής, σχεδόν πάντα δεν με νοιάζει.

Δεν είναι όλα τα λάθη κακά

Ένα απλό και συνηθισμένο παράδειγμα είναι ένα πρόγραμμα που αναζητά ένα αρχείο σε πολλά σημεία ταυτόχρονα, όπως ένα κέλυφος που αναζητά έναν κατάλογο που περιέχει ένα εκτελέσιμο αρχείο:

$ strace sh -c uname
...
stat("/home/user/bin/uname", 0x7ffceb817820) = -1 ENOENT (No such file or directory)
stat("/usr/local/bin/uname", 0x7ffceb817820) = -1 ENOENT (No such file or directory)
stat("/usr/bin/uname", {st_mode=S_IFREG|0755, st_size=39584, ...}) = 0
...

Τα ευρετικά όπως το "τελευταίο αποτυχημένο αίτημα πριν από την αναφορά σφάλματος" είναι καλές στην εύρεση σχετικών σφαλμάτων. Όπως και να έχει, είναι λογικό να ξεκινάμε από το τέλος.

Τα σεμινάρια προγραμματισμού C μπορούν να σας βοηθήσουν να κατανοήσετε τις κλήσεις συστήματος.

Οι τυπικές κλήσεις στις βιβλιοθήκες C δεν είναι κλήσεις συστήματος, αλλά μόνο ένα λεπτό στρώμα επιφάνειας. Έτσι, εάν καταλαβαίνετε τουλάχιστον λίγο πώς και τι πρέπει να κάνετε στο C, θα είναι ευκολότερο για εσάς να κατανοήσετε τα αποτελέσματα του ίχνους κλήσης συστήματος. Για παράδειγμα, αντιμετωπίζετε προβλήματα με τον εντοπισμό σφαλμάτων κλήσεων σε συστήματα δικτύου, δείτε το ίδιο κλασικό Bija's Guide to Network Programming.

Ένα πιο περίπλοκο παράδειγμα αποσφαλμάτωσης

Είπα ήδη ότι το παράδειγμα του απλού εντοπισμού σφαλμάτων είναι ένα παράδειγμα του τι πρέπει να αντιμετωπίσω κυρίως όταν εργάζομαι σκελετό. Ωστόσο, μερικές φορές απαιτείται πραγματική έρευνα, επομένως εδώ είναι ένα πραγματικό παράδειγμα πιο προηγμένου εντοπισμού σφαλμάτων.

bcron - χρονοπρογραμματιστής επεξεργασίας εργασιών, μια άλλη υλοποίηση του *nix daemon cron. Είναι εγκατεστημένο στον διακομιστή, αλλά όταν κάποιος προσπαθεί να επεξεργαστεί το χρονοδιάγραμμα, συμβαίνει αυτό:

# crontab -e -u logs
bcrontab: Fatal: Could not create temporary file

Εντάξει, αυτό σημαίνει bcron προσπάθησε να γράψει ένα συγκεκριμένο αρχείο, αλλά δεν τα κατάφερε και δεν θα παραδεχτεί γιατί. Αποκάλυψη σκελετό:

# strace -o /tmp/trace crontab -e -u logs
bcrontab: Fatal: Could not create temporary file
# cat /tmp/trace
...
openat(AT_FDCWD, "bcrontab.14779.1573691864.847933", O_RDONLY) = 3
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f82049b4000
read(3, "#Ansible: logsaggn20 14 * * * lo"..., 8192) = 150
read(3, "", 8192)                       = 0
munmap(0x7f82049b4000, 8192)            = 0
close(3)                                = 0
socket(AF_UNIX, SOCK_STREAM, 0)         = 3
connect(3, {sa_family=AF_UNIX, sun_path="/var/run/bcron-spool"}, 110) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f82049b4000
write(3, "156:Slogs #Ansible: logsaggn20 1"..., 161) = 161
read(3, "32:ZCould not create temporary f"..., 8192) = 36
munmap(0x7f82049b4000, 8192)            = 0
close(3)                                = 0
write(2, "bcrontab: Fatal: Could not creat"..., 49) = 49
unlink("bcrontab.14779.1573691864.847933") = 0
exit_group(111)                         = ?
+++ exited with 111 +++

Υπάρχει ένα μήνυμα σφάλματος κοντά στο τέλος γράφω, αλλά αυτή τη φορά κάτι είναι διαφορετικό. Πρώτον, δεν υπάρχει σχετικό σφάλμα κλήσης συστήματος, το οποίο συνήθως εμφανίζεται πριν από αυτό. Δεύτερον, είναι σαφές ότι κάπου κάποιος έχει ήδη διαβάσει το μήνυμα σφάλματος. Φαίνεται ότι το πραγματικό πρόβλημα είναι κάπου αλλού, και bcrontab απλά αναπαράγει το μήνυμα.

Αν κοιτάξεις άντρας 2 διάβασε, μπορείτε να δείτε ότι το πρώτο όρισμα (3) είναι ένας περιγραφέας αρχείου, τον οποίο το *nix χρησιμοποιεί για όλη την επεξεργασία I/O. Πώς μπορώ να μάθω τι αντιπροσωπεύει ο περιγραφέας αρχείου 3; Στη συγκεκριμένη περίπτωση, μπορείτε να τρέξετε σκελετό με χειριστή Ναι (δείτε παραπάνω) και θα σας το πει αυτόματα, αλλά για να καταλάβετε τέτοια πράγματα, είναι χρήσιμο να γνωρίζετε πώς να διαβάζετε και να αναλύετε τα αποτελέσματα ιχνών.

Η πηγή ενός περιγραφέα αρχείου μπορεί να είναι μία από τις πολλές κλήσεις συστήματος (όλα εξαρτώνται από το τι προορίζεται ο περιγραφέας - μια κονσόλα, μια υποδοχή δικτύου, το ίδιο το αρχείο ή κάτι άλλο), αλλά όπως και να έχει, αναζητούμε καλεί επιστρέφοντας 3 (δηλαδή αναζητούμε "= 3" στα αποτελέσματα ανίχνευσης). Σε αυτό το αποτέλεσμα υπάρχουν 2 από αυτά: ανοιχτό στην κορυφή και πρίζα Στη μέση. ανοιχτό ανοίγει το αρχείο αλλά κοντάΤο (3) θα δείξει στη συνέχεια ότι κλείνει ξανά. (Rake: οι περιγραφείς αρχείων μπορούν να επαναχρησιμοποιηθούν όταν ανοίγουν και κλείνουν). Κλήση υποδοχή () κατάλληλο γιατί είναι το τελευταίο πριν ανάγνωση(), και αποδεικνύεται ότι το bcrontab λειτουργεί με κάτι μέσω μιας υποδοχής. Η επόμενη γραμμή δείχνει ότι ο περιγραφέας αρχείου σχετίζεται με υποδοχή τομέα unix στην πορεία /var/run/bcron-spool.

Επομένως, πρέπει να βρούμε τη διαδικασία που σχετίζεται με υποδοχή unix στην άλλη πλευρά. Υπάρχουν μερικά προσεγμένα κόλπα για αυτόν τον σκοπό, τα οποία είναι και τα δύο χρήσιμα για τον εντοπισμό σφαλμάτων σε αναπτύξεις διακομιστή. Το πρώτο είναι να χρησιμοποιήσετε netstat ή νεότερο ss (κατάσταση πρίζας). Και οι δύο εντολές εμφανίζουν τις ενεργές συνδέσεις δικτύου του συστήματος και λαμβάνουν τη δήλωση -l για να περιγράψει τις υποδοχές ακρόασης, καθώς και τον χειριστή -p για να εμφανίσετε προγράμματα συνδεδεμένα στην πρίζα ως πελάτη. (Υπάρχουν πολλές πιο χρήσιμες επιλογές, αλλά αυτές οι δύο αρκούν για αυτήν την εργασία.)

# ss -pl | grep /var/run/bcron-spool
u_str LISTEN 0   128   /var/run/bcron-spool 1466637   * 0   users:(("unixserver",pid=20629,fd=3))

Αυτό υποδηλώνει ότι ο ακροατής είναι η εντολή διακομιστής inix, που εκτελείται με αναγνωριστικό διεργασίας 20629. (Και, κατά σύμπτωση, χρησιμοποιεί τον περιγραφέα αρχείου 3 ως υποδοχή.)

Το δεύτερο πραγματικά χρήσιμο εργαλείο για την εύρεση των ίδιων πληροφοριών ονομάζεται lsof. Παραθέτει όλα τα ανοιχτά αρχεία (ή τους περιγραφείς αρχείων) στο σύστημα. Ή μπορείτε να λάβετε πληροφορίες για ένα συγκεκριμένο αρχείο:

# lsof /var/run/bcron-spool
COMMAND   PID   USER  FD  TYPE  DEVICE              SIZE/OFF  NODE    NAME
unixserve 20629 cron  3u  unix  0x000000005ac4bd83  0t0       1466637 /var/run/bcron-spool type=STREAM

Το Process 20629 είναι ένας διακομιστής με μεγάλη διάρκεια ζωής, ώστε να μπορείτε να τον επισυνάψετε σκελετό χρησιμοποιώντας κάτι σαν strace -o /tmp/trace -p 20629. Εάν επεξεργαστείτε μια εργασία cron σε άλλο τερματικό, θα λάβετε μια έξοδο ίχνους με ένα σφάλμα. Και ιδού το αποτέλεσμα:

accept(3, NULL, NULL)                   = 4
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7faa47c44810) = 21181
close(4)                                = 0
accept(3, NULL, NULL)                   = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=21181, si_uid=998, si_status=0, si_utime=0, si_stime=0} ---
wait4(0, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], WNOHANG|WSTOPPED, NULL) = 21181
wait4(0, 0x7ffe6bc36764, WNOHANG|WSTOPPED, NULL) = -1 ECHILD (No child processes)
rt_sigaction(SIGCHLD, {sa_handler=0x55d244bdb690, sa_mask=[CHLD], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7faa47ab9840}, {sa_handler=0x55d244bdb690, sa_mask=[CHLD], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7faa47ab9840}, 8) = 0
rt_sigreturn({mask=[]})                 = 43
accept(3, NULL, NULL)                   = 4
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7faa47c44810) = 21200
close(4)                                = 0
accept(3, NULL, NULL)                   = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=21200, si_uid=998, si_status=111, si_utime=0, si_stime=0} ---
wait4(0, [{WIFEXITED(s) && WEXITSTATUS(s) == 111}], WNOHANG|WSTOPPED, NULL) = 21200
wait4(0, 0x7ffe6bc36764, WNOHANG|WSTOPPED, NULL) = -1 ECHILD (No child processes)
rt_sigaction(SIGCHLD, {sa_handler=0x55d244bdb690, sa_mask=[CHLD], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7faa47ab9840}, {sa_handler=0x55d244bdb690, sa_mask=[CHLD], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7faa47ab9840}, 8) = 0
rt_sigreturn({mask=[]})                 = 43
accept(3, NULL, NULL

(Τελευταίος αποδέχομαι() δεν θα ολοκληρωθεί κατά τον εντοπισμό.) Και πάλι, δυστυχώς, αυτό το αποτέλεσμα δεν περιέχει το σφάλμα που αναζητούμε. Δεν βλέπουμε μηνύματα που στέλνει ή λαμβάνει το bcrontag από την πρίζα. Αντίθετα, πλήρης έλεγχος της διαδικασίας (κλωνοποίηση, περίμενε4, SIGCHLD κ.λπ.) Αυτή η διαδικασία γεννά μια θυγατρική διαδικασία, η οποία, όπως μπορείτε να μαντέψετε, κάνει την πραγματική δουλειά. Και αν πρέπει να πιάσετε τα ίχνη της, προσθέστε την στην κλήση strace -f. Αυτό θα βρούμε όταν αναζητήσουμε το μήνυμα σφάλματος στο νέο αποτέλεσμα με strace -f -o /tmp/trace -p 20629:

21470 openat(AT_FDCWD, "tmp/spool.21470.1573692319.854640", O_RDWR|O_CREAT|O_EXCL, 0600) = -1 EACCES (Permission denied) 
21470 write(1, "32:ZCould not create temporary f"..., 36) = 36
21470 write(2, "bcron-spool[21470]: Fatal: logs:"..., 84) = 84
21470 unlink("tmp/spool.21470.1573692319.854640") = -1 ENOENT (No such file or directory)
21470 exit_group(111)                   = ?
21470 +++ exited with 111 +++

Τώρα, αυτό είναι κάτι. Η διεργασία 21470 λαμβάνει ένα σφάλμα "δεν επιτρέπεται η πρόσβαση" όταν προσπαθεί να δημιουργήσει ένα αρχείο στη διαδρομή tmp/spool.21470.1573692319.854640 (σχετικά με τον τρέχοντα κατάλογο εργασίας). Αν απλώς γνωρίζαμε τον τρέχοντα κατάλογο εργασίας, θα γνωρίζαμε επίσης την πλήρη διαδρομή και θα μπορούσαμε να καταλάβουμε γιατί η διαδικασία δεν μπορεί να δημιουργήσει το προσωρινό της αρχείο σε αυτόν. Δυστυχώς, η διαδικασία έχει ήδη τερματιστεί, επομένως δεν μπορείτε απλώς να τη χρησιμοποιήσετε lsof -p 21470 για να βρείτε τον τρέχοντα κατάλογο, αλλά μπορείτε να εργαστείτε προς την αντίθετη κατεύθυνση - αναζητήστε κλήσεις συστήματος PID 21470 που αλλάζουν τον κατάλογο. (Εάν δεν υπάρχουν, το PID 21470 πρέπει να τα έχει κληρονομήσει από τη μητρική του και αυτό έχει ήδη ολοκληρωθεί lsof -σελ δεν μπορεί να βρεθεί.) Αυτή η κλήση συστήματος είναι chdir (το οποίο είναι εύκολο να το ανακαλύψετε με τη βοήθεια σύγχρονων διαδικτυακών μηχανών αναζήτησης). Και εδώ είναι το αποτέλεσμα των αντίστροφων αναζητήσεων με βάση τα αποτελέσματα ανίχνευσης, μέχρι το διακομιστή PID 20629:

20629 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7faa47c44810) = 21470
...
21470 execve("/usr/sbin/bcron-spool", ["bcron-spool"], 0x55d2460807e0 /* 27 vars */) = 0
...
21470 chdir("/var/spool/cron")          = 0
...
21470 openat(AT_FDCWD, "tmp/spool.21470.1573692319.854640", O_RDWR|O_CREAT|O_EXCL, 0600) = -1 EACCES (Permission denied) 
21470 write(1, "32:ZCould not create temporary f"..., 36) = 36
21470 write(2, "bcron-spool[21470]: Fatal: logs:"..., 84) = 84
21470 unlink("tmp/spool.21470.1573692319.854640") = -1 ENOENT (No such file or directory)
21470 exit_group(111)                   = ?
21470 +++ exited with 111 +++

(Αν χαθείτε, ίσως θέλετε να διαβάσετε την προηγούμενη ανάρτησή μου σχετικά με τη διαχείριση διεργασιών *nix και τα κελύφη.) Επομένως, ο διακομιστής PID 20629 δεν έλαβε άδεια δημιουργίας αρχείου στη διαδρομή /var/spool/cron/tmp/spool.21470.1573692319.854640. Πιθανότατα, ο λόγος για αυτό είναι οι κλασικές ρυθμίσεις δικαιωμάτων συστήματος αρχείων. Ας ελέγξουμε:

# ls -ld /var/spool/cron/tmp/
drwxr-xr-x 2 root root 4096 Nov  6 05:33 /var/spool/cron/tmp/
# ps u -p 20629
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
cron     20629  0.0  0.0   2276   752 ?        Ss   Nov14   0:00 unixserver -U /var/run/bcron-spool -- bcron-spool

Εκεί είναι θαμμένος ο σκύλος! Ο διακομιστής εκτελείται ως cron χρήστη, αλλά μόνο ο root έχει άδεια εγγραφής στον κατάλογο /var/spool/cron/tmp/. Απλή εντολή chown cron /var/spool/cron/tmp/ θα αναγκάσει bcron λειτουργήσει σωστά. (Εάν δεν ήταν αυτό το πρόβλημα, τότε ο επόμενος πιο πιθανός ύποπτος είναι μια μονάδα ασφαλείας πυρήνα όπως το SELinux ή το AppArmor, οπότε θα έλεγξα το αρχείο καταγραφής μηνυμάτων του πυρήνα με dmesg.)

Σε συνολικά

Τα ίχνη κλήσεων συστήματος μπορεί να είναι συντριπτικά για έναν αρχάριο, αλλά ελπίζω να έχω δείξει ότι είναι ένας γρήγορος τρόπος για τον εντοπισμό σφαλμάτων μιας ολόκληρης κατηγορίας κοινών προβλημάτων ανάπτυξης. Φανταστείτε να προσπαθείτε να διορθώσετε μια πολυδιεργασία bcronχρησιμοποιώντας ένα πρόγραμμα εντοπισμού σφαλμάτων βήμα προς βήμα.

Η ανάλυση των αποτελεσμάτων του ίχνους προς τα πίσω κατά μήκος μιας αλυσίδας κλήσεων συστήματος απαιτεί δεξιότητα, αλλά όπως είπα, σχεδόν πάντα, η χρήση σκελετό, παίρνω απλώς το αποτέλεσμα του ίχνους και ψάχνω για σφάλματα ξεκινώντας από το τέλος. ΤΕΛΟΣ παντων, σκελετό με βοηθά να εξοικονομήσω πολύ χρόνο στον εντοπισμό σφαλμάτων. Ελπίζω να είναι χρήσιμο και σε εσάς.

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο