操作系统管理软硬件资源,用户进程只能直接或间接的通过系统调用访问系统资源,而用户进程与内核运行于不同的权限空间,需要进行用户态到内核态的转变,这一转变是通过系统中断实现。现以getpid为例:
在用户态通过API接口编程如下:
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
return printf("pid is %d\n", getpid()) > 0;
}
getpid 为应用编程接口(API)函数,提供统一标准接口,实现是通过系统调用获得进程ID。 系统调用函数由唯一的调用号标识,x86构架下在文件 arch/x86/include/asm/unistd.h 指定,实现文件根据32位系统与64为系统的不同分别实现在相同目录的unistd_32.h和unistd_64.h文件,现通过64位系统分析,32位系统原理相同。
#define __NR_getpid 39
__SYSCALL(__NR_getpid, sys_getpid)
可以看到getpid系统调用号为 39, 当然这在不同的系统上并不一定相同,getpid API函数的封装正是封装了种种不同。
系统初始化时创建名为 sys_call_table
的表项,其中存储了系统调用函数,而__NR_*
调用号正是表项索引,sys_call_table
的定义 32构架位于 arch/x86/kernel/syscall_table_32.S
,sys_call_table
为起始地址依次存储一系列函数地址。
:::
ENTRY(sys_call_table)
.long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */
.long sys_exit
.long ptregs_fork
.long sys_read
.long sys_write
.long sys_open /* 5 */
.long sys_close
.long sys_waitpid
.long sys_creat
.long sys_link
...
64位构架位于arch/x86/kernel/syscall_64.c
,这里干脆定义成了一个sys_call_ptr_t
类型数组,其初始化由宏__SYSCALL
完成。
const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
/*
*Smells like a like a compiler bug -- it doesn't work
*when the & below is removed.
*/
[0 ... __NR_syscall_max] = &sys_ni_syscall,
#include <asm/unistd_64.h>
};
系统调用时,根据 sys_call_table[__NR_getpid]
获得 sys_getpid
地址调用之。如此,我们便可通过修改 sys_call_table
的初始化和__NR_*
的定义来增加自己的系统调用和修改原来系统调用的执行方式。
倘若增加我们自己的系统调用,那接下来一问题是我们如何调用?getpid函数由glibc库封装,接下来需要看如何直接调用该内核函数。当然,通过源码可以看到getpid的实现即为 sys_getpid
函数,但因为调用需要进行特权级的转变,所以直接调用sys_getpid
函数是行不通的。
在Linux内核 2.6.20 以前,是存在一系列宏方便调用的,不同参数调用_syscall0
,_syscall1
,_syscall2
等等,但是据说为了防止内核漏洞取消了这些宏,当然,从 2.6.19 拷贝出这些宏的实现,还是可以使用的,这里只是简单说明一下原理即可。
/* 摘自2.6.20内核源码 include/asm-x86_64/unistd.h */
#define __syscall "syscall"
...
#define _syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile (__syscall \
: "=a" (__res) \
: "0" (__NR_##name) : __syscall_clobber ); \
__syscall_return(type,__res); \
/* 摘自2.6.20内核源码 include/asm-i386/unistd.h */
#define _syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name)); \
__syscall_return(type,__res); \
}
可以看到,64位系统使用 syscall 指令进行系统调用,32系统使用 int 0×80指令。以64位系统举例,直接调用getpid的代码如下:
#include <sys/types.h>
#include <stdio.h>
int main()
{
pid_t pid = 0;
asm (
"syscall" \
: "=a"(pid) \
: "0"(39) \
);
printf("pid is %d\n", pid);
return 0;
}
如此一来,我们便可通过通过syscall指令或者int指令调用我们自己定义的系统函数。
更进一步,再调用syscall指令之后,系统进行用户态向内核态转变,这个过程分为两部分,一部分是硬件支持,即cpu所作操作,另一部分,是软件支持,位于 arch/x86/kernel/entry_32.S
和 arch/x86/kernel/entry_64.S
文件的system_call
,这里暂不分析。