格友 | 格蠹老雷
小時候曾經目睹過貓與蛇戰鬥,面對昂首發威的毒蛇,小貓不慌不忙,揮舞前爪,沉着冷靜,看準時機進攻,膽大心細。
在網上搜一下,可以看到很多貓蛇戰鬥的照片,看來貓蛇之戰是很多人都喜歡看的“精彩節目”。
(照片來自搜尋引擎)
再來一張更清晰一些的。
(照片來自搜尋引擎)
之是以想到貓蛇之戰,是因為今天在“格友會講”群裡一位同行問了一個很有深度的問題。
(前方内容隻适合技術控,其他讀者止步)
簡單說問題是,調試器是如何通路不能通路的記憶體的。
看了這個問題,我立刻覺得這位同行是有功力的。因為普通的程式員是問不出這樣的問題的。
要了解這個問題,必須有些底層的基礎。
第一個基礎是要有保護模式的概念。很多同行都知道,今天的CPU是運作在所謂的保護模式中,軟體通路的記憶體空間都是虛拟空間。而且這個虛拟空間中的内容是分三六九等的,是分平民區和富人區的,是分道路和深坑的。因為此,通路記憶體時是要小心的,有些地方可以通路,有些地方一通路就可能出大問題的,爆炸崩潰甚至“死亡”的。
大多數的應用程式崩潰和系統藍屏都是因為通路了不該通路的地方。
第二個基礎是對調試器有比較深的認識,知道在調試器裡可以放心大膽地想通路哪裡就通路哪裡,不用那麼小心。
舉例來說,在普通程式裡,如果通路空位址,那麼不死也傷半條命(處理不好,就被系統殺了)。但是在調試器裡,dd 0沒有問題,調試器會給出一串串可愛的問号,代表不可通路,子虛烏有。
6: kd> dd 0
00000000`00000000 ???????? ???????? ???????? ????????
00000000`00000010 ???????? ???????? ???????? ????????
00000000`00000020 ???????? ???????? ???????? ????????
00000000`00000030 ???????? ???????? ???????? ????????
00000000`00000040 ???????? ???????? ???????? ????????
00000000`00000050 ???????? ???????? ???????? ????????
00000000`00000060 ???????? ???????? ???????? ????????
00000000`00000070 ???????? ???????? ???????? ????????
那麼問題來了,為啥普通程式一碰就爆炸,而調試器通路卻安然無恙呢?
坦率說,第一次在腦海中出現這個問題時,也令我困惑了一陣。直到後來發現了核心中的一個神秘機制。這個機制是跨作業系統的,Windows中有,Linux也有,而且都是相同的名字,叫Probe。
有點令人詫異的是,連函數名很類似,比如Windows(NT核心)中的兩個函數為:
6: kd> x nt!probe*
fffff800`06581d70 nt!ProbeForWrite (void)
fffff800`06518ad0 nt!ProbeForRead (<no parameter info>)
而Linux核心中的兩個函數為:
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的源代碼,可以看到很多地方調用了上面兩個函數:
簡單來說,核心裡封裝了兩個特殊的函數,提供給包括調試器在内的一些特殊客戶使用。
接下來的問題是,probe函數内部是如何做的呢?有關的源代碼如下。
(更完整的請見https://elixir.bootlin.com/linux/v4.8/source/mm/maccess.c#L23 )
其中的關鍵是在__copy動作前後分别有:
pagefault_disable();
pagefault_enable();
也就是先禁止了pagefault,通路好之後再啟用。這有點像是在耍蛇之前,先把它的毒牙包上。
繼續深挖,在目前的Linux核心實作中,是維護一個計數器:pagefault_disabled。
(https://elixir.bootlin.com/linux/v5.0-rc8/source/include/linux/uaccess.h)
在處理頁錯誤的do_page_fault函數中,會判斷這個标志,如果發現禁止條件,則忽略這次通路錯誤。
講到這裡,問題說清了一半,要繼續深追的話,還有一些細節,今天有點晚了,改日再叙。