2. Fundamental Concepts
What is a computer?
This sounds like a simple question, but it cuts to the heart of what we do as programmers. For now, let's just say that a "computer" is anything that executes a program. A "program" is just a series of instructions, and executing a program means starting at the beginning of the series of instructions and following them one-by-one. (If you read a program and follow the instructions yourself, congratulations! You are a computer!)
Every computer has a specific set of instructions that it knows how to follow. We call
this set of instructions a computer's instruction set (very creative, I know).
There are a number of ways to represent an instruction set, but for now let's assume
that the instructions in the instruction set are represented by numbers. So, a program
is just a list of numbers, each one defining a certain action to perform. Here's a
hypothetical example instruction set:
This is basically the instruction set for Logo, a "turtle graphics" programming language
that lets you direct a robot with a pen to move over a sheet of paper and create
Image by Valiant Technology Ltd., CC-BY-SA 3.0.
- 1: move forward
- 2: turn left
- 3: turn right
A program running on a computer with this instruction set that moves forward three times, turns right, moves forward twice, turns left, and moves forward four times would look like this:
1 1 1 3 1 1 2 1 1 1 1
Working with Data
Often the instructions that the computer needs to execute take some form of data as well. A common thing that computers do is add numbers together; it is much simpler to have one instruction that adds rather than a whole set of instructions like this:
- 1: add 1
- 2: add 2
- 3: add 3
- 4: add 4
Or, alternatively, a single "add 1" instruction that you have to call multiple times, which would be equally difficult to use. A program that adds 1,000 to a number would take 1,000 times as much storage space (and take 1,000 times as long to execute) as a program that just added 1!
The data that goes with an instruction has to be part of the program, somewhere. Different programming languages take different approaches to this problem. Some programming languages require you to keep "code" (instructions) and data completely separate, while others combine the two. Each approach has its pros and cons, but for now let's look at combined instructions and data.
For our hypothetical "add some numbers" computer, the instruction set might look like this:
- 1: store next number as "first number"
- 2: add next number to first number, if stored
A program that adds the numbers 2 and 7 would look like this:
1 2 2 7
Stepping through the program one number at a time, we see the instruction "1", "store the next number as first number". The next number is "2", so 2 is stored as the first number. We then see the instruction "2", "add next number to first number". The next number is "7", so our program adds 7 to 2, with the result being 9. Here the data and instructions are intermixed. Seeing "1 2 2 7", it is impossible to know which "2"s are the instruction "add next number to first number" and which are the literal number "2" without starting at the beginning and stepping through the entire program.
Where does the result (9) live? How do we do anything with the result later in our program? And what does it mean to "store" something?
As we have just seen, programs often need a place to temporarily store some data. Most computers accomplish this by providing registers - small places inside the processor that can each hold one value. "Values" here are really just numbers; as we've done with instructions in the instruction set, we can take any kind of value and represent it with a number as long as we have some kind of mapping between the numbers and the things they represent. As an example, Unicode represents every possible character, from every writing system on Earth, with a 32-bit number. (More on "bits" in just a bit.) Registers can be fully generic, or they can be tied to specific kinds of functionality. The NES' processor, for example, has a register called the accumulator, often abbreviated to "A", that handles all math operations. The 6502's instruction set has instructions that work like this:
- store next number in accumulator
- add next number to accumulator, result in accumulator
- put number from accumulator somewhere
This solves the problem of where to put numbers and how to access them. But there is still one open question: when we "put number from accumulator somewhere", where is "somewhere"? The 6502 in the NES only has three registers, so complicated programs like games can't use only registers for storing results.
Computers make available to programs some amount of (non-permanent) memory to store things temporarily, allowing the computer to have a small number of (expensive) registers while still allowing for a reasonable amount of values to be stored outside of the program itself. This memory is made available as a series of register-sized boxes, each holding one value and referred to by number. The NES provides your program with two kilobytes (2KB) of memory space, numbered from zero to 2,047 - a memory space's number is referred to as its address, just like a house number. So, the 6502 instruction set we looked at above is really more like this:
- store next number in accumulator
- add next number to accumulator, result in accumulator
- put number from accumulator into memory address of next number
This leads to our final question for this chapter - how are all of these numbers represented inside the computer?
Up to this point, we have been using "standard", decimal (base 10) numbers. These are the kinds of numbers that we use every day - numbers like "2" or "7" or "2,048". Computers, however, operate through electrical currents that can either be "on" or "off", with no in-between. These currents form the basis of all data inside the computer, and as a result computers use binary (base 2) numbers.
The smallest unit of information a computer can process is a "bit", or "binary digit". A bit stores one of a two values - 0 or 1, "on" or "off". If we combine more than one bit as a single number, we can represent a larger range of values. Two bits, for example, can represent four different values:
00 01 10 11
Three bits let us represent eight different values:
000 001 010 011 100 101 110 111
Each bit we add allows us to represent twice as many values, in the same way that each decimal digit we add to a decimal number lets us represent ten times as many values (1 → 10 → 100 → 1,000). Eventually, we reach eight bits working together to represent a single value, which is so common that it has its own name: a byte. A byte can store one of 256 values. Since four bits is half of a byte, it is occasionally referred to as a nybble, which is incredibly cute. A nybble can hold one of 16 values.
Computers, including video game consoles, are often described as being a certain number of bits. Modern desktop/laptop computers are generally "64-bit", older versions of Windows like Windows XP are called "32-bit operating systems", and the NES is an "8-bit" system. What all of these are referring to is the register size of the computer - how many bits a single register can hold at one time. Slightly complicating things, the NES' address bus is actually 16 bits wide, meaning the NES can deal with 65,536 different memory addresses instead of just 256. Each memory address still only holds one byte, though. Since the NES is an "8-bit" computer, its registers each hold an 8-bit value (one byte). Additionally, each memory address can hold one byte.
How do we work with numbers larger than 255? It is not uncommon for someone playing Super Mario Bros. to get a score in the tens of thousands, far too large a number to represent in one byte. When we need to represent a value that is larger than what one byte can hold, we use more than one byte. Two bytes (16 bits) can hold one of 65,536 different values, and as we add more bytes our representational power increases sharply. Three bytes can store a value up to 16,777,215, and four bytes can store a value up to 4,294,967,295. When we use more than one byte in this way, we are still limited by the register size of the computer. To work with a 16-bit number on an 8-bit system, we need to fetch or store the number in two parts - the "low" byte on the right, and the "high" byte on the left. Dealing with these larger-than-one-register values is why processors have a defined endianness - i.e., which byte comes first when dealing with large numbers. "Little-endian" processors, like the 6502, take the low byte first followed by the high byte. "Big-endian" processors, like the Motorola 68000, do the opposite, expecting the high byte to come first followed by the low byte. Most modern processors are little-endian, since Intel's hugely popular x86 architecture is little-endian.
Since the 6502 that powers the NES works with eight bits of data at a time, smaller numbers still take eight bits to represent. This can be inefficient, so it is common to represent multiple smaller values in a single byte when needed. One byte can hold two four-bit numbers, or four two-bit numbers, or even eight individual on/off values (we call these "flags").
As an example, the byte
10110100 could represent:
- One 8-bit value: 180
- Two 4-bit values: 11 (
1011) and 4 (
- Four 2-bit values: 2 (
10), 3 (
11), 1 (
01), and 0 (
- Eight on/off (or true/false) values: on, off, on, on, off, on, off, off
- Any other combination of bit lengths that add up to eight
To help us talk about these multiple-values-in-one-byte scenarios, it's common to number the bits inside a byte, much like how we can name the "low" and "high" bytes in a 16-bit value. The bit on the far right is "bit 0", and bits count up toward "bit 7" on the far left. Here's an example:
byte: 1 0 1 1 0 1 0 0 bit #: 7 6 5 4 3 2 1 0
Making Data Human-Readable
As we have seen, bytes are a very flexible way to represent a variety of data types in a computer system. The downside of using bytes, though, is that they are difficult to read. It takes work to see "10110100" and mentally translate it to the decimal number "180". When an entire program is represented as a series of bytes, the problem is made exponentially worse.
To solve this problem, most code is represented as hexadecimal numbers. "Hexadecimal" is a fancy way of saying "base 16"; a single hexadecimal ("hex") digit can hold one of sixteen values. Here are the numbers from zero to fifteen represented in hexadecimal:
0 1 2 3 4 5 6 7 8 9 a b c d e f
Hex notation is useful because a hex digit and a nybble store the same range of values. This means we can represent a byte using two hex digits, a much more compact and easy-to-read representation.
|Decimal||Binary (1 byte)||Hex|
Dealing with numbers that could be decimal, binary, or hexadecimal can
be confusing. The number "10", for example, represents 10 if it is
decimal, 2 if it is binary, or 16 if it is hexadecimal. To make clear
what value we are referring to, the convention is to use prefixes.
10 is a decimal number,
%10 is a binary number,
$10 is a hexadecimal number.
Putting It All Together
Now that we've covered a wide range of topics related to how computers (and programs) work, let's take one more look at the entire process of what happens when you run a program.
First, the program itself is represented as a series of bytes (what we call machine code). Each byte is either an instruction for the processor, or a piece of data that goes with an instruction.
The processor starts at the beginning of the program and repeatedly carries out a three-step process. First, the processor fetches the next byte from the program. The processor has a special register called the program counter, which keeps track of what byte number of the program is next. The program counter (or "PC") works together with a register called the address bus, responsible for retrieving and storing bytes from the program or from memory, to fetch bytes.
Next, the processor decodes the byte it fetched, figuring out which entry in its instruction set the byte corresponds to (or which instruction the data byte goes with). Finally, it executes the instruction, which will make changes to the processor registers or memory. The processor increments the program counter by one to fetch the next byte of the program, and the cycle begins again.