In this module we'll cover basic assembly/machine language concepts, and go through several assembly code examples. By the end of it, you'll know enough to write short, one-function assembly programs, and use many of the most common assembly statements.
Let's start with a brief look at machine language . As you may already know from earlier programming study, computers do not actually "understand" high-level languages like C, Python or Java. Computers understand only machine language: instructions in the form of binary or numerical values, stored in the computer memory and run by the CPU.
Some high-level languages, e.g. C or C++, must first be translated or compiled into equivalent machine language. The CPU then runs the machine language translation. For other languages, e.g. Python, are interpreted . To execute Python code, the CPU runs a Python interpreter program, which reads Python and performs the indicated actions. However, the interpreter itself is in machine language, so ultimately, any running software comes down to machine language.
There is not one universal machine language. Each type of CPU or processor has its own. So, the machine language understood by the processor in an Intel-based PC is unintelligible to the ARM processor in a Raspberry Pi, and vice versa. In a later module we'll look at ** Instruction Set Architecture or ISA -- the design of machine languages. We'll see what machine languages have in common, and how they differ.
For this course, we'll study the machine language of the ARM processor in the Raspberry Pi. The term ARM (Advanced RISC Machine) describes an entire family of CPUs or processors, made by many different companies, all having essentially the same machine language and internal design. Such processors are found in phones, small computers like Arduinos or Raspberry Pis, and inside of many products that include a small computer to control them (usually called embedded systems ). The ARM machine language is so popular because it's elegant and cleverly designed. It illustrates all major machine language concepts quite well, but it's also much simpler than machine languages like MASM, the machine language for Intel PCs. "RISC" stands for Reduced Instruction Set Computing, as opposed to Complex Instruction Set Computing [CISC] such as MASM.
As a reality check, try searching for "ARM assembly" vs "MASM assembly" on tech job posting sites or Google Trends. You'll usually find that ARM is more popular.
As mentioned already, machine language instructions are composed of a number of bits -- binary 1s and 0s. In some machine languages, the number of bits varies per type of machine language instruction, but ARM machine language instruction are all exactly 32 bits long. Here's an example, just to show you what machine language looks like:
00000010100100010010000000101010
This particular instruction says to add 42 to the contents of register 1, and put the result into register 2, but only if a prior operation resulted in a zero value, but you're not expected to understand that by looking at the bits. By the end of the course, you'll be able to, but even so, no one actually codes this way.
Any significant program requires at least thousands of such instructions, and you can imagine that writing programs in bit form is more than a little tedious, not to mention error prone. In the bad old days, you could actually program a computer by toggling binary machine language instructions into it using physical switches on its front panel. I actually did such programming for pay while in college. It was an interesting experience -- once. But even then, direct binary machine language coding was obsolete. I was just working on a machine that lacked an assembler .
An assembler is a program that translates textual descriptions of machine language instructions into binary form. It's a kind of compiler, in that it translates a language the computer doesn't understand into the binary machine language that the computer does understand. But while a compiler translates higher level languages like C++, an assembler translates assembly language : a simpler language where each statement describes one machine language instruction in a mnemonic form. The binary instruction above, for instance, looks like this in ARM assembly language:
ADDSEQ R2, R1, #42
Now, that's not exactly human-readable either, but it sure beats "00000010100100010010000000101010". Once you learn the mnemonics and the funny punctuation, you can write your machine language program in assembly, use an assembler to translate it to real machine language, and then run the translation.
So, assembly statements map directly to machine language instructions, and practical machine language coding is always done in assembly language. As a result, "assembly" and "machine language" are commonly used as near synonyms. From here on, we'll use "assembly" as a generic term covering both assembly language and machine language.
In a later module, we'll examine, bit-by-bit (literally), a number of ARM machine language instructions. But for now, let's briefly take apart the instruction we've been using as an example. The point is not to understand it completely, but rather to get an impression of how a machine language instruction is put together, and how its pieces relate to the parts of the corresponding assembly instruction.
The 32 bits in "00000010100100010010000000101010" are actually 6 groups or fields of bits, all strung together, each describing a different aspect of the instruction:
0000 0010100 1 0001 0010 000000101010
The first three bit fields correspond to the ADDSEQ part of the assembly instruction:
The next three bit fields correspond to the "R1, R2, #42" part of the assembly. They tell what data to add, and where to put the result. Two of them deal with registers , which are memory locations inside the CPU used to store temporary values during computations. (Much more on registers later.)
If you're up on your binary math, you might recognize that those final three fields are the numbers 1, 2, and 42 in binary form. But if that's not obvious, wait a bit and we'll go over binary math in detail.
Before we get into details, it's worth stepping back and asking why we would study assembly programming in the first place. There are several good reasons to do so, but first, let's note two "reasons" that are actually invalid.
"There's a lot of demand for assembly programming". No, there isn't. Very little code is written directly in assembly these days.
"If you want the fastest possible program, you need to write it directly in assembly, not a high-level language". No, you don't. A modern optimizing C compiler will produce machine language that's faster and tighter than anything you could write. And that compiler can be almost spooky-smart about optimizing subtle aspects of the code you'd never even consider. Some people describe C itself as the "21st century assembler".
So, if there's not a lot of assembly programming done, and if a good C compiler produces the fastest possible code, why bother with assembly? Here are four reasons, one practical and three educational:
First, there are some actions you can do in assembly that you can't do in any high-level language, even C. These specialized operations arise often in embedded systems and in operating systems, and when they do, assembly programming is unavoidable.
A few examples:
Second, understanding assembly shows how the machine works behind the scenes, and thus informs high-level programming, and compiler design.
Third, assembly programs clearly illustrate underlying runtime data structures such as runtime stacks, memory segments, etc. We'll see this in the code examples that follow.
Fourth, assembly programming is excellent exercise for dealing with binary concepts, including numerical notations, bit operations, shifts, etc.
In the next segment, we'll go over binary numbers and some other fundamentals, before moving on to assembly code examples.