理解系统调用的关键在于洞悉系统调用号是联系用户模式与内核模式的纽带。而在Solaris x64平台上,系统调用号被保存在寄存器RAX中,从用户模式传递到内核模式。一旦进入内核模式,内核的sys_syscall入口程序就根据保存在RAX中的系统调用号,从内核维护的系统调用表(sysent)中查询出对应的系统调用处理程序,从而进行系统调用。系统调用最多支持6个参数,参数被顺序保存在寄存器RDI, RSI, RDX, RCX, R8, R9中完成传递。另外,从用户模式陷入内核模式,通过汇编指令syscall实现切换,而从内核模式返回到用户模式,则通过汇编指令sysret完成切换。
1 系统调用概述
1.1 什么是系统调用
在现代操作系统中,用户的应用程序访问并使用内核所提供的各种服务的途径被称之为系统调用(syscall)。
1.2 为什么需要系统调用
第一,系统调用可以为用户空间提供访问硬件资源的统一接口,以至于用户程序不必去关注具体的硬件操作。比如,读写文件时,用户完全没有必要关心文件存放在何种磁盘上,也不用关心文件在何种文件系统上。
第二,系统调用可以对操作系统进行保护,保证系统的稳定和安全。系统调用的存在规定了用户进程进入操作系统内核的具体方式。换言之,用户进程访问内核的路径是事先规定好了的,只能从规定的位置进入内核,而不允许随便跳入内核。有了这样的进入内核的统一访问路径上的限制,才能充分保证内核的安全。
1.3 系统调用与C库函数的关系
内核提供的系统调用在C库中都有相应的封装函数。系统调用与其封装的C库函数名称常常相同。例如: modctl系统调用在C库中的封装函数即为modctl函数,其实现位于modctl.s汇编文件中。
1.4 系统调用与系统命令的关系
系统命令位于C库函数的上一层,是利用C库函数实现的可执行程序。例如: 命令modinfo调用C库函数modctl()查询内核模块的信息。而C库函数封装了进入内核的系统调用,modctl()使用syscall指令(有别于int 0x80, 是一种快速系统调用指令)进入内核。
1.5 系统调用与系统函数的关系
内核函数与C库函数的区别仅仅是内核函数在内核中实现,因此必须遵循内核编程的规则。系统调用最终必须具有明确的操作。用户应用程序通过系统调用进入内核后,会执行系统调用对应的内核函数,也就是系统调用服务例程。例如:modctl系统调用的服务例程是内核函数modctl()。
系统调用过程如下图所示:
2 Solaris x64系统调用实现原理
Solaris 支持x64和sparc两种平台,目前内核都是64位,但是支持32位和64位的应用程序,因此,32位和64位的系统调用都是支持的。为简单起见,接下来的讨论只阐述x64平台上的64位系统调用。
2.1 AMD64 ABI基础
理解Solaris X64系统调用,不可避免地需要了解一下基本的AMD64 ABI。Solaris x64实现遵循的ABI文档是:
System V Application Binary Interface, AMD64 Architecture Processor Supplement
这里使用简化的ABI文档: http://www.x86-64.org/documentation/abi.pdf
A.2 AMD64 Linux Kernel Conventions ... A.2.1 Calling Conventions The Linux AMD64 kernel uses internally the same calling conventions as user-level applications (see section 3.2.3 for details). User-level applications that like to call system calls should use the functions from the C library. The interface between the C library and the Linux kernel is the same as for the user-level applications with the following differences: 1. User-level applications use as integer registers for passing the sequence %rdi, %rsi, %rdx, %rcx, %r8 and %r9. The kernel interface uses %rdi, %rsi, %rdx, %r10, %r8 and %r9. 2. A system-call is done via the syscall instruction. The kernel destroys registers %rcx and %r11. 3. The number of the syscall has to be passed in register %rax. 4. System-calls are limited to six arguments, no argument is passed directly on the stack. 5. Returning from the syscall, register %rax contains the result of the system-call. A value in the range between -4095 and -1 indicates an error, it is -errno. 6. Only values of class INTEGER or class MEMORY are passed to the kernel.
另外,来自d3s.mff.cuni.cz/teaching/crash_dump_analysis的slides可以作为参考。 【贴两张主要的截图】
下面给出一个内核函数反汇编后的例子帮助理解ABI。
函数原型: ibt_status_t ibt_suggest_alt_path(ibt_channel_hdl_t channel, ibt_execution_mode_t mode, ibt_suggest_alt_path_info_t *alt_path, void *priv_data, ibt_priv_data_len_t priv_data_len, ibt_spr_returns_t *ret_args); 用mdb -k进入内核反汇编 root# mdb -k > ibt_suggest_alt_path::dis ibt_suggest_alt_path: pushq %rbp ; save rbp ibt_suggest_alt_path+1: movq %rsp,%rbp ; ibt_suggest_alt_path+4: subq $0x30,%rsp ; ibt_suggest_alt_path+8: movq %rdi,-0x8(%rbp) ; arg1 : rdi ibt_suggest_alt_path+0xc: movq %rsi,-0x10(%rbp) ; arg2 : rsi ibt_suggest_alt_path+0x10: movq %rdx,-0x18(%rbp) ; arg3 : rdx ibt_suggest_alt_path+0x14: movq %rcx,-0x20(%rbp) ; arg4 : rcx ibt_suggest_alt_path+0x18: movq %r8,-0x28(%rbp) ; arg5 : r8 ibt_suggest_alt_path+0x1c: movq %r9,-0x30(%rbp) ; arg6 : r9 ibt_suggest_alt_path+0x20: pushq %rbx ; save rbx ibt_suggest_alt_path+0x21: pushq %r12 ; save r12 ibt_suggest_alt_path+0x23: pushq %r13 ; save r13 ibt_suggest_alt_path+0x25: pushq %r14 ; save r14 ibt_suggest_alt_path+0x27: pushq %r15 ; save r15 ... ibt_suggest_alt_path+0xa11: popq %r15 ; restore r15 ibt_suggest_alt_path+0xa13: popq %r14 ; restore r14 ibt_suggest_alt_path+0xa15: popq %r13 ; restore r13 ibt_suggest_alt_path+0xa17: popq %r12 ; restore r12 ibt_suggest_alt_path+0xa19: popq %rbx ; restore rbx ibt_suggest_alt_path+0xa1a: leave ; restore rsp, rbp ibt_suggest_alt_path+0xa1b: ret ; > $q // leave == movq %rbp, %rsp + popq %rbp
2.2 系统调用号
每一个系统调用都有一个独一无二的系统调用号。操作系统最多支持512个系统调用。如果一个系统调用被废弃,那么它对应的系统调用号将被保留,而不能分配给新的系统调用使用。所有系统调用号位于文件/etc/name_to_sysnum中。ABI规定了系统调用号是由寄存器rax传递给内核的,例如: modctl的系统调用号为152 (=0x98), 从modctl::dis的输出中我们可以看出,在执行syscall指令之前,%eax == 0x98。
root# egrep "modctl" /etc/name_to_sysnum modctl 152 root# echo "modctl::dis" | mdb /lib/64/libc.so.1 modctl: movq %rcx,%r10 modctl+3: movl $0x98,%eax modctl+8: syscall modctl+0xa: jb -0x126d30 <__cerror> modctl+0x10: xorq %rax,%rax modctl+0x13: ret
- modctl()的实现可参见usr/src/lib/libc/common/sys/modctl.s
有关系统调用号的定义,见源文件usr/src/uts/common/sys/syscall.h,
例如:
#define SYS_modctl 152
内核在进入sys_syscall()后,根据寄存器rax中存储的系统调用号查找相应的系统调用内核函数。
2.3 系统调用表
Solaris内核维护了一张系统调用表,表中的每一个元素是一个struct sysent。
2.3.1 结构体struct sysent
321 /* 322 * Structure of the system-entry table. 323 * 324 * Changes to struct sysent should maintain binary compatibility with 325 * loadable system calls, although the interface is currently private. 326 * 327 * This means it should only be expanded on the end, and flag values 328 * should not be reused. 329 * 330 * It is desirable to keep the size of this struct a power of 2 for quick 331 * indexing. 332 */ 333 struct sysent { 334 char sy_narg; /* total number of arguments */ 335 #ifdef _LP64 336 unsigned short sy_flags; /* various flags as defined below */ 337 #else 338 unsigned char sy_flags; /* various flags as defined below */ 339 #endif 340 int (*sy_call)(); /* argp, rvalp-style handler */ 341 krwlock_t *sy_lock; /* lock for loadable system calls */ 342 int64_t (*sy_callc)(); /* C-style call hander or wrapper */ 343 };
root# mdb -k > ::sizeof struct sysent sizeof (struct sysent) = 0x20 > ::offsetof sysent sy_callc offsetof (sysent, sy_callc) = 0x18, sizeof (...->sy_callc) = 8
注意:结构体sysent的大小为0x20(=32), 系统调用服务例程sy_callc在结构体sysent中的偏移为0x18。后面我们分析sys_syscall()汇编代码的时候会用到0x20, 0x18这两个数字。
2.3.2 系统调用表struct sysent sysent[NSYSCALL]
o 宏NSYSCALL定义于头文件usr/src/uts/common/sys/systm.h中,
#define NSYSCALL 256 /* number of system calls */
o sysent[NSYSCALL]定义于源文件usr/src/uts/common/os/sysent.c中
/* * Native sysent table. */ struct sysent sysent[NSYSCALL] = { /* 0 */ IF_LP64( SYSENT_NOSYS(), SYSENT_C("indir", indir, 1)), /* 1 */ SYSENT_CI("exit", rexit, 1), ... /* 152 */ SYSENT_CI("modctl", modctl, 6), ... /* 255 */ SYSENT_CI("umount2", umount2, 2) ... };
o 宏SYSENT_CI定义于源文件 usr/src/uts/common/os/sysent.c中,
#define SYSENT_CI(name, call, narg) { (narg), SE_32RVAL1, NULL, NULL, (llfcn_t)(call) }
o 宏SE_32RVAL1定义于头文件 usr/src/uts/common/sys/systm.h中,
#define SE_32RVAL1 0x0 /* handler returns int32_t in rval1 */
o 以modctl为例,其在sysent表中被展开后就是:
{6, 0x0, NULL, NULL, (llfcn_t)modctl}
o 用mdb查看一下,
> (sysent + 0x20 * 0t152)::print -Ta struct sysent fffffffffc243480 struct sysent { fffffffffc243480 char sy_narg = '