Gdb Shows Incorrect Arguments of Functions for Stack Frames

GDB shows incorrect arguments of functions for stack frames

Looking at the disassembly, gdb is stopped at the first instruction of function foo, before the function prologue (which sets up the stack and frame pointers) has been run:

(gdb) step
9 foo(a);
(gdb) step
foo (v=21845) at t.c:3
3 void foo(int v){
(gdb) disas
Dump of assembler code for function foo:
=> 0x0000555555555149 <+0>: endbr64
0x000055555555514d <+4>: push %rbp
0x000055555555514e <+5>: mov %rsp,%rbp
0x0000555555555151 <+8>: sub $0x10,%rsp
0x0000555555555155 <+12>: mov %edi,-0x4(%rbp)
0x0000555555555158 <+15>: mov -0x4(%rbp),%eax
0x000055555555515b <+18>: mov %eax,%esi
0x000055555555515d <+20>: lea 0xea0(%rip),%rdi # 0x555555556004
0x0000555555555164 <+27>: mov $0x0,%eax
0x0000555555555169 <+32>: callq 0x555555555050 <printf@plt>
0x000055555555516e <+37>: nop
0x000055555555516f <+38>: leaveq
0x0000555555555170 <+39>: retq
End of assembler dump.

Gdb's step command normally steps over a function's prologue, that is, it stops the program after the prologue has run. Here, gdb apparently doesn't recognize the instruction endbr64 as being part of any known prologue.

We can see that &v is beyond the bounds of the current stack frame:

(gdb) p &v
$1 = (int *) 0x7fffffffe3fc
(gdb) i r rbp rsp
rbp 0x7fffffffe420
rsp 0x7fffffffe408

Since the new stack frame hasn't been set up yet, gdb will read a garbage value for v.

Stepping a few more instructions will set up the stack frame and spill v from %edi to -0x4(%rbp):

(gdb) stepi
=> 0x000055555555514d <foo+4>: push %rbp
(gdb) stepi
=> 0x000055555555514e <foo+5>: mov %rsp,%rbp
(gdb) stepi
=> 0x0000555555555151 <foo+8>: sub $0x10,%rsp
(gdb) stepi
=> 0x0000555555555155 <foo+12>: mov %edi,-0x4(%rbp)
(gdb) stepi
4 printf(" BAR = %d\n", v);
=> 0x0000555555555158 <foo+15>: mov -0x4(%rbp),%eax

Verify that &v is now within the stack frame, and examine v's value:

(gdb) p &v
$2 = (int *) 0x7fffffffe3fc
(gdb) i r rbp rsp
rbp 0x7fffffffe400
rsp 0x7fffffffe3f0
(gdb) p v
$3 = 8

Why did this happen

Gcc emits endbr64 when given the -fcf-protection option, which has been the default in Ubuntu's gcc since version 19.10.

One workaround

If you compile your program with -fcf-protection=none, gdb can recognize and run the prologue before stopping, and it will show the correct value of v:

(gdb) step
9 foo(a);
(gdb) step
foo (v=8) at t.c:4
4 printf(" BAR = %d\n", v);
(gdb) disas
Dump of assembler code for function foo:
0x0000555555555139 <+0>: push %rbp
0x000055555555513a <+1>: mov %rsp,%rbp
0x000055555555513d <+4>: sub $0x10,%rsp
0x0000555555555141 <+8>: mov %edi,-0x4(%rbp)
=> 0x0000555555555144 <+11>: mov -0x4(%rbp),%eax
0x0000555555555147 <+14>: mov %eax,%esi
0x0000555555555149 <+16>: lea 0xeb4(%rip),%rdi # 0x555555556004
0x0000555555555150 <+23>: mov $0x0,%eax
0x0000555555555155 <+28>: callq 0x555555555030 <printf@plt>
0x000055555555515a <+33>: nop
0x000055555555515b <+34>: leaveq
0x000055555555515c <+35>: retq
End of assembler dump.

Fixed in the new gdb

It looks like support for the endbr instructions was added to gdb in March 2020, so things should be fine if you can use gdb 10.1 or later:

$ ~/gdb10.1/bin/gdb -q t
...
(gdb) step
9 foo(a);
(gdb) step
foo (v=8) at t.c:4
4 printf(" BAR = %d\n", v);
(gdb) disas
Dump of assembler code for function foo:
0x0000555555555149 <+0>: endbr64
0x000055555555514d <+4>: push %rbp
0x000055555555514e <+5>: mov %rsp,%rbp
0x0000555555555151 <+8>: sub $0x10,%rsp
0x0000555555555155 <+12>: mov %edi,-0x4(%rbp)
=> 0x0000555555555158 <+15>: mov -0x4(%rbp),%eax
0x000055555555515b <+18>: mov %eax,%esi
0x000055555555515d <+20>: lea 0xea0(%rip),%rdi # 0x555555556004
0x0000555555555164 <+27>: mov $0x0,%eax
0x0000555555555169 <+32>: call 0x555555555050 <printf@plt>
0x000055555555516e <+37>: nop
0x000055555555516f <+38>: leave
0x0000555555555170 <+39>: ret
End of assembler dump.

GDB showing incorrect function arguments on entry to C function

Although the GCC manual explicitly says:

-O0
Reduce compilation time and make debugging produce the expected results.
This is the default.

Of course, you could debate the "expected results" part...

But it also goes on to state

-Og
Optimize debugging experience. -Og should be the optimization level of choice
for the standard edit-compile-debug cycle, offering a reasonable level of
optimization while maintaining fast compilation and a good debugging experience.
It is a better choice than -O0 for producing debuggable code because some
compiler passes that collect debug information are disabled at -O0.

Using -Og should solve the issue.

UPDATE: I found that the best option for debugging might often actually be -O0 since -Og allows for many variables to be optimized out because of the as-if rule. There is an issue about this from 2016.

Why are there gaps between arguments, variables and the frame pointer in a function stack frame?

This should put you on the right path, but doesn't answer the actual gap:

  • The stack grows downwards and you are trying to read it upwards
  • What you are seeing as &a, &b and &c are not the passed parameters, but local storage that can be used if (e.g.) you say a=1 within function(). I believe that these get optimised out if you don't do -O0
  • a, b and c are passed through registers to the function instead of the stack, so you won't find them in there twice. I.e. the caller doesn't push them to the stack.
  • buffer1 and buffer2 are not aligned in the stack and are packed together as strings.

E.g. Just after (before) buffer2, you can find the saved RBP value and then the return address. For me:

(gdb) p &buffer1
$102 = (char (*)[6]) 0x7fffffffde82
(gdb) p &buffer2
$103 = (char (*)[11]) 0x7fffffffde77

(buffer1 ends at 0x7fffffffde87)

And then the saved RBP:

(gdb) p/x (char [8]) *0x7fffffffde88
$104 = {0xb0, 0xde, 0xff, 0xff, 0xff, 0x7f, 0x0, 0x0}

And then the return address:

(gdb) p/x (char [8]) *0x7fffffffde90
$105 = {0x80, 0x51, 0x55, 0x55, 0x55, 0x55, 0x0, 0x0}

Which you can also see from gdb:

(gdb) info frame
Stack level 0, frame at 0x7fffffffde98:
rip = 0x55555555513f in function (c.c:3); saved rip = 0x555555555180
^^^^^^^^^^^^^^
called by frame at 0x7fffffffdec0
source language c.
Arglist at 0x7fffffffde88, args: a=1, b=2, c=3
Locals at 0x7fffffffde88, Previous frame's sp is 0x7fffffffde98
Saved registers:
rbp at 0x7fffffffde88, rip at 0x7fffffffde90
^^^^^^^^^^^^^^^^^^^^^^

You can also see this by looking at the assembly code:

gcc -S c.c -o c.s

or if you prefer intel:

gcc -masm=intel -S c.c -o c.s

I don't know why gcc leaves that gap though:

    mov     DWORD PTR -36[rbp], edi
mov DWORD PTR -40[rbp], esi
mov DWORD PTR -44[rbp], edx
mov DWORD PTR -6[rbp], 1633771873 <-- aaaa
mov WORD PTR -2[rbp], 97 <-- a\0
movabs rax, 7089336938131513954 <--
mov QWORD PTR -17[rbp], rax
mov WORD PTR -9[rbp], 25186 <-- bb
mov BYTE PTR -7[rbp], 0 <-- \0

Why does gdb show a different parameter order for a function

Why does gdb show that the 2 parameters have different order ?

GDB doesn't know anything about the source (except possibly where on disk it was located at build time).

It is able to display parameters (and their values) because the compiler told it (by embedding debug info into the object file) what parameters are, in what order they appear, their types, and how to compute their value.

So why would a compiler re-order function arguments?

The function is static, so it can't be called from outside of the current translation unit. Thus the compiler is free to re-order the parameters, so long as it also re-orders the arguments at every call site.

Still, why would it do that? General answer: optimization (compiler found it more convenient to pass them in this order). Detailed answer would require digging into GCC (or whatever compiler was used to build this code) source.

How can I examine the stack frame with GDB?

For the current stack frame:

  • info frame lists general info about the frame (where things start in memory, etc.)
  • info args lists arguments to the function
  • info locals lists local variables stored in the frame

GDB corrupted stack frame - How to debug?

Those bogus adresses (0x00000002 and the like) are actually PC values, not SP values. Now, when you get this kind of SEGV, with a bogus (very small) PC address, 99% of the time it's due to calling through a bogus function pointer. Note that virtual calls in C++ are implemented via function pointers, so any problem with a virtual call can manifest in the same way.

An indirect call instruction just pushes the PC after the call onto the stack and then sets the PC to the target value (bogus in this case), so if this is what happened, you can easily undo it by manually popping the PC off the stack. In 32-bit x86 code you just do:

(gdb) set $pc = *(void **)$esp
(gdb) set $esp = $esp + 4

With 64-bit x86 code you need

(gdb) set $pc = *(void **)$rsp
(gdb) set $rsp = $rsp + 8

Then, you should be able to do a bt and figure out where the code really is.

The other 1% of the time, the error will be due to overwriting the stack, usually by overflowing an array stored on the stack. In this case, you might be able to get more clarity on the situation by using a tool like valgrind

Does GDB shows wrong data when run over an compiler optimized binary image?

It appear to be an issue with compiler.
Output of info address arg2 showed that 2nd argument arg2 doesn't contains an entry in stack location.



Related Topics



Leave a reply



Submit