最近跟人聊到,自己曾經透過存取位址溢位
例如使用指標 (pointer),或者指派一個超過陣列 (array)大小的索引值
成功把外好幾層函式的變數內容
甚至是return address也改掉,程式指令不知道跳去哪了
對方很驚訝問:「程式不是會自己幫你作check嗎?
或者作業系統應該不是會終止程式嗎?
這個問題蠻多人問過的,有需要講一下

 

程式的原則要什麼動作
就一定要有寫在機器碼才會執行
這動作不是自己寫,就是其他程式幫忙加
原則上是由編譯器 (compiler)幫忙的
存取陣列的時候大致上組合語言步驟如下

  1. 1. 載入陣列的記憶體位址(頭端)
  2. 2. 載入經由開發者放入位移運算子(shift operator,[])中的位移值
        若是文字常量(literal value)就編成載入立即值 
        若是變數就載入變數儲存值
  3. 3. 經由陣列「記憶體位址」加上「位移值乘以元素大小」得到目標的記憶體位址
  4. 4. 載入或寫入記憶體值完成存取

C編譯要做的事情很簡單
就是把程式碼以最低要求編出組語即可
這保證寫C程式的人對於自己的程式有最大的自由度
也求能減少產生的組語減低運算或空間消耗
原則上所定義的語法指令已經非常基礎
開發者有最大的調校權,由這些基礎指令自行再組合出需要的功能
所以除了要保證的語意功能外,不會再有額外的動作
因此不會再另外多一段條件式判斷(if...else...)去檢查並產生錯誤

 

試想,若每次存取的地方都需要編入一段條件判斷
這時候程式的大小就不只增加一點點了
且不是只有加條件判斷式那麼簡單
編譯器還要在執行檔中填入每個陣列的資訊記載陣列大小
在每段陣列存取中再載入這個陣列大小以便做比較
且若是靜態記憶體配置(如全域與區域變數)
編譯器作業期間還有辦法掌握資訊
若是動態產生的記憶體區塊(malloc...等)
或者被傳遞到指標、或其他函式的參數列當中
編譯器也不知如何事先決定陣列大小,或者陣列來源
要針對所有的狀況檢查根本是癡人說夢
因為超出能掌握的範圍,根本無法作出判斷
C編譯器唯一能做判斷的狀況是
陣列位移運算子中放的是文字常量
因為文字常量不像變數無法預期數值為何
編譯器作業期間可以完全掌握,能夠準確報出錯誤,停止編譯

換成指標其實也是一樣的狀況
指標的靈活度與不可掌握性又比陣列還要高出太多
編譯器可能需要加入很多的檢查程式碼
檢查每次存取是否會超出這個函式堆疊框(function stack frame)範圍
非常不符合經濟效益
且若面臨指標或參數傳遞,也無法檢查

 

至於作業系統要做幫忙檢查那更不可能
首先是作業系統的執行方式,問的人可能把作業系統想得太神奇了
可能潛意識認為使用者程式的執行
是透過作業系統一道一道讀取再作執行
事實上一旦程式開始跑,它就成為作業系統的一部份
作業系統怎麼跑,使用者程式就怎麼跑
除非使用到系統呼叫或者 CPU發送中斷 (Interrupt)
否則不可能執行到作業系統本身的程式碼
 

另外作業系統只負責硬體資源的調度管理
保證程式間不會互相干擾
保證在可以確實掌控的資訊中不對自己產生危害
除此之外不假設程式對硬體存取的用途,剩下的就是其他人的責任
作業系統是會對一定範圍的記憶體位址限制存取
除了不該存取的特定位址範圍,其他都是合法的記憶體位址
就算超過函式堆疊的範圍
若都是合法那當然不能管、也無從限制其用途

 

假設不要看上面兩個問題(因為看了這段就不用講了)
可能有人會問,但是函式的堆疊不是都是固定格式的嗎?
透過 stack pointer、frame pointer找到一個函式的範圍
但很不幸的,函式堆疊的格式是由編譯器來決定的
甚至在組合語言時代是由開發人員來編寫
不同的編譯器,編寫的方式會有所不同
作業系統根本不清楚實際上一支程式是由哪個編譯器所編成

arrow
arrow

    wylokgo101 發表在 痞客邦 留言(0) 人氣()