3.1 程序編碼
1.計算機系統使用了多種不一樣形式的抽象,對於機器級編程來講,兩種抽象尤其重要:編程
- 指令集體系結構(ISA):定義了處理器狀態、指令的格式,以及每條指令對狀態的影響
- 機器級程序使用的存儲器地址是虛擬地址:提供的存儲器模型看上去是一個很是大的字節數組
2.反彙編器使用的指令命名規則與GCC生成的彙編代碼使用的有區別。反彙編省略了指令結尾的q,給call和ret指令添加了q後綴。數組
3.可執行程序反彙編和對.c反彙編產生的代碼有差異。對於可執行文件的反彙編,連接器將代碼的地址移到了一段不一樣的地址範圍,連接器的任務之一就是爲函數調用找到匹配的函數的可執行代碼的位置。數據結構
3.2 數據格式
GCC生成的彙編代碼指令都有一個字符的後綴,代表操做數的大小、。後綴l能夠表示4字節整數和8字節雙精度浮點數,可是並無歧義,由於浮點數使用的是一組徹底不一樣的指令和寄存器。函數
3.3 訪問信息
1.x86-64的CPU包含一組16個存儲64位值的通用目的寄存器,用來存儲整數數據和指針。oop
2.不一樣操做數可能被分爲三種類型,分別爲當即數(表示常數)、寄存器(表示某個寄存器的內容)、內存引用(根據計算出來的地址訪問某個內存位置)。測試
3.傳送指令兩個操做數不能都指向內存位置。MOV指令只會更新目的操做數指定的那些寄存器字節或內存位置。惟一的例外是movl指令以寄存器做爲目的時,會把寄存器的高位4字節設置爲0。movq指令只能以表示爲32位補碼數字的當即數做爲源操做數,而後把這個值符號擴展獲得64位的值,放到目的位置。編碼
4.MOVZ類中的指令把目的中剩餘的字節填充爲0,MOVS類中的指令經過符號擴展來填充,把源操做的最高位進行復制。它們均以寄存器或內存地址做爲源,以寄存器做爲目的。url
把4字節源值零擴展到8字節邏輯上應該是movzlq,但並無這樣的指令。可使用movl來實現(movl指令會把寄存器的高位4字節設置爲0)。spa
3.4 算術和邏輯操做:
若是寄存器%eax的值爲x,那麼指令leal 3(%edx, %edx, 2),%eax
將設置%eax的值爲2x+3。.net
移位量能夠是一個當即數,或者放在單字節寄存器%cl中。左移指令有SAL和SHL,二者效果同樣,都是將右邊填上0,而右移指令不一樣,SAR執行算術移位(填上符號位),而SHR執行邏輯移位(填上0)。
無符號數乘法(mulq)和補碼乘法(imulq)要求一個參數必須在%rax中,另外一個做爲指令的源操做數給出。乘積存放在%rdx(高64位)和%rax(低64位);有符號除法idivl 將寄存器 %rdx(高32位)和 %rax(低32位)中的64位數做爲被除數,而除數做爲指令的操做數給出。指令將商存儲在%rax中,將餘數存儲於%rdx中。
3.5 控制
1.條件碼寄存器描述了最近算術或邏輯運算的屬性,能夠檢測這些寄存器來執行條件分支指令:
-
CF:進位標誌。可用來檢查無符號操做的溢出。如:(unsigned)t < (unsigned)
-
ZF:零標誌。如:(t == 0)
-
SF:符號標誌。如:(t < 0)
-
OF:溢出標誌,最近的操做致使了補碼溢出。如:(a<0==b<0)&&(t<0!=a<0)
2.leaq 指令不會設置條件碼,除過前面提到的指令外,CMP(和SUB行爲同樣)和TEST(和ADD行爲同樣)指令會設置條件碼,但不改變任何其餘寄存器。testq %rax %rax
用來檢查 %rax 是零、正數仍是負數。
3.條件碼一般不會直接讀取,一般使用的方法有三種:
- 能夠根據條件碼的某種組合,將一個字節設置爲0或者1。
- 能夠條件跳轉到程序的某個其餘部分。
- 能夠有條件的傳送數據
SET指令時條件碼的組合,執行比較指令,根據計算t=a-b設置條件碼。有符號比較測試基於SF、OF和ZF的組合,無符號比較測試基於CF和ZF。
4.jump 指令有三種跳轉方式:直接跳轉、間接跳轉(‘*’後跟一個操做數指示符)、其餘條件跳轉(根據條件碼的某個組合,或者跳轉,或者繼續執行代碼序列中的下一條指令)。
經常使用的PC相對的對於跳轉指令的編碼會將目標指令的地址與緊跟在跳轉指令後面那條指令的地址之間的差做爲編碼。
5.彙編中沒有do-while、while和for相應的指令存在,能夠用條件測試和跳轉組合起來實現循環的效果。大多數彙編器中都要先將其餘形式的循環轉換成do-while格式。
do-while的通用形式能夠翻譯成以下所示的條件和goto語句:
loop: body-statement t=test-expr; if(t) goto loop;
while循環第一種翻譯方式跳轉到中間:
goto test; loop: body-statement test: t=test-expr; if(t) goto loop;
第二種翻譯方式爲首先用條件分支,若是初始條件不成立就跳過循環,轉化爲do-while循環:
t=test-expr; if(!t) goto done; loop: body-statement t=test-expr; if(t) goto loop; done:
for循環能夠很容易轉換成while循環,進而轉換成do-while形式:
init-expr; t=test-expr; if(!t) goto done; loop: body-statement update-expr; t=test-expr; if(t) goto loop; done:
switch語句的跳轉表是一個數組,表項i是一個代碼段的地址,這個代碼段實現當開關索引值等於i時程序應該採起的動做。
3.6 過程
1.過程提供了一種封裝代碼的方式,用一組指定的參數和一個可選的返回值實現了某種功能。而後,能夠在程序中不一樣的地方調用這個函數。過程機制的構建須要實現傳遞控制、傳遞數據、分配和釋放內存。
2.過程P能夠傳遞最多6個整數值,若是Q須要更多參數,P能夠在調用Q前在本身的棧幀裏存儲好這些參數。寄存器最多傳輸6個小於等於64位的數據,並經過%rax返回數據。若是一個函數有大於6個整型參數,超出6個的部分就經過保存在調用者的棧幀來傳遞。
3.%rbx、%rbp和%r12~%15被調用者保存,在使用前被調用者要把這裏面的值保存好,保證其值在返回時和調用時是同樣的(這裏就像有我有一輛豪車,能夠把車子借給朋友使用,可是必定要把鑰匙保存好,用完了以後還回來),這讓我想到了以前看過的彙編代碼在被調用函數的第一步都是 push %ebp.
4.全部其餘寄存器,除了%rsp爲調用者保存,意味着任何函數都能修改它們,則在調用前首先保存好這個數據是調用者的責任。(這裏的調用者就像頗有票子的王健林同樣,兒子王思聰能夠無償的使用王健林的票子) 參考《深刻理解計算機系統》| 程序的機器級表示。
5.遞歸的調用其實與其餘函數的調用是同樣的,由於每一個過程調用在棧中都有私有的空間,多個未完成調用的局部變量不會相互影響。
3.7 數據分配和訪問
1.設 xA 表示起始位置,則訪問數組元素 A[i] 的位置在 xA+ L*i,L爲數據類型的大小(單位爲字節)。數組元素的訪問通常藉助存儲器引用指令。如計算 int 型的 E[i]: E 的地址存放在 %rdx 中,而 i 存放在 %rcx 中。movl (%rdx,%rcx,4),%eax
表示計算地址 xE+4i,並讀取這個存儲器位置的值,將結果放到 %eax 中。
2.若是 P 是一個執行類型 T 的數據的指針,P 的值爲 xP,那麼表達式 P+i 的值爲 xP + L*i,L 是數據類型T的大小。假設整型數組 E 的起始地址和整數索引 i 分別存放在 %rdx 和 %rcx 中,下面是一些與 E 有關的表達式,能夠明顯看出 leal 和 movl 的區別(前者產生地址,後者引用內存):
3.數組的嵌套,也就是數組的數組。對於數組 int A[5][3],能夠將 A 當作是一個有 5 個元素的數組,而每一個元素都是 3 個 int 類型的數組。計算D[R][C](int 型)的地址:
&D[i][j] = xD + L(C * i + j)
因爲每組有 C 個數據,因此跳過一組就要乘以C,跳過I組就 C*i 個,再加上偏移的 j 就是所求地址。
3.8 異質的數據結構
1.結構:全部的組成部分在存儲器中連續存放,指向結構的指針指向結構的第一個字節。
2.聯合:容許以多種類型來引用一個對象,總大小等於它最大字段的大小,而指向一個聯合的指針,引用的是數據結構的起始位置。
3.x86-64系統對齊要求爲:對於任何須要K字節的標量數據類型的起始地址必須是K的倍數。彙編中.align 8
要求後面的數據起始位置是8的倍數。結構體的對齊除了要知足每一個字段的對齊要求,還須要考慮總體的結構知足怎樣的對齊要求。 如:
struct test { int i; int j; char c; };
咱們能保證起始地址4字節對齊要求,但struct s2 d[4]
就不能知足 d 的每一個元素的對齊要求,由於這些元素的地址分別爲xd,xd+9,xd+18和xd+27,因此爲s2分配12個字節。
3.9 在機器級程序中將控制與數據結合起來
1.void * 表示通用指針,malloc函數返回一個通用指針,而後轉換成一個有類型的指針。
2.指針從一個類型轉爲另一個類型,只是伸縮因子變化,不改變它的值。如 p 是一個 char * 類型的指針,值爲p,(int * )p + 7
計算爲 p+28 ,而(int * )(p + 7)
計算爲 p+7。
3.C對數組引用不作邊界檢查,同時局部變量和狀態信息(寄存器值和返回指針等)都存放在棧中,這使得越界的數組寫操做會破壞存儲在棧中的狀態信息。常見的狀態破壞稱爲緩衝區溢出。
棧是向低地址增加的,數組緩衝區是向高地址增加的。故上圖所示 buf[8] 在輸入超過 8 個時就會覆蓋棧上存儲的某些信息。若是破壞了存儲的返回地址,那麼ret指令會使程序跳轉到徹底意想不到的地方(如跳轉到攻擊代碼)。使用gets或strcpy、strcat、sprintf等能致使存儲溢出的函數(不須要告訴它們目標緩衝區的大小就產生一個字節序列),都不是好的編程習慣。
4 對抗緩衝區溢出攻擊的方法:
- 棧隨機化:使得棧的位置在程序每次運行時都有變化。實現的方式是程序開始時,在棧上分配一段0--n字節之間的隨機大小空間
- 棧破壞檢測:在棧中任何局部緩衝區與棧狀態之間存儲一個特殊的金絲雀值。這個金絲雀值是在程序每次運行時隨機產生的,所以,攻擊者沒有簡單的辦法知道它是什麼。在恢復寄存器狀態和從函數返回以前,程序檢查這個金絲雀值是否被該函數的某個操做或者函數調用的某個操做改變了。若是是,那麼程序異常終止
- 限制可執行代碼區域:限制那些可以存放可執行代碼的存儲器區域
3.10 浮點代碼
浮點數操做和整數操做很相似,指令命名上有區別,故此部分簡述。
1.AVX浮點體系結構容許數據存儲在16個YMM寄存器中,每一個YMM寄存器是256位。對標量數據**(單個數據)**操做時,寄存器只保存浮點數,並且只使用低32位(float)或64位(double)。
2.浮點傳送指令:
3.浮點轉換指令:
4.%xmm0~%xmm7最多能夠傳遞8個浮點參數,額外參數經過棧傳遞。%xmm0返回浮點數。XMM寄存器都是調用者保存,被吊用着不用保存就覆蓋這些寄存器中的任意一個。當函數包含指針、整數和浮點數混合的參數時,指針和整數經過通用寄存器傳遞,浮點值經過XMM寄存器傳遞。
5.浮點運算操做(第一個源操做數S1能夠是XMM寄存器或內存位置,第二個源操做數和目的操做數必須是XMM寄存器):
6.AVX浮點操做不能以當即數值做爲操做數,編譯器須要爲全部常量分配和初始化存儲空間(從標號爲 .LC2 的內存位置讀出 1.8,從標號爲 .LC3 的內存位置讀出 32.0):
7.位級操做:
8.浮點比較操做(S2必須在XMM寄存器中):
浮點比較指令會設置ZF、CF、奇偶標誌位PF(當浮點操做數中任一個時NaN會設置該位):
3.11 問題及解決
B中最大爲long,因此以8字節對齊,我想固然地將 i 後填充4,c和d後填充7,總共爲32字節。看了答案是16字節後,我意識到 i、c、d 「拼」一塊兒依然小於8字節,因此應該是在它們後填充2字節,總共就爲16字節。若是像我那樣作的話就太浪費存儲空間了。
E中8字節對齊,P3結構體數組中第2、三個元素c[2]、c[3] 2字節還能和P2結構體的i、c、d 「拼」,爲何答案 t 的起始位置爲24了,像是沒把它們拼一塊兒,直接在c[3]後擴充6字節?最後想了想結構體填充的規則,若是拼一塊兒 t 的起始位置爲 18,不是8的倍數。
另外有一問題未解決,習題3.9中在一片movq、salq、sarq中出現了movl,感受有點奇怪,雖然只有最低位的字節指示着移位量的解釋能接受,那在這裏使用 movq 和 movl 有什麼區別?是效率上的區別嗎?書中還有不少地方出現 q、l、b、w 「混用」的例子,何時該用何時不應用呢?