天天看点

Linux Kernel源码阅读: x86-64 系统调用实现细节(一)

作者:码农之心

特别说明:该文章前两天发布过,但一直在审核中。看头条网友说字数太多可能一直处于审核中状态,我把该文章拆分成几个章节发布,如影响阅读体验还请见谅。

0、前言

本文采用Linux 内核 v3.10 版本

本文不涉及调试、跟踪及异常处理的细节

一、系统调用简介

系统调用是用户空间程序与内核交互的主要机制。系统调用与普通函数调用不同,因为它调用的是内核里的代码。使用系统调用时,需要特殊指令以使处理器权限转换到内核态。另外,被调用的内核代码由系统调用号来标识,而不是函数地址。

系统调用整体流程如下图所示:

Linux Kernel源码阅读: x86-64 系统调用实现细节(一)

二、从 Hello world 说起

我们以一个 Hello world 程序开始,逐步进入系统调用的学习。下面是用汇编代码写的一个简单的程序:

.section .data
 msg:
     .ascii "Hello World!\n"
 len = . - msg
 
 .section .text
 .globl  main
 main:
 
     # ssize_t write(int fd, const void *buf, size_t count)
     mov $1, %rdi            # fd
     mov $msg, %rsi          # buffer
     mov $len, %rdx          # count
     mov $1, %rax            # write(2)系统调用号,64位系统为1
     syscall
 
     # exit(status)
     mov $0, %rdi            # status
     mov $60, %rax           # exit(2)系统调用号,64位系统为60
     syscall           

编译并运行:

$ gcc -o helloworld helloworld.s 
 $ ./helloworld
 Hello world!
 $ echo $?
 0           

上面这段代码,是直接从我的一篇文章 使用 GNU 汇编语法编写 Hello World 程序的三种方法拷贝过来的。那篇文章里还提到了使用int 0x80软中断和printf函数实现输出的方法,有兴趣的可以去看下。

三、系统调用约定

代码虽然正确运行了,但是我们得知道为什么这么写。x86-64 ABI文档 第A.2.1节,描述了调用约定:

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:

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.

A system-call is done via the syscall instruction. The kernel clobbers registers %rcx and %r11 but preserves all other registers except %rax.

The number of the syscall has to be passed in register %rax.

System-calls are limited to six arguments, no argument is passed directly on the stack.

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.

Only values of class INTEGER or class MEMORY are passed to the kernel.

可以看出,系统调用约定了以下几个方面:

  • 参数相关
  • 系统调用号
  • 系统调用指令
  • 返回值及错误码

3.1 系统调用的入参

3.1.1 参数顺序

当使用 syscall进行系统调用时,参数与寄存器的对应关系如下图所示:

参数1 参数2 参数3 参数4 参数5 参数6
%rdi %rsi %rdx %r10 %r8 %r9

该对应关系也可以从 arch/x86/entry/entry_64.S 里找到。

/*
  * 64-bit SYSCALL instruction entry. Up to 6 arguments in registers.
  *
  * This is the only entry point used for 64-bit system calls.  The
  * hardware interface is reasonably well designed and the register to
  * argument mapping Linux uses fits well with the registers that are
  * available when SYSCALL is used.
  *
  * SYSCALL instructions can be found inlined in libc implementations as
  * well as some other programs and libraries.  There are also a handful
  * of SYSCALL instructions in the vDSO used, for example, as a
  * clock_gettimeofday fallback.
  *
  * 64-bit SYSCALL saves rip to rcx, clears rflags.RF, then saves rflags to r11,
  * then loads new ss, cs, and rip from previously programmed MSRs.
  * rflags gets masked by a value from another MSR (so CLD and CLAC
  * are not needed). SYSCALL does not save anything on the stack
  * and does not change rsp.
  *
  * Registers on entry:
  * rax  system call number
  * rcx  return address
  * r11  saved rflags (note: r11 is callee-clobbered register in C ABI)
  * rdi  arg0
  * rsi  arg1
  * rdx  arg2
  * r10  arg3 (needs to be moved to rcx to conform to C ABI)
  * r8   arg4
  * r9   arg5
  * (note: r12-r15, rbp, rbx are callee-preserved in C ABI)
  *
  * Only called from user space.
  *
  * When user can change pt_regs->foo always force IRET. That is because
  * it deals with uncanonical addresses better. SYSRET has trouble
  * with them due to bugs in both AMD and Intel CPUs.
  */           

3.1.2 参数数量

系统调用参数限制为6个。

3.1.3 参数类型

参数类型限制为 INTEGER 和 MEMORY。这里的类型是x86-64 ABI 里定义的概念,可以在第3.2.3节 Parameter Passing看到具体的描述:

INTEGER This class consists of integral types that fifit into one of the general purpose registers.

MEMORY This class consists of types that will be passed and returned in memory via the stack.

3.2 返回值及错误码

当从系统调用返回时,%rax里保存着系统调用结果;如果是-4095 至 -1之间的值,表示调用过程中发生了错误。

3.3 系统调用号

系统调用号通过%rax传递。

3.4 系统调用指令

系统调用通过指令syscall来执行。

Intel 64 and IA-32 Architectures Software Developer Manuals(以下简称 Intel SDM ) Volume 2B 第 4.3 节对 syscall指令的描述如下:

SYSCALL invokes an OS system-call handler at privilege level 0. It does so by loading RIP from the IA32_LSTAR MSR (after saving the address of the instruction following SYSCALL into RCX). (The WRMSR instruction ensures that the IA32_LSTAR MSR always contain a canonical address.)

SYSCALL also saves RFLAGS into R11 and then masks RFLAGS using the IA32_FMASK MSR (MSR address C0000084H); specifically, the processor clears in RFLAGS every bit corresponding to a bit that is set in the IA32_FMASK MSR.

SYSCALL loads the CS and SS selectors with values derived from bits 47:32 of the IA32_STAR MSR.

根据说明,执行syscall指令时,会进行以下操作:

  • 把syscall指令的下一条指令(也就是返回地址)存入 %rcx 寄存,然后把指令指针寄存器 %rip 替换成IA32_LSTAR MSR寄存器里的值。
  • 把 rflags 标志寄存器的值保存到 %r11,然后把 rflags 的值与 IA32_FMASK MSR 里的值做掩码运算。
  • 把 IA32_STAR MSR寄存器里第32~47位加载到 CS 和 SS 段寄存器。

总之,就是先保存现场,然后跳转到IA32_LSTAR(Long system target address register) MSR(Model specific register)寄存器指定的地址上去。

那么这个地址是什么时候存入IA32_LSTAR MSR中去的呢?

四、系统调用初始化

在Linux启动之时,会进行一系列的初始化过程。其中,系统调用的初始化在文件arch/x86/kernel/cpu/common.c中:

// file: arch/x86/kernel/cpu/common.c
 void syscall_init(void)
 {
     /*
      * LSTAR and STAR live in a bit strange symbiosis.
      * They both write to the same internal register. STAR allows to
      * set CS/DS but only a 32bit target. LSTAR sets the 64bit rip.
      */
     wrmsrl(MSR_STAR,  ((u64)__USER32_CS)<<48  | ((u64)__KERNEL_CS)<<32);
     wrmsrl(MSR_LSTAR, system_call);
     wrmsrl(MSR_CSTAR, ignore_sysret);
     
     ......
 
     /* Flags to clear on syscall */
     wrmsrl(MSR_SYSCALL_MASK,
            X86_EFLAGS_TF|X86_EFLAGS_DF|X86_EFLAGS_IF|
            X86_EFLAGS_IOPL|X86_EFLAGS_AC);
 }           

4.1 MSRs

在64位模式下,x86 CPU 提供了以下几个寄存器来配合系统调用相关指令使用:

• IA32_KERNEL_GS_BASE — Used by SWAPGS instruction.

• IA32_LSTAR — Used by SYSCALL instruction.

• IA32_FMASK — Used by SYSCALL instruction.

• IA32_STAR — Used by SYSCALL and SYSRET instruction.

这四种MSR寄存器的说明如下,详见Intel SDM Volume 4 第2.1节。

Register Address Architectural MSR Name / Bit Fields MSR/Bit Description Comment
C000_0081H IA32_STAR System Call Target Address (R/W) If CPUID.80000001:EDX.[29] = 1
C000_0082H IA32_LSTAR IA-32e Mode System Call Target Address (R/W) Target RIP for the called procedure when SYSCALL is executed in 64-bit mode. If CPUID.80000001:EDX.[29] = 1
C000_0083H IA32_CSTAR IA-32e Mode System Call Target Address (R/W) Not used, as the SYSCALL instruction is not recognized in compatibility mode. If CPUID.80000001:EDX.[29] = 1
C000_0084H IA32_FMASK System Call Flag Mask (R/W) If CPUID.80000001:EDX.[29] = 1
C000_0102H IA32_KERNEL_GS_BASE Swap Target of BASE Address of GS (R/W) If CPUID.80000001:EDX.[29] = 1

系统调用初始化时,使用了MSR_STAR、MSR_LSTAR、MSR_CSTAR、MSR_SYSCALL_MASK这四个宏,它们定义在arch/x86/include/uapi/asm/msr-index.h头文件中。可以看到,这四个宏定义的是寄存器的地址:

// file: arch/x86/include/uapi/asm/msr-index.h
 /* CPU model specific register (MSR) numbers */
 
 /* x86-64 specific MSRs */
 #define MSR_STAR        0xc0000081 /* legacy mode SYSCALL target */
 #define MSR_LSTAR       0xc0000082 /* long mode SYSCALL target */
 #define MSR_CSTAR       0xc0000083 /* compat mode SYSCALL target */
 #define MSR_SYSCALL_MASK    0xc0000084 /* EFLAGS mask for syscall */           

4.2 段选择子

另外,__USER32_CS 和 __KERNEL_CS 宏定义在arch/x86/include/asm/segment.h 头文件中。其中,__USER32_CS 和 __KERNEL_CS分别为用户态代码段选择子和内核态代码段选择子。__USER32_CS宏引用了GDT_ENTRY_DEFAULT_USER32_CS宏,该宏是用户态代码段在GDT(Global Descriptor Table)中的索引。__KERNEL_CS宏引用了GDT_ENTRY_KERNEL_CS宏,该宏是内核态代码段在GDT中的索引。

// file: arch/x86/include/asm/segment.h
 #define GDT_ENTRY_KERNEL_CS 2
 #define GDT_ENTRY_DEFAULT_USER32_CS 4
 #define __USER32_CS   (GDT_ENTRY_DEFAULT_USER32_CS*8+3)
 #define __KERNEL_CS (GDT_ENTRY_KERNEL_CS*8)           

可以看到,内核态段选择子等于 GDT索引*8,而用户态段选择子等于GDT索引*8+3,这是由段描述符的结构决定的。在x86架构中,段寄存器和段选择子都是16位的,但是这16位并不是全部用来存储索引值,而是由三部分组成:

  • RPL(Requested Privilege Level)位。段选择子最低2位(位0~1)称为请求权限级别位,保存的是段权限级别;因为RPL有2位,可以有0~3四种权限,目前Linux只使用到了0和3这两个级别,其中内核程序运行在0级别,用户程序运行在3级别。
  • TI位,即表指示位(Table Indicator Flag)。段选择子的位2是TI位,TI位用来指示段的保存位置:是保存在全局描述符表GDT中,还是在本地描述符表LDT(Local Descriptor Table )中。当TI位为1时,表示在LDT中,当TI位为0时,表示在GDT中。
  • 位3~15,才是真正保存索引的位置。

从以上分析可知,段描述符最低3位有其他用途不能用来存放索引,所以要把索引值左移3位(相当于乘以8)才能放到索引区。另外,因为用户态的权限级别为3,我们看到所有的用户段都要加3,相当于把用户态的RPL级别硬编码到程序里了。

段选择子的位分布情况见下图,详细信息请查阅Intel SDM Volume 3A:第3.42 Segment Selectors节。

Linux Kernel源码阅读: x86-64 系统调用实现细节(一)

4.3 wrmsr指令

根据Intel SDM Volume 2D文档的描述,wrmsr指令会把%edx:%eax的值写入指定的64位 MSR 寄存器中,具体写入哪个寄存器,是通过 %ecx 指定的。%edx的值存入MSR中的高32位,%eax的值存入MSR的低32位。在64位系统中,这三个寄存器的高32位会被忽略。

wrmsrl宏是对wrmsr指令的封装,其参数msr指定了要保存的MSR寄存器,参数val是要保存的内容,其中val的高32位保存到%edx,低32位保存到%eax。

4.4 初始化过程

wrmsrl(MSR_STAR,  ((u64)__USER32_CS)<<48  | ((u64)__KERNEL_CS)<<32);           

这行指令把用户代码段选择子(__USER32_CS)写入MSR_STAR[48:63],把内核代码段选择子(__KERNEL_CS)写入MSR_STAR[32:47]。

其中__KERNEL_CS是给syscall指令使用的。执行syscall指令时,要从用户态切换到内核态,CPU 会根据__KERNEL_CS来更新代码段寄存器%cs和栈段寄存器%ss,伪代码如下(详细内容请参考 Intel SDM Vol. 2B 中 syscall指令):

CS.Selector := IA32_STAR[47:32] AND FFFCH (* Operating system provides CS; RPL forced to 0 *)

SS.Selector := IA32_STAR[47:32] + 8; (* SS just above CS *)

__USER32_CS是给sysret指令用的。执行sysret指令时,需要从内核态切换到用户态,cpu会根据__USER32_CS来更新%cs和%ss,伪代码如下(详细内容请参考 Intel SDM Vol. 2B 中 sysret指令):

IF (operand size is 64-bit)

THEN CS.Selector := IA32_STAR[63:48]+16;

ELSE CS.Selector := IA32_STAR[63:48];

FI;

CS.Selector := CS.Selector OR 3; (* RPL forced to 3 *)

SS.Selector := (IA32_STAR[63:48]+8) OR 3; (* RPL forced to 3 *)

wrmsrl(MSR_LSTAR, system_call);           

这行代码把system_call入口地址存入到MSR_LSTAR寄存器。syscall指令会把该地址加载到到%rip寄存器,从该地址开始执行。

/* Flags to clear on syscall */
     wrmsrl(MSR_SYSCALL_MASK,
            X86_EFLAGS_TF|X86_EFLAGS_DF|X86_EFLAGS_IF|
            X86_EFLAGS_IOPL|X86_EFLAGS_AC);           

这几行代码,定义了EFLAGS掩码位,并把它们保存到MSR_SYSCALL_MASK寄存器。syscall指令执行时,凡是MSR_SYSCALL_MASK中置位的标志位,都会从EFALGS中清除,伪代码如下:

RFLAGS := RFLAGS AND NOT(IA32_FMASK);

特别说明一下,因为初始化时,掩码中包含中断标志位X86_EFLAGS_IF,所以syscall指令执行时,中断是禁止的。

关联链接:

Linux Kernel源码阅读: x86-64 系统调用实现细节(二)

Linux Kernel源码学习: x86-64 系统调用实现细节(完结篇)

继续阅读