Chapter 15: Program failure
The introduction frames program failure around its causes, sketches three ways failures arise, and limits the chapter’s scope to failures that manifest during execution.
After this chapter you will be able to
- Causes versus failure behavior. Explain why examining whether and why a program fails is different from focusing on its behavior after an error.
- Three origins of failure. Distinguish a wrongdoing that directly causes failure, gradual deterioration of execution state, and a failure caused by combining actions that are valid in isolation.
- Direct causes of failure. Explain that a wrongdoing is an action, event, or omission that directly leads to a program failure, and distinguish a plausible cause from something merely associated with the failure.
- Invalid integer arithmetic. Explain why division or modulo by zero, negating INT_MIN, and certain bit shifts are arithmetic violations that a program should avoid.
- Platform-dependent floating-point exceptions. Distinguish floating-point division by zero on a platform that supports infinity from an operation that causes program failure, and describe how the floating-point environment and feature tests matter.
- Array bounds for pointer arithmetic. Explain why adding an integer to or subtracting one from a pointer must stay within an array object’s bounds, and why the section recommends array indexing for element access.
- Numeric conversion limits. Explain why conversion to an unsigned type is always defined, while conversions to signed integer, integer–floating-point, or other floating-point types can be problematic.
- Conversion versus subsequent use. Distinguish a failure at the conversion operation from a later problem using a successfully converted value.
- Library call value violations. Distinguish arguments that a C library call does not allow from values that make the requested result unrepresentable. Give an example of an invalid argument from the section.
- Wrong-type object access. Explain why accessing an object through the wrong type is generally erroneous in C and how avoiding unnecessary conversions between pointer types reduces that risk.
- Matching function prototypes. Explain why a function must be called with the prototype with which it was defined, including how a cast function pointer or inconsistent prototypes across translation units can lead to a failing call.
- Access violations and safeguards. Explain why access violations commonly arise through pointers or arrays, give examples from the section, and describe the programmer’s responsibility for preventing them.
- Uninitialized storage misinterpretation. Explain what a value misinterpretation is and why accessing uninitialized storage is its most important case in this section.
- Non-value bit representations. Explain why directly changing an object’s representation bytes can create a value misinterpretation, using bool as the section’s example.
- Proof before unreachable. Explain that unreachable() asserts a control path will never be reached in any execution, so it should be used only when that claim can be proved. Describe why the section favors array parameters with bounds over relying on external information to rule out null pointers.
- No substitute for unreachable. Distinguish unreachable() from an operation that causes undefined behavior or explicitly ends execution. Explain why such an operation does not tell the compiler that a control path will never be taken.
- Collective program state degradation. Explain why program state degradation is harder to tackle than a wrongdoing: it results from several actions interacting rather than one identifiable action.
- Unbounded recursion and resource exhaustion. Explain why recursive calls must make progress toward a well-defined stopping condition and what can happen if they do not.
- Stack exhaustion through recursion. Explain how unbounded recursion can exhaust storage for function call contexts.
- Detectable heap allocation failure. Distinguish detectable heap exhaustion from stack exhaustion and describe what an allocation failure signals.
- Scarce resource accumulation. Explain that a program can exhaust runtime resources other than storage when it reserves them without calling the corresponding release functions. Describe the two resources reserved by fopen.
- Distant Event Failures. Explain how an unfortunate incident differs from a wrongdoing or resource exhaustion, and use the traffic collision analogy to illustrate why it can be hard to track.
- Escalating state degradation. Explain why continuing execution after stack or heap exhaustion can make a failure more serious.
- Unsequenced expression accesses. Explain why reading and modifying the same object within one arithmetic expression is unsafe, and why the compiler may not detect the problem.
- Signal and thread races. Distinguish the pointer collision that could be guarded by testing p == q from race conditions involving a signal handler or concurrent threads accessing the same object.
- Context restrictions on library use. Explain that C library functions and macros must be used only in contexts permitted by the C standard. Give the section’s examples: calling signal in a multithreaded program is not allowed, and setjmp may appear only in specific places within expressions.
- Cyclic resource dependency. Explain how a deadlock prevents progress when entities trying to access resources become trapped in a cycle of dependencies, using the congested roundabout as an example.
- Progressless execution as failure. Explain why an execution that repeatedly visits a finite set of states without observable side effects counts as a failure, even though it keeps running.
- Livelock without forward progress. Describe a livelock as a threaded-program failure in which components remain active but make no effective progress, using the cyclic traffic detours to explain why it can be hard to detect.
- Preconditions for fallible operations. Explain that wrongdoings should be avoided during normal execution by testing an operation’s preconditions and taking an alternative path when they are not met.
- Resource exhaustion error indicators. Describe why resource-related state degradation is detected after the fact and name the two general error indicators given for operations that might exhaust resources.
- Limits of incident handling. Explain why unfortunate incidents are difficult to handle, how interruption or termination may limit damage, and why Byzantine failure calls for prevention through careful algorithm design.
- Error detection and reporting. Explain why a reliable function checks both its entry preconditions and errors that arise during execution, and describe how fprintnumbers reports distinct conditions with negative return values.
- Cleanup after allocation. Trace how the examples ensure allocated memory is freed even when output, buffer enlargement, or length checking fails.
What it builds on
- Signed integers (chapter 5) · Signed integer values have a limited representable range.
- Shift operators (chapter 5) · Bit shifts operate on integer values.
- Pointer addition (chapter 11) · Adding an integer to a pointer.
- Array operations (chapter 6) · Accessing array elements.
- Pointer subtraction and difference (chapter 11) · Pointer subtraction and pointer differences.
- Implicit conversions (chapter 5) · The idea of converting a value to another type.
- Explicit conversions (chapter 12) · Conversions between pointer types and other types.
- Effective types (chapter 12) · Rules for accessing objects through a different type
Section by section
Introduction
The introduction frames program failure around its causes, sketches three ways failures arise, and limits the chapter’s scope to failures that manifest during execution.
- Causes versus failure behavior: Explain why examining whether and why a program fails is different from focusing on its behavior after an error.
Why it matters: Looking at causes helps you reason about failures rather than only their consequences. - Three origins of failure: Distinguish a wrongdoing that directly causes failure, gradual deterioration of execution state, and a failure caused by combining actions that are valid in isolation.
Why it matters: These distinctions keep you from assuming every failure has one directly faulty action. - Execution failure scope: Distinguish the failures addressed here from language-constraint errors generally diagnosed during compilation and from excluded compilation, linkage, or startup failures.
Why it matters: This boundary clarifies why successful compilation does not rule out the failures under discussion.
Key terms: undefined behavior, program failure, wrongdoings, program state degradation, unfortunate incidents, program execution, compile time, compilation, linkage, startup failures
15.1 Wrongdoings
Introduces wrongdoings as actions, events, or omissions that directly cause program failure, using a car accident to distinguish plausible causes from things merely present.
- Direct causes of failure: Explain that a wrongdoing is an action, event, or omission that directly leads to a program failure, and distinguish a plausible cause from something merely associated with the failure.
Why it matters: It helps you focus on what directly caused a failure instead of blaming something merely connected to it.
Key terms: wrongdoings, program failures
15.1.1 Arithmetic violations
This section explains when arithmetic on numbers and pointers is invalid, and why floating-point behavior depends on the platform.
- Invalid integer arithmetic: Explain why division or modulo by zero, negating INT_MIN, and certain bit shifts are arithmetic violations that a program should avoid.
Why it matters: Avoiding these operations prevents failures whose behavior may differ across platforms. - Platform-dependent floating-point exceptions: Distinguish floating-point division by zero on a platform that supports infinity from an operation that causes program failure, and describe how the floating-point environment and feature tests matter.
Why it matters: A numerical program cannot assume the same floating-point failure behavior on every platform. - Array bounds for pointer arithmetic: Explain why adding an integer to or subtracting one from a pointer must stay within an array object’s bounds, and why the section recommends array indexing for element access.
Why it matters: A plausible-looking address does not make out-of-bounds pointer arithmetic valid. - Pointer subtraction and comparison limits: Describe the same-array requirement for subtracting or relationally comparing pointers, and the additional ptrdiff_t range limit on subtraction.
Why it matters: Two pointers cannot be safely ordered or subtracted solely because their addresses can be represented.
Key terms: exceptional condition, division by zero, modulo by zero, INT_MIN, bit-shift, floating-point environment, INFINITY, <fenv.h>, FE_DIVBYZERO, fetestexcept
15.1.2 Invalid conversions
This section identifies conversions that can fail or produce nonportable results and distinguishes a valid conversion from permission to use its result.
- Numeric conversion limits: Explain why conversion to an unsigned type is always defined, while conversions to signed integer, integer–floating-point, or other floating-point types can be problematic.
Why it matters: A conversion that compiles may still give a nonportable result or encounter a range or precision problem. - Pointer conversion constraints: Describe the two pointer-conversion problems identified here and the stated exception for uintptr_t.
Why it matters: Changing a pointer's type is not automatically a valid conversion. - Conversion versus subsequent use: Distinguish a failure at the conversion operation from a later problem using a successfully converted value.
Why it matters: Checking whether a conversion succeeds is separate from checking whether its result may be used as intended.
Key terms: unsigned type, modulus, implementation defined, precision, pointer, uintptr_t, aligned, conversion, pointer-to-pointer conversion, accessed
15.1.3 Value violations
This subsection explains how wrong values can cause C library calls to fail and why detecting such violations usually requires checking the functions’ specifications.
- Library call value violations: Distinguish arguments that a C library call does not allow from values that make the requested result unrepresentable. Give an example of an invalid argument from the section.
Why it matters: These are two distinct ways that wrong values can make a library call invalid. - Detecting library value violations: Explain why compiler or static-analyzer detection is not enough to identify value violations in C library calls.
Why it matters: A tool warning can help, but the function’s specification is needed to determine which values are permitted.
Key terms: invalid values, call arguments, not representable, static analyzer tools, specifications
15.1.4 Type violations
This section explains how accessing objects or calling functions through the wrong type can cause failure, and why avoiding unnecessary pointer conversions and mismatched function prototypes helps prevent it.
- Wrong-type object access: Explain why accessing an object through the wrong type is generally erroneous in C and how avoiding unnecessary conversions between pointer types reduces that risk.
Why it matters: Avoiding unnecessary pointer conversions removes a common route to wrong-type access. - Matching function prototypes: Explain why a function must be called with the prototype with which it was defined, including how a cast function pointer or inconsistent prototypes across translation units can lead to a failing call.
Why it matters: A call is not made valid by giving the function a different apparent type.
Key terms: type violations, pointers, different type, prototype, function pointer, translation units
15.1.5 Access violations
This section identifies common access violations in C and explains why code using pointers and arrays must guard against them.
- Access violations and safeguards: Explain why access violations commonly arise through pointers or arrays, give examples from the section, and describe the programmer’s responsibility for preventing them.
Why it matters: An access that looks easy to perform can have catastrophic consequences if its required conditions do not hold.
Key terms: access violations, pointers, arrays, null pointer dereference
15.1.6 Value misinterpretation
This section explains how accessing an object whose bits have no valid interpretation for its type can cause a value misinterpretation, and why representation bytes should not be changed directly.
- Uninitialized storage misinterpretation: Explain what a value misinterpretation is and why accessing uninitialized storage is its most important case in this section.
Why it matters: An object can exist in storage without yet holding a value that is valid to read through its type. - Non-value bit representations: Explain why directly changing an object’s representation bytes can create a value misinterpretation, using bool as the section’s example.
Why it matters: Changing raw bytes can produce a pattern that the object’s type does not define as a valid value.
Key terms: value misinterpretation, indeterminate representation, uninitialized, malloc, calloc, non-value representation, bool, representation bytes
15.1.7 Explicit invalidation
This section explains what C23’s unreachable() asserts, when that assertion is justified, and why other failures cannot stand in for it.
- Proof before unreachable: Explain that unreachable() asserts a control path will never be reached in any execution, so it should be used only when that claim can be proved. Describe why the section favors array parameters with bounds over relying on external information to rule out null pointers.
Why it matters: A path that merely ought not to occur is not necessarily one you can safely declare unreachable. - No substitute for unreachable: Distinguish unreachable() from an operation that causes undefined behavior or explicitly ends execution. Explain why such an operation does not tell the compiler that a control path will never be taken.
Why it matters: Crashing or exiting on a path does not make the same claim as declaring that the path cannot occur. - Source of the wrongdoing: Explain why, if execution reaches unreachable(), the wrongdoing is the earlier decision that led execution there, not the invocation considered in isolation.
Why it matters: When investigating this failure, look for why execution entered the supposedly impossible path.
Key terms: unreachable(), array parameters with bounds, null pointers, undefined state, exit, abort, unreachable, wrongdoing
15.2 Program state degradation
The section explains why program state degradation is harder to address than an individual wrongdoing.
- Collective program state degradation: Explain why program state degradation is harder to tackle than a wrongdoing: it results from several actions interacting rather than one identifiable action.
Why it matters: Looking for a single culprit can miss how the actions together produce the problem.
Key terms: program state degradation, wrongdoings, interplay of several actions
15.2.1 Unbounded recursion
This subsection explains how recursion without reliable progress or a well-defined stopping condition can exhaust function-call resources and crash a program.
- Unbounded recursion and resource exhaustion: Explain why recursive calls must make progress toward a well-defined stopping condition and what can happen if they do not.
Why it matters: A stopping condition alone is not enough if recursive calls do not move toward it.
Key terms: program state degradation, recursion, progress, bottom of the recursion, resources for function calls
15.2.2 Storage exhaustion
This section explains how stack and heap storage can be exhausted, and why heap allocation failure is easier to detect.
- Stack exhaustion through recursion: Explain how unbounded recursion can exhaust storage for function call contexts.
Why it matters: It connects runaway recursion to the finite storage needed for calls. - Variable-length array stack usage: Describe why variable-length arrays can cause stack overflow and why their effect on stack usage is not always adverse.
Why it matters: Runtime-sized arrays make stack demand less predictable, but can sometimes avoid reserving space for an unnecessarily large array. - Detectable heap allocation failure: Distinguish detectable heap exhaustion from stack exhaustion and describe what an allocation failure signals.
Why it matters: A reported allocation failure gives a program a chance to respond before its state silently degrades.
Key terms: function call contexts, stack, stack overflow, unbounded recursion, variable-length arrays, VLAs, variably modified (VM) types, heap, null pointer, storage exhaustion
15.2.3 Other scarce resources
The section explains that runtime resources beyond storage can be exhausted, how their use accumulates, and whether exhaustion can be detected.
- Scarce resource accumulation: Explain that a program can exhaust runtime resources other than storage when it reserves them without calling the corresponding release functions. Describe the two resources reserved by fopen.
Why it matters: Releasing a resource matters because continued reservations can eventually exhaust it. - Resource exhaustion reporting: Distinguish the usual ability to detect resource exhaustion from the exceptions named in the section.
Why it matters: A program cannot rely on every resource-reserving operation to report exhaustion.
Key terms: scarce resources, release, fopen, stream, file, failure mode, va_start, va_copy
15.3 Unfortunate incidents
This section explains unfortunate incidents as rare failures caused by distant events aligning, using a traffic collision to show why they are hard to anticipate.
- Distant Event Failures: Explain how an unfortunate incident differs from a wrongdoing or resource exhaustion, and use the traffic collision analogy to illustrate why it can be hard to track.
Why it matters: This distinction helps you recognize failures whose cause may lie far from where the failure becomes visible.
Key terms: unfortunate incidents, wrongdoings, resource exhaustion, collisions
15.3.1 Escalating state degradation
Continuing execution after resource exhaustion can turn degraded program state into an incident with wider consequences.
- Escalating state degradation: Explain why continuing execution after stack or heap exhaustion can make a failure more serious.
Why it matters: A resource failure may be dangerous to ignore even when the program appears able to continue.
Key terms: state degradation, stack or heap exhaustion, unfortunate incidents
15.3.2 Collisions and race conditions
This section explains how unsequenced access to the same object can cause failures, including less obvious cases involving pointers, signal handlers, and threads.
- Unsequenced expression accesses: Explain why reading and modifying the same object within one arithmetic expression is unsafe, and why the compiler may not detect the problem.
Why it matters: Avoiding conflicting accesses in one expression prevents results that cannot be reliably predicted. - Pointer aliasing collisions: Describe how two pointer expressions can unexpectedly access the same object and why code that sets the pointers far apart can cause a failure at their later use.
Why it matters: Pointer aliases can hide a conflicting access until a seemingly unrelated expression is evaluated. - Signal and thread races: Distinguish the pointer collision that could be guarded by testing p == q from race conditions involving a signal handler or concurrent threads accessing the same object.
Why it matters: Concurrent access needs safeguards beyond a check for whether two pointers are equal.
Key terms: sequencing, unsequenced access, side effects, p == q, *p, *q, unsequenced accesses, race conditions, signal handler, threads of execution
15.3.3 Inappropriate library calls and macro invocations
This section shows how calling a C library function or invoking a macro outside its permitted context can jeopardize execution or portability.
- Context restrictions on library use: Explain that C library functions and macros must be used only in contexts permitted by the C standard. Give the section’s examples: calling signal in a multithreaded program is not allowed, and setjmp may appear only in specific places within expressions.
Why it matters: A call or macro invocation can cause trouble even when its name and arguments look right, because the surrounding context matters.
Key terms: signal, multithreaded program, setjmp macro, portability, C standard
15.3.4 Deadlocks
This section explains deadlock as a cyclic resource dependency, illustrates it with a congested roundabout, and locates deadlocks in multithreaded C programs.
- Cyclic resource dependency: Explain how a deadlock prevents progress when entities trying to access resources become trapped in a cycle of dependencies, using the congested roundabout as an example.
Why it matters: Avoiding individual access conflicts does not necessarily ensure that the whole system can make progress. - Deadlocks in C programs: State where deadlocks can occur in C programming and describe the section’s qualification about proving their absence.
Why it matters: It identifies the programming context in which this failure matters without suggesting that freedom from deadlock is easy to establish.
Key terms: deadlock failure, cyclic chain of dependencies, resource conflicts, multithreaded context, deadlocks
15.4 Series of unfortunate events
This section explains how executions can fail by continuing without observable progress, using loops, recursion, and threaded livelocks as examples.
- Progressless execution as failure: Explain why an execution that repeatedly visits a finite set of states without observable side effects counts as a failure, even though it keeps running.
Why it matters: It helps you recognize a failure that does not look like a crash or a stopped program. - Limits of loop diagnosis: Explain why the example loop with obscure cannot automatically be judged a systematic failure, and distinguish what changes when its counter is signed rather than unsigned.
Why it matters: A loop’s shape alone does not tell you whether its execution makes progress. - Livelock without forward progress: Describe a livelock as a threaded-program failure in which components remain active but make no effective progress, using the cyclic traffic detours to explain why it can be hard to detect.
Why it matters: Visible activity can conceal a system that never gets anywhere.
Key terms: finite set of states, observable side effects, progress, obscure, visible side effect, [[unsequenced]], INT_MAX, livelock, deadlock, detours
15.5 Dealing with failures
The section distinguishes how to avoid wrongdoings, detect resource-related degradation, and limit or prevent harder-to-detect unfortunate incidents.
- Preconditions for fallible operations: Explain that wrongdoings should be avoided during normal execution by testing an operation’s preconditions and taking an alternative path when they are not met.
Why it matters: Checking before an operation can keep an avoidable wrongdoing out of normal execution. - Resource exhaustion error indicators: Describe why resource-related state degradation is detected after the fact and name the two general error indicators given for operations that might exhaust resources.
Why it matters: Resource exhaustion may only become apparent when an operation reports what happened. - Limits of incident handling: Explain why unfortunate incidents are difficult to handle, how interruption or termination may limit damage, and why Byzantine failure calls for prevention through careful algorithm design.
Why it matters: When failure cannot reliably be detected or caught, prevention matters more than recovery.
Key terms: preconditions, wrongdoings, program state degeneration, error returns, errno, unfortunate incidents, signals, trap, termination functions, Byzantine failure
15.6 Error checking and cleanup
The section shows how to detect and report errors while preserving state and freeing allocated memory, including when cleanup requires a goto.
- Error detection and reporting: Explain why a reliable function checks both its entry preconditions and errors that arise during execution, and describe how fprintnumbers reports distinct conditions with negative return values.
Why it matters: Checking and distinguishing failures lets a caller respond rather than treating a failed operation as a success. - Preserving errno on failure: Explain why the example saves errno on entry and restores it during error cleanup.
Why it matters: An error path should not inadvertently leave errno changed by work the function performed. - Cleanup after allocation: Trace how the examples ensure allocated memory is freed even when output, buffer enlargement, or length checking fails.
Why it matters: A failed operation should not turn an allocated buffer into a memory leak. - Disciplined cleanup jumps: Explain why the optimized example uses goto for exceptional cleanup and state the limits on where such a jump should go.
Why it matters: A clearly marked exceptional jump can make cleanup reliable while keeping the normal path readable.
Key terms: preconditions, error conditions, negative constant values, error macros, errno, global state, error_cleanup, malloc, realloc, free
15.7 Summary
Where this is used later
- (chapter intro) (chapter 16) · The risk of invalid memory access.
- Signal handlers (chapter 19) · Signal handlers provide one setting in which the access problem arises.
- Critical data and critical sections (chapter 20) · Critical data and critical sections address concurrent access by threads.
- Long jumps (chapter 19) · Provides the introductory restriction on setjmp before its more detailed treatment.
- Ensure liveness (chapter 20) · Provides the meaning of deadlock for the later case analysis.
- More sophisticated thread management (chapter 20) · Examines thread management, including the thread termination mentioned here.
- Short jumps (chapter 19) · Earlier use of goto for error checking and cleanup.
Concepts explained in this chapter
- program failureProgram failure is execution that can no longer be relied upon because the program has entered a state resulting from undefined behavior.
- exceptionAn exception is an abnormal execution condition that can interrupt ordinary computation and potentially terminate a program.
- failureAn error condition is a detected circumstance indicating that an operation cannot proceed normally or has not produced a valid result.
- errnoerrno is a C library-provided modifiable object or macro designating an integer error indicator associated with the current thread or execution context.
- gotogoto is a C jump statement that transfers execution to a labeled statement within the same function.
- freefree is a C library function that releases dynamically allocated storage previously obtained from a compatible allocation function.
- reallocrealloc is a C library function that changes the size of an allocated storage region while preserving its existing contents up to the applicable size.
- resource cleanupCleanup is the release or restoration of resources and state needed when an operation completes or fails.
- pointer arithmeticPointer arithmetic is the C operation of forming pointer values by adding or subtracting integer offsets relative to elements of an array object.
- feature testA feature test is a compile-time or configuration-time check that determines whether an implementation supports a specified language or library feature.
- fenv.hfenv.h is the C standard header that provides access to the floating-point environment and its status and control facilities.
- floating-point environmentThe floating-point environment is the implementation state that controls and records floating-point exception conditions and related behavior.
- indexingArray indexing is the C operation of selecting an array element with an index expression, such as A[i].
- pointer comparisonPointer comparison is an operation that evaluates an ordering or equality relationship between pointer values.
- program state degradationProgram state degradation is a worsening of the execution state that progressively reduces the program's ability to continue correctly.
- recursionRecursion is a technique in which a computation is defined or performed by calls to the same computation on smaller or simpler inputs.
- livelockA livelock is a concurrency failure in which threads continue executing but repeatedly make no effective progress in response to one another.
- loopA for statement is a C iteration statement that controls repeated execution using initialization, a continuation condition, and an iteration expression.
- undefined behaviorUndefined behavior is a C execution condition for which the standard imposes no requirements on the program's subsequent behavior.
- signalAn asynchronous signal is an externally generated notification delivered to a running program independently of its current instruction sequence.
- preconditionA platform precondition is a required property or capability of the execution environment that must hold for code to be valid or executable.
- race conditionA race condition is an execution condition in which the result depends on the relative timing or interleaving of accesses by concurrent or asynchronously executing code.
- unsequenced operationUnsequenced evaluation is an evaluation relationship in which the relative order of two evaluations is not specified and they may be interleaved.
- alignmentAlignment is the implementation-defined requirement that an object or structure member begin at an address suitable for its type.
- implementation definedImplementation-defined behavior is behavior for which the C implementation chooses among permitted alternatives and documents its choice.
- stackA stack is a last-in, first-out storage structure in which function-call contexts are added and removed as calls and returns occur.
- variable-length arrayA variable-length array is a C array whose bound is determined at run time rather than being an integer constant expression.
- booleanbool is a C type whose values represent logical truth and falsity.
- non-value representationA non-value representation is a bit pattern that does not represent any valid value of its object type.
- deadlockA deadlock is a condition in which multiple execution entities are permanently blocked because each depends on a resource or action held up by another in a cycle.
- setjmp/longjmpA long jump is a nonlocal control transfer that can move execution across function-call boundaries.
These guides are generated from the text of Manning books by Manning's knowledge graph and link to the sections they draw on. The full books are available on liveBook and at manning.com.
