本节内容
我们上节说了函数调用的时候,首先函数是被线程执行的,这个线程要执行函数调用的话必须要有内存分配,内存分为两块,一块称为堆,一块称为栈。每个线程都会有自己的栈内存,栈内存是个大整块,调用的时候通过BP或者SP这两个寄存器来维持当前函数需要操作哪块内存,当你都操作完了以后,直接来调整BP或者SP寄存器的位置就可以把你所调用函数的所分配的栈桢空间释放掉。这个释放和在堆上释放是不一样的,因为这里释放后内存完全可以用来干别的事情。但是栈上的内存释放了以后那个内存还在,因为整个栈内存是个整体。这就是整个一大块,我们只不过就是调用时候通过两个寄存器来确定当前操作的时候在这一大块中操作哪一个区域,所以这是有很大区别的。
栈上内存用BP和SP来操作一整块内存的一个区域,用完之后把SP寄存器指回去,那块空间接下来调用其它函数时候进行复用。也就是你的搞明白,首先整个栈内存是一大块,是一整块,它没有说释放某块内存这样的一个说法。除非就有一种可能,就是把整个栈空间释放掉。
但是在堆上我们申请了一段内存,我们不用的时候可以把这块释放掉,因为我们在一个函数里面可以多次调用堆内存分配,然后可以分块释放。栈上没有内存释放这种说法。所以这就有个好处在栈上只需要调整两个寄存器BP、SP的位置就可以来决定这个内存当前是正在用或者说是可以被其它函数调用来覆盖掉。所以有这样一个说法,我们尽可能把对象分配到栈上。因为不需要执行释放操作。因为现场恢复时候只需要调整寄存器,那块内存就变得可复用状态了。但是在堆上你必须要释放,在栈上的效率显然是要高很多。而且栈这种特性就决定了它是有顺序操作的机制,所以它的效率就高很多。那么你在堆上分配时候要么手动释放要么有垃圾回收器来释放。垃圾回收器只管堆上的东西,栈上它是不管的。所以我们在栈上分配的时候,一是效率比较高,第二不会给垃圾回收器带来负担。
我们现在知道了每个函数调用的时候都会在栈上用两个寄存器划出一个区域来存储参数、返回值、本地变量类似这样的一些内容,这个区域我们称之为叫栈桢。那么多级调用时候所有的栈桢串在一起我们称之为调用堆栈。
那么究竟有哪些东西分配在栈上呢?比如说在函数里面
这种东西默认情况下肯定分配在栈上,
- x=10
这个时候这东西在堆上还是在栈上呢?这时候实际上有两种东西,第一malloc的确是在堆上分配一个内存空间,这个内存空间分配完了之后得有个指针指向它。所以这地方严格来说有两个东西。第一个是堆上的内存块,还有个指针变量,这个指针变量可能是在栈上。指针本身是个标准的变量,它是有内存空间的,它没有内存空间的话地址怎么写进去,因为我们知道我们可以给指针赋值的,能给它赋值肯定是个对象,没有对地址赋值这样一个说法,地址肯定不能赋值的。所以指针和地址不是一回事。指针是一个标准的变量,里面存了地址信息而已。所以指针和地址完全不是一个东西,不要混合一谈。复合对象是不是分配在堆上也未必,这得看不同的语言对复合对象怎么定义了,比如说结构体算不算复合对象,数组算不算复合对象,默认情况在栈上分配没有问题,当然里面可以用指针指向堆上其它的地址。你别忘了当里面有指针指向别的对象的时候,这个指针本身它依然是在栈上的。比如说我有个复合对象结构体,有个x和一个指针p,指针p指向堆上一个内存对象,堆上内存对象不属于结构体本身的内容。因为只有这个指针属于这个结构体,至于这个指针指向谁和这个结构体没关系,这结构体本身是完全分配在栈上的。只不过结构体里面有个东西记录了堆上的地址信息而已。
- *p=malloc()
接下来了解对象参数究竟怎么去分配的。
- $ cat test.c
- #include <stdio.h>
- #include <stdlib.h>
- __attribute__((noinline)) void info(int x)
- {
- printf("info %d\n", x);
- }
- __attribute__((noinline)) int add(int x, int y)
- {
- int z = x + y;
- info(z);
- return z;
- }
- int main(int argc, char **argv)
- {
- int x = 0x100;
- int y = 0x200;
- int z = add(x, y);
- printf("%d\n", z);
- return 0;
- }
三个变量,x、y、z
- $ gcc - g - O0 - o test test.c#编译,去掉优化
使用gdb调试
- $ gdb test
- $ b main #符号名上加上断点,mian函数加上断点
- $ r #执行,这时在main函数上中断了
- $ set disassembly-flavor intel #设置intel样式
- $ disass #反汇编
main函数不是你程序真正的入口,而是你用户代码的入口,因为大部分程序在执行main函数之前它会有其它初始化的操作。
- Dump of assembler code for function main:
- 0x0000000000400570 <+0>: push rbp
- 0x0000000000400571 <+1>: mov rbp,rsp
- 0x0000000000400574 <+4>: sub rsp,0x20 #给main函数分配了16进制20字节的栈桢空间。
- 0x0000000000400578 <+8>: mov DWORD PTR [rbp-0x14],edi
- 0x000000000040057b <+11>: mov QWORD PTR [rbp-0x20],rsi
- => 0x000000000040057f <+15>: mov DWORD PTR [rbp-0xc],0x100
- 0x0000000000400586 <+22>: mov DWORD PTR [rbp-0x8],0x200
- 0x000000000040058d <+29>: mov edx,DWORD PTR [rbp-0x8]
- 0x0000000000400590 <+32>: mov eax,DWORD PTR [rbp-0xc]
- 0x0000000000400593 <+35>: mov esi,edx
- 0x0000000000400595 <+37>: mov edi,eax
- 0x0000000000400597 <+39>: call 0x400548 <add>
- 0x000000000040059c <+44>: mov DWORD PTR [rbp-0x4],eax
- 0x000000000040059f <+47>: mov eax,DWORD PTR [rbp-0x4]
- 0x00000000004005a2 <+50>: mov esi,eax
- 0x00000000004005a4 <+52>: mov edi,0x40064d
- 0x00000000004005a9 <+57>: mov eax,0x0
- 0x00000000004005ae <+62>: call 0x400400 <printf@plt>
- 0x00000000004005b3 <+67>: mov eax,0x0
- 0x00000000004005b8 <+72>: leave
- 0x00000000004005b9 <+73>: ret
- End of assembler dump.
我们看到所有的空间都是基于BP寄存器的寻址。
- $ cat test.go
- package main
- import "log"
- func info(x int) {
- log.Printf("info %d\n", x)
- }
- func add(x, y int) int {
- z: =x + y info(z)
- return z
- }
- func main() {
- x,
- y: =0x100,
- 0x200 z: =add(x, y)
- println(z)
- }
- $ go build - gcflags "-N -l" - o test test.go
- $ gdb test
- $ b mian.main #打断点
- $ r #运行
- $ set disassembly-flavor intel #设置intel样式
- $ disass #反汇编
- => 0x0000000000401140 <+0>: mov rcx,QWORD PTR fs:0xfffffffffffffff8
- 0x0000000000401149 <+9>: cmp rsp,QWORD PTR [rcx+0x10]
- 0x000000000040114d <+13>: jbe 0x4011a9 <main.main+105>
- 0x000000000040114f <+15>: sub rsp,0x30 #首先为这个空间分配了48字节栈桢空间
- 0x0000000000401153 <+19>: mov QWORD PTR [rsp+0x28],0x100
- 0x000000000040115c <+28>: mov QWORD PTR [rsp+0x20],0x200
- 0x0000000000401165 <+37>: mov rax,QWORD PTR [rsp+0x28]
- 0x000000000040116a <+42>: mov QWORD PTR [rsp],rax #x参数复制到rsp+0位置
- 0x000000000040116e <+46>: mov rax,QWORD PTR [rsp+0x20]
- 0x0000000000401173 <+51>: mov QWORD PTR [rsp+0x8],rax #y参数复制到rsp+8位置
- 0x0000000000401178 <+56>: call 0x4010f0 <main.add>
- 0x000000000040117d <+61>: mov rax,QWORD PTR [rsp+0x10]
- 0x0000000000401182 <+66>: mov QWORD PTR [rsp+0x18],rax
- 0x0000000000401187 <+71>: call 0x425380 <runtime.printlock>
- 0x000000000040118c <+76>: mov rax,QWORD PTR [rsp+0x18]
- 0x0000000000401191 <+81>: mov QWORD PTR [rsp],rax
- 0x0000000000401195 <+85>: call 0x425a10 <runtime.printint>
- 0x000000000040119a <+90>: call 0x4255b0 <runtime.printnl>
- 0x000000000040119f <+95>: call 0x425400 <runtime.printunlock>
- 0x00000000004011a4 <+100>: add rsp,0x30
- 0x00000000004011a8 <+104>: ret
- 0x00000000004011a9 <+105>: call 0x44b160 <runtime.morestack_noctxt>
- 0x00000000004011ae <+110>: jmp 0x401140 <main.main>
- |---------+---sp
- | 100 |
- |---------|---+8
- | 200 |
- |---------|--+10
- | |
- |---------|--+18
- | |
- |---------|--+20
- | y=200 |
- |---------|--+28
- | x=100 |
- |---------|--+30
go语言所有东西都是基于SP做加法,因为在go语言里它不使用BP寄存器,它把BP寄存器当作普通寄存器来用。它不用BP寄存器来维持一个栈桢,它只用SP指向栈顶就可以了,这跟它的内存管理策略有关系。
在add函数执行之前,首先做了参数复制,就是说函数调用时候参数是被复制的,理论上所有参数都是复制的,传指针复制的是指针而不是指针指向的目标,指针本身是被复制的,通过这个代码我们就看到复制过程。
.......
这个系列的每篇文章有大半篇幅内容属于付费阅读。提供微信支付或支付宝支付打赏50元备注留言手动提供付费文章访问密码。
来源: http://www.cnblogs.com/lyj/p/foundation_11_args.html