Nowadays, the idea that computers do so many things by only interpreting and processing a sequence made of 0’s and 1’s is widely spread. But how can we truly understand how this works?
Previously to the first appearance of assembly language, implemented in the Electronic Delay Storage Automatic Calculator (EDSAC) computer back in 1949, programmers had a non-intuitive and exhaustive work of developing programs using numeric codes for each specific operation. In this tutorial, we’ll introduce the Assembly language.
Before getting into details, we should have a clear idea of the levels of programming languages and how the architecture of a computer looks like.
2. Theory Background
The reason why we can use computers for such a variety of applications is that they are flexible and can be programmed to store and perform calculations using the Central Processing Unit (CPU) in different types of codes and paradigms.
2.1. Programming Languages
Considering the level of abstraction and programming languages, machine code has the lowest possible level of abstraction. This is the sequence of 0’s and 1’s that we mentioned in the introduction. We, as humans, cannot take a quick look and understand what is going on. Oppositely, these languages are easily understood and processed by a computer.
Let’s climb one step in the abstraction level, and now we have the low-level programming languages in which Assembly languages are located. At this level, we can create our code using a more readable set of instructions which are now represented by mnemonics instead of large numbers. To convert an assembly code to machine code, an Assembler is used.
Lastly, we have high-level programming languages, which are close to the natural language that we use in our lives. In this group, we can find languages such as Java, Python, C++, etc. Code written in this level is converted to machine code by compilers or interpreters, depending on the language.
2.2. Computer Architecture
Most of the computers that we use are built with a processor designed following the John von Neumann Architecture containing: a memory unit, an Arithmetic/Logic Unit (ALU), and a Control Unit (CU).
The main idea is that the data can be stored in the same space as the programs. In this way, the machine will be capable of manipulating both of them speedily.
The memory unit has several registers, which are high-speed, temporary memory available for the CPU to perform its required operations. We will take a closer look at an assembly code, and it will be clear how the registers are used to achieve the desired goal:
3. Code Example
To illustrate how the CPU will modify the values in the registers, we will compare two simple MIPS assembly programs to their high-level implementation code achieving the same goal.
We chose the MIPS language for the sake of simplicity, but several assembly languages exist, such as Intel x86, ARM, and SPARC.
First, we need to clarify that when we invoke the syscall method in an assembly code, the system will perform the actions accordingly to what is stored in its registers.
Let’s start with the Hello World version for assembly:
.text # code section .globl main # starting point: must be global main: li $v0,4 # code 4 loaded on the register v0 indicating that we will print a string la $a0, msg # the address to the string is loaded on the a0 register syscall # system routine will call the function to print string li $v0,10 # code for exit is loaded on register v0 syscall # the syscall will finish the program .data # data section msg: .asciiz "Hello World!\n"
Now, we’ll write the Python code version for the Hello World application:
In the second example, we’ll write a code that receives an integer and returns the sum from 1 to , so if the input is 4, the output will be 10 since 4+3+2+1 = 10.
.text .globl main main: li $v0,4 # code 4 loaded on the register v0 indicating that we will print a string la $a0, msg1 # the address to the string msg1 is loaded on the a0 register syscall # system routine is called and the string is printed li $v0,5 # code 5 loaded to read a value from the user input syscall move $t0, $v0 # the value of N is stored on t0 li $t1, 0 # counter i initialized li $t2, 0 # sum initialized to 0 loop: addi $t1, $t1, 1 # i = i + 1 add $t2, $t2, $t1 # sum = sum + i beq $t0, $t1, exit # branch if equal: jump to exit if i != N, otherwise continue j loop # Jump to loop exit: li $v0, 4 # code 4 loaded on the register v0 indicating that we will print a string la $a0, msg2 # the address to the string msg2 is loaded on the a0 register syscall li $v0,1 # code 1 loaded on the register v0 indicating that we will print a integer move $a0, $t2 # the address to the sum to be printed is loaded on the a0 register syscall li $v0,4 # code 4 loaded on the register v0 indicating that we will print a string la $a0, lf # the address to the string lf is loaded on the a0 register syscall li $v0,10 # exit syscall .data msg1: .asciiz "\nNumber of integers N? " msg2: .asciiz "\nSum = " lf: .asciiz "\n"
In Python, the following code would output the same result:
def example(n): return sum(range(n+1)) n = int(input("Number of integers N? ")) print("Sum = ", example(n))
We could have done a more complex version of the Python code using loops, but in order to make the difference between the two codes even bigger, we chose an optimized version for the last code.
We can imagine how hard it would be to develop complex applications such as 3D mobile games or webpages using only assembly language. A simple task, as we saw in the second example, is achieved in a code with a number of lines more than 7 times bigger than the same program in a high-level language.
Embedded systems and applications that have real-time and high-performance requirements or even limited computational power are usually designed in such low-level languages in order to optimize the resources and to have complete control of what happens in the CPU.