The Power of 10: Rules for Developing Safety-Critical Code

Ο Gerard J. Holzmann του NASA/JPL δίνει 10 κανόνες, εύκολους να τους θυμάται κανείς, για τη συγγραφή safety critical code. Παρόλαυτά είναι καλό να τους θυμόμαστε και όταν γράφουμε “κανονικό” κώδικα. Κυρίως γιατί σε αντίθεση με άλλα standard, δεν είναι σελίδες επί σελίδων, άρα μπορούμε να τους θυμόμαστε εύκολα και δεν φτάνουν να κυβερνάνε ακόμα και την αισθητική του κώδικά μας (ή ακόμα και το πόσα “spaces” είναι το [TAB]). Οι 10 κανόνες είναι:

Adhering to a set of 10 verifiable coding rules can make the analysis of critical software components more reliable.

  1. Restrict all code to very simple control flow constructs—do not use goto statements, setjmp or longjmp constructs, or direct or indirect recursion.
  2. Give all loops a fixed upper bound. It must be trivially possible for a checking tool to prove statically that the loop cannot exceed a preset upper bound on the number of iterations. If a tool cannot prove the loop bound statically, the rule is considered violated.
  3. Do not use dynamic memory allocation after initialization.
  4. No function should be longer than what can be printed on a single sheet of paper in a standard format with one line per statement and one line per declaration. Typically, this means no more than about 60 lines of code per function.
  5. The code’s assertion density should average to minimally two assertions per function. Assertions must be used to check for anomalous conditions that should never happen in real-life executions. Assertions must be side-effect free and should be defined as Boolean tests. When an assertion fails, an explicit recovery action must be taken such as returning an error condition to the caller of the function that executes the failing assertion. Any assertion for which a static checking tool can prove that it can never fail or never hold violates this rule.
  6. Declare all data objects at the smallest possible level of scope.
  7. Each calling function must check the return value of nonvoid functions, and each called function must check the validity of all parameters provided by the caller.
  8. The use of the preprocessor must be limited to the inclusion of header files and simple macro definitions. Token pasting, variable argument lists (ellipses), and recursive macro calls are not allowed. All macros must expand into complete syntactic units. The use of conditional compilation directives must be kept to a minimum.
  9. The use of pointers must be restricted. Specifically, no more than one level of dereferencing should be used. Pointer dereference operations may not be hidden in macro definitions or inside typedef declarations. Function pointers are not permitted.
  10. All code must be compiled, from the first day of development, with all compiler warnings enabled at the most pedantic setting available. All code must compile without warnings. All code must also be checked daily with at least one, but preferably more than one, strong static source code analyzer and should pass all analyses with zero warnings.

Όλο το άρθρο παρουσιάζει εξαιρετικό ενδιαφέρον και ειδικότερα η ανάλυση του σκεπτικού πίσω από τους εμπειρικούς αυτούς κανόνες.

[via The Power of 10: Rules for Developing Safety-Critical Code]

7 thoughts on “The Power of 10: Rules for Developing Safety-Critical Code

  1. Δεν είμαι σίγουρος ότι όλοι αυτοί οι κανόνες έχουν νόημα όταν δε γράφεις κώδικα για το Mars Rover ή κάτι αντίστοιχο εν πάσει περιπτώσει.

    Για παράδειγμα ο (3). Αν έχεις περιορισμένα/δεδομένα resources για το πρόγραμμά σου το καταλαβαίνω. Στη γενική περίπτωση όμως πώς θα αποθηκεύσεις δεδομένα που σου έρχονται από τον έξω κόσμο; Με buffers συγκεκριμένου μεγέθους; Και όταν γεμίσουν θα τους στοιβάζεις σε μια δομή δεδομένων πάλι συγκεκριμένου μεγέθους; Και ότι έρθει επιπλέον το πετάς, ακόμη και αν έχεις μνήμη στη διάθεσή σου; Και καλά, έστω ότι τα έχεις υπολογίσει όλα στην εντέλεια και σου χρειάζεται χ μνήμη για buffer/cache/whatever στο παρόν σύστημα. Όταν θα κάνεις αναβάθμιση του hardware θα έχεις μνήμη που θα κάθεται; (Φυσικά το Mars Rover που θα κάτσει για πάντα στον Άρη μέχρι να εξαϋλωθεί δεν πρόκειται να του γίνει ποτέ αναβάθμιση)

    Επίσης το (9). Με περιορισμό στο βάθος του pointer dereferencing δεν έχεις πολύ χρήσιμα πράγματα, όπως trampolining και virtual dispatch για να υλοποιήσεις method overriding και κατ’ επέκταση OOP. Αν και δε νομίζω πως η χρήση ΟΟΡ θα ήταν αποδεκτή από το συγγραφέα έτσι κι αλλιώς.

    Τα υπόλοιπα πάντως τα προσυπογράφω, ειδικά όσα δεν απευθύνονται σε C κώδικα μόνο.

  2. 8/10 από εσένα είναι καλό score αποδοχής. Από την άλλη, παρόλο που για συμβατικό software οι κανόνες αυτοί μπορεί να είναι υπερβολικοί, δεν παύουν να είναι χρήσιμοι, όταν είναι στο μυαλό μας.

    Κοιτάζοντας το βιβλίο “Γλώσσες Προγραμματισμού (σημειώσεις)” (το θυμάσαι; Είναι των Παπαγεωργίου και Σταφυλοπάτη) διαβάζω:

    ΜΗΝ ΑΝΑΒΑΛΛΕΙΣ ΠΟΤΕ ΓΙΑ ΤΟ ΧΡΟΝΟ ΕΚΤΕΛΕΣΗΣ ΟΤΙ ΜΠΟΡΕΙΣ ΝΑ ΚΑΝΕΙΣ ΣΤΟΝ ΧΡΟΝΟ ΜΕΤΑΦΡΑΣΗΣ

    Νομίζω “συμβατικοποιήσαμε” τον #3. Αλλά ακόμα και εάν δε το κάναμε, νομίζω πως το πνεύμα του #3 έχει να κάνει με το εάν κάποιος έχει μελετήσει / μοντελοποιήσει το πρόβλημα που προσπαθεί να λύσει με το πρόγραμμά του. Είναι καλό να ξέρεις πόσο θα είναι και πως θα έρχεται το input, να ξέρεις τις απαιτήσεις σε μνήμη και να “πιάνεις” τις εξαιρέσεις.

    Όσο για το #9, ξέρεις τη γνώμη μου για τον OOP, ή τουλάχιστον το πως γίνεται market στα νεαρά μυαλά: Μη σας νοιάζουν οι κλάσεις του συστήματος, μπορείτε να κληρονομήσετε ότι θέλετε και να αλλάξετε τα υπόλοιπα και επιπλέον έχουμε και αυτό που το λένε πολυμορφισμό για να είναι τα πράγματα ακόμα καλύτερα. Δίνουμε μπαζούκας σε κάποιον που έχει ανάγκη από μια μυγοσκοτώστρα και μετά παραπονιόμαστε που δεν πετυχαίνει τα μυγάκια.

  3. :-) Eιδικά τώρα που μεγαλώνω, το ΟΟΡ έχει αλλάξει όψη στη σκέψη μου. Διαλέγω composition over inheritance σχεδόν σε κάθε περίπτωση.

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

  4. Οι κανόνες (3) και (9) με παραξενεύουν λίγο.

    Το (3) έρχεται σε πλήρη αντίθεση με το GNU Coding Standards, και φαίνεται να “προωθεί” τη χρήση statically sized buffers. Έχω δει άπειρες βλακείες να γράφονται με στυλ “έλα, εντάξει, ποιός θα χρειαστεί ποτέ πάνω από 256 bytes για filename…” οπότε αν και καταλαβαίνω το σκεπτικό, μου ξυνίζει λίγο. Από την άλλη, στο Solaris ας πούμε, το maxusers tunable καθορίζει το στατικό initial μέγεθος από κάμποσες δομές του πυρήνα, ακριβώς για να είναι προβλέψιμο το μέγεθός τους (και να λειτουργεί ως implicit resource limit). Δεν ξέρω… μάλλον θα προτιμούσα κάτι σαν “όταν πιστεύετε ότι υπάρχει λόγος ένα resource να είναι περιορισμένο, προσπαθείστε να κρατήσετε ‘bounded’ το μέγεθος των dynamic allocations που κάνετε γι αυτό το resource”. Αυτό μοιάζει και με τα slab sizes των allocators ή αντίστοιχες δομές, και μου φαίνεται πιο … αποδεκτό :)

    Το (9) είναι επίσης λίγο υπερβολικό επίσης. Χωρίς function pointers δε θα είχαμε filesystems, sockets, το VFS, ή άλλα χρήσιμα abstractions. Πιθανόν να μπορούσαμε να γράψουμε spaghetti function dispatch, αλλά δε μ’ αρέσει αυτό καθόλου όταν το βλέπω.

    If αυτό, τότε τάδε function call, αλλιώς αν εκείνο, το άλλο function, αλλιώς αν κάτι τρίτο, το παραπέρα specialized function, αλλιώς το default function, και ούτω καθεξής. Όταν βλέπω τέτοια constructs συνήθως παραπονιέμαι ότι “τo πρόγραμμα φωνάζει από μακριά πως θέλει dispatch table”.

  5. @Giorgos Keramidas:
    Οι κανόνες (3) και (9) έχουν νόημα όταν μιλάμε για embedded systems, και ειδικότερα αυτά τα οποία έχουν[*] περιορισμένα resources. Οπότε πρέπει να έχεις συνέχεια στο μυαλό σου πως το σύστημα έχει όρια και δεν πρέπει να τα ξεπερνάς.

    Ακόμα όμως και στην περίπτωση που μιλάμε για “κανονικό” σύστημα, το να έχεις static buffers δεν είναι το ίδιο με το να λες “έλα μωρέ, ποιος θα χρειαστεί filename μεγαλύτερο από 256 χαρακτήρες, βάλε τόσο να έχει και αέρα”. Στην πραγματικότητα σημαίνει πως όταν βάζεις όρια στο πρόγραμμά σου, πρέπει και να τα ελέγχεις και να τα τηρείς.

    Όσο για τον κανόνα #9, πιο σημαντικό IMHO είναι το “Pointer dereference operations may not be hidden in macro definitions or inside typedef declarations”. Σκέψου να στείλεις ένα upgrade στον Phoenix και να την πατήσεις από pointer που δεν τον έχει πιάσει ούτε ο simulator, ούτε το τοπικό αδερφάκι του που κάνεις τις δοκιμές.

    [*] – Ή αν δεν έχουν σήμερα, θα έχουν μετά από κάποιο καιρό, γιατί π.χ. στο Phoenix δεν μπορείς να προσθέσεις ούτε RAM, ούτε να αλλάξεις επεξεργαστή (Αυτό μου θυμίζει κάπως τον V’ger).

  6. Όπως λέει ο adamo αυτοί οι κανόνες έχουν νόημα σε embedded & mission critical systems, όπου δεν νοείται να έχεις non-deterministic behavior. Ή με άλλα λόγια:

    * Τι θα κάνει το rover/shuttle όταν η malloc αποτύχει;

  7. Κατ’αρχάς critical code δεν έχουν μόνο τα διαστημόπλοια. Έχουν και τα αεροπλάνα καθώς και όλοι οι device drivers. Επειδή εγώ λοιπόν γράφω μέρα-νύχτα device drivers, here are my two cents…

    #1 Restrict all code to very simple control flow constructs—do not use goto statements, setjmp or longjmp constructs, or direct or indirect recursion.

    Πρόσθεσε εδώ και το DON’T USE FRIGGIN’ EXCEPTIONS for non-exceptional reasons. Τα έχει πει και ο Raymond Chen και ο Joel. Τα exceptions σκίζουν το control flow, μην τα χρησιμοποιείτε.
    Τα goto “προς τα κάτω” επιτρέπονται, μην γινόμαστε υπερβολικοί και υποκριτές. Τι είναι δηλαδή το “break” πέρα από ένα labeless goto με well-known target; Απλά συνήθως τα απαγορεύουν γιατί γίνεται μετά γίνεται misuse.

    #3 Do not use dynamic memory allocation after initialization.

    Πες τα ν’αγιάσεις… Εμείς στον κώδικα των drivers κατά το initialization κάνουμε allocate τα λεγόμενα lookaside lists, δηλαδή λίστες με έτοιμα memory blocks γνωστού μεγέθους τα οποία χρειαζόμαστε κατά συρροή στα I/O operations. Έτσι έχουμε και καλύτερο performance, και δεν προκαλούμε kernel heap fragmentation (άουτς… ) και παρέχουμε εγγυήσεις καλής λειτουργίας γιατί αν το lookaside list αδειάσει απλά αποτυγχάνεις το operation και καθάρισες. Αυτός είναι κώδικας που θα μπεις στον κόπο να τον τεστάρεις, ενώ το generic “out of memory” αποκλείεται.

    Θα μου πεις “Δηλαδή να έχω δικό μου memory manager για κάθε structure που γίνεται allocate δυναμικά;;;”. Ναι φίλε, αν θέλεις να θεωρείς τον κώδικά σου critical πρέπει να ιδρώσεις και λιγάκι.

    #4 No function should be longer than … one line per statement …

    Θεός. Για τις functions είναι προφανές. Το άλλο όμως;
    Το “Χ=++Υ;” είναι ΔΥΟ statements όχι ένα.
    Γράφτο λοιπόν:
    ++Y;
    X=Y;
    και ΑΠΟΚΛΕΙΕΤΑΙ ΠΟΤΕ ΝΑ ΤΟ ΚΑΝΕΙΣ ΛΑΘΟΣ. ΑΠΟΚΛΕΙΕΤΑΙ.

    #5 The code’s assertion density should …

    Θεός και πάλι. Κώδικας χωρίς asserts είναι για την παιδική χαρά, όχι για critical projects. Critical ανάμεσα στα άλλα σημαίνει και ελαχιστοποίηση του χρόνου εντοπισμού των προβλημάτων και ελαχιστοποίηση της πιθανότητας μια “αθώα” αλλαγίτσα να προκαλέσει απρόσμενα και καλά κρυμμένα side-effects. Asserts come to the rescue.

    #7 … each called function must check the validity of all parameters provided by the caller.

    Αυτό εμείς το κάνουμε στα “external boundaries”. Εκεί δηλαδή όπου μπαίνει input στο κάθε “σύστημα” (ή στο κάθε module). Έτσι οι εσωτερικές ρουτίνες έχουν εγγύηση ότι οι παράμετροι πάνω στις οποίες δουλεύουν είναι valid. Oι εσωτερικές ρουτίνες παίζουν με assert ενώ οι boundary ρουτίνες κάνουν διεξοδικούς ελέγχους και return error code με το παραμικρό λαθάκι.

    Μάλιστα επειδή πουλάμε και SDK, όλες οι ρουτίνες του API στο debug build βγάζουν και debug message για το ποιά παράμετρος τους κάθησε στο λαιμό ΚΑΙ ΓΙΑΤΙ. Λίγη δουλίτσα παραπάνω για τον προγραμματιστή, που εξαργυρώνεται σε ΑΤΕΛΕΙΩΤΕΣ γλυτωμένες ΩΡΕΣ support.

    #8 The use of the preprocessor …

    Εγώ θα πώ “The use of the preprocessor requires wisdom”. Αν έχεις διαβάσει αρκετά σχετικά με macros ώστε να ξέρεις τα pitfalls έχει καλώς. Τα macros είναι μεν επικύνδινα αλλά ταυτόχρονα και υπερπολύτιμα εργαλεία.

    #9 The use of pointers …

    Σωστός. Η ανάγκη για multiple chained pointer dereferences δείχνει data structure design χωρίς mission critical attitude. Είναι δύσκολο στο debugging και εύκολο στο να τα κάνεις μαντάρα και να χάσεις τη μπάλα.

    #10 … with all compiler warnings enabled … All code must compile without warnings… All code must also be checked daily …

    Όλοι συμφωνούν, ελάχιστοι το κάνουν.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s