I am having an issue with some inline assembly. I am writing a compiler, and it is compiling to assembly, and for portability i made it add the main function in C and just use inline assembly. Though even the simplest inline assembly is giving me a segfault. Thanks for your help
int main(int argc, char** argv) {
__asm__(
"push $1\n"
);
return 0;
}
CodePudding user response:
TLDR at bottom. Note: everything here is assuming x86_64
.
The issue here is that compilers will effectively never use push
or pop
in a function body (except for prologues/epilogues).
Consider this example.
When the function begins, room is made on the stack in the prologue with:
push rbp
mov rbp, rsp
sub rsp, 32
This creates 32 bytes of room for main
. Then notice how throughout the function, instead of pushing items to the stack, they are mov
'd to the stack through offsets from rbp
:
mov DWORD PTR [rbp-20], edi
mov QWORD PTR [rbp-32], rsi
mov DWORD PTR [rbp-4], 2
mov DWORD PTR [rbp-8], 5
The reason for this is it allows for variables to be stored anywhere at anytime, and loaded from anywhere at anytime without requiring a huge amount of push
/pop
s.
Consider the case where variables are stored using push
and pop
. Say a variable is stored early on in the function, let's call this foo
. 8 variables on the stack later, you need foo
, how should you access it?
Well, you can pop everything until foo
, and then push everything back, but that's costly.
It also doesn't work when you have conditional statements. Say a variable is only ever stored if foo
is some certain value. Now you have a conditional where the stack pointer could be at one of two locations after it!
For this reason, compilers always prefer to use rbp - N
to store variables, as at any point in the function, the variable will still live at rbp - N
.
NB: On different ABIs (such as i386 system V), parameters to arguments may be passed on the stack, but this isn't too much of an issue, as ABIs will generally specify how this should be handled. Again, using i386 system V as an example, the calling convention for a function will go something like:
push edi ; 2nd argument to the function.
push eax ; 1st argument to the function.
call my_func
; here, it can be assumed that the stack has been corrected
So, why does push
actually cause an issue?
Well, I'll add a small asm
snippet to the code
At the end of the function, we now have the following:
push 64
mov eax, 0
leave
ret
There's 2 things that fail now due to pushing to the stack.
The first is the leave
instruction (see this thread)
The leave instruction will attempt to pop
the value of rbp
that was stored at the beginning of the function (notice the only push
that the compiler generates is at the start: push rbp
).
This is so that the stack frame of the caller is preserved following main
. By pushing to the stack, in our case rbp
is now going to be set to 64
, since the last value pushed is 64
. When the callee of main
resumes it's execution, and tries to access a value at say, rbp - 8
, a crash will occur, as rbp - 8
is 0x38
in hex, which is an invalid address.
But that assumes the callee even get's execution back!
After rbp
has it's value restored with the invalid value, the next thing on the stack will be the original value of rbp
.
The ret
instruction will pop
a value from the stack, and return to that address...
Notice how this might be slightly problematic?
The CPU is going to try and jump to the value of rbp
stored at the start of the function!
On nearly every modern program, the stack is a "no execute" zone (see here), and attempting to execute code from there will immediately cause a crash.
So, TLDR: Pushing to the stack violates assumptions made by the compiler, most importantly about the return address of the function. This violation causes program execution to end up on the stack (generally), which will cause a crash