Calling printf prevents segfaulting

491 views Asked by At

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?

2

There are 2 answers

1
Joachim Isaksson On BEST ANSWER

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 leavebefore 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 use leave which pops %ebp without first restoring its content into %esp. The ret 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. Since printf 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;

0x0804847d <throw+35>:  pop    %ebp

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 :-/

0
Bo Persson On

You are "restoring" ESP to a value saved in another function. Probably not a useful value here.

The difference with the "magic" code is that it makes the compiler save and restore the stack frame in the throw function.

A leave instruction at the end is equivalent to

mov    %ebp, %esp
pop    %ebp

which just might get the stack pointer back to what it was at the function entry.