Examine the Stack using GDB




Hello all, after the 1st article Behind the scenes! in our collection "Into the wild!" and after mention not once the stack
So, this article is about undestanding the stack, why and how it works.

Basically, the stack is when a program starts executing, a certain continous section of memory is set aside for the program called the stack.

In brief talking, the stack is a container of objects that are inserted and removed according to the last-in first-out (LIFO) principle.
In the pushdown stacks only two operations are allowed: push the item into the stack, and pop the item out of the stack.
A stack is a limited access data structure - elements can be added and removed from the stack only at the top. push adds an item to the top of the stack, pop removes the item from the top.
A helpful analogy is to think of a stack of books; you can remove only the top book, also you can add a new book on the top.

Alright, this is a brief talking about the stack, I will not go further in that, cause there is a lot of tutorials and many many articles talking about the stack..
What I will talk here is how to examine the stack and debugg it.
Let's say you have a running program, suddenly it has stopped. The first thoughts you will have to know is where/how it stopped and how it got there.
Well, to know all of that you need to know in the first place that each time your program performs a function call, the information about where in your program the call was made from
is saved in a block of data called a stack frame. The frame also contains the arguments of the call and the local variable of the function that was called. All the stack frames
are allocated in a specific region of memory called the call stack.
To debug the stack we need to have some tools, one of them and which I consider a very important tool to a developer/kernel hacker is GDB When our program stopps, we can use GDB as a debugger to examine the stack and see all of its informations.
Let's say we are running our program using gdb, once it is stopped our sweet gdb will automatically select the currently excuting frame and describes it breifly as the frame command does.
Keep reading to know further details about that.
The call stack is divided up into contiguous pieces called stack frames, or frames for short; each frame is the data associated with one call to one function.
The frame contains the arguments given to the function, the function's local variables, and the address at which the function is executing.
When your program is started, the stack has only one frame, that of the function main. This is called the initial frame or the outermost frame.
Each time a function is called, a new frame is made. Each time a function returns, the frame for that function invocation is eliminated. If a function is recursive, there can be many frames for the same function.
The frame for the function in which execution is actually occurring is called the innermost frame. This is the most recently created of all the stack frames that still exist.
Inside your program, stack frames are identified by their addresses.
A stack frame consists of many bytes, each of which has its own address; each kind of computer has a convention for choosing one of those bytes whose address serves as the address of the frame.
Usually this address is kept in a register called the frame pointer register while execution is going on in that frame.
GDB assigns numbers to all existing stack frames, starting with zero for the innermost frame, one for the frame that called it, and so on upward.
These numbers do not really exist in your program; they are assigned by GDB to give you a way of designating stack frames in GDB commands.
Some compilers provide a way to compile functions so that they operate without stack frames. (For example, the gcc option `-fomit-frame-pointer' generates functions without a frame.)
This is occasionally done with heavily used library functions to save the frame setup time. GDB has limited facilities for dealing with these function invocations.
If the innermost function invocation has no stack frame, GDB nevertheless regards it as though it had a separate frame, which is numbered zero as usual, allowing correct tracing of the function call chain.
However, GDB has no provision for frameless functions elsewhere in the stack.

Uuh, but wait all this seems bored, I even got bored writing all of that xD. Let's do a practice showcase together.
Let's write a simple program and debugg it so, we will understand all of the stuff menioned above!
#include

int add_numbers(int n1, int n2)
{
int sum=n1+n2;
return sum;
}
int main()
{
int n1=1;
int n2=2;
int sum;

sum=add_numbers(n1,n2);
printf("The sum of 1 and 2 is %d\n",sum);

return 0;
}

Let's compile our code ; gcc -g stack_analyse.c
And let's turn to our sweety GDB
gdb -q ./a.out
Reading symbols from a.out...done.
(gdb) disass main
Dump of assembler code for function main:
0x0000000000000664 <+0>: push %rbp
0x0000000000000665 <+1>: mov %rsp,%rbp
0x0000000000000668 <+4>: sub $0x10,%rsp
0x000000000000066c <+8>: movl $0x1,-0xc(%rbp)
0x0000000000000673 <+15>: movl $0x2,-0x8(%rbp)
0x000000000000067a <+22>: mov -0x8(%rbp),%edx
0x000000000000067d <+25>: mov -0xc(%rbp),%eax
0x0000000000000680 <+28>: mov %edx,%esi
0x0000000000000682 <+30>: mov %eax,%edi
0x0000000000000684 <+32>: callq 0x64a
0x0000000000000689 <+37>: mov %eax,-0x4(%rbp)
0x000000000000068c <+40>: mov -0x4(%rbp),%eax
0x000000000000068f <+43>: mov %eax,%esi
0x0000000000000691 <+45>: lea 0x9c(%rip),%rdi # 0x734
0x0000000000000698 <+52>: mov $0x0,%eax
0x000000000000069d <+57>: callq 0x530
0x00000000000006a2 <+62>: mov $0x0,%eax
0x00000000000006a7 <+67>: leaveq
0x00000000000006a8 <+68>: retq
End of assembler dump.
(gdb) disass add_numbers
Dump of assembler code for function add_numbers:
0x000000000000064a <+0>: push %rbp
0x000000000000064b <+1>: mov %rsp,%rbp
0x000000000000064e <+4>: mov %edi,-0x14(%rbp)
0x0000000000000651 <+7>: mov %esi,-0x18(%rbp)
0x0000000000000654 <+10>: mov -0x14(%rbp),%edx
0x0000000000000657 <+13>: mov -0x18(%rbp),%eax
0x000000000000065a <+16>: add %edx,%eax
0x000000000000065c <+18>: mov %eax,-0x4(%rbp)
0x000000000000065f <+21>: mov -0x4(%rbp),%eax
0x0000000000000662 <+24>: pop %rbp
0x0000000000000663 <+25>: retq
End of assembler dump.
(gdb)

We have disassambled the two functions (main and add_numbers). Let's now add some breakpoints
(gdb) list main
5 int sum=n1+n2;
6 return sum;
7 }
8
9 int main()
10 {
11 int n1=1;
12 int n2=2;
13 int sum;
14
(gdb) list
15 sum=add_numbers(n1,n2);
16 printf("The sum of 1 and 2 is %d\n",sum);
17
18 return 0;
19 }
(gdb) break 15
Breakpoint 1 at 0x67a: file analyse_the_stack.c, line 15.
(gdb) break add_numbers
Breakpoint 2 at 0x654: file analyse_the_stack.c, line 5.
(gdb) break 6
Breakpoint 3 at 0x65f: file analyse_the_stack.c, line 6.
(gdb) break 16
Breakpoint 4 at 0x68c: file analyse_the_stack.c, line 16.
(gdb)

We have list our source code and have breakpointed the line 15 and that's before pushing the arguments of add_numbers() on the stack. The 2nd breakpoint is set after prolog of add_numbers(). The prolog is:
0x000000000000064a <+0>: push %rbp
0x000000000000064b <+1>: mov %rsp,%rbp

The 3rd breakpoint was set before leaving the add_numbers(), while the 4th breakpoint set after leaving it.
Between those two breakpoints (3 and 4) the epilog of add_numbers() is excuted. This is the epilog:
0x0000000000000662 <+24>: pop %rbp
0x0000000000000663 <+25>: retq

Alright, let's run our program after putting those breakpoints and analyse the registers of the stack (RSP, RBP and RIP).
To see informations about the registers, we need first to run the program r stands for run. Then i r rsp rbp rip will list for us the detailed informations about those registers.

(gdb) r
Starting program: /home/naeil/a.out

Breakpoint 1, main () at analyse_the_stack.c:15
15 sum=add_numbers(n1,n2);
(gdb) i r rsp rbp rip
rsp 0x7fffffffe060 0x7fffffffe060
rbp 0x7fffffffe070 0x7fffffffe070
rip 0x55555555467a 0x55555555467a

RSP is smaller than RBP because the stack grows in the direction of smaller addresses. As it can be seen in the assembly code, RIP points to pushing on the stack the second argument of add_function().
If you noticed already or not, the next instruction after leaving add_numbers() is at the address 0x0000000000000689. This is the return code of the call function.

Let's continue to the next stage and investigate the RSP, RBP and RIP after the prolog of the add_numbers function. Moreover, let's analyse the memory starting from the top of the stack in the direction
of the higher addresses.
(gdb) cont
Continuing.

Breakpoint 2, add_numbers (n1=1, n2=2) at analyse_the_stack.c:5
5 int sum=n1+n2;
(gdb) i r rsp rbp rip
rsp 0x7fffffffe050 0x7fffffffe050
rbp 0x7fffffffe050 0x7fffffffe050
rip 0x555555554654 0x555555554654
(gdb) x/20xw $rsp
0x7fffffffe050: 0xffffe070 0x00007fff 0x55554689 0x00005555
0x7fffffffe060: 0xffffe150 0x00000001 0x00000002 0x00000000
0x7fffffffe070: 0x555546b0 0x00005555 0xf7a41f6a 0x00007fff
0x7fffffffe080: 0x00000000 0x00000000 0xffffe158 0x00007fff
0x7fffffffe090: 0x00040000 0x00000001 0x55554664 0x00005555

As it can be seen the italique line is the following items pushed on the stack.
0x00000001 is the 1st argument of add_numbers().
0x00000002 is the 2nd argument of add_numbers().
Lastly, 0xffffe150 is the current RBP, the one from the main function.
After pushing current RBP on the stack, RSP has been copied to RBP, which is used as a reference in add_numbers() when accessing arguments and local variable of this function (see the assembly code). RIP points to the address of the next instruction after the prolog of add_numbers().
Let's move forward, I am trying to summarize as much as possible cause this article seems to be so damn long x)
So, we continue in the program and we will analyze the memory before leaving add_numbers().

(gdb) cont
Continuing.

Breakpoint 3, add_numbers (n1=1, n2=2) at analyse_the_stack.c:6
6 return sum;
(gdb) x/20xw $rsp
0x7fffffffe050: 0xffffe070 0x00007fff 0x55554689 0x00000003
0x7fffffffe060: 0xffffe150 0x00000001 0x00000002 0x00000000
0x7fffffffe070: 0x555546b0 0x00005555 0xf7a41f6a 0x00007fff
0x7fffffffe080: 0x00000000 0x00000000 0xffffe158 0x00007fff
0x7fffffffe090: 0x00040000 0x00000001 0x55554664 0x00005555

In the meantime, the sum of arguments of add_numbers() has been calculated and pushed on the stack (italique).
Let's continue running the program and analyze RSP, RBP and RIP after leaving the function add_numbers().

(gdb) cont
Continuing.

Breakpoint 4, main () at analyse_the_stack.c:16
16 printf("The sum of 1 and 2 is %d\n",sum);
(gdb) i r rsp rbp rip
rsp 0x7fffffffe060 0x7fffffffe060
rbp 0x7fffffffe070 0x7fffffffe070
rip 0x55555555468c 0x55555555468c

In the meantime, the epilog of add_numbers() has been executed and the control returned to main().
RBP has been popped off the stack and points to the previous address the one before calling add_numbers(). The return address has also been popped off the stack and the
instruction at this address was executed. RIP points to the address of the next instruction. RSP points to the previous address (the one before calling add_numbers()).

I hope you liked the article, I will try my best to write another useful aritcles, please let me know about your feedback. :)

o/