比如這樣一段代碼:

int *p = (int*)malloc(10 * sizeof(int));
for(int i = 0; i &< 100; ++i) p[i] = i;

當i = 10就會越界,但是指針只是一個無符號整數,系統怎麼判斷出越界的呢?p指向的地址中只能保存10個int型整數這個信息保存在哪兒?


C語言本身規定越界是ub的,所以大多數實現下,編譯後的代碼很可能就不判斷越界,也就是說如果你訪問到界後面的內存,若這個內存有數據,可能就正常讀寫,只是寫亂了其他區域

如果是說debug模式能提醒你越界,一般是通過其他一些方式,比如vc會額外申請一段空間填cc,或把數組作為對象來實現等,類似於給你弄個虛擬環境跑程序,當然可以每步檢查了


自己判斷啊……人肉


這題沒法答.

  1. 指針不是無符號數. 指針是一個定址的工具. CPU有多少種定址方式, 指針就有多少種表示. 再考慮C++中有成員函數指針, 它就沒法簡單用一個數字表示. 參考此文, 指針存儲長度都不一樣.
  2. 指針訪問越界是指訪問, 不是指計算. 就說, p+ x 這個操作不越界, 讀p[x] 或寫 p[x] = 1 對應的地址才越界.
  3. 讀寫越界是CPU的陷入門控制, 一般應用編程並不會主動使用, 也就是說上面的代碼放在發布模式下它能運行,只是不知道它會導致什麼結果, 所謂未定義行為. 但在在調試模式下,調試器會對讀寫操作做檢查, 報越界錯誤. 長度信息是malloc函數在分配時寫好,想知道寫在哪, 可以去看malloc的文檔.
  4. 非常建議讀一下計算機導論, 最少知道一點計算機原理再尋根問底, 或者不求甚解也不錯.


是硬體判斷的,當你訪問到一個物理上不存在的地址,或者沒有許可權訪問的地址,硬體上就會捕捉到這個異常,然後系統就可以讀取到異常信息了。


首先強調本文用巨簡略且甚至錯誤的言論來做科普,只適合看個大概:

進程通過malloc(或其他內存分配介面)返回給你的內存地址是虛擬的地址,其按照某種映射關係映射到真實的物理地址。比喻一下,就是每個線程都拿著自己的一篇密文(虛擬內存地址)和一個密碼本(頁表),當進程A視圖訪問某個地址時,實際訪問的地址是A的密文+A的密碼本=A要訪問的實際地址。CPU訪問這個實際地址來讀取進程A期望的數據。當CPU執行完A的一個時間片切換到另一條進程B時,除了棧,寄存器狀態等上下文要進行切換,這個密碼本(頁表)也會被切換到B的密碼本。再切換回A時同理。

這樣做的一大好處就是,進程A和進程B實際訪問的內存是隔離的。我們假設A和B希望共享內存,這時進程B把自己分配好的一段內存地址寫到一個文件里,A進程再讀入這個文件(因為文件內容對任何線程都是一樣的),那麼A就能知道B分配的那一段內存嗎?不行的。因為A和B的內存頁表不同。線程A只拿到了B的虛擬內存地址,而在A的頁表中這個虛擬地址指向的位置並不是正確的真實地址(甚至這個虛擬地址在頁表中就不存在)。這樣,道理上說A進程無論寫出什麼花兒來都不可能入侵到B進程的內存中去。掛掉只掛A一個,比如不可能A的指針寫飛了,把QQ給殺死了。(當然也有跨進程的攻擊內存的方法,不搞安全我就不清楚了,咱不要較真)

那麼,一旦發生虛擬地址在頁表中不存在或其他類似情況,CPU就會爆出缺頁異常(請注意是硬體異常,非C++異常)。操作系統會捕獲這個異常,之後的具體行為取決於操作系統和你寫的軟體(例如開啟windows結構化異常之後,這種異常是可以被你的程序捕獲的)。由此,你的指針越界行為便被系統所獲知,因此題主你的指針越界行為就暴露了。

缺頁異常可以干很多事,除了報錯以外,操作系統處理缺頁異常時還可以干很多很多事,例如,當觸發缺頁異常時,操作系統會捕獲這個異常,然後去檢查這段缺頁的內存是不是實際上已經被操作系統交換到了硬碟里做虛擬內存使用。如果不是的話再報錯不遲。

那麼問題還沒有結束,我們還剩下兩個問題:

1.頁表是怎麼實現的?

2.題主的情況里為什麼i=10的時候才掛?

前面我只是宏觀上大概說說,特意沒說頁表的實現,就是想把這倆問題放一起說。我們假設有一台不存在的電腦,裡面有4G內存,那麼內存地址只需要32位就可以表示了,寫成16進位類似於:

0x80000000

我們希望每一個虛擬內存地址都映射到某個真實的內存地址上,

最繁瑣的實現就是任意虛擬內存地址可以映射到任意真實地址上,那麼我們就要建立一個巨大的hash表來達到這種映射。很顯然這樣做不大經濟。

最簡單的實現就是整個虛擬內存地址映射到整個真實地址上,那麼我們不需要建立任何映射就行了,但顯然這樣做沒有卵用。

那麼,考慮兩點取個中間值好了。我們這樣設計:

首先內存的低位四個16進位值全都是真實的,例如0x80000000,它的後四個地址的真實物理地址就是0000,不需要映射。

然後我們設計前面的8000未必是真實的,即8000的地址的真實地址可能被映射到任意位置,例如8000映射到1FFF上。這種映射被存在一個hash表裡。

這時,當進程A想訪問自己的0x80000000地址時,通過頁表可查得首地址的實際真實地址為1FFF,尾部不變,則實際地址為0x1FFF0000。

這就解釋了一個問題,我們編程時有時需要「內存對齊」操作。例如對齊16位元組等等。可是進程所看到的內存地址都是假的,你怎麼知道你在進程內存地址里看到的對齊的內存在物理地址里也是對齊的呢?----它肯定是對齊的,因為內存地址的低位就是真實的物理地址。

系統和CPU是什麼時候把8000映射到真實的1FFF的呢?簡單說就發生在malloc的時候。那麼這個問題就很簡單了。假設我們還是要訪問0x80000000地址,然而你沒有分配基於8000這個開頭的任何地址,那麼CPU往頁表裡一找,8000這個key壓根就沒有與其對應的value。ok!啥也別說了你越界了!

上文只是為了說著方便,所以假想了一台電腦,實際的x86的內存地址應該是按照4096為一頁來計算的(沒記錯的話)。在內存實際分配時,系統總是分配給你4096的整數倍的內存。只不過malloc並不是最底層的內存分配,它內部維護了一些結構,把系統一次給你的4096這麼大的內存塊又繼續拆分成了若干小塊。這就導致了有時候指針越界系統不知道(因為還在這個4096里,沒有觸發缺頁),有時候系統就能知道(觸發了缺頁異常)。

但是但是但是,再說回題主的代碼,可能還真不是因為缺頁異常。剛才已經提到,我們不可能光靠缺頁異常來發現內存的bug。實際開發中內存剛越界就觸發缺頁異常已經是很友好的情況了。真正可怕的是內存越界時,在CPU看來該進程還是在自己的頁中訪問內存,並沒有觸發缺頁異常。針對這種錯誤,目前很多編譯器都會在debug編譯時,調用debug版本的malloc。debug版本的malloc和release的malloc有何區別呢?一大區別就是debug版本的malloc加入了巨量的調試代碼,專門用來避免CPU捕獲不到的內存越界。具體實現我實在不知道是怎麼弄的。就這樣了。是的本文結尾就是這麼突然。


誰跟你說指針只是一個無符號數?

指針就是指針。

你C語言是譚浩強教的?


推薦閱讀:
相关文章