前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Linux内核之旅/张凯捷——系统调用分析(2)

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

作者头像
Linux阅码场
发布2019-10-08 15:59:50
1.9K0
发布2019-10-08 15:59:50
举报
文章被收录于专栏:LINUX阅码场

在《系统调用分析(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()函数:

代码语言:javascript
复制
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:

代码语言:javascript
复制
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寄存器的初始化:

代码语言:javascript
复制
void enable_sep_cpu(void)

主要内容:

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

代码语言:javascript
复制
#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的代码,其中执行系统调用的代码是:

代码语言:javascript
复制
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()函数:

代码语言:javascript
复制
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)编写包含系统调用的程序:

代码语言:javascript
复制
#include <fcntl.h>

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

代码语言:javascript
复制
$ 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调试跟踪:

代码语言:javascript
复制
Dump of assembler code for function main:

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

sysdeps/posix/open64.c中:

代码语言:javascript
复制
#include <fcntl.h>

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

代码语言:javascript
复制
{

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

代码语言:javascript
复制
INLINE_SYSCALL (name, 4, a1, a2, a3, a4, a5)

根据相关宏定义展开:

代码语言:javascript
复制
SYSCALL_CANCEL (openat, AT_FDCWD, file, oflag, mode);

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

代码语言:javascript
复制
# define INLINE_SYSCALL(name, nr, args...) \

根据相关宏定义展开:

代码语言:javascript
复制
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:

代码语言:javascript
复制
# 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()函数:

代码语言:javascript
复制
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:

代码语言:javascript
复制
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中:

代码语言:javascript
复制
__visible void do_syscall_64(unsigned long nr, struct pt_regs *regs)

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

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

代码语言:javascript
复制
USERGS_SYSRET64

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

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-01-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Linux阅码场 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 3.1 系统调用初始化
  • 3.3 sysenter的系统调用处理
    • 3.3.2 linux4.20内核sysenter系统调用
    • 4.2 syscall系统调用初始化
    • 4.3 执行syscall
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档