在本章的學習中,咱們經過學習C語言提供的抽象層下面的東西,來了解機器及編程。經過讓編譯器產生機器級程序彙編代碼表示,咱們瞭解了編譯器和它的優化能力。不論咱們是在用C語言仍是用JAVA或是其餘的語言編程時,咱們會忽視程序的機器級的實現。機器語言不須要被編譯,能夠直接被CPU執行,其執行速度十分快。可是機器語言的讀寫性與移植性較高級語言低。高級語言被編譯後便成爲了彙編語言,彙編語言十分接近機器語言。以後彙編代碼會轉化爲機器語言。雖然現代的編譯器能幫助咱們將高級語言轉化爲彙編語言,解決了很多問題,可是對於一個嚴謹的程序員來講,須要作到可以閱讀和理解彙編語言。好比C語言缺少邊界檢查問題,使得許多程序容易出現緩衝區溢出,雖然最近的運行系統提供了安全保護,並且編譯器幫助使得程序更安全,可是這已經使許多系統容易受到惡意入侵者的攻擊。linux
前段時間一直想換電腦,轉了幾家電腦店發現我是一個連i7處理器都不瞭解的電腦盲,爲何最新的電腦都換成了i7處理器?和以前的i5等等有什麼區別?經過這一章的學習我對處理器也有了一個大概的瞭解。Intel處理器最的早是8086,它是十六位的微處理器,做爲第一代單芯片,8086知名度是至關的高。以後又有8028六、i38六、i48六、Pentium、PentiumPro、Pentium/MMX、PentiumⅡ、PentiumIII等等的一系列處理器的出現。目前最新的處理器是core i7處理器,既支持超線程,也有多核。引入AVX,並擴展至AVX2,增長了更多的指令和指令格式。每一個時間上相繼的處理器都是向後兼容的。Intel稱其指令集爲IA32,也就是Intel32位體系結構,也就是咱們日常所說的x86。程序員
計算機系統使用了多種不一樣形式的抽象,利用更簡單的抽象模型來隱藏實現的細節。
對於機器級編程來講,其中兩種抽象尤其重要:編程
一、指令集體系結構或指令級框架:它定義了處理器狀態、指令的格式,以及每條指令對狀態的影響。
IA32將程序的行爲描述成好像每條指令時按順序執行的,一條指令結束後,下一條再開始。(實際上處理器併發地執行許多指令,可是能夠採起措施保證總體行爲與ISA指定的順序執行徹底一致)數組
二、機器級程序使用的存儲器地址是虛擬地址:提供的存儲器模型看上去是一個很是大的字節數組。存儲器系統的實際實現是將多個硬件存儲器和操做系統軟件組合起來。安全
程序存儲器(program memory)包含:程序的可執行機器代碼、操做系統須要的一些信息、棧、堆。程序存儲器用虛擬地址來尋址(此虛擬地址不是機器級虛擬地址)。操做系統負責管理虛擬地址空間(程序級虛擬地址),將虛擬地址翻譯成實際處理器存儲器中的物理地址(機器級虛擬地址)。網絡
這一部分還講到了預處理,編譯,彙編,連接,在上學期的課程中有學過,沒有那麼陌生。用c語言寫一個代碼文件mstore.c:
在命令行上使用「-S」選項,就能看到C語言編譯器產生的彙編代碼:數據結構
Linux> gcc -Og -S mstore.c
經過ls查看能夠發現,產生了一個彙編文件mstore.s,彙編代碼文件包含各類聲明,包括下面幾行:
上面每一個縮進去的行都對應於一條機器指令,好比,pushq指令表示應該將寄存器%rbx的內容壓入程序棧中。這段代碼中已經除去了全部關於局部變量名或數據類型的信息。下面咱們使用「-c」命令,gcc會編譯並彙編該代碼:併發
Linux> gcc -Og -c mstore.c
產生了目標代碼文件mstore.o,它是二進制格式的,因此沒法直接查看。1368字節的文件mstore.o中有一段14字節的序列,它的十六進制表示爲:框架
53 48 89 d3 e8 00 00 00 00 48 89 03 5b c3
這就是上面列出的彙編指令對應的目標代碼,機器執行的程序只是一個字節序列,它是對一系列指令的編碼。機器對產生這些指令的源代碼幾乎一無所知。
查看機器代碼文件的內容,有一類稱爲反彙編器,帶「-d」命令行:linux> objdump -d mstore.o, 結構以下:
左右兩邊實際是等價的,右邊是給出的彙編語言。函數
這是IA32中央處理器所包含的一組八個存儲單元的32位存儲器。前六個是通用寄存器,對它們的使用沒有限制。前三個寄存器(%eax,%ecx,%edx)的保存和恢復慣例不一樣於接下來的三個寄存器(%ebx,%esi,%edi)。最後兩個寄存器保存着指向程序棧重要位置的指針,稱爲棧指針和幀指針。數據存放在寄存器中進行加減乘除等一些操做。原來的寄存器是16位的因此如圖所示藍色部分是0-15,以後寄存器進行了擴充,變成了32位的即0-31。
對於C和彙編代碼中的語句,默認的是按照語句或指令在程序中出現的順序來執行的。
(1)條件碼
CPU包含了一組單個位的條件碼寄存器,它們描述了最近的算術或邏輯操做的屬性。常見的條件碼爲:進位標誌(CF)、零標誌(ZF)、符號標誌(SF)、溢出標誌(OF)
(2)訪問條件碼
兩種最經常使用的訪問的條件碼的方法不是直接讀取它們,而是根據條件碼的某個組合,設置一個整數寄存器或是執行一條分支指令。每一個指令根據條件碼的某個組合,將一個字節設置爲0或者1。同一個機器指令能夠有不一樣的名字。
(3)跳轉指令
跳轉指令會致使執行切換到程序中的一個全新的位置。這些跳轉的目的地一般用一個標號指明。jmp指令是無條件跳轉的,能夠直接跳轉也能夠間接跳轉。
(4)循環
do-while、while、 for
(5)switch語句
switch語句提供了根據一個整數索引值進行多重分支的能力。在處理具備多種可能結果的測試時,特別有用。它經過使用跳轉表使代碼更加高效。
一個過程調用包括將數據(以過程參數和返回值的形式)和控制從代碼的一部分傳遞到另外一部分。另外,它還必須在進入時爲過程的局部變量分配空間,並在退出時釋放這些空間。
(1)棧幀結構
棧幀結構指的是爲單個過程分配的那部分棧。棧幀的最頂端是以兩個指針定界的,寄存器%ebp做爲幀指針,寄存器%esp做爲棧指針。棧指針是能夠移動的,因此大多數信息的訪問都是相對於幀指針的 。
(2)轉移控制
call :過程調用
leave:爲返回準備棧
ret:從過程調用中返回
(3)遞歸過程
異質結構是指不一樣數據類型的數組組合,好比C語言當中的結構(struct)與聯合(union)。我經過了兩個簡單的c程序來區分告終構和聯合。
首先分別寫一個struct.c文件和union.c文件以下:
運行一下,發現結果不一樣:
這正是由於上面咱們提到過的對齊的緣由,只不過在struct中,對齊不是地址對齊也不是棧分配空間對齊,而是數據對齊。爲了提升數據讀取的速度,通常狀況下會將數據以2的指數倍對齊,具體是二、四、8仍是16,得根據具體的硬件設施以及操做系統來決定。這樣作的好處是,處理器能夠統一的一次性讀取4(也多是其它數值)個字節,而再也不須要針對特殊的數據類型讀取作特殊處理。
與結構體不一樣的是,聯合會複用內存空間,以節省內存。它與結構體最大的區別就在於,對a、b、c賦值時,聯合會覆蓋掉以前的賦值,輸出a,b,c中最大字節值,而結構體則不會,結構體能夠同時保存a、b、c的值。
強制類型轉換的優先級高於加大,指針從一個類型轉爲另一個類型,只改變它的類型,不改變它的值。如 p 是一個 char * 類型的指針,值爲p,(int * )p + 7 計算爲 p+28 ,而(int * )(p + 7)計算爲 p+7。
緩衝區溢出
一般,在棧中分配某個字節數組來保存一個字符串,可是字符串的長度超出了爲數組分配的空間。C對於數組引用不進行任何邊界檢查,並且局部變量和狀態信息,都存在棧中。這樣,對越界的數組元素的寫操做會破壞存儲在棧中的狀態信息。當程序使用這個被破壞的狀態,試圖從新加載寄存器或執行ret指令時,就會出現很嚴重的錯誤。
void echo() { char buf[8] ; gets(buf) ; puts(buf) ; }
因爲棧是向地地址增加的,數組緩衝區是向高地址增加的。故,長一些的字符串會致使gets覆蓋棧上存儲的某些信息。
隨着字符串變長,下面的信息會被破壞:
輸入的字符數量 被破壞的狀態
0---7 無
8---11 保存的%ebx的值
12---15 保存的%ebp的值
16---19 返回地址
20+ caller中保存的狀態
若是破壞了存儲%ebp的值,那麼基址寄存器就不能正確地恢復,所以調用者就不能正確地引用它的局部變量或參數。
若是破壞了存儲的返回地址,那麼ret指令會使程序跳轉到徹底意想不到的地方。
緩衝區溢出的一個更加致命的使用就是讓程序執行它原本不肯意執行的函數。這是一種最多見的經過計算機網絡攻擊系統安全的方法。一般,輸入給程序一個字符串,這個字符串包含一些可執行代碼的字節編碼,稱爲攻擊代碼,另外還有一些字節會用一個指向攻擊代碼的指針覆蓋返回地址。那麼,執行ret指令的效果就是跳轉到攻擊代碼。
一般,使用gets或其餘任何能致使存儲溢出的函數,都不是好的編程習慣。不幸的是,不少經常使用庫函數,包括strcpy、strcat、sprintf,都有一個屬性——不須要告訴它們目標緩衝區的大小,就產生一個字節序列。
對抗緩衝區溢出攻擊
一、棧隨機化
爲了在系統中插入攻擊代碼,攻擊者不但要插入代碼,還要插入指向這段代碼的指針,這個指針也是攻擊字符串的一部分。產生這個指針須要知道這個字符串放置的棧地址。在過去,程序的棧地址很是容易預測,在不一樣的機器之間,棧的位置是至關固定的。
棧隨機化的思想使得棧的位置在程序每次運行時都有變化。所以,即便許多機器都運行相同的代碼。它們的棧地址都是不一樣的。
實現的方式是:程序開始時,在棧上分配一段0--n字節之間的隨機大小空間。程序不使用這段空間,可是它會致使程序每次執行時後續的棧位置發生了變化。
在Linux系統中,棧隨機化已經變成了標準行爲。(在linux上每次運行相同的程序,其同一局部變量的地址都不相同)
二、棧破壞檢測
在C語言中,沒有可靠的方法來防止對數組的越界寫,可是,咱們可以在發生了越界寫的時候,在沒有形成任何有害結果以前,嘗試檢測到它。
最近的GCC版本在產生的代碼中加入了一種棧保護者機制,用來檢測緩衝區越界,其思想是在棧中任何局部緩衝區與棧狀態之間存儲一個特殊的金絲雀值。這個金絲雀值是在程序每次運行時隨機產生的,所以,攻擊者沒有簡單的辦法知道它是什麼。
在恢復寄存器狀態和從函數返回以前,程序檢查這個金絲雀值是否被該函數的某個操做或者函數調用的某個操做改變了。若是是,那麼程序異常終止。
三、限制可執行代碼區域 限制那些可以存放可執行代碼的存儲器區域。在典型的程序中,只有保存編譯器產生的代碼的那部分存儲器才須要是可執行的,其餘部分能夠被限制爲只容許讀和寫。 如今的64位處理器的內存保護引入了」NX」(不執行)位。有了這個特性,棧能夠被標記爲可讀和可寫,可是不可執行,檢查頁是否可執行由硬件來完成,效率上沒有損失。