函数栈的调用

        本篇主要通过反汇编与GDB两种方式分析函数栈的使用过程。


反编译方法:


  • gcc

-S Stop after the stage of compilation proper; do not assemble. The output is in the
form of an assembler code file for each non-assembler input file specified.

By default, the assembler file name for a source file is made by replacing the suffix
.c, .i, etc., with .s.

Input files that don't require compilation are ignored.



gcc -S -o [source].s [source].c


  • objdump

gcc stack.c -o stack.o
objdump -S ./stack.o > stack_objdump.s


  • 编译环境:

环境: cenos7, X86_64

kernel: 3.10.0-862.el7.x86_64

gcc: gcc version 4.8.5 20150623 (Red Hat 4.8.5-36) (GCC)

  • 实验代码:

[root@centosgpt stack]# cat stack.c
#include <stdio.h>

int f(int x, int y)
{
    int l = 2;
    int sum = x + y + l;
    return sum;
}

int main()
{
    int x = 1;
    int y = 2;
    f(x, y);
    return 0;
}
  • 使用gcc反编译

f:
.LFB0:
; 标识.eh_frame入口,初始化一些CFI数据结构
        .cfi_startproc
; 保存rbp
        pushq   %rbp
; 此位置距离CFA基地址距离为16
        .cfi_def_cfa_offset 16
; 寄存器RSP值为 CFA-16
        .cfi_offset 6, -16
;  rbp设置为指向当前栈顶rsp的位置
        movq    %rsp, %rbp
;  更新CFA基址寄存器 为 rsp
        .cfi_def_cfa_register 6

;  将函数参数传入栈中 
        movl    %edi, -20(%rbp)
        movl    %esi, -24(%rbp)
;  本地变量传入栈中 
        movl    $2, -4(%rbp)

;  实现加法操作 sum=x+y+l
        movl    -24(%rbp), %eax
        movl    -20(%rbp), %edx
        addl    %eax, %edx
        movl    -4(%rbp), %eax
        addl    %edx, %eax
; 结果存放到  eax
        movl    %eax, -8(%rbp)
        movl    -8(%rbp), %eax
; 弹出return address
        popq    %rbp

; ; 重新定义CFA = rsp + 8
        .cfi_def_cfa 7, 8
        ret
; 关闭cfi功能
        .cfi_endproc

main:
; x存放 %rbp-4 地址最终传入到 %edi寄存器
; y存放 %rbp-8 地址最终传入到 %esi寄存器
; 这个地方已经没有相关栈操作了
        movl    $1, -4(%rbp)
        movl    $2, -8(%rbp)
        movl    -8(%rbp), %edx
        movl    -4(%rbp), %eax
        movl    %edx, %esi
        movl    %eax, %edi
        call    f

函数栈的作用:

       函数调用过程中会把要传递给函数的参数, 寄存器的值,局部变量压栈,函数调用完毕后,这些信息再出栈。


  • 寄存器

          ESP: 指向当前函数栈栈顶

          EBP:栈帧基地址,当前栈底部,方便访问本地变量和函数参数

          EIP: 指向下一条指令地址


  • 函数栈存放内容

          函数参数

          返回地址

          当前EBP

  • The red zone

          实验的时候发现,X86_64环境下,RSP指针并没有移动, 是编译器的一个优化,使得子函数减少相关栈指针移动。

The 128-byte area beyond the location pointed to by %rsp is considered to be reserved and shall not be modified by signal or interrupt handlers.8 Therefore,functions may use this area for temporary data that is not needed across function calls. In particular, leaf functions may use this area for their entire stack frame, rather than adjusting the stack pointer in the prologue and epilogue. This area is
known as the red zone
  • 使用gdb调试

         1. 生成可调试的执行文件

                    gcc -g stack.c -o stack

         2. 开始调试 

           gdb stack

         3. 显示汇编代码   

0x0000000000400513 <+32>: callq 0x4004cd <f>
0x0000000000400518 <+37>: mov $0x0,%eax
其中0x0000000000400518为return address

(gdb) disas f
Dump of assembler code for function f:
   0x00000000004004cd <+0>:     push   %rbp
   0x00000000004004ce <+1>:     mov    %rsp,%rbp
   0x00000000004004d1 <+4>:     mov    %edi,-0x14(%rbp)
   0x00000000004004d4 <+7>:     mov    %esi,-0x18(%rbp)
   0x00000000004004d7 <+10>:    movl   $0x2,-0x4(%rbp)
   0x00000000004004de <+17>:    mov    -0x18(%rbp),%eax
   0x00000000004004e1 <+20>:    mov    -0x14(%rbp),%edx
   0x00000000004004e4 <+23>:    add    %eax,%edx
   0x00000000004004e6 <+25>:    mov    -0x4(%rbp),%eax
   0x00000000004004e9 <+28>:    add    %edx,%eax
   0x00000000004004eb <+30>:    mov    %eax,-0x8(%rbp)
   0x00000000004004ee <+33>:    mov    -0x8(%rbp),%eax
   0x00000000004004f1 <+36>:    pop    %rbp
   0x00000000004004f2 <+37>:    retq
End of assembler dump.
(gdb) disas main
Dump of assembler code for function main:
   0x00000000004004f3 <+0>:     push   %rbp
   0x00000000004004f4 <+1>:     mov    %rsp,%rbp
   0x00000000004004f7 <+4>:     sub    $0x10,%rsp
   0x00000000004004fb <+8>:     movl   $0x1,-0x4(%rbp)
   0x0000000000400502 <+15>:    movl   $0x2,-0x8(%rbp)
   0x0000000000400509 <+22>:    mov    -0x8(%rbp),%edx
   0x000000000040050c <+25>:    mov    -0x4(%rbp),%eax
   0x000000000040050f <+28>:    mov    %edx,%esi
   0x0000000000400511 <+30>:    mov    %eax,%edi
   0x0000000000400513 <+32>:    callq  0x4004cd <f>
   0x0000000000400518 <+37>:    mov    $0x0,%eax
   0x000000000040051d <+42>:    leaveq
   0x000000000040051e <+43>:    retq
End of assembler dump.

            4. 设置4个断点   

(gdb) list
3       int f(int x, int y)
4       {
5           int l = 2;
6           int sum = x + y + l;
7           return sum;
8       }
9
10      int main()
11      {
12          int x = 1;
13          int y = 2;
14          f(x, y);
15          return 0;
16      }

(gdb) b 14
(gdb) b f
(gdb) b 7
(gdb) b 15

        5. 断点 1处分析,调用f前

            RSP地址比RBP要小说明,栈是向低地址方向增长的, 另外RIP存放的是f函数下一条指令。也就是return address, rdi 存放为函数f参数。

           RBP 地址 0x7fffffffe4d0

           0x7fffffffe4c0: 0xffffe5b0 0x00007fff 0x00000002 0x00000001

            可以看到函数参数 y(0x00000002) x( 0x00000001) 已经入栈了。

(gdb) run
Starting program: /root/src/stack/stack

Breakpoint 1, main () at stack.c:14
14          f(x);

(gdb)  i r rsp rbp rip
rsp            0x7fffffffe4c0   0x7fffffffe4c0
rbp            0x7fffffffe4d0   0x7fffffffe4d0
rip            0x400509 0x400509 <main+22>

(gdb)  x/20xw $rsp
0x7fffffffe4c0: 0xffffe5b0      0x00007fff      0x00000002      0x00000001
0x7fffffffe4d0: 0x00000000      0x00000000      0xf7a303d5      0x00007fff
0x7fffffffe4e0: 0x00000000      0x00000000      0xffffe5b8      0x00007fff
0x7fffffffe4f0: 0x00000000      0x00000001      0x004004f3      0x00000000
0x7fffffffe500: 0x00000000      0x00000000      0xfb5dae0a      0xab7950d3

          6. 断点 2处分析,调用f 

            完成了return address 和 上一个断点显示的rbp的入栈,RSP地址比RBP要小说明,栈是向低地址方向增长的, 另外RIP存放的是f函数下一条指令。也就是return address

            1.  return address  0x0000000000400518

            2. RBP旧地址地址 0x00007fff 0xffffe4d0  (见断点1)

(gdb) cont
(gdb)  i r rsp rbp rip rdi rsi
rsp            0x7fffffffe4b0   0x7fffffffe4b0
rbp            0x7fffffffe4b0   0x7fffffffe4b0
rip            0x4004d7 0x4004d7 <f+10>
rdi            0x1      1
rsi            0x2      2

(gdb)  x/20xw $rsp
0x7fffffffe4b0: 0xffffe4d0      0x00007fff      0x00400518      0x00000000
0x7fffffffe4c0: 0xffffe5b0      0x00007fff      0x00000002      0x00000001
0x7fffffffe4d0: 0x00000000      0x00000000      0xf7a303d5      0x00007fff
0x7fffffffe4e0: 0x00000000      0x00000000      0xffffe5b8      0x00007fff
0x7fffffffe4f0: 0x00000000      0x00000001      0x004004f3      0x00000000

          7. 断点 3处分析,f返回前

          f执行本地变量入栈,本地变量l 放到了“red zone”, rsp并没有移动

     0x7fffffffe4a0: 0x00000000 0x00000000 0x00000005 0x00000002
(gdb) cont
Continuing.

Breakpoint 3, f (y=1) at stack.c:7
7           return 0;
(gdb) i r rsp rbp rip rdi rsi
rsp            0x7fffffffe4b0   0x7fffffffe4b0
rbp            0x7fffffffe4b0   0x7fffffffe4b0
rip            0x4004ee 0x4004ee <f+33>
rdi            0x1      1
rsi            0x2      2

(gdb)  x/20xw $rsp
0x7fffffffe4b0: 0xffffe4d0      0x00007fff      0x00400518      0x00000000
0x7fffffffe4c0: 0xffffe5b0      0x00007fff      0x00000002      0x00000001
0x7fffffffe4d0: 0x00000000      0x00000000      0xf7a303d5      0x00007fff
0x7fffffffe4e0: 0x00000000      0x00000000      0xffffe5b8      0x00007fff
0x7fffffffe4f0: 0x00000000      0x00000001      0x004004f3      0x00000000


(gdb)  x/20xw 0x7fffffffe4a0
0x7fffffffe4a0: 0x00000000      0x00000000      0x00000005      0x00000002
0x7fffffffe4b0: 0xffffe4d0      0x00007fff      0x00400518      0x00000000
0x7fffffffe4c0: 0xffffe5b0      0x00007fff      0x00000002      0x00000001
0x7fffffffe4d0: 0x00000000      0x00000000      0xf7a303d5      0x00007fff
0x7fffffffe4e0: 0x00000000      0x00000000      0xffffe5b8      0x00007fff

          8. 断点 4处分析,main返回前

           f执行完毕, RBP恢复断点1时的 状态

(gdb) cont
Continuing.

Breakpoint 4, main () at stack.c:15
15          return 0;
(gdb) i r rsp rbp rip rdi rsi
rsp            0x7fffffffe4c0   0x7fffffffe4c0
rbp            0x7fffffffe4d0   0x7fffffffe4d0
rip            0x400518 0x400518 <main+37>
rdi            0x1      1
rsi            0x2      2

(gdb)  x/20xw $rsp
0x7fffffffe4c0: 0xffffe5b0      0x00007fff      0x00000002      0x00000001
0x7fffffffe4d0: 0x00000000      0x00000000      0xf7a303d5      0x00007fff
0x7fffffffe4e0: 0x00000000      0x00000000      0xffffe5b8      0x00007fff
0x7fffffffe4f0: 0x00000000      0x00000001      0x004004f3      0x00000000
0x7fffffffe500: 0x00000000      0x00000000      0xfb5dae0a      0xab7950d3
  • 使用gdb调试6个以上参数

        根据极客时间刘超《趣谈操作系统》14节, 如果栈帧超过6个,也会入栈只是,是被调用函数入栈,这里验证一下。代码如下:

[root@centosgpt stack]# cat stack.c
#include <stdio.h>

int f(int x, int y, int a, int b,
      int c, int d, int e, int g)
{
    int l = 2;
    int sum = x + y + l + a + b + c + d + e + g;
    return sum;
}

int main()
{
    int x = 1;
    int y = 2;
    int a = 3;
    int b = 3;
    int c = 3;
    int d = 3;
    int e = 3;
    int g = 3;
    f(x, y, a, b, c, d, e, g);
    return 0;
}

 1. 设置断点   

                 在f调用过程前后设置断点

(gdb) list
4             int c, int d, int e, int g)
5       {
6           int l = 2;
7           int sum = x + y + l + a + b + c + d + e + g;
8           return sum;
9       }
10
11      int main()
12      {
13          int x = 1;
(gdb) list
14          int y = 2;
15          int a = 3;
16          int b = 4;
17          int c = 5;
18          int d = 6;
19          int e = 7;
20          int g = 8;
21          f(x, y, a, b, c, d, e, g);
22          return 0;
23      }
(gdb) list
Line number 24 out of range; stack.c has 23 lines.
(gdb) b 21
Breakpoint 1 at 0x40055f: file stack.c, line 21.
(gdb) b f
Breakpoint 2 at 0x4004e5: file stack.c, line 6.
(gdb) b 8
Breakpoint 3 at 0x40051a: file stack.c, line 8.
(gdb) b 22
Breakpoint 4 at 0x400587: file stack.c, line 22.
(gdb) run

 2. 断点1 调用f前分析

              x,y,a,b,c,d,e,g分别放入 rbp-4,rbp-8, rbp-c, rbp-10, rbp-14, rbp-18,rbp-1c,rbp-20,

             其中x,y,a,b,c,d, 后存入eax,esi,edx,ecx,  r8,r9;

             e,g 分别借助edi 放入rsp, rsp+8, 注意这个地方后续再f中会引用, 

             大于6个参数部分, 调用函数会单独把超过6个参数的,放入栈中。

             (调试过程中, 使用ni 执行callq 前)

(gdb) i r rsp rbp
rsp            0x7fffffffe4a0   0x7fffffffe4a0
rbp            0x7fffffffe4d0   0x7fffffffe4d0

(gdb) x/20xw $rsp
0x7fffffffe4a0: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffe4b0: 0x00000008      0x00000007      0x00000006      0x00000005
0x7fffffffe4c0: 0x00000004      0x00000003      0x00000002      0x00000001
0x7fffffffe4d0: 0x00000000      0x00000000      0xf7a303d5      0x00007fff
0x7fffffffe4e0: 0x00000000      0x00000000      0xffffe5b8      0x00007fff

(gdb) ni
...
(gdb) disas
Dump of assembler code for function main:
   0x000000000040051f <+0>:     push   %rbp
   0x0000000000400520 <+1>:     mov    %rsp,%rbp
   0x0000000000400523 <+4>:     sub    $0x30,%rsp
   0x0000000000400527 <+8>:     movl   $0x1,-0x4(%rbp)
   0x000000000040052e <+15>:    movl   $0x2,-0x8(%rbp)
   0x0000000000400535 <+22>:    movl   $0x3,-0xc(%rbp)
   0x000000000040053c <+29>:    movl   $0x4,-0x10(%rbp)
   0x0000000000400543 <+36>:    movl   $0x5,-0x14(%rbp)
   0x000000000040054a <+43>:    movl   $0x6,-0x18(%rbp)
   0x0000000000400551 <+50>:    movl   $0x7,-0x1c(%rbp)
   0x0000000000400558 <+57>:    movl   $0x8,-0x20(%rbp)
   0x000000000040055f <+64>:    mov    -0x18(%rbp),%r9d
   0x0000000000400563 <+68>:    mov    -0x14(%rbp),%r8d
   0x0000000000400567 <+72>:    mov    -0x10(%rbp),%ecx
   0x000000000040056a <+75>:    mov    -0xc(%rbp),%edx
   0x000000000040056d <+78>:    mov    -0x8(%rbp),%esi
   0x0000000000400570 <+81>:    mov    -0x4(%rbp),%eax
   0x0000000000400573 <+84>:    mov    -0x20(%rbp),%edi
   0x0000000000400576 <+87>:    mov    %edi,0x8(%rsp)
   0x000000000040057a <+91>:    mov    -0x1c(%rbp),%edi
   0x000000000040057d <+94>:    mov    %edi,(%rsp)
   0x0000000000400580 <+97>:    mov    %eax,%edi
=> 0x0000000000400582 <+99>:    callq  0x4004cd <f>
   0x0000000000400587 <+104>:   mov    $0x0,%eax
   0x000000000040058c <+109>:   leaveq
   0x000000000040058d <+110>:   retq

(gdb) i r rsp rbp
rsp            0x7fffffffe4a0   0x7fffffffe4a0
rbp            0x7fffffffe4d0   0x7fffffffe4d0

(gdb) x/20xw $rsp
0x7fffffffe4a0: 0x00000007      0x00000000      0x00000008      0x00000000
0x7fffffffe4b0: 0x00000008      0x00000007      0x00000006      0x00000005
0x7fffffffe4c0: 0x00000004      0x00000003      0x00000002      0x00000001
0x7fffffffe4d0: 0x00000000      0x00000000      0xf7a303d5      0x00007fff
0x7fffffffe4e0: 0x00000000      0x00000000      0xffffe5b8      0x00007fff

 2. 断点2 进入f分析

             0x7fffffffe490: 0xffffe4d0 0x00007fff 0x00400587 0x00000000

            rbp入栈, return address入栈

(gdb) i r rsp rbp eax esi edx ecx r8 r9
rsp            0x7fffffffe490   0x7fffffffe490
rbp            0x7fffffffe490   0x7fffffffe490
eax            0x1      1
esi            0x2      2
edx            0x3      3
ecx            0x4      4
r8             0x5      5
r9             0x6      6


(gdb) x/20xw $rsp
0x7fffffffe490: 0xffffe4d0      0x00007fff      0x00400587      0x00000000
0x7fffffffe4a0: 0x00000007      0x00000000      0x00000008      0x00000000
0x7fffffffe4b0: 0x00000008      0x00000007      0x00000006      0x00000005
0x7fffffffe4c0: 0x00000004      0x00000003      0x00000002      0x00000001
0x7fffffffe4d0: 0x00000000      0x00000000      0xf7a303d5      0x00007fff

 3. 断点3  f执行完毕分析

         RSP, RBP栈帧没有变化,

          对于参数额x,  y,  a, b , c, d , 存入 rbp-14 到  rbp-28区域

          对于参数额e,g 直接取了rbp+10, rbp+18, 

         rbp当前值  0x7fffffffe490  + 10 = 0x7fffffffe4a0:

         0x7fffffffe4a0: 0x00000007 0x00000000 0x00000008 0x00000000

(gdb)  i r rsp rbp eax esi edx ecx r8 r9
rsp            0x7fffffffe490   0x7fffffffe490
rbp            0x7fffffffe490   0x7fffffffe490
eax            0x26     38
esi            0x2      2
edx            0x1e     30
ecx            0x4      4
r8             0x5      5
r9             0x6      6


(gdb)  x/32xw  0x7fffffffe460
0x7fffffffe460: 0x00000000      0x00000000      0x00000006      0x00000005
0x7fffffffe470: 0x00000004      0x00000003      0x00000002      0x00000001
0x7fffffffe480: 0x00000000      0x00000000      0x00000026      0x00000002
0x7fffffffe490: 0xffffe4d0      0x00007fff      0x00400587      0x00000000
0x7fffffffe4a0: 0x00000007      0x00000000      0x00000008      0x00000000
0x7fffffffe4b0: 0x00000008      0x00000007      0x00000006      0x00000005
0x7fffffffe4c0: 0x00000004      0x00000003      0x00000002      0x00000001
0x7fffffffe4d0: 0x00000000      0x00000000      0xf7a303d5      0x00007fff

(gdb) disas
Dump of assembler code for function f:
.... ;
;x, y, a, b, c, d 放入red zone
   0x00000000004004d1 <+4>:     mov    %edi,-0x14(%rbp)
   0x00000000004004d4 <+7>:     mov    %esi,-0x18(%rbp)
   0x00000000004004d7 <+10>:    mov    %edx,-0x1c(%rbp)
   0x00000000004004da <+13>:    mov    %ecx,-0x20(%rbp)
   0x00000000004004dd <+16>:    mov    %r8d,-0x24(%rbp)
   0x00000000004004e1 <+20>:    mov    %r9d,-0x28(%rbp)

;本地变量入栈
   0x00000000004004e5 <+24>:    movl   $0x2,-0x4(%rbp)

; e,g 从原有堆栈获取
...
   0x000000000040050d <+64>:    mov    0x10(%rbp),%eax
   0x0000000000400510 <+67>:    add    %eax,%edx
   0x0000000000400512 <+69>:    mov    0x18(%rbp),%eax
   0x0000000000400515 <+72>:    add    %edx,%eax
...

 4. 断点4  f执行完毕返回main分析

           f执行完毕, RBP恢复断点1时的 状态

(gdb) i r rsp rbp eax esi edx ecx r8 r9
rsp            0x7fffffffe4a0   0x7fffffffe4a0
rbp            0x7fffffffe4d0   0x7fffffffe4d0
eax            0x26     38
esi            0x2      2
edx            0x1e     30
ecx            0x4      4
r8             0x5      5
r9             0x6      6

(gdb) x/32xw $rsp
0x7fffffffe4a0: 0x00000007      0x00000000      0x00000008      0x00000000
0x7fffffffe4b0: 0x00000008      0x00000007      0x00000006      0x00000005
0x7fffffffe4c0: 0x00000004      0x00000003      0x00000002      0x00000001
0x7fffffffe4d0: 0x00000000      0x00000000      0xf7a303d5      0x00007fff
0x7fffffffe4e0: 0x00000000      0x00000000      0xffffe5b8      0x00007fff
0x7fffffffe4f0: 0x00000000      0x00000001      0x0040051f      0x00000000
0x7fffffffe500: 0x00000000      0x00000000      0xbf8a8500      0x1f3a00e6
0x7fffffffe510: 0x004003e0      0x00000000      0xffffe5b0      0x00007fff

总结:

            本文是极客时间刘超《趣谈操作系统》课堂联系, 函数栈在函数调用过程中用于传递函数参数, 函数返回地址, 及本地变量, 在实验的过程中发现编译器会在编译的过程中添加调试信息CFI, 也验证了专栏讲到 “的rdi、rsi、rdx、rcx、r8、r9 这 6 个寄存器用于传递存储函数调用时的 6 个参数。如果超过 6 的时候,还会用到栈”。

            在实验中发现参数传递使用了寄存器,使用时还是会放入栈中进行运算

            另外与专栏里讲到的不太一样的的是, 传递参数大于6个的,会被单独的放入栈顶,不过这个操作是调用函数做的。

            当传递参数大于6个的时候, 前六个参数是被调用函数通过寄存器,放入自己的栈中,大于6个的参数由调用函数压栈。 

 


参考:

Where the top of the stack is on x86

Stack frame layout on x86-64

Stack analysis with GDB

GAS: Explanation of .cfi_def_cfa_offset

What are CFI directives in Gnu Assembler (GAS) used for?

CFI-directives

CFI directives in assembly files

leave

(ABI) for AMD64 – Intel® Developer Zone

 

Be First to Comment

发表回复