0
点赞
收藏
分享

微信扫一扫

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

westfallon 2022-01-09 阅读 87
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的中断处理程序。**让我们看一下实际执行此操作的代码。
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 file , fd_install 将 fd 和 struct 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 ,负责恢复先前压栈的寄存器,返回用户态。系统调用执行完毕。

汇编

在目前主流的系统调用库(glibc) 中,int 0x80 只有在硬件不支持快速系统调用(sysenter / syscall)的时候才会调用,但目前的硬件都支持快速系统调用,所以为了能够看看 int 0x80 的效果,我们手撸汇编:

#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);
}

这段代码首先通过 int 0x80 调用系统调用 open 得到 fd (由 eax 返回),再作为 read 的参数传入,从而读出了文件中的内容。但比较奇怪的是如果 buffer 存储在栈中 (buffer[80]),则调用 read 失败。只有将 buffer 作为全局变量或存储在堆中,才能调用成功。希望有知道的大大指点一下。

执行下看看

[hezz coding_test 15:37]$touch /tmp/test
[hezz coding_test 15:37]$vi /tmp/test 
[hezz coding_test 15:37]$gcc syscall_asm.c
[hezz coding_test 15:37]$./a.out 
4
111

总结

传统系统调用(int 0x80) 通过中断/异常实现,在执行 int 指令时,发生 trap。硬件找到在中断描述符表中的表项,在自动切换到内核栈 (tss.ss0 : tss.esp0) 后根据中断描述符的 segment selector 在 GDT / LDT 中找到对应的段描述符,从段描述符拿到段的基址,加载到 cs ,将 offset 加载到 eip。最后硬件将 ss / sp / eflags / cs / ip / error code 依次压到内核栈。返回时,iret 将先前压栈的 ss / sp / eflags / cs / ip 弹出,恢复用户态调用时的寄存器上下文。

举报

相关推荐

0 条评论