IT engineering student here. We're asked to play around with context switching and one particular assignment had us implement a rather crude try/throw system. Here's the code we've been writing:
struct ctx_s {
int esp;
int ebp;
};
struct ctx_s * pctx;
typedef int (func_t)(int); /* a function that returns an int from an int */
int try(func_t *f, int arg)
{
/* saving context by storing values of %esp and %ebp */
asm ("movl %%esp, %0"
: "=r"((*pctx).esp)
:
);
asm ("movl %%ebp, %0"
: "=r"((*pctx).ebp)
:
);
/* calling the function sent to try(), returning whatever it returns */
return f(arg);
}
int throw(int r)
{
printf("MAGIC PRINT\n");
static int my_return = 0;
/* ^ to avoid "an element from initialisation is not a constant" */
my_return = r;
/* restituting context saved in try() */
asm ("movl %0, %%esp"
:
: "r"((*pctx).esp)
);
asm ("movl %0, %%ebp"
:
: "r"((*pctx).ebp)
);
/* this return will go back to main() since we've restored try()'s context
so the return address is whatever called try... */
/* my_return is static (=> stored in the heap) so it's not been corrupted,
unlike r which is now the second parameter received from try()'s context,
and who knows what that might be */
return my_return;
}
pctx is a global pointer to a simple structure holding two int's, f is a function that calls throw() sending some return code #define'd to 42, and main() essentially allocates pctx, does result=try(f, 0) and prints result. We expect result to be 42.
Now, you may have spotted the MAGIC PRINT in throw(). It's here for reasons not totally clear ; basically, most (not all) students were segfaulting inside throw() ; calling printf() inside this function made the program work seemingly correctly, and the teachers think any system call would have worked as well.
Since I didn't really get their explanations, I tried comparing the assembly codes generated with gcc -S for both versions (with and without printf()), but I couldn't make much of it. Setting a breakpoint at throw()'s opening brace (line 33) and disassembling with gdb gave me this:
Without printf():
Breakpoint 1, throw (r=42) at main4.c:38
(gdb) disass
Dump of assembler code for function throw:
0x0804845a <throw+0>: push %ebp
0x0804845b <throw+1>: mov %esp,%ebp
0x0804845d <throw+3>: mov 0x8(%ebp),%eax
0x08048460 <throw+6>: mov %eax,0x8049720
0x08048465 <throw+11>: mov 0x8049724,%eax
0x0804846a <throw+16>: mov (%eax),%eax
0x0804846c <throw+18>: mov %eax,%esp
0x0804846e <throw+20>: mov 0x8049724,%eax
0x08048473 <throw+25>: mov 0x4(%eax),%eax
0x08048476 <throw+28>: mov %eax,%ebp
0x08048478 <throw+30>: mov 0x8049720,%eax
0x0804847d <throw+35>: pop %ebp
0x0804847e <throw+36>: ret
End of assembler dump.
(gdb) c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0xb7e846c0 in ?? ()
With printf():
Breakpoint 1, throw (r=42) at main4.c:34
(gdb) disassemble
Dump of assembler code for function throw:
0x0804845a <throw+0>: push %ebp
0x0804845b <throw+1>: mov %esp,%ebp
0x0804845d <throw+3>: sub $0x18,%esp
0x08048460 <throw+6>: movl $0x80485f0,(%esp)
0x08048467 <throw+13>: call 0x8048364 <puts@plt>
0x0804846c <throw+18>: mov 0x8(%ebp),%eax
0x0804846f <throw+21>: mov %eax,0x804973c
0x08048474 <throw+26>: mov 0x8049740,%eax
0x08048479 <throw+31>: mov (%eax),%eax
0x0804847b <throw+33>: mov %eax,%esp
0x0804847d <throw+35>: mov 0x8049740,%eax
0x08048482 <throw+40>: mov 0x4(%eax),%eax
0x08048485 <throw+43>: mov %eax,%ebp
0x08048487 <throw+45>: mov 0x804973c,%eax
0x0804848c <throw+50>: leave
0x0804848d <throw+51>: ret
End of assembler dump.
(gdb) c
Continuing.
MAGIC PRINT
result = 42
Program exited normally.
I don't really know what to make of that. Obviously things are happening differently but I'm finding it quite hard to understand what's going on in either case... So my question is, essentially: how does calling printf make throw not segfault?
Ok, this is a bit loose of an analysis since I can't see the try part, but judging from standard calling conventions, your method containing the try will save
%esp
to%ebp
, decrease%esp
to make space for local variables and run your "try" code that saves%esp
and%ebp
.Normally, when a function exits, it reverts those changes by using
leave
before return. Leave will restore%ebp
into%esp
, pop%ebp
and do its return. This makes sure that%esp
is restored to its point before space for the local variables was reserved.The problem in the version without
printf
is that it doesn't useleave
which pops%ebp
without first restoring its content into%esp
. Theret
instruction will pop a local variable and return to that. Not the very best outcome.My suspicion is that since your function has no local variables, the compiler sees no reason to restore
%esp
from%ebp
. Sinceprintf
reserves space on the stack, the compiler knows in that version that%esp
must be restored before returning.If you want to test the theory, just compile to assembler, replace;
with a leave instruction and assemble the result. It should work just as well.
Alternately, I suspect you could indicate to gcc in your asm instructions that
%esp
has been clobbered, and thereby make it generate a leave instead.EDIT: Apparently marking
%esp
as clobbered is essentially a NOOP in gcc :-/