C温故补缺(十五):栈帧

栈帧

概念

栈帧:也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构,每次函数的调用,都会在调用栈(call stack)上维护一个独立的栈帧(stack frame)

栈帧的内容

  • 函数的返回地址和参数

  • 临时变量:包括函数的非静态局部变量,以及编译器自动生成的其他临时变量

  • 函数调用上下文

  • 两个指针:ebp又称帧指针(frame pointer),指向当前栈帧的底部; esp,又称栈针织(stack pointer),始终指向栈顶

函数调用

函数调用过程中分:函数调用者(caller)和被调用的函数(callee)

调用者需要知道被调用函数的返回值

被调用函数需要知道传入的参数和返回的地址

步骤:

  1. 参数入栈:将参数按照调用约定依次压入系统栈

  2. 返回地址入栈:将当前代码区调用指令的下一条指令地址压入栈,供函数返回时继续执行,也就是保护现场和恢复现场

  3. 代码跳转:处理器将代码区跳转到被调用函数的入口处

  4. 栈帧调整:

  5. 将调用者的ebp压入栈,保存指向栈底ebp地址(用于恢复现场),此时esp指向新的栈顶位置

  6. 将当前栈帧切换到新栈帧(将esp值装入ebp,跟新栈底),此时ebp指向栈顶,

  7. 给新栈帧分配空间

函数返回

步骤:

  1. 保存被调用函数的返回值到eax寄存器

  2. 恢复esp同时收回局部变量空间

  3. 将上一个栈帧底部位置恢复到ebp

  4. 弹出当前元素栈顶元素,从栈中取到返回地址,并跳转到该位置,也就是再回到调用者,执行后续代码

举例说明

c代码:

int sum(int x,int y){
    int z=x+y;
    return z;
}
int main(){
    int a=1;
    int b=3;
    int c=sum(a,b);
}

汇编,且关闭编译器优化-O0

    .file    "test.c"
    .text
    .globl    sum
    .def    sum;    .scl    2;    .type    32;    .endef
    .seh_proc    sum
sum:
    pushq    %rbp
    .seh_pushreg    %rbp
    movq    %rsp, %rbp
    .seh_setframe    %rbp, 0
    subq    $16, %rsp
    .seh_stackalloc    16
    .seh_endprologue
    movl    %ecx, 16(%rbp)
    movl    %edx, 24(%rbp)
    movl    16(%rbp), %edx
    movl    24(%rbp), %eax
    addl    %edx, %eax
    movl    %eax, -4(%rbp)
    movl    -4(%rbp), %eax
    addq    $16, %rsp
    popq    %rbp
    ret
    .seh_endproc
    .def    __main;    .scl    2;    .type    32;    .endef
    .globl    main
    .def    main;    .scl    2;    .type    32;    .endef
    .seh_proc    main
main:
    pushq    %rbp
    .seh_pushreg    %rbp
    movq    %rsp, %rbp
    .seh_setframe    %rbp, 0
    subq    $48, %rsp
    .seh_stackalloc    48
    .seh_endprologue
    call    __main
    movl    $1, -4(%rbp)
    movl    $3, -8(%rbp)
    movl    -8(%rbp), %edx
    movl    -4(%rbp), %eax
    movl    %eax, %ecx
    call    sum
    movl    %eax, -12(%rbp)
    movl    $0, %eax
    addq    $48, %rsp
    popq    %rbp
    ret
    .seh_endproc
    .ident    "GCC: (x86_64-posix-seh-rev0, Built by MinGW-W64 project) 8.1.0"

过程详解:

补充:整个过程中虽然看不到RIP,但它一直被使用,RIP每次都指向下一条将要运行的指令

每次取出一条指令时,RIP就会自动偏移,指向下一条指令,如图:

当发生函数调用时,也就是call时,callq 指令会自动将rip压入栈,并将rip指向被调用的函数,如

先RIP指向 callq f(),下一次执行就是调用函数f(),查看此时的rsp

接着执行该指令

ip跳到了f()内的第一条指令push %rbq,再查看rsp

rsp中存入了0x00401586,正是call的下一条指令的位置

且也可以查看变量在栈帧内的存储形式

将1赋值给变量b,即mov 1 -4(%bp),查看内存

就是在bp的偏移4字节处

再来看ret,ret是将之前存的RIP给出栈,经过sub分配空间然后再add释放空间,pop rbp后,rsp刚好在旧的rip处

此时执行ret,会自动执行pop rip,也就恢复了现场

也就是说:

call f的本质是:

push %rip
mov f,%rip

ret的本质是:

pop %rip
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值