关于堆栈:在汇编代码中,.cfi指令如何工作?

In assembly code, how .cfi directive works?

[汇编代码]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
main:
.LFB0:
    .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    $5, 20(%esp)
    movl    $3, 24(%esp)
    movl    24(%esp), %eax
    movl    %eax, 4(%esp)
    movl    20(%esp), %eax
    movl    %eax, (%esp)
    call    add
    movl    %eax, 28(%esp)
    movl    $0, %eax
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .globl  add
    .type   add, @function
add:
.LFB1:
    .cfi_startproc
    pushl   %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    subl    $16, %esp
    movl    12(%ebp), %eax
    movl    8(%ebp), %edx
    addl    %edx, %eax
    movl    %eax, -4(%ebp)
    movl    -4(%ebp), %eax
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc

[源代码]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int add(int k, int l);

int main(int argc, char **argv) {
        int a, b, ret;
        a = 5;
        b = 3;
        ret = add(a, b);
        return 0;
}

int add(int k, int l) {
        int x;
        x = k + l;
        return x;
}

我正在汇编语言级别研究c函数的调用约定。

如您所知,.cfi用于添加调试信息。我已经阅读了一些CFI文章,并且知道了每个指令的含义。

在上面的汇编代码中,.cfi_def_cfa_offset 8.cfi_offset 5 -8指令连续出现。这在"主要"功能和"增加"功能中再次发生。

但是,我不知道为什么会这样。我所知道的是.cfi_def_cfa_offset.cfi_offset用于创建保留内存来存储调试信息。在此代码中,该偏移量首先设置为+8,然后设置为-8。结果是...没有剩余的内存空间来存储cfi。我对吗?

我认为堆栈段的工作方式是这样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
.cfi_startproc
|-------------|
|  whatever   | <- %esp = CFA      ↑ increase address
|-------------|
|             |                    ↓ stack grow
|_____________|



.pushl  %ebp
|-------------|
|  whatever   |
|-------------|
|   %ebp      | <- %esp
|_____________|


.cfi_def_cfa_offset 8
|-------------|
|  whatever   |  <- %esp
|-------------|
|   whatever  |
|-------------|
|   %ebp      |
|-------------|



.cfi_offset 5 -8
|-------------|
|  whatever   |  
|-------------|
|   whatever  |
|-------------|
|   %ebp      | <- %esp
|-------------|



 subl $32, %esp
|-------------|
|   whatever  |
|-------------|
|    %ebp     |
|-------------|
|             |
|-------------|
|             |
|-------------|
|             |
|-------------|
|             |
|-------------|
|             |
|-------------|
|             |
|-------------|
|             |
|-------------|
|             | <- %esp
|-------------|



 movl $5, 20(%esp)
|-------------|
|   whatever  |
|-------------|
|    %ebp     |
|-------------|
|             |
|-------------|
|             |
|-------------|
|      5      |
|-------------|
|             |
|-------------|
|             |
|-------------|
|             |
|-------------|
|             |
|-------------|
|             | <- %esp
|-------------|

等等...

问题2。

在过程add中,来自调用程序功能的参数将移至被调用程序功能寄存器。

1
2
    movl    12(%ebp), %eax
    movl    8(%ebp), %edx

但是,在我的计算中,8(%ebp)没有指向调用程序堆栈的顶部。因为,

1)在pushl %ebp处,%esp减去4

2)在cfi_offset 5, -8处,%esp被剔除8(这样,我忽略了.cfi_def_cfa_offset 8。我不确定)

因此,调用方函数堆栈的顶部应以此方式为12(%ebp),并且8(%ebp)指向存储的调用方函数的基指针。

我不知道我在哪里...我需要你的帮助。

-添加

CFI指令是什么意思? (还有更多问题)

这样的问题几乎与我相似。但是没有人清楚地回答这个问题。


.cfi指令不会生成任何汇编代码。它们不会执行,也不会改变呼叫框的布局。

取而代之的是,它们告诉需要解散堆栈的工具(调试器,异常解散器)有关框架的结构(以及解散方法)。这些信息不会与指令一起存储,而是存储在程序的另一部分中(请参见注1)。

让我们看一下这个片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
main:
.LFB0:
    .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    $5, 20(%esp)
    movl    $3, 24(%esp)
    movl    24(%esp), %eax
    movl    %eax, 4(%esp)
    movl    20(%esp), %eax
    movl    %eax, (%esp)
    call    add
    movl    %eax, 28(%esp)
    movl    $0, %eax
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc

汇编程序将在.text段中汇编指令,并在另一节(.eh_frame.debug_frame)中编译.cfi指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ gcc -m32 -g test.s -c -o a.out
$ objdump -d a.out
[...]
00000000 <main>:
   0:   55                      push   %ebp
   1:   89 e5                   mov    %esp,%ebp
   3:   83 e4 f0                and    $0xfffffff0,%esp
   6:   83 ec 20                sub    $0x20,%esp
   9:   c7 44 24 14 05 00 00    movl   $0x5,0x14(%esp)
  10:   00
  11:   c7 44 24 18 03 00 00    movl   $0x3,0x18(%esp)
  18:   00
  19:   8b 44 24 18             mov    0x18(%esp),%eax
  1d:   89 44 24 04             mov    %eax,0x4(%esp)
  21:   8b 44 24 14             mov    0x14(%esp),%eax
  25:   89 04 24                mov    %eax,(%esp)
  28:   e8 fc ff ff ff          call   29 <main+0x29>
  2d:   89 44 24 1c             mov    %eax,0x1c(%esp)
  31:   b8 00 00 00 00          mov    $0x0,%eax
  36:   c9                      leave  
  37:   c3                      ret

请注意main函数的代码中仅如何存在指令。 CFI在其他地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ readelf -wF a.out
Contents of the .eh_frame section:

00000000 00000014 00000000 CIE"zR" cf=1 df=-4 ra=8
   LOC   CFA      ra      
00000000 esp+4    c-4  

00000018 0000001c 0000001c FDE cie=00000000 pc=00000000..00000038
   LOC   CFA      ebp   ra      
00000000 esp+4    u     c-4  
00000001 esp+8    c-8   c-4  
00000003 ebp+8    c-8   c-4  
00000037 esp+4    u     c-4

CFI是描述框架布局的信息(不是本机CPU指令)。

例如,让我们看下面的代码片段:

1
2
3
4
.cfi_startproc
pushl   %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8

.cfi_startproc

cfi_startproc初始化该功能的CFI。此时,CFA(规范帧地址,即调用方帧中%rsp的地址)由%esp + 4给出(因为调用方在call指令中推送了返回地址):

1
2
whatever              <- CFA
return address  (ra)  <- %esp

CFI指令在.eh_frame中被"编译":

1
2
   LOC   CFA      ebp   ra      
00000000 esp+4    u     c-4

.cfi_def_cfa_offset.cfi_offset

pushl %ebp指令中,此内容不再成立:cfa ≠ %esp + 4,因为%esp已更改。完成此指令后,我们有了cfa = %esp + 8。调试器需要知道这一点,并且.cfi_def_cfa_offset 8指令正在.eh_frame节中为调试器生成适当的信息:.cfi_def_cfa_offset 8cfa = %esp + 8中的偏移量设置为8。

1
2
3
whatever             <- CFA  = %esp + 8
return address (ra)
caller %ebp          <- %esp (= CFA - 8)

pushl %ebp的目的是将调用方中的%ebp值保存在堆栈中。调试器需要知道此值的保存位置,以展开堆栈并恢复调用方框架。 .cfi_offset 5, -8指令指示调试器寄存器5(%ebp)已由cfa - 8中的前一条指令保存。

.eh_frame表的下一个条目中找到此信息:

1
2
3
   LOC   CFA      ebp   ra
[...]      
00000001 esp+8    c-8   c-4

笔记

注1:在某些情况下,这些信息是调试信息的一部分,这意味着如果未使用调试信息编译文件,则该信息可能在运行时不存在,并且可能根本不存在于文件中。