x86_64 calling conventions and stack frames
Asked Answered
A

4

13

I am trying to make sense out of the executable code that GCC (4.4.3) is generating for an x86_64 machine running under Ubuntu Linux. In particular, I don't understand how the code keeps track of stack frames. In the old days, in 32-bit code, I was accustomed to seeing this "prologue" in just about every function:

push %ebp
movl %esp, %ebp

Then, at the end of the function, there would come an "epilogue," either

sub $xx, %esp   # Where xx is a number based on GCC's accounting.
pop %ebp
ret

or simply

leave
ret

which accomplishes the same thing:

  • Set the Stack Pointer to the top of the current frame, just below the return address
  • Restore the old Frame Pointer value.

In 64-bit code, as I see it through an objdump disassembly, many functions do not follow this convention--they do not push %rbp and then save %rsp to %rbp, How does a debugger like GDB build a backtrace?

My real goal here to is to try to figure out a reasonable address to consider as the top (highest address) of the user stack when execution reaches the start of an arbitrary function further into the program, where perhaps the Stack Pointer has moved down. For the "top," for instance, the original address of argv would be ideal--but I have no access to it from an arbitrary function that main calls. I had at first thought that I could use the old backtrace method: chasing saved Frame Pointer values until the value saved is 0--then, the next one after that can count as the highest practical value. (This is not the same as getting the address of argv, but it will do--say, to find out the Stack Pointer value at _start or whatever _start calls [e.g., __libc_start_main].) Now, I don't know how to get the equivalent address in 64-bit code.

Thanks.

Abdullah answered 24/12, 2011 at 15:28 Comment(5)
Hm indeed. And it's not just with -fomit-frame-pointer.Infarct
Have you tried -fno-omit-frame-pointer? Can you compile this other code with that flag?Condominium
The source code to libunwind might be useful.Thiosinamine
Thanks for all three of these comments. I think the issue here is that my library is actually a modified version of GCC libgomp, so I build it using the Gnu build system in place and try to avoid changing the defaults wherever possible. I believe that GCC compiles by default with -O2, which I'm pretty sure includes -fomit-frame-pointer. After posting, but before I saw Firoze's comment, I did look at the code of glibc's debug/backtrace.c, which is what led me to go looking for __libc_stack_end, which is how I found a somewhat reasonable and general solution.Abdullah
sub $xx, %esp is part of the prologue. It reserves space on the stack. The epilogue does add $xx, %esp to return the stack pointer to pointing at something that needs to be popped. (Or in simple cases leave includes mov %ebp, %esp, so you can use it without adjusting ESP first.)Contractive
C
7

I think the difference is that omitting the frame pointer is simply more encouraged in amd64. A footnote on page 16 of the abi says

The conventional use of %rbp as a frame pointer for the stack frame may be avoided by using %rsp (the stack pointer) to index into the stack frame. This technique saves two instructions in the prologue and epilogue and makes one additional general-purpose register (%rbp) available.

I don't know what GDB does. I assume that when compiled with -g, objects have magic debugging information that allows GDB to reconstruct what it needs. I don't think I've tried GDB on a 64-bit machine without debugging info.

Chante answered 24/12, 2011 at 15:42 Comment(3)
My experience with x86-64 has shown that the debugger uses additional information to know the stack frame size, which saves the instructions but makes debugging and unwinding a pain.Hypochlorite
Yep, as I suspected. And does it all bork up when the executable is compiled without debugging info?Chante
Thank you. That recommendation in the ABI does account for what's going on--but it still leaves me wondering how I can solve my problem. I need to get--roughly speaking--the value of the Stack Pointer when execution entered main, from an arbitrary function that comes after main in the call graph. The value can be a higher than the actual value of the top of main's stack frame, so long as it's within the process's stack, but the closer to the top of main's stack frame, the better.Abdullah
A
3

GDB uses the DWARF CFI for unwinding. For unstripped binaries compiled with -g, this will be in the .debug_info section. For stripped x86-64 binaries, there's unwind info in the .eh_frame section. This is defined in the x86-64 ABI, section 3.7, page 56. Handling this info yourself is pretty hard, since parsing DWARF is very involved, but I believe libunwind contains support for it.

Amygdalin answered 24/8, 2012 at 11:19 Comment(2)
I'm pretty sure it's always in the .eh_frame section, which is why it's still there after stripping. The way you describe it, strip would have to find that info in .debug_info and copy it into .eh_frame, and unwind would have to check both location...Contractive
.debug_info contains extra info about local variables inside stack frames, but .eh_frame always contains sufficient info to unwind the stack. (i.e. the size of each stack frame, and where callee-saved registers are saved, but not which variable is stored where.)Contractive
H
1

If the address of argv is what you want, why not just save a pointer to it in main?
Trying to unwind the stack would be highly unportable, even if you get it to work.
Even if you do manage to go back over the stack, it isn't obvious that the first function's frame pointer would be NULL. The first function on the stack doesn't return, but calls a system call to exit, and therefore its frame pointer is never used. There's no good reason why it would be initialized to NULL.

Hallucinate answered 24/12, 2011 at 15:54 Comment(1)
Thanks. Alas, no, I can't save the pointer in main. I am writing a user-level library to link with arbitrary code, so I can't touch the original code (except to add an #include)--or would prefer to avoid doing so if at all possible. As to your second point, I had the impression that kernels such as that of Linux do follow the convention of setting the Frame Pointer to NULL before passing control to a user process, precisely for this purpose. But maybe that's just an older convention that not all systems follow anymore.Abdullah
A
1

Assuming that I am linking with glibc (which I am doing), it looks as if I can solve this problem for practical purposes with the glibc global symbol __libc_stack_end:

extern void * __libc_stack_end;

void myfunction(void) {
  /* ... */
  off_t stack_hi = (off_t)__libc_stack_end;
  /* ... */
}
Abdullah answered 25/12, 2011 at 0:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.