這段時間又碰到系統調用這個家夥,結果在我目前用的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