gcc -Og -o p p1.c p2.c
採用gcc編譯c語言java
gcc命令會調用一些列程序:算法
其中目標代碼和可執行文件都是機器代碼編程
計算機系統對複雜的機器級編程進行抽象:數組
彙編代碼接近機器代碼,但提供了更好的可讀性安全
x86-64機器代碼與c差距巨大,處理器中不少狀態在c語言隱藏可是在x86-64中均可見:數據結構
程序內存包含了下面幾個部分:架構
機器代碼須要知道程序的虛擬內存地址便可,由操做系統將虛擬內存地址轉爲物理內存地址函數
在機器代碼中,內存中不存在數據結構和對象,只有一個大字節數組和其中的虛擬地址性能
源代碼mstore.c學習
gcc -Og -S mstore.c
:生成彙編文件mstore.s
gcc -Og -c mstore.c
:生成二進制目標代碼文件mstore.o
objdump -d mstore.o
:經過反彙編器將機器代碼翻譯成彙編代碼
因爲最先從16位體系結構發展起來,Intel稱字(word)表示16位數據類型,32位和64位就成爲雙字和四字
c語言的數據類型與Intel數據類型的映射關係:
彙編代碼中操做指令後跟上這些數據類型的後綴,能夠代表是操做哪一種類型的數據。如movb表示傳送字節
16個整數寄存器分別能夠存儲64位值,具體用來存整數和指針
大多指令有N個操做數,表明一個操做指令所使用的源數據值和放置的目的位置。操做數有三種類型:
將數據從源位置複製到目的位置的指令,有三種mov,movz,movs:前者作普通拷貝,高位保持不變;後二者用於將較小的源值拷貝到較大的目的地址,並採用0或者符號位擴展高位
mov
源和目的地能夠是當即數、寄存器或內存地址,但不能同時是內存地址,拷貝以後不作任何變更(例外:movl的目的地是寄存器時,高四位字節置零)
movz
movs
程序棧存放在內存的某個區域,將棧視爲一個大樹組的話,棧頂在數組低位,並向「下」擴展
分爲三種類型:
leaq:將內存數據讀取到寄存器,相似movq
示例:
通常使用leaq運算的指令會比movq的指令更少,所以每每leaq更高效
有兩個操做數:
描述64位相乘(得128位數字)和128位整除的指令,思想是將128位數採用兩個64位寄存器存儲
上述全部指令都是順序執行,爲了支持c語言條件語句、循環語句、分支語句,引入jump指令
### 條件碼
除了整數寄存器,cpu還維護一組單個位條件碼寄存器,記錄最近算術和邏輯運算的狀態。檢測這些屬性來執行條件分支指令,經常使用的四個:
上述指令除了leaq,都會設置條件碼
上述指令,CMP相似SUB,TEST相似AND,區別在CMP和TEST只設置條件碼而不修改寄存器的值
一般不會直接讀取條件碼,經常使用的使用方法爲:
間接跳轉中,區分跳轉須要區分跳轉目標是從寄存器仍是內存地址讀出來的
jmp *%rax
:用寄存器%rax的值做爲跳轉目的地
jmp *(%rax)
:用寄存器%rax中的值做爲地址,讀出該地址內存的值,做爲跳轉目的地
彙編中jmp到目標位置,在機器碼中,實現方式爲:
示例採用PC相對的編碼方式:
PC相對編碼更經常使用,相比絕對地址編碼的優點在於:
設置條件碼,並經過JUMP類型的指令跳轉到指定分支。這種方式簡單、通用
計算一個條件操做的兩種結果,而後根據條件是否知足從中選取一個。這種使用受限,可是性能更優,GCC只要在兩個表達式都只是很簡單的指令時,纔會選擇條件傳送,不然,絕大多數狀況都是採用條件控制
上圖列舉了x86-64的一些條件傳送指令
條件傳送高效的祕密:處理器的流水線機制。流水線機制就是將一條指令拆成多個步驟(從內存取指令、肯定指令類型、從內存讀數據、執行算術運算、向內存寫數據、更新PC),經過重疊連續指令的步驟(執行指令運算的同時取下一條指令),保證流水線充滿了待執行的指令,提升了高性能。這樣就必須預測後續指令,若是出現條件分支,可能致使預測錯誤,致使必須丟棄取到的指令並從新獲取,致使加大指令執行的時鐘週期,致使性能降低
do-while循環:
while循環:
gcc -Og
模式下翻譯方式:
gcc -O1
模式下翻譯方式:
for循環:
翻譯成while循環的兩種模式(取決於優化等級)
switch會利用跳轉表來實現高效跳轉到正確的分支。跳轉表就是一個數組,存儲了全部分支代碼段的地址
上圖彙編代碼,就是一個跳轉表的聲明:
上圖表是switch語句從原始C代碼到彙編代碼的翻譯過程:
在分支量較大,並且跳轉表索引密集的時候,GCC會自動選擇跳轉表來優化switch語句。經過跳轉表直接定位代碼段,致使分支較多的狀況下,switch語句相比if-else更高效
過程調用的實現機制涉及三個方面(假設過程P調用過程Q,Q執行後返回P):
涉及過程調用、控制轉移的指令:
帶星號表示間接調用,就是獲取給操做數的值做爲地址,進行調用
假設存在程序,main函數調用multstore函數:
main
multstore
運行時棧
流程說明(%rip爲程序計數器PC的值,%rsp爲棧指針的值):
P調用Q,P須要將前6個參數複製到寄存器,從Q返回時,Q須要將惟一的返回值寫入%rax
圖表中列出參數在指定的寄存器中的存放順序
當參數N>6時,P須要爲7 ~ N個參數分配棧空間,並按N到7的前後順序壓入棧,保證第7個參數在棧頂,只有分配了棧空間,才能將控制轉移給Q,Q來訪問棧空間上的參數
如圖展現,前六個參數在寄存器,後兩個在棧
這裏最後一個參數不在棧頂而是在%rsp+8的位置,是由於在參數入棧以後,返回地址也入棧了
雖然不少時候局部變量能夠存在寄存器,可是如下這些狀況須要必須爲局部變量分配棧空間:
實例解析1
實例解析2
寄存器是全部過程的共享空間,須要保證過程P調用過程Q的時候,Q不會篡改P等會要用的寄存器
爲防止上述狀況,x86-64定義了一組規範:
其中着重關注:
保存這些寄存器值的方案:
實例講解:
函數P中有兩個值在彙編代碼中存在被修改的風險:
上述兩個保存操做使用了%rbp和%rbx這兩個被調用者保存的寄存器,爲保證P返回後還能正常使用,P須要提早保存它的值
保證遞歸正常工做的機制:
實例講解:
每次調用都會將n減1,因此須要提早保留n的值保證後續正常使用
最右邊一、八、四、8這些因子,表明char、char指針、int、double指針的字節大小
設E爲int類型數組,想要獲取第i的元素E[i]。E的地址存放在%rdx,i的值存放在%rcx,則獲取E[i]的方式爲:
movl (%rdx,%rcx,4),%eax
int A[5][3]
等價於
typedef int row3_t[3]; row3_t A[5];
即:A是個包含5個元素的數組,每一個元素都是一個存儲了3個元素的數組
這個嵌套數組A也可視爲5行3列的二維數組
A的佔用字節爲5*3*4=60
公式:對任意二維數組T D[R][C]
,元素D[i][j]的內存地址爲&D[i][j]=Xd + L*(C*i + j)
編譯器訪問二維數組中元素的方式爲根據上述公式計算出元素內存地址,而後使用MOV命令讀取該地址的值
給出彙編代碼訪問A[i][j]的方式:
該算法將12i這個乘法表達式拆成多個加法表達式,這是利用了第二章乘以常數這一節的特性
根據上圖定義的定長二維數組(矩陣),給出下面矩陣乘法的算法:
每次計算矩陣元素時,都得使用上一小節給出的公式,GCC會針對這種狀況進行優化,減小對公式的直接使用,轉而依賴指針運算
int A[n1][n2]
,容許n一、n2是表達式,編譯期沒法得知該數組的大小。該功能在C99才引入
聲明變長數組時,須要將參數n放到參數A[n][n]以前
上圖爲彙編代碼訪問A[n][n]數組中,A[i][j]元素的方式。相比定長數組中,因爲編譯期間沒法知曉n的值,沒法對4(n*j)
進行優化(拆分紅多個加法)
上圖爲GCC對變長數組的矩陣相乘算法的優化,並無給出彙編代碼。跟定長數組中的優化同樣,避免使用公式,轉而採用指針運算(變長數組直接使用公式的效果更差,由於不知道n的大小,沒法對4(n*j)
進行優化)
假設struct rec*類型的變量r存放在寄存器%rdi,編譯器會將r->j=r->i
翻譯成:
更復雜的實例:
綜上,結構的各字段在編譯時會轉換成內存地址
結構和聯合:
在內存佔用上的區別:
上圖中展現的佔用量包含了數據對齊,這部分下一節介紹
相比結構,聯合在存儲多個字段的時候佔用內存更小。但改善空間不大的時候,使用聯合繞過了C語言類型系統,容易致使bug產生
實例:訪問不一樣數據類型的位模式
強轉生成的u,值與d同樣,可是二者位表示大相徑庭(0.0除外)
使用聯合生成的u,位模式跟d如出一轍,可是值不一樣(0.0除外)。參考java庫中的Double.doubleToRawLongBits
實例:分段表示位模式,須要注意機器字節順序
爲了簡化訪問基本數據類型的方式,一般須要讓基本數據類型的地址是其字節大小的整數倍。
針對結構:
不對齊:
對齊:
存在溢出風險的程序:
該程序的棧空間爲:
若是用戶輸入的字節超過24字節,就會破壞調用者的棧幀。根據用戶輸入的字節數,程序會受到以下破壞:
這個破壞容易致使程序受到攻擊。具體方式爲:
這樣程序的意圖就遭到了篡改
對抗溢出攻擊的方式有兩種:
以前的機器代碼,在編譯期就能肯定須要爲棧幀分配多少空間。有些函數(如alloca)在運行期纔會在棧上(malloc是在堆上)分配空間
上圖代碼是一個實例:存在變長數組,編譯期沒法肯定數組分配空間(n未知),因此這部分空間須要運行時分配,可是分配後該如何釋放這部分空間,並正確的返回到返回地址所在的位置?
x86-64採用寄存器%rbp做爲幀指針(幀指針只會在實現變長棧幀時纔會用),用來保存動態分配以前棧指針的位置,在函數返回前經過leave指令釋放棧指針和恢復幀指針
leave等價於:
介紹了程序計數器、整數寄存器、條件碼寄存器,如今只有向量寄存器沒有介紹,這部分關聯了浮點數的機器級操做方式
處理器定義了浮點體系結構,關係着浮點數據操做如何映射到機器上,這個結構包含:
本文檔基於AVX2,在Core i7 Haswell處理器引入。該體系結構容許數據存在16個YMM寄存器,在對標量數據操做時,這些寄存器只保存浮點數,並只是用低32位(float)或64位(double)
傳送:
傳送實例:
其中都是對float類型數據進行賦值(傳遞)。之因此出現%rdi和%rsi這兩個整數寄存器,是由於它們記錄了某個浮點數的內存地址,這個內存地址是整數
轉換:
下面兩個代碼是GCC在單精度和雙精度作轉換所生成的,意圖不明:
結合浮點體系結構一節圖示,XMM寄存器就能夠處理向函數傳遞浮點參數,以及從函數返回浮點值
實例:
經過實例觀察到:
因此浮點運算中,編譯器須要爲常量提早分配內存空間,以後將浮點指令會使用內存地址做爲操做符,來使用常量
上圖,編譯器在標號爲.LC2和.LC3的內存地址存儲了常量1.8和32.0
上述浮點比較指令會設置三個條件碼:
無序狀況就是兩個操做數中存在NAN的狀況,這是會將PF設爲1
條件碼的設置條件爲: