簡介
通用寄存器可用於傳送和暫存數據,也可參與算術邏輯運算,並保存運算結果。除此之外,它們還各自具有一些特殊功能。通用寄存器的長度取決於機器字長,匯編語言程序員必須熟悉每個寄存器的一般用途和特殊用途,只有這樣,才能在程序中做到正確、合理地使用它們。
16位cpu通用寄存器共有 8 個:AX,BX,CX,DX,BP,SP,SI,DI.
八個寄存器都可以作為普通的數據寄存器使用。
但有的有特殊的用途:AX為累加器,CX為計數器,BX,BP為基址寄存器,SI,DI為變址寄存器,BP還可以是基
指針,SP為堆棧指針。
32位cpu通用寄存器共有 8 個: EAX,EBX,ECX,EDX,EBP,ESP,ESI,EDI功能和上面差不多
分類
數據寄存器
數據寄存器主要用來保存操作數和運算結果等信息,從而節省讀取操作數所需占用總線和訪問存儲器的時間。[1]
32位CPU有4個32位的通用寄存器EAX、EBX、ECX和EDX。對低16位數據的存取,不會影響高16位的數據。這些低16位寄存器分別命名為:AX、BX、CX和DX,它和先前的CPU中的寄存器相一致。 [1]
4個16位寄存器又可分割成8個獨立的8位寄存器(AX:AH-AL、BX:BH-BL、CX:CH-CL、DX:DH-DL),每個寄存器都有自己的名稱,可獨立存取。程序員可利用數據寄存器的這種“可分可合”的特性,靈活地處理字/字節的信息。 [1]
寄存器AX和AL通常稱為累加器(Accumulator),用累加器進行的操作可能需要更少時間。累加器可用於乘、除、輸入/輸出等操作,它們的使用頻率很高; 寄存器BX稱為基地址寄存器(Base Register)。它可作為存儲器指針來使用; 寄存器CX稱為計數寄存器(Count Register)。在循環和字符串操作時,要用它來控制循環次數;在位操作中,當移多位時,要用CL來指明移位的位數; 寄存器DX稱為數據寄存器(Data Register)。在進行乘、除運算時,它可作為默認的操作數參與運算,也可用於存放I/O的端口地址。 [1]
在16位CPU中,AX、BX、CX和DX不能作為基址和變址寄存器來存放存儲單元的地址,但在32位CPU中,其32位寄存器EAX、EBX、ECX和EDX不僅可傳送數據、暫存數據保存算術邏輯運算結果,而且也可作為指針寄存器,所以,這些32位寄存器更具有通用性。詳細內容請見第3.8節——32位地址的尋址方式。[1]
變址寄存器
32位CPU有2個32位通用寄存器ESI和EDI。其低16位對應先前CPU中的SI和DI,對低16位數據的存取,不影響高16位的數據。 [1]
寄存器ESI、EDI、SI和DI稱為變址寄存器(Index Register),它們主要用於存放存儲單元在段內的偏移量,用它們可實現多種存儲器操作數的尋址方式(在第3章有詳細介紹),為以不同的地址形式訪問存儲單元提供方便。 變址寄存器不可分割成8位寄存器。作為通用寄存器,也可存儲算術邏輯運算的操作數和運算結果。 [1]
它們可作一般的存儲器指針使用。在字符串操作指令的執行過程中,對它們有特定的要求,而且還具有特殊的功能。[1]
指針寄存器
32位CPU有2個32位通用寄存器EBP和ESP。其低16位對應先前CPU中的SBP和SP,對低16位數據的存取,不影響高16位的數據。 寄存器EBP、ESP、BP和SP稱為指針寄存器(Pointer Register),主要用於存放堆棧內存儲單元的偏移量,用它們可實現多種存儲器操作數的尋址方式(在第3章有詳細介紹),為以不同的地址形式訪問存儲單元提供方便。指針寄存器不可分割成8位寄存器。作為通用寄存器,也可存儲算術邏輯運算的操作數和運算結果。[1]
段寄存器
段寄存器是根據內存分段的管理模式而設置的。內存單元的物理地址由段寄存器的值和一個偏移量組合而成的,這樣可用兩個較少位數的值組合成一個可訪問較大物理空間的內存地址。[1]
指令指針寄存器
32位CPU把指令指針擴展到32位,並記作EIP,EIP的低16位與先前CPU中的IP作用相同。 指令指針EIP、IP(Instruction Pointer)是存放下次將要執行的指令在代碼段的偏移量。在具有預取指令功能的系統中,下次要執行的指令通常已被預取到指令隊列中,除非發生轉移情況。所以,在理解它們的功能時,不考慮存在指令隊列的情況。 在實方式下,由於每個段的最大范圍為64K,所以,EIP中的高16位肯定都為0,此時,相當於只用其低16位的IP來反映程序中指令的執行次序。[1]
主要用途
通用寄存器數據
寄存器AX乘、除運算,字的輸入輸出,中間結果的緩存
AL字節的乘、除運算,字節的輸入輸出,十進制算術運算
AH字節的乘、除運算,存放中斷的功能號
BX存儲器指針
CX串操作、循環控制的計數器
CL移位操作的計數器
DX字的乘、除運算,間接的輸入輸出
變址
變址
分類示意圖
SP堆棧的棧頂指針
指令指針IP/EIP
標志位寄存器Flag/EFlag
32位
CPU的
段寄存器16位CPU的
段寄存器ES 附加段寄存器
CS 代碼段寄存器
SS 堆棧段寄存器
DS 數據段寄存器
新增加的
段寄存器FS 附加段寄存器
GS 附加段寄存器
相關信息
寄存器是CPU內部重要的數據存儲資源,用來暫存數據和地址,是匯編程序員能直接使用的硬件資源之一。由於寄存器的存取速度比內存快,所以,在用匯編語言編寫程序時,要盡可能充分利用寄存器的存儲功能。
運算器結構
寄存器一般用來保存程序的中間結果,為隨后的指令快速提供操作數,從而避免把中間結果存入內存,再讀取內存的操作。在高級語言(如:C/C++語言)中,也有定義變量為寄存器類型的,這就是提高寄存器利用率的一種可行的方法。
另外,由於寄存器的個數和容量都有限,不可能把所有中間結果都存儲在寄存器中,所以,要對寄存器進行適當的調度。根據指令的要求,如何安排適當的寄存器,避免操作數過多的傳送操作是一項細致而又周密的工作。
閱讀提示:通過增加CPU的寄存器數量來提升64位處理器的處理速度和性能。
64 位寄存器
寄存器是一個系統可提供的最快內存類型。它們創建並存儲 CPU 操作和其他計算的結果。32 位 x86 CPU 包括 8 個通用寄存器。64 位 x64 處理器有 16 個寄存器。 Itanium 處理器擁有更先進的寄存器引擎(128 個浮點寄存器和120 個通用寄存器),並且支持更復雜的寄存器操作,這些都是通過全新的體系結構實現的。通過提供更多寄存器以及更多寄存器空間,64 位處理器(以及為它們編寫的應用程序和操作系統)可以更有效地處理數據,每個時鍾周期可以移動更多信息。
增加的可靠性
Windows 64 位系統更可靠、更靈活並且更安全,所有這一切加在一起就是更高的可靠性。64 位版本的 Microsoft Windows XP Professional 和 Windows Server 2003 支持機器檢查體系結構(Machine Check Architecture,MCA),該體系結構為所有作為機器檢查結果而報告的硬件錯誤提供一個到操作系統的接口。隨着新的可靠性功能(如多路徑 I/O 或動態系統分區)添加到未來版本的操作系統,在允許這些功能恢復到發生嚴重硬件錯誤之前的狀態時,MCA 能夠起到重要的作用。
32位CPU所含有的寄存器有:
4個數據寄存器(EAX、EBX、ECX和EDX)
2個變址和指針寄存器(ESI和EDI)
2個指針寄存器(ESP和EBP)
6個段寄存器(ES、CS、SS、DS、FS和GS)
1個指令指針寄存器(EIP)
1個標志寄存器(EFlags)
其中EAX、EBX、ECX、EDX、ESP、EBP、ESI、EDI 這8個稱為通用寄存器
32位的CPU包含了8個32位的通用寄存器
EAX與AX不是獨立的,他們屬於一個包含的關系,EAX是32位的寄存器,而AX是EAX的低16位,AX又可以分為8位的 AL(低8位寄存器) 和 AH(高8位寄存器) 寄存器
同樣的 EBX與BX、BL、BH; ECX和CX、CL,CH; EDX和DX、DL、DH 都可以分為這些。
如果對上面所說的寄存器還疑惑不解的話,這里有一張網友提供的圖來幫助大家理解:
先請看圖,圖看懂了就基本解決這個了疑問了。
00000000 00000000 00000000 00000000
|===============EAX===============|--32個0, 4個字節
|======AX=======|--16個0, 2個字節,
|==AH===|-----------8個0, 1個字節,
|===AL==|---8個0, 1個字節,
EAX上面每個0是一位,一段里面是8個0也就是8位(bit); 一共4段,因為1個字節=8 bit, 所以是32位 = 4個字節,下面的AX也是如此,他們並不是相互獨立的,而是一個整體,可以說是包含的關系,當然他們有他們各自存在的價值。
下面是常用的寄存器的功能和用處:
AX:乘、除運算,字的輸入輸出,中間結果的緩存
AL:字節的乘、除運算,字節的輸入輸出,十進制算術運算
AH:字節的乘、除運算,存放中斷的功能號
BX:存儲器指針
CX:串操作、循環控制的計數器
CL:移位操作的計數器
DX:字的乘、除運算,間接的輸入輸出
SP:堆棧的棧頂指針
說了這么多,我想你們已經對32為CPU的寄存器有所了解,那么接下來我來說一下64位CPU的寄存器:
中心句:64位CPU的寄存器與32位CPU的寄存器大同小異。
64位CPU所用的通用寄存器是64位的通用寄存器,換句話說,通用寄存器為64為的CPU才能被公認為是64為的CPU.
CPU位數有兩種不同的定義方式:
有用CPU核心中通用寄存器的位寬定義的,也有用數據總線位寬定義。
后一種定義方式確實就等於是用於傳遞數據的引腳的數量,
不過大多數人(包括我)更傾向於用前一種定義——
因為假設用后一種定義方式,那么早在Pentium Pro / Ⅱ / Ⅲ 時代,
就已經使用64位數據總線了,但是大多數人不認為它們是 64 位CPU。
(64位浮點寄存器,32位通用寄存器,36位地址總線,64位數據總線。)
我們更傾向於認為只有使用 64位通用寄存器的CPU才是真正的 64位,
而前面說的那種64位數據線,32位通用寄存器的CPU只能算是 32位的。
64為CPU和32位CPU的寄存器數目一樣,只不過是寄存器的位數變成了64,也就是說,處理器一次可以運行64bit的數據,而32為的CPU一次只可以運行32bit的數據,所以說64位的CPU的處理能力要高於32位的CPU,但是必須具備3個條件,才能發揮出優勢:
- 具備一個64位的CPU
- 具備一個64為的操作系統(系統軟件)
- 在系統上運行的是64位的應用軟件
X86-64寄存器和棧幀
CPU/寄存器/內存
因為要了解多線程,自然少不了一些硬件知識的科普,我沒有系統學習過硬件知識,僅僅是從書上以及網絡上看來的,如果有錯誤請指出來。
CPU,全名Central Processing Unit(中央處理器)。這是一塊超大規模的集成電路,包含上億的晶體管,是一台計算機的運算核心(Core)和控制核心(ControlUnit)。它的功能主要是解釋計算機指令以及處理計算機軟件中的數據。
它的主要構成是:運算器、控制器、寄存器
運算器:可以執行定點或浮點算術運算操作、移位操作以及邏輯操作,也可執行地址運算和轉換。
控制器:主要是負責對指令譯碼,並且發出為完成每條指令所要執行的各個操作的控制信號。其結構有兩種:一種是以微存儲為核心的微程序控制方式;一種是以邏輯硬布線結構為主的控制方式。
寄存器:寄存器部件,包括寄存器、專用寄存器和控制寄存器。通用寄存器又可分定點數和浮點數兩類,它們用來保存指令執行過程中臨時存放的寄存器操作數和中間(或最終)的操作結果。 通用寄存器是中央處理器的重要部件之一。
工作過程:
第一階段,提取,從存儲器或高速緩沖存儲器中檢索指令(為數值或一系列數值)。由程序計數器(Program Counter)指定存儲器的位置。(程序計數器保存供識別程序位置的數值。換言之,程序計數器記錄了CPU在程序里的蹤跡。)
第二階段:解碼(控制器)
第三階段:執行,算術邏輯單元(ALU,Arithmetic Logic Unit)將會連接到一組輸入和一組輸出。輸入提供了要相加的數值,而輸出將含有總和的結果。ALU內含電路系統,易於輸出端完成簡單的普通運算和邏輯運算(比如加法和位元運算)。如果加法運算產生一個對該CPU處理而言過大的結果,在標志暫存器里可能會設置運算溢出(Arithmetic Overflow)標志。
第四階段:回寫,緩沖Cache或者更大更廉價的低俗存儲器(內存、硬盤等等)
寄存器:是集成電路中非常重要的一種存儲單元,通常由觸發器組成。在集成電路設計中,寄存器可分為電路內部使用的寄存器和充當內外部接口的寄存器這兩類。內部寄存器不能被外部電路或軟件訪問,只是為內部電路的實現存儲功能或滿足電路的時序要求。而接口寄存器可以同時被內部電路和外部電路或軟件訪問,CPU中的寄存器就是其中一種,作為軟硬件的接口,為廣泛的通用編程用戶所熟知。
常見類型
1)數據寄存器- 用來儲存整數數字(參考以下的浮點寄存器)。在某些簡單/舊的CPU,特別的數據寄存
2)寄存器
3)寄存器
4)器是累加器,作為數學計算之用。
5)地址寄存器- 持有存儲器地址,用來訪問存儲器。在某些簡單/舊的CPU里,特別的地址寄存器是索引寄存器(可能出現一個或多個)。
6)通用目的寄存器(GPRs) - 可以保存數據或地址兩者,也就是說它們是結合數據/地址 寄存器的功用。
7)浮點寄存器(FPRs) - 用來儲存浮點數字。
8)常數寄存器- 用來持有只讀的數值(例如0、1、圓周率等等)。
9)向量寄存器- 用來儲存由向量處理器運行SIMD(Single Instruction, Multiple Data)指令所得到的數據。
10)特殊目的寄存器- 儲存CPU內部的數據,像是程序計數器(或稱為指令指針),堆棧寄存器,以及狀態寄存器(或稱微處理器狀態字組)。
11)指令寄存器(instruction register)- 儲存現在正在被運行的指令。
12)索引寄存器(index register)- 是在程序運行時用來更改運算對象地址之用。
特點
寄存器又分為內部寄存器與外部寄存器,所謂內部寄存器,其實也是一些小的存儲單元,也能存儲數據。但同存儲器相比,寄存器又有自己獨有的特點:
①寄存器位於CPU內部,數量很少,僅十四個
②寄存器所能存儲的數據不一定是8bit,有一些寄存器可以存儲16bit數據,對於386/486處理器中的一些寄存器則能存儲32bit數據
③每個內部寄存器都有一個名字,而沒有類似存儲器的地址編號。
作用
1.可將寄存器內的數據執行算術及邏輯運算
2.存於寄存器內的地址可用來指向內存的某個位置,即尋址
3.可以用來讀寫數據到電腦的周邊設備。
簡單的說:指令解析 - 數據/操作(寄存器)- 回寫(cache/memory/disk)
計算機的存儲層次(memory hierarchy)之中,寄存器最快,內存其次,最慢的是硬盤。同樣都是晶體管存儲設備,為什么寄存器比內存快呢?Mike Ash寫了一篇很好的解釋,非常通俗地回答了這個問題,有助於加深對硬件的理解。
原因一:距離不同
距離不是主要因素,但是最好懂,所以放在最前面說。內存離CPU比較遠,所以要耗費更長的時間讀取。
以3GHz的CPU為例,電流每秒鍾可以振盪30億次,每次耗時大約為0.33納秒。光在1納秒的時間內,可以前進30厘米。也就是說,在CPU的一個時鍾周期內,光可以前進10厘米。因此,如果內存距離CPU超過5厘米,就不可能在一個時鍾周期內完成數據的讀取,這還沒有考慮硬件的限制和電流實際上達不到光速。相比之下,寄存器在CPU內部,當然讀起來會快一點。距離對於桌面電腦影響很大,對於手機影響就要小得多。手機CPU的時鍾頻率比較慢(iPhone 5s為1.3GHz),而且手機的內存緊挨着CPU。
原因二:硬件設計不同(1 Byte表示一個字節, 1B=8bit)
最新的iPhone 5s,CPU是A7,寄存器有6000多位(31個64位寄存器,加上32個128位寄存器)。而iPhone 6s的內存是1GB,約為80億位(bit)。這意味着,高性能、高成本、高耗電的設計可以用在寄存器上,反正只有6000多位,而不能用在內存上。因為每個位的成本和能耗只要增加一點點,就會被放大80億倍。事實上確實如此,內存的設計相對簡單,每個位就是一個電容和一個晶體管,而寄存器的設計則完全不同,多出好幾個電子元件。並且通電以后,寄存器的晶體管一直有電,而內存的晶體管只有用到的才有電,沒用到的就沒電,這樣有利於省電。這些設計上的因素,決定了寄存器比內存讀取速度更快。
原因三:工作方式不同
寄存器的工作方式很簡單,只有兩步:(1)找到相關的位,(2)讀取這些位。
內存的工作方式就要復雜得多:
(1)找到數據的指針。(指針可能存放在寄存器內,所以這一步就已經包括寄存器的全部工作了。)
(2)將指針送往內存管理單元(MMU),由MMU將虛擬的內存地址翻譯成實際的物理地址。
(3)將物理地址送往內存控制器(memory controller),由內存控制器找出該地址在哪一根內存插槽(bank)上。
(4)確定數據在哪一個內存塊(chunk)上,從該塊讀取數據。
(5)數據先送回內存控制器,再送回CPU,然后開始使用。
內存的工作流程比寄存器多出許多步。每一步都會產生延遲,累積起來就使得內存比寄存器慢得多。為了緩解寄存器與內存之間的巨大速度差異,硬件設計師做出了許多努力,包括在CPU內部設置緩存Cache、優化CPU工作方式,盡量一次性從內存讀取指令所要用到的全部數據等等。
上面說到”緩存“,大部分程序員都知道什么是軟件架構中緩存的概念。這里所說的緩存是指硬件“高速緩沖存儲器”,是存在於主存與CPU之間的一級存儲器(常見於計算機cpu性能指標中:一級緩存、二級緩存,高配置的服務器會有三級緩存), 由靜態存儲芯片(SRAM)組成,容量比較小但速度比主存高得多, 接近於CPU的速度。在計算機存儲系統的層次結構中,是介於中央處理器和主存儲器之間的高速小容量存儲器。它和主存儲器一起構成一級的存儲器。高速緩沖存儲器和主存儲器之間信息的調度和傳送是由硬件自動進行的。高速緩沖存儲器最重要的技術指標是它的命中率(一級緩存(a=n*80%) - 二級緩存(b=a*80%) - 三級緩存(c=b*80%))。所謂的命中就是在緩存上讀取到指定的數據。
既然是緩存,那么大小肯定是有局限,也就是說不是所有cpu需要的數據都能在緩存中命中,因為它有着自己的更新策略。如下
1. 根據程序局部性規律可知:程序在運行中,總是頻繁地使用那些最近被使用過的指令和數據。這就提供了替換策略的理論依據。綜合命中率、實現的難易及速度的快慢各種因素,替換策略可有隨機法、先進先出法、最近最少使用法等。
(1).隨機法(RAND法)
隨機法是隨機地確定替換的存儲塊。設置一個隨機數產生器,依據所產生的隨機數,確定替換塊。這種方法簡單、易於實現,但命中率比較低。
(2).先進先出法(FIFO法)
先進先出法是選擇那個最先調入的那個塊進行替換。當最先調入並被多次命中的塊,很可能被優先替換,因而不符合局部性規律。這種方法的命中率比隨機法好些,但還不滿足要求。先進先出方法易於實現,
(3).最近最少使用法(LRU法)
LRU法是依據各塊使用的情況, 總是選擇那個最近最少使用的塊被替換。這種方法比較好地反映了程序局部性規律。 實現LRU策略的方法有多種。
2 在多體並行存儲系統中,由於 I/O 設備向主存請求的級別高於 CPU 訪存,這就出現了 CPU 等待 I/O 設備訪存的現象,致使 CPU 空等一段時間,甚至可能等待幾個主存周期,從而降低了 CPU 的工作效率。為了避免 CPU 與 I/O 設備爭搶訪存,可在 CPU 與主存之間加一級緩存,這樣,主存可將 CPU 要取的信息提前送至緩存,一旦主存在與 I/O 設備交換時, CPU 可直接從緩存中讀取所需信息,不必空等而影響效率。
3 目前提出的算法可以分為以下三類(第一類是重點要掌握的):
(1)傳統替換算法及其直接演化,其代表算法有 :①LRU( Least Recently Used)算法:將最近最少使用的內容替換出Cache ;②LFU( Lease Frequently Used)算法:將訪問次數最少的內容替換出Cache;③如果Cache中所有內容都是同一天被緩存的,則將最大的文檔替換出Cache,否則按LRU算法進行替換 。④FIFO( First In First Out):遵循先入先出原則,若當前Cache被填滿,則替換最早進入Cache的那個。
(2)基於緩存內容關鍵特征的替換算法,其代表算法有:①Size替換算法:將最大的內容替換出Cache②LRU— MIN替換算法:該算法力圖使被替換的文檔個數最少。設待緩存文檔的大小為S,對Cache中緩存的大小至少是S的文檔,根據LRU算法進行替換;如果沒有大小至少為S的對象,則從大小至少為S/2的文檔中按照LRU算法進行替換;③LRU—Threshold替換算法:和LRU算法一致,只是大小超過一定閾值的文檔不能被緩存;④Lowest Lacency First替換算法:將訪問延遲最小的文檔替換出Cache。
(3)基於代價的替換算法,該類算法使用一個代價函數對Cache中的對象進行評估,最后根據代價值的大小決定替換對象。其代表算法有:①Hybrid算法:算法對Cache中的每一個對象賦予一個效用函數,將效用最小的對象替換出Cache;②Lowest Relative Value算法:將效用值最低的對象替換出Cache;③Least Normalized Cost Replacement(LCNR)算法:該算法使用一個關於文檔訪問頻次、傳輸時間和大小的推理函數來確定替換文檔;④Bolot等人 提出了一種基於文檔傳輸時間代價、大小、和上次訪問時間的權重推理函數來確定文檔替換;⑤Size—Adjust LRU(SLRU)算法:對緩存的對象按代價與大小的比率進行排序,並選取比率最小的對象進行替換。
概要
說到x86-64,總不免要說說AMD的牛逼,x86-64是x86系列中集大成者,繼承了向后兼容的優良傳統,最早由AMD公司提出,代號AMD64;正是由於能向后兼容,AMD公司打了一場漂亮翻身戰。導致Intel不得不轉而生產兼容AMD64的CPU。這是IT行業以弱勝強的經典戰役。不過,大家為了名稱延續性,更習慣稱這種系統結構為x86-64。
X86-64在向后兼容的同時,更主要的是注入了全新的特性,特別的:x86-64有兩種工作模式,32位OS既可以跑在傳統模式中,把CPU當成i386來用;又可以跑在64位的兼容模式中,更加神奇的是,可以在32位的OS上跑64位的應用程序。有這種好事,用戶肯定買賬啦。
值得一提的是,X86-64開創了編譯器的新紀元,在之前的時代里,Intel CPU的晶體管數量一直以摩爾定律在指數發展,各種新奇功能層出不窮,比如:條件數據傳送指令cmovg,SSE指令等。但是GCC只能保守地假設目標機器的CPU是1985年的i386,額。。。這樣編譯出來的代碼效率可想而知,雖然GCC額外提供了大量優化選項,但是這對應用程序開發者提出了很高的要求,會者寥寥。X86-64的出現,給GCC提供了一個絕好的機會,在新的x86-64機器上,放棄保守的假設,進而充分利用x86-64的各種特性,比如:在過程調用中,通過寄存器來傳遞參數,而不是傳統的堆棧。又如:盡量使用條件傳送指令,而不是控制跳轉指令。
寄存器簡介
先明確一點,本文關注的是通用寄存器(后簡稱寄存器)。既然是通用的,使用並沒有限制;后面介紹寄存器使用規則或者慣例,只是GCC(G++)遵守的規則。因為我們想對GCC編譯的C(C++)程序進行分析,所以了解這些規則就很有幫助。
在體系結構教科書中,寄存器通常被說成寄存器文件,其實就是CPU上的一塊存儲區域,不過更喜歡使用標識符來表示,而不是地址而已。
X86-64中,所有寄存器都是64位,相對32位的x86來說,標識符發生了變化,比如:從原來的%ebp變成了%rbp。為了向后兼容性,%ebp依然可以使用,不過指向了%rbp的低32位。
X86-64寄存器的變化,不僅體現在位數上,更加體現在寄存器數量上。新增加寄存器%r8到%r15。加上x86的原有8個,一共16個寄存器。
剛剛說到,寄存器集成在CPU上,存取速度比存儲器快好幾個數量級,寄存器多了,GCC就可以更多的使用寄存器,替換之前的存儲器堆棧使用,從而大大提升性能。
讓寄存器為己所用,就得了解它們的用途,這些用途都涉及函數調用,X86-64有16個64位寄存器,分別是:
%rax,%rbx,%rcx,%rdx,%esi,%edi,%rbp,%rsp,%r8,%r9,%r10,%r11,%r12,%r13,%r14,%r15。
其中:
- %rax 作為函數返回值使用。
- %rsp 棧指針寄存器,指向棧頂
- %rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函數參數,依次對應第1參數,第2參數。。。
- %rbx,%rbp,%r12,%r13,%14,%15 用作數據存儲,遵循被調用者使用規則,簡單說就是隨便用,調用子函數之前要備份它,以防他被修改
- %r10,%r11 用作數據存儲,遵循調用者使用規則,簡單說就是使用之前要先保存原值
棧幀
棧幀結構
C語言屬於面向過程語言,他最大特點就是把一個程序分解成若干過程(函數),比如:入口函數是main,然后調用各個子函數。在對應機器語言中,GCC把過程轉化成棧幀(frame),簡單的說,每個棧幀對應一個過程。X86-32典型棧幀結構中,由%ebp指向棧幀開始,%esp指向棧頂。
函數進入和返回
函數的進入和退出,通過指令call和ret來完成,給一個例子
#include
#include </code>
int foo ( int x )
{
int array[] = {1,3,5};
return array[x];
} /* ----- end of function foo ----- */
int main ( int argc, char *argv[] )
{
int i = 1;
int j = foo(i);
fprintf(stdout, "i=%d,j=%d\n", i, j);
return EXIT_SUCCESS;
} /* ---------- end of function main ---------- */
命令行中調用gcc,生成匯編語言:
Shell > gcc –S –o test.s test.c
Main函數第40行的指令Callfoo其實干了兩件事情:
- Pushl %rip //保存下一條指令(第41行的代碼地址)的地址,用於函數返回繼續執行
- Jmp foo //跳轉到函數foo
Foo函數第19行的指令ret 相當於:
- popl %rip //恢復指令指針寄存器
棧幀的建立和撤銷
還是上一個例子,看看棧幀如何建立和撤銷。
說題外話,以”點”做為前綴的指令都是用來指導匯編器的命令。無意於程序理解,統統忽視之,比如第31行。
棧幀中,最重要的是幀指針%ebp和棧指針%esp,有了這兩個指針,我們就可以刻畫一個完整的棧幀。
函數main的第30~32行,描述了如何保存上一個棧幀的幀指針,並設置當前的指針。
第49行的leave指令相當於:
Movq %rbp %rsp //撤銷棧空間,回滾%rsp。
Popq %rbp //恢復上一個棧幀的%rbp。
同一件事情會有很多的做法,GCC會綜合考慮,並作出選擇。選擇leave指令,極有可能因為該指令需要存儲空間少,需要時鍾周期也少。
你會發現,在所有的函數中,幾乎都是同樣的套路,我們通過gdb觀察一下進入foo函數之前main的棧幀,進入foo函數的棧幀,退出foo的棧幀情況。
Shell> gcc -g -o testtest.c
Shell> gdb --args test
Gdb > break main
Gdb > run
進入foo函數之前:
你會發現rbp-rsp=0×20,這個是由代碼第11行造成的。
進入foo函數的棧幀:
回到main函數的棧幀,rbp和rsp恢復成進入foo之前的狀態,就好像什么都沒發生一樣。
可有可無的幀指針
你剛剛搞清楚幀指針,是不是很期待要馬上派上用場,這樣你可能要大失所望,因為大部分的程序,都加了優化編譯選項:-O2,這幾乎是普遍的選擇。在這種優化級別,甚至更低的優化級別-O1,都已經去除了幀指針,也就是%ebp中再也不是保存幀指針,而且另作他途。
在x86-32時代,當前棧幀總是從保存%ebp開始,空間由運行時決定,通過不斷push和pop改變當前棧幀空間;x86-64開始,GCC有了新的選擇,優化編譯選項-O1,可以讓GCC不再使用棧幀指針,下面引用 gcc manual 一段話 :
-O also turns on -fomit-frame-pointer on machines where doing so does not interfere with debugging.
這樣一來,所有空間在函數開始處就預分配好,不需要棧幀指針;通過%rsp的偏移就可以訪問所有的局部變量。說了這么多,還是看看例子吧。同一個例子, 加上-O1選項:
Shell>: gcc –O1 –S –o test.s test.c
分析main函數,GCC分析發現棧幀只需要8個字節,於是進入main之后第一條指令就分配了空間(第23行):
Subq $8, %rsp
然后在返回上一棧幀之前,回收了空間(第34行):
Addq $8, %rsp
等等,為啥main函數中並沒有對分配空間的引用呢?這是因為GCC考慮到棧幀對齊需求,故意做出的安排。再來看foo函數,這里你可以看到%rsp是如何引用棧空間的。等等,不是需要先預分配空間嗎?這里為啥沒有預分配,直接引用棧頂之外的地址?這就要涉及x86-64引入的牛逼特性了。
訪問棧頂之外
通過readelf查看可執行程序的header信息:
紅色區域部分指出了x86-64遵循ABI規則的版本,它定義了一些規范,遵循ABI的具體實現應該滿足這些規范,其中,他就規定了程序可以使用棧頂之外128字節的地址。
這說起來很簡單,具體實現可有大學問,這超出了本文的范圍,具體大家參考虛擬存儲器。別的不提,接着上例,我們發現GCC利用了這個特性,干脆就不給foo函數分配棧幀空間了,而是直接使用棧幀之外的空間。@恨少說這就相當於內聯函數唄,我要說:這就是編譯優化的力量。
寄存器保存慣例
過程調用中,調用者棧幀需要寄存器暫存數據,被調用者棧幀也需要寄存器暫存數據。如果調用者使用了%rbx,那被調用者就需要在使用之前把%rbx保存起來,然后在返回調用者棧幀之前,恢復%rbx。遵循該使用規則的寄存器就是被調用者保存寄存器,對於調用者來說,%rbx就是非易失的。
反過來,調用者使用%r10存儲局部變量,為了能在子函數調用后還能使用%r10,調用者把%r10先保存起來,然后在子函數返回之后,再恢復%r10。遵循該使用規則的寄存器就是調用者保存寄存器,對於調用者來說,%r10就是易失的,舉個例子:
#include <stdio.h>
#include <stdlib.h>
void sfact_helper ( long int x, long int * resultp)
{
if (x<=1)
*resultp = 1;
else {
long int nresult;
sfact_helper(x-1,&nresult);
*resultp = x * nresult;
}
} /* ----- end of function foo ----- */
long int
sfact ( long int x )
{
long int result;
sfact_helper(x, &result);
return result;
} /* ----- end of function sfact ----- */
int
main ( int argc, char *argv[] )
{
int sum = sfact(10);
fprintf(stdout, "sum=%d\n", sum);
return EXIT_SUCCESS;
} /* ---------- end of function main ---------- */
命令行中調用gcc,生成匯編語言:
Shell>: gcc –O1 –S –o test2.s test2.c
在函數sfact_helper中,用到了寄存器%rbx和%rbp,在覆蓋之前,GCC選擇了先保存他們的值,代碼6~9說明該行為。在函數返回之前,GCC依次恢復了他們,就如代碼27-28展示的那樣。
看這段代碼你可能會困惑?為什么%rbx在函數進入的時候,指向的是-16(%rsp),而在退出的時候,變成了32(%rsp) 。上文不是介紹過一個重要的特性嗎?訪問棧幀之外的空間,這是GCC不用先分配空間再使用;而是先使用棧空間,然后在適當的時機分配。第11行代碼展示了空間分配,之后棧指針發生變化,所以同一個地址的引用偏移也相應做出調整。
X86時代,參數傳遞是通過入棧實現的,相對CPU來說,存儲器訪問太慢;這樣函數調用的效率就不高,在x86-64時代,寄存器數量多了,GCC就可以利用多達6個寄存器來存儲參數,多於6個的參數,依然還是通過入棧實現。了解這些對我們寫代碼很有幫助,起碼有兩點啟示:
- 盡量使用6個以下的參數列表,不要讓GCC為難啊。
- 傳遞大對象,盡量使用指針或者引用,鑒於寄存器只有64位,而且只能存儲整形數值,寄存器存不下大對象
讓我們具體看看參數是如何傳遞的:
#include <stdio.h>
#include <stdlib.h>
int foo ( int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7 )
{
int array[] = {100,200,300,400,500,600,700};
int sum = array[arg1]+ array[arg7];
return sum;
} /* ----- end of function foo ----- */
int
main ( int argc, char *argv[] )
{
int i = 1;
int j = foo(0,1,2, 3, 4, 5,6);
fprintf(stdout, "i=%d,j=%d\n", i, j);
return EXIT_SUCCESS;
} /* ---------- end of function main ---------- */
命令行中調用gcc,生成匯編語言:
Shell>: gcc –O1 –S –o test1.s test1.c
Main函數中,代碼31~37准備函數foo的參數,從參數7開始,存儲在棧上,%rsp指向的位置;參數6存儲在寄存器%r9d;參數5存儲在寄存器%r8d;參數4對應於%ecx;參數3對應於%edx;參數2對應於%esi;參數1對應於%edi。
Foo函數中,代碼14-15,分別取出參數7和參數1,參與運算。這里數組引用,用到了最經典的尋址方式,-40(%rsp,%rdi,4)=%rsp + %rdi *4 + (-40);其中%rsp用作數組基地址;%rdi用作了數組的下標;數字4表示sizeof(int)=4。
結構體傳參
應@桂南要求,再加一節,相信大家也很想知道結構體是如何存儲,如何引用的,如果作為參數,會如何傳遞,如果作為返回值,又會如何返回。
看下面的例子:
#include <stdio.h>
#include <stdlib.h>
struct demo_s {
char var8;
int var32;
long var64;
};
struct demo_s foo (struct demo_s d)
{
d.var8=8;
d.var32=32;
d.var64=64;
return d;
} /* ----- end of function foo ----- */
int
main ( int argc, char *argv[] )
{
struct demo_s d, result;
result = foo (d);
fprintf(stdout, "demo: %d, %d, %ld\n", result.var8,result.var32, result.var64);
return EXIT_SUCCESS;
} /* ---------- end of function main ---------- */
我們缺省編譯選項,加了優化編譯的選項可以留給大家思考。
Shell>gcc -S -o test.s test.c
上面的代碼加了一些注釋,方便大家理解,
問題1:結構體如何傳遞?它被分成了兩個部分,var8和var32合並成8個字節的大小,放在寄存器%rdi中,var64放在寄存器的%rsi中。也就是結構體分解了。
問題2:結構體如何存儲? 注意看foo函數的第15~17行注意到,結構體的引用變成了一個偏移量訪問。這和數組很像,只不過他的元素大小可變。
問題3:結構體如何返回,原本%rax充當了返回值的角色,現在添加了返回值2:%rdx。同樣,GCC用兩個寄存器來表示結構體。
恩, 即使在缺省情況下,GCC依然是想盡辦法使用寄存器。隨着結構變的越來越大,寄存器不夠用了,那就只能使用棧了。
總結
了解寄存器和棧幀的關系,對於gdb調試很有幫助;過些日子,一定找個合適的例子和大家分享一下。
參考
1. 深入理解計算機體系結構
2. x86系列匯編語言程序設計