函数调用栈简单认识(美化版)

函数调用的栈帧

每一个函数都有一块栈空间,叫做栈帧,研究函数调用栈其实就是研究主调函数与被调函数栈帧之间的问题

栈帧的作用是保存并传递被调函数的参数、被调函数的返回地址(也就是主调函数中调用完被调函数后应该执行的下一句)、被调函数的返回值、保存函数的局部变量

AMD64 CPU 提供了2个与栈相关的寄存器:

  • rsp寄存器, 始终指向函数调用栈栈顶

  • rbp寄存器, 一般用来指向函数栈帧的开始位置

举一个例子:A( )->B( )->C( )

 

 

 

这里就可以发现几点信息:

(1)被调函数的返回值和返回地址保存在主调函数中,而被调函数中主要保存函数的临时变量

(2)栈的生长方向是由高到低,但是数据的填充方向是由低到高的

(3)rsp和rbp寄存器是唯一的,两者永远指向当前函数的栈顶和栈底

函数调用的汇编

常见的汇编指令

call/ret

 call  目标地址
 ret

call指令在函数调用的过程中的作用是将当前rip的值压入栈帧中(被调函数的返回地址),然后将rip的值修改为目标地址,因为rip寄存器的主要作用是存储计算机下一条执行指令的地址,所以这一步之后开始执行目标地址的指令

ret指令与call指令相反,ret指令将栈中rip的值弹出栈,从而保证主调函数能够接着执行

leave

leave 指令没有操作数, 它一般放在函数的尾部 ret 指令之前, 用于调整 rsp 和 rbp, 这条指令相当于:

 mov %rbp, %rsp//将rbp的值赋值给rsp
 pop %rbp//将rbp出栈

在执行 ret 指令之前, 需要确保 rbp 寄存器的值已经恢复到 "被调用函数栈帧的起始位置", rsp 寄存器的值是 "被调用函数的 栈帧的结束位置(也就是调用函数的开始位置)". (leave指令就可以实现该功能)

pop/push

先来介绍一下这两个指令的意义,这两个指令的作用在于当发生函数调用时,主调函数尚未执行完成,因此被调函数不能将主调函数的栈帧覆盖,所以需要push和pop指令来创建新的栈帧来存储被调函数的信息

 push 源操作数
 pop 目标操作数

push和pop指令都会修改rsp的值

push 入栈时, rsp 寄存器的值先减去 8 把栈位置留出来, 然后把操作数复制到 rsp 所指定位置. push 指令相当于:

 sub $8, %rsp
 mov 源操作数, 0(%rsp)

pop 出栈时, 先把 rsp 寄存器所指位置的数据复制到目的操作数中, 然后 rsp 寄存器的值加 8. pop 指令相当于:

 mov 0(%rsp), 目标操作数
 add $8, %rsp

函数调用的实例

程序实例

 #include <stdio.h>
 
 int sum(int a, int b)
 {
     int s = a + b;
 
     return s;
 }
 
 int main(int argc, char *argv[])
 {
     int n = sum(1, 2);
 
     printf("n: %d\n", n);
 
     return 0;
 }

gdb调试

gdb 调试命令:

- run, r : 运行程序, 当遇到断点后, 程序会在断点处停止运行, 等待用户输入下一步命令 - continue, c : 继续执行, 到下一个断点处(或运行结束) - next, n : 单步调试, 如果有函数调用, 不进入此函数体 - step, s : 单步调试, 如果有函数调用, 则进入函数 - finish : 运行程序, 知道当前函数完成后返回, 并打印函数返回时堆栈地址和返回值以及参数值等信息 - quit, q : 退出 gdb

断点设置:

- break , b : 在第n行处设置断点 - b : 在函数 func() 的入口设置断点, 如: b main - delete <断点号n>: 删除第n个断点 - disable/enable <断点号n>: 暂停/启用第n个断点

- info b : 显示断点设置状况

打印表达式: print <表达式>, p <表达式>: 其中的"表达式"是当前测试程序的有效表达式, 例如: p var(打印变量var的值), p fun(22) 调用函数fun() display <表达式>: 在每次单步进行指令后, 紧接着输出被设置的表达式及值. 如 dispaly a

info function: 查询函数 info : 显示当前堆栈页的所有变量.

程序调试

 Dump of assembler code for function main:
 => 0x0000555555554664 <+0>:    push   rbp
    0x0000555555554665 <+1>:    mov    rbp,rsp
    0x0000555555554668 <+4>: sub    rsp,0x20
    0x000055555555466c <+8>:    mov    DWORD PTR [rbp-0x14],edi
    0x000055555555466f <+11>: mov    QWORD PTR [rbp-0x20],rsi
    0x0000555555554673 <+15>: mov    esi,0x2
    0x0000555555554678 <+20>: mov    edi,0x1
    0x000055555555467d <+25>: call   0x55555555464a <sum>
    0x0000555555554682 <+30>: mov    DWORD PTR [rbp-0x4],eax
    0x0000555555554685 <+33>: mov    eax,DWORD PTR [rbp-0x4]
    0x0000555555554688 <+36>: mov    esi,eax
    0x000055555555468a <+38>: lea    rdi,[rip+0xa3]        # 0x555555554734
    0x0000555555554691 <+45>: mov    eax,0x0
    0x0000555555554696 <+50>: call   0x555555554520 <printf@plt>
    0x000055555555469b <+55>: mov    eax,0x0
    0x00005555555546a0 <+60>: leave  
    0x00005555555546a1 <+61>: ret    
 i r  rbp  rsp  rip//观察rbp、rsp、rip寄存器的值
 rsp            0x7fffffffe028 0x7fffffffe028
 rbp            0x5555555546b0 0x5555555546b0 <__libc_csu_init>
 rip            0x555555554664 0x555555554664 <main>

开始执行第一条指令:push rbp,push指令将rsp减8,使栈空间向低地址生长,扩充空间,存储主调函数(也就是调用main函数的函数)的帧基地址的信息

 i r rsp rbp  rip
 rsp            0x7fffffffe020 0x7fffffffe020//rsp的值减去8
 rbp            0x5555555546b0 0x5555555546b0 <__libc_csu_init>
 rip            0x555555554665 0x555555554665 <main+1>

紧接着执行下一条指令: mov rbp, rsp,这条指令将rsp的值赋值给了rbp,现在rbp和rsp同时指向一个位置

 i r rsp rbp  rip
 rsp            0x7fffffffe020 0x7fffffffe020
 rbp            0x7fffffffe020 0x7fffffffe020
 rip            0x555555554668 0x555555554668 <main+4>

sub rsp, 0x20,将rsp减去20字节,继续扩充栈帧

 i r rsp rbp  rip
 rsp            0x7fffffffe000 0x7fffffffe000
 rbp            0x7fffffffe020 0x7fffffffe020
 rip            0x55555555466c 0x55555555466c <main+8>
 0x55555555466c <main+8>     mov    dword ptr [rbp - 0x14], edi//向被调函数sum传递参数
 0x55555555466f <main+11>    mov    qword ptr [rbp - 0x20], rsi//默认是rdi、rsi、rdx顺序
 0x555555554673 <main+15>    mov    esi, 2
 0x555555554678 <main+20>    mov    edi, 1
 i r rip rsp rbp
 rip            0x55555555467d 0x55555555467d <main+25>
 rsp            0x7fffffffe000 0x7fffffffe000
 rbp            0x7fffffffe020 0x7fffffffe020

接下来开始调用sum函数

 0x55555555467d <main+25>    call   sum                <sum>
 rip            0x555555554682   0x555555554682 <main+30>
 rsp            0x7fffffffe000 0x7fffffffe000
 rbp            0x7fffffffe020 0x7fffffffe020

call指令先将下一条指令的值压入栈中,之后修改rip为目标地址

之后是sum函数的执行流程

 Dump of assembler code for function sum:
    0x000055555555464a <+0>:    push   rbp
    0x000055555555464b <+1>: mov    rbp,rsp
    0x000055555555464e <+4>: mov    DWORD PTR [rbp-0x14],edi
    0x0000555555554651 <+7>: mov    DWORD PTR [rbp-0x18],esi
    0x0000555555554654 <+10>: mov    edx,DWORD PTR [rbp-0x14]
    0x0000555555554657 <+13>: mov    eax,DWORD PTR [rbp-0x18]
    0x000055555555465a <+16>: add    eax,edx
    0x000055555555465c <+18>: mov    DWORD PTR [rbp-0x4],eax
    0x000055555555465f <+21>: mov    eax,DWORD PTR [rbp-0x4]
    0x0000555555554662 <+24>: pop    rbp
    0x0000555555554663 <+25>: ret    
 End of assembler dump.

sum 函数并未像 main 函数一样通过调整 rsp 寄存器的值来给 sum 函数预留用于局部变量和临时变量的栈空间, 那这是不是说明 sum 函数就没有使用栈来保存局部变量呢? 其实不是, 从后面的分析看到, sum 函数的局部变量 s 还是保存在栈上的. 没有预留为什 么可以使用呢, 原因在于栈上的内存不需要在应用层代码中分配, 操作系统已经给我们分配好了, 可以尽管使用. main函数需要调整 rsp寄存器的值是因为它需要使用 call 指令来调用 sum 函数, 而 call 指令会自动把rsp的值减去 8 然后把函数的返回地址保存到 rsp所指定的栈内存位置(即, call指令会自动将当前的rip的地址入栈), 如果 main 函数不调整 rsp 的值, 则 call 指令保存函数 返回地址时会覆盖局部变量或临时变量的值; 而 sum 函数中没有任何指令会自动使用 rsp 寄存器来保存数据到栈上, 所以不需要调整 rsp寄存器.

 ret

ret指令将之前压入栈中的rip的值取出,然后将rip的值加8

    0x555555554691 <main+45>    mov    eax, 0//返回sum函数返回值

然后是printf函数

 0x0000000000400561 <+33>:mov   -0x4(%rbp),%eax # 将变量 n 的值放入 eax
 0x0000000000400564 <+36>:mov   %eax,%esi       # 函数 printf 的第2个参数
 0x0000000000400566 <+38>:mov   $0x400604, %edi # 函数 printf 的第1个参数
 0x000000000040056b <+43>:mov   $0x0,%eax       # 函数 printf 返回值
 0x0000000000400570 <+48>:callq 0x400400 <printf@plt> # 函数调用
 0x0000000000400575 <+53>:mov   $0x0,%eax       # main 函数返回值

然后是leave指令, leave 指令先把 rbp 寄存器的值复制给 rsp, 这样, rsp 就指向了 rbp 的栈单元, 然后使该内存单元当中的值 POP出给 rbp 寄存器, 这样 rbp 和 rsp 就恢复到刚进入 main 函数时的状态了.

 rip            0x5555555546a1   0x5555555546a1 <main+61>
 rsp            0x7fffffffe028 0x7fffffffe028
 rbp            0x5555555546b0 0x5555555546b0 <__libc_csu_init>

到此, main 函数就只剩下 retq 指令了, 该条指令前面 sum 已经分析过了, 这条指令指向完成之后就会返回到调用 main 函数的函 数中继续执行.

posted @ 2023-05-25 20:17  alexlance  阅读(449)  评论(0编辑  收藏  举报