Soo I'm trying to understand how Assembler works with stack frames etc. I did some exercises and disassembled some C-Code with GDB using Intel notation. The task now is to find out how the variable 'r' is calculated with transfer of parameters between the 'main' and 'f' functions. I just started learning and kinda got lost on what this example is actually doing. Any ideas or some tips on where to get started?
It's a recursive programm working with faculty:
#include <stdio.h>
unsigned int f(unsigned int i) {
if (i>1) {
return i * f(i-1);
} else {
return 1;
}
}
int main() {
unsigned int i=5, r=0;
r = f(i);
printf("i = %d, f(i) = %d\n", i, r);
}
The Assembler Code looks like this:
#include <stdio.h>
unsigned int f(unsigned int i) {
1149: f3 0f 1e fa endbr64
114d: 55 push rbp
114e: 48 89 e5 mov rbp,rsp
1151: 48 83 ec 10 sub rsp,0x10
1155: 89 7d fc mov DWORD PTR [rbp-0x4],edi
if (i>1) {
1158: 83 7d fc 01 cmp DWORD PTR [rbp-0x4],0x1
115c: 76 13 jbe 1171 <f 0x28>
return i * f(i-1);
115e: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]
1161: 83 e8 01 sub eax,0x1
1164: 89 c7 mov edi,eax
1166: e8 de ff ff ff call 1149 <f>
116b: 0f af 45 fc imul eax,DWORD PTR [rbp-0x4]
116f: eb 05 jmp 1176 <f 0x2d>
} else {
return 1;
1171: b8 01 00 00 00 mov eax,0x1
}
}
1176: c9 leave
1177: c3 ret
int main() {
1178: f3 0f 1e fa endbr64
117c: 55 push rbp
117d: 48 89 e5 mov rbp,rsp
1180: 48 83 ec 10 sub rsp,0x10
unsigned int i=5, r=0;
1184: c7 45 f8 05 00 00 00 mov DWORD PTR [rbp-0x8],0x5
118b: c7 45 fc 00 00 00 00 mov DWORD PTR [rbp-0x4],0x0
r = f(i);
1192: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8]
1195: 89 c7 mov edi,eax
1197: e8 ad ff ff ff call 1149 <f>
119c: 89 45 fc mov DWORD PTR [rbp-0x4],eax
CodePudding user response:
Study the calling convention for your environment. A overview of the many calling conventions for a number of architecures: https://www.dyncall.org/docs/manual/manualse11.html
The calling convention specifies:
Where parameters and return values must appear at the one single point of transfer of control of the instruction stream from the caller to the callee. For parameters being passed, that single point is after the call is made and before the first instruction of the callee (and for return values, at the point where the callee finishes and just before execution resumes in the caller).
Many conventions combine parameter passing in CPU registers with stack memory for parameters that don't fit in CPU registers. And even some that don't use CPU registers for parameters still use CPU registers for return values.
What registers a function is allowed to clobber vs. must preserve. Call-clobbered registers can be assigned new values without concern Call-preserved registers can be used but must be restored to the value they had upon entry before returning to the caller. The advantage of call-preserved registers is that since they are preserved by a call, you can use them for variables that need to survive another call.
The meaning & treatment of the stack pointer, regarding memory below and above the current pointer, and alignment requirements for stack allocation.
If the function allocates stack space in some manner, then memory parameters will appear to move farther away from the top of the stack (they don't actually move, of course, but become larger offsets from the current stack or frame pointer). Compilers know this and adjust their access to stack memory accordingly.
Some compilers set up frame pointers to refer to stack memory. A frame pointer is a copy of the stack pointer made at some point in the prologue. Frame pointers are not always necessary but facilitate exception handling and stack unwinding, as well as dynamic stack allocation.