Operator
Notation
Consider
the infix expression (X · Y) + (W · U), with parentheses added to
make the evaluation order perfectly obvious.
This
is an arithmetic expression written in standard form, called “infix form”. There
are two other forms, prefix and postfix, which are occasionally used in
software.
Infix
form has the operator in between two operands, as in “X + Y”. It is what we are used to,
but it is hard for computers to process.
The
two other forms are prefix and postfix.
I give the LISP variant of prefix.
Prefix: (+ ( · X Y) ( · W U) )
Postfix: XY·WU·+
Implicit
in each of these examples is the assumption that single letter variables are
used.
Each
of the prefix and postfix is easier for a computer to interpret than is infix.
The
assumption is that the expression is scanned either left to right or right to
left.
Here, we arbitrarily scan left to right.
We
return to this expression, but first consider a much simpler expression.
Problem with
Scanning an Infix Expression
Suppose
that we have a five symbol expression.
We are scanning left to right.
We assume: all variables are single
alphabetic characters
Each alphabetic
characters is taken singly and represents a variable
At
the beginning we have five unknown symbols:
We
read the first character and find it to be a variable: X
We
read the next character and find it to be an operator: X +
Something is being added.
We
read the next character and find it to be a variable: X + Y .
Can
we do the addition now? It depends on
what the next character is.
There
are two basic possibilities: X +
Y + Z
X
+ Y · Z
In
the first option, we can do the addition immediately, forming (X + Y) to which
Z is
later added. In the second option, we
must wait and first do the multiplication to get
(Y · Z) to which X is added.
Scanning a
Postfix Expression
With
the same assumptions, we scan a five symbol postfix expression.
At
the beginning we have five unknown symbols:
We
read the first character and find it to be a variable: Y
We
read the next character and find it also to be a variable: Y Z
At this point we need an operator.
We
read the next character and find it to be an operator: Y Z ·
Because this is postfix, the
evaluator can immediately
form the product term (Y · Z)
We
read the next character and find it to be a variable: Y Z · X
At this point, we need another
operator.
We have two operands: (Y · Z) and X.
We
read the last character and find it to be an operator: Y Z · X +
We do the addition and have the
term (Y · Z) + X.
The Stack
Data Type
Stacks
are LIFO (Last In, First Out) data structures. Stacks are the data structure most
naturally fit to evaluate expressions in postfix notation.
The
stack is best viewed as an abstract data type with specific methods, each of
which
has a well defined semantic (we can say exactly what it does).
We can also consider the stack as having a top. Items are added to
the stack top and then removed from the stack top.
The
position of the stack top is indicated by a stack pointer (SP).
More on this later; it has two different definitions, each of which is valid.
The
stack data type has three necessary methods, and two optional methods.
The
mandatory methods are Push, Pop, and IsEmpty.
The
standard optional methods are Top, and IsFull.
Note: When studying computer
networks, we speak of a “protocol stack”, which has
nothing to do with the ADT
(Abstract Data Type) stack that we study today.
We shall discuss the
TCP/IP protocol stack later in this course.
Pushing
Items onto the Stack
Consider
the sequence: Push (X), Push (Y), and Push (Z).
This sequence adds three
items to the stack, in the specified order.
Push X
Push
Y
Push
Z
After
the third operation, Z is at the top of the stack and is the first to be
removed
when a Pop operation is performed.
The
SP (Stack Pointer) is a data item internal to the stack that indicates the
location
of the top of the stack. It is never
made public by any proper stack implementation.
Evaluation
of the Postfix Expression Y Z · X +
Postfix
notation is also called RPN for Reverse Polish Notation.
Rules: 1. Scan
left to right.
2. Upon reading a variable, push it onto the stack.
3. Upon reading a dyadic operator, pop two variables from the
stack,
perform the
operation, and push the result.
We
read the first character and find it to be a variable: Y
We
read the next character and find it also to be a variable: Y Z
We
read the next character and find it to be an operator: Y Z ·
Evaluation
of the Postfix Expression Y Z · X +
Continues
We
read the next character and find it to be a variable: Y Z · X
We
read the last character and find it to be an operator: Y Z · X +
After
this, the stack will be popped and the value placed into some variable.
This
example just looked at the expression, without considering the assignment.
Stack–Based
(Zero Operand) Machine
Evaluate
the expression Z = (X · Y) + (W · U)
which must be evaluated in its postfix (RPN) version: XY·WU·+
Push X
X |
|
|
|
|
Push Y
X |
Y |
|
|
|
Mult
X·Y |
|
|
|
|
Push W
X·Y |
W |
|
|
|
Push U
X·Y |
W |
U |
|
|
Mult
X·Y |
W·U |
|
|
|
Add
X·Y +W·U |
|
|
|
Pop Z
|
|
|
|
|
More
Explicit Evaluation: W = Y · Z
It
might be good at this point to make explicit a point that many consider
obvious.
Suppose
that before the evaluation above, W = 0, Y = 10, and Z = 15.
The
push–pop code to execute this instruction will go as follows:
PUSH Y // Value of Y is placed on the stack.
PUSH Z // Value of Z is placed onto the stack.
MULT // Pop the top two values, multiply
them
// and push the result onto
the stack.
POP W
// Pop the value at stack top and place into W.
At
the start, here is what we have.
More
Explicit Evaluation: Part 2
PUSH Y // Value of Y is placed on the stack.
PUSH Z // Value of Z is placed onto the stack.
More
Explicit Evaluation: Part 3
MULT // Pop the top two values, multiply
them
// and push the result onto the stack.
POP W
// Pop the value at stack top and place into W.
Practical
Evaluation of Postfix Operators
Here are some steps for manual evaluation.
1. Scan the postfix expression left to
right. Find the first operator.
2. Determine how many operands that
operator requires. Call it N.
Select the previous N
operands, apply to the operator.
3. Remove the operator and its operands
from the expression.
Replace these with the
operands results.
4. Repeat
Here is an example, using single digit numbers to
start with.
6
5 + 4 2 – *
(6
5 +) 4 2 – *
(11)
4 2 – *
(11)
(4 2 –) *
(11)
(2) *
22
Practical
Evaluation of Prefix Operators
Note: We assume Lisp notation, with full use of
parentheses.
1. Scan the
expression left to right. If the
expression does not contain another
parenthesized expression, evaluate it.
Otherwise, attempt to evaluate its
subexpression. Formally, this is viewed as a tree traversal.
2. Keep “moving
up” until the last is evaluated. Here is
an example.
Again, it will start with single digit
numbers in order to be more easily read.
Evaluate the prefix expression: ( + ( * 4 2 ) ( – 5 2 ) ( * ( + 6 4) (+ 1 1 ) ) )
(* 4 2) can be evaluated, so: ( + 8 ( – 5 2 ) ( * ( + 6 4) (+ 1 1 ) ) )
(– 5 2) is defined as 5 – 2, so: ( + 8 3 ( * ( + 6 4) (+ 1 1 ) ) )
(+ 6 4) can be evaluated, so: ( + 8 3 ( * 10 (+ 1 1 ) ) )
(+ 1 1) can be evaluated, so: ( + 8 3 ( * 10 2 ) )
(* 10 2) can be evaluated, so: ( + 8 3 20 )
(+ 8 3 20) = 31, so: 31
Expression
Tree for the Prefix Evaluation
The expression is ( + ( * 4 2 ) ( – 5 2 ) ( * ( + 6 4)
(+ 1 1 ) ) ).
Software
to evaluate this prefix expression would first build this tree (not very hard
to do)
and then evaluate it. For example, the
node (+ 6 4) would be replaced by 10, etc.
Expression
Tree Evaluation
Single
Accumulator Machine
Evaluate the expression Z = (X · Y) + (W · U)
This requires an extra storage location, which I call
T for “Temp”.
Load X //
AC now has X
Mult Y // AC now has (X · Y)
Store T //
Now T = (X · Y). AC
still has (X · Y)
Load W //
AC now has W
Mult U // AC now has (W · U)
Add T //
AC now has (X · Y) + (W · U)
Store Z //
And so does Z.
Multiple
Register Machines
Evaluate the expression Z = (X · Y) + (W · U)
Standard
CISC Approach (There are many variants of this one.)
Load R1, X //
R1 has X
Mult R1,
Y // R1 has (X · Y)
Load R2, W //
R2 has W
Mult R2, U // R2 has (W · U)
Add R1, R2 //
R1 has (X · Y) + (W · U)
Store R1, Z // And so does Z
Load–Store RISC
Load R1, X //
R1 has X
Load R2, Y //
R2 has Y
Mult R1, R2 // R1 has (X · Y)
Load R2, W // R2
now has W
Load R3, U //
R3 has U
Mult R2, R3 // R2 has (W · U)
Add R1, R2 //
R1 has (X · Y) + (W · U)
Store R1, Z //
And so does Z.
Instruction
Types
There are basic instruction types that are commonly
supported.
1. Data
Movement (Better called “Data Copy”)
These copy bytes of data from
a source to a destination.
If X and Y are 32–bit real
numbers, then the instruction Y = X makes a
copy of the four bytes
associated with X and places them in the address for Y.
2. Arithmetic The standard arithmetic operators are
usually supported.
If division is supported, one
usually also has the mod and rem functions.
On business–oriented machines,
decimal arithmetic is always supported.
Graphics–oriented machines usually
support saturation arithmetic.
Real number arithmetic is
often not handled directly in the CPU, but by
a coprocessor attached to it.
Early
on (Intel 80486 / 80487) this was a cost consideration.
RISC machines follow this
approach in order to keep the CPU simple.
3. Boolean
Logic Here I differ from the book’s
description. Boolean instructions
are often used for non–Boolean
purposes, such as bit manipulation.
The
real Boolean instructions are the conditional branches.
More Instruction
Types
4. Bit
Manipulation These use
instructions that appear to be Boolean, but
in fact operate
differently. This is a distinction lost
on many students,
perhaps because it is rarely
important. More on this in a moment.
5. Input
/ Output The computer must
communicate with external devices,
including permanent storage,
printers and keyboards, process control devices
(through dedicated I/O
registers), etc.
A few
architectures have a dedicated input device and a dedicated output
device. All commercial machines have “addressable”
I/O devices; i.e., the
CPU issues a device identifier
that appears to be an address to select the device.
From
the CPU viewpoint, each I/O device is nothing more than a set of registers
(control, status, and data)
and some timing constraints.
6. Transfer
of Control These alter the normal
sequential execution of code.
At the primitive machine
language level, all we have is unconditional jumps,
conditional jumps, and
subroutine invocations. Higher level
languages elaborate
these features with
“structured constructs” such as conditional statements,
counted loops, and conditional
loops.
More on
Logical Instructions vs. Bitwise Instructions
This is quite important in C, C++, and Java.
Consider the standard implementation of Boolean values
as 8–bit bytes. This
is done for convenience in addressing by the CPU, as single bits are not
addressable.
Let A =
0000 0000 C =
0000 0010
B = 0000 0001 D = 0000 0011
Logical operators in C++ AND && Expression && Expression
OR || Expression || Expression
Bitwise operators in C++ AND & Expression
& Expression
OR | Expression | Expression
XOR ^ Expression ^ Expression
Logical Bitwise
A && B = 0 (FALSE) A & B = 0000 0000
A || B = 1 (TRUE) A | B = 0000 0001
C && D = 1 (TRUE) C & D = 0000 0010
C || D = 1 (TRUE) C | D = 0000 0011
Source: The Annotated
C++ Reference Manual (Sections 5.11 – 5.15)
Margaret Ellis and Bjarne
Stroustrup, Addison–Wesley, 1990.
A Context
for Bitwise Operators
For simplicity I consider a very old (late 1960’s)
Line Printer, a predecessor to today’s
laser printer. We examine the
Status/Control register for the LP–11.
This register is called “LPS” for “Line Printer
Status” in the literature.
We have here two status bits and a control bit.
Status bits
Bit 15 Error If Error = 1, then there is a device
error, such as
power
off, no paper in the printer, etc.
Bit 7 Done If
Done = 1, the printer is ready for the next line.
Control bit
Bit 6 IE If
IE = 1, the printer is enabled to raise an interrupt
whenever
Done becomes 1 or Error becomes 1.
More on the
LPS Register
Why this arrangement of bits?
The PDP–11, for which the LP–11 was used, did not
support 8–bit arithmetic.
A 16–bit integer was the smallest that the CPU would handle.
Viewed as a 16–bit signed integer, we note that the
error bit (Bit 15) is the
sign bit. To test for an error, we just
read the LPS into a register and test if it is negative.
Testing the
Done Bit
Recall that the Done Bit is bit 7 and that 0000 0000 1000 0000 is 0x0080.
LPS E000 0000 DI00 0000
0x0080 0000 0000 1000 0000
LPS & 0x0080 0000 0000 D000 0000
If ( 0 = = (LPS & 0x0080) ) then
the Line Printer is Not Done
Still More
on the LPS Register
Testing
and Setting the Interrupt Enable Bit
Recall that the Done Bit is bit 6 and
that 0000 0000 0100 0000 is 0x0040.
1111
1111 1011 1111 is 0xFFBF.
Testing the
Interrupt Enable Bit
LPS E000 0000 DI00 0000
0x0040 0000 0000 0100 0000
LPS & 0x0040 0000 0000 0I00 0000
If ( 0 = = (LPS & 0x0040) ) then the Line Printer Interrupt is disabled.
Yet More on
the LPS Register
Enabling
Interrupts (Setting the I Bit)
LPS E000 0000 DI00 0000
0x0040 0000 0000 0100 0000
LPS | 0x0040 E000 0000 D100 0000
Setting LPS = LPS | 0x0040 enables the interrupt and leaves the other bits
unchanged.
Disabling
Interrupts (Clearing the I Bit)
LPS E000 0000 DI00 0000
0xFFBF 1111 1111 1011 1111
LPS & 0xFFBF E000 0000 D000 0000
Setting LPS = LPS & 0xFFBF disables the interrupt and leaves the other bits
unchanged.
Overview of
Addressing Modes
Many computer instructions involve memory, and must
generate memory addresses.
In this introduction, we shall discuss addressing
modes in general terms. Later,
we shall investigate examples specifically based on IA–32 code.
All modes of addressing memory are based on an EA (Effective Address),
which
is the actual memory address issued by the control unit.
First, we study some modes that do not address memory.
Immediate
Mode
In this mode, there is no reference to memory. The argument is part of the instruction.
The simplest example is the Boz–7 instruction
HALT. There is no argument.
In the IA–32 trap , int 21h, the hexadecimal value is part of the machine code.
The machine language would have two bytes: 0xCC 21; the first byte is the opcode.
Here is another example with decimal notation. MOV EAX, 1234.
This sets the value in the EAX register to decimal 1234.
Register
Direct
Here is a simple IA–32 example: MOV EAX, EBX.
It moves the value in the EBX register into the EAX register. Memory is not referenced.
Calculating the Effective Address
By definition of the term “Effective Address”, all
memory references use that address
when accessing memory. M[EA] denotes the
contents of the effective address.
Consider two basic operators: Load from memory and
Store to memory
Load Register from Memory: R ¬ M[EA]
Store Register to Memory: M[EA] ¬ (R)
Understanding addressing modes then becomes equivalent
to understanding how
to calculate the Effective Address.
Each instruction referencing memory has fields
(collections of bits) indicating how
to compute the EA. In general, the only
fields used are:
the
Address Field this contains an address, which may be
modified.
the
Index Register this contains the
register used as “array index”.
If this field is 0, indexing is not
used.
the
Indirect Bit this is used for Indirect
and Indexed–Indirect addressing.
If
this is 1, indirect addressing is used; otherwise not.
Instructions that do not use an address field
conventionally have that field set to 0.
Direct and Indirect Addressing
Opcode |
Registers |
Mode Flags |
Address Field |
In each of direct and indirect addressing, the
computation is simple:
Just copy the address field itself.
In direct
addressing, the address field contains the address of the argument.
In indirect
addressing, the address field contains the address of a pointer to the
argument. Put another way, the contents
of the memory indicated by the address
field contain the address of the argument.
In subroutine linkage, argument passing by preference
basically uses indirect addressing.
Direct and Indirect Addressing (Example)
Consider the following address map as an example for
our addressing modes.
Z represents address 0x0A. Z == 0x0A and M[Z] = 5.
Address |
5 |
6 |
7 |
8 |
9 |
A |
B |
C |
D |
E |
F |
10 |
11 |
Contents |
11 |
32 |
2C |
1E |
56 |
5 |
7A |
10 |
3 |
F |
E |
D |
8 |
Direct Addressing LDR %R1, Z
Effective
Address: EA = Z Here EA = 0x0A.
Effect: R1 ¬ M[Z] Here R1
¬ M[0x0A] or R1 = 5.
Indirect
Addressing LDR %R1, *Z
Effective
Address: EA = M[Z] Here EA = M[0x0A] = 5
Effect: R1 ¬ M[EA] = M [ M[Z] ]
Here
R1 ¬ M[0x05] or R1 = 11.
NOTE: These
examples are based on the Boz–7 architecture, rather simpler than
the IA–32. The Boz–7 is a didactic design by the author
of these notes.
Indexed Addressing
This is used to support array addressing in all
computers. Languages such as C and
C++ implement this directly using “zero based” arrays.
In this mode, the contents of a general–purpose
register are used as an index.
The contents of the register are added to the address
field to form the effective address.
EA = (Address Field) + (Register)
Although we expect the index to be smaller than the
value of the address, this
may not always (or often) be the case.
The contents of the address field should be considered
as having a different data type
from those of the register, which holds a signed integer.
Indexed Addressing (Example)
LDR %R1, Z,
3 //
Z indexed by the general–purpose register R3.
Z represents address 0x0A. Z == 0x0A and M[Z] = 5. (R3) = = 4
Address |
5 |
6 |
7 |
8 |
9 |
A |
B |
C |
D |
E |
F |
10 |
11 |
Contents |
11 |
32 |
2C |
1E |
56 |
5 |
7A |
10 |
3 |
F |
E |
D |
8 |
Effective address: EA = Z + (%R3) Here EA = 0x0A + 4 = 0x0E
Effect: R1 ¬ M[EA] = M[0x0E] or R1 = F
In this, the most common, variant of indexed
addressing we see Z as an array.
The first five elements of the array are placed in memory as follows:
Memory
Map |
0x0A |
0x0B |
0x0C |
0x0D |
0x0E |
Contents: |
Z[0] |
Z[1] |
Z[2] |
Z[3] |
Z[4] |
Based
Addressing
This
is quite similar to indexed addressing, so much so that it seems redundant.
This
mode arises from an entirely different consideration.
Indexed addressing basically supports the use of arrays.
Based addressing supports the Operating System in partitioning the
user address space.
User addresses are mapped into physical addresses that cannot conflict.
Indexed–Indirect
Addressing
This
is a combination of both modes.
Question: Which is done first?
Pre–Indexed Indirect: The indexing
is done first. We have an array of
pointers.
Post–Indexed Indirect: The indirect
is done first. We have a pointer to an
array.
Indexed–Indirect
Addressing (Example)
Let Z refer to address
0xA, so that M[Z] = = 0x05. Let (R3) =
0x07.
Address |
5 |
6 |
7 |
8 |
9 |
A |
B |
C |
D |
E |
F |
10 |
11 |
Contents |
11 |
32 |
2C |
1E |
56 |
5 |
7A |
9 |
3 |
F |
E |
D |
8 |
Pre–Indexed Indirect: LDR
%R1, *Z, 3
Effective address EA = M[ Z + (R3) ] = M[0xA + 0x7]
=
M[ 0x11] = 0x08.
Effect R1¬ M[ M[Z + (R3)] ] or R1 ¬ M[0x08] or R1 ¬ 1E
Post–Indexed Indirect: LDR
%R1, *Z, 3
Effective address EA = M[Z] + (R3) = M[0x0A] + 0x07
=
0x05 + 0x07 = 0x0C.
Effect R1
¬ M[0x0C] or R1 ¬ 0x09.
Double
Indirect Addressing: Use 1
Suppose a common data structure
that is used by a number of programs.
In modern systems, this could be a DLL (Dynamic Linked Library) module.
Each program contains a pointer
to this structure, used to access that structure.
Singly indirect addressing, in
which the pointer is stored in every program, presents
problems when the data block is moved by the memory manager. Every reference to
that block must be found and adjusted.
In doubly indirect addressing,
all programs have a pointer to a data structure which itself
is a pointer. When the data block is
moved, only this one pointer must be adjusted.
As an extra benefit, this
intermediate pointer can be expanded to a data structure
containing an ACL (Access Control List) and other descriptors for the data
block.
Double
Indirect Addressing: Use 2
The Operating System uses
double indirection to activate a device driver associated with
a given I/O device. For each type of I/O
device, the O/S stores the address of a
“device vector”, which contains the
actual address of the software to handle the device.
This arrangement allows the
operating system and the device drivers to be developed
independently. The only fixed location
is the address of this device vector.
When the driver software is
loaded, the O/S places it in the memory location that is most
convenient for the current configuration and copies that start address into the
address part
of the device vector. Double indirection
is then used to call the driver.
This is really a “vectored interrupt” mechanism,
described more fully in a later chapter.