Unified Extensible Firmware Interface (UEFI) is a well-established specification that defines a software interface to control the start-up of complex microprocessor systems, including initializing firmware, loading drivers and applications, all the way through to launching an operating system. UEFI on ARM is an excellent solution to control the booting of ARM-based servers and client computing devices, as it is small, fast and offers remote access capabilities.
Porting and debugging this flexible and powerful interface, however, is not a trivial task if you are not well geared for it. This blog shows step-by-step how to build the UEFI software and debug it at source level, including all dynamically loaded modules (DLLs), using the ARM® Development Studio 5 (DS-5™) toolchain.
UEFI has been developed by a community of companies, and ARM has been actively contributing to this initiative for several years. The "Tianocore" project on SourceForge offers Open Source implementations of the UEFI specification, and ARM ports are available to users under the BSD license. In addition, Linaro is maintaining a UEFI tree based on the SourceForge Tianocore subversion tree.
The toolchains supported for compiling UEFI for ARM include the ARM Compiler (as supplied in DS-5 Professional, a new, rebranded version of RVCT), GCC and XCode.
UEFI is quite a large package of source code, and porting this to your own ARM-based platform can be a daunting task. Thankfully, ready-made example ports for ARM-based systems such as ARM Versatile™ Express, BeagleBoard and Samsung Origen are already available. DS-5 Professional Edition provides a complete UEFI development environment that enables you to:
- fetch the UEFI source code via the Eclipse git plug-in
- build the source code using the ARM Compiler
- download the executables to a software model (a Cortex™-A9x4 RTSM is provided with DS-5) or to a hardware target (available separately)
- run/debug the code at source-level using DS-5 Debugger
The DS-5 Debugger component in ARM DS-5 toolchain allows you to debug all phases of the UEFI control flow, including SEC (Security - Set up Temp memory), PEI (Pre-EFI - Initialize Memory), DXE (Driver Execution - Dispatch list of UEFI / DXE drivers) and BDS (Boot Device Selection - Run Setup) phases.
First launch DS-5 Debugger on a host machine. If you do not have the DS-5 toolchain installed on your machine, you can download a free 30-day evaluation license. Then set up DS-5 for UEFI development as described here, including creating a debug launch configuration that loads the UEFI firmware RTSM_VE_CORTEX-A9_EFI.fd .
Establish a connection to the Cortex-A9 x4 software model (RTSM_VE_Cortex-A9_MPx4) by double-clicking on your debug launch configuration. After connecting to the model and loading the UEFI firmware, DS-5 Debugger shows you something like this:
DS-5 Debugger screen views after connecting to the model and loading the UEFI firmware
The first few instructions are 32-bit ARM assembler that performs some essential CPU initialization and sets up a basic C run-time environment (e.g. stack). You can single-step through this assembler, but the more interesting C code sections come fairly soon after, compiled as 16-/32-bit Thumb.
UEFI is modular, and is structured as a set of DLLs that are loaded into memory at run time. The addresses at which the modules are loaded are stored in tables in memory. The DLLs are compiled by the ARM Compiler into ELF files containing DWARF debug data. These are converted into PECOFF as required by the UEFI Specification. UEFI loads the PECOFF DLLs at run-time, and DS-5 Debugger loads the DWARF debug data from the ELF files at debug time.
To debug UEFI at source-level, a Jython script is provided in the ArmPlatformPkg. This script (cmd_load_symbols.py) handles all the complexities of searching memory for loaded modules, and calculating offset addresses to load debug information for symbols. In the Command field, enter:
source C:\git\edk2/ArmPlatformPkg/Scripts/Ds5/cmd_load_symbols.py -f (0x08000000,0x00280000) -m (0x80000000,0x40000000) -a
This will load all the debug symbols for the platform with firmware at 0x08000000 and DRAM at 0x80000000, for all currently loaded modules. At this stage, only ArmPlatformSec.dll has been loaded, so only debug symbols for this will be loaded. The full list of symbols for this module then appears in the Functions view, including CEntryPoint. Set a breakpoint on CEntryPoint with:
and run to this breakpoint by clicking on the Continue button (or pressing F8). Program execution will stop at CEntryPoint in Sec.c:
Debugging UEFI's C entry point at source level
Now that debugging information has been loaded into DS-5 Debugger, you can now debug the code in ArmPlatformSec.dll at source level. You can run, stop, set breakpoints, view variables, view registers, view disassembly, view memory, and so on in the normal way.
Resume program execution (Continue/F8), and stop it after a few seconds (Interrupt/F9). UEFI will have started loading other modules, as shown in its serial console output:
The log of UEFI's module loading activity
This output shows which DLLs have been loaded at which addresses, and (as an alternative to using cmd_load_symbols.py) which add-symbol-file command to use to debug them at source level.
DS-5's Jython scripting capabilities provide powerful ways to execute scripts and visualise system states. For example, you can drag'n'drop cmd_load_symbols.py from the file system into the Scripts view. You can also associate parameters with the script, for example, the parameters "-f (0x08000000,0x00280000) -m (0x80000000,0x40000000) -a" mentioned earlier, by clicking on the "(...)" button on the Scripts view menu bar:
Associating parameters with a script in the Scripts view
Having done that, you can then simply double-click on the script in the Scripts view to execute it with its parameters. Debug symbols will now be loaded into DS-5 Debugger for the other loaded modules.
To see which DLLs have symbols loaded by the cmd_load_symbols.py script, add the "-v" switch to the list of parameters. You will then see the loaded DLLs listed in the Commands view:
UEFI's loaded DLLs listed in the Commands view
Resume program execution (Continue/F8), and let UEFI continue to load all the modules, and complete its count-down before attempting to launch an OS. UEFI's command-line boot loader output appears in the model's simulated display:
UEFI's boot loader
The platform's timer is ticking, generating Interrupts at regular intervals. To debug the interrupt handler, set a breakpoint with:
and run to this breakpoint (Continue/F8). Program execution will stop at IrqInterruptHandler in PL390GicDxe.c. You can now single-step through the interrupt handler, with full source-level symbols, and view Variables, Memory, Registers, etc.
Debugging UEFI's interrupt handler
To summarize, the DS-5 toolchain provides a complete UEFI development environment that enables you to fetch, build, download and run the UEFI software, and to debug its loaded DLLs at source-level, using the powerful Jython scripting features of DS-5 Debugger.
It is also possible to debug the UEFI Self Certification Test (SCT) with DS-5, using the same Jython scripts. The Self Certification Test is not easy to debug but, thanks to DS-5, this process can be made much easier.
More advanced UEFI implementations will also be coming soon, for example, UEFI for the ARMv8 architecture is under development, and DS-5 Debugger for ARMv8 will be able to debug this too, including its loaded DLLs, using its flexible Jython scripting - watch this space!