Skip to main content

Section 9.1 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.” Let us start with the minimal C program— a main function that does nothing.

/* doNothingProg1.c
 * The minimum components of a C program.
 * 2017-09-29: Bob Plantz
 */

int main(void)
{
  return 0;
}
Listing 9.1.1. A “null” program (C).

This program does nothing except return a zero, but it provides a good start for learning about the assembly language structure of a program.

Note: Most of the programs from here on in this book will be presented in three versions. The “(C)” at the end of the caption in this listing indicates that this is the C version. Similarly, I will use “(gcc asm)” for the assembly language generated by the compiler and “(prog asm)” for assembly language written by a programmer.

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 as described in Programming with GNU Software[10]. The description here assumes a single C source file, filename.c.

Preprocessing

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 command line option, which writes the resulting C source code to standard out.

Compilation

Translates 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 command line option, which writes the assembly language source code to filename.s.

Assembly

Translation of the assembly language into machine code by invoking the as program. Compilation can be stopped at the end of the assembly phase with the -c command line option, which writes the machine code to filename.o.

Linking

The process of combining the machine code that results from the assembly phase with other machine code from the standard C libraries and other machine code modules, and resolving the addresses. 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 command line option.

Next, we will use the -S command line option to look at the assembly language that the compiler produces:

pi@rpi3:~/book-progs/chap09 $gcc -S -O0 doNothingProg1.c

This creates the file doNothingProg1.s (Listing 9.1.2), which contains the assembly language generated by the 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 tells the compiler not to 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.)

	.arch armv6
	.eabi_attribute 28, 1
	.eabi_attribute 20, 1
	.eabi_attribute 21, 1
	.eabi_attribute 23, 3
	.eabi_attribute 24, 1
	.eabi_attribute 25, 1
	.eabi_attribute 26, 2
	.eabi_attribute 30, 6
	.eabi_attribute 34, 1
	.eabi_attribute 18, 4
	.file	"doNothingProg1.c"
	.text
	.align	2
	.global	main
	.syntax unified
	.arm
	.fpu vfp
	.type	main, %function
main:
	@ args = 0, pretend = 0, frame = 0
	@ frame_needed = 1, uses_anonymous_args = 0
	@ link register save eliminated.
	str	fp, [sp, #-4]!
	add	fp, sp, #0
	mov	r3, #0
	mov	r0, r3
	add	sp, fp, #0
	@ sp needed
	ldr	fp, [sp], #4
	bx	lr
	.size	main, .-main
	.ident	"GCC: (Raspbian 6.3.0-18+rpi1) 6.3.0 20170516"
	.section	.note.GNU-stack,"",%progbits
Listing 9.1.2. A “null” program (gcc asm).

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 9.1.2 was generated by release 4.9.2 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 my assembly language version of a “do nothing” program in Listing 9.1.3. Naturally, I have added comments to improve readability. After examining what the assembly language programmer did we will return to Listing 9.1.2 and look at the assembly language generated by the compiler.

@ doNothingProg2.s
@ Minimum components of a C program, in assembly language.
@ 2017-09-29: Bob Plantz 

@ Define my Raspberry Pi
        .cpu    cortex-a53
        .fpu    neon-fp-armv8
        .syntax unified         @ modern syntax

@ Program code
        .text
        .align  2
        .global main
        .type   main, %function
main:
        str     fp, [sp, -4]!   @ save caller frame pointer
        add     fp, sp, 0       @ establish our frame pointer

        mov     r3, 0           @ return 0;
        mov     r0, r3          @ return values go in r0

        sub     sp, fp, 0       @ restore stack pointer
        ldr     fp, [sp], 4     @ restore caller's frame pointer
        bx      lr              @ back to caller
Listing 9.1.3. A “null” program (prog asm).
Assembly Language

A set of mnemonics that have a one-to-one correspondence to the machine language.

Mnemonic

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

\begin{equation*} \hex{0xe3a03000} \end{equation*}

copies the value \(0\) into all 32 bits of the r3 register in the CPU. Even if you have never seen assembly language before, the mnemonic representation of this instruction in Listing 9.1.2,

mov     r3, 0

probably makes much more sense to you than the machine code, even without the programmer's comment.

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:

  • The first three lines begin with the ‘@’ character. The rest of the line is written in English and is easily read. The ‘@’ character any place on a line designates the rest of the line as a comment. Just as with a high-level language, comments are intended solely for the human reader and have no effect on the program.

  • The fourth line is blank. Blank lines should be used to improve readability. (Well, they will improve readability once you learn how to read assembly language.)

  • The remaining eighteen lines are organized into columns. They probably do not make much sense to you at this point because they are written in assembly language, but if you look carefully, each of the assembly language lines is organized into four possible fields:

    label:    operation    operand(s)    @ comment
    

    The assembler requires at least one space or tab character to separate the fields. When writing assembly language, your program will be much easier to read if you use the tab key to move from one field to the next.

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 will be given shortly.

    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. An assembly language mnemonic is translated into actual machine instructions, which are copied into memory when the program is to be executed. Each machine instruction will occupy four bytes of memory.

    2. An assembler directive or pseudo op 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. A name that has meaning to the assembler, e.g., the name of a register.

    2. A name that is made up by the programmer, e.g., the name of a variable or a constant..

    3. A literal—or explicit—value, e.g., the integer 75.

    An ARM instruction can have from zero to three operands. All except the store instruction are specified in the order:

    operation [destination[, source1[, source2[, source3]]]]

    We will see the difference with the store instruction shortly.

  4. The comment field is all the text immediately following an ‘@’ character. Since assembly language is not as easy to read as higher-level languages, good programmers will place a comment on almost every line. It is common to have entire lines that are comments.

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, except as noted below. An identifier may be any length, and all characters are significant. Identifiers are called symbols in info as; read about Symbol Names. Case is also significant. For example, “myLabel” and “MyLabel” are different. (This is obviously a bad thing to do.) 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 either of these characters 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.

Now we turn attention to the specific file in Listing 9.1.3. The first three directives:

.cpu    cortex-a53
.fpu    neon-fp-armv8
.syntax unified         @ modern syntax

identify the characteristics of the ARM processor this code will run on. There are many variations of the ARM architecture, and the assembler needs to know which one this code is intended for. The appropriate values for each directive for the various Raspberry Pi models are given in Table 9.1.4. Please note that I have only tested these on my original Raspberry Pi Model B and my Raspberry Pi 3 Model B. You may have to experiment to get the correct values for your Raspberry Pi. Possible values can be found in the info as documentation. The .syntax directive will be further explained in Section 9.2.

Table 9.1.4. Assembly language directives to specify your Raspberry Pi model. Note: I have only tested these on my original Raspberry Pi Model B and my Raspberry Pi 3 Model B.
Raspberry Pi .cpu .fpu
Pi Zero
Pi 1 A+ arm1176jzf-s vfp
Pi 1 B+
Pi 2 B cortex-a7 neon-vfpv4
Pi 3 B cortex-a53 neon-fp-armv8

Part way down the listing you see the

.text

assembler directive. 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, which has a specific format. In our environment 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:

Text Segment

Where program instructions and constant data are stored. The operating system prevents a program from changing anything stored in the text segment, treating it as read-only memory during program execution. Also called code segment.

Data Segment

Where global variables and static local variables are stored. Both read-only and read-write data segments can occur in a program. It remains in place for the duration of program execution.

Stack Segment

Where automatic local variables and the data that links functions are stored. It is read-write memory that is allocated and deallocated dynamically as the program executes.

Heap Segment

The pool of memory available when a C program calls the malloc function (or C++ calls new). It is read-write memory that is allocated and deallocated by the program.

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 ELF 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, for example, 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 Chapter 10. Further details can be found by reading the man page for elf and sources like [9]. 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 first assembler directive in the text segment,

.align  2

has one operand, 2. For the ARM, this tells the assembler to ensure that the lowest two bits of the starting address of the generated code are zero. That is, the addressing is adjusted, incremented if necessary, to be a multiple of four. Each machine instruction is four bytes long, so this ensures proper alignment of the instructions in memory. (For other processors the meaning may be different. For example, for the Intel/AMD processors it specifies byte alignment. So you would use “.align 4” to get an address that is a multiple of four. Read the info as documentation for your specific processor.)

This is followed by the

.global  main

assembler directive, which 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 .global directive makes the name globally known, analogous to defining an identifier outside a function body in C/C++. 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.

Next we see

.type   main, %function

which declares the label, main, as the name of a function in the program.

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.

Before discussing the actual assembly language instructions, let us go back the the remaining assembler directives generated by the compiler in Listing 9.1.2. ARM CPUs are produced with various options. The first 13 lines in this listing identify this code as being intended for the ARM CPU used on the Raspberry Pi. Since we will only execute the programs in this book on a Raspberry Pi, most of this information is not needed. In particular, we do not need the .eabi_attributes directives.

The line,

.file:  "doNothingProg1.c"

simply identifies the original C source code file. The .size directive gives the number of bytes in the code, and the .ident directive lists the version of the compiler that produced this assembly language. We will not need to use these directives in the assembly language we write.

The .section directive provides information about the memory used for the stack. We do not need to use it for this purpose in the assembly language we write in this book, but you will see another use for it later.