本節內容 - C參數複製,返回值 - Go參數複製,返回值 - 優化模式對參數傳遞的影響 ...
本節內容
- C參數複製,返回值
- Go參數複製,返回值[付費閱讀]
- 優化模式對參數傳遞的影響[付費閱讀]
我們上節說了函數調用的時候,首先函數是被線程執行的,這個線程要執行函數調用的話必須要有記憶體分配,記憶體分為兩塊,一塊稱為堆,一塊稱為棧。每個線程都會有自己的棧記憶體,棧記憶體是個大整塊,調用的時候通過BP或者SP這兩個寄存器來維持當前函數需要操作哪塊記憶體,當你都操作完了以後,直接來調整BP或者SP寄存器的位置就可以把你所調用函數的所分配的棧楨空間釋放掉。這個釋放和在堆上釋放是不一樣的,因為這裡釋放後記憶體完全可以用來乾別的事情。但是棧上的記憶體釋放了以後那個記憶體還在,因為整個棧記憶體是個整體。這就是整個一大塊,我們只不過就是調用時候通過兩個寄存器來確定當前操作的時候在這一大塊中操作哪一個區域,所以這是有很大區別的。
棧上記憶體用BP和SP來操作一整塊記憶體的一個區域,用完之後把SP寄存器指回去,那塊空間接下來調用其它函數時候進行復用。也就是你的搞明白,首先整個棧記憶體是一大塊,是一整塊,它沒有說釋放某塊記憶體這樣的一個說法。除非就有一種可能,就是把整個棧空間釋放掉。
但是在堆上我們申請了一段記憶體,我們不用的時候可以把這塊釋放掉,因為我們在一個函數裡面可以多次調用堆記憶體分配,然後可以分塊釋放。棧上沒有記憶體釋放這種說法。所以這就有個好處在棧上只需要調整兩個寄存器BP、SP的位置就可以來決定這個記憶體當前是正在用或者說是可以被其它函數調用來覆蓋掉。所以有這樣一個說法,我們儘可能把對象分配到棧上。因為不需要執行釋放操作。因為現場恢復時候只需要調整寄存器,那塊記憶體就變得可復用狀態了。但是在堆上你必須要釋放,在棧上的效率顯然是要高很多。而且棧這種特性就決定了它是有順序操作的機制,所以它的效率就高很多。那麼你在堆上分配時候要麼手動釋放要麼有垃圾回收器來釋放。垃圾回收器只管堆上的東西,棧上它是不管的。所以我們在棧上分配的時候,一是效率比較高,第二不會給垃圾回收器帶來負擔。
我們現在知道了每個函數調用的時候都會在棧上用兩個寄存器划出一個區域來存儲參數、返回值、本地變數類似這樣的一些內容,這個區域我們稱之為叫棧楨。那麼多級調用時候所有的棧楨串在一起我們稱之為調用堆棧。
那麼究竟有哪些東西分配在棧上呢?比如說在函數裡面x=10
這種東西預設情況下肯定分配在棧上,*p=malloc()
這個時候這東西在堆上還是在棧上呢?這時候實際上有兩種東西,第一malloc的確是在堆上分配一個記憶體空間,這個記憶體空間分配完了之後得有個指針指向它。所以這地方嚴格來說有兩個東西。第一個是堆上的記憶體塊,還有個指針變數,這個指針變數可能是在棧上。指針本身是個標準的變數,它是有記憶體空間的,它沒有記憶體空間的話地址怎麼寫進去,因為我們知道我們可以給指針賦值的,能給它賦值肯定是個對象,沒有對地址賦值這樣一個說法,地址肯定不能賦值的。所以指針和地址不是一回事。指針是一個標準的變數,裡面存了地址信息而已。所以指針和地址完全不是一個東西,不要混合一談。複合對象是不是分配在堆上也未必,這得看不同的語言對複合對象怎麼定義了,比如說結構體算不算複合對象,數組算不算複合對象,預設情況在棧上分配沒有問題,當然裡面可以用指針指向堆上其它的地址。你別忘了當裡面有指針指向別的對象的時候,這個指針本身它依然是在棧上的。比如說我有個複合對象結構體,有個x和一個指針p,指針p指向堆上一個記憶體對象,堆上記憶體對象不屬於結構體本身的內容。因為只有這個指針屬於這個結構體,至於這個指針指向誰和這個結構體沒關係,這結構體本身是完全分配在棧上的。只不過結構體裡面有個東西記錄了堆上的地址信息而已。
接下來瞭解對象參數究竟怎麼去分配的。
C參數複製,返回值
$ 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寄存器的定址。
Go參數複製,返回值[付費閱讀]
$ 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元備註留言手動提供付費文章訪問密碼。