天天看點

從新版本系統調用學習宏定義的用法

    這段時間又碰到系統調用這個家夥,結果在我目前用的3.0.x核心裡全變樣了。為了将這個問題弄明白,還得自己動手才行,這裡非常感謝CSDN的"海風林影"兄弟提供的博文和相關參考文獻,還是那句話“成果和榮耀歸于前輩”。

   很多人也都發現,在2.6.28及其之後的核心源碼裡,系統調用的寫法發生了比較大的變化,出現了大量宏定義的代碼。在源代碼裡,以前的諸如open()系統調用的sys_open()函數,現在僅僅能找到其聲明,而其定義卻“找不到”了。如果你把系統調用的宏展開後就會發現,以前的sys_open()依然安然無恙地躺在哪裡。

   這裡我們仍然以open系統調用為例。在核心源碼(當然這裡的核心版本要大于等于2.6.28)fs目錄下的open.c源檔案裡,我們會看到下面這樣的代碼:

點選(此處)折疊或打開

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

{

    long ret;

    if (force_o_largefile())

        flags |= O_LARGEFILE;

    ret = do_sys_open(AT_FDCWD, filename, flags, mode);

    /* avoid REGPARM breakage on x86: */

    asmlinkage_protect(3, ret, filename, flags, mode);

    return ret;

}

   當我們用gcc -E參數把該定義所涉及的所有宏就地擴充之後,在我的x86_32平台上就會得到下面這樣的結果:

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

 long ret;

 if (force_o_largefile())

  flags |= O_LARGEFILE;

 ret = do_sys_open(AT_FDCWD, filename, flags, mode);

 asmlinkage_protect(3, ret, filename, flags, mode);

 return ret;

   那麼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)形式的呢?其實就是宏的一些基本用法而已,我們來簡單分析一下。在include/linux/syscall.h裡有下面一組宏:

#define SYSCALL_DEFINE0(name)     asmlinkage long sys_##name(void)

#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__)

   這裡每個SYSCALL_DEFINE後面的數字說明了,名為name的系統調用接收幾個輸入參數。這些宏都由一個基礎宏SYSCALL_DEFINEx來實作:

#define SYSCALL_DEFINEx(x, sname, ...) \

   __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

   繼續找__SYSCALL_DEFINEx宏的定義:

#define __SYSCALL_DEFINEx(x, name, ...) \

   asmlinkage long sys##name(__SC_DECL##x(__VA_ARGS__))

   關于__SC_DECL##x最後也被展開,其定義如下:

#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__)

#define __SC_DECL4(t4, a4, ...) t4 a4, __SC_DECL3(__VA_ARGS__)

#define __SC_DECL5(t5, a5, ...) t5 a5, __SC_DECL4(__VA_ARGS__)

#define __SC_DECL6(t6, a6, ...) t6 a6, __SC_DECL5(__VA_ARGS__)

   __SC_DECLx宏實作了系統調用輸入參數類型和參數名的結合,因為宏定義時是無法确定參數類型的。我們來逐漸分解一下open系統調用的實作:

步驟

宏擴充内容

1

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

2

SYSCALL_DEFINEx(3, _open, __VA_ARGS__)

3

__SYSCALL_DEFINEx(3, _open, __VA_ARGS__)

4

asmlinkage long sys_open(__SC_DECL3(__VA_ARGS__))

5

asmlinkage long sys_open(const char __user *filename, __SC_DECL2(__VA_ARGS__))

6

asmlinkage long sys_open(const char __user *filename, int flags, __SC_DECL1(__VA_ARGS__))

7

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

   至于新版核心系統調用為什麼要這麼改,這裡我們先按下這個問題不表,待會兒回答,我們先看看這裡面宏定義擴充的用法。

    1、宏定義之“雙井”運算符##

    在宏定義中可以用##運算符把運算符前後兩個預處理符号連接配接成一個預處理符号。例如:

#define VAR(n) x##n

  假如我們代碼裡有用到諸如VAR(1)、VAR(2)、VAR(3)或者VAR(4)的語句時,它們在編譯預處理階段就分别被替換成了x1、x2、x3和x4了。就像:

   中的__SC_DECL##x一樣,當x分别為1、2、3、4、5或者6時該宏就被替換成了__SC_DECL1、__SC_DECL2、__SC_DECL3、__SC_DECL4、__SC_DECL5或者__SC_DECL6。

   2、與“雙井”運算符比較形似的還有“單井”運算符#

    該運算符主要用于建立字元串,運算符的後面必須跟至少一個形參。例如,兩個形參的情況:

#define tempfile(path) #path "/%s"

char *bin=tempfile(/bin/ls);

    那麼最後bin=” /bin/ls/%s”

    在tempfile宏定義裡,無論#後面的path與”/%s”字元串之間有多少空格,或者多少Tab。在執行宏展開時#後面輸入參數之間的所有空格、Tab都将被忽略。但要注意如果空格或者Tab出現在雙引号””之内,例如:

#define tempfile(path) #path " /%s"

    那麼這些空格或者Tab會被原封不動的儲存下來并應用到最終字元串裡。

    是以,在#運算符後面多個形參之間的空格和Tab符号在宏展開時都會被删掉,而每個形參内部的空格或Tab則會被原封不動的儲存下來。再來看個稍微複雜一點的例子:

#define STR(s) #s

fputs(STR(strncmp("ab\"c\0d", "abc", '\4"')== 0) STR(: @\n), s);

    展開結果為:fputs("strncmp(\"ab\\\"c\\0d\",

\"abc\", '\\4\"') == 0" ": @\n", s);

    這個例子說明:如果實參中包含字元常量或字元串,則宏展開之後字元串的界定符"要替換成\",字元常量或字元串中的\和"字元要替換成\\和\"。注意,STR(: @\n)裡的\n裡的\是不會被替換的。

    3、可變參數表…

    在函數定時我們已經見過這種形式,例如C庫的printf函數的聲明:

int printf(const char *format, ...);

    最開始時可變參數清單的形式隻能用在函數定義裡,在C99規範裡GCC用一個名為__VA_ARGS__保留關鍵字實作了宏定義也可使用可變數目參數的目的。如果有如下定義:

#define debug(...) printf(__VA_ARGS__)

debug(“result=%d\n”,result);

    那麼在編譯預處理階段将被展開成printf(“result=%d\n”,result);

    上述宏定義中“…”為一個可變長的參數清單,而__VA_ARGS__則會将宏定義中的省略号“…”所表示的内容原封不動的抄到__VA_ARGS__所在的位置上,即__VA_ARGS__本身就是一個可變長參數的宏,是C99标準規範裡新增的。

    然後我們來回頭看一下上面的例子。SYSCALL_DEFINE3的原型如下:

   open系統調用被修飾成:

    是以,在進一步展開SYSCALL_DEFINEx時(原型如下):

    其中,宏__VA_ARGS__所代表的參數清單就是“const char __user *, filename, int, flags, umode_t, mode”。同樣地情況,最後在替換__SC_DECL3(__VA_ARGS__),__VA_ARGS__的變化過程是:

__SC_DECL3:__VA_ARGS__ =>const char __user *, filename, int, flags, umode_t, mode

__SC_DECL2:__VA_ARGS__ => int, flags, umode_t, mode

__SC_DECL1:__VA_ARGS__ => umode_t, mode

    最終__SC_DECL3(__VA_ARGS__)就變成了:const char __user *filename, int flags, umode_t,

mode,即open系統調用的三個輸入參數。

    我們可以看到宏定義在核心大牛們的手裡是被玩得多麼服帖。那麼有些朋友可能會問了:一個系統調用至于弄這麼複雜麼?難道大牛們在show他們的技能?當然絕對不是醬紫滴。

    既然問題已經很明确了,接下來就是如何解決的問題了。通常情況我們都會這樣做:既然在執行系統調用時使用者空間沒有進行寄存器的符号擴充,那麼我們就在系統調用函數前加入一些彙編代碼,将寄存器進行符号擴充不就OK了麼。但問題是:系統調用前的代碼都是公共的,是以并不能将某個寄存器一定符号擴充。

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

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系統調用的擴充情況就變成如下的樣子了:

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

static inline long SYSC_open(const char __user * filename, int flags, umode_t mode);

asmlinkage long SyS_open((long)filename, (long)flags, (long)mode)

  __SC_TEST3(const, char __user * filename, int, flags, umode_t, mode);

  return (long)SYSC_open(const char __user * filename, int flags, umode_t mode);

SYSCALL_ALIAS(sys_open, SyS_open);

static inline long SYSC_open(const char __user * filename, int flags, umode_t mode)

  …

   __SC_TEST3 宏沒繼續展開,因為它是編譯時檢查類型是否錯誤的代碼,和我們這裡讨論的關系不大。另外,SYSCALL_ALIAS 宏也沒展開,意思即 sys_open 函數的别名是 SyS_open,這樣一來當執行系統調用sys_open時由于别名宏的修飾,其實就是在調用SyS_open,而該函數的所有輸入參數皆為long類型,該函數又直接調用 SYSC_open,而 SYSC_open 函數的參數又轉化為 sys_open 原來正确的類型。這樣一來就消除了使用者空間不保證參數符号擴充的問題了,因為此時實際上系統調用函數由 SyS_open 函數完成了,它來保證 32 位寄存器參數正确的符号擴充。

參考文獻:

http://blog.csdn.net/hazir/article/details/11835025

繼續閱讀