我们习惯在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系统调用为例,

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);
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)
{

}
  • 第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 产生的原因、解决方法,由于涉及到我并不熟悉的体系结构平台,所以上面只是我根据网上仅有很少的资料进行推断出来的,肯定不是很准确,希望大家能指正!

参考资料

Linux Kernel代码艺术——系统调用宏定义的更多相关文章

  1. Linux Kernel 代码艺术——编译时断言

    本系列文章主要写我在阅读Linux内核过程中,关注的比较难以理解但又设计巧妙的代码片段(不关注OS的各个模块的设计思想,此部分我准备写在“深入理解Linux Kernel” 系列文章中),一来通过内核 ...

  2. Linux Kernel 代码艺术——编译时断言【转】

    转自:http://www.cnblogs.com/hazir/p/static_assert_macro.html 本系列文章主要写我在阅读Linux内核过程中,关注的比较难以理解但又设计巧妙的代码 ...

  3. Linux Kernel代码艺术——数组初始化

    前几天看内核中系统调用代码,在系统调用向量表初始化中,有下面这段代码写的让我有点摸不着头脑: const sys_call_ptr_t sys_call_table[__NR_syscall_max+ ...

  4. Linux Kernel代码艺术——数组初始化【转】

    转自:http://www.cnblogs.com/hazir/p/array_initialization.html 前几天看内核中系统调用代码,在系统调用向量表初始化中,有下面这段代码写的让我有点 ...

  5. Linux kernel中常见的宏整理

    0x00 宏的基本知识 // object-like #define 宏名 替换列表 换行符 //function-like #define 宏名 ([标识符列表]) 替换列表 换行符 替换列表和标识 ...

  6. Linux下C编程通过宏定义打开和关闭调试信息

    GCC支持宏定义 gcc -Dmacro,将macro定义为1,我们可以利用这点在我们的代码中加入宏定义开关. #ifdef DEBUG #define pdebug(format, args...) ...

  7. linux中offsetof与container_of宏定义

    linux内核中offsetof与container_of的宏定义 #define offsetof(TYPE, MEMBER)    ((size_t) &((TYPE *)0)->M ...

  8. [PHP] PHP源码常用代码中的宏定义

    PHP源码常用代码宏定义:#define 宏名 字符串#表示这是一条预处理命令,所有的预处理命令都以#开头.define是预处理命令.宏名是标识符的一种,命名规则和标识符相同.字符串可以是常数.表达式 ...

  9. 使用linux kernel代码编译perf工具

    环境:Qemu + ARMv8 perf是一款综合性分析工具,大到系统全局性性能,再小到进程线程级别,甚至到函数及汇编级别. 在内核源码目录下执行编译脚本: #!/bin/bash cross_com ...

随机推荐

  1. 高效率去掉js数组中重复项

    Array类型并没有提供去重复的方法,如果要把数组的重复元素干掉,那得自己想办法: function unique(arr) { var result = [], isRepeated; for (v ...

  2. 无法解决 equal to 操作中 "SQL_Latin1_General_CP1_CI_AS" 和 "Chinese_PRC_CI_AS"

    无法解决 equal to 操作中 "SQL_Latin1_General_CP1_CI_AS" 和 "Chinese_PRC_CI_AS" 之间 2011-0 ...

  3. Oracle11g 配置 ST_GEOMETRY

    安装环境:ArcGIS Desktop10.2.1 .ArcSDE10.2.134940. Oracle11.2.0.1 操作系统:Windows Server 2012R2 DataCenter 安 ...

  4. Lind.DDD.Manage项目核心技术分享

    回到目录 关于Lind.DDD.Manager的培训与学习 讲解:张占岭 花名:仓储大叔 主要框架:Lind.DDD,Lind.DDD.Manager 关于Lind.DDD.Manager 由于数据模 ...

  5. PHP的变量和常量

  6. 这个jQuery导航菜单怎么样

    效果体验:http://keleyi.com/keleyi/phtml/jqtexiao/39.htm HTML文件代码: <!DOCTYPE html> <html xmlns=& ...

  7. 关于webStrom-11.1配置less且自动生成.css和自动压缩为.min.css/.min.js

    网上看过很多配置思路,自己总结了以下, 就把我个人配置的顺序以及材料分享下,webstrom以下简称WB 1.配置less需要安装nodejs,自行安装.因为要用到npm.我是直接把npm解压到C盘根 ...

  8. arcgis server10.2.2发布地图基础服务的具体步骤

    1.直接打开制作好的.mxd文档,比如这里: 2.打开mxd文档之后,打开菜单:file-share as -services 弹出地图发布服务的界面: 点击publish之后,耐心的等待一段时间,地 ...

  9. Unable to extract 64-bitimage. Run Process Explorer from a writeable directory

    Unable to extract 64-bitimage. Run Process Explorer from a writeable directory When we run Process E ...

  10. 如何正确响应ArcGIS JavaScript API中图形的鼠标事件

    在使用ArcGIS JavaScript API编写程序的时候,程序员往往需要完成这样一个功能:点击地图上的图形,自动进行专题GIS数据查询,当在地图非图形区域上点击时,自动进行底图兴趣点查询. 由于 ...