__libc_start_main
is called by the entry point code (usually in a file called crt0.S
or similar) and that code usually sets up the initial EBP
value (usually to 0, to denote end of the call stack for the debuggers). Here's a sample entry point code from a random ELF binary:
_start:
xor ebp, ebp
pop esi
mov ecx, esp
and esp, 0FFFFFFF0h
push eax
push esp ; stack_end
push edx ; rtld_fini
push offset __libc_csu_fini ; fini
push offset __libc_csu_init ; init
push ecx ; ubp_av
push esi ; argc
push offset main ; main
call ___libc_start_main
hlt
While __libc_start_main
itself could initialize EBP
to 0, this would make it appear as if it's the first code in the binary when looking at the call stack, which is not correct (the execution begins at _start
, the entry point).
Besides, __libc_start_main
can be (and often is) written in C instead of assembly, and it's easier for the compiler to generate the standard prolog code for it instead of making some kind of special case exception.
As for overwriting the value, you don't get a crash because:
ebp
is not actually used by the caller of ___libc_start_main
(the entry point code)
___libc_start_main
does not actually return, since it calls exit()
with the return value of main()
to directly exit to the OS, so your corrupted ebp
does not matter
In the unlikely event that ___libc_start_main
does return (e.g. due to a bug in libc), the entry point code will execute the hlt
instruction which is privileged so the process will be killed by the OS.