Have you, the programmer, ever really thought about how it all actually works? Am sure you have…
printf("Hello, world! value = %d\n", 41+1);
and it works. But it’s ‘C’ code – the microprocessor cannot possibly understand it; all it “understands” is a stream of binary digits – machine language. So, who or what transforms source code into this machine language?
The compiler of course! How? It just does (cheeky). So who wrote the compiler? How?
Ah. Compiler authors figure out how by reading a document provided by the microprocessor (cpu) folks – the ABI – Application Binary Interface.
People often ask “But what exactly is an ABI?”. I like the answer provided here by JesperE:
"... If you know assembly and how things work at the OS-level, you are conforming to a certain ABI. The ABI govern things like how parameters are passed, where return values are placed. For many platforms there is only one ABI to choose from, and in those cases the ABI is just "how things work". However, the ABI also govern things like how classes/objects are laid out in C++. This is necessary if you want to be able to pass object references across module boundaries or if you want to mix code compiled with different compilers. ..."
Another way to state it:
The ABI describes the underlying nuts and bolts of the mechanisms that systems software such as the compiler, linker, loader – IOW, the toolchain – needs to be aware of: data representation, function calling and return conventions, register usage conventions, stack construction, stack frame layout, argument passing – formal linkage, encoding of object files (eg. ELF), etc.
Having a minimal understanding of :
- a CPU’s ABI – which includes stuff like
- it’s procedure calling convention
- stack frame layout
- ISA (Instruction Set Architecture)
- registers and their internal usage, and,
- bare minimal assembly language for that CPU,
helps to no end when debugging a complex situation at the level of the “metal”.
With this in mind, here are a few links to various CPU ABI documents, and other related tutorials:
- x86 ABI
- ARM-32 (Aarch32)
- ARM-64 (Aarch64)
- MIPS ABI
However, especially for folks new to it, reading the ABI docs can be quite a daunting task! Below, I hope to provide some simplifications which help one gain the essentials without getting completely lost in details (that probably do not matter).
Often, when debugging, one finds that the issue lies with how exactly a function is being called – we need to examine the function parameters, locals, return value. This can even be done when all we have is a binary dump – like the well known core file (see man 5 core for details).
Intel x86 – the IA-32
On the IA-32, the stack is used for function calling, parameter passing, locals.
Stack Frame Layout on IA-32
[... <-- Bottom; higher addresses. PARAMS ...] RET addr [SFP] <-- SFP = pointer to previous stack frame [EBP] [optional] [... LOCALS ...] <-- ESP: Top of stack; in effect, lowest stack address
Intel 64-bit – the x86_64
On this processor family, the situation is far more optimized. Registers are used to pass along the first six arguments to a function; the seventh onwards is passed on the stack. The stack layout is very similar to that on IA-32.
<Original image: from Intel manuals>
Actually, the above register-set image applies to all x86 processors – it’s an overlay model:
- the 32-bit registers are literally “half” the size and their prefix changes from R to E
- the 16-bit registers are half the size of the 32-bit and their prefix changes from E to A
- the 8-bit registers are half the size of the 16-bit and their prefix changes from A to AH, AL.
The first six arguments are passed in the following registers as follows:
RDI, RSI, RDX, RCX, R8, R9
(By the way, looking up the registers is easy from within GDB: just use it’s info registers command).
An example from this excellent blog “Stack frame layout on x86-64” will help illustrate:
On the x86_64, call a function that receives 8 parameters – ‘a, b, c, d, e, f, g, h’. The situation looks like this now:
What is this “red zone” thing above? From the ABI doc:
The 128-byte area beyond the location pointed to by %rsp is considered to be reserved and shall not be modified by signal or interrupt handlers. Therefore, functions may use this area for temporary data that is not needed across function calls. In particular, leaf functions may use this area for their entire stack frame, rather than adjusting the stack pointer in the prologue and epilogue. This area is known as the red zone.
Basically it’s an optimization for the compiler folks: when a ‘leaf’ function is called (one that does not invoke any other functions), the compiler will generate code to use the 128 byte area as ‘scratch’ for the locals. This way we save two machine instructions to lower and raise the stack on function prologue (entry) and epilogue (return).
<Credits: some pics shown below are from here : ‘ARM University Program’, YouTube. Please see it for details>.
The Aarch32 processor family has seven modes of operation: of these, six of them are privileged and only one – ‘User’ – is the non-privileged mode, in which user application processes run.
When a process or thread makes a system call, the compiler has the code issue the SWI machine instruction which puts the CPU into Supervisor (SVC) mode.
The Aarch32 Register Set:
Register usage conventions are mentioned below.
Function Calling on the ARM-32
The Aarch32 ABI reveals that it’s registers are used as follows:
|R0||a1||Argument registers, passing values, don’t need to be preserved,
results are usually returned in R0
|R4||v1||Variable registers, used internally by functions, must be preserved if used. Essentially, r4 to r9 hold local variables as register variables.
(Also, in case of the SWI machine instruction (syscall), r7 holds the syscall #).
|R10||sl||Stack Limit / stack chunk handle|
|R11||fp||Frame Pointer, contains zero, or points to stack backtrace structure|
|R12||ip||Procedure entry temporary workspace|
|R13||sp||Stack Pointer, fully descending stack, points to lowest free word|
|R14||lr||Link Register, return address at function exit|
(APCS = ARM Procedure Calling Standard)
When a function is called on the ARM-32 family, the compiler generates assembly code such that the first four integer or pointer arguments are placed in the registers r0, r1, r2 and r3. If the function is to receive more than four parameters, the fifth one onwards goes onto the stack. If enabled, the frame pointer (very useful for accurate stack unwinding/backtracing) is in r11. The last three registers are always used for special purposes:
- r13: stack pointer register
- r14: link register; in effect, return (text/code) address
- r15: the program counter (the PC)
The PSR – Processor State Register – holds the system ‘state’; it is constructed like this:
Hope this helps!