DEV Community

Imaculate
Imaculate

Posted on

When do you ever need assembly?

If the objective of your application is to print to screen, there are better ways to achieve it. Assembly is the least qualified language for the job. Below are just few examples to demonstrate this.

Python

print("Hello world")

Go

import "fmt"
func main() {
    fmt.Println("hello world")
}

C#

using System;
public class HelloWorld
{
   public static void main(String[] args)
   {
       Console.WriteLine("HelloWorld!");
   }
}

C++

#include<iostream>    
using namespace std;
int main() 
{
    cout<<"Hello World";      
    return 0; 
}

Hardly 7 lines of code even for C++. If like me you've ever wondered what happens under the hood, read on. What if the above language API's were not available? What if all you had were registers, Memory, Operating System (OS) and Assembly compiler and linker. For simplicity this will be a Intel-based X-86 Unix System.

Lets start with basics, how would you print a character to screen?
We will have make use of the kernel services which in-turn interfaces with the hardware. These services are available as subroutines called syscalls and are invoked by raising events called interrupts. When an interrupt is raised,
program execution is paused and control is transferred to the CPU which reads specific registers to determine the action to take, after which program execution resumes where it left of.

Of the different syscalls, of interest to us is the one that displays characters to the monitor: sys_write. To invoke this, eax register has to have 4(system call number), ebx register has to have 1 to specify stdout output, the address of word to be written need be placed in ecx register and the length of the word need be in register edx. The problem is now simple enough; define the string, place it's address in ecx, place its length (13) in edx, place 4 in eax, place 1 in ebx then raise an interrupt. The program below should do the trick.

section .text        ; sections required for 
   global _start     ;must be declared for linker (ld)

_start:             ;entry point marker, required for linker
   mov  edx,len      ;message length
   mov  ecx,msg     ;message to write
   mov  ebx,1       ;file descriptor (stdout)
   mov  eax,4       ;system call number (sys_write)
   int  0x80        ;call kernel

   mov  eax,1       ;system call number (sys_exit)
   int  0x80        ;call kernel

section .data
msg db 'Hello, world!', 0xa  ; string to be printed followed by newline
len equ $ - msg     ;gets length of the string and stores it in variable len

This does the job, but still raises questions, it almost seems as if the invoked functions and interrupts have magic. If CPU's work with bits, how does it print strings? After an interrupt, how does the CPU where to resume to? How would this change if the registers were 16 bit wide? These are valid questions that should probably be addressed in another post. A major concern you may have as a high-level language developer is why do this at all? High-level languages exist for good reason; to increase developer's productivity, allow them to focus on business logic instead of registers.

Correct! We shouldn't be writing applications in assembly, but we should know about language internals. I've learnt a thing or two just from the above example.
Look at Java snippets below.

System.out.println("**************")
System.out.println("Welcome user!")
System.out.println("**************\nWelcome user!")

Two ways of achieving the same objective, but after reading assembly above, I'm inclined to prefer the latter snippet. In the absence of compiler optimization, the second program will be more performant because it requires only one interrupt to write to screen. For each interrupt, the CPU has to save context to Stack, perform a syscall, then resume context; the less it happens, the faster the program. Performance may not be important if the block is not called often but it is a major problem if it is called in a loop or responds to frequent requests or worse responds frequently in a loop.

Do you need assembly for your job? Probably not. Will it make you more productive? Hard to say. Will it make you a better developer? Absolutely yes! Happy hacking!

Top comments (3)

Collapse
 
aeiche profile image
Aaron Eiche

While many/most of us will probably never use Assembly, I think understanding it, and understanding how microprocessors work if really valuable.

Incidentally, I've been looking at NES programming, which is done Assembly. It's really fascinating.

Collapse
 
imaculate3 profile image
Imaculate

Programming games in Assembly sounds like a lot of fun!

Collapse
 
connellpaxton profile image
connell-paxton • Edited

Just as a heads up, you forgot a newline for the c++ one,
it would be best to write it like:

#include <iostream>
int main() {
    std::cout << "Hello World" << std::endl;
}

Also it is worth noting that the Java snippets you
showed actually doesn't translate to the assembly,

Heres why:

Java is run on something called the "JVM" (Java Virtual Machine)
The JVM interprets byte code.
In a normal (compiled) language the compilation pipeline looks something like this:

Source Code
|        |       | compiler
|        |    IR (internal Representation)
|        |       |
|    Object File
|        |  linker
Executable

(there are compilers that are "one-pass" such as tcc which skip the two intermediate states)

In Java, it goes from source into bytecode

Source Code
   |
Byte Code

Our executable from the compilation pipe is run like this:

+---------------+
|  Executable   |
+---------------+
| CPU | Kernel  |
+-----+---------+

(its kinda hard to show this, but essentially the kernel runs on the CPU, but doesn't interprete the executable, instead it loads it into memory and responds to any interrupts)

But in the Java example, that goes one level Higher:

+---------------+
|    ByteCode   |
+---------------+
| Java Runtime  |
+---------------+
| CPU | Kernel  |
+-----+---------+

Where the Java Runtime is an executable that interprets the bytecode