hook leaveq retq

  Linux中的局部变量和栈  2015-01-08 13:52:05

分类: LINUX

本文主要关注Linux环境中局部变量存放的位置,以及栈中的数据分布。
一、问题:
1、局部变量存放在栈中,动态分配的内存存放在堆中,看似一个基本常识,但事实真是如此么?
2、栈中的数据是临时的,当函数退出时会自动释放,也是一个基本常识,但这是如何做到的呢?
3、栈中的具体数据分布如何?

二、概念及原理
1、通常情况下,局部变量确实放在栈中,动态分配的内存确实放在堆中,但这不是绝对的,这里面有一些关键点需要主要:
  1)动态分配的内存是在程序运行时,动态分配的,其分配和管理是由C库和内核负责的,对Linux来说,确实在堆中分配(使用brk和mmap系统调用分配),具体的分配算法和管理维护由具体的C库决定,比如glibc和google的tcmalloc的差异就非常大。
  2)局部变量和栈的使用和分配,是由编译器决定的,也就是在程序运行前就决定了,是静态的,跟程序自身和Linux内核关系不大。不同的编译器对于局部变量和栈的使用和处理方式上有差别,只是通常会将局部变量放在栈中,但不是一定会这样做。有时局部变量也不使用栈,而直接使用寄存器(见后面的例子)。
2、每个函数都有自己的栈帧,即每个函数都会使用属于自己的一段栈空间,指向栈帧的指针存放在专门的寄存器RBP中,栈顶指针存放在RSP寄存器中。在进入某函数前,会先将当前IP(指令指针)的下一条指令地址压入栈中,所为该函数的返回地址;当进入某函数后,通常会先执行如下两条汇编指令:
push   %rbp
mov    %rsp,%rbp
第一条指令将当前栈帧指针寄存器EBP压栈,此时的EBP是指向上一级函数的栈帧的(即指向上一级函数占用的栈区域的最开始的地址)。
第二条指令将当前的栈顶指针(实质为上一级函数的堆栈的末尾)的值赋给RBP,此后RBP即指向了上级函数的堆栈的末尾。
进入该函数后,在其中分配的局部变量通常在栈中分配,当函数返回时,如何丢弃掉这些使用的局部变量,使其恢复到上级函数的栈状态呢?
答案是利用进入函数时保存的RBP。在函数返回时,通常会执行如下两条汇编指令:
movq %rbp, %rsp
popq %rbp
这两条指令,跟进入该函数时执行的两条指令是对应的。
第一条指令将当前的RBP寄存器的值赋给RSP,也就是说使当前的栈顶指针指向RBP指向的位置,而此时RBP指向的就是“上级函数的堆栈的末尾”,如此RSP就指向回了上级函数的栈顶了,就恢复到进入该函数之前的堆栈状态了。
第二条指令从堆栈中弹出之前压入栈的RBP,之前压入栈的RBP是执行上上级函数堆栈末尾的,如此,当上级函数返回时,就可以再次利用RBP,使栈顶指针RSP指向上上级函数的栈顶了。

3、通常情况下(不同编译器和编译选项下,情况可能不同),栈中的数据分布如下:
栈顶 --> -------
             ......
         子函数局部变量1
子函数局部变量2
         上级函数RBP(栈帧指针)
         子函数返回地址
         子函数参数1
         子函数参数2
  父函数局部变量1
父函数局部变量2
         上级函数RBP(栈帧指针)
         父函数返回地址
         父函数参数1
         父函数参数2
          ......

三、实例
1、局部变量在栈中分配实例
1)简单的C程序local-var.c

点击(此处)折叠或打开

  1. #include <stdio.h>
  2. void test1(){
  3.         int test_lvar=0;
  4.         int local_var=254;
  5.         int local_var1=1;


  6. }

  7. void test(){
  8.         int test_lvar=0;
  9.         int local_var=254;
  10.         int local_var1=1;
  11.         unsigned int status = 2;
  12.         test1();
  13. }

  14. int main(){
  15.         int local_var=254;
  16.         int local_var1=1;
  17.         unsigned int status = 2;
  18.         test();
  19. return 0;
  20. }

2)编译:gcc local-var.c
3)
反汇编查看堆栈分配使用情况:objdump -d a.out

点击(此处)折叠或打开

  1. 0000000000400474 <test1>:
  2.   400474:    55 push %rbp
  3.   400475:    48 89 e5 mov %rsp,%rbp
  4.   400478:    c7 45 f4 00 00 00 00 movl $0x0,-0xc(%rbp)
  5.   40047f:    c7 45 f8 fe 00 00 00 movl $0xfe,-0x8(%rbp)
  6.   400486:    c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp)
  7.   40048d:    c9 leaveq 
  8.   40048e:    c3 retq 

  9. 000000000040048f <test>:
  10.   40048f:    55 push %rbp
  11.   400490:    48 89 e5 mov %rsp,%rbp
  12.   400493:    48 83 ec 10     sub $0x10,%rsp
  13.   400497:    c7 45 f0 00 00 00 00 movl $0x0,-0x10(%rbp)
  14.   40049e:    c7 45 f4 fe 00 00 00 movl $0xfe,-0xc(%rbp)
  15.   4004a5:    c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp)
  16.   4004ac:    c7 45 fc 02 00 00 00 movl $0x2,-0x4(%rbp)
  17.   4004b3:    b8 00 00 00 00 mov $0x0,%eax
  18.   4004b8:    e8 b7 ff ff ff callq 400474 <test1>
  19.   4004bd:    c9 leaveq 
  20.   4004be:    c3 retq 

  21. 00000000004004bf <main>:
  22.   4004bf:    55 push %rbp
  23.   4004c0:    48 89 e5 mov %rsp,%rbp
  24.   4004c3:    48 83 ec 10     sub $0x10,%rsp
  25.   4004c7:    c7 45 f4 fe 00 00 00 movl $0xfe,-0xc(%rbp)
  26.   4004ce:    c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp)
  27.   4004d5:    c7 45 fc 02 00 00 00 movl $0x2,-0x4(%rbp)
  28.   4004dc:    b8 00 00 00 00 mov $0x0,%eax
  29.   4004e1:    e8 a9 ff ff ff callq 40048f <test>
  30.   4004e6:    c9 leaveq 
  31.   4004e7:    c3 retq 
  32.   4004e8:    90 nop
  33.   4004e9:    90 nop
  34.   4004ea:    90 nop
  35.   4004eb:    90 nop
  36.   4004ec:    90 nop
  37.   4004ed:    90 nop
  38.   4004ee:    90 nop
  39.   4004ef:    90 nop

可以清楚的看到所有函数的局部变量都在堆栈中分配。
但是,在函数退出时,为啥没有看到恢复RBP和RSP指令呢?
答案是用了leaveq指令:
leaveq和retq中的q是指64位操作数。
leaveq相当于:
movq %rbp, %rsp
popq %rbp
leaveq跟函数进入时的如下操作是对应的:
push   %rbp
mov    %rsp,%rbp
有些指令集也把上述的两条指令叫做enterq。

retq相当于:
popq %rip
而与retq对应的是callq,相当于:
pushq %rip
jmpq addr
这里也体现了call指令和jmp指令的区别:call指令会自动将返回地址压栈,而jmp指令不会,也就是说call指令执行完后会回来,而jmp后就不会回来了。

2、局部变量不在栈中分配实例
在内核中,经常简单局部变量不在栈中分配的实例,这种情况下,通常是通过寄存器直接保存局部变量,可能是内核或相关编译选项的优化,具体没去深入研究了,如下示例做参考。
该示例通过crash工具分析vmcore所得,函数代码为:

点击(此处)折叠或打开

  1. irqreturn_t handle_IRQ_event(unsigned int irq, struct pt_regs *regs,
  2.                  struct irqaction *action)
  3. {
  4.     irqreturn_t ret, retval = IRQ_NONE;
  5.     unsigned int status = 0;


  6.     trace_irq_entry(irq, regs);


  7.     handle_dynamic_tick(action);


  8.     if (!(action->flags & IRQF_DISABLED))
  9.         local_irq_enable_in_hardirq();


  10.     do {
  11.         ret = action->handler(irq, action->dev_id, regs);
  12.         if (ret == IRQ_HANDLED)
  13.             status |= action->flags;
  14.         retval |= ret;
  15.         action = action->next;
  16.     } while (action);


  17.     if (status & IRQF_SAMPLE_RANDOM)
  18.         add_interrupt_randomness(irq);
  19.     local_irq_disable();


  20.     trace_irq_exit(irq, retval);


  21.     return retval;
  22. }

其中局部变量ret由寄存器eax保存,而status变量用ebx保存。反汇编如下:

点击(此处)折叠或打开

  1. crash> dis -l handle_IRQ_event
  2. /usr/src/debug/kernel-2.6.18/linux-2.6.18.x86_64/kernel/irq/handle.c: 134
  3. 0xffffffff80010b30 <handle_IRQ_event>: push %r14
  4. include/trace/irq.h: 7
  5. 0xffffffff80010b32 <handle_IRQ_event+2>: cmpl $0x0,3859215(%rip) # 0xffffffff803bee48 <__tracepoint_irq_entry.10785+8>
  6. /usr/src/debug/kernel-2.6.18/linux-2.6.18.x86_64/kernel/irq/handle.c: 134
  7. 0xffffffff80010b39 <handle_IRQ_event+9>: mov %rsi,%r14
  8. 0xffffffff80010b3c <handle_IRQ_event+12>: push %r13
  9. 0xffffffff80010b3e <handle_IRQ_event+14>: push %r12
  10. 0xffffffff80010b40 <handle_IRQ_event+16>: mov %edi,%r12d
  11. 0xffffffff80010b43 <handle_IRQ_event+19>: push %rbp
  12. 0xffffffff80010b44 <handle_IRQ_event+20>: mov %rdx,%rbp
  13. 0xffffffff80010b47 <handle_IRQ_event+23>: push %rbx
  14. include/trace/irq.h: 7
  15. 0xffffffff80010b48 <handle_IRQ_event+24>: je 0xffffffff80010b68 <handle_IRQ_event+56>
  16. 0xffffffff80010b4a <handle_IRQ_event+26>: mov 3859199(%rip),%rbx # 0xffffffff803bee50 <__tracepoint_irq_entry.10785+16>
  17. 0xffffffff80010b51 <handle_IRQ_event+33>: test %rbx,%rbx
  18. 0xffffffff80010b54 <handle_IRQ_event+36>: je 0xffffffff80010b68 <handle_IRQ_event+56>
  19. 0xffffffff80010b56 <handle_IRQ_event+38>: mov %r14,%rsi
  20. 0xffffffff80010b59 <handle_IRQ_event+41>: mov %r12d,%edi
  21. 0xffffffff80010b5c <handle_IRQ_event+44>: callq *(%rbx)
  22. 0xffffffff80010b5e <handle_IRQ_event+46>: add $0x8,%rbx
  23. 0xffffffff80010b62 <handle_IRQ_event+50>: cmpq $0x0,(%rbx)
  24. 0xffffffff80010b66 <handle_IRQ_event+54>: jmp 0xffffffff80010b54 <handle_IRQ_event+36>
  25. /usr/src/debug/kernel-2.6.18/linux-2.6.18.x86_64/kernel/irq/handle.c: 142
  26. 0xffffffff80010b68 <handle_IRQ_event+56>: testb $0x20,0x8(%rbp)
  27. 0xffffffff80010b6c <handle_IRQ_event+60>: jne 0xffffffff80010b6f <handle_IRQ_event+63>
  28. include/asm/irqflags.h: 80
  29. 0xffffffff80010b6e <handle_IRQ_event+62>: sti 
  30. 0xffffffff80010b6f <handle_IRQ_event+63>: xor %r13d,%r13d
  31. 0xffffffff80010b72 <handle_IRQ_event+66>: xor %ebx,%ebx
  32. /usr/src/debug/kernel-2.6.18/linux-2.6.18.x86_64/kernel/irq/handle.c: 146
  33. 0xffffffff80010b74 <handle_IRQ_event+68>: mov 0x38(%rbp),%rsi
  34. 0xffffffff80010b78 <handle_IRQ_event+72>: mov %r14,%rdx
  35. 0xffffffff80010b7b <handle_IRQ_event+75>: mov %r12d,%edi
  36. 0xffffffff80010b7e <handle_IRQ_event+78>: callq *0x0(%rbp)
  37. /usr/src/debug/kernel-2.6.18/linux-2.6.18.x86_64/kernel/irq/handle.c: 147
  38. 0xffffffff80010b81 <handle_IRQ_event+81>: cmp $0x1,%eax
  39. 0xffffffff80010b84 <handle_IRQ_event+84>: jne 0xffffffff80010b89 <handle_IRQ_event+89>
  40. /usr/src/debug/kernel-2.6.18/linux-2.6.18.x86_64/kernel/irq/handle.c: 148
  41. 0xffffffff80010b86 <handle_IRQ_event+86>: or 0x8(%rbp),%ebx
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值