系统调用探寻

1. outline

1.1 概念

操作系统区分内核空间和用户空间,内核空间拥有更高的权限,可以直接操作硬件,而用户空间则需要通过内核空间来访问硬件,也就是说内核空间有对HW管理的功能,而系统调用其实是操作系统中提供的一些基本的功能函数;

操作系统中处理进程间的上下文切换,提供异常控制流的机制,CPU的底层实现 + kernel中底层的实现,大致分为中断、陷阱、故障、终止等几种,系统调用是通过陷阱的方式实现的,实质是通过软中断,进入到异常向量表,根据svc类型跳转到系统调用表,进而定位到所需要的接口位置;

所以系统调用就是用户进程需要访问某些资源的时候必须通过内核空间,而系统通过异常控制流这样的机制提供了调用方式;

1.2 涉及到的目录

基于android P、kernel 4.9

  • /bionic/libc/arch-arm64/syscalls/
  • /bionic/libc/tools/gensyscalls.py
  • /bionic/libc/kernel/uapi/asm-generic/unistd.h
  • /include/uapi/asm-generic/unistd.h
  • /arch/arm64/kernel/sys.c
  • /arch/arm64/kernel/entry.S
  • /include/linux/syscalls.h
  • /kernel/net/socket.c

2. 从name调用到sys_name

从逻辑上讲,此部分需要实现如下几个点:

  1. 如何触发软中断,进入异常向量表;
  2. 如何确认我要访问的接口位置;

2.1 如何触发软中断

在android系统中存在一个目录,这里是所有提供的系统调用的上层汇编实现:
这里的文件都是通过gensyscall.py脚本生成的

  1. 目录内容:生成的汇编文件
  2. 以__socket.S为例:
#include <private/bionic_asm.h>

ENTRY(__socket)
    mov     x8, __NR_socket //将socket系统标号传给x8
    svc     #0 // supervisor call

//猜测这几句的含义是超过最大范围后跳转到set err
    cmn     x0, #(MAX_ERRNO + 1) //给x0寄存器值 + 4095
    cneg    x0, x0, hi
    b.hi    __set_errno_internal  // 比较结果是无符号大于,执行标号,否则不跳转

    ret
END(__socket)
.hidden __socket

简单理解就是在这里直接进入了异常向量表的处理;

2.2 如何正确的找到合适的函数实现位置:

其中__NR_socket在unistd.h中定义:#define __NR_socket 198

  1. 查看异常处理,即kernel中的entry.S文件:
    异常向量表
    总共四组,对应如下四种case:

    这部分为网上引用:
    (1)运行级别不发生切换,从ELx变化到ELx,使用SP_EL1,这种情况在Linux kernel都是不处理的,使用invalid entry。
    (2)运行级别不发生切换,从ELx变化到ELx,使用SP_ELx。这种情况下在Linux中比较常见。
    (3)异常需要进行级别切换来进行处理,并且使用aarch64模式处理,比如64位用户态程序发生系统调用,CPU会切换到EL0,并且使用aarch64模式处理异常。
    (4)异常需要进行级别切换来进行处理,并且使用aarch32模式处理。比如32位用户态程序发生系统调用,CPU会切换到EL0,并且使用aarch32模式进行处理。

    则系统调用,按照逻辑划分应该属于上述第三种case:

  2. 找到el0_sync:
    el0_sync

  3. 跳转到 el0_svc:
    el0_svc
    这里终于走完了流程,找到了比较核心的内容:
    具体arm64汇编指令集后续找到相关文档再详细了解,这里的基本含义就是load 进来sys_call_table,从w8寄存器中读取系统调用号;

  4. 系统调用表:
    syscall table
    这里用了一些比较高级的数组初始化的方式:
    [0 … __NR_syscalls - 1] = sys_ni_syscall 含义为首先将数组内容全部初始化为sys_ni_syscall函数
    然后将unistd.h加进来赋值:

#define __NR_socket 198
__SYSCALL(__NR_socket, sys_socket)
#define __SYSCALL(nr, sym)	[nr] = sym

编译出来就是:[198] = sys_socket

到这里,我们找到sys call table指针,并根据调用号,找到其中第198个位置,其中存放的是sys_socket,则我们终于找到了sys_socket;

3. 从SYSCALL_DEFINEn(name) 转换为 sys_name

从逻辑上讲,此部分需要实现如下几个点:

  1. 如何提供一种common的机制,可以让接口的实现按照通用的命名规则即可转换为sys_name;
    • 函数转换,中间插入了一个转换为long再转换回来的过程;
    • 参数转换

3.1 函数的宏转换

linux中各种common的机制都喜欢设计为一个宏,这样底层开发者就可以很方便的参照基本的实现来使用对应宏就可以了(总感觉被玩出花来了),这里也一样,以socket为例,首先从SYSCALL_DEFINE3开始看起:

  1. socket实际定义的位置:socket.c
//只关注定义,具体内容不是这里要讨论的
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
  1. SYSCALL_DEFINE3定义:
#define SYSCALL_DEFINE0(sname)					\
	SYSCALL_METADATA(_##sname, 0);				\
	asmlinkage long sys_##sname(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_DEFINE3(socket, int, family, int, type, int, protocol)
==>SYSCALL_DEFINEx(3, _##socket, VA_ARGS)
==>SYSCALL_DEFINEx(3, _##socket, int, family, int, type, int, protocol)

ps: VA_ARGS 是可变参数的宏,参数列表的最后一个参数为省略号,在预编译的时候会被实参所替换,是在c99规范中新增的;

  1. SYSCALL_DEFINEx的转换:
#define SYSCALL_DEFINEx(x, sname, ...)				\
	SYSCALL_METADATA(sname, x, __VA_ARGS__)			\
	__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

这里SYSCALL_METADATA是在打开CONFIG_FTRACE_SYSCALL时候用的,这里暂时不用关注;
转换完后:

__SYSCALL_DEFINEx(3, _socket, int, family, int, type, int, protocol)

  1. __SYSCALL_DEFINEx 转换
#define __SYSCALL_DEFINEx(x, name, ...)					\
	asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))	\
		__attribute__((alias(__stringify(SyS##name))));		\
	static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__));	\
	asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__));	\
	asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__))	\
	{								\
		long ret = SYSC##name(__MAP(x,__SC_CAST,__VA_ARGS__));	\
		__MAP(x,__SC_TEST,__VA_ARGS__);				\
		__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));	\
		return ret;						\
	}								\
	static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__))

这个转换比较复杂,转换为如下内容:

  • syssocket 设置个别名 SySsocket
  • 定义和实现 SySsocket:实际上就是调用SYSCsocket
  • 定义和实现SYSCsocket :实际上实现位置就是socket.c中的实现;
	asmlinkage long sys_socket(__MAP(3,__SC_DECL,__VA_ARGS__))	
		__attribute__((alias(__stringify(SyS_socket))));		
	static inline long SYSC_socket(__MAP(3,__SC_DECL,__VA_ARGS__));	
	asmlinkage long SyS_socket(__MAP(3,__SC_LONG,__VA_ARGS__));	
	asmlinkage long SyS_socket(__MAP(3,__SC_LONG,__VA_ARGS__))	
	{								
		long ret = SYSC_socket(__MAP(3,__SC_CAST,__VA_ARGS__));	
		__MAP(3,__SC_TEST,__VA_ARGS__);				
		__PROTECT(3, ret,__MAP(3,__SC_ARGS,__VA_ARGS__));	
		return ret;						
	}								
	static inline long SYSC_socket(__MAP(3,__SC_DECL,__VA_ARGS__))

也就是说通过上述的宏转换,在预编译的时候已经可以将我们实现的SYSCALL_DEFINE3(socket 转换为sys_socket了

参数的宏转换

这部分中还有一个比较复杂的地方,之前没有介绍到:参数转换,这个很复杂,也是上述说的用来修复64位漏洞的地方

涉及到的两个功能:

  1. 类型转换,可以看到这里有转为long再转回来的过程;
  2. 传入参数类型和value组合,即socket有三个参数,但是传入时为“int , fd” 这种,所以需要做个转换;

具体实现:
3. 上述涉及到的参数类型

__MAP(3,__SC_DECL,VA_ARGS)
__MAP(3,__SC_LONG,VA_ARGS)
__MAP(3,__SC_CAST,VA_ARGS)
__MAP(3,__SC_TEST,VA_ARGS)
__PROTECT(3, ret,__MAP(3,__SC_ARGS,VA_ARGS)

  1. 涉及到的宏
#define __MAP0(m,...)
#define __MAP1(m,t,a) m(t,a)
#define __MAP2(m,t,a,...) m(t,a), __MAP1(m,__VA_ARGS__)
#define __MAP3(m,t,a,...) m(t,a), __MAP2(m,__VA_ARGS__)
#define __MAP4(m,t,a,...) m(t,a), __MAP3(m,__VA_ARGS__)
#define __MAP5(m,t,a,...) m(t,a), __MAP4(m,__VA_ARGS__)
#define __MAP6(m,t,a,...) m(t,a), __MAP5(m,__VA_ARGS__)
#define __MAP(n,...) __MAP##n(__VA_ARGS__)

#define __SC_DECL(t, a)	t a
#define __TYPE_IS_L(t)	(__same_type((t)0, 0L))
#define __TYPE_IS_UL(t)	(__same_type((t)0, 0UL))
#define __TYPE_IS_LL(t) (__same_type((t)0, 0LL) || __same_type((t)0, 0ULL))
#define __SC_LONG(t, a) __typeof(__builtin_choose_expr(__TYPE_IS_LL(t), 0LL, 0L)) a //判断t的类型如果是ll则用ll,否则用l
#define __SC_CAST(t, a)	(t) a
#define __SC_ARGS(t, a)	a
#define __SC_TEST(t, a) (void)BUILD_BUG_ON_ZERO(!__TYPE_IS_LL(t) && sizeof(t) > sizeof(long))

#define __PROTECT(...) asmlinkage_protect(__VA_ARGS__)
  1. 转换过程:

__MAP(3,__SC_DECL,VA_ARGS) 添加上实参
__MAP(3,__SC_DECL, int, family, int, type, int, protocol) 转换为
__MAP3(__SC_DECL, int, family, int, type, int, protocol) 转换为
__SC_DECL(int, family), __MAP2(__SC_DECL, int, type, int, protocol)
int family, __MAP2(__SC_DECL, int, type, int, protocol)
int family, __SC_DECL(int, type), __MAP1(__SC_DECL, int, protocol)
int family, int type , __MAP1(__SC_DECL, int, protocol)
int family, int type , __SC_DECL( int, protocol)
int family, int type , int protocol

说白了,这套map宏就是在将参数中的type和value组合成标准类型,去除‘,’功能;
上述是以__SC_DECL为例,如果是__SC_LONG就是在过程中以LL存储,__SC_CAST就是强转类型;

则经过一系列转换后(为方便理解):

	asmlinkage long sys_socket(int family,  int type , int protocol)	
		__attribute__((alias(__stringify(SyS_socket))));		
	//传入为family、type、protocol	
	asmlinkage long SyS_socket(long family,  long type , long protocol);	
	//这里将int转为long
	asmlinkage long SyS_socket(long family,  long type , long protocol)	
	{								
		long ret = SYSC_socket((long) family,  (long) type , (long) protocol);	
		__MAP(3,__SC_TEST,__VA_ARGS__);	//检查参数			
		__PROTECT(3, ret,__MAP(3,__SC_ARGS,__VA_ARGS__));	
		return ret;						
	}	
	static inline long SYSC_socket(int family,  int type , int protocol);
	//实现也是family、type、protocol								
	static inline long SYSC_socket(int family,  int type , int protocol)

4. 附录

4.1 数组实现系统调用表

第二章节中已经可以看到系统调用表的定义,对比如下的数组初始化方式则可以直接换算出来:
数组定义的方式

4.2 系统调用复杂宏处理的安全漏洞

2009年曾爆出关于Linux系统调用在某些64平台发现有漏洞CVE-2009-0029,为了修复该问题,自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位平台上,这就出现了现在的做法

4.3 涉及到的一些基本用法

  1. ‘##’ 用于连接符,将两个字串连接为一个
  2. type __builtin_choose_expr (const_exp, exp1, exp2)
    编译时处理,如果const_expr的结果非0,那么生成exp1,且返回类型type也与exp1表达式的类型一致;否则生成exp2,并且返回类型type也与exp2的类型一致。
    由于是编译时行为,因此exp1与exp2表达式所产生的目标代码是互斥的,生成了exp1就不会存在exp2。
  3. asmlinkage
#ifdef __cplusplus
#define CPP_ASMLINKAGE extern "C"
#else
#define CPP_ASMLINKAGE
#endif

#ifndef asmlinkage
#define asmlinkage CPP_ASMLINKAGE
#endif
  1. asmlinkage_protect
/*
 * This is used by architectures to keep arguments on the stack
 * untouched by the compiler by keeping them live until the end.
 * The argument stack may be owned by the assembly-language
 * caller, not the callee, and gcc doesn't always understand
 * that.
 *
 * We have the return value, and a maximum of six arguments.
 *
 * This should always be followed by a "return ret" for the
 * protection to work (ie no more work that the compiler might
 * end up needing stack temporaries for).
 */
/* Assembly files may be compiled with -traditional .. */
#ifndef __ASSEMBLY__
#ifndef asmlinkage_protect
# define asmlinkage_protect(n, ret, args...)	do { } while (0)
#endif
#endif

参考:
https://blog.csdn.net/tanli20090506/article/details/71487570

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值