linux copy_from/to_user原理
轉(zhuǎn)載地址:http://www.poluoluo.com/server/201107/138420.html
在研究dahdi驅(qū)動(dòng)的時(shí)候,見到了一些get_user,put_user的函數(shù),不知道其來由,故而搜索了這篇文章,前面對(duì)Linux內(nèi)存的框架描述不是很清晰,描述的有一點(diǎn)亂,如果沒有剛性需求,建議不用怎么關(guān)注,倒不如直接看那幾個(gè)圖片。對(duì)我非常有用的地方就是幾個(gè)函數(shù)的介紹,介紹的比較詳細(xì),對(duì)應(yīng)用有需求的可以著重看一個(gè)這幾個(gè)函數(shù)。
Linux 內(nèi)存
在 Linux 中,用戶內(nèi)存和內(nèi)核內(nèi)存是獨(dú)立的,在各自的地址空間實(shí)現(xiàn)。地址空間是虛擬的,就是說地址是從物理內(nèi)存中抽象出來的(通過一個(gè)簡(jiǎn)短描述的過程)。由于地址空間是虛擬的,所以可以存在很多。事實(shí)上,內(nèi)核本身駐留在一個(gè)地址空間中,每個(gè)進(jìn)程駐留在自己的地址空間。這些地址空間由虛擬內(nèi)存地址組成,允許一些帶有獨(dú)立地址空間的進(jìn)程指向一個(gè)相對(duì)較小的物理地址空間(在機(jī)器的物理內(nèi)存中)。不僅僅是方便,而且更安全。因?yàn)槊總€(gè)地址空間是獨(dú)立且隔離的,因此很安全。
但是與安全性相關(guān)聯(lián)的成本很高。因?yàn)槊總€(gè)進(jìn)程(和內(nèi)核)會(huì)有相同地址指向不同的物理內(nèi)存區(qū)域,不可能立即共享內(nèi)存。幸運(yùn)的是,有一些解決方案。用戶進(jìn)程可以通過 Portable Operating System Interface for UNIX? (POSIX) 共享的內(nèi)存機(jī)制(shmem)共享內(nèi)存,但有一點(diǎn)要說明,每個(gè)進(jìn)程可能有一個(gè)指向相同物理內(nèi)存區(qū)域的不同虛擬地址。
虛擬內(nèi)存到物理內(nèi)存的映射通過頁(yè)表完成,這是在底層軟件中實(shí)現(xiàn)的(見圖 1)。硬件本身提供映射,但是內(nèi)核管理表及其配置。注意這里的顯示,進(jìn)程可能有一個(gè)大的地址空間,但是很少見,就是說小的地址空間的區(qū)域(頁(yè)面)通過頁(yè)表指向物理內(nèi)存。這允許進(jìn)程僅為隨時(shí)需要的網(wǎng)頁(yè)指定大的地址空間。
圖 1. 頁(yè)表提供從虛擬地址到物理地址的映射?
由于缺乏為進(jìn)程定義內(nèi)存的能力,底層物理內(nèi)存被過度使用。通過一個(gè)稱為 paging(然而,在 Linux 中通常稱為 swap)的進(jìn)程,很少使用的頁(yè)面將自動(dòng)移到一個(gè)速度較慢的存儲(chǔ)設(shè)備(比如磁盤),來容納需要被訪問的其它頁(yè)面(見圖 2 )。這一行為允許,在將很少使用的頁(yè)面遷移到磁盤來提高物理內(nèi)存使用的同時(shí),計(jì)算機(jī)中的物理內(nèi)存為應(yīng)用程序更容易需要的頁(yè)面提供服務(wù)。注意,一些頁(yè)面可以指向文件,在這種情況下,如果頁(yè)面是臟(dirty)的,數(shù)據(jù)將被沖洗,如果頁(yè)面是干凈的(clean),直接丟掉。
圖 2. 通過將很少使用的頁(yè)面遷移到速度慢且便宜的存儲(chǔ)器,交換使物理內(nèi)存空間得到了更好的利用?
MMU-less?架構(gòu)
不是所有的處理器都有 MMU。因此,uClinux 發(fā)行版(微控制器 Linux)支持操作的一個(gè)地址空間。該架構(gòu)缺乏 MMU 提供的保護(hù),但是允許 Linux 運(yùn)行另一類處理器。
選擇一個(gè)頁(yè)面來交換存儲(chǔ)的過程被稱為一個(gè)頁(yè)面置換算法,可以通過使用許多算法(至少是最近使用的)來實(shí)現(xiàn)。該進(jìn)程在請(qǐng)求存儲(chǔ)位置時(shí)發(fā)生,存儲(chǔ)位置的頁(yè)面不在存儲(chǔ)器中(在存儲(chǔ)器管理單元 [MMU] 中無映射)。這個(gè)事件被稱為一個(gè)頁(yè)面錯(cuò)誤 并被硬件(MMU)刪除,出現(xiàn)頁(yè)面錯(cuò)誤中斷后該事件由防火墻管理。該棧的詳細(xì)說明見 圖 3。
Linux 提供一個(gè)有趣的交換實(shí)現(xiàn),該實(shí)現(xiàn)提供許多有用的特性。Linux 交換系統(tǒng)允許創(chuàng)建和使用多個(gè)交換分區(qū)和優(yōu)先權(quán),這支持存儲(chǔ)設(shè)備上的交換層次結(jié)構(gòu),這些存儲(chǔ)設(shè)備提供不同的性能參數(shù)(例如,固態(tài)磁盤 [SSD] 上的一級(jí)交換和速度較慢的存儲(chǔ)設(shè)備上的較大的二級(jí)交換)。為 SSD 交換附加一個(gè)更高的優(yōu)先級(jí)使其可以使用直至耗盡;直到那時(shí),頁(yè)面才能被寫入優(yōu)先級(jí)較低的交換分區(qū)。
圖 3. 地址空間和虛擬 - 物理地址映射的元素?
并不是所有的頁(yè)面都適合交換。考慮到響應(yīng)中斷的內(nèi)核代碼或者管理頁(yè)表和交換邏輯的代碼,顯然,這些頁(yè)面決不能被換出,因此它們是固定的,或者是永久地駐留在內(nèi)存中。盡管內(nèi)核頁(yè)面不需要進(jìn)行交換,然而用戶頁(yè)面需要,但是它們可以被固定,通過 mlock(或 mlockall)函數(shù)來鎖定頁(yè)面。這就是用戶空間內(nèi)存訪問函數(shù)的目的。如果內(nèi)核假設(shè)一個(gè)用戶傳遞的地址是有效的且是可訪問的,最終可能會(huì)出現(xiàn)內(nèi)核嚴(yán)重錯(cuò)誤(kernel panic)(例如,因?yàn)橛脩繇?yè)面被換出,而導(dǎo)致內(nèi)核中的頁(yè)面錯(cuò)誤)。該應(yīng)用程序編程接口(API)確保這些邊界情況被妥善處理。
內(nèi)核 API
現(xiàn)在,讓我們來研究一下用戶操作用戶內(nèi)存的內(nèi)核 API。請(qǐng)注意,這涉及內(nèi)核和用戶空間接口,而下一部分將研究其他的一些內(nèi)存 API。用戶空間內(nèi)存訪問函數(shù)在表 1 中列出。
表 1. 用戶空間內(nèi)存訪問 API
| 函數(shù) | 描述 |
| access_ok | 檢查用戶空間內(nèi)存指針的有效性 |
| get_user | 從用戶空間獲取一個(gè)簡(jiǎn)單變量 |
| put_user | 輸入一個(gè)簡(jiǎn)單變量到用戶空間 |
| clear_user | 清除用戶空間中的一個(gè)塊,或者將其歸零。 |
| copy_to_user | 將一個(gè)數(shù)據(jù)塊從內(nèi)核復(fù)制到用戶空間 |
| copy_from_user | 將一個(gè)數(shù)據(jù)塊從用戶空間復(fù)制到內(nèi)核 |
| strnlen_user | 獲取內(nèi)存空間中字符串緩沖區(qū)的大小 |
| strncpy_from_user | 從用戶空間復(fù)制一個(gè)字符串到內(nèi)核 |
正如您所期望的,這些函數(shù)的實(shí)現(xiàn)架構(gòu)是獨(dú)立的。例如在 x86 架構(gòu)中,您可以使用 ./linux/arch/x86/lib/usercopy_32.c 和 usercopy_64.c 中的源代碼找到這些函數(shù)以及在 ./linux/arch/x86/include/asm/uaccess.h 中定義的字符串。
當(dāng)數(shù)據(jù)移動(dòng)函數(shù)的規(guī)則涉及到復(fù)制調(diào)用的類型時(shí)(簡(jiǎn)單 VS. 聚集),這些函數(shù)的作用如圖 4 所示。
圖 4. 使用 User Space Memory Access API 進(jìn)行數(shù)據(jù)移動(dòng)?
access_ok 函數(shù)
您可以使用 access_ok 函數(shù)在您想要訪問的用戶空間檢查指針的有效性。調(diào)用函數(shù)提供指向數(shù)據(jù)塊的開始的指針、塊大小和訪問類型(無論這個(gè)區(qū)域是用來讀還是寫的)。函數(shù)原型定義如下:
access_ok( type, addr, size );
type 參數(shù)可以被指定為 VERIFY_READ 或 VERIFY_WRITE。VERIFY_WRITE 也可以識(shí)別內(nèi)存區(qū)域是否可讀以及可寫(盡管訪問仍然會(huì)生成 -EFAULT)。該函數(shù)簡(jiǎn)單檢查地址可能是在用戶空間,而不是內(nèi)核。
get_user 函數(shù)
要從用戶空間讀取一個(gè)簡(jiǎn)單變量,可以使用 get_user 函數(shù),該函數(shù)適用于簡(jiǎn)單數(shù)據(jù)類型,比如,char 和 int,但是像結(jié)構(gòu)體這類較大的數(shù)據(jù)類型,必須使用 copy_from_user 函數(shù)。該原型接受一個(gè)變量(存儲(chǔ)數(shù)據(jù))和一個(gè)用戶空間地址來進(jìn)行 Read 操作:
get_user( x, ptr );
get_user 函數(shù)將映射到兩個(gè)內(nèi)部函數(shù)其中的一個(gè)。在系統(tǒng)內(nèi)部,這個(gè)函數(shù)決定被訪問變量的大小(根據(jù)提供的變量存儲(chǔ)結(jié)果)并通過 __get_user_x 形成一個(gè)內(nèi)部調(diào)用。成功時(shí)該函數(shù)返回 0,一般情況下,get_user 和 put_user 函數(shù)比它們的塊復(fù)制副本要快一些,如果是小類型被移動(dòng)的話,應(yīng)該用它們。
put_user 函數(shù)
您可以使用 put_user 函數(shù)來將一個(gè)簡(jiǎn)單變量從內(nèi)核寫入用戶空間。和 get_user 一樣,它接受一個(gè)變量(包含要寫的值)和一個(gè)用戶空間地址作為寫目標(biāo):
put_user( x, ptr );
和 get_user 一樣,put_user 函數(shù)被內(nèi)部映射到 put_user_x 函數(shù),成功時(shí),返回 0,出現(xiàn)錯(cuò)誤時(shí),返回 -EFAULT。
clear_user 函數(shù)
clear_user 函數(shù)被用于將用戶空間的內(nèi)存塊清零。該函數(shù)采用一個(gè)指針(用戶空間中)和一個(gè)型號(hào)進(jìn)行清零,這是以字節(jié)定義的:
clear_user( ptr, n );
在內(nèi)部,clear_user 函數(shù)首先檢查用戶空間指針是否可寫(通過 access_ok),然后調(diào)用內(nèi)部函數(shù)(通過內(nèi)聯(lián)組裝方式編碼)來執(zhí)行 Clear 操作。使用帶有 repeat 前綴的字符串指令將該函數(shù)優(yōu)化成一個(gè)非常緊密的循環(huán)。它將返回不可清除的字節(jié)數(shù),如果操作成功,則返回 0。
copy_to_user 函數(shù)
copy_to_user 函數(shù)將數(shù)據(jù)塊從內(nèi)核復(fù)制到用戶空間。該函數(shù)接受一個(gè)指向用戶空間緩沖區(qū)的指針、一個(gè)指向內(nèi)存緩沖區(qū)的指針、以及一個(gè)以字節(jié)定義的長(zhǎng)度。該函數(shù)在成功時(shí),返回 0,否則返回一個(gè)非零數(shù),指出不能發(fā)送的字節(jié)數(shù)。
copy_to_user( to, from, n );
檢查了向用戶緩沖區(qū)寫入的功能之后(通過 access_ok),內(nèi)部函數(shù) __copy_to_user 被調(diào)用,它反過來調(diào)用 __copy_from_user_inatomic(在 ./linux/arch/x86/include/asm/uaccess_XX.h 中。其中 XX 是 32 或者 64 ,具體取決于架構(gòu)。)在確定了是否執(zhí)行 1、2 或 4 字節(jié)復(fù)制之后,該函數(shù)調(diào)用 __copy_to_user_ll,這就是實(shí)際工作進(jìn)行的地方。在損壞的硬件中(在 i486 之前,WP 位在管理模式下不可用),頁(yè)表可以隨時(shí)替換,需要將想要的頁(yè)面固定到內(nèi)存,使它們?cè)谔幚頃r(shí)不被換出。i486 之后,該過程只不過是一個(gè)優(yōu)化的副本。
copy_from_user 函數(shù)
copy_from_user 函數(shù)將數(shù)據(jù)塊從用戶空間復(fù)制到內(nèi)核緩沖區(qū)。它接受一個(gè)目的緩沖區(qū)(在內(nèi)核空間)、一個(gè)源緩沖區(qū)(從用戶空間)和一個(gè)以字節(jié)定義的長(zhǎng)度。和 copy_to_user 一樣,該函數(shù)在成功時(shí),返回 0 ,否則返回一個(gè)非零數(shù),指出不能復(fù)制的字節(jié)數(shù)。
copy_from_user( to, from, n );
該函數(shù)首先檢查從用戶空間源緩沖區(qū)讀取的能力(通過 access_ok),然后調(diào)用 __copy_from_user,最后調(diào)用 __copy_from_user_ll。從此開始,根據(jù)構(gòu)架,為執(zhí)行從用戶緩沖區(qū)到內(nèi)核緩沖區(qū)的零拷貝(不可用字節(jié))而進(jìn)行一個(gè)調(diào)用。優(yōu)化組裝函數(shù)包含管理功能。
----------------------------------------------------------------------------------------------------------------------------------
copy_to_user分析
在學(xué)習(xí)Linux內(nèi)核驅(qū)動(dòng)的時(shí)候,一開始就會(huì)碰到copy_from_user和copy_to_user這兩個(gè)常用的函數(shù)。這兩個(gè)函數(shù)在內(nèi)核使用的非常頻繁,負(fù)責(zé)將數(shù)據(jù)從用戶空間拷貝到內(nèi)核空間以及將數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間。在4年半前初學(xué)Linux內(nèi)核驅(qū)動(dòng)程序的時(shí)候,我只是知道這個(gè)怎么用,并沒有很深入的分析這兩個(gè)函數(shù)。這次研究?jī)?nèi)核模塊掛載的時(shí)候,又碰到了它們。決定還是認(rèn)真跟蹤一下函數(shù)。首先這兩個(gè)函數(shù)的原型在arch/arm/include/asm/uaccess.h文件中:
這兩個(gè)函數(shù)從結(jié)構(gòu)上來分析,其實(shí)都可以分為兩個(gè)部分: 1、首先檢查用戶空間的地址指針是否有效(難點(diǎn)) 2、調(diào)用__copy_from_user和__copy_to_user函數(shù)
在這個(gè)分析中,我們先易后難。首先看看具體數(shù)據(jù)拷貝功能的__copy_from_user和__copy_to_user函數(shù)
對(duì)于ARM構(gòu)架,沒有單獨(dú)實(shí)現(xiàn)這兩個(gè)函數(shù),所以他們的代碼位于include/asm-generic/uaccess.h
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 好了如何拷貝數(shù)據(jù)我們已經(jīng)了解了,現(xiàn)在我們來看看前面的用戶空間指針檢測(cè)函數(shù)access_ok,這其實(shí)是一個(gè)宏定義,位于arch/arm/include/asm/uaccess.h文件中:
現(xiàn)在我們來仔細(xì)分析__range_ok這個(gè)宏:
(1)unsigned long flag, roksum;\\定義兩個(gè)變量
(2)__chk_user_ptr(addr);\\定義是一個(gè)空函數(shù)
? ? ?但是這個(gè)函數(shù)涉及到__CHECKER__宏的判斷,__CHECKER__宏在通過Sparse(Semantic Parser for C)工具對(duì)內(nèi)核代碼進(jìn)行檢查時(shí)會(huì)定義的。在使用make C=1或C=2時(shí)便會(huì)調(diào)用該工具,這個(gè)工具可以檢查在代碼中聲明了sparse所能檢查到的相關(guān)屬性的內(nèi)核函數(shù)和變量。
? ? 如果定義了__CHECKER__,在網(wǎng)上的資料中這樣解釋的:__chk_user_ptr和__chk_io_ptr在這里只聲明函數(shù),沒有函數(shù)體,目的就是在編譯過程中Sparse能夠捕捉到編譯錯(cuò)誤,檢查參數(shù)的類型。
? ? 如果沒有定義__CHECKER__,這就是一個(gè)空函數(shù)。
(3)接下來的匯編,我適當(dāng)?shù)胤g如下:
? ? ?adds %1, %2, %3
roksum =?addr +?size 這個(gè)操作影響狀態(tài)位(目的是影響是進(jìn)位標(biāo)志C)
以下的兩個(gè)指令都帶有條件CC,也就是當(dāng)C=0的時(shí)候才執(zhí)行。
如果上面的加法指令進(jìn)位了(C=1),則以下的指令都不執(zhí)行,flag就為初始值current_thread_info()->addr_limit(非零值),并返回。
如果沒有進(jìn)位(C=0),就執(zhí)行下面的指令
? ? sbcccs %1, %1, %0?
roksum =?roksum -?flag,也就是(addr +?size)- (current_thread_info()->addr_limit),操作影響符號(hào)位。
如果(addr +?size)>=(current_thread_info()->addr_limit),則C=1
如果(addr +?size)<(current_thread_info()->addr_limit),則C=0
當(dāng)C=0的時(shí)候執(zhí)行以下指令,否則跳過(flag非零)。 ? ??movcc %0, #0 ? ? flag = 0,給flag賦值0
?(4)flag;?
? ? 返回flag值
綜上所訴:__range_ok宏其實(shí)等價(jià)于:
如果(addr +?size)>=(current_thread_info()->addr_limit),返回非零值
如果(addr +?size)<(current_thread_info()->addr_limit),返回零
而access_ok就是檢驗(yàn)將要操作的用戶空間的地址范圍是否在當(dāng)前進(jìn)程的用戶地址空間限制中。這個(gè)宏的功能很簡(jiǎn)單,完全可以用C實(shí)現(xiàn),不是必須使用匯編。個(gè)人理解:由于這兩個(gè)函數(shù)使用頻繁,就使用匯編來實(shí)現(xiàn)部分功能來增加效率。
? ??從這里再次可以認(rèn)識(shí)到,copy_from_user與copy_to_user的使用是結(jié)合進(jìn)程上下文的,因?yàn)樗麄円L問“user”的內(nèi)存空間,這個(gè)“user”必須是某個(gè)特定的進(jìn)程。通過上面的源碼就知道,其中使用了current_thread_info()來檢查空間是否可以訪問。如果在驅(qū)動(dòng)中使用這兩個(gè)函數(shù),必須是在實(shí)現(xiàn)系統(tǒng)調(diào)用的函數(shù)中使用,不可在實(shí)現(xiàn)中斷處理的函數(shù)中使用。如果在中斷上下文中使用了,那代碼就很可能操作了根本不相關(guān)的進(jìn)程地址空間。
? ? 其次由于操作的頁(yè)面可能被換出,這兩個(gè)函數(shù)可能會(huì)休眠,所以同樣不可在中斷上下文中使用。
用戶進(jìn)程傳來的地址是虛擬地址,這段虛擬地址可能還未真正分配對(duì)應(yīng)的物理地址。對(duì)于用戶進(jìn)程訪問虛擬地址,如果還未分配物理地址,就會(huì)觸發(fā)內(nèi)核缺頁(yè)異常,接著內(nèi)核會(huì)負(fù)責(zé)分配物理地址,并修改映射頁(yè)表。這個(gè)過程對(duì)于用戶進(jìn)程是完全透明的。但是在內(nèi)核空間發(fā)生缺頁(yè)時(shí),必須顯式處理,否則會(huì)導(dǎo)致內(nèi)核oops。
-----------------------------------------------------------------------------------
關(guān)于access_ok中的檢查如果(addr +?size)<(current_thread_info()->addr_limit),返回零,閱讀如下內(nèi)容有助于理解
#include?<linux/kernel.h>
#include?<linux/module.h>
#include?<linux/init.h>
#include?<linux/fs.h>
#include?<linux/string.h>
#include?<linux/mm.h>
#include?<linux/syscalls.h>
#include?<asm/unistd.h>
#include?<asm/uaccess.h>
#define?MY_FILE?"/root/LogFile"
char?buf[128];
struct?file?*file?=?NULL;
static?int?__init init(void)
{
????????mm_segment_t old_fs;
????????printk("Hello, I'm the module that intends to write messages to file.\n");
????????if(file?==?NULL)
????????????????file?=?filp_open(MY_FILE,?O_RDWR?|?O_APPEND?|?O_CREAT,?0644);
????????if?(IS_ERR(file))?{
????????????????printk("error occured while opening file %s, exiting...\n",?MY_FILE);
????????????????return?0;
????????}
????????sprintf(buf,"%s",?"The Messages.");
????????old_fs?=?get_fs();
????????set_fs(KERNEL_DS);
????????file->f_op->write(file,?(char?*)buf,?sizeof(buf),?&file->f_pos);
????????set_fs(old_fs);
????????return?0;
}
static?void?__exit fini(void)
{
????????if(file?!=?NULL)
????????????????filp_close(file,?NULL);
}
module_init(init);
module_exit(fini);
MODULE_LICENSE("GPL");
其中:
????typedef?struct?{
?????????unsigned?long?seg;
????}?mm_segment_t;
????#define?KERNEL_DS????MAKE_MM_SEG(0xFFFFFFFFUL)
????#define?MAKE_MM_SEG(s)????((mm_segment_t)?{?(s)?})
基本思想:
???一個(gè)是要記得編譯的時(shí)候加上-D__KERNEL_SYSCALLS__ ??
? 另外源文件里面要#include ? <linux/unistd.h> ??
? 如果報(bào)錯(cuò),很可能是因?yàn)槭褂玫木彌_區(qū)超過了用戶空間的地址范圍。一般系統(tǒng)調(diào)用會(huì)要求你使用的緩沖區(qū)不能在內(nèi)核區(qū)。這個(gè)可以用set_fs()、get_fs()來解決。在讀寫文件前先得到當(dāng)前fs: ??
? mm_segment_t ? old_fs=get_fs(); ??
? 并設(shè)置當(dāng)前fs為內(nèi)核fs:set_fs(KERNEL_DS); ??
? 在讀寫文件后再恢復(fù)原先f(wàn)s: ? set_fs(old_fs); ??
? set_fs()、get_fs()等相關(guān)宏在文件include/asm/uaccess.h中定義。 ??
? 個(gè)人感覺這個(gè)辦法比較簡(jiǎn)單。 ??
? ??
? 另外就是用flip_open函數(shù)打開文件,得到struct file *的指針fp。使用指針fp進(jìn)行相應(yīng)操作,如讀文件可以用fp->f_ops->read。最后用filp_close()函數(shù)關(guān)閉文件。 filp_open()、filp_close()函數(shù)在fs/open.c定義,在include/linux/fs.h中聲明。??
解釋一點(diǎn):
????系 統(tǒng)調(diào)用本來是提供給用戶空間的程序訪問的,所以,對(duì)傳遞給它的參數(shù)(比如上面的buf),它默認(rèn)會(huì)認(rèn)為來自用戶空間,在->write()函數(shù)中, 為了保護(hù)內(nèi)核空間,一般會(huì)用get_fs()得到的值來和USER_DS進(jìn)行比較,從而防止用戶空間程序“蓄意”破壞內(nèi)核空間;
?? 而現(xiàn)在要在內(nèi)核空間使用系統(tǒng)調(diào)用,此時(shí)傳遞給->write()的參數(shù)地址就是內(nèi)核空間的地址了,在USER_DS之上(USER_DS ~ KERNEL_DS),如果不做任何其它處理,在write()函數(shù)中,會(huì)認(rèn)為該地址超過了USER_DS范圍,所以會(huì)認(rèn)為是用戶空間的“蓄意破壞”,從 而不允許進(jìn)一步的執(zhí)行; 為了解決這個(gè)問題; set_fs(KERNEL_DS);將其能訪問的空間限制擴(kuò)大到KERNEL_DS,這樣就可以在內(nèi)核順利使用系統(tǒng)調(diào)用了!
補(bǔ)充:
??? 我看了一下源碼,在include/asm/uaccess.h中,有如下定義:?
????#define MAKE_MM_SEG(s) ((mm_segment_t) { (s) })?
??? #define KERNEL_DS MAKE_MM_SEG(0xFFFFFFFF)?
??? #define USER_DS MAKE_MM_SEG(PAGE_OFFSET)?
??? #define get_ds() (KERNEL_DS)?
??? #define get_fs() (current->addr_limit)?
??? #define set_fs(x) (current->addr_limit = (x))?
而它的注釋也很清楚:?
/*?
* The fs value determines whether argument validity checking should be?
* performed or not. If get_fs() == USER_DS, checking is performed, with?
* get_fs() == KERNEL_DS, checking is bypassed.?
*?
* For historical reasons, these macros are grossly misnamed.?
*/?
因此可以看到,fs的值是作為是否進(jìn)行參數(shù)檢查的標(biāo)志。系統(tǒng)調(diào)用的參數(shù)要求必須來自用戶空間,所以,當(dāng)在內(nèi)核中使用系統(tǒng)調(diào)用的時(shí)候,set_fs(get_ds())改變了用戶空間的限制,即擴(kuò)大了用戶空間范圍,因此即可使用在內(nèi)核中的參數(shù)了
----------------------------------------------------------------------------------------------------
下文介紹了內(nèi)核處理缺頁(yè)中斷的機(jī)制。 利用異常表處理 Linux 內(nèi)核態(tài)缺頁(yè)異常
前言
在程序的執(zhí)行過程中,因?yàn)橛龅侥撤N障礙而使 CPU 無法最終訪問到相應(yīng)的物理內(nèi)存單元,即無法完成從虛擬地址到物理地址映射的時(shí)候,CPU 會(huì)產(chǎn)生一次缺頁(yè)異常,從而進(jìn)行相應(yīng)的缺頁(yè)異常處理。基于 CPU 的這一特性,Linux 采用了請(qǐng)求調(diào)頁(yè)(Demand Paging)和寫時(shí)復(fù)制(Copy On Write)的技術(shù)
1. 請(qǐng)求調(diào)頁(yè)是一種動(dòng)態(tài)內(nèi)存分配技術(shù),它把頁(yè)框的分配推遲到不能再推遲為止。這種技術(shù)的動(dòng)機(jī)是:進(jìn)程開始運(yùn)行的時(shí)候并不訪問地址空間中的全部?jī)?nèi)容。事實(shí)上,有一部分地址也許永遠(yuǎn)也不會(huì)被進(jìn)程所使用。程序的局部性原理也保證了在程序執(zhí)行的每個(gè)階段,真正使用的進(jìn)程頁(yè)只有一小部分,對(duì)于臨時(shí)用不到的頁(yè),其所在的頁(yè)框可以由其它進(jìn)程使用。因此,請(qǐng)求分頁(yè)技術(shù)增加了系統(tǒng)中的空閑頁(yè)框的平均數(shù),使內(nèi)存得到了很好的利用。從另外一個(gè)角度來看,在不改變內(nèi)存大小的情況下,請(qǐng)求分頁(yè)能夠提高系統(tǒng)的吞吐量。當(dāng)進(jìn)程要訪問的頁(yè)不在內(nèi)存中的時(shí)候,就通過缺頁(yè)異常處理將所需頁(yè)調(diào)入內(nèi)存中。
2. 寫時(shí)復(fù)制主要應(yīng)用于系統(tǒng)調(diào)用fork,父子進(jìn)程以只讀方式共享頁(yè)框,當(dāng)其中之一要修改頁(yè)框時(shí),內(nèi)核才通過缺頁(yè)異常處理程序分配一個(gè)新的頁(yè)框,并將頁(yè)框標(biāo)記為可寫。這種處理方式能夠較大的提高系統(tǒng)的性能,這和Linux創(chuàng)建進(jìn)程的操作過程有一定的關(guān)系。在一般情況下,子進(jìn)程被創(chuàng)建以后會(huì)馬上通過系統(tǒng)調(diào)用execve將一個(gè)可執(zhí)行程序的映象裝載進(jìn)內(nèi)存中,此時(shí)會(huì)重新分配子進(jìn)程的頁(yè)框。那么,如果fork的時(shí)候就對(duì)頁(yè)框進(jìn)行復(fù)制的話,顯然是很不合適的。
在上述的兩種情況下出現(xiàn)缺頁(yè)異常,進(jìn)程運(yùn)行于用戶態(tài),異常處理程序可以讓進(jìn)程從出現(xiàn)異常的指令處恢復(fù)執(zhí)行,使用戶感覺不到異常的發(fā)生。當(dāng)然,也會(huì)有異常無法正常恢復(fù)的情況,這時(shí),異常處理程序會(huì)進(jìn)行一些善后的工作,并結(jié)束該進(jìn)程。也就是說,運(yùn)行在用戶態(tài)的進(jìn)程如果出現(xiàn)缺頁(yè)異常,不會(huì)對(duì)操作系統(tǒng)核心的穩(wěn)定性造成影響。 那么對(duì)于運(yùn)行在核心態(tài)的進(jìn)程如果發(fā)生了無法正常恢復(fù)的缺頁(yè)異常,應(yīng)該如何處理呢?是否會(huì)導(dǎo)致系統(tǒng)的崩潰呢?是否能夠解決好內(nèi)核態(tài)缺頁(yè)異常對(duì)于操作系統(tǒng)核心的穩(wěn)定性來說會(huì)產(chǎn)生很大的影響,如果一個(gè)誤操作就會(huì)造成系統(tǒng)的Oops,這對(duì)于用戶來說顯然是不能容忍的 。本文正是針對(duì)這個(gè)問題,介紹了一種Linux內(nèi)核中所采取的解決方法。
在讀者繼續(xù)往下閱讀之前,有一點(diǎn)需要先說明一下,本文示例中所選的代碼取自于Linux-2.4.0,編譯環(huán)境是gcc-2.96,objdump的版本是2.11.93.0.2,具體的版本信息可以通過以下的命令進(jìn)行查詢:
$ gcc -v
Reading specs from /usr/lib/gcc-lib/i386-redhat-linux/2.96/specs
gcc version 2.96 20000731 (Red Hat Linux 7.3 2.96-110)
$ objdump -v
GNU objdump 2.11.93.0.2 20020207
Copyright 2002 Free Software Foundation, Inc.
回頁(yè)首
GCC的擴(kuò)展功能
由于本文中會(huì)用到GCC的擴(kuò)展功能,即匯編器as中提供的.section偽操作,在文章開始之前我再作一個(gè)簡(jiǎn)要的介紹。此偽操作對(duì)于不同的可執(zhí)行文件格式有不同的解釋,我也不一一列舉,僅對(duì)我們所感興趣的Linux中常用的ELF格式的用法加以描述,其指令格式如下:
.section NAME[, "FLAGS"]
大家所熟知的C程序一般由以下的幾個(gè)部分組成:代碼段(text section)、初始化數(shù)據(jù)段(data section)、非初始化數(shù)據(jù)段(bss section)、棧(heap)以及堆(stack),具體的地址空間布局可以參考《UNIX環(huán)境高級(jí)編程》一書。
在Linux內(nèi)核中,通過使用.section的偽操作,可以把隨后的代碼匯編到一個(gè)由NAME指定的段中。而FLAGS字段則說明了該段的屬性,它可以用下面介紹的單個(gè)字符來表示,也可以是多個(gè)字符的組合。
'a' 可重定位的段
'w' 可寫段
'x' 可執(zhí)行段
'W' 可合并的段
's' 共享段
舉個(gè)例子來說明,讀者在后面會(huì)看到的:.section .fixup, "ax"
這樣的一條指令定義了一個(gè)名為.fixup的段,隨后的指令會(huì)被加入到這個(gè)段中,該段的屬性是可重定位并可執(zhí)行。
回頁(yè)首
內(nèi)核缺頁(yè)異常處理
運(yùn)行在核心態(tài)的進(jìn)程經(jīng)常需要訪問用戶地址空間的內(nèi)容,但是誰(shuí)都無法保證內(nèi)核所得到的這些從用戶空間傳入的地址信息是"合法"的。為了保護(hù)內(nèi)核不受錯(cuò)誤信息的攻擊,需要驗(yàn)證這些從用戶空間傳入的地址信息的正確性。
在老版本的Linux中,這個(gè)工作是通過函數(shù)verify_area來完成的:
extern inline int verify_area(int type, const void * addr, unsigned long size)
該函數(shù)驗(yàn)證了是否可以以type中說明的訪問類型(read or write)訪問從地址addr開始、大小為size的一塊虛擬存儲(chǔ)區(qū)域。為了做到這一點(diǎn),verify_read首先需要找到包含地址addr的虛擬存儲(chǔ)區(qū)域(vma)。一般的情況下(正確運(yùn)行的程序)這個(gè)測(cè)試都會(huì)成功返回,在少數(shù)情況下才會(huì)出現(xiàn)失敗的情況。也就是說,大部分的情況下內(nèi)核在一些無用的驗(yàn)證操作上花費(fèi)了不算短的時(shí)間,這從操作系統(tǒng)運(yùn)行效率的角度來說是不可接受的。
為了解決這個(gè)問題,現(xiàn)在的Linux設(shè)計(jì)中將驗(yàn)證的工作交給虛存中的硬件設(shè)備來完成。當(dāng)系統(tǒng)啟動(dòng)分頁(yè)機(jī)制以后,如果一條指令的虛擬地址所對(duì)應(yīng)的頁(yè)框(page frame)不在內(nèi)存中或者訪問的類型有錯(cuò)誤,就會(huì)發(fā)生缺頁(yè)異常。處理器把引起缺頁(yè)異常的虛擬地址裝到寄存器CR2中,并提供一個(gè)出錯(cuò)碼,指示引起缺頁(yè)異常的存儲(chǔ)器訪問的類型,隨后調(diào)用Linux的缺頁(yè)異常處理函數(shù)進(jìn)行處理。
Linux中進(jìn)行缺頁(yè)異常處理的函數(shù)如下:
asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
……………………
__asm__("movl %%cr2,%0":"=r" (address));
……………………
vma = find_vma(mm, address);
if (!vma)
goto bad_area;
if (vma->vm_start <= address)
goto good_area;
if (!(vma->vm_flags & VM_GROWSDOWN))
goto bad_area;
if (error_code & 4) {
if (address + 32 < regs->esp)
goto bad_area;
……………………
bad_area:
……………………
no_context:
/* Are we prepared to handle this kernel fault? ?*/
if ((fixup = search_exception_table(regs->eip)) != 0) {
regs->eip = fixup;
return;
}
………………………
}
首先讓我們來看看傳給這個(gè)函數(shù)調(diào)用的兩個(gè)參數(shù):它們都是通過entry.S在堆棧中建立的(arch/i386/kernel/entry.S),參數(shù)regs指向保存在堆棧中的寄存器,error_code中存放著異常的出錯(cuò)碼,具體的堆棧布局參見圖一(堆棧的生成過程請(qǐng)參考《Linux內(nèi)核源代碼情景分析》一書)
該函數(shù)首先從CPU的控制寄存器CR2中獲取出現(xiàn)缺頁(yè)異常的虛擬地址。由于缺頁(yè)異常處理程序需要處理的缺頁(yè)異常類型很多,分支也很復(fù)雜。基于本文的主旨,我們只關(guān)心以下的幾種內(nèi)核缺頁(yè)異常處理的情況:
1. 程序要訪問的內(nèi)核地址空間的內(nèi)容不在內(nèi)存中,先跳轉(zhuǎn)到標(biāo)號(hào)vmalloc_fault,如果當(dāng)前訪問的內(nèi)容所對(duì)應(yīng)的頁(yè)目錄項(xiàng)不在內(nèi)存中,再跳轉(zhuǎn)到標(biāo)號(hào)no_context;
2. 缺頁(yè)異常發(fā)生在中斷或者內(nèi)核線程中,跳轉(zhuǎn)到標(biāo)號(hào)no_context;
3. 程序在核心態(tài)運(yùn)行時(shí)訪問用戶空間的數(shù)據(jù),被訪問的數(shù)據(jù)不在內(nèi)存中
a) 出現(xiàn)異常的虛擬地址在進(jìn)程的某個(gè)vma中,但是系統(tǒng)內(nèi)存無法分配空閑頁(yè)框(page frame),則先跳轉(zhuǎn)到標(biāo)號(hào)out_of_memory,再跳轉(zhuǎn)到標(biāo)號(hào)no_context;
b) 出現(xiàn)異常的虛擬地址不屬于進(jìn)程任一個(gè)vma,而且不屬于堆棧擴(kuò)展的范疇,則先跳轉(zhuǎn)到標(biāo)號(hào)bad_area,最終也是到達(dá)標(biāo)號(hào)no_context。
從上面的這幾種情況來看,我們關(guān)注的焦點(diǎn)最后集中到標(biāo)號(hào)no_context處,即對(duì)函數(shù)search_exception_table的調(diào)用。這個(gè)函數(shù)的作用就是通過發(fā)生缺頁(yè)異常的指令(regs->eip)在異常表(exception table)中尋找下一條可以繼續(xù)運(yùn)行的指令(fixup)。這里提到的異常表包含一些地址對(duì),地址對(duì)中的前一個(gè)地址表示出現(xiàn)異常的指令的地址,后一個(gè)表示當(dāng)前一個(gè)指令出現(xiàn)錯(cuò)誤時(shí),程序可以繼續(xù)得以執(zhí)行的修復(fù)地址。
如果這個(gè)查找操作成功的話,缺頁(yè)異常處理程序?qū)⒍褩V械姆祷氐刂?#xff08;regs->eip)修改成修復(fù)地址并返回,隨后,發(fā)生異常的進(jìn)程將按照fixup中安排好的指令繼續(xù)執(zhí)行下去。當(dāng)然,如果無法找到與之匹配的修復(fù)地址,系統(tǒng)只有打印出出錯(cuò)信息并停止運(yùn)作。
那么,這個(gè)所謂的修復(fù)地址又是如何生成的呢?是系統(tǒng)自動(dòng)生成的嗎?答案當(dāng)然是否定的,這些修復(fù)指令都是編程人員通過as提供的擴(kuò)展功能寫進(jìn)內(nèi)核源碼中的。下面我們就來分析一下其實(shí)現(xiàn)機(jī)制。
回頁(yè)首
異常表的實(shí)現(xiàn)機(jī)制
筆者取include/asm-i386/uaccess.h中的宏定義__copy_user編寫了一段程序作為例子加以講解。
/* hello.c */
#include <stdio.h>
#include <string.h>
#define __copy_user(to,from,size) \
do { \
int __d0, __d1;\
__asm__ __volatile__(\
"0: rep; movsl\n"\
" movl %3,%0\n"\
"1: rep; movsb\n"\
"2:\n" ?\
".section .fixup,\"ax\"\n"\
"3: lea 0(%3,%0,4),%0\n"\
" jmp 2b\n"\
".previous\n" ?\
".section __ex_table,\"a\"\n"\
" .align 4\n"? ? ? ?\
" .long 0b,3b\n"\
" .long 1b,2b\n"\
".previous" ?\
: "=&c"(size), "=&D" (__d0), "=&S" (__d1)\
: "r"(size & 3), "0"(size / 4), "1"(to), "2"(from)\
: "memory"); ?\
} while (0)
int main(void)
{
const char*string = "Hello, world!";
char buf[20];
unsigned long ?n, m;
m = n = strlen(string);
__copy_user(buf, string, n);
buf[m] = '\0';
printf("%s\n", buf);
exit(0);
}
先看看本程序的執(zhí)行結(jié)果:
$ gcc hello.c -o hello
$ ./hello
Hello, world!
顯然,這就是一個(gè)簡(jiǎn)單的"hello world"程序,那為什么要寫得這么復(fù)雜呢?程序中的一大段匯編代碼在內(nèi)核中才能體現(xiàn)出其價(jià)值,筆者將其加入到上面的程序中,是為了后面的分析而準(zhǔn)備的。
系統(tǒng)在核心態(tài)運(yùn)行的時(shí)候,參數(shù)是通過寄存器來傳遞的,由于寄存器所能夠傳遞的信息有限,所以傳遞的參數(shù)大多數(shù)是指針。要使用指針?biāo)赶虻母髩K的數(shù)據(jù),就需要將用戶空間的數(shù)據(jù)拷貝到系統(tǒng)空間來。上面的__copy_user在內(nèi)核中正是扮演著這樣的一個(gè)拷貝數(shù)據(jù)的角色,當(dāng)然,內(nèi)核中這樣的宏定義還很多,筆者也只是取其中的一個(gè)來講解,讀者如果感興趣的話可以看完本文以后自行學(xué)習(xí)。
如果讀者對(duì)于簡(jiǎn)單的嵌入式匯編還不是很了解的話,可以參考《Linux內(nèi)核源代碼情景分析》一書。下面我們將程序編譯成匯編程序來加以分析:
$ gcc -S hello.c
/* hello.s */
movl -60(%ebp), %eax
andl $3, %eax
movl -60(%ebp), %edx
movl %edx, %ecx
shrl $2, %ecx
leal -56(%ebp), %edi
movl -12(%ebp), %esi
#APP
0: rep; movsl
movl %eax,%ecx
1: rep; movsb
2:
.section .fixup,"ax"
3: lea 0(%eax,%ecx,4),%ecx
jmp 2b
.previous
.section __ex_table,"a"
.align 4
.long 0b,3b
.long 1b,2b
.previous
#NO_APP
movl %ecx, %eax
從上面通過gcc生成的匯編程序中,我們可以很容易的找到訪問用戶地址空間的指令,也就是程序中的標(biāo)號(hào)為0和1的兩條語(yǔ)句。而程序中偽操作.section的作用就是定義了.fixup和__ex_table這樣的兩個(gè)段,那么這兩段在可執(zhí)行程序中又是如何安排的呢?下面就通過objdump給讀者一個(gè)直觀的概念:
? ? ? ? ? ? ? ? ? $ objdump --section-headers hello
hello: ? ? file format elf32-i386
Sections:
Idx Name ? ? ? ? ?Size ? ? ?VMA ? ? ? LMA ? ? ? File off ?Algn
? 0 .interp ? ? ? 00000013 ?080480f4 ?080480f4 ?000000f4 ?2**0
? ? ? ? ? ? ? ? ? CONTENTS, ALLOC, LOAD, READONLY, DATA
………………………………
? 9 .init ? ? ? ? 00000018 ?080482e0 ?080482e0 ?000002e0 ?2**2
? ? ? ? ? ? ? ? ? CONTENTS, ALLOC, LOAD, READONLY, CODE
?10 .plt ? ? ? ? ?00000070 ?080482f8 ?080482f8 ?000002f8 ?2**2
? ? ? ? ? ? ? ? ? CONTENTS, ALLOC, LOAD, READONLY, CODE
?11 .text ? ? ? ? 000001c0 ?08048370 ?08048370 ?00000370 ?2**4
? ? ? ? ? ? ? ? ? CONTENTS, ALLOC, LOAD, READONLY, CODE
?12 .fixup ? ? ? ?00000009 ?08048530 ?08048530 ?00000530 ?2**0
? ? ? ? ? ? ? ? ? CONTENTS, ALLOC, LOAD, READONLY, CODE
?13 .fini ? ? ? ? 0000001e ?0804853c ?0804853c ?0000053c ?2**2
? ? ? ? ? ? ? ? ? CONTENTS, ALLOC, LOAD, READONLY, CODE
?14 .rodata ? ? ? 00000019 ?0804855c ?0804855c ?0000055c ?2**2
? ? ? ? ? ? ? ? ? CONTENTS, ALLOC, LOAD, READONLY, DATA
?15 __ex_table ? ?00000010 ?08048578 ?08048578 ?00000578 ?2**2
? ? ? ? ? ? ? ? ? CONTENTS, ALLOC, LOAD, READONLY, DATA
?16 .data ? ? ? ? 00000010 ?08049588 ?08049588 ?00000588 ?2**2
? ? ? ? ? ? ? ? ? CONTENTS, ALLOC, LOAD, DATA
? ? ? ? ? ? ? ? ? CONTENTS, READONLY
………………………………
?26 .note ? ? ? ? 00000078 ?00000000 ?00000000 ?0000290d ?2**0
? ? ? ? ? ? ? ? ? CONTENTS, READONLY
上面通過objdump顯示出來的可執(zhí)行程序的頭部信息中,有一些是讀者所熟悉的,例如.text、.data以及被筆者省略掉的.bss,而我們所關(guān)心的是12和15,也就是.fixup和__ex_table。對(duì)照hello.s中段的定義來看,兩個(gè)段聲明中的FLAGS字段分別為'ax'和'a',而objdump的結(jié)果顯示,.fixup段是可重定位的代碼段,__ex_table段是可重定位的數(shù)據(jù)段,兩者是吻合的。
那么為什么要通過.section定義獨(dú)立的段呢?為了解開這個(gè)問題的答案,我們需要進(jìn)一步看看我們所寫的代碼在可執(zhí)行文件中是如何表示的。
$objdump --disassemble --section=.text hello
hello: ? ? file format elf32-i386
Disassembly of section .text:
8048498: 8b 45 c4 ? ? ? ? ? ??mov ? ? 0xffffffc4(%ebp),%eax
804849b: 83 e0 03 ? ? ? ? ? ??and ? ??$0x3,%eax
804849e: 8b 55 c4 ? ? ? ? ? ??mov ? ? 0xffffffc4(%ebp),%edx
80484a1: 89 d1 ? ? ? ? ? ? ??mov ? ? %edx,%ecx
80484a3: c1 e9 02 ? ? ? ? ? ??shr ? ??$0x2,%ecx
80484a6: 8d 7d c8 ? ? ? ? ? ??lea ? ??0xffffffc8(%ebp),%edi
80484a9: 8b 75 f4 ? ? ? ? ? ??mov ? ? 0xfffffff4(%ebp),%esi
80484ac: f3 a5 ? ? ? ? ? ? ??repz movsl?%ds:(%esi),%es:(%edi)
80484ae: 89 c1 ? ? ? ? ? ? ??mov ? ? %eax,%ecx
80484b0: f3 a4 ? ? ? ? ? ? ??repz movsb?%ds:(%esi),%es:(%edi)
80484b2: 89 c8 ? ? ? ? ? ? ??mov ? ? %ecx,%eax
前面的hello.s中的匯編片斷在可執(zhí)行文件中就是通過上面的11條指定來表達(dá),讀者也許會(huì)問,由.section偽操作定義的段怎么不見了?別著急,慢慢往下看,由.section偽操作定義的段并不在正常的程序執(zhí)行路徑上,它們是被安排在可執(zhí)行文件的其它地方了:
$objdump --disassemble --section=.fixup hello
hello: ? ? file format elf32-i386
Disassembly of section .fixup:
08048530 <.fixup>:
8048530: 8d 4c 88 00 ? ? ? ? ?lea ? ?0x0(%eax,%ecx,4),%ecx
8048534: e9 79 ff ff ff ? ? ??jmp ? ?80484b2 <main+0x42>
由此可見,.fixup是作為一個(gè)單獨(dú)的段出現(xiàn)在可執(zhí)行程序中的,而此段中所包含的語(yǔ)句則正好是和源程序hello.c中的兩條語(yǔ)句相對(duì)應(yīng)的。
將.fixup段和.text段獨(dú)立開來的目的是為了提高CPU流水線的利用率。熟悉體系結(jié)構(gòu)的讀者應(yīng)該知道,當(dāng)前的CPU引入了流水線技術(shù)來加快指令的執(zhí)行,即在執(zhí)行當(dāng)前指令的同時(shí),要將下面的一條甚至多條指令預(yù)取到流水線中。這種技術(shù)在面對(duì)程序執(zhí)行分支的時(shí)候遇到了問題:如果預(yù)取的指令并不是程序下一步要執(zhí)行的分支,那么流水線中的所有指令都要被排空,這對(duì)系統(tǒng)的性能會(huì)產(chǎn)生一定的影響。在我們的這個(gè)程序中,如果將.fixup段的指令安排在正常執(zhí)行的.text段中,當(dāng)程序執(zhí)行到前面的指令時(shí),這幾條很少執(zhí)行的指令會(huì)被預(yù)取到流水線中,正常的執(zhí)行必然會(huì)引起流水線的排空操作,這顯然會(huì)降低整個(gè)系統(tǒng)的性能。
下面我們就可以看到異常表是如何形成的了:
$objdump --full-contents --section=__ex_table hello
hello: ? ? file format elf32-i386
Contents of section __ex_table:
8048578 ac840408 30850408 b0840408 b2840408 ?....0...........
由于x86使用小尾端的編址方式,上面的這段數(shù)據(jù)比較凌亂。讓我把上面的__ex_table中的內(nèi)容轉(zhuǎn)變成大家通常看到的樣子,相信會(huì)更容易理解一些:
8048578 80484ac 8048530 80484b0 80484b2 ?....0...........
上面的紅色部分就是我們最感興趣的地方,而這段數(shù)據(jù)是如何形成的呢?將前面objdump生成的可執(zhí)行程序中的匯編語(yǔ)句和hello.c中的源程序結(jié)合起來看,就可以發(fā)現(xiàn)一些有趣的東西了!
先讓我們回頭看看hello.c中__ex_table段的語(yǔ)句 .long 0b,3b。其中標(biāo)簽0b(b代表backward,即往回的標(biāo)簽0)是可能出現(xiàn)異常的指令的地址。結(jié)合objdump生成的可執(zhí)行程序.text段的匯編語(yǔ)句可以知道標(biāo)簽0就是80484ac:
原始的匯編語(yǔ)句: 0: ?rep; movsl
鏈接到可執(zhí)行程序后: 80484ac:f3 a5 repz movsl %ds:(%esi),%es:(%edi)
而標(biāo)簽3就是處理異常的指令的地址,在我們的這個(gè)例子中就是80484b0:
原始的匯編語(yǔ)句: 3: ?lea 0(%eax,%ecx,4),%ecx
鏈接到可執(zhí)行程序后: 8048530:8d 4c 88 00 lea 0x0(%eax,%ecx,4),%ecx
因此,相應(yīng)的匯編語(yǔ)句
.section __ex_table,"a"
.align 4
.long 0b,3b
就變成了: 8048578 80484ac 8048530 …………
這樣,異常表中的地址對(duì)(80484ac,8048530)就誕生了,而對(duì)于地址對(duì)(80484b0 80484b2)的生成,情況相同,不再贅述。
讀到這兒了,有一件事要告訴讀者的是,其實(shí)例子中異常表的安排在用戶空間是不會(huì)得到執(zhí)行的。當(dāng)運(yùn)行在用戶態(tài)的進(jìn)程訪問到標(biāo)簽0處的指令出現(xiàn)缺頁(yè)異常時(shí),do_page_fault只會(huì)將該指令對(duì)應(yīng)的進(jìn)程頁(yè)調(diào)入內(nèi)存中,使指令能夠重新正確執(zhí)行,或者直接就殺死該進(jìn)程,并不會(huì)到達(dá)函數(shù)search_exception_table處。
也許有的讀者會(huì)問了,既然不執(zhí)行,前面的例子和圍繞例子所展開的討論又有什么作用呢?大家大可打消這樣的疑慮,我們前面的分析并沒有白費(fèi),因?yàn)檎嬲膬?nèi)核異常表中地址對(duì)的生成機(jī)制和前面講述的原理是完全一樣的,筆者通過一個(gè)運(yùn)行在用戶空間的程序來講解也是希望讓讀者能夠更加容易的理解異常表的機(jī)制,不至于陷入到內(nèi)核源碼的汪洋大海中去。現(xiàn)在,我們可以自己通過objdump工具查看一下內(nèi)核中的異常表:
$objdump --full-contents --section=__ex_table vmlinux
vmlinux: ? ? file format elf32-i386
Contents of section __ex_table:
c024ac80 e36d10c0 e66d10c0 8b7110c0 6c7821c0
……………………
做一下轉(zhuǎn)化:
c024ac80 c0106de3 c0106de6 c010718b c021786c
上面的vmlinux就是編譯內(nèi)核所生成的內(nèi)核可執(zhí)行程序。和本文給出的例子相比,唯一的不同就是此時(shí)的地址對(duì)中的異常指令地址和修復(fù)地址都是內(nèi)核空間的虛擬地址。也正是在內(nèi)核中,異常表才真正發(fā)揮著它應(yīng)有的作用。
回頁(yè)首
總結(jié)
下面我對(duì)前面所講述的內(nèi)容做一個(gè)歸納,希望讀者能夠?qū)?nèi)核缺頁(yè)異常處理有一個(gè)清楚的認(rèn)識(shí):
進(jìn)程訪問內(nèi)核地址空間的"非法"地址c010718b
存儲(chǔ)管理部件(MMU)產(chǎn)生一個(gè)缺頁(yè)異常;
CPU調(diào)用函數(shù)do_page_fault;
do_page_fault調(diào)用函數(shù)search_exception_table(regs->eip == c010718b);
search_exception_table在異常表中查找地址c010718b,并返回地址對(duì)中的修復(fù)地址c021786c;
do_page_fault將堆棧中的返回地址eip修改成c021786c并返回;
代碼按照缺頁(yè)異常處理程序的返回地址繼續(xù)執(zhí)行,也就是從c021786c開始繼續(xù)執(zhí)行。
將驗(yàn)證用戶空間地址信息"合法"性的工作交給硬件來完成(通過缺頁(yè)異常的方式)其實(shí)就是一種Lazy Computation,也就是等到真正出現(xiàn)缺頁(yè)異常的時(shí)候才進(jìn)行處理。通過本文的分析可以看出,這種方法與本文前面所提到的通過verify_area來驗(yàn)證的方法相比,較好的避免了系統(tǒng)在無用驗(yàn)證上的開銷,能夠有效的提高系統(tǒng)的性能。 此外,在分析源碼的過程中讀者會(huì)發(fā)現(xiàn),異常表并不僅僅用在缺頁(yè)異常處理程序中,在通用保護(hù)(General Protection)異常等地方,也同樣用到了這一技術(shù)。
由此可見,異常表是一種廣泛應(yīng)用于Linux內(nèi)核中的異常處理方法。在系統(tǒng)軟件的設(shè)計(jì)中,異常表也應(yīng)該成為一種提高系統(tǒng)穩(wěn)定性的重要手段
總結(jié)
以上是生活随笔為你收集整理的linux copy_from/to_user原理的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: vmalloc 实现
- 下一篇: 宏EXPORT_SYMBOL在内核中的作