Chapter 7
Programming in Assembly Language

Since writing the original version of this book, the default for the gcc compiler has changed to produce Position Independent Code (PIC) and Position Independent Executables (PIE), which are not covered in this book. To make things match the examples in this book:

While reading this chapter, you should also consult the info resources available in most GNU/Linux installations for both the make and the as programs. Appendix B provides a general tutorial for writing Makefiles, but you need to get the details from info. info is especially important for learning about as’s assembler directives.

You should also reread the Development Environment section on page xx.

Creating a program in assembly language is essentially the same as creating one in a high-level compiled language like C, C++, Java, FORTRAN, etc. We will begin the chapter by looking in detail at the steps involved in creating a C program. Then we will look at which of these steps apply to assembly language programming.

7.1 Creating a New Program

You have probably learned how to program using an Integrated Development Environment (IDE) , which incorporates several programs within a single user interface:

  1. A text editor is used to write the source code and save it in a file.
  2. A compiler translates the source code into machine language that can be executed by the CPU.
  3. A linker is used to integrate all the functions in your program, including externally accessed libraries of functions, and to determine where each component will be loaded into memory when the program is executed.
  4. A loader is used to load the machine code version of the program into memory where the CPU can execute it.
  5. A debugger is used to help the programmer locate errors that may have crept into the program. (Yes, none of us is perfect!)

You enter your source code in the text editor part, click on a “build” button to compile and link your program, then click on a “run” button to load and execute the program. There is typically a “debug” button that loads and executes the program under control of the debugger program if you need to debug it. The individual steps of program preparation are obscured by the IDE user interface. In this book we use the GNU programming environment in which each step is performed explicitly.

Several excellent text editors exist for GNU/Linux, each with its own “personality.” My “favorite” changes from time to time. I recommend trying several that are available to you and deciding which one you prefer. You should avoid using a word processor to create source files because it will add formatting to the text (unless you explicitly specify text-only). Text editors I have used include:

GUI interfaces are available for both vi and emacs. Any of these, and many other, text editors would be an excellent choice for the programming covered in this book. Don’t spend too much time trying to pick the “best” one.

The GNU programming tools are executed from the command line instead of a graphical user interface (GUI). (IDEs for Linux and Unix are typically GUI frontends that execute GNU programming tools behind the scenes.) The GNU compiler, gcc, creates an executable program by performing several distinct steps [22]. The description here assumes a single C source file, filename.c.

  1. Preprocessing. This resolves compiler directives such as #include (file inclusion), #define (macro definition), and #if (conditional compilation) by invoking the program cpp. Compilation can be stopped at the end of the preprocessing phase with the -E option, which writes the resulting C source code to standard out.
  2. Compilation itself. The source code that results from preprocessing is translated into assembly language. Compilation can be stopped at the end of the compilation phase with the -S option, which writes the assembly language source code to filename.s.
  3. Assembly. The assembly language source code that results from compilation is translated into machine code by invoking the as program. Compilation can be stopped at the end of the assembly phase with the -c option, which writes the machine code to filename.o.
  4. Linking. The machine code that results from assembly is linked with other machine code from standard C libraries and other machine code modules, and addresses are resolved. This is accomplished by invoking the ld program. The default is to write the executable file, a.out. A different executable file name can be specified with the -o option.

7.2 Program Organization

Programs written in C are organized into functions. Each function has a name that is unique within the program. Program execution begins with the function named “main.”

I recommend that you create a separate directory for each program you write. Place all the source files, plus the Makefile (see Appendix B) for the program in this directory. This will help you keep your program files organized.

Let us consider the minimum C program, Listing 7.1.

 
1/* 
2 * doNothingProg1.c 
3 * The minimum components of a C program. 
4 * Bob Plantz - 6 June 2009 
5 */ 
6 
7int main(void) 
8{ 
9    return 0; 
10}
Listing 7.1: A “null” program (C).

The only thing this program does is return a zero.

Despite the fact that this program accomplishes very little, some instructions need to be executed just to return zero. In order to see what takes place, we first translate this program from C to assembly language with the GNU/Linux command:

     gcc -S -O0 doNothingProg1.c

This creates the file doNothingProg1.s (see Listing 7.2), which contains the assembly language generated by the gcc compiler. The two compiler options used here have the following meanings:

-S
Causes the compiler to create the .s file, which contains the assembly language equivalent of the source code. The machine code (.o file) is not created.
-O0
Do not do any optimization. For instructional purposes, we want to see every step of the assembly language. (This is upper-case “oh” followed by the numeral zero.)
 
1        .file  "doNothingProg1.c" 
2        .text 
3        .globlmain 
4        .type  main, @function 
5main: 
6.LFB0: 
7        .cfi_startproc 
8        pushq  %rbp 
9        .cfi_def_cfa_offset 16 
10        .cfi_offset 6, -16 
11        movq  %rsp, %rbp 
12        .cfi_def_cfa_register 6 
13        movl  $0, %eax 
14        popq  %rbp 
15        .cfi_def_cfa 7, 8 
16        ret 
17        .cfi_endproc 
18.LFE0: 
19        .size  main, .-main 
20        .ident"GCC: (Ubuntu/Linaro 4.7.0-7ubuntu3) 4.7.0" 
21        .section      .note.GNU-stack,"",@progbits
Listing 7.2: A “null” program (gcc assembly language). The lines that begin with .cfi are meant for error recovery and debugging and are not relevant to the concepts discussed in this book.
Unlike the relationship between assembly language and machine language, there is not a one-to-one relationship between higher-level languages and assembly language. The assembly language generated by a compiler may differ across different releases of the compiler, and different optimization levels will generally affect the code that is generated by the compiler. The code in Listing 7.2 was generated by release 4.7.0 of gcc and the optimization level was -O0 (no optimization). If you attempt to replicate this example, your results may vary.

This is not easy to read, even for an experienced assembly language programmer. So we will start with the program in Listing 7.3, which was written in assembly language by a programmer (rather than by a compiler). Naturally, the programmer has added comments to improve readability.

 
1# doNothingProg2.s 
2# Minimum components of a C program, in assembly language. 
3# Bob Plantz - 6 June 2009 
4 
5        .text 
6        .globl  main 
7        .type   main, @function 
8main:   pushq   %rbp        # save callers frame pointer 
9        movq    %rsp, %rbp  # establish our frame pointer 
10 
11        movl    $0, %eax    # return 0 to caller 
12        movq    %rbp, %rsp  # restore stack pointer 
13        popq    %rbp        # restore callers frame pointer 
14        ret                 # back to caller
Listing 7.3: A “null” program (programmer assembly language).

After examining what the assembly language programmer did we will return to Listing 7.2 and look at the assembly language generated by the compiler.

Assembly language provides of a set of mnemonics that have a one-to-one correspondence to the machine language. A mnemonic is a short, English-like group of characters that suggests the action of the instruction. For example, “mov” is used to represent the instruction that copies (“moves”) a value from one place to another. Thus, the machine instruction

     4889E5

copies the entire 64-bit value in the rsp register to the rbp register. Even if you have never seen assembly language before, the mnemonic representation of this instruction in Listing 7.2,

9     movq    %rsp, %rbp  # establish our frame pointer

probably makes much more sense to you than the machine code. (The ‘q’ suffix on “mov” means a quadword (64 bits) is being moved.)

Strictly speaking, the mnemonics are completely arbitrary, as long as you have an assembler program that will translate them into the desired machine instructions. However, most assembler programs more or less use the mnemonics used in the manuals provided by CPU vendors.

The first thing to notice is that assembly language is line-oriented. That is, there is only one assembly language statement on each line, and none of the statements spans more than one line. A statement can continue onto subsequent lines, but this requires a special line-continuation character. This differs from the “free form” nature of C/C++ where the line structure is irrelevant. In fact, good C/C++ programmers take advantage of this to improve the readability of their code.

Next, notice that the pattern of each line falls into one of three categories:

Let us consider each field:

  1. The label field allows us to give a symbolic name to any line in the program. Since each line corresponds to a memory location in the program, other parts of the program can then refer to the memory location by name.
    1. A label consists of an identifier immediately followed by the “:” character. You, as the programmer, must make up these identifiers. The rules for creating an identifier are given below.
    2. Notice that most lines are not labeled.
  2. The operation field provides the basic purpose of the line. There are two types of operations:
    1. assembly language mnemonic — The assembler translates these into actual machine instructions, which are copied into memory when the program is to be executed. Each machine instruction will occupy from one to five bytes of memory.
    2. assembler directive(pseudo op) — Each of these operations begins with the period (“.”) character. They are used to direct the way in which the assembler translates the file. They do not translate directly into machine instructions, although some do cause memory to be allocated.
  3. The operand field specifies the arguments to be used by the operation. The arguments are specified in several different ways:
    1. an explicit — or literal — value, e.g., the integer 75.
    2. a name that has meaning to the assembler, e.g., the name of a register.
    3. a name that is made up by the programmer, e.g., the name of a variable or a constant.

    Different operations require differing numbers of operands — zero, one, two, or three.

  4. The comment field is just like a comment line, except it takes up only the remainder of the line. Since assembly language is not as easy to read as higher-level languages, good programmers will place a comment on almost every line.

The rules for creating an identifier are very similar to those for C/C++. Each identifier consists of a sequence of alphanumeric characters and may include other printable characters such as “.”, “_”, and “$”. The first character must not be a numeral. An identifier may be any length, and all characters are significant. Case is also significant. For example, “myLabel” and “MyLabel” are different. Compiler-generated labels begin with the “.” character, and many system related names begin with the “_” character. It is a good idea to avoid beginning your own labels with the “.” or the “_” character so that you do not inadvertently create one that is already in use by the system.

Integers can be used as labels, but they have a special meaning. They are used as local labels, which are sometimes useful in advanced assembly language programming techniques. They will not be used in this book.

The assembler program, as, will translate the file doNothingProg2.s (see Listing 7.3) into machine code and provide the memory allocation information for the operating system to use when the program is executed. We will first describe the contents of this file, then look at the GNU commands to convert it into an executable program.

Now we turn attention to the specific file in Listing 7.3, doNothingProg2.s. On line 5 you recognize

5        .text

as an assembler directive because it starts with a period character. It directs the assembler to place whatever follows in the text section.

What does “text section” mean? When a source code file is translated into machine code, an object file is produced. The object file organization follows the Executable and Linking Format (ELF). ELF files can be seen from two different points of view. Programs that store information in ELF files store it in sections. The ELF standard specifies many different types of sections, each depending on the type of information stored in it.

The .text directive specifies that when the following assembly language statements are translated into machine instructions, they should be stored in a text section in the object file. Text sections are used to store program instructions in machine code format.

GNU/Linux divides memory into different segments for specific purposes when a program is loaded from the disk. The four general categories are:

The operating system needs to view an ELF file as a set of segments. One of the functions of the ld program is to group sections together into segments so that they can be loaded into memory. Each segment contains one or more sections. This grouping is generally accomplished by arrays of pointers to the file, not necessarily by physically moving the sections. That is, there is still a section view of the ELF file remaining. So the information stored in an ELF file is grouped into sections, but it may or may not also be grouped into segments.

When the operating system loads the program into memory, it uses the segment view of the ELF file. Thus the contents of all the text sections will be loaded into the text segment of the program process.

This has been a very simplistic overview of ELF sections and segments. We will touch on the subject again briefly in Section 8.1. Further details can be found by reading the man page for elf and sources like [13] and [21]. The readelf program is also useful for learning about ELF files. It is included in the binutils collection of the GNU binary tools so is installed along with as and ld.

The assembler directive on line 6

6        .globl  main

has one operand, the identifier “main.” As you know, all C/C++ programs start with the function named “main.” In this book, we also start our assembly language programs with a main function and execute them within the C/C++ runtime environment. The .globl directive makes the name globally known, analogous to defining an identifier outside a function body in C/C++.1 That is, code outside this file can refer to this name. When a program is executed, the operating system does some preliminary set up of system resources. It then starts program execution by calling a function named “main,” so the name must be global in scope.

One can write stand-alone assembly language programs. In GNU/Linux this is accomplished by using the __start label on the first instruction in the program. The object (.o) files are then linked using the ld command directly rather than use gcc. See Section 8.6.

The assembler directive on line 7

7        .type   main, @function

has two operands: a name and a type. The name is entered into the symbol table (see Section 7.3). In addition to the machine code, the object file contains the symbol table along with information about each symbol. The ELF format recognizes two types of symbols: data and function. The .type directive is used here to specify that the symbol main is the name of a function.

None of these three directives get translated into actual machine instructions, and none of them occupy any memory in the finished program. Rather, they are used to describe the characteristics of the statements that follow.

IMPORTANT!
You need to distinguish
assembler directives
— instructions to the assembler (the program that translates assembly language into machine code).

from

assembly language instructions
— the code that gets translated into machine code.

What follows next in Listing 7.3 are the actual assembly language instructions. They will occupy memory when they are translated. The first instruction is on line 8:

8    main:   pushq   %rbp        # save callers frame pointer

It illustrates the use of all four fields on a line of assembly language.

  1. First, there is a label on this line, main. Since this name has been declared as a global name (with the assembler directive .globl main), functions defined in other files can call this function by name. In particular, after the operating system has loaded this function into memory, it can call main, and execution will start with this line.
  2. The operation is a pushq instruction, which stands for “push quadword.” It “pushes” a value onto the call stack. This will be explained in Section 8.2 (page 551). For now, this is a technique for temporarily saving the value stored in the operand.

    The “quadword” part of this instruction means that 64 bits are moved. As you will see in more detail later, as requires that a single letter be appended to most instructions:

    b “byte” operand is 8 bits
    w “word” operand is 16 bits
    l “long” operand is 32 bits
    q “quadword” operand is 64 bits

    to specify the size of the operand(s).

  3. There is one operand, %rbp. The GNU assembler requires the “%” prefix on the operand to indicate that this is the name of a register in the CPU. This instruction saves the 64-bit value in the rbp register on the call stack.

    The value in the rbp register is an address. In 64-bit mode addresses can be 64 bits long, and we have to save the entire address.

  4. Finally, we have added a comment to this line. The comment shows that the purpose of this instruction is to save the value that the calling function was using as a frame pointer. (The reasons for doing this will be explained in Chapter 8.)

The next line

9            movq    %rsp, %rbp  # establish our frame pointer

uses only three of the fields.

  1. First, there is no label on this line. Notice that the label field is left blank by using the tab key to indent into the second field, the operation field. It is important for readability that you use the tab key to keep the beginning of each field lined up in a column.
  2. The operation is a movq instruction, which stands for “move quadword.” It “moves” a bit pattern from one location to another. Actually, “copy” is probably a better term than “move,” because it does not change the bit pattern in the place copied from. But “move” has become the accepted terminology for this operation.
  3. There are two operands, %rsp and %rbp. Again, the “%” prefix to each operand means that it is the name of a register in the CPU.
    1. The order of the operands in as is: source, destination.
    2. Thus this instruction copies the 64-bit value in the rsp register to the rbp register in the CPU.
  4. Finally, I have added a comment to this line. The comment shows that the purpose of this instruction is to establish a new frame pointer in this function. (Again, the reasons for doing this will be explained in Chapter 8.)

As the name of this “program” implies, it does not do anything, but it still must return to the operating system. GNU/Linux expects the main function to return an integer to it, and the return value is placed in the eax register. Zero means that the program executed with no errors. This may not make a lot of sense to you at this point, but it should become clearer later in the book. Returning the integer zero to the operating system is accomplished on line 12:

11            movl    $0, %eax    # return 0 to caller
  1. This line also has no label. After indenting, it begins with a movl instruction.
  2. The first operand is prefixed with a “$” character, which indicates that the operand is to be taken as a literal value. That is, the source operand is the integer zero. You recognize that the second operand is the eax register in the CPU. This instruction places a copy of the 32-bit integer zero in the eax register.

    Even though the CPU is in 64-bit mode, 64-bit integers are seldom needed. So the default behavior of environment is to use 32 bits for ints. 64-bit ints can be specified in C/C++ with either the long or the long long modifier. In assembly language the programmer would use quadwords for integers. (As pointed out on page 460 this instruction also zeros the high-order 32 bits of the rax register. But you should not write code that depends upon this behavior.)

  3. The comment on this line shows that the purpose of this instruction is to return a zero to the calling function (the operating system).

The first two instructions in this function,

8    main:   pushq   %rbp        # save callers frame pointer 
9            movq    %rsp, %rbp  # establish our frame pointer

form a prologue to the actual processing that is performed by the function. They changed some values in registers and used the call stack. Before returning to the operating system, it is essential that an epilogue be executed to restore the values. The following two instructions accomplish this.

12            movq    %rbp, %rsp  # restore stack pointer 
13            popq    %rbp        # restore callers frame pointer
  1. No labels are used on these lines. The movq instruction ensures that the stack pointer is moved back to the location where the rbp register was saved. Since the stack pointer was not used in this function, this instruction is not necessary here. But your program will crash if the stack pointer is not in the correct location when the next instruction is executed, so it is a good idea to get into the habit of always using both these instructions at the end of a function.
  2. The popq instruction copies the 64-bit value on the top of the call stack into the operand and moves the stack pointer accordingly. (You will learn about using the stack pointer in Section 8.2.) The operand in this case is the rbp register.
  3. The comment states that the reason for the popq instruction is to restore the frame pointer value for the calling function (the operating system since this is main).

Finally, this function must return to the function that called it, which is back in the operating system.

14            ret        # back to caller
  1. This line has no label. And the instruction does not specify any operands. This is the instruction for returning program control back to the function that called this one. In this particular case, since this is the main function, control is passed back to the operating system.
  2. Here is an example of an instruction that changes the value in the instruction pointer register (rip) in order to alter the linear flow of the program. We will see later the mechanism that is used to implement this.
  3. The comment on this line briefly describes the reason for the instruction.

7.2.1 First instructions

As you can see from this example, even a function that does nothing requires several instructions. The most commonly used assembly language instruction is

movs source, destination

where s denotes the size of the operand:

s meaning number of bits
b byte 8
w word 16
l longword 32
q quadword 64

In the Intel syntax, the size of the data is determined by the operand, so the size character (b, w, l, or q) is not appended to the instruction, and the order of the operands is reversed:

Intel® Syntax

mov destination, source

The mov instruction copies the bit pattern from the source operand to the destination operand. The bit pattern of the source operand is not changed. If the destination operand is a register and its size is less than 64 bits, the effect on the other bits in the register is shown in Table 7.1.


size destination bits remaining bits



8 7 – 0 63 – 8 are unchanged
8 15 – 8 63 – 16, 7 – 0 are unchanged
16 15 – 0 63 – 16 are unchanged
32 31 – 0 63 – 32 are set to 0




Table 7.1: Effect on other bits in a register when less than 64 bits are changed.

The mov instruction does not affect the rflags register. In particular, neither the CF nor the OF flags are affected. No more than one of the two operands may be a memory location. Thus, in order to move a value from one memory location to another, it must be moved from the first memory location into a register, then from that register into the second memory location. (Accessing data in memory will be covered in Sections 8.1 and 8.4.)

The other instructions used in this “do nothing” program — pushq, popq, and ret — use the call stack. The call stack will be discussed in Section 8.2, which will then allow us to discuss these instructions. For now, you should memorize how to use them as “boilerplate” for the prologue and epilogue of each function.

7.2.2 A Note About Syntax

If you have any experience with x86 assembly language, the syntax used by the GNU assembler, as, will look a little strange to you. In principle, the syntax is arbitrary. A programmer could invent any sort of assembly language and write a program that would translate it into the appropriate machine code. But most CPU manufacturers publish a manual with a suggested assembly language syntax for their CPU.

Most assemblers for the x86 CPUs follow the syntax suggested by Intel®, but as uses the AT&T syntax. It is not radically different from Intel’s. Some of the more striking differences are:

AT&T

Intel®

operand order:

source, destination

destination, source

register names:

prefixed with the “%” character, e.g., %eax

just the name, e.g., eax

literal values:

prefixed with the “$” character, e.g., $123

just the value, e.g., 123

operand size:

use the b, w, l, or q suffix on opcode to denote byte, word, long, or quadruple word

determined by the register specification (more complicated if operand is stored in memory)

The GNU assembler, as, does not require the size suffix on instructions in all cases. From the info documentation for as:

9.13.4 Instruction Naming

-------------------------

Instruction mnemonics are suffixed with one character modifiers which
specify the size of operands.  The letters ‘b’, ‘w’, ‘l’ and ‘q’
specify byte, word, long and quadruple word operands.  If no suffix is
specified by an instruction then ‘as’ tries to fill in the missing
suffix based on the destination register operand (the last one by
convention).  Thus, ‘mov %ax, %bx’ is equivalent to ‘movw %ax, %bx’;
also, ‘mov $1, %bx’ is equivalent to ‘movw $1, bx’.  Note that this is
incompatible with the AT&T Unix assembler which assumes that a missing
mnemonic suffix implies long operand size.  (This incompatibility does
not affect compiler output since compilers always explicitly specify
the mnemonic suffix.)

It is recommended that you get in the habit of using the size suffix letters when you begin writing your own assembly language. This will help you to avoid introducing obscure bugs in your code.

The assembler directives are typically not specified by the CPU manufacturer, so you will see a much wider variety of syntax, depending on the particular assembler program. We will not try to list any differences here.

The GNU assembler, as, also supports the Intel® syntax. The assembler directive .intel_syntax says that following assembly language is written in the Intel® syntax; .att_syntax says it is written in AT&T syntax. Using Intel® syntax, the assembly language code in Listing 7.3 would be written

main:   push   rbp
        mov    rbp, rsp
Intel® Syntax         mov    eax, 0
        mov    rsp, rbp
        pop    rbp
        ret

Keep in mind that gcc produces assembly language in AT&T syntax, so you will undoubtedly find it easier to use that when you write your own code. The .intel_syntax directive might be useful if somebody gives you an entire function written in Intel® syntax assembly language.

The syntax rules for our particular assembler, as, are described in an on-line manual that is in the GNU info format. as supports some two dozen computer architectures, so it is a challenge to wade through the info manual to find what you need. On the other hand, it provides the most up to date information. And it is especially important for learning how to use assembler directives because they are specific to the assembler.

Now would be a good time to start learning how to use info for as. As you encounter new assembly language concepts in this book, also look them up in info for as. If you are unfamiliar with info, at the GNU/Linux prompt, simply type

        $ info info

for a nice tutorial.

7.2.3 The Additional Assembly Language Generated by the Compiler

First, notice that the compiler-generated labels (.LFB0, .LFE0) each begin with a period character, just like assembler directives. You can tell that they are labels because of the “:” immediately following.

If you compare the assembly language program in Listing 7.3 with that generated by the compiler in Listing 7.2, you can see that the compiler includes much more information in the file. Most of this information will not be used elsewhere in this book. We explain it here for completeness.

The first line,

1            .file   "doNothingProg1.c"

identifies the name of the C source file. When you write in assembly language this information clearly does not apply.

There are six .cfi_(Call Frame Information) directives in this code on lines 7, 9, 10, 12, 15, and 17. These are used to provide information that is helpful in error recovery and debugging the program when it is running. We will not use them in this book. The gcc option to generate assembly language code without them is -fno-asynchronous-unwind-tables. This will be discussed in more detail below.

The eight lines

5main: 
6.LFB0: 
7        .cfi_startproc 
8        pushq  %rbp 
9        .cfi_def_cfa_offset 16 
10        .cfi_offset 6, -16 
11        movq  %rsp, %rbp 
12        .cfi_def_cfa_register 6

set up the call stack for this function. The use of the call stack will be explained in more detail in Section 8.2 on page 551 and in subsequent Sections.

The labels generated by the compiler, .LFB0 and .LFE0 are used to mark the beginning and end of the program to improve the linking efficiency. Our programs will be very simple, so we will not need such labels.

Notice that the lines after the labels main, .LFB0, and .LFE0, are blank. The assembler does not generate any machine code for either of these two lines, so they do not take up any memory. The next thing that comes in memory is the

7            pushq   %rbp

instruction. Thus, both labels, main and .LFB0, apply to the address where this instruction is located.

We can use the -fno-asynchronous-unwind-tables option to turn off the .cfi directives feature, as shown in Listing 7.4. The GNU/Linux command is:

     gcc -S -O0 -fno-asynchronous-unwind-tables doNothingProg1.c

which gives the compiler-generated assembly language in Listing 7.4.

 
1        .file  "doNothingProg1.c" 
2        .text 
3        .globlmain 
4        .type  main, @function 
5main: 
6        pushq  %rbp 
7        movq  %rsp, %rbp 
8        movl  $0, %eax 
9        popq  %rbp 
10        ret 
11        .size  main, .-main 
12        .ident"GCC: (Ubuntu/Linaro 4.7.0-7ubuntu3) 4.7.0" 
13        .section      .note.GNU-stack,"",@progbits
Listing 7.4: A “null” program (gcc assembly language). We have used the -fno-asynchronous-unwind-tables compiler option to remove the .cfi directives.

Lines 19 – 21 in Listing 7.2 are the same as lines 11 – 13 in Listing 7.4. They also use directives that do not apply to the programs we will be writing in this book.

Finally, you may have noticed that the main label is on a line by itself in Listing 7.2 but not in Listing 7.3. When there is only a label on a line, no machine instructions are generated, and no memory is allocated. Thus, the label really applies to the next line. It is common to place labels on their own line so that longer, easier to read labels can be used while still keeping the operations visually lined up in a column. This technique is illustrated in Listing 7.5.

 
1# doNothingProg3.s 
2# The minimum components of a C program, written in assembly 
3# language. Same as doNothingProg2.s, except with the main 
4# label on its own line. 
5# Bob Plantz - 7 June 2009 
6 
7        .text 
8        .globl  main 
9        .type   main, @function 
10main: 
11        pushq   %rbp        # save callers frame pointer 
12        movq    %rsp, %rbp  # establish our frame pointer 
13 
14        movl    $0, %eax    # return 0 to caller 
15        movq    %rbp, %rsp  # restore stack pointer 
16        popq    %rbp        # restore callers frame pointer 
17        ret                 # back to caller
Listing 7.5: The “null” program rewritten to show a label placed on its own line.

7.2.4 Viewing Both the Assembly Language and C Source Code

The gcc compiler provides a set of options that will allow you to generate a listing that shows both the assembly language and the corresponding C statement(s). This will allow you to more easily see the assembly language that the compiler generates to implement a C statement in assembly language. Compiling the program in Listing 7.1 with the command:

      $ gcc -O0 -g -Wa,-adhls doNothingProg1.c > doNothingProg1.lst

generates the assembly language code in Listing 7.6.

 
1GAS LISTING /tmp/ccYnmi4d.s               page 1 
2 
3 
4   1                         .file  "doNothingProg1.c" 
5   2                         .text 
6   3                   .Ltext0: 
7   4                         .cfi_sections  .debug_frame 
8   5                         .globlmain 
9   7                   main: 
10   8                   .LFB0: 
11   9                         .file 1 "doNothingProg1.c" 
12   1:doNothingProg1.c **** /* 
13   2:doNothingProg1.c ****  * doNothingProg1.c 
14   3:doNothingProg1.c ****  * The minimum components of a C program. 
15   4:doNothingProg1.c ****  * Bob Plantz - 6 June 2009 
16   5:doNothingProg1.c ****  */ 
17   6:doNothingProg1.c **** 
18   7:doNothingProg1.c **** int main(void) 
19   8:doNothingProg1.c **** { 
20  10                         .loc 1 8 0 
21  11                         .cfi_startproc 
22  12 0000 55                 pushq  %rbp 
23  13                   .LCFI0: 
24  14                         .cfi_def_cfa_offset 16 
25  15                         .cfi_offset 6, -16 
26  16 0001 4889E5             movq  %rsp, %rbp 
27  17                   .LCFI1: 
28  18                         .cfi_def_cfa_register 6 
29   9:doNothingProg1.c ****     return 0; 
30  19                         .loc 1 9 0 
31  20 0004 B8000000           movl  $0, %eax 
32  20      00 
33  10:doNothingProg1.c **** } 
34  21                         .loc 1 10 0 
35  22 0009 5D                 popq  %rbp 
36  23                   .LCFI2: 
37  24                         .cfi_def_cfa 7, 8 
38  25 000a C3                 ret 
39  26                         .cfi_endproc 
40  27                   .LFE0: 
41  29                   .Letext0:  
42 
43GAS LISTING /tmp/ccYnmi4d.s               page 2 
44 
45 
46DEFINED SYMBOLS 
47                            *ABS*:0000000000000000 doNothingProg1.c 
48     /tmp/ccYnmi4d.s:7      .text:0000000000000000 main 
49 
50NO UNDEFINED SYMBOLS
Listing 7.6: Assembly language embedded in C source code listing. The line number in the C source file is also indicated with the .loc assembler directive.

The “-g” option tells the compiler to include symbols for debugging. “-Wa,” passes the immediately following options to the assembly phase of the compilation process. Thus, the options passed to the assembler are “-adhls”, which cause the assembler to generate a listing with the following characteristics:

As you can see above, the secondary letters can be combined with one “-a.” The listing is written to standard out, which can be redirected to a file. We gave this file the “.lst” file extension because it cannot be assembled.

Aside: The version of gcc used on Ubuntu 12.04 generates .cfi directives for the assembler. The -fno-dwarf2-cfi-asm option causes them not to be generated, but then unwind tables are generated. The -fno-asynchronous-unwind-tables option causes neither to be generated in the assembly language code. This may differ in other versions of gcc and other Linux distributions.

7.2.5 Minimum Program in 32-bit Mode

The x86-64 processors can also run in 32-bit mode. Most GNU/Linux distributions also provide a 32-bit version. Some distributions are only available in 32-bit.

The gcc option to compile a program for 32-bit mode is -m32. Listing 7.7 shows the assembly language generated by the GNU/Linux command:

     gcc -S -O0 -m32 -fno-asynchronous-unwind-tables doNothingProg1.c

 
1        .file  "doNothingProg1.c" 
2        .text 
3        .globlmain 
4        .type  main, @function 
5main: 
6        pushl  %ebp 
7        movl  %esp, %ebp 
8        movl  $0, %eax 
9        popl  %ebp 
10        ret 
11        .size  main, .-main 
12        .ident"GCC: (Ubuntu/Linaro 4.7.0-7ubuntu3) 4.7.0" 
13        .section      .note.GNU-stack,"",@progbits
Listing 7.7: A “null” program (gcc assembly language in 32-bit mode).

The only differences between the 32-bit and 64-bit versions are that all the instructions use the “l” suffix to indicate “longword” because addresses are 32 bits, and only the 32-bit portion of the registers is used. That is, esp instead of rsp, etc.

An assembly language programmer would comment the code, as shown in Listing 7.8.

 
1# doNothingProg4.s 
2# The minimum components of a C program, written in assembly 
3# language. A 32-bit version of doNothingProg1.s. 
4# Bob Plantz - 7 June 2009 
5 
6        .text 
7        .globl  main 
8        .type   main, @function 
9main: 
10        pushl   %ebp        # save callers frame pointer 
11        movl    %esp, %ebp  # establish our frame pointer 
12        movl    $0, %eax    # return 0 to caller 
13        movl    %ebp, %esp  # restore stack pointer 
14        popl    %ebp        # restore callers frame pointer 
15        ret                 # back to caller
Listing 7.8: A “null” program (programmer assembly language in 32-bit mode).

This assembly language version also includes the instruction to restore the stack pointer near the end of the function:

13            movl    %ebp, %esp  # restore stack pointer

Since this function does not use the stack, this instruction is not required, but including it is a good habit to establish as you start to write your own functions in assembly language.

7.3 Assemblers and Linkers

We present a highly simplified view of how assemblers and linkers work here. The goal of this presentation is to introduce the concepts. Most assemblers and linkers have capabilities that go far beyond the concepts described here (e.g., macro expansion, dynamic load/link). We leave a more thorough discussion of assemblers and linkers to a book on systems programming.

7.3.1 Assemblers

An assembler must perform the following tasks:

Since the numeric value of an address may be required before an instruction can be translated into machine language, there is a problem with forward references to memory locations. For example, a code sequence like:

1# if (response == y’) 
2        cmpb    $y, response  # was it y’? 
3        jne     noChange        # no, there is no change 
4 
5# then print the "save" message 
6        movq    $saveMsg, %rbx  # point to first char 
7saveLoop: 
8        cmpb    $0, (%rbx)      # at null character? 
9        je      saveEnd         # yes, exit loop 
10 
11        movl    $1, %edx        # no, send one byte 
12        movq    %rbx, %rsi      #    at this location 
13        movl    $STDOUT, %edi   #        to screen. 
14        call    write 
15        incq    %rbx            # increment char pointer 
16        jmp     saveLoop        # check at top of loop 
17saveEnd: 
18        jmp     allDone         # skip over false block 
19 
20# else print the "discard" message 
21noChange: 
22        movq    $discardMsg, %rbx # point to first char

creates a problem for the assembler when it needs to translate the

3        jne     noChange # no, there is no change

instruction on line 3. (Don’t forget that assembly language is line oriented; translation is done one line at a time.) When this code sequence is executed, the immediately previous instruction (cmpb    $’y’, response) compares the byte stored at location response with the character ‘y’. If they are not equal, i.e., a ‘y’ is not stored at location response, the jne instruction causes program flow to jump to location noChange. In order to accomplish this action, the translation of this instruction (the machine code) must include a numerical value that specifies how far to jump. That is, it must include the distance, in number of bytes, between the jne instruction and the memory location labeled noChange on line 23. In order to compute this distance, the assembler must determine the address that corresponds to the label noChange when it translates this instruction, but the assembler has not even encountered the noChange label, much less determined its corresponding address.

The simplest solution is to use a two-pass assembler:

  1. The first pass builds a symbol table, which provides an address for each memory label.
  2. The second pass performs the actual translation into machine language, consulting the symbol table for numeric values of the symbols.

Algorithm 7.1 is a highly simplified description of how the first pass of an assembler works.


Algorithm 7.1: First pass of a two-pass assembler.
1:  LocationCounter 0
2:  Get first line of source code.
3:  while more lines do
4:   if line has a label then
5:   SymbolTable.Symbol label
6:   SymbolTable.Location LocationCounter
7:   end if
8:   Determine number of bytes required by the line when assembled.
9:   LocationCounter LocationCounter + NumberOfBytes
10:   Get next line of source code.
11:  end while

The symbol table is carried from the first pass to the second. The second pass also consults a table of operation codes, which provides the machine code corresponding to each instruction mnemonic. A highly simplified description of the second pass is given in Algorithm 7.2.


Algorithm 7.2: Second pass of a two-pass assembler.
1:  LocationCounter 0
2:  Get first line of source code.
3:  while more lines do
4:   if line is an instruction then
5:   Find machine code from Op-Code Table.
6:   Find symbol value from SymbolTable.
7:   Assemble instruction into machine code.
8:   else
9:   Carry out directive.
10:   end if
11:   Write machine code to object file.
12:   Determine number of bytes used.
13:   LocationCounter LocationCounter + NumberOfBytes
14:   Get next line of source code.
15:  end while

7.3.2 Linkers

Look again at the code sequence above. On line 14 there is the instruction:

        call    write

This call to the write function is a reference to a memory label outside the file being assembled. Thus, the assembler has no way to determine the address of write for the symbol table during the first pass. The only thing the assembler can do during the second pass is to leave enough memory space for the address of write when it assembles this instruction. The actual address will have to be filled in later in order to create the entire program. Filling in these references to external memory locations is the job of the linker program.

The algorithm for linking functions together is very similar to that of the assembler. The same forward reference problem exists. Again, the simplest solution is to use a two-pass linker program.

The highly simplified algorithm in Algorithms 7.3 and 7.4 also provide for loading the entire program into memory. The functions are linked together as they are loaded. In practice, this is seldom done. For example, the GNU linker, ld, does not load the program into memory. Instead, it creates another machine language file — the executable program. The executable program file contains all the functions of the program with all the cross-function memory references resolved. Thus ld is a link editor program.

Getting even more realistic, many of the functions used by a program are not even included in the executable program file. They are loaded as required when the program is executing. The link editor program must provide dynamic links for the executable program file.

However, you can get the general idea of linking separately assembled (or compiled) functions together by studying the algorithms in Algorithms 7.3 and 7.4. In particular, notice that the assembler (or compiler) must include other information in addition to machine code in the object file. The additional information includes:

  1. The name of the function.
  2. The name of each external memory reference.
  3. The location relative to the beginning of the function where the external memory reference is made.


Algorithm 7.3: First pass of a two-pass linker.
1:  LocationCounter 0
2:  Open first object file.
3:  while more object files do
4:   GlobalSymbolTable.Symbol function name
5:   GlobalSymbolTable.Location LocationCounter
6:   Determine number of bytes required by the function.
7:   LocationCounter LocationCounter + NumberOfBytes
8:   Open next object file.
9:  end while


Algorithm 7.4: Second pass of a two-pass linker.
1:  MemoryPointer address from OS
2:  Open first object file.
3:  while more object files do
4:   while more machine code do
5:   CodeByte Read byte of code from object file.
6:   MemoryPointer CodeByte ⊳  ’*’ is C/C++ dereference operator
7:   MemoryPointer MemoryPointer + 1
8:   end while
9:   while more external memory references do
10:   Get value from GlobalSymbolTable.
11:   Determine determine where value should be stored.
12:   Store value in code that was just loaded.
13:   end while
14:   Open next object file.
15:  end while

7.4 Creating a Program in Assembly Language

Since we are concerned with assembly language in this book, let us go through the steps of creating a program for the assembly language source code in Listing 7.5. Figure 7.1 is a screen shot of what I did with my typing in boldface. The notation I use here assumes that I am doing this for a class named CS 252, and my instructor has specified that each project should be submitted in a directory named CS252lastNameNN , where lastName is the student’s surname and NN is the project number.2 I have appended .0 to the project folder name for my own use. As I develop my project, subsequent versions will be numbered .1, .2, ….


____________________________________________________________________________________

bob$ mkdir CS252plantz01.0
bob$ cd CS252plantz01.0/
bob$ ls
bob$ pwd /home/bob/CS252/CS252plantz01.0
bob$ gedit doNothingProg.s

This is where I used gedit to enter the program from Listing 7.5, saved the program, and quit gedit.

bob$ ls
doNothingProg.s
bob$ as –gstabs -o doNothingProg.o doNothingProg.s
bob$ ls
doNothingProg.o doNothingProg.s
bob$ gcc -o doNothingProg doNothingProg.o
bob$ ls
doNothingProg doNothingProg.o doNothingProg.s
bob$ ./doNothingProg
bob$
____________________________________________________________________________________

Figure 7.1: Screen shot of the creation of a program in assembly language.


Let us go through the steps in Figure 7.1 one line at a time, explaining each line.
bob$ mkdir CS252plantz01.0

I create a directory named “CS252plantz01.0.” All the files that you create for each program should be kept in a separate directory only for that program.

bob$ cd CS252plantz01.0/

I make the newly created subdirectory the current working directory.

bob$ ls
bob$ pwd /home/bob/CS252/CS252plantz01.0

These two commands show that the new subdirectory is empty and where my current working directory is located within the file hierarchy.

bob$ gedit doNothingProg.s

This starts up the gedit program and creates a new file named “doNothingProg.s.” I typed my program using the text editor, saved the file, and quit the text editor. You may use any text editor. Avoid using a word processor because it will probably add formatting codes to the file.

bob$ ls
doNothingProg.s

This shows that I have created the file, doNothingProg.s.

bob$ as –gstabs -o doNothingProg.o doNothingProg.s
bob$ ls
doNothingProg.o doNothingProg.s

On the first line, I invoke the assembler, as. The –gstabs option directs the assembler to include debugging information with the output file. We will very definitely make use of the debugger! The -o option is followed by the name of the output (object) file. You should always use the same name as the source file, but with the .o extension. The second command simply shows the new file that has been created in my directory.

bob$ gcc -o doNothingProg doNothingProg.o
bob$ ls
doNothingProg doNothingProg.o doNothingProg.s

Next I link the object file. Even though there is only one object file, this step is required in order to bring in the GNU/Linux libraries needed to create an executable program. As in as, the -o option is used to specify the name of a file. In the linking case, this will be the name of the final product of our efforts.

Note:
The linker program is actually ld. The problem with using it directly, for example,
ld -o doNothingProg doNothingProg.o *** DOES NOT WORK ***

is that you must also explicitly specify all the libraries that are used. By using gcc for the linking, the appropriate libraries are automatically included in the linking.

bob$ ./doNothingProg
bob$

Finally, I execute the program (which does nothing).

7.5 Instructions Introduced Thus Far

This summary shows the assembly language instructions introduced thus far in the book. It should be sufficient for doing the exercises in the current chapter. The page number where the instruction is explained in more detail, which may be in a subsequent chapter, is also given. The summary will be repeated and updated, as appropriate, at the end of each succeeding chapter in the book. This book provides only an introduction to the usage of each instruction. You need to consult the manuals ([2][6], [14][18]) in order to learn all the possible uses of the instructions.

7.5.1 Instructions

data movement:
opcode source destination action page





movs $imm/%reg %reg/mem move 506





movs mem %reg move 506





popw %reg/mem pop from stack 566





pushw $imm/%reg/mem push onto stack 566










s = b, w, l, q; w = l, q

arithmetic/logic:
opcode source destination action page





cmps $imm/%reg %reg/mem compare 676





cmps mem %reg compare 676





incs %reg/mem increment 698










s = b, w, l, q

program flow control:
opcode location action page




call label call function 546




je label jump equal 679




jmp label jump 691




jne label jump not equal 679




ret return from function 583








7.6 Exercises

The functions you are asked to write in these exercises are not complete programs. You can check that you have written a valid function by writing a main function in C that calls the function you have written in assembly language. Compile the main function with the -c option so that you get the corresponding object (.o) file. Assemble your assembly language file. Make sure that you specify the debugging options when compiling/assembling. Use the linking phase of gcc to link the .o files together. Run your program under gdb and set a breakpoint in your assembly language function. (Hint: you can specify the source file name in gdb commands.) Now you can verify that your assembly language function is being called. If the function returns a value, you can print that value in the main function using printf.

7-1

7.2) Write the C function:

/* f.c */  
int f(void) {  
   return 0;  
}

in assembly language. Make sure that it assembles with no errors. Use the -S option to compile f.c and compare gcc’s assembly language with yours.

7-2

7.2) Write the C function:

/* g.c */  
void g(void) {  
}  

in assembly language. Make sure that it assembles with no errors. Use the -S option to compile g.c and compare gcc’s assembly language with yours.

7-3

7.2) Write the C function:

/* h.c */  
int h(void) {  
   return 123;  
}

in assembly language. Make sure that it assembles with no errors. Use the -S option to compile h.c and compare gcc’s assembly language with yours.

7-4

7.2) Write three assembly language functions that do nothing but return an integer. They should each return different, non-zero, integers. Write a C main function to test your assembly language functions. The main function should capture each of the return values and display them using printf.

7-5

7.2) Write three assembly language functions that do nothing but return a character. They should each return different characters. Write a C main function to test your assembly language functions. The main function should capture each of the return values and display them using printf.

7-6

7.2, §6.5) Write an assembly language function that returns four characters. The return value is always in the eax register in our environment, so you can store four characters in it. The easiest way to do this is to determine the hexadecimal value for each character, then combine them so you can store one 32-bit hexadecimal value in eax.

Write a C main function to test your assembly language function. The main function should capture the return values and display them using the write system call.

Explain the order in which they are displayed.