1、概述
系统调用时用户空间程序访问内核的唯一方式,glibc(c库)除了实现标准c规范所需的库函数之外,还提供了一套封装例程,将系统调用封装后供用户编程使用。所以,系统调用也属于一种API。那么,用户空间程序访问系统调用的方法有哪些呢?
a、使用封装函数
#include
......
getpid();
......getpid正是glibc库对系统调用sys_getpid提供的封装函数,具体定义位于/usr/include/unistd.h中。(从这里我们也可以看出:#include语句正是从/usr/include目录中开始搜索的)
extern __pid_t getpid(void) __THROW
__THROW是一个宏,在c里面,这个宏完全没有定义,在c++里面,该宏表示这个函数支持C++里面的throw异常功能。
b、使用通用系统调用
除了使用封装函数之外,glibc还提供了系统调用的统一封装函数。我们试想一下:如果我们更新了内核版本,而新的内核版本中添加了新的系统调用,但是glibc库没有提供对该系统调用的封装,这个时候,通用系统调用就派上用场了。
extern long int syscall(long int sysno,...) __THROW
#include
...
syscall(__NR_getpid);
......
__NR_getpid的定义位于/usr/include/asm/unistd_32.h中。
c、使用内联汇编
比如getpid使用内联汇编来实现:
......
int i;
asm volatile("int $80"\
:"=a" (i)\
: "0" (__NR_getpid)\
);
......
2、系统门
在使用内联汇编实现系统调用的时候,我们看到其实就是执行了一条int $80指令,然后将输出保存在eax寄存器中。输入为__NR_getpid.接下来我们分析一下int $80到底做了哪些工作。为了明白这条指令具体做了哪些工作,我们首先要了解一下linux中"门"的概念。其实,从字面意思上我们就能明白,“门”代表了内核的入口。当程序想执行一些内核代码(中断,系统调用等)时,就必须先经过这个“门”。
在linux系统中比较常用的门包括:
enum {
GATE_INTERRUPT = 0xE,//中断门
GATE_TRAP = 0xF,
GATE_CALL = 0xC,
GATE_TASK = 0x5,
};我们在本文中主要分析陷阱门,各种门描述符的定义如下:(各种门的描述字段跟上面说的定义是一致的,比如:陷阱门就是0xc)。其实中断门和陷阱门基本上是一致的,区别在于对IF(中断标志位的处理不同)。中断门在程序流程转移的过程中会清除IF标志位。)
因此我们这里简要分析一下linux中中断的处理流程。
在linux系统中有一个48位的IDTR寄存器,高32位就是IDT表在内存中的位置,低16位是表的大小。我们由IDTR寄存器就可以找到IDT表的位置。然后根据中断号找到IDT表中具体的表项。每个表项的描述在上面已经给出。从这个表项中我们能够得到offset(中断处理程序入口地址在段内的偏移量),而段基址就要从GDT/LDT中查找了。这样(段基址+段内偏移量)就能得到中断处理程序的地址了。那么,linux内核是在什么时候填充每个表项呢?我们在这里具体分析一下陷阱门的填充(因为系统调用就用到了这个”门“)内核在什么时候填充这个表项,使得int $80就能指向系统调用的入口地址呢?
具体的调用流程如下:
start_kernel
------>trap_init
------>set_system_trap_gate(SYSCALL_VECTOR, &system_call)
set_system_trap_gate函数正是填充系统调用门描述符的函数,SYSCALL_VECTOR正是0x80.
# define SYSCALL_VECTOR0x80
这个80就是"门"描述符在IDT表中的索引。
接下来我们具体分析一下:
static inline void set_system_trap_gate(unsigned int n, void *addr)
{
BUG_ON((unsigned)n > 0xFF);
_set_gate(n, GATE_TRAP, addr, 0x3, 0, __KERNEL_CS);
}
set_system_trap_gate只是简单的调用了_set_gate函数。GATE_TRAP就是门的类型,从命名上我们也可以看出正是“陷阱门”。addr正是我们所需要的系统调用程序的入口地址,在这里,这个地址正是内核函数system_call的地址。
dpl是特权描述符,对于该门而言,dpl=3,说明了这是一个用户态程序可以访问的陷阱门。__KERNEL_CS是该“门”的段描述符,说明了段选择符是”内核代码段“。关于这个段描述符women可以参考下面的图:
也就是说,通过__KERNEL_CS(12)我们可以查找到GDT表中的相应表项,从而得到段基址,其实在linux中,每个段都是从0x00000000开始的4G的空间。
static inline void _set_gate(int gate, unsigned type, void *addr,
unsigned dpl, unsigned ist, unsigned seg)
{
gate_desc s;
pack_gate(&s, type, (unsigned long)addr, dpl, ist, seg);
/*
* does not need to be atomic because it is only done once at
* setup time
*/
write_idt_entry(idt_table, gate, &s);
}
static inline void pack_gate(gate_desc *gate, unsigned char type,
unsigned long base, unsigned dpl, unsigned flags,
unsigned short seg)
{
gate->a = (seg << 16) | (base & 0xffff);
gate->b = (base & 0xffff0000) | (((0x80 | type | (dpl << 5)) & 0xff) << 8);
}
仔细分析一下pack_gate的代码,就会发现其跟上面图片上的定义是一致的。
通过上面的分析我们发现:int $80将程序的执行转向了内核函数system_call。在分析这个函数之前,我们首先介绍一下内核栈的概念。对于每一个用户进程而言,都有一个内核栈和用户栈。分别用于用户进程在内核空间和用户空间执行时存储函数调用参数,局部变量和其他辅助数据。
在内核中,数据结构pt_regs结构体与用户空间程序进入内核空间时将寄存器的值压入内核堆栈的顺序是一致的,对应关系如下图所示。
0(%esp) - %ebx
* 4(%esp) - %ecx
* 8(%esp) - %edx
* C(%esp) - %esi
*10(%esp) - %edi
*14(%esp) - %ebp
*18(%esp) - %eax
*1C(%esp) - %ds
*20(%esp) - %es
*24(%esp) - %fs
*28(%esp) - %gssaved iff !CONFIG_X86_32_LAZY_GS
*2C(%esp) - orig_eax
*30(%esp) - %eip
*34(%esp) - %cs
*38(%esp) - %eflags
*3C(%esp) - %oldesp
*40(%esp) - %oldss
*
/arch/x86/kernel/entry_32.S是整个系统调用执行的详细的汇编代码,在这里我们做一下简要分析。
ENTRY(system_call)
RING0_INT_FRAME# can't unwind into user space anyway
pushl_cfi %eax# save orig_eax
SAVE_ALL
GET_THREAD_INFO(%ebp)
# system call tracing in operation / emulation
testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
jnz syscall_trace_entry
cmpl $(NR_syscalls), %eax
jae syscall_badsys
syscall_call:
call *sys_call_table(,%eax,4)
movl %eax,PT_EAX(%esp)# store the return value我们可以看出system_call函数进行一些相关的工作之后,就将程序的执行转到了相应的中断处理程序的入口地址了。