Golang Calling Convention

1. Introduction

寫程式的時候為了確保使用的 API 如想像般地運作,時常得追進 Standard Library [1] 裡才能一探究竟,但一部份的 Golang Runtime [2] 是由 Assembly Code 撰寫的,追進去的時候就是一團混亂的開始。

這團混亂引起了我的好奇心,想要了解一下 Golang 的 Calling Convention 如何在 Functions 之間傳遞 ParametersReturn Values,同時也想知道 Function 前後時常出現的程式究竟有什麼功能。

但是 Golang Compiler (gc) 透過 go tool compile -S 產出的 Assembly Code 還滿有 Plan 9 Assembler [3] 風格的,一方面跟 Plan 9 Assembler 不熟,另一方面是沒辦法透過 GDB 對照著執行檔做單步追蹤,這樣就有點像是讀了技術文,卻很少透過實做去實踐想法一樣,始終是有點差距的,為了縮短想像與實際上的落差,直接透過 GDB 來觀察 x86-64 Assembly Code 還是比較方便的。

「A Quick Guide to Go’s Assembler」[4] 有介紹 gc 的 Assembly Language。

我的目標不是透過窮舉所有的組合去得到結果,而是透過觀察匯聚出一些概念。

請保持獨立思考,若是發現有錯誤的地方,歡迎寄信到網頁左上角的 FB 訊息裡。如果我回很慢的話,可能是有事情正在忙碌中,請多包涵。


接下來會從 main() 開始一部份一部份地拆解和觀察,如果遇到標示 TL;DR 的地方,代表真的很長,可以選擇稍後再閱讀。

2. main()

main() 作為程式的進入點,是讓人感覺有點特別的,先來觀察一下 main() 本人好了。

1 // example1.go
2 package main
3
4 func main() {
5 myFunc0()
6 }
7
8 func myFunc0() {
9 }

編譯環境如下:

  1. x86-64 (AMD64) Ubuntu 16.04
  2. go1.8.3
  3. go build -gcflags “-N -l” example1.go
  4. objdump -S example1 > example1.s

2.1. Assembly Code

1 // example1.s
2 000000000044d650 <main.main>:
3 package main
4
5 func main() {
6 44d650: 64 48 8b 0c 25 f8 ff mov %fs:0xfffffffffffffff8,%rcx
7 44d657: ff ff
8 44d659: 48 3b 61 10 cmp 0x10(%rcx),%rsp
9 44d65d: 76 1a jbe 44d679 <main.main+0x29>
10 44d65f: 48 83 ec 08 sub $0x8,%rsp
11 44d663: 48 89 2c 24 mov %rbp,(%rsp)
12 44d667: 48 8d 2c 24 lea (%rsp),%rbp
13 myFunc0()
14 44d66b: e8 10 00 00 00 callq 44d680 <main.myFunc0>
15 }
16 44d670: 48 8b 2c 24 mov (%rsp),%rbp
17 44d674: 48 83 c4 08 add $0x8,%rsp
18 44d678: c3 retq
19 package main
20
21 func main() {
22 44d679: e8 12 86 ff ff callq 445c90 <runtime.morestack_noctxt>
23 44d67e: eb d0 jmp 44d650 <main.main>
24
25 000000000044d680 <main.myFunc0>:
26 myFunc0()
27 }
28
29 func myFunc0() {
30 }
31 44d680: c3 retq

在 example1.s 裡搜尋 main.myFunc0,很快就能找到這段程式碼了,約略地看了一下,大概可以分為 4 個區塊來觀察,分別是 Stack Pointer, Frame Pointer, Function Call, More Stack。

Stack Pointer (紫色):rsp 減了 8 Bytes 之後,在 retq (Return) 前加了回來,目的應該是為了還原 rsp,讓 retq 可以得到存入 Stack 的返回位址,進而回到 main.main 的 Caller,當然,這一減一加之間清空了這段的 Stack,也呈現出 Stack 不必額外做管理 (e.g. Garbage Collection) 就能回收記憶體的優點。

Frame Pointer (藍色):透過 mov 將舊的 rbp 存入 rsp,並將存有舊 rbp 的這格 rsp 位址作為新的 rbp,新的 rbp 代表新的 Frame 就此展開;在清空這段 Stack 之前再一次透過 mov 將舊的 rbp 取回,藉此回到上一個 Frame,除了 Frame 的變動之外,目前沒有使用到 rbp 的跡象。

Function Call (綠色):如同預期般地 callq (Call) 了 myFunc0 的記憶體位址 (0x44d680),進入 myFunc0 後就 retq 了。

More Stack (黃色):將黃色的部份剪到 §2.2 來看。

2.2. More Stack

1 // example1.s
2 000000000044d650 <main.main>:
3 ...
4 44d650: 64 48 8b 0c 25 f8 ff mov %fs:0xfffffffffffffff8,%rcx
5 44d657: ff ff
6 44d659: 48 3b 61 10 cmp 0x10(%rcx),%rsp
7 44d65d: 76 1a jbe 44d679 <main.main+0x29>
8 ...
9 44d679: e8 12 86 ff ff callq 445c90 <runtime.morestack_noctxt>
10 44d67e: eb d0 jmp 44d650 <main.main>

這裡看起來像是有一個迴圈!,mov 將某一個 Data Structure 的位址存進了 rcx,並將此 Data Structure 裡的一個成員 (Offset 0x10) 與 rsp 進行 cmp (Compare)。

這個 Data Structure 就是 g struct (Goroutine)。§4 (TL;DR)

由於 cmp 在 AT&T Syntax 是後者減掉前者 [5],所以 jbe 的意思是:當 %rsp <= 0x10(%rcx) (g.stackguard0 [6]) 的時候,就 Jump 到 0x44d679。透過 runtime.morestack_noctxt (0x445c90) 加大 Goroutine 的 Stack,返回後再次 jmp (Jump) 回到 main.main 的第一行 (0x44d650),不斷重複 (cmp, jbe) 直到 Goroutine Stack 夠大為止。

在 x86-64 Linux 中 rsp 是往低位址 (往零) 長大的,這個迴圈會保持 rsp 高於 g.stackguard0


Stack Pointer, Frame Pointer, Function Call, More Stack 這幾個區塊很常以類似的形式出現在 Function 前後作為固定班底,先認識它們,有助於將傳遞 Parameters 和 Return Values 的邏輯區分開來。

接下來要看的是 Golang 如何在 Functions 之間傳遞與取得訊息。

3. How To Pass Parameters And Return Values

1 // example2.go
2 package main
3
4 func main() {
5 var l0 int = 0x3
6 l1, l2 := myFunc1(0x7, 0x11)
7 var l3 int = l1 + 0x5
8 _ = l0 + l2 + l3
9 }
10
11 func myFunc1(p0 int, p1 int) (int, int) {
12 return p0, p1 + 0x13 // (r0, r1)
13 }

example2.go 比 example1.go 增加了 Local Variables, Parameters 和 Return Values,為了簡化觀察的複雜度,只使用 int [7] 作為變數型別。

Assembly Code 的觀察過程可以參考 §5 (TL;DR)

將上述觀察過程中的 Stack 抽出來整理,並以 rbp 作為 Stack Frame 的起始位址,則 main.main 的 Stack Frame 如以下著色的部份 (3~12):

1 --- Stack ---
2 rsp0 Return address of the caller of main.main
3 rbp0 -0x8 +0x40 Frame pointer of the caller of main.main
4 l0 -0x10 +0x38 +0x40 Local variable of main.main
5 l1 -0x18 +0x30 +0x38 Local variable of main.main
6 l2 -0x20 +0x28 +0x30 Local variable of main.main
7 l3 -0x28 +0x20 +0x28 Local variable of main.main
8 r1 -0x30 +0x18 +0x20 Return value of main.myFunc1
9 r0 -0x38 +0x10 +0x18 Return value of main.myFunc1
10 p1 -0x40 +0x8 +0x10 Parameter of main.myFunc1
11 p0 -0x48 rsp1 +0x8 Parameter of main.myFunc1
12 rsp2 Return address of main.main

採用的是 Stack-based Calling Convention。

Paremters 和 Return Values 都是透過 Stack 傳遞的,不是透過 Registers。不過似乎有在討論是否要改成 Register-based Calling Convention [25][26]

Local Variables 的排列順序與 Parameters 以及 Return Values 不同。

在 x86-64 從高位址往低位址看,Local Variables 是照著 Source Code 的順序排列的,如 (l0, l1, l2, l3),但 Parameters 和 Return Values 是與 Source Code 順序相反的,如 (p1, p0) 和 (r1, r0)。

使用 Stack Pointer 做 Offset,而不是 Frame Pointer。

其實在 go1.7 之前,預設是不會產出 Frame Pointer (rbp) 的 [8],go1.7 和 go1.8 一直以來都是對 rsp 做 Offset 來描述 Local Variables, Parameters 和 Return Values 等等的位址,產出 Frame Pointer 似乎單純只是為了給外部工具使用的,例如 Linux Perf, Intel VTune。

3.1. Caller-saved Or Callee-saved

Frame Pointer (rbp) 是 Callee-saved。Stack Pointer (rsp) 也算它是。

callq Instruction 作為 Caller 和 Callee 的分界來看,Callee 確實必須自己將 Caller 的 rbp 存在 Stack 裡,並且在 retq 前還原回 rbp §2.1§5.2§5.6,所以確定 Frame Pointer 是 Callee-saved。

不過 Stack Pointer 就比較不一樣了,Callee 不會將 Caller 的 rsp 存進 Stack 裡,減和加 (使用和還原) 的邏輯是以程式的形式保存在 Text Section 中,rsp 確實是被 Callee 保存了,但比起「Save」一詞,或許說 Stack Pointer 是 Callee-preserved 可能還比較恰如其分些,不過事實上並沒有這個詞存在就是了。


對於 Golang Calling Convention 的觀察,到此應該已經匯聚出一些概念了,如果要再細追 Closure, Structure, Interface, Function Pointer 等等是如何傳遞的話,篇幅可能就太長了,到這裡先告一個段落好了。

結束囉!,後續為兩個補充說明用的章節 TL;DR

4. What’s In %fs:-8?

這章節是探索 %fs:0xfffffffffffffff8 的過程,藉此銜接 §2.2. More Stackrsp 究竟與誰做 cmp 的邏輯。

在 Linux User-space (x86-64) 裡使用 fs Register 可能是與 TLS (Thread Local Storage) 有關的 [10],加上存入 fs 似乎只能透過 sys_arch_prctl (System Call) 才能做的到,那就使用 GDB> catch syscall 158 [9] 來捕捉看看這個 System Call 好了,隨即在 runtime·settls [11] 捕捉到它,Caller 分別是 asm_amd64.s 的 runtime·rt0_go 與 sys_linux_amd64.s 的 runtime·clone

4.1. runtime·rt0_go

1 // example1.s
2 TEXT runtime·rt0_go(SB),NOSPLIT,$0
3 ...
4 LEAQ runtime·m0+m_tls(SB), DI
5 445995: 48 8d 3d 64 67 05 00 lea 0x56764(%rip),%rdi # 49c100 <runtime.m0+0x60>
6 CALL runtime·settls(SB)
7 44599c: e8 af 3c 00 00 callq 449650 <runtime.settls>
8 ...
9 LEAQ runtime·g0(SB), CX
10 44592f: 48 8d 0d 0a 64 05 00 lea 0x5640a(%rip),%rcx # 49bd40 <runtime.g0>
11 MOVQ CX, g(BX)
12 445936: 64 48 89 0c 25 f8 ff mov %rcx,%fs:0xfffffffffffffff8
13 44593d: ff ff

runtime·rt0_gom0.tls 的位址 (不是值) 交由 runtime·settls 加 8 後存入了 fs [12][22]m0 的型別是 m struct [13][23],此時 %fs:-8m0.tls 的位址,runtime·rt0_go 隨後又把 runtime·g0 的位址存入 %fs:0xfffffffffffffff8 (%fs:-8) [14],這相當於是存入了 m0.tlsm0.tls 為一個 [6]uintptr 的陣列,所以 m0.tls[0] 指向了 runtime·g0

runtime·g0 的型別是 g struct[15][16]

m0.tls[0] 是一個 uintptr,可以用來存放指向任何型別的 Pointer,如果是用來指向與 g struct 完全不同型別的話,在 §2.2 使用 rcx 的時候,應該會發現程式邏輯在進行異質型別的判斷,不然無法確定指向為何,但沒有,所以我猜 m0.tls[0] 固定指向 g struct 的可能性較高。

%fs:-8 (m.tls[0]) 指向 g struct 的可能性較高。

4.2. runtime·clone

1 // example1.s
2 // int32 clone(int32 flags, void *stk, M *mp, G *gp, void (*fn)(void));
3 TEXT runtime·clone(SB),NOSPLIT,$0
4 ...
5 MOVQ mp+16(FP), R8
6 4495ae: 4c 8b 44 24 18 mov 0x18(%rsp),%r8
7 MOVQ gp+24(FP), R9
8 4495b3: 4c 8b 4c 24 20 mov 0x20(%rsp),%r9
9 ...
10 LEAQ m_tls(R8), DI
11 4495e9: 49 8d 78 60 lea 0x60(%r8),%rdi
12 CALL runtime·settls(SB)
13 4495ed: e8 5e 00 00 00 callq 449650 <runtime.settls>
14 ...
15 get_tls(CX)
16 ...
17 MOVQ R9, g(CX)
18 4495f6: 64 4c 89 0c 25 f8 ff mov %r9,%fs:0xfffffffffffffff8
19 4495fd: ff ff

runtime·clone 將第三個參數 (指向 m struct 的 Pointer) 存入 r8,也將第四個參數 (指向 g struct 的 Pointer) 存入 r9runtime·clone 本身除了使用 System Call 創建 Thread 之外 [17][24],部份行為也與 runtime·rt0_go 類似,它也將 m.tls 的位址交由 runtime·settls 加 8 後存入 fs,此時 %fs:-8m.tls,然後又將 r9 也就是指向 g struct 的 Pointer 存入%fs:0xfffffffffffffff8 (%fs:-8),這也就等於是將 m.tls[0] 指向了 g struct

%fs:-8 (m.tls[0]) 指向了 g struct

其實到這裡就足以說明了 %fs:-8 指向的型別了,但實際使用 GDB 觀察的時候,§2.2%fs:-8 並沒有剛好指向 runtime·g0 或是 runtime·cloneg struct 參數,意思就是說,%fs:-8 (m.tls[0]) 指向了一個下落不明g struct

好奇心的驅使,想把它找出來! XD

§2.2 %fs:-8 的值使用 GDB> info symbol 反查,沒有找到這個記憶體位址對應的 Symbol;如果是使用 new() 在 Run-time 生出來的話,我猜 Compile-time 應該就沒有對應的 Symbol 了吧,整個 Go1.8.3 Source Code 裡只有 malg() 才使用 new(g) [18],在這裡設個 Break Point 蹲草看看,同時記下執行到 main.main 之前所有 new(g) 生出來的記憶體位址後,果然有一組與 %fs:-8 指的位址相同,找到了!

在 Golang M:N Concurrency Model 中,M 個 Goroutine 在 N 個 Thread 裡流轉執行著,所以 TLS 裡指向與當時初始化不同的 g struct 是符合邏輯的。


接下來這個章節描述了釐清 Calling Convention 的過程,我試著將它分段並結構化來觀察,即使如此,其實還是相當繁瑣的。

5. Clarify The Calling Convention

為了了解 §3 example2.go 是如何在 Functions 之間交換訊息的,必須進一步地探索它的 Assembly Code,經過 §2.1 §2.2 累積的概念後,已經可以開始對 example2.s 抽絲剝繭了。

5.1. Assembly Code

1 // example2.s
2 000000000046ccc0 <main.main>:
3 package main
4
5 func main() {
6 46ccc0: 64 48 8b 0c 25 f8 ff mov %fs:0xfffffffffffffff8,%rcx
7 46ccc7: ff ff
8 46ccc9: 48 3b 61 10 cmp 0x10(%rcx),%rsp
9 46cccd: 76 59 jbe 46cd28 <main.main+0x68>
10 46cccf: 48 83 ec 48 sub $0x48,%rsp
11 46ccd3: 48 89 6c 24 40 mov %rbp,0x40(%rsp)
12 46ccd8: 48 8d 6c 24 40 lea 0x40(%rsp),%rbp
13 var l0 int = 0x3
14 46ccdd: 48 c7 44 24 38 03 00 movq $0x3,0x38(%rsp)
15 46cce4: 00 00
16 l1, l2 := myFunc1(0x7, 0x11)
17 46cce6: 48 c7 04 24 07 00 00 movq $0x7,(%rsp)
18 46cced: 00
19 46ccee: 48 c7 44 24 08 11 00 movq $0x11,0x8(%rsp)
20 46ccf5: 00 00
21 46ccf7: e8 34 00 00 00 callq 46cd30 <main.myFunc1>
22 46ccfc: 48 8b 44 24 10 mov 0x10(%rsp),%rax
23 46cd01: 48 89 44 24 30 mov %rax,0x30(%rsp)
24 46cd06: 48 8b 44 24 18 mov 0x18(%rsp),%rax
25 46cd0b: 48 89 44 24 28 mov %rax,0x28(%rsp)
26 var l3 int = l1 + 0x5
27 46cd10: 48 8b 44 24 30 mov 0x30(%rsp),%rax
28 46cd15: 48 83 c0 05 add $0x5,%rax
29 46cd19: 48 89 44 24 20 mov %rax,0x20(%rsp)
30 _ = l0 + l2 + l3
31 }
32 46cd1e: 48 8b 6c 24 40 mov 0x40(%rsp),%rbp
33 46cd23: 48 83 c4 48 add $0x48,%rsp
34 46cd27: c3 retq
35
36 func main() {
37 46cd28: e8 d3 66 ff ff callq 463400 <runtime.morestack_noctxt>
38 46cd2d: eb 91 jmp 46ccc0 <main.main>
39
40 func myFunc1(p0 int, p1 int) (int, int) {
41 46cd30: 48 c7 44 24 18 00 00 movq $0x0,0x18(%rsp)
42 46cd37: 00 00
43 46cd39: 48 c7 44 24 20 00 00 movq $0x0,0x20(%rsp)
44 46cd40: 00 00
45 return p0, p1 + 0x13 // (r0, r1)
46 46cd42: 48 8b 44 24 08 mov 0x8(%rsp),%rax
47 46cd47: 48 89 44 24 18 mov %rax,0x18(%rsp)
48 46cd4c: 48 8b 44 24 10 mov 0x10(%rsp),%rax
49 46cd51: 48 83 c0 13 add $0x13,%rax
50 46cd55: 48 89 44 24 20 mov %rax,0x20(%rsp)
51 46cd5a: c3 retq

Compile 環境與 §2 相同,上述深灰色的部份可以參考 §2.2 More Stack,就不再贅述。接下來就一行一行來觀察 Golang 如何配置 Local Variables、以及如何傳遞 Parameters 和 Return Values。

5.2. Go Into main.main

1 --- Assembly Code ---
2 ...
3 46cccf: 48 83 ec 48 sub $0x48,%rsp
4 46ccd3: 48 89 6c 24 40 mov %rbp,0x40(%rsp)
5 46ccd8: 48 8d 6c 24 40 lea 0x40(%rsp),%rbp
6
7 --- Stack ---
8 [rsp0]

rsp0 代表最一開始 rsp 指向的位址,並以括號括起來代表目前是以它為準,當 rsp一有變動就會增加後面的數字以做區別。


1 --- Assembly Code ---
2 ...
3 46cccf: 48 83 ec 48 sub $0x48,%rsp
4 46ccd3: 48 89 6c 24 40 mov %rbp,0x40(%rsp)
5 46ccd8: 48 8d 6c 24 40 lea 0x40(%rsp),%rbp
6
7 --- Stack ---
8 rsp0
9 -0x8
10 -0x10
11 -0x18
12 -0x20
13 -0x28
14 -0x30
15 -0x38
16 -0x40
17 -0x48 [rsp1]

rsp 被異動了,所以 rsp0 +1 為 rsp1


1 --- Assembly Code ---
2 ...
3 46cccf: 48 83 ec 48 sub $0x48,%rsp
4 46ccd3: 48 89 6c 24 40 mov %rbp,0x40(%rsp)
5 46ccd8: 48 8d 6c 24 40 lea 0x40(%rsp),%rbp
6
7 --- Stack ---
8 rsp0
9 rbp0 -0x8 +0x40 Frame pointer of the caller of main.main
10 -0x10 +0x38
11 -0x18 +0x30
12 -0x20 +0x28
13 -0x28 +0x20
14 -0x30 +0x18
15 -0x38 +0x10
16 -0x40 +0x8
17 -0x48 [rsp1]

將 main.main Caller 的 rbp 存入 Stack。


1 --- Assembly Code ---
2 ...
3 46cccf: 48 83 ec 48 sub $0x48,%rsp
4 46ccd3: 48 89 6c 24 40 mov %rbp,0x40(%rsp)
5 46ccd8: 48 8d 6c 24 40 lea 0x40(%rsp),%rbp
6
7 --- Stack ---
8 rsp0
9 rbp0 -0x8 +0x40 Frame pointer of the caller of main.main
10 -0x10 +0x38
11 -0x18 +0x30
12 -0x20 +0x28
13 -0x28 +0x20
14 -0x30 +0x18
15 -0x38 +0x10
16 -0x40 +0x8
17 -0x48 [rsp1]

rsp1 + 0x40 存進 rbp

新的 Stack Frame 從 rsp1 + 0x40 展開。

5.3. Before Calling myFunc1

1 --- Assembly Code ---
2 var l0 int = 0x3
3 46ccdd: 48 c7 44 24 38 03 00 movq $0x3,0x38(%rsp)
4 46cce4: 00 00
5 l1, l2 := myFunc1(0x7, 0x11)
6 46cce6: 48 c7 04 24 07 00 00 movq $0x7,(%rsp)
7 46cced: 00
8 46ccee: 48 c7 44 24 08 11 00 movq $0x11,0x8(%rsp)
9 46ccf5: 00 00
10 46ccf7: e8 34 00 00 00 callq 46cd30 <main.myFunc1>
11 46ccfc: 48 8b 44 24 10 mov 0x10(%rsp),%rax
12
13 --- Stack ---
14 rsp0
15 rbp0 -0x8 +0x40 Frame pointer of the caller of main.main
16 l0 -0x10 +0x38 Local variable of main.main
17 -0x18 +0x30
18 -0x20 +0x28
19 -0x28 +0x20
20 -0x30 +0x18
21 -0x38 +0x10
22 -0x40 +0x8
23 -0x48 [rsp1]

example2.go 裡只有 l0 是 0x3。


1 --- Assembly Code ---
2 var l0 int = 0x3
3 46ccdd: 48 c7 44 24 38 03 00 movq $0x3,0x38(%rsp)
4 46cce4: 00 00
5 l1, l2 := myFunc1(0x7, 0x11)
6 46cce6: 48 c7 04 24 07 00 00 movq $0x7,(%rsp)
7 46cced: 00
8 46ccee: 48 c7 44 24 08 11 00 movq $0x11,0x8(%rsp)
9 46ccf5: 00 00
10 46ccf7: e8 34 00 00 00 callq 46cd30 <main.myFunc1>
11 46ccfc: 48 8b 44 24 10 mov 0x10(%rsp),%rax
12
13 --- Stack ---
14 rsp0
15 rbp0 -0x8 +0x40 Frame pointer of the caller of main.main
16 l0 -0x10 +0x38 Local variable of main.main
17 -0x18 +0x30
18 -0x20 +0x28
19 -0x28 +0x20
20 -0x30 +0x18
21 -0x38 +0x10
22 -0x40 +0x8
23 p0 -0x48 [rsp1] Parameter of main.myFunc1

example2.go 裡只有 p0 是 0x7。


1 --- Assembly Code ---
2 var l0 int = 0x3
3 46ccdd: 48 c7 44 24 38 03 00 movq $0x3,0x38(%rsp)
4 46cce4: 00 00
5 l1, l2 := myFunc1(0x7, 0x11)
6 46cce6: 48 c7 04 24 07 00 00 movq $0x7,(%rsp)
7 46cced: 00
8 46ccee: 48 c7 44 24 08 11 00 movq $0x11,0x8(%rsp)
9 46ccf5: 00 00
10 46ccf7: e8 34 00 00 00 callq 46cd30 <main.myFunc1>
11 46ccfc: 48 8b 44 24 10 mov 0x10(%rsp),%rax
12
13 --- Stack ---
14 rsp0
15 rbp0 -0x8 +0x40 Frame pointer of the caller of main.main
16 l0 -0x10 +0x38 Local variable of main.main
17 -0x18 +0x30
18 -0x20 +0x28
19 -0x28 +0x20
20 -0x30 +0x18
21 -0x38 +0x10
22 p1 -0x40 +0x8 Parameter of main.myFunc1
23 p0 -0x48 [rsp1] Parameter of main.myFunc1

example2.go 裡只有 p1 是 0x11。


1 --- Assembly Code ---
2 var l0 int = 0x3
3 46ccdd: 48 c7 44 24 38 03 00 movq $0x3,0x38(%rsp)
4 46cce4: 00 00
5 l1, l2 := myFunc1(0x7, 0x11)
6 46cce6: 48 c7 04 24 07 00 00 movq $0x7,(%rsp)
7 46cced: 00
8 46ccee: 48 c7 44 24 08 11 00 movq $0x11,0x8(%rsp)
9 46ccf5: 00 00
10 46ccf7: e8 34 00 00 00 callq 46cd30 <main.myFunc1>
11 46ccfc: 48 8b 44 24 10 mov 0x10(%rsp),%rax
12
13 --- Stack ---
14 rsp0
15 rbp0 -0x8 +0x40 Frame pointer of the caller of main.main
16 l0 -0x10 +0x38 Local variable of main.main
17 -0x18 +0x30
18 -0x20 +0x28
19 -0x28 +0x20
20 -0x30 +0x18
21 -0x38 +0x10
22 p1 -0x40 +0x8 Parameter of main.myFunc1
23 p0 -0x48 rsp1 Parameter of main.myFunc1
24 [rsp2] Return address of main.main

callq 會將 0x46ccfc 作為 myFunc1 的返回位址 Push 進 Stack,然後 Jump 到main.myFunc1 (0x46cd30)。

5.4. Go Into myFunc1

1 --- Assembly Code ---
2 46cd30: 48 c7 44 24 18 00 00 movq $0x0,0x18(%rsp)
3 46cd37: 00 00
4 46cd39: 48 c7 44 24 20 00 00 movq $0x0,0x20(%rsp)
5 46cd40: 00 00
6 return p0, p1 + 0x13 // (r0, r1)
7 46cd42: 48 8b 44 24 08 mov 0x8(%rsp),%rax
8 46cd47: 48 89 44 24 18 mov %rax,0x18(%rsp)
9 46cd4c: 48 8b 44 24 10 mov 0x10(%rsp),%rax
10 46cd51: 48 83 c0 13 add $0x13,%rax
11 46cd55: 48 89 44 24 20 mov %rax,0x20(%rsp)
12 46cd5a: c3 retq
13
14 --- Stack ---
15 rsp0
16 rbp0 -0x8 +0x40 Frame pointer of the caller of main.main
17 l0 -0x10 +0x38 +0x40 Local variable of main.main
18 -0x18 +0x30 +0x38
19 -0x20 +0x28 +0x30
20 -0x28 +0x20 +0x28
21 0 -0x30 +0x18 +0x20
22 0 -0x38 +0x10 +0x18
23 p1 -0x40 +0x8 +0x10 Parameter of main.myFunc1
24 p0 -0x48 rsp1 +0x8 Parameter of main.myFunc1
25 [rsp2] Return address of main.main

可能是因為 myFunc1 太過簡略了,Compiler 沒有為它建立自己的 Stack Frame。

將 Stack 裡的兩個值設為零,目前還無法辨別為哪些變數。


1 --- Assembly Code ---
2 46cd30: 48 c7 44 24 18 00 00 movq $0x0,0x18(%rsp)
3 46cd37: 00 00
4 46cd39: 48 c7 44 24 20 00 00 movq $0x0,0x20(%rsp)
5 46cd40: 00 00
6 return p0, p1 + 0x13 // (r0, r1)
7 46cd42: 48 8b 44 24 08 mov 0x8(%rsp),%rax
8 46cd47: 48 89 44 24 18 mov %rax,0x18(%rsp)
9 46cd4c: 48 8b 44 24 10 mov 0x10(%rsp),%rax
10 46cd51: 48 83 c0 13 add $0x13,%rax
11 46cd55: 48 89 44 24 20 mov %rax,0x20(%rsp)
12 46cd5a: c3 retq
13
14 --- Stack ---
15 rsp0
16 rbp0 -0x8 +0x40 Frame pointer of the caller of main.main
17 l0 -0x10 +0x38 +0x40 Local variable of main.main
18 -0x18 +0x30 +0x38
19 -0x20 +0x28 +0x30
20 -0x28 +0x20 +0x28
21 0 -0x30 +0x18 +0x20
22 r0 -0x38 +0x10 +0x18 Return value of main.myFunc1
23 p1 -0x40 +0x8 +0x10 Parameter of main.myFunc1
24 p0 -0x48 rsp1 +0x8 Parameter of main.myFunc1
25 [rsp2] Return address of main.main

example2.go 裡只有 r0 被設為 p0


1 --- Assembly Code ---
2 46cd30: 48 c7 44 24 18 00 00 movq $0x0,0x18(%rsp)
3 46cd37: 00 00
4 46cd39: 48 c7 44 24 20 00 00 movq $0x0,0x20(%rsp)
5 46cd40: 00 00
6 return p0, p1 + 0x13 // (r0, r1)
7 46cd42: 48 8b 44 24 08 mov 0x8(%rsp),%rax
8 46cd47: 48 89 44 24 18 mov %rax,0x18(%rsp)
9 46cd4c: 48 8b 44 24 10 mov 0x10(%rsp),%rax
10 46cd51: 48 83 c0 13 add $0x13,%rax
11 46cd55: 48 89 44 24 20 mov %rax,0x20(%rsp)
12 46cd5a: c3 retq
13
14 --- Stack ---
15 rsp0
16 rbp0 -0x8 +0x40 Frame pointer of the caller of main.main
17 l0 -0x10 +0x38 +0x40 Local variable of main.main
18 -0x18 +0x30 +0x38
19 -0x20 +0x28 +0x30
20 -0x28 +0x20 +0x28
21 r1 -0x30 +0x18 +0x20 Return value of main.myFunc1
22 r0 -0x38 +0x10 +0x18 Return value of main.myFunc1
23 p1 -0x40 +0x8 +0x10 Parameter of main.myFunc1
24 p0 -0x48 rsp1 +0x8 Parameter of main.myFunc1
25 [rsp2] Return address of main.main

example2.go 裡只有 r1 被設為 p1 + 0x13。

原來這兩個值就是 r0r1 呀。


1 --- Assembly Code ---
2 46cd30: 48 c7 44 24 18 00 00 movq $0x0,0x18(%rsp)
3 46cd37: 00 00
4 46cd39: 48 c7 44 24 20 00 00 movq $0x0,0x20(%rsp)
5 46cd40: 00 00
6 return p0, p1 + 0x13 // (r0, r1)
7 46cd42: 48 8b 44 24 08 mov 0x8(%rsp),%rax
8 46cd47: 48 89 44 24 18 mov %rax,0x18(%rsp)
9 46cd4c: 48 8b 44 24 10 mov 0x10(%rsp),%rax
10 46cd51: 48 83 c0 13 add $0x13,%rax
11 46cd55: 48 89 44 24 20 mov %rax,0x20(%rsp)
12 46cd5a: c3 retq
13
14 --- Stack ---
15 rsp0
16 rbp0 -0x8 +0x40 Frame pointer of the caller of main.main
17 l0 -0x10 +0x38 +0x40 Local variable of main.main
18 -0x18 +0x30 +0x38
19 -0x20 +0x28 +0x30
20 -0x28 +0x20 +0x28
21 r1 -0x30 +0x18 +0x20 Return value of main.myFunc1
22 r0 -0x38 +0x10 +0x18 Return value of main.myFunc1
23 p1 -0x40 +0x8 +0x10 Parameter of main.myFunc1
24 p0 -0x48 [rsp1] +0x8 Parameter of main.myFunc1
25 rsp2 Return address of main.main

retq 會將 rsp2 裡的 Return Address (0x46ccfc) 從 Stack 裡 Pop 到 rip (Program Counter),相當於 Pop Stack 之後立刻 Jump 回 main.main 的 0x46ccfc。

5.5. After Calling myFunc1

1 --- Assembly Code ---
2 46ccf7: e8 34 00 00 00 callq 46cd30 <main.myFunc1>
3 46ccfc: 48 8b 44 24 10 mov 0x10(%rsp),%rax
4 46cd01: 48 89 44 24 30 mov %rax,0x30(%rsp)
5 46cd06: 48 8b 44 24 18 mov 0x18(%rsp),%rax
6 46cd0b: 48 89 44 24 28 mov %rax,0x28(%rsp)
7 var l3 int = l1 + 0x5
8 46cd10: 48 8b 44 24 30 mov 0x30(%rsp),%rax
9 46cd15: 48 83 c0 05 add $0x5,%rax
10 46cd19: 48 89 44 24 20 mov %rax,0x20(%rsp)
11
12 --- Stack ---
13 rsp0
14 rbp0 -0x8 +0x40 Frame pointer of the caller of main.main
15 l0 -0x10 +0x38 Local variable of main.main
16 l1 -0x18 +0x30 Local variable of main.main
17 -0x20 +0x28
18 -0x28 +0x20
19 r1 -0x30 +0x18 Return value of main.myFunc1
20 r0 -0x38 +0x10 Return value of main.myFunc1
21 p1 -0x40 +0x8 Parameter of main.myFunc1
22 p0 -0x48 [rsp1] Parameter of main.myFunc1

example2.go 裡 l1 是被設為 r0


1 --- Assembly Code ---
2 46ccf7: e8 34 00 00 00 callq 46cd30 <main.myFunc1>
3 46ccfc: 48 8b 44 24 10 mov 0x10(%rsp),%rax
4 46cd01: 48 89 44 24 30 mov %rax,0x30(%rsp)
5 46cd06: 48 8b 44 24 18 mov 0x18(%rsp),%rax
6 46cd0b: 48 89 44 24 28 mov %rax,0x28(%rsp)
7 var l3 int = l1 + 0x5
8 46cd10: 48 8b 44 24 30 mov 0x30(%rsp),%rax
9 46cd15: 48 83 c0 05 add $0x5,%rax
10 46cd19: 48 89 44 24 20 mov %rax,0x20(%rsp)
11
12 --- Stack ---
13 rsp0
14 rbp0 -0x8 +0x40 Frame pointer of the caller of main.main
15 l0 -0x10 +0x38 Local variable of main.main
16 l1 -0x18 +0x30 Local variable of main.main
17 l2 -0x20 +0x28 Local variable of main.main
18 -0x28 +0x20
19 r1 -0x30 +0x18 Return value of main.myFunc1
20 r0 -0x38 +0x10 Return value of main.myFunc1
21 p1 -0x40 +0x8 Parameter of main.myFunc1
22 p0 -0x48 [rsp1] Parameter of main.myFunc1

example2.go 裡 l2 是被設為 r1


1 --- Assembly Code ---
2 46ccf7: e8 34 00 00 00 callq 46cd30 <main.myFunc1>
3 46ccfc: 48 8b 44 24 10 mov 0x10(%rsp),%rax
4 46cd01: 48 89 44 24 30 mov %rax,0x30(%rsp)
5 46cd06: 48 8b 44 24 18 mov 0x18(%rsp),%rax
6 46cd0b: 48 89 44 24 28 mov %rax,0x28(%rsp)
7 var l3 int = l1 + 0x5
8 46cd10: 48 8b 44 24 30 mov 0x30(%rsp),%rax
9 46cd15: 48 83 c0 05 add $0x5,%rax
10 46cd19: 48 89 44 24 20 mov %rax,0x20(%rsp)
11
12 --- Stack ---
13 rsp0
14 rbp0 -0x8 +0x40 Frame pointer of the caller of main.main
15 l0 -0x10 +0x38 Local variable of main.main
16 l1 -0x18 +0x30 Local variable of main.main
17 l2 -0x20 +0x28 Local variable of main.main
18 l3 -0x28 +0x20 Local variable of main.main
19 r1 -0x30 +0x18 Return value of main.myFunc1
20 r0 -0x38 +0x10 Return value of main.myFunc1
21 p1 -0x40 +0x8 Parameter of main.myFunc1
22 p0 -0x48 [rsp1] Parameter of main.myFunc1

再來一個 l3 確認 l1, l2 會被包在 l0, l3 之間的 Local Variable 區域。


1 --- Assembly Code ---
2 46cd19: 48 89 44 24 20 mov %rax,0x20(%rsp)
3 _ = l0 + l2 + l3
4 }
5 46cd1e: 48 8b 6c 24 40 mov 0x40(%rsp),%rbp
6 46cd23: 48 83 c4 48 add $0x48,%rsp
7 46cd27: c3 retq
8
9 --- Stack ---
10 rsp0
11 rbp0 -0x8 +0x40 Frame pointer of the caller of main.main
12 l0 -0x10 +0x38 Local variable of main.main
13 l1 -0x18 +0x30 Local variable of main.main
14 l2 -0x20 +0x28 Local variable of main.main
15 l3 -0x28 +0x20 Local variable of main.main
16 r1 -0x30 +0x18 Return value of main.myFunc1
17 r0 -0x38 +0x10 Return value of main.myFunc1
18 p1 -0x40 +0x8 Parameter of main.myFunc1
19 p0 -0x48 [rsp1] Parameter of main.myFunc1

可能是因為最佳化的關係,被 Compiler 發現 _ (Blank Identifier) [19] 整串都沒有被使用到,所以就沒有產出對應的程式碼了吧。

話說,我不是把最佳化關掉了 (-N §2)!,可能這太基本了,稱不上是最佳化的一部份。

5.6. Before Returning From main.main

1 --- Assembly Code ---
2 46cd19: 48 89 44 24 20 mov %rax,0x20(%rsp)
3 _ = l0 + l2 + l3
4 }
5 46cd1e: 48 8b 6c 24 40 mov 0x40(%rsp),%rbp
6 46cd23: 48 83 c4 48 add $0x48,%rsp
7 46cd27: c3 retq
8
9 --- Stack ---
10 rsp0
11 rbp0 -0x8 +0x40 Frame pointer of the caller of main.main
12 l0 -0x10 +0x38 Local variable of main.main
13 l1 -0x18 +0x30 Local variable of main.main
14 l2 -0x20 +0x28 Local variable of main.main
15 l3 -0x28 +0x20 Local variable of main.main
16 r1 -0x30 +0x18 Return value of main.myFunc1
17 r0 -0x38 +0x10 Return value of main.myFunc1
18 p1 -0x40 +0x8 Parameter of main.myFunc1
19 p0 -0x48 [rsp1] Parameter of main.myFunc1

main.main Caller 的 rbp0 還原到 rbp,代表 main.main 的 Stack Frame 結束並回到 Caller 的 Stack Frame 了。

Frame Pointer (rbp) 是 Callee-saved。


1 --- Assembly Code ---
2 46cd19: 48 89 44 24 20 mov %rax,0x20(%rsp)
3 _ = l0 + l2 + l3
4 }
5 46cd1e: 48 8b 6c 24 40 mov 0x40(%rsp),%rbp
6 46cd23: 48 83 c4 48 add $0x48,%rsp
7 46cd27: c3 retq
8
9 --- Stack ---
10 [rsp0]
11 rbp0 -0x8 +0x40 Frame pointer of the caller of main.main
12 l0 -0x10 +0x38 Local variable of main.main
13 l1 -0x18 +0x30 Local variable of main.main
14 l2 -0x20 +0x28 Local variable of main.main
15 l3 -0x28 +0x20 Local variable of main.main
16 r1 -0x30 +0x18 Return value of main.myFunc1
17 r0 -0x38 +0x10 Return value of main.myFunc1
18 p1 -0x40 +0x8 Parameter of main.myFunc1
19 p0 -0x48 rsp1 Parameter of main.myFunc1

還原 rspmain.main 一開始時減掉的 0x48。

Stack Pointer (rsp) 應該也算是 Callee-saved 吧,見 §3.1 的想法。


1 --- Assembly Code ---
2 46cd19: 48 89 44 24 20 mov %rax,0x20(%rsp)
3 _ = l0 + l2 + l3
4 }
5 46cd1e: 48 8b 6c 24 40 mov 0x40(%rsp),%rbp
6 46cd23: 48 83 c4 48 add $0x48,%rsp
7 46cd27: c3 retq
8
9 --- Stack ---
10 rsp0 Return address of the caller of main.main

retq 會將 rsp0 Pop 到 rip (Program Counter),所以可以推論出 rsp0 存放的是 main.main Caller 的 Return Address。

main.main 的 Caller 其實就是 runtime.main [20]

7. Reference

[1]: golang.org. (?/go1.8.3). Standard Library, Packages

[2]: github.com/golang/go. (2017-05-24/go1.8.3). runtime

[3]: Rob Pike. (?). A Manual for the Plan 9 assembler.

[4]: golang.org. (2017-05/go1.8.3). A Quick Guide to Go's Assembler.

[5]: nrz. (2013-02-14). What does 0x4 from cmp 0x4(%esi),%ebx assembly instruction mean?

[6]: github.com/golang/go. (2017-01-05/go1.8.3). g struct, runtime2.go

[7]: golang.org. (?/go1.8.3). What is the size of an int on a 64 bit machine?, Frequently Asked Questions (FAQ)

[8]: Russ Cox. (2016-05-26). build: enable frame pointers by default

[9]: github.com/torvalds/linux. (2017-03-03). 64-bit system call numbers and entry vectors

[10]: osdev.org. (2014-11-18). x86-64, Thread Local Storage

[11]: github.com/golang/go. (2017-05-24/go1.8.3). runtime·settls, sys_linux_amd64.s

[12]: github.com/golang/go. (2017-01-05/go1.8.3). runtime·rt0_go, asm_amd64.s

[13]: github.com/golang/go. (2017-01-10/go1.8.3). proc.go

[14]: github.com/golang/go. (2017-01-05/go1.8.3). runtime·rt0_go, asm_amd64.s

[15]: github.com/golang/go. (2017-01-10/go1.8.3). proc.go

[16]: github.com/golang/go. (2017-01-05/go1.8.3). g struct, runtime2.go

[17]: github.com/golang/go. (2017-05-24/go1.8.3). runtime·clone, sys_linux_amd64.s

[18]: github.com/golang/go. (2017-01-10/go1.8.3). malg, proc.go

[19]: golang.org. (?/go1.8.3). The blank identifier, Effective Go.

[20]: github.com/golang/go. (2017-01-10/go1.8.3). runtime.main, proc.go

[21]: Wikipedia. (2017-08-17). TL;DR

[22]: github.com/golang/go. (2017-05-24/go1.8.3). runtime·settls, sys_linux_amd64.s

[23]: github.com/golang/go. (2017-01-05/go1.8.3). m struct, runtime2.go

[24]: github.com/torvalds/linux. (2017-03-03). 64-bit system call numbers and entry vectors

[25]: dr2chase. (2017-01-11). proposal: cmd/compile: define register-based calling conventio

[26]: David Chase. (2017-01-10). Proposal: Passing Go arguments and results in registers