深入理解程序的本质
hi ,大家好,這篇文章源于知乎一些問題,真正的技術高手,無非是對計算機程序本質的深刻理解,水平高低,就是看你計算機的理解深度,這句話,大家可以慢慢體會,很多計編程問題,寫代碼有bug,解決bug慢等問題,本質上來說,都是你對計算機理解不夠深刻導致的。
整理:極客重生
來源:https://fengmuzi2003.gitbook.io/csapp3e/di-3-zhang-ji-qi-ji-bian-cheng
導讀???????????????
很久很久以前(其實也沒有那么久,畢竟計算機科學的發展也才那么幾十年而已),程序員都是用二進制編碼的,后來開始用匯編語言,今天,事情發生了很大的變化,人們開始使用高級語言,或者說已經工作在更高的抽象層次上了。你可能會有疑惑:為什么今天我們還要花力氣來學習機器級別的編程方式呢?
這里的機器級編程包含了兩種含義,一個是可以直接在機器上運行的二進制指令,另一個是匯編語言(就是編譯器產生的代碼),對于我們來說,兩者都屬于機器級別,兩個概念可以互換,它們之所以很重要,因為它們是連接你所編寫的高級語言代碼和機器之間的紐帶,是實實在在的基石,理解這里面的一些底層工作的原理還是很有必要的,實際上這也是CSAPP區別于其他課程的一個顯著點,但這并不是要求你徒手寫匯編代碼(現代編譯器可能比你更精通這一點,或許也比你更有耐心),只是希望當你遇到需要閱讀一點點匯編代碼的時候不至于驚慌無措,當然了,如果你想成為一名系統程序員或者想成為一名黑客,那么這個話題就很重要了。
歷史上出現過很多知名的指令集架構,比如Alpha, SPARC,PowerPC,MIPS等,但是今天最流行的指令集架構是x86(-64),ARM,RISC-V,本課程把重點放在了英特爾x86-64上,畢竟講課的話總是限定在某種處理器上講起來也相對容易一些嘛,指令集架構(ISA)的地位非常重要,就是在那里,軟件遇見了硬件!
歷史上出現過很多成功的指令集架構,但是時過境遷,當今主流的指令集架構,如下圖所示:
基本概念
Instructure Set Architecture:指令集架構 (包括指令規格,寄存器等),簡稱ISA,它是軟硬件之間的“合同”
Mircoarchitecture:指令集架構的具體實現方式 (比如流水線級數,緩存大小等),它是可變的
Machine Code:機器碼,也就是機器可以直接執行的二進制指令
Assembly Code:匯編碼,也就是機器碼的文本形式 (主要是給人類閱讀)
從匯編碼/機器碼的角度來看計算機系統程序員的角度 vs 微體系結構 (程序員可見 vs 程序員不可見),在CPU方面,開放給程序員的編程接口只是PC,寄存器,條件碼,其他的內部信息比如CPU內部的Cache對程序員來說都是不可見的,內存角度來講,大部分的ISA支持字節尋址方式 (即Byte尋址,實際上還有Bit尋址,32-bit尋址,64-bit尋址等,只是比較少見而已),絕大多數的ISA具有確定的大小端模式 (有些ISA可變)。
編譯過程
其次,理解C程序的編譯過程:源代碼 -> 編譯 -> 匯編 -> 鏈接 -> 可執行文件 -> 裝載 -> 執行
以下是x86-64平臺編譯后的匯編碼和機器碼,注意:不同的平臺(ISA不同)和不同的編譯器會產生完全不同的機器碼,但是無論如何大家可以看到最終產生的機器碼無非就是0和1的組合,關鍵在于機器知道該怎樣來解釋它們。
重要思想:程序就是一系列(被編碼了的)字節序列 (看上去和數據一模一樣),這就是所謂的馮諾依曼結構計算機,即程序存儲型計算機,馮諾依曼結構由于EDVAC項目的技術報告分發而開始被人們熟知。
馮諾依曼以及EDVAC項目的技術報告學習方式
機器級編程-I:基礎
歷史:Intel在開發自己的64位指令集架構 (Itanium) 的時候遭遇了失敗,部分原因在于它和之前的 IA32 指令集不兼容,而且性能也達不到預期,最終不得不轉而采用AMD的64位指令集x64-86.
重點提示:了解 Intel x86-64的寄存器組(下圖所示),基礎指令集,包括數據傳送(包括壓棧和出棧),算術和邏輯運算,特別需要留意<源操作數>和<目的操作數>在具體指令中的方向!
學習C語言和x86匯編語言之間的關系的一個絕佳方式就是逆向工程,你可以使用GNU提供的工具例如 objdump 或者 GDB 查看反匯編代碼來學習!
機器級編程-II:控制
重點提示:理解條件碼 (CF,ZF,SF,OF),分支(Conditional Move => 分支預測相關),循環
備注:編譯器比你想象的要聰明,例如,你寫的switch語句可能會被優化為 jump table,還會消除無用的語句(Dead code elimination)等,匯編代碼有時候不僅僅是C代碼的直譯,也就是說:編譯器可以執行不同程度的優化,那么你很有可能會一下子很難理解編譯器生成的匯編代碼,請不要害怕,多點耐心,試著自己分析看看,說不定你會恍然大悟,贊嘆編譯器的聰明之處!關于編譯器的優化在第5章會有更多的探討。
機器級編程-III:過程
重點提示:函數調用的過程 :控制權轉移 (含返回地址的保存),參數傳遞,內存管理 (棧),控制權返回.
備注:無論何種 ISA,函數調用過程大同小異,只是在具體的指令或者在ABI (Application Binary Interface) 層面略有不同而已,比如不同的ISA會有不同的 Calling Convention,也就是調用規則,它是調用者 Caller 和被調者 Callee 之間的某種合約,比如哪些寄存器用來傳遞參數,哪些寄存器用來存放返回值,哪些寄存器調用者/被調者可以放心使用等 (Caller Saved & Callee Saved),理解Prologue & Epilogue!
請務必理解對應的投影片中的內容,如果你真的理解了遞歸函數的調用過程,那么恭喜你,你學會了 !
機器級編程-IV:數據
Bits, Bytes, and Integers
道生一,一生二,二生三,三生萬物:
重點提示:理解C語言中的數組和指針在機器級是如何表示的,理解字節對齊的作用 (有些指令集架構是強制要求字節對齊的,即使不要求也應該做到字節對齊,不僅能節省空間,更重要的是會影響訪問性能)
機器級編程-V:進階
重點提示:理解典型的內存布局 (棧,共享庫,堆,代碼段,數據段 ...),如下圖所示:
理解緩沖區溢出導致的安全問題,以下是一個簡單的示例程序(gcc編譯參數:-fno-stack-protector):
備注:我們這里講緩沖區溢出的時候,重點討論的是棧溢出的問題,實際上還有堆溢出的漏洞。
簡單的示例程序(運行gets前的棧)
簡單的示例程序(運行gets后的棧:兩種情況)
如何避免緩沖區溢出問題?
1. 程序員層面,避免調用不安全的函數,比如,fgets代替gets,strncpy代替strcpy
2. 操作系統層面,增加保護機制,例如ASLR (地址空間隨機化),讓攻擊者難以猜測地址(依然可以攻破)
實際上,今天的絕大多數系統在默認情況下是啟用ASLR的,可以通過以下命令查看:
0? 沒有隨機化,也就是關閉 ASLR
1? 保留的隨機化,其中共享庫、棧、mmap 以及 VDSO 將被隨機化
2? 完全的隨機化,在 1 的基礎上,通過 brk() 分配的內存空間也將被隨機化
注意:在用GDB調試時,可以通過set disable-randomization命令開啟或者關閉地址空間隨機化,默認是關閉隨機化的,也就是on狀態,具體參見:https://sourceware.org/gdb/onlinedocs/gdb/Starting.html
3. 硬件層面,對棧區增加權限保護:NX (No-eXecute),gcc編譯選項 -z ?execstack/noexecstack
思考:如何繞過NX?一種方式是ROP(就是你在Attack Lab實驗中的 Phase4~Phase5),另外一種攻擊方式是 ret2libc(不能返回到我寫的代碼,返回libc的代碼總可以吧 ... 這種方式需要自己構建棧幀 ...)
4. 編譯器層面,緩沖區溢出的檢測(Stack Guard),又被稱作?!敖鸾z雀”(Canary),故事:人們發現將金絲雀可以檢測一氧化碳的濃度,于是煤礦工人將金絲雀帶入煤礦,一旦超標,鳥類會在礦工面前死去或者出現生病的癥狀,這可以作為有毒氣體(要是一氧化碳)的預警信號。
思考:“金絲雀值” 應該存放在哪里?還是前面那個簡單的示例程序(gcc編譯參數:-fstack-protector):
緩沖區溢出檢測的例子(gcc編譯參數:-fstack-protector)
備注:所謂 “道高一尺魔高一丈”,黑客會利用其它的機器級別的特性來進行針對性的攻擊,例如,ROP攻擊, ROP全稱Return-Oriented Programming,就是對棧上的返回地址進行利用的一種攻擊方式,ROP的攻擊方法是借用代碼段里面的多個片段指令拼湊成一段有效的邏輯,從而達到攻擊的目的,片段指令一般稱之為Gadget,即利用Gadget + retq,我們可以利用多個retq跳到不同的Gadget來實現我們完整的攻擊流。
C語言復習
實驗解讀(Bomb Lab)
提示用戶輸入正確的字符串來拆掉炸彈,如果任何一個不正確,炸彈就會“爆炸”,你必須通過逆向工程來解除炸彈,這會讓你理解匯編語言,學習如何使用GDB來調試程序,設計得很有意思。
實驗相關說明(CMU的助教講解):Bomb Lab實驗說明
請首先學習和熟悉GDB的使用方法:
http://csapp.cs.cmu.edu/3e/docs/gdbnotes-x86-64.pdf
然后學習一些匯編的基礎知識:
Intel 64 and IA-32 Architectures Software Developer's Manuals
經過了6個步驟,拆除炸彈后大概是這個樣子(實際上還隱藏了一個彩蛋,這里沒有畫出)
實驗解讀(Attack Lab)
要求大家利用Code Injection Attacks (代碼注入攻擊)和 ROP(返回導向的編程)這兩種方法來攻擊程序,對現有程序進行控制流劫持,執行非法程序代碼,模擬當黑客的感覺,同時學會如何預防這些攻擊手段。
實驗相關說明(CMU的助教講解):Attack Lab實驗說明 (前3題是CIA實驗,后2題是ROP實驗)
開始實驗之前,請仔細閱讀實驗說明(講義):
http://csapp.cs.cmu.edu/3e/labs.html
溫馨提示:計算機安全中的不少攻擊和防御方法表面上看起來不同, 但如果深入研究的話,會發現它們其實是相似的或有關聯,反過來,有些內容看起來相似, 本質上卻有所不同,這就是知識點的相關性,只有將不同的知識點聯系起來, 才能在腦海中形成知識體系,計算機安全知識更新很快,每天都有新的漏洞和攻擊出現。有了扎實的知識體系,就不會疲于學習這些新知識,因為很多東西萬變不離其宗。
抽象很重要,但是作為學生,請不要 “總是” 習慣忽略細節,導致只懂理論,不會實踐,從CMU精心設計的實驗可以看出,一個細節沒搞清楚, 攻擊就無法成功,作為一個主動學習者,我們有時候需要多問一個為什么,比如:怎樣讓我寫的程序不能被GDB追蹤調試?另外一方面,很多時候學生沒有興趣或者學不會,問題可能真的不在學生身上,而是老師沒有認真思考如何教,讓學生真正有學會的感覺,像是杜文亮教授這樣的老師就讓我很感動:
https://www.handsonsecurity.net/
你可以真正學到有價值的東西!
延伸閱讀
英特爾官方提供的開發者手冊:Intel 64 and IA-32 Architectures Software Developer's Manuals
如果你想徹底搞懂C指針,強烈推薦你看看這個視頻課程:4小時徹底掌握C指針 - 頂尖程序員圖文講解
關于C語言和x86匯編語言之間的關系 (含函數調用過程等),還可以參考印度理工的:C語言和匯編語言
關于C語言指針和數組的關系以及內存的更多討論,請參考本人拙作:關于指針,數組,內存的思考
現代C語言相關的信息可以參考這本書(出自INRIA,在法國的地位相當于我國的中科院):現代C語言
文中的視頻地址:
https://fengmuzi2003.gitbook.io/csapp3e/di-3-zhang-ji-qi-ji-bian-cheng
- END -
看完一鍵三連在看,轉發,點贊
是對文章最大的贊賞,極客重生感謝你
推薦閱讀
Linux調度系統全景指南
深入理解零拷貝技術
深入理解DPDK程序設計|Linux網絡2.0
總結
- 上一篇: 经典算法刷题笔记pdf
- 下一篇: 为什么字节跳动选择使用 Go 语言?