booting Windows from a VHD

The easiest way to have multiple Windows versions available on the same machine is to place some of them into VHDs, and then you can boot an OS directly from a VHD. The boot loader stays shared between all of them on the original C: drive which might have or not have its own Windows too), just each VHD gets its own entry created in the Boot Configuration Database, and the OS can be selected through a menu at boot time. The drive letters will usually shift when you boot from a VHD: the VHD with the OS would be assigned the letter C:, and the other  drives will move, although it's possible to tell an image to use a different drive letter.

Before we go into the mechanics of it, an important note: the image in the VHD must be generalized. When Windows boots for the first time, it configures certain things, like the machine name, various initial values for generation of the random GUIDs, some hardware configuration information, which are commonly known as the specialization. Duplicating the specialized images is a bad idea, and might not work altogether. The right way to do it is by either generating a fresh VHD that had never been booted yet or by taking a booted image and generalizing it with the Sysprep tool.

An of easy way to add a VHD to the boot menu is to mount it on some drive, say E:, and run:

 bcdboot e:\windows /d /addlast

Bcdboot will create an entry for it. Along the way it will make sure that the boot loader on the disk is at least as new as the image on the VHD, updating the boot loader from VHD if necessary. An older boot loader might now be able to load the newer version of Windows, so this update is a good thing. The option /d says to keep the default boot OS rather changing it to the new VHD, and /addlast tells to add the new OS to the end of the list rather than to the front.

A caveat is that for bcdboot to work, the VHD must be mounted on a drive letter, not on a directory. If you try to do something like

 bcdboot.exe e:\vhd\img1\mountdir\Windows /d

then bcdboot will create an incorrect path in the BCD entry that includes all the current mount path, and the VHD won't boot.

By the way, if you use Bitlocker on your machine, make sure to temporarily disable it before messing with the BCD modifications, or you'll have to enter the full long decryption key on the next reboot by the PowerShell command:

 Suspend-BitLocker -RebootCount 1

This command temporarily disables the BitLocker until the next reboot, when it gets auto-enabled back. The reason for this is that normally this key gets stored in a TPM which requires the boot sequence signature to match the remembered one to divulge this information. Changing the boot loader code or data changes this signature. And no, apparently there is no way to generate this signature other than by actually booting the machine and remembering the result. So the magic suspension command copies the key to a place on disk, and on the next reboot puts the key back into TPM along with the new boot signature, removing the key from the disk.

Now about what goes on inside, and what else can be done. BCD contains a number of sections of various types. Two entry types are particularly important for this discussion: the Boot Manager and the Boot Loader. You can see them by running

 bcdedit /enum

There is one Boot Manager section. Boot manager is essentially the first part of the boot loader, and the selection of the image to boot happens there. And there is one Boot Loader section per each configured OS image, describing how to load that image.

The more interesting entries in the Boot Manager section are:

default is the GUID of the Boot Loader section that will be booted by default. On a booted system the value of default is usually shown as {current} . This is basically because bcdedit defines two symbolic GUIDs for convenience: {current} is the GUID of the Boot Loader section of the currently booted OS, {default} is the GUID of the Boot Loader section that is selected as default in the Boot Manager section. There also are some other pre-defined GUIDs used for specific section types, like {bootmgr} used for the Boot manager section.

By the way, be careful with the curly braces when calling bcdedit from PowerShell: PowerShell has its own syntactic meaning for them, so make sure to always put the strings that contain the curly braces into quotes.

displayorder is the list of Boot Loader section GUIDs for the boot menu.

timeout is the timeout in seconds before the default OS is booted automatically.

The Boot Loader section has quite a few interesting settings:

device and osdevice tell the disk that contain the OS. They would be usually set to the same values, although technically I think device is the disk that contains the Winloader (the last stage of the boot loader than then loads the kernel) while osdevice is the disk that contains the OS itself. Their values are formatted as "subtype=value", like "partition=C:" to load the OS directly from a partition or "vhd=[locate]\vhds\img1.vhd" to boot from a VHD.  The partition names in this VHD string have multiple possible formats. "[locate]" means that the boot loader will automatically go through all the drives it finds and try to find a file at this path. A string like "[e:]" will mean the specific drive E: at the time when you run bcdedit. This is an important distinction, since when booting from the different VHDs the drive mappings may be different (and very probably will be different at least between the VHDs and the OS on the main partition). In this format bcdedit finds and stores in its database the resolved partition ID, not the letter as such, so it can find the partition later no matter what letter it gets. If you run "bcdedit /enum" later when booted from a different VHD, the letter shown will match the mapping in that OS. And finally the string like "e:" will mean the partition that is seen as E: by the boot manager, and this might be difficult to predict right, so it's probably better not used. For all I can tell, in the "partition=" specification the letter is always treated similar to the "[e:]" format for VHD, i.e. the stored value is the resolved identity of the partition.

path is the path of Winloader (the last stage of the boot loader that loads the kernel) on the device. It comes in two varieties: winload.exe for the classically-partitioned disks with MBR and winload.efi for the machines with the UEFI BIOS that use the new GPT format of the partition table. If you use the wrong one, it won't boot, so the best bet is to copy the type from another Boot Loader entry that is known to be right. The path to it might also come in two varieties: either "\WINDOWS\system32" or "\Windows\System32\Boot". The first path is a legacy one that would still work on the Windows client or full server. The second one is the new one that would work on all the Windows versions, including the tiny ones like NanoServer, IOT or Phone.

description is the name shown in the boot menu.

systemroot is the location of the OS on the osdevice, usually just "\WINDOWS".

hypervisorlaunchtype enables the Hyper-V, "Auto" is a good value for it.

bootmenupolicy selects how the menu for the OS selection is displayed. The value placed there by the usual Windows install is "standard" which does the selection in the graphical mode and is quite slow painful: basically, the graphical-mode selection is done in Winloader, so if you select a different OS, it has to go through the getting a different Windloader that matches that OS, which is done by remembering the selection somewhere on disk and then rebooting the machine, so that next time the right Winloader is picked. The much better value is "legacy" which does the selection in the basic text mode directly in the Boot Manager, so the boot happens fast.

bootstatuspolicy can be set to "DisplayBootFailures" for the better diagnostics.

bootlog and sos enable some kinds of extra diagnostics when set to "yes". I'm not sure where exactly does this diagnostics go.

detecthal forces the re-enumeration of the available hardware on boot when set to "yes". It doesn't matter for the generalized images that would do this anyway. But it might help when moving a VHD with an OS from one physical machine to another.

By the way, bcdedit has two ways of specifying the settings, one for the current section, another one for a specific section. For the current section it looks simply like:

 bdcedit /detecthal yes

For a specific section (identified by a GUID or one of the symbolic pseudo-GUIDs) this becomes:

 bcdedit /set {SectionGuid} detecthal yes

You can also select the BCD store that bcdedit acts on. For an MBR machine the store is normally located in C:\Boot\BCD.  For an EFI machine the BCD store is located in a separate EFI System partition, under \EFI\Microsoft\Boot\BCD. If you're really interested in looking at the System partition, you can mount it with Disk Manager or with PowerShell. There is a bit of a caveat with mounting the System partitions: it can't be mounted to a path, only to a drive letter, and if you unmount it, that drive letter becomes lost until the next reboot. If you want to say look at the system partitions on a lot of  VHDs, a better strategy is to change the partition type from System to Basic, mount it, do your thing, then unmount it and change the type back to System as shown in this example. This way you won't leak the drive letters.

Returning to the subject,  I've made a script that helps create the BCD entries for the VHDs at will. It uses my sed for PowerShell for parsing the output of bcdedit. The main function is Add-BcdVhd and is used like this:

 Add-BcdVhd -Path C:\vhd\img1.vhd -Description "Image 1" -Reset

Here is the implementation:

 $bindir = Split-Path -parent $PSCommandPath 
Import-Module -Force -Scope Global "$bindir\TextProc.psm1"

$ErrorActionPreference = "Stop"

function Get-BootLoaderGuid
{
<#
.SYNOPSIS
Extracts the GUID of a Boot Loader entry from the output of
bcdedit /v or /enum. The entry is identified by its description or by its
device, or otherwise just the first entry.
#>
    param(
        ## The output from bcdedit /v.
        [string[]] $Text,
        ## Regexp pattern of the description used in the boot menu, to identify the section.
        [string] $DescMatch,
        ## Regexp pattern of the device used in this the section.
        [string] $DevMatch
    )

    $script:cur_desc = $DescMatch
    $script:cur_dev = $DevMatch
    
    $Text | xsed -Select "START",{
        if ($_ -match "^Windows Boot Loader") {
            $script:found_desc = !$cur_desc
            $script:found_dev = !$cur_dev
            $script:ident = $null
            skip-textselect
        }
    },{
        if ($_ -match "^identifier ") {
            $script:ident = $_
        }

        if ($cur_desc -and $_ -match "^description ") {
            $d = $_ -replace "^description *", ""
            if ($d -match $cur_desc) {
                $script:found_desc = $true
            }
        }

        if ($cur_dev -and $_ -match "^device ") {
            $d = $_ -replace "^device *", ""
            if ($d -match $cur_dev) {
                $script:found_dev = $true
            }
        }

        if ($ident -and $found_desc -and $found_dev) {
            Set-OneLine $ident
            Skip-TextSelect "END"
        }

        if ($_ -match "^$") {
            Skip-TextSelect "START"
        }
    },"END" | % { $_ -replace "^.*({[^ ]+}).*", '$1' }
}
Export-ModuleMember -Function Get-BootLoaderGuid

function Add-BcdVhd
{
    <#
    .SYNOPSIS
    Add a new VHD image to the list of the bootable images.
    #>

    param(
        ## Path to the VHD image (can be any, will be automatically converted
        ## to the absolute path without a drive.
        [Parameter(
            Mandatory = $true
        )]
        [string] $Path,
        ## The user-readable description that will be used in the boot menu.
        [Parameter(
            Mandatory = $true
        )]
        [string] $Description,
        ## Enable the debugging mode
        [switch] $BcdDebug,
        ## Enable the eventing mode
        [switch] $BcdEvent,
        ## For a fresh VHD that was never booted, there is no need to
        ## force the fioorce the detection of HAL.
        [switch] $Fresh,
        ## Enable the boot diagnostic settings.
        [switch] $Diagnose,
        ## If the entry exists, delete it and create from scratch.
        [switch] $Reset
    )

    # Convert the path to absolute and drop the drive letter
    $Path = Split-Path -NoQualifier ((Get-Item $Path).FullName)

    # Escape the regexp characters
    $pathMatch = $Path -replace "([\[\]\\\.\(\)\*\+])", '\$1'
    $pathMatch = "^vhd=.*\]$pathMatch`$"

    $descMatch = $Description -replace "([\[\]\\\.\(\)\*\+])", '\$1'
    $descMatch = "^$descMatch`$"

    $bcd = @(bcdedit /enum)
    if (!$?) { throw "Bcdedit error: $bcd" }

    # Check if this section is already defined
    $guid_by_descr = Get-BootLoaderGuid -Text $bcd -DescMatch $descMatch
    $guid_by_path = Get-BootLoaderGuid -Text $bcd -DevMatch $pathMatch

    #Write-Host "DEBUG Path match: $pathMatch"
    #Write-Host "DEBUG Descr match: $descMatch"
    #Write-Host "$guid_by_descr by descriprion, $guid_by_path by path"

    if ($guid_by_descr -ne $guid_by_path) {
        throw "Found conflicting definitions of existing sections: $guid_by_descr by descriprion, $guid_by_path by path"
    }

    $guid = $guid_by_descr

    if ($guid -and $Reset) {
        bcdedit /delete "$guid"
        if (!$?) { throw "Bcdedit error." }
        $guid = $null
    }

    if (!$guid) {
        Write-Host "Copying the current entry"
        $bcd = $(bcdedit /copy "{current}" /d $Description)
        if (!$?) { throw "Bcdedit error: $bcd" }
        $guid = $bcd -replace "^The entry was successfully copied to {(.*)}.*", '{$1}'
        if ($guid) {
            Write-Host "The new entry has GUID $guid"
        } else {
            throw "Bcdedit error: $bcd"
        }
    }

    $oldentry = @(bcdedit /enum $guid)
    if (!$?) { throw "Bcdedit error: $bcd" }

    bcdedit /set $guid device "vhd=[locate]$Path"
    if (!$?) { throw "Bcdedit error." }
    bcdedit /set $guid osdevice "vhd=[locate]$Path"
    if (!$?) { throw "Bcdedit error." }
    if (!$Fresh) {
        bcdedit /set $guid detecthal yes
        if (!$?) { throw "Bcdedit error." }
    }

    # Enable debugging.
    if ($BcdDebug) {
        bcdedit /set $guid debug yes
        if (!$?) { throw "Bcdedit error." }
        bcdedit /set $guid bootdebug yes
        if (!$?) { throw "Bcdedit error." }
    }
    if ($BcdEvent) {
        bcdedit /set $guid debug no
        if (!$?) { throw "Bcdedit error." }
        bcdedit /set $guid event yes
        if (!$?) { throw "Bcdedit error." }
    }
    bcdedit /set $guid inherit "{bootloadersettings}"
    if (!$?) { throw "Bcdedit error." }

    # enable Hyper-v start if it's installed
    bcdedit /set $guid hypervisorlaunchtype auto
    if (!$?) { throw "Bcdedit error." }

    # The more sane boot menu.
    bcdedit /set $guid bootmenupolicy Legacy
    if (!$?) { throw "Bcdedit error." }
    bcdedit /set $guid bootstatuspolicy DisplayBootFailures
    if (!$?) { throw "Bcdedit error." }

    # Other useful diagnostic settings
    if ($Diagnose) {
        bcdedit /set $guid bootlog yes
        if (!$?) { throw "Bcdedit error." }
        bcdedit /set $guid sos on
        if (!$?) { throw "Bcdedit error." }
    }

    # This is strictly needed only for CSS but doesn't hurt on other SKUs,
    # must use the path with "Boot", but preserve .exe vs .efi.
    $oldpath = $oldentry | ? { $_ -match "^path " } | % { $_ -replace "^path *","" }
    if (!$oldpath) {
        throw "The current BCD entry doesn't have a path value???"
    }
    $leaf = Split-Path -Leaf $oldpath

    bcdedit /set $guid path "\Windows\System32\Boot\$leaf"
    if (!$?) { throw "Bcdedit error." }

    # Print the settings after editing.
    bcdedit /enum $guid
}
Export-ModuleMember -Function Add-BcdVhd