Arm Compiler for Embedded (AC6) is not 100% compatible with the Arm GNU Toolchain (GCC). AC6 uses a proprietary linker and requires an Arm scatter-loading file instead of a GNU-style linker script to arrange code and data in memory. The next-generation Arm Toolchain for Embedded (ATfE) is 100% open source and uses the LLVM ‘lld’ linker, which accepts GNU-style linker scripts. Does this mean that any embedded project that currently builds with GCC, will build with ATfE without modification?
No, ATfE and GCC are not fully compatible. Some modifications are required to build a GCC project with ATfE. These changes stem mainly from differences between the C libraries: Picolibc for ATfE and Newlib for the GCC toolchain. In this blog post, I look at the startup_Armv8-Ax1 example project that is provided as part of the Arm Development Studio suite. It is a small project with a linker script and boot code that already builds with GCC, so it is an ideal project for experimentation.
The changes I have set out here are the minimum needed to get the existing GCC version of the project building and running with ATfE. Migrating other projects might take additional work, but the example here does at least show you the kind of changes that are necessary. There is enough information in this blog post to get you started on migrating other projects. There is a lot more information available in the Arm Toolchain for Embedded (ATfE) user and migration guides, and in the Picolibc documentation.
Diffs of the various files are attached to the end of this blog post for reference. I worked with the semihosted option (all input and output is routed through the debugger) rather than the UART option.
Before we get started on the migration proper, we should talk a little about startup code. Startup code handles all the early initialization, like filling the RO and ZI memory regions and setting up the stack and heap. If there is no startup code in the toolchain’s libraries, or the toolchain startup code is not suitable for your use, you need to write your own.
Picolibc's startup code serves as a basic starting point. Arm has created additional startup code variants, suffixed with “fvp”, to make it easier to test on FVPs (Fixed Virtual Platforms). While this startup code was not originally intended to be used in production code, it does enough for many use-cases. If it works for your project, that is great, although please keep in mind it could change in future ATfE releases. Otherwise, you must write custom startup code. The Picolibc startup code does enough for the example project here, which is why you will see the “fvp” variants being used.
The changes needed in the makefile include handling the target triple, and pulling in some suitable startup code.
GCC provides separate compilers for 32-bit and 64-bit code, embedding the target triple in the driver name. For ATfE there is only one compiler for 32-bit and 64-bit code, and the target triple is passed as an option. So we need to rename the driver from aarch64-none-elf-gcc to clang, but add --target=aarch64-none-elf to the options for both the compiler and the linker.
GCC uses the --specs option to control the default libraries, but with ATfE that is not needed and we can remove both the --specs lines from the makefile. However, we do need to control any startup code that we need to take from the compiler crt0. Exactly how we do this changes depending on which of the beta releases is being used and where the project is targeted. The Arm Toolchain for Embedded 20.0.0 beta release offers four options:
Option
Library flags
Use-case
1
-lcrt0
EL1 startup, useable on a QEMU model, without semihosting functionality
2
-lcrt0-semihost -lsemihost
EL1 startup, useable on a QEMU model, with added semihosting functionality
3
-lcrt0-fvp
EL3 startup, usable on FastModels FVP, without semihosting functionality
4
-lcrt0-semihost-fvp -lsemihost
EL3 startup, usable on FastModels FVP, with added semihosting functionality
Since this project targets an Arm Fast Models Fixed Virtual Platform (FVP), the ATfE 20.0.0 beta release supports -lcrt0-fvp for the non-semihosted option and -crt0-semihost-fvp -lsemihost for the semihosted option. Both require the -nostartfiles prefix.
The *-fvp startup variants were added for the 20.0.0 beta release. If using the 19.1.1 beta release, -crt0 or -crt0-semihost -lsemihost would be the options to use for this project.
Additional linker modifications can be applied in the makefile:
These are the only required makefile modifications. This gets us to a point where all the source files assemble or compile correctly, but the link fails because of missing symbols.
The easiest missing symbols to start with are __bss_start and __bss_size. The original GCC linker script contained a .bss section like this:
.bss : { . = ALIGN(4); __bss_start__ = .; *(.bss*) *(COMMON) . = ALIGN(4); __bss_end__ = .; }
The required modification is straightforward. The C library used in ATfE (Picolibc) uses different symbols than the C library used in the GCC toolchain, so we need to change the .bss section:
.bss : { . = ALIGN(4); __bss_start = .; *(.bss*) *(COMMON) . = ALIGN(4); __bss_end = .; __bss_size = __bss_end - __bss_start; }
This resolves the missing symbols at link time but causes a compile-time error due to references to the original symbols in the source code. There is two ways to fix this. Define both sets of symbols in the linker script (useful if you need to build the project with both ATfE and GCC). Or make corresponding changes to the source (avoids some duplication if you only need to use ATfE).
The next set of missing symbols (__data_source __data_size, __tls_base, __arm64_tls_tbc_offset), certainly in this project, are not so clear-cut. The original GCC linker script lacks .data, .tdata or .tbss sections. For this project with GCC, they were not strictly necessary and the build worked without them. However ATfE has more strict requirements, in particular Picolibc uses thread local storage for its implementation of certain routines even if the project itself does not need multithreading. So we need to add .data, .tdata and .tbss sections, they end up looking like this:
.data : { __data_start = . ; *(.data .data.* .gnu.linkonce.d.*) } __data_source = LOADADDR(.data); .tdata : { . = ALIGN(8); __tls_base = .; *(.tdata .tdata.*) __data_end = .; } __data_size = __data_end - __data_start; .tbss (NOLOAD) : { __bss_start = .; *(.tbss .tbss.*) *(.tcommon) . = ALIGN(8); __tls_end = .; __tbss_end = .; } __tls_align = MAX(ALIGNOF(.tdata), ALIGNOF(.tbss)); __arm64_tls_tcb_offset = MAX(16, __tls_align); .bss : { . = ALIGN(8); *(.bss .bss.*) *(COMMON) . = ALIGN(8); __bss_end = .; } __bss_size = __bss_end - __bss_start ;
These sections are standard and commonly used in embedded projects. Note that the __bss_start symbol has jumped to the start of the .tbss section.
The final missing symbol is initialise_monitor_handles. For the semihosted case, that was provided by the GCC crt0 and calls SYS_OPEN. It is not provided by ATfE, which does things a slightly different way: to the extent that we can just remove the call from startup.S. However, an implementation is still needed by the non-semihosted case, it is given in retarget.c and initializes the UART. So we have to make a choice: make the call in startup.S conditional, or add a stub definition to retarget.c for the semihosted case.
With those changes made, the project builds! So we can load it into an FVP (Fixed Virtual Platform) using Arm Development Studio (ArmDS) and test it.
At first, all looks good. The code loads into the FVP, and ArmDS sets the program counter (PC) to the entry point, allowing control. We can step through the code, success!
However this does not last long. Somewhere in the initialization code, something nasty happens and we end up stuck in a branch-to-self. Something is wrong.
The culprit is a missing Global Offset Table (GOT) entry in the linker script. Adding a GOT section is necessary with ATfE, and we can add it just before the .tdata section:
.got : { *(.got.plt) *(.got) }
With this change, the project is successfully migrated from GCC to ATfE and executes correctly.
The next-generation Arm Toolchain for Embedded (ATfE) is more compatible with the Arm GNU Toolchain (GCC) than AC6 is. In particular, it accepts GNU-style linker scripts instead of Arm scatter-loading files. This means it is much easier to port a project from GCC to ATfE than it is to migrate from AC6, and it is easier for a project to maintain compatibility with both GCC and ATfE than it was with AC6.
However, some modifications are required, and a GCC project will not build automatically with ATfE. The key changes needed to migrate this example project from GCC to ATfE are:
The migration process followed these steps:
We should note that the startup_Armv8-Ax1 project used for this blog post was written some time ago. Compilers have advanced and there is additional changes we could make to bring the project up to date and make it more portable. But I will leave those for another blog post: this one is focused on migrating the existing project from GCC to ATfE.