«

»

Nov
24

An Initiation to the Secret Society of Assemblers

One of the best skillsets I have learned over the past decade has been assembler programming.  I have long been an avid fan of programming in C, however, like many people, I often struggled with pointers and managing memory.  It was only after I began programming in assembly that I really developed an appreciation for the operations on the machine level.  Once I understood what these operations were truly doing and how the language was translated into machine instructions, my programming in C improved almost overnight.

In this series of posts, I am going to attempt to impart some of my knowledge in programming in assembly.  I’m planning to do two series, back to back.  The first will be on an introduction to assembly programming on a RISC architecture machine, using MIPS.   The following series will be on x86 programming using IA32.  I’ll focus on presenting the AT&T syntax (used by GCC) as there are very few resources online for this assembly; however, I’ll also intermix some of the more common Intel syntax (used by MASM on DOS/Windows and NASM on Linux).  If I do not get bogged down again with course work, I would also like to chase these two with a follow-up series on the Intel64 (AMD64) assembler.

First off, I would like to list a few reasons why learning an assembler is still practical in today’s world.  As a Teaching Assistant for the Computer Science department, I often get glares and questions from students when I mention that they will have to study anything lower level than Java.  We have a lot of students who complain with a sort of visceral anguish over having to code in C, which is something perceived to be obsolete and useless in the modern computing society.  With this sort of near universal panning of C for being low-level, assembly must seem even more so absurd to learn for programming purposes.  It is not so ironic, however, that our biggest supporters typically come from across the hallway by the Electrical Engineering students.

Assembly is, on its face, nothing more than a language of mnemonic codes that can be translated one for one with machine instructions.  Now this is, of course, a bit of an over-simplification as some modern assemblers are built on more complicated macros that provide the programmer with some additional capacity.  That said, assembly exists, for all purposes, at that one-to-one level with machine programming.  Every line of assembly you write will be loaded onto the processor at some point and executed atomically.

So, how does this become a beneficial skill to know?  First, if you are working with any custom hardware or using any microcontrollers, odds are that you will not have any of these high level languages around to let you program on.  The team who have created the microcontroller or microprocessor have designed hardware in such a way that it responds to various states natively.  These states, represented by high and low levels on individual wires, are passed into multiplexers and are used to change the control logic on the Arithmetic Logic Unit (ALU), to select the registers for use, to select the memory location to fetch for usage, and so forth.  These individual bits of data are routed throughout the processor to control the instruction execution, input, and the output of data.

Setting these bits used to be a mechanical process and was done on the front panel of computers.  An operator would have a table of binary codes that equated to specific instructions and codes that represented registers, then they would use physical switches to set wires to high and low levels and then another button to advance the clock to execute the instruction and give the operator time to input the next instruction.  This process was automated using a technique from the player piano days by using punch cards to pre-load the sequence of switch inputs for an instruction.  These cards would be fed through a reader like a deck of cards and each one would set the processor’s next instruction.  On modern systems, this has been replaced by digital means, using bits to represent individual switch states in an instruction.  These bits are still encoded using the same techniques.  For example, the following is a machine language line of code for a RISC processor:

000000 01010 01001 01000 00000 100000

This sequence is loaded into the processor and separated out.  The first block 000000 is sent into a Control multiplexer.  This multiplexer translates the sequence into a bit that enables the ALU Control Unit.  The second block consists of 5 wires and is sent into the Register Control unit.  This unit activates the register $t2 and enables it to send its full data to the first input of the ALU.  The third block activates register $t1 and enables it to send its full data to the second input of the ALU.  The fourth block selects register $t0 as the destination for the output of the ALU to be stored into.  The fifth block is fed into a sign extender and into a shifter to set how much to shift the value by.  This result is then enabled or disabled based on the operation.  In this case, the operation is not a shift operation, so it is not used.  The final block is the arithmetic operation to be performed.  These six wires are fed into a multiplexer that drives the selector of the ALU.  This case selects addition in the ALU as the operation to perform.

In the end, this cryptic sequence of numbers sets 32 physical wires to either high or low states to configure the processor to perform a single operation.  Once this operation completes, the resulting data from the ALU is sent back into the register control block, which will store the data into a register or into physical memory as specified by the instruction.  For programming purposes, we can code entirely in these sequences of bits, however, it is very difficult to accurately remember and use sequences of bits, and it is even harder to debug by visual inspection.  This problem gave birth to a very simple solution: use mnemonic codes.  Each set of the above numbers can be replaced by a simple human-readable code of ASCII characters.  The assembler can then read the codes and then translated them back to the binary digits for the machine to use.  The above sequence of bits is normally written using the MIPS assembler in the following way:

add $t0, $t2, $t1

This is a much simpler way to write programs!  This contains all of the data needed by an assembler to translate it back to the above binary sequence.  Notice here that there are only four symbols, whereas above there are six binary strings.  Since the add instruction does not use the shifter (shamt) field, it is omitted in programming.  The assembler translates add back to 000000 SSSSS TTTTT DDDDD 00000 100000, where the SSSSS is the source register code ($t2 = 01010), TTTTT is the second source register code ($t1 = 01001), and DDDDD is the destination register code ($t0 = 01000).  These three register codes are manually specified in the instruction, but the first, fifth, and sixth bit strings are added directly from the operation code alone.

So, assembly is just machine code with mnemonics for human readability.  How is this a good thing to know these days?  Going back to an earlier thought, microcontrollers and custom processors all use the same sort of physical stages that I described earlier.  The problem is that once these chips are manufactured, there is often little to no direct programming support for them.  Programming on these custom chips is commonly done by taking machine code (which is unique to this processor) and loading it onto an EEPROM or some other form of memory that is incorporated into the controller.  This machine code then executes at power-on of the processor.  Often, this execution is done without any form of an operating system, it is merely your code that runs at startup.  Since this machine langage is custom, finding a compiler that can produce this machine code can be impossible.  So, you are limited in your programming to using the manufacturer’s assembler and their mnemonics.  Knowing the fundamentals of assembly programming can save you if you want to do programming for certain custom hardware.

If you want to be a hero and raise to the level of demi-god, there is another really cool thing you can do at this stage.  We all know that compilers will take a high level langage, such as C, and convert them into machine language executables, ready to run on a target machine.  Compilers accomplish this in multiple steps, however.  The first step is to do a syntactical analysis of your high level code, to ensure that you are issuing correct instructions.  The second step of a compiler is to do semantic analysis to interpret the meaning of each of your instructions.  This step is accompanied by various levels of optimization, however, it will result in Intermediate Code (IC) generation.  This IC is commonly in the form of assembly!  Your compiler is converting your high level language into assembly.  This assembly is then assembled into native machine language.

If you wanted to be cool, you could then take this new microcontroller, with its custom assembly, and write a compiler that will compile C (or some other C-like language) into your custom assembly.  This would enable you to program natively on your custom hardware in C!  This is commonly done by the manufacturers, who will release their hardware along with a custom version of gcc or some other compiler for the architecture.  Even with this custom compiler, you will often not have full access to hardware!  There are limitations to the C language and many of the custom hardware operations will have no equivalency.  So even with a custom compiler, it is often necessary to do some inline programming in assembly.  This entails writing your code in both C and assembly as needed.  For example:

#include<stdio.h>
int main(void){
int x = 1337;
int y = 42;
printf(“X is %d, Y is %d\n”, x, y);
asm (“movl %%eax, %%edx;”
“movl %%ecx, %%eax;”
“movl %%edx, %%ecx;”
:”=a”(x), “=c”(y)
:”a”(x), “c”(y)
:”%edx”);
printf(“X is %d, Y is %d\n”, x, y);
return 0;
}

This is a simple C program that uses inlined assembly (IA-32 AT&T syntax).  This is a trivial example to show how this can be accomplished in a program.  In this case, I’m declaring two integers in C, then swapping their values in Assembly, and verifying the swap in C again.  This will result the expected output:

kandrea@zeus:~$ ./at
X is 1337, Y is 42
X is 42, Y is 1337

This is a crucial trick to know how to do if you plan to explore some of the non-standard features of a processor that you are programming for in a high level language.

Finally, in addition to programming for custom hardware or writing a compiler, assembly is essential to know if you plan to do any reverse engineering of code.  Decompilers exist for C, but they are by and large un-useful because compilers excel at optimizing away logic.  Logic is irrelevant to a compiler; all it cares about is speed and accuracy.  I once wrote a very long C program to show myriad examples of assembly operations for a student.  I compiled the code and disassembled it to show the student the assembly, but was shocked when this was all I found:


pushl %ebp
movl %esp, %ebp
movl $0, %eax
movl %ebp, %esp
popl %ebp
ret

The compiler inspected my code and saw that I was only using local variables inside of a function.  These local variables were never returned or referenced anywhere.  There were no printf statements and none of these values resulted in any meaningful change to the system.  The compiler realized this and classified the entire function as a non-op, so it stripped out all of my code and replaced it with a single line to return 0.  While this is perhaps the most extreme example of optimization, your compiler will frequently destroy all logical meaning from your original C code and reorder things around just enough to be un-recognizable in the assembly.  The only saving grace here is that the operations will result in the same output, meaning, with a solid knowledge of assembly, you can interpret the tea leaves of disassembled functions and determine what they are doing.  From this analysis, you can often come up with an equivalent function in C.  This level of reverse engineering is actually best performed from the assembly directly.

Of course, these reasons to learn assembly also gloss over the comprehension aspects of understanding assembly programming.  My ability to program in all languages was greatly benefited from learning how to program in assembly.

I will leave this introductory post here and will begin the series on programming in MIPS for a RISC architecture with my next post.  I will be using a simulator (SPIM) and will include enough code and sample programs to demonstrate some of the fundamental programming constructs.

Kevin Andrea (“Demi-God of CS367″)

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>