程序代写案例-SWEN90010

欢迎使用51辅导,51作业君孵化低价透明的学长辅导平台,服务保持优质,平均费用压低50%以上! 51fudao.top
High Integrity Systems Engineering
Subject Notes for SWEN90010
School of Computing
and
Information Systems
The University of Melbourne r>1
Contents
1 Introduction to High Integrity Systems Engineering 7
1.1 Subject information . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.2 Introduction to high integrity systems . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.3 Subject outline . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2 Safety-critical and Security-critical systems engineering 12
2.1 An introduction to software safety . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.2 Why safety engineering? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.3 Safety engineering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.4 Accidents and safety engineering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.4.1 Case Study: The A320 Airbus . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.4.2 Case study: Warsaw, Okecie – 14 September 1993 . . . . . . . . . . . . . . . . 20
2.4.3 Case Study: Boeing 737 MAX . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.4.4 The role of accident analysis in safety engineering . . . . . . . . . . . . . . . . 25
2.5 Tools and techniques for safety engineering . . . . . . . . . . . . . . . . . . . . . . . . 27
2.5.1 Safety standards and safety lifecycles . . . . . . . . . . . . . . . . . . . . . . . 27
2.5.2 Preliminary hazard analysis . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
2.6 Hazard Analysis Methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
2.6.1 HAZOPS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
2.6.2 Fault Tree Analysis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
2.6.3 FMEA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
2.7 Threat Modelling for Security . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
2.7.1 The STRIDE Method . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
2.7.2 Attack Trees . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
3 Model-based specification 49
2
3.1 What is formal specification? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
3.2 What is model-based specification? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
3.3 The costs of formal specification . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
3.4 Logic and set theory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
3.5 Introduction to Alloy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
3.5.1 Key characteristics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
3.6 Alloy Logic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
3.6.1 Everything is a relation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
3.6.2 Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
3.6.3 Temporal Logic: specifying systems that change over time . . . . . . . . . . . . 61
3.7 Alloy Language . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
3.7.1 Modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
3.7.2 Signatures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
3.7.3 Facts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
3.7.4 Predicates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
3.7.5 Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
3.7.6 Assertions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
3.7.7 The complete LastPass example . . . . . . . . . . . . . . . . . . . . . . . . 65
3.8 Analysis with the Alloy Analyser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
3.8.1 Runs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
3.8.2 Checks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
3.8.3 Scope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
3.9 Abstraction and incremental specification of state machines using Alloy operators . . . . 69
3.9.1 The specification process . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
3.10 Trace-Based Modelling in Alloy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
3.11 Verification and validation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
4 Introduction to Ada 78
4.1 The history of Ada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
4.2 Ada for programming “in the large” . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
4.3 Hello World . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
4.4 Specifications and program bodies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
4.5 Comments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
4.6 Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
3
4.6.1 The predefined Ada type system . . . . . . . . . . . . . . . . . . . . . . . . . . 83
4.6.2 Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
4.6.3 Defining new types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
4.7 Control Structures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
4.8 Procedural Abstractions and Functional Abstractions . . . . . . . . . . . . . . . . . . . 90
4.8.1 Parameter Modes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
4.8.2 Calling subprograms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
4.9 access Types: Passing Parameters By Reference . . . . . . . . . . . . . . . . . . . . 91
4.9.1 Access Types to Read-Only Data . . . . . . . . . . . . . . . . . . . . . . . . . 94
4.9.2 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
4.10 Example: Insertion sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
4.11 Concurrency with tasks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
4.12 Further reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
5 Safe programming language subsets 100
5.1 Principles of correctness by construction . . . . . . . . . . . . . . . . . . . . . . . . . . 100
5.2 Safe programming language subsets . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
5.3 SPARK . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
5.3.1 SPARK Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
5.3.2 What is left out of Ada? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
5.3.3 Why bother with SPARK or other safe programming language subsets? . . . . . 106
5.4 SPARK Examiner . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
5.4.1 Annotations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
5.4.2 SPARK Examiner . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
6 Design by contract 111
6.1 Introduction to Design by Contract . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
6.1.1 History . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
6.1.2 Some Definitions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
6.2 Specifying ADT interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
6.2.1 ADT interfaces as natural-language comments . . . . . . . . . . . . . . . . . . 112
6.2.2 ADT interfaces as invariants, preconditions, and postconditions . . . . . . . . . 113
6.2.3 ADT interfaces as contracts . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
6.2.4 Formal contracts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
4
6.2.5 Advantages and disadvantages of formal contracts . . . . . . . . . . . . . . . . 117
6.3 Contracts in SPARK . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
6.3.1 Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
6.3.2 Similarity to Alloy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
6.3.3 Tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
6.4 Additional reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
7 Reasoning about program correctness 122
7.1 Introduction to reasoning about programs . . . . . . . . . . . . . . . . . . . . . . . . . 123
7.1.1 The correctness of binary search . . . . . . . . . . . . . . . . . . . . . . . . . . 123
7.2 A small programming language . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
7.3 Hoare logic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
7.3.1 Introduction to Hoare logic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
7.3.2 An introductory example — Incrementing an integer . . . . . . . . . . . . . . . 127
7.4 The rules of Hoare logic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
7.4.1 Assignment axiom . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
7.4.2 Consequence rule . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
7.4.3 Sequential composition rule . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
7.4.4 Empty statement axiom . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
7.4.5 Conditional rule . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
7.4.6 Iteration rule . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
7.4.7 Establishing loop invariants . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
7.4.8 Array assignment rule . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
7.4.9 Procedure call rule . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
7.4.10 Other rules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
7.5 Mechanising Hoare logic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
7.6 Dijkstra’s weakest precondition calculus . . . . . . . . . . . . . . . . . . . . . . . . . . 147
7.6.1 Transformers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
7.6.2 Program proof . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
7.7 Additional reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
8 Advanced Verification 149
8.1 Pointers and Aliasing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
8.2 Safe Programming with Pointers: Avoiding Unsafe Aliasing . . . . . . . . . . . . . . . 150
5
8.2.1 Unsafe Aliasing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
8.2.2 Avoiding Unsafe Aliasing in SPARK . . . . . . . . . . . . . . . . . . . . . . . 151
8.2.3 Anti-Aliasing Assumptions in SPARK . . . . . . . . . . . . . . . . . . . . . . . 152
8.2.4 SPARK Pointer Checks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
8.2.5 Other Programming Languages: Rust . . . . . . . . . . . . . . . . . . . . . . . 153
8.3 Separation Logic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
8.3.1 Pointers in C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
8.3.2 Separation Logic (for C) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
8.3.3 The Meaning of Separation Logic Statements . . . . . . . . . . . . . . . . . . . 156
8.3.4 The Rules of Separation Logic . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
8.3.5 An Example Proof . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
8.4 Security: Information Flow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
9 Fault-tolerant design 162
9.1 Introduction to fault-tolerance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
9.2 Hardware Redundancy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
9.2.1 Redundancy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
9.2.2 Static pairs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
9.2.3 Redundancy and voting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
9.2.4 N-Modular Redundancy (NMR) . . . . . . . . . . . . . . . . . . . . . . . . . . 170
9.3 Software Redundancy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
9.3.1 N-Version Programming . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
9.3.2 Recovery blocks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
9.4 Byzantine Failures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
9.5 Information Redundancy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
9.5.1 Duplication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
9.5.2 Parity coding . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
9.5.3 Checksums . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179
9.6 Fault tolerance in practice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179
9.6.1 Airbus A310–A380 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179
9.6.2 Boeing 777 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
9.6.3 Boeing 737 MAX Flight Control Redesign . . . . . . . . . . . . . . . . . . . . 180
9.7 Additional reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
6
Chapter 1
Introduction to High Integrity Systems
Engineering
1.1 Subject information
Subject aims
The main aim of the subject is to explore the principles, techniques and tools used to analyse, design
and implement high integrity systems. High integrity systems are systems that must operate with critical
levels of security, safety, reliability, or performance. The engineering methods required to develop high
integrity systems go well beyond the standards for less critical software.
Topics will include:
• high integrity system definition;
• programming languages for high-integrity systems;
• safety and security analysis;
• modelling and analysis;
• fault tolerance; and
• proving program correctness.
Subject outcomes
On completion of this subject students should be able to:
• Demonstrate an understanding of the issues facing high integrity systems developers.
• Demonstrate the ability to analyse highly dependable systems and to synthesise requirements and
operating parameters from their analysis.
7
• Demonstrate the ability to choose and make trade-offs between different software and systems
architectures to achieve multiple objectives.
• Develop algorithms and code to meet high integrity requirements.
• Demonstrate the ability to assure high integrity systems.
Generic Skills
The subject is a technical subject. The aim is to explore the various approaches to building software with
specific attributes into a system.
The subject will also aim to to develop a number of generic skills.
• We encourage independent research in order to develop your ability to learn independently, assess
what you learn, and apply what you learn.
• You will be developing experience at empirical software engineering, and the interpretation and
assessment of quantitative and qualitative data.
• Finally, and perhaps most importantly, you will be encouraged to develop critical analysis skills
that will complement and round what you have learnt so far in your degree.
Preconditions
The reason for giving preconditions is that the specify the knowledge assumed at the start of the subject
and consequently the knowledge and skills upon which the subject builds. Preconditions also allow
students from a Universities other than The University of Melbourne and who do not have the formal
prerequisites listed above to judge whether or not they have the knowledge required for the subject.
Specifically the subject assumes:
1. Knowledge and experience of software engineering processes and practices, the risks associated
with software and system development, quality and quality assurance, software process, and soft-
ware project management.
2. Knowledge and experience of requirements elicitation and analysis techniques, requirements mod-
elling and specification, use-case modelling, and UML or similar modelling notations.
3. Knowledge and experience of software architectures, architectural styles and design patterns, de-
sign with UML or similar graphical notation, developing code to meet architectural and detailed
designs.
4. Knowledge and experience of discrete mathematics, including predicate logic and formal proof.
Resources
The LMS contains information about the subject staff, mode of delivery, and assessment criteria. It is the
primary resource for the subject.
8
All announcements will be made via the LMS.
It also contains all assignment sheets, notes, lecture recordings, tutorial/workshop sheets and solutions.
http://www.lms.unimelb.edu.au/.
1.2 Introduction to high integrity systems
What is a high integrity system?
High integrity systems a software controlled systems in which, in the event they fail, could result in harm
to humans (including loss of life), harm on the environment, mass destruction of property, or harm to
society in general.
There are four broad areas that are commonly considered as high integrity systems:
• Safety-critical systems: Systems whose failure may result in harm to humans or the environment.
Examples include aerospace systems, medical devices, automotive systems, power systems, and
railway systems.
• Security-critical systems: Systems whose failure may result in breach of security. Examples in-
clude defence systems, government data stores.
• Mission-critical systems: Systems whose failure may result in the failure of some deliberative
missions. Examples include navigation systems on autonomous aerospace vehicles, computer-
aided dispatch systems (e.g. in emergency services), and command-and-control systems in armed
forces.
• Business-critical systems: Systems whose failure may result in extreme financial loss to a business
or businesses. Examples include stock exchange systems, trade platforms, and even accounting
systems (e.g. in a bank).
What is high integrity systems engineering?
The high impact of failure of a high integrity system means that the software controlling the system
must be highly dependable. The standard software engineering methods used for constructing good soft-
ware systems typically do not lead to such dependability. As such, specialised methods for engineering
software are required for high integrity systems.
Avizienis et al.’s work on dependability attributes help us to understand high integrity systems based on
six key attributes, shown in Figure 1.1. For a single high integrity system, one or more of the above
attributes will be of high importance.
How do we achieve high-integrity?
High integrity is achieved using a combination of:
• Rigorous engineering processes, such as strict configuration management, auditing, and review
process. We will not introduce any new theory on these topics in this subject.
9
Maintainability
Integrity
Availability
Safety
Reliability
Confidentiality
Dependability
Readiness
for use Continuity of
Service
No catastrophic
consequences
for users or the
environment
No unauthorised
disclosure of
confidential
information
No improper
system changes
The ability to undergo
repairs and evolution.
Figure 1.1: Dependability model from “Avizienis, Laprie & Randell: Fundamental Concepts of Relia-
bility”
• Specialised analysis and design methods, such as safety & hazard analysis and fault prediction
methods. These will be covered in Part II of the subject.
• Specialised modelling and analysis methods/notations, such as mathematical logic and set theory.
These will be covered in Part III of the subject.
• Fault tolerant design methods, such as redundancy, design diversity, and fault detection mecha-
nisms. These will be covered in Part IV of the subject.
• Specialised assurance methods, such as model checking, proof, and model-based testing. These
will be covered in Part V of the subject.
How do we achieve high-integrity?
Methods that are otherwise considered too expensive to apply to “standard” software systems become
cost-effective in high integrity domains. This is because the cost of failure far outweighs the effort
involved required to engineer a dependable product.
Of importance is that high dependability cannot be demonstrated by just running large set of tests. Al-
though testing still plays an important role in high integrity systems, a really large set of tests is still
usually only a fraction of all possible behaviour. As such, testing does not demonstrate dependability.
Perhaps the important lesson to learn from this introduction is:
“Program testing can be used to show the presence of bugs, but never to show their ab-
sence!” — Edsger W. Dijkstra [1].
1.3 Subject outline
Now that we have a better idea about high integrity systems engineering, we define the subject outline:
Part I: Introduction to high integrity systems.
10
Part II: Safety- and security-critical systems.
Topics will be drawn from: safety engineering, and accident & hazard analysis for high-
integrity systems; security analysis, cryptography.
Part III: Modelling and analysis.
Topics will be drawn from: discrete maths for software engineering, model-based specifica-
tion and analysis, proof, and lightweight verification.
Part IV: High integrity systems design.
Topics will be drawn from: reliability, fault detection, fault containment, fault tolerant design,
and design by contract.
Part V: Assurance.
Topics will be drawn from: proving program correctness, programming languages for high
integrity systems, safe-subset programming languages, programming tools for high integrity
verification, and model-based testing.
Bibliography
[1] E. W. Dijkstra. Notes on structured programming. T. H. Report 70-WSK-03, Technological Univer-
sity Eindhoven, Department of Mathematics, 1970.
11
Chapter 2
Safety-critical and Security-critical
systems engineering
2.1 An introduction to software safety
The trend in systems engineering is toward multi-disciplinary systems. Many of these systems are de-
ployed outside of the standard desktop domain on which programming is typically taught. Such a trend
has safety implications: if a piece of software is used to control a hardware system, it may have safety
related issues.
For example, consider the following list of applications that have severe safety implications if they fail:
• Computer Aided Dispatch Systems (CAD);
• The A320 Airbus Electronic Flight Control System (EFCS);
• The Boeing 737 MAX 8 MCAS system
• Biomedical technology, such as pacemakers, and surgical robots;
• Train protection systems;
• Space shuttle;
• Automated vehicle braking systems; and
• Chemical plant control systems.
Such systems have influence well beyond the desktop and an individual user. Software itself cannot harm
people – it is merely an abstraction. However, software, when used to control hardware or other parts of
a system (e.g. to make decisions or send orders on behalf of a human), can be a root cause of accidents
and incidents.
The systems described above all rely on an integration of different technologies, and different engineering
disciplines. What is perhaps new to most people in this subject is the reliance on computer systems and
software to perform safety critical system functions.
12
Aims
The aim of the module on safety engineering is to develop an understanding of the safety engineering
approach to developing safety-critical systems. Specifically:
• We will study accidents to get an idea of how they arise, how they are analysed, and what can be
done to prevent or mitigate accidents.
• We will study safety engineering processes as the pathway to developing and assuring safety criti-
cal systems.
• We will study the methods of safety analysis and assurance that can be applied at all stages of the
development process.
2.2 Why safety engineering?
In the examples above, software is part of a much larger and much more complex environment. Running
a huge set of tests to show that your software behaves as you expected does not really impact safety a
great deal.
In safety-critical systems, testing is a necessary but insufficient means of assuring a safe system. Re-
member that testing is about finding faults – not about showing correctness, and especially not about
showing safety.
Safety and security is not just a software issue.
System safety and security involve the hardware that the software is running on, the physical devices to
which the software is connected the people which must operate the systems, and operating procedures in
force.
Software and its relation to safety
The following are important themes in safety engineering:
• Software is always part of a larger system.
• Design faults can exist at various levels:
– Written requirements do not conform to the actual requirements.
– The specification does not conform to the written requirements.
– Source code does not conform to the specification.
– Object code does not match the source code.
• Incorrect requirements are a major cause of accidents.
• Simple design faults rarely cause accidents on their own.
• Human error is inevitable:
“Components developed over several years are hard to modify. Components which have
taken several million years to evolve are almost impossible to modify.”
13
Is safety equivalent to correctness?
One initial mistake to make is to think that, provided our software is correct, it will be safe. This is
fallacious thinking.
Correctness – Correctness alone does guarantee safety.
Consider a knife. If we assume that it is a good knife and so does not contain any “faults” then is it safe?
The answer of course relies on how it interacts with its environment, not on whether or not it contains
faults. If I do not get my thumb out of the way while cutting, I can harm my thumb. The knife is not
safe. But it can still function correctly.
Now, let’s turn to a systems engineering example. I can design a braking systems for a car that is 100%
correct, but 0% safe. My design consists of a braking system with a brake pedal that does nothing.
Implementing the design correctly is straightforward: do nothing. But this is surely not safe – unless
the wheels are designed to be square. Again, safety results from how our system interacts with its
environment.
This does not imply that we should not also strive for correctness. An incorrectly implemented design is
more likely to result in safety concerns than a correctly implemented one.
Is safety equivalent to reliability?
Is safety then about reliability?
Reliability – Reliability alone does not guarantee safety.
Consider a box of matches. An unreliable match — that is, one that fails to perform its function according
to specification — is far more likely to be “safe” than a reliable match.
A common misconception is that if we just get all of the faults out of our system and stop it from failing
then all will be well and our system will be “safe”.
Reliability is the probability that a system will work without failure for a specified period of time in a
given environment. Thus, reliability is about failure.
Reliability measurements for software are statistical; for example, failure intensity (the number of fail-
ures observed per unit time), the Mean Time Between Failure (MTBF), or failure probability gained from
testing the computer based part of the system.
The word failure is the key to understanding software reliability.
Reliability and safety
A failure is a deviation from the specified system behaviour.
A failure in a safety critical system is more commonly a deviation from the intended behaviour – which
is to be safe – and especially where the deviation can lead to harm or loss.
In the reliability usage of the word failure, a system may be unreliable but still safe, and worse, it may
be completely reliable but totally unsafe.
14
Example 2.1
Let’s consider an example to demonstrate the different between reliability and safety.
We have already seen that correctness is not the same as safety through the example of the braking
system. A similar example can distinguish the difference between reliability and safety. Consider a
collection of systems for aircraft.
If I fly on an aircraft, I want a set of systems that always fly (reliable), while also not crashing (safe).
However, I can easily implement an unreliable and safe system: never start the engines. The plan will
not be able to move, so will be safe. However, it will not be reliable.
Now, consider the converse: something that is reliable but unsafe. We can consider the braking system
example above: simply design our aircraft system to have no brakes. I can implement the braking system
reliability, but the aircraft will not be safe.
While this example is somewhat extreme, it distinguishes the difference between reliability and safety
well. 2
2.3 Safety engineering
Safety engineering is the set of processes, methods, and techniques for assuring the system safety. The
safety engineering approach is characterised by monitoring safety throughout the product development
process – just like other quality attributes.
The safety engineering process ends by ensuring that there is enough evidence to build a safety case to
demonstrate that the product is safe. Typically, it is not sufficient just to say (or show) that a large set of
tests have been run, or that a design has been carefully crafted. In most countries, regulatory authorities
require that a safety case demonstrates that safety concerns have been identified, the system has been
designed to consider them, and has been implemented to its design.
Definition 2.1
A safety case is a high level argument that is presented to regulatory bodies, or customers, to demonstrate
that a system is safe. They are generally built up using all of the evidence gathered during the exploratory,
deductive, inductive and design processes. 2
There are several phases throughout the system development lifecycle in which safety engineering plays
an important role:
1. Requirements Analysis and Product Definition – Preliminary Hazard Analysis and other meth-
ods are used to determine the hazards or potential situations that can cause harm or environmental
damage. Their potential causes and impacts are listed.
2. Design and Implementation Phases – To guide the design, one typically uses a deductive ap-
proach: one that starts with potential situation of harm or injury and works back to design or
implementation elements. is used. This is typically followed up by an inductive approach: one
that starts from component or subsystem failures.
During these phases, the system designers and safety engineers work closely to ensure that the
design considers the high-risk safety concerns. Systems that must defend against situations of
harm or damage must typically be ultra-reliable or must have sufficient redundancy so that a safety
related subsystem failure does not lead to a safety related system failure. This is the principle of
15
“Defending in Depth” against potential accidents. We will discuss more about redundancy later in
this subject.
3. Safety Cases – Finally, throughout the process, a safety case is constructed, which is used to
convinced the customer or regulator authority that the system is safe. That is, all non-trivial hazards
are identified and the design considers these and deals with them appropriately.
Concepts in safety engineering
The terminology used here is consistent with common usage but beware that terminology differs between
standards. Safety as it concerns us in this subject is chiefly concerned with physical products.
• Platform — The largest engineered artifact that contains the software, the physical devices on
which the software will run (and which connect people to the system), and the operating proce-
dures.
• Unsafe — A product is unsafe if it causes unacceptable harm; for example, in the loss of life,
injury or environmental damage.
Note an important point here. Considering software in isolation from the rest of the system does
not tell us anything about its safety or otherwise. When analysing a system for safety the whole
system, including hardware, other software systems, humans, and the environment, must be con-
sidered.
• Safe — A product is safe if it is not unsafe.
• Accident — An accident is an unintended event, or more typically an unintended sequence of
events, leading to harm.
• Incident — An event that significantly reduces the safety of a platform but does not lead to an
accident.
• Hazard — a physical situation or state of a platform that can lead to an accident.
• Failure — An unintended condition of a system that can lead to a hazard. IEC–61508 states that
a failure occurs when the delivered service deviates from the specified service.
• Error — An unintended state that can lead to a failure.
• Flaw – A design defect that can give rise to an error under specific conditions.
• Fault — An event that occurs when a flaw is activated resulting in an error.
Note the the definition of fault is different in safety engineering than in software testing.
2.4 Accidents and safety engineering
Understanding accidents is key to understanding safety. In this section, we look at accidents and causal-
ity.
First, let’s consider some pertinent examples of accidents in which software was a primary contributor:
16
Therac-25 – Between June 1985 and January 1987, six people died after receiving more than 100 times
the intended radiation dose from Therac-25 radiation therapy machine.
A commission into the accident concluded that the primary cause of the radiation overdose was
from poor software design, not from coding errors.
A320 Airbus Accidents – The Lufthansa A320 accident at Warsaw airport, and the Mulhouse-Habsheim
Airport accident (discussed in more detail below).
London Ambulance – In October 1992, the failure of the London Ambulance Services’ new Com-
puter Aided Dispatch (CAD) system resulted in over 30 deaths and problems in dealing with
life-threatening incidents [1].
An investigation concluded that the system was poorly designed and implemented, leading to
significant delays in the assigning of ambulances. In some cases, an ambulance took up to 11
hours to arrive.
An upgrade in July 2006 led to repeated system crashes in July and August, and operators reverted
to old pen-and-paper methods for ambulance dispatch.
On 8 June 2011, a completely new CAD system, called CommandPoint, was commissioned. On
9 June 2011, the system developed technical problems and operators again had to revert to to old
pen-and-paper methods. The London Ambulance Service reverted to the previous CAD system on
the same day, and CommandPoint was not re-instated until May 2012.
Boeing 737 MAX – Two Boeing 737 MAX 8 aircraft crashed, killing all passengers and crew on board,
on October 29 2018 and 10 March 2019. At the time of writing (February 2020) the causes
of these accidents are still under investigation. However a flight-control software system called
MCAS (Maneuvering Characteristics Augmentation System) of 737 MAX is widely believed to
have been a major contributing factor.
2.4.1 Case Study: The A320 Airbus
The Airbus A320 is a fly by wire aircraft, in which the pilot’s commands are transmitted to the flight
control surfaces as electronic signals rather than mechanically via cables and pulleys.
It has been described as a “network with an aircraft wrapped around it”. There are about 150 individual
computers on board each of which must communicate with the others.
Fly by wire (and drive by wire) are ubiquitous in modern transport systems. They have many advantages
over the old mechanical systems [3]:
• Saving in weight.
• Enhanced control of the aircraft (e.g. anti-lock brakes).
• Simplify retraining.
• Simplify maintenance (i.e. software instead of hardware).
• The flight engineer is made redundant.
• Enhanced safety by creating a protective flight envelope and thereby reducing pilot error.
• Marketing advantage.
17
A320 On Board Systems
The A320 has the following on-board systems:
• Electronic Flight Control System (EFCS).
• Flight Management and Guidance Control system (FMGC).
• Full Authority Digital Engine Control system (FADEC).
• Electronic Instruments – the “Glass Cockpit”.
Habsheim – 26 June 1988
The intention of the flight was to demonstrate the aircraft’s capability at low altitude. It made the first
pass slowly at 34ft/10m, flaps extended and landing gear down, successfully.
The second pass was to be done at speed in a clean configuration (landing gear raised). The aircraft
failed to regain altitude, ploughed into a forest at the end of the runway, and caught fire, killing 3 of the
130 passengers and injuring 34 others.
An official investigation of the accident attributed it to the following causes:
• low fly-over; lower than surrounding obstacles;
• low speed with engines at flight idle; and
• late application of “go around” thrust.
Together with the conclusion that the aircraft was flight worthy and that no malfunction of the on-board
systems contributed to the crash, the verdict was pilot error.
However, an interesting observation has been made by many safety experts that many more aircraft
accidents are attributed to pilot error when the pilot dies than when the pilot lives. This indicates a
culture of blaming the pilot first, and then seeking their explanation second.
Further to this, many accidents attributed to “pilot error” are of the form that: “the aircraft did X, but
the pilots did not notice Y, therefore, they caused the accident”. For example, in some cases, pilots and
co-pilots have failed to notice warning lights. This can be considered quite unfair on the pilots: if neither
the pilot nor the co-pilot noticed a warning light, surely it could be considered a failure of the warning
light system for not being noticeable? If we agree with the quote in Section 2.2 that human error is
inevitable, we should build our systems to consider that humans will make errors, so the system must try
to accommodate them.
The Pilot’s Version
The pilot actually survived this crash and so his version has been recorded as well as the official finding.
The pilot’s versions stated:
• Company operating procedures were not followed:
18
– flying lower than the specified minimum runway fly-over altitude (100ft in landing configu-
ration and 300ft in clean configuration according to Air France specifications).
– The flight dossier including Habsheim and its surrounding environment arrived too late for
the crew to study it and the crew were never verbally briefed. The first that the crew new
about 30–40ft high trees where when they were 15s flight time away.
• The height chosen for the fly-over was 100ft. The crew thought that they were at 100ft.
– The captain decided to use the vertical speed indicator and the barometric altimeter to control
height (accuracy±30ft). The reason was that the radio altimeter uses a digital display. At low
altitude the terrain numbers change so rapidly that it is impossible to read. The barometric
altimeter, however, uses an analogue display.
– The barometric altimeter was checked in the pre-flight checks but was faulty; it displayed
67ft just before take-off but before the wheels had left the ground.
– The crew were used to a large airport where the control tower is 30m high and the runway
is 3km long rather than a control tower 12m high and a runway 900m long as at Habsheim.
The crew were also deceived by other visual cues.
• But technical factors contributed to the accident as well;
– There are two Flight Management Guidance Computers (FMGC), one acting as the primary
guidance system and one as a standby. These were not communicating prior to the accident.
The pilots had to enter all of the flight data by hand into both the primary and the standby
computers. Note: if there is a discrepancy between the two FMGCs and the standby was
needed then the navigation screens would go blank (a fault noted a day earlier). The crew
decided to enter only one data point to minimise data entry errors.
– The engines took almost 9s to spool up rather than the 5s that the engines are physically
capable of. The pilot’s view, however, disagrees with the official report that the engines
performed according to specification.
– Pitch control anomalies in the functioning of the Electronic Flight Control Systems (EFCSs).
What can be concluded?
“L’affaire Habsheim is not over yet” – Mellor [3].
• The spool-up timing of the engines is critical. If it is 5s as claimed then this makes the pilot’s
story suspect, but if it is 9s as claimed by the pilot then the official findings become seriously
questionable.
• Whatever the crew’s mistakes were that led to the situation over Habsheim there is evidence that
the functioning of the on-board systems led the pilot to a situation (Hazard) that he could not
recover from. This is a point of some controversy still.
• The complexity of the A320’s systems seems to be part of the cause for the controversy.
However, the most important conclusion that we should take from the case study is:
The accident, like many others, was a failure of a system which included operational procedures, crew
training and environment as well as the on-board systems.
19
2.4.2 Case study: Warsaw, Okecie – 14 September 1993
The Kulmbach, a Lufthansa A320-200, was making its final approach on runway 11 at Okecie airport,
Warsaw with 64 passengers and a crew of six on board. There was a violent storm and the water was not
draining well from the newly resurfaced runway. The runway is 2800m in length but the aircraft touched
down 750m along the runway at a speed of 170 knots (see Figure 2.1). This was too late and too fast but
a fully laden A320 can stop in 1000m. In this case there was an accident and the aircraft hit an earthen
embankment at the end of the runway, killing the co-pilot and one passenger.
750m
1700m
2800m
D = 750m
S = 170knots
T = 0sec
No Braking
D = 1750m
S = 154 knots
T = 12sec
Braking
D = 2800m
S = 70 knots
T = 30sec
Braking
Approximate
analysis; detailed
reports are not
yet available.
Figure 2.1: The Lufthansa A320-200 at Okecie airport, Warsaw.
Braking logic: Landing an A320 normally
Figure 2.2 presents an architectural view of the A320 braking system. The weight on the wheels (WoW)
and the altitude (measure by the altimeter) are the two inputs.
The logic of the braking system is as follows:
brake if and only if:
(WoW > 12 tonnes) and
(wheels spinning > 72 km/hr or
(wheels spinning and radia alt < 10 feet ))
The Okecie accident sequence
1. The crew were informed that the wind on the ground was coming from the right and slight in front
of the aircraft at 10 knots. The crew were not informed that during the final approach the wind
changed to a tail wind of 25 knots.
2. To compensate for the cross wind coming from the front of the aircraft the crew had chosen a
landing speed of 152 knots rather than the standard 132 knots which was normal procedure in bad
weather of this kind.
3. Expecting a side wind the crew had banked the plane to the right. The result was that only the right
landing gear touched the runway. The left gear did not come down for a further 9s.
A/G is indicated by both WOW switches. The LGCIU could not signal A/G and so the SEC did
not deploy the ground spoilers and the FADECs did not deploy reverse thrust.
20
FADEC
BSCU
SEC
PILOT
INTERFACE
EFCS
LGCIU
Altimeter
WoW
Landing Gear Control
Interface Unit
Electronic Flight
Control System
Spolier and Elevator
Control
Braking and Steering
Control Unit
Full Authority Digital
Engine Control
Figure 2.2: Braking system for an A320 landing normally
4. The wheel that had made contact with the runway was aqua-planing and so did not rotate at the
required speed corresponding to 72 knots. Thus the other condition for the SEC to deploy spoilers
was not true.
Conclusions from the Okecie accident
There is a lack of published evidence in the case of the Kulmbach accident at Okecie. Some conclusions
are drawn by Mellor [3]:
• The crash was not due solely to pilot error. The pilot may have contributed to the accident by
selection of the landing speed but the Kulmbach should have been able to stop in the distance even
at the speed of 170 knots.
• The drainage of water from the runway and the weather conditions were necessary conditions for
the accident but probably not sufficient on their own. Several other aircraft landed successfully on
runway 11 both before and after the Kulmbach in the similar conditions.
• The A320 systems may have performed to specifications but did not behave as required. In retro-
spect, the braking logic is restrictive.
Again, it was the failure of the entire system that led to the accident.
21
2.4.3 Case Study: Boeing 737 MAX
On 29 October 2018 a Lion Air Flight 610 crashed killing all passengers and crew. On 10 March 2019
Ethiopian Airlines Flight 302 also crashed, leaving no survivors. Both aircraft were Boeing 737 MAX 8;
346 people were killed.
The precise causes of each of these incidents is still to be determined, yet it is widely believed that a major
contributing factor to both was a software flight-control system of the 737 MAX known as Maneuvering
Characteristics Augmentation System (MCAS).
MCAS is a software component of the 737 MAX’s flight control computer. It is understood to have been
added to the 737 MAX in an attempt to have the 737 MAX mimic the handling of a prior 737 series
aircraft, the 737 NG.
The following is a synthesis of the current understanding of MCAS and its role in these accidents, as
drawn from a range of news reports available online in Febuary 2020. This synthesis should therefore
be treated as informed speculation as gleaned from reputable news media. However it should not be
considered authoritative. Because both accidents are still under investigation, authoritative sources on
the precise causes of the accidents are not yet available.
Why was MCAS added? The 737 MAX has larger engines than prior 737 series, and those engines
are fitted in a different position. Specifically, in order to achieve increased efficiency compared to the
737 NG, the MAX uses a different kind of engine. Those engines are bigger than the ones on the NG.
Because of their increased size, they need to be mounted in a different place on the wings, compared to
prior 737 series.
Changing the placement of the engines means that the aerodynamics of the aircraft are different. In
particular, it means that the aircraft becomes more likely to tilt upwards more steeply than is desired,
while in flight.
MCAS’s goal is to compensate for the MAX’s tendancy to tilt upwards. There seems to be some debate
about whether MCAS was introduced because (a) the tendancy to tilt upwards could lead to the plane
stalling (e.g. at low speeds), or (b) whether it was introduced only because the tendancy to tilt upwards
would mean that the MAX would simply handle differently to the prior NG series 737s. In either case,
any deviation in its handling from the prior NG series 737s would have meant that extra training for
pilots, who were already trained to fly the NGs, would have been required.
As we understand, avoiding extra training was important for ensuring the MAX was as cheap as possible
for arilines to buy and to use. We discuss this more below.
How MCAS Operated The MCAS system attempts to detect when the aircraft’s nose tilts up too
steeply. Technically, the tilt of the aircraft is referred to as its angle of attack (AOA). MCAS attempts to
detect when the AOA is too high and, in that case, its job is to automatically push the nose of the plane
downwards, lowering the AOA.
The aircraft includes AOA sensors. MCAS determines when the AOA is too high by reading one of the
AOA sensors. When it determines that AOA is too high, it adjusts the aircraft’s horizontal stabiliser,
which causes the nose to pitch downward. When it does this, we will say that MCAS is activated.
22
Immediate Cause of the Accidents It is believed that the immediate cuase of the accidents was that
MCAS repeatedly activated. That caused it to repeatedly push the nose of the plane downward, each
time it repeatedly activated, forcing the plane to fly downwards without the pilots being able to correct
its trajectory until it eventually crashed into the Earth.
It is believed that the repeated activation of MCAS was caused by it receiving faulty AOA sensor read-
ings.
Contributing Factors
There are a number of factors that contributed to these accidents that are worth understanding.
Lack of Redundancy The first is that, while the 737 MAX has two AOA sensors, the MCAS system
took input from only one of them. Thus there was no redundancy that would have allowed it to detect
that it was receiving erroneous AOA data. We will discuss more about the need for redundancy later in
the subject. However note for now that the single AOA sensor becomes a single point of failure in the
MCAS design. As both accidents appear to indicate, a failure of a single AOA sensor led to the plane
crashing without pilots being able to intervene.
Lack of Human Override It is believed that in each accident, the pilots repeatedly attempted to try
to bring the nose of the plane upwards to correct its trajectory, by moving the control column. However
MCAS was designed so that it could not be overridden by the pilots moving the control column. This
was in contrast to previous 737 series.
Lack of Documentation and Crew Training Because MCAS was considered to be a part of the flight
control system, it was not desribed in the flight manual or in training materials. It is believed that this
was a deliberate choice to maximise the appearance of similarity to the previous generation 737 NG.
As noted earlier, the goal was to minimise the cost of the aircraft to airlines by ensuring that pilots and
crew who were already trained in the NG series would not need much additional training to fly the MAX
series.
Thus the pilots were likely unaware of the MCAS system, its potential effects on the plane, nor how to
correct for those effects.
Culture: Economics vs Safety
Studying the 737 MAX 8 incidents and their believed causes shows that a range of factors contributed to
them. These factors share some common themes. Perhaps the most striking of which is a repeated desire
to apparently prioritise lowering cost over redundancy and safety.
For example, the desire to ensure as much commonality between the handling of the MAX and the
earlier NG series, while wanting to use larger, more efficient engines, is believed to have been driven
by economic concerns. Firstly, more efficient engines mean that the plane is cheaper for arlines to run,
because it uses less fuel. Secondly, commonality with the earlier NG series means that little additional
training is required for pilots, which further reduces the cost to airlines who purchase the plane. So both
23
of these choices appear to have been driven by economic concerns: to make the 737 MAX as attractive
as possible to potential buyers.
As we have seen, these choices however led to the introduction of the flawed MCAS software system.
We discussed already how MCAS’s design was flawed, because it took readings from only one of the
AOA sensors, leading to a single point of failure. It is reported that the reason for this design choice was
also economic. Specifically, that design ensured that extra flight simulator training would not be required
by pilots.
If true, this indicates that the MCAS design violated one of the most fundamental principles of engineer-
ing reliable systems—namely, avoiding single points of failure that can lead to catastrophic consequences—
for the sake of saving (or, rather, making) money.
So not only did economic factors drive the introduction of MCAS, but they also drove its flawed design.
Since these indicents, whistleblowers have also larger concerns about Boeing’s aircraft and its engi-
neering processes. These include concerns about the oxygen system on the 787 Dreamliner, as well
as concerns about the operation of certain Boeing factories in the US, in which safety was not given
sufficient priority.
While we of course cannot make judgements about the culture at Boeing specifically, together these
reports highlight the critical role that culture plays in an organisation. Decisions to prioritise econimics
over safety (whether made implicitly or explicitly) are ultimately a product of an organisation’s culture
and values.
Response and Fix
MCAS Fix At the time of writing (February 2020), Boeing is currently working to fix the problems
identified with the MCAS system. Its reported fix involves three changes:
1. Having MCAS take AOA readings from both AOA sensors. If the two sensors disagree too much,
MCAS will assume one is faulty and will not activate. This guards against a single faulty sensor.
MCAS will also only activate when both sensors agree and move from a low AOA reading to a
high one. This gaurds against a fault in which both sensors get stuck reading a faulty high value.
2. When MCAS activates, it will now activate once only rather than repeatedly.
3. MCAS’s ability to move the horizontal stabiliser has been reduced, thereby allowing the pilots to
override it by moving the control column.
As part of the ongoing investigation into the 737 MAX, an additional point of vulnerability was in-
dentified in the MAX’s flight control systems. This was the potential for bit-flip errors to occur in the
hardware running the flight control computer. As a result the architecture of the flight control system
was overhaulded to increase redundancy. We discuss this further in subsection 9.6.3, when we discuss
fault tolerant design.
Broader Consequences and Reflections On 10 March 2019, all 737 MAX aircraft were indefinitely
grounded, pending investigation of both incidents. As of February 2020, all 737 MAX remain indefi-
nitely grounded. Aside from the 346 deaths, it remains unclear what precise economic cost the grounding
24
will have on Boeing (remembering of course that the MAX’s original design was motivated by a desire
to save money).
In December 2019, Beoing removed its CEO over the incidents and their handling. In 2019, airlines
reportedly cancelled 183 orders for the 737 MAX. Each MAX reportedly costs at least 100 million US
dollars. Those cancelled orders alone therefore represent a potential loss of 18.3 billion US dollars. Of
course that cost doesn’t account for indirect costs, including those of investigation, remediation and re-
certification of the aircraft, nor of those due to lost production (since production was halted in January
2020).
It has been reported that the grounding is projected to lead to losses of 18.4 billion US in future.
The 737 MAX case study neatly illustrates the oft cited engineering maxim that the best (i.e. the cheapest
and fastest over the long term) way to build a system is to built it right the first time. (As opposed to
taking shortcuts that lead to having to re-engineer a system once it is in production.)
Returning to the theme of culture, we may conclude that in safety-critical engineering, a culture that
prioritises economics over safety may lead to short-term economic gains, but may ultimately lead not
only to catastrophic loss of life but also to higher economic costs over the long term.
2.4.4 The role of accident analysis in safety engineering
Accidents, and the study of accidents, are an important part of safety engineering. The most useful
way to identify hazards in safety-critical systems is to look at similar systems and study the factors that
contributed to accidents (and incidents) in those systems, and to specify safety requirements from both
these and the system models (see Figure 2.3). Accident analyses are used during Preliminary Hazard
Analysis (for example during HAZOP) to understand potential accidents, causes consequences and risks.
Accident
Analysis
Prelim.
Hazard
Analysis
System
Models
Safety
Requirements
Figure 2.3: The role of accidents in safety engineering
Analysing Accidents: Event Chains
The common method for analysing accidents is to create an event chain leading back to a set of accident
causes. Figure 2.4 shows an example of an event chain for an accident in a steam boiler.
25
Moisture
Tank
Ruptured
Weakened
MetalCorrosion
Moisture
and Fragments Projected or
People
Injured
Equipment
Damaged
Figure 2.4: An event chain for a steam boiler
Each event in the chain is the cause of the next; for example, moisture is the cause of corrosion, corrosion
is the cause of weakened metal in the boiler, moisture and weakened metal is the cause of a ruptured
tank, etc.
Causality
When can it be said that event A causes event B?
In accident analysis one common way is through the counterfactual definition of causality.
Definition 2.2
A counterfactual is commonly defined as a conditional expressing what could have happened under
different conditions, but did not happen. It is typically expressed in the subjunctive mood: a conditional
(or if-then) statement indicating what would be the case if its antecedent were true although it is not true.
2
As an example, consider the counterfactual: “They would have done better on their assignments, if they
had first done the workshops” (take a hint from this).
In accident analysis, “A causes B” if we assume that A did not occur then B would not have occurred.
Note that this does not mean that A is the sole cause of B. As in the accidents described above, often
multiple factors contribute in unlikely sequences to cause a single accident.
Counterfactuals and Causality
In the steam boiler example we would reason as follows:
If the metal had not been corroded then the metal would not have been weakened.
The metal was corroded but the counterfactual deals with what could have been — in this case, if we
remove the corrosion then we can imagine that the metal would not have been weakened (based on our
knowledge of corrosion and metal). Consequently the counterfactual is true and we conclude that the
corrosion of metal causes the weakening.
The essential nature of counterfactuals in accident analysis is the imaged absence of one of the precon-
dition events leading directly to the event being studied. If the consequent is true when we imagine that
26
the antecedent is true then the counterfactual is true — that is, we imagine the absence of the cause A
and if the event B no longer occurs then A causes B.
Counterfactuals and correlation
We must also be careful not to confuse causal relationship with correlations.
If the were the case that the barometer falls then a storm would occur.
In this case the barometer falling and the storm occurring are not causally linked — the counterfactual is
incidental and not true.
2.5 Tools and techniques for safety engineering
The aim of this section is to examine some of the concepts, tools and techniques involved in safety
engineering in a bit more depth.
The problem in safety engineering is to develop – with a high level of assurance:
• Systems that do not lead to unintended harm or loss;
• Systems that continue to function in the presence of component faults;
• Systems that perform within hard real-time bounds; and
• Systems that protect data.
2.5.1 Safety standards and safety lifecycles
Software engineering for safety critical systems involves numerous standards each of which requires
additional activities in order to guarantee due care with safety critical functions.
For example, in IEC–61508, the safety lifecycle is used to identify:
• key activities where safety analysis techniques are used; and
• the relation between safety critical analysis methods and traditional lifecycle processes.
Engineering for safety, like engineering for other product attributes such as performance, correctness,
and usability, is something that must be done over the entire lifecycle. Figure 2.5 shows a possible safety
engineering process, and notes some of the techniques and types of methods used in safety engineering.
For each project, or each iteration of a project, the phases of requirements, design, and implementation
will have corresponding verification and validation techniques. At the requirements phase, the focus is
more on exploratory hazard analysis to determine what could go wrong, while at later phases, it is about
designing the system to prevent these.
In the remainder of this chapter, we look at some safety engineering techniques. First, we present a more
general overview of preliminary hazard analysis.
27
Prelim. Hazard
Analysis:
Exploratory (HAZOP)
Safety Case:
Deductive and
Inductive
Inductive
Requirements
Architecture
Detailed Design
Implementation
Unit Test
Integration Test
Acceptance Test
Deductive (Fault
Trees)
Deductive or
Inductive (FTA or
FMEA)
Inductive
Figure 2.5: A safety engineering process
2.5.2 Preliminary hazard analysis
A preliminary hazard analysis is used to identify the hazards and accident scenarios associated with a
new system. It is essentially an information collation and filtering exercise, and is usually the first activity
carried out in the safety lifecycle. Preliminary hazard analysis methods ask: “What possible accidents or
incidents could occur?”
In this section, we look at some of the basic concepts in preliminary hazard analysis.
Phase 1 — Preliminary hazard identification
This is the phase that identifies the hazards and accident scenarios associated with the new system.
• Inputs – Information sources
– Incident and hazard data: previous in-service incident and accident data of similar systems
(usually as checklists).
– Analyses: previous dependability analyses for similar systems.
• Method
– Group based: members experienced in the domain. This is important because people who
have domain knowledge and knowledge about accidents are far more likely to think of acci-
dents for the new system.
– What ifs: hypothesise safety related difficulties.
• Output
– Preliminary hazard list: a list of all hazards, their causes.
28
Phase 2 — Preliminary Hazard Analysis (PHA)
This occurs after the preliminary hazard identification and usually at the later stages of requirements
analysis and early stages of design. It has the following properties:
• Possibly included into preliminary design reviews.
• Early identification of hazards and causes in designs.
• It is a poorly defined method:
– uses brainstorming and questionnaires; and
– depends on experience and domain knowledge.
• Checklists are commonly used, in an attempt to make the method more systematic, and less reliant
on experience and domain knowledge.
Inputs
• design (or perhaps a detailed requirements model).
• preliminary hazard list
Outputs
• list of causes for hazards based on the design.
• risk assessment and risk classes (provisional).
• integrity levels (provisional).
• target failure rates (provisional).
Hazard Logs
The results of the preliminary hazard identification and preliminary hazard analysis form the basis of a
hazard log.
Definition 2.3
A hazard log lists the hazards, their causes and their severity, as well as other data such as target frequen-
cies or hazard types. It is a living document. It evolves during the course of the safety analysis, and can
be part of the common literature in special fields such as avionics. 2
For example, a preliminary hazard analysis may be summarised in the following hazard log.
Ref. Hazard Frequency Severity Cause Integrity Target
Level
. . .
. . .
29
Risk
Risk is function of the severity of a hazardous event and the likelihood of it occurring. The frequency of
the event is the probability that the event occurs in a given period of time.
Assessing Risk
To arrive at a qualitative risk assessment, the idea is to assess consequences against frequencies. Stan-
dards usually provide a table for this.
The safety standard IEC – 61508 classifies consequences into:
catastrophic critical
marginal negligible
It classifies frequencies into:
frequent probable occasional
remote improbable incredible
From these two classifications, the standard defines the following table, which classifies each conse-
quence/frequency pair into one of four risk classes.
Frequency Consequence
Catastrophic Critical Marginal Negligible
Frequent I I I II
Probable I I II III
Occasional I II III III
Remote II III III IV
Incredible IV IV IV IV
The four risks classes, according to IEC – 61508, are:
Class I Intolerable risk.
Class II Undesirable risk and tolerable only if risk reduction is impractical.
Class III Tolerable risk if the cost of risk reduction would exceed the improvement gained.
Class IV Negligible risk.
Using this, we can interpret that frequent events with a catastrophic must not be tolerated, while incredi-
bly rare events in general can be tolerated.
Integrity Levels
Definition 2.4
Safety integrity is the likelihood of a safety related system satisfactorily performing the required safety
functions under all stated conditions. 2
For example, a system with known failure rates for subsystems:
30
Integrity Probability of failure to
Level perform system function
4 ≥ 10−5 to < 10−4
3 ≥ 10−4 to < 10−3
2 ≥ 10−3 to < 10−2
1 ≥ 10−2 to < 10−1
Note: the distinction between risk classes and integrity levels. Risk classes are assigned to hazards.
Integrity levels are assigned to systems.
Assigning Integrity Levels
It is not always possible to remove hazards entirely. Safety engineers must instead reduce hazard risks
to tolerable risk levels. The amount of risk reduction determines the integrity level. The more that the
system is required to reduce the risk of hazards the higher the system’s integrity level.
There are a number of ways of assigning integrity levels to subsystems, and these can be broken into two
categories:
1. Quantitative methods – first determine the uncontrolled risk and then target risk levels numerically,
by analysing the failure rates of subsystems from similar situations;
2. Qualitative methods – use risk graphs to determine risk qualitatively.
Some standards, such as IEC-61508, provide for integrity levels which are set according to the severity
of the accident.
2.6 Hazard Analysis Methods
Hazard analysis methods fall into three categories:
1. exploratory: these methods use expert knowledge and some systematic techniques to imagine
potential hazards and their impacts;
2. causal: these methods work from system designs and try to deduce what failures are possible from
this; and
3. consequence: these methods work backwards from possible failures to see how they could occur.
Table 2.1 summarises these three types of methods. In the remainder of this chapter, we will explore the
three methods listed as examples in Table 2.1.
2.6.1 HAZOPS
HAZard and OPerability Studies (HAZOPS) is a technique that originated in the chemical processing
industry. It has been adapted to many other industries and specifically in software engineering to software
HAZOPS and — surprisingly — the study of interfaces in human-computer interaction.
31
Technique Example Methods Good For . . .
Exploratory HAZOP (Hazard and Operabil-
ity Studies)
Exploring possible hazards, their causes and
for completeness, but is labour intensive and
hard to change.
Causal Fault Trees This is the classic deductive approach and is
good for determining what combinations of
component failures lead to hazards.
Consequence FMEA (Failure Modes and Ef-
fects Analysis)
This is the classical inductive approach used
for working from component failures (failure
modes) back to hazards and is good for veri-
fying ideas and designs.
Table 2.1: Types of hazard analysis methods.
A HAZOP study is a well-established technique for preliminary hazard analysis (PHA). The process of
identifying hazards requires creativity, lateral thinking, and expert knowledge. The HAZOPS method
was introduced into software engineering to provide some repeatable and systematic process into hazard
analysis.
The basic technique is straightforward: a system consists of some behaviour. A HAZOP study explores
the behaviour by taking a set of generic guidewords that describe some alternative to intended behaviour,
and exploring what hazards could occur if that alternate behaviour occurred.
Note: this is different to testing where we wish to find deviations from specified behaviour.
Guidewords
The guidewords for every HAZOP study are the same1, and are listed in Table 2.2.
Guide Word Deviation
NO or NONE This is the complete negation of the design intention. No part of the
intention is achieved and nothing else happens.
MORE This is a quantitative increase.
LESS This is a quantitative decrease.
AS WELL AS All the design intention is achieved together with additions.
PART OF Only some of the design intention is achieved.
REVERSE The logical opposite of the intention is achieved.
OTHER THAN Complete substitution, where no part of the original intention is
achieved but something quite different happens.
EARLY Something happens earlier than expected relative to clock time.
LATE Something happens later than expected relative to clock time.
BEFORE Something happens before it is expected, relating to order or sequence.
AFTER Something happens after it is expected, relating to order or sequence.
Table 2.2: HAZOPS guidewords
1Although there is nothing to prevent software engineers from adding or removing guidewords.
32
Roles
There are several roles that can take part in a HAZOP study, including:
1. A Team leader who is responsible for:
• Planning and Preparation.
• Chairing the HAZOPS meetings.
• Signing any documentation.
• Ensuring follow-up work is completed.
2. Recorder who is responsible for:
• Documenting the analysis.
• Participating in the study.
3. Designers
• Understand and explain the (potential) design.
• Answer questions about the process.
4. User(s)
• May be operators, or representatives of other user groups such as maintainers of the system.
• Supply information about the context in which the system will be used.
• Help decide issues that may affect safety.
5. Expert(s). The key function of experts is to explore:
• Ask questions.
• Suggest deviations, causes and effects.
Process
The process for a HAZOP study is outlined in Figure 2.6, taken from [2]. From this figure, we can see
that each design item, such as an event in a process, or a transition in a state machine, is inspected. For
each item, the item is analysed using the HAZOP guidewords.
For example, consider our earlier idea of brake-by-wire system for a car. One design item contains the
intended behaviour that, when the brake pedal is pushed, a signal must be sent to the ECU immediately
to let it know that this event has occurred. Using a HAZOP study to analyse what happens if the systems
deviates from intention, we can apply the guidewords NONE, EARLY, and LATE, and then reason the
following:
• NONE: If no signal is sent, the ECU will not know that the brake has been pushed, so will not
send the signal to apply the brakes to the wheels. This will result in the vehicle not decelerating,
ultimately resulting in a hazard.
33
Figure 2.6: A HAZOP study process.
• EARLY: We want the signal to be sent immediately, so a signal cannot be sent early, and therefore,
no hazard occurs.
• LATE: If the signal is sent late, the ECU will process it late and send a signal late to the brakes.
This will result in the vehicle decelerating later than anticipated, ultimately resulting in a hazard.
The result is a log of the entire analysis. For each design item + guideword pair that may result in a
hazard, the following details are recorded:
1. The deviation from intent (the interpretation of the design item + guideword for this system).
2. The causes of the deviation.
3. The consequences of the deviations.
4. Any safeguards that are in place to prevent the deviation or its cause.
5. A list of recommendations for mitigating the problem.
A template/examples for this is shown in Table 2.3.
34
ID Guideword Deviation Causes Consequences Safeguards Recommendations
1.1 NONE . . . . . . . . . . . . . . .
1.2 EARLY . . . . . . . . . . . . . . .
1.3 LATE . . . . . . . . . . . . . . .
. . .
Table 2.3: A template for a HAZOP log.
Limitations
There are several limitations with using HAZOPS as preliminary a hazard analysis technique:
1. It requires a well-defined description of the intended behaviour. In some projects, this may not be
available until quite late.
2. It is extremely time and resource intensive. Several people in a team analysing every design
item for every guideword is time consuming, and the people in the team are generally highly
experienced.
Despite this, the process is creative and challenging, and many people find it rewarding and inter-
esting. Not me though . . .
3. To conform to most safety standards, to devise a safety case, and to enable traceability of decisions,
the result of the entire analysis must be recorded. That is, the outcomes of each guideword +
design item pair must be recorded, including causes, consequences, and recommendations for
mitigating the problem. This results in large amounts of documentation that need to be reviewed
and maintained.
4. It focuses on single failures that cause of deviation from intent. However, an accident can occur
due to a series of unlikely circumstances along a chain of events in a system. Despite this, the use
of guidewords may prompt system designers to consider chains of deviations.
Example 2.2
The following example of a stepper motor HAZOP study is taken from Wei and Zhangjin [5].
A stepper motor is a electric motor used for rotation of mechanical parts. The rotation is divided into
discrete number of equal size steps. The position of the motor can therefore be determined without the
use of sensors for feedback, provided the motor size is known. Figure 2.7 shows the workings of a
stepper motor: a single step moves the teeth around until they are locked in, so counting the steps means
that the position of the motor is known. The system is real-time, so when the controller of the motor
sends a message for it to rotate, power on, etc., it must do it within a specified (and short!) time frame.
The controller uses a C interface to the control the library, shown in Figure 2.8. Motor identifiers can be
created, and a specified motor can be powered on/off, moved forward a number of steps, and braked.
First, we need to infer what the deviations from intent are for each guideword:
35
Figure 2.7: An overview of a stepper motor.
void write_lpt(struct motor_t *motor);
struct motor_t stepper_motor_setup(int motor);
void stepper_motor_power_on(struct motor_t *motor);
void stepper_motor_power_off(struct motor_t *motor);
void stepper_motor_move_steps(struct motor_t *motor);
void stepper_motor_set_safe(struct motor t *motor);
void stepper_motor_brake(struct motor t *motor);
Figure 2.8: C interface for a stepper motor.
Item Guidewords
NONE MORE LESS EARLY LATE BEFORE AFTER REVERSE ...
Power No Over Under Voltage Voltage — — Backflow
voltage voltage voltage change change
Step No Less More — Late — Wrong Wrong
steps steps steps time order direction
Brake No Too Too Too Too Wrong Wrong Accel-
brake quickly slowly early late order order erate
. . . . . .
Now that we have the deviations from intent, we need to analyse what the possible hazards that arise as
a consequence of these. We do just part of the functionality for moving the stepper motor:
36
3.0 stepper motor move steps(struct motor t *motor);
ID Guideword Deviation Causes Consequences Safeguards Recommendations
3.1 NONE No steps No power to
motor
Motor does
not move
3.2 MORE More steps Power too
high
1. Motor
goes too
fast. 2.
Motor may
burn out.
Install a voltome-
ter. Do not apply
voltage is over a
specified threshold
3.2 LESS Less steps Low power
due to old
wire or large
resistor
Motor ro-
tates too
slowly
Install a voltome-
ter. Raise an error
is voltage is under a
specified threshold
. . .
From just these few guidewords, we have noted some potential hazards and provided recommendations
for them. Clearly, from the report, some knowledge of stepper motors and their operation is required,
even for this small part of the analysis. 2
2.6.2 Fault Tree Analysis
Fault trees are one of the classic deductive techniques for hazard analysis and also one of the most
common deductive technique in use today. Fault trees are typically drawn by working back from hazards
to basic causes. Fault trees are a graphical notation and consist of events and gates.
Event and gate symbols
Figures 2.9 and 2.10 show the symbols that are used in fault-tree analysis, dividing into event symbols
and gate symbols respectively.
Method
The basic method for working with fault trees is the following:
1. Select an event: Initially we select top-level events, which are typically the hazards, or events that
can lead to the hazards, identified in the PHA.
2. Identify immediate, necessary and sufficient causes of this event:
• Immediate: means that we need to avoid missing out intermediate events and should apply
the “think small” principle;
• Necessary: means determining those events which must occur for the top-level event to occur
and these should be linked with an AND gate;
• Sufficient: means getting those events which alone are sufficient to cause the top-level events
on their own and these are linked with an OR gate.
37
Basic Event
Undeveloped Event
An initiating fault requiring no further action
An event which is not developed further, either because
it is considered unnecessary or because insuficient
information is available.
Intermediate Event
Normal Event
Conditioning Event
An event arising from the combination of other,
more basic events.
An event which is expected to occur as part of the
normal operation of the system.
Specific restrictions that apply to some types of logic gates,
for example the PRIORITY AND or INHIBIT gates.
Figure 2.9: Fault tree symbols
AND gate
OR gate
INHIBIT gate
All of the inputs must occur for the output to occur
One or more of the input events must occur for the
output to occur
EXCLUSIVE OR gate
PRIORITY AND gate
The output occurs if the input events occur in a specific
sequence which is described in a conditioning event.
The output occurs if exactly one of the input events
occurs.
The output occurs if the single input event occurs in
the presence of the enabling condition which is described
in the conditioning event.
Figure 2.10: Fault tree gates
3. Classify intermediate events:
• Basic event requires no further decomposition.
• Compound defect, which may one of (1) a primary failure, which is a simple component
38
failure and may be classified as a basic event; (2) a secondary failure in which components
fail because of external influences and this usually requires further investigation; or (3) com-
mand failure in which components receive incorrect signals and this always requires further
investigation.
• System defect, which is not attributable to a single component and always requires further
investigation.
4. Repeat as required.
Some heuristics
There are some some heuristics that have evolved over time for the correct construction of fault trees:
1. All inputs to a gate should be defined before any one is examined in more detail.
2. The output of a gate must never be directly linked to the input of another gate – all outputs of gates
must go to events.
3. The text in event boxes should be complete in the sense that they should always say what the event
is and when it occurs.
4. Causes always chronologically precede consequences.
Example: Chemical mixing plan
Consider the simple chemical mixing plant shown in Figure 2.11 and its accompanying fault tree [2]. The
top-level event that leads to a hazard in this case is that the tank may overflow and cause environmental
damage.
The immediate, necessary and sufficient causes of this event are that the valve marked A is closed and
that the valve marked B is open. Note that these events must occur before the tank overflows. The fault
tree does not investigate the causes of the event “valve A closed” further.
On the other hand, valve B is open if valve B has failed, which is a component failure but is not inves-
tigated further, or an incorrect signal has been sent to valve B, which is a command failure and must be
investigated further.
2.6.3 FMEA
Failure Modes and Effects Analysis (FMEA) is a tabular technique and is one of the most widely applied
inductive (consequence analysis) techniques in industry.
The method specifies that safety engineers should focus on a single component failure, or failure mode,
and work forward through subsystems to determine the consequence of the failure mode on the rest of
the system. There is a great deal of variation in FMEA practice and, like most hazard analysis methods,
it is hard to give a general algorithm for conducting an FMEA.
39
Controller
X
Y
B
A
Tank overflows
Valve
A
Closed
Valve B open
Valve
B
Failed
Incorrect Control
to Valve B
Failed
Controller
Level Sensing
Failed
Sensor X
Failed
Sensor Y
Faield
Figure 2.11: A design of a simple chemical mixing plant and its corresponding fault tree.
Applications
FMEA is good at identifying individual elements or operations within a system that render it vulnerable;
that is, finding single points of failure. Industries/organisations that frequently use FMEA include: con-
sumer products, automotive, home appliances, aerospace, NASA, Department of Defence, and process
industries (e.g. chemical processing).
Failure modes
An FMEA is about identifying the failure modes of a system.
Definition 2.5
A failure mode is a manner in which a failure can occur, in terms of failure of the item function (such as
a function to a sub-system or component) being studies. 2
Typical failure modes are:
1. Premature operation.
2. Failure to operate at the required time.
3. Failure to stop operation at the required time.
4. Failure during operation — and this is specific to the equipment.
5. Degraded operational capacity.
40
6. Excessive operational capacity.
There is a symmetry here between failure modes and the guidewords for HAZOPS; e.g. excessive oper-
ational capacity and the guideword “MORE”.
Method
Step 1 Define the system to be analysed and obtain necessary designs, descriptions, diagrams, charts,
component lists, etc. The idea is to understand in depth what exactly it is that you are analysing.
• All of a subsystem or just part of it?
• What subsystems or operators must be protected?
• What other subsystems must the subsystem being analysed interact with?
Step 2 FMEA Analysis then uses two questions to guide the analysis.
1. For each analysed element, what are the failure modes?
• The failure modes can be identified from testing or prior experience with the component; or
• By appeal to similar components used before; or are
• Technology specific failure modes; or
• Can be analysed from damaged components
2. For each failure mode, what are the failure effects?
The safety engineer will provide general descriptions of the failure mode and effects, which may
also include severity and probability assessments.
Worksheet
The result of an FMEA is an FMEA worksheet – a monster of a spreadsheet that documents the failure
modes and effects, along with other items, such as causes, probabilities, severities, and recommendations.
A worksheet template is shown in Figure 2.12.
Limitations
Like other safety analysis methods, FMEA has several limitations:
• Frequently, human errors and hostile environments are overlooked.
• Because the technique examines individual faults of system elements taken singly, the combined
effects of coexisting failures are not considered.
• If the system is at all complex, the process can be tedious and time consuming.
41
• Failure probabilities can be hard to estimate.
• Sometimes FMEA is done only to satisfy the altruistic urge to “do safety”.
FMEA will find and summarise system vulnerability to single point failures, and it will require lots of
time, money and effort. An important question to ask before undertaking an FMEA is: how will the
results be used?
2.7 Threat Modelling for Security
In this section we will discuss common threat modelling methods. Similar to hazard analysis, which
aims to enumerate hazards, threat modelling aims to enumerate potential security threats to a system.
The STRIDE methodology, developed at Microsoft, is a common threat modelling method that has some
similarities with the HAZOPS method (see subsection 2.6.1).
Attack Trees are another common method for threat modelling that has similarities to Fault Tree Analysis
(see subsection 2.6.2).
We briefly introduce both, drawing heavily on the opening chapters of [4]: Threat modeling: Designing
for security, by Adam Shostack. Wiley, 2014. You can learn more there, and this book is currently
available as an e-book from the University’s library: http://library.unimelb.edu.au.
2.7.1 The STRIDE Method
The STRIDE method, akin to HAZOPS, can be seen as a systematic process for brainstorming secu-
rity threats to a system. While HAZOPS focuses on design items, in which one tends to concentrate
on the interactions and communications between system components, STRIDE instead focuses on the
interactions that occur between a system’s trust boundaries.
Trust Boundaries
Definition 2.6
A trust boundary separates one part of a system from another, where the two parts are under the control
of different entities who are trusted to differing degrees. 2
In any system, there will be parts that we as system designers control, such as the software or the hard-
ware components that we supply ourselves. There will also be other parts that we don’t control: for
instance, if we are using an external cloud provider then we likely will not have full control over those
parts of our system that run on the cloud platform. A trust boundary exists, therefore, between the parts
of the system that we control and those parts that we do not control. For the parts we don’t control,
some may be under the control of one entity while others might be under the control of another. If we
trust these entities to differing degrees (e.g. perhaps we trust one more than the other because we have
a contractual relationship with them) then a trust boundary will also exist between those parts of the
system.
Example 2.3
Suppose we are building a website for a business, like the University. The website itself will be hosted
42
on University servers. However, media like videos and images will be served via a third-party content
distribution network (CDN). This scenario is depicted in Figure 2.13.
Each component in this system lives inside a separate trust boundary. In particular, while the web server
might be under our control, the user and her web browser is not, and neither is the third-party CDN.
However, since we have a contractual relationship with the third-party CDN, we are willing to trust it not
to act in a way that would violate that contract. Therefore, we trust it more than we trust the users (and
potential attackers who might be on the network). 2
The STRIDE Acronym
STRIDE is an acronym: each letter corresponds to a class of threats. The STRIDE approach is about
considering each class of threats and how it might apply to the system. This is somewhat analogous to
HAZOPS in which each guideword is applied to a system to enumerate hazards.
In a similar way, for each of the classes of threat that STRIDE describes, we consider all of the ways
in which that class of threat might apply to our system, i.e. what sorts of attacks might be carried out.
STRIDE’s purpose is to help systematically enumerate the attacks or threats that our system might be
exposed to, and to force us to think about which threats are more serious and should be mitigated in our
system’s design.
STRIDE therefore forces security engineers to “think like attackers”. The threat classes that each letter
of the STRIDE acronym correspond to are:
Spoofing: pretending to be something or someone that you are not.
Tampering: modifying something you are not supposed to.
Repudiation: claiming that you didn’t do something (when you actually did do it)
Information Disclosure: revealing or stealing information that you are not supposed to be able to ac-
cess.
Denial of Service: preventing others from accessing a system, e.g. by crashing it, making it too slow to
use, exhausting its storage, etc.
Elevation of Privilege: being able to do something that you, technically, you are not allowed to to (e.g.
obtaining Administrator or root access on a server).
We consider each of them briefly, illustrating each using the example system above by giving a few
examples. Note that the lists below are not exhaustive.
Also note that the different threat categories are not non-overlapping. Some threats might legitimately
belong in multiple categories. For instance, suppose an attacker is able to modify the access logs for a
system. This threat could be a repudiation threat, since changing the logs might allow them to claim that
they did not do some action that would have otherwise been recorded in the log. However this threat is
also a tampering threat.
The goal with STRIDE is not to achieve a perfect categorisation of threats. But rather to aid as a tool to
help threat brainstorming.
43
Spoofing This threat occurs when an attacker pretends to be someone or something that they are not.
For instance, an attacker might impersonate a legitimate user of a system (e.g. if they can guess the user’s
password). Or, in the case of a phishing attack, an attacker might impersonate a legitimate website.
Spoofing threats violate the security goal of authentication.
Example 2.4 (Spoofing Threats for Figure 2.13) • One user pretends to be another user of the web-
site.
• The attacker impersonates the university’s website (via a phishing attack).
• A third party site contains a link to one of the university’s pages that causes the user to perform
some action on the university site; Cross-Site Request Forgery (CSRF) attack.
2
Tampering This threat occurs when some data is modified that is not allowed to be modified. The
modification could occur to data at rest (e.g. stored in a database or on a website) or do data in transit
(e.g. as it is being transmitted over the network).
Tampering threats violate the security goal of integrity.
Example 2.5 (Tampering Threats for Figure 2.13) • A rogue administrator for the CDN company
tampers with the website content hosted on the CDN.
• An attacker tampers with data in transit between the web server and the CDN.
• The attacker modifies the website contents (website defacement).
2
Repudiation This threat occurs when an attacker is able to do something bad, but then later on claim
that they did not do it. It usually occurs when a system does not have sufficient logging of actions, or
when the logs are not properly protected.
Repudiation threats violate the security goal of non-repudiation.
Example 2.6 (Repudiation Threats for Figure 2.13) • The attacker deletes or modifies the access
logs for the web server.
• The attacker fills up the disk space on which the log file is being stored.
2
Information Disclosure This threat occurs when an attacker is able to learn information that they
should not be able to learn. It also includes attacks in which an attacker is able to cause sensitive
information to be publicly released.
Information disclosure threats violate the security goal of confidentiality.
Example 2.7 (Information Disclosure Threats for Figure 2.13) • The attacker is able to steal the
private key used to secure the users’ connections to the university web site.
• A rogue administrator at the CDN accesses sensitive website media.
44
• An attacker is able to steal the password file for the website.
2
Denial of Service This threat occurs when an attacker is able to prevent the system from providing
service to a legitimate user. Denial of Service threats are often much harder to stop than other kinds of
threats.
Denial of Service threats violate the security goal of availability.
Example 2.8 (Denial of Service Threats for Figure 2.13) • The attacker commands a bot army to
flood the website with traffic.
• The offsite CDN server goes down.
2
Elevation of Privilege This threat occurs when an attacker is able perform some action that the sys-
tem’s security policy does not allow. The most common form of this threat is when an attacker is able
to gain “superuser” access (i.e. Administrator or root access) to a system, when logged-in as an ordinary
user.
Elevation of Privilege threats violate the security goal of access control.
Example 2.9 (Elevation of Privilege Threats for Figure 2.13) • Somebody connects directly to the
CDN server to directly access website content that would otherwise be password-protected.
• The web server does not perform access control checks, allowing one website user to access an-
other user’s content by issuing appropriate HTTP GET requests.
2
2.7.2 Attack Trees
Similar to fault trees, attack trees aim to deductively explore all of the ways in which a threat might be
realised. The simple variant of attack trees that we consider here has only one type of node, representing
some event that takes place.
Similarly to fault trees, the root node of the tree is the ultimate threat or event whose occurrence we are
worried about. The attack three answers the question of “how could this event occur”?
The children of each node in the tree are the immediate causes or ways in which the event that the node
represents might be realised or might occur.
When a node has multiple children, they usually represent different ways in which an event can occur.
In comparison to fault trees, they therefore represent an implicit OR node. However different attack tree
variants can contain expliti AND and OR nodes, similarly to fault trees.
Given their similarity to fault trees, we do not discuss attack trees in detail here. Consult Shostack [4]
for further details or see the various online resources on attack trees for more information.
45
Bibliography
[1] Anthony Finkelstein and John Dowell. A comedy of errors: the London Ambulance Service case
study. In Proceedings of the 8th International Workshop on Software Specification and Design,
page 2. IEEE Computer Society, 1996.
[2] J. McDermid, M. Nicholson, D. Pumfrey, and P. Fenelon. Experience with the application of HAZOP
to computer-based systems. In Proceedings of the Tenth Annual Conference on Computer Assurance,
pages 37–48. IEEE, 1995.
[3] Peter Mellor. CAD: computer-aided disaster. High Integrity Systems, 1(2):101–156, 1994.
[4] Adam Shostack. Threat modeling: Designing for security. John Wiley & Sons, 2014.
[5] Z. Wei and W. Zhangjin. HAZOP introduction.
46
Figure 2.12: An FMEA worksheet template
47
Web
Server
Third-
Party
CDN
User’s
Browser
Figure 2.13: STRIDE Example System.
48
Chapter 3
Model-based specification
In this chapter, we will look at model-based specification — one style of formal specification. We will
look at the Alloy language for specification, and how this can be used to model systems and check
properties about the models. In the workshops, we will also look at a tool, called the Alloy Analyser for
bounded checking of Alloy specifications.
This chapter currently covers Alloy 6, released in late 2021.
3.1 What is formal specification?
Formal specification is the use of mathematical notation to describe properties of a system. The use of
mathematical notation provides precision that is not possible with natural language specification, which
is subject to ambiguity and incompleteness. Formal specifications provide the specifier with a way to
formally define what a system must do, without saying how it should do it. Thus, they are as formal
as a programming language, but they do not constrain the design of the underlying algorithms or data
structures.
Because formal specifications do not constrain design detail, and are independent of their implementa-
tion, they can be produced early in the project lifecycle; e.g. at the requirements and architectural design
phases. At these phases, they are particularly valuable. As well as providing an unambiguous refer-
ence for system designers and implementers, the mathematical nature of formal specifications provides
software engineers with the opportunity to prove properties about the specification, such as correctness,
completeness, and consistency, but also other properties, including safety and security.
In this chapter, we will use the Alloy specification language [1]. Alloy is based on the Z language [4],
which is the most widely used and probably most widely known formal specification language. Alloy
sacrifices the expressiveness of Z for computational tractability; that is, some expressive features in Z are
removed to allow us to automatically check properties of Alloy specifications.
In this subject, we use Alloy for two reasons:
1. It offers an intuitive notation.
2. The Alloy Analyser tool for checking Alloy specifications is a state-of-the-art research prototype,
which has been applied to many industry systems, and is supported by a large community of users.
49
3.2 What is model-based specification?
Model-based specification is an approach to formal, system specification in which the system is defined
as a state machine model. That is, there is a state representing the data in the system, and operations
specify the transitions that can take the system from one state to another. This is much like the idea
of a module/package in Ada: the variables define the state, and the subprograms define the transitions.
The clean mapping from model-based specifications to modules is one reason that model-based specifi-
cation has become the preferred method for formal specification, as opposed to axiomatic or algebraic
approaches, which specify the properties of the systems via sets of axioms.
Model-based specification in the software engineering lifecycle. Model-based specification is not
a requirements elicitation technique, nor is it generally a requirements engineering technique. Many
model-based specifications are finalised during architectural design, once the components of the system
are known. However, the formalisation of the behaviour of system components implicitly leads to a
formalisation of those system requirements that the components are implement. As such, model-based
specification falls over the phases of requirements engineering and architectural design. We should be
comfortable with this, as in modern software engineering, the line between requirements and architec-
tural design is blurred.
Figure 3.1, adopted from Sommerville [3, Ch. 27] shows the relationship between different activities
in the early software engineering phases, and how model-based specification is typically carried out in
parallel with other activities. Information flow between the activities is two-way — information is always
passed between the requirements and design phases.
Figure 3.1: Model-based specification in the software process.
Languages for model-based specification. There are several well-known languages for model-based
specification, including VDM, B, Z, and Alloy. These languages all have commonalities, especially
that data types are based on sets and relations, and operations are specified using predicate logic. UML
statecharts are another example of model-based specification, although the level of formality in these is
less than in VDM, B, Z, and Alloy, as the intended areas of use are different.
50
Model-based specification and natural language. Like programming languages, formal, model-based
specifications cannot be understood fully by a simple read over. As such, documenting the specification
using natural language is important, just like commenting program code. Natural language descriptions
not only allow the reader to understand the specification at a superficial level without having to worry
about detail, but also allow the reader to understand the formal details, by guiding them through the
meaning.
3.3 The costs of formal specification
Using a rigorous process and method for formally specifying system properties can clearly be a costly
exercise. The activity of specifying the precise detail of systems and sub-systems using mathematical
notation requires careful thought and increased knowledge about the systems. Further, they must come
under the same scrutiny of quality assurance using reviews and inspections as other requirements speci-
fication techniques.
The question to be asked is: “Is the cost worth it?”.
A major limitation to people using formal specification, even in high integrity domains, is that the per-
ceived cost is too high. However, studies repeatedly show the following: the cost of specification is
higher, but some of the added costs are recouped later in the process. Figure 3.2, taken from Phillips [2],
shows data from a project, comparing the number of errors found per thousand lines of code of using the
Z specification language, against not using formal specification at all. The project from which the data
comes from is the CICS project undertaken at IBM. The project introduced 268,000 lines of new code
into a system already containing 500,000 lines of code. Of the new code, 48,000 lines was engineered
from a formal specification written in Z, and Z was used at the product-, component-, and module-level
design stages. Others studies show similar results.
Let’s analyse this figure and ask ourselves why formal specification would reduce the costs of later
phases. There are a number of reasons for this:
1. Using mathematical notation removes ambiguity from specifications, which is a cause of many
problems in software. This helps to limit the amount of re-work in later phases, driving down their
costs. In high-integrity systems, requirements typically change must less frequently than in say,
enterprise systems, so the models do not require major changes every increment.
2. Some cost incurred during design and implementation is simply trying to decide exactly what the
behaviour of the system could be. Using natural language to specify requirements means that
we can be shorthanded and therefore miss or ignore important detail. Using mathematical notation
does not afford this because it is easy to show incompleteness, and many of the issues that generally
present themselves during design and implementation must be solved during specification instead.
In other words, the people doing the specification do some of the work typically undertaken at the
design and implementation phases.
3. Despite opinion to the contrary, designing and implementing a system from a formal specification
is more straightforward than doing so from natural language requirements. For a novice, this may
not be the case, but for someone who is as comfortable with mathematical notation as they are
with programming languages, this is most definitely the case.
4. Using formal notation offers the possibility of proving that our specification satisfied certain prop-
erties. Discovering in later phases that our specification does not satisfy some properties is more
51
Figure 3.2: The costs of formal specification on the CICS project.
costly than discovering these in early phases.
5. Post-release costs are reduced due to the fact that less mistakes are introduced over the process;
or more accurately, the same number of mistakes are made, but more are made and found during
specification, rather than post release.
A simple (and common) objection to formal specification is that it is just too hard: the formality is too
difficult for the average programmer to grasp. I agree that this is case. I would not advocate employing
formal specification on a project with programmers who have no experience at it. However, I disagree
that this has to be the case. The average programmer is already intelligent enough to have learnt a formal
language: all programming languages are formal. As such, we know they are equipped to deal with
formal languages, even at the specification language.
However, an important extension to this argument is the following: formal specification is much simpler
than programming — we only have to specify what a system should do, whereas our program must specify
how to do it.
If programmers were trained in formal specification languages the same way they were trained in pro-
52
gramming languages, reading and using formal specifications would not be an issue. Thus, the cost of
formal specification would be significantly less than the cost of implementation – although its value may
not be.
That is not to say that formal specification should be used in every organisation or on every project: it is
only cost effective when the cost of releasing software with faults is greater than the cost of preventing
them. In environments where the biggest issue is responding to change (e.g. change of customer prefer-
ences, change of requirements from a client), ensuring the software is correct is not as high of a priority
as getting something out that works for most normal cases.
In high integrity systems, formal specification is generally only applied to those parts of the system
that contain high-integrity requirements. Other parts of the system, for example, data storage or user
interface, are engineered using more standard techniques.
3.4 Logic and set theory
Most formal model-based specification languages are based on first-order predicate logic and set theory;
both topics with which all students in this subject are expected to be familiar. The set theory part of Alloy
contains the standard set theoretic concepts, such as set comprehension, power sets, set operators (union,
intersection, subset, etc.), Cartesian products, relations, and functions. The predicate logic part of Alloy
contains propositional operators (“and”, “or”, “implies”, etc.) and existential and universal quantifiers
(“for all”, “there exists”).
The most important concepts with which you are expected to be familiar are:
• First-order logic: conjunction (“and”), disjunction (“or”), implication, negation, universal quan-
tification (“for all”), and existential quantification (“there exists”).
• Set theory: set membership, set comprehension, set union, set intersection, set difference, and
subsets.
• Relations: Cartesian product, domain and range of relations, sequences, partial functions, and total
functions.
If you are not familiar these concepts, or the details are hazy, Chapter 3 of Daniel Jackson’s book Software
Abstractions: Logic, Language, and Analysis [1], provides a detail description of these concepts, and how
to express them in Alloy.
If you cannot access this, Chapters 2-9 in Woodcock and Davies book “Using Z” [4] provides a detailed
description of basic logic and set theory, which can also be read lightly to pick up the most important
aspects. A PDF version of this book is available on the LMS for download.
Why sets and predicates?
Most model-based languages are based on sets and predicates. The reasons are quite simple:
1. Set theory (sets, relations, functions, etc.) provides us with enough notation to specify the data of
any system. Just as importantly though, it restricts us from specifying design detail, and forces us
to think more abstractly — something which is important at the specification level.
53
2. Predicate logic provides us with sufficient expressive power to specify what changes the data of a
system can undergo over time (or the effect of operations on data). Just as importantly, it restricts
us from specifying how those changes should be implemented.
These first two points are important: requirements engineering is already difficult enough without
us thinking about design and implementation problems at the same time.
3. Together, proof theory and understanding of set theory and predicate logic is strong enough that
we can prove many properties specified using these parts. We can take advantage of hundreds of
years of work in these areas and apply this to software engineering — a field that is relatively new
in comparison.
Example 3.1
As an example of using logic and set theory to specify software and system behaviour, and how this
differs from programming, consider the trivial example of sorting a list of integers.
To specify the behaviour of a procedure that takes a (possibly unsorted) list and sorts it, we have to say
that:
1. each element in the input list is present in the output list (that is, the output list is a permutation of
the input list); and
2. each element of the output list is less than or equal to the next (that is, the list is sorted).
We can specify what it means for a list of integers to be sorted:
∀ list : seqZ • sorted(list) ⇔ ∀ i : 1 . .#list − 1 • list(i) ≤ list(i + 1)
This states that, for all sequences of integers, that sequence is sorted if and only if : for each index (except
the last) in the list, the integer at that index is less than or equal to the integer at the next index.
We can also specify what it means for one list to be a permutation of the other.
∀ a, b : seqZ • permutation(a, b) ⇔ ∀ el : Z • count(el, a) = count(el, b)
in which count is defined as:
∀ el : Z; list : seqZ; c : N • count(el, list) = c ⇔ c = #(list B {el})
That is count(el, list) = c if and only if c is the number of elements when we take the range restriction
of list on el only.
Finally, we can specify the precondition and postcondition of a procedure using these concepts:
procedure sort(in list : in seqZ; out list : out seqZ) is
pre #in list > 1
post sorted(out list) ∧ ispermutation(in list, out list)
Here, we have added a superficial precondition that the list is not empty.
So, this example specifies the functional behaviour of sorting a list, but does NOT specify how the list
should be sorted. Any correct algorithm for sorting integers will suffice to fulfil the specification above.
2
54
Example 3.2
Using the definitions above, we can specify the behaviour of a binary search algorithm in a straightfor-
ward manner:
procedure binary search(list : in seqZ; target : in Z; index : out Z) is
pre sorted(list)
post target = list(index) ∨ (target = −1⇔ target 6∈ ran(list))
In this example, we have called the procedure “binary search”, but only because we specify in the
precondition that the input list must be sorted. Other than this, the algorithm is not constrained by any
particular binary search algorithm (e.g. recursive or iterative), and in fact, does not truly constrain it to a
binary search algorithm at all.
We can see from this example: specifying binary search behaviour is much neater than specifying the
algorithm to achieve it.
2
3.5 Introduction to Alloy
In this chapter, we will present the Alloy language. We will present only a small subset of the language,
but it is enough to allow specification of and reasoning about abstract state machines. The Alloy has a
rich notation for combining predicates, which allow flexible and incremental building of specifications.
We will not go into detail on all of these operators, but we will use some of the basic components to
enable clean specification.
An Alloy reference manual is available online at:
https://alloytools.org/spec.html
It provides detailed syntax and semantic definitions.
3.5.1 Key characteristics
There are a few key ideas to Alloy that are worth noting.
Alloy = logic + language + analysis
The complete Alloy framework consists of three parts:
1. Logic: This consists of first-order predicate logic, plus a simple relational calculus.
2. Language: This consists of a handful of operators for structuring specifications, such as specifying
types, predicates, functions, and assertions.
3. Analysis: This performs an exhaustive search (in bounded domains) for counterexamples using
SAT (satisifiability check).
55
Non-specialised logic
The logic of Alloy is not tied to a particular paradigm, such as concurrent modelling or state machines.
However, as of Alloy 6, it has a number of useful features for modelling state machines. In this subject,
we will focus primarily on using Alloy as a model-based language for specifying state machines.
Not proof, but counterexamples
The focus of the Alloy Analyser is not to prove a model has a particular property, but instead is to find
counterexamples that demonstrate the property does not hold. Finding such a counterexample demon-
strates that the property does not hold, while failing to find one demonstrates that the property may hold.
In particular, Alloy encourages the use of small, finite domains on which to do analysis. That is, rather
than search for a counterexample in a large or finite set of objects, it encourages (at least during initial
analysis) to use small domains of only a few objects; e.g. only three usernames, URLs, and passwords.
This approach has two advantages:
1. Using small domains means the analysis is much faster, as there are less objects to check.
2. Most assertions are (initially) wrong, and small counterexamples to them can be found. Having
small counterexamples means understanding the counterexample itself is more straightforward.
The aim of the Alloy Analyser though is not to search a handful of arbitrary “test” cases in a small
domain, but search for all possible cases in that domain. The real domain may be infinite, but a small,
finite set representing that domain will find almost all flaws.
3.6 Alloy Logic
In this section, we present the logic used in Alloy, which is based on predicate logic and relational
calculus.
We will use the example of a password management system, such as LastPass, which remembers the
username and password combinations for particular websites (represented by their URL).
3.6.1 Everything is a relation
Alloy uses relations for all data types, including scalars and sets. Sets are just unary relations; e.g.:
Username = {(U0), (U1), (U2)}
URL = {(UR0), (UR1), (UR2)}
Password = {(P0), (P1), (P2)}
scalars are just singleton sets:
myUsername = {(U0)}
myPassword = {(P1)}
and relations are just sets of tuples:
56
passwords = {(U0, P1), (U1, P2)}
This above represents a mapping of usernames to passwords, in which username U0 maps to password
P1. An alternative could be to have a password for a particular URL:
urlPasswords = {(U0, UR0, P1), (U0, UR1, P2), (U1, UR0, P2)}
Here, user U0 has two passwords for two different URLs (UR0 and UR1), while user U1 has only one
password.
An important restriction of relations in Alloy is that all relations are first-order; that is, we cannot have
sets of sets, or relations of relations. If using a set of sets is a natural way to represent a problem, we
instead need to find another representation. This restrictions are is what permits the Alloy language to
be (relatively) easily analysed by automated tools.
3.6.2 Operators
Set operators Sets (represented as unary relations) are a commonly-used data type in formal mod-
elling. In Alloy, the main set operators are included in the logic:
Operator Meaning Example
+ union {(U0)} + {(U1)}
& intersection {(U0)} & {(U1)}
- difference {(U0), (U1)} - {(U1)}
in subset {(U1)} in {(U0), (U1)}
= equality {(U0), (U1)} = {(U0), (U1)}
Cross product A cross product, represented using the operator × in set theory, is represented as the
following in Alloy:
Operator Meaning Example
-> cross product Username->Password
As an example, consider the cross products taken from URLs, usernames, and passwords:
Username = {(U0, U1, U2)}
URL = {(UR0, UR1, UR2)}
Password = {(P0, P1, P2)}
Username->Password = {(U0, P0), (U0, P1), (U0, P2),
(U1, P0), (U1, P1), (U1, P2),
(U2, P0), (U2, P1), (U2, P2)}
Username->URL->Password = {(U0, UR0, P0), (U0, UR0, P1), (U0, UR0, P2),
(U0, UR1, P0), (U0, UR1, P1), (U0, UR1, P2),
...
(U2, UR2, P0), (U2, UR2, P1), (U2, UR2, P2)}
Join operators There are two operators for joining relations in Alloy:
57
Operator Meaning Example
. dot join URL.passwords
[] box join (Username->Password)[myUsername]
These operators are are related via the following properties:
a.b = b[a]
a.b.c[d] = d.(a.b.c)
A dot join is a relational join, which maps the range of the relation on the left to the domain of the relation
on the right. For example:
A B A.B
(a,b)
,,
%%
(a,d,c) (a,c,c)
(a,c)
,,
(b,c,c)
22
(a,a,d)
(b,d) (c,c,c)
22
(b,a,d)
55
For the relations:
myUsername = {(U0)}
myUrl = {(UR1)}
urlPasswords = {(U0, UR0, P1), (U0, UR1, P2), (U1, UR0, P2)}
the following hold:
myUsername.urlPasswords = {(UR0, P1), (UR1, P2)}
myUsername.urlPasswords[myUrl] = {(P2)}
which matches the U0 in myUsername to the same value in urlPasswords.
Restriction and override The concepts of domain restriction, range restriction, and function override
are represented as follows:
Operator Meaning Example
<: domain restriction urlPasswords <: myUrl
:> range restriction urlPasswords :> Password
++ function override urlPasswords ++ (myUsername->myUrl->newPassword)
Function override is defined as: A ++ B = (A - (domain[B] <: A) + B).
Informally, domain restriction, A <: B takes the relation A and restricts it to contain only the tuples
whose domain is in B, while range restriction, A :> C restricts A to contain only the tuples whose
range is in C. For example, if we have:
myUsername = {(U0)}
myUrl = {(UR1)}
someUsernames = {(U0), (U1)}
somePasswords = {(P1), (P3)}
newPassword = {(P4)}
58
urlPasswords = {(U0, UR0, P1), (U0, UR1, P2), (U1, UR0, P2), (U2, UR3, P3)}
then the following hold:
urlPasswords <: someUsernames = {(U0, UR0, P1), (U0, UR1, P2), (U1, UR0, P2)}
urlPasswords :> somePasswords = {(U0, UR0, P1), (U2, UR3, P3)}
For override, we supply at least one new tuple, which is added to the relation, but with the further
constraint that it overrides the value of any existing tuple with the same domain; e;g.:
urlPasswords ++ (myUserName->myUrl->newPassword) =
{(U0, UR0, P1), (U0, UR1, P4), (U1, UR0, P2), (U2, UR3, P3)}
Propositional logic operators
Alternative
Operator Operator Meaning
! not negation
&& and conjunction
|| or disjunction
=> implies implication
<=> iff if-and-only-if
Predicate logic quantifiers Assuming that x is a variable, e is a set expression, and F is a formula, the
following are valid formula:
Operator Meaning
all x : e | F F holds for every x in e
no x : e | F F holds for no x in e
some x : e | F F holds for at least one x in e
lone x : e | F F holds for at most one x in e
one x : e | F F holds for exactly one x in e
For example, the following two predicates specify that: (1) there is at least one password for the user
corresponding to myUsername; and (2) no username has more than one password:
(1) some pwd : Password | (myUsername->pwd) in passwords
(2) all user : Username | lone pwd : Password | pwd in user.passwords
The last four of these keywords can also be applied to relations:
Operator Meaning
no e e has no elements/tuples
some e e has at least one element/tuple
lone e e has at most one element/tuple
one e e has exactly one element/tuple
For example, we can re-write the two examples above as:
59
(1) some myUserame.passwords
(2) all user : Username | lone user.passwords
Set declarations In predicates and other constructs that use sets, modifiers are used to restrict the
elements in declarations:
Operator Meaning
x : set e x is a subset of e
x : one e x is a singleton subset of e
x : lone e x is an empty or singleton subset of e
x : some e x is a non-empty subset of e
x : e x : one e
Set comprehensions New sets can be constructed from expressions:
Operator Meaning
{x : e, y : f | F} A set of tuples that satisfy formula F
We can construct a set of usernames that have the password myPassword:
{user : Username | user in password.myPassword}
Cardinalities
Operator Meaning
#e The number of tuples in expression e
0,1,2 Integer literals
+, - Addition, subtraction
=, <=, <, >=, > Arithmetic operators
Note that there is no multiplication or division! Multiplication and division are not included in the
language because SAT solvers are typically unable to deal with them in the general case.
let formula and expressions A let expression is a shorthand:
Operator Meaning
let x = e | formula
let x = e | expression
For example:
60
all user : Username | let p = user.passwords | lone p && some p
3.6.3 Temporal Logic: specifying systems that change over time
As of Alloy 6, released in late 2021, Alloy’s logic now includes termporal operators that allow one to
talk not only about whether something might be true in a single state but also in future or even past states.
These changes add features to Alloy’s logic that do not make it less expressive but do make it easier to
specify state machines and, in particular, transitions between states and properties that need to hold at
certain times.
Next State The ’ operator is used to evaluate an expression in the next state. For instance, we can
specify that the value of user.passwords does not change between the current state and the next.
user.passwords’ = user.passwords
Temporal Predicate Operators Given predicates P and Q, the temporal operators allow us to talk
about whether they hold in certain states before or after the current one. Specifically, a series of states of
our model is called a trace. Given a trace, suppose the current state is the ith state in the trace. Then we
define what it means for each of the temporal operators to hold in state i as follows:
Operator Meaning
always P P holds in all states ≥ i
after P P holds in the i + 1th state
eventually P P holds in some jth state where j ≥ i
P until Q P holds in some state ≥ i, and Q holds for every kth state such that i ≤ k < j
P releases Q Q is true in every state ≥ i up to and including a state k in which P is true,
or there is no such k in which case Q holds in any state ≥ i
P ; Q P is true in the ith state and Q is true in the i + 1th state
For instance we can specify that in the next state user.passwords is empty:
after (no user.passwords)
3.7 Alloy Language
The Alloy language provides constructs to structuring specifications using the logic presented in the
previous section.
3.7.1 Modules
Alloy modules are analogous to modules in programming languages, so are used to model self-contained
specifications that can be imported into other modules and re-used.
61
3.7.2 Signatures
Signatures are essentially type declarations:
Operator Meaning
sig A {} Declare a set of atoms called A
sig B {} Declare a set of atoms called B, which is disjoint from A
sig A, B {} As above
sig B extends A {} Declate sets A and B where B a subset of A (B in A)
abstract sig A {} An abstract set of atoms
sig B, C extends A {} B and C are disjoint, and ((B+C) in A)
sig A {f : e} f is a binary relation of type f : A -> one e
The var keyword can be applied to signatures or to fields to specify things that can change over time.
For example we can declare a set A of atoms as a a variable set: var sig A {}. Doing so means that
the atoms contained in A in the current state might not be contained in A in future states, and vice-versa,
i.e. atoms from A can appear and disappear over time, depending on how we constrain the transitions of
our model.
The var keyword is more often applied to signature fields, to model fields whose values can change over
time.
For example, we can construct types for keeping track of passwords at particular URLs:
sig URL {}
sig Username {}
sig Password {}
sig PassBook {var password : Username -> URL -> Password}
Here, PassBook models a set of “known” username and URL combinations, and the passwords for
these. There is, at most, one password per pair (passwords is a function).
Note that if A is a non-var set, then we cannot write var sig B extends A {}, i.e. variable
signatures cannot extend non-variable ones. However, non-variable sigs can extend variable ones.
3.7.3 Facts
Facts are (possibly named) constraints that are assumed to hold in all initial states. They are introduced
using the fact keyword.
Operator Meaning
fact f { F } Declare a fact called f that introduces a formula F.
For example, we can express that a PassBook can have at most one password for each user/URL pair:
fact NoDuplicates
{
always all pb : PassBook, user : Username, url : URL | lone pb.password[user][url]
62
}Note that this fact uses the always keyword to say that it holds not only in the initial state, but in all
subsequent states too (e.g. after certain operations might be performed on the PassBook).
Such facts can be used to model invariants over the data types: constraints that always hold for the data
types.
3.7.4 Predicates
Predicates are named formula with declared parameters. Predicates can be accessed from within other
predicates.
Operator Meaning
pred p [x : e] { F } Declare a predicate called p, which has variable(s) x, and formula F
In this subject, we will primarily be using predicates to introduce operations over a state. That is, a set
of signatures will define the state space, and predicates will define the possible transitions over that state
space.
For example, we can have operations that add and delete passwords to a PassBook:
//Add a password for a new user/url pair
pred add [pb : PassBook, url : URL, user: Username, pwd: Password] {
//no pb.password[user][url]
pb.password’ = pb.password + (user->url->pwd)
}
//Delete an existing password
pred delete [pb : PassBook, url : URL, user: Username] {
one pb.password[user][url]
pb.password’ = pb.password - (user->url->Password)
}
Notice how we use pb.password’ to refer to the password mapping in the next state after these
operations have run.
The add predicate specifies an operation over a PassBook pb, and three input variables user, url, and
password.
The first line of the predicate part states that there is not already a password for this user/URL pair (the
set formed by pb.password[user][url] is empty). This is a precondition: the behaviour of the
operation is not defined if this doesn’t hold. A precondition specifies an assumption made about the
initial state from which an operation occurs. We refer to that state as the pre-state.
The second line is the postcondition, which specifies what transition should happen if the precondi-
tion holds. In this case, a union (+) is used with the ’ symbol to specify that in the subsequent state
pb.password is the same as in the pre-state, but with the new details added. The subsequent state
reached after an operation is performed is called the post-state, so pb.password’ refers to the value
of pb.password in the post-state.
The delete predicate specifies an operation that deletes a password from the PassBook. The precon-
dition requires that a password exists, and the postcondition uses set difference to remove the tuple from
63
the relation of passwords.
Predicates can be accessed within other predicates. For example, we could break the precondition of the
add predicate into its own predicate, and reference this:
//True if and only if the user has no password for this URL
pred preAdd [pb : PassBook, url : URL, user: Username] {
no pb.password[user][url]
}
//Add a password for a new user/URL pair
pred add [pb, url : URL, user: Username, pwd: Password] {
preAdd [pb, url, user]
pb.password’ = pb.password + (user->url->pwd)
}
The semantics of the add operation is exactly as if the body of preAdd was included where it was
referenced, with the appropriate variable renamings.
Note that this is not sequence composition. That is, preAdd is not “executed”, followed by the line after
it. The formulae in preAdd and add are evaluated as one larger conjunction.
3.7.5 Functions
Functions are named expressions with declared parameters and expressions. They can be ‘invoked’ by
predicates or other functions by providing an expression for each parameter.
Operator Meaning
fun f [x : e1] : e2 { F } Declare a function called f, which has variable(s) x, for-
mula F, and is of type e2
For example, we can lookup the password for a given user/URL pair:
//Return the password for a given user/URL pair
fun lookup [pb: PassBook, url : URL, user : Username] : lone Password {
pb.password[user][url]
}
Note the expression/type given to the function: (lone Password). This function can be used as an
expression as part of another formula. For example:
//Check if a user’s passwords for two urls are the same
pred samePassword [pb : PassBook, url1, url2 : URL, user : Username] {
lookup [pb, url1, user] = lookup [pb, url2, user]
}
3.7.6 Assertions
Assertions are named constraints that are intended to follow from a model. We can express assertions
using formula using:
64
Operator Meaning
assert a { F } Declare an assertion called a, with the formula F
For example, we can express the following assertion which says that the add predicate “works”:
//If we add a new password, then we get this password when we look it up
assert addWorks {
all pb : PassBook, url : URL, user : Username, p : Password |
add [pb, url, user, p] => (after (lookup [pb, url, user]) = p)
}
Notice how we use the after operator to talk about what should be true in the state after add has
occurred (i.e. in the post-state).
Assertions can be checked by the Alloy Analyser (see Section 3.8).
Typically, assertions play two roles: (1) to express some properties that we should hold on the model,
which we can then check to see if there are flaws in our model (or flaws in our assertions!); and (2)
to express essential properties of the model that are higher than facts or properties; e.g. relationship
between predicates; which give us a better understanding of the model itself, and possible ways to refine
or restructure it.
3.7.7 The complete LastPass example
module LastPass
/*
* LastPass password map
*
* A simple example to explain basics of Alloy.
*
* The ’PassBook’ keeps track of a set of users’ passwords for a set of URLs.
* For each User/URL pair, there is one password. Passwords can be added,
* deleted, looked up, and changed.
* A user can also request all of their password for all URLs.
*
* author: Tim Miller; updated by Toby Murray for Alloy 6, including simplifications
*/
sig URL {}
sig Username {}
sig Password {}
sig PassBook {var password : Username -> URL -> Password}
fact NoDuplicates
{
always all pb : PassBook, user : Username, url : URL | lone pb.password[user][url]
}
//Add a password for a new user/url pair
pred add [pb : PassBook, url : URL, user: Username, pwd: Password] {
//no pb.password[user][url]
pb.password’ = pb.password + (user->url->pwd)
}
65
//Delete an existing password
pred delete [pb : PassBook, url : URL, user: Username] {
one pb.password[user][url]
pb.password’ = pb.password - (user->url->Password)
}
//Update an existing password
pred update [pb : PassBook, url : URL, user: Username, pwd: Password] {
one pb.password[user][url]
pb.password’ = pb.password ++ (user->url->pwd)
}
//Return the password for a given user/URL pair
fun lookup [pb: PassBook, url : URL, user : Username] : lone Password {
pb.password[user][url]
}
//Check if a user’s passwords for two urls are the same
pred samePassword [pb : PassBook, url1, url2 : URL, user : Username] {
lookup [pb, url1, user] = lookup [pb, url2, user]
}
//Retrieve all of the passwords and the url they are for, for a given user
pred retrieveAll [pb: PassBook, user : Username, pwds : URL -> Password] {
pwds = (pb.password)[user]
}
//Initialise the PassBook to be empty
pred init [pb: PassBook] {
no pb.password
}
//If we add a new password, then we get this password when we look it up
assert addWorks {
all pb : PassBook, url : URL, user : Username, p : Password |
add [pb, url, user, p] => (after (lookup [pb, url, user]) = p)
}
//If we update an existing password, then we get this password when we look it up
assert updateWorks {
all pb : PassBook, url : URL, user : Username, p, p2 : Password |
after (lookup [pb, url, user]) = p =>
(add [pb, url, user, p2] => (after (lookup [pb, url, user]) = p2))
}
//If we add and then delete a password, we are back to ’the start’
assert deleteIsUndo {
all pb : PassBook, url : URL, user : Username, pwd : Password |
(add [pb, url, user, pwd] ; delete [pb, url, user])
=> pb.password = pb.password’’
}
run add for 3 but 1 PassBook
run add for 5 URL, 5 Username, 10 Password, 1 PassBook
check addWorks for 3 but 1 PassBook expect 0
check updateWorks for 3 but 1 PassBook expect 0
check deleteIsUndo for 1 but 1 PassBook
66
3.8 Analysis with the Alloy Analyser
The Alloy language supports two types of construct that are related specifically to the Alloy Analyser.
That is, they say nothing about the model itself, but are merely there to tell the analyser what to check,
and to provide some parameters for this checking.
In these notes, we briefly discuss the main concepts in analysing Alloy models, and we will explore this
more in the workshops.
3.8.1 Runs
The run command instructs the analyser to search for instances of values that satisfy a predicate:
Operator Meaning
run p scope Find a valid binding of variables for predicate or function p, such that these values
satisfy F, using the declared scope.
For example, given the operation add defined in the previous section, we can check that the add is
satisfiable in a domain of size three using:
run add for 3
This instructs the analyser to search for values for the variables, assuming that there are three instances
of each signature in the universe; that is, three usernames, three URLs, three passwords, and three pass-
books. We can restrict different variables to have different sizes. For instance, add talks only about one
Passbook pb, so it makes sense to restrict the size of this set to just 1 when running the predicate.
run add for 3 but 1 PassBook
As well as bounding the size of signatures, we can also bound the number of transitions that Alloy ex-
plores. For instance, add talks only about a pre-state and a post-state, i.e. only one transition. Therefore
it makes sense to run it for just a transition between two states:
run add for 3 but 1 PassBook, 1..2 steps
This specifies that the domain should be of size three, except only one PassBook, and only two states
(state 1 and 2), i.e. one transition.
If an instance can be found, this means that the predicate/function is indeed satisfiable. If an instance
cannot be found, this means predicate/function is possibly unsatisfiable. It may be that the scope we used
is simply not large enough to find a combination that satisfies the formula.
The idea of scope is discussed in more detail in Section 3.8.3.
3.8.2 Checks
The check command instructs the analyser to search for instances of values that violate an assertion —
a counterexample to the assertion:
67
Operator Meaning
check a scope Find a counterexample to the assertion a, such that these values satisfy not F,
using the declared scope.
For example, given the assertion addWorks defined in the previous section, we can attempt to find a
counterexample to the assertion in a domain of size three using:
check addWorks for 3
If a counterexample can be found, this means that the assertion does not hold on the model; i.e. the
assertion is incorrect. If a counterexample cannot be found, then the assertion may hold, or there may be
a counterexample that exists outside of the specified scope.
3.8.3 Scope
The idea of scope is straightforward. The Alloy Analyser does bounded checking, meaning that it can
(potentially) try every combination of assignments of instances to variables – although the searching
algorithms are much more sophisticated and eliminated many infeasible combinations early on. For
models that use the var keyword, the scope also includes the number of transitions in the model to
search up to.
The scope can be modified to increase or decrease the number of instances that are used in the search.
For example, we can use the following:
run add
run add for 5
run add for 5 but 3 PassBook
run add for 5 URL, 5 Username, 10 Password, 2 PassBook
run add for 5 but 1..2 steps
The first line omits a scope, in which case the default scope of three is used. When the scope doesn’t
include a bound on the steps the default is to check for 1..10 steps. The second command specifies
that five instances of all signatures should be used, while the third says the same, except only create three
signatures for PassBook. The fourth one explicitly lists the number of instances for each signature
type.
The final one also specified a bound on the number of steps (transitions to search). Perhaps confusingly,
1 steps refers to 0 transitions, i.e. to traces that contain a single state. In general k steps means traces
containing k states and k-1 transitions. Therefore “1..2 steps” means traces with a single transition.
One can of course search for instances of add within longer traces but doing so doesn’t make much
sense for this model, since add only talks about two states (one transition).
There are several other ways to restrict the scope used in analysis. See Jackson’s book on Alloy [1] for
information on these.
The small scope hypothesis
By finding instances and counterexamples to predicates, functions, and assertions, we are essentially just
doing exhaustive testing using a bounded set of instances. As such, we cannot prove that an assertion
68
holds for all instances in the universe. This is essentially like testing, but in actually, it is much more
powerful.
The Alloy philosophy relies on the small scope hypothesis, which is:
Most faults have small counterexamples.
That is, if an assertion in incorrect, it often, but not always, has a counterexample that exists in a small
scope of around 3-5 instances per signature and a handful of transitions (around 10). The Alloy Analyser
exhaustively analyses all cases within the declared scope, therefore, if a counterexample exists within
that scope, it will be found. This is in contrast to testing, which will select a small subset of the scope as
test inputs.
Using larger scopes increases the chance of finding a counterexample of course, but also takes more CPU
time and memory to analyse. A good strategy is to start with a small scope of three and a few transitions,
which is usually sufficient to find flaws in our models early on, eliminate these flaws, and once all of our
runs and checks pass, extend the scope to be much larger, and leave the analyser to search for a period
while we go for a coffee.
3.9 Abstraction and incremental specification of state machines using Al-
loy operators
In the previous section, we saw how to create operation predicates from smaller operation predicates;
e.g. by specifying the precondition of the add operation in its own predicate preAdd and referencing
this. Using predicates as building blocks in this way is an excellent abstraction mechanism that can be
used to our advantage in specifying systems.
The formal specification process is not as simple as sitting down and writing it. As with programming,
specifications should be put together incrementally. Abstraction mechanisms can be used to perform this
top down, allowing us to specify greater detail once we have a better understanding of the problem at
hand.
3.9.1 The specification process
Figure 3.3 outlines a process model for defining a formal specification. Clearly, this model is idealised,
and at any step, there is a likelihood of returning to any of the earlier steps; however, it serves well
as a model. The problem is tackled top down. Given a set of informal requirements, we define the
system components and specify them. After this, we bring all components together. To specify a single
component in isolation, we first consider the data types, and then the state, init operation, and normal
operations. This is a specification of how the components works in an ideal world. After this, we add
the exceptional behaviour. The total operations are then composed from the normal and exceptional
behaviour operations. Finally, the component specifications are composed into a system specification.
State machines
In these notes, we focus on the use of Alloy to specify and analyse abstract state machines: that is, an
abstract model containing a state (e.g. PassBook) and transitions between these states. The LastPass
69
Figure 3.3: A process to develop a model-based specification of a system.
password storage system defined in this chapter is one such example.
To do this, we used the abstract state machine pattern. Using this pattern, we use predicates to specify
operations on a global state. An abstract state machine in Alloy will have a structure something similar
to:
sig State { ... }
pred init [s: State] { ... }
pred inv [s: State] { ... }
pred op1 [s: State] { ... }
...
pred opN [s: State] { ... }
in which init is the initialisation operation, inv is the state invariant, and Op1 ... OpN is a set of
N operations, such as add and delete in the password storage system. A state invariant is a formula
that all operations in the specification must preserve.
Operation preconditions
We have already seen an example of splitting a precondition from its postcondition in an operation. How-
ever, an alternative way, which is preferred by many modellers, is to specify the precondition and post-
condition together as separate predicates, and then bring them together in a single operation predicate.
For example, we can declare operations addPre and addPost for the precondition and postcondition
respectively, and then conjoin them in addOk:
//True if and only if the user has no password for this URL
pred preAdd [pb: PassBook, url : URL, user: Username] {
no pb.password[user][url]
}
//Add a password for a user/url pair
pred postAdd [pb : PassBook, url : URL, user: Username, pwd: Password] {
pb.password’ = pb.password + (user->url->pwd)
}
//Add a password for a *new* user/url pair
pred addOk [pb : PassBook, url : URL, user: Username, pwd: Password] {
preAdd [pb, url, user]
postAdd [pb, url, user, pwd]
}
70
This has the effect of being more verbose, but providing a clear separation between the preconditions
and the postconditions that specify the transitions, which many people find preferable.
Exceptional cases
An important step in Figure 3.3 is the separation of “normal” cases from “exceptional” cases. The idea is
to specify normal cases for a model first, which are usually the most difficult parts, and then to enumerate
all the relatively-easier exceptional cases after this, many of which just “fall into place”.
To achieve this, we specify the normal and exceptional cases in separate predicates, and then combine
them using disjunction in one operation predicate:
//Add a password for a *new* user/url pair
pred addOk [pb : PassBook, url : URL, user: Username,
pwd: Password, report : one Report] {
preAdd [pb, url, user]
postAdd [pb, url, user, pwd]
report in Success
}
//Fail to add a password that already exists
pred addDuplicate [pb : PassBook, url : URL, user: Username,
report : one Report] {
not preAdd [pb, url, user]
pb.password’ = pb.password
report in Failed
}
//Add a password for a *new* user/url, otherwise, add nothing
pred add [pb : PassBook, url : URL, user: Username, pwd: Password,
report : one Report] {
addOk [pb, url, user, pwd, report]
or
addDuplicate [pb, url, user, report]
}
So, the add predicate specifies an operation in which we attempt to add a duplicate entry, in which case
nothing changes, or the entry is not a duplicate, in which case we add the new entry.
Note the introduction of the variable report : Report, which is used as an output variable to specify
whether the operation was successful or not. The Report signature is declared as:
//Report signatures for informing whether an opertion was successful or not
abstract sig Report {}
sig Failed extends Report {}
sig Success extends Report {}
Thus, report must take on a value from either Success or Failed, which are disjoint sets.
This is useful in assertions where we can specify a case that we expect to e.g. fail, and check that it failed
by simply checking that report in Failed.
71
Invariant preservation
When using the abstract state machine pattern, Alloy assertions provide the opportunity to check that the
invariant is preserved by an operation, using the following template:
assert initEstablishesInv {
all s : State | init [s] => inv [s]
}
check initEstablishesInv
//for each operation
assert opNPreservesInv {
all s : State, x : E, ... |
inv [s] and opN [s, x, ... ] => after (inv [s])
}
check opNPreservesInv
This says that the initial state satisfies the state invariant, and for each operation, if the state invariant
holds on an arbitrary pre-state s, and we execute the operation, the state invariant holds on the post-state
reached after the operation finishes.
As an example, consider this for the init and add operations:
assert initEstablishes {
all pb : PassBook | init [pb] => inv [pb]
}
assert addPreservesInv {
all pb : PassBook, user : Username, url : URL,
pwd : Password, report : Report |
inv [pb] and add [pb, url, user, pwd, report] => after (inv [pb])
Such checks can be useful not only for checking state invariants, which may satisfy safety properties, but
also to check sanity of operations.
For example, checking this assertion using the Alloy Analyser on the delete operation actually re-
vealed a fault that had not been detected before, which was subtle, but which modelled the semantics
completely wrong. The line that removed the tuple from the pb.passbook relation was:
pb.password’ = pb.password’ - (user->url->Password)
Can you see what the fault was?
This predicate states that pb.password is equal in the post-state to itself with the tuple subtracted. The
second occurrence of pb.password’ should have been pb.password. This major semantic prob-
lem had not been discovered using check assertions deleteIsUndo and deleteDuplicateDoesNothing,
due to a strange coincidence in how the operation was modelled and how the way these assertions were
written.
Bounds When checking that the invariant holds initially, of course only one State is needed. Since
no operation occurs, a steps bound of 1..1 is sufficient.
72
When checking that each operation preserves the invariant, likewise only one State is needed, and a
steps bound of 1..2 is sufficient (assuming operations only cause a single state transition to occur).
Summary
So, by using the abstract state machine pattern, and structuring the model in a way that separates normal
from exceptional behaviour, we can incrementally build up a model using a process such as the one in
Figure 3.3.
Further, the abstract state machine pattern then lends itself naturally to some assertions such as checking
whether invariants hold.
3.10 Trace-Based Modelling in Alloy
Alloy 6’s features for modelling variable signatures and fields, and its temporal logic, are ideal for trace-
based models, i.e. ones that talk not just about single transitions (involving at most two states) but models
that talk about sequences of such transitions. In that case one can specify temporal properties that need
to hold over all such sequences. We call those sequences traces.
To construct such a model we use fact declarations to constrain the set of traces that Alloy considers.
Specifically, we need to specify the allowed initial states, from which traces can begin, and the allowed
transitions that can occur between states.
Specifying the initial state Recall that we use a predicate init to specify the initial state. Recall also
that a fact declaration specifies what needs to be true of the initial state in a trace. Therefore we can
specify that the initial states in the trace must satisfy init as follows:
fact {
all s : State | init[s]
}
Note that we do not use the always keyword here: doing so would specify that not only the inital state
satisfies init but all subsequent states in the trace do too!
Specifying the allowed transitions To specify the allowed transitions between states we use a second
fact declaration. Assume we have predicates op1. . .opN to specify the operations (transitions) in our
system. Also assume we have a predicate unchanged[s] which holds for a State s when the post-
state is equal to the pre-state, and so specifies what it means for a state to be unmodified by a transition.
Then we can specify that the only allowed transitions are those given by op1. . .opN or unchanged as
follows:
always {
all s : State | op1[s] or op2[s] or ... or opN[s] or unchanged[s]
}
This fact says that in all states of the trace (not just the initial ones) one of the operation predicates must
hold, i.e. there is a transition to a subsequent state given by one of the system’s operations or the state
remains unchanged (the subsequent state is equal to the current one).
73
The inclusion of the unchanged predicate is important for technical reasons to do with how traces
are represented in Alloy. In particular, Alloy traces represent infinite sequences of transitions as lasso
traces: traces where the final state then transitions to some state already in the trace (possibly the final
one). Without including the unchanged[s] possibility above, we rule out many such traces that Alloy
should consider when analysing our model. Including it means Alloy will consider at least all traces of
finite sequences of operations, each drawn from op1. . .opN above, up to the steps bound, since the
final state is always free to loop back to itself via the unchanged transition.
We will see examples of this kind of model in lectures, including use of the temporal operators to search
for interesting behaviours of it.
3.11 Verification and validation
As with any software engineering artefact, verification and validation (V&V) are important. There are
several techniques for V&V of formal specifications:
1. Animation: Animation is the process of executing a specification with the intention of finding
problems. The formal, unambiguous nature of specifications means that execution is often possi-
ble. However, because Alloy (and many other formal specification languages) is built on first-order
logic, not all specifications are executable.
2. Model checking: This is the process of attempting to execute every operation in every possible
state of a system to show that certain properties hold; or more accurately, the find counterexamples
of the properties. Model checking for model-based languages is difficult because the sheer number
of states may be exponentially large, if not infinite. There is a lot of research around the world
looking at how to reduce the number of states for model checking while maintaining its soundness.
If you have taken the subject Modelling Complex Software Systems, you would used model check-
ing to detect deadlock and to prove temporal logic properties of concurrency models.
3. Review: The specification can be subject to peer review, like any other document. This remains
one of the most important and useful style of review for formal specification, despite advanced
tools that can reason about them automatically.
4. Proof : Automated proof is the holy grail of formal specification. Proof is the most valuable of all
techniques, because, unlike animation, model checking, reviews, and testing, if a property about
a model can be proved, then we can say with strong conviction that it is true (although in reality,
there is always the possibility that the proof itself is wrong!).
In this section, we discuss proof in more detail. This is not a major part of the subject, however, proof is an
important motivation as to why practitioners adopt formal methods in software and systems engineering.
Woodcock and Davies [4] present a far more thorough and valuable use of proof in specification. In this
section, we discuss the implications of proof and present a trivial example.
The importance of proof
If we have a specification of a system, then by reasoning about it using proof, we are likely to detect
problems. If we have not started implementing the system, this provides us with an opportunity to find
74
those problems before they become costly to fix. Further to this, the very process of constructing a proof
can assist us in finding new requirements. That is, if we notice that we cannot prove a property about
a system because some exceptional state has not been considered, we may need to add a requirement
for this exceptional state. As Woodcock and Davies state: “The practice of proof makes for better
specifications”.
There is a commonly-held opinion that proof is not possible on large-scale systems. Industrial application
of formal specification and proof shows this opinion to be wrong. Proof can be applied to real systems
running in real environments, and can provide cost savings too. There are situations where proof is not
relevant or necessary, but to high integrity systems especially, there are situations where it is desirable
and almost necessary. There may be some situations where a sketch of a proof is sufficient. The trick
in apply proof is to know which proofs are worth attempting, which are worth sketching, and which are
worth leaving.
It is worth referencing the following quote:
“Things like even software verification, this has been the Holy Grail of computer science
for many decades but now in some very key areas, for example, driver verification we’re
building tools that can do actual proof about the software and how it works in order to
guarantee the reliability.” — Bill Gates, Chairman of Microsoft, speaking at WinHEC 2002
in Seattle, Washington, USA.
What can be proved?
Proof is used in some high integrity systems for cases in which the outcome of failure is catastrophic; e.g.
resulting in death or complete mission failure. The types of properties that can be proved vary, however,
some interesting properties that have been proved about formal models include:
• Proofs that certain security protocols cannot be cracked. More importantly, there have been cases
of security protocols where proofs have demonstrated that they can be cracked.
The Needham-Schroeder Public-Key Protocol is a protocol intended to provide mutual authen-
tication between two parties communicating over a network, proposed by Roger Needham and
Michael Schroeder in 1978. It was considered to be a failproof way to establish identity between
two communicators, however, it was not until Gavin Lowe applied formal proof to the protocol in
1995 that it was discovered that the protocol failed to prevent “man-in-the-middle” attacks.
• Proofs about safety properties. Many safety-critical systems need to ensure certain safety prop-
erties hold. For example, railway signalling systems need to ensure that, given an intersection of
tracks, at no stage will overlapping tracks both have non-red signals. That is, only one train will be
allowed on the intersection at any time. These properties are specified a temporal logic properties,
and it can be shown that a formal specification preserves these properties.
• In many life-critical systems, formal proof is applied to minimise the risk of causing harm or death.
In many systems employed on NASA space missions, proof is applied to demonstrate that certain
cases cannot go wrong. In particular, NASA has been known to reject entire proposals for systems
on aerospace systems or life-support systems for astronauts unless model checking or proof have
been used to demonstrate mission-critical properties (mainly around safety).
75
What can’t proof do?
It is important not to say that if we “prove” our specification, then it is correct. When we do proofs
about a formal specification, we are only proving that certain properties hold. We can say with some
conviction that, if our proof can be discharged, then the property holds. However, this is only valuable if
the property is correct. Further, we can do many proofs on a system, but omit an important one. Thus, a
set of proofs is only as strong as the set of properties they show.
In most cases, proofs are not automated. That is, we cannot provide a specification and a property, and
then have it proved. There are a large set of proofs for which this is possible, but there is another set for
which it is currently not. The holy grail of theorem proving is exactly to prove things automatically, but
for now, it remains a pipedream. For many logics, it has been proved that not all provable properties of
the system can be automatically proved.
An example
As a small example of how proof words, we’ll use the chemical storage system outlined in Chapter 2.
Example 3.3
In the chemical storage system, the storage tank has a maximum limit, and if that limit is breached,
serious safety consequences may result. However, the overfilling problem is resolved using careful
specification, and we can prove that the storage tank will not overflow, providing our specification is
implemented correctly.
If we consider a Fill operation for filling the take, it will consist of a precondition checking that the
amount going into the tank will not overflow, and a postcondition calculating the new amount. The safety
property we want to check is that the new amount does not overlow the tank:
• Precondition: contents + amount? ≤ capacity
• Postcondition: contents′ = amount? + contents
• Safety property: contents′ ≤ capacity (which is also the state invariant).
We can then structure our proof as follows:
Precondition⇒ (Postcondition⇒ Safety property)
That is, if the precondition is true, then the postcondition implies that the safety property holds.
For the Fill operation, this is straightforward to discharge. We have to prove:
contents + amount? ≤ capacity ⇒
(contents′ = contents + amount?⇒ contents′ ≤ capacity)
Using the one-point rule, we can replace all instances of contents’ with contents + amount?:
contents + amount? ≤ capacity⇒
(contents + amount? = contents + amount?⇒ contents + amount? ≤ capacity)
The middle proposition contents + amount? = contents + amount? is trivially true, so can be removed
from the entire proposition, leaving:
contents + amount? ≤ capacity⇒ contents + amount? ≤ capacity,
76
which is itself trivially true.
So, we have proved that, for every possible state of the system, if the precondition holds, the storage
container will not be filled beyond capacity. This is much stronger than throwing a million tests at it to
see if we can find an input/state pair that violates it.
2
The above example is trivial in comparison to the types of proofs that are discharged on industrial scale
systems, however, it illustrates how proof can be applied to a model-based specification.
Despite the small size, the proof required a few steps are several lines of text. However, significant tool
support is available for theorem proving. Using automated and interactive theorem provers, many large
proofs can be discharged in a fairly straightforward manner, and the documentation of the proof can be
generated automatically and stored digitally.
Bibliography
[1] Daniel Jackson. Software Abstractions: logic, language, and analysis. MIT press, 2012.
[2] Mark Phillips. CICS/ESA 3.1 experiences. In Z User Workshop, Oxford, pages 179–185, 1989.
[3] Ian Sommerville. Software Engineering. Pearson Higher Education, USA, 9th edition, 2010.
[4] Jim Woodcock and Jim Davies. Using Z: specification, refinement, and proof, volume 1. Prentice
Hall, 1996.
77
Chapter 4
Introduction to Ada
In this chapter, the programming language called Ada is introduced. We will use Ada in this subject
for two reasons. First, due to its features, it is heavily used in high integrity domains, especially safety-
critical systems. Second, later in the course, we will look at a language called SPARK, which is a safe
programming subset of Ada, and has tool support for high integrity systems.
4.1 The history of Ada
Ada is a structured, imperative programming language with static typing, object-orientation, and concur-
rency built in. It was designed by a company called CII Honeywell Bull, who were contracted by the
United States Department of Defence (DoD) in 1977 to provide a new language to replace the hundreds
of programming languages they were using at the time. The DoD were concerned that many of the lan-
guages were specific to particular hardware, and thus not reusable, and were becoming obsolete. Further,
many of the languages did safe, modular programming, which had emerged as the best way to handle
large-scale, complex software systems.
At the time, four contractors were asked to provide proposals for a new language, having been given a
set of criteria. The CII Honeywell Bull group’s proposal was chosen as the successful proposal, and the
Ada language was then designed.
Initially intended to be for embedded and real-time systems, Ada has become highly used in high integrity
systems in general, especially in the safety-critical domain. Further, it was used as a general purpose
programming language in the 1980s, however, due to its property of being designed to be read rather
than written, many people do not consider it suitable for the more “enterprise-y” applications, where
time-to-market is considered more important than software integrity.
The language is named after Augusta Ada King, Countess of Lovelace (more generally known as Ada
Lovelace, who is credited as being the first even computer programmer. In 1843, when translating notes
a seminar by Charles Babbage on his analytical engine (the first programmable machine) from French
to English, Ada Lovelace included an addendum showing how an algorithm for calculating Bernoulli
numbers using the analytical engine, which is considered the first program ever written. However, there
is significant controversy as to her actual intellectual input into the algorithm, but much of this is incon-
sistent with Babbage’s own recollection.
78
4.2 Ada for programming “in the large”
A major criteria given by the DoD for a new programming language is that it must provide strong support
for good software engineering practices that scale well to very large software systems ( 106 lines of code,
and large development teams).
There are several aspects of Ada that contribute to this:
• A strong, static and safe type system. In particular, all type errors can be detected automatically at
compile time.
• Modularity. Ada modules (called “packages”) can be compiled separately from all other packages.
Interfaces can also be compiled without the need for an implementation, meaning that some design
errors can be detected before implementation begins.
• Information hiding. In providing interfaces/packages, Ada separates interfaces from implementa-
tion, and provides fine-grained control over visibility.
• Readability. This is an important aspect. Ada favours the reader of the program over the writer.
That is, the aim is to make Ada programs easier to read (compared to other languages), possibly
at the expense of making them slightly harder to write. This is because a program is written once
but read many times during maintenance.
• Portability. Ada does not include any platform-specific operators or data types, and can thus be
compiled on many platforms.
• Standardisation. Ada is an ANSI and ISO standard.
It is these properties, plus others, that makes Ada a great fit for high integrity systems, and is why it is
still used in so many high integrity domains today. The language itself continues to evolve, and a new
ISO/IEC standard for Ada was published in December 2012.
4.3 Hello World
A great way to introduce a programming language to people with programming experience is to start
programs. So, let’s start with a very traditional program to introduce Ada:
Listing 4.1: hello world.adb
1 with Ada.Text_IO;
2
3 procedure Hello_World is
4 begin
5 Ada.Text_IO.Put_Line("Hello, world!");
6 end Hello_World;
Note the following about this program:
• The with statement adds the Ada.Text IO package to the program. This is basically a library
import, similar to the “import” functionality in Java, or the #include keyword in C.
79
• Block structure is given by begin. . . end pairs. Thus, a procedure begins and ends with these
keywords.
• An Ada main procedure does not need to be called “main”, as in some other languages. Any
simple name is fine so we have used Hello World.
• The name-space is not flat. Therefore, to access the functions declared in the Ada.Text IO package,
we need to prefix them with the package name.
Package renaming
To avoid having to prefix functions with this long name each time, we can use renaming:
Listing 4.2: hello world renaming.adb
1 with Ada.Text_IO;
2
3 procedure Hello_World_Renaming is
4 package IO renames Ada.Text_IO;
5 begin
6 IO.Put_Line("Hello, world!");
7 IO.New_Line;
8 IO.Put_Line("...using package renaming!");
9 end Hello_World_Renaming;
Compiling Hello World
Ada programs are divided into compilation units. An Ada specification (or interface) is usually given
the extension .ads (ADa Specification) and the body of the program is given the extension .adb (ADa
Body).
The file name must be the same as the compilation unit name and each compilation unit must hold the
name of the procedure to be compiled1. For example, the hello world program must be placed in the file
hello world.adb.
All compilation units must be compiled, bound and linked.
The notes for this subject will use the GNAT Ada compiler, which is downloadable from http://
libre.adacore.com/download/ for Linux and Windows (native, JVM, and .NET)... and Mind-
storm! The notes and workshops assume GNAT on Linux. Students are free to use the Windows com-
piler, but note that the staff cannot give much useful support on using these tools on Windows.
The GNAT Ada compiler can be used on the command line. To compile the hello world program, use
the following commands:
1. gcc -c hello world.adb generates the files hello world.ali – the object code – and
hello world.o – static analysis information.
2. gnatbind hello world uses hello world.ali to bind the files.
3. gnatlink hello world uses hello world.ali to link the files.
1Although some Ada compilers are forgiving and just produce a warning in this case
80
Alternatively, and more conveniently, use the following:
1. gnatmake hello world.adb is a single command that performs all three of the above steps.
The result is an executable program called hello world.
You can use the command gnatclean -c hello world to delete any compiler-generated files.
4.4 Specifications and program bodies
Ada programs consist of packages, procedures, and functions. Packages are synonymous with modules,
functions are defined as in other languages, and procedures are functions with no return value. More on
procedures and functions later.
Specifications show the types, functions, procedures and tasks that are publicly available for an Ada
program.
As an example, we can make a simple Greetings package with the interface below, defined in the file
greetings.ads:
Listing 4.3: greetings.ads
1 package Greetings is
2 procedure Hello;
3 procedure Goodbye;
4 function Talk(Text : in String) return Integer;
5 end Greetings;
This declares two procedures, Hello and Goodbye, but does not define their implementation. The
package body defines the implementation, possibly including hidden data types (we’ll see an example of
this later), defined in the file greetings.adb:
Listing 4.4: greetings.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 package body Greetings is
4 procedure Hello is
5 begin
6 Put_Line("Hello, world!");
7 end Hello;
8
9 procedure Goodbye is
10 begin
11 Put_Line("Goodbye, world!");
12 end Goodbye;
13
14 function Talk(Text : in String) return Integer is
15 begin
16 Put_Line(Text);
17 return 1;
18 end Talk;
19 end Greetings;
81
The names, parameter types, and return types must match between the specification and the body.
Because the two procedures are contained in a package, neither of them are directly executable. Instead,
they must be executed from another procedure. Below is a code listing that calls the two procedures in
succession:
Listing 4.5: greetings main.adb
1 with Greetings; use Greetings;
2
3 procedure Greetings_Main is
4 I : Integer;
5 begin
6 Hello;
7 I := Talk(Text => "What’s happening?");
8 GoodBye;
9 end Greetings_Main;
Note in this example a new keyword: use. The statement use Ada.Text IO makes all of the public
entities accessible without qualification in the body of the package, meaning that we no longer need to
prefix Put Line with the package name. However, use this with caution: two packages can have the
public entities with the same name, so importing both results in an ambiguity.
Exercise 4.1
Create two packages that contain a procedure of the same name. Import both of these using the with
and use keywords, into a new program that simply calls the ambiguously declared procedure. Try to
compile and run this program to see what happens. 2
Following the one-unit-per-file rule the program must be placed in three separate files:
1. greetings.ads: contains the specification of the package.
2. greetings.adb: contains the implementation of the package.
3. greetings main.adb: contains the main program that makes use of the Greetings pack-
age.
To compile this program, we can compile the package and the program, and then bind them, as follows:
1. gcc -c gmain.adb
2. gcc -c greetings.adb
3. gnatbind greetings main
4. gnatlink greetings main
Alternatively, issue the single command:
1. gnatmake greetings main
The gnatmake command will locate and compile any packages used in the main program, provided
that it can find them on the path. To avoid having to set up paths, we’ll only deal with programs in a
single directory.
82
4.5 Comments
Comments can be added to programs using the prefix --:
Listing 4.6: Comments
1 -- this part ignored
2 Put_Line("Hello, world!"); -- this part too!
4.6 Types
The Ada type system is a major part of its success. It is designed for programmers to create abstractions
that model real world objects. As such, the type system is not based on a collection of pre-defined types,
but instead, allows designers to create new types.
Of particular note in the type system are the following properties:
• Strong typing: types are incompatible with one another.
• Static typing: type checked while compiling.
• Abstraction: types represent the real world data or the problem at hand and computer representa-
tions of the data.
• Name equivalence: not structural equivalence as used in most other languages. Two types are
compatible if and only if they have the same name; not if they just happen to have the same size or
bit representation.
4.6.1 The predefined Ada type system
Despite this, there are still a handful of predefined types in Ada. Predefined types are used as interfaces
between different packages or libraries. The predefined library uses these types too.
The following predefined types are declared in the standard package:
• Integer: The standard integer type but must cover at least integers in the range−215+1 . . . 215−1.
• Float: The type of floating point numbers with mantissa and exponent but the implementation
does not follow the IEEE standard.
• Duration: A fixed point type used for timing and representing a period of time in seconds.
• Character: A special form of enumeration type. There are three predefined kinds of character
types: 8-bit characters, 16-bit characters, and 32-bit characters.
• String: Three indefinite array types, of 8-bit characters, 16-bit characters, and 32-bit characters
respectively.
• Boolean: A Boolean in Ada is an enumeration of False and True with special semantics.
83
The packages System and System.Storage Elements predefine some types that are primarily useful for
low-level programming and interfacing to hardware:
• System.Address: An address in memory.
• System.Storage Elements.Storage Offset: An offset, which can be added to an address, to ob-
tain a new address. You can also subtract one address from another to get the offset between them.
Together, Address and Storage Offset and their operators provide address arithmetic.
• System.Storage Elements.Storage Count: A subtype of Storage Offset which cannot be neg-
ative, and represents the memory size of a data structure (similar to C’s size t).
• System.Storage Elements.Storage Element: In most computers, this is a byte. Formally, it is
the smallest unit of memory that has an address.
• System.Storage Elements.Storage Array: An array of Storage Elements without any mean-
ing, useful when doing raw memory access.
Figure 4.1 shows the Ada type hierarchy. A type inherits properties from types above it in the hierarchy.
Ada Types
Elementary Types Composite Types
Access Types Scalar Types Array Types Record Types Protected Types Task Types
Discrete Types Real Types
Signed Types Modular Types
Enumeration Types Integer Types
Decimal Types Ordinary Types
Float Types Fixed Types
Figure 4.1: The Ada Type hierarchy.
Converting to/from strings
All scalar types (refer to Figure 4.1) have two special attributes called ’Value and ’Image, which are
used for converting to strings to values, and back. Both are functions that are called on the type, not the
individual values (the Java metaphor is a static method, as opposed to an instance method).
The following code listing demonstrates how to convert a string to an integer, and back again:
Listing 4.7: value image attributes.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
84
3 procedure Value_Image_Attributes is
4 An_Integer : Integer;
5 A_String : String := "5";
6 begin
7 An_Integer := Integer’Value(A_String);
8 Put_Line("Hello, world!" & Integer’Image(An_Integer));
9 end Value_Image_Attributes;
Exercise 4.2
Write a program that converts a string containing a non-number to an integer. Compile and run this
program to see what happens. 2
4.6.2 Arrays
Arrays in Ada are somewhat different to many other high-level programming languages – and in fact, are
more powerful as well. One key difference is in the indices of the array. In many languages, the indices
of arrays are integers, starting at either 0 or 1. However, in Ada, array indices can be of any discrete type,
such as characters, enumerations, etc.
As an example, consider the following program, which declares to arrays of type Character, but each
with a different type of index: in one case, an range of integers; and in the other, a range of characters.
Elements in the array are accessed using integers for the array Int Index, but characters for the array
Ch Index.
Listing 4.8: array examples.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
3
4 procedure Array_Examples is
5 Int_Index : array(Integer range 5..10) of Character :=
6 (’a’, ’b’, ’c’, ’d’, ’e’, ’f’);
7 Ch_Index : array(Character) of Character;
8 begin
9 Put(Int_Index(5)); -- accessing using an integer index
10 New_Line;
11
12 Ch_Index(’a’) := ’z’; -- setting using a char index
13 Put(Ch_Index(’a’)); -- accessing using a char index
14 New_Line;
15
16 -- setting and getting an array ’slice’
17 Int_Index(6 .. 8) := (’X’, ’Y’, ’Z’);
18
19 -- array attributes "’First" and "’Last"
20 Put(Ch_Index’First);
21 New_Line;
22 Put(Int_Index’Last);
23 New_Line;
24 end Array_Examples;
Arrays have four special attributes: ’First, ’Last, ’Length, and ’Range. The first two of these
do not return the first and last elements of the array, but the first and last elements of the type of the array.
85
Thus, in the above example, Int Index’Last will return 10, and Ch Index’First will return the
final ASCII character (null).
The ’Length attribute returns the length of the array type (and of the array – the entire array is allocated
in Ada), while the ’Range attribute returns the range of indices; e.g. Int Index’Range will return
5..10.
Array ’First ’Last ’Length ’Range
Int Index 5 10 6 5..10
Ch Index 〈null〉 y¨ 255 〈null〉 .. y¨
The pre-defined type String is a character array. However, the indices need not start at 0 or 1. Consider
the following example, taken from the Ada Wikibook2:
Listing 4.9: Attributes of strings
1 Hello_World : constant String := "Hello World!";
2 World : constant String := Hello_World (7 .. 11);
3 Empty_String : constant String := "";
In this example, the attribute values are:
Array ’First ’Last ’Length ’Range
Hello World 1 12 12 1 .. 12
World 7 11 5 7 .. 11
Empty 1 0 0 1..0
Thus, strings can be indexed from any range of integers, and it is not necessarily that case that ’Length
= ’Last.
4.6.3 Defining new types
A new type is introduced with the syntax:
type T is . . .
Consider the following example of defining a type for dates.
Listing 4.10: A data type for dates
1 type Day_type is range 1 .. 31;
2 type Month_type is range 1 .. 12;
3 type Year_type is range 1800 .. 2100;
4 type Hours is mod 24;
5 type DayOfWeek is (Monday, Tuesday, Wednesday, Thursday,
6 Friday, Saturday, Sunday);
7
8 type Date is
9 record
10 Day : Day_type;
11 Month : Month_type;
12 Year : Year_type;
2See http://en.wikibooks.org/wiki/Ada_Programming/Types/array
86
13 end record;
A record is a composite type that groups one of more data type together into a single data type, similar
to a struct in C.
All types are incompatible with each other. Therefore, the following code snippet it illegal, because
Day type and Month type are different and incompatible types, even though the value 8 can be
assigned to either:
Listing 4.11:
1 A : Day_type := 8;
2 B : Month_type := A; -- illegal!
4.7 Control Structures
As a structured language, the control flow of the program is structured into statements. The following
control structures are the most pertinent in the Ada language.
Assignment Assignment in Ada is written as
X := E
where X is a variable and E is an expression.
Assignment is read as “X becomes equal to E”.
Conditionals if-then-else and case:
Listing 4.12: A conditional
1 if x < y then
2 Put_Line ("True");
3 else
4 Put_Line ("False");
5 end if;
Listing 4.13: A case statement
1 case x is
2 when 1 => Put_Line ("First");
3 when 2 => Put_Line ("Second");
4 when 3 => Put_Line ("Third");
5 when others => Put_Line ("None!");
6 end case;
Unconditionals return and goto:
Listing 4.14: Returning a value
1 function Use_Return return Integer is
2 X : Integer := 5;
3 Y : Integer := 10;
4 begin
5 return X + Y;
6 end Use_Return;
87
Listing 4.15: Using a goto
1 function Use_Goto return Integer is
2 X : Integer := 5;
3 Y : Integer := 10;
4 Z : Integer;
5 begin
6 Z := X + Y;
7
8 goto Label;
9
10 Z := 0;
11
12 <
欢迎咨询51作业君
51作业君

Email:51zuoyejun

@gmail.com

添加客服微信: abby12468