EABI - Why any embedded software engineer should know about it.
This post is to introduce the EABI to who has never read about it.
ABI and EABI- Don’t be confused with API.
As a software engineer/developer/architect or whoever works in software engineering, you probably know API standing for Application Programming Interface. API simply means the way how you interact with a software stack/library using programming [language], some of these examples are Android API, The Linux Kernel API,…
In lower layer software, there is thing called Application Binary Interface (now refered as ABI), that is the specification defines how a program as a binay objects interact with others binary objects. For instance, when you write a hello word program in C, you use the function printf
which is implemented in the a standard library , which is normally delivered as a compiled library suchas libc.a
. When our software calls printf
the ABI defines what to look at the printf
function in the library, and where the arugments of the printf
is passed.
Conventional software engineer (just to to differentiate with embedded software engineer) normally care just a little bit about the ABI, as these things is covered by the compiler we are using, or by the underlying software interface the OS vendors provides. For the embedded software engineer, we are more caring about the EABI, it is the subset of ABI which normally defined by the CPU architecture’s author. For instance, ARM company publishes the EABI specification for their ARM CPU families. We normally with the lowest layer in the software stack with is not unsual to doing some assembly debug and alike, thus understanding the EABI is essential. Let’s digging abit into details.
Why we should know about EABI?
To take full controls of our program
You know, some compiler may provide us some non-standardized keyword to define an interrupt service routine (ISR), however, it is not always the case. From my personal experience, there are C compilers that strictly follow the C standard that they don’t differentiate between conventional function and ISR whatsover, thus, we need to dig in the MCU’s datasheet to finding the information about how interrupt controller in the MCU work. However, MCU datasheet may give you what registers you need to initialize the interrupt or define the interrupt source, it does not fully give you full picture how how interrupt is handled in the MCU as this is normally defined in the separated EABI specification.
For interrupt, the EABI defines where interrupt base register is stored, how the stack and context is involved during the interrupt execution. This is normally our job to specify these information in our source code (often the case the startup code and linker script) such as where is the base address for the interrupt vector table, which function belongs to which vector,… Futhermore, we also need to understand which context is automatically handled by the CPU itself and which one need to handle by us, for example, in an mordern MCU I am working with, the interrupts handling of the CPU is very advance that will automatically store lots of general purpose register upon the interrupt trigger and automatically restore it when exit the interrupt; by understand this fact, we will have some idea to implement the interrupt efficently without push/pop more temporary data into the program stack.
To debug (!?) and to write the higher performance code.
You may be familiar with debugging using jtag hardware debugger. Sometime, we want to view the parameters, temporary variables,… and unfortunately, it is not always works, sometime we just cannot see the parameters ond temp variables because it is optimized out by the compiler. That likely the case when we compile our code with space and speed optimization, in this case, the parameters and temp variable may be located in the general purpose registers. In this case, if we understand the EABI, we would likely know that which parameter goes to Register X, which parameters goes to register Y. For instance, in the 32-bit CPU made by vendor A, it doesn’t differentiate between the address and the data variable, the parameters just being passed by using limited general purpose registers. But in the 32-bit CPU made by vendor B, it use separated bank of register for address and data (let’s call it A register and D register), thus, the compiler will optimize in the way that the pointer is located in A register and data is located in the D register). By understanding this, when these parameters doesn’t appear in your stack frame, you know it is in the general purpose register, and you can just observing the value general purpose register. This is also the case for return value as in most case, the return value is storted in a implicit register.
By understanding the parameters passing defined by EABI, we will know the optimal number of parameters to pass to a function before it using stack memory. If the EABI said it use D1, D2, D3 and D4 for scalar parameters, it is obviously the best idea to structure the propram to use at most 4 scalar parameters. For this, one of the interesting case study I also want to mention is where we are intended to passing a struct to a function. Normally, if the struct size is larger than one register of 32 bit, it will be stored in stack, then we can doing the trick by writing the function that parse the member of struct directly to the function. The following code snipet illustrate what I have just described.
typedef struct
{
uint32_t data1;
uint32_t data2;
uint32_t data3;
}example_struct_t;
example_struct_t example_struct = {1u, 2u, 3u};
/* The following function probably store data in to stack, because size of struct is 3 words (12 bytes) */
uint32_t foo_process_struct_origin(example_struct_t data)
{
uint32_t ret;
/* do something */
return ret
}
/* The above function can be rewritten with the following function, but use 3 params */
uint32_t foo_process_struct_optimize(uint32_t data1,uint32_t data2,uint32_t data3)
{
uint32_t ret;
/* do something */
return ret
}
void bar(void)
{
uint32_t result;
result = foo_process_struct_origin( example_struct);
result 2 = foo_process_struct_optimize( example_struct.data1,
example_struct.data2,
example_struct.data3);
}
To setup the tools chain correctly.
If we ever doing cross -compiling for some embedded linux, you would be familiar with the long compiler name such as arm-linux-gnueabihf, the arm and linux parts are obviously define the CPU and the OS the compiler is generated for (I failed to mentioned that different OS can also have different ABI), the gnueabihf mean the compiler is intentional for GNU-eabi with the hardfloat support. Understanding the eabi is important that we won’t mix different library built for different incompatible (E)ABI, such as we cannot use the standard C library build for x86 for Windows to link with the printf function which is compiled for x86 on Linux, for the following reason:
-
The executable(ELF file or EXE file) layout is obviously different betwene EABI.
-
The Parameters passing can be different, the procedure call may be different.
-
The ways diffentent compilers compile our code are different.
Thus, understanding EABI can help you to chose the compatible toolchain for your project.
Danger Notice: For serious project, it is highly recommended not to mix the different compiler in the project (for instance different static library is build with different compiler version). If the compiler mixing is inevitable, it shall be documented and highlighted as it may cause the serious side effect.