Codementor Events

Learning Assembly Language

Published Jun 12, 2019Last updated Dec 08, 2019

Assembly language is the human readable equivalent to the lowest software level of computer programming — machine code.

While the computer understands all programs as numbers, where various different numbers instruct the computer to do different operations, this is too tediuos for human consumption (let alone authoring).  Therefore, humans program using assembly language, which has an almost 1:1 correspondence to machine code.

Prerequisits:

Learn the C programming language.  If you know C (or C++), assembly will be easier.

C# and Java are good languages, but there few-to-no tools to show you what C# and Java code looks like in assembly language.  (In fact Java and C# have their own byte code assembly language that is rather different, and distant, from real hardware CPU instructions set architectures.)

From C (and C++) code we can use the compilers to show us what the assembly language looks like.  C is a low-level language, and if you know how each construct in C translates into assembly language, you'll know assembly language!  Examples of the C constructs we want to know for assembly language are:

C Construct Assembly Language Equivalent
local variable CPU register or stack memory location
global variable access global or const memory location
array reference a pattern of addition and memory access
goto unconditional branch instruction
if statement a pattern in if-goto: conditional test & branch instructions
while loop a pattern in if-goto
assignment modification of register or memory
arithmetic arithmetic instructions
parameter passing modification of register or stack memory
parameter receiving access register or stack memory
function call call instruction
function entry prologue pattern
function exit epilogue pattern
return value register modification and epilogue

How Assembly Language Differs from other Programming Languages

In assembly language, you'll be exposed to the bare resources of the underlying CPU hardware.  (Further, things we can write in C in one line take several lines in assembly.)

  • You'll be aware of things like CPU registers, and the call stack — these resources have to be managed in order for a program to work (and in other languages, these resources are automatically managed for us.)  Awareness of these resources is critial to function calling and returning.  (CPU registers are scace in comparison with memory, however, they are superfast, and also consistent in their performance.)

  • There are no structured statements in assembly language — so, all flow of control constructs have to be programmed in an "If-Goto" Style (in other languages, the compiler translates structured statements into If-Goto.)  The if-goto style uses labels as targets.  (While C supports the use of goto and labels, programmers prefer structured statements most of the time so we don't often see goto's and labels in C.)

Assembly Language vs. Machine Code

Assembly language is meant for humans to both read and write.

Labels

In assembly language, we use lots of labels.  The usages of the labels translates into numbers in machine code and the labels disappear in the machine code.  Labels (vs. using numbers directly) make it possible to make small changes programs — without them even a small change would actually be a huge change.

The processor needs to know where an operand is, perhaps a data operand or perhaps a code location.  For code locations, for example, typically the processor will want to know how far away that items from the code location it is currently executing — this is called pc-relative addressing.  So, to execute an if-then statement, we might tell the processor: under certain conditions, skip ahead N instructions, where the N represent the size or count of instructions to be skipped (or not), i.e. N is the size of the then-part — that would be in machine code.  If we were to add an instruction to the then-part, the number of instructions to skip goes up (and if we remove an instruction from the then-part, goes down).

Using labels automatically accounts for changes (e.g. adding, removing instructions) — a branch instruction that uses a label as the target accomodates changes in the count of how many instructions are to be conditionally skipped.

Pseudo Instructions

Most intructions in assembly language translate 1:1 into machine code instructions.  However, occasionally, we have to tell the assembler something that doesn't directly translate into machine code.  Typically examples are telling the assembler that the following instructions are code (usually via a .text directive) vs. data (usually via a .data directive).  Like regular instructions, these instructions are typically written on their own line, however, the inform the assembler rather than generate machine code instructions directly.

Writing Assembly Language

Similarities with C

In C, we write statements; these statements run consecutively one after another, by default sequentially, until some control flow construct (loop, if) changes the flow.  In assembly language the same is true.

In C, statements have effects on variables — we can consider that separate statements communicate with each other via variables.  The same is true in assembly language: instructions communicate with each other via the effect they have on variables, though as there are more instructions (necessarily so, as the instructions are simpler), there will also necessarily be more variables, many are very short-lived temporaries used to communicate from one instruction to a next.

Basic Assembly Langauge Programming

We alternate between register selection and instruction selection.  Since assembly language instructions are fairly simple, we often need to use several instructions, yes, interconnected via values as variables in cpu registers.  When this happens, we will need to select a register to hold intermediate results.

Register Selection

To select register, we need a mental model of which registers are busy and which are free.  With that knowledge we can choose a free register to use to hold a temporary (short-lived) result.  That register will remain busy until our last use of it, then it returns to being free — once free it can be repurposed for something completely different.  (In some sense, the processor doesn't really know we're doing this constant repurposing as it doesn't know the intent of the program.)

Instruction Selection

Once we have established the resources to be used, we can choose the specific instruction we need.  Often we won't find a instruction that does exactly what we want, so we'll have to do a job with 2 or more instructions, which means, yep, more register selection for temporaries.

For example, MIPS and RISC V are similar architectures that provide a compare & branch instruction, but they can only compare two cpu registers.  So, if we want to see if a byte in a string is a certain character (like a newline), then we have to provide (load) that newline value (a numeric constant) in a cpu register before we can use the compare & branch instruction.

Function Calling & Returning

A program consists of maybe thousands of functions!  While these functions have to share memory, memory is vast, and so they can get along.  However, with one CPU there are limited and fixed number of registers (such as 32 or 16), and, all the functions have to share these registers!!

In order to make that work, software designers define conventions — which can be thought of as rules — for sharing these limited fixed resources.  Further, these conventions are necessary in order for one function (a caller) to call another function (a callee).  The rules are referred to as the Calling Convention, which identifies for caller/callee, where to place/find arguements and return values.  A calling convention is part of the Application Binary Interface, which broadly tells us how a function should communicate with the outside world (other functions) in machine code.  The calling convention is defined for software usage (the hardware doesn't care).

These documents tell us what to do in parameter passing, register usage, stack allocation/deallocation, and returning values.  These documents are published by the instruction set provider, or compiler provider, and are well-known so that code from various places can interoperate properly.  The published calling conventions should be taken as given, even though the hardware doesn't know about it.

Without such documentation, we wouldn't know how to do these things (parameter passing, etc..).  We don't have to use standard calling conventions, but it is impractical to invent our own for anything but a simple toy program run on a simulator.  Rolling our own calling convention means no interoperability with other code, which we need to do in any realistic program, like making system and library calls.

The hardware doesn't know or care about the software usage conventions.  It just blindly executes instruction after instruction (really fast).  For example, to the hardware, most registers are equivalent, even though the registers are partitioned or assigned particular uses by the software conventions.

Dedicated Registers

Some CPU registers are dedicated to specific purposes.  For example, most CPU's these days have some form of stack pointer.  On MIPS and RISC V the stack pointer is an ordinary registers undifferentiated from the others, except by definition of the software conventions — the choice of which register is arbitrary, but is indeed chosen, and this choice is published so we all know which register it is.  On x86, on the other hand, the stack pointer is supported by dedicated push & pop instructions (as well as call and return instructions), so for the stack pointer register there is only one logical choice.

Register Partioning

In order to share the other registers among thousands of functions, they are partitioned, by software convention, typically into four sets:

  • Registers for passing parameters
  • Registers for receiving return values
  • Registers that are "volatile"
  • Registers that are "non-volatile"

Parameter & Return Registers

Parameter registers are used for passing parameter!  By adhereing to the calling convention, callee's know where to find their arguments, and callers know where to place the argument values.  Parameter registers are used for parameter passing by the rules set forth in the calling convention, and, when more parameters are passed than the convention for the CPU allows, the remainder of the parameters are passed using stack memory.

Return value registers hold function return values, so when a function completes, its job is to have put its return value in the proper register, prior to returning flow of control to the caller.  By adhereing to the calling convention, both the caller and callee know where to find the function return value.

Volatile & Non-Volatile Registers

Volatile registers are free to be used by any function without concern for the previous usage of or values in that register.  These are good for temporaries needed to interconnect an instruction sequence, and, they can also be used for local variables.  However, since these registers are free to be used by any function, we cannot count on them holding their values across a function call — by (software convention) definition!  Thus after function returns its caller cannot tell what values would be in those registers — any function is free to repurpose them, though with the knowledge that we cannot rely on their values being preserved after a called function returns.  Thus, these registers are used for variables and temporaries that do not need to span a function call.

For variables that do need to span a function call, there are non-volatile registers (and stack memory, too).  These registers are preserved across a function call (by software that adheres to the calling convention).  Thus, if a function wishes to have a variable in a fast CPU register, but that variable is live across a call (set before a call, and used after the call), we can choose a non-volatile register, with the (calling convention defined) requirement that the register's previous value is restored upon return to its caller.  This requires saving the non-volatile register's current value (into stack memory) prior to repurposing the register, and restoring it (using the previous value from stack memory) upon return.

Terminology & Resources

Wikipedia Articles
Calling convention
Application Binary Interface
Instruction Set Architecture

Codementor Articles
Tranforming Structured Code into If-Goto Style

Copyright (c) 2019 Erik L. Eidt

Discover and read more posts from Erik Eidt
get started
post comments3Replies
Curt Harvey
5 years ago

I coded IBM 360 assembler back in the late 70s through the mid 80s before I was dragged, kicking and screaming, into client server. I’m curious about the reason for this post. It does not offer enough information to teach anyone how to do anything with assembler, nor does it give any useful insight in why anyone would be motivated to learn assembly language.
Just curious…

Erik Eidt
5 years ago

Hi Curt, my clients come to me already motivated to learn assembly, so I don’t need to do that! However, many beginners need some basic information about how assembly language is different from high level language. We have to start somewhere!

Curt Harvey
5 years ago

I’m glad, though surprised, that there are still opportunities for assembly language programming. I’ll be retiring one of these years so perhaps I’ll touch bases with you to see if I can get back into it.
Java can be so boring at times, but it’s still a lot more fun than BASIC ;-)