Retinix
News
About Retinix
Articles
Projects
Links

Making Bootable Linux CDs

Introduction

Why put linux on a bootable CD? Why not! Its a great idea if you need to take a 'toolkit' of programs to a customer site. You can fit quite a lot on a 650mb CD. The ThinkNIC takes this CD based Linux approach in creating a purpose built thin client. DemoLinux is a distribution that only works on a CD. The possibilities are endless.... probably 8-).

Research

Unfortunately, when I first started looking for information on the net, there didn't seem to be too much info about how to make bootable Linux CDs. I have , however, now found a few good sites. Check out the following: The standard for booting CDs is called 'El Torito'. The concept is that your BIOS treats your CD like a floppy initially and expects to find the 'image' of a bootable floppy on the CD. Your imaginary boot floppy is meant to have CDROM drivers on it which can somehow access the CD in full. This seemed overly complicated until I came across SysLinux. Its a boot loader that is part of Debian I think. It primarily is a floppy bootloader (like LOADLIN), but it now has an extra bit called ISOLINUX, that simplifies booting off a CD. The key benefit is that you no longer need a special floppy image. ISOLINUX will load the kernel and an initrd for you and away you go.

The boot process

The usual Linux boot process is something like:
  • The PC starts up and runs LILO or some other bootloader
  • LILO knows where the kernel image is and starts to load it
  • The kernel runs. When its finished doing all its checks, it attempts to mount the root filesystem. The major and minor numbers for this device are usually encoded in the kernel itself or passed to it as arguments from LILO
  • Once the file system is mounted, /sbin/init is executed and your system starts up as per your inittab ... and your /etc/rc*.d scripts
Our CD boot process is somewhat different. Again we need a boot loader, but we don't necessarily know what device our CDROM is. It could be /dev/hdb, /dev/hdc, /dev/hdd. Even if we told the boot loader where the kernel is, we would still need to tell the kernel where its root filesystem is. ISOLINUX helps us get around this, by working out where the CD is. This allows us to boot the kernel, but it doesn't really help us to load the initial root filesystem. Many boot disks use a thing called an initrd (Initial ram disk) to get around this. initrd is an initial root filing system running in RAM. It loads prior to when the kernel attempts to mount the 'real' root file system. Yes this is odd. The idea is is that your initrd starts up, loads some critical modules, then mounts your real root file system.

Now have a look at my CD startup process.

  • The CD is installed with ISOLINUX, so it boots first.
  • ISOLINUX loads the kernel from the /isolinux directory on the CD.
  • ISOLINUX now loads the initrd.gz compressed ext2 file system. Its important to note that ISOLINUX loads this, and not the kernel. The kernel will grab it later.
  • The kernel starts up and eventually decompresses the initrd.gz to ram (in /dev/ram0 actually) and mounts it as root. You have to enable INITRD support in the kernel to have this happen.
  • The kernel tries to execute the /linuxrc file in the new root filesystem.
  • The linuxrc program tries to mount the CDROM (It has to make a few guesses to work out where it is), then creates another ram based ext2 filing system in /dev/ram1. The new root filesystem is retrieved off the CD and effectively 'copied' into the /dev/ram1 device. We mount the new filesystem just so we can add a softlink in its /dev directory for the CD (so w don't have to work out where the CD is again).
  • When the linuxrc script finishes, control returns to the kernel and it attempts to mount its configured root filesystem. In this case, I've rdev'd the kernel to make it use /dev/ram1 as the root filesystem. This will mount our newly created ram based root file system. In this file system I have an /sbin/init and the system starts up running entirely in RAM.
So why does it have to be a two phase process? Linux doesn't really know how to boot off a CD yet. The beauty of the initrd phase is that the initrd filesystem is loaded by the bootloader (ie. not the kernel). It means that we can effectively boot off any device, so long as the bootloader is able to read from it.

Basic Requirements

First, you're going to need a machine to create the CD on. I'm using Slackware 7.1 on a machine with a HP9100i CDwriter. I installed the cdrecord, mkisofs etc tools. I've had to compile into the kernel all the necessary SCSI options to use the IDE CD-Writer (See the CD Writer HOWTO), plus I have loop file support compiled in. I'd suggest you buy yourself a couple of blank CD-RWs to play with as you're going to be formatting, burning, trying it out ... repeatedly.

You'll also need Syslinux. You need Nasm to build it. However, if you're too lazy to do this, all you need is the ISOLINUX.BIN file that is created.

Create a kernel for the CD

Now you need to create a kernel that can load the initrd. I'll assume you know how to build a kernel. All the experimentation here was done with a 2.2.18 kernel. You must compile in initrd support and RAM disk support. I am using the default 4096K RAM disk size. Other things that you'll need are ISO9660 filesystem support, ext2 filesystem support. Once you have this kernel, you need to set the root device on it:
   eg. Say you've just done a make bzImage

    rdev /usr/src/linux/arch/i386/boot/bzImage /dev/ram1

   NB: If you don't have a /dev/ram1 on your system, create one with:

    mknod -m 640 /dev/ram1 b 1 1

Create a directory tree

You're going to have to create a directory structure to build all this. I have a base directory /src/iso and everything sits under it. The key directories are:

cdimage/ This is what mkisofs will use to write an image to the CD
initrd/ Holds the tree of our initrd filesystem
root/ Holds the tree of our real (final) root filesystem

Setting up initrd

The contents of initrd are essentially a mini working linux system. You'll need some libraries, a shell and a few tools, notably 'mke2fs, mount, umount'. The key part is the /linuxrc script. Here's mine:
#!/bin/sh

echo "INITRD startup"

mount -t proc none /proc
# find the CDROM
CD=`fcd`
mount -t iso9660 $CD /cdrom


PATH=$PATH:/sbin:/usr/sbin

if [ -r  /cdrom/rootfs.gz ]; then 
    echo "   expanding real root fs..."
    gunzip -c /cdrom/rootfs.gz | dd of=/dev/ram1

    mkdir /ram
    mount /dev/ram1 /ram

    cd /ram/dev
    ln -s "$CD" cdrom
    cd /
    umount /ram
    umount /cdrom
    umount /proc
    echo "INITRD all done"
    exit
fi
    
# if we arrive here, we drop into the emergency shell. 
exec /bin/sh
We mount /proc so that some system info becomes visible, then mount the CD. fcd is a program I wrote that simply scans all the IDE devices until it sees the first CDROM drive. Now we need to get our real root filesystemi into /dev/ram1. I have it stored in a tarred gzip file on the CD. I originally used mke2fs to create the filesystem. It actually requires quite a lot of extra libraries to make mke2fs work. Being the minimalist that I am, I just created a blank 4mb ext2 filesystem and copied the real root fs tree to it, then gzipped it. All the linuxrc does is to gunzip it into a dd and voila we have our real root filesystem.

The last step is simply to exit. The kernel should now take over, and mount /dev/ram1 as /, and try to run /sbin/init.

Burning a CD with a kernel and initrd

Putting a kernel and initrd on the CD provides the basics of what we're trying to achieve, so you might like to try burning a CD at this stage to see whether it works.

The contents of our CD will look like the following:

 /
  isolinux/
           isolinux.cfg   - the isolinux config file
           isolinux.bin   - the isolinux boot program
           vmlinuz        - our kernel with initrd support
           initrd.gz      - our initial ram disk
The isolinux.cfg file contains:
   label linux
    kernel vmlinuz
    append initrd=initrd.gz
Its similar to a LILO config. The main things are the kernel name and the append string which includes the name of our initrd. The fact that its gzipped doesn't matter. the kernel will automatically decompress it later.

To burn the CD, I have the cd tree shown above under my /src/iso/cdimage directory (.ie I have a /src/iso/cdimage/isolinux directory). I run mkisofs as per the isolinux.doc that comes with Syslinux:

        mkisofs -o /iso.img -b isolinux/isolinux.bin -c isolinux/boot.cat \
                -no-emul-boot -boot-load-size 4 -boot-info-table -l \
                -R -r /src/iso/cdimage
The boot.cat file is created by the mkisofs command. The -l , -R and -r options are essentially for RockRidge extensions which allow us to have softlinks on the CD and mixed case filenames. We should end up with a couple of meg iso.img file. You can now burn this to the CD using cdrecord (I'll leave this up to you as your speed and dev settings are undoubtedly different to me).

Reboot your system, enter the BIOS setup screens to check that your system will boot off a CD and load the CD and see if it boots. Hopefully, you should see 'Loading vmlinuz', then 'Loading initrd.gz', then the kernel should do its stuff. You should end up at a shell prompt. You won't be able to do much though. Because there was no real ramdisk.tgz file to load, you're stuck inside the initrd.

The 'Real' root filesystem

You can't do too much with an initrd only system. You really need to create a useable root filesystem. Again, I'm using a 4mb RAM disk, so its a bit of a squeeze (why didn't I go for a bigger RAM disk? I just hate wasting space, and I wanted this to be useable on machines with 16mb of ram or more). I chose Busybox to provide most of my /bin tools. In 0.51 of Busybox, you even get things like vi and wget. Its getting fatter at 250Kb or so these days with every tool compiled in (NB: You can reduce its code size by editing Config.h and commenting out the defines for the tools you don't want), but 250K is fine for our 4mb filesystem. I've put some libraries in too:
   ld-2.1.3.so
   ld-linux.so.2 -> ld-2.1.3.so
   libbz2.so.1.0 -> libbz2.so.1.0.0
   libbz2.so.1.0.0
   libc-2.1.3.so
   libc.so.6 -> libc-2.1.3.so
   libcom_err.so.2 -> libcom_err.so.2.0
   libcom_err.so.2.0
   libdl-2.1.3.so
   libdl.so.2 -> libdl-2.1.3.so
   libe2p.so.2 -> libe2p.so.2.3
   libe2p.so.2.3
   libext2fs.so.2 -> libext2fs.so.2.4
   libext2fs.so.2.4
   libm-2.1.3.so
   libm.so.6 -> libm-2.1.3.so
   libncurses.so.5 -> libncurses.so.5.0
   libncurses.so.5.0
   libtermcap.so.2 -> libtermcap.so.2.0.8
   libtermcap.so.2.0.8
   libuuid.so.1 -> libuuid.so.1.2
   libuuid.so.1.2
Not all of these are really required to get the system up and running. I think some are left over from when I had mke2fs. /bin is just busybox and a sh*#load of soft links. I have an /sbin/init (not linked to busybox, its a real sysvinit). In /etc I have an inittab, termcap and an rc.d directory. Currently I just have an rc.S startup script that is run during the sysinit phase of the init startup. The key parts of my inittab are:
   id:1:initdefault:

   si::sysinit:/etc/rc.d/rc.S
   l0:0:wait:/bin/sh
   l1:1:wait:/bin/sh
/etc/fstab looks like:
   /dev/ram1       /        ext2        defaults   1   1
   none             /dev/pts  devpts     gid=5,mode=620  0   0
   none             /proc    proc        defaults   0   0
I also have an empty /initrd directory. This means that once that the initrd phase has finished, and the real root filesystem on /dev/ram1 is mounted, the initrd filesystem (on /dev/ram0) is moved under /initrd. This means we can unmount it and free the ramdisk space.

My /etc/rc.d/rc.S script looks like:

#!/bin/sh

PATH=/bin;export PATH
echo "System init"
mount -t proc none /proc
mount -o remount,rw /
echo "Find the extras on the CD and mount it"
if [ -r /dev/cdrom ] ; then
   mount -t iso9660 /dev/cdrom /cdrom
   if [ -r /cdrom/part1.ext2 ] ;then
      echo "Mounting loopback device"
      mount -o loop,ro /cdrom/part1.ext2 /a
   else
      echo "Mounting HD partition instead"
      mount /dev/hda9 /a
   fi
fi
cd /
umount /initrd
freeramdisk /dev/ram0
First we mount /proc, and then remount / (remember we have a valid entry for / in /etc/fstab now) as read/write. Now we check if the /dev/cdrom device is there (it should have been created in the initrd phase), then mount the cd. The next bits are part of my 'in-progress' work on bootable CDs. In order to gain access to more libraries and binaries on the CD, we try and mount a loop filesystem off the CD in /part1.ext2. We mount it on /a (I just wanted to save typing). However, since I'm still debugging this, I just get it to mount an existing partition on my hard disk on /a. /a contains a bin directory and other directories for starting up X windows. Potentially, it could contain everything else one would usually find in a linux distribution.

Currently (23 April 01), I haven't finalised how things will be structured in the root filesystem. I don't really want to add more to it, as I need some space for writing logs and other files. The default behaviour is to look for libs in /lib and /usr/lib. The latter directory doesn't exist, but I have the ability to soft-link it to a directory under /a.

An example ISO image to try out

I've uploaded a very basic ISO that you can burn to a CD and have a play with. It doesn't do too much as yet, but it has the full set of busybox 0.51 commands available and I've added some scripts to simplify the setting up of your lan card and IP address details.

First, download the iso.img.gz file. Decompress it with gunzip and burn it to a CD (I'd suggest a CDRW) using cdrecord or if you want to do it from Windows, you probably just need to rename it to blah.iso and Easy CD Creator or whathaveyou should be able to burn it.

The CD contains my own initrd.gz and rootfs.gz. You can have a look at these by decompressing them and mounting them as ext2 filesystems on a loopback mount.

Reboot with the CD in your CD drive and you should get some ISOLINUX message and away it goes. I've enabled framebuffer support, so if you have an old PC you may not see past the initrd.gz loading bit. If your video card is VESA 2 compliant you should get a penguin up the top of the screen followed by the usual blurb of console messages.

You'll get an sulogin prompt (please enter the root password or press ctrl-D), so enter the root password (a single space) and you should be at a bash prompt.

To configure your LAN card enter cfgcard and follow the prompts. The list of cards that is shown is simply the modules directory on the CD. Cards like 3c509 are obvious, but others are not. I have an rtl8139 based card which is the most common el-cheapo 10/100Mbps card you can buy.

Now do a cfglan which allows you to set the IP address, netmask and default gateway etc.

Everything else is up to you. You can get DNS working if you just create an /etc/resolv.conf. Busybox includes nslookup and telnet, so have fun.

Note that to keep the size of the ISO quite small, I have not included any X related stuff.... for now.

A smaller initrd

If you look at the iso.img.gz image I uploaded to play with you'll notice that the initrd.gz file is quite large. Around 600kb. Being the minimalist I am, I thought this was an awful waste considering the limited job that the initrd does. Most of the space is taken up with the enormous libc library. I could have looked at smaller libc libraries or tried statically linking a C program, but having spent my early programming days dabbling in assembly language, I thought it a good opportunity to write the equivalent of my linuxrc file in 386 assembly.

With a little help from the framework behind the great asmutils (see www.linuxassembly.org), I hacked together a little program that did everything except gunzip the rootfs.gz file. Instead, I just dd'd an uncompressed copy directly from the disk. Of course, I was a bit perturbed by the length of time it was taking to load a 4mb file from CDROM, so I looked into writing my own gunzip in assembly (well I actually searched to see if anyone else had achieved this feat ... sadly no). Figuring that gunzip was too difficult to convert from C into assembly, I looked at other compression sources and eventually came across a thing called the 624. It was designed for the 4k intro 'scene'. I think the basic goal of it was to crunch a 6k assembly program down to about 4k and attach a very small decruncher to it. The author had the compressor part written in C and the decruncher part written in 386 asm. Cool. I just mangled the code so that it wasn't compressing such small files and it wasn't decompressing as part of an executable.

My complete linuxrc (that doesn't require any external libraries) is now about 1kbyte in length. Cool. Here's the source for linuxrc.asm. The source for the compressor is pcomp.c. You'll need to compile it with something like gcc -o pcomp pcomp.c. The new linuxrc first looks for an uncompressed 4mb filesystem file called 'rootfs' in the root directory of the CD. If that doesn't exist it looks for a file called 'rootfs.lz' in the same directory. This must be a file compressed with pcomp (To run pcomp, just do something like 'pcomp rootfs' and it should create a 'rootfs.lz' file).

Finally, the new improved 'basic' iso image to play with is iso.small.gz. Download it, gunzip it, and burn it to a CD and give it a go. The root password is a 'space', and the cfgcard and cfglan stuff should work.

Framework for extensibility

OR ... how to add your own extra bits. These so called 'basic images' that I have to download here all have the /usr directory in the ram based root filesystem as a softlink to /cdrom/usr. Which means that you can add your own tools and utilities under /usr on the CD. ie. Create a /usr/bin on the CD and add in all the tools that Busybox doesn't include. Add some libraries into /usr/lib ... and what I've been working on is the basic requirements to get X up and running. One thing to remember is that you really do have to burn the CD with RockRidge extensions on in order for softlinks and mixed case filenames to be created properly on the CD.

To build it all yourself, you'll have to do something like this from a real linux system:

   losetup /dev/loop3 /iso.small
   mount -t iso9660 /dev/loop3 /mnt
   mkdir /cdimage
   cd /mnt
   tar cf - . | (cd /cdimage ; tar xf - )
   # Now make sure you have a /cdimage/usr directory and add your own
   # tools in there. When you're finished, just use mkisofs on the /cdimage
   # directory to build a new image to burn to the CD.

Framebuffer

One of my experimental aims is to have an X windows environment on a boot CD. To achieve the widest possible compatibility, I've chosen to enable the Framebuffer console mode and to use the XF86_FBDev X server (its just the one from Slackware 7.1 at the moment). Note: Even though I am aiming for wide compatibility just so I can run X, Framebuffer mode doesn't work with pre VESA 2.0 video cards which means you may not want to add in Framebuffer if all you really need is a console prompt. To activate Framebuffer console mode you need to make sure some things are compiled into the kernel, typically this includes:
   [*] VGA text console
   [*] Video mode selection support
   [*] Support for frame buffer devices (EXPERIMENTAL)
   [*] VESA VGA graphics console
   [*] Advanced low level driver options
   <*> 8 bpp packed pixels support
   <*> 16 bpp packed pixels support
   <*> 24 bpp packed pixels support
   <*> 32 bpp packed pixels support
   <*> VGA characters/attributes support
   [*] Select compiled-in fonts
   [*]   VGA 8x8 font
   [*]   VGA 8x16 font
The other thing that I didn't realise until later is you have to make sure you set a graphical mode for the console when it boots in order to use the X server in default mode. This means putting a specific vga= setting appended to the kernel at boot time. Specifically, you need to change the /isolinux/isolinux.cfg file on the CD so it looks something like:
label linux
 kernel vmlinuz
 append initrd=initrd.gz vga=791
The '791' means to start up in 1024x768x16bit colour mode. Hard coding the display resolution is fine if you know for certain that your video card/monitor can handle it, but what I've done is to let the user choose a display option at boot time. My isolinux.cfg looks like this:
timeout 30
prompt 1
display menu.txt
default 1
label 1
 kernel vmlinuz
 append initrd=initrd.gz

label 2
 kernel vmlinuz
 append initrd=initrd.gz vga=788

label 2
 kernel vmlinuz
 append initrd=initrd.gz vga=791
menu.txt is a simple text file that looks like:
   1) Text Mode
   2) 800x600 x 16bit colour
   3) 1024x768 x 16bit colour
The user just enters '1' if they want text mode, 2 for 800x600 and so on.

Notes

  • The isolinux.cfg file seems rather dodgy if I put something like:
      label linux
         kernel vmlinuz
         append initrd=initrd.gz root=/dev/ram
    
    but seems to work OK if I take out the root=/dev/ram and simply use rdev to set the root fs in the kernel to /dev/ram1.
  • You can't make /linuxrc start init properly. It just keeps spitting out a 'Usage' string. I think its because init is unable to start as pid 1.
  • mkisofs will only write 8.3 style uppercase only filenames by default. If you want filenames up to 31 chars long, then specify the -l option. If you want mixed case filenames you need to enable the Rock Ridge extensions with the -R option, and may want to use the -r option as well which assigns sensible user/group ownership to all files. You will definately need the -R option if you want to copy whole unix directory trees to the CD.

May 04,2001

1