從貓蛇之戰(zhàn)三看內(nèi)核戲CPU
小時(shí)候曾經(jīng)目睹過貓與蛇戰(zhàn)斗,面對昂首發(fā)威的毒蛇,小貓不慌不忙,揮舞前爪,沉著冷靜,看準(zhǔn)時(shí)機(jī)進(jìn)攻,膽大心細(xì)。
在網(wǎng)上搜一下,可以看到很多貓蛇戰(zhàn)斗的照片,看來貓蛇之戰(zhàn)是很多人都喜歡看的“精彩節(jié)目”。
(照片來自搜索引擎)
再來一張更清晰一些的。
(照片來自搜索引擎)
之所以想到貓蛇之戰(zhàn),是因?yàn)榻裉煸?ldquo;格友會(huì)講”群里一位同行問了一個(gè)很有深度的問題。
(前方內(nèi)容只適合技術(shù)控,其他讀者止步)
簡單說問題是,調(diào)試器是如何訪問不能訪問的內(nèi)存的。
看了這個(gè)問題,我立刻覺得這位同行是有功力的。因?yàn)槠胀ǖ某绦騿T是問不出這樣的問題的。
要理解這個(gè)問題,必須有些底層的基礎(chǔ)。
***個(gè)基礎(chǔ)是要有保護(hù)模式的概念。很多同行都知道,今天的CPU是運(yùn)行在所謂的保護(hù)模式中,軟件訪問的內(nèi)存空間都是虛擬空間。而且這個(gè)虛擬空間中的內(nèi)容是分三六九等的,是分平民區(qū)和富人區(qū)的,是分道路和深坑的。因?yàn)榇?,訪問內(nèi)存時(shí)是要小心的,有些地方可以訪問,有些地方一訪問就可能出大問題的,爆炸崩潰甚至“死亡”的。
大多數(shù)的應(yīng)用程序崩潰和系統(tǒng)藍(lán)屏都是因?yàn)樵L問了不該訪問的地方。
第二個(gè)基礎(chǔ)是對調(diào)試器有比較深的認(rèn)識,知道在調(diào)試器里可以放心大膽地想訪問哪里就訪問哪里,不用那么小心。
舉例來說,在普通程序里,如果訪問空地址,那么不死也傷半條命(處理不好,就被系統(tǒng)殺了)。但是在調(diào)試器里,dd 0沒有問題,調(diào)試器會(huì)給出一串串可愛的問號,代表不可訪問,子虛烏有。
- 6: kd> dd 0
- 00000000`00000000 ???????? ???????? ???????? ????????
- 00000000`00000010 ???????? ???????? ???????? ????????
- 00000000`00000020 ???????? ???????? ???????? ????????
- 00000000`00000030 ???????? ???????? ???????? ????????
- 00000000`00000040 ???????? ???????? ???????? ????????
- 00000000`00000050 ???????? ???????? ???????? ????????
- 00000000`00000060 ???????? ???????? ???????? ????????
- 00000000`00000070 ???????? ???????? ???????? ????????
那么問題來了,為啥普通程序一碰就爆炸,而調(diào)試器訪問卻安然無恙呢?
坦率說,***次在腦海中出現(xiàn)這個(gè)問題時(shí),也令我困惑了一陣。直到后來發(fā)現(xiàn)了內(nèi)核中的一個(gè)神秘機(jī)制。這個(gè)機(jī)制是跨操作系統(tǒng)的,Windows中有,Linux也有,而且都是相同的名字,叫Probe。
有點(diǎn)令人詫異的是,連函數(shù)名很類似,比如Windows(NT內(nèi)核)中的兩個(gè)函數(shù)為:
- 6: kd> x nt!probe*
- fffff800`06581d70 nt!ProbeForWrite (void)
- fffff800`06518ad0 nt!ProbeForRead (<no parameter info>)
而Linux內(nèi)核中的兩個(gè)函數(shù)為:
- root@gedu-VirtualBox:/home/gedu/labs/linux-source-4.8.0# sudo cat /proc/kallsyms | grep "\bprobe_ke"
- ffffffff811a5f00 W probe_kernel_read
- ffffffff811a5fc0 W probe_kernel_write
搜一下KDB/KGDB的源代碼,可以看到很多地方調(diào)用了上面兩個(gè)函數(shù):
簡單來說,內(nèi)核里封裝了兩個(gè)特殊的函數(shù),提供給包括調(diào)試器在內(nèi)的一些特殊客戶使用。
接下來的問題是,probe函數(shù)內(nèi)部是如何做的呢?有關(guān)的源代碼如下。
(更完整的請見https://elixir.bootlin.com/linux/v4.8/source/mm/maccess.c#L23 )
其中的關(guān)鍵是在__copy動(dòng)作前后分別有:
- pagefault_disable();
- pagefault_enable();
也就是先禁止了pagefault,訪問好之后再啟用。這有點(diǎn)像是在耍蛇之前,先把它的毒牙包上。
繼續(xù)深挖,在目前的Linux內(nèi)核實(shí)現(xiàn)中,是維護(hù)一個(gè)計(jì)數(shù)器:pagefault_disabled。
(https://elixir.bootlin.com/linux/v5.0-rc8/source/include/linux/uaccess.h)
在處理頁錯(cuò)誤的do_page_fault函數(shù)中,會(huì)判斷這個(gè)標(biāo)志,如果發(fā)現(xiàn)禁止條件,則忽略這次訪問錯(cuò)誤。
講到這里,問題說清了一半,要繼續(xù)深追的話,還有一些細(xì)節(jié),今天有點(diǎn)晚了,改日再敘。