Anatomy of an assembly file
prerequisite
Read this first:
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 + 2Most 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:
| Directive | What it tells the assembler |
|---|---|
.data | what follows is data the program can read and change |
.text | what follows is code |
.string "..." | put these characters in memory, followed by a zero byte |
.word 30 | put this number in memory as a 4-byte word |
.balign 4 | skip ahead to the next address that is a multiple of 4 |
.global main | make 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 writetotal_rinstead ofw19, so each register's job is in its name. The_rending marks the name as a register.fp(the frame pointer) andlr(the link register, which holds the address to return to), the usual names forx29andx30, 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 programThe > 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.
- The first two lines are comments that say what the program does. Every course file starts this way.
- The
definelines create the namesfp,lr,total_randSTART. .datastarts the data section, andfmt_total:labels a format string in it. A format string is the textprintfprints, with%dmarking where a number goes..textstarts the code..balign 4moves the next instruction to an address divisible by 4, where every instruction has to sit, and.global mainmakesmainvisible to the C library.main:labels the first instruction ofmain, where your own code starts once the C library's start-up code calls it.stpandmov fp, spare the prologue. They savefpandlr, whichmainmust restore before it returns, and mark where its stack space starts.ldpandretat 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, 0just before the epilogue sets the valuemainreturns. 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 retThe 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.
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
- In
loop: sub n_r, n_r, 1 // one fewer, name the label, the opcode, the operands and the comment. - Does
.stringproduce a machine instruction? What does it produce? - Why must
mainbe declared.global? - Which command runs first when building
prog.asm, and what file does it produce? - A program has
define(max, w20)and, further down,fmt: .string "max = %d\n". What does it print whenw20holds 9?
answers
show answers
- The label is
loop, the opcodesub, the operandsn_r, n_r, 1, and the comment// one fewer. - No; it puts the string's bytes, and a zero byte after them, into memory.
- So the C library's start-up code, which lives in another file, can find
mainand call it. m4 prog.asm > prog.s, which produces the plain assembly fileprog.s.w20 = 9.
Practice
- The macro that ate my message: m4 replaces a
definename even inside a string. Fix the program without touching the text between the quotes. - Basic quiz: ARMv8 assembly and Fill in the blank: ARMv8 assembly (core). Both also reach into the next few lessons, so come back to them after Registers and immediates.