Understanding the binary system

Why is it that computers only work with zeros and ones? Why can't they work directly with text or images, for example? The answer is that it is rather easy to build circuits that can represent two states. If you have an electrical wire, you can either run electricity through it or not. The flow or no flow of electricity could represent several things, such as on or off, true or false, or zero or one. Let's think of these two states as zero and one for now, with zero representing no electricity flowing and one symbolizing that we do have flow. If we can serve these two states, we could add more wires and, by doing that, have more zeros and ones.

But what could we possibly do with all of these zeros and ones? Well, the answer is that we can do almost anything. For example, with only zeros and ones, we can represent any integer by using the binary numeral system. Let's demonstrate how that works.

To understand binary numbers, we must start by looking at the decimal numeral system. In the decimal system, we work with 10 digits, from 0 to 9. When we count, we go through these digits until we reach 9. Now we have run out of digits, so we start over from zero and add a one in front of it, forming the number 10. Then, we continue until we reach 19, then we do the same thing again; start over from zero and increase the value in front of the zero by one, so we get 20.

Another way to think about different numeral systems is to think about the value a position represents. Let's consider an example. The number 212 has the digit 2 in two places, but their position gives them two different values. If we start from the right and move to the left, we can say that we take the first digit, 2, and multiply it by 1. Then, we take the second digit, 1, and multiply it by 10. Finally, we take the last digit, 2, and multiply it by 100. If we move from right to left, each step is worth 10 times as much as the previous step. Take a look at this calculation represented in the following table:

Table 1.2. The positional values of a decimal number

When using the binary system, we do the same thing, but only using the digits 0 and 1. We start our counting with 0, followed by 1. At this point, we run out of digits, so we start over from 0, adding a 1 in front of it.

Counting in binary looks like this:

0, 1, 10, 11, 100, 101, 110, 111, 1000, 1001, 1010, 1011, 1100, 1101, 1110, 1111, and so on

When it comes to the values each position has for binary numbers, it works just as it does with decimal numbers. However, the value for each position is not multiplied by 10 but instead by 2. We multiply the first digit by 1, the second digit by 2, the third digit by 4, and so on. To make things simpler, we could say that a one in a particular position means that the number representing that position shall be a part of the final value, and zero means it shall not. Take a look at this table for binary number 11010100:

Table 1.3: Interpreting binary number 11010100

Here, we have ones at the positions represented by 128, 64, 16, and 4, so now we can add them together (we can ignore the positions with zeros as adding zero to something will not make any difference) to get what the binary number of 11010100 is in decimal form, which is 212.

If we want to convert a decimal number, say 27, into binary, we start by thinking how far we can go through the sequence of positional values: 1, 2, 4, 8, 16, and so on. Which is the largest of these that we can find that is smaller than or equal to 27? The answer is 16, so the first 1 in this binary number will be at this position. On all positions before 16, we can insert 0:

Figure 1.7: Finding the first position that is less than or equal to 27

We then subtract 16 from 27 and get 11 and repeat the process with this value. The largest value that is less than or equal to 11 is 8:

Figure 1.8: Finding the first position that is less than or equal to 8

We subtract 8 from 11 and get 3. The next value, 4, is larger than 3, so we insert a 0 at this position:

Figure 1.9: We encounter a position that is greater than 3 so we insert a 0

As we have not inserted a 1 yet, we keep the value of 3 and try to find a value that works for it. The next one, 2, is less than or equal to 3, so we insert a 1 here and then subtract 2 from 3 and get 1:

Figure 1.10: 2 is less than 3, so we insert a 1 at this position

We repeat this until we reach 0:

Figure 1.11: When we have reached the end, we have arrived at the complete binary number

We now know that 27 will be 11011 in binary. We can ignore the leading zeros.

When we have one single binary digit, we call it a bit, and if we place them in groups of 8 bits, we call them a byte. One byte can hold values between 0 and 255. This is because a 1 in all positions (11111111) will be 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1= 255.

By using lots of zeros and ones, the computer can represent any number in binary form, and if it can represent numbers, it can serve other things too, such as text.

Understanding ASCII and Unicode

If you give each letter of the English alphabet a numerical value, you could represent text with numbers. We could, for example, say that A=1, B=2, and so on. The computer does not use these values for the letters, but instead, it can either use something that is called the ASCII table (pronounced as-key) or another representation that is called Unicode. It is not important to understand exactly how they work; the only thing we need to understand is that a number can represent every character. This number can then be looked up using either the ASCII table or Unicode.

The ASCII table uses one byte to represent different characters. The table starts with characters that are non-printable. Eventually, it reaches the characters in the English alphabet. So, A, for example, is 65, B is 66, and so on. 255 characters will not take us far as we have lots of different alphabets around the world, and we also want to represent other symbols. That is why we also have Unicode. Its mapping to inpidual characters is not as direct as it is in the ASCII table, but all we need to know right now is that with it, we can use numbers to represent characters.

Note

Non-printable characters are symbols that are not used for visual representation; for example, when we need a way to indicate a tab or a new line, or if printing text to a printer, we want the printer to continue to the next page.

Representing other forms of data

We've learned how to represent text in binary, but what about things other than text and numbers? What about images? And video? And sound?

Images are made up of pixels, and three values, RGB, represent each pixel. These values tell the computer how much red, green, and blue a pixel has:

Figure 1.12: Three values represent a single pixel, indicating how much red, green, and blue it has

A video is nothing more than a composite of many images, so every frame is an image; therefore, it can be represented the same way.

A waveform can represent sound. Each peak and valley can be a number:

Figure 1.13: Audio depicted as a waveform

Now that we know how the computer can represent data, we have to find out how it processes it. To understand that, we must first pe into a corner of mathematics that is called Boolean algebra.

Boolean algebra

George Boole, who lived between 1815 and 1864, was a self-taught English mathematician and the inventor of Boolean logic, which is the basis of how all our computers work.

Boolean logic, sometimes referred to as Boolean algebra, is a form of mathematics that works with only two values: true and false. It also defines three operations that we can perform on these two values: AND, OR, and NOT.

NOT is the simplest of these operations as all it does is just switch the value, so not true is false, and not false is true. For example, if I say, "It is raining today," this statement can be true or false. It is true if it rains and false if it is not. If I instead say, "It is NOT raining today," then the statement will be true if it doesn't rain and false if it does.

AND takes two statements that can be either true or false and evaluates them into a single value. The outcome will be true if both incoming values are true and false in all other situations. If I say, "It is raining today, AND I have a blue umbrella," the statement will only be true if both parts are true, that is, if it is actually raining and my umbrella is actually blue. However, if it is raining but my umbrella is pink, what I say will be false, even though half of it was true.

OR works on two parts, just like AND, but now only one of the two must be true to make the statement true. If I say, "Today I will go to the beach OR I will go to town," then the statement will be true whether I either go to the beach or to town, and also if I manage to do both.

We can illustrate how these three operations work in something called a truth table. A truth table is a way to describe how an input of the true and false values is transformed by an operation. The input is often referred to as P if we only have one input value, or P and Q if we have two. The result is shown in the last column.

If P is the input value, the truth table for NOT will look like this:

Table 1.4: The truth table for NOT

For AND, the truth table looks like this if P and Q are the input:

Table 1.5: The truth table for AND

For OR, the truth table looks like this:

Table 1.6: The truth table for OR

As you can see, the only way an AND operation can be true is if both parts are true, and the only time OR can be false is if both parts are false.

When Claude Shannon, an American mathematician and electrical engineer, published his master's degree thesis in 1937, A Symbolic Analysis of Relay and Switching Circuits, he based his work on the ideas of Boole. From Shannon's ideas, Boolean logic made its way into our modern computers because, with the help of the simple operations that Boole defined, we could transform any value that can be in one of two states: true or false, on or off, or, in the case of binary numbers, one or zero.

We can accomplish these operations with the help of transistors. There is no need for us to go into the details of how a transistor works – just knowing that it can be used to represent true/false, on/off, or 0/1 is enough. We can then connect several transistors into different configurations to accomplish operations such as AND, OR, and NOT. These combinations are called gates, so we will have a group of transistors called an AND gate, one that is called an OR gate, and one that is called a NOT gate. These gates can then be connected further to construct circuits that can add, subtract, multiply, and pide. We have now built a machine that can represent both numbers and these basic operations. We have done this using only numbers, and all these numbers will be in binary, so we have a machine that only works with zeros and ones: the computer.

Machine code – the native language of the computer

Now that we have circuits that can perform some basic operations on numbers, and we have data in the form of numbers, we can start to write programs that will perform operations on the data. We can do that with the only thing the computer understands: machine code. As numbers can represent everything, the instructions we give to the computer will be – yes, that's right – just numbers.

Each processor type has a specific set of instructions. That is why a program written for a Mac can't run on a PC running Windows, for example. So, the instructions can be machine code. Machine code has several operations, called opcodes. The operations can be things such as AND, OR, ADD, and so on. Each opcode has a unique number. For example, AND could have an opcode value of 1, and OR could have an opcode value of 9.

The processor will also have several registers. A register is a small area, sometimes referred to as a data holding place, where the processor can store data it is currently working with. Before executing an operation, we will need to move the data we want as input to the operation, from memory, into some of these registers. The result of the operation, the output, is also stored in a register. In reality, things are a bit more complicated than this, but we do not need to go into all the details here.

We can now recall the image of the four operations that were common for all computers: input, storage, process, and output. We first get some input, and it will go to the computer's memory for storage. The processor will then retrieve it from its registers and perform operations on it, which is the process part. When we have the result of the operations, it will go back into the memory so that it can later be sent to the output.

One way to write these instructions is to use something called an assembly. This is a way of writing a program where we use three-letter abbreviations for the opcodes and have names for the registers. By doing this, it will be easier to read and understand the instructions we give. We can then use a program that can translate the assembly code into machine code.

The assembly language is the first programming language we encounter. The assembly language can look like this:

mov     eax, 14

mov     ebx, 10

add     eax, ebx

Here, we are moving (mov) the value of 14 into one of the registers, called eax, and then we are moving the value of 10 into another register, called ebx. We are then performing the add operation on the contents of these two registers. The result will be written back into a register; perhaps eax will be reused for this.

If the move operation has an opcode of 136 and the add operation has an opcode of 1, we can use these values together with the numerical representations of the registers to have all of this in only numerical format. And, as we know, everything that is numerical can be represented in binary form, that is, with zeros and ones.

Now we have all that, we need to look at some machine code.

Example machine code

Remember that the instructions we give will be different depending on what processor and operating system we use. The following is an example of what machine code can look like for a program printing the text Hello, World! to the screen on a computer using the Linux operating system:

b8    21 0a 00 00   

a3    0c 10 00 06   

b8    6f 72 6c 64   

a3    08 10 00 06   

b8    6f 2c 20 57   

a3    04 10 00 06   

b8    48 65 6c 6c   

a3    00 10 00 06   

b9    00 10 00 06   

ba    10 00 00 00   

bb    01 00 00 00   

b8    04 00 00 00   

cd    80            

b8    01 00 00 00   

cd    80            

When looking at this program, we can write the numbers in binary or decimal format if we want to. However, to make it easier to read, we often use hexadecimal numbers as we can then use fewer digits. For example, 15 in the decimal format (two digits) is 1111 (four digits) in binary, but only F (one digit) in hexadecimal. It is just more compact – that is the only reason we do this.

Don't worry if you don't understand anything about the machine code program. It is not supposed to be readable for humans; however, for the computer, this all makes sense.

Writing code in machine code is error-prone. A number in the wrong place can be the difference between success and disaster. The natural next step, therefore, has been to create something more comfortable for humans to read and write, which the computer can then translate into machine code. One such measure has been the creation of the assembly language that we talked about earlier.

Here is the same program, written in the assembly language:

section     .text

global      _start                               

_start:                                         

    mov     edx,len                             

    mov     ecx,msg                             

    mov     ebx,1                               

    mov     eax,4                               

    int     0x80                                

    mov     eax,1                               

    int     0x80                                

section     .data

msg     db  'Hello, world!',0xa                 

len     equ $ - msg                             

As you can see, this is still not that easy to understand. In the next chapter, we will learn how to write the same program using languages that resemble human language to a much higher degree.