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
2)编译:gcc local-var.c
3) 反汇编查看堆栈分配使用情况:objdump -d a.out
可以清楚的看到所有函数的局部变量都在堆栈中分配。
但是,在函数退出时,为啥没有看到恢复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所得,函数代码为:
其中局部变量ret由寄存器eax保存,而status变量用ebx保存。反汇编如下:
一、问题:
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
点击(此处)折叠或打开
- #include <stdio.h>
- void test1(){
- int test_lvar=0;
- int local_var=254;
- int local_var1=1;
-
-
- }
-
- void test(){
- int test_lvar=0;
- int local_var=254;
- int local_var1=1;
- unsigned int status = 2;
- test1();
- }
-
- int main(){
- int local_var=254;
- int local_var1=1;
- unsigned int status = 2;
- test();
- return 0;
- }
2)编译:gcc local-var.c
3) 反汇编查看堆栈分配使用情况:objdump -d a.out
点击(此处)折叠或打开
- 0000000000400474 <test1>:
- 400474: 55 push %rbp
- 400475: 48 89 e5 mov %rsp,%rbp
- 400478: c7 45 f4 00 00 00 00 movl $0x0,-0xc(%rbp)
- 40047f: c7 45 f8 fe 00 00 00 movl $0xfe,-0x8(%rbp)
- 400486: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp)
- 40048d: c9 leaveq
- 40048e: c3 retq
-
- 000000000040048f <test>:
- 40048f: 55 push %rbp
- 400490: 48 89 e5 mov %rsp,%rbp
- 400493: 48 83 ec 10 sub $0x10,%rsp
- 400497: c7 45 f0 00 00 00 00 movl $0x0,-0x10(%rbp)
- 40049e: c7 45 f4 fe 00 00 00 movl $0xfe,-0xc(%rbp)
- 4004a5: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp)
- 4004ac: c7 45 fc 02 00 00 00 movl $0x2,-0x4(%rbp)
- 4004b3: b8 00 00 00 00 mov $0x0,%eax
- 4004b8: e8 b7 ff ff ff callq 400474 <test1>
- 4004bd: c9 leaveq
- 4004be: c3 retq
-
- 00000000004004bf <main>:
- 4004bf: 55 push %rbp
- 4004c0: 48 89 e5 mov %rsp,%rbp
- 4004c3: 48 83 ec 10 sub $0x10,%rsp
- 4004c7: c7 45 f4 fe 00 00 00 movl $0xfe,-0xc(%rbp)
- 4004ce: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp)
- 4004d5: c7 45 fc 02 00 00 00 movl $0x2,-0x4(%rbp)
- 4004dc: b8 00 00 00 00 mov $0x0,%eax
- 4004e1: e8 a9 ff ff ff callq 40048f <test>
- 4004e6: c9 leaveq
- 4004e7: c3 retq
- 4004e8: 90 nop
- 4004e9: 90 nop
- 4004ea: 90 nop
- 4004eb: 90 nop
- 4004ec: 90 nop
- 4004ed: 90 nop
- 4004ee: 90 nop
- 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所得,函数代码为:
点击(此处)折叠或打开
- irqreturn_t handle_IRQ_event(unsigned int irq, struct pt_regs *regs,
- struct irqaction *action)
- {
- irqreturn_t ret, retval = IRQ_NONE;
- unsigned int status = 0;
-
-
- trace_irq_entry(irq, regs);
-
-
- handle_dynamic_tick(action);
-
-
- if (!(action->flags & IRQF_DISABLED))
- local_irq_enable_in_hardirq();
-
-
- do {
- ret = action->handler(irq, action->dev_id, regs);
- if (ret == IRQ_HANDLED)
- status |= action->flags;
- retval |= ret;
- action = action->next;
- } while (action);
-
-
- if (status & IRQF_SAMPLE_RANDOM)
- add_interrupt_randomness(irq);
- local_irq_disable();
-
-
- trace_irq_exit(irq, retval);
-
-
- return retval;
- }
其中局部变量ret由寄存器eax保存,而status变量用ebx保存。反汇编如下:
点击(此处)折叠或打开
- crash> dis -l handle_IRQ_event
- /usr/src/debug/kernel-2.6.18/linux-2.6.18.x86_64/kernel/irq/handle.c: 134
- 0xffffffff80010b30 <handle_IRQ_event>: push %r14
- include/trace/irq.h: 7
- 0xffffffff80010b32 <handle_IRQ_event+2>: cmpl $0x0,3859215(%rip) # 0xffffffff803bee48 <__tracepoint_irq_entry.10785+8>
- /usr/src/debug/kernel-2.6.18/linux-2.6.18.x86_64/kernel/irq/handle.c: 134
- 0xffffffff80010b39 <handle_IRQ_event+9>: mov %rsi,%r14
- 0xffffffff80010b3c <handle_IRQ_event+12>: push %r13
- 0xffffffff80010b3e <handle_IRQ_event+14>: push %r12
- 0xffffffff80010b40 <handle_IRQ_event+16>: mov %edi,%r12d
- 0xffffffff80010b43 <handle_IRQ_event+19>: push %rbp
- 0xffffffff80010b44 <handle_IRQ_event+20>: mov %rdx,%rbp
- 0xffffffff80010b47 <handle_IRQ_event+23>: push %rbx
- include/trace/irq.h: 7
- 0xffffffff80010b48 <handle_IRQ_event+24>: je 0xffffffff80010b68 <handle_IRQ_event+56>
- 0xffffffff80010b4a <handle_IRQ_event+26>: mov 3859199(%rip),%rbx # 0xffffffff803bee50 <__tracepoint_irq_entry.10785+16>
- 0xffffffff80010b51 <handle_IRQ_event+33>: test %rbx,%rbx
- 0xffffffff80010b54 <handle_IRQ_event+36>: je 0xffffffff80010b68 <handle_IRQ_event+56>
- 0xffffffff80010b56 <handle_IRQ_event+38>: mov %r14,%rsi
- 0xffffffff80010b59 <handle_IRQ_event+41>: mov %r12d,%edi
- 0xffffffff80010b5c <handle_IRQ_event+44>: callq *(%rbx)
- 0xffffffff80010b5e <handle_IRQ_event+46>: add $0x8,%rbx
- 0xffffffff80010b62 <handle_IRQ_event+50>: cmpq $0x0,(%rbx)
- 0xffffffff80010b66 <handle_IRQ_event+54>: jmp 0xffffffff80010b54 <handle_IRQ_event+36>
- /usr/src/debug/kernel-2.6.18/linux-2.6.18.x86_64/kernel/irq/handle.c: 142
- 0xffffffff80010b68 <handle_IRQ_event+56>: testb $0x20,0x8(%rbp)
- 0xffffffff80010b6c <handle_IRQ_event+60>: jne 0xffffffff80010b6f <handle_IRQ_event+63>
- include/asm/irqflags.h: 80
- 0xffffffff80010b6e <handle_IRQ_event+62>: sti
- 0xffffffff80010b6f <handle_IRQ_event+63>: xor %r13d,%r13d
- 0xffffffff80010b72 <handle_IRQ_event+66>: xor %ebx,%ebx
- /usr/src/debug/kernel-2.6.18/linux-2.6.18.x86_64/kernel/irq/handle.c: 146
- 0xffffffff80010b74 <handle_IRQ_event+68>: mov 0x38(%rbp),%rsi
- 0xffffffff80010b78 <handle_IRQ_event+72>: mov %r14,%rdx
- 0xffffffff80010b7b <handle_IRQ_event+75>: mov %r12d,%edi
- 0xffffffff80010b7e <handle_IRQ_event+78>: callq *0x0(%rbp)
- /usr/src/debug/kernel-2.6.18/linux-2.6.18.x86_64/kernel/irq/handle.c: 147
- 0xffffffff80010b81 <handle_IRQ_event+81>: cmp $0x1,%eax
- 0xffffffff80010b84 <handle_IRQ_event+84>: jne 0xffffffff80010b89 <handle_IRQ_event+89>
- /usr/src/debug/kernel-2.6.18/linux-2.6.18.x86_64/kernel/irq/handle.c: 148
- 0xffffffff80010b86 <handle_IRQ_event+86>: or 0x8(%rbp),%ebx