[Contents]
Copyright © 2015 jsd

How to Create a Bootable USB or SDcard Drive
John Denker

*   Contents

1  Introduction

Here are some suggestions and instructions for how to create a bootable flash drive (either USB drive or SDcard) that actually works, using only standard Linux command-line tools.

The resulting structure has a number of advantages. For one thing, the flash drive will have a read/writeable “auxiliary” partition where you can easily save stuff that you want to persist across reboots. This includes crucial things like random seeds for the random number generator, your personal SSH keys, perhaps a hostname and/or IP address, et cetera.

By way of contrast: The objective is not to make a bootable flash drive based on some arbitrary .iso image. This is not possible, for multiple reasons, as discussed in section 10.5. However, certain .iso images such as the Ubuntu Live distributions contain files that can be used for creating a usable flash drive. Also certain other objects such as memtest86+.elf and/or memtest86+.bin can be booted from a flash drive, as we shall see.

2  First Step

Plug in the flash drive. Presumably it will show up as "/dev/sd?" or "/dev/mmcblk?". You can look through /dev to find the device you want, or you can use the handy script given in section 11.

  :; ./find-attached-disk.pl

3  Safety Measures

For safety sake, define an environment variable $DISK:

  export DISK
# for the most-recently attached disk
  :; find-attached-disk.pl
  :; eval $(find-attached-disk.pl | tail -1)
  :; set | grep ^DISK
# or by hand:
  :; export DISK=/dev/sdX          # possibly /dev/sdb or whatever

The point is, the name $DISK is resistant to typos. Without these safety measures, you would be only a single-letter typo away from wiping out the system disk on your machine.

4  Partition the Drive

Look at the size of the Live .iso file, so we can make the partition slightly bigger than that:

  :; du -s ubuntu-16.04.1-desktop-amd64.iso
1135684 ubuntu-16.04.1-desktop-amd64.iso

The du command gives the size in kiB; drop three digits to get a nice slight overestimate of the size in MiB. Unless you are super-short on space, round up a little bit, so that later you can replace the .iso with a bigger one without having to reformat the disk.

Rounding up from 1136 to 1500 Mib is reasonable. Use cfdisk or the like to set up partitions. If it asks what type of partition-table to use, choose “dos”. Within the parition table, create partition #1 of this size, with parition-type 0xef (’EFI’). Create partition #2 to take up the rest of the space, with partition-type 0x83 (’Linux’).

  :; cfdisk ${DISK}

Be sure to Write the setup to disk, by giving the Capital W command to cfdisk. When it asks you to confirm, answer “yes” using all three letters of the word.

Don’t use gparted, for multiple reasons: (1) It refuses to run without root privileges, even if the permissions on $DISK would allow it. This is unbelievably stupid. It is the opposite of safety. (2) Gparted won’t let you select a partition-type unless it knows how to format it. In particular, even though it recognizes type 0xef, it won’t let you create a partition of that type (presumably because doesn’t know how to format it). (3) It requires a windowing environment, whereas cfdisk runs just fine on a dumb terminal (using ncurses).

Check the work so far:

 :; fdisk -l ${DISK}

Disk /dev/sdc: 1.9 GiB, 2021654528 bytes, 3948544 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0xc9b83b63

Device     Boot   Start     End Sectors  Size Id Type
/dev/sdc1          2048 3074047 3072000  1.5G ef EFI (FAT-12/16/32)
/dev/sdc2       3074048 3948543  874496  427M 83 Linux

Another check:

  :; sfdisk -d ${DISK}
# partition table of /dev/sdc
unit: sectors

/dev/sdc1 : start=     2048, size=  3072000, Id=ef
/dev/sdc2 : start=  3074048, size=   874496, Id=83
/dev/sdc3 : start=        0, size=        0, Id= 0
/dev/sdc4 : start=        0, size=        0, Id= 0

5  Format the Aux Partition

Create a file system, and assign it the “aux” label:

  mkfs.ext4 ${DISK}2 -L aux
  partprobe ${DISK}

Also, here’s a useful trick to guarantee room for grub. This matters if the device had previously been used with no partition table, just one filesystem taking up the whole device, such as you could easily get by trying dd if=*.iso of=$DISK or the like.

  :; dd if=/dev/zero of=${DISK} bs=512 seek=1 count=2047

Reference: http://askubuntu.com/questions/158299/why-does-installing-grub2-give-an-iso9660-filesystem-destruction-warning

6  Install the Operating System Image(s) etc.

6.1  Obtain ISO Image

Choose an appropriate operating system .iso image. Download it from the provider if necessary. A good example is the "Ubuntu Live" image.

6.2  Hybridize the Image, Maybe

It is not necessary to modify the Ubuntu Live image, but other images may require hybridization.

>>   : 1; isohybrid --partok foo.iso    # not needed for Ubuntu
>>   # ignore warning message about size >1024
>>   # observe foo.iso updated /in place/

This is teach the image to handle being chainloaded on a non-optical medium, as opposed to being loaded natively from an optical medium. The --partok option teaches it to handle things when it occupies a partition rather than occupying the whole disk.

6.3  Emplace the Image

Copy the bits:

  :; time dd if=ubuntu-16.04.1-desktop-amd64.iso of=${DISK}1 bs=8K
     # takes about 7 minutes

The copy (using dd) does a treeemendous amount of read-ahead and write-behind. The write-behind cannot be interrupted, not even by kill -9.

6.4  Install Other Images

If desired:

  :; mount ${DISK}2 /mnt/aux     # if not already mounted
  :; cp $path_to/memtest86+.bin /mnt/aux/boot/
  :; cp $path_to/memtest86+.elf /mnt/aux/boot/

6.5  Endow It With Some Entropy

Give the new system some randomness of its own ... in a couple of plain files, and in the grub environment:

  :; mount ${DISK}2 /mnt/aux     # if not already mounted
  :; dd iflag=fullblock if=/dev/random bs=512 count=4 of=/mnt/aux/random.true
  :; dd iflag=fullblock if=/dev/urandom bs=512 count=4 of=/mnt/aux/random.pseudo
  :; ./mk-grub-random.sh

This is an important step. It is a first step in a three-step process. The second step is to get grub to offer the randomness to the kernel. This is done using the grub environment, in mk-grub-cfg.sh with help from mk-grub-random.sh.

The third step is to get the Linux /dev/random driver to do something useful with the randomness that is being offered. This step is not yet completed.

7  Install the Boot Machinery

You have two options:

7.1  Installing the Syslinux Chainloader

One option is to use the simple 440-byte chainloader provided by the syslinux-common package. This has the advantage of simplicity.

        :; dd if=/usr/lib/syslinux/mbr/mbr.bin of=/$drive

7.2  Installing and Configuring Grub

Another option is to install grub on the flash drive. This has the advantage of providing a possible route to provide randomness to the kernel.

  :; install -d /mnt/aux        # create the mount-point
  :; mount ${DISK}2 /mnt/aux
  :; grub-install --root-directory=/mnt/aux ${DISK}
    # takes about half a minute

Create a grub.cfg to tell grub what to do, and where to find the various images. The script to do this is shown in section 11.

  :; mount ${DISK}2 /mnt/aux     # if not already mounted
  :; ./mk_grub_cfg.sh

According to the web, the grub.cfg file is supposed to need things like:

    insmod part_msdos
    insmod ntfs
    insmod iso9660

However, experiments indicate that none of those are necessary with current versions of grub (mid 2015).

8  Unmount the Aux Partition

Bad things happen if you forget to do this.

  :; umount ${DISK}2

9  Test the New System

It is convenient to use qemu to test the flash drive. You can use the handy script, as spelled out in section 11. Invoke it as:

  :; /boot-usb-test.sh

10  Lore and Miscellaneous Notes

10.1  Grub Loopback

I am aware that grub has a nice loopback feature. It means you could copy the .iso image to the flash drive as a plain file (rather than using dd to create an ISO-9660 partition). The advantage is that this would simplify layout of the flash drive. The disadvantage is that it would increase the boot-time complexity. Possibly it would increase the run-time complexity as well; I don’t know.

In any case, I prefer to unpack the thing once and for all, creating an ISO-9660 partition.

10.2  Looking at the .iso Image

If you want to see the structure inside the .iso image, you can use something like this:

  :; mount -o ro,loop ubuntu-16.04.1-desktop-amd64.iso /mnt/play

What you see in “/mnt/play” is what the target machine will see in “/” when it runs.

10.3  No Such Device

If grub fails with error messages bout “no such device” or “no such partition” don’t panic ... but don’t try to debug it.

The only cure I know is to start over. That is: unmount any partions associated with $DISK*, unplug the drive, plug it back in, and re-do all the steps outlined in the main part of this document.

Hint: If you copy bits to the drive while the kernel has parts of it mounted, things will get badly screwed up. The sync command won’t fix it. As far as I can tell, the only way to get un-screwed is to start over.

10.4  Partition ID

Some non-Linux folks use 0x96 to represent ISO-9660; reference: https://en.wikipedia.org/wiki/Partition_type

You could theoretically change it via:

  :; sfdisk --print-id ${DISK} 1
  :; sfdisk --change-id ${DISK} 1 96

However, things seem to work OK if we set it to type 0xef, aka EFI, so let’s do that. I’m not sure it matters; I don’t think grub or any of our other friends care very much about partition-type (aka partition-ID). If there is a filesystem on the partition, there does not seem to be much of a requirement for partition-ID to match the filesystem-type.

USB sticks typically come from the factory with a partition table formatted onto them (unlike floppy disks). There is only one partition. Sometimes there is a ten- or fifteen-megabyte chunk of unused space before the start of the partition; other than that, the partition covers the whole disk (excluding the usual 2048-sector boot area).

10.5  Various Things That Don’t Work

11  Appendix: Some Useful Scripts

#! /bin/bash

<<\EoF cat > /mnt/aux/boot/grub/grub.cfg
#begin grub.cfg

set timeout_style=menu
set timeout=10

load_env
echo setting random.seed=${randomseed}
sleep -v 5

menuentry "Debian Live" {
    set root='hd0,msdos1'
    linux /casper/vmlinuz.efi persistent boot=casper noeject noprompt splash toram random.seed=${randomseed} --
    initrd /casper/initrd.lz
}

menuentry "memtest86+.bin" { # works
    insmod ntfs
    set root='hd0,msdos2'
    linux16 /boot/memtest86+.bin
}

menuentry "memtest86+.elf" {    # works
    set root='hd0,msdos2'
    knetbsd /boot/memtest86+.elf
}

#end grub.cfg
EoF

mk-grub-cfg.sh     (DL)

#! /bin/bash

# Possible usage:
#  ALTROOT=/mnt/aux/ mk-grub-random.sh

# I hate redirecting stderr ... but dd gives me
# no other way to reduce verbosity.

randomseed=$(2>/dev/null dd iflag=fullblock             \
        if=/dev/urandom count=1 bs=24 | base64)

# Note the 0: here.
# It means zero bits of physics entropy,
# i.e. pseudorandom, not true random
grub-editenv ${ALTROOT}/boot/grub/grubenv set randomseed="0:$randomseed"

mk-grub-random.sh     (DL)

#! /usr/bin/perl -w

# Read syslog to find when each USB or MMC drive was attached
# If it gets removed and re-attached, only the last one counts.
# If it was attached so long ago that the event is
# no longer visible in syslog, this approach fails.
# If there are more than one such devices, each one
# is shown, in order of time of attachment.

use strict;
use Symbol 'gensym';
use DateTime;

my @moname = ("Jan","Feb","Mar","Apr","May","Jun",
              "Jul","Aug","Sep","Oct","Nov","Dec");
my $tz = DateTime::TimeZone::Local->TimeZone();

main: {
  my %map = ();
  my $sequence = -1;     # keep track of chronological order

# Read /dev/disk/by-id
# Fairly robust way of finding all usb drives.
  foo: {
    my $dh = gensym();
    my $dir= '/dev/disk/by-id';
    my $simple = $dir;
    $simple =~ s'/$'';
    opendir($dh, $dir) || die;
# typical thing we are interested in:
#> /dev/disk/by-id/usb-SanDisk_Ultra_4C531001640417112401-0:0 -> ../../sdb
#> /dev/disk/by-id/mmc-SL64G_0x436a2d8a -> ../../mmcblk0
#
# and in contrast, not interested in:
#> /dev/disk/by-id/usb-USB_2.0_USB_Flash_Drive_2cf51c40bc0343-0:0-part1 -> ../../sdc1

    while(my $entry = readdir $dh) {
      if ($entry =~ m'^(usb|mmc)' && $entry !~ m'-part[0-9]*$') {
        my $fn = "$simple/$entry";
        my $target = readlink $fn;
        if (defined $target) {
           #### print "$fn -> $target\n";
           my $rslt = {};
           my @status = stat $fn;
           my $ctime = $status[10];
           $target =~ s'[./]*'';
           $$rslt{drive} = $target;
           $$rslt{date} = format_date($ctime);
           $$rslt{seq} = $ctime;
           $map{$target} = $rslt;
        } # else not a symlink, should never happen
      }
    }
    closedir $dh;
  }

# sort so that they come out in chronological order:
  for my $drive (sort {${$map{$a}}{seq} <=> ${$map{$b}}{seq}} keys %map) {
    my $rslt = $map{$drive};
    my $special = "/dev/$$rslt{drive}";
    if (-b $special) {
      my $cmd = "blockdev --getsz $special";
      my $pipe = gensym();
      open ($pipe, '-|', $cmd)
        || die "Cannot pipe from '$cmd' : $!\n";
      my $size = <$pipe>;
      if (close $pipe) {
        $size /= 2;            # convert from sectors to kiB
        $size /= 1024;            # convert to MiB
      } else {
        $size = '???';          # probably "permission denied"
                                # when trying to do --getsz
      }

      my $s1 = "DISK=$special";
      my $s2 = "DISKsize__MiB=$size";
      my $s3 = "DISKdate='$$rslt{date}'";
      printf ("%-20s %-20s %s\n", $s1, $s2, $s3);
    }
  }
}

sub format_date{
  my ($epoch) = @_;

  my $dt = DateTime->from_epoch( epoch => $epoch, time_zone => $tz);
  my $year   = $dt->year;
  my $month  = $dt->mon;  # 1-12 - you can also use '$dt->mon'
  my $day    = $dt->day;    # 1-31 - also 'day_of_month', 'mday'
  my $dow    = $dt->day_of_week; # 1-7 (Monday is 1) - also 'dow', 'wday'
  my $hour   = $dt->hour; # 0-23
  my $minute = $dt->minute; # 0-59 - also 'min'
  my $second = $dt->second; # 0-61 (leap seconds!) - also 'sec'
  my $doy    = $dt->day_of_year; # 1-366 (leap years) - also 'doy'
  my $doq    = $dt->day_of_quarter; # 1.. - also 'doq'
  my $qtr    = $dt->quarter; # 1-4
  my $ymd    = $dt->ymd; # 1974-11-30
     $ymd    = $dt->ymd('/'); # 1974/11/30 - also 'date'
  my $hms    = $dt->hms; # 13:30:00
#    $hms    = $dt->hms('|'); # 13|30|00 - also 'time'

  return "$moname[$month-1] $day $hms";
}

find-attached-disk.pl     (DL)

#! /bin/bash

# Note that usermode networking supports TCP and UDP but not ICMP.  In
# particular, ping will not work.

# Useful hint: Sometimes it's nice to pass the "-snapshot" argument

# Note: requires "modprobe kvm-intel"
# which loads "kvm" as well as "kvm-intel"

if test -z "${USB}" ; then
  1>&2 echo "Please export USB=/dev/something"
  exit 1
fi

mounted=$( awk '{
  if (match($1, "^'"${USB}"'") ) printf "%s ", $1;
}' /proc/mounts )

if test -n "$mounted" ; then
  1>&2 echo "You probably want to unmount some stuff:"
  for part in $mounted ; do
    1>&2 echo "  :; umount $part"
  done
  exit 1
fi

if ! lsmod | grep -w -q '^kvm_intel' ; then
  1>&2 echo 'Requires KVM modules.'
  1>&2 echo 'Hint:    sudo modprobe kvm_intel'
  exit 1
fi

qemu-system-x86_64              \
        -hda ${USB}             \
        -boot order=c           \
        -enable-kvm             \
        -m 1024M                \
        -rtc base=utc           \
        -device virtio-rng-pci  \
        "$@"

boot-usb-test.sh     (DL)
[Contents]
Copyright © 2015 jsd