C versus Assembly Programming

When programming in a high-level language, such as C, we are shielded from the detailed, machine-level implementation of our program. In contrast, when writing programs in assembly code, a programmer must specify exactly how the program manages memory and the low-level instructions the program uses to carry out the computation. Most of the time, it is much more productive and reliable to work at the higher level of abstraction provided by a high-level language. The type checking provided by a compiler helps detect many program errors and makes sure we reference and manipulate data in consistent ways. With modern, optimizing compilers, the generated code is usually at least as efficient as what a skilled, assembly-language programmer would write by hand. Best of all, a program written in a high-level language can be compiled and executed on a number of different machines, whereas assembly code is highly machine specific. Even though optimizing compilers are available, being able to read and understand assembly code is an important skill for serious programmers. By invoking the compiler with appropriate flags, the compiler will generate a file showing its output in assembly code. Assembly code is very close to the actual machine code that computers execute. Its main feature is that it is in a more readable textual format, compared to the binary format of object code. By reading this assembly code, we can understand the optimization capabilities of the compiler and analyze the underlying inefficiencies in the code. As we will experience in Chapter 5 (of the book), programmers seeking to maximize the performance of a critical section of code often try different variations of the source code, each time compiling and examining the generated assembly code to get a sense of how efficiently the program will run. Furthermore, there are times when the layer of abstraction provided by a high-level language hides information about the run-time behavior of a program that we need to understand. For example, when writing concurrent programs using a thread package (covered in Chapter 11 of the book), it is important to know what type of storage is used to hold the different program variables. This information is visible at the assembly code level. The need for programmers to learn assembly code has shifted over the years from one of being able to write programs directly in assembly to one of being able to read and understand the code generated by optimizing compilers.

In these notes we present the details of a particular assembly language and see how C programs get compiled into this form of machine code. Reading the assembly code generated by a compiler involves a different set of skills than writing assembly code by hand. We must understand the transformations typical compilers make in converting the constructs of C into machine code. Relative to the computations expressed in the C code, optimizing compilers can rearrange execution order, eliminate unneeded computations, re-place slow operations such as multiplication by shifts and adds, and even change recursive computations into iterative ones. Understanding the relation between source code and the generated assembly can of-ten be a challenge— much like putting together a puzzle having a slightly different design than the picture on the box. It is a form of reverse engineering— trying to understand the process by which a system was created by studying the system and working backward. In this case, the system is a machine-generated, assembly language program, rather than something designed by a human. This simplifies the task of re-verse engineering, because the generated code follows fairly regular patterns, and we can run experiments, having the compiler generate code for many different programs. A brief history of the Intel architecture is the starting point of the companion document Machine-Level Programs on Linux/IA322. Intel processors have grown from rather primitive 16-bit processors in 1978 to the mainstream machines for today’s desktop computers. The architecture has grown correspondingly with new features added and the 16-bit architecture transformed to support 32-bit data and addresses. The result is a rather peculiar design with features that make sense only when viewed from a historical perspective. It is also laden with features providing backward compatibility that are not used by modern compilers and operating systems. We will focus on the subset of the features used by GCC and Linux. This allows us to avoid much of the complexity and arcane features of IA32.

The best references on IA32 are from Intel. Two useful references are part of their series on software development: the basic architecture manual gives an overview of the architecture from the perspective of an assembly-language programmer, and the instruction set reference manual gives detailed descriptions of the different instructions. These references (included in the companion document above mentioned) contain far more information than is required to understand Linux code. In particular, with flat mode addressing, all of the complexities of the segmented addressing scheme can be ignored. Also note that the GAS format used by the Linux assembler is very different from the standard format used in Intel documentation and by other compilers (particularly those produced by Microsoft). One main distinction is that the source and destination operands are given in the opposite order On a Linux machine, running the command info as will display information about the assembler. One of the subsections documents machine-specific information, including a comparison of GAS with the more standard Intel notation. Note that GCC refers to these machines as “i386”— it generates code that could even run on a 1985 vintage machine.

This document also complements the above-mentioned one, namely: ·  How control constructs in C, such as if, while, and switch statements, are implemented;

  •  Examine the problems of out of bounds memory references and the vulnerability of systems to buffer overflow attacks;
  • Some tips on using the GDB debugger for examining the runtime behavior of a machine-level program;
  • A brief presentation of GCC’s support for embedding assembly code within C programs; in some applications, the programmer must drop down to assembly code to access low-level features of the machine; embedded assembly is the best way to do this.


Learn More :