以下文章均為拜讀公眾號(hào) 源碼游記 的筆記 http://mp.weixin.qq.com/mp/homepage?__biz=MzU1OTg5NDkzOA==&hid=1&sn=8fc2b63f53559bc0cee292ce629c4788&scene=18#wechat_redirect
- 預(yù)備知識(shí)
1. 寄存器
我們一般用到的寄存器有三種
-
通用寄存器
rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp, r8, r9, r10, r11, r12, r13, r14, r15寄存器讨越。CPU對(duì)這16個(gè)通用寄存器的用途沒有做特殊規(guī)定。
但是這些寄存器有一些默認(rèn)用途,
在傳參的時(shí)候,前6個(gè)參數(shù)分別為:rax, rbx, rcx, rdx, rsi, rdi畜晰。
-
rsp 棧頂寄存器 & rbp 椚鹂穑基址寄存器
這2個(gè)寄存器都跟函數(shù)調(diào)用棧相關(guān)。其中rsp一般存在棧頂?shù)牡刂罚鴕bp是棧幀起始地址扫步。編譯器一般用這2個(gè)寄存器來獲取函數(shù)局部變量或者參數(shù)
-
指令寄存器、程序計(jì)算寄存器
rip寄存器河胎,它用來存放下一條即將執(zhí)行的指令的地址。
-
段寄存器
fs和gs寄存器游岳,在go中使用fs寄存器實(shí)現(xiàn)線程本地存儲(chǔ)其徙。
2. 內(nèi)存
- 內(nèi)存的單元為字節(jié)胚迫,每一個(gè)字節(jié)都有一個(gè)地址
- 變量連續(xù)存儲(chǔ):何大于一個(gè)字節(jié)的變量在內(nèi)存中都存儲(chǔ)在相鄰連續(xù)的的幾個(gè)內(nèi)存單元之中;
- 大端&小端
- 大端:高字節(jié)-> 低地址
- 小端:高字節(jié)-> 高地址
說明:
rsp 指的是棧頂是已經(jīng)使用的地址唾那。非下一條地址访锻。
3. 函數(shù)調(diào)用棧
函數(shù)是以棧的方式調(diào)用的。
程序運(yùn)行時(shí)布局圖:
進(jìn)程在虛擬地址的布局如上闹获。一個(gè)進(jìn)程把內(nèi)存分為了4個(gè)部分:
- 代碼區(qū): 包括被CPU執(zhí)行的機(jī)器代碼和只讀數(shù)據(jù)比如字符串常量期犬。一旦加載完成就不會(huì)再變化
- 數(shù)據(jù)區(qū):包括程序的全局變量和靜態(tài)變量(c語言有靜態(tài)變量,而go沒有)避诽。一旦加載完成就不會(huì)再變化
- 堆:動(dòng)態(tài)分配的內(nèi)存在堆中龟虎。
- 棧:函數(shù)調(diào)用棧
下面重點(diǎn)說函數(shù)調(diào)用棧
它在函數(shù)中扮演著重要的角色
- 保存 函數(shù)中的局部變量
- 傳遞 在函數(shù)調(diào)用中傳遞參數(shù)
- 返回 把函數(shù)的返回值返回
- 保存 函數(shù)的返回地址
每個(gè)函數(shù)在執(zhí)行過程中都需要使用一塊內(nèi)存來保存上述的這些值,我們稱這塊棧內(nèi)存為棧幀(stack frame)沙庐。當(dāng)發(fā)生函數(shù)調(diào)用的時(shí)候鲤妥,被調(diào)用者不能覆蓋調(diào)用者的棧幀旗芬,所以需要把調(diào)用者的棧幀push
到棧上王财,等調(diào)用完成再pop。
另外裁厅,在AMD64 Linux平臺(tái)古涧,棧是從高向低方向生成的垂券。其中就使用了上面提到的2個(gè)寄存器
- rsp
- rbp
舉例花盐,假設(shè)有如下調(diào)用關(guān)系A()->B()->C()
,則有如下的調(diào)用關(guān)系
需要注意:
- 函數(shù)調(diào)用時(shí)羡滑,參數(shù)和返回值都是存放在調(diào)用者的棧幀中。(這個(gè)會(huì)影響到行程的匯編代碼)
- go語言把參數(shù)和返回值都是放在棧上算芯。(gcc 是吧參數(shù)和返回值放到寄存器中)
當(dāng)C柒昏、B函數(shù)運(yùn)行完,A調(diào)用D得到如圖
如上圖熙揍,D覆蓋了之前B职祷、C的內(nèi)存。
正因?yàn)闂5膬?nèi)存會(huì)被覆蓋,所以在C語言中才不能返回局部變量的指針有梆。但是Go因?yàn)橛袃?nèi)存逃逸是尖,則不會(huì)有這樣的問題。
key note
- 每個(gè)進(jìn)程地址一致是靠虛擬內(nèi)存機(jī)制保證的泥耀。
- caller save,callee save
4. 匯編指令
5. Go 匯編語言
go中的runtime有部分代碼是用匯編寫的饺汹。但是它的匯編語言并非針對(duì)特定體系結(jié)構(gòu)的匯編代碼,而是go語言引入的的偽匯編plan9.
TODO: go blog about plan9
go匯編和AT&T 差不多痰催,但是也有區(qū)別,下面主要對(duì)其做說明逸吵。(因?yàn)楹罄m(xù)調(diào)度分析的時(shí)候需要看匯編)
寄存器映射
除此go還引入幾個(gè)虛擬寄存器:(所謂虛擬寄存器扫皱,就是沒有任何硬件寄存器與之對(duì)應(yīng))啸罢。這些寄存器一般用來存放內(nèi)存地址胎食,引入他們的目的是為了方便程序員和編譯器 用來定位內(nèi)存中的代碼和數(shù)據(jù)厕怜。
FP虛擬寄存器
主要用來引用函數(shù)參數(shù)。前面提到過go的參數(shù)都在棧上琅捏。所以引入FP可以方便我們獲取到參數(shù)地址柄延。
比如可以使用firstarg+0(FP)
來引用調(diào)用者傳遞來的第一個(gè)參數(shù)缀程,用secondarg+8(FP)
來引用第二個(gè)參數(shù)杨凑。這里``firstarg和
secondarg`都是無意義的符號(hào),編譯器不關(guān)心也不解讀蜒程。舉例:
go 中有個(gè)gogo函數(shù),接受一個(gè)gobuf指針
// src/runtime/stubs.go:129
func gogo(buf *gobuf)
對(duì)應(yīng)的匯編部分如下
// func gogo(buf *gobuf)
// restore state from Gobuf; longjmp
TEXT runtime·gogo(SB), NOSPLIT, $16-8
MOVQ buf+0(FP), BX // gobuf -> BX
MOVQ gobuf_g(BX), DX // gp.sched.g = dx
...
由上忌锯,可以看到通過FP獲取到參數(shù)汉规≌胧罚回想之前的棧碟狞,F(xiàn)P并不在當(dāng)前的函數(shù)棧幀上,其關(guān)系如圖频祝。
SB虛擬寄存器:
上面的示例代碼中有TEXT runtime·gogo(SB), NOSPLIT, $16-8
常空。其中SB保存的是程序地址空間的的起始地址漓糙。之前棧中代碼區(qū)的地方昆禽。這個(gè)SB寄存器保存的值就是代碼區(qū)的起始地址蝇庭,它主要用來定位全局符號(hào)哮内。Go匯編中的函數(shù)定義、函數(shù)調(diào)用纹因、全局變量以及對(duì)其引用都會(huì)用到這個(gè)SB寄存器辐怕。
函數(shù)的其他定義:
-
TEXT runtime·gogo(SB)
:指明在代碼區(qū)定義了一個(gè)名字叫 gogo的全局函數(shù)从绘,該函數(shù)屬于runtime包 -
NOSPLIT
:指示編譯器不要再這個(gè)函數(shù)中插入檢查棧是否溢出的代碼僵井。(TODO) -
$16-8
:16代表函數(shù)棧幀為16字節(jié),8代碼函數(shù)的參數(shù)和返回值一共8個(gè)字節(jié)农曲。
6. 函數(shù)調(diào)用過程
7. 系統(tǒng)調(diào)用
操作內(nèi)核
https://mp.weixin.qq.com/s/YPiYNPa3xVD9Il1HeB5pTw
8. 操作系統(tǒng)的線程和線程調(diào)度
要深入理解goroutine的調(diào)度器乳规,就需要對(duì)操作系統(tǒng)線程有個(gè)大致的了解暮的,因?yàn)間o的調(diào)度系統(tǒng)是建立在操作系統(tǒng)線程之上的淌实,所以接下來我們對(duì)其做一個(gè)簡單的介紹拆祈。
很難對(duì)線程下一個(gè)準(zhǔn)確且易于理解的定義,特別是對(duì)于從未接觸過多線程編程的讀者來說咙咽,要搞懂什么是線程可能并不是很容易犁珠,所以下面我們拋開定義直接從一個(gè)C語言的程序開始來直觀的看一下什么是線程互亮。之所以使用C語言豹休,是因?yàn)镃語言中我們一般使用pthread線程庫,而使用該線程庫創(chuàng)建的用戶態(tài)線程其實(shí)就是Linux操作系統(tǒng)內(nèi)核所支持的線程凤巨,它與go語言中的工作線程是一樣的敢茁,這些線程都由Linux內(nèi)核負(fù)責(zé)管理和調(diào)度留美,然后go語言在操作系統(tǒng)線程之上又做了goroutine伸刃,實(shí)現(xiàn)了一個(gè)二級(jí)線程模型捧颅。
什么時(shí)候發(fā)生調(diào)度
- 用戶使用系統(tǒng)調(diào)用
- 硬件中斷尤其是時(shí)鐘中斷
線程保存著程序運(yùn)行的上下文
- 寄存器中的值
- 下一條指令
- 棧
所以當(dāng)進(jìn)行線程切換的時(shí)候需要把這些都保存下
參考:
9. 線程本地存儲(chǔ)
線程本地存儲(chǔ)又叫線程局部存儲(chǔ)扣典,其英文為Thread Local Storage慎玖,簡稱TLS
TLS 在用戶側(cè)代碼是一個(gè)變量凄吏,但是在編譯層次確實(shí)2個(gè)地址。所以用戶使用的時(shí)候就可以使用一個(gè)變量訪問2個(gè)地址图柏。
我們需要知道fs段基址是多少蚤吹,雖然我們可以用gdb命令查看fs寄存器的值随抠,但fs寄存器里面存放的是段選擇子(segment selector)而不是該段的起始地址