Recently we were reached by person interested in running CoreOS on apu2. CoreOS is a very interesting system from security point of view. It was created to support containers and scalability out of the box. Unfortunately it requires firmware supporting GPT. At that point I was not sure if I can utilize GRUB GPT support on apu2, but this led to other questions:
- Is it possible to boot UEFI-aware OS on PC Engines apux boards?
- What level of security can I get with UEFI-aware OS in comparison to coreboot?
Those questions were much more interesting to firmware developer, because of that we decided to triage coreboot UEFI payload on PC Engines apu2 platform. For interested ones in that topic I recommend to take look at video from coreboot conference 2016. All my modifications of edk2 for the article below can be found in 3mdeb edk2 fork For those interested in UEFI-aware OS booting this blog post can be useful, but I also plan to write something straightforward that can be used and read by APUx platform users.
NOTE: this blog post wait so long for publishing that coreboot community
provided and improved support for tianocore payload. It can be chosen from
menuconfig
and adds some coreboot specific patches that improve overall
support. Please use option:
|
|
Manual method still can be useful to try vanilla edk2 and hack with it.
apu2 firmware with UEFI/EDK2 payload
Let’s start with building apu2 mainline. First follow this instruction and build mainline version of coreboot. Meanwhile you can take care of EDK2 CorebootPkg build:
|
|
On my Debian testing I had to explicitly change compilers to gcc-5
and
g++-5
:
|
|
Otherwise build fails. After that I was able to build UEFIPAYLOAD.fd
:
|
|
Build result is located in
Build/CorebootPayloadPkgIA32/DEBUG_GCC5/FV/UEFIPAYLOAD.fd
. Following
build and integration instructions I added build result as
An ELF executable payload
.
It is important to deselect secondary payloads like memtest86+
and
sortbootorder
to avoid compilation issues.
EDK2 developer’s script
For all interested in EDK2 code development I strongly advise to follow Laszlo’s guide. In case of apu2 useful script may be:
|
|
Hacking UEFI payload to boot on apu2
CbSupportDxe assert
First problem I faced was assert in CbSupportDxe.c
related to adding 1MB
memory for LAPIC. Reservation happens in CbSupportDxe entry point.
|
|
Interestingly CbSupportDxe seems to not have problem with finding GUID HOB, which is right after asserting reservation. It also passes installation of ACPI and SMBIOS table. I just commented that reservation and moved forward to see what will happen.
PcRtcEntry assert
Next problem happens during initialization of RTC:
|
|
Unfortunately Real Time Clock is required architecture protocol and cannot be
omitted. First problem with this code was an incorrect state of
Valid RAM and Time
(VRT
) bit in RTC Date Alarm register (aka Register D). By
checking AMD Bios and Kernel Developer’s Guide (BKDG) I was not able to find
issue with RTC. Reading all registers was done correctly and register layout
seemed to be standardized for RTC devices. I faced one very strange situation
after leaving apu2 for a night. First boot passed through above assert and
finished booting much further (in BDS). This was very suspicious like timing or
HW initialization issue. Final log looked like that:
|
|
In debug logs there was nothing suspicious. Apparently register D of RTC
returned correct value in VRT
register. Finally it turned out that VRT
was
incorrectly described in datasheet as read-only. Register D initialization
function caused setting VRT
bit to 0 what further led to Device Error
assert. I fixed that problem by removing initialization from PcRtcInit
.
Random unexpected behaviors
One of other behaviors worth to note was unexpected coreboot reset after applying power:
|
|
Booting to UEFI Shell on apu2 I had “freeze” after
|
|
First is gEfiShellEnvironment2Guid
and second gEfiShellInterfaceGuid
, so I
decided to take a look where those GUIDs are used and hook there to see what may
be wrong. After poking around I realized that those came from binary included in
repository. What is included can be modified by changing SHELL_TYPE
variable.
When using BUILD_SHELL
I see little bit different output:
|
|
Control is passed in BDS code by calling StartImage
. For some reason I
couldn’t print my logs to debug by printk
. I verified that I’m in correct code
by placing assert, code was interrupted in correct place but not serial log.
Trying to change DebugLib
and provide correct SerialIoLib
led to reboot. I
fixed that by removing DebugLib
from libraries section in Ia32X64 DSC:
|
|
However this showed me hang in DoShellPrompt
on function code:
|
|
Explaining ConIn, ConOut and ErrOut
Big kudos to Laszlo Ersek who is well known from creating and maintaining OVMF.
He pointed me to code in ArmVirtPkg
where workaround for my problem was
implemented. I read through code from
ArmVirtPkg/Library/PlatformBootManagerLib/PlatformBm.c
and meant that I have
to modify ConIn
, ConOut
and ErrOut
variables. It was because those
variables miss device path to UART device. ConIn
, ConOut
and ErrOut
are
global variables defined in UEFI spec. Those variables are available in boot
time, runtime and are non volatile. This means that those variables are
available during boot phase before firmware calls ExitBootServices and after
that during system runtime. Those variables can be changed, but change takes
effect after boot.
So in short those variables define where we can find input, output and std error
device. As described in mailing thread serial port can be reached through
SerialPortLib
API and it worked for me during boot phase. Precisely what
worked for me was BaseSerialPortLib16550
.
I assume methods in this library are not available in runtime and that’s why
switching to Shell caused no output. Second method is through
EfiSimpleTextOutProtocol
. Full explanation can be found in mentioned thread,
but in short it is required to add device path of UART to mentioned global
variables so those can be used. My understanding of stack is:
|
|
BaseSerialPortLib16550
works on I/O and MMIO level to initialize and provide
read/write capability for 16550 compatible UART device. This lib is utilized by
SerialDxe
DXE driver. SerialDxe
produce gEfiSerialIoProtocolGuid
and
gEfiDevicePathProtocolGuid
. First abstracts any type of I/O device and provide
communication capability for it. Second gives ability of provides information
about generic path/location information of physical or logical device (more
information in UEFI spec). gEfiSerialIoProtocolGuid
is consumed by
TerminalDxe
UEFI driver, which is responsible for producing Simple Text Input
and Output protocols on top of Serial IO protocol. Those protocols give API like
ReadKeyStroke
, WaitForKey
, OutputString
, ClearScreen
,
SetCursorPosition
and other that help in handling input and output data. Those
protocols then can be used by Shell application to provide interactive
experience.
Source code
As I mentioned at beginning code is available on 3mdeb git repo. With it
you can build coreboot.rom that boots to UEFI Shell. There are plenty things to
do i.e. map
and probably other commands do not work properly. Feel free to
contribute.
Summary
Above steps gave me ability to enable UEFI payload on top of coreboot firmware. This configuration seems to heavily use AGESA, which is a very similar to Intel FSP being responsible for big part of hardware initialization as well as exposing artifacts for UEFI-aware payload, bootloader and operating system. This blog post can open possibilities to boot UEFI-aware OSes on PC Engines apu2 platform as well as give ability to research AGESA firmware more extensively. If you are interested in enabling UEFI-aware operating system on your platform that already support coreboot do not hesitate to contact us. If you have any other questions or comments post those below.