In this article, I am going to introduce how to write MIPS assembly. All the examples introduced will able to be assembled or compiled into actual executable programs. They can be executed on a MIPS machine running Linux. So, let's forget the simulator and come to real world :-P. The first is the most simple program.
1 .data 2 .text 3 .global __start 4 __start: 5 li $4, 88 6 li $2, 4001 7 syscallline 1: is an assembler directive which specifies that data section starts from here. It is empty since we don't have data section in this example.
line 2: specifies that text section starts from here.
line 3: specifies that __start is globally visible to all other modules that are part of the executable, and visible to the GNU linker. The symbol __start is required by the GNU Linker to specify the first instruction to be executed in a program, must always be a global one (same as _start for x86).
line 4: specifies the entry of __start
line 5: loads immediate value 88 to $a0
line 6: loads immediate value 4001 to $v0
line 7: calls the OS to perform system call.
line 5~7: executes the system call according to the number stored in $v0. For MIPS O32 ABI, the number starts from 4000, and exit() will be executed in this example. (You may reference linux kernel source include/asm-mips/unistd.h) The status code is stored in $a0, and we may check it by 'echo $?'.
Assemble the source code with the cross assembler.
$ mipsel-linux-uclibc-as -o exit1.o exit1.s
Link the object into an executable program. $ mipsel-linux-uclibc-ld -o exit1 exit1.o
Then, test the program. $ exit1
$ echo $?
88
$ echo $?
88
OK. We have the skeleton of basic assembly source code. Normally, we don't write something like 'li $2, 4001', that is un-readable. It is easier to leverage definition of header files and the power of compiler. The same example calling exit() could be re-written.
1 #include "asm/regdef.h" 2 #include "asm/unistd.h" 3 4 .data 5 .text 6 .global main 7 main: 8 .set noreorder 9 .cpload t9 # PIC ABI crap (function prologue) 10 .set reorder 11 li a0, 99 12 li v0, __NR_exit 13 syscallline 1: we need register definitions
line 2: we need system call index definitions
line 6~7: is just like main() in a C program. Compiler will do some magic stuff (between __start and main) for our ease leveraging its power.
line 8~10: is the prologue of every function. Our program will compiled into a PIC, and linked with C library. We need the compiler to calculate a correct offset for global pointer register. It is a must when we want to make a call to the dynamic C library. We are going to have the same prologue in the following examples.
Compile the source code with cross compiler.
$ mipsel-linux-uclibc-gcc -o exit2 exit2.S
Test the program. $ exit2
$ echo $?
99
$ echo $?
99
Then, we may try to make some calls in our program. Instruction jal let us call a procedure. It will automatically place the return address in $ra (GPR 31), so that we can easily return back by 'jr ra'. The following example calculates the value of (2^3 + 5^2).
1 # The illustration the use of function. 2 # This program will compute the value of 2^3 + 5^2. 3 4 #include "asm/regdef.h" 5 #include "asm/unistd.h" 6 7 .data 8 .text 9 .global main 10 main: 11 .set noreorder 12 .cpload t9 # PIC ABI crap (function prologue) 13 .set reorder 14 subu sp, 4 # reserve 4bytes from stack 15 li a0, 2 16 li a1, 3 17 jal power # calculate 2^3 18 nop 19 sw v0, 4(sp) # push the value of 2^3 20 nop 21 li a0, 5 22 li a1, 2 23 jal power # calculate 5^2 24 nop 25 lw a0, 4(sp) # pop the value of 2^3 26 nop 27 addu sp, 4 # release 4bytes to stack 28 add a0, v0 # add with 5^2 29 li v0, __NR_exit 30 syscall 31 32 .type power, @function 33 power: 34 add v0, a0, zero # v0 = a0 35 subu a1, 1 36 beqz a1, ans # power is 1 37 nop 38 ploop: 39 mul v0, v0, a0 # v0 * a0 40 subu a1, 1 # decrease the power 41 bnez a1, ploop # next power 42 nop 43 ans: 44 jr ra 45 nopline 14: reserves 4 bytes from stack that we are going to use.
line 15~16: passes argument to the function, power(). The first argument ($a0) is base, and the second argument ($a1) is exponent.
line 17: calls power().
line 18: is branch delay slot for jal
line 19~20: when power() returns, the value of 2^3 will be stored in $v0. We save is to the stack.
line 21~24: calls power() to calculate 5^2. It will be stored in $v0.
line 25~26: pop the stored value of 2^3 to $a0.
line 27: releases memory back to stack that we reserved.
line 28: adds $v0 (containing 5^2) with $a0 (containing 2^3).
line 29~30: calls exit(). The final answer is in $v0.
line 32: marks the symbol power as being a function name.
line 34: $v0 = $a0
line 35~37: if the exponent is equal to 1, the answer (in $v0) is equal to the base.
line 38~42: a loop to repeat calculation of multiplying base for (exponent - 1) times
line 43~45: returns to the original branch with final answer stored in $v0
Compile the source code with cross compiler.
$ mipsel-linux-uclibc-gcc -o power power.S
Check the value of 2^3 + 5^2. $ power
$ echo $?
33
$ echo $?
33
Note that the previous example is a simple usage of function. Is is recommended to follow the O32 ABI calling convention for a better compatibility.
With reference to previous example, you can write a hello-world program with write system call. There is also an example provided in Linux MIPS mailing list. How about a hello-world program calling C library printf? Take a look at the followed example.
1 #include "asm/regdef.h" 2 3 .globl main 4 main: .ent main 5 .frame sp, 32, ra 6 .set noreorder 7 .cpload t9 8 .set reorder 9 10 subu sp, sp, 32 11 sw ra, 28(sp) 12 .cprestore 24 13 la a0, hello 14 jal printf 15 16 lw ra, 28(sp) 17 addiu sp, sp, 32 18 jr ra 19 .end main 20 21 .data 22 hello: .asciz "Hello world\n"line 4: marks the entry of main, the .ent pseudo-op is used by the debugger.
line 5: tells compiler frame register is $sp, frame size is 32 bytes, and return register is $ra.
line 6~8: is the prologue described in previous example.
line 10: reserves the stack memory for 32 bytes. That includes space for local variables, saved registers, and argument area. Please reference the calling convention article for detail.
line 11: pushes the return address. When we call other functions, return address will be changed. Thus, it must be saved first.
line 12: restores the global pointer back. Because we will make a call that clobbers $gp, we have to restore the $gp value before returning. The idea is similar to line 11 that saving $ra.
line 13: the argument is string "Hello world"
line 14: make a call to printf
line 16: restores back the return address
line 17: release the stack that we reserved
line 18: returns from main function
line 19: marks the end of main
line 22: the "Hello world" string is stored in data section
Compile the source code with cross compiler.
$ mipsel-linux-uclibc-gcc -o printf printf.S
Test the hello-world program. $ printf
Hello world
Hello world
Having these examples for reference, I think it will be easier if you'd like to write assembly together with C programs.
References:
- Documentation for binutils
- Documentation for gas
- Andreas Klappenecker, Computer Arcchitecture -- Lecture3, Lecture4
- SPIM, A MIPS32 Simulator
- An Introduction to GNU assembler
- TIGCC Team, GNU Assembler
- Deniel J. Ellard, MIPS Assembly Language Programming
- MIPSpro™ Assembly Language Programmer's Guide
- Linux MIPS wiki
- Jonathan Bartlett, Programming from the Ground Up
6 comments:
32 bytes o 32 bits?
I think you're asking the 4th example, line 5 and line 10. They are both bytes. When we try to access memory, it is usually byte aligned (or even word aligned).
Thanks for tis tutorial. I'm using it on Emtec Gdium laptop with MIPS CPU. To make some examples work I had to change asm/regdef.h into sys/regdef.h.
Hello.
I think that you made a mistake with names of registers $4 and $2 in the first example. These registers have names $a0 and $v0 respectively. You use these right register names in your second example.
I have got a question about the third example. You reserve 4 bytes in a stack for a local variable (but you don't address to it). In next instructions you use adress 4(sp) to this local variable, but it's adress of previous 4 bytes in stack. Am I right?
Yes. 4 bytes space in the stack is reserved to store the value of 2^3. Thanks for correcting my typo on alternate register names.
Post a Comment