Computer Architecture: CPUs -- Assembly Languages and Programming Paradigm



Home | Forum | DAQ Fundamentals | DAQ Hardware | DAQ Software

Input Devices
| Data Loggers + Recorders | Books | Links + Resources


AMAZON multi-meters discounts AMAZON oscilloscope discounts


1. Introduction

Previous sections describe processor instruction sets and operand addressing. This section discusses programming languages that allow programmers to specify all the de tails of instructions and operand addresses. The section is not a tutorial about a language for a particular processor. Instead, it provides a general assessment of features commonly found in low-level languages. The section examines programming paradigms, and explains how programming in a low-level language differs from programming in a conventional language. Finally, the section describes software that translates a low-level language into binary instructions.

Low-level programming and low-level programming languages are not strictly part of computer architecture. We consider them here, however, because such languages are so closely related to the underlying hardware that the two cannot be separated easily.

Subsequent sections return to the focus on hardware by examining memory and I/O facilities.

2. Characteristics of a High-level Programming Language

Programming languages can be divided into two broad categories:

-- High-level languages

-- Low-level languages

A conventional programming language, such as Java or C, is classified as a high level-language because the language exhibits the following characteristics:

-- One-to-many translation

-- Hardware independence

-- Application orientation

-- General-purpose

-- Powerful abstractions

One-To-Many Translation. Each statement in a high-level language corresponds to multiple machine instructions. That is, when a compiler translates the language into equivalent machine instructions, a statement usually translates into several instructions.

Hardware Independence. High-level languages allow programmers to create a pro gram without knowing details about the underlying hardware. For example, a high level language allows a programmer to specify floating point operations, such as addition and subtraction, without knowing whether the ALU implements floating point arithmetic directly or uses a separate floating point coprocessor.

Application Orientation. A high-level language, such as C or Java, is designed to allow a programmer to create application programs. Thus, a high-level language usually includes I/O facilities as well as facilities that permit a programmer to define arbitrarily complex data objects.

General-Purpose. A high-level language, like C or Java, is not restricted to a specific task or a specific problem domain. Instead, the language contains features that allow a programmer to create a program for an arbitrary task.

Powerful Abstractions. A high-level language provides abstractions, such as procedures, that allow a programmer to express complex tasks succinctly.

3. Characteristics of a Low-level Programming Language

The alternative to a high-level language is known as a low-level language and has the following characteristics:

-- One-to-one translation

-- Hardware dependence

-- Systems programming orientation

-- Special-purpose

-- Few abstractions

One-To-One Translation. In general, each statement in a low-level programming language corresponds to a single instruction on the underlying processor. Thus, the translation to machine code is one-to-one.

Hardware Dependence. Because each statement corresponds to a machine instruction, a low-level language created for one type of processor cannot be used with another type of processor.

Systems Programming Orientation. Unlike a high-level language, a low-level language is optimized for systems programming - the language has facilities that allow a programmer to create an operating system or other software that directly controls the hardware.

Special-Purpose. Because they focus on the underlying hardware, low-level languages are only used in cases where extreme control or efficiency is needed. For ex ample, communication with a coprocessor usually requires a low-level language.

Few Abstractions. Unlike high-level languages, low-level languages do not pro vide complex data structures (e.g., strings or objects) or control statements (e.g., if then-else or while). Instead, the language forces a programmer to construct abstractions from low-level hardware mechanisms†.

4. Assembly Language

The most widely used form of low-level programming language is known as assembly language, and the software that translates an assembly language program into a binary image that the hardware understands is known as an assembler.

It is important to understand that the phrase assembly language differs from phrases such as Java language or C language because assembly does not refer to a single language. Instead, a given assembly language uses the instruction set and operands from a single processor. Thus, many assembly languages exist, one for each processor.

Programmers might talk about MIPS assembly language or Intel x86 assembly language. To summarize:

Because an assembly language is a low-level language that incorporates specific characteristics of a processor, such as the instruction set, operand addressing, and registers, many assembly languages exist.

[†Computer scientist Alan Perlis once quipped that a programming language is low-level if programming requires attention to irrelevant details. His point is that because most applications do not need direct control, using a low-level language creates overhead for an application programmer without any real benefit.]

The consequence for programmers should be obvious: when moving from one processor to another, an assembly language programmer must learn a language. On the down side, the instruction set, operand types, and register names often differ among assembly languages. On the positive side, most assembly languages tend to follow the same basic pattern. Therefore, once a programmer learns one assembly language, the programmer can learn others quickly. More important, if a programmer understands the basic assembly language paradigm, moving to a new architecture usually involves learning new details, not learning a new programming style. The point is:

Despite differences, many assembly languages share the same fundamental structure. Consequently, a programmer who understands the assembly programming paradigm can learn a new assembly language quickly.

To help programmers understand the concept of assembly language, the next sections focus on general features and programming paradigms that apply to most assembly languages. In addition to specific language details, we will discuss concepts such as macros.

5. Assembly Language Syntax and Opcodes

5.1 Statement Format

Because assembly language is low-level, a single assembly language statement corresponds to a single machine instruction. To make the correspondence between language statements and machine instructions clear, most assemblers require a program to contain a single statement per line of input. The general format is:

label: opcode operand1 , operand2 , ...

where label gives an optional label for the statement (used for branching), opcode specifies one of the possible instructions, each operand specifies an operand for the instruction, and whitespace separates the opcode from other items.

5.2 Opcode Names

The assembly language for a given processor defines a symbolic name for each instruction that the processor provides. Although the symbolic names are intended to help a programmer remember the purpose of the instruction, most assembly languages use extremely short abbreviations instead of long names. Thus, if a processor has an instruction for addition, the assembly language might use the opcode add. However, if the processor has an instruction that branches to a new location, the opcode for the instruction typically consists of a single letter, b, or the two-letter opcode br. Similarly, if the processor has an instruction that jumps to a subroutine, the opcode is often jsr.

Unfortunately, there is no global agreement on opcode names even for basic operations. For example, most architectures include an instruction that copies the contents of one register to another. To denote such an operation, some assembly languages use the opcode mov (an abbreviation for move), and others use the opcode ld (an abbreviation for load).

5.3 Commenting Conventions

Short opcodes tend to make assembly language easy to write but difficult to read.

Furthermore, because it is low-level, assembly language tends to require many instructions to achieve a straightforward task. Thus, to ensure that assembly language pro grams remain readable, programmers add two types of comments: block comments that explain the purpose of each major section of code, and a detailed comment on each individual line to explain the purpose of the line.

To make it easy for programmers to add comments, assembly languages often al low comments to extend until the end of a line. That is, the language only defines a character (or sequence of characters) that starts a comment. One commercial assembly language defines the pound sign character (#) as the start of a comment, a second uses a semicolon to denote the start of a comment, and a third has adopted the C ++ comment style and uses two adjacent slash characters. A block comment can be created in which each line begins with the comment character, and a detailed comment can be added to each line of the program. Programmers often add additional characters to surround a block comment. For example, if the pound sign signals the start of a comment, the block comment below explains that a section of code searches a list to find a memory block of a given size:

Most programmers place a comment on each line of assembly code to explain how the instruction fits into the algorithm. For example, the code to search for a memory block might begin:

Although details in the example above may seem obscure, the point is relatively straightforward: a block comment before a section of code explains what the code accomplishes, and a comment on each line of code explains how that particular instruction contributes to the result.

6. Operand Order

One frustrating difference among assembly languages causes subtle problems for programmers who move from one assembly language to another: the order of operands.

A given assembly language usually chooses a consistent operand order. For example, consider a load instruction that copies the contents of one register to another register.

In the example code above, the first operand represents the target register (i.e., the register into which the value will be placed), and the second operand represents the source register (i.e., the register from which the value will be copied). Under such an interpretation, the statement:

l ld dr r55,,rr3 3# #l load dt thhe ea address so of fl liisst ti inntto or r5 5

copies the contents of register 3 into register 5. As a mnemonic aid to help them remember the right-to-left interpretation, programmers are told to think of an assignment statement in which the expression is on the right and the target of the assignment is on the left.

As an alternative to the example code, some assembly languages specify the opposite order -- the source register is on the left and the target register is on the right. In such assembly languages, the code above is written with operands in the opposite order:

l ld dr r33,,rr5 5# #l looaad dt thhe ea addddrreesss so of fl liisst ti inntto or r5 5

As a mnemonic aid to help them remember the left-to-right interpretation, programmers are told to think of a computer reading the instruction. Because text is read left to right, we can imagine the computer reading the opcode, picking up the first operand, and depositing the value in the second operand. Of course, the underlying hardware does not process the instruction left-to-right or right-to-left - the operand order is only assembly language syntax.

Operand ordering is further complicated by several factors. First, unlike our examples above, many assembly language instructions do not have two operands. For example, an instruction that performs bitwise complement only needs one operand. Further more, even if an instruction has two operands, the notions of source and destination may not apply (e.g., a comparison). Therefore, a programmer who is unfamiliar with a given assembly language may need to consult a manual to find the order of operands for a given opcode.

Of course, there can be a significant difference between what a programmer writes and the resulting binary value of the instruction because the assembly language merely uses notation that is convenient for the programmer. The assembler can reorder operands during translation. For example, the author once worked on a computer that had two assembly languages, one produced by the computer's vendor and another produced by researchers at Bell Labs. Although both languages were used to produce code for the same underlying computer, one language used a left-to-right interpretation of the operands, and the other used a right-to-left interpretation.

7. Register Names

Because a typical instruction includes a reference to at least one register, most assembly languages include a special way to denote registers. For example, in many assembly languages, names that consist of the letter r followed by one or more digits are reserved to refer to registers. Thus, a reference to r10 refers to register 10.

However, there is no universal standard for register references. In one assembly language, all register references begin with a dollar sign followed by digits; thus, $10 refers to register 10. Other assemblers are more flexible: the assembler allows a programmer to choose register names. That is, a programmer can insert a series of declarations that define a specific name to refer to a register. Thus, one might find declarations such as:

The chief advantage of allowing programmers to define register names arises from increased readability: a programmer can choose meaningful names. For example, sup pose a program manages a linked list. Instead of using numbers or names like r6,a programmer can give meaningful names to the registers:

Of course, allowing programmers to choose names for registers can also lead to unexpected results that make the code difficult to understand. For example, consider reading a program in which a programmer has used the following declaration:

r r3 3r register r8 8# #d define en name er r3 3t to ob be er register r8 8! !

The points can be summarized:

Because registers are fundamental to assembly language programming, each assembly language provides a way to identify registers. In some languages, special names are reserved; in others, a programmer can assign a name to a register.

8. Operand Types

As Section 7 explains, a processor often provides multiple types of operands. The assembly language for each processor must accommodate all operand types that the hardware offers. As an example, suppose a processor allows each operand to specify a register, an immediate value (i.e., a constant), a memory location, or a memory location specified by adding an offset in the instruction to the contents of a register. The assembly language for the processor needs a syntactic form for each possible operand type.

We said that assembly languages often use special characters or names to distinguish registers from other values. In many assembly languages, for example, 10 refers to the constant ten, and r10 refers to register ten. However, some assembly languages require a special symbol before a constant (e.g., #10 to refer to the constant ten).

Each assembly language must provide syntactic forms for each possible operand type. Consider, for example, copying a value from a source to a target. If the processor allows the instruction to specify either a register (direct) or a memory location (indirect) as the source, the assembly language must provide a way for a programmer to distinguish the two. One assembly language uses parentheses to distinguish the two possibilities:

The point is:

An assembly language provides a syntactic form for each possible operand type that the processor supports, including a reference to a register, an immediate value, and an indirect reference to memory.

9. Assembly Language Programming Paradigm and Idioms

Because a programming language provides facilities that programmers use to structure data and code, a language can impact the programming process and the resulting code. Assembly language is especially significant because the language does not pro vide high-level constructs nor does the language enforce a particular style. Instead, assembly language gives a programmer complete freedom to code arbitrary sequences of instructions and store data in arbitrary memory locations.

Experienced programmers understand that consistency and clarity are usually more important than clever tricks or optimizations. Thus, experienced programmers develop idioms: patterns that they use consistently. The next sections use basic control structures to illustrate the concept of assembly language idioms.

10. Coding an IF Statement In Assembly

We use the term conditional execution to refer to code that may or may not be executed, depending on a certain condition. Because conditional execution is a fundamental part of programming, high-level languages usually include one or more statements that allow a programmer to express conditional execution. The most basic form of conditional execution is known as an if statement.

In assembly language, a programmer must code a sequence of statements to per form conditional execution. FIG. 1 illustrates the form used for conditional execution in a typical high-level language and the equivalent form used in a typical assembly language.


FIG. 1 (a) Conditional execution as specified in a high-level language, and (b) the equivalent assembly language code.

As the figure indicates, some processors use a condition code as the fundamental mechanism for conditional execution. Whenever it performs an arithmetic operation or a comparison, the ALU sets the condition code. A conditional branch instruction can be used to test the condition code and execute the branch if the condition code matches the instruction. Note that in the case of emulating an if statement, the branch instruction must test the opposite of the condition (i.e., the branch is taken if the condition is not met). For example, consider the statement:

if (a==b){x}

If we assume a and b are stored in registers five and six, the equivalent assembly language is:

11. Coding an IF-THEN-ELSE In Assembly

The if-then-else statement found in high-level languages specifies code to be executed for both the case when a condition is true and when the condition is false. FIG. 2 shows the assembly language equivalent of an if-then-else statement.


FIG. 2 (a) An if-then-else statement used in a high-level language, and (b) the equivalent assembly language code.

12. Coding a FOR-LOOP in Assembly

The term definite iteration refers to a programming language construct that causes a piece of code to be executed a fixed number of times. A typical high-level language uses a for statement to implement definite iteration. FIG. 3 shows the assembly language equivalent of a for statement.

Definite iteration illustrates an interesting difference between a high-level language and assembly language: location of code. In assembly language, the code to implement a control structure can be divided into separate locations. In particular, although a programmer thinks of the initialization, continuation test, and increment as being specified in the header of a for statement, the equivalent assembly code places the increment after the code for the body.

13. Coding a WHILE Statement In Assembly

In programming language terminology, indefinite iteration refers to a loop that executes zero or more times. Typically, a high-level language uses the keyword while to indicate indefinite iteration. FIG. 4 shows the assembly language equivalent of a while statement.


FIG. 3 (a) A for statement used in a high-level language, and (b) the equivalent assembly language code using register 4 as an index.


FIG. 4 (a) A while statement used in a high-level language, and (b) the equivalent assembly language code.

14. Coding a Subroutine Call in Assembly

We use the term procedure or subroutine to refer to a piece of code that can be invoked, perform a computation, and return control to the invoker. The terms procedure call or subroutine call to refer to the invocation. The key idea is that when a subroutine is invoked, the processor records the location from which the call occurred, and resumes execution at that point once the subroutine completes. Thus, a given subroutine can be invoked from multiple points in a program because control always passes back to the location from which the invocation occurred.

Many processors provide two basic assembly instructions for procedure invocation.

A jump to subroutine (jsr) instruction saves the current location and branches to a sub routine at a specified location, and a return from subroutine (ret) instruction causes the processor to return to the previously saved location. FIG. 5 shows how the two assembly instructions can be used to code a procedure declaration and two invocations.


FIG. 5 (a) A declaration for procedure x and two invocations in a high level language, and (b) the assembly language equivalent.

15. Coding a Subroutine Call With Arguments In Assembly

In a high-level language, procedure calls are parameterized. The procedure body is written with references to parameters, and the caller passes a set of values to the procedure that are known as arguments. When the procedure refers to a parameter, the value is obtained from the corresponding argument. The question arises: how are arguments passed to a procedure in assembly code? Unfortunately, the details of argument passing vary widely among processors. For example, each of following three schemes has been used in at least one processor†:

-- The processor uses a stack in memory for arguments

-- The processor uses register windows to pass arguments

-- The processor uses special-purpose argument registers

As an example, consider a processor in which registers r1 through r8 are used to pass arguments during a procedure call. FIG. 6 shows the assembly language code for a procedure call on such an architecture.

16. Consequence for Programmers

The consequence of a variety of argument passing schemes should be clear: the assembly language code needed to pass and reference arguments varies significantly from one processor to another. More important, programmers are free to invent new mechanisms for argument passing that optimize performance. For example, memory references are slower than register references. Thus, even if the hardware is designed to use a stack in memory, a programmer might choose to increase performance by passing some arguments in general-purpose registers rather than memory.

[†The storage used for a return address (i.e., the location to which a ret instruction should branch) is often related to the storage used for arguments.]


FIG. 6 (a) A declaration for parameterized procedure x and two invocations in a high-level language, and (b) the assembly language equivalent for a processor that passes arguments in registers.

The point is:

No single argument passing paradigm is used in assembly languages because a variety of hardware mechanisms exist for argument passing. In addition, programmers sometimes use alternatives to the basic mechanism to optimize performance (e.g., passing values in registers).

17. Assembly Code for Function Invocation

The term function refers to a procedure that returns a single-value result. For ex ample, an arithmetic function can be created to compute sine(x) - the argument specifies an angle, and the function returns the sine of the angle. Like a procedure, a function can have arguments, and a function can be invoked from an arbitrary point in the program. Thus, for a given processor, function invocation uses the same basic mechanisms as procedure invocation.

Despite the similarities between functions and procedures, function invocation re quires one additional detail: an agreement that specifies exactly how the function result is returned. As with argument passing, many alternative implementations exist. Processors have been built that provide a separate, special-purpose hardware register for a function return value. Other processors assume that the program will use one of the general-purpose registers. In any case, before executing a ret instruction, a function must load the return value into the location that the processor uses. After the return occurs, the calling program extracts and uses the return value.

18. Interaction Between Assembly and High-level Languages

Interaction is possible in either direction between code written in an assembly language and code written in a high-level language. That is, a program written in a high-level language can call a procedure or function that has been written in assembly language, and a program written in assembly language can call a procedure or function that has been written in a high-level language. Of course, because a programmer can only control the assembly language code and not the high-level language code, the assembly program must follow the calling conventions that the high-level language uses.

That is, the assembly code must use exactly the same mechanisms as the high-level language uses to store a return address, invoke a procedure, pass arguments, and return a function value.

Why would a programmer mix code written in assembly language with code writ ten in a high-level language? In some cases, assembly code is needed because a high level language does not allow direct interaction with the underlying hardware. For ex ample, a computer that has special graphics hardware may need assembly code to use the graphics functions. In most cases, however, assembly language is only used to optimize performance -- once a programmer identifies a particular piece of code as a bottleneck, the programmer writes an optimized version of the code in assembly language. Typically, optimized assembly language code is placed into a procedure or function; the rest of the program remains written in a high-level language. As a result, the most common case of interaction between code written in a high-level language and code written in assembly language consists of a program written in a high-level language calling a procedure or function that is written in an assembly language.

The point is:

Because writing application programs in assembly language is difficult, assembly language is reserved for situations where a high-level language has insufficient functionality or results in poor performance.

19. Assembly Code for Variables and Storage

In addition to statements that generate instructions, assembly languages permit a programmer to define data items. Both initialized and uninitialized variables can be declared. For example, some assembly languages use the directive .word to declare storage for a sixteen-bit item, and the directive .long to declare storage for a thirty-two bit item. FIG. 7 shows declarations in a high-level language and equivalent assembly code.


FIG. 7 (a) Declaration of variables in a high-level language, and (b) equivalent variable declarations in assembly language.

The keywords .word and .long are known as assembly language directives.

Although it appears in the same location that an opcode appears, a directive does not correspond to an instruction. Instead, a directive controls the translation. The directives in the figure specify that storage locations should be reserved to hold variables. In most assembly languages, a directive that reserves storage also allows a programmer to specify an initial value. Thus, the directive:

x x: :. .word d9 9449 9

reserves a sixteen bit memory location, assigns the location the integer value 949, and defines x to be a label (i.e., a name) that the programmer can use to refer to the location.

20. Example Assembly Language Code

An example will help clarify the concepts and show how assembly language idioms apply in practice. To help compare x86 and ARM architectures, we will use the same example for each architecture. To make the example clear, we begin with a C program and then show how the same algorithm can be implemented in assembly language.

Instead of using a long, complex program to show all the idioms, we will use a trivial example that demonstrates a few basics. In particular, it will show indefinite iteration and conditional execution. The example consists of a piece of code that prints an initial list of the Fibonacci sequence. The first two values in the sequence are each

1. Each successive value is computed as the sum of the preceding two values. Thus, the sequence is 1, 1, 2, 3, 5, 8, 13, 21, and so on.

To ensure our example relates to concepts from computer architecture, we will arrange the code to print all values in the Fibonacci sequence that fit into a two's complement thirty-two-bit signed integer. As the sequence is generated, the code will count the number of values greater than 1000, and will print a summary.

20.1 The Fibonacci Example in C

FIG. 8 shows a C program that computes each value in the Fibonacci sequence that fits in a thirty-two-bit signed integer. The program uses printf to print each value.

It also counts the number of values greater than 1000, and uses printf to print the total as well as a summary of the final values of variables that are used in the computation.



FIG. 8 An example C program that computes and prints values in the Fibonacci sequence that fit into a thirty-two-bit signed integer.

FIG. 9 shows the output that results when the program runs. The last line of the output gives the value of variables a, b, and tmp after the while loop finishes. Vari able a (1,836,311,903 in decimal) is 6D73E55F in hex. Notice that variable tmp has value B11924E1, which has the high-order bit set. As Section 3 explains, when tmp is interpreted as a signed integer, the value will be negative, which is why the loop terminated. Also note that variable n, which counts the number of Fibonacci values has the final value 30; the value can be verified by counting lines of output with values greater than 1000.


FIG. 9 The output that results from running the program in FIG. 8.

20.2 The Fibonacci Example in x86 Assembly Language

FIG. 10 shows x86 assembly code that generates the same output as the pro gram in FIG. 8. The code uses the gcc calling conventions to call printf.



FIG. 10 An x86 assembly language program that follows the C program shown in FIG. 8.

20.3 The Fibonacci Example in ARM Assembly Language

FIG. 11 shows ARM assembly code that generates the same output as the pro gram in FIG. 8. Neither the x86 nor the ARM code has been optimized. In each case, instructions can be eliminated by keeping variables in registers. As an example, a small amount of optimization has been done for the ARM code: registers r4 through r8 are initialized to contain the addresses of variables a, b, n, tmp, and the format string fmt1. The registers remain unchanged while the program runs because called subprograms are required to save and restore values. Thus, when calling printf to print vari able a, the code can use a single instruction to move the address of the format into the first argument register (r0):

mov r0, r8

The code can also use a single instruction to load the value of a into the second argument register (r1):

ldr r1, [r4]

Exercises suggest ways to improve the code.


FIG. 11 An ARM assembly language program that follows the algorithm shown in FIG. 8.

21. Two-Pass Assembler

We use the term assembler to refer to a piece of software that translates assembly language programs into binary code for the processor to execute. Conceptually, an assembler is similar to a compiler because each takes a source program as input and produces equivalent binary code as output. An assembler differs from a compiler, however, because a compiler has significantly more responsibility. For example, a compiler can choose how to allocate variables to memory, which sequence of instructions to use for each statement, and which values to keep in general-purpose registers. An assembler cannot make such choices because the source program specifies the exact details.

The difference between an assembler and compiler can be summarized:

Although both a compiler and an assembler translate a source pro gram into equivalent binary code, a compiler has more freedom to choose which values are kept in registers, the instructions used to implement each statement, and the allocation of variables to memory.

An assembler merely provides a one-to-one translation of each statement in the source program to the equivalent binary form.

Conceptually, an assembler follows a two-pass algorithm, which means the assembler scans through the source program two times. To understand why two passes are needed, observe that many branch instructions contain forward references (i.e., the label referenced in the branch is defined later in the program). When the assembler first reaches a branch statement, the assembler cannot know which address will be associated with the label. Thus, the assembler makes an initial pass, computes the address that each label will have in the final program, and stores the information in a table known as a symbol table. The assembler then makes a second pass to generate code. FIG. 12 illustrates the idea by showing a snippet of assembly language code and the relative lo cation of statements.


FIG. 12 A snippet of assembly language code and the locations assigned to each statement for a hypothetical processor. Locations are determined in the assembler's first pass.

During the first pass, the assembler computes the size of instructions without actually filling in details. Once the assembler has completed its first pass, the assembler will have recorded the location for each statement. Consequently, the assembler knows the value for each label in the program. In the figure, for example, the assembler knows that label4 starts at location 0x20 (32 in decimal). Thus, when the second pass of the assembler encounters the statement:

br label4

the assembler can generate a branch instruction with 32 as an immediate operand.

Similarly, code can be generated for each of the other branch instructions during the second pass because the location of each label is known.

It is not important to understand the details of an assembler, but merely to know that:

Conceptually, an assembler makes two passes over an assembly language program. During the first pass, the assembler assigns a lo cation to each statement. During the second pass, the assembler uses the assigned locations to generate code.

Now that we understand how an assembler works, we can discuss one of the chief advantages of using an assembler: automatic recalculation of branch addresses. To see how automatic recalculation helps, consider a programmer working on a program. If the programmer inserts a statement in the program, the location of each successive statement changes. As a result, every branch instruction that refers to a label beyond the insertion point must be changed.

Without an assembler, changing branch labels can be tedious and prone to errors.

Furthermore, programmers often make a series of changes while debugging a program.

An assembler allows a programmer to make a change easily - the programmer merely reruns the assembler to produce a binary image with all branch addresses updated.

22. Assembly Language Macros

Because assembly language is low-level, even trivial operations can require many instructions. More important, an assembly language programmer often finds that sequences of code are repeated with only minor changes between instances. Repeated sequences of code make programming tedious, and can lead to errors if a programmer uses a cut-and-paste approach.

To help programmers avoid repetitious coding, many assembly languages include a parameterized macro facility. To use a macro facility, a programmer adds two types of items to the source program: one or more macro definitions and one or more macro expansions. Note: C programmers will recognize assembly language macros because they operate like C preprocessor macros.

In essence, a macro facility adds an extra pass to the assembler. The assembler makes an initial pass in which macros are expanded. The important concept is that the macro expansion pass does not parse assembly language statements and does not handle translation of the instructions. Instead, the macro processing pass takes as input an assembly language source program that contains macros, and produces as output an assembly language source program in which macros are expanded. That is, the output of the macro preprocessing pass becomes the input to the normal two-pass assembler.

Many assemblers have an option that allows a programmer to obtain a copy of the expanded source code for use in debugging (i.e., to see if macro expansion is proceeding as the programmer planned).

Although the details of assembly language macros vary across assembly languages, the concept is straightforward. A macro definition is usually bracketed by keywords (e.g., macro and endmacro), and contains a sequence of code. For example, FIG. 13 illustrates a definition for a macro named addmem that adds the contents of two memory locations and places the result in a third location.


FIG. 13 An example macro definition using the keywords macro and endmacro. Items in the macro refer to parameters a, b, and c.

Once a macro has been defined, the macro can be expanded. A programmer invokes the macro and supplies a set of arguments. The assembler replaces the macro call with a copy of the body of the macro, substituting actual arguments in place of formal parameters. For example, FIG. 14 shows the assembly code generated by an expansion of the addmem macro defined in FIG. 13.


FIG. 14 An example of the assembly code that results from an expansion of macro addmem.

It is important to understand that although the macro definition in FIG. 13 resembles a procedure declaration, a macro does not operate like a procedure. First, the declaration of a macro does not generate any machine instructions. Second, a macro is expanded, not called. That is, a complete copy of the macro body is copied into the assembly program. Third, macro arguments are treated as strings that replace the corresponding parameter. The literal substitution of arguments is especially important to understand because it can yield unexpected results. For example, consider FIG. 15 which illustrates how an illegal assembly program can result from a macro expansion.


FIG. 15 An example of an illegal program that can result from an expansion of macro addmem. The assembler substitutes arguments without checking their validity.

As the figure shows, an arbitrary string can be used as an argument to the macro, which means a programmer can inadvertently make a mistake. No warning is issued until the assembler processes the expanded source program. For example, the first argument in the example consists of the string 1+, which is a syntax error. When it expands the macro, the assembler substitutes the specified string which results in:

load r1, 1+

Similarly, substitution of the second argument, %*J, results in:

load r2, %*J which makes no sense. However, the errors will not be detected until after the macro expander has run and the assembler attempts to assemble the program. More important, because macro expansion produces a source program, error messages that refer to line numbers will reference lines in the expanded program, not in the original source code that a programmer submits.

The point is:

A macro expansion facility preprocesses an assembly language source program to produce another source program in which each macro in vocation is replaced by the text of the macro. Because a macro processor uses textual substitution, incorrect arguments are not detected by the macro processor; errors are only detected by the assembler after the macro processor completes.

23. Summary

Assembly languages are low-level languages that incorporate characteristics of a processor, such as the instruction set, operand addressing modes, and registers. Many assembly languages exist, one or more for each type of processor. Despite differences, most assembly languages follow the same basic structure.

Each statement in an assembly language corresponds to a single instruction on the underlying hardware; the statement consists of an optional label, opcode, and operands.

The assembly language for a processor defines a syntactic form for each type of operand the processor accepts.

Although assembly languages differ, most follow the same basic paradigm. There fore, we can specify typical assembly language sequences for conditional execution, conditional execution with alternate paths, definite iteration, and indefinite iteration.

Most processors include instructions used to invoke a subroutine or function and return to the caller. The details of argument passing, return address storage, and return of values to a caller differ. Some processors place arguments in memory, and others pass arguments in registers.

An assembler is a piece of software that translates an assembly language source program into binary code that the processor can execute. Conceptually, an assembler makes two passes over the source program: one to assign addresses and one to generate code. Many assemblers include a macro facility to help programmers avoid tedious coding repetition; the macro expander generates a source program which is then assembled. Because it uses textual substitution, macro expansion can result in illegal code that is only detected and reported by the two main passes of the assembler.

EXERCISES

1. State and explain the characteristics of a low-level language.

2. Where might a programmer expect to find comments in an assembly language program?

3. If a program contains an if-then-else statement, how many branch instructions will be per formed if the condition is true? If the condition is false?

4. What is the assembly language used to implement a repeat statement?

5. Name three argument passing mechanisms that have been used in commercial processors.

6 Write an assembly language function that takes two integer arguments, adds them, and re turns the result. Test your function by calling it from C.

7. Write an assembly language program that declares three integer variables, assigns them 1, 2, and 3, and then calls printf to format and print the values.

8. Programmers sometimes mistakenly say assembler language. What have they confused, and what term should they use?

9. In FIG. 12, if an instruction is inserted following label4 that jumps to label2, to what address will it jump? Will the address change if the new instruction is inserted before label1?

10. Look at FIG. 8 to see the example Fibonacci program written in C. Can the program be redesigned to be faster? How?

11. Optimize the Fibonacci programs in Figures 10 and 11 by choosing to keep values in registers rather than writing them to memory. Explain your choices.

12. Compare the x86 and ARM versions of the Fibonacci program in Figures 10 and 11.

Which version do you expect to require more code? Why?

13. Use the -S option on gcc to generate assembly code for a C program. For example, try the program in FIG. 8. Explain all the extra code that is generated.

14. What is the chief disadvantage of using an assembly language macro instead of a function?

PREV. | NEXT

Related Articles -- Top of Page -- Home

Updated: Tuesday, April 25, 2017 8:39 PST