Table of Contents
Preamble
I just realized: the title can be shortened to CCCC lol. Well anyway, the whole world is going apeshit about ARM, but who takes care of poor MIPS? Maybe I was looking in the wrong places, but I couldn’t really find a comprehensive guide about this topic, only sporadic Stack Overflow questions, and generic, or even vague answer. I wanted MIPS cross-compilation because I have a Ubiquiti ER-X, and that happens to be running on MIPS, and I happen to be a friggin’ nerd who wants to run custom stuff on it.
To clarify, this guide is about little-endian MIPS32, also known as mipsel
. I’ll use these terms interchangeably from now.
Let’s cut to the chase!
Docker
Install Docker, because this whole thing’s gonna get messy, so you wanna contain that trash in your containers. (You get it? They’re called containers for a reason!) Also, if you screw something up, it’s so much easier to start over. Once done with the Docker setup, create a mips
directory, then start your container:
docker run -it --rm --mount type=bind,source="$(pwd)"/mips,target=/mips ubuntu:18.04
Why Ubuntu 18.04? Because it’s kind of middle of the road, usually not too old and not too new. Of course, there are exceptions to that, then you can just switch to 16.04 or 20.04, that’s the whole beauty of Docker.
As a first step, you want to obtain an initial cache of repo data, and update any outdated packages:
apt -y update && apt -y dist-upgrade
Toolchain
First some crucial tools:
apt -y install wget git sudo unzip mc file
Then the C and C++ compilers for MIPS, and QEMU to make it possible to run MIPS binaries on your x86_64 PC:
apt -y install gcc-mipsel-linux-gnu g++-mipsel-linux-gnu qemu qemu-user
Hello World
Time to compile your first MIPS binary. Create hello.c
:
#include <stdio.h>
int main()
{
printf("Yo dawg!\n");
return 0;
}
Now turn this source code into an executable, or binary with the compiler:
mipsel-linux-gnu-gcc hello.c -o hello
Now try executing it:
$ ./hello
bash: ./hello: No such file or directory
But the file is there, why does Bash say it doesn’t exist? Because Bash is stupid af. The real reason is that because there’s an architecture mismatch. Your machine is (most likely) an x86_64 one:
$ uname -p
x86_64
How about that hello program though?
$ file hello
hello: ELF 32-bit LSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), dynamically linked, interpreter /lib/ld.so.1, for GNU/Linux 3.2.0, BuildID[sha1]=bf7879e1e3c4071b9f350f562a8757a49d3f738f, not stripped
Oops, that’s slightly different, isn’t it? So what can you do? Use QEMU:
$ qemu-mipsel -L /usr/mipsel-linux-gnu hello
Yo dawg!
Awesome. Now you can compile and run MIPS executables. You can try it out, copy this hello
file to your EdgeRouter and see if it runs. It should.
GNU Make
Most established open source software uses GNU Make for compilation. Install it first:
apt -y install make binutils
C
Let’s try compiling zlib, since it has virtually no dependencies.
# start in the home dir
cd
# download zlib
wget https://zlib.net/zlib-1.2.11.tar.gz
# extract it
tar xf zlib-1.2.11.tar.gz
# enter the source tree
cd zlib-1.2.11
# prepare the software for compilation, and point it to your MIPS compiler and linker, use the /usr/local prefix so that it won't mix with the system files
CC=mipsel-linux-gnu-gcc CC_LD=mipsel-linux-gnu-ld ./configure --prefix=/usr/local
# perform the actual compilation, run as many threads as your CPU has
make -j $(nproc)
# install the compiled binaries
make -j $(nproc) install
C++
For this example, we use Jansson, which is another very portable and simple to build library. Notice the new CXX
variable being used, which is used to tell what C++ compiler to use.
$ cd
$ wget https://digip.org/jansson/releases/jansson-2.13.1.tar.gz
$ tar xf jansson-2.13.1.tar.gz
$ cd jansson-2.13.1
$ CC=mipsel-linux-gnu-gcc CXX=mipsel-linux-gnu-g++ CC_LD=mipsel-linux-gnu-ld ./configure --prefix=/usr/local
checking whether we are cross compiling... configure: error: in `/root/jansson-2.13.1':
configure: error: cannot run C compiled programs.
If you meant to cross compile, use `--host'.
See `config.log' for more details
Well then. As you can see, every software is a little different. So let’s do as it says:
CC=mipsel-linux-gnu-gcc CXX=mipsel-linux-gnu-g++ CC_LD=mipsel-linux-gnu-ld ./configure --prefix=/usr/local --host=mipsel-linux-gnu
make -j $(nproc)
make -j $(nproc) install
Easy as pie.
Dependencies
So what if you want to build software that depends on other software? Let’s try out OpenSSL, it’s such an ubiquitous library, and it also happens to optionally depend on zlib, so it’s a match made in heaven.
cd
wget https://www.openssl.org/source/openssl-1.1.1i.tar.gz
tar xf openssl-1.1.1i.tar.gz
cd openssl-1.1.1i
Now, OpenSSL has its own custom Perl Configure
script (notice the different capitalization). There’s a short help printed via ./Configure --help
, but it’s not too talkative, so if you want to dig deeper, read the INSTALL
file. TLDR you need the linux-mips32
flag for mipsel
compilation, and the zlib
flag to tell OpenSSL to build with zlib support:
$ ./Configure --prefix=/usr/local linux-mips32 zlib
$ make -j$(nproc)
/bin/sh: 1: gcc: not found
Makefile:700: recipe for target 'apps/app_rand.o' failed
make[1]: *** [apps/app_rand.o] Error 127
make[1]: Leaving directory '/root/openssl-1.1.1i'
Makefile:172: recipe for target 'all' failed
make: *** [all] Error 2
Oops. We don’t want to use gcc
, that’s the native compiler, we want mipsel-linux-gnu-gcc
. For zlib, we used the CC
and CC_LD
variables to achieve that, but OpenSSL’s Configure has a flag for that, --cross-compile-prefix
:
$ ./Configure linux-mips32 --prefix=/usr/local zlib --cross-compile-prefix=mipsel-linux-gnu-
$ make -j$(nproc)
crypto/comp/c_zlib.c:35:11: fatal error: zlib.h: No such file or directory
# include <zlib.h>
^~~~~~~~
compilation terminated.
Still not quite there. Now it uses the proper compiler, but can’t find the zlib parts we just installed. So what can you do? After consulting the INSTALL
file, here’s the proper Configure line:
$ ./Configure linux-mips32 --prefix=/usr/local zlib --cross-compile-prefix=mipsel-linux-gnu- --with-zlib-include=/usr/local/include --with-zlib-lib=/usr/local/lib
$ make -j$(nproc)
$ make -j$(nproc) install
OpenSSL is cool, because unlike zlib, it has actual executables that you can run.
$ qemu-mipsel -L /usr/mipsel-linux-gnu /usr/local/bin/openssl version
/usr/local/bin/openssl: error while loading shared libraries: libssl.so.1.1: cannot open shared object file: No such file or directory
Except when you can’t. Well yeah, that library is under /usr/local/lib
:
$ qemu-mipsel -L /usr/local /usr/local/bin/openssl version
/lib/ld.so.1: No such file or directory
Ah yeah, the MIPS ld.so is still under /usr/mipsel-linux-gnu
. So how do we resolve this?
$ LD_LIBRARY_PATH=/usr/local/lib qemu-mipsel -L /usr/mipsel-linux-gnu /usr/local/bin/openssl version
OpenSSL 1.1.1i 8 Dec 2020
Now we’re talking!
Meson & Ninja
Think of Meson as a different configure
script and of Ninja as a different make
. Install them:
apt -y install ninja-build meson
Now grab Jose, which is a utility that luckily happens to depend on the libs we just built, Obtain it:
cd
git clone https://github.com/latchset/jose.git
mkdir jose/build
cd jose/build
Now try to do something:
$ meson .. --prefix=/usr/local
The Meson build system
Version: 0.45.1
Source dir: /root/jose
Build dir: /root/jose/build
Build type: native build
Project name: jose
meson.build:1:0: ERROR: Unknown compiler(s): ['cc', 'gcc', 'clang']
The follow exceptions were encountered:
Running "cc --version" gave "[Errno 2] No such file or directory: 'cc': 'cc'"
Running "gcc --version" gave "[Errno 2] No such file or directory: 'gcc': 'gcc'"
Running "clang --version" gave "[Errno 2] No such file or directory: 'clang': 'clang'"
Well then. It says native build, that can’t be good. Apparently you need a so-called “cross file” for a Meson cross-compile. Something like this should do, save it as /usr/local/etc/mipsel.cross
:
[binaries]
c = '/usr/bin/mipsel-linux-gnu-gcc'
cpp = '/usr/bin/mipsel-linux-gnu-g++'
ar = '/usr/bin/mipsel-linux-gnu-ar'
strip = '/usr/bin/mipsel-linux-gnu-strip'
pkgconfig = 'pkg-config'
exe_wrapper = '/usr/local/bin/qemu-mipsel-wrapper'
[properties]
has_function_printf = true
needs_exe_wrapper = true
c_arch = 'mipsel'
cc_arch = 'mipsel'
bits = 32
[build_machine]
system = 'linux'
cpu_family = 'x86_64'
endian = 'little'
cpu = 'x86_64'
[host_machine]
system = 'linux'
cpu_family = 'x86_64'
endian = 'little'
cpu = 'x86_64'
[target_machine]
system = 'linux'
cpu_family = 'mips'
endian = 'little'
cpu = 'mips'
What’s that /usr/local/bin/qemu-mipsel-wrapper
though? That’s our old friend, qemu-mipsel -L /usr/mipsel-linux-gnu
, but it seems Meson is so bleeding-edge that it only accepts executable paths, but not arguments to them. So create this /usr/local/bin/qemu-mipsel-wrapper
file like so:
#!/bin/bash
qemu-mipsel -L /usr/mipsel-linux-gnu "$@"
Also make sure it’s executable:
chmod +x /usr/local/bin/qemu-mipsel-wrapper
After all that trouble, we’re finally back to our Meson command:
$ meson .. --prefix=/usr/local --cross-file /usr/local/etc/mipsel.cross
The Meson build system
Version: 0.45.1
Source dir: /root/jose
Build dir: /root/jose/build
Build type: cross build
Project name: jose
meson.build:1:0: ERROR: Unknown compiler(s): ['cc', 'gcc', 'clang']
The follow exceptions were encountered:
Running "cc --version" gave "[Errno 2] No such file or directory: 'cc': 'cc'"
Running "gcc --version" gave "[Errno 2] No such file or directory: 'gcc': 'gcc'"
Running "clang --version" gave "[Errno 2] No such file or directory: 'clang': 'clang'"
Why the hell is that? We told Meson to use the mipsel tools, why is it looking for native GCC? I tell you why: because it’s dumb af. So lacking better options, install native GCC to calm Meson down:
apt -y install gcc
Try again:
$ meson .. --cross-file /usr/local/etc/mipsel.cross --prefix=/usr/local
The Meson build system
Version: 0.45.1
Source dir: /root/jose
Build dir: /root/jose/build
Build type: cross build
Project name: jose
Native C compiler: cc (gcc 7.5.0 "cc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0")
Cross C compiler: /usr/bin/mipsel-linux-gnu-gcc (gcc 7.5.0)
Host machine cpu family: x86_64
Host machine cpu: x86_64
Target machine cpu family: mips
Target machine cpu: mips
Build machine cpu family: x86_64
Build machine cpu: x86_64
meson.build:29:0: ERROR: Pkg-config not found.
Gee, one step at a time I guess. Install pkg-config:
apt -y install pkg-config
So it’ll work now, right? Right?
$ meson .. --prefix=/usr/local --cross-file /usr/local/etc/mipsel.cross
The Meson build system
Version: 0.45.1
Source dir: /root/jose
Build dir: /root/jose/build
Build type: cross build
Project name: jose
Native C compiler: cc (gcc 7.5.0 "cc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0")
Cross C compiler: /usr/bin/mipsel-linux-gnu-gcc (gcc 7.5.0)
Host machine cpu family: x86_64
Host machine cpu: x86_64
Target machine cpu family: mips
Target machine cpu: mips
Build machine cpu family: x86_64
Build machine cpu: x86_64
Cross dependency zlib found: YES 1.2.11
Dependency threads found: YES
Cross dependency jansson found: YES 2.13.1
Cross dependency libcrypto found: YES 1.1.1i
Configuring jose.h using configuration
Checking if "-Wl,--version-script=..." links: YES
Program ./jose-alg found: YES (/root/jose/tests/./jose-alg)
Program ./jose-fmt found: YES (/root/jose/tests/./jose-fmt)
Program ./jose-b64-enc found: YES (/root/jose/tests/./jose-b64-enc)
Program ./jose-b64-dec found: YES (/root/jose/tests/./jose-b64-dec)
Program ./jose-jwk-eql found: YES (/root/jose/tests/./jose-jwk-eql)
Program ./jose-jwk-exc found: YES (/root/jose/tests/./jose-jwk-exc)
Program ./jose-jwk-gen found: YES (/root/jose/tests/./jose-jwk-gen)
Program ./jose-jwk-pub found: YES (/root/jose/tests/./jose-jwk-pub)
Program ./jose-jwk-use found: YES (/root/jose/tests/./jose-jwk-use)
Program ./jose-jwk-thp found: YES (/root/jose/tests/./jose-jwk-thp)
Program ./jose-jws-fmt found: YES (/root/jose/tests/./jose-jws-fmt)
Program ./jose-jws-ver found: YES (/root/jose/tests/./jose-jws-ver)
Program ./jose-jws-sig found: YES (/root/jose/tests/./jose-jws-sig)
Program ./jose-jwe-fmt found: YES (/root/jose/tests/./jose-jwe-fmt)
Program ./jose-jwe-dec found: YES (/root/jose/tests/./jose-jwe-dec)
Program ./jose-jwe-enc found: YES (/root/jose/tests/./jose-jwe-enc)
Traceback (most recent call last):
File "/usr/share/meson/mesonbuild/mesonmain.py", line 361, in run
app.generate()
File "/usr/share/meson/mesonbuild/mesonmain.py", line 150, in generate
self._generate(env)
File "/usr/share/meson/mesonbuild/mesonmain.py", line 197, in _generate
intr.run()
File "/usr/share/meson/mesonbuild/interpreter.py", line 2991, in run
super().run()
File "/usr/share/meson/mesonbuild/interpreterbase.py", line 173, in run
self.evaluate_codeblock(self.ast, start=1)
File "/usr/share/meson/mesonbuild/interpreterbase.py", line 195, in evaluate_codeblock
raise e
File "/usr/share/meson/mesonbuild/interpreterbase.py", line 189, in evaluate_codeblock
self.evaluate_statement(cur)
File "/usr/share/meson/mesonbuild/interpreterbase.py", line 204, in evaluate_statement
return self.method_call(cur)
File "/usr/share/meson/mesonbuild/interpreterbase.py", line 490, in method_call
return obj.method_call(method_name, self.flatten(args), kwargs)
File "/usr/share/meson/mesonbuild/interpreter.py", line 1154, in method_call
value = fn(state, args, kwargs)
File "/usr/share/meson/mesonbuild/interpreterbase.py", line 79, in wrapped
return f(s, node_or_state, args, kwargs)
File "/usr/share/meson/mesonbuild/modules/pkgconfig.py", line 303, in generate
version, pcfile, conflicts, variables)
File "/usr/share/meson/mesonbuild/modules/pkgconfig.py", line 176, in generate_pkgconfig_file
ofile.write('Name: %s\n' % name)
UnicodeEncodeError: 'ascii' codec can't encode character '\xe9' in position 9: ordinal not in range(128)
That’s just Python for ya. After hours of messing around, it turned out to be a locale problem. The default locale in Docker is POSIX, and Meson just loves to shit its pants on POSIX. So let’s do something about it:
apt -y install locales
locale-gen en_US.UTF-8
export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8
Now it better be working!
meson .. --prefix=/usr/local --cross-file /usr/local/etc/mipsel.cross
ninja -j$(nproc)
ninja -j$(nproc) install
Holy damn, it actually does. But why is libjose.so
under the /usr/local/lib/x86_64-linux-gnu
directory? It’s not an x86_64 lib. Fortunately you can work around that:
meson .. --prefix=/usr/local --cross-file /usr/local/etc/mipsel.cross --libdir=lib
Custom Directories
In these examples we used the /usr/local
prefix, which is a standard Unix path for user-installed software. If you use something different, your build tools will probably not find them out of the box. There are several ways to fix this, one example was the Configure flags we used for OpenSSL to correctly locate zlib.
In the case of Meson, it usually uses pkg-config, which is essentially just a bunch of .pc files under various lib/pkgconfig
folders. You can point Meson to such folders with the PKG_CONFIG_PATH
variable, e.g.:
PKG_CONFIG_PATH=/opt/lib/pkgconfig meson .. --cross-file /usr/local/etc/mipsel.cross
Welp, that was a mouthful. Have fun with your MIPS builds!