Getting GNU/Linux on Android phones
WARNING: This guide is not yet finished, as I ran into some problems with my S6500 (working with a phone which has a broken screen can be painful). I aim to finish it one day, but I’ve published it as-is for now in hope that it will be useful to someone.
What follows is mostly just a collection of notes on the process of getting GNU/Linux (typically Debian) running as the only OS on most Android phones and also an account of getting Debian on a Samsung Galaxy mini 2 (S6500). I assume that it’s possible to overwrite the Android partitions on a phone.
1. Getting original firmware files
This isn’t needed, but it’s nice to be able to start from a working example, if only to see what format the firmware files have. On some phones it also might not be possible to flash only one partition, so it’s semi-important (very model-dependent) to have all the non-modified partitions handy.
2. Modifying the partition table
You can do this if you want to maximise the storage space for the GNU/Linux root, but it’s entirely optional and heavily depends on the way the device and its bootloader are set up. For example, on Samsung phones the partition layout is dictated by a so-called PIT file, which has a custom binary format. It can be modified by tools like “PIT magic”. There are also ways to change partition layout by kernel parameters. This is, for example, used by the more recent Cyanogenmod port for the Motorola Defy by Quarx. I don’t know how the partition layout is stored on other devices, but it’s sometimes GPT.
If you don’t want to deal with changing the partition layout or can’t create a continuous block for the root partiton, you could use LVM to join existing partitions instead.
3. Modifying firmware files
Most firmware packages (for Android devices, anyway) are just an archive containing images of all the flashed partitions, so it’s easy to modify them. You might not even need to get the firmware if you already know the format your flashing tool expects. The only usual exception is the boot image, which is also the only file you’ll need to modify. It uses a custom Android format and contains the kernel, initramfs ramdisk and kernel parameters. There are tools to deal with this format in the Android source tree, but as that’s a very big bundle of repos, I prefer third-party tools, for example abootimg.
The initramfs is a compressed (mostly gzip) cpio newc archive. (The newc part is very important. It’s a cpio variant and the only one the kernel supports. This early in the process debugging capabilities are very limited, so it can take a very long time to figure out why the custom boot.img isn’t booting. Just add -H newc
to the cpio command with which you create the new initramfs and everything should work)
The initramfs is sometimes also called “initrd”. This isn’t exactly correct, as the initrd is an older way of accomplishing what today’s initramfs was. The main difference is that it’s a filesystem image instead of a cpio archive. They’re interchangable in some locations.
As you want the ownership information on the files preserved, it’s needed to be root when working with them. There are also some tricks you could do (like setting the --owner
flag of cpio to root:root if you don’t want any more complicated permissions), but none are as easy as just working as root.
Sample command to decompress initramfs:
mkdir initramfs
cd initramfs
gunzip -c ../initramfs.img | sudo cpio -i
Sample command to compress initramfs:
cd initramfs
find | cpio -o -H newc | gzip - > ../initramfs.img
You’ll also have to decide how you want to get your final filesystem on the device. You can pick one of the partitions and put it there, but that doesn’t work when you want to use LVM. The best way then is, in my opinion, to create a basic init with networking or mass storage capabilities and then transfer the filesystem over USB.
4. Custom init
While the Android init is nice and can be used in a GNU/Linux system, it’s probably better to create a minimal custom init (a shell script is easy to create and is good for this kind of task) and give control to the official distribution init when booted (or not, if you don’t mind managing services yourself).
The init should be located at /init and should do these things:
- Set the PATH
- Mount all necessary filesystems (/dev, /dev/pts, /proc, /sys, the new root)
- Continue execution
If you’re writing the init as a shell script, you’ll need a shell and some basic utilities. This is a great usecase for busybox. You could compile a binary by hand, but that’d mean getting a cross-compilation toolchain working and actually configuring and compiling busybox, so I think it’s much better to just download a prebuilt binary from the busybox homepage.
Keep in mind that the busybox shell has almost no builtins and things like echo are separate commands. So either prefix them with the busybox executable or make symlinks to busybox.
Command to create symlinks to busybox (assuming ARM busybox on a non-ARM host):
for applet in $(qemu-arm busybox | sed -ne '/defined/,/^$/ {/defined/d; s/^\t//g; s/,//g; p}' | tr '\n' ' ')
do
ln -s busybox $applet
done
5. Debugging early init
Although an early init script doesn’t do very much and won’t be very long in the end, it’s hard to write in one shot without any debugging ability. It’s also possible to have to debug a debug feature, so it’s important to choose the right debug method for a development phase. There are basically two ways to debug init:
1. Writing to a file on a persistent filesystem
This method works well mostly to confirm that the init code is even running and to print out the state of the system at certain point in the init script, but the lack of interactivity makes it painful to use for more complicated tasks. You can only do this if you can access files on a persistent filesystem on the device, but that’s almost always the case (for example, by ADB from a custom recovery).
2. Creating a network over USB and using telnet/ssh
While this method is much more complex to set up than the previous, it has several advantages. The biggest one is interactivity, which very much helps when trying to do something in a foreign environment. Another one is the ability to transfer files (by using nc or ssh). Just make sure to mount /dev/pts, as most telnet (and ssh) daemons allocate a pty for every new connection.
You’ll need to somehow get network communication over USB. One way is to recompile the kernel with only the Ethernet adapter USB gadget enabled. However, you might also enable the gadget with a stock Android kernel by using files in /sys.
You’ll also need to configure the network on both sides. You can either setup a DHCP server on one of the sides (it’s probably easier to do it on the computer side, but there are small DHCP servers that could reasonably fit in initramfs and having one there has the advantage of easier setup on the computer - there’d be no need to turn on some connection sharing mode) or use a static configuration. I’ve made a quick script to simplify this from the computer side:
#!/usr/bin/env bash
INET_IFACE=wlp2s0
if ip a | grep "usb0" > /dev/null
then
USB_IFACE="usb0"
else
USB_IFACE="$(ip a | grep -Eo "enp.s..u.u.(i.)?")"
fi
ip addr add 10.0.10.1/24 dev $USB_IFACE
ip link set up dev $USB_IFACE
iptables -t nat -I POSTROUTING -o $INET_IFACE -s 10.0.10.1/24 -j MASQUERADE
iptables -I FORWARD -i $USB_IFACE -s 10.0.10.1/24 -j ACCEPT
iptables -I FORWARD -i $INET_IFACE -s 10.0.10.1/24 -j ACCEPT
echo 1 > /proc/sys/net/ipv4/ip_forward
echo 1 > /proc/sys/net/ipv4/conf/wlp2s0/forwarding