天天看点

从新版本系统调用学习宏定义的用法

    这段时间又碰到系统调用这个家伙,结果在我当前用的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

继续阅读