这段时间又碰到系统调用这个家伙,结果在我当前用的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、宏定义之“双井”运算符##
在宏定义中可以用##运算符把运算符前后两个预处理符号连接成一个预处理符号。例如:
假如我们代码里有用到诸如VAR(1)、VAR(2)、VAR(3)或者VAR(4)的语句时,它们在编译预处理阶段就分别被替换成了x1、x2、x3和x4了。就像:
-
#define __SYSCALL_DEFINEx(x, name, ...) \
-
asmlinkage long sys##name(__SC_DECL##x(__VA_ARGS__))
中的__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的原型如下:
-
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
open系统调用被修饰成:
-
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
所以,在进一步展开SYSCALL_DEFINEx时(原型如下):
-
#define SYSCALL_DEFINEx(x, sname, ...) \
-
__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
其中,宏__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他们的技能?当然绝对不是酱紫滴。
2009年曾爆出关于Linux系统调用在某些64平台发现有漏洞,为了修复该问题,自2.6.28之后系统调用才变成了现在这个样子。该问题基本描述是:在Linux 2.6.28及以前版本内核中,IBM/S390、PowerPC、Sparc64以及MIPS 64位平台的ABI要求在系统调用时,用户空间程序将系统调用中32位的参数存放在64位的寄存器中要做到正确的符号扩展,但是用户空间程序却不能保证做到这点,这样就会可以通过向有漏洞的系统调用传送特制参数便可以导致系统崩溃或获得权限提升。
既然问题已经很明确了,接下来就是如何解决的问题了。通常情况我们都会这样做:既然在执行系统调用时用户空间没有进行寄存器的符号扩展,那么我们就在系统调用函数前加入一些汇编代码,将寄存器进行符号扩展不就OK了么。但问题是:系统调用前的代码都是公共的,因此并不能将某个寄存器一定符号扩展。
在Linux内核中,解决这个问题的办法很巧妙,它先将系统调用的所有输入参数都当成long类型(64位),然后再强制转化到相应的类型,这样就能解决问题了。如果去每个系统调用中一一这么做,这是一般程序员选择的做法,但写内核的大牛们不仅要完成功能,而且完成得有艺术!所以在上述IBM/S390、PowerPC、Sparc64以及MIPS 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系统调用的扩展情况就变成如下的样子了:
-
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 位寄存器参数正确的符号扩展。
由于某些体系结构是不存在此类问题的,如x86_64等,Linux内核定义了一个配置选项CONFIG_HAVE_SYSCALL_WRAPPERS,一开始介绍的扩展的宏定义是在没有配置该选项扩展的结果,如果是S390、PowerPC、Sparc 64等平台就需要配置该选项。和CVE-2009-0029漏洞类似的还有X86_64位系统爆出的漏洞,感兴趣的朋友可以去了解一下。
参考文献:
http://blog.csdn.net/hazir/article/details/11835025
阅读(1151) | 评论(0) | 转发(0) |