专栏首页LINUX阅码场Linux内核之旅/张凯捷——系统调用分析(2)

Linux内核之旅/张凯捷——系统调用分析(2)

在《系统调用分析(1)》Linux内核之旅/张凯捷——系统调用分析(1)中,首先介绍了系统调用的概念,并对早期通过软中断(int 80)来进行系统调用的相关过程进行了分析,最后分析和介绍了为了提高系统调用的响应执行速度的两种机制——vsyscall和vDSO。

本篇文章将介绍和分析在指令层面上对系统调用响应速度的优化——快速系统调用指令,32位下使用的sysenter/sysexit;64位下使用的syscall/sysret,以及linux内核中为了支持这些快速系统调用指令所做的相关操作。并且在linux-4.20内核,glibc-2.23版本环境下编写了用户态系统调用程序并对程序运行追踪分析。

sysenter / sysexit

0

3

之前说到的vsyscalls和vDSO都是从机制上对系统调用速度进行的优化,但是使用软中断来进行系统调用需要进行特权级的切换这一根本问题没有解决。

为了解决这一问题,Intel x86 CPU从Pentium II (Family6, Model 3, Stepping 3)之后,开始支持快速系统调用指令sysenter/sysexit

在《英特尔®64和IA-32架构软件开发人员手册合并卷》中可以找到sysenter指令的相关描述:

Executes a fast call to a level 0 system procedure or routine. SYSENTER is a companion instruction to SYSEXIT. The instruction is optimized to provide the maximum performance for system calls from user code running at privilege level 3 to operating system or executive procedures running at privilege level 0. When executed in IA-32e mode, the SYSENTER instruction transitions the logical processor to 64-bit mode; otherwise, the logical processor remains in protected mode. Prior to executing the SYSENTER instruction, software must specify the privilege level 0 code segment and code entry point, and the privilege level 0 stack segment and stack pointer by writing values to the following MSRs: • IA32_SYSENTER_CS (MSR address 174H) — The lower 16 bits of this MSR are the segment selector for the privilege level 0 code segment. This value is also used to determine the segment selector of the privilege level 0 stack segment (see the Operation section). This value cannot indicate a null selector. • IA32_SYSENTER_EIP (MSR address 176H) — The value of this MSR is loaded into RIP (thus, this value references the first instruction of the selected operating procedure or routine). In protected mode, only bits 31:0 are loaded. • IA32_SYSENTER_ESP (MSR address 175H) — The value of this MSR is loaded into RSP (thus, this value contains the stack pointer for the privilege level 0 stack). This value cannot represent a non-canonical address. In protected mode, only bits 31:0 are loaded. These MSRs can be read from and written to using RDMSR/WRMSR. The WRMSR instruction ensures that the IA32_SYSENTER_EIP and IA32_SYSENTER_ESP MSRs always contain canonical addresses.

主要信息有:

(1)sysenter与sysexit指令配套,可以以比较高的执行效率在用户态执行要在系统态执行的系统调用。

(2)在IA-32e模式下执行时,sysenter指令将逻辑处理器转换为64位模式,否则逻辑处理器保持在保护模式。

(3)执行sysenter指令之前,需要将下列MSR(Model Specific Registers)中写入值来指定Ring0代码段、代码入口点、Ring0堆栈段和堆栈指针:

- IA32_SYSENTER_CS(174H):指定要执行Ring0代码的代码段选择符,也能得出目标Ring0所用堆栈段的段选择符

- IA32_SYSENTER_EIP(176H):指定要执行的Ring0代码的起始地址

- IA32_SYSENTER_ESP(175H):指定要执行的Ring0代码所使用的栈指针

(4)使用rdmsr/wrmsr读取和写入MSR

下面基于linux-2.6.39内核进行分析:

3.1 系统调用初始化

从linux内核启动流程入手:start_kernel() -> chenk_bugs() -> identify_boot_cpu() -> sysenter_setup() & enable_sep_cpu()

3.1.1 页面初始化和映射

首先执行sysenter_setup()函数来支持之前提到的vDSO机制,

将vdso32-sysenter.so动态链接库装载进vsyscall页中,在arch/x86/vdso/vdso32-setup.c可以找到sysenter_setup()函数:

int __init sysenter_setup(void)

sysenter_setup()函数的主要工作:

(1)调用get_zeroed_page()获得一个被填充为0的物理页,返回该页在内核地址空间的线性地址。

(2)调用宏virt_to_page得到syscall_page地址对应的page管理结构地址并赋值给vdso32_page[0]。

(3)随后判断支持哪些指令,从而做不同处理,可以看到优先级是syscall > sysenter > int80。

(4)将vdso32_sysenter_start地址赋给vsyscall,然后用memcpy()将vsyscall拷贝到对应的页,最后用relocate_vdso()进行重定向。

arch/x86/vdso/vdso32.S中可以看到vdso32_sysenter_start就是vdso32-sysenter.so:

vdso32_sysenter_start:

即将vdso32-sysenter.so拷贝到对应的页中,在《系统调用分析(1)》的vDSO介绍中提到的arch_setup_additional_pages函数便是把拷贝到的页的内容映射到用户空间。

3.1.2 相关MSR寄存器的初始化

arch/x86/vdso/vdso32-setup.c中的enable_sep_cpu()函数完成相关MSR寄存器的初始化:

void enable_sep_cpu(void)

主要内容:

(1) MSR_IA32_SYSENTER_*的声明在arch/x86/include/asm/msr-index.h中,可以看到对应的MSR寄存器地址:

#define MSR_IA32_SYSENTER_CS     0x00000174

(2)将__KERNEL_CS设置到MSR_IA_SYSENTER_CS中。

(3)将tss->x86_tss.sp1栈地址设置到MSR_IA32_SYSENTER_ESP中。

(4)将ia32_sysenter_target(sysenter指令的接口函数)设置到MSR_IA32_SYSENTER_EIP。

3.2 sysenter和sysexit的指令操作

在Ring3的代码调用sysenter指令之后,CPU做出如下操作:

  1. 将SYSENTER_CS_MSR的值装在到cs寄存器。
  2. 将SYSENTER_EIP_MSR的值装在到eip寄存器。
  3. 将SYSENTER_CS_MSR的值加8(Ring0的堆栈段描述符)装载到ss寄存器。
  4. 将SYSENTER_ESP_MSR的值装载到esp寄存器。
  5. 将特权级切换到Ring0。
  6. 如果EFLAGS寄存器的VM标志被置位,则清除该标志。
  7. 开始执行指定的Ring0代码。

在Ring0代码执行完毕,调用sysexit指令退回Ring3时,CPU会做出如下操作:

  1. 将SYSENTER_CS_MSR的值加16(Ring3的代码段描述符)装载到cs寄存器。
  2. 将寄存器edx的值装载到eip寄存器。
  3. 将SYSENTER_CS_MSR的值加24(Ring3的堆栈段描述符)装载到ss寄存器。
  4. 将寄存器ecx的值装载到esp寄存器。
  5. 将特权级切换到Ring3。
  6. 继续执行Ring3的代码。

3.3 sysenter的系统调用处理

3.3.1 linux2.6.39内核sysenter系统调用

正如刚才对IA32_SYSENTER_EIP寄存器中传入sysenter的系统调用函数入口地址ia32_sysenter_target。

arch/x86/ia32/ia32entry.S中可以看到sysenter指令所要执行的系统调用处理程序ia32_sysenter_target的代码,其中执行系统调用的代码是:

sysenter_dispatch:

可以看到sysenter指令会直接到系统调用表中找到相应系统调用处理程序去执行。

3.3.2 linux4.20内核sysenter系统调用

在linux4.20内核中,对IA32_SYSENTER_EIP寄存器中传入的是entry_SYSENTER_32函数。

arch/x86/entry/entry_32.S中可以看到entry_SYSENTER_32()函数:

ENTRY(entry_SYSENTER_32)
   pushfl
   pushl   %eax
   BUG_IF_WRONG_CR3 no_user_check=1
   SWITCH_TO_KERNEL_CR3 scratch_reg=%eax
   popl    %eax
   popfl
   movl    TSS_entry2task_stack(%esp), %esp
   
.Lsysenter_past_esp:
   pushl   $__USER_DS      /* pt_regs->ss */
   pushl   %ebp            /* pt_regs->sp (stashed in bp) */
   pushfl              /* pt_regs->flags (except IF = 0) */
   orl $X86_EFLAGS_IF, (%esp)  /* Fix IF */
   pushl   $__USER_CS      /* pt_regs->cs */
   pushl   $0          /* pt_regs->ip = 0 (placeholder) */
   pushl   %eax            /* pt_regs->orig_ax */
   SAVE_ALL pt_regs_ax=$-ENOSYS    /* save rest, stack already switched */
   testl   $X86_EFLAGS_NT|X86_EFLAGS_AC|X86_EFLAGS_TF, PT_EFLAGS(%esp)
   jnz .Lsysenter_fix_flags
   
.Lsysenter_flags_fixed:
   TRACE_IRQS_OFF
   movl    %esp, %eax
   call    do_fast_syscall_32
...
   sysexit
...

entry_SYSENTER_32()函数主要工作:

(1)之前说到sysenter指令会将SYSENTER_ESP_MSR的值装载到esp寄存器,但是里面保存的是sysenter_stack的地址,所以通过movl TSS_entry2task_stack(%esp), %esp语句,修正esp寄存器保存进程的内核栈。

(2)SAVE_ALL和pushl等操作将相关寄存器压栈,保存现场。

(3)调用do_fast_syscall_32 -> do_syscall_32_irqs_on() 从系统调用表中找到相应处理函数进行调用。

(4)最后popl相关寄存器返回现场,调用sysexit指令返回。

syscall / sysret

0

4

在32位下Intel提出快速系统调用指令sysenter/sysexit,AMD提出syscall/sysret,到64位时统一使用syscall指令。

在《英特尔®64和IA-32架构软件开发人员手册合并卷》可以找到syscall指令的相关信息:

图 4-1 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. However, the CS and SS descriptor caches are not loaded from the descriptors (in GDT or LDT) referenced by those selectors. Instead, the descriptor caches are loaded with fixed values. See the Operation section for details. It is the responsibility of OS software to ensure that the descriptors (in GDT or LDT) referenced by those selector values correspond to the fixed values loaded into the descriptor caches; the SYSCALL instruction does not ensure this correspondence. The SYSCALL instruction does not save the stack pointer (RSP). If the OS system-call handler will change the stack pointer, it is the responsibility of software to save the previous value of the stack pointer. This might be done prior to executing SYSCALL, with software restoring the stack pointer with the instruction following SYSCALL (which will be executed after SYSRET). Alternatively, the OS system-call handler may save the stack pointer and restore it before executing SYSRET.

4.1 系统调用追踪

基于linux-4.20内核,glibc-2.23版本,编写用户态程序进行系统调用,使用gdb追踪运行调用过程, 分析过程如下:

(1)编写包含系统调用的程序:

#include <fcntl.h>

(2)编译生成可执行文件:

$ gcc -o open -g -static open.c
$ file openopen: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=c7781966fa4acbecf2b489a3eea145912f3f81d0, not stripped

(3)gdb调试跟踪:

Dump of assembler code for function main:

(4)可以看到系统调用open要执行的是<open64+29>, open64定义在glibc源码路

sysdeps/posix/open64.c中:

#include <fcntl.h>

(5)看到其实是执行__lib_open,__libc_open定义在glibc源码路径sysdeps/unix/sysv/linux/generic/open.c:

{

(6)最后执行到SYSCALL_CANCEL宏,glibc源码路径sysdeps/unix/sysdep.h里有着SYSCALL_CANCEL宏定义:

INLINE_SYSCALL (name, 4, a1, a2, a3, a4, a5)

根据相关宏定义展开:

SYSCALL_CANCEL (openat, AT_FDCWD, file, oflag, mode);

(7)INLINE_SYSCALL之后宏定义与硬件和os有关,在glibc源码路径sysdeps/unix/sysv/linux/x86_64/sysdep.h中定义:

# define INLINE_SYSCALL(name, nr, args...) \

根据相关宏定义展开:

INLINE_SYSCALL(openat, 4, AT_FDCWD, file, oflag, mode)
-> INTERNAL_SYSCALL(openat,  , 4, AT_FDCWD, file, oflag, mode)
-> INTERNAL_SYSCALL_NCS(__NR_openat,  , 4, AT_FDCWD, file, oflag, mode )

(8)经过一系列展开,最终到达INTERNAL_SYSCALL_NCS:

# define INTERNAL_SYSCALL_NCS(name, err, nr, args...) \

(9)可以看到LOAD_ARGS_##nr把参数args展开,LOAD_REGS_##nr设置相应参数到相应地寄存器中,汇编嵌入调用syscall指令执行系统调用。

4.2 syscall系统调用初始化

基于linux-4.20内核源码进行分析:

syscall系统调用初始化在内核启动执行路径中:start_kernel() -> trap_init() -> cpu_init() -> syscall_init()。

arch/x86/kernel/cpu/common.c中可以看到syscall_init()函数:

void syscall_init(void)

syscall_init()函数源码可以看到对相应地MSR寄存器进行初始化:

(1)向MSR_STAR的32 ~ 47位写入内核态的cs,向48 ~ 64位设置用户态的cs。

(2)向MSR_LSTAR写入entry_SYSCALL_64函数入口地址。

4.3 执行syscall

执行syscall,会跳转到entry_SYSCALL_64,在arch/x86/entry/entry_64.S中可以找到entry_SYSCALL_64:

swapgs    movq    %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)

(1)保存现场将相关寄存器中的值压栈,包括:

- 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

(2)调用do_syscall_64来继续执行,在arch/x86/entry/common.c中:

__visible void do_syscall_64(unsigned long nr, struct pt_regs *regs)

syscall_trace_enter取出系统调用号 nr;到sys_call_table中去找到nr号对应的系统调用服务程序去执行后返回值放入ax。

(3)全部执行完毕后会调用USERGS_SYSRET64返回:

USERGS_SYSRET64

本文对32位和64位下的快速系统调用指令进行了介绍和分析,通过对用户态进行系统调用的程序执行过程追踪,以及对linux-2.6.39和linux-4.20内核源码中支持快速系统调用相关部分进行分析,了解了进行系统调用的执行过程和内核对快速系统调用的相关操作。

下篇将基于Linux-5.0-rc2内核,添加系统调用,完成一个”系统调用日志收集系统“,并对系统调用分析进行总结。 ----

本文分享自微信公众号 - Linux阅码场(LinuxDev),作者:张凯捷

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-01-24

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • From High Ceph Latency to Kernel Patch with eBPF/BCC

    There are a lot of tools for debugging kernel and userspace programs in Linux. M...

    Linux阅码场
  • 广成子:值得收藏-史上最全Linux ps命令详解

      大概在十多年前,我当时还是一个产品经理。由于一些工作的原因,需要向运维工程师学习一些linux常用命令。当使用linux ps这个十分常用的命令时,遇到了一...

    Linux阅码场
  • 宋宝华: Linux中的1024——给阅码场Linuxer们的节日祝福

    1024是程序员的狂欢节。基于二进制的原理,程序员通常会把1024当做一个整数而不是1000。程序员这个行业处理“bit”,当然这个行业“苦逼”,这也让我轻松地...

    Linux阅码场
  • 一条长sql的排错过程

    过程 有这样一条长sql,由于环境原因,对select.....in...... 语法限制使用,因此以left join语法代替,原来只需要统计一天的数据汇总...

    java达人
  • Cordova 运行 Web 应用

    Cordova 非常的流行,因为它可以让 Web 开发人员来创建移动应用, 而且还可以通过 JavaScript 来调用设备硬件 API (GPS、蓝牙等)。

    beginor
  • UE4学习笔记(七): AI

    逍遥剑客
  • 如何在CentOS 7上安装和加固Memcached

    像Memcached这样的内存对象缓存系统可以通过在内存中临时存储信息,保留频繁或最近请求的记录来优化后端数据库性能。通过这种方式,它们可以减少对数据库的直接请...

    GeekZ
  • Python新手教程:40行python代码写一个桌面翻译器

    master = Tk() # 实例过程 master.title('ZZQ--翻译软件') # 标题命名 master.geometry('400x96+41...

    一墨编程学习
  • MongoDB新增字段,删除字段

    我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invi...

    十月梦想
  • 0712-6.2.0-HBase快照异常

    这个问题是由于CDH6.2.0上在进行HBase Snapshot Restore的过程中,会先进行is_enabled的操作。但假如这个表是已经被drop掉的...

    Fayson

扫码关注云+社区

领取腾讯云代金券