Linux Kernel代碼藝術——系統調用宏定義

我們習慣在SI(Source Insight)中閱讀Linux內核,SI會建立符號表數據庫,能非常方便地跳轉到變量、宏、函數等的定義處。但在處理系統調用的函數時,卻會遇到一些麻煩:我們知道系統調用函數名的特點是sys_×××,例如我們想找open函數的內核系統調用代碼,在SI提供的符號表中搜索sys_open,能找到函數的聲明:

asmlinkage long sys_open(const char __user *filename, int flags, umode_t mode);

原本SI提供從函數名按住Ctrl單擊鼠標左鍵能跳轉到定義處的功能,但運用在系統調用函數sys_open上卻失敗了,這是什麼回事呢?

系統調用宏定義展開

經過分析,原來內核中系統調用採用了宏定義,如這裏的sys_open就被定義爲:

SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)

可以猜測出這個宏定義展開之後就是上面函數聲明那樣的,難怪SI不能跳轉到系統調用的定義處呢!

下面以open系統調用爲例分析這個宏是如何展開的:

首先在 include/linux/syscall.h 中有下面這樣的宏定義:

#define SYSCALL_DEFINE3(name, ...)                 \
   SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)

針對這個宏定義有幾點說明:

  1. 反斜槓\:當宏定義過長需要換行時,在行尾要加上換行標誌“\”;
  2. …:省略號代表可變的部分,下面用__VA_AEGS__ 代表省略的變長部分;
  3. ##:分隔連接方式,它的作用是先分隔,然後進行強制連接,例如:
#define VAR(type, name) type name##_##type
VAR(int, var1);
展開之後就是:
int var1_int;

那麼:

SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
展開之後是:
SYSCALL_DEFINEx(3, _open, __VA_ARGS__)
這又是一個宏,根據宏定義:
#define SYSCALL_DEFINEx(x, sname, ...)             \
   __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

SYSCALL_DEFINEx(3, _open, __VA_ARGS__)
展開爲:
__SYSCALL_DEFINEx(3, _open, __VA_ARGS__)

再根據宏定義:
#define __SYSCALL_DEFINEx(x, name, ...)                    \
   asmlinkage long sys##name(__SC_DECL##x(__VA_ARGS__))

__SYSCALL_DEFINEx(3, _open, __VA_ARGS__)
展開爲:
asmlinkage long sys_name(__SC_DECL3(__VA_ARGS__))

這裏 __VA_ARGS__ 是 const char __user *, filename, int, flags, umode_t, mode,而同樣__SC_DECL3 又是一組宏定義:

#define __SC_DECL1(t1, a1) t1 a1
#define __SC_DECL2(t2, a2, ...) t2 a2, __SC_DECL1(__VA_ARGS__)
#define __SC_DECL3(t3, a3, ...) t3 a3, __SC_DECL2(__VA_ARGS__)

這樣,一步一步地展開:

__SC_DECL3(const char __user *, filename, int, flags, umode_t, mode)
==>   __SC_DECL3(const char __user *, filename, int, flags, umode_t, mode)
==>   const char __user* filename, __SC_DECL2( int, flags, umode_t, mode)
==>   const char __user* filename, int flags, __SC_DECL1(umode_t, mode)
==>   const char __user* filename, int flags, umode_t mode

最終:

SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
宏定義展開之後就成爲:
asmlinkage long sys_open(const char __user *filename, int flags, umode_t mode);

正如我們之前猜測的那樣。

如何在 SI 中找到系統調用源代碼

回到開始的話題,既然不能直接通過系統調用聲明跳轉到定義的代碼處,那麼怎樣在 SI 快速找到系統調用的源碼呢?通過上面的sys_open 的展開,相信大家已經知道帶有三個參數的系統調用展開的過程,由於系統調用中最多可以帶有六個參數,那麼Linux 內核中定義了一組宏用來展開帶有不同參數的系統調用:

#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)

那麼有了這些宏之後,系統調用定義處的部分就可以用宏來代替了,如:

  • fork系統調用,就可以定義爲 SYSCALL_DEFINE0(fork)
  • brk系統調用,就可以定義爲 SYSCALL_DEFINE1(brk, unsigned long)
  • creat 系統調用,就可以定義爲 SYSCALL_DEFINE2(creat, const char __user *, pathname, umode_t, mode)
  • ...

可以找出規律,SYSCALL_DEFINE 後面跟系統調用所帶的參數個數n,第一個參數爲系統調用的名字,然後接2*n個參數,每一對指明系統調用的參數類型及名字。那麼下次我們想在 SI 中找某個系統調用的代碼時,使用 SI 提供的全局搜索功能(快捷鍵 Ctrl-/),以open系統調用爲例,輸入 SYSCALL_DEFINE3(open ,如圖1所示。有了這個方法以後就再不用擔心找不到系統調用的內核代碼了。


圖1 SI中搜索sys_open函數代碼

到此,我們學習到了宏定義的一些高級用法(如…、##等),還知道了如何在SI中通過搜索找系統調用代碼,學習過程中還會不時感慨開發 Linux 內核這些大牛們怎能將宏運用得如此出神入化。如果只知道這些肯定還是不夠的,我們試想一下爲什麼要用宏定義把系統調用搞得這麼複雜?直接用展開的形式不好麼?可以肯定的是內核開發者不是單純地秀代碼技巧,至於這樣寫帶來的好處是什麼?

漏洞 CVE-2009-0029 解析

如果我們查看2.6.28 之前的代碼,系統調用確實沒有這樣寫,但在2009年64位 Linux 內核在某些64位平臺下被發現系統調用有漏洞,爲了修復該漏洞系統調用才改寫成現在這樣的。該漏洞被命名爲 CVE-2009-0029 ,對該漏洞的簡單描述如下:

The ABI in the Linux kernel 2.6.28 and earlier on s390, powerpc, sparc64, and mips 64-bit platforms requires that a 32-bit argument in a 64-bit register was properly sign extended when sent from a user-mode application, but cannot verify this, which allows local users to cause a denial of service (crash) or possibly gain privileges via a crafted system call.

意思是說,在Linux 2.6.28及以前版本內核中,IBM/S390、PowerPC、Sparc64以及MIPS 64位平臺的ABI要求在系統調用時,用戶空間程序將系統調用中32位的參數存放在64位的寄存器中要做到正確的符號擴展,但是用戶空間程序卻不能保證做到這點,這樣就會可以通過向有漏洞的系統調用傳送特製參數便可以導致系統崩潰或獲得權限提升。

舉例來說,假如下面是個系統調用的內核代碼,參數是32位的無符號整型,但使用的64位寄存器傳參,上面提及到平臺的ABI要求32爲參數存放在64位寄存器中要符號擴展,由程序的調用者來完成,在系統調用的函數中則由用戶程序來保證進行了正確的寄存器符號擴展,但用戶空間程序卻無法保證。

asmlinkage long sys_example(unsigned int index)
{
    if (index > 5)
        return -EINVAL;
    return example_array[index];
}

在上面程序中,調用程序必須將索引符號擴展爲64位,如傳入參數index=3,那麼將寄存器的低32位賦值爲3,並未修改高32位,此時該寄存器的高32位假設爲0xFFFFFFFF,在進入該系統調用函數時,由於編譯器認爲你已經進行符號擴展了,所以直接引用64位寄存器的值代表index,此時index=-4294967293,判斷不大於5,返回example_array[-4294967293],很可能訪問到一塊沒有權限訪問的地址空間或者其他地址異常的錯誤而導致程序崩潰。

怎麼去解決這個問題呢,也許你會想既然用戶空間沒有進行寄存器的符號擴展,那麼我在系統調用函數之前加入一些彙編代碼將寄存器進行符號擴展,但有個問題是,系統調用前代碼都是公共的,因此並不能將某個寄存器一定符號擴展。

在Linux內核中,解決這個問題的辦法很巧妙,它先將所有參數都當成long類型(64位),然後再強制轉化到相應的類型,這樣就能解決問題了。如果去每個系統調用中一一這麼做,這是一般程序員選擇的做法,但寫內核的大牛們不僅要完成功能,而且完成得有藝術!這就出現了現在的做法,定義了下面的宏:

#define __SYSCALL_DEFINEx(x, name, ...)                    \
   asmlinkage long sys##name(__SC_DECL##x(__VA_ARGS__));       \
   static inline long SYSC##name(__SC_DECL##x(__VA_ARGS__));   \
   asmlinkage long SyS##name(__SC_LONG##x(__VA_ARGS__))        \
   {                               \
       __SC_TEST##x(__VA_ARGS__);              \
       return (long) SYSC##name(__SC_CAST##x(__VA_ARGS__));    \
   }                               \
   SYSCALL_ALIAS(sys##name, SyS##name);                \
   static inline long SYSC##name(__SC_DECL##x(__VA_ARGS__))

那麼仍然是以open系統調用爲例,

 1 SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
 2 {
 3  
 4 }
 5 
 6 展開之後如下:
 7 asmlinkage long sys_open(const char __user * filename, int flags, umode_t mode);
 8 static inline long SYSC_open(const char __user * filename, int flags, umode_t mode);
 9 asmlinkage long SyS_open((long)filename, (long)flags, (long)mode)
10 {
11  __SC_TEST3(const, char __user * filename, int, flags, umode_t, mode);   
12  return (long)SYSC_open(const char __user * filename, int flags, umode_t mode);
13 }
14 SYSCALL_ALIAS(sys_open, SyS_open);
15 static inline long SYSC_open(const char __user * filename, int flags, umode_t mode)
16 {
17  
18 }
  • 第11行 __SC_TEST3 宏沒展開,其實是編譯時檢查類型是否錯誤的代碼,和我們這裏討論的關係不大,展開之後也是個很有意思的宏定義,可以參考我這裏的一篇文章 。
  • 第14行SYSCALL_ALIAS 宏夜沒展開,意思即 sys_open 函數的別名是 SyS_open。
  • 因此,系統調用調轉到 sys_open 處即執行第三行的 SyS_open 函數,注意該函數的參數全爲 long 類型,該函數又直接調用 SYSC_open,而 SYSC_open 函數的參數又轉化爲 sys_open 原來正確的類型。這樣一來就消除了用戶空間不保證參數符號擴展的問題了,因爲此時實際上系統調用函數由 SyS_open 函數調用了,它來保證 32 位寄存器參數正確的符號擴展。

由於某些體系結構是不存在此類問題的,如x86_64等,Linux內核定義了一個配置選項CONFIG_HAVE_SYSCALL_WRAPPERS,一開始介紹的擴展的宏定義是在沒有配置該選項擴展的結果,如果是S390、PowerPC、Sparc 64等平臺就需要配置該選項。

說明

本文前面部分主要介紹一些表面的東西,比較簡單,對於後面部分是我思考的部分,在去年讀內核時我就有這個疑問——爲什麼內核要這樣把簡單的系統調用定義成這麼複雜的宏?這兩天通過查一下資料終於找到能解釋這個問題的理由,原因在於漏洞 CVE-2009-0029 導致系統調用函數定義不能直接用那個原型,內核大牛們就寫出了現在這樣的代碼。但需要說明的是,對於 CVE-2009-0029 產生的原因、解決方法,由於涉及到我並不熟悉的體系結構平臺,所以上面只是我根據網上僅有很少的資料進行推斷出來的,肯定不是很準確,希望大家能指正!

參考資料

發佈了73 篇原創文章 · 獲贊 60 · 訪問量 45萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章