How do computers read code?

In your first programming class, you wereprobably introduced to the idea of a compiler. Here’s how it works.

You first write out your program. But a computer can’t read it yet. So, in order to actually run your program, you first need to pass it through this special program called the compiler. Then, out pops a new version of your programthat can be read by a computer.

It was probably then tested by running it with a bunch of inputs and expected outputs or something. So there are two versions of your program. The one you wrote but a computer can’t read, and the magically generated one that a computer can read.

Except, it’s better than magic. The compiler is a complex machinethat bridges the gap between human-readable code and computer-readable code. So, what exactly is the compiler doing, and what does the executable version of your program actually look like? What? You say you have no idea what I’m talking about? Oh, I think I know what might have happened. In your first programming class,you probably just used an IDE, in which case this whole process was hidden from you.

When you click the run button, your work issaved, your program is built, and it runs automatically. So now that we’re up to speed, what does the executable look like, and how does the compiler… What? You still don’t know what I’m talkingabout? All you did was Python scripting??? sigh Oh my god there are so many edge cases.

Ok, this is what happens to programs in general,deal with it, how does it work, I don’t know, let’s find out! At a low level, computers processors can onlydo a small number of things. They can read and write to memory, and theycan do math with numbers they are holding.

Modern processors do other things too, butthis is basically it. Now, an executable program, the one generatedby the compiler, is just a list of instructions for the processor to follow, written in binary.

The instructions are things like… read thesebytes from memory, do stuff to them, write bytes to memory, jump forward this many lines,jump back this many lines but only if this flag is set,… stuff like that. A program, expressed in a list ofbinary instructions is called machine code, and this is the kind of program thatyour computer can actually read.

But why does a computer processor only readprograms that look like this? Why this specifically? Well in short, here’s how a processor works. The processor already containsthe circuitry to do all of these instructions, but the correct circuitry only gets connected together when the corresponding instruction gets fed into it.

The 1s and 0s in the instruction cause certaintransistors to open or close, which ends up connecting the correct circuitry togetherto execute that instruction. If you want to look more into how this works, you might wanna check out crash course computer science particularly episodes 5 through 8.

Episodes 3 and 4 are also helpful if you needa refresher on binary and logic gates. Though you could just watch my video too… And for the record, crash course isn’t payingme to recommend them. I just really like this series. But in short, just know that executable programslook like this.

But when you learned to program, you didn’tneed know anything about these complicated machine code things like memory management,operations on bytes, or conditional jumps. Programming was about variables, and if statements,and loops, and functions.

Well, these things are just higher-level constructs that make it easier for humans to think about programming. A program, expressed in this form, is calledsource code. It’s the version of a programthat a human understands, and thus the version that mosthumans actually write code in.

The compiler’s job is to take this sourcecode that is human-readable, and turn it into machine code that is computer-readable. But how does it do that? How does it turn a string of text into a listof instructions in binary? Let’s look at some examples:

Here’s a pretty simple program. Declare a variable of type integerthat we’ll call x. Then assign it a value of 3. For now, this program only existsas source code. I know it looks like it has some kind of structure,but for the computer, it’s just a meaningless sequence of characters.

It’s just text. And I know that this program doesn’t reallydo anything useful, but we’re starting simple. Let’s pass this source code into the compilerand see what it does. The compiler first divides the text up intoindividual “tokens”.

It’s kind of like the compiler is figuringout what the “words” are in this program. Then, the tokens are organized into a hierarchicalstructure known as a parse tree, which is like figuring out what the “grammar” isin this program, the structure.

Then, the compiler records context about theprogram, including variable and function names. This is the stuff that a computer needs tokeep track of in different parts of the program. In our case, the only context we need is thevariable x.

Oh, and the main function too but that’sless important for us. The final step is to traverse the tree, andfigure out some machine code that would effectively do the same thing as this particular sourcecode.

I just wanna that, typically, compilers don'tgo directly from the parse tree to the machine code. There's usually a few intermediate steps thatwe're going to skip over. This is what the machine instructions looklike in binary. It’s a little hard to read and interpret,so let’s shorten it by writing in hexadecimal. Actually, it’s still a pain to read.

Let’s write it out as assembly code, which is amore human-readable version of the machine code. That’s better. Now, we’re going to ignore this stuff here. Just know that it’s responsible for startingand ending the main function, which determines where the program starts and ends. This is the instruction that corresponds tothe assignment of the variable x.

This instruction says to “move” the number3 into this memory location. Let’s run the program and see what happens… And as we’d expect, the number 3 got putinto that memory location. It looks like the compiler decided that thispart of memory is where the variable x lives. And that’s it.

The compiler took our source code, which saidto take a variable x and assign it the value 3, and translated it into machine code, whichsays to place the value 3 in this part of memory. So, our program is kind of boring right now. What happens if we change it? Let’s add a line to increment x by one,after it’s been assigned 3.

We assign it the value of x’scurrent value plus 1. Now, we pass it into the compiler again togenerate a new version of our machine code. The compiler finds tokens,… …parses,… …contextualizes,… …and generates. It looks like there’s only a single newinstruction: to “add” the value of 1 to this memory location.

After all, this is the location that the compilerdecided that x lives. Modifying the variable x is equivalent to modifyingthe value stored in this memory location. Let’s run it… The value 3 appears in that memory locationdesignated for the variable x, and then the value becomes 4.

Now so far, we’ve seen how variable assignmentsand simple mathematical operations get translated. But it’s not so simple for if-statements,loops and functions. There are no machine instructions that aredirect equivalents. Instead, we need to emulate their behaviourwith instructions that do exist. Let’s start with an if-statement.

In an if-statement, we only execute the codein this block if this condition is true. If the condition is false, we skip over this code. In assembly, the code inside the block getstranslated normally, but before it, we have some instructions evaluating the condition,and then we have a conditional jump instruction.

In this case, to jump past the block we wantto skip, but only if we’re supposed to skip it. The processor knows whether or not we’resupposed to take this jump based on the result of the previous instruction. That instruction temporarily set some flagsin the processor so we could remember the result by the time we got to this conditionaljump to skip over this block.

If we’re not supposed to skip it, then theprocessor ignores the jump, and continues normally, conveniently executing the codeinside the block. Notice that these machine instructions areeffectively doing the same thing as our if-statement? Let’s see now jumps can emulate other sourcecode behaviour.

There’s the if-else-statement. Only execute this block if this condition is true. Otherwise, execute this block. In assembly, we have the code in the firstblock, and the code in the second block. Before the first block, we have a comparisonand a conditional jump, like what we had earlier.

In between the blocks, we have a regular,non-conditional jump instruction. This is so that execution skips the secondblock if it just finished the first one, which is kind of how the if-else-statement worksif you think about it. Then there’s the while-loop.

Only execute this block if this conditionis true, and keep executing it over and over again until it’s not true anymore. In assembly, we have the block’s code, theinstructions to evaluate the condition, and the jumps emulating the loop. Functions are a little more complicated.

Basically, functions encapsulate a code block,so that it can be used in multiple parts of a program. Most programmers out there should know thatthey also isolate context and they can do recursion and stuff. This is what its equivalent assembly codelooks like.

Let’s run it to see what it does. Hitting the function call, we save all contextinto memory, allocate new space on top of it, execute the function code (which may involve calling more functions), and pop back down once it’s done.

This makes it possible for functions to callother functions, or even themselves. You just push more memory, and pop back downwhen you’re done. So that’s how compilers work! They take your source code, and make machine code. But there’s one problem.

If you compile your program on one computerand then copy it over to another computer and try to run it, it might not work. If the new computer has a different operatingsystem or has a different processor model, it probably uses different machine instructions.

So if you want your program to be able torun on this new computer, you'd better be able to compile to that computer’s machine code.

And if your program’s users might run yoursoftware on different platforms, unless you’re distributing the source, you’re gonnaneed to keep a copy of an executable for every platform you want to run on. Every. Single. One. Some languages like Java sneak around thisissue.

Instead of machine code, Java gets compiledto an intermediate representation known as bytecode. And then the bytecode can get sent to othercomputers where it gets converted to that specific computer’s machine code when theprogram is run via an interpreter.

It’s a bit of a compromise. You get better portability, though it’sless efficient. But regardless, the language you write yourcode in is compatible with a wide variety of processors and operating systems.

Can you imagine what it was like back in theday when computer programming meant putting assembly or even machine code onto punch cards? Not only would you need to figure out thecorrect holes to punch for each instruction.

You wouldn’t even be able to use your programon a different computer model, because the other model expected you to punchdifferent holes. Things are a little nicer these days. You just write a program, compile it, andtest it with inputs and outputs, if only it were that simple all thanks to the peoplewho wrote the special program: the compiler.

But, remember that the compiler is a program itself. If people use compilers to develop programs,how was the compiler developed? Well, it was probably written and compiledin another language, or even in the same language, compiling a compiler with a previous versionof itself.

If we follow this chain backwards, at somepoint, we reach the origins of development tools: programs, written directly in machinecode, that help you write other programs; literally automating part of the process ofcreating automation.

The history of computer languages is pretty complex. No wonder it took decades to get to wherewe are now! Remember that the next time you’re writing code. We have all these beautiful things like syntaxhighlighting, static analysis, object-oriented programming, functional programming, libraries,linkers, build tools, and debuggers.

But it’s still amazing that we can justtell our computers to follow our exact instructions… …at the push of a button. Nope, wait… There we go, the push of a button. Programming isn’t so hard…

Post a comment