Build a Virtual Machine Guest

This is a step-by-step guide to building a bootable virtual machine image using the grub boot-loader. It is primarily aimed at quick testing of custom kernels when it isn't necessary to test the kernel on physical hardware. The basic steps can be amended to suit.

# work as root
sudo su

# create a 2GB sparse disk image
dd if=/dev/zero of=linux-2.6.25.img bs=1M count=0 seek=2048

# attach the image to a device
losetup /dev/loop0 linux-2.6.25.img 

# configure the disk with one partition
fdisk /dev/loop0 <<END
> n
> p
> 1
> t
> 83
> p
> w
Device contains neither a valid DOS partition table, nor Sun, SGI or OSF disklabel
Building a new DOS disklabel with disk identifier 0x567b812e.
Changes will remain in memory only, until you decide to write them.
After that, of course, the previous content won't be recoverable.

Warning: invalid flag 0x0000 of partition table 4 will be corrected by w(rite)

Command (m for help): Command action
   e   extended
   p   primary partition (1-4)
Partition number (1-4): First cylinder (1-261, default 1): Using default value 1
Last cylinder or +size or +sizeM or +sizeK (1-261, default 261): Using default value 261

Command (m for help): Selected partition 1
Hex code (type L to list codes): 
Command (m for help): 
Disk /dev/loop0: 2147 MB, 2147483648 bytes
255 heads, 63 sectors/track, 261 cylinders
Units = cylinders of 16065 * 512 = 8225280 bytes
Disk identifier: 0x567b812e

      Device Boot      Start         End      Blocks   Id  System
/dev/loop0p1               1         261     2096451   83  Linux

Command (m for help): The partition table has been altered!

Calling ioctl() to re-read partition table.

WARNING: Re-reading the partition table failed with error 22: Invalid argument.
The kernel still uses the old table.
The new table will be used at the next reboot.
Syncing disks.

# refresh the view of the disk by disconnecting and reconnecting it
losetup -d /dev/loop0 && losetup /dev/loop0 linux-2.6.25.img

# find out which sector the partition begins at and how large it is
 fdisk -ul /dev/loop0

Disk /dev/loop0: 2147 MB, 2147483648 bytes
255 heads, 63 sectors/track, 261 cylinders, total 4194304 sectors
Units = sectors of 1 * 512 = 512 bytes
Disk identifier: 0x567b812e

      Device Boot      Start         End      Blocks   Id  System
/dev/loop0p1              63     4192964     2096451   83  Linux

# Start is sector 63 so, in bytes, the partition starts at: 512 bytes-per-sector x 63 sectors = 32256 bytes

# create a device for the partition starting at offset 32,256 bytes, so it can be formatted
losetup -o 32256 /dev/loop1 linux-2.6.25.img 

# fdisk will have reserved some free space at the end of the 'drive' but mkfs.ext won't
# be aware of it. Therefore, specifically set the file-system size to match the partition size.
# In the fdisk report above it shows the number of 1024-byte blocks used by the partition: 2096451
# That is used, with some added calculation, to correct the file-system:

mkfs.ext3 -b 4096 /dev/loop1 $(fdisk -lu /dev/loop0 | awk '$1 ~ /^\/dev\/loop0p1.*/ {print ($4 * 1024/4096)-1;}')

e2fsck -f /dev/loop1

# At this point the file-system should be consistent with the partition table

# somewhere to mount the new file system
mkdir linux
mount /dev/loop1 linux

# install a  basic system, enough to get started
sudo debootstrap --variant=buildd --components="main,universe" --include="language-pack-en-base" \
 hardy linux/

# configure some essential system settings
echo "LANG=en_GB" >> linux/etc/environment
rm linux/etc/localtime
ln -s /usr/share/zoneinfo/Europe/London linux/etc/localtime
ls -l linux/etc/localtime
echo -e "auto lo eth0\niface lo inet loopback\niface eth0 inet dhcp\n" > linux/etc/network/interfaces

# name server should match the local address dnsmasq is listening on, that is attached to the host interface kvm0
# kvm0 is the dedicated interface for hosting all virtual machines and is set up separately
echo -e "nameserver\n" > linux/etc/resolv.conf
echo -e "test-vm\n" > linux/etc/hostname

# prepare to use it as a chroot
mount -o bind /proc linux/proc
mount -o bind /dev linux/dev

# now install a base distro
chroot linux /bin/bash -c "apt-get -y --force-yes install ubuntu-minimal"

# configure the file system table
export UUID=$(vol_id --uuid /dev/loop1);
echo "proc /proc proc defaults 0 0" > linux/etc/fstab
echo "UUID=${UUID} / ext3 defaults,errors=remount-ro 0 1" >> linux/etc/fstab

# copy the built kernel image and supporting files into place
# these could come from the kernel build directory
cp /home/all/SourceCode/linux/builds/linux-2.6/arch/x86/boot/bzImage linux/boot/vmlinuz-2.6.25
cp /home/all/SourceCode/linux/builds/linux-2.6/ /boot/
cp /home/all/SourceCode/linux/builds/linux-2.6/.config /boot/config-2.6.25

# install the kernel modules
# note -C causes make to change to the kernel source directory, and the O= causes the kernel Makefile
# to use an out-of-tree build directory as the source of the binary modules
sudo make -C /home/all/SourceCode/linux/linux-2.6 O=../builds/linux-2.6 \
 INSTALL_MOD_PATH=/home/all/VirtualMachines/linux modules_install

# update (create) the initial ram disk image
chroot linux /bin/bash -c "update-initramfs -c -k 2.6.25"
# you should see: update-initramfs: Generating /boot/initrd.img-2.6.25

# it needs to be bootable so install grub
chroot linux /bin/bash -c "apt-get install grub"
chroot linux /bin/bash -c "mkdir -p /boot/grub"

# copy grub boot loader files into the correct location
chroot linux /bin/bash -c "cp linux/usr/lib/grub/x86_64-pc/{,e2fs_}stage* linux/boot/grub/"

# now we have to use a trick to install grub stage1 to the master boot record on /dev/loop0
# first, create a grub bootable image
cat linux/boot/grub/stage1 linux/boot/grub/stage2 >grub-boot.img

# now boot it in a virtual machine and use it to install grub to the disk image
kvm -boot a -fda grub-boot.img -hda /dev/loop0

# In the Virtual Machine, at the grub prompt do:
 grub> root (hd0,0)
 grub> find /boot/grub/stage2
 grub> setup (hd0)
  Checking if "/boot/grub/stage1" exists... yes
  Checking if "/boot/grub/stage2" exists... yes
  Checking if "/boot/grub/e2fs_stage1_5" exists... yes
  Running "embed /boot/grub/e2fs_stage1_5 (hd0)"... 16 sectors are embedded.
  Running "install /boot/grub/stage1 (hd0) (hd0)1+16 p (hd0,0)/boot/grub/stage2 /boot/grub/menu.lst"... succeeded

 grub> halt

# The VM should have halted and quit at that point

chroot linux /bin/bash -c "update-grub"
Searching for GRUB installation directory ... found: /boot/grub
Cannot determine root device.  Assuming /dev/hda1
This error is probably caused by an invalid /etc/fstab
Searching for default file ... Generating /boot/grub/default file and setting the default boot entry to 0
Searching for GRUB installation directory ... found: /boot/grub
Testing for an existing GRUB menu.lst file ... 

Could not find /boot/grub/menu.lst file. Would you like /boot/grub/menu.lst generated for you? (y/N) y
Searching for splash image ... none found, skipping ...
Found kernel: /boot/vmlinuz-2.6.25
Updating /boot/grub/menu.lst ... done

# mostly done - detach ready for use
chroot linux /bin/bash -c "/etc/init.d/sysklogd stop"
chroot linux /bin/bash -c "/etc/init.d/klogd stop"
umount linux/dev
umount linux/proc
umount linux

# ensure the loop devices are disconnected
losetup -sa
/dev/loop0: [0808]:12140623 (linux-2.6.25.img)
/dev/loop1: [0808]:12140623 (linux-2.6.25.img), offset 32256
losetup -d /dev/loop1
losetup -d /dev/loop0
losetup -sa

# finally, time to use it in the Virtual Machine!
kvm -boot c -hda linux-2.6.25.img -m 512 -k en-gb

From now on only the revised kernel image needs to be updated:

losetup -o 32256 /dev/loop1 linux-2.6.25.img
mount /dev/loop1 linux
cp /home/all/SourceCode/linux/builds/linux-2.6/arch/x86/boot/bzImage linux/boot/vmlinuz-2.6.25
cp /home/all/SourceCode/linux/builds/linux-2.6/ /boot/
cp /home/all/SourceCode/linux/builds/linux-2.6/.config /boot/config-2.6.25

# might not need to reinstall the modules if only the kernel image was updated
sudo make -C /home/all/SourceCode/linux/linux-2.6 O=../builds/linux-2.6 \
 INSTALL_MOD_PATH=/home/all/VirtualMachines/linux modules_install

chroot linux /bin/bash -c "update-initramfs -c -k 2.6.25"

# ready to try again
umount /dev/loop1
losetup -d /dev/loop1