Our first example assembly program, Prg1.s, is just about the shortest one that produces any output.
.global _start _start: mov R0, #42 mov R7, #1 SWI 0 # Terminate program with exit code 42
Let's start with the first two lines.
.global _start _start:
The line _start: is a label, marking the assembly instruction after it with that name. The operating system doesn't assume your assembly program starts at the first instruction. It needs a label named _start to mark the starting instruction of the program.
The _start label must be visible from outside of the program. We'll use plenty of labels in future programs, but most will be used only from within the program, for instance to mark places in the code where data is stored, or locations to jump to under given circumstances. Only a few labels, like _start are used from outside the program. These must be marked as "global", or universally visible. That's what the .global _start line does.
The global _start is an assembly directive. Both it and the label add extra information about the program without actually generating any machine language, so technically they are not assembly instructions. The next few lines, however, are.
The rest of the program comprises three assembly instructions. ARM assembly instructions start with an abbreviation, generally 3-6 letters long, describing the operation the instruction performs. Following this are registers, constants, and other arguments needed by the operation. All are heavily abbreviated, as is typical for assembly languages.
The first two instructions are movs. These are like simple assignment statements in C or other languages: they "move" a value into a register. The target register is listed first after the mov, followed by a comma and the value to move into the register. So again, they're like assignment statements in that they copy from right to left.
The value to be copied may be a number, as in our example. (Numeric constants in ARM assembly must be preceded by a #). So, the two mov instructions put a 42 into register 0, and a 1 into register 7. We'll use the register diagram shown here to visualize the 16 registers in the ARM processor.
The second argument of a mov may be other things than a constant. For instance:
mov R1, R2
will copy R2 into R1. But, a mov is not as powerful as a C assignment statement. You could not, for instance, assign the sum of R2 and R3 into R1:
mov R1, R2+R3 (Nope, doesn't work)
That would require an add instruction, which we'll get to in the next example.
And the size of the constant you may assign is limited, so this wouldn't work either:
mov R1, #12345 (Too large a value)
There is a way to put a large constant into a register, but this involves setting the constant up in memory as part of the initial program content, and loading it into a register with a ldr ("load register") instruction, which we'll get to in another few examples.
The exact limit on constant size in a mov is actually rather complex, and is a topic we'll cover in a later module when we closely examine the design of the ARM machine language. For now, you may assume any value up to and including 255 will work, and that some values higher than that will also work.
Why is 255 not too surprising a limit on the size of the constant?
Answer 1
mov instruction are devoted to specifying a constant. (Plus some more bits we'll examine in that later module.)
We'll see lots of different assembly instructions in the upcoming programs, but all of them begin with an opcode (operation code) like mov, followed by comma-separated operands. The operands are often registers, but may sometimes be constants, or more complex things. The opcode and register names are case-insensitive: MOV and mov are synonyms, as are R0 and r0. Arbitrary spaces may be added between opcode and operands, and between two operands, but are not required. But, each assembly instruction must appear on one line. Instructions may not be broken over several lines, nor crammed together on one line.
The SWI instruction performs a software interrupt, in order to make a call to the operating system (a system call) to end the program. This is an advanced instruction for an introductory example, but it's in there for two reasons:
Assembly programs don't end automatically. They must make a system call to terminate. (This is true of C programs, too, but the C compiler supplies the needed system call automatically as part of translation to assembly.)
It allows the program to produce an exit code. Any program running under Unix must provide an integer exit code. In C you do this by returning a value from main, usually return 0, but any value up to 255 is allowed.
Exit codes are often ignored. You may not even have known they existed. But when you run a program from the Unix commandline, you can print its exit code immediately after running it by typing
echo $?
An exit code is the simplest way to get output from an assembly program. Upcoming examples will call C library functions like printf, but this requires rather detailed assembly code to set up the function parameters and make the call, something the C compiler handles for you automatically when you use printf in a C program. We'll wait for a few more examples before doing this, and just stick with exit codes for now.
You don't need to know all the details of system calls at this point. Just remember that setting R0 to a desired exit code value, setting R7 to 1, and running SWI 0 will make a system call to end the program with the exit code in R0.
The SWI instruction also has a comment describing its behavior. Comments begin with a @, either at the start of a line, or after a assembly instruction to provide a right-hand side comment. Comments may not span multiple lines, the way they can in some languages like C. If you want a multiline comment, put an @ at the start of each line of the comment.
To run our program, we first need to translate the assembly code to machine language. In a language like C, the translation process is called compiling, and you use a compiler to do it. But translation of assembly code is termed assembling, and we run an assembler to do it.
The ARM assembler is as, and you run it from the commandline thus:
as Prg1.s -o Prg1.o
The first argument after as is the file containing the assembly code to translate, with .s as the expected suffix. (The suffix .a is already used for other purposes in Unix.). Following this is a -o flag, for "output", and the name of a file into which to put the machine language translation. Now we have a Prg1.o file in the directory as well.
But, the Prg1.o file doesn't yet contain a runnable program. There may be library code that must be included with the translation to make it work. (For instance, when our examples use printf, we'll need to add that function to the machine language translation.)
To include needed libraries and create a runnable executable, we must run Prg1.o through the loader program ld:
ld Prg1.o -o Prg1
This program takes the machine language translation file, adds libraries, and produces an executable program Prg1, which you designate with a -o as for as. (Omitting the -o Prg1 is allowed, and gets you a default name of a.out for the executable.)
Now we have an executable program, which we run by giving the executable file as a command:
./Prg1
No output appears, since we have just an exit code, but running
echo $?
displays the exit code of 42.
In the next segment we'll examine a more complex assembly program.