Consider the assembly program below:
.section .data
.section .text
.global _start
_start:
pushl $85 #make it obvious in memory
popl %ebx
movl $1, %eax
int $0x80
It pushes 85 onto the stack and then pops it right away, after which the program closes.
after assembling it with as (GNU assembler) and linking, I opened it with gdb
as --gstabs --32 main.s -o main.o
ld main.o -o main -m elf_i386
gdb main
I set the break point at _start and ran the program.
I know that the esp register points to top of the stack. When we push values onto the stack, the address that esp is pointing to is decreased by 4 (since we are working with long values here ) to point to the next location.
after that I first check where esp points to. then run the next instruction (which is to push $85 onto the stack) and again check to see where esp point to, now.
(gdb) info register esp
esp 0xffffcb60 0xffffcb60
(gdb) next
(gdb) info register esp
esp 0xffffcb5c 0xffffcb5c
Which everything seems fine.
I checked the values held at each of the locations pointed to by esp of.
(gdb) x/d 0xffffcb60
0xffffcb60: 1
(gdb) x/d 0xffffcb5c
0xffffcb5c: 85
Now here is my question: from what I understand, esp is used to point to the next item on the stack, so when is points to
0xffffcb5c
it means that from 0xffffcb5c to 0xffffcb58 will hold the pushed value.
then why when I use x/d 0xffffcb60 I get 1 instead of 85?
Perhaps the x/d command examines the 4 bytes before that location up to the specified location.
1 is argc; it was already on the stack before your push, as specified by the AMD64 System V ABI for the initial process state at _start. The kernel put it there1.
After a push, ESP points at the thing just pushed: push works like ESP -= 4; mov src, (%esp). (Except when src is ESP itself; see What is an assembly-level representation of pushl/popl %esp? for pseudocode that covers the general case).
After your push, ESP = 0xffffcb5c, and GDB shows you the 4-byte integer value there is the 85 you pushed. The addresses of those bytes are 0xffffcb5c (the low byte, holding the actual 85 since x86 is little-endian), and ...5d, ...5e, ...5f all holding 0. (Which you can see with x /8b $esp to see 8 bytes each separately, starting at where ESP points.)
If you were to do another push after this, those 4 bytes would be stored at 0xffffcb58 to ...b.
There is no magic; GDB doesn't work differently because you gave it a stack address.
See also the bottom of the x86 tag wiki for GDB asm tips. Very good that you're already using it; many different bugs in asm have the same symptoms (crash or no output), so watching register values change as you single-step is vastly more useful for finding out which part of your program isn't running the way you thought it would.
A stack where the stack pointer points at the value you just pushed is called a "full" stack, as opposed to pointing at the next empty slot being an "empty" stack. The difference is pre vs. post-decrement of the stack pointer. Or increment for an ascending instead of descending stack. Fun fact: the ARM architecture has the instruction mnemonic stmfd, store-multiple full-descending, which works like x86's push. It's an alias for stmdb (decrement-before). But ARM has the full set of increment-before, increment-after, and decrement-after, allowing full or empty stacks that grow upwards or downward. But ARM is normally used with full-descending stacks, like x86, and ARM assemblers define push as an alias for stmdb using the stack pointer. Thumb mode compact instructions make that implicit, allowing only decrement-before. I mention all this just to point out that other ways for push to work would have been possible, but full-descending is the one x86 picked, and the normal one across most ISAs.
Footnote 1:
The kernel didn't use push to store that value, though. While running the sys_execve kernel function that handles an execve system call (by the shell for example, after forking), its RSP would be pointing at the kernel stack for that task. But this value is on your user-space stack. It would have allocated pages for a fresh stack and copied stuff to it, before iret or sysret to return to user-space in your newly-execed process. (Also restoring the user-space register state as part of switching to user-space, including the stack pointer. That's how your ESP ends up pointing near the top of the stack region, with usable memory below and command-line + environment above.)