Home > Software engineering >  Naked function and stacking procedure
Naked function and stacking procedure

Time:04-26

I searched the Internet, but I didn't quite get it. It's about stacking. When a c function called another a c function, each function procedure has own responsibility for protect last data in registers and stacks. There is calee and caller stacking procedure and caller saves r0-r3, lr, etc. and callee saves r4-r11 if it use them. Also before when callee function works, there is some additional procedure named as epilogue and prologue (I am working on stm32f4).

I do not fully understand why the prologue and epilogue are needed. I would be very happy if there is a resource you can offer to learn it.

But my main question is about naked function and stacking procedure. When we use the Naked attribute, the epilogue and prologue procedure are not performed. But if I use the naked function, will the caller and callee procedure be applied or i have to do this in my application code ? I'm really confused about this point. I would very appreciate it if you could answer these questions. Thank you.

CodePudding user response:

On ARM there isn't really much of a "prologue" and "epilogue" as there is on x86 and other processors.

At the start of the function there is usually just as single push instruction, which includes some set of registers including the link register:

push {r4-r6,lr}

And at the end of the function there is a single pop instruction, including the program counter:

pop {r4-r6,pc}

This both restores the saved registers and returns to the caller.

Saving registers is only performed if the compiler decides it needs them. In particular it only needs the link register the function calls other functions. In many small functions, there is no "prologue" at all, and at the end there is just a single instruction to return:

bx lr

The naked function attribute tells the compiler not to generate these instructions. The main purpose of this as far as I can tell on ARM is if you are going to write the entire function in inline assembly and manage all the registers yourself, but you still want to put the function in a C or C file. In my opinion this is a very untidy thing to do. If the function is entirely in assembly then it should be in an assembly file. Inline assembly should be used in C or C functions where the compiler manages the register allocation, and you just need access to particular instructions that you cannot get from C or C .

Other details:

I don't believe the naked attribute changes anything in the code that calls the function (but I do not know about every possible architecture).

Note that on ARM when starting a function that will call other functions you must push an even number of registers to keep the stack aligned.

Note that when starting a variadic function (such as printf) you must start with:

push {r0-r3}

This ensures that the first few function arguments are contiguously in memory on the stack along with the later ones that did not fit in these registers.

On processors other than Cortex-M a much more complicated prologue and epilogue is required interrupt handler functions. This usually requires adding an extra attribute (eg: attribute((interrupt(N)))). On Cortex-M such as your STM32 the hardware interrupt controller sets up the registers exactly as a software would call a function, so nothing special is required.

CodePudding user response:

Prologue and Epilogue are not a requirement for the function. The compiler may insert them for optimization, readability, debugging (see shadow stack space on x64).

Your naked function does not need a prologue and epilogue it can have one if you'd like it ; however, the calling conventions which is what you're referring to regarding volatile/non volatile registers and argument passing applies regardless.

If you were to for example have two or more naked functions or functions written strictly in assembly where the functions call each other then it's still advised to use the standard calling convention but you don't have to. You can get away with avoiding it. Have a look at these two functions written in assembly:

int addtwoints(int x, int y);
int calladdtwoints(int x, int y);

Our C code is going to call calladdtwoints which is an assembly function which we write. Our calladdtwoints is going to call addtwoints another assembly function which we write.

Now in the x86_64 calling convention, rax holds the return value. Rdi is the first argument , rsi is the second argument.

Here's our C code:

#include <stdio.h>
int addtwoints(int x, int y);
int calladdtwoints(int x, int y);
int main(){
    int n = calladdtwoints(10,20);
    printf("%d\n",n);
}

When the compiler calls calladdtwoints it moves 10 into rdi and 20 into rsi as expected

->  0x100003f88 < 8>:  mov    rdi, 0xa
    0x100003f8d < 13>: mov    rsi, 0x14
    0x100003f92 < 18>: call   0x100003fa5

Our assembly function calladdtwoints knows the first argument is in rdi and the second is in rsi, our other assembly function addtwoints knows the same thing. Because we know this we can actually avoid using the calling convention here. Here is calladdtwoints

_calladdtwoints:
    mov rcx,rdi
    mov rax,rsi
    call _addtwoints
    mov rax,rdi
    ret

So it takes the first argument rdi, moves it into rcx and second argument in rax. After the call is made, it's expecting the result to be in rdi so it moves that into rax because the C function calling it (the caller) is expecting the result in rax.

Now our addtwoints function if it were following standard calling convention it HAS to check registers rdi and rsi for the arguments. Because we're calling it from our own assembly function, we're going to instead check rcx, and rax and move the results into rdi.

_addtwoints:
    mov rdi,rcx
    add rdi,rax
    ret

Now if we execute our code, the result which gets printed is "30" which is exactly what we want, 10 20 is 30 right? Sure.

But what happens if we now instead of calling calladdtwoints from main, we call addtwoints directly

#include <stdio.h>
int addtwoints(int x, int y);
int calladdtwoints(int x, int y);
int main(){
    int n = addtwoints(10,20);
    printf("%d\n",n);
}

Now the code breaks and the result which gets printed is undefined. Why? Because our addtwoints doesn't adhere to the calling convention which our compiler is EXPECTING. We got around it by using calladdtwoints to create some random register usage but because we can doesn't mean we should.

So ith all that said, YES you should ABSOLUTELY adhere to the calling convention of the compiler. If you don't, you might get C functions calling your naked functions with undefined results.

  • Related