組件
計(jì)算機(jī)是一種數(shù)據(jù)處理設(shè)備,它由CPU和內(nèi)存以及外部設(shè)備組成宁赤。CPU負(fù)責(zé)數(shù)據(jù)處理剑勾,內(nèi)存負(fù)責(zé)存儲(chǔ)烹笔,外部設(shè)備負(fù)責(zé)數(shù)據(jù)的輸入和輸出罕扎,它們之間通過(guò)總線連接在一起憎夷。CPU內(nèi)部主要由控制器佛舱、運(yùn)算器和寄存器組成掏膏〈缆幔控制器負(fù)責(zé)指令的讀取和調(diào)度链蕊,運(yùn)算器負(fù)責(zé)指令的運(yùn)算執(zhí)行,寄存器負(fù)責(zé)數(shù)據(jù)的存儲(chǔ)谬泌,它們之間通過(guò)CPU內(nèi)的總線連接在一起滔韵。每個(gè)外部設(shè)備(例如:顯示器、硬盤掌实、鍵盤陪蜻、鼠標(biāo)、網(wǎng)卡等等)則是由外設(shè)控制器贱鼻、I/O端口宴卖、和輸入輸出硬件組成。外設(shè)控制器負(fù)責(zé)設(shè)備的控制和操作邻悬,I/O端口負(fù)責(zé)數(shù)據(jù)的臨時(shí)存儲(chǔ)症昏,輸入輸出硬件則負(fù)責(zé)具體的輸入輸出,它們間也通過(guò)外部設(shè)備內(nèi)的總線連接在一起拘悦。
上面的計(jì)算機(jī)系統(tǒng)結(jié)構(gòu)圖中我們可以看出硬件系統(tǒng)的這種組件化的設(shè)計(jì)思路總是貫徹到各個(gè)環(huán)節(jié)齿兔。在這套設(shè)計(jì)思想(馮.諾依曼體系架構(gòu))里面,總是有一部分負(fù)責(zé)控制础米、一部分負(fù)責(zé)執(zhí)行分苇、一部分則負(fù)責(zé)存儲(chǔ),它之間進(jìn)行交互以及接口通信則總是通過(guò)總線來(lái)完成屁桑。這種設(shè)計(jì)思路一樣的可以應(yīng)用在我們的軟件設(shè)計(jì)體系里面:組件和組件之間通信通過(guò)事件的方式來(lái)進(jìn)行解耦處理医寿,而一個(gè)組件內(nèi)部同樣也需要明確好各個(gè)部分的職責(zé)(一部分負(fù)責(zé)調(diào)度控制、一部分負(fù)責(zé)執(zhí)行實(shí)現(xiàn)蘑斧、一部分負(fù)責(zé)數(shù)據(jù)存儲(chǔ))靖秩。
緩存
一個(gè)完整的CPU系統(tǒng)里面有控制部件、運(yùn)算部件還有寄存器部件竖瘾。
其中寄存器部件的作用就是進(jìn)行數(shù)據(jù)的臨時(shí)存儲(chǔ)沟突。既然有內(nèi)存作為數(shù)據(jù)存儲(chǔ)的場(chǎng)所,那么為什么還要有寄存器呢捕传?答案就是速度和成本惠拭。我們知道CPU的運(yùn)算速度是非常快的,如果把運(yùn)算的數(shù)據(jù)都放到內(nèi)存里面的話那將大大降低整個(gè)系統(tǒng)的性能职辅。解決的辦法是在CPU內(nèi)部開(kāi)辟一小塊臨時(shí)存儲(chǔ)區(qū)域棒呛,并在進(jìn)行運(yùn)算時(shí)先將數(shù)據(jù)從內(nèi)存復(fù)制到這一小塊臨時(shí)存儲(chǔ)區(qū)域中,運(yùn)算時(shí)就在這一小快臨時(shí)存儲(chǔ)區(qū)域內(nèi)進(jìn)行域携。我們稱這一小塊臨時(shí)存儲(chǔ)區(qū)域?yàn)榧拇嫫鞔孛搿R驗(yàn)榧拇嫫骱瓦\(yùn)算器以及控制器是非常緊密的聯(lián)系在一起的,它們的頻率一致秀鞭,所以運(yùn)算時(shí)就不會(huì)因?yàn)閿?shù)據(jù)的來(lái)回傳輸以及各設(shè)備之間的頻率差異導(dǎo)致系統(tǒng)性能的整體下降趋观。你可能又會(huì)問(wèn)為什么不把整個(gè)內(nèi)存都集成進(jìn)CPU中去呢?答案其實(shí)還是成本問(wèn)題锋边!
因?yàn)镃PU速度很快拆内,相應(yīng)的寄存器也需要存取很快,二者速度上要匹配宠默,所以這些寄存器的制作難度大,選材精灵巧,而且是集成到芯片內(nèi)部搀矫,所價(jià)格高。而內(nèi)存的成本則相對(duì)低廉刻肄,而且從工藝上來(lái)說(shuō)瓤球,我們不可能在CPU內(nèi)部集成大量的存儲(chǔ)單元。
運(yùn)算的問(wèn)題通過(guò)寄存器解決了敏弃,但是還存在一個(gè)問(wèn)題:我們知道程序在運(yùn)行時(shí)是要將所有可執(zhí)行的二進(jìn)制指令代碼都裝載到內(nèi)存里面去卦羡,CPU每執(zhí)行一條指令前都需要從內(nèi)存中將指令讀取到CPU內(nèi)并執(zhí)行。如果按這樣每次都從內(nèi)存讀取一條指令來(lái)依次執(zhí)行的話麦到,那還是存在著CPU和內(nèi)存之間的處理瓶頸問(wèn)題绿饵,從而造成整體性能的下降。這個(gè)問(wèn)題怎么解決呢瓶颠?答案就是高速緩存拟赊。
其實(shí)在CPU內(nèi)部不僅有為解決運(yùn)算問(wèn)題而設(shè)計(jì)的寄存器,還集成了一個(gè)部分高速緩存存儲(chǔ)區(qū)域粹淋。高度緩存的制造成本要比寄存器低吸祟,但是比內(nèi)存的制造成本高,容量要比寄存器大桃移,但是比內(nèi)存的容量小很多屋匕。雖然沒(méi)有寄存器和運(yùn)算器之間的距離那么緊密,但是要比內(nèi)存到運(yùn)算器之間的距離要近很多借杰。一般情況下CPU內(nèi)的高速緩存可能只有幾KB或者幾十KB那么大过吻。
正是通過(guò)高速緩存的引入,當(dāng)程序在運(yùn)行時(shí)第步,就可以預(yù)先將部分在內(nèi)存中要執(zhí)行的指令代碼以及數(shù)據(jù)復(fù)制到高速緩存中去疮装,而CPU則不再每次都從內(nèi)存中讀取指令而是直接從高速緩存依次讀取指令來(lái)執(zhí)行缘琅,從而加快了整體的速度。當(dāng)然要預(yù)讀取哪塊內(nèi)存區(qū)域的指令和數(shù)據(jù)到緩存上以及怎么去讀取這些工作都交給操作系統(tǒng)去調(diào)度完成廓推,這里面的算法和邏輯也非常的復(fù)雜刷袍,大家可以通過(guò)學(xué)習(xí)操作系統(tǒng)相關(guān)的課程去了解,這里就不再展開(kāi)了樊展∩胛疲可以看出高速緩存的作用解決了不同速度設(shè)備之間的數(shù)據(jù)傳遞問(wèn)題。在實(shí)際中CPU內(nèi)部可能不止設(shè)有一級(jí)高速緩存专缠,有可能會(huì)配備兩級(jí)到三級(jí)的高速緩存雷酪,越高級(jí)的高速緩存速度越快,容量越低涝婉,而越低級(jí)的高度緩存則速度越慢哥力,但是容量越大。比如iPhoneX上的搭載的arm處理器A11里面除了固有的37個(gè)通用寄存器外墩弯,L1級(jí)緩存的容量是64KB吩跋, L2級(jí)緩存的容量達(dá)到了8M(這么大的二級(jí)緩存,都有可能在你的程序代碼少時(shí)可以一次性將代碼讀到緩存中去運(yùn)行)渔工, 沒(méi)有配備三級(jí)緩存锌钮。
我們知道在軟件設(shè)計(jì)上有一個(gè)所謂的空間換時(shí)間的概念,就是當(dāng)兩個(gè)對(duì)象之間進(jìn)行交互時(shí)因?yàn)槎咛幚硭俣炔⒉灰恢聲r(shí)引矩,我們就需要引入緩存來(lái)解決讀寫(xiě)不一致的問(wèn)題梁丘。比如文件讀寫(xiě)或者socket通信時(shí),因?yàn)镮O設(shè)備的處理速度很慢旺韭,所以在進(jìn)行文件讀寫(xiě)以及socket通信時(shí)總是要將讀出或者寫(xiě)入的部分?jǐn)?shù)據(jù)先保存到一個(gè)緩存中氛谜,然后再統(tǒng)一的執(zhí)行讀出和寫(xiě)入操作。
可以看出無(wú)論是在硬件層面上還是在軟件層面上区端,當(dāng)兩個(gè)組件之間因?yàn)樗俣葐?wèn)題不能進(jìn)行同步交互時(shí)混蔼,就可以借助緩存技術(shù)來(lái)彌補(bǔ)這種不平衡的狀況
指令中的寄存器
CPU執(zhí)行的每條指令都由操作碼和操作數(shù)組成,簡(jiǎn)單理解就是要對(duì)誰(shuí)(操作數(shù))做什么(操作碼)珊燎。在CPU內(nèi)部要運(yùn)算的數(shù)據(jù)總是放在寄存器中惭嚣,而實(shí)際的數(shù)據(jù)則有可能是放在內(nèi)存或者是IO端口中。因此我們的程序其實(shí)大部分時(shí)間就是做了如下三件事情:
- 1.把內(nèi)存或者I/O端口的數(shù)據(jù)讀取到寄存器中
- 2.將寄存器中的數(shù)據(jù)進(jìn)行運(yùn)算(運(yùn)算只能在寄存器中進(jìn)行)
- 3.將寄存器的內(nèi)容回寫(xiě)到內(nèi)存或者I/O端口中
這三件事情都是跟寄存器有關(guān)悔政,寄存器就是數(shù)據(jù)存儲(chǔ)的中轉(zhuǎn)站晚吞,非常的關(guān)鍵,因此在CPU所提供的指令中谋国,如果操作數(shù)有兩個(gè)時(shí)至少要有一個(gè)是寄存器槽地。
;下面部分是arm64指令示例:
mov x0, #0x100 ;將常數(shù)0x100賦值給寄存器x0
mov x1, x0 ;將寄存器x0的值賦值給寄存器x1
ldr x3, [sp, #0x8] ;將棧頂加0x8處的內(nèi)存值賦值給x3寄存器
add x0, x1, x2 ;x0 = x1 + x2 可以看出運(yùn)算的指令必須放在寄存器中
sub x0, x1, x2 ;r0 = x1 - x2
str x1, [sp, #0x08] ;將寄存器x1中的值保存到棧頂加0x8處的內(nèi)存處。
;下面部分是x64指令示例(AT&T匯編):
mov $0x100, %rax ;將常數(shù)0x100賦值給寄存器rax
mov %rax, %rbx ;將寄存器rax的值賦值給rbx寄存器
movq 8(%rax), %rbx ;將寄存器rax中的值+8并將所指向內(nèi)存中的數(shù)據(jù)賦值給rbx寄存器
寄存器的分類
寄存器是CPU中的數(shù)據(jù)臨時(shí)存儲(chǔ)單元,不同的CPU體系結(jié)構(gòu)中的寄存器的數(shù)量是不一致的比如: arm64體系下的CPU就提供了37個(gè)64位的通用的寄存器捌蚊,而x64體系下的CPU就提供了16個(gè)64位的通用寄存器集畅。在說(shuō)分類之前要說(shuō)一下寄存器的長(zhǎng)度問(wèn)題。有時(shí)候我們看匯編代碼時(shí)會(huì)發(fā)現(xiàn)代碼中出現(xiàn)了x0, w0(arm64); 或者rax, eax, ax, al(x64)缅糟。 它們之間有什么關(guān)系嗎挺智? 寄存器是存儲(chǔ)單元,意味著它具備一定的容量窗宦,也就是每個(gè)寄存器能保存的最大的數(shù)值是多少赦颇,也就是寄存器的位數(shù)。不同CPU架構(gòu)下的寄存器的位數(shù)有差別赴涵,這個(gè)跟CPU的字長(zhǎng)有關(guān)系媒怯。一般情況下64位字長(zhǎng)的CPU提供的寄存器的容量是64個(gè)bit位,而32位字長(zhǎng)的CPU提供的寄存器的容量是32個(gè)bit位髓窜。比如arm64體系下的CPU提供的37個(gè)通用寄存器的容量都是8個(gè)字節(jié)的扇苞,所以每個(gè)寄存器能保存的數(shù)值范圍就是(0到2^64次方)。
對(duì)于x64系的CPU來(lái)說(shuō)寄纵,如果寄存器以r開(kāi)頭則表明的是一個(gè)64位的寄存器杨拐,如果以e開(kāi)頭則表明是一個(gè)32位的寄存器,同時(shí)系統(tǒng)還提供了16位的寄存器以及8位的寄存器擂啥。32位的寄存器是64位寄存器的低32位部分并不是獨(dú)立存在的,16位寄存器則是32位寄存器的低16位部分并不是獨(dú)立存在的帆阳,8位寄存器則是16位寄存器的低8位部分并不是獨(dú)立存在的哺壶。
對(duì)于arm64系的CPU來(lái)說(shuō), 如果寄存器以x開(kāi)頭則表明的是一個(gè)64位的寄存器蜒谤,如果以w開(kāi)頭則表明是一個(gè)32位的寄存器山宾,在系統(tǒng)中沒(méi)有提供16位和8位的寄存器供訪問(wèn)和使用。其中32位的寄存器是64位寄存器的低32位部分并不是獨(dú)立存在的鳍徽。
不管寄存器的長(zhǎng)度如何资锰,它們有些用來(lái)存放將要執(zhí)行的指令地址,有些用來(lái)存儲(chǔ)要運(yùn)算的數(shù)據(jù)阶祭,有些用來(lái)存儲(chǔ)計(jì)算的結(jié)果狀態(tài)绷杜,有些用來(lái)保存內(nèi)存的基地址信息,有些用來(lái)保存要運(yùn)算的浮點(diǎn)數(shù)濒募。因此CPU中的寄存器可以按照作用進(jìn)行如下分類:
1.數(shù)據(jù)地址寄存器
數(shù)據(jù)地址寄存器通常用來(lái)做數(shù)據(jù)計(jì)算的臨時(shí)存儲(chǔ)鞭盟、做累加、計(jì)數(shù)瑰剃、地址保存等功能齿诉。定義這些寄存器的作用主要是用于在CPU指令中保存操作數(shù),在CPU中當(dāng)做一些常規(guī)變量來(lái)使用。所以我們的代碼里面看到的以及用到的最多的寄存器就是這些寄存器:
如果你仔細(xì)觀察一些匯編代碼中的寄存器的使用粤剧,其實(shí)你會(huì)發(fā)現(xiàn)一些特點(diǎn):
- 在x64體系中RAX以及arm64體系中的X0一般都用來(lái)保存函數(shù)的返回值
- 在函數(shù)調(diào)用時(shí)的參數(shù)傳遞在x64體系中分別保存在RDI,RSI,RDX,RCX,R8,R9...歇竟;而在arm64體系中則分別保存在X0,X1,X2,....中。
- arm64體系中的XZR,WZR表示為一個(gè)特殊的寄存器抵恋,就是用來(lái)表示0
- arm64體系中的X8一般用來(lái)表示全局變量或者常量的偏移地址焕议。而 X16,X17則有特殊的用途一般用來(lái)保存間接調(diào)用時(shí)的函數(shù)地址。
- arm64中的X29寄存器特殊用于保存函數(shù)棧的基址寄存器(X29也叫FP)馋记,所以一般不能用于其他用途号坡。
2.Intel架構(gòu)CPU的段寄存器
早期的16位實(shí)模式程序中的內(nèi)存訪問(wèn)都是基于物理地址的,而且還把整個(gè)程序拆分為數(shù)據(jù)段梯醒、代碼段宽堆、棧段、擴(kuò)展段四個(gè)區(qū)域茸习,每個(gè)內(nèi)存區(qū)段內(nèi)的地址編碼都是相對(duì)于這個(gè)段的偏移來(lái)設(shè)置的畜隶,因此為了定位和區(qū)分這些內(nèi)存區(qū)段,CPU分別設(shè)置了CS,DS,SS,ES四個(gè)寄存器來(lái)保存這些段的基地址号胚。后來(lái)隨著CPU和操作系統(tǒng)的發(fā)展籽慢,應(yīng)用程序不再直接訪問(wèn)物理內(nèi)存地址了,而是訪問(wèn)由操作系統(tǒng)提供的虛擬內(nèi)存地址猫胁,同時(shí)也不再把整個(gè)內(nèi)存空間劃分為數(shù)據(jù)段和代碼段了箱亿,而是提供一個(gè)從0開(kāi)始的平坦連續(xù)的內(nèi)存空間了,同時(shí)將程序所能訪問(wèn)的內(nèi)存區(qū)域和操作系統(tǒng)內(nèi)核所能訪問(wèn)的內(nèi)存區(qū)域進(jìn)行了隔離弃秆,我們稱這樣的程序?yàn)?code>保護(hù)模式下運(yùn)行的程序届惋。 因此這時(shí)候里面的CS,DS,SS,ES寄存器的作用將不再用于保存內(nèi)存區(qū)域的基地址了,同時(shí)還增加了FS,GS兩個(gè)寄存器菠赚,這6個(gè)寄存器的作用變?yōu)榱吮4娌僮飨到y(tǒng)進(jìn)入用戶態(tài)還是核心態(tài)以及進(jìn)行用戶態(tài)和核心態(tài)之間進(jìn)行切換上下文數(shù)據(jù)的功能了脑豹。也就是在保護(hù)模式下運(yùn)行的程序我們將不需要也沒(méi)有權(quán)利去訪問(wèn)這些段寄存器了。如果你想了解更加具體的內(nèi)容請(qǐng)搜索:全局描述符表與局部描述符表 相關(guān)的知識(shí)衡查。在arm體系的CPU中則沒(méi)有專門提供這些所謂的段寄存器:
上圖是平坦內(nèi)存模式和分段內(nèi)存模式下的應(yīng)用結(jié)構(gòu)
這里面需要澄清的是我們的程序內(nèi)存區(qū)域雖然從物理上不再劃分為代碼段瘩欺、數(shù)據(jù)段、棧段幾個(gè)獨(dú)立的內(nèi)存空間拌牲。但是在平坦內(nèi)存模式下我們依然保留了代碼段俱饿、數(shù)據(jù)段、棧段的劃分塌忽,每個(gè)段的基地址都是從0開(kāi)始稍途,只是各種類型的數(shù)據(jù)存放到了不同的內(nèi)存空間中去了,也就是說(shuō)程序分段的機(jī)制由硬件劃分轉(zhuǎn)化為了軟件劃分了砚婆。
3.棧寄存器
棧的概念械拍,在學(xué)習(xí)數(shù)據(jù)結(jié)構(gòu)的時(shí)候就已經(jīng)有了解突勇,棧是一塊具有后進(jìn)先出功能的存儲(chǔ)區(qū)域,在進(jìn)行操作時(shí)我們總是只能將數(shù)據(jù)壓入棧頂坷虑,或者將數(shù)據(jù)從棧頂彈出來(lái)甲馋。
棧空間和操作 因?yàn)楦↑c(diǎn)數(shù)的存儲(chǔ)以及其運(yùn)算的特殊性谒养,所以CPU中專門提供FPU以及相應(yīng)的浮點(diǎn)數(shù)寄存器來(lái)處理浮點(diǎn)數(shù),除了一些浮點(diǎn)數(shù)狀態(tài)和控制寄存器(比如四舍五入的處理方式等)外主要就是一些保存浮點(diǎn)數(shù)的寄存器: 現(xiàn)在的CPU除了支持標(biāo)量運(yùn)算外明郭,還支持向量運(yùn)算买窟。向量運(yùn)算在圖形處理相關(guān)的領(lǐng)域用得非常的多。為了支持向量計(jì)算系統(tǒng)了也提供了眾多的向量寄存器达址,以及SSE和SIMD指令集: 狀態(tài)寄存器用來(lái)保存指令運(yùn)行結(jié)果的一些信息趁耗,比如相加的結(jié)果是否溢出沉唠、結(jié)果是否為0、以及是否是負(fù)數(shù)等苛败。CPU的某些指令會(huì)根據(jù)運(yùn)行的結(jié)果來(lái)設(shè)置狀態(tài)寄存器的狀態(tài)位满葛,而某些指令則是根據(jù)這些狀態(tài)寄存器中的值來(lái)進(jìn)行處理。比如一些條件跳轉(zhuǎn)指令或者比較指令等等罢屈。我們?cè)诟呒?jí)語(yǔ)言里面的條件判斷最終在轉(zhuǎn)化為機(jī)器指令時(shí)嘀韧,機(jī)器指令就是根據(jù)狀態(tài)寄存器里面的特殊位置來(lái)進(jìn)行跳轉(zhuǎn)的。 我們知道程序代碼是保存在內(nèi)存中的柔昼,那CPU又是如何知道要執(zhí)行哪一條保存在內(nèi)存中的指令呢?這就是通過(guò)指令寄存器來(lái)完成的炎辨。因?yàn)閮?nèi)存中的指令總是按線性序列保存的捕透,CPU只是按照編制好的程序來(lái)執(zhí)行指令。因此CPU內(nèi)提供一個(gè)指令寄存器來(lái)記錄CPU下一條將要執(zhí)行的指令的內(nèi)存地址碴萧,這樣每次執(zhí)行完畢一條指令后乙嘀,CPU就根據(jù)指令寄存器中所記錄的地址到內(nèi)存中去讀取指令并執(zhí)行,同時(shí)又將下一條指令的內(nèi)存地址保存到指令寄存器中破喻,就這樣就重復(fù)不斷的處理來(lái)完成整個(gè)程序的執(zhí)行虎谢。 但是這里面有兩問(wèn)題: 前面不是說(shuō)CPU內(nèi)有高速緩存嗎?怎么又說(shuō)每次都去訪問(wèn)內(nèi)存呢低缩?而且保存還是內(nèi)存的地址呢嘉冒。 這是沒(méi)有問(wèn)題的,指令寄存器中保存的確實(shí)是下一條指令在內(nèi)存中的地址咆繁,但是操作系統(tǒng)除了將部分內(nèi)存區(qū)域中的指令保存到高速緩存外還會(huì)建立一個(gè)內(nèi)存地址到高速緩存地址之間的映射關(guān)系數(shù)據(jù)結(jié)構(gòu)讳推。因此即使是指令寄存器中保存的是內(nèi)存地址,但是在指令真實(shí)執(zhí)行時(shí)CPU就會(huì)根據(jù)指令寄存器中的內(nèi)存地址以及內(nèi)部建立的內(nèi)存和高速緩存的映射關(guān)系來(lái)轉(zhuǎn)化為指令在高速緩存中的地址來(lái)讀取指令并執(zhí)行玩般。當(dāng)然如果發(fā)現(xiàn)指令并不在高速緩存中時(shí)银觅,CPU就會(huì)觸發(fā)一個(gè)中斷并告訴操作系統(tǒng),操作系統(tǒng)再根據(jù)特定的策略從內(nèi)存中再次讀取一塊新的內(nèi)存數(shù)據(jù)到高速緩存中坏为,并覆蓋掉原先保存在高速緩存中的內(nèi)容究驴,然后CPU再次讀取高速緩存中的指令后繼續(xù)執(zhí)行。 如果說(shuō)指令寄存器每次都是保存的順序執(zhí)行指令的話那么怎么去實(shí)現(xiàn)跳轉(zhuǎn)邏輯呢匀伏? 答案是跳轉(zhuǎn)指令和函數(shù)調(diào)用指令的存在洒忧。我們的用戶態(tài)中的代碼不能去人為的改變指令寄存器的值,也就是不能對(duì)指令寄存器進(jìn)行賦值够颠,因此默認(rèn)情況下指令寄存器總是由CPU內(nèi)部設(shè)置為下一條指令的地址熙侍,但是跳轉(zhuǎn)指令和函數(shù)調(diào)用指令例外,這兩條指令的主要作用就是用來(lái)改變指令寄存器的內(nèi)容履磨,正是因?yàn)樘D(zhuǎn)功能才使得我們的程序可以不只按順序去執(zhí)行而是具有條件執(zhí)行和循環(huán)執(zhí)行代碼的能力蛉抓。 在x64體系的CPU中提供了一個(gè)64位的指令寄存器RIP,而在arm64體系的CPU中則提供了一個(gè)64位的PC寄存器剃诅。需要再次強(qiáng)調(diào)的是指令寄存器保存的是下一條將要執(zhí)行的指令的內(nèi)存地址巷送,而不是當(dāng)前正在執(zhí)行的指令的內(nèi)存地址。 這里再看一下arm64體系下的PC和LR寄存器矛辕,我們先看下面一張圖: PC寄存器和LR寄存器 上面列出的都是我們?cè)诰幊虝r(shí)會(huì)用到的寄存器擦剑,其實(shí)CPU內(nèi)部還有很多專門用于控制的寄存器以及用于調(diào)試的寄存器,這些寄存器一般都提供給操作系統(tǒng)使用或者用于CPU內(nèi)部調(diào)試使用芥颈。這里就不再進(jìn)行介紹了惠勒,感興趣的同學(xué)可以去下載一本x64或者arm手冊(cè)進(jìn)行學(xué)習(xí)和了解。 這里面需要澄清的是上述中的寄存器名稱只是匯編語(yǔ)言里面對(duì)寄存器的一個(gè)別稱或者有意義的命名爬坑,我們知道機(jī)器指令是二進(jìn)制數(shù)據(jù)纠屋,一條機(jī)器指令里面無(wú)論是操作碼還是操作數(shù)都是二進(jìn)制編碼的,二進(jìn)制數(shù)據(jù)太過(guò)晦澀難以理解盾计,所以才有了匯編語(yǔ)言的誕生售担,匯編語(yǔ)言是一種機(jī)器指令的助記語(yǔ)言,他只不過(guò)是以人類更容易理解的自然語(yǔ)言的方式來(lái)描述一條機(jī)器指令而已署辉。所以雖然上面的寄存器看到的是一個(gè)個(gè)字母族铆,但是在機(jī)器語(yǔ)言里面,則是通過(guò)給寄存器編號(hào)來(lái)表示某個(gè)寄存器的哭尝。還記得在我的介紹指令集的文章里面哥攘,你有看到過(guò)里面的虛擬CPU里面的寄存器的定義嗎: 上面的枚舉你可以看到我們?cè)诖a里面用Reg0, Reg1...來(lái)表示虛擬的寄存器編號(hào),但是實(shí)際的寄存器編號(hào)則分別為0材鹦,1... 真實(shí)中的CPU的寄存器也是如此編號(hào)的逝淹,我們來(lái)看下面一段代碼,以及其中的機(jī)器指令: mov指令的二進(jìn)制結(jié)構(gòu)如下: 上圖是arm64中的mov指令的結(jié)構(gòu) 可見(jiàn)上面的二進(jìn)制機(jī)器指令中關(guān)于寄存器部分的字段Rd分別從0到2而出現(xiàn)了差異桶唐,從而說(shuō)明了寄存器讀寫(xiě)的編碼規(guī)則栅葡。寄存器編碼的機(jī)制和內(nèi)存地址編碼是同樣的原理和機(jī)制,CPU訪問(wèn)內(nèi)存數(shù)據(jù)時(shí)總是要指定內(nèi)存數(shù)據(jù)所在的地址尤泽,同樣CPU訪問(wèn)某個(gè)寄存器時(shí)一樣的要通過(guò)寄存器編碼來(lái)完成欣簇,這些東西統(tǒng)統(tǒng)都體現(xiàn)在指令里面。 我們的代碼并不是只在單線程中執(zhí)行安吁,而是可能在多個(gè)線程中執(zhí)行醉蚁。那么這里你就可能會(huì)產(chǎn)生一個(gè)疑問(wèn)燃辖?既然進(jìn)程中有多個(gè)線程在并行執(zhí)行鬼店,而CPU中的寄存器又只有那么一套,如果不加處理豈不會(huì)產(chǎn)生數(shù)據(jù)錯(cuò)亂的場(chǎng)景黔龟?答案是否定的妇智。我們知道線程是一個(gè)進(jìn)程中的執(zhí)行單元滥玷,每個(gè)線程的調(diào)度執(zhí)行其實(shí)都是通過(guò)操作系統(tǒng)來(lái)完成。也就是說(shuō)哪個(gè)線程占有CPU執(zhí)行以及執(zhí)行多久都是由操作系統(tǒng)控制的巍棱。具體的實(shí)現(xiàn)是每創(chuàng)建一個(gè)線程時(shí)都會(huì)為這線程創(chuàng)建一個(gè)數(shù)據(jù)結(jié)構(gòu)來(lái)保存這個(gè)線程的信息惑畴,我們稱這個(gè)數(shù)據(jù)結(jié)構(gòu)為線程上下文, 寄存器數(shù)據(jù)被切換的問(wèn)題也同樣會(huì)出現(xiàn)在函數(shù)的調(diào)用上,舉個(gè)例子來(lái)說(shuō):假設(shè)我們正在調(diào)用foo1函數(shù)萝究,在foo1中我們的代碼指令會(huì)用到x0,x1,x2等寄存器進(jìn)行數(shù)據(jù)運(yùn)算和存儲(chǔ)免都。假設(shè)我們?cè)趂oo1中的某處調(diào)用foo2函數(shù),這時(shí)候因?yàn)閒oo2函數(shù)內(nèi)部的代碼指令也可能會(huì)用到x0,x1,x2等寄存器帆竹。绕娘。那么問(wèn)題就來(lái)了,因?yàn)閒oo2內(nèi)部的執(zhí)行會(huì)改變x0,x1,x2寄存器的內(nèi)容栽连,那么當(dāng)foo2函數(shù)返回并再次執(zhí)行foo1下面的代碼時(shí)险领,就有可能x0,x1,x2等寄存器的內(nèi)容被改動(dòng)而跟原先的值不一致了,從而導(dǎo)致數(shù)據(jù)錯(cuò)亂問(wèn)題的發(fā)生秒紧。那么這又是如何解決的呢绢陌?解決的方法就是由編譯器在編譯出機(jī)器指令時(shí)按一定的規(guī)則進(jìn)行編譯(這是一種ABI規(guī)則,什么是ABI后續(xù)我會(huì)詳細(xì)介紹)熔恢。 我們知道在高級(jí)語(yǔ)言中定義的變量無(wú)論是局部還是全局變量或者是堆內(nèi)存分配的變量都是在內(nèi)存中存儲(chǔ)的脐湾。編譯為機(jī)器指令后,對(duì)內(nèi)存數(shù)據(jù)進(jìn)行處理時(shí)則總是要將內(nèi)存中的數(shù)據(jù)轉(zhuǎn)移到寄存器中進(jìn)行叙淌,然后再將處理的結(jié)果寫(xiě)回到內(nèi)存中去秤掌,這種場(chǎng)景會(huì)發(fā)生在每次進(jìn)行變量訪問(wèn)的情形中愁铺。我們來(lái)看如下的高級(jí)語(yǔ)言代碼: 雖然我們?cè)趂oo1和foo2里面都定義了a,b,c三個(gè)變量,但是因?yàn)檫@三個(gè)變量分別保存在foo1和foo2的不同棧內(nèi)存區(qū)闻鉴,他們都是局部變量因此兩個(gè)函數(shù)之間的變量是不會(huì)受到影響的茵乱。但是如果是機(jī)器指令則不一樣了,因?yàn)檫\(yùn)算時(shí)總是要將內(nèi)存數(shù)據(jù)移動(dòng)到寄存器中去孟岛,但是寄存器只有一份瓶竭。 從上面的代碼對(duì)應(yīng)關(guān)系可以看出在验,每次高級(jí)語(yǔ)言的賦值處理總是先讀取再計(jì)算然后再寫(xiě)回三步,因此當(dāng)調(diào)用foo2函數(shù)前,所有寄存器其實(shí)都是處于空閑的或者可以被任意修改的狀態(tài)堵未。而調(diào)用完畢函數(shù)后要訪問(wèn)變量時(shí)又再次從內(nèi)存讀取到寄存器腋舌,運(yùn)算完畢后再立即寫(xiě)回到內(nèi)存中。正是這種每次訪問(wèn)數(shù)據(jù)時(shí)都從內(nèi)存讀取到寄存器渗蟹,處理后立即再寫(xiě)會(huì)內(nèi)存的機(jī)制就足以保證了即使在函數(shù)調(diào)用函數(shù)時(shí)也不會(huì)出現(xiàn)數(shù)據(jù)混亂的問(wèn)題發(fā)生块饺。 上面是對(duì)寄存器復(fù)用的兩種不同的策略:空間換時(shí)間和時(shí)間換空間。 在軟件設(shè)計(jì)中當(dāng)存在有某個(gè)共享資源被多個(gè)系統(tǒng)競(jìng)爭(zhēng)或者使用時(shí)我們就可以考慮采用上面的兩種不同方案來(lái)解決我們的問(wèn)題雌芽。
從上面可以看出要維護(hù)一個(gè)棧區(qū)域就必須要提供2個(gè)寄存器迄损,一個(gè)寄存器用來(lái)保存棧的基地址也就是棧的底部定躏,而一個(gè)寄存器則用來(lái)保存棧的偏移也就是棧的頂部。在一般的系統(tǒng)中芹敌,我們都將棧的基地址設(shè)置在內(nèi)存的高位痊远,而將棧頂?shù)刂吩O(shè)置在內(nèi)存的低位。因此每當(dāng)有進(jìn)棧操作時(shí)則將棧頂?shù)刂愤M(jìn)行遞減氏捞,而當(dāng)有出棧操作時(shí)則將棧頂?shù)刂愤f增碧聪。棧的這種特性,使得他非常適合于保存函數(shù)中定義的局部變量液茎,以及函數(shù)內(nèi)調(diào)用函數(shù)的情況逞姿。
在x64體系的CPU中,提供了一個(gè)專門的RBP寄存用來(lái)保存棧的基地址捆等, 同時(shí)提供一個(gè)專門的RSP寄存器來(lái)保存棧的棧頂?shù)刂罚?/strong>滞造;而arm64體系的CPU中則沒(méi)有設(shè)置專門的棧基址寄存器而是一般用X29寄存器來(lái)保存棧的基地址(至少在iOS的64位系統(tǒng)里面是如此的)栋烤,但是設(shè)置一個(gè)SP寄存器來(lái)保存棧的棧頂?shù)刂贰?/p>
4.浮點(diǎn)和向量寄存器
5.狀態(tài)寄存器。
缠捌。在x64體系的CPU中提供了一個(gè)64位的RFLAGS寄存器來(lái)作為狀態(tài)寄存器锄贷;arm64體系的CPU則提供了一個(gè)32位的CPSR寄存器來(lái)作為狀態(tài)寄存器译蒂。
狀態(tài)寄存器的內(nèi)容由CPU內(nèi)部進(jìn)行置位,我們的程序中不能將某個(gè)數(shù)值賦值給狀態(tài)寄存器谊却。
6.指令寄存器(程序計(jì)數(shù)器)
從上面的圖中我們可以看出PC寄存器和LR寄存器所表示的意義:PC寄存器保存的是下一條將要執(zhí)行的指令的內(nèi)存地址笑跛,而不是當(dāng)前正在執(zhí)行的指令的內(nèi)存地址付魔。LR寄存器則保存著最后一次函數(shù)調(diào)用指令的下一條指令的內(nèi)存地址。那么LR寄存器有什么作用嗎堡牡?答案就是為了做函數(shù)調(diào)用棧跟蹤抒抬,我們的程序在崩潰時(shí)能夠?qū)⒑瘮?shù)調(diào)用棧打印出來(lái)就是借助了LR寄存器來(lái)實(shí)現(xiàn)的。具體的實(shí)現(xiàn)原理我會(huì)在后面的文章里面詳細(xì)介紹晤柄。7.其他寄存器
寄存器的編碼
//定義寄存器編號(hào)
typedef enum : int {
Reg0,
Reg1,
Reg2,
Reg3
} RegNum;
mov x0, #0x0 ;0xD2800000
mov x1, #0x0 ;0xD2800001
mov x2, #0x0 ;0xD2800002
寄存器的復(fù)用
1.線程切換時(shí)的寄存器復(fù)用
每個(gè)線程的上下文中有一部分?jǐn)?shù)據(jù)是用來(lái)保存當(dāng)前所有寄存器的副本航徙。
每當(dāng)操作系統(tǒng)暫停一個(gè)線程時(shí)如贷,就會(huì)將CPU中的所有寄存器的當(dāng)前內(nèi)容都保存到線程上下文數(shù)據(jù)結(jié)構(gòu)中。而操作系統(tǒng)要讓另外一個(gè)線程執(zhí)行時(shí)則將要執(zhí)行的線程的上下文中保存的所有寄存器的內(nèi)容再寫(xiě)回到CPU中到踏,并將要運(yùn)行的線程中上次保存暫停的指令也賦值給CPU的指令寄存器杠袱,并讓新線程再次執(zhí)行∥迅澹可以看出操作系統(tǒng)正是通過(guò)這種機(jī)制保證了即使是多線程運(yùn)行時(shí)也不會(huì)導(dǎo)致寄存器的內(nèi)容發(fā)生錯(cuò)亂的問(wèn)題楣富。因?yàn)槊慨?dāng)線程切換時(shí)操作系統(tǒng)都幫它們將數(shù)據(jù)處理好了。下面的部分線程上下文結(jié)構(gòu)正是指定了所有寄存器信息的部分://這個(gè)結(jié)構(gòu)是linux在arm32CPU上的線程上下文結(jié)構(gòu)伴榔,代碼來(lái)自于:http://elixir.free-electrons.com/linux/latest/source/arch/arm/include/asm/thread_info.h
//這里并沒(méi)有保存所有的寄存器纹蝴,是因?yàn)锳BI中定義linux在arm上運(yùn)行時(shí)所使用的寄存器并不是全體寄存器,所以只需要保存規(guī)定的寄存器的內(nèi)容即可踪少。這里并不是所有的CPU所保存的內(nèi)容都是一致的塘安,保存的內(nèi)容會(huì)根據(jù)CPU架構(gòu)的差異而不同。
//因?yàn)閕OS的內(nèi)核并未開(kāi)源所以無(wú)法得到iOS定義的線程上下文結(jié)構(gòu)秉馏。
//線程切換時(shí)要保存的CPU寄存器耙旦,
struct cpu_context_save {
__u32 r4;
__u32 r5;
__u32 r6;
__u32 r7;
__u32 r8;
__u32 r9;
__u32 sl;
__u32 fp;
__u32 sp;
__u32 pc;
__u32 extra[2]; /* Xscale 'acc' register, etc */
};
//線程上下文結(jié)構(gòu)
struct thread_info {
unsigned long flags; /* low level flags */
int preempt_count; /* 0 => preemptable, <0 => bug */
mm_segment_t addr_limit; /* address limit */
struct task_struct *task; /* main task structure */
__u32 cpu; /* cpu */
__u32 cpu_domain; /* cpu domain */
struct cpu_context_save cpu_context; /* cpu context */
__u32 syscall; /* syscall number */
__u8 used_cp[16]; /* thread used copro */
unsigned long tp_value[2]; /* TLS registers */
#ifdef CONFIG_CRUNCH
struct crunch_state crunchstate;
#endif
union fp_state fpstate __attribute__((aligned(8))); /*浮點(diǎn)寄存器*/
union vfp_state vfpstate; /*向量浮點(diǎn)寄存器*/
#ifdef CONFIG_ARM_THUMBEE
unsigned long thumbee_state; /* ThumbEE Handler Base register */
#endif
};
2.函數(shù)調(diào)用時(shí)的寄存器復(fù)用
void foo2()
{
int a = 20;
a = a + 2;
int b = 30;
b = b * 3;
int c = a + b;
}
void foo1()
{
int a = 10;
int b = 20;
int c = 30;
a += 10;
b += 10;
c += 10;
foo2();
c = a + b;
}
因此解決的方法就是高級(jí)語(yǔ)言里面的每一行代碼在編譯為機(jī)器指令時(shí)總是先將數(shù)據(jù)從內(nèi)存讀取到寄存器中,處理完畢后立即寫(xiě)回到內(nèi)存中去渠羞,中間并不將數(shù)據(jù)進(jìn)行任何在寄存器上的緩存
原文