39,59 €
C++ is a general-purpose programming language with a bias toward system programming as it provides ready access to hardware-level resources, efficient compilation, and a versatile approach to higher-level abstractions.
This book will help you understand the benefits of system programming with C++17. You will gain a firm understanding of various C, C++, and POSIX standards, as well as their respective system types for both C++ and POSIX. After a brief refresher on C++, Resource Acquisition Is Initialization (RAII), and the new C++ Guideline Support Library (GSL), you will learn to program Linux and Unix systems along with process management. As you progress through the chapters, you will become acquainted with C++'s support for IO. You will then study various memory management methods, including a chapter on allocators and how they benefit system programming. You will also explore how to program file input and output and learn about POSIX sockets. This book will help you get to grips with safely setting up a UDP and TCP server/client.
Finally, you will be guided through Unix time interfaces, multithreading, and error handling with C++ exceptions. By the end of this book, you will be comfortable with using C++ to program high-quality systems.
Das E-Book können Sie in Legimi-Apps oder einer beliebigen App lesen, die das folgende Format unterstützen:
Seitenzahl: 606
Veröffentlichungsjahr: 2018
Copyright © 2018 Packt Publishing
All rights reserved. No part of this book may be reproduced, stored in a retrieval system, or transmitted in any form or by any means, without the prior written permission of the publisher, except in the case of brief quotations embedded in critical articles or reviews.
Every effort has been made in the preparation of this book to ensure the accuracy of the information presented. However, the information contained in this book is sold without warranty, either express or implied. Neither the author, nor Packt Publishing or its dealers and distributors, will be held liable for any damages caused or alleged to have been caused directly or indirectly by this book.
Packt Publishing has endeavored to provide trademark information about all of the companies and products mentioned in this book by the appropriate use of capitals. However, Packt Publishing cannot guarantee the accuracy of this information.
Commissioning Editor:Richa TripathiAcquisition Editor: Shriram ShekharContent Development Editor: Tiksha SarangTechnical Editor: Riddesh DawneCopy Editor: Safis Editing Project Coordinator: Prajakta NaikProofreader: Safis EditingIndexer: Tejal Daruwale SoniGraphics: Jisha ChirayilProduction Coordinator: Arvindkumar Gupta
First published: December 2018
Production reference: 1211218
Published by Packt Publishing Ltd. Livery Place 35 Livery Street Birmingham B3 2PB, UK.
ISBN 978-1-78913-788-0
www.packtpub.com
Mapt is an online digital library that gives you full access to over 5,000 books and videos, as well as industry leading tools to help you plan your personal development and advance your career. For more information, please visit our website.
Spend less time learning and more time coding with practical ebooks and videos from over 4,000 industry professionals
Improve your learning with skill plans designed especially for you
Get a free eBook or video every month
Mapt is fully searchable
Copy and paste, print, and bookmark content
Did you know that Packt offers eBook versions of every book published, with PDF and ePub files available? You can upgrade to the eBook version at www.packt.com and, as a print book customer, you are entitled to a discount on the eBook copy. Get in touch with us at [email protected] for more details.
At www.packt.com, you can also read a collection of free technical articles, sign up for a range of free newsletters, and receive exclusive discounts and offers on Packt books and eBooks.
Dr. Rian Quinn is a Chief Technology Officer (CTO) in the Advanced Technologies Business Unit at Assured Information Security, Inc. focused on trusted computing, hypervisor related technologies, machine learning/artificial intelligence, and cyber security for more than 10 years and has 9 years of technical management and business development experience. He holds a Ph.D. in Computer Engineering, specializations in information assurance and computer architectures, from Binghamton University. He is the co-founder and lead developer of the Bareflank Hypervisor, and is an active member of several open source projects, including Microsoft's Guideline Support Library (GSL) and OpenXT.
Will Brennan is a C++/Python software engineer based in London with experience of working on high-performance image processing and machine learning applications.
If you're interested in becoming an author for Packt, please visit authors.packtpub.com and apply today. We have worked with thousands of developers and tech professionals, just like you, to help them share their insight with the global tech community. You can make a general application, apply for a specific hot topic that we are recruiting an author for, or submit your own idea.
Title Page
Copyright and Credits
Hands-On System Programming with C++
Dedication
About Packt
Why subscribe?
Packt.com
Contributors
About the author
About the reviewer
Packt is searching for authors like you
Preface
Who this book is for
What this book covers
To get the most out of this book
Download the example code files
Conventions used
Get in touch
Reviews
Getting Started with System Programming
Technical requirements
Understanding system calls
The anatomy of a system call
Learning about different types of system calls
Console input/output
Memory allocation
File input/output
Networking
Time
Threading and process creation
System call security risks
SYSRET
Meltdown and Spectre
Benefits of using C++ when system programming
Type safety in C++
Objects of C++
Templates used in C++
Functional programming associated with C++
Error handling mechanism in C++
APIs and C++ containers in C++
Summary
Questions
Further reading
Learning the C, C++17, and POSIX Standards
Technical requirements
Beginning with the C standard language
How the standard is organized
Environment
Language
Libraries
How a C program starts
All about linking
Static libraries
Dynamic libraries
Scope
Pointers and arrays
Libraries
Learning about the C++ standard
How the standard is organized
General conventions and concepts
Language syntax
Libraries
Linking C++ applications
Scope
Pointers versus references
Libraries
Beginning with the POSIX standard
Memory management
Filesystems
Sockets
Threading
Summary
Questions
Further reading
System Types for C and C++
Technical requirements
Exploring C and C++ default types
Character types
Integer types
Floating – point numbers
Boolean
Learning standard integer types
Structure packing
Summary
Questions
Further reading
C++, RAII, and the GSL Refresher
Technical requirements
A brief overview of C++17
Language changes
Initializers in if/switch statements
Additions to compile-time facilities
Namespaces
Structured bindings
Inline variables
Changes in the library
String View
std::any, std::variant, and std::optional
Resource Acquisition Is Initialization (RAII)
The Guideline Support Library (GSL)
Pointer ownership
Pointer arithmetic
Contracts
Utilities
Summary
Questions
Further Reading
Programming Linux/Unix Systems
Technical requirements
The Linux ABI
The System V ABI
The register layout
The stack frame
Function prologs and epilogs
The calling convention
Exception handling and debugging
Virtual memory layout
Executable and Linkable Format (ELF)
ELF sections
ELF segments
The Unix filesystem
Unix processes
The fork() function
The wait() function
Interprocess communication (IPC)
Unix pipes
Unix shared memory
The exec() function
Output redirection
Unix signals
Summary
Questions
Further reading
Learning to Program Console Input/Output
Technical requirements
Learning about stream-based IO
The basics of stream
Advantages and disadvantages of C++ stream-based IO
Advantages of C++ stream-based IO
Disadvantages of C++ stream-based IO
Beginning with user-defined types
Safety and implicit memory management
Common debugging patterns
Performance of C++ streams
Learning about manipulators
Recreating the echo program
Understanding the Serial Echo server example
Summary
Questions
Further reading
A Comprehensive Look at Memory Management
Technical requirements
Learning about the new and delete functions
The basics for writing a program
Aligning memory
nothrow
Placement of new
Overloading
Understanding smart pointers and ownership
The std::unique_ptr{} pointer
The std::shared_ptr pointer
Learning about mapping and permissions
The basics
Permissions
Smart pointers and mmap()
Shared memory
Learning importance of memory fragmentation
External fragmentation
Internal fragmentation
Internal over external fragmentation
External over internal fragmentation
Summary
Questions
Further reading
Learning to Program File Input/Output
Technical requirements
Opening a file
Different ways to open a file
Modes for opening a file
Reading and writing to a file
Reading from a file
Reading by field
Reading bytes
Reading by line
Writing to a file
Writing by field
Writing bytes
Understanding file utilities
Paths
Understanding the logger example
Learning about the tail file example
Comparing C++ versus mmap benchmark
Summary
Questions
Further reading
A Hands-On Approach to Allocators
Technical requirements
Introducing the C++ allocators
Learning about the basic allocator
Understanding the allocator's properties and options
Learning the properties
The value pointer type
Equality
Different allocation types
Copying equal allocators
Moving equal allocators
Exploring some optional properties
Optional functions
Studying an example of stateless, cache–aligned allocator
Compiling and testing
Studying an example of a stateful, memory–pool allocator
Compiling and testing
Summary
Questions
Further reading
Programming POSIX Sockets Using C++
Technical requirements
Beginning with POSIX sockets
Beginning with APIs
The socket() API
The bind() and connect() APIs
The listen() and accept() APIs
The send(), recv(), sendto(), and recvfrom() APIs
Studying an example on the UDP echo server
Server
The client logic
Compiling and testing
Studying an example on the TCP echo server
Server
The client logic
Compiling and testing
Exploring an example on TCP Logger
Server
The client logic
Compiling and testing
Trying out an example for processing packets
The client logic
Compiling and testing
Processing an example of processing JSON
Server
The client logic
Compiling and testing
Summary
Questions
Further reading
Time Interfaces in Unix
Technical requirements
Learning about POSIX time.h APIs
Learning about the types of APIs
The time() API
The ctime() typedef
The localtime() and gmtime() APIs
The asctime() function
The strftime() function
The difftime() function
The mktime() function
The clock() function
Exploring C++ Chrono APIs
The system_clock() API
The time_point API
Duration
The steady_clock function
The high_resolution_clock function
Studying an example on the read system clock
Compiling and testing
Studying an example on high-resolution timer
Compiling and testing
Summary
Questions
Further reading
Learning to Program POSIX and C++ Threads
Technical requirements
Understanding POSIX threads
The basics of POSIX threads
Yielding
Synchronization
Exploring C++ threads
The basics of C++ threads
Yielding
Synchronization
Studying an example on parallel computation
Compiling and testing
Studying an example on benchmarking with threads
Compiling and testing
Studying an example on thread logging
Compiling and testing
Summary
Questions
Further reading
Error – Handling with Exceptions
Technical requirements
Error handling POSIX-style
Learning about set jump exceptions
Understanding exception support in C++
Studying an example on exception benchmark
Compiling and testing
Summary
Questions
Further reading
Assessments
Chapter 1
Chapter 2
Chapter 3
Chapter 4
Chapter 5
Chapter 6
Chapter 7
Chapter 8
Chapter 9
Chapter 10
Chapter 11
Chapter 12
Chapter 13
Other Books You May Enjoy
Leave a review - let other readers know what you think
With this book, we aim to provide you with an understanding of Linux/Unix system programming, a reference manual on Linux system calls, and an insider's guide to writing smarter, faster code using C++. The book will explain the differences between POSIX standard functions and special services offered by modern C++. This book will also teach the reader about basic I/O operations, such as reading from, and writing to, files, advanced I/O interfaces, memory mappings, optimization techniques, thread concepts, multithreaded programming, POSIX threads, interfaces for allocating memory and optimizing memory access, basic and advanced signal interfaces, and their role on the system. This book will also explain clock management, including POSIX clocks and high-resolution timers. Finally, this book uses modern examples and references to provide up-to-date relevance to C++ and the wider community, including the Guideline Support Library and its role in system programming.
This book is for beginner to advanced Linux and general UNIX programmers working with C++, or anyone looking for a general overview of Linux, C++17, and/or systems programming with POSIX, C, and C++. Although this book covers a lot of topics on modern C++, its focus is on system programming. It is expected that the reader already has a general familiarity with C and C++, as both will be leveraged throughout this book.
Chapter 1, Getting Started with System Programming, lays the foundation for the book, helping to define what system programming is by providing some basic examples and explaining the benefits of system programming with C++.
Chapter 2, Learning the C, C++17, and POSIX Standards, reviews the C, C++, and POSIX standards, providing an overview of the facilities provided by each standard with respect to system programming, as well as a general overview of the topics that will be discussed throughout this book.
Chapter 3, System Types for C and C++, provides a comprehensive overview of the system types that are provided by C and C++ and how they are used when carrying out system programming. This chapter will also discuss many of the pitfalls associated with the native types and how to overcome them.
Chapter 4, C++, RAII, and the GSL Refresher, provides a general overview of the additions provided by C++17. This chapter will also discuss the benefits of Resource Acquisition Is Initialization (RAII), and how to leverage it when carrying out system programming. This chapter will conclude with an overview of the Guideline Support Library, which is used throughout this book to help maintain C++ core guideline compliance.
Chapter 5, Programming Linux/Unix Systems, provides a comprehensive overview of programming on Linux /UNIX-based systems, including an overview of the System V specification, programming Linux processes, and Linux-based signals.
Chapter 6, Learning to Program Console Input/Output, provides a complete overview of how to leverage C++ to program console input and output, including std::cout and std::cin. More advanced topics, such as how to handle custom types, will also be discussed.
Chapter 7, A Comprehensive Look at Memory Management, provides a complete review of the memory management facilities provided by both C and C++. In this chapter, we will review the shortcomings of C and how modern C++ can be used to overcome many of these shortcomings.
Chapter 8, Learning to Program File Input/Output, reviews how to read and write to files using C++17 and compare these facilities to those provided by C. In addition, we will dive into the std::filesystem additions provided by C++17 for working with files and directories on disk.
Chapter 9, A Hands-On Approach to Allocators, covers C++ allocators and how they can be leveraged to perform system programming. Unlike most other attempts at describing C++ allocators, we will walk you through how to create multiple, real-world examples of stateful allocators, including a memory pool allocator, and demonstrate its potential performance benefits.
Chapter 10, Programming POSIX Sockets Using C++, provides an overview of how to program POSIX sockets (in other words, network programming) using C++ with a series of examples. In this chapter, we will also discuss some of the issues associated with POSIX sockets and how they can be overcome.
Chapter 11, Time Interfaces in Unix, provides a thorough overview of the time interfaces provided by both C and C++ and how they can be used together to deal with time while system programming, including how to use the interface for benchmarking.
Chapter 12, Learning to Program POSIX and C++ Threads, discusses the thread programming and synchronization facilities provided by both POSIX and C++ and how they interrelate. We will also provide a series of examples that demonstrate how to leverage these facilities.
Chapter 13, Error - Handling with Exceptions, covers both C and C++ error handling, including C and C++ exceptions. In this chapter, we will also walk through a series of examples that demonstrate the benefits of leveraging C++ exceptions over traditional C error handling.
The reader should have a general knowledge of C and C++ and be capable of writing, compiling, and executing C and C++ applications on Linux. In order to execute the examples in this book, the reader should also have access to an Intel-based computer running Ubuntu Linux 17.10 or higher. The reader should also ensure that GCC 7.0 or higher is installed using the following:
sudo apt-get install build-essential
You can download the example code files for this book from your account at www.packtpub.com. If you purchased this book elsewhere, you can visit www.packtpub.com/support and register to have the files emailed directly to you.
You can download the code files by following these steps:
Log in or register at
www.packtpub.com
.
Select the
SUPPORT
tab.
Click on
Code Downloads & Errata
.
Enter the name of the book in the
Search
box and follow the onscreen instructions.
Once the file is downloaded, please make sure that you unzip or extract the folder using the latest version of:
WinRAR/7-Zip for Windows
Zipeg/iZip/UnRarX for Mac
7-Zip/PeaZip for Linux
The code bundle for the book is also hosted on GitHub athttps://github.com/PacktPublishing/Hands-On-System-Programming-with-CPP/. In case there's an update to the code, it will be updated on the existing GitHub repository.
We also have other code bundles from our rich catalog of books and videos available athttps://github.com/PacktPublishing/. Check them out!
Feedback from our readers is always welcome.
General feedback: Email [email protected] and mention the book title in the subject of your message. If you have questions about any aspect of this book, please email us at [email protected].
Errata: Although we have taken every care to ensure the accuracy of our content, mistakes do happen. If you have found a mistake in this book, we would be grateful if you would report this to us. Please visit www.packtpub.com/submit-errata, selecting your book, clicking on the Errata Submission Form link, and entering the details.
Piracy: If you come across any illegal copies of our works in any form on the internet, we would be grateful if you would provide us with the location address or website name. Please contact us at [email protected] with a link to the material.
If you are interested in becoming an author: If there is a topic that you have expertise ini and you are interested in either writing or contributing to a book, please visit authors.packtpub.com.
Please leave a review. Once you have read and used this book, why not leave a review on the site that you purchased it from? Potential readers can then see and use your unbiased opinion to make purchase decisions, we at Packt can understand what you think about our products, and our authors can see your feedback on their book. Thank you!
For more information about Packt, please visit packtpub.com.
In this chapter, we will discuss what system programming is (that is, the act of making system calls to the operating system to perform an action on your behalf), and go into the pros and cons of both system programming, and system programming with C++.
In this chapter, we will review the following:
System calls, including what they are, how to execute them, and the potential security risks associated with them
The benefits of using C++ when system programming
In order to follow the examples in this chapter, the reader must have:
A Linux-based system capable of compiling and executing C++17 (for example, Ubuntu 17.10+)
GCC 7+
CMake 3.6+
An internet connection
An operating system is a piece of software designed to execute one or more applications simultaneously, while also providing the resources needed for those applications to execute. To accomplish this, the operating system must be capable of dividing hardware resources between all the applications executing on the system at the same time.
For example, most personal computers (PCs) have a single hard disk that stores all the files being used by the owner of the PC. On modern PCs, it's likely the user will want to execute several applications at once—for example, a web browser and an office suite.
Both of these applications will need exclusive access to the hard disk at various times while executing. In the case of the web browser, this might be to cache websites to disk, while in the case of the office suite, this might be to store documents.
It's the operating system's responsibility to manage the applications and their access to the hard disk, to ensure that both the web browser and the office suite are able to execute properly.
To accomplish this, operating systems provide an application programming interface (API) that applications can leverage to accomplish their tasks. Accessing the hard disk is an example of one of these tasks. The read() and write() functions are examples of APIs provided by POSIX-compliant operating systems for reading from and writing data to file descriptors.
Under the hood, these APIs make calls to the operating system using an application binary interface (ABI) called a system call. The act of making system calls to accomplish tasks provided by the operating system is called system programming, which is the main focus of this book.
Almost every application that executes on a POSIX-compliant operating system must make a couple of system calls. Here, we outline some of the system call types that will be explored in this book.
If you have ever executed a command-line application, you willbe familiar with the concept of console-based input/output. This is especially true with respect to POSIX-compliant operating systems. When outputting to the console, you can either output to stdout (typically used for normal output) or stderr (typically used for outputting error messages).
Outputting to stdout and stderr is accomplished by an application performing a system that asks the operating system to deliver a character buffer to these output devices. (It should be noted that, in this book, we typically state that we are outputting tostdout, not printing to the console.)
The reason for this is that, on POSIX-compliant systems, your application doesn't actually know where it is sending the text to. The application leverages an API to output to stdout. This can be accomplished by:
Writing to a dedicated file handle (that is,
stdout
)
Using C APIs such as
printf
Using C++ APIs such as
std::cout
Forking an application that outputs to
stdout
for you (for example, by using
echo
)
Most of these examples, when all is said and done, make a system call to the operating system to transfer a character buffer to a device that manages stdout or stderr. In some cases, this causes the operating system to relay the resulting character buffer to the parent process (likely your shell), which will ultimately make another system call to display the character buffer on the screen.
However your operating system decides to handle this, a device driver exists in the operating system that manages the physical monitor used to display text, and the simple APIs the application calls to output text (for example, printf and std::cout) eventually provide this device driver with the requested character buffer.
Although, on most systems, the text being output to stdout is usually provided to your shell and eventually displayed on the screen, this doesn't have to be the case. Since the application is making a system call to output the character buffer, the operating system is free to forward this data to a serial device, log file, as input to another application, and so on.
This flexibility is one of the reasons POSIX-compliant operating systems are so powerful, and why learning how to properly make system calls is so important.
Memory is another resource that an application must request using a system call. Most applications are given global and stack memory resources when the application is first executed, along with a small heap of memory that the application can use when calls to functions such as malloc() and free() are made.
If the application only uses the memory that it is initially given in this heap, no extra memory needs to be requested by the application. If, however, heap memory runs out, the application's malloc() or free() engine will have to ask the operating system (via a system call) for more memory.
To do this, the operating system will extend the end of the application by adding more physical memory to the application. The malloc() or free() engine is then able to make use of this additional memory, until more is needed.
On systems with limited RAM, when a request for additional memory is made, the operating system has to take memory from other applications that aren't currently executing. It does this by swapping these applications to disk, an operation that is expensive to perform.
For this reason, on resource-constrained systems, calls to malloc() or free() should not be made in time-critical code, as the time it takes to execute these functions can vary greatly.
We will go into further detail on memory management in Chapter 7, A Comprehensive Look at Memory Management.
Reading and writing to a file is another common use case for most applications that requires making system calls.
It should be noted that on POSIX-compliant systems, reading and writing to a file descriptor doesn't always mean reading and writing to a file on a storage device. Instead, the system calls you make write to character or block devices. This could be a storage device, but could also be a console device, or even a virtual device such as /dev/random, which provides random data when read.
In Chapter 8, Learning to Program File Input/Output, we will provide more information about file input/output system programming.
Networking is another common use case that requires making system calls. On POSIX-compliant systems, we perform network-based system programming by working with POSIX sockets. Sockets provide an API for programming the Network Interface Controller (NIC), and support logic (for example, the TCP/IP stack) within the operating system.
Networking itself is an extremely complicated topic, deserving of its own book, but thankfully, the system calls needed to perform this type of programming are simple, with the majority of the gory details being handled by the operating system.
In Chapter 10, Programming POSIX Sockets Using C++, we will go into further detail on how to make these types of system calls using the socket API.
Some readers might find it surprising to know that even performing simple tasks such as getting the current date and time require system calls to ask the operating system for this information. Even to this day, a dedicated chip (with a battery, in case of loss of power) is provided on the system to maintain the current date and time.
If this information is needed, a system call must be made to request it. When this happens, the operating system will ask the device driver responsible for managing the chip what date and time it is currently storing, and then this information will be returned to the application.
It should be noted that not all time interfaces require system calls. For example, most high-resolution timers, which are designed to compare a high-resolution number before and after an operation has taken place, do not need the operating system to perform this action. This is because these high-resolution timers usually exist directly in the CPU, and their values can be extracted using a simple instruction.
The downside to these types of timers is that their values in and of themselves are usually meaningless (that is, the difference between the values returned is what provides meaning, not the values themselves). Essentially, these timers are usually nothing more than a counter that increments each time the CPU ticks (that is, executes an instruction).
Since modern CPUs can dynamically change their frequency, the values these counters store depends on how long the CPU has executed since the previous power cycle, and at what frequency the CPU was set while it was executing.
There isn't even a guarantee that the value in one counter will be the same as the value read in another counter on another physical core, as each physical core is capable of changing its own frequency independently of other cores on multi-core CPUs.
The benefit of high-resolution timers is that they can be executed extremely quickly (as you are just executing an instruction that reads a counter in the CPU). The difference between two measured values can be used to carry out tasks such as measuring how long it takes to execute small functions—a task that usually doesn't work with standard timers, as they don't have enough granularity.
In Chapter 11, Time Interfaces in Unix, we will go over these details and even provide an example of how to do this yourself.
Executing multiple tasks simultaneously can be accomplished by asking the operating system to create additional threads (or even new processes). This is a common task in system programming, and there are numerous system calls to get the job done.
A process is a unit of execution that has a set of resources assigned to it (for example, memory, file descriptors, and so on.) Each application is made up of at least one process, but they can contain more than one (for example, a shell is an application that is specifically designed to run several child processes).
Each process is scheduled by the operating system to execute for a limited amount of time before the next process is given access to the CPU, and this cycle continues as needed.
Threads are like processes, but they share the same resources as other threads of the same process. Threads provide an application with an opportunity to create tasks that are capable of executing in parallel, without the need for inter-process communication methods. In Chapter 12, Learning to Program POSIX and C++ Threads, we will learn how to program threads using both POSIX and C++ APIs.
System calls are not without their security risks. Even on modern hardware, and using CPU architectures other than Intel, executing more than one process within an operating system with full isolation between processes is nearly impossible.
Although modern hardware and modern operating systems work hard to provide the best possible isolation and security, it should always be assumed that other, malicious processes executing alongside yours may be able to spy on what you're doing, including sensitive tasks such as decrypting user data.
This is another topic that deserves its own book, but here, we will briefly discuss two different, recent security vulnerabilities that affect system programming.
The fast system call interface provided by Intel and AMD was not without its issues. As stated previously, for fast system calls to work, the hardware, operating system, and applications must coordinate. This is to ensure that ABI information is handled properly, to allow the operating system to execute a system call without the need for the hardware to save the entire CPU state before execution begins.
The same applies when the system call is complete, and control must be handed back to the application. To accomplish this, the operating system must load the application's stack, and then execute the SYSRET instruction, which returns control to the application.
The problem with this approach is that a non-maskable interrupt (NMI) could fire between the operating system loading the application's stack and the execution of SYSRET. The result of this race condition is that an NMI (which is code that executes with root privileges) would be executed using the application's stack and not the kernel's stack, resulting in a possible security vulnerability or corruption.
Thankfully, there are ways for modern operating systems to prevent this type of attack, which most operating systems, such as Linux, can and do leverage.
The Meltdown and Spectre attacks are a modern examples of just how complicated system calls are to implement. To support the fast execution of system calls, the kernel's memory is mapped into each application using a memory layout technical called the 3:1 split, which refers to the three-to-one ratio of application memory to kernel memory.
To prevent an application from reading/writing kernel memory, which may or may not contain highly-sensitive information such as encryption keys and passwords, modern CPU architectures provide a mechanism to lock down the kernel portion of this memory, such that only the kernel is capable of seeing it all. The application is only able to see its deprivileged portion of that memory.
To improve the performance of these modern CPUs, most architectures, including Intel, AMD, and ARM, incorporate a technology called speculative execution. For example, look at the following code:
if (x) { do_y();}do_z();
The CPU doesn't know whether x is true or false until it executes this instruction. If the CPU assumes that x is true, it can enhance performance by saving some CPU cycles. If x does, in fact, end up being true, the CPU saves cycles, whereas if x is actually false, the penalty is usually worth the risk, especially if the CPU can make an educated guess as to the likelihood of x being true instead of false (for example, if the CPU executed this statement in the past and x was true).
This type of optimization is called speculative execution. The CPU is executing code, even though it's possible the code may later turn out to be invalid and need to be undone.
Speculative execution attacks such as Meltdown and Spectre exploit this process to bypass the memory protections that protect the system call interface between an application and its kernel. This is done by convincing the CPU to speculatively execute an instruction that would typically cause a security violation (for example, attempting to read a password from kernel memory).
If the CPU speculatively executes this type of instruction, there will be a gap between the CPU loading the password into the CPU's cache, and the CPU figuring out that a security violation has occurred. If the CPU is interrupted during this gap (using what is called a transient instruction), the password will be left in the CPU's cache, even though the instruction never actually completed its execution.
To recover the password from the cache, attackers leverage additional attacks on the CPU called side-channel attacks, which are specifically designed to read the contents of a CPU's cache without performing a direct memory operation.
The end result is that an attacker is capable of setting up an elaborate set of conditions that will eventually allow them to recover sensitive information stored in the kernel, using nothing more than an unprivileged application (which could be a website you happened to click on while looking for cat videos).
If this seems complicated, that's because it is. These types of attacks are extremely sophisticated. The goal of these examples is to provide a brief overview of why system calls are not without their issues. Depending on the CPU and operating system you're executing on, you might have to take special care when handling sensitive information while system programming.
Although the focus of this book is on system programming and not C++, and we do provide a lot of examples in C, there are several benefits to system programming in C++ compared to standard C.
Note that this section assumes some general knowledge of C++. A more complete explanation of the C++ standard will be provided in Chapter 2, Learning the C, C++17, and POSIX Standards.
