【问题标题】:What do the CFI directives mean? (and some more questions)CFI 指令是什么意思? (还有一些问题)
【发布时间】:2014-08-19 04:05:48
【问题描述】:

好的,这将是一个很长的问题。我试图了解“缓冲区溢出”是如何工作的。我正在阅读 aleph1 的 Smashing the stack for fun and profit 并且刚刚获得了以下代码的反汇编:

void function(int a, int b, int c) {
   char buffer1[5];
   char buffer2[10];
}

void main() {
  function(1,2,3);
}

使用 GCC 的 -S 标志的差异给了我:

    .file   "example1.c"
    .text
    .globl  function
    .type   function, @function
function:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $48, %rsp
    movl    %edi, -36(%rbp)
    movl    %esi, -40(%rbp)
    movl    %edx, -44(%rbp)
    movq    %fs:40, %rax
    movq    %rax, -8(%rbp)
    xorl    %eax, %eax
    movq    -8(%rbp), %rax
    xorq    %fs:40, %rax
    je  .L2
    call    __stack_chk_fail
.L2:
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   function, .-function
    .globl  main
    .type   main, @function
main:
.LFB1:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    $3, %edx
    movl    $2, %esi
    movl    $1, %edi
    call    function
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE1:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 4.8.2-19ubuntu1) 4.8.2"
    .section    .note.GNU-stack,"",@progbits

.cfi 指令不在 Aleph1 的论文中,我猜当时没有使用它们。我已阅读 this question on SO 并且知道它们被 GCC 用于异常处理。我还阅读了another question on SO,我知道 .LFB0、.LFE0、.LFE1 和 .LFB1 是标签,但我有以下疑问:

  1. 我知道 .cfi 指令用于异常处理,但我不明白它们的含义。我一直是here,我看到了一些定义,例如:

.cfi_def_cfa 寄存器,偏移量

.cfi_def_cfa 定义了计算 CFA 的规则为:取地址来自 注册并添加偏移量。

但是,如果您查看我在上面放置的反汇编代码,您没有找到任何寄存器名称(例如 EAX、EBX 等),而是在那里找到了一个数字(I通常找到'6'),我不知道那应该是一个寄存器。特别是,谁能解释.cfi_def_cfa_offset 16.cfi_offset 6, -16.cfi_def_cfa_register 6.cfi_def_cfa 7, 8是什么意思?另外,CFA 是什么意思?我问这个是因为大多数在书籍/论文中,程序序言就像:

 pushl %ebp
 movl %esp,%ebp
 subl $20,%esp

但是,现在我认为现代计算机中的程序prolog如下:

    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $48, %rsp

最初我认为使用 CFI 指令而不是 sub 助记符来设置偏移量,但事实并非如此;尽管使用了 CFI 指令,sub 命令仍在使用中。

  1. 我知道每个程序都有标签。但是,为什么一个过程中有多个嵌套标签?在我的例子中, main 有 .LFB1 和 .LFE2 标签。需要多个标签是什么?同样,function 过程具有标签 .LFB0、.L2 和 .LFE0

  2. 这两个过程的最后 3 行似乎用于一些内务处理功能(可能是告诉过程的大小?)但我不确定它们是什么意思。谁能解释一下它们的含义和用途?

编辑:

(增加一个问题)

  1. CFI 指令是否占用空间?因为在程序“function”中,每个int参数占用4个字节,个数为3,所以所有参数在内存中占用12个字节。接下来,第一个 char 数组占用 8 个字节(将 5 个字节向上舍入为 8 个字节),下一个 char 数组占用 12 个字节(将 10 个字节向上舍入为 12 个字节),因此整个 char 数组占用 20 个字节。所有这些加起来,参数和局部变量只需要 12+20=32 个字节。

    但在“函数”过程中,编译器减去 48 个字节来存储值。为什么?

【问题讨论】:

  • 这些是 GAS 识别并用于发出展开相关数据的宏。用于 C++ 的 Itanium ABI 描述了这需要什么(GCC 实现了该 ABI)。展开数据存储在其他地方(在目标文件中查找eh-frame 部分);它本质上是通过使用指令指针作为键的关联查找来定位的。
  • 如果你编译为 C 和 C++,你会得到不同的结果吗?另外,永远不要void main()。这是int main()
  • 本页解释了使用 cfi 指令来获取 Dwarf-2 调试信息,从而避免了帧指针 - logix.cz/michal/devel/gas-cfi
  • 您可以在不展开表的情况下进行编译以摆脱它们(使用-fno-asynchronous-unwind-tables)。也就是说,它们不会以任何方式影响生成的机器代码,它们绝对不会“替换”任何sub 指令。您可以放心地忽略它们。

标签: c++ c gcc assembly x86


【解决方案1】:

CFI 代表调用帧信息。这是编译器描述函数中发生的事情的方式。调试器可以使用它来显示调用堆栈,链接器可以使用它来综合异常表,进行堆栈深度分析和其他类似操作。

实际上,它描述了处理器寄存器等资源存储在哪里以及返回地址在哪里。

CFA代表调用帧地址,表示调用函数的栈指针位置的地址。这是获取堆栈中下一帧的信息所必需的。

【讨论】:

  • 好的。但是为什么 .cfi_def_cfa_offset、.cfi_offset、.cfi_def_cfa_register 在两个过程中是一样的呢?
  • CFI 指令也占用任何空间吗?因为在“函数”过程中,3 个整数每个占用 4 个字节,而 char 数组应该占用 20 个字节,总计 12+20 = 32 个字节。那么为什么要减去 48?
  • cfi 指令本身不占用任何空间,它们只提供元信息。但是,如果链接器综合异常表,它们会占用空间,但是,它们将被放置在与实际代码不同的位置。
  • 好的。如果你知道的话,你能解释一下为什么要减去 48 吗?
  • 对不起,我不能。过去几年我使用过可能的架构,但我从未使用过 x86。它归结为 char 数组如何在内存中对齐和填充,如果函数需要任何临时变量(例如堆栈检查代码)等。也许您可以将其作为单独的 SO 问题发布?
【解决方案2】:

根据您在逆向工程中的要求,我将我的 cmets 的内容作为答案放在这里(我不知道这是否会继续存在,因为我看到在那里对您的问题进行投票和投票的激烈竞争)

Lindy Dancer 回答了什么 cfi and cfa means (call frame information) 和 (call frame address)

.L<num> 表示在 x64 GCC 中根据 Google 中的各种花絮的标签以以下格式命名所有标签以 .L 开头并以 a numeral 结尾,因此 .L1 , .L2 , .L....infinity 是标签

根据谷歌和一些较早的SO 答案BF<num> 表示函数开始,EF<num> 表示FUNCTION-END

所以.LBF0 , .LBF1 . LBF.....infinity.LFE0 ,......., .LFE....infinity

表示每个函数中的函数开始和函数结束,编译器可能需要这些函数来处理一些内部需求,所以此时你应该忘记它们,除非非常需要深入研究编译器内部

另一个标签 .L2 用于解决函数中的分支指令 je

je  .L2

每个编译器也会对齐和填充对参数和局部变量的访问到某个边界

我不能确定,但​​我认为对于 GCC,x64 默认对齐是 16 个字节 所以如果你要求一个奇怪的预订,比如

char foo[5] 或
字节等等 [10]

索引5 and 10 甚至对于x86 也没有对齐

对于 5 x86 compiler will assign 8 bytes and for 10 16 bytes

同样明智的x64 gcc might assign 16 bytes 处理您的每个请求

您实际上不应该担心编译器为什么会这样做

当你试图理解汇编逻辑时,只关注地址

如果编译器决定它will put x at rbp +/- X 它将also access it at the same location 贯穿该变量的作用域或生命周期

【讨论】:

  • 嘿,非常感谢!这回答了我的所有问题!
【解决方案3】:

48 是跳过参数和本地人。 5 字节数组在 8 字节边界上对齐,10 字节在 16 字节边界上对齐。每个参数占用 8 个字节,因此参数 3*8 加上本地参数 8 + 16 得到 24+24 或 48。您可以在 gdb 中通过询问每个内容的地址来查看它。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-03-03
    • 2012-10-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-22
    • 1970-01-01
    相关资源
    最近更新 更多