Linux系统调用之二——调用机制

0 背景

之前的博客有对系统调用进行了概述,有兴趣的小伙伴可以翻看下:Linux系统调用之一——概述导论,这里是主要来介绍下系统调用的机制。

无论是GUI、应用程序,还是命令行接口最终都需要使用系统调用来实现。

当我们要打开文件(open)然后进行写入(write)或者分配内存(malloc)时,此时将会切换到内核态,虽然我们并察觉不到;之后内核对调用进行检查,如果通过,则按照指令执行相应的操作,分配相应的资源。

这种机制被称为系统调用,用户态进程发起调用,切换到内核态,内核态完成,返回用户态继续执行,这也是用户态唯一主动切换到内核态的合法手段(exception 和 interrupt 是被动切换)。

先以fork()为例通过图示来大致看下系统调用流程:
在这里插入图片描述
在Linux发展历史上,x86 的系统调用实现经历了 int / iret 到 sysenter / sysexit 再到 syscall / sysret 的演变。由于通过软中断0x80的方式来实现系统调用实在太慢了,目前只要硬件支持,大部分使用的都是sysenter / sysexit或者syscall / sysret。但无论从学还是理解上,还是要先从软中断开始,所以本文也将主要通过分析0x80软中断来介绍系统调用机制。

1 0x80中断

1.1 中断处理函数entry_INT80_32

Linux内核初始化时,为中断编号128(0x80)注册一个名为entry_INT80_32的中断处理程序。,每当有0x80的中断进来,将执行函数entry_INT80_32,让我们看一下实际执行此操作的代码。
PS:本文分析基于Linux kernel 4.9.76 ,glibc 2.25.90
文件:arch/x86/kernel/traps.c

void __init trap_init(void)
{
 /* ..... other code ... */
/*CONFIG_IA32_EMULATION //该宏为允许在64位内核中运行32位代码.
我们通过宏CONFIG_X86_32包含内容分析
#ifdef CONFIG_IA32_EMULATION 
	set_system_intr_gate(IA32_SYSCALL_VECTOR, entry_INT80_compat);
	set_bit(IA32_SYSCALL_VECTOR, used_vectors);
#endif
*/
#ifdef CONFIG_X86_32
	set_system_intr_gate(IA32_SYSCALL_VECTOR, entry_INT80_32);
	set_bit(IA32_SYSCALL_VECTOR, used_vectors);
#endif
 /* ..... other code ... */
}

其中 IA32_SYSCALL_VECTOR在头文件arch/x86/include/asm/irq_vectors.h被定义为0x80set_system_intr_gate 用于在**中断描述符表(IDT)**上设置系统调用门。

也就是Linux内核保留了一个0x80软件中断,用户程序可以通过该中断触发内核,然后硬件根据中断向量号在 IDT 中找到对应的表项,即中断描述符。

现在问题是内核如何知道它应该执行哪个系统调用呢?实际上程序编译时会预先将系统调用号放入eax寄存器中。系统调用相关参数将放在其余的通用寄存器中。

当0x80中断来时,从entry_INT80_32开始执行,其定义在 arch/x86/entry/entry_32.S,通过汇编编写。

ENTRY(entry_INT80_32)
	ASM_CLAC
	pushl	%eax			/* pt_regs->orig_ax */
	SAVE_ALL pt_regs_ax=$-ENOSYS	/* save rest */

	/*
	 * User mode is traced as though IRQs are on, and the interrupt gate
	 * turned them off.
	 */
	TRACE_IRQS_OFF

	movl	%esp, %eax
	call	do_int80_syscall_32
.Lsyscall_32_done:

通过pushl %eax将存在eax中的系统调用号压栈,然后SAVE_ALL将其他寄存器的值压栈,这些寄存器放着调用函数的参数,参看以下代码:

.macro SAVE_ALL pt_regs_ax=%eax
    cld
    PUSH_GS
    pushl   %fs
    pushl   %es
    pushl   %ds
    pushl   \pt_regs_ax
    pushl   %ebp
    pushl   %edi
    pushl   %esi
    pushl   %edx
    pushl   %ecx
    pushl   %ebx
    movl    $(__USER_DS), %edx
    movl    %edx, %ds
    movl    %edx, %es
    movl    $(__KERNEL_PERCPU), %edx
    movl    %edx, %fs
    SET_KERNEL_GS %edx
.endm

压栈完毕,TRACE_IRQS_OFF关闭中断,然后movl %esp, %eax将当前栈指针保存到eax,然后 call do_int80_syscall_32调用do_int80_syscall_32,以上是中断处理函数entry_INT80_32的定义。

1.2 中断处理函数do_syscall_32_irqs_on

继续分析,接下来就调用到了do_int80_syscall_32该函数定义在arch/x86/entry/common.c

/* Handles int $0x80 */
__visible void do_int80_syscall_32(struct pt_regs *regs)
{
	enter_from_user_mode();/* Called on entry from user mode with IRQs off*/
	local_irq_enable();/* unconditionally enable interrupts */
	do_syscall_32_irqs_on(regs);
}

前面两个我把英文注释加上了,不再细看,主要关注函数do_syscall_32_irqs_on,同样定义在arch/x86/entry/common.c

static __always_inline void do_syscall_32_irqs_on(struct pt_regs *regs)
{
    struct thread_info *ti = current_thread_info();
    unsigned int nr = (unsigned int)regs->orig_ax;

#ifdef CONFIG_IA32_EMULATION
    current->thread.status |= TS_COMPAT;
#endif

    if (READ_ONCE(ti->flags) & _TIF_WORK_SYSCALL_ENTRY) {
        /*
         * Subtlety here: if ptrace pokes something larger than
         * 2^32-1 into orig_ax, this truncates it.  This may or
         * may not be necessary, but it matches the old asm
         * behavior.
         */
        nr = syscall_trace_enter(regs);
    }

    if (likely(nr < IA32_NR_syscalls)) {
        /*
         * It's possible that a 32-bit syscall implementation
         * takes a 64-bit parameter but nonetheless assumes that
         * the high bits are zero.  Make sure we zero-extend all
         * of the args.
         */
        regs->ax = ia32_sys_call_table[nr](
            (unsigned int)regs->bx, (unsigned int)regs->cx,
            (unsigned int)regs->dx, (unsigned int)regs->si,
            (unsigned int)regs->di, (unsigned int)regs->bp);
    }

    syscall_return_slowpath(regs);
}

该函数的参数regs,其实指的就是先前在 entry_INT80_32 依次被压入栈的寄存器值。定义在arch/x86/include/asm/ptrace.h中,__i386__中的定义如下:

#ifdef __i386__
struct pt_regs {
	unsigned long bx;
	unsigned long cx;
	unsigned long dx;
	unsigned long si;
	unsigned long di;
	unsigned long bp;
	unsigned long ax;
	unsigned long ds;
	unsigned long es;
	unsigned long fs;
	unsigned long gs;
	unsigned long orig_ax;
	unsigned long ip;
	unsigned long cs;
	unsigned long flags;
	unsigned long sp;
	unsigned long ss;
};
#else /* __i386__ */

先取出系统调用号,ia32_sys_call_table[nr]( (unsigned int)regs->bx, (unsigned int)regs->cx,(unsigned int)regs->dx, (unsigned int)regs->si,(unsigned int)regs->di, (unsigned int)regs->bp);根据寄寄存器中存储的函数参数从系统调用表(ia32_sys_call_table) 中取出对应的处理函数,然后syscall_return_slowpath(regs)再调用该函数。

1.3系统调用表ia32_sys_call_table

其实还还有很关键的一部分就是系统调用表ia32_sys_call_table,上一步的函数处理中就是根据寄存器参数,从系统调用表中取出对应处理函数的。
IDT定义在arch/x86/entry/syscall_32.c

/* System call table for i386. */

#include <linux/linkage.h>
#include <linux/sys.h>
#include <linux/cache.h>
#include <asm/asm-offsets.h>
#include <asm/syscall.h>

#define __SYSCALL_I386(nr, sym, qual) extern asmlinkage long sym(unsigned long, unsigned long, unsigned long, unsigned long, unsigned long, unsigned long) ;
#include <asm/syscalls_32.h>
#undef __SYSCALL_I386

#define __SYSCALL_I386(nr, sym, qual) [nr] = sym,

extern asmlinkage long sys_ni_syscall(unsigned long, unsigned long, unsigned long, unsigned long, unsigned long, unsigned long);

__visible const sys_call_ptr_t ia32_sys_call_table[__NR_syscall_compat_max+1] = {
	/*
	 * Smells like a compiler bug -- it doesn't work
	 * when the & below is removed.
	 */
	[0 ... __NR_syscall_compat_max] = &sys_ni_syscall,
#include <asm/syscalls_32.h>
};

以上是syscall_32.c的全部代码,并未发现IDT定义,那应该在头文件中,实际上在未编译的内核中根本找不到syscalls_32.h 这个头文件,事实上我们需要编译kernel后才会出现。
而实际上syscalls_32.h又依赖于syscall_32.tbl
文件:syscall_32.tbl

#
# 32-bit system call numbers and entry vectors
#
# The format is:
# <number> <abi> <name> <entry point> <compat entry point>
#
# The abi is always "i386" for this file.
#
0	i386	restart_syscall		sys_restart_syscall
1	i386	exit			sys_exit
2	i386	fork			sys_fork			sys_fork
3	i386	read			sys_read
4	i386	write			sys_write
5	i386	open			sys_open			compat_sys_open
...
...
381	i386	pkey_alloc		sys_pkey_alloc
382	i386	pkey_free		sys_pkey_free

共383个系统调用函数,而编译后的rch/x86/include/generated/asm/syscalls_32.h

__SYSCALL_I386(0, sys_restart_syscall, )
__SYSCALL_I386(1, sys_exit, )
#ifdef CONFIG_X86_32
__SYSCALL_I386(2, sys_fork, )
#else
__SYSCALL_I386(2, sys_fork, )
#endif
__SYSCALL_I386(3, sys_read, )
__SYSCALL_I386(4, sys_write, )
#ifdef CONFIG_X86_32
__SYSCALL_I386(5, sys_open, )
#else
__SYSCALL_I386(5, compat_sys_open, )
...

说明 syscalls_32.h 是在编译过程中动态生成的,感兴趣的小伙伴可以去查看脚本arch/x86/entry/syscalls/syscalltbl.sh。arch/x86/syscalls/syscall_32.tbl
这样我们的系统调用表如下:

__visible const sys_call_ptr_t ia32_sys_call_table[__NR_syscall_compat_max+1] = {
   [0] = sys_restart_syscall,
   [1] = sys_exit,
   [2] = sys_fork,
   [3] = sys_read,
   [4] = sys_write,
   [5] = sys_open,
   ...
};

1.4以系统调用sys_open为例分析

这里通过软中断和AT&T汇编写一个简单的系统调用,并不规范,至少要退出。(可以直接编译)

int main(int argc, char *argv[])
{
  asm ("movl $0x05, %eax\n" /* 设置系统调用号 */
       "movl $1, %ebx\n"    /* 设置系统调用参数 */
       "movl $2, %ecx\n"    /* 设置系统调用参数 */
       "int $0x80"          /* 进入系统调用中断 */
       );
}

movl $0x05, %eax可见调用号为0x05,所以这里调用了sys_open,其定义在fs/open.c,而这里我们传入3个参数

SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
{
	if (force_o_largefile())
		flags |= O_LARGEFILE;

	return do_sys_open(AT_FDCWD, filename, flags, mode);
}

关于SYSCALL_DEFINE3和相关宏定义如下:

#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)

#define SYSCALL_DEFINEx(x, sname, ...)                \
        SYSCALL_METADATA(sname, x, __VA_ARGS__)       \
        __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

#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__))

其中SYSCALL_METADATA 保存了调用的基本信息,供调试程序跟踪使用(kernel 需开启 CONFIG_FTRACE_SYSCALLS)。

__SYSCALL_DEFINEx 用于拼接函数,函数名被拼接为 sys##_##open,参数也通过 __SC_DECL 拼接,最终得到展开后的定义:

asmlinkage long sys_open(const char __user * filename, int flags, umode_t mode)
{
    if (force_o_largefile())
        flags |= O_LARGEFILE;

    return do_sys_open(AT_FDCWD, filename, flags, mode);
}

这里也就是说,将do_sys_open(AT_FDCWD, filename, flags, mode)封装为3参数的sys_open,同理,其他数量参数也是如是封装。

long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
	struct open_flags op;
	int fd = build_open_flags(flags, mode, &op);
	struct filename *tmp;

	if (fd)
		return fd;

	tmp = getname(filename);
	if (IS_ERR(tmp))
		return PTR_ERR(tmp);

	fd = get_unused_fd_flags(flags);
	if (fd >= 0) {
		struct file *f = do_filp_open(dfd, tmp, &op);
		if (IS_ERR(f)) {
			put_unused_fd(fd);
			fd = PTR_ERR(f);
		} else {
			fsnotify_open(f);
			fd_install(fd, f);
		}
	}
	putname(tmp);
	return fd;
}

getname 将处于用户态的文件名拷到内核态,然后通过 get_unused_fd_flags 获取一个没用过的文件描述符,然后 do_filp_open 创建 struct filefd_installfdstruct file 绑定(task_struct->files->fdt[fd] = file),然后返回 fd

fd一直返回到 do_syscall_32_irqs_on ,被设置到 regs->ax (eax) 中。接着返回 entry_INT80_32 继续执行,最后执行 INTERRUPT_RETURN 。INTERRUPT_RETURN 在 arch/x86/include/asm/irqflags.h 中定义为 iret ,负责恢复先前压栈的寄存器,返回用户态。系统调用执行完毕。

最后这一部分是看了好些英文博客,博主感觉还不是很清晰,后面再慢慢梳理梳理~

2 汇编实现

这里是复制的大神的代码,感觉比较好,所以直接贴过来了,也供大家学习下。(里面有些汇编我暂时还不懂,肯定写不了,但不耽误看)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(){
    char * filename = "/tmp/test";
    char * buffer = malloc(80);
    memset(buffer, 0, 80);
    int count;
    __asm__ __volatile__("movl $0x5, %%eax\n\t"
                         "movl %1, %%ebx\n\t"
                         "movl $0, %%ecx\n\t"
                         "movl $0664, %%edx\n\t"
                         "int $0x80\n\t"
                         "movl %%eax, %%ebx\n\t"
                         "movl $0x3, %%eax\n\t"
                         "movl %2, %%ecx\n\t"
                         "movl $80, %%edx\n\t"
                         "int $0x80\n\t"
                         "movl %%eax, %0\n\t"
                         :"=m"(count)
                         :"g"(filename), "g"(buffer)
                         :"%eax", "%ebx", "%ecx", "%edx");
    printf("%d\n", count);
    printf("%s\n", buffer);
    free(buffer);
}

在当前主流的系统调用库(glibc) 中,软中断int 0x80实现系统调用 只有在硬件不支持快速系统调用(sysenter / syscall)的时候才会调用,而我们使用的的硬件目前都支持快速系统调用,所以为了能够看看 int 0x80 的效果,所以也就找了这段代码学习。

这段代码首先通过 int 0x80 调用系统调用 open 得到 fd (由 eax 返回),再作为 read 的参数传入,从而读出了文件中的内容。

执行下看看

[hezz coding_test 15:37]$touch /tmp/test
[hezz coding_test 15:37]$vi /tmp/test #写入hello world!
[hezz coding_test 15:37]$gcc syscall_asm.c
[hezz coding_test 15:37]$./a.out 
13
hello world!

3 总结

传统系统调用(int 0x80) 通过中断/异常实现,在执行 int 指令时,发生 trap,陷入内核,切换至内核态;然后硬件根据寄存器中参数在中断描述符表中的对应表项表项,返回文件描述符(fd)。
最后硬件将 ss / esp / eflags / cs / eip / error code 依次压到内核栈。返回时,iret 将先前压栈的 ss / esp / eflags / cs / eip 弹出,恢复用户态调用时的寄存器上下文。
在这里插入图片描述
另外关于驱动设备文件file_operation对应函数是可以自己实现的,当我们打开驱动设备文件后,就建立了文件中struct filestruct file_operation对应的关系。我们以write系统调用为例看一下:
文件:fs/read.write.c

SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
		size_t, count)
{
	struct fd f = fdget_pos(fd);
	ssize_t ret = -EBADF;

	if (f.file) {
		loff_t pos = file_pos_read(f.file);
		ret = vfs_write(f.file, buf, count, &pos);
		if (ret >= 0)
			file_pos_write(f.file, pos);
		fdput_pos(f);
	}

	return ret;
}

关键代码在vfs_write.

ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
	ssize_t ret;

	if (!(file->f_mode & FMODE_WRITE))
		return -EBADF;
	if (!(file->f_mode & FMODE_CAN_WRITE))
		return -EINVAL;
	if (unlikely(!access_ok(VERIFY_READ, buf, count)))
		return -EFAULT;

	ret = rw_verify_area(WRITE, file, pos, count);
	if (!ret) {
		if (count > MAX_RW_COUNT)
			count =  MAX_RW_COUNT;
		file_start_write(file);
		ret = __vfs_write(file, buf, count, pos);
		if (ret > 0) {
			fsnotify_modify(file);
			add_wchar(current, ret);
		}
		inc_syscw(current);
		file_end_write(file);
	}

	return ret;
}

继续跟入__vfs_write

ssize_t __vfs_write(struct file *file, const char __user *p, size_t count,
		    loff_t *pos)
{
	if (file->f_op->write)
		return file->f_op->write(file, p, count, pos);
	else if (file->f_op->write_iter)
		return new_sync_write(file, p, count, pos);
	else
		return -EINVAL;
}

if (file->f_op->write)
return file->f_op->write(file, p, count, pos);

关键就是这一句,如果我们打开的驱动设备文件,有没有自己实现的write函数,如果有则调用该驱动自定义的write函数。

参考
The Definitive Guide to Linux System Calls
System call
SYSCALL_DEFINE3 宏定义

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值