Assembly Terminology & Flow Control
CS 301 Lecture, Dr. Lawlor, 2005/09/16
Assembly Terminology
A "Register" is just an on-chip place to
store data. It's not memory. They're stored right on the
chip, they're incredibly fast (access is 0 clocks--even faster than
cached memory) and they're only accessible via assembly. There
are many sorts of registers:
- The "Program Counter" (PC), or "Instruction Pointer" (IP) stores
the address of the next instruction to execute. Sometimes the PC
isn't easy to directly read or write; other times, like in UEMU, it's
not a very special register (just register 0xF, no different from 0xE
except the CPU reads from it).
- "Special Purpose" registers are shadowy weird things used by the
operating system, for interrupt handling, fault detection, performance
monitoring, etc. Access is normally via weird specialized
instructions. We'll talk more about these later.
- "General purpose" registers are everything else--used by assembly
programmers and compilers to get work done. Ancient machines only
had one general-purpose register (called "the accumulator"). UEMU has 16 registers, but one's always the program counter, so only 15 are general-purpose. x86
has a bunch of registers, but only maybe 6 are really general-purpose
(depending on your definition of "general"). PowerPC has 32
general-purpose registers. IA64 (Itanium) has 128 general-purpose
registers.
An "Instruction" is a binary code that tells the CPU to do
something. On modern machines, instructions are always an
integral number of bytes long. On "RISC" machines like UEMU,
PowerPC, MIPS, or Itanium, instructions are fixed-size: often 32 or 64
bits. On older "CISC" machines, like x86 or m68k, instructions
can be as short as one byte, or dozens of bytes long. Instruction
encoding terminology includes:
- "Opcode", the first chunk of the instruction that tells the
machine generally what to do. The opcode determines how the
remaining bits are interpreted.
- "Immediate", describes a value stored right in the instruction,
not in memory or a register. E.g., "0x93 is the ADD IMMEDIATE
opcode. It is followed by 8 bits of immediate data, so 0x93 01
would add 1".
Flow Control--Simple Conditionals
So you're running along: loading up registers with immediate and user
values, doing arithmetic, and printing stuff out. That's all easy
enough. But what happens when you want to write an "if" statement?
if (a<b) {
c=a+b;
}
In UEMU, the *only* conditional is
the 0xC opcode--if register[j]>=register[k], increment
register[i]. We can turn this into a conditional "skip next
instruction" by using the program counter (register F) as the
destination. For example, the instruction 0xCFAB increments the
program counter, and skips the next instruction, *if* register
A>=register B. If the body of the "if" statement is just one
instruction, we *want* to skip that instruction if the condition
doesn't hold!
So overall, we can translate the above "if" into
0xCFAB /* if (a>=b), skip the next instruction */
0xACAB /* c=a+b; */
which, if a<b, fails the test and just proceeds to the addition
normally. If (!a<b), which is the same as (a>=b), then the
increment actually fires, and the addition is skipped. The best
way to check if you've coded a conditional properly (or any piece of code) is to imagine executing the thing, step by step.
Complicated Conditionals
The above trick works fine if the body of the conditional is only one
instruction, but what if it's longer? The standard (bizarre)
solution is to set up a weird nest of JUMP (or goto) statements, like
this (in C) translation of "if (A) ... else ...". It's pretty
easy if you can flip the test around as above:
if (!A) goto END; /* skip "then" block if comparison failse */
... long bunch of stuff when the if succeeds ...
END: ... rest of program
This trick also makes it easy to do short-circuit boolean operations,
like "if (A&&B)", where we want to immediately give up if A or B is
false.
if (!A) goto END; /* A failed */
if (!B) goto END; /* B failed */
... long bunch of stuff when the if succeeds ...
END: ... rest of program
If you really need the test to go the right way around, you can do the "skip THEN" trick:
if (A) goto THEN; /* executes "then" block if comparison succeeds */
/* otherwise */ goto END; /* skips the "then" if the comparison fails */
THEN: ... long bunch of stuff when the if succeeds ...
END: ... rest of program
The basic idea is to use goto statements (since they're only one
instruction long) to splice the "THEN" part of the if out of the rest
of the program. In most assembly languages, a comparison does a jump
if it succeeds, so the UEMU conditional skip-followed-by-jump are a
single instruction.
The "skip THEN" trick also works for short-circuit boolean OR, like "if (A||B)":
if (A) goto THEN; /* executes "then" block if A true */
if (B) goto THEN; /* executes "then" block if B true */
/* otherwise */ goto END; /* skips the "then" if the comparisons fail */
THEN: ... long bunch of stuff when the if succeeds ...
END: ... rest of program
The goto trick makes it easy to add an "ELSE" block too:
if (...) goto THEN;
/* otherwise */ goto ELSE;
THEN: ... long bunch of stuff when the if succeeds ...
goto END;
ELSE: ... long bunch of stuff when the if fails ...
goto END;
END: ... rest of program