How to cross-compile C/C++ code to MIPS assembly


Cross Compile With Clang

Install clang/llvm:

Cross-compile some code

Run this:

$ clang -O3 -S -target mips -mcpu=mips32 some_program_file.c

Note the following command line options:

  1. -O3 This flag indicates the highest level of optimization. You can also try using -Os, which optimizes for size instead of speed.
  2. -S This flag tells the compiler to generate an assembly file instead of machine code binary.
  3. -target mips This flag tells clang to cross-compile to the MIPS architecture instead of the architecture you're running the compiler on. The argument of -target should actually be a "target triple": see Clang Cross Compilation docs for more information. If you have llvm installed, you should be able to list all the parameters to this "target triple" using the CLI command llc -march=mips -mattr=help.
  4. -mcpu=mips32 This flag indicates the specific CPU you are writing for. mips32 gets us very close to the R2000 that MIPSym emulates.

Note that clang does not come with a standard library for MIPS, so if you start #includeing your favorite library headers, you'll get compiler errors unless you install those headers yourself.

If you want to cross-compile code that uses standard libraries, see the following section.


Cross Compile With GCC

The GCC MIPS cross-compilers are readily available in binary form on Linux. If you have loads of patience, you may be interested in compiling a GCC MIPS cross-compiler from source on some other OS; this is a non-trivial task that I cannot help you with.

On my installation of Ubuntu 18.04, binaries of gcc-mips are available in the standard built-in repositories. If you are not on Ubuntu 18.04, you may need to add some new repositories. Run a search for "gcc MIPS" and you should find what you need.

Windows: You can get this to work using Windows Subsystem Linux 2. This will require you to install a Linux distribution (there are several options) on a virtual machine running on your Windows desktop. One of the choices is Ubuntu 18.04; I can confirm that these instructions will work on that distro, but I haven't tried the others.

1.) Install the MIPS GCC cross compiler.

Find GCC cross compiler packages

The gcc cross compiler is named something like gcc-mips-linux-gnu or gcc-8-mips-linux-gnu. You can see what's available in the apt package manager by running apt search:

$ apt search --names-only '^gcc-.*mips-linux-gnu$'

This command uses a regular expression to cut down on the number of results. My machine lists over 500 packages related to gcc mips cross-compilation, and the regular expression cuts this down to the 10 most relevant. In this guide, I will install the default gcc/g++, which corresponds to GCC-7.5.0.

You should be able to use apt search to find MIPS cross compilers on any Linux distribution that uses the apt package manager. Before Ubuntu 18.04 was released, you had to use apt-cache instead of apt to run the search command; keep this in mind in case apt search doesn't work.

On Linux distros that do not use apt, you should be able to query your package manager for packages that include the terms gcc and mips; you will be able to find something useful on most distros.

Install the GCC cross compiler

You found the package you wanted, now install it:

$ sudo apt install gcc-mips-linux-gnu g++-mips-linux-gnu

Please note that after you install the package gcc-mips-linux-gnu, you cannot simply run gcc-mips-linux-gnu from a terminal. This package actually installs a binary called mips-linux-gnu-gcc in your /usr/bin directory. Let's try to run it now, just to make sure it installed correctly:

$ mips-linux-gnu-gcc --version

If this command fails to run, then either the cross-compiler was not installed, or you have the wrong program name.

For future reference, if you installed a package with apt and you can't figure out what you actually installed, you can use this command to find out:

$ dpkg -L gcc-mips-linux-gnu

dpkg is the Debian package manager. apt is a user-friendly front-end for dpkg, but occasionally there are some things that apt cannot do. The dpkg -L command lists everything that a particular package installed, and where it went. In this case, gcc-mips-linux-gnu installed several manuals, tools, and the mips-linux-gnu-gcc compiler.

2.) Cross compile some code.

I used a simple hello world program, provided below:

// hello.c
#include <stdio.h>

int main(void){
    puts("Hello world\n");
    return 0;
}

To compile this program, use this command:

$ mips-linux-gnu-gcc -O3 -S -mfp32 -march=R2000 hello.c

This command will result in an assembly file called 'hello.s'. If you want to compile c++ code, replace the 'gcc' part with 'g++', like this:

$ mips-linux-gnu-g++ -O3 -S -mfp32 -march=R2000 hello.cpp

Please note that the current MIPSym assembler cannot assemble this file directly; gcc outputs several assembler directives that the MIPSym assembler does not understand, and gcc writes all of the registers as numbers ($0-$31), while MIPSym only understands the registers by their descriptive names ($t0-$t9, $s0-$s7, etc). In addition, gcc links the output to gcc's version of stdio, which MIPSym cannot do.

When I ran gcc on the hello.c file, I wound up with a 46-line assembly program. Most of this program is assembler directives that MIPSym doesn't understand and code that sets up the linkage to stdio. Using g++ instead, I get 54 lines. Using iostream instead of stdio, I get 110 lines!

To check the length of your output, try word-count:

$ wc -l hello.s

If you want the MIPSym assembler to compile code produced by gcc, take special care with the delay slots. I have found that some cross compilers don't use the delay slot properly: much of the code that gcc outputs is written with the delay slot in mind, but occasionally I can find a few instances where the delay slot should contain a nop, but instead it contains the instruction that should come after the nop.

3.) Understand the compiler flags you just used.

On linux, you shouldn't ever pass flags into a command-line program without taking some time to understand what they do. To do this, you can check the gcc manpage by typing:

$ man mips-linux-gnu-gcc

You can also check their online documentation. You'll find the MIPS flags here.

For a brief rundown of all the command-line arguments, try the --help flag:

$ mips-linux-gnu-gcc --help

Be aware that gcc has over 2000 flags, so don't try to learn them all. These are the ones that I used:

  1. -O3 This flag specifies the highest level of optimization. This flag is pretty important if you want to generate a readable quantity of assembly. My c++ version of 'hello world', using iostream, generates 110 lines of code with this flag, and 202 without it! You can also try using -Os, which optimizes for size instead of speed.
  2. -S This flag tells gcc to generate an assembly file instead of machine code binary.
  3. -mfp32 The '-m' flags generally indicate a machine-specific directive. This flag tells gcc to use 32-bit floating point registers.
  4. -march=R2000 This flag specifies the R2000 processor. Gcc supports quite a few processors; check the website listed above for more.

4.) Use gcc's assembler to assemble an object file.

To assemble hello.s, use this command:

$ mips-linux-gnu-gcc -c -EB hello.s

The -c flag compiles and assembles input files so that they will become object files, but does not link them into executable files. The -EB flag tells gcc to produce big-endian output.

This command will produce hello.o. You are encouraged to open this file in objdump, or a GUI hex editor like GHex, and see what you find. In the data section, you should be able to find the string "Hello world\n". In the text section, you should be able to locate the machine code that corresponds to the assembly opcodes in hello.s.

You can also try using gcc to assemble your own assembly files written for MIPSym. GCC won't understand any of the MIPSym syscalls or macros, and it may have trouble with some of the pseudocodes, but you should be able to write something that it can assemble.