From C to Assembly

Embedded Systems with ARM Cortex-M #3

Going from C to Assembly

Before we study the syntax (grammar) and semantics (meaning) of assembly instructions, let us first examine the key differences between C and assembly.

C, like many other high-level programming languages, makes powerful abstraction of computer hardware to hide from programmers the details of how computation is implemented. High-level languages make program codes more concise, more portable, and easier to develop and debug.

However, the assembly language, a low-level programming language, offers programmers not only almost complete and fine-grained control of the underlying hardware, but also the flexibility of specifying how a computation should be carried out. Hence, it is often that a well-written assembly program is more efficient than its C counterpart is. Besides abstracting a microprocessor at different levels, some assembly instructions have no equivalent implementation in C.

In the below figure, we use a simple example, which computes the sum of two signed integers (1 and -2), to compare the hardware abstraction of C and assembly. We assume the variable x is stored in memory. Note that a variable may be stored in a register instead of in memory to improve the computation speed.

  • C abstracts away much detail of complex low-level computing operations. Accordingly, C provides a friendly and convenient programming interface to programmers. Because of strong abstraction, the same C program can be recompiled for two different hardware platforms, as given below:
Platform 1Platform 2
Signed integer representationTwo's complementOne's complement
Size of an integer (bits)3216
Operation on immediate numbers?NoYes
Dat endianBig endianLittle endian
  • In contrast, assembly language requires programmers to understand low-level details of the instruction set that this specific microprocessor supports. For example:

    • How many bits does an integer take in memory?

    • What is the data layout of the signed integer x in memory?

    • How are memory locations specified?

    • How is the integer x retrieved from memory?

    • How many operands can an addition support?

    • How is an overflow or carry handled on an addition?

Instruction Set Architecture Types

In general, there are three types of instruction set architecture (ISA):

Accumulator-Based Instruction Set

One of the ALU source operands is implicitly stored in a special register called accumulator, and the ALU result is saved into the accumulator.

The programmer does not have to specify this operand and the destination register in the program.

The accumulator-based instruction set was popular in the 1950s.

Stack-Based Instruction Set

All ALU operands are assumed to be on top of the stack, and the ALU result is also placed on top of the stack.

The stack is a special region of memory. Thus, programmers need to push the value of operands onto the stack before an ALU operation is called.

The stack-based instruction set was used in the 1960s.

Load-Store Instruction Set

ALU source or destination operands can be any general-purpose registers. ALU cannot directly use data stored in memory as operands.

ALU can only access data in memory by using load or store instructions.

Most modern processors are based on a load-store instruction set.

In the load-store instruction set, many arithmetic and logic instructions typically support two source operands that are stored in registers. The second operand of some instructions can also be a constant number, encoded directly in the instruction.

Load-store instruction set allows effective use of registers.

Compared with the other two types of instruction sets, the load-store instruction set is faster in performance. The accumulator-based instruction set must make an extra copy to store one of the source operands in the accumulator. The performance of a stack-based instruction set is undermined by the performance of memory because ALU must access the memory repeatedly. However, because there are many general-purpose registers available, the load-store instruction set can take full advantage of temporal locality exhibited in almost all applications, effectively reducing the number of accesses to slow memory.

In a load-store instruction set, data stored in memory cannot be ALU operands directly. Therefore, if we want to change some data in memory, software needs to perform a sequence of load-modify-store operations. The software should do the following:

  1. loads target data from memory to a register

  2. modifies the value in the register

  3. stores the new value in the register back to the memory

Example:

int x = -2;
x = x + 1;

To increment the value of variable x stored in memory by one, a load-modify-store sequence is carried out in four steps in a sequential order:

  1. set up the memory address

  2. load data from memory

  3. perform addition

  4. store new value back to memory

LDR r0, =x      ;Step 1: Setup address (Load memory address of x into r0)
LDR r1, [r0]    ;Step 2: Load (Register r0 holds the memory address of x)
ADD r1, r1, #1  ;Step 3: Modify (Increase the value in register r1 by 1)
STR r1, [r0]    ;Step 4: Store (Save the content in register r1 into memory)

Note an integer takes four bytes in memory. The 32-bit two's complement of -2 is 0xFFFFFFFE. Assume this number is stored in contiguous memory locations, starting at 0x20000000. The second LDR instruction loads this 32-bit integer into register r0 (LDR stands for load register). The last step is to save the 32-bit result (0xFFFFFFFF, i.e., -1) back to the memory region. After these four steps, the byte stored at memory location 0x20000003, 0x20000002, 0x20000001 and 0x20000000 is 0xFF, 0xFF, 0xFF, and 0xFF.

Assembly Instruction Format

A machine instruction consists of:

  • a binary operation code (opcode) denoting a specific operation to be carried out

  • zero or more operands specifying the inputs of the operation

In an assembly program, each binary opcode is replaced by its symbolic abbreviation, called instruction mnemonic. Using human-readable mnemonics instead of binary opcode makes developing an assembly program simpler and more convenient.

The general format of an assembly instruction for ARM Keil compilers is as follows:

label        mnemonic operand1, operand2, operand3    ; comments
  • label instruction is a reference to the memory address of this instruction. The assembler either replaces the label with the actual numeric memory address or memory address offset when generating the binary executable. The label is optional and must be unique within the same assembly program file. The label should start at the beginning of a line, without any leading whitespace. The instruction can start a new line, as shown below:
label
        mnemonic operand1, operand2, operand3    ; comments
  • The mnemonic represents the operation to be performed.

  • The number of operands varies, depending on each instruction. Some instructions have no operands. The comma "," is used to separate operands. Some instruction allows constant numbers (also called immediate numbers) as operands.

    • operand1 is the destination register

    • operand2 and operand3 are source operands

    • operand2 is usually a register

    • operand3 may be a register, an immediate number, a register shifted by a constant number of bits (using the Barrel shifter), or a register plus an offset (used for memory access)

  • Everything after the semicolon";" is a comment, which is an annotation explicitly declaring programmers' intentions or assumptions.

For GNU compilers, the instruction format is slightly different:

label: mnemonic operand1, operand2, operand3 /* comment */

Assembly Examples

Adding Two Integers

ADD r0, r2, r3    : r0 = r2 + r3

ADD is a mnemonic for arithmetic addition, register r0 is the destination operand, and registers r2 and r3 are two source operands. Register names are case-insensitive. We can also write r0 as R0, r1 as R1, and so on.

Subtracting an Immediate Number

SUB r3, r0, #3    ; r3 = r0 - 3

SUB is mnemonic for subtraction, register r3 is the destination operand, register r0 is the minuend, and the immediate number 3 is the subtrahend.

Setting the Value of a Register

MOV r0, #'M'    ; r0 = ASCII value of 'M' (0x4D)

MOV instruction sets the value of r0 to the ASCII value of character M. A constant number has the prefix '#'.

Variants of the ADD Instruction

ADD r1, r2, r3    ; r1 = r2 + r3
ADD r1, r3        ; r1 = r1 + r3
ADD r1, r2, #4    ; r1 = r2 + 4
ADD r1, #15       ; r1 = r1 +15

If the destination operand (operand1) is the same as the first source operand (operand2), the destination operand can be omitted.

The second operand (operand2) can have some variations (such as using Barrel shifter, and memory addressing), and it is often written as Op2 in the instruction description. For example, the add instruction is described as follows:

ADD {Rd,} Rn, Op2    ; Rd = Rn + Op2

The curly brackets "{ }" mean the destination operand Rd is optional if Rn is the same as Rd.

Inline Barrel Shifter

ADD r0, r2, rl, LSL #2     ; r0 = r2 + rl << 2 = r2 + 4 x rl
MOV r0, r2, ASR #2         ; r0 = r2/4 (signed division)
MOV r0, r0, ROR #16         ; Swap the top and bottom halfword

In many instructions, the last operand (operand2 or operand3) can have different formats. We can use the Barrel shifter to shift or rotate the last operation.