4. Building systems: the manual way (harder)

When a computer boots, the control is given to the following components in sequence:

  1. Hardware
  2. BIOS (Basic input/output system)
  3. Boot loader (e.g., Syslinux)
  4. Kernel (e.g., Linux)
  5. System (e.g., /init)

You first need to learn how to build your own systems : these are user-space programs written in Haskell and using the haskus-system which are compiled statically.

Then you need to build a Linux kernel and to put your systems into ramdisks.

Finally you can test your systems with QEMU and distribute them on physical devices (USB sticks, CD-ROM) or online (as .iso images).

4.1. Building systems

Suppose we have the following HelloWorld system code and that we want to build it.

import Haskus.System

main :: IO ()
main = runSys' <| do
   term <- defaultTerminal
   writeStrLn term "Hello World!"
   waitForKey term
   powerOff

You can use Cabal as for any other Haskell program. However, we want to build a static executable, hence the .cabal file must contain an executable section such as:

executable HelloWorld
   main-is: HelloWorld.hs
   build-depends:
      base,
      haskus-system
   default-language: Haskell2010
   ghc-options:      -Wall -static -threaded
   cc-options:       -static
   ld-options:       -static -pthread
   #extra-lib-dirs: /path/to/static/libs

If static versions of the libgmp, libffi and glibc libraries (used by GHC’s runtime system) are not available on your system, you have to compile them and to indicate to the linker where to find them: uncomment the last line in the previous extract of the .cabal file (the extra-lib-dirs entry) and modify it so that the given path points to a directory containing the static libraries.

Finally, we recommend using stack to ensure that you are using appropriate versions of GHC and of haskus-system. Example of stack.yaml contents:

resolver: lts-8.15

packages:
- '.'
- location:
    git: git@github.com:haskus/haskus-system
    commit: 33ba0413f2adae33f66f78e51f7e9e52e63758f1
  extra-dep: true

ghc-options:
   "haskus-system": -freduction-depth=0 -fobject-code

extra-deps:
- haskus-binary-0.6.0.0
- haskus-utils-0.6.0.0

Finally use stack build to compile the program.

Examples

The haskus-system-examples repository contains several examples of such systems (including this HelloWorld program).

Refer to its .cabal file and to its stack.yaml file.

4.2. Creating ramdisks

To execute a system with the Linux kernel, the easiest way is to create a ramdisk: an image of the file-system containing your system (basically a zip file).

To do that, put your system files into a directory /path/to/my/system. Then execute:

(cd /path/to/my/system ; find . | cpio -o -H newc | gzip) > myimage.img

You need to have the cpio and gzip programs installed. It builds a ramdisk file named myimage.img in the current directory.

4.3. Building the Linux kernel

The Linux kernel is required to execute systems using haskus-system. Leaving aside modules and firmwares, a compiled Linux kernel is a single binary file that you can use with QEMU to execute your own systems.

To build Linux, you first need to download it from http://kernel.org and to unpack it:

wget https://www.kernel.org/pub/linux/kernel/v4.x/linux-4.9.8.tar.xz
tar xf linux-4.9.8.tar.xz

Then you need to configure it. We recommend at least the following:

cd linux-4.9.8

# default configuration for the X86-64 target
make x86_64_defconfig

# enable some DRM (graphics) drivers
./scripts/config -e CONFIG_DRM_BOCHS
./scripts/config -e CONFIG_DRM_RADEON
./scripts/config -e CONFIG_DRM_NOUVEAU
./scripts/config -e CONFIG_DRM_VIRTIO_GPU

# fixup configuration (use default values)
make olddefconfig

If you know what you are doing, you can configure it further with:

make xconfig

Finally, build the kernel with:

make -j8

Copy the resulting kernel binary that you can use with QEMU for instance:

cp arch/x86/boot/bzImage linux-4.9.8.bin

You can also copy built modules and firmwares with:

make modules_install INSTALL_MOD_PATH=/path/where/to/copy/modules
make firmware_install INSTALL_FW_PATH=/path/where/to/copy/firmwares

4.4. Testing with QEMU

To test a system with QEMU, we recommend that you first build a ramdisk containing it, say myimage.img. We suppose your system (i.e., the user-space program) is stored in /my/system in the ramdisk.

You also need to build a recent Linux kernel, say linux.bin.

To launch QEMU, use the following command line:

qemu-system-x86_64
   -kernel linux.bin
   -initrd myimage.img
   -append "rdinit=/my/system"

We recommend the following options for QEMU:

# make QEMU faster by using KVM
-enable-kvm

# use newer simulated hardware
-machine q35

# make pointer handling better by simulating a tablet
-usbdevice "tablet"

# redirect the guest Linux console on the host terminal
-serial stdio
-append "console=ttyS0"

# enable better sound device
-soundhw "hda"

# make the guest Linux output more quiet
-append "quiet"

4.5. Distributing systems

To distribute your systems, we will create a directory /my/disk containing:

  • your system (in a ramdisk)
  • the Linux kernel
  • the boot-loader files (including its configuration)

A boot-loader is needed as it loads Linux and the ramdisk containing your system. We use Syslinux boot-loader but you can use others such as GRUB. Note that you don’t need a boot-loader when you test your system with QEMU because QEMU acts as a boot-loader itself.

To distribute your systems, you can install the boot-loader on a device (e.g., USB stick) and copy the files in the /my/disk directory on it. Or you can also create a .iso image to burn on a CD-ROM (or to distribute online).

Downloading Syslinux

You first need to download and unpack the Syslinux boot-loader:

wget http://www.kernel.org/pub/linux/utils/boot/syslinux/syslinux-6.03.tar.xz
tar xf syslinux-6.03.tar.xz

Creating the disk directory

You need to execute the following steps to create your disk directory:

Create some directories:

mkdir -p /my/disk/boot/syslinux

Copy Syslinux:

find syslinux-6.03/bios *.c32 -exec cp {} /my/disk/boot/syslinux ;
cp syslinux-6.03/bios/core/isolinux.bin /my/disk/boot/syslinux/

Copy the Linux kernel:

cp linux-4.9.8.bin /my/disk/boot/

Copy the system ramdisk:

cp myimage.img /my/disk/boot/

Finally, we need to configure the boot-loader by creating a file /my/disk/boot/syslinux/syslinux.cfg containing:

DEFAULT main
PROMPT 0
TIMEOUT 50
UI vesamenu.c32

LABEL main
MENU LABEL MyOS
LINUX  /boot/linux-4.9.8.bin
INITRD /boot/myimage.img
APPEND rdinit="/my/system"

Replace /my/system with the path of your system in the myimage.img ramdisk.

Creating a bootable device

To create a bootable device (e.g., bootable USB stick), you have to know its device path (e.g., /dev/XXX) and the partition that will contain the boot files (e.g., /dev/XXX_N).

You can use fdisk and mkfs.ext3 to create an appropriate partition.

You have to install Syslinux MBR:

sudo dd bs=440 if=syslinux-6.03/bios/mbr/mbr.bin of=/dev/XXX

Then you have to copy the contents of the disk directory on the partition and configure it to be bootable:

sudo mount /dev/XXX_N /mnt/SOMEWHERE
sudo cp -rf /my/disk/* /mnt/SOMEWHERE
sudo syslinux-6.03/bios/extlinux/extlinux --install /mnt/SOMEWHERE/boot/syslinux
sudo umount /mnt/SOMEWHERE

Now your device should be bootable with your system!

Creating a bootable CD-ROM

To create a bootable CD-ROM, you first need to create a .iso disk image with the xorriso utility:

xorriso -as mkisofs
   -R -J                            # use Rock-Ridge/Joliet extensions
   -o mydisk.iso                    # output ISO file
   -c boot/syslinux/boot.cat        # create boot catalog
   -b boot/syslinux/isolinux.bin    # bootable binary file
   -no-emul-boot                    # does not use legacy floppy emulation
   -boot-info-table                 # write additional Boot Info Table (required by SysLinux)
   -boot-load-size 4
   -isohybrid-mbr syslinux-6.03/bios/mbr/isohdpfx_c.bin  # hybrid ISO
   /my/disk

It should create a mydisk.iso file that you can burn on a CD or distribute online.