Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Understanding C pointers using GDB by examining core and call stack

I have been professionally coding in C for a while but am still stumped by some pointer related questions. I would really appreciate SO community's help in understanding below problem.

Following code crashed and generated core file.

void func1()    // Frame 1 in GDB stack trace.  
{ 
    UTYPE  *ptr;  // pointer to user defined type  
    ...

    // data is of type UTYPE and has valid contents.
    // lets say its address is 0x100 
    ptr = &data;     --- (1)  
    ...

    func2(ptr);      --- (2) 
    ...
} 

void func2(UTYPE *inp)    // Frame 0 in GDB stack trace.  
{
    if(!inp)         --- (3) 
        return; 
    ...

    // another_ptr is of UTYPE * which is a NULL.  
    inp = another_ptr;   ---- (4)  

    /* Did not check for NULL and dereference inp and CRASH */    ---- (5) 
} 

Simplified backtrace from GDB:

Frame 0: 
    func2(inp = 0x0) 
    // crash at line (5) due to dereference 

Frame 1: 
    func1: func2(0x0)  
    // `ptr` at line (2) is 0x0. Why is this so? 

Why is ptr shown as 0x0 (NULL) in Frame 1?

When func2() is called, its call stack looks as follows:

  | //local vars  | 
  |               | 
  | another_ptr = |
  |      NULL     |
  +---------------+
  | return addr   |
  +---------------+
  | input args    |
  | copy of ptr   |
  |   contents    |
  |     0x100     |

For func1(), its call stack should look like:

  |               | 
  | ptr = 0x100   |
  |               |
  +---------------+
  | return addr   |
  +---------------+
  | input args    |
  |  none in this |
  |  func         |

When inp becomes NULL in func2() in line (4), how is it reflected in func1()?

like image 896
Bhaskar Avatar asked Oct 01 '22 17:10

Bhaskar


2 Answers

When a function is called in C, the parameters are copied into registers or pushed onto the stack. The called function can reuse those registers and stack locations for any purpose. Often, but not always, a parameter is kept in the same register or stack location for the entire lifetime of the function call.

On a 32-bit system, the first argument to a function - in your case, inp - is often located on the stack, 12 bytes away from the location that the stack frame's base pointer points to. See stackoverflow.com: what exactly is program stack's growth direction.

When gdb does a backtrace, the only guidance it has from the compiler is something like "the first argument to func2 is named inp and is a 4-byte value of type *UTYPE located at a 12-byte offset from the %ebp register".

If, somewhere in func2, you alter inp, as you do at location (4), then any backtrace from that point on may very well show the altered value of inp, in your case, 0. The value that inp had when func2 was entered is lost forever, unless the compiler has been clever enough to include guidance like "the first argument to func2 is named inp and is a 4-byte value of type *UTYPE and its value upon entry to func2 can be found by unwinding the stack to the previous frame and looking at the value of ptr, which is located at a -4-byte offset from the %ebp register." The newer versions of the DWARF debugging format can specify things like this, I believe.

I cannot explain why your gdb's backtrace shows ptr in func1's frame as having the value 0. Setting inp to NULL should have no effect on ptr's value nor on gdb's ability to show ptr's value.

like image 173
Mark Plotnick Avatar answered Oct 05 '22 10:10

Mark Plotnick


GDB constructs the call stack from the stack, and since inp, which is a parameter for func2, is 0, GDB assumes that the passed parameter is 0, and so it says that func2 was called with 0.

Stack at crash time is something like that:

another_ptr = 0  ( func2 local variable )
return address to func1                   
inp = 0          ( func2 parameter )
ptr              ( func1 local variable )
like image 40
MByD Avatar answered Oct 05 '22 11:10

MByD