972
edits
[unchecked revision] | [unchecked revision] |
Artoria2e5 (talk | contribs) |
m (Fix lint errors) |
||
(46 intermediate revisions by 13 users not shown) | |||
Line 2:
== UEFI basics ==
===Downloading UEFI images===
If you choose [[VirtualBox]] for virtualization, then UEFI is already included, no need to download the image manually. You just have to enable it in the VM's preferences by clicking "Settings" / "Systems" / "Enable EFI (special OSes only)" checkbox.
Otherwise for emulation and virtual machines, you'll need an OVMF.fd firmware image. It might be tricky to find, so here are some alternative download links too:
* [https://github.com/tianocore/tianocore.github.io/wiki/OVMF TianoCore Download] (official link)
* [https://github.com/BlankOn/ovmf-blobs OVMF-blobs] (unofficial precompiled 32 bit and 64 bit versions, easy to use)
* [https://www.kraxel.org/repos/ RPM packages] (official TianoCore packages)
* [https://packages.debian.org/sid/ovmf Debian packages]
* [https://packages.gentoo.org/useflags/ovmf Gentoo packages]
* [https://aur.archlinux.org/packages/ovmf-git/?comments=all Arch packages]
Under Linux, you can also install these with your distro's package manager, for example:
'''Debian / Ubuntu'''
<syntaxhighlight lang="bash">
# apt-get install ovmf
</syntaxhighlight>
'''RedHat / CentOS'''
<syntaxhighlight lang="bash">
# yum install ovmf
</syntaxhighlight>
'''MacOS'''
Use the OVMF-blobs repo.
'''Windows'''
Use the OVMF-blobs repo or download the RPM version, then use ''7-Zip File Manager'' to extract the OVMF.fd file from the downloaded archive.
===UEFI vs. legacy BIOS===
A common misconception is that UEFI
====Platform initialization====
On a legacy system, BIOS performs all the usual platform initialization (memory controller configuration, PCI bus configuration and BAR mapping, graphics card initialization, etc.), but then drops into a backwards-compatible real mode environment. The bootloader must enable the A20 gate, configure a GDT and an IDT, switch to protected mode, and for x86-64 CPUs, configure paging and switch to long mode.
UEFI firmware performs those same steps, but it also enables the A20 gate and prepares
Additionally, the platform initialization procedure of UEFI firmware is standardized. This allows UEFI firmware to be extended in a vendor-neutral way.
====Boot mechanism====
A legacy BIOS loads a 512 byte flat binary blob from the MBR of the boot device into memory at physical address 7C00 and jumps to it. The bootloader cannot return back to BIOS. UEFI firmware loads an arbitrary sized UEFI application (a relocatable PE executable file) from a FAT partition on a GPT
====System discovery====
A legacy bootloader scans memory for structures like the [[EBDA]], [[SMBIOS]], and [[ACPI]] tables. It uses PIO to talk to the root [[PCI]] controller and scan the PCI bus. It is possible that redundant tables may be present in memory (for example, the [[MP_Specification|MP]] table in the SMBIOS contains information that's also present in the ACPI [[DSDT]] and [[MADT]]) and the bootloader can choose which to use.
When UEFI firmware calls a UEFI application's entry point function, it passes a "System Table" structure, which contains pointers to all of the system's ACPI tables, memory map, and other information relevant to an OS. Legacy tables (like
====Convenience functions====
Line 27 ⟶ 58:
====Development environment====
Legacy bootloaders can be developed in any environment that can generate flat binary images: NASM, GCC, etc. UEFI applications can be developed in any language that can be compiled and linked into a [[PE]] executable file and supports the calling convention used to access functions established in memory by the UEFI firmware. In practice this means one of
[[EDK2]] is a large and complex, yet feature filled environment with its own build system. It can be configured to use GCC, LLVM, MinGW, Microsoft Visual C++, etc. as a cross-compiler. Not only can it be used to compile UEFI applications, but it can also be used to compile UEFI firmware to be flashed to a BIOS ROM.
[[
[[POSIX-UEFI]] is very similar to GNU-EFI, but it is distributed mainly as a source, not as a binary library, has ANSI C like names and works with GCC as well as LLVM CLang. It's shipped with a Makefile that sets up the compiler flags for you.
====Emulation====
[[OVMF]], a popular open source UEFI firmware, has been ported to the QEMU (but not Bochs) emulated machine. Because it implements the UEFI specification, it behaves very similarly to commercial UEFI firmware on real machines. (OVMF itself is built with TianoCore, and its source ship with is as well, but pre-built images are available.)
===Legacy bootloader or UEFI application?===
Line 79 ⟶ 111:
Note that UEFI applications are not signed by the PK, unless the PK also happens to be in the KEK.
Not all UEFI firmwares support Secure Boot, although it is a requirement for Windows
An easy way out to use a loader that is signed by Microsoft, and allows you to load another binary signed by a key and certificate owned by you (called MOK, Machine Owner's Key). Such a loader is [https://github.com/rhboot/shim shim], used by RedHat, Fedora, Suse, Ubuntu, Arch and many other distros to load GRUB. The filename of the EFI executable is hardwired in shim, but if you rename your loader to GRUBX64.EFI (or GRUBIA32.EFI), you sign it with your MOK key and certificate using [https://github.com/imedias/sbsigntool sbsigntool], then you can load any loader in Secure Boot you want.
===How to use UEFI===
Line 87 ⟶ 121:
A good starting point is writing a UEFI application that uses the System Table to fetch a memory map, and uses the "File" protocol to read files from FAT-formatted disks. The next step might be to use the System Table to locate ACPI tables.
==Developing with POSIX-UEFI==
:{{Main|POSIX-UEFI}}
One option to compile UEFI applications on POSIX like systems is POSIX-UEFI. It provies a [[libc]]-like API for your EFI application, and ships with a Makefile that can detect and set up the toolchain for you. It can use GCC or LLVM, and defaults to using the host compiler, but a cross compiler is still recommended.
It uses POSIX style typedefs (like ''uintn_t'' instead of ''UINTN''), and it does not ship with the standard EFI headers. You can still get interfaces not covered by POSIX-UEFI (such as GOP) by installing the EFI headers from GNU-EFI or EDK2. Also, it compiles with the MS ABI, meaning that UEFI services can be called natively (i.e., without uefi_call_wrapper) so long as your apps are compiled with it as well.
The traditional "Hello, world" UEFI program goes like this.
<syntaxhighlight lang="c">
#include <uefi.h>
int main (int argc, char **argv)
{
printf("Hello, world!\n");
return 0;
}
</syntaxhighlight>
Makefile looks like this:
<syntaxhighlight lang="make">
TARGET = main.efi
include uefi/Makefile
</syntaxhighlight>
Run make to build it. The result of this process is a PE executable file ''main.efi''.
==Developing with GNU-EFI==
:{{Main|GNU-EFI}}
GNU-EFI can be used to develop both 32-bit and 64-bit UEFI applications. This section will address 64-bit UEFI applications only, and assumes that the development environment itself is running on an x86_64 system, so that no cross-compiler is needed. For a more thorough walk-through of a proper (non-gnu-efi) development environment, see [[UEFI App Bare Bones]].
GNU-EFI includes four things:
* '''crt0-efi-x86_64.o''': A CRT0 (C runtime initialization code) that provides an entry point that UEFI firmware will call when launching the application, which will in turn call the "efi_main" function that the developer writes.
Line 102 ⟶ 161:
The traditional "Hello, world" UEFI program is shown below.
<
#include <efi.h>
#include <efilib.h>
Line 114 ⟶ 173:
return EFI_SUCCESS;
}
</syntaxhighlight>
A few notes:
* efi.h is included so we can use types like EFI_STATUS, EFI_HANDLE, and EFI_SYSTEM_TABLE.
* When creating a 32-bit UEFI application, EFIAPI is empty; GCC will compile the "efi_main" function using the standard C calling convention. When creating a 64-bit UEFI application, EFIAPI expands to "__attribute__((ms_abi))" and GCC will compile the "efi_main" function using Microsoft's x64 calling convention, as specified by UEFI. Only functions that will be called directly from UEFI (
* "InitializeLib" and "Print" are convenience functions provided by libefi.a with prototypes in efilib.h. "InitializeLib" lets libefi.a store a reference to the ImageHandle and SystemTable provided by BIOS. "Print" uses those stored references to print a string by reaching out to UEFI-provided functions in memory. (Later on we will see how to find and call UEFI-provided functions manually.)
This program is compiled and linked as below.
<
$ gcc main.c \
-c \
Line 157 ⟶ 216:
main.so \
main.efi
</syntaxhighlight>
The result of this process is a 44 kB PE executable file ''main.efi''. On a real project you'll probably want to use make or another build tool, and may need to build a cross-compiler. The way GNU-EFI works is a bit contrived: you are wrapping an ELF file built by your normal compiler into PE.
Line 166 ⟶ 225:
The recommended way to use OVMF (for QEMU 1.6 or newer) is with a <code>pflash</code> parameter. The instructions below assume you have an OVMF image split into separate CODE and VARS sections.
<
$ qemu-system-x86_64 -cpu qemu64 \
-drive if=pflash,format=raw,unit=0,file=path_to_OVMF_CODE.fd,readonly=on \
-drive if=pflash,format=raw,unit=1,file=path_to_OVMF_VARS.fd \
-net none
</syntaxhighlight>
If you prefer to work on a terminal without a display, or via SSH/telnet, you will want to run QEMU without graphics support, using the <code>-nographic</code> flag.
Line 182 ⟶ 241:
===Creating disk images===
:{{Main|Bootable Disk}}
To launch a UEFI application you will need to create a disk image and present it to QEMU. UEFI firmware expects UEFI applications to be stored in a FAT12, FAT16, or FAT32 file system on a [[GPT]] or [[MBR]]-partitioned disk. Many firmwares only support FAT32, so that's what you'll want to use. Depending on your platform, there are several different ways to create a disk image containing your UEFI application, but they all start by creating a zeroed disk image file. The minimum FAT32 partition size is 33,548,800 bytes, plus you will need space for the primary and secondary GPT tables, plus some slack space so the partition can be aligned correctly. Throughout these examples we will be creating a 48,000,000 byte (93750 512-byte sectors, or 48 MB) disk image.
<syntaxhighlight lang="bash">
$ dd if=/dev/zero of=/path/to/uefi.img bs=512 count=93750
</syntaxhighlight>
====uefi-run helper application====
The uefi-run application is useful for quick testing. It creates a temporary FAT image containing your EFI application and starts qemu.
<
$ uefi-run -b /path/to/OVMF.fd -q /path/to/qemu app.efi -- <extra_qemu_args>
</syntaxhighlight>
uefi-run is not currently packaged for any distribution. You can install it using cargo (the Rust package manager) though ("cargo install uefi-run").
Line 198 ⟶ 259:
====Linux, root required====
This approach requires root privileges and uses '''gdisk''', '''losetup''', and '''mkdosfs'''. First, use gdisk to create a GPT partition table with a single [[EFI System Partition]].
<
$ gdisk /path/to/uefi.img
GPT fdisk (gdisk) version 0.8.10
Line 232 ⟶ 293:
The new table will be used at the next reboot.
The operation has completed successfully.
</syntaxhighlight>
Now you have disk image with [[GPT|GUID partition table]] on it and an unformatted EFI partition starting at sector 2048. Unless you deviated from the commands shown above, the disk image will use 512-byte sectors, so the EFI partition starts at byte 1,048,576 and is 46,934,528 bytes in length. Use losetup to present the partition to Linux on a loopback device.
<
losetup --offset 1048576 --sizelimit 46934528 /dev/loop0 /path/to/uefi.img
</syntaxhighlight>
(If /dev/loop0 is already in use you will need to select a different loopback device.)
Format the partition for FAT32 with mkdosfs.
<
mkdosfs -F 32 /dev/loop0
</syntaxhighlight>
The partition can now be mounted, so that we can copy files to it. In this example we use the "/mnt" directory, but you could also create a local directory for temporary use.
<
mount /dev/loop0 /mnt
</syntaxhighlight>
Copy any UEFI applications you want to test to the file system.
<
$ cp /path/to/main.efi /mnt/
$ ...
</syntaxhighlight>
Finally, unmount the partition and free the loopback device.
<
$ umount /mnt
$ losetup -d /dev/loop0
</syntaxhighlight>
''uefi.img'' is now a disk image containing primary and secondary GPT tables, containing a single partition of type EFI, containing a FAT32 file system, containing one or more UEFI applications.
Line 268 ⟶ 329:
====Linux, root not required====
This approach uses '''parted''', '''mformat''', and '''mcopy''' and can be performed with user privileges. First, use parted to create primary and secondary GPT headers, and a single EFI partition spanning the same range as the approach above.
<
$ parted /path/to/uefi.img -s -a minimal mklabel gpt
$ parted /path/to/uefi.img -s -a minimal mkpart EFI FAT16 2048s 93716s
$ parted /path/to/uefi.img -s -a minimal toggle 1 boot
</syntaxhighlight>
Now create a new temporary image file that will contain the EFI partition data and use mformat to format it with FAT16.
<
dd if=/dev/zero of=/tmp/part.img bs=512 count=91669
mformat -i /tmp/part.img -h 32 -t 32 -n 64 -c 1
</syntaxhighlight>
Use mcopy to copy any UEFI applications you want to test to the file system.
<
$ mcopy -i /tmp/part.img /path/to/main.efi ::
$ ...
</syntaxhighlight>
Finally, write the partition image into the main disk image.
<
$ dd if=/tmp/part.img of=/path/to/uefi.img bs=512 count=91669 seek=2048 conv=notrunc
</syntaxhighlight>
''uefi.img'' is now a disk image containing primary and secondary GPT tables, containing a single partition of type EFI, containing a FAT16 file system, containing one or more UEFI applications.
Line 295 ⟶ 356:
====FreeBSD, root required====
This approach requires root privileges and uses '''mdconfig''', '''gpart''', '''newfs_msdos''', and '''mount_msdosfs'''. First, create a device node that presents the zeroed disk image as a block device. This will let us work on it using standard partitioning and formatting tools.
<
$ mdconfig -f /path/to/uefi.img
md0
</syntaxhighlight>
In this example the new block device is ''md0''. Now create the empty primary and secondary GPT tables on the device.
<
$ gpart create -s GPT md0
md0 created
</syntaxhighlight>
Now we can add a partition to the disk. We'll specify an "EFI" partition, which just means that GPT will set that partition's GUID to the special "EFI" type. Not all BIOSs require this, and the partition will still be able to be mounted and browsed normally on Linux, FreeBSD, and Windows.
<
$ gpart add -t efi md0
md0p1 added
</syntaxhighlight>
Next, create a FAT16 file system on the new partition. You can specify various parameters for the file system if you'd like, but it isn't necessary. Ideally you would create a FAT32 partition for best firmware compatibility but FreeBSD seems to create FAT32 partitions that OVMF can't read.
<
$ newfs_msdos -F 16 md0p1
newfs_msdos: trim 2 sectors to adjust to a multiple of 9
/dev/md2p1: 93552 sectors in 11694 FAT16 clusters (4096 bytes/cluster)
BytesPerSec=512 SecPerClust=8 ResSectors=1 FATs=2 RootDirEnts=512 Media=0xf0 FATsecs=46 SecPerTrack=9 Heads=16 HiddenSecs=0 HugeSectors=93681
</syntaxhighlight>
The partition can now be mounted, so that we can copy files to it. In this example we use the ''/mnt'' directory, but you could also create a local directory for temporary use.
<
$ mount_msdosfs /dev/md0p1 /mnt
</syntaxhighlight>
Copy any UEFI applications you want to test to the file system.
<
$ cp /path/to/main.efi /mnt/
$ ...
</syntaxhighlight>
Finally, unmount the partition and free the block device.
<
$ umount /mnt
$ mdconfig -d -u md0
</syntaxhighlight>
''uefi.img'' is now a disk image containing primary and secondary GPT tables, containing a single partition of type EFI, containing a FAT16 file system, containing one or more UEFI applications.
Line 346 ⟶ 407:
First, let's create a temporary folder that will contains all files and folders required for booting UEFI.
<
$ mkdir -p diskImage/EFI/BOOT
</syntaxhighlight>
Secondly, let's copy the boot application to the required location:
<
$ cp bootx64.efi diskImage/EFI/BOOT/BOOTX64.EFI
</syntaxhighlight>
Finally, let's create a disk image partitioned with GPT, formatted with fat32 (-fs fat32), overriding destination file if needed (-ov), define disk size (-size 48m), define volume name (-volname NEWOS), the file format which the disk will be encoded (-format UDTO - the same used for DVDs/CDs) and the source folder containing the files that will be copied to the new disk:
<
$ hdiutil create -fs fat32 -ov -size 48m -volname NEWOS -format UDTO -srcfolder diskImage uefi.cdr
</syntaxhighlight>
uefi.cdr should be ready to be used by QEMU.
Line 366 ⟶ 427:
===Launching UEFI applications===
Once your disk image is ready, you can invoke QEMU as below.
<
$ qemu-system-x86_64 -cpu qemu64 -bios /path/to/OVMF.fd -drive file=uefi.disk,if=ide
</syntaxhighlight>
When OVMF drops into the UEFI shell, you will see an additional entry in the "Mapping table", labeled "FS0". This indicates that the firmware detected the disk, discovered the partition, and was able to mount the file system. You can explore the file system by switching to it using the DOS-style syntax "FS0:", as illustrated below.
Line 400 ⟶ 461:
===Bootable UEFI applications===
The boot order NVRAM variables determine where firmware will look for UEFI applications to be launched at boot. Although this can be changed (for example, an OS installer might customize the boot entry for the hard drive to which it was installed) firmware typically looks for a UEFI application named "
Unlike a UEFI application launched from the shell, if a bootable UEFI application returns BIOS will continue searching for other boot devices.
Line 414 ⟶ 475:
* Phoenix (SecureCore, TrustedCore, AwardCore).
* Insyde (InsydeH20).
== UEFI applications in detail ==
Line 469 ⟶ 527:
Applications must either load an OS and exit boot services, or return from the main function (in which case the boot loader will look for the next app to load).
Drivers must initialize and then return 0 on success, or an error code. A computer might fail
==== Memory ====
Line 482 ⟶ 540:
However, memory used by the runtime drivers must '''never''' be touched - the runtime drivers stay active and loaded for as long as the computer runs.
One way to see a breakdown of a PE file containing a UEFI application is by<
Its output is quite long. Among other things, it shows the '''subsystem''', that is the type of the UEFI image, mentioned earlier.
Line 496 ⟶ 554:
Note that functions strictly internal to the application can use whatever calling convention the developer chooses.
==== POSIX-UEFI, GNU-EFI and GCC ====
{{Main|GNU-EFI}}
cdecl is the standard calling convention used by GCC, so no special attributes or modifiers are needed for writing the main entry point or calling UEFI functions in an x86 UEFI application developed with GNU-EFI. For x86-64, however, the entry point function must be declared with the "___attribute___((ms_abi))" modifier and all calls to UEFI-provided functions must be made through the "uefi_call_wrapper" thunk. This thunk is called with cdecl, but then translates to the Microsoft x86-64 calling convention before calling the requested UEFI function. This is necessary because older releases of GCC do not support specifying calling conventions for function pointers.
For [[POSIX-UEFI]], which also uses GCC, your entry point looks like the standard main(), and no special ABI is required. Also the build environment takes care of the compiler flags for you, so you can simply call UEFI functions without "uefi_call_wrapper", no matter if you're using the host gcc or a cross-compiler.
For developer convenience, both POSIX-UEFI and GNU-EFI provides the "EFIAPI" macro, which expands to "cdecl" when targeting x86 and "__attribute__(ms_abi))" when targeting x86-64. Additionally, the "uefi_call_wrapper" thunk will simply pass the call through on x86. This allows the same source code to target x86 and x86-64. For example, the following main function will compile with the correct calling convention on both x86 and x86-64 and the call through the "uefi_call_wrapper" thunk will select the correct calling convention to use when calling the UEFI function (in this case, printing a string).
<syntaxhighlight lang="c">
EFI_STATUS EFIAPI efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
{
Line 509 ⟶ 570:
return status;
}
</syntaxhighlight>
=== Language binding ===
UEFI applications are typically written in C, although bindings could be written for any other language that compiles to machine code. Assembly is also an option; a [[uefi.inc]] is available for [[FASM]] that allows UEFI applications to be written as below.
<
format pe64 dll efi
entry main
Line 537 ⟶ 598:
section '.reloc' fixups data discardable
</syntaxhighlight>
As a UEFI application contains normal x86 or x86-64 machine code, inline assembly is also an option in compilers that support it.
Line 548 ⟶ 609:
=== My UEFI application hangs/resets after about 5 minutes ===
When control is handed to your UEFI application by firmware, it sets a watchdog timer for 5 minutes, after which the firmware is reinvoked as it assumes your application has hung. The firmware in this case will normally try to reset the system (although the OVMF firmware in VirtualBox simply causes the screen to go black and hang). To counteract this, you are required to refresh the watchdog timer before it times out. Alternatively, you can disable it completely with code like <
=== My bootloader hangs if I use user defined EFI_MEMORY_TYPE values ===
For the memory management functions in EFI, an OS is meant to be able to use "memory type" values above 0x80000000 for its own purposes. In the
== See also ==
Line 559 ⟶ 620:
* [[EFI System Partition]]
* [[PE]] file format
* [[
* [[POSIX-UEFI]]
* [[GNU-EFI]]
* [https://github.com/nebulaeonline/nebulae/tree/UefiBarebones Uefi Barebones MSVC/Clang/Visual Studio]
=== External Links ===
* [
* [http://www.tianocore.org/ Intel TianoCore EDK2]
* [https://github.com/tianocore/tianocore.github.io/wiki/OVMF OVMF firmware images] for use with [[QEMU]]
* [https://web.archive.org/web/20181028201454/http://wiki.phoenix.com/wiki/index.php/Main_Page Phoenix UEFI Wiki]
* [http://x86asm.net/articles/others/index.html Several articles about UEFI]
* [https://www.microsoft.com/en-us/download/details.aspx?id=19509 PE specification covering the (U)EFI binary format]
* [https://uefi.blogspot.com/ Blog about UEFI, with bits about UEFI development]
* [https://web.archive.org/web/20160316192235/http://internshipatdell.wikispaces.com/file/view/How+to+build+an+UEFI+application.pptx Presentation guiding through simple UEFI application setup]
* [
* [https://gitlab.com/bztsrc/posix-uefi POSIX-UEFI] documentation and source
* [[Wikipedia:Extensible_Firmware_Interface|Wikipedia Article on EFI]]
[[Category:x86]]
[[Category:x86-64]]
[[Category:ARM]]
[[Category:Firmware]]
|