Lecture 1: Course Overview and Computer Organization

🎥 Lecture video (Brown ID required)
💻 Lecture code
❓ Post-Lecture Quiz (due 11:59pm, Monday, January 29).

Course overview

Welcome to CS 300 / CSCI 0300: Fundamentals of Computer Systems!

This is an intermediate systems course in the CS department, brought to you by these folks.

You can find all details on policies in the missive, and the plan for the semester, including assignment due dates, on the the schedule page. The first lab (Lab 0) and Project 1 (Snake) are out today.

We use post-lecture quizzes instead of clicker questions to help you to review the lecture material. These quizzes are due at 11:59pm Eastern time the day before the next lecture.

We will look at topics arranged in four main blocks during the course, building up your understanding across the whole systems stack:

  1. Computer Systems Basics: programming in systems programming languages (C and C++); memory; how code executes; and why machine and memory organization mean that some code runs much faster than other code that implements the same algorithm.
    Project: you will learn programming in C and data representation in memory via by building a game (Snake), and you will develop your own debugging memory allocator, helping you understand common memory bugs in systems programming languages (DMalloc).

  2. Fundamentals of Operating Systems: how your computer runs even buggy or incorrect programs without crashing; how it keeps your secrets in one program safe from others; and how an operating system like Windows, Mac OS X, or Linux works.
    Project: you will implement memory isolation via virtual memory in your own toy operating system (WeensyOS)!

  3. Concurrency and Parallel Programming: how your computer can run multiple programs at the same time; how you can share memory even if running programs on multiple processors; why programming with concurrency is very difficult; and how concurrency is behind almost all major web services today.
    Project: you will implement the server-side component of a key-value store, which is a core piece of infrastructure at large internet companies (e.g., Instagram, Facebook, and Airbnb all make heavy use of key-value stores). To make your store handle requests from many users at the same time, you will support multithreading, which lets you scale to the resources offered by modern servers.

  4. Distributed Systems: what happens when you use more than one computer and have computers communicate over networks like the Internet; how hundreds of computer can work together to solve problems far too big for one computer; and how programs running distributedly can survive the failure of participating computers.
    Project: you will implement a sharded key-value storage service that can (in principle) scale over hundreds or thousands of machines.

The first block, Computer Systems Basics, begins today. It's about how we represent data and code in terms the computer can understand. But today's lecture is also a teaser lecture, so we'll see the material raise questions that you cannot yet answer (but you will, hopefully, at the end of the course!).

Machine organization

Why are we covering this?
In real-world computing, processor time and memory cost money. Companies like Facebook, Google, or Airbnb spend billions of dollars a year on their computing infrastructure, and constantly buy more servers. Making good use of them demands more than just fast algorithms – a good organization of data in memory can make an algorithm run much faster than a poor one, and save substantial money! To understand why certain data structures are better suited to a task than others, we need to look into how the computer and, in particulary, its memory is organized.

Your computer, in terms of physics, is just some materials: metal, plastic, glass, and silicon. These materials on their own are incredibly dumb, and it's up to us to orchestrate them to achieve a task. Usually, that task is to run a program that implements some abstract idea – for example, the idea of a game, or of an algorithm.

There is an incredible amount of systems infrastructure in our computers to make sure that when you write program code to realize your idea, the right pixels appear on the screen, the right noises come from the speakers, and the right calculations are done. Part of the theme of this course is to better understand how that "magic" infrastructure works.

Why try to understand it, you might wonder? Good question. One answer is that the best programmers are able to operate across all levels of "the stack", seamlessly diving into the details under the hood to understand why a program fails, why it performs as it does, or why a bug occurs. Another reason is that many of the concepts we encounter once we pull back on how systems work, it turns out that even the details of specific systems (e.g., Windows, OS X, Linux) vary, a surprisingly small number of fundamental concepts explain why computers are as powerful and useful as they are today.

Let's start with the key components of a computer:

These components need to work together to achieve things, and we need to understand them all in order to understand why code and systems behave the way they do.

Today, we will particularly focus on memory. A computer's memory is like a vast set of mailboxes like you might find them at a post office. Each box can hold one of 256 numbers: 0 through 255. This is called a byte (a byte is a number between 0 and 255, and corresponds to 8 bits, which each are a 0 or a 1).

A "post-office box" in computer memory is identified by an address. On a computer with M bytes of memory, there are M such boxes, each having as its address a number between 0 and M−1. My laptop has 8 GB (gibibytes) of memory, so M = 8×230 = 233 = 8,589,934,592 boxes (and possible memory addresses)!

       0     1     2     3     4                         2^33 - 1    <- addresses
    +-----+-----+-----+-----+-----+--     --+-----+-----+-----+
    |     |     |     |     |     |   ...   |     |     |     |      <- values
    +-----+-----+-----+-----+-----+--     --+-----+-----+-----+

Powers of ten and powers of two. Computers are organized all around the number two and powers of two. The electronics of digital computers are based on the bit, the smallest unit of storage, which a base-two digit: either 0 or 1. More complicated objects are represented by collections of bits: for example, a byte is 8 bits. Using binary bits has many advantages: for example, error correction is much easier if presence or absence of electric current just represents "on" or "off". This choice influences many layers of hardware and system design. Memory chips, for example, have capacities based on large powers of two, such as 230 bytes. Since 210 = 1024 is pretty close to 1,000, 220 = 1,048,576 is pretty close to a million, and 230 = 1,073,741,824 is pretty close to a billion, it’s common to refer to 230 bytes of memory as "a gigabyte," even though that actually means 109 = 1,000,000,000 bytes (SI units are base 10). But when trying to be precise, it's better to use terms that explicitly signal the use of powers of two, such as gibibyte: the "-bi-" component means “binary.”

All variables and data structures we use in our programs, and indeed all code that runs on the computer, need to be stored in these byte-sized memory boxes. How we lay them out can have major consequences for safety and performance!

Binary and hexadecimal numbers

Why are we covering this?
All modern digital computers are based on the binary system, so it is important to understand how base-2 numbers work. In practice, most people working in computing write these numbers out using the hexadecimal system, which makes for more compact numbers. You'll see hexadecimal numbers a lot in the course, so we want to get you familiar with this representation early.
Binary (base 2)

Computers use electricity to store and represent data. This naturally leads to a binary system, where the presence of current or electrical charge represents the number 1, and absence of it represents 0. This system became successful because it makes it easy to build reliable hardware. Other systems, like tenary (base 3), were much harder to make work in terms of the electronics.

Binary numbers are in base 2, while our normal (decimal) numbers use base 10. To disambiguiate, people often use subscript notion to show the base: 1012 is a binary number, while 12310 is a decimal number. In binary, each symbol (or digit) in the number can either be 0 or 1, while in decimal, each symbol/digit can take one of ten values (0 to 9).

How do you convert between binary and decimal, you might wonder? This is where positional notation comes in handy. In base 10, each position in a number grows in value by a factor of 10: 12310 = 1 * 102 + 2 * 101 + 3 * 100 = 100 + 20 + 3. We can apply the same idea in binary: each symbol/digit corresponds to 0 or 1 (the only two symbols/digit values that exist in base 2) times two raised to a power equal to the position in the number, counted from the right. So, you'd multiply the rightmost symbol/digit by 20, the second-from-right symbol by 21, the next by 22, etc., to get a decimal number from a binary one.

Here's an example: 1012 = 1 * 22 + 0 * 21 + 1 * 20 = 4 + 0 + 1 = 510. Now go and do a few of these examples yourself! (Consider 10012 and 1000'00012, which correspond to 910 and 12910, respectively.)

Hexadecimal (base 16)

Writing binary numbers gets annoying quickly: since you can only use zeroes and ones, the numbers become very long. For example, decimal number 123410 expressed in binary is 100'1101'00102 (the apostrophes serve to delineate groups of four binary digits; they have no meaning in terms of number value). This is a very long number, and it would be nice if we could express it more concisely!

This is where the hexadecimal system comes in handy. In hexadecimal, rather than having 2 or 10 possible symbols/digits, there are 16. They include the "normal" decimal digits 0 through 9, but also the letters A through F, which represent decimal values 10, 11, 12, 13, 14, and 15, for a total of 16 possible values.

For example, 8D16 is a hexadecimal number composed of "digits" 8 and D. How do we convert it to a decimal or binary number? The same idea of positional notation applies: the rightmost digit gets multipled by 160 (i.e., 1), the next by 161, and so on. So, in our example, 8D16 = 8 * 161 + D * 160 = 8 * 161 + 13 * 160 = 8 * 16 + 13 = 14110.

Programmers and computer scientists love hexadecimal because it has a neat property: four binary digits map exactly to one hexadecimal digit. Think about why this is the case! Using four binary digits, you can represent 24 = 16 different values. How convenient! As a result, translating a binary number into hexadecimal allows us to express it in a much shorter way: for example, 100'1101'00102 becomes 4D216 in hexadecimal.

Now recall that a byte consists of eight bits: eight binary digits, each of which can be 0 or 1. As a result, we can always represent a byte using exactly two hexadecimal digits. You'll see this representation of bytes in the next example.

Programs as bytes: adding numbers

Why are we covering this?
The only place where a computer can store information is memory. This example illustrates that even the program itself ultimately consists of bytes stored in memory boxes; and that the meaning of a given set of boxes depends on how we tell the computer to interpret them (much like with different-sized numbers). Things that don't seem like programs can actually act as programs!

We talked about storing data – like text or numbers – in memory, but where does the actual code for our programs live? It turns out it is also just bytes in memory. The processor needs to know what calculations to run, and we tell it by putting bytes in memory that the processor interprets as machine code, even though in other situations they may represent data like numbers or an image.

And with the right sequence of magic bytes in the right place, we can make almost any piece of data in memory run as code. Consider, for example the add program in the datarep folder of the lecture code. Its add function is defined in addf.c, and when you compile this code by running make, you get a file called addf.o that actually contains the byte representation of the add function. To look at it, you can run objdump -S addf.o in your course container, and you'll see that the add function is encoded as four bytes (0x8d 0x04 0x37 0xc3 in hexadecimal notation).

Programs are just bytes!

We can look at the contents of addf.o using a tool called objdump. objdump -d addf.o prints two things below the <add> line: on the left, the bytes in the file in hexadecimal notation (8d 04 37 c3), and on the right, a human-readable version of what these bytes mean in computer machine language (specifically, in a language called "x86-64 assembly", which is the language my laptop's Intel processor understands).

addf.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 :
   0:	8d 04 37             	lea    (%rdi,%rsi,1),%eax
   3:	c3                   	retq
        ^                       ^
        | bytes in file         | their human-readable meaning in x86-64 machine language
        | in hexadecimal        | (not stored in the file; objdump generated this)
        | notation
What does the machine language mean?

We don't know machine language yet, and though we will touch on it briefly later in the course, you'll never need to memorize it. But to give you an intution, lea means to add integers, and retq tells the processor to return to the calling function.

Let's focus on the bytes. When the program runs, these bytes are stored somewhere in memory. The processor, which on its own is just a dumb piece of silicon, then reads these bytes and interprets them as instructions as to what to do next.

Now we can figure out how we could add numbers using the course logo: our crucial bytes, 8d 04 37 c3 occur inside the JPEG file of the course logo. If we just tell the processor to look in the right place, it can execute these bytes. To do that, I use the addin.c program, which asks the operating system to load the file specified in its first argument into memory, and then tells the processor to look for bytes to execute at the offset specified as the second argument. If we put the right offset (10302 decimal), the processor executes 8d 04 37 c3 and adds numbers! The image decoder, meanwhile, just interprets these bytes (which I changed manually) as data and turns them into slightly discoloured pixels.

What about the party emoji code? That secret will be revealed in the next lecture :-)

Next time, we will look into how to use bytes to represent larger numbers and how to build data structures from them, and get start programming in C.