AArch64 Playground
4.3 · Anatomy of an assembly file

Anatomy of an assembly file

prerequisite

An assembly program looks dense the first time you open one, but it is made of only a handful of kinds of line. Once you can name each kind, you can read any program in the course from top to bottom. This lesson takes one short program apart, shows what the m4 step does to it, and ends with the trap m4 sets for a careless name.

The four fields of a statement

A line that holds an instruction is called a statement. It has up to four fields, always in this order:

  • A label names the address of the line, so other code can refer to it. It starts at the left margin and ends with a colon.
  • The opcode, also called the mnemonic, names the operation: add, ldr, bl.
  • The operands are what the operation works on: registers, constants or labels, separated by commas. On AArch64 the destination comes first.
  • A comment starts with // and runs to the end of the line. The assembler ignores it.
label:  opcode  operands                    // commenttop:    add     total_r, total_r, 2         // total_r = total_r + 2

Most lines use only some of the fields. A label can sit on a line by itself, and then it names the instruction on the next line. Course programs indent every instruction by 8 spaces and line the operands up in a column, so the opcodes and the operand lists read as two straight tracks.

The 2 above is an immediate: a constant written straight into the instruction. Course programs write it bare. Other ARM material writes #2, which means the same thing.

Directives talk to the assembler

An opcode that starts with a dot, such as .data or .string, is a directive, also called a pseudo-op. It is an instruction to the assembler, not to the CPU, and it produces no machine instruction of its own. These are the ones every course program uses:

DirectiveWhat it tells the assembler
.datawhat follows is data the program can read and change
.textwhat follows is code
.string "..."put these characters in memory, followed by a zero byte
.word 30put this number in memory as a 4-byte word
.balign 4skip ahead to the next address that is a multiple of 4
.global mainmake the name main visible outside this file

.data and .text switch between sections: separate areas of the finished program, one for data and one for code. A file can switch back and forth, and the assembler gathers each section's pieces together.

.global main matters: the C library code that starts your program looks for a function called main, and it can find it only if the name is global.

A line such as size = 8 is also for the assembler. It gives a number a name that later lines can use in its place. The stack lessons use this for frame sizes.

m4 names

Course programs are written in .asm files, and each one passes through a tool called m4 before the assembler sees it. m4 is a macro processor: it copies the file through, replacing every name defined with define(name, text) by its text. It knows nothing about assembly; it only swaps words.

Course programs use it for two things:

  • Register aliases. define(total_r, w19) lets the program write total_r instead of w19, so each register's job is in its name. The _r ending marks the name as a register. fp (the frame pointer) and lr (the link register, which holds the address to return to), the usual names for x29 and x30, are defined the same way, first in every file.
  • Named constants. define(START, 40) gives a number a name. Constants are written in capitals.

m4 reads the file from top to bottom, so a name works only on the lines after its define. That is why the defines sit at the very top.

The two build steps

Building a course program takes two commands, and a third runs it:

m4 total.asm > total.s      # step 1: m4 swaps the names; total.s is plain assemblygcc total.s -o total        # step 2: gcc assembles it and links in the C library./total                     # run the program

The > sends m4's output into the file total.s instead of onto the screen. On an AArch64 Linux machine, such as the ARM servers you log in to for the course, gcc then turns the .s file into machine code and links it: joins it with the C library, which supplies printf and the start-up code that calls main. When you press run on this site, the playground does both steps for you.

One program, line by line

The program below uses every kind of line at least once. Run it: it prints total = 42. Then read it from the top with the notes that follow.

loading editor...

regfile

N clearZ clearC clearV clear

x0–x30 are the integer registers.

X0arg00x0000000000000000
X1arg10x0000000000000000
X2arg20x0000000000000000
X3arg30x0000000000000000
X4arg40x0000000000000000
X5arg50x0000000000000000
X6arg60x0000000000000000
X7arg70x0000000000000000
X8ind0x0000000000000000
X90x0000000000000000
X100x0000000000000000
X110x0000000000000000
X120x0000000000000000
X130x0000000000000000
X140x0000000000000000
X150x0000000000000000
X16ip00x0000000000000000
X17ip10x0000000000000000
X18pr0x0000000000000000
X190x0000000000000000
X200x0000000000000000
X210x0000000000000000
X220x0000000000000000
X230x0000000000000000
X240x0000000000000000
X250x0000000000000000
X260x0000000000000000
X270x0000000000000000
X280x0000000000000000
X29fp0x0000000000000000
X30lr0x0000000000000000
SP0x0000000080000000
PC0x0000000000400000
console

Output prints here as your program runs.

Press step or run under the editor, or feed stdin from the box below.

not assembled

example 1try it: run it, or step one instruction at a timeOpen in playground
  • The first two lines are comments that say what the program does. Every course file starts this way.
  • The define lines create the names fp, lr, total_r and START.
  • .data starts the data section, and fmt_total: labels a format string in it. A format string is the text printf prints, with %d marking where a number goes.
  • .text starts the code. .balign 4 moves the next instruction to an address divisible by 4, where every instruction has to sit, and .global main makes main visible to the C library.
  • main: labels the first instruction of main, where your own code starts once the C library's start-up code calls it.
  • stp and mov fp, sp are the prologue. They save fp and lr, which main must restore before it returns, and mark where its stack space starts. ldp and ret at the bottom are the epilogue: they put those registers back and return. Every function has this shape, and The stack and the frame pointer explains it.
  • mov w0, 0 just before the epilogue sets the value main returns. 0 tells whoever ran the program that it succeeded.
  • In between is the work: two instructions make 42, and three more hand the format string and the number to printf.

note

main uses w19 here without saving it first. Course programs do this in main. Any other function you write must save the registers it uses from x19 to x28 and put them back before it returns, because its caller may be keeping values there.

What m4 hands to the assembler

Here is the body of the same program after step 1. Every name has been replaced by its text. m4 even rewrote the names inside the comments, because it does not know what a comment is; the assembler skips comments, so no harm is done. The define lines themselves turn into empty lines at the top of the file.

main:        stp     x29, x30, [sp, -16]!          // save x29 and x30        mov     x29, sp        mov     w19, 40              // w19 = 40        add     w19, w19, 2         // w19 = 42        mov     w1, w19                 // argument 2: the number for %d        ldr     x0, =fmt_total              // argument 1: the format string        bl      printf        mov     w0, 0                       // main returns 0: success        ldp     x29, x30, [sp], 16            // restore x29 and x30        ret

The trap: a name inside a string

Because m4 swaps every matching word, it also swaps words inside strings. The program below names its register laps and prints a message that contains the word laps. Run it: instead of laps = 3 it prints

w19 = 3

because m4 turned the string into "w19 = %d\n" before the assembler saw it. m4 matches whole words only, so the label fmt_laps is safe; the lone word laps inside the quotes is not.

To fix it, rename the alias to laps_r in the define line and in the two lines that use it, and run again. The string no longer holds a defined name, and the program prints laps = 3.

loading editor...

regfile

N clearZ clearC clearV clear

x0–x30 are the integer registers.

X0arg00x0000000000000000
X1arg10x0000000000000000
X2arg20x0000000000000000
X3arg30x0000000000000000
X4arg40x0000000000000000
X5arg50x0000000000000000
X6arg60x0000000000000000
X7arg70x0000000000000000
X8ind0x0000000000000000
X90x0000000000000000
X100x0000000000000000
X110x0000000000000000
X120x0000000000000000
X130x0000000000000000
X140x0000000000000000
X150x0000000000000000
X16ip00x0000000000000000
X17ip10x0000000000000000
X18pr0x0000000000000000
X190x0000000000000000
X200x0000000000000000
X210x0000000000000000
X220x0000000000000000
X230x0000000000000000
X240x0000000000000000
X250x0000000000000000
X260x0000000000000000
X270x0000000000000000
X280x0000000000000000
X29fp0x0000000000000000
X30lr0x0000000000000000
SP0x0000000080000000
PC0x0000000000400000
console

Output prints here as your program runs.

Press step or run under the editor, or feed stdin from the box below.

not assembled

example 2try it: run it, or step one instruction at a timeOpen in playground

pitfall

The same swap happens in labels and comments. A short, common word such as sum, count or n is likely to turn up somewhere else in the file; sum_r, count_r and n_r are not. The _r ending is there to keep register names out of the way.

Check yourself

  1. In loop: sub n_r, n_r, 1 // one fewer, name the label, the opcode, the operands and the comment.
  2. Does .string produce a machine instruction? What does it produce?
  3. Why must main be declared .global?
  4. Which command runs first when building prog.asm, and what file does it produce?
  5. A program has define(max, w20) and, further down, fmt: .string "max = %d\n". What does it print when w20 holds 9?

answers

show answers
  1. The label is loop, the opcode sub, the operands n_r, n_r, 1, and the comment // one fewer.
  2. No; it puts the string's bytes, and a zero byte after them, into memory.
  3. So the C library's start-up code, which lives in another file, can find main and call it.
  4. m4 prog.asm > prog.s, which produces the plain assembly file prog.s.
  5. w20 = 9.

Practice