在C中获取帧指针

时间:2012-11-18 03:20:20

标签: c assembly x86

我正在尝试在我的C程序中获取FP,我尝试了两种不同的方式,但它们都与我运行GDB时的方式不同。

我尝试的第一种方法,我在C中为Assembly函数创建了一个协议函数:

int* getEbp();

我的代码如下:

int* ebp = getEbp(); 
printf("ebp: %08x\n", ebp); // value i get here is 0xbfe2db58

while( esp <= ebp )       
    esp -= 4;

printf( "ebp: %08x, esp" ); //value i get here is 0xbfe2daec

我的汇编代码

getEbp:
    movl %ebp, %eax
    ret

我尝试使原型函数只返回一个int,但这也与我的GDB输出不匹配。我们正在使用x86程序集。

编辑:拼写错误,我的getEsp函数看起来与另一个完全相同:

getEsp:
    movl %esp, %eax
    ret

2 个答案:

答案 0 :(得分:5)

  1. 读取寄存器时,最好使用GCC extended inline assembly syntax
  2. 如果您在单独的汇编程序文件中编译它,getEbp()看起来应该可以正常工作。
  3. 您的getEsp()显然不正确,因为它没有考虑来电者推送的地址。
  4. 这是一个代码片段,通过扩展的内联asm获取ebp,并通过追逐帧指针进行堆栈展开:

    struct stack_frame {
            struct stack_frame *prev;
            void *return_addr;
    } __attribute__((packed));
    typedef struct stack_frame stack_frame;
    
    void backtrace_from_fp(void **buf, int size)
    {
            int i;
            stack_frame *fp;
    
            __asm__("movl %%ebp, %[fp]" :  /* output */ [fp] "=r" (fp));
    
            for(i = 0; i < size && fp != NULL; fp = fp->prev, i++)
                    buf[i] = fp->return_addr;
    }
    

    我将展示两种读取下面寄存器的工作实现。纯粹的asm函数在get_ebp()中为get_esp()getbp.S。作为内联函数实现的另一组是get_esp_inline()顶部的get_ebp_inline()test-getbp.c

    getbp.S

    .section .text
    /* obviously incurring the cost of a function call
       to read a register is inefficient */
    .global get_ebp
    get_ebp:
    movl %ebp, %eax
    ret
    
    .global get_esp
    get_esp:
    /* 4: return address pushed by caller */
    lea 4(%esp), %eax
    ret
    

    test-getbp.c

    #include <stdio.h>
    #include <stdint.h>
    
    /* see http://sourceware.org/systemtap/wiki/UserSpaceProbeImplementation */
    #include <sys/sdt.h>
    
    int32_t *get_ebp(void);
    int32_t *get_esp(void);
    
    __attribute__((always_inline)) uintptr_t *get_ebp_inline(void)
    {
        uintptr_t *r;
        __asm__ volatile ("movl %%ebp, %[r]" : /* output */ [r] "=r" (r));
        return r;
    }
    
    __attribute__((always_inline)) uintptr_t *get_esp_inline(void)
    {
        uintptr_t *r;
        __asm__ volatile ("movl %%esp, %[r]" : /* output */ [r] "=r" (r));
        return r;
    }
    
    int main(int argc, char **argv)
    {
        uintptr_t *bp, *sp;
    
        /* allocate some random data on the stack just for fun */
        int a[10] = { 1, 3, 4, 9 };
        fprintf(fopen("/dev/null", "r"), "%d\n", a[3]);
    
        STAP_PROBE(getbp, getbp); /* a static probe is like a named breakpoint */
        bp = get_ebp();
        sp = get_esp();
        printf("asm: %p, %p\n", (void*)bp, (void*)sp);
        bp = get_ebp_inline();
        sp = get_esp_inline();
        printf("inline: %p, %p\n", (void*)bp, (void*)sp);
        return 0;
    }
    

    我们现在可以编写一个GDB脚本来转储ebpesp,同时使用上面getbp中定义的test-getbp.c静态探测。

    test-getbp.gdb

    file test-getbp
    
    set breakpoint pending on
    break -p getbp
    commands
    silent
    printf "gdb: 0x%04x, 0x%04x\n", $ebp, $esp
    continue
    end
    
    run
    quit
    

    验证函数是否返回与GDB相同的数据:

    $ gdb -x test-getbp.gdb
    < ... >
    gdb: 0xffffc938, 0xffffc920
    asm: 0xffffc938, 0xffffc920
    inline: 0xffffc938, 0xffffc920
    < ... >
    

    反汇编test-getbp main()会产生:

       0x08048370 <+0>: push   %ebp
       0x08048371 <+1>: mov    %esp,%ebp
       0x08048373 <+3>: push   %ebx
       0x08048374 <+4>: and    $0xfffffff0,%esp
       0x08048377 <+7>: sub    $0x10,%esp
       0x0804837a <+10>:    movl   $0x8048584,0x4(%esp)
       0x08048382 <+18>:    movl   $0x8048586,(%esp)
       0x08048389 <+25>:    call   0x8048360 <fopen@plt>
       0x0804838e <+30>:    movl   $0x9,0x8(%esp)
       0x08048396 <+38>:    movl   $0x8048590,0x4(%esp)
       0x0804839e <+46>:    mov    %eax,(%esp)
       0x080483a1 <+49>:    call   0x8048350 <fprintf@plt>
       0x080483a6 <+54>:    nop
       0x080483a7 <+55>:    call   0x80484e4 <get_ebp>
       0x080483ac <+60>:    mov    %eax,%ebx
       0x080483ae <+62>:    call   0x80484e7 <get_esp>
       0x080483b3 <+67>:    mov    %ebx,0x4(%esp)
       0x080483b7 <+71>:    movl   $0x8048594,(%esp)
       0x080483be <+78>:    mov    %eax,0x8(%esp)
       0x080483c2 <+82>:    call   0x8048320 <printf@plt>
       0x080483c7 <+87>:    mov    %ebp,%eax
       0x080483c9 <+89>:    mov    %esp,%edx
       0x080483cb <+91>:    mov    %edx,0x8(%esp)
       0x080483cf <+95>:    mov    %eax,0x4(%esp)
       0x080483d3 <+99>:    movl   $0x80485a1,(%esp)
       0x080483da <+106>:   call   0x8048320 <printf@plt>
       0x080483df <+111>:   xor    %eax,%eax
       0x080483e1 <+113>:   mov    -0x4(%ebp),%ebx
       0x080483e4 <+116>:   leave  
       0x080483e5 <+117>:   ret 
    

    nop <main+54>是静态探测器。请参阅两个printf调用的代码,了解如何读取寄存器。

    顺便说一下,代码中的这个循环对我来说似乎很奇怪:

    while( esp <= ebp )       
        esp -= 4;
    

    你不是说

    while (esp < ebp)
        esp +=4 
    

答案 1 :(得分:4)

由于您依赖于特定于实现的详细信息,因此您需要提供有关目标的更多信息以获得准确的答案。您没有指定架构,编译器或操作系统,这些都是回答您的问题所必需的。

根据您引用的寄存器名称进行有根据的猜测以及您使用&amp; t语法的事实,我将假设这是i386并且您正在使用gcc。

实现此目的的最简单方法是使用gcc变量属性,您可以尝试使用gcc特定语法来请求特定寄存器。

#include <stdint.h>
#include <stdio.h>

int main(int argc, char **argv)
{
     const uintptr_t register framep asm("ebp");

     fprintf(stderr, "val: %#x\n", framep);

     return 0;
 }

另一种方法是使用内联汇编来加载值,如下所示:

#include <stdint.h>
#include <stdio.h>

int main(int argc, char **argv)
{
    uintptr_t framep;

    asm("movl %%ebp, %0" : "=r" (framep));

    fprintf(stderr, "val: %#x\n", framep);

    return 0;
}

这要求32位寄存器进行写操作(=修饰符),并将其加载到framep上。编译器负责提取您声明的值。

在gdb中,您可以打印该值并验证它是否与输出匹配。

(gdb) b main
Breakpoint 1 at 0x40117f: file ebp2.c, line 8.
(gdb) r
Starting program: /home/zero/a.exe
[New Thread 4664.0x1290]
[New Thread 4664.0x13c4]

Breakpoint 1, main (argc=1, argv=0x28ac50) at ebp2.c:8
8           asm("movl %%ebp, %0" : "=r" (framep));
(gdb) n
10          fprintf(stderr, "val: %#x\n", framep);
(gdb) p/x framep
$1 = 0x28ac28
(gdb) p/x $ebp
$2 = 0x28ac28
(gdb) c
Continuing.
val: 0x28ac28
[Inferior 1 (process 4664) exited normally]
(gdb) q

请记住,您不能依赖此行为,即使在x86 gcc上也可以配置为不使用帧指针并手动跟踪堆栈使用情况。这通常被Microsoft称为FPO,或者在其他平台上称为omit-frame-pointer。这个技巧释放了另一个用于通用目的的寄存器,但使调试变得更加复杂。

你是正确的,eax通常用于x86调用约定中的返回值,我不知道为什么你帖子上的评论声称使用了堆栈。