標籤:

linux在系統調用進入內核時,為什麼要將參數從用戶空間拷貝到內核空間?不能直接訪問,或是使用memcpy嗎?非要使用copy_from_user才行嗎?


首先,內核不能信任任何用戶空間的指針。必須對用戶空間的指針指向的數據進行驗證。如果只做驗證不做拷貝的話,那麼在隨後的運行中要隨時受到其它進/線程可能修改用戶空間數據的威脅。所以必須做拷貝。(有人提到在 copy 過程中數據依然可以被修改。是的,但是這種修改不能稱為「篡改」。因為這種修改是在「合法性檢查」之前發生的,影響的是用戶進程的正確性,而不是內核對數據的驗證。copy 只保證最後被使用的數據是被驗證的數據,至於有沒有 race 去破壞被傳入的數據本身的正確性不在內核責任之內。要注意,「合法性」不等於「正確性」。)

其次,上面說的是 Linux 的具體實現。Linux 中內核空間和用戶空間共享線性地址,也就是 cr3 不變。但是從邏輯上來說,內核和用戶空間完全可以是不同的兩套地址空間。OS X 就是用這種方式讓 32-bit kernel 運行 64-bit app 的。參見 地址空間劃分(一)。所以從設計角度來說,應該讓代碼能通用兼顧這種邏輯上的考慮。如果考慮考內核和用戶空間不能共享線性地址,那麼數據拷貝就是必要的。


在現代通用操作系統裡面,cpu運行指令時,它的運級別分為用戶態和內核態這兩個態,內核要保護應用程序,不能讓用戶態的數據對內核進行污染。

那用戶態要委託內核完成某個服務時(比如打開文件,訪問文件內容),必須通過系統調用完成。系統調用傳參,跟函數傳參是比較類似的,分為基礎類型和內存塊類型這兩類。

1. 對於基礎類型,通過寄存器可以直接拷貝傳遞

2. 對內存塊類型,C語言沒有語言類型上的支持,必須通過指針進行傳遞,然後再訪問指針指向的內存空間

如果你在Linux下要寫一個字元驅動,必須定義一個file_operations結構,實現該文件的寫操作細節,它的簽名如下:

ssize_t XXX_drviver_write(struct file *filp, const char __user *buf, size_t len, loff_t *ppos)

上述的len參數為基礎類型,而buf就是內存塊類型,你在該函數應該實現將buf指向並且長度為len的緩衝區寫到驅動所表示的文件裡面。

這裡會遇到幾個問題:

1. buf 指針是不是一個合法地址

2. 如果buf 指針是一個合法地地,但是該buf指針的空間,內核還沒有給它分配物理地址空間怎麼辦

3. 如果黑客故意將buf值寫成一個精心構造的內核地址,那驅動需要往該buf拷貝數據時(通過是read操作),那不是將數據寫到內核態了嗎?那黑客就可以通過這個問題來修改內核代碼,控制內核執行,達成目標。

如果直接使用memcpy,上述這3個問題都無法解決。如果遇到的是場景1)和2),那麼內核會Oops,如果是3),則攻擊很可能成功。

copy_from_user和copy_to_user就是用來保證內核態安全地訪問(讀和寫)用戶態內存空間。

copy_from_user/copy_to_user 的實現原理非常簡單,如下:

1. 如果buf空間屬於內核態空間,直接返回出錯,不處理(這是解決上述場景3)

2. copy_from_user/copy_to_user使用精心布置的訪存彙編實現,並指這個彙編指令所在的地址全部登記起來(稱為extable表)。運行時出現上述場景1)和2),首先會發生缺頁異常,進入內核do_page_fault流程;然後檢查出錯的PC地址是不是早已在extable登記好的,如果是,同表示該缺頁異常是copy_from_user/copy_to_user函數產生的。最後才檢查該地址是否為該進程的合法地址,如果是則分配物理頁並處理,否則就是非法地址,把進程給殺死(發送sigsegv信號)。


三個原因:

1. 檢查用戶態的那個地址的地址範圍,避免被傳進一個內核的地址。這可以避免用戶態的惡意程序用這種方法偷內核的信息

2. 和do_page_fault()協同(請注意copy_x_user是一個平台相關代碼),保證pagefault的處理流程可以知道這時缺頁是可接受的(每個平台的方法不同),否則簡單的內核缺頁會導致內核直接崩潰

3. 做一些其他每次和內核交互都要乾的事情,比如最新的Shadow Memory功能,就插在這個流程中

用memcpy當然也是可以的,只是你要自己搞定上面的事情。

補充1:題意理解問題

其他的答案,有不少都理解錯了題意。

原題是這樣描述的:「linux在系統調用進入內核時,為什麼要將參數從用戶空間拷貝到內核空間?不能直接訪問,或是使用memcpy嗎?非要使用copy_from_user才行嗎?」

很明顯,第一個問號才是問題,後面兩個都是補充。他問:「為什麼要把參數從用戶空間拷貝到內核空間」,而不是「直接訪問」,或者用「memcpy"來訪問,卻要」用copy_from_user"訪問。這三個問號明顯是一體的。問的就是如果我用比如ioctl(fd, cmd, arg)做系統調用,為什麼arg指向的內容需要用copy_from_user來訪問,而不能直接通過「*arg」來訪問,或者直接memcpy來拷貝,而需要通過copy_from_user拷貝到內核以後再訪問。也很顯然不是問,為什麼fd, cmd, arg本身需要copy_from_user(因為這些參數本來就不需要copy_from_user)。

而實際上這個是可以的,如果你在用戶態對這個參數perfault一下(讀寫一下就可以了),或者在內核做一個gup,然後你直接訪問,一般來說一點問題沒有,但這樣確實是不保險的,原因就是我前面說的三個考慮。

補充2:再獨立看看其他答案的破綻

1. 馮的答案,現在排在第一,實際上這個答案錯得最沒邊。他說「內核不能信任任何用戶空間的指針。必須對用戶空間的指針指向的數據進行驗證」,我不明白下面那七十多個贊是怎麼來的,這句話你們看得懂么?一個數據,你怎麼通過copy_x_user來判斷它是對的還是錯的?指針本身的還可以用範圍來進行校驗,指針指向的數據,你拿什麼來校驗?還有後來討論中說什麼要保護這些數據不被其他進程修改。難道你不知道現在都是多核系統的嗎?除了大家都用spinlock,你拿什麼來保護一個數據「不被其他進程修改」?

經評論區提醒,他可能確實是簡單理解錯題意了,可能認為問的是「數據為什麼不能在用戶空間直接使用,而需要拷貝回內核空間再使用」,所以他回答成了「需要拷貝回內核保證數據不再發生變化」。如果這樣理解,這個問題和回答都沒有什麼價值。

2. @唐浩然 的答案,討論中發現,也是簡單理解錯題意了。

3. @張裕坤 的答案,他的分析基本上都是正確的,只是少了一個我前面提到的第一個理由

4. 老狐狸9527 的答案,他說的fixup的原理是正確的,但他有一個知識點有問題:如果一個地址沒有映射,結果是缺頁(MMU翻譯的時候就錯了)異常,而不是「拷貝不進去,而系統還不知道」。

其他暫時看來離題太遠,沒有什麼可說的。


1:為什麼要將參數copy到內核空間: 因為系統調用 的處理程序只能在內核空間運行,而且處理程序要使用用戶進程的 內核態堆棧,(經知友張裕坤 提醒,我原來的說法是錯誤的,我 原來認內核不能直接訪問用戶空間而 需要 映射用戶 頁面到內核 地址空間。 正確的說法是, 內核代碼可以直接訪問 用戶 空間),至於為什麼要使用內核堆棧,見:linux為什麼需要內核棧,系統調用時直接使用用戶棧不行嗎? - Linux - 知乎

2:不能直接訪問 或使用memcpy嗎?。

直接訪問 ?指參數還是指數據?

使用memcpy ,問題也不太明確:

A: 你是說系統調用的 參數 傳遞為何不使用 memcpy ,

B:還是說 非系統調用 參數的 具體「數據」 在用戶 空間和內核空間 之前的傳遞為何不使用memcpy?

兩者是有區別的,

對於A:

系統調用 的參數 傳遞是通過 寄存器傳遞的。而且,傳遞的參數多為指針類型,指向用戶空間數據。 為何不直接從用戶 態堆棧copy到內核態堆棧,截個圖(出自《深入理解LInux內核 第三版 》 408頁)

對於B::具體數據的傳輸 是 使用 copy_from_user /copy_to_user, 且此函數的 參數(由用戶空間通過寄存器傳遞過來的 指針,指向用戶數據區), 至於為什麼 不使用memcpy,見第3 個回答

3:為什麼使用copy_from_user 而不直接用memcpy

我是搬運工:(robert love大神提到了copy_to_user和memcpy, 不過原理上應該和題主問的copy_from_user差不多):

Linux Kernel: How does copy_to_user work?

Why can"t you just call, say, memcpy? Two reasons. One, the kernel is capable of writing to any memory. User process"s can"t. copy_to_user needs to check dst to ensure it is accessible and writable by the current process. Two, depending on the architecture, you can"t simply copy data from kernel to user-space. You might need to do some special setup first, invalidate certain caches, or use special operations.

Let"s look at what copy_to_user does on everyone"s favorite architecture, x86. First, copy_to_user checks that dst is writable by calling access_ok on dst with a type of VERIFY_WRITE. If access_ok returns nonzero, then copy_to_user proceeds to copy. Next, on x86 processors up to and including 486, the destination pages must be pinned in memory, as the page tables could change at anytime. On x86 revisions after 486, the WPbit is honored from ring zero and this is not necessary. Finally, copy_to_user, via __copy_to_user_ll, copies the memory using what is simply an optimized version of memcpy.」

大意就是用戶 程序,不能訪問內核空間。而內核可以訪問任意內存。copy_to_user需要 通過當前進程來 檢查目標的可寫許可權(而用戶 進程無法檢查內核空間目標地址的 寫許可權 ) 。 還有硬體架構(應該指的是cpu的特殊級要求)要求用戶 空間到內核空間的數據傳輸要做一些 特殊的設置,或者使用更特殊的operations.


@唐浩然 感覺你還是沒答對,linux在系統調用進入內核時,為什麼要將參數從用戶空間拷貝到內核空間?不能直接訪問,或是使用memcpy嗎?非要使用copy_from_user才行嗎? - Linux - 知乎 和 linux為什麼需要內核棧,系統調用時直接使用用戶棧不行嗎? - Linux - 知乎 這兩個問題好像你都沒說對(第2個問題好像你修改了答案,確實和安全有關),我試著自己的理解來說一下吧。。

首先這裡有一篇關於系統空間和用戶空間的博客:Linux用戶空間與內核空間 - 不積跬步,無以至千里 - 博客頻道 - CSDN.NET

因為本人今年11月份才開始看浙大毛德操老師的linux內核源代碼情景分析一書,現在才看到進程那一章,說的不對,歡迎指出。。

所以

以下我所說的都是關於intel 32位 x86的CPU,Linux版本為2.4

以下我所說的都是關於intel 32位 x86的CPU,Linux版本為2.4

以下我所說的都是關於intel 32位 x86的CPU,Linux版本為2.4

關於linux在系統調用進入內核時,為什麼要將參數從用戶空間拷貝到內核空間?不能直接訪問,或是使用memcpy嗎?非要使用copy_from_user才行嗎?這個問題,我認為:

1.linux在系統調用進入內核時,為什麼要將參數從用戶空間拷貝到內核空間?

答:進程由於系統調用進入內核,內核是可以識別所有虛存地址 [0, 4G) 的,否則sethostname(const char *name, seze_t len) -&> sys_sethostname -&> copy_from_user() -&> generic_copy_from_user() -&> access_ok() -&> __copy_user_zeroing 中 copy_from_user 的 dst 是系統空間地址,src 是 sethostname 通過寄存器傳進來的用戶空間地址,這裡就說不通了,所以內核是可以識別用戶空間地址的,當然也可以識別系統空間地址,這裡因為sethostname要修改內核系統空間的hostname,並持久化,所以用copy_from_user拷貝;

但是如果系統調用只是傳用戶空間地址的指針參數 src,使用一下src指向的內容,但不持久化內容進內核空間,這時,為什麼也要copy_from_user拷貝參數src?讓我們反問一下,不拷貝,直接用用戶空間的地址指針參數 src,可不可以?答案是:可以的,只要if(verify_area(檢查指針的合法性))即可,(舊答案這裡認為進程可能有切換,引起缺頁,但今天看到Linux內核源碼情景分析第358頁說:進程系統調用時不會因為時間片到了的中斷引起進程切換),所以考慮到sethostname這種一開始就要持久化數據到系統空間和可能出現指針不合法沒有映射的情況,內核保守的使用了copy_from_user把參數從用戶空間複製到系統空間。。

這裡和cpl,dpl沒有關係(系統調用陷入內核後,cs段對應的cpl為0,此時ds段對應的dpl也為0,如果有一個用戶空間的虛存地址(ox12345678,小於3G),這個地址對應的頁目錄項的u_s許可權位為1,內核是可以訪問它,段式內存的保護機制不關使用頁表映射的Linux什麼事,cpl和dpl保護的是中斷門或人為的設置不正確的ds段引起的cpu全面保護)。。

2.或是在內核中使用memcpy嗎?非要使用copy_from_user才行嗎?

答:可以使用memcpy,內核能識別訪問用戶空間的虛存地址src,如果if(檢測指針的合法性) {memcpy();} 這樣就行,但src可能不合法,使用copy_from_user就不用每次都檢查指針的合法性。。

比較memcpy的彙編代碼和內核copy_from_user的彙編代碼,關於數據複製,它們的邏輯是一樣的:先使用了彙編指令 rep 來4個位元組4個位元組的複製,然後剩餘的位元組(小於4個)就一個一個複製。。

當源src這個指針指向的內容合法時(src ,src + len在進程的用戶虛存區間內,並且建立了映射),兩者邏輯一樣,效率一樣;

但src這個指針有可能不合法,儘管是少數,所以要檢測指針的合法性,在內核中這樣寫 if(檢測指針的合法性) {memcpy();} 也可以,但這樣檢查很耗時間,畢竟大多數的指針都合法,從而新版取消了指針合法性的檢查,使用copy_from_user,萬一碰上了壞指針,複製到中間一半位元組時出現越界,那就發生頁面異常,在 do_page_fault 代碼會跳轉到 copy_from_user 中的異常處理代碼嘗試修復地址,把剩餘的位元組拷貝為0(詳細情況見浙大毛德操老師的linux內核源代碼情景分析一書的第249 - 254頁)

關於 linux為什麼需要內核棧,系統調用時直接使用用戶棧不行嗎? - Linux - 知乎 這個問題,我認為:

1.用用戶棧肯定不行!

讓我們反問一下,假設可以,使用用戶棧,如果這時進程的用戶代碼用一個假密碼調用了驗證密碼的系統調用(假設為access_psw_ok()),access_psw_ok中就會取出真正的密碼(當然加了密)放進access_psw_ok的函數棧里,和假密碼比對,系統調用返回不正確,退出內核,esp正確跳轉,進程進入用戶代碼,這時如果系統內核棧和用戶棧共用,棧中還殘留著真正的密碼(在當前用戶函數的esp下方),接著的用戶代碼就可以通過c的一些越界訪問到esp的下方,這樣會出現安全問題。。

2.不能用用戶棧,那麼在內核使用一個公共的,這樣不就很省內存?可惜這樣也不行,如果進程 A 系統調用陷入內核棧,運行一半,進程切換為 B,也系統調用陷入內核棧,運行一半,再切換回 A ,這時公共的內核棧就錯亂了。。

所以把系統棧放進內核,而且是每個進程一個,個個不同,在 fork 新進程時,在內核空間分配8KB,底部為進程的task_stuct控制塊,剩餘大概7KB作為進程的內核棧,注意這個內核棧是不可過於往下增長的,那樣會破壞task_stuct,所以寫內核代碼和驅動代碼禁止過多函數嵌套調用,或著在棧上使用很占空間的自動變數值。。


拷貝參數只是為了安全,在拷貝的時候還要檢查指針所指的內存是否有效,是否超過用戶空間的範圍等等,Linux和windows都是這樣做的。

用copy_from_user則屬於Linux自己的問題,在內核態中發生用戶空間的缺頁異常屬於限制級的問題,所以需要copy_from_user註冊一個exception table,這樣才能處理,否則無法處理缺頁,具體去看代碼。而windows沒有這個問題,可以任意去訪問用戶空間,缺頁異常處理程序認為這是正常行為。

多說一句,Linux下的系統調用默認參數來自用戶空間,即使從內核發起調用,所以有set_fs這個東西。而windows在內核中直接使用zw系列函數,直接指示是內核發起的調用,參數可以存在於內核空間。

補充一下:像windows上參數是否拷貝怎麼拷貝是可以變化的,因此有buffered io,direct io等方法,這樣像WriteFile這些api的緩衝區處理不是像Linux一樣一成不變。


《linux內核設計與實現》系統調用 那章說的挺清楚的


前面幾位朋友都答的挺精彩的。我這裡補充一個arm的copy_xxx_user的坑,

首先這可能是個bug。x64在4.2.0上沒這個問題,但arm(32bit)在3.14是有問題的。

這塊有一個特奇妙的坑,以copy to user為例,即便是用戶空間合法,但內存還處在未映射狀態(即已經分配,但還不存在頁表項或映射的情況下),copy_to_user不會報任何錯誤,但數據卻拷貝不出來,或只拷貝部分數據。。。這個問題可以通過對這塊內存先清零如bzero,而後再讓kernel進行copy_to_user,數據就可以完整拷出來了。

然後再說,memcpy這個事情,其實copy to,from的實現跟memcpy沒有兩樣。多出來的就是上面的fixup。

至於用戶和內核空間數據拷貝,是低效的,安全的實現,所以,現在有很多實現引入了0拷貝技術來解決這種問題,例如dpdk,sendfile等,盡量減少這種數據拷貝問題。


在論壇里問了一下,copy_from_user一方面是許可權的問題(檢查是否越界),另一方面是處理缺頁異常,要求訪問用戶空間的任何函數都必須是可重入的(缺頁處理程序能正常返回到異常發生的地方);

不知道是不是這樣。


題主如果有了解過處理器的架構,就會知道一個處理器內核會擁有mpu或者mmu的設計,這些硬體單元會提供地址訪問保護和管理的功能,可以講應用程序通過操作系統對低層驅動和內核進行隔離,如果非法的地址訪問都會導致處理器的一些硬體異常,所以當應用程序通過參數訪問內核的時候會存在不同的堆棧空間,這時候就需要操作系統來對這個操作進行管理了,總之,根本的目的就是保證系統代碼的可靠性和健壯


可以參考一下《freebsd設計與實現》中的「2.3 kernel services」這一section的給出的另外一種安全方面的考慮:即使通過了許可權檢查,也要防止在系統調用執行期間參數被其他進程修改從而繞過許可權檢查。


涉及到許可權的問題。 memcpy不可以吧! 你去看看memcpy是怎麼實現di。


Linux內核源碼沒讀過,所以以Windows舉例。

這個問題問得很好,很多做驅動開發的新手都會在類似的問題上出錯或產生疑問。目前為止,我看到的所有答案都是錯的,他們根本沒理解「安全」和操作系統內存管理、R0和R3區別。

首先明確一點,memcpy任何時候都可以調用,而且幾乎是最高效的應用層和內核數據交互。保證數據可控的情況下(在R3做校驗,並且確保數據不會被改動),在Windows內核開發時這是比較推薦的方式。

問題就在於檢查數據太麻煩,或者根本沒辦法確保。所以偷懶的方式是先把應用層數據拷貝到系統緩存,再給內核和驅動模塊使用。這是效率最低的方式,但是最安全。

此外還有一種,我們都知道內存地址只是虛擬地址,真正的內容都在物理地址上。所以可以做一次映射,既不用複製內容,又可以保證內核的安全。Windows中用MDL很容易實現,Linux應該也有類似機制(mmap之類的?)。

說回Linux,雖然沒看過源碼,但是操作系統的設計理念大同小異。自己寫內核模塊肯定是可以直接複製應用層代碼的,即memcpy,這點題主不用疑惑。系統調用是Linux內核開發人員實現的,做什麼都應該是安全上的考慮。

至於有些人說內核不能使用memcpy,你是來搞笑的嘛?memcpy是啥知道不?


看一下這個函數的源碼,和memcpy對比一下不就知道了???


現代操作系統,比如linux是中的進程是工作在內核空間或用戶空間的。發生系統調用之後,陷入內核。靠cpu的寄存器傳遞參數。而且內核的內存的地址空間和用戶空間的地址空間是不一樣的。用戶空間的內存有分頁和虛擬內存機制。

memcpy這種函數完全是一個C庫函數,只能工作在用戶空間,貌似不會引起內核的系統調用。

至於為什麼要多了一層複製,我認為是內核為了提高對底層物理設備的快速有效訪問,不得不設計和增加了緩衝cache。這個cache在內核空間,原則上用戶空間不可以直接訪問,所以就多了一層從內核空間到用戶空間的內存區的操作。

建議關注一下操作系統的「內存管理」的章節。

以上如果有誤敬請諒解。


推薦閱讀:

出於學習目的想安裝 Linux 系統(最好也在 C 盤),應該如何安裝?
Linux開發入門需要具備哪些條件?
linux系統哪個版本好?
既然 Windows 在用戶量和生態體系上都能碾壓 Linux,為什麼還有人說 Linux 比 Windows 好?
Linux為什麼在桌面領域還是小眾範圍的?

TAG:Linux | Linux內核 |