【问题标题】:Difference between memory indirect call and register indirect call内存间接调用和寄存器间接调用的区别
【发布时间】:2012-01-21 17:53:30
【问题描述】:

内存间接调用和寄存器间接调用有什么区别?

我正在尝试学习一些有关 linux rootkit 检测的知识,如何识别反汇编内存中的此类调用?编译前它们在 C 语言中的外观如何?

【问题讨论】:

    标签: c linux assembly


    【解决方案1】:

    间接分支是分支到存储在寄存器或内存位置中的地址的分支。分支指令的操作数是存放分支地址的寄存器或内存位置。

    查看维基百科页面了解更多信息: http://en.wikipedia.org/wiki/Indirect_branch

    在 C 中,取决于实现(和 CPU),当通过函数指针调用函数时,通常会进行间接分支。由于switch 语句的一些启发式使用函数指针(通过跳转表),间接分支也可以在switch 语句中找到。

    【讨论】:

    • 谢谢解释,但是什么时候用寄存器,什么时候用内存呢?我读过在内存中间接调用比在寄存器间接调用中更容易找到调用者的地址。为什么?
    • 因为内存的内容之后很容易被读取,而寄存器的内容几乎会立即被丢弃。
    • 首先它取决于平台,因为某些 mcu 仅支持寄存器的间接分支。对于两者都支持的人,从内存中间接调用可能需要更多的 CPU 周期。然后,正如@noah1989 所说,您不想将寄存器保存太久,因为在某些平台上没有太多寄存器,而内存更便宜,可用于在程序的生命周期内存储地址。
    • 间接调用也可以用于共享库的函数调用。
    【解决方案2】:

    内存间接调用是从内存中获取被调用者地址的调用,而寄存器间接调用则从寄存器中获取地址。

    调用的实现很大程度上取决于所使用的体系结构和编译器。在 x86 上,内存和寄存器调用都是可能的,因此由编译器决定什么是最佳的。

    看一个简单的测试:

    #include "stdlib.h"
    #include "stdio.h"
    #include "time.h"
    
    void example_fun(int param)
    {
        printf("%d\n", param);
    }
    
    void fn1(void)
    {
        printf("fn1\n");
    }
    
    void fn2(void)
    {
        printf("fn2\n");
    }
    
    typedef void (*fn)(void);
    
    int main(void)
    {
        void (*fp) (int) = &example_fun;
        int c;
        fn fn_arr[] = {&fn1, &fn2};
    
        (*fp)(2);
        srand(time(NULL));
        c = rand() / (RAND_MAX / 2);
        switch (c)
        {
            case 0: (*(fn_arr[0]))(); break;
            case 1: (*(fn_arr[1]))(); break;
            default: (*fp)(1);
        }
    }
    

    生成的程序集是(gcc 4.6.1 没有任何优化):

        .file   "indirect.c"
        .section    .rodata
    .LC0:
        .string "%d\n"
        .text
        .globl  example_fun
        .type   example_fun, @function
    example_fun:
    .LFB0:
        .cfi_startproc
        pushl   %ebp
        .cfi_def_cfa_offset 8
        .cfi_offset 5, -8
        movl    %esp, %ebp
        .cfi_def_cfa_register 5
        subl    $24, %esp
        movl    $.LC0, %eax
        movl    8(%ebp), %edx
        movl    %edx, 4(%esp)
        movl    %eax, (%esp)
        call    printf
        leave
        .cfi_restore 5
        .cfi_def_cfa 4, 4
        ret
        .cfi_endproc
    .LFE0:
        .size   example_fun, .-example_fun
        .section    .rodata
    .LC1:
        .string "fn1"
        .text
        .globl  fn1
        .type   fn1, @function
    fn1:
    .LFB1:
        .cfi_startproc
        pushl   %ebp
        .cfi_def_cfa_offset 8
        .cfi_offset 5, -8
        movl    %esp, %ebp
        .cfi_def_cfa_register 5
        subl    $24, %esp
        movl    $.LC1, (%esp)
        call    puts
        leave
        .cfi_restore 5
        .cfi_def_cfa 4, 4
        ret
        .cfi_endproc
    .LFE1:
        .size   fn1, .-fn1
        .section    .rodata
    .LC2:
        .string "fn2"
        .text
        .globl  fn2
        .type   fn2, @function
    fn2:
    .LFB2:
        .cfi_startproc
        pushl   %ebp
        .cfi_def_cfa_offset 8
        .cfi_offset 5, -8
        movl    %esp, %ebp
        .cfi_def_cfa_register 5
        subl    $24, %esp
        movl    $.LC2, (%esp)
        call    puts
        leave
        .cfi_restore 5
        .cfi_def_cfa 4, 4
        ret
        .cfi_endproc
    .LFE2:
        .size   fn2, .-fn2
        .globl  main
        .type   main, @function
    main:
    .LFB3:
        .cfi_startproc
        pushl   %ebp
        .cfi_def_cfa_offset 8
        .cfi_offset 5, -8
        movl    %esp, %ebp
        .cfi_def_cfa_register 5
        andl    $-16, %esp
        subl    $32, %esp
        movl    $example_fun, 24(%esp)
        movl    $fn1, 16(%esp)
        movl    $fn2, 20(%esp)
        movl    $2, (%esp)
        movl    24(%esp), %eax
        call    *%eax
        movl    $0, (%esp)
        call    time
        movl    %eax, (%esp)
        call    srand
        call    rand
        movl    %eax, %ecx
        movl    $-2147483645, %edx
        movl    %ecx, %eax
        imull   %edx
        leal    (%edx,%ecx), %eax
        movl    %eax, %edx
        sarl    $29, %edx
        movl    %ecx, %eax
        sarl    $31, %eax
        movl    %edx, %ecx
        subl    %eax, %ecx
        movl    %ecx, %eax
        movl    %eax, 28(%esp)
        movl    28(%esp), %eax
        testl   %eax, %eax
        je  .L6
        cmpl    $1, %eax
        je  .L7
        jmp .L9
    .L6:
        movl    16(%esp), %eax
        call    *%eax
        jmp .L10
    .L7:
        movl    20(%esp), %eax
        call    *%eax
        jmp .L10
    .L9:
        movl    $1, (%esp)
        movl    24(%esp), %eax
        call    *%eax
    .L10:
        leave
        .cfi_restore 5
        .cfi_def_cfa 4, 4
        ret
        .cfi_endproc
    .LFE3:
        .size   main, .-main
        .ident  "GCC: (Ubuntu/Linaro 4.6.1-9ubuntu3) 4.6.1"
        .section    .note.GNU-stack,"",@progbits
    

    在所有情况下,我们的调用都被转换为call *%eax,这是一个内存间接调用,执行位于%eax 指向的地址的函数。添加优化会使结构混乱,但机制仍然是寄存器调用。

    调用共享动态库(参见示例here)只得到call <function_name>。咨询x86 Developer Manualcall 接受寄存器、内存或指针操作数。链接器在启动时正在处理这样的调用,并且函数的名称被实际的内存地址替换。因此,调用共享库(最终)会导致内存间接调用。

    除了这种容易复现的情况外,您还可以遇到直接地址的操作,例如call dword ptr ds:[00923030h](描述为here)或类似的jmp。我无法提供合理的解释它们来自哪里。

    那么区分寄存器调用和内存调用就很容易了:只需查看操作数即可。

    如果您对 rootkit 检测感兴趣,最好只获取一些现有的 rootkit 并并排查看其源代码和生成的程序集。

    我对编译器和架构不是很有经验,所以我可能会犯一些错误。

    希望对你有帮助。

    【讨论】:

      猜你喜欢
      • 2014-06-07
      • 1970-01-01
      • 1970-01-01
      • 2018-08-20
      • 2011-08-04
      • 1970-01-01
      • 2023-01-31
      • 2017-07-15
      • 1970-01-01
      相关资源
      最近更新 更多