Professional Documents
Culture Documents
Computers Broad & Shallow by Benjamin Crysup
Computers Broad & Shallow by Benjamin Crysup
Benjamin Crysup
©2018 Benjamin Crysup
All Rights Reserved
ii
Preface
iii
iv
Contents
I How to computer 1
1 How to build transistors from silicon 3
v
II How to computer science 163
12 How to dope 165
vi
Part I
How to computer
1
Chapter 1
How to build
transistors from
silicon
Bits
Presumably, you’ve heard that computers make use
of bits and bytes. While technically true, without
knowing exactly what those are, that is an absolutely
useless statement. Additionally, there are two views
of what bits are that, while similar, carry enough
differences to confuse matters.
From an information theory standpoint, a bit
is a fundamental quantity of information: the
ability to distinguish between two states. These two
states could be zero or one, yes/no, hot/cold, A/B,
situation normal/everything’s on fire. Exactly what
the states are is not particularly relevant, just
that there are two possible states the system could
be in. If there are more possibilities, then each
bit of information cuts the number of possibilities
in half.
For instance, suppose you’re in an elevator with
four other people (two on your left, two on your
3
right), and you smell a fart. If you’d like to
know who did it, you might first ask whether it
was somebody on your left or right (one bit of
information), then on that side, which of the two
people was responsible (a second bit).
4
the paper is intact or punched. This method has been
in use since the 1830’s, and is mechanically simple
(the detector can be as simple as a well of mercury,
a needle, and wire thread).
5
The first byte of a funny cat photo.
Electricity
When two charged particles are close together, they
exert a force on each other. If they have the same
charge, they will push themselves apart, and if they
have opposite charges, they will pull themselves
closer together.
6
Electrons -, Protons +
7
placed at the same location would gain twice as much
(total) energy when they move away. The same goes
for larger numbers of electrons, so when describing
this system, one only needs to describe the energy
gain per amount of charge. A very common unit to
describe the amount of energy charges can gain is
the Volt. Since electrons like to move away from
negative charges and towards positive ones, electrons
will move from negative voltages towards positive
voltages.
This suggests another way to represent a bit.
If one has a wire, they can examine the potential:
if the wire is at a negative potential (electrons
want to leave), it is interpreted as zero/no/cold,
while if the wire is at a positive potential, it is
interpreted as one/yes/hot.
As a side note, you may have noticed that batteries
are usually labeled with a positive terminal and a
negative terminal. This matches the above definition
of voltage: electrons tend to migrate from the
negative terminal (which is at a negative voltage)
towards the positive terminal (which is at a positive
voltage). So, in many cases, examining the potential
of the wire is equivalent to asking which terminal of
a battery the wire is connected to.
8
Semiconductors
If voltage is being used to represent a bit, a fairly
easy way to transport the value is by using a wire.
Metals happen to be fairly good conductors, as the
atoms each have a large number of electrons, many of
which are very far from the nucleus and very loosely
bound. With the dispersed nature of the electron
orbitals, the electrons in a metal can move very
easily, meaning that if the metal is attached to a
constant voltage, the entirety of the metal equalizes
to that voltage.
9
Some crystal structures are easy to draw. Diamond
not so much.
10
are tightly bound, they cannot move in response
to a voltage difference, so pure silicon is a poor
conductor of electricity.
The word “pure” turns out to be very important. If
there are impurities in the silicon, they introduce
faults in the crystal structure and destabilize the
neighboring bonds, which in turn introduce mobile
charges. If a group five element (phosphorous,
arsenic), which has five valence electrons, is
introduced, the crystal structure of the silicon
imposes four bonds on the atom (in a tetrahedral
arrangement), producing an extra electron that is
free to wander around the crystal, in turn producing
silicon that can conduct electricity.
11
It’s worth repeating that the protons are going
nowhere: the electrons from the neighbors move to
fill the hole.
Transistors
Silicon “doped” with a group five element,
n-type silicon, has mobile negative charges that
can conduct electricity. Silicon doped with a
group three element, p-type silicon, has mobile
electron deficient regions (that can also conduct
electricity). If a region of n-type silicon borders
a region of p-type silicon, the mobile electrons
will scatter randomly and eventually encounter the
electron deficient regions, creating a region of
silicon without carrier charges (an insulator).
12
A line in the sand (a.k.a. a diode).
13
The charges shown are only the excess charges; there
are a large number of paired protons and electrons
just hanging around.
14
The symbol used in this book for an n-channel
Metal-Oxide Semiconductor Field Effect Transistor.
15
Challenges
1) The methods to store and transmit bits presented
in this chapter are by no means the only options.
Come up with three other possible ways to store or
transmit data.
16
Chapter 2
Boolean Arithmetic
By now, you’re probably familiar with the basics of
arithmetic. For example, if you have 5 moneys and 3
moneys, you can ADD them to get 8 moneys.
3 + 5 = 8
Lots of moneys.
17
For some strange reason, philosophers don’t like
working with numbers: they prefer to work with
questions of True and False. The variables they
work with are termed Boolean variables, and are
surprisingly useful to the computer engineer: a
Boolean can take one of two values, just as a bit can
take on one of two values.
Just as there are basic operations that can be
performed with numbers (add, subtract, multiply,
divide), there are basic operations that can be
performed with Booleans. There are three operators
that are of immediate interest to us.
The first of these is the NOT operator. The NOT
operator takes one value, and changes it from false
to true, and vice versa. Many programming languages
use an exclamation point “!” for NOT, so that’s what
we’ll use.
! True = False
! False = True
18
Boolean Logic
This Boolean arithmetic allows us to construct some
basic logical statements. For instance, suppose
you receive regular reports about the status of a
chemical tank. If the reports say that everything is
O.K., then you don’t have to do anything. However,
if the reports say things are NOT alright, that would
be a good time to panic.
Panic = ! AllOK
Hold your breath.
19
And of course, you know that the shambling horror
coming towards you is a zombie if it is rotting AND
hungers for your brains.
Truth Tables
These operations can be chained together to produce
some relatively complicated logic. For example,
when fighting off the undead hoards, you may need
to decide whether to stand your ground or run for the
hills. This decision will likely be decided by how
much ammunition you have handy, how close the horde
is, and whether you have any open room to run.
20
Run = can_run & (low_ammo | zombies_close)
Run = False
21
You have chosen... poorly.
22
The NOT operator takes ONE value and produces ONE
result. So, a NOT gate will take one wire as input
and will produce one wire as output: where these
wires come from and go to are not our problem right
now.
Where did they come from? Where did they go? Where
did they come from, Cotton-eyed Joe?
23
Half a NOT.
24
A Complementary MOS NOT gate.
25
Output = !Input
26
These things show up everywhere.
AND/OR Gates
The circuits for AND gates and OR gates are fairly
similar. Both have two inputs (A and B) and one
output. For an OR gate, if either input is True, the
Output should be connected to the positive voltage.
This can be done with two n-channel transistors, one
for each input.
Half an oar.
27
this, we need two p-channel transistors.
A CMOS OR gate.
28
A CMOS AND gate.
Running Gates
Going back to our decision of whether or not to run
from the zombie hoard: suppose we have three wires,
one carrying whether running is even an option,
another carrying whether we are low on ammunition and
the last carrying whether the zombies are close. Our
expression for running was
29
If we want to create a circuit that decides whether
we should run, we’ll need an AND gate and an OR gate.
30
Challenges
1) Just like algebra, there are many ways to go about
manipulating and simplifying Boolean expressions.
Look up a few and see if you can simplify
(A & (B | !B)) | (C & !(A | C))
2) With the circuits for NOT, AND and OR, care was
taken to avoid having a direct connection between the
positive terminal and the negative. That event is
called an electrical short. Do some research into
what an electrical short can do.
31
32
Chapter 3
How to build
computation circuits
from gates
R1 = !A | !(B | C)
33
A B C R1
0 0 0 1
0 0 1 1
0 1 0 1
0 1 1 1
1 0 0 1
1 0 1 0
1 1 0 0
1 1 1 0
1) Scan the truth table and find all rows for which
the result is True.
A B C R1
0 0 0 1
0 0 1 1
0 1 0 1
0 1 1 1
1 0 0 1
- - - -
- - - -
- - - -
34
A B C R1
0 0 0 1 !A & !B & !C
0 0 1 1 !A & !B & C
0 1 0 1 !A & B & !C
0 1 1 1 !A & B & C
1 0 0 1 A & !B & !C
- - - -
- - - -
- - - -
R2 =
(!A&!B&!C) | (!A&!B&C) | (!A&B&!C) | (!A&B&C) |
(A&!B&!C)
This equation produces the exact same results as
the original (you might check this by constructing a
truth table). In step 2, equations were built that
were True for only one condition, and step 3 combined
them into an expression that was True for any valid
condition.
In this particular case, using R2 is a bad idea,
since we have the original statement (R1) which
is MUCH simpler. This method tends to produce
complicated expressions, so it’s really only useful
if you have a truth table without the expression that
created it.
The Multiplexer
In order to build a computer, there are three
circuits you need to understand: if you understand
how to build a multiplexer, an adder and a flip-flop,
you have enough knowledge to struggle through a full
computer. It won’t be easy, but at that point it’s
a matter of effort rather than knowledge (can’t vs.
won’t). We’re covering the multiplexer and the adder
in this chapter, and we’ll start with the simplest,
the multiplexer.
35
Suppose you’re a general, you have reports coming
in from two of your officers (Allen and Bert), and
that you know you have spies in your organization.
Et tu, Brute?
36
Symbol for a multiplexer.
A B C Out
0 0 0 0
0 0 1 0
0 1 0 1
0 1 1 0
1 0 0 0
1 0 1 1
1 1 0 1
1 1 1 1
37
A B C Out
- - - -
- - - -
0 1 0 1
- - - -
- - - -
1 0 1 1
1 1 0 1
1 1 1 1
Step 1
A B C Out
- - - -
- - - -
0 1 0 1 !A & B & !C
- - - -
- - - -
1 0 1 1 A & !B & C
1 1 0 1 A & B & C
1 1 1 1 A & B & !C
Step 2
38
Output = A if C otherwise B
Binary Numbers
The next item is the adder: a circuit that can add
two numbers. However, before we can build that, we
need to know how to store numbers using bits. We
are going to limit ourselves to integers, or whole
quantities. The best example happens to be money:
it’s possible to have one penny, ten pennies, zero
pennies (sucks to be there, but it happens), or
negative one thousand pennies (in which case you owe
somebody $10). However, you can’t have half a penny.
39
Unlike the Spanish real, nobody will take a penny
that you cut into eight bits.
40
many pennies as you can for dimes. So, for instance,
if you have 23¢, you would have two dimes and three
pennies.
41
This basic process is repeated for each group of
ten: once you have ten of something, you group it
and count it as the next largest group (I’m ignoring
nickels, quarters and half-dollars to make a point).
This is the basic idea of Arabic numerals: the
rightmost digit stores the number of “ones” (pennies)
you have. The digit to the left of that stores the
number of “tens” (dimes), and the digit to the left
of that stores the number of “hundreds” (dollars).
As an example, the number 123 is composed of one
“hundred”, two “tens” and three “ones”.
42
One eight, zero fours, one two and one one. Also
known as eleven.
43
Addition
Let’s say you have 263¢, and you rob somebody who
has 675¢. After you make good on your getaway, you’d
like to know how much money you have.
Big money.
All in.
44
Then, you’d move on to the dimes. In this case,
your six dimes would be combined with the seven
stolen dimes to get thirteen dimes. Since we’re
collecting groups of ten, we trade ten dimes for a
dollar, leaving three dimes.
45
This is the basic idea of addition: each
unit/denomination/place is collected, and any results
larger than nine add one to the next group (called a
carry).
46
when we’re collecting the “two” bits (the second
digit), we only need to know three things to tell
what the sum is. We need to know the “two” bits
from both source numbers, and we need to know whether
the “ones” bit produced a carry. For all positions
you do the same thing with those three pieces of
information, so if we can build a circuit that
produces a result for a single bit, we can produce
many of those “full adders” and chain them together
to add however many bits we want.
A B C_in S C_out
0 0 0 0 0
0 0 1 1 0
0 1 0 1 0
0 1 1 0 1
1 0 0 1 0
1 0 1 0 1
1 1 0 0 1
1 1 1 1 1
S = (!A&!B&C_in)|(!A&B&!C_in)|(A&!B&!C_in)|(A&B&C_in)
47
C_out =
(!A&B&C_in)|(A&!B&C_in)|(A&B&!C_in)|(A&B&C_in)
S = (!A&!B&C_in)|(!A&B&!C_in)|(A&!B&!C_in)|(A&B&C_in)
C_out =
(!A&B&C_in)|(A&!B&C_in)|(A&B&!C_in)|(A&B&C_in)
48
Three bits. That’s all of eight values.
49
Challenges
1) The multiplexer presented in this chapter selects
between two options. Design a multiplexer that
selects between four options.
50
Chapter 4
Motivation
Up to this point, all of our circuits have calculated
their results immediately: once the inputs have been
fed to the circuit, the output changes to match. For
simple problems this may be satisfactory, but as your
problems grow more complex, the circuits required to
solve them become increasingly complicated. Worse
still, it is incredibly difficult to use this type of
circuit to build a general purpose machine: if you
build an adder, all it will ever do is addition.
A solution to these problems is to break up the
computation into multiple steps. For instance, if
you have four numbers that need to be added together,
you might add them together two at a time. If the
computation is broken up into steps, then you can
reuse parts of your circuitry for each step: adding
four numbers takes one adder if done in steps, but
three if done all at once.
Of course, this doesn’t happen for free.
In particular, we need some way to store the
51
intermediate results between steps. This is what
a flip-flop circuit does. However, unlike the
adder and multiplexer, the flip-flop is relatively
complicated: building it requires having a clock
signal to drive the computation onward and being able
to build an SR latch.
SR Latch
The first thing we need is a Set-Reset latch.
This device is a circuit with two inputs named,
surprisingly, Set and Reset.
52
I’ve yet to see a good symbol for these.
53
Memory. Terrible memory, but memory.
54
It can remember a one.
55
Explosives may be a bit excessive.
The Clock
The SR-latch provides some memory, but you may have
noticed that the output changes immediately when the
inputs change. This means that performing sequential
logic using them is a very delicate balancing act, as
you have to worry about how long it takes each signal
to travel along the wires. Since we were trying to
avoid complexity, this is counterproductive. What
we need is some way to stop information from going
forward until we are ready: what we need is a clock.
Not quite.
56
It’s hip to be square (wave).
57
The wise among you will know not to try this.
58
can use it to prevent the latches from pushing their
data along until everything is synchronized.
D Flip-Flop
With a clock signal and two SR-latches, it’s possible
to build a circuit that stores its input one clock
cycle, and parrots it the next (a D flip-flop). To
do this, we need to gate one of the latches so it
only stores when the clock is positive (to store the
input). This can be done by adding a few gates on
the inputs so that, when the clock is negative, Set
and Reset are held at negative.
59
With exactly one extra gate.
60
The “D” stands for “data”. Or possibly “delay”. Or
maybe “dammit”.
Multiplication
The entire reason for building a flip-flop was to
break up computation into stages: it might be
helpful to see how this is done. A good candidate
61
for this is multiplication. Building a multiplier
using the truth table method is possible, but a huge
pain (the adder could be designed in isolation, but
each bit in a multiplier depends on all the bits in
the numbers).
However, there are multiple ways to break up
multiplication into steps. Multiplication is simply
repeated addition: if a defendant in a trial wants
to bribe each of the twelve jurors $10, then the
total amount of money needed is $10 + $10 + $10 +
$10 + $10 + $10 + $10 + $10 + $10 + $10 + $10 + $10.
That’s equivalent to 12 x $10, or 120 dollars.
62
Repeat ad-nauseum.
Much simpler.
63
From hero to zero.
64
Are we there yet?
65
Done
66
Challenges
1) A D flip-flop parrots the data (D) after one
clock cycle. A T flip-flop flips its output (true
to false, false to true) whenever its input is one.
See if you can design one.
67
68
Chapter 5
How to build a
computer
The Goal
All the examples up to this point have been single
purpose machines. The idea of a single purpose
computer is old: there are mechanical astrolabes
dating back to ancient Greece, and battleships in
the 1940’s used analog computers to calculate how to
angle the guns.
69
Shoot the moon.
70
performs them.
Basic Organization
Unless you go with a very exotic architecture, most
computers consist of a place to store the current
state/data of the program, a place to store the
instructions (that describe how that state should
be modified), and a bunch of circuitry that takes
the current state and the current instruction and
COMPUTES the next state.
71
A LOT of computer science is simply managing
complexity.
72
Seven bytes. A staggering amount of memory.
73
Can you remember seven things?
Instructions
If you look at the von Neumann architecture,
you’ll notice that there is no place marked off for
instructions. That’s because instructions have to be
encoded as bits (our computer only deals with bits),
which memory is perfectly capable of storing. A von
Neumann architecture uses the same memory for storing
instructions and data.
74
Why reinvent the wheel or waste space?
75
Simplicity itself.
76
Register0 = Register1 + Register2
IP = IP + 2
Instruction Flavors
Now that we have out instructions encoded, it’s
worth asking what instructions our computer will
have. If you look at any mainstream architecture,
you will find a fairly large number of instructions.
However, they tend to fall into one (or more) of four
categories.
The first category is that of simple computation.
These are instructions that take values in registers,
perform computation (add, subtract, multiply,
divide), and put the result into a register. These
are the workhorse instructions: any flavor of
arithmetic that the computer needs to do is done
with these instructions. The most common flavors
of arithmetic are numerical (add, subtract, etc...),
Boolean (and, or, not) and, perhaps strangely,
comparisons (7 > 3 is True). Different architectures
add their own spins in the name of speed (x86
includes some vector operations, and ARM includes a
free bit shift with every instruction), but in the
end it’s all simple computation.
The second category is memory management. Unless
your problem is small enough to fit in the registers
(only 64 bytes), there must be some way to move data
77
between registers and bulk storage. In particular,
you need to be able to get a byte from an address
(stored in a register) and put that byte into a
register.
78
An important question is how many bytes we have
access to. For example, if our registers hold eight
bit numbers, and we use a register to specify a
location in memory, our memory can only hold 256
bytes. However, if our registers hold sixteen
bit numbers, our memory can hold 65536 values, and
thirty-two bits give us 4294967296 values. If you’ve
ever wondered what people meant when they said a
machine was 16, 32 or 64 bit, this is (mostly) what
they’re referring to, and is called the “word size”.
Since registers tend to be larger than a byte,
there are typically instructions to move an entire
register worth of data at a time. So, if the machine
used 16 bit registers, there is probably a command to
get two bytes from RAM, and another to set two bytes
in RAM.
The third category is program flow control.
Normally, the instruction pointer goes straight
through instructions: instruction 1, then 2, then
3, and so on. However, you might want a program that
responds to changing events. For instance, if your
program is managing a heart monitor and a patients
pulse drops below 20 bpm, your program should start
sounding an alarm.
79
jump can be used to skip past the alarm code if
everything is alright, while going through the alarm
code if something’s wrong.
Beeeeeeeeeep.
if Register1 is non-zero
IP = Register2
80
if Register1 is zero
IP = IP + 2
Programs
Once you have your instructions, you just need to
figure out how to combine them to do what you want,
and you need to run them. For running the program,
there is usually some piece of logic (either baked
into the computer, or loaded in by something baked
into the computer) that can read the program’s bytes
from an external source (usually a hard drive) and
copy them into memory. Once they’re in memory, the
81
computer just needs to set the instruction pointer to
the first byte and start running.
Combining the instructions into a coherent program
is a fairly broad topic, and represents the dividing
line between hardware (transistors, gates and
circuits) and software (programs). It also happens
to be the topic of the second quarter of this book.
82
Challenges
1) The organization presented in this chapter is the
most common, but it’s not the only one. Research a
few other types of architectures, then compare and
contrast them with the von Neumann architecture.
83
84
Chapter 6
How to program
arithmetic
Assembly
A running program is a large collection of bytes.
If you really wanted to, you could arrange the
bytes yourself to produce the commands you want the
computer to run. However, the human eye was never
meant to look at long lists of numbers: the ones and
zeros will start bleeding into each other.
85
look at words and extract meaning (this is called
READING). Processor designers have noticed this, and
have written programs that will read in a specially
formatted text file and produce machine code. These
programs are called assemblers, and the text files
they read are called assembly files.
When figuring out how programs work, you will
have to work with machine instructions; this does
not mean you have to work with machine code. Most
lines in an assembly file turn into one instruction
for the machine: some assemblers provide a few
extra features, but by and large there is a one
to one equivalence between assembly and machine
code. In other words, programming in assembly IS
programming in machine code, just without the hassle
of remembering bit patterns. This means that an
assembly programmer knows exactly what the machine
will do in response to their program, and they can
squeeze out the full measure of performance from
their machines.
86
in your phone) look nothing like those for an x86
processor (the processor in your laptop), so assembly
written for an ARM processor will not run on x86, and
vice versa.
87
decidedly NOT a RISC processor: it is a complicated
mess of a machine. However, if you program, you will
run into it, so this book will use x86 as an example.
That doesn’t mean that the full complexity of x86
will be presented: this book will use a subset of
x86 that looks like a RISC code. If you take this
up for a living, know that you should NOT program an
x86 machine the way this book does (there is a whole
lot of stuff I’m skipping). This book will also use
32 bit x86: while 64 bit is present and dominant,
x86 is backwards compatible, and the standard calling
convention is easier for 32 bit. Additionally, I
will use the syntax of the GNU compiler suite (it’s
what I’m most familiar with); if you use a different
compiler, the format may be different.
That said, let’s start this trainwreck. For a user
level program, your view of the x86 state is eight
32-bit registers (eax, ebx, ecx, edx, esi, edi, ebp,
esp), two Boolean registers (s, z), one instruction
pointer, and up to 4 billion bytes of memory.
88
x86 Arithmetic Instructions
x86 provides a whole host of arithmetic instructions.
Here are seven useful ones.
addl source, destination
Calculates the sum of source and destination, puts
the result in destination, and sets s and z based on
whether the sum was negative, zero or positive. As
an example, “addl %eax, %ebx” will sum up eax and
ebx, and will put the result in ebx.
89
result in destination, and sets s and z based on the
result. As an example, “negl %eax” will calculate
the negative of eax (if eax holds 5, this will
produce -5) and store that in eax.
andl source, destination
Calculates a bitwise AND on source and destination,
puts the result in destination, and sets s
and z based on the result. As an example,
“andl %eax, %ebx” will perform 32 AND computations
(the first bit of eax & the first bit of ebx, the
second of eax & the second of ebx, etc...), and will
put all 32 results into ebx.
orl source, destination
Calculates a bitwise OR on source and destination,
puts result in destination, and sets s and z.
“orl %eax, %ebx” ORs eax and ebx, and puts the result
in ebx.
notl destination
Calculates a bitwise NOT on destination and puts the
result in destination (does not change s and z).
“notl %eax” will turn every one in eax into a zero,
and vice versa.
movl source, destination
Copies the value of source to destination (does not
change s and z). “movl %eax, %ebx” will set ebx to
the value in eax.
90
movl number, destination
This will put a numerical value into the destination.
“movl $123, %eax” will put 123 into eax. Note that
addresses are also numerical values (including the
addresses of instructions). The reason this command
is in the memory section is that, technically, this
does move a value in from memory (code is stored in
memory).
Arithmetic
With these thirteen commands, it’s possible to
construct the basics of programming. We just need
to figure out how to chain them together to do what
we want.
The first thing worth going over is how to
construct complicated arithmetic expressions in
assembly. You’ll notice that all of the arithmetic
commands deal with only one or two numbers: if
we want to calculate something more complicated,
we’ll need a sequence of commands. Our first
order of business is to use assembly as a glorified
calculator.
91
Suppose you are an accountant for the local...
legitimate business, and you need to determine
whether one of the customers is paid up.
92
Sleeping with the fishes.
Variables
Now suppose you have another customer, who has 10
transactions on his account. If you remember, 32
bit x86 only has eight registers. We can’t store
all the numbers in registers, but we have memory to
work with. Most assemblers will let you add data
as well as code, so you might start by putting your
transactions in a data section.
.data
#int transaction1 = -250;
transaction1:
.long -250
#int transaction2 = -50;
transaction2:
.long -50
#int transaction3 = 275;
transaction3:
.long 275
# //and 7 more
The “.data” is an assembler directive: it does not
show up in the machine code, but lets the assembler
know to produce data instead of code. The items
beginning with “#” are comments: they are ignored
by the assembler (if you are familiar with C, you
93
will know why the comments are written as they are;
if not, re-read this chapter after reading chapter
nine).
Most assemblers also allow you to specify labels:
a lot of code revolves around addresses, and figuring
out the address of something in code is tedious (and
error prone) arithmetic. This arithmetic is not
particularly hard, so the assembler can do it for
you. A label is a name followed by a colon.
And, at last, we get to the thing that actually
stores the data. The “.long” tells the assembler
that the numbers should be stored in 32 bits. The
numbers should explain themselves.
The code section (“.text”) becomes a little more
complicated. We need to get the address of each
transaction, load it into a register, and add it to
our accumulator. The first thing we need to do is
zero the accumulator.
.text
# int* transactionLoc;
# int curTransaction;
# int sum = 0;
movl $0, %eax
94
# transactionLoc = &transaction1;
movl $transaction1, %ebx
# curTransaction = *transactionLoc;
movl (%ebx), %ecx
# sum = sum + curTransaction;
addl %ecx, %eax
# transactionLoc = &transaction2;
movl $transaction2, %ebx
# curTransaction = *transactionLoc;
movl (%ebx), %ecx
# sum = sum + curTransaction;
addl %ecx, %eax
# transactionLoc = &transaction3;
movl $transaction3, %ebx
# curTransaction = *transactionLoc;
movl (%ebx), %ecx
# sum = sum + curTransaction;
addl %ecx, %eax
# //and 7 more
95
Challenges
1) When I explained the thirteen commands, I was
paving over a lot of complexity. As an example,
it is possible to add an item in a register with an
item in memory. Look up how, and rewrite the second
example (the 10 transaction customer) making use of
this.
96
Chapter 7
97
Do a barrel roll.
98
in question: it could be as simple as reading
a memory location, or it could require handling
interrupts). Those pieces of information are stored
in variables, so the assembly file you’ve been given
to work on looks like:
.data
#int planeTemperature;
planeTemperature:
.long 0
#bool pilotWantsEject;
pilotWantsEject:
.long 0
.text
#void testEject(){
testEject:
#TODO
#}
When the program has run, some other piece of
code will have changed planeTemperature to the
temperature, and pilotWantsEject to zero (for no) or
something else (for yes). All we need to do is fill
in code at TODO to figure out if we need to eject the
pilot, and if so, jump to some other code that will
actually do that (which somebody else will provide:
again, it’s specific to the equipment).
Conditional Execution
If we determine that we need to eject, the provided
place to jump to will be called “ejectPilot”. The
first thing we should test is whether the temperature
is too high, which means we need to load the current
temperature.
movl $planeTemperature, %eax
movl (%eax), %ebx
We’ll also need to load in the threshold (200).
99
to test this (that I’ve told you about). However,
we have a command (subl) that will change condition
codes. If we subtract the plane temperature from
200, we will get a negative value if the temperature
is larger than 200. If the temperature is less
than 200, we’ll get a positive value, and if it’s
equal we’ll get zero. So, we can test whether the
temperature is too high via subtraction.
100
is zero, but will jump to ejectPilot for any other
value. So, the whole of our code looks like:
.data
#int planeTemperature;
planeTemperature:
.long 0
#bool pilotWantsEject;
pilotWantsEject:
.long 0
.text
#void testEject(){
testEject:
# if (planeTemperature > 200)
movl $planeTemperature, %eax
movl (%eax), %ebx
movl $200, %eax
subl %ebx, %eax
# {
# ejectPilot();
movl $ejectPilot, %ecx
js *%ecx
# }
# if (pilotWantsEject)
movl $pilotWantsEject, %eax
movl (%eax), %ebx
movl $0, %eax
subl %eax, %ebx
movl $skipJumpEject, %eax
jz *%eax
# {
movl $ejectPilot, %eax
jmp *%eax
#}
skipJumpEject:
# Whatever comes after your code
#}
101
...dangit.
Subroutines
The technical name for a block of code we can use and
reuse is a subroutine. There were a few subroutines
in disguise in the previous section (“ejectPilot”
and “testEject”). Additionally, there were a few
things we didn’t go over. The first of these is
that, if it’s meant to be used in multiple places,
the subroutine will need to be told where to return
to. For instance, suppose we’re writing a subroutine
to change the angle of the ailerons.
102
...wait, how did you escape the zombies?
103
address is returned to, we should then go on to do
other things (like check the plane temperature),
confident that the ailerons have been moved.
ourReturnAddress:
movl %planeTemperature, %ebx
#...
Nevermind.
104
.text
# void angleAileron(int Angle){
angleAileron:
movl $angleAileronArgumentAngle, %eax
movl (%eax), %ebx
# ... a whole bunch of code
And at the end, it will need to return.
jmp *%edx
105
If angleAileron sees a lot of use, you will only have
one copy.
Stacks
Older languages (FORTRAN) actually stored their
arguments in variables. This works well enough,
until you have multiprocessor machines, which can
have two processors working on the same memory at the
same time: if they ever run the same subroutine,
they will trash each other’s data. Recursive
subroutines also break when using variables for
arguments: recursion is where a subroutine calls
itself, and is extremely useful for handling trees,
but if a subroutine calls itself (and arguments are
passed in variables), it will trash its own variables
in the process.
So, modern programming languages use a calling
convention that relies on a stack. When the program
first starts, it carves out a substantial section of
memory for its own use, and stores the first location
in a register (on x86 it is esp, the “extended stack
pointer”). Instead of saying the argument lives at
a specific address ($angleAileronArgumentAngle is a
specific number, such as 828), they say it lives some
number of bytes away.
106
Using a stack allows you to have multiple processors
(each with their own registers, including esp) using
the same memory.
107
.text
# our main code
#//move esp back 8 bytes
movl $-8, %eax
addl %eax, %esp
# angle = 15;
movl $4, %eax
addl %esp, %eax
movl $15, %ebx
movl %ebx, (%eax)
The rest of the call remains the same (save the
return address, jump to the subroutine). The only
other thing that’s different is that, when the main
code is returned to, it needs to restore its stack
pointer (esp).
movl $8, %eax
addl %eax, %esp
108
Challenges
1) If you have two 32-bit boolean variables (varA
and varB), see if you can write some assembly that
calculates (varA | varB).
109
110
Chapter 8
Text
Our computer is built to handle bits. It happens
that there is a relatively straightforward way
to approach bits as integers. However, text
is a different beast. There’s not a direct,
systematic way to go between bits and letters: a
bit distinguishes between two possibilities, but the
choice of which option corresponds to 1 and which to
0 is fairly arbitrary.
That said, this book is written in English, so I
assume you can read English. In this day and age, if
you’re programming for English, you’re using ASCII:
ASCII is a code that equates numbers to characters.
So, if you see the number 70, you can look up what
that is in ASCII (capital F), while the number 117
corresponds to a different character (lowercase u).
111
What starts with “F” and ends with “uck”?
112
It’s easy to understand why the program caught fire:
take a close look at the gears.
# putchar(‘H’);
movl $72, %eax
movl $retaddr1, %edx
movl $putchar, %ebx
jmp *%ebx
retaddr1:
113
# putchar(‘e’);
movl $101, %eax
movl $retaddr2, %edx
movl $putchar, %ebx
jmp *%ebx
retaddr2:
# putchar(‘l’);
movl $108, %eax
movl $retaddr3, %edx
movl $putchar, %ebx
jmp *%ebx
retaddr3:
#// and 9 more
Loops
Writing the same code thirteen times is a little
ridiculous: it gets even worse if you’re using
longer strings of text. What would be nice is if
we could write one small piece of code that can write
the whole string. One possible way to do this is,
first, to store the text as data instead of putting
it in the code.
.data
# char* textHW = “Hello, World!”;
textHW:
.long 72, 101, 108, 108, 111, 44, 32
.long 87, 111, 114, 108, 100, 33, 0
(Two .long statements do the same thing as a single
.long statement with all the values). Our code might
first get the location of our data.
114
loopTest:
movl (%ecx), %eax
movl $0, %ebx
subl %ebx, %eax
movl $loopEnd, $ebx
jz *%ebx
If the current character is zero, we skip to the
end. Otherwise, we go on to the next code (printing
a character; remember that eax still holds the
character).
movl $retaddr1, %edx
movl $putchar, %ebx
jmp *%ebx
retaddr1:
And after we print, we need to move to the next
character (4 bytes ahead) and repeat.
movl $4, %ebx
addl %ebx, %ecx
movl $loopTest, %ebx
jmp *%ebx
Putting it all together, we get:
.data
# char* textHW = “Hello, World!”;
textHW:
.long 72, 101, 108, 108, 111, 44, 32
.long 87, 111, 114, 108, 100, 33, 0
.text
# char* curCharAddr = textHW;
movl $textHW, %ecx
loopTest:
# while(*curCharAddr)
movl (%ecx), %eax
movl $0, %ebx
subl %ebx, %eax
movl $loopEnd, $ebx
jz *%ebx
#{
# putchar(*curCharAddr);
movl $retaddr1, %edx
movl $putchar, %ebx
115
jmp *%ebx
retaddr1:
# curCharAddr = curCharAddr + 1;
movl $4, %ebx
addl %ebx, %ecx
#}
movl $loopTest, %ebx
jmp *%ebx
loopEnd:
#//The rest of your code.
116
Free Malloc
There is one last thing we need to cover before we
can stop banging our heads on assembly. So far,
we’ve been baking our text into the program itself:
while this approach can be made to work with variable
input, it does require that we know the size ahead of
time. This is not always the case: as an example,
let’s say you’re worried about spies reading the
messages you send out, and want to obscure what
you’re writing.
Jcpfu wr!
117
I came, I saw, I ran.
118
csaPlainText (at location 44) holds the location of
the plaintext (128). Our cyphertext will live at
location 249, which we got from... somewhere.
119
Exactly how this subroutine got the memory in the
first place varies from system to system.
120
With the number of bytes we need, we can call the
malloc subroutine.
movl $malloc, %ebx
movl $retaddr1, %edx
jmp *%ebx
retaddr1:
This will give us an address we can store the shifted
characters at (in eax). The next thing to do is
initialize: get the number of characters (into ebx),
the rotation (ecx) and the original text (edx). It
would also be a good idea to store a copy of the
address in eax (we’ll use edi).
movl %eax, %edi
movl $csaNumChars, %ebx
movl (%ebx), %ebx
movl $csaRotation, %ecx
movl (%ecx), %ecx
movl $csaPlainText, %edx
movl (%edx), %edx
Then comes the first part of the loop: testing. We
need to see if the number of characters (remaining)
is zero.
loopTest:
movl $0, %esi
subl %ebx, %esi
movl $loopEnd, %esi
jz *%esi
Then comes the part of the loop that actually does
something, the body. We need to get the current
character, shift it, and store it.
movl (%edx), %esi
addl %ecx, %esi
movl %esi, (%eax)
Then comes the last part of the loop: the update.
We need to move the two character addresses and
decrease the number of characters to move.
121
movl $4, %esi
addl %esi, %eax
addl %esi, %edx
movl $-1, %esi
addl $esi, %ebx
After the update, we need to go back to the test.
movl $loopTest, %esi
jmp *%esi
loopEnd:
And finally, after the loop, we should restore the
address of the original text (into eax) and the
address of the shifted text (into ebx).
movl $csaPlainText, %eax
movl (%eax), %eax
movl %edi, %ebx
When we return, the calling code will have access to
both the original text and the cypher text.
Since we carved out memory for the cypher text, at
some point the calling code will have to surrender
it using another subroutine (called free). If it
didn’t, over time the program would use more and more
memory each time it called malloc (malloc won’t use
a reserved byte, but calling free un-reserves the
bytes). Eventually, the computer would run out of
unreserved memory, and the program would crash (this
is just one of the causes of random crashing).
122
Challenges
1) Look up an ASCII code table. Figure out what
numbers are used to represent “I am 1337.”.
123
124
Chapter 9
125
We hope and pray somebody else wrote the compiler.
126
If you wanted to do strange things with addresses,
you either worked directly in assembly, or you
waited until 1972, when Dennis Ritchie created the
C programming language. C’s fundamental type, the
pointer, is simply an address. One of the main draws
of C is that it works at the computer’s level: it’s
fairly easy to see how the constructs of C might
translate to assembly. In fact, I’ve been marking
the assembly examples with the C code that might have
produced them: you might go back and review after
reading this chapter.
This chapter will cover C, but it will be very
fast. If this is your first crack at C, you might
want to go find some additional resources. If this
is your first crack at programming at all, even more
so (also, well done making it this far).
Hello World
If you are following along, this is the first part
of the book where I expect you to actually run your
code. This is because, for a new programmer, getting
your tools set up is often the most frustrating part
of the experience: once you get “Hello World” or
an equivalent running, you can add small steps to
build to a working design. The tools for working
with assembly are (usually) very arcane, byzantine
and draconian: the tools for the C programming
language aren’t easy, but they’re easy enough for
you to figure out on your own. However, if you have
a friend who has used C before, solicit their help.
If you are in a class, and you’ve gotten C working
before, offer to assist others.
Your first challenge is to download a compiler for
C++ (a C++ compiler will handle C, and we’ll use C++
later: I suggest the GNU G++ compiler) and use it
to run the “Hello World” program. In the previous
chapter, we went over how to program it in assembly.
Luckily, it’s substantially shorter in C. Open up
a text editor, write the following, and save it as
“hello.cpp” (and remember which folder you saved it
in).
127
#include <stdio.h>
int main(){
printf(“%s”, “Hello World!\n”);
return 0;
}
128
It seems you owe us ninety bucks.
#include <stdio.h>
int aGlobalIntegerVariable;
int main(){
return 0;
}
129
“main” tells the compiler where to start running
code. If we want to do arithmetic (for instance,
adding up a debtor’s balance), we can use some
operators baked into the language (+-*/ for
numerics, &|! for Booleans, and > < >= <= == != for
comparisons). We can then calculate and store the
balance with the following.
#include <stdio.h>
int balance;
int main(){
balance = -250 - 50 + 275;
return 0;
}
#include <stdio.h>
int balance;
int transaction1 = -250;
int transaction2 = -50;
int transaction3 = 275;
int main(){
balance = transaction1 + transaction2;
balance = balance + transaction3;
return 0;
}
130
type of data the function returns (“angleAileron”
doesn’t report anything, so a special marker, void,
would be used). So, if you wanted to define an angle
aileron function, you would start by writing:
void testEject(
int planeTemperature,
bool pilotWantsEject
){
if((planeTemperature > 200) || pilotWantsEject){
eject();
}
}
131
to zero, skip the code. In this example, if the
plane temperature is less than (or equal to) 200, and
the pilot does not want to eject, the call to “eject”
is skipped (“movl $ifend, %eax”, “jz *%eax”).
#include <stdio.h>
int main(){
const char* toPrint = “Hello World!”;
//TODO
}
132
#include <stdio.h>
int main(){
const char* toPrint = “Hello World!”;
int curNumber = 0;
char curLetter = *(toPrint + curNumber);
while(curLetter){
putchar(curLetter);
curNumber = curNumber + 1;
curLetter = *(toPrint + curNumber);
}
return 0;
}
133
#include <stdio.h>
#include <stdlib.h>
char* caesarShift(
const char* plainText,
int numChars,
int rotation
),{
char* cypherText = malloc(numChars+1);
int curNumber = 0;
while( *(plainText + curNumber) ){
char curChar = *(plainText + curNumber);
char rotated = curChar + rotation;
*(cypherText + curNumber) = rotated;
curNumber = curNumber + 1;
}
*(cypherText + numChars - 1) = 0;
return cypherText;
}
int main(){
const char* message = “Hello World!”;
char* garbled = caesarShift(message, 12, 2);
printf(“%s”, message);
printf(“%s”, “\nbecame\n”);
printf(“%s”, garbled);
free(message);
return 0;
}
134
Hello World
#include <stdio.h>
int main(){
printf(“%s”, “Hello World!\n”);
return 0;
}
135
Challenges
1) Get a compiler installed on your system, and run
“Hello World”.
136
Chapter 10
How to program in an
organized manner
Linking
The C compiler reads one file and turns the
statements inside it into assembly (and/or machine
code). If there are subroutines, it will find a
space to put them, and the same goes for variables.
However, if you look carefully at the “Hello World”
example, you might notice something: we use a
subroutine called “printf”, but we never actually
provide code for “printf”. “printf” is a fairly
complicated subroutine, so the code has to be
provided somewhere, yet our code doesn’t have it.
However, the C compiler you installed comes with
a whole collection of library subroutines: in most
cases (including the GNU compiler), this library is
pre-compiled. In order to compile and collect a C
file, the GNU compiler works in three passes. The
second (yes, second) pass produces an object file:
it takes all the information in the starting file
and produces assembly (with a few missing pieces of
information): this is the actual compile step. The
way C is designed, the only information needed about
things in other files is the address they wind up at:
137
so, the third pass collects multiple object files and
fills in all of the missing addresses (linking).
138
information, we could write (at the top of the file)
declarations for functions we’re going to use but are
defined elsewhere.
int main(){
printf(“%s”, “Hello World!\n”);
return 0;
}
139
Fill in the other blanks.
140
programs up into manageable pieces: put your
function declarations into one or more header (“.h”)
files, and you can split the code into as many source
(“.cpp”) files as you see fit. Just remember, your
own header files should be referenced inside quotes
(#include “yourlib.h”) and standard library headers
should be in angle brackets (#include <stdlib.h>).
Structs
Up to now, we’ve been dealing with base types (char,
int, double) and pointers to those base types (char*,
int*, double*). This is fine if our program only
deals with simple concepts. However, suppose we
wanted to write a program for a self-driving car.
141
Until you are sick of it.
void planRoute(
double* location,
double* rotation,
int gasLevel,
double** otherDrivers,
double** roadPoints,
int** roadTris,
char* pleaseNoMore
);
142
typedef struct{
double x;
double y;
double z;
double orientation;
int gasLevel;
bool killAllHumans;
} CarState;
143
#include “cardecs.h”
#include <stdio.h>
int main(){
CarState mycar;
mycar.killAllHumans = false;
if(mycar.killAllHumans){
printf(“%s”, “Make your time.”);
}
return 0;
}
#include “cardecs.h”
#include <stdio.h>
void moveCar(CarState* toMove){
toMove->x = toMove->x + 0.5;
}
int main(){
CarState mycar;
mycar.x = 0.0;
CarState* mycarptr = &mycar;
moveCar(mycarptr);
return 0;
}
144
You’re all DOOMED!
145
Happy trees.
typedef struct{
char stuff;
} Enemy;
typedef struct{
int moreStuff;
} Player;
typedef struct{
double yetMoreStufff;
} PowerUp;
146
Enemy** allEnemies;
Player** allPlayers;
PowerUp** allPowers;
void updateState(){
Enemy** curEnemy = allEnemies;
while(*curEnemy){
Enemy* toUpdate = *curEnemy;
enemyUpdateFunc(toUpdate);
curEnemy = curEnemy + 1;
}
Player** curPlayer = allPlayers;
while(*curPlayer){
Player* toUpdate = *curPlayer;
playerUpdateFunc(toUpdate);
curPlayer = curPlayer + 1;
}
PowerUp** curPower = allPowers;
while(*curPower){
PowerUp* toUpdate = *curPower;
powerUpdateFunc(toUpdate);
curPower = curPower + 1;
}
}
147
typedef struct{
void* myupdatefunc;
char stuff;
} Enemy;
typedef struct{
void* myupdatefunc;
int moreStuff;
} Player;
typedef struct{
void* myupdatefunc;
double yetMoreStufff;
} PowerUp;
class GameObject{
public:
virtual void myUpdateFunc() = 0;
};
148
what a specific GameObject can do. The keywords
“public” and “virtual” can, for now, be ignored (just
include them in your code).
The actual things in the game (Player, Enemy,
PowerUp) can be declared (in a header) as:
void Enemy::myUpdateFunc(){
stuff = ‘K’;
}
void Player::myUpdateFunc(){
moreStuff = getJoystick();
}
void PowerUp::myUpdateFunc(){
yetMoreStuff = yetMoreStuff + 0.1;
}
149
Indirection and abstraction are the cause of (and
solution to) all the problems in computer science.
GameObject** allItems;
void updateState(){
GameObject** curItem = allItems;
while(*curItem){
GameObject* toUpdate = *curItem;
toUpdate->myUpdateFunc();
curItem = curItem + 1;
}
}
150
in the vtable, and calls it (using toUpdate as an
implicit argument). Each item has a different entry
in its vtable (“Enemy” stores the subroutine that
sets “stuff” to the letter K, while “Player” stores
the one that reads the joystick), so each item will
run the appropriate subroutine.
151
Challenges
1) It’s also possible to declare a variable without
defining it (i.e. carving out memory for it).
Look up the “extern” keyword, and try to figure
out the difference between “int curCount;” and
“extern int curCount;”.
152
Chapter 11
How to program a
compiler
Compiler Overview
So far, we’ve been assuming somebody else has written
a compiler for us. For most systems, for most
people, this is a good assumption. However, far too
many programmers take this as an excuse to not learn
about it, claiming compiler writing is black magic.
In actuality, if you know how to program, writing a
functional compiler is straightforward (if tedious):
writing an optimizing compiler IS black magic, but I
won’t be covering that here.
A compiler typically runs in several passes:
lexing, parsing, code generation, assembly and
linking.
153
Black magic is for offense, and I find this
offensive.
154
pop pop push.
Lexing
The first pass in a compiler is to lex the input. A
program file is stored as a long stream of bytes. We
read in the file one byte at a time. However, while
ASCII characters are single bytes, our numbers might
have more than one digit. We need some way to break
the file into usable pieces and figure out what the
pieces are (in this case, the pieces can be numbers
and the plus sign).
So, regardless of how we got our program into
memory:
155
bool isWhitespace(char toTest){
bool isTab = (toTest == 9);
bool isNewline = (toTest == 10);
bool isSpace = (toTest == 32);
return isTab || isNewline || isSpace;
}
const char* lexSkipWhitespace(const char* theProg){
const char* curStart = theProg;
while(isWhitespace(*curStart)){
curStart = curStart + 1;
}
return curStart;
}
Interpreting
The next thing to do is add a parsing phase, where
the relation of each token to everything else is
figured out. Parsing can be a very complicated
affair (recursion is almost always required);
however, this is a simple enough language that we can
parse and produce code at the same time.
Before we produce code, however, we might start
by writing a simpler program: an interpreter. An
interpreter reads in a program and “simulates” the
effects of that program: it can be helpful to write
156
an interpreter to get a feel for what the code the
compiler produces must do.
while(*ourProg){
...
}
while(*ourProg){
if(isWhitespace(*ourProg)){
ourProg = lexSkipWhitespace(ourProg);
}
...
}
157
while(*ourProg){
if(isWhitespace(*ourProg)){
ourProg = lexSkipWhitespace(ourProg);
}
else if(*ourProg == 43){
int valA = *theStack;
theStack = theStack - 1;
int valB = *theStack;
*theStack = valA + valB;
ourProg = ourProg + 1;
}
...
}
while(*ourProg){
if(isWhitespace(*ourProg)){
ourProg = lexSkipWhitespace(ourProg);
}
else if(*ourProg == 43){
int valA = *theStack;
theStack = theStack - 1;
int valB = *theStack;
*theStack = valA + valB;
ourProg = ourProg + 1;
}
else if(isDigit(*ourProg)){
int toPush = atoi(ourProg);
ourProg = lexFindNumberEnd(ourProg);
theStack = theStack + 1;
*theStack = toPush;
}
}
158
Code Generation
The problem with an interpreter is, if we want to
run our program, we need our interpreter (it also
tends to be slower, since an interpreter has to do a
lot of parsing stuff while running the program). If
we want an actual compiler, we’ll have to generate
assembly code that accomplishes what we want. To
keep it simple, we’ll just print our assembly to the
standard output.
This pass usually has some boilerplate code that
will always be generated: for instance, code to set
up our stack.
while(*ourProg){
...
}
while(*ourProg){
if(isWhitespace(*ourProg)){
ourProg = lexSkipWhitespace(ourProg);
}
...
}
159
while(*ourProg){
if(isWhitespace(*ourProg)){
ourProg = lexSkipWhitespace(ourProg);
}
else if(*ourProg == 43){
//get value
printf(“%s\n”, “movl (%edx), %eax”);
//move stack
printf(“%s\n”, “movl $-4, %ebx”);
printf(“%s\n”, “addl %ebx, %edx”);
//get value
printf(“%s\n”, “movl (%edx), %ebx”);
//add
printf(“%s\n”, “addl %ebx, %eax”);
//store
printf(“%s\n”, “movl %eax, (%edx)”);
//go to next character
ourProg = ourProg + 1;
}
...
}
160
while(*ourProg){
if(isWhitespace(*ourProg)){
ourProg = lexSkipWhitespace(ourProg);
}
else if(*ourProg == 43){
printf(“%s\n”, “movl (%edx), %eax”);
printf(“%s\n”, “movl $-4, %ebx”);
printf(“%s\n”, “addl %ebx, %edx”);
printf(“%s\n”, “movl (%edx), %ebx”);
printf(“%s\n”, “addl %ebx, %eax”);
printf(“%s\n”, “movl %eax, (%edx)”);
ourProg = ourProg + 1;
}
else if(isDigit(*ourProg)){
//lex and parse the number
int toPush = atoi(ourProg);
ourProg = lexFindNumberEnd(ourProg);
//move the stack
printf(“%s\n”, “movl $4, %ebx”);
printf(“%s\n”, “addl %ebx, %edx”);
//put the number on the stack
printf(“movl $%d, %%eax\n”, toPush);
printf(“%s\n”, “movl %eax, (%edx)”);
}
}
161
Challenges
1) The example bakes the text to compile into the
program. See if you can write some code to read in
the text to compile from standard input (you might
look up the “gets” subroutine).
162
Part II
How to computer
science
163
Chapter 12
How to dope
Disks
The basic idea of a transistor is relatively simple:
mix some group 5 elements into one region of a
silicon crystal, and bracket it by two regions
of silicon doped with group 3 elements. Slap an
insulator over the middle, slap some metal on that,
and attach some wires.
165
If you have a bulk source of transistors, you can
then wire them up. Conceptually, this is a great
way to view a computer, and historically computers
actually were built with this “transistor-transistor
logic”. However, it’s a terrible way to actually
build a computer: the resulting circuit will be a
bulky rat’s nest of wires.
Modern processors fit into a one inch square,
and contain an insane number of transistors; to do
this, you need some fairly complicated industrial
processes. The whole process starts by getting some
silicon; since silicon is one of the more common
elements (on earth at least), this should be easy.
However, most of that silicon is bound to oxygen in
silicon dioxide (sand and glass).
166
Purer, if possible.
Unclean, unclean.
167
De-crystallization
168
The heating element is called phlogiston.
Photolithography
Building all these transistors in one square inch
requires producing very small transistors: at
the time of writing this, common processors have
transistors 14 nanometers wide (140 atoms across).
You aren’t doing that by hand: forget your twitching
muscles, your heart pumping blood makes your hand
twitch (and I doubt you want to stop your heart for
the amount of time you’d need to do this). We need
some way to automate the process; since many of the
individual steps in automating the process will alter
everything they touch, we need some way to limit what
can be touched.
This is achieved by coating the surface of the
disk with a special chemical. This chemical needs
to be sensitive to light: being exposed to light
should make it harden up and stick to the surface.
169
A lot of polymerization reactions are initiated by
light (specifically, light produces radicals, which
then produce a chain reaction), so those are a fairly
common choice.
170
Woof.
Guard dog.
Ion Implantation
Now we need to dope (i.e. add impurities to) the
silicon. However, we can’t just melt the silicon and
mix the impurities in: we’ve only got 140 atoms to
171
work with, and melting is relatively indiscriminate.
Our solution: indiscriminate gunfire.
172
Generate ions, run them through a voltage difference,
and you have an ion gun.
173
This sucks.
174
Transistors and Wires
With the processes above, it’s possible to build an
integrated circuit on a chip. To do so, you just
need to do a whole bunch of steps. For example, you
might coat, implant, grind, coat, implant, grind,
coat, deposit insulator, grind, coat, deposit metal,
grind, coat, deposit insulator, grind, coat, deposit
metal and grind.
Anyway, once you’ve finished with a disk, you just
need to prepare it for its final application. You
probably need to cut it (it’s more economical to
make many processors at once), add some wires (so
you can hook up your chip to something else) and add
a coating to protect the chip. Finally, you need
to come up with some way to market your processor
(advertising is outside the scope of this book).
175
Challenges
1) There are other methods you can use to purify a
material. Look up a few, and discuss how applicable
they would be to chip manufacture.
176
Chapter 13
How to data
Arrays
Up to now, all of our programs have been pretty
direct: we haven’t done much to organize our data,
mostly because it came to us in a usable form.
However, if you write larger programs, or smaller
programs with a tricky setup, it can be helpful
to know a few basic ways to organize your data.
Additionally, it’s possible to write a code library
for these common structures: you can write it once,
do it right, and use that same code over and over
(in fact, many languages give you implementations of
these in their standard library).
The first data structure we’ll go over is the
array: an array is a collection of elements. You
can get an element by an index, or you can set an
element. We’ve seen this before, though I didn’t
give it the name “array”. RAM is an array of bytes,
and if you use C’s malloc to get memory, you’ve
created an array.
177
A Ray that everyone can love.
178
array (or things that look like arrays) and doing
stuff to each item is a stupidly common operation.
The following is the basic code for doing so: commit
it to memory.
char* theArray;
int length;
...
int i = 0;
while(i < length){
curVal = theArray[i];
//other stuff
i = i + 1;
}
179
Lists
In addition to arrays, there is a related data
structure called a list. A list can get an element
at an index, or set an element at an index (exactly
like an array). However, you can also add and remove
items from the list (an array has its size fixed when
it’s made); this is useful if you don’t know ahead
of time how much data you will be working with (i.e.
reading in data from a file).
A list can do a lot of things, so most language
designers don’t bake it into their language: they
provide a class to do the job in their standard
library. There is a non-obvious question if we do
this: how do we store the data in the list? The
most natural option is as an array: you have a class
that contains an array, get and set just use the
array’s get and set, and add/remove make a new array
and copy over the data.
180
along with a pointer to the next structure.
181
one of the uses of classes. You can have multiple
implementations of a list (i.e. ArrayList and
LinkedList), and if both are subclasses of List,
then you can swap out one for the other with minimal
issue.
Maps
The next item of interest is the associative array
(also known as the Map). The way these work is
you give the map an item, and it will tell you what
is associated with that item. If you’ve ever used
a phone book or building directory, you’ve used a
“map”.
Perhaps the most straightforward way to store a
map is as a pair of lists. As an example, suppose
you’re making a menu: you have the names of the
dishes (the “keys” of this map) in one list, and the
corresponding prices (the “values”) in another.
182
be shown in the next chapter (using trees).
Trees
A tree is a hierarchy: at the top of a business, you
have the CEO, then upper management, then management,
then the peons.
183
Trees aren’t common, but they aren’t exactly rare
either.
class Node{
public:
void* data;
List childNodes;
}
184
The order we look at these people.
185
the natural approach. In most other cases, recursion
is probably not the right option.
Graphs
Rounding out the set of data structures is the graph.
Programmers have some weird naming conventions:
their word for dictionary is “Map”, and their
word for map is “Graph” (not to be confused with a
depiction of data).
A graph is a collection of nodes, and connections
between them. The simplest example is a road map:
you have cities (nodes) and roads between the cities
(connections).
class Node{
public:
void* data;
List linkedNodes;
}
186
The most common way to loop through all nodes is
something called breadth first search: you start
at a node, and note where its links point (ignoring
nodes you’ve already been through). This can be done
with two lists: one for the nodes that have been
spotted but not examined, and one for the nodes that
have been examined.
List openNodes;
List closedNodes;
while(openNodes.length() > 0){
Node* curNode = openNodes.get(0);
openNodes.remove(0);
//do something with the current node
List* links = &curNode.linkedNodes;
int i = 0;
while(i < links.length()){
Node* linkNode = links.get(i);
bool handled = contains(&closedNodes, linkNode);
bool spotted = contains(&openNodes, linkNode);
if( !(handled || spotted) ){
openNodes.add(linkNode, 0);
}
i = i + 1;
}
}
While there are still nodes you have seen but not
examined (“while(openNodes.size() > 0)”), get one of
those nodes, do something with it, and run through
its links (you REALLY need to memorize how to loop
through a list). If those linked nodes have not
already been seen (“!(linkHandled || linkSpotted)”),
add them to the list of nodes to handle.
We need all this extra code to make sure we only
hit each node once: in the worst case, it’s possible
for us to get stuck forever in a loop.
187
There are three loops in this graph that we could get
stuck in.
188
Challenges
1) Your preferred language probably provides a
standard implementation of a list and a map. Look
them up, and try to determine how they work (wrapper
around pointer/array, linked list, or something
else). If your language has a tree and graph type,
examine those as well.
189
190
Chapter 14
How to algorithm
Sorting
We now have a few canned answers to the question of
how we store data. It turns out there are a few
canned answers to how we can manipulate the data;
these canned answers show up everywhere (in some form
or another), so it’s worth at least knowing about
them (especially since many programming languages
provide implementations for you).
One of the most useful is rearranging a list of
items into a preferred order (i.e. sorting a list).
For instance, let’s say we’re going into the used car
business.
191
most expensive car on the lot; that requires knowing
which car is the most expensive. If our program has
a list of cars on the lot, we need to sort that list:
we can’t just find the largest, since the customer
probably won’t buy the first thing we show them.
There are many ways to go about this: one of my
favorites is called “bubble sort”. In bubble sort,
you run through the list seeing if everything is in
order: if not, you swap the two items that are out
of order and run through the list again. You keep
running through the list until it’s sorted.
List toSort;
bool loopAgain = true;
while(loopAgain){
loopAgain = false;
int i = 1;
while(i < toSort.size()){
Car* carA = toSort.get(i-1);
Car* carB = toSort.get(i);
if(carA->cost < carB->cost){
toSort.set(i-1, carB);
toSort.set(i, carA);
loopAgain = true;
}
}
}
192
Bubblesort, parking lot edition.
Violence discount?
193
Assymptotic Dominance
We have a canned subroutine (an algorithm) called
bubble sort. An important question to ask is how
long it takes to run: we’d like to get an answer
while the customer is in our office.
194
while loop N times: if the most expensive car is at
the end of the list, we have to run N times for it to
migrate to the start of the list.
Better Sorting
Normally, for an algorithm that has to touch
everything, the best you can possibly do is O(N )
(doubling input doubles time). For sorting N items
by comparison, we can’t quite get there, but we can
do better than O(N 2 ). One possible option is called
“mergesort”.
Mergesort hinges on two facts: first, merging two
sorted lists is an O(N ) operation. If you have two
lists of cars, each with their most expensive one
first, you only have to look at two cars to find the
most expensive.
195
Which is bigger: 8000 or 7000?
196
Become three lists of two.
197
Anybody still use these things?
When you build the map, you can sort the list of
names (making the map will be at least O(N ), we’re
trying to make searching faster). Then, you can
build a tree: take the middle-most name (probably
beginning with M or N), and put every name that comes
before it on one side, and every name that comes
after on the other.
198
If you then repeat this process at each level, you
have a tree of names (and contact info).
199
Quark is strange.
Dijkstra
Let’s say you have a road map (a graph), and you want
to know the shortest route from Cheyenne Mountain to
Roswell.
200
For no particular reason.
201
From Cheyenne Mountain, our direct options are Pueblo
and Alamosa.
202
Going to Pueblo, then Alamosa, is shorter than just
going to Alamosa (the actual roads curve).
203
Nothing to see here. Move along.
204
Challenges
1) There are many sorting algorithms. Look up a
few, and pick one to code up (my absolute favorite
is bogosort).
205
206
Chapter 15
Non-volatile Storage
Inside the processor itself, we use flip-flops to
implement registers. While bulk memory could be
built this way, most modern processors use a cheaper
option (DRAM). However, both options are ephemeral:
there is no such thing as a perfect insulator,
so any electrical system you build is going to
leak. Consequently, any system that stores data in
electrical voltage is going to lose that data if the
power goes out. How quickly it leaks varies, but it
will eventually fade away.
This is a real issue if any of your data is
important.
207
“OperatorManual.txt” not found.
208
want to load data into memory before actually doing
anything. This is also why we don’t just use the
long term storage as main memory.
209
Pick your track by moving the read head.
210
Naming Conventions and Inodes
So, we have a bunch of tracks, which contain a bunch
of sectors, which contain a bunch of data. We’ve
filled all that storage, shut down the computer, and
then turned it back on. You’re computer sees a large
amount of data, but unless you left some breadcrumbs
to follow when you return, it will just look like
noise.
211
Each sector holds a large number (4096) of bytes.
212
Weird blackmail is always the most interesting.
Streams
There’s a whole lot to keep track of when playing
with file systems. For instance, many file systems
213
allow you to split the data of a file into multiple
locations (put the first 4k here, and the last 12k
there). This allows you to make full use of the
hard drive: if you don’t allow this, a few small
files placed in key locations can prevent people from
writing an intact large file.
214
If you can split a file, the only thing that matters
is how many blocks are unused.
215
File Structure
When you write a program that writes to the disk,
you get to specify exactly what gets written: the
computer just treats it as a stream of bytes. If you
want this information to be useful when you read it
back in, you’re probably going to add some structure
to it. This begs the question, how do you organize
it?
For simple data, having a global structure tends to
be sufficient. For example, if you have a table of
numbers, you might write the width, then the height,
then all the data, row-by-row, column by column.
You might look up the csv file format for stuff like
this.
216
There’s more than one way to organize data: it’s up
to you to pick the best for your application.
217
Either way can be made to work, but forgetting to
swap for different processors creates weird bugs.
Compression
So, you’ve come up with a structure for your files,
you’ve run a simple example, and you realize you’re
writing huge files. Assuming you’ve organized your
file structure the best you can, how do you fix this?
Well, files can be large for two reasons. The first
is simply being disorganized: adding boilerplate
information and needlessly repeating things.
218
a program to fix: these are called lossless
compression algorithms. Most of these algorithms
are based on finding and noting repeated sequences.
For example, instead of using 4300 bytes to store
100 repetitions, you can use 47: 43 for the text to
repeat, and 4 for the number of repetitions.
Heeeeere’s Johnny.
219
algorithms typically have to be purpose built for the
type of data you’re compressing: a general purpose
algorithm doesn’t know what’s important and what’s
not.
220
Challenges
1) There are several different types of file system,
including FAT, NTFS and HFS. Look up a few, and
compare and contrast them.
221
222
Chapter 16
Walkie-Talkie
Our goal in this chapter is to get computers across
the globe to share data with each other. That starts
with getting two computers in the same room to share
data.
We store and convey bits as voltage through a wire,
so the first option is to run a wire between the two
computers. If you do this, you need some way to
mark the start and end of a bit (the two computers
probably don’t have the same clock speed, and if they
do, they probably aren’t synchronized): this quickly
becomes an exercise in analog circuitry. However,
at the end of the day, connecting two computers is
basically running a wire between them.
223
No surprises so far.
224
Our computers also have to handle more individual
connections.
225
As the Brits call it, a rooter.
226
Postcards
We have our router copying messages from one computer
to another: however, what happens if we want to send
a message to a computer that is not in the same room?
Well, this computer will not be attached to our
router; however, if you’ve paid your internet bill,
your router will be connected to another router.
227
The first hop.
228
The worst link limits your communication.
229
The contents of the message, like file formats,
depend on the requirements of the communicating
programs.
Phonecalls
The Internet Protocol is a very basic protocol:
there are many people involved, some of which
are malicious, so IP is very fault tolerant.
Unfortunately, this means you, the programmer, get
very few guarantees. If you send a message, any
of the routers between you and the destination can
decide to not send the message.
In addition, your internet service provider could
be doing maintenance: when you send one packet, it
goes one way, while the next packet gets sent another
direction. This means that a packet you sent later
could get to the destination earlier.
230
You might think this is unlikely, until you remember
exactly how many people are involved.
231
Are we there yet? Are we there yet? Are we there
yet?
232
One computer can provide multiple programs/services
(in this case, file transfers and http pages).
233
An entirely accurate phone book for the internet.
Fodszqujpo
So, now we’ve connected our computer to every other
computer on the internet. We are very trusting
people: we may not be very smart.
So, there are (at least) two ways connecting to
random computers can cause us trouble. The first is
if we blindly follow instructions from the internet:
it’s usually a very bad idea to let people you don’t
trust give you code to run. If you’re writing a
program that takes user input over the internet, do
not let them run code on your machine (at least, not
without very careful design).
This sounds easier than it is: as an example,
when your internet browser opens a website, it
makes a connection with a server and reads in an
HTML page. That HTML page describes the contents
234
of the page (safe) and allows JavaScript code to
describe how the page changes in response to the
user (potentially very dangerous). If the people
programming your browser were not careful, running
that JavaScript could lead to nasty and, worse still,
subtle problems.
235
We got a lead.
236
comes up with two pieces of information, one used to
garble the message, and the other used to un-garble.
The server can give everyone and their dog the garble
key, and everyone and their dog can use that key to
send messages to the server. As long as the server
keeps the un-garble key secret, there should be no
problem.
237
Challenges
1) Your computer’s operating system provides routines
for TCP. Look up how to use them, and see if you
can write a simple server that prints whatever it
receives, and a client that sends “Hello, World!”.
238
Chapter 17
How to stimulate
A Bit of Calculus
Most simulations are trying to calculate how
something changes, either in space or in time.
The mathematical language of change is calculus.
Normally, this might constitute a problem (calculus
scares people for some reason). However, most of
the difficulties in calculus stem from trying to find
an exact answer. We’re working on the computer: we
don’t care about the exact answer, we just want good
enough.
Accordingly, we just need to understand the basics:
what is a derivative, and what is an integral. Both
of these operations work on something called a
function: a function takes one or more inputs, and
produces an output.
f (x, y) = x2 + sin(y)
You’ve seen this before: the programming term for
this is subroutine (and the C standard actually calls
its subroutines functions).
239
A useful question to ask about a function is: if
I change one of its inputs by some small amount, how
much does the function result change?
∆x → ∆f (x, y)
This is typically phrased as the “slope of the
tangent line”:
∆f (x, y)
∆x
and this can be approximated by
f (x + ∆x, y) − f (x, y)
∆x
∂f (x, y)
∂x
But, we’re not mathematicians: we don’t care about
being exact.
Moving Mountains
Derivatives are half of the calculus fun: the other
half is integrals. Suppose you want to put in a
football field, and you need to level a plot of land.
240
Proper American football.
241
20 foot drops don’t end well.
242
A standard football field is 160ft by 360ft (split
into twelve pieces is 30 feet per piece).
243
Then again, neither are known for being able to
football.
Internal Ballistics
For the pure programmers, you typically have a
differential equation dumped in your lap, which you
are then asked to solve. How that expression for the
derivative is obtained depends on who is giving it
to you. A problem I happen to like is solving for
the velocity of a pellet from an air rifle. This
system starts with a pellet in the chamber, with high
pressure air on one side, and atmospheric pressure
air on the other.
244
the time (t), the position (x) and the velocity (v).
Time just runs on like a river: the state of our
system is a function of time (we can ask what the
position is at time 2, but asking what the time is
at velocity 7 feels backwards). By definition, the
derivative of position is velocity (this is worth
remembering).
∆x
=v
∆t
Newton’s second law gives us an expression for the
derivative of velocity (acceleration) in relation to
the applied force (F) and the mass of the projectile
(m): this is also worth remembering.
∆v F
=
∆t m
At this point, the engineer (me) dumps an
expression for F into your lap (if you’re curious,
look up the definition of pressure, Boyle’s law, and
the area of a circle):
∆x(0) = ∆t ∗ v0
245
πR2 (P0 xx00 − Patm )
∆v(0) = ∆t ∗
m
We have the changes from time 0 to time 0.001: if
we want the values, we just need to add the starting
value.
x1 = x0 + ∆t ∗ v0
Hot Rod
We have a problem where the value varies in time:
however, we can also have a problem where the value
varies in space. As a simple example, let’s look at
a hot rod.
246
Who puts a blast furnace next to the freezer?
∆ ∆T
∆x −G(x)
=
∆x k
k is a constant of the material (steel conducts
heat faster than StyrofoamTM ). You’ll also notice
that we’ve got a derivative of a derivative: a
derivative measures change, and change can change
(e.g. acceleration).
Again, since we’re working approximately, we choose
some distance scale: let’s split our rod into five
pieces (∆x = 1/4th the length of the rod). Each
piece has its own temperature (Ti )
247
If that was a hot rod, this is a chop shop.
∆ ∆T
∆x
∆T
(x) − ∆T
∆x (x − ∆x)
= ∆x
∆x ∆x
Differencing again, this becomes:
T (x+∆x)−T (x)
∆ ∆T
∆x ∆x − T (x)−T∆x
(x−∆x)
=
∆x ∆x
This simplifies to
∆ ∆T
∆x Ti+1 − 2Ti + Ti−1
=
∆x ∆x2
There are three places we don’t know the
temperature: we can use our discretized derivative
to build three relations.
T2 − 2T1 + TH −G(x1 )
=
∆x2 k
T3 − 2T2 + T1 −G(x2 )
=
∆x2 k
248
TL − 2T3 + T2 −G(x3 )
2
=
∆x k
Three equations, with three unknowns, means we can
solve this system for the temperature in the rod.
To do this, use Gaussian elimination (I assume you
remember your algebra).
This problem setup is called a boundary value
problem (we know the value at the boundaries, what
is its value in the middle). This particular method
for solving these problems is called the finite
difference method (there are others).
249
Challenges
1) There’s a major logical problem with the air rifle
program. Can you spot it? (Here’s a hint: how fast
is the pellet moving once it leaves the muzzle?) How
would you fix it/cope with it?
250
Chapter 18
251
Vibrations are created by some disturbance in the
environment (somebody pounding on a drum, a rock
hitting the water, gunfire), and moves through the
environment at some speed (in freezing air, 331
meters per second, or 1087 feet per second). These
vibrations create temporary spikes in pressure, which
specialized hair cells in your ear respond to.
Those hairs responding triggers neurons, which in
turn signal that response to the brain, which somehow
figures out that sound is playing.
Listening
Our goal is to re-create a set of vibrations (or,
equivalently, pressures): our first challenge is
to figure out how to get the pressures we want to
play. We can generate them from code, although
sometimes you want to measure and store sounds from
the environment for later use (especially if the
sound is complex, like speech).
The device used to measure the sound is called
a microphone. There are several different types,
but all of them have a circuit where a vibrating
element produces different voltages. One design
relies on the piezoelectric effect: some crystals
(like quartz) will produce a voltage if subjected to
a mechanical stress (like vibration). If you hook
up such a crystal to a diaphragm (a big sheet that
reacts to vibrations), it’s possible to measure the
voltage produced by the crystal.
252
Plus some foam to cut back on popping (from wind and
spit).
253
interval, the difference between the discrete samples
and the original sound becomes imperceptible (i.e.
not important). The question is, how small is small
enough: to answer that, there are two pieces of
information we need.
First, sound tends to form a wave: waves are
described by amplitude (loudness) and frequency
(number of spikes per second).
Do the wave.
254
I didn’t say it’d be a good re-creation.
Playing Samples
So, now we know the sound we want to play, we just
have to play it. Many devices that produce sampled
audio have special circuitry (a sound card) that
will store some number of samples, and turn those
samples into voltages. The most complex item in
these cards is a digital to analog converter: these
are circuits that will take a digital signal (an
integer) and produce a corresponding voltage. There
are many approaches to this: a simple one starts by
generating a range of voltages, and using a bank of
gates and transistors to select one of them.
255
Thought we were done with this stuff, didn’t you?
256
This problem is known as real-time programming:
your program must do something within a time limit.
In particular, this is a soft real-time problem,
since nothing will blow up if you blow it.
257
Playing Notes
If you run the numbers, you will realize that storing
any significant amount of sound data will take a lot
of storage. Most modern systems have the memory for
it, but some specialized systems don’t, and it might
be worth freeing up memory anyway. There are other
ways to treat sound besides a collection of pressure
samples.
Most musical instruments produce sound via
resonance. The key sound generating structures on
these instruments allow certain frequencies to grow
and add to each other: this filters a random input
(like friction on a wire or breath over a pipe) so
that only the preferred frequencies come through.
258
Pictured: the extent of my knowledge of music
theory.
Fourier Series
There is a little bit of advanced math that comes
in damn handy when dealing with sounds. A Fourier
transform allows you to take sound data and determine
which frequencies are playing: a lot of filters and
effects rely on the Fourier transform.
259
Because bass is the only part that matters.
260
is all sines and cosines that cleanly repeat in P
seconds (i.e. repeat once, twice, thrice, etc...).
S(t) = a0
2π 2∗2π
+ a1 cos P t + a2 cos P t + ...
2π 2∗2π
+ b1 sin P t + b2 sin P t + ...
then you can split up your signal (i.e. determine
the coefficients) via
1 P
Z
a0 = S(t)dt
P 0
2 P
n ∗ 2π
Z
an>0 = S(t)cos t dt
P 0 P
2 P
n ∗ 2π
Z
bn = S(t)sin t dt
P 0 P
These are continuous integrals: however, we have
discrete data. Since we know how to do an integral
in a discrete manner (refer to the football example
in the previous chapter), we know how to do these
integrals (or we can at least figure it out). If
you know your trigonometry, you can then extract an
261
amplitude from pairs of sine and cosine (with the
same frequency).
262
Challenges
1) Go find some software that will let you record
and edit (sampled) sound data (there is probably free
software available). Install it, and record your
voice saying something silly.
263
264
Chapter 19
Pretty Lights
Before we put things on the screen, we need to know
what the screen is. There are a couple of different
types of displays in use: the most traditional is
the beta radiation king, the cathode ray tube. A
CRT has a source of electrons next to a large charge
collection: whenever electrons are loosed, they zoom
off towards the screen, where they hit phosphors that
in turn produce light.
265
A heated plate produces electrons, a voltage makes
them move, and magnetic fields (from the coils) bend
their path.
266
raster display, and is one of the most technically
straightforward displays to control from the
computer. If you’re using a typical hardware setup,
with memory separate from the CPU, you can add a chip
that scans through memory, and sends pixel commands
to a display.
Color
Our screens have a few limitations: the biggest is
that we only have a black and white (or black and
green) display, but we’d like to have color. It
turns out that your eyes are most sensitive to three
wavelengths of light, one of which registers as red,
another green, and a third blue.
Combinations of wavelengths stimulate multiple
flavors of cone at once, and your brain interprets
that as a secondary/tertiary color: so, red and
blue produce a purple/magenta hue. So, if we want
our display to work in color, one option is to add
multiple flavors of phosphor: each pixel can have
267
different amounts of each primary color (red, green
and blue).
Sprite Sheets
We also need some way to store data for the screen.
Since the screen is a raster display, it makes sense
to store images in a raster format. The human eye
can distinguish about 200 different shades of each
primary color, so a natural choice is to store how
bright each phosphor should be in a byte. So, our
first pixel would require three bytes: one for red,
one for green, and the last for blue. Then, we can
store multiple pixels for a single line, and multiple
lines for a single image.
268
Simple file formats (bitmap comes to mind) also store
pixels this way.
269
Play them quickly and it looks like it’s walking.
Vector Graphics
The hardware we work with is a raster display: the
most straightforward image format to program mimics
this structure. However, art deals with shape,
value, color and texture, not pixels. As a result,
while producing an image, an artist might like to
work with shape et. al. rather than pixels.
270
In a raster format, a circle is a bunch of square
pixels.
271
There are many ways to do this, some smarter than
others.
3D
Suppose you’re an architect, and you want to
visualize what your building will look like before
sinking $2 million into it.
272
Looking like $#!* isn’t supposed to be literal.
273
We need some way to represent those surfaces: the
most common is to approximate it with a very large
number of triangles.
274
With triangles, a smarter option is to start with
the eye point and view direction, and figure out
where the three corners of the triangle are on the
screen. Once you know where the triangle is, you can
rasterize it. Of course, figuring this out requires
a fair bit of math: how’s your trigonometry?
275
Challenges
1) Find a raster image editor and produce an
image: bonus points for something offensive and/or
entertaining.
276
Chapter 20
Windowing
Older computers only had one program running at any
given time. In this situation, letting that program
hit video memory directly is perfectly fine: what’s
mine is mine, and what’s yours is nothing. However,
the moment you have two programs sharing a screen,
that becomes a very bad idea.
277
Think advertisers are annoying now? Imagine what
they could do with direct memory access.
278
this window, which begs the question, what should we
put in there?
Common Elements
Before we answer that question, we need to answer a
simpler one: what might we put in there? The first
and least interesting thing is static text. Static
text can tell the user what they’re looking at, but
it doesn’t really do much. Additionally, static
images fill the same role as text.
For accepting user input, the type of control we
use depends on what type of data we need. For text,
the standard option is the text box: a region of
space that accepts a single line of text.
279
Don’t sign something you don’t understand.
280
than one option can be selected). The combo box is
similar to the list, except that it only shows all
the options when the user is interacting with it.
281
Using a menu for a menu. Meta-menus.
282
I just want to check my e-mail.
283
pressed, key released, mouse moved, mouse button
pressed, mouse button released. Operating systems
might add some higher level ones (window needs close,
for when the user clicks the “x” at the top of the
window), but more complex logic typically falls to
the program itself.
284
It’s pronounced nucular.
285
If they have the same real world control, consider
giving them the same virtual control.
286
A horse is a horse, of course of course (a horse
provides 1hp).
287
Challenges
1) For your choice of system and language, find out
how to get a window on the screen (you might consider
using a cross platform library: the operating system
controls for the interface are uniformly terrible).
288
Chapter 21
How to multitask
Multiple Programs
There are two main reasons to have multiple programs
running at the same time. The first is the obvious
one: if you have two processors working on a
problem, they can get that problem done in half the
amount of time.
289
The second, less obvious reason is responsiveness.
Older, single-task computers had, at any given time,
one program running: if that program checked user
input and responded to it, great, otherwise you got
to sit and look at a blank screen until the program
finished running.
So, even on a machine with a single processor
(where the obvious benefit doesn’t come into play)
it can still be useful to support having multiple
programs/processes running. On a single processor
machine, the processor is designed to, every so
often, stop running the current program and start
running the operating system. The operating system
then decides which program runs next; that program
runs for a bit until it gets stopped in favor of
another program.
290
people can’t use the same part of the table at the
same time.
Data Races
Let’s say you’ve gotten a plate of steak and a fork,
and you sit down at the table. You look around and
realize you forgot to get a knife, so you get up to
go get one. Then your friend comes over with his
plate. He sees the fork already on the table, and
starts eating some mashed potatoes.
291
I like mashed potatoes too, but if you’re going to
steal a fork, why not take the steak as well?
292
running the following code.
.data
x:
.long 0
.text
...
#x = x + 1;
movl $x, %eax
movl (%eax), %ebx
movl $1, %ecx
addl %ecx, %ebx
movl %ebx, (%eax)
A thread can be stopped at any time. So, it might
happen that process 1 loads x from memory, but is
stopped before adding; process 2 then runs, loading
x. If that happens, it doesn’t matter what happens
next, x is getting set to 1 (both processes read
zero, add one, and store). However, process 1 might
run through all the commands before process 2 loads
x: in that case, x gets set to 2 (process 1 adds
one, then process 2 adds one). We ran the same code
twice, and got a different answer: this is not good.
It’s the operating system that decides which thread
gets to run when: consequently, the operating system
has to provide some way to manage access to data.
The most common solution is the mutually exclusive
lock: switching over to C, our code might look like
#include <qthread.h>
int x = 0;
lock x_lock;
void threadCode(){
lockMutex(&x_lock);
x = x + 1;
unlockMutex(&x_lock);
}
293
If not, the operating system marks it as owned and
lets the program continue. However, if it is owned,
the operating system will halt the current thread
(and let another thread run) until the process that
owns the lock calls “unlockMutex”. When writing a
multithreaded program, any time you edit a shared
variable (or read a shared variable that may also be
edited), you need to use a lock.
Deadlock
It should be obvious that whenever you take a lock,
at some point you need to return it and let other
people use the thing. What might not be obvious
is that we can cause threads to hang even if our
code will return the lock after we’re through. For
example, suppose you grab the key to the bathroom,
head in, and notice it’s out of toilet paper.
294
neither of you can use the restroom.
Produce Consume
With locks, you can have two threads working at the
same time without corrupting each other’s state.
However, our threads have no way to let the operating
system know they have nothing to do. If a thread has
nothing to do, we don’t want the operating system to
run it (it will just waste time).
There are two ways we can go about doing this. One
is to signal the operating system that our thread
should not run for some amount of time: this is
typically used in video games and other programs with
a heavy emphasis on user interaction (if you need to
display new information every 14 milliseconds, but
finish your calculations in 7, you can sleep for 7
milliseconds while other programs/threads work).
295
An alternate approach is to suspend a thread
until another thread signals otherwise. There are
several approaches to this, but they are all used
for producer/consumer type workloads. If one thread
produces tasks that other threads need to run, those
other threads can’t do anything until the producer
thread has had a chance to make something.
296
Challenges
1) Look up the multithreading library for your
system, and identify how to create a thread, create
a lock, lock a lock, and unlock a lock.
297
298
Chapter 22
How to grammar
Why?
Suppose you’re writing a program that takes text
input from the user. Furthermore, suppose you want
that text to have a certain format (i.e. you need an
e-mail to send spam to).
Thing is, smart programmers have low opinions of
their users. If you ask a user for an e-mail, you
might get a phone number, social security number,
their mother’s maiden name, or anything but an
e-mail. Additionally, even good users can mistype
something. So, if you are taking input from a
user, you should probably check that it is formatted
correctly before doing anything with it. This is one
of the uses of text parsing libraries.
Another use of text parsing facilities is breaking
up a large text file. Suppose you have a large text
file (such as a program), and you want to break it
up into recognizable pieces (that’s a number, that’s
a name, that’s a keyword). You could code this up
by hand (we did in chapter 11); however, it can be
faster and easier to use an automated tool (called a
parser generator) to do this.
299
Finite Automata
The first thing we need to answer is how do we
actually recognize a language. The mechanics
actually depend on how complex the language is, but
for the simplest, most common problem (the regular
languages), the preferred mechanic is the finite
automata. A finite automata is a collection of
states, and a description of when to change state
(based on the text).
300
followed by a collection of letters and numbers,
followed by at least one dot and a collection of
letters and numbers.
So far: “”
301
Fail is an X: once a failure, always a failure.
So far: “LETTERNUMS”
302
So far: “LETTERNUMS @”
303
So far: “LETTERNUMS @ LETTERNUMS . LETTERNUMS”
304
If we see a period in the accept state, we need to
see some more letters.
305
Run multiple automata to differentiate e-mails and
dollar amounts.
Regular Expressions
I said above that finite automata were used for
regular languages. Regular languages are languages
that can be recognized with a finite amount of
memory (the state you are in can be represented
with a single number). While they are tested using
finite automata, they are specified using regular
expressions. A regular expression is one of five
things:
1) Nothing (for those cases where silence is valid).
2) A single letter.
3) A sequence of regular expressions.
4) A choice between regular expressions.
5) A repeated regular expression.
As an example, a vowel is one of aeiou; each
individual letter is a regular expression (“a”,
“e”, “i”, “o”, and “u”), and a vowel is a regular
306
expression of type 4 (“a | e | i | o | u”). Single
letters are written as is, sequences are just written
one after the other (“ab” is a followed by b), choice
is done using the pipe (as in the vowel example), and
repetition is represented with an asterisk (“a*” is
zero or more as).
An alternate way to represent choices of single
characters is to use brackets, so a vowel could also
be written as “[aeiou]”; any character in a range is
written similarly (all lower case letters would be
written “[a-z]”).
Finite automata tend to be fairly difficult to
write, while regular expressions tend to be compact.
As an example, a regular expression for a letter or
number is
Grammar
I mentioned that regular expressions and finite
automata are for “regular” languages. A natural
question to ask is what other kinds of languages you
might have. Answering that question requires being
able to talk about the structure of that language;
the structure of a language is called its grammar.
A formal grammar deals with terminals and
nonterminals: the terminals are the “words”
307
(the actual text that gets written), while the
nonterminals are groupings (clauses and the like).
A grammar is a collection of rules stating what
nonterminals can contain. As an example, in English,
a sentence is a subject, a verb phrase, and an
optional object.
You have any idea how long it’s been since I’ve made
a parse tree for English?
A -> b C
308
memory). For instance, the e-mail example could be
written
309
statement (Expr) might have the following grammar.
310
The next step up in complexity is the context
sensitive grammar, where the allowed groupings depend
on context. In other words, each rule has context
surrounding the nonterminal to expand (and that
context is preserved). An example could be how, in
English, a number specification (2) forces you to use
the plural form of the noun (“two men” rather than
“two man”).
311
Challenges
1) Any serious programming language has a regular
expression library. Find one for your preferred
language, and figure out how to make it test whether
a string matches a regular expression.
312
Chapter 23
How to lengua
313
design: is the language low level (i.e. matches the
assembly code) or high level (uses its own, complex
primitives).
One of the helpful things with high level languages
is garbage collection. If you remember, in C,
whenever we allocated memory (malloc), we had to
make sure we returned it when we were done with
it (free). If your main primitive is the pointer,
that’s about the best you can do (the programmer
can do absolutely strange things and still have a
valid program). However, if your main primitive
is a structure or class, it becomes possible for
the compiler/interpreter to manage the frees
automatically.
One way to do this is to count the number of
pointers pointing at any given structure. If nobody
knows where the structure is, nobody can use that
structure, so it’s useless memory (that can be
returned).
314
the high level language does things, you’re stuck
with it, while a low level language would let you do
what you want.
Static vs Dynamic
The next big split is between static and dynamic
languages. A static language is one where everything
is known at compile time: while this cannot be done
(if we knew everything, we would know the answer and
wouldn’t need to run the program), some languages
get closer than others. In particular, types are an
important aspect.
One of the best examples of a statically typed
language is C. If you remember, in C we have to
specify the types of the variables in our program.
We can’t have a variable named “x”, we need to
have an int variable named “x”. When we created
a subroutine, we had to say what type of thing is
returned.
On the other hand, a dynamically typed language
is one where type information is left unknown until
the program is run. As an example, LISP will let you
declare a subroutine as
315
up. Another advantage of dynamic languages is that
they’re more flexible: in LISP we can call square
with an integer, real or even a rational and the
subroutine will run and give us the appropriate
value with an appropriate type. In C, the subroutine
expects a double, and is giving us a double.
But, there are a few drawbacks to this. The first
is that everything needs to carry type information
with it. So, in C, an integer is a single word
(i.e. 4 bytes on a 32 bit system). However, in
LISP an integer is, at a minimum, two words (a type
specifier and the payload). In languages that add a
lot of information to types (Python), this can bloat
a large-but-manageble set of data into an unusable
mess.
316
void* square(void* x){
int* typePtr = (int*)x;
void* valPtr = x + 1;
int xType = *((int*)x);
if(xType == 0){
int val = *((int*)valPtr);
int res = val * val;
//and package result
}
else if(xType == 1){
double val = *((double*)valPtr);
double res = val * val;
//and package result
}
//...
}
317
Being buzzed has been shown to increase coding
productivity.
Compiled vs Interpreted
A third major split is between compiling or
interpreting. As a reminder, a compiler reads in a
program in one language and produces an executable:
you send that executable to someone else, and their
processor runs the machine code. On the other hand,
an interpreter takes the program (in its original
language) and “simulates” the effects of the code.
In point of fact, every language can be either
compiled or interpreted. However, some languages
lend themselves to one option over the other.
More dynamic languages tend to be better suited
to interpreting (it’s easier to write flexible
interpreting code than to bake that flexibility
into the compiler, and there’s less of an advantage
to compiling in the first place), while static
languages tend to be better suited to compilation.
Additionally, low level languages tend to be easier
to write compilers for (since the differences between
the source and the assembly are smaller).
An obvious advantage of compilation is speed:
having the CPU run its own flavor of assembly is
always going to be faster than running a simulation
318
of a program. Additionally, compiling your code
allows you to send the executable to your customers,
rather than your source. If you’re trying to keep
creative control over a project, this is useful.
Interpreting requires that the person who wants
to run the code has the simulator program on their
machine. This requirement ranges from trivial (when
you have control over the computers in question)
to impossible (when J.Q. Public is trying to run
your program). However, interpreting has a few
advantages. One is a short turnaround time: being
able to quickly see the effects of changes can be
helpful when debugging a piece of code. It’s also
easier to get error information from an interpreter.
The biggest advantage, however, is scripting. As
an example, let’s say you’re writing a program that
manages a large collection of files on a hard drive:
part of a System to Operate (there’s got to be a name
for this sort of program). There are some basic
operations that everyone is going to need (moving,
copying, deleting). However, you can’t anticipate
everyone’s needs: some strange person is going to
need to delete every file beginning with “BOB”.
319
interpreter to run, which allows the user to add code
for their own requirements. Windows accomplishes
this with batch files, while *nix (Linux and Mac)
allow you to write shell scripts.
There are other programs that use this same basic
paradigm: one of the biggest is a web browser (HTML
is used to define a webpage, JavaScript defines how
it reacts to the user, and the browser interprets
both to draw a pretty picture).
Assorted Weirdness
The three splits above are some of the biggest
considerations: if you’re selecting a language for
programming, then those are the items you really
have to watch. However, there are some other things
to watch: many of these revolve around the main
primitive. For example, many programmers will swear
by functional programming languages (where the main
primitive is the function): your mileage may vary.
In addition to programming languages, there are
some domain specific languages tailored to solve a
particular problem. Some major examples are hardware
description languages (used to describe connections
between gates, such as VHDL and Verilog), data
storage languages (used to organize data, such as
XML or JSON) and database languages (used to extract
information from a database, such as SQL).
320
Challenges
1) Go look up three programming languages. Write the
same simple program in each (if you need inspiration,
write bubble sort), and compare and contrast them.
321
322
Chapter 24
How to big
Planning
When you start any big project, the first thing you
must do is plan. There are many different approaches
to this: some people plan things out to the n-th
degree, leaving no part of the project up to fate.
Other people’s plans consist only of vague goals,
relying on grit to make everything work together.
This reflects different skillsets: some people
are good at planning, while some people are good at
winging it. I can’t tell you your talents.
What little I can do is tell you how I do things.
I’ve put my 10000+ hours into writing code: as a
result, I am more tolerant of “winging it” than
someone new to programming. However, I also
like building code to last: that requires good
interfaces, which requires good planning.
When starting a new project, the first thing I
do is sit down and figure out the main “actors”
in the program: the big pieces of data and logic.
This allows me to make a (simple) class diagram: a
description of the data structures in your program
(and how they relate). For instance, a video game
might have a controller loop, and types for each
of the main entities in the game (player, enemy,
pickup).
323
There is a standard for this (UML), but if you NEED
the details, you’re doing it wrong.
324
The minigun suggests Quake, the headcrab zombie
suggests Half-Life.
Documentation
Good documentation will cover a lot of sin. Every
programming language provides some way to add
documentation: comments that the compiler ignores,
but allow you to add information for the hapless
programmer. Writing good comments will allow you
to write a piece of code, let it sit for years
(or even decades), and come back to it and have a
325
chance of figuring out what you were thinking. Good
documentation will also help you play well with
others: as an example, look at the following code.
you will know (or have some idea) what the subroutine
does, even if you don’t know how it works. Proper
documentation comes in two parts. The first is by
adding comments (in C, this is any line beginning
with “//”). Comments allow you to add text that
the compiler ignores. Exactly how you add comments
depends on your own style.
I write short subroutines (less than one page per
subroutine), so I tend to not put comments inside
subroutines. For every subroutine, I add comments
explaining what the subroutine does, what each of
the arguments is (“@param”), and what it returns
(“@return”). I like the javadoc format (the first
production language I learned was Java), so that’s
the format I use. I also explain any weird error
cases that might show up (“@throws”, for reasons that
will soon become apparent).
The first part of proper documentation is good
comments. The second (and more important) part is
326
good naming. Look again at the two examples: in
one, the subroutine is called “sp”, in the other,
it’s called “swap”. If you saw a random call to
sp (“sp(&a, &b)”), you would have to look up the
function. But, if you saw a random call to swap
(“swap(&val1, &val2)”), you could guess what it did.
When you write code, use intelligent names: typing
a longer name now will save you more time in the long
run.
Flavors of Error
If your program has a problem (and it does), there
are three times at which you can spot it. The
first is at compile time: you’ve done something
so strange the compiler can spot it for you. This
shows up if you’ve broken the grammer of the language
(i.e. you’ve mispelt a keyword, or havent matched
parenthesis. This also shows up if you try to use
a variable that doesn’t exist, or if you mess up the
types (trying to set an integer to text).
327
int calcTax(int cost){
//valid code
//but the processor will choke
return cost / 0;
}
Exceptions
As much as possible, you want to turn logic errors
into run-time errors: with only the tools I’ve laid
328
out for you so far, that’s not an easy task. For
instance, suppose you’re writing a program to grade
essays. The easiest way to grade an essay is to see
how long it is.
329
FILE* myopen(const char* name, int* errRet){
*errRet = 0;
...
if(problem){
*errRet = -1;
}
...
}
int fopenErr;
FILE* curRes = myopen(“filenam”, &fopenErr);
if(fopenErr){
...
}
330
it automatically. When you throw an exception, the
current block of code stops running. If you did not
add any code to handle an exception, the exception
keeps killing code blocks until it either hits
handling code or stops the entire program. In C++,
you throw an exception with the keyword “throw” and a
value (that contains extra information to, hopefully,
tell you what exactly went wrong).
try{
FILE* fp = myopen(filenam);
}
catch(const char* errMess){
printf(“Give me a file that exists, stupid.”);
}
331
For fun on a long trip, if your passengers fall
asleep, park in front of a brick wall, scream and
wail on the horn.
332
while(i<7){
double tv = sqrt(a)-cos(b);
printf(“%d”,i*tv);
i++;
}
double tv = sqrt(a)-cos(b);
while(i<7){
printf(“%d”,i*tv);
i++;
}
333
Challenges
1) There are multiple types of UML diagram. Look
up how a class UML diagram works, pick a problem you
want to work on, and break it up into classes.
334
Afterword
335
For the specifics about how transistors are made,
check out Silicon VLSI Technology: Fundamentals,
Practice and Modeling (by James Plummer, published by
Pearson Education, 2009).
If you want to go more in depth on this subject
(i.e. actually make a computer, rather than just
think about it), check out The Elements of Computing
Systems: Building a Modern Computer from First
Principles (by Noam Nisan and Shimon Schocken,
published by MIT Press, 2008).
If you want to learn more about C, you’d be hard
pressed to beat the original: check out The C
Programming Language (by Brian Kernighan and Dennis
Ritchie, published by Prentice Hall, 1978).
There is a lot of stuff in C++ I skipped (partially
because I have my... issues with the philosophy
behind those features). However, if you’re going
to properly learn the language, going to the source
is not a bad idea: you might check out Programming:
Principles and Practice Using C++ (by Bjarne
Stroustrup, published by Addison-Wesley Professional,
2014).
For most other languages, I’ve done most of my
learning online. However, for Java, I can suggest
Learning Java (by Patrick Niemeyer and Daniel Leuck,
published by O’Reiley Media, 2013).
For some more details on common algorithms and
data structures, Donald Knuth’s “The Art of Computer
Programming” is worth cracking open (provided
he ever finishes the damn thing). Published by
Addison-Wesley Professional, 2011.
If your algebra, geometry or trigonometry are weak,
you might take a look at Precalculus Mathematics in
a Nutshell (by George F. Simmons, published by Wipf &
Stock Publishers, 2003).
A surprisingly handy math skill to have in
computer science is discrete mathematics: the book
I was taught from was Discrete and Combinatorial
Mathematics (by Ralph Grimaldi; published by Pearson
Addison Wesley, 2004)
For more on computational mathematics, Numerical
Recipes: The Art of Scientific Computing is a decent
choice (by William Press, Saul Teukolskg, William
Vetterling and Brian Flannery; published by Cambridge
336
University Press, 2007).
More information on how networking works can be
found in Computer Networking: A Top-Down Approach
(by Jim Kurose and Keith Ross; published by Pearson,
2016).
The Essential Guide to User Interface Design: An
Introduction to GUI Design Principles and Techniques
(Wilbert Galitz, Published by Wiley, 2007) is an
excellent compendium of information.
A large part of my approach to large projects
comes from the “Xtreme Programming” crowd. My
introduction to this philosophy was Planning
Extreme Programming (by Kent Beck and Martin Fowler,
published Addison-Wesley Professional, 2000).
For learning, video games tend to be a good project
choice because they require the use of almost all the
various subsystems of a computer. For information
on the programming of video games, you might check
out Game Coding Complete (by Mike McShaffry and David
Graham; published by Cengage Learning PTR, 2012).
For a good laugh, check out XKCD (xkcd.com, by
Randall Munroe). His Thing Explainer book is also
an intriguing time (published by Houghton Mifflin
Harcourt, 2015).
337