IA与Linux任务管理机制

Published: 2021年09月19日

In Kernel.

硬件机制

X86

在IA上,会用TR(Task Register)寄存器指向一个名为任务状态段TSS的结构:

该结构存储了一个任务的部分上下文信息,如下:

在执行到Call/Jmp/IRET或遇到Exception/Interrupt时,会发生任务切换,X86下的TSS的描述符如下(64位的类似只是扩展了地址长度):

它和其他段描述符类似,只简单介绍下几个域:

  1. B(busy):表示当前任务处于Busy状态,当任务被调入后它将变为Busy状态,并在(Call/Exception/Interrupt)下被换出时保持该标志,只有非Busy的任务可以被调入,这可以防止任务出现递归调用。由于IA32任务使用Previous Task Link保存上一个任务,出现递归时只能覆盖上一次的内容从而导致错误,因此任务是无法递归的。
  2. G:为0时表示TSS必须大于等于0x67而小于TSS的最大大小减1。

X86下的TSS基础结构如下,操作系统可在此基础上进行扩展:

它里面存放的是任务的上下文,它们在任务被切出时刷新保存现场,在任务被调入时读出恢复现场,里面的数据可分为两类,动态域与静态域,前者由硬件自动刷新,后者通常由软件修改,图里的大都易懂,这里简单介绍几个:

  1. Previous Task Link:当出现嵌套任务时(Call/Interrupt/Exception)记录被换出的任务的TSS的段选择子,否则应该置0。还有个和嵌套任务有关的域位于EFLAGS,即NT(Nested Task),当进入嵌套任务时它将被置位,IRET返回时会判断它并加载上一个任务。
  2. I/O Map Base Address: 当存在IO映射表时,记录其偏移基址,通过此可实现为非特权任务赋予访问IO的权限。
  3. T: 调试标志,设置后当调入该任务会产生debug异常。
  4. SSP(Shadow Stack Pointer):存储任务的影子栈指针寄存器,影子栈是程序栈的影子,但它只保存返回地址且只有控制流转移指令和特殊指令能读写,程序返回时会判断程序栈和影子栈的值,通过这实现更好的控制流防护。
  5. SS0-2/ESP0-2:指向0-2特权级的栈,为什么没有SS3呢?不公平!!

任务门的描述符如下:

如下三种方式可切换任务,它们都是指向了任务描述符:

通过任务门的方式比较好理解,而通过TR寄存器调用时,其实就是把TR当CS使用,根据它的Type域CPU能识别出是TS,于是就可以进行任务切换:

如使用如下代码跳转:

mov WORD [tss_pointer + 4], <TSS_selector>
jmp FAR [tss_pointer]

X64

x64不支持上述的任务切换方式,任务切换完全由软件实现,但是也需要至少一个TSS结构,如下图:

Linux任务结构

Linux并没有使用CPU提供的任务切换机制,但也逃不过硬件制约,因此它的实现为每CPU一个TSS,这样每次切换任务时通过软件完成上下文的保存(X86和X64再次统一,不过也不知道是谁先来的),据悉原因是软硬件速度差别不大但软件更灵活能实现更多检查... ​

Linux使用task_struct结构来表示一个任务,使用thread_info来存储线程信息,相关结构各版本都有变化,2.6.39的如下:

union thread_union {
    struct thread_info thread_info;
    unsigned long stack[THREAD_SIZE/sizeof(long)];
};

// https://elixir.bootlin.com/linux/v2.6.39/source/arch/x86/include/asm/thread_info.h#L26
struct thread_info {
    struct task_struct  *task;      /* main task structure */
    struct exec_domain  *exec_domain;   /* execution domain */
    __u32           flags;      /* low level flags */
    __u32           status;     /* thread synchronous flags */
    __u32           cpu;        /* current CPU */
    int         preempt_count;  /* 0 => preemptable,
                           <0 => BUG */
    mm_segment_t        addr_limit;
    struct restart_block    restart_block;
    void __user     *sysenter_return;
#ifdef CONFIG_X86_32
    unsigned long           previous_esp;   /* ESP of the previous stack in
                           case of nested (IRQ) stacks
                        */
    __u8            supervisor_stack[0];
#endif
    int         uaccess_err;
};

https://elixir.bootlin.com/linux/v2.6.39/source/include/linux/sched.h#L1193
struct task_struct {
    volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
    void *stack;
    atomic_t usage;
    unsigned int flags; /* per process flags, defined below */
    unsigned int ptrace;

    int lock_depth;     /* BKL lock depth */
    ...
}

#define get_current() (current_thread_info()->task)
#define current get_current()

在这种情况下,三者关系如下图:

Linux任务切换

Linux任务调度系统有很多算法用于决定切换哪个任务,此处只关注怎么切换的,linux-2.6.32\arch\x86\kernel\process_32.c:

__notrace_funcgraph struct task_struct *
__switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{
    struct thread_struct *prev = &prev_p->thread,
                 *next = &next_p->thread;

    // 从每CPU区域获取当前CPU的TSS
    int cpu = smp_processor_id();
    struct tss_struct *tss = &per_cpu(init_tss, cpu);
    bool preload_fpu;

    // 若下一个任务使用MATH并且使用过fpu至少5次说明它很大概率不久会再使用fpu,为了避免到时候trap的切换开销,这里就直接装载它
    preload_fpu = tsk_used_math(next_p) && next_p->fpu_counter > 5;

    // 通过上一个任务的status判断是否使用了fpu,若使用则将当前的fpu保存到prev的上下文,并设置ts,重置status:TS_USEDFPU
    __unlazy_fpu(prev_p);

    // 装载下一个的任务的fpu,否则它将会在第一次使用事由异常处理去装入
    if (preload_fpu)
        prefetch(next->xstate);

    // 将next的sp0存入tss的sp0
    load_sp0(tss, next);

    // 将当前的gs保存到prev的gs域里
    lazy_save_gs(prev->gs);


    // 将next的三个TLS段加载到当前CPU的GDT里
    load_TLS(next, cpu);

    // 若内核不运行在ring0(某些虚拟化环境),popf不会处理这些,此时若下一个任务和上一个任务的iopl不同,则手动装载下一个任务的iopl
    if (get_kernel_rpl() && unlikely(prev->iopl != next->iopl))
        set_iopl_mask(next->iopl);

    // 在必要时处理调试寄存器
    if (unlikely(task_thread_info(prev_p)->flags & _TIF_WORK_CTXSW_PREV ||
             task_thread_info(next_p)->flags & _TIF_WORK_CTXSW_NEXT))
        __switch_to_xtra(prev_p, next_p, tss);

    // 若已经加载了FPU则清除TS(Task Switch)标志,否则它会在再次使用FPU陷入时被清除
    if (preload_fpu)
        clts();

    // 虚拟化时可做的hook
    arch_end_context_switch(next_p);

    // 若已装载FPU则恢复下一个任务的status:TS_USEDFPU并增加fpu_counter计数
    if (preload_fpu)
        __math_state_restore();

    // 若有一个任务使用了gs则加载下一个任务的gs
    if (prev->gs | next->gs)
        lazy_load_gs(next->gs);

    // 将next写入当前CPU的current_task处
    percpu_write(current_task, next_p);

    return prev_p;
}

64位类似,但是在处理段寄存器时存在差异,如下linux-2.6.32\arch\x86\kernel\process_64.c

__notrace_funcgraph struct task_struct *
__switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{
    ...
    savesegment(es, prev->es);
    if (unlikely(next->es | prev->es))
        loadsegment(es, next->es);

    savesegment(ds, prev->ds);
    if (unlikely(next->ds | prev->ds))
        loadsegment(ds, next->ds);

    savesegment(fs, fsindex);  // 此处处理的是索引,选择子
    savesegment(gs, gsindex);

    if (unlikely(fsindex | next->fsindex | prev->fs)) {
        loadsegment(fs, next->fsindex);
        if (fsindex)
            prev->fs = 0;
    }

    if (next->fs)
        wrmsrl(MSR_FS_BASE, next->fs);  // 此处设置的是描述符结构
    prev->fsindex = fsindex;

    if (unlikely(gsindex | next->gsindex | prev->gs)) {
        load_gs_index(next->gsindex);
        if (gsindex)
            prev->gs = 0;
    }

    if (next->gs)
        wrmsrl(MSR_KERNEL_GS_BASE, next->gs);
    prev->gsindex = gsindex;


    prev->usersp = percpu_read(old_rsp);
    percpu_write(old_rsp, next->usersp);
    percpu_write(current_task, next_p);

    percpu_write(kernel_stack,
          (unsigned long)task_stack_page(next_p) +
          THREAD_SIZE - KERNEL_STACK_OFFSET);

    ...
}

系统调用

系统调用的ABI可通过man指令查看,而具体调用号需要看对应版本的syscall_table_32.S文件:

betamao@DESKTOP: ~ $ man syscall
Arch/ABI    Instruction           System  Ret  Ret  Error    Notes
call #  val  val2
───────────────────────────────────────────────────────────────────
i386        int $0x80             eax     eax  edx  -
x86-64      syscall               rax     rax  rdx  -        5
x32         syscall               rax     rax  rdx  -        5

Arch/ABI      arg1  arg2  arg3  arg4  arg5  arg6  arg7  Notes
──────────────────────────────────────────────────────────────
i386          ebx   ecx   edx   esi   edi   ebp   -
x86-64        rdi   rsi   rdx   r10   r8    r9    -
x32           rdi   rsi   rdx   r10   r8    r9    -

注:X32 ABI与X86-64共用系统调用表,为了区别在使用X32时需要在系统调用号上或上__X32_SYSCALL_BIT(0x40000000),另外有些结构长度不能单纯减半,因此它有些独有的版本;用这个可以绕一些沙盒...

int80

它在trap_init中初始化系统调用的:

void __init trap_init(void)
{
    ...
    # define SYSCALL_VECTOR         0x80
    set_system_trap_gate(SYSCALL_VECTOR, &system_call);
    ...
}

其实现如下(删除了所有CFI信息):

ENTRY(system_call)
    SWAPGS_UNSAFE_STACK  ;; SWAPGS_UNSAFE_STACK=swapgs 切换GS
ENTRY(system_call_after_swapgs)

    movq    %rsp,PER_CPU_VAR(old_rsp)  ;; 保存RSP
    movq    PER_CPU_VAR(kernel_stack),%rsp ;; 切换到内核态的栈

    ENABLE_INTERRUPTS(CLBR_NONE)  ;; ENABLE_INTERRUPTS(x)=sti 启用中断
    SAVE_ARGS 8,1  ;; 把参数(RDI,RSI,RDX,<RCX>,RAX,R8,R9,R10,R11)保存在栈里,不保存RCX,并留一个空用于保存orgi_RAX
    movq  %rax,ORIG_RAX-ARGOFFSET(%rsp)  ;; 保存系统调用号
    movq  %rcx,RIP-ARGOFFSET(%rsp) ;; 保存用户态要执行的下一条指令的地址
    GET_THREAD_INFO(%rcx)  ;; 将thread_info地址存入RCX
    testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%rcx)  ;; 若有跟踪进入标志则先跳转到tracesys执行
    jnz tracesys
system_call_fastpath:
    cmpq $__NR_syscall_max,%rax  ;; 判断系统调用号是否合法
    ja badsys
    movq %r10,%rcx
    call *sys_call_table(,%rax,8)  ;; 调用对应的系统调用实现
    movq %rax,RAX-ARGOFFSET(%rsp)  ;; 将返回值存入

ret_from_sys_call:
    movl $_TIF_ALLWORK_MASK,%edi  ;; 在返回用户空间前还需要的做的事的掩码

;; 不能直接返回,需要判断一些标志,此处检测是否还有任务需要处理,没有再返回
sysret_check:
    LOCKDEP_SYS_EXIT  ;; 检测死锁
    GET_THREAD_INFO(%rcx)   ;; 
    DISABLE_INTERRUPTS(CLBR_NONE)  ;; 禁用中断
    TRACE_IRQS_OFF
    movl TI_flags(%rcx),%edx  ;; 获取线程信息标志,存于EDX
    andl %edi,%edx
    jnz  sysret_careful  ;; 说明在返回前还有其他事件需要处理,不能直接返回,判断的点见下文

    TRACE_IRQS_ON
    movq RIP-ARGOFFSET(%rsp),%rcx  ;; 恢复RCX,这里记录了用户态的指令地址
    RESTORE_ARGS 0,-ARG_SKIP,1  ;; 恢复之前保存的所有参数
    movq    PER_CPU_VAR(old_rsp), %rsp  ;; 恢复RSP
    USERGS_SYSRET64  ;; USERGS_SYSRET64 =   swapgs; sysretq; 返回用户空间

sysret_careful:
    bt $TIF_NEED_RESCHED,%edx  ;; 是否需要进行调度
    jnc sysret_signal
    TRACE_IRQS_ON
    ENABLE_INTERRUPTS(CLBR_NONE)
    pushq %rdi
    call schedule  ;; 进入调度
    popq  %rdi
    jmp sysret_check

sysret_signal:
    TRACE_IRQS_ON
    ENABLE_INTERRUPTS(CLBR_NONE)
    FIXUP_TOP_OF_STACK %r11, -ARGOFFSET
    jmp int_check_syscall_exit_work

badsys:
    movq $-ENOSYS,RAX-ARGOFFSET(%rsp)  ;; 直接设置RAX为ENOSYS并退出
    jmp ret_from_sys_call

tracesys:
    SAVE_REST
    movq $-ENOSYS,RAX(%rsp) /* ptrace can change this for a bad syscall */
    FIXUP_TOP_OF_STACK %rdi
    movq %rsp,%rdi
    call syscall_trace_enter
    LOAD_ARGS ARGOFFSET, 1
    RESTORE_REST
    cmpq $__NR_syscall_max,%rax
    ja   int_ret_from_sys_call  /* RAX(%rsp) set to -ENOSYS above */
    movq %r10,%rcx  /* fixup for C */
    call *sys_call_table(,%rax,8)
    movq %rax,RAX-ARGOFFSET(%rsp)
    /* Use IRET because user could have changed frame */

GLOBAL(int_ret_from_sys_call)
    DISABLE_INTERRUPTS(CLBR_NONE)
    TRACE_IRQS_OFF
    testl $3,CS-ARGOFFSET(%rsp)
    je retint_restore_args
    movl $_TIF_ALLWORK_MASK,%edi

GLOBAL(int_with_check)
    LOCKDEP_SYS_EXIT_IRQ
    GET_THREAD_INFO(%rcx)
    movl TI_flags(%rcx),%edx
    andl %edi,%edx
    jnz   int_careful
    andl    $~TS_COMPAT,TI_status(%rcx)
    jmp   retint_swapgs

int_careful:
    bt $TIF_NEED_RESCHED,%edx
    jnc  int_very_careful
    TRACE_IRQS_ON
    ENABLE_INTERRUPTS(CLBR_NONE)
    pushq %rdi
    CFI_ADJUST_CFA_OFFSET 8
    call schedule
    popq %rdi
    CFI_ADJUST_CFA_OFFSET -8
    DISABLE_INTERRUPTS(CLBR_NONE)
    TRACE_IRQS_OFF
    jmp int_with_check

int_very_careful:
    TRACE_IRQS_ON
    ENABLE_INTERRUPTS(CLBR_NONE)

int_check_syscall_exit_work:
    SAVE_REST
    testl $_TIF_WORK_SYSCALL_EXIT,%edx
    jz int_signal
    pushq %rdi
    leaq 8(%rsp),%rdi   # &ptregs -> arg1
    call syscall_trace_leave
    popq %rdi
    andl $~(_TIF_WORK_SYSCALL_EXIT|_TIF_SYSCALL_EMU),%edi
    jmp int_restore_rest

int_signal:
    testl $_TIF_DO_NOTIFY_MASK,%edx
    jz 1f
    movq %rsp,%rdi      # &ptregs -> arg1
    xorl %esi,%esi      # oldset -> arg2
    call do_notify_resume
1:  movl $_TIF_WORK_MASK,%edi
int_restore_rest:
    RESTORE_REST
    DISABLE_INTERRUPTS(CLBR_NONE)
    TRACE_IRQS_OFF
    jmp int_with_check
END(system_call)


retint_swapgs:  
    DISABLE_INTERRUPTS(CLBR_ANY)
    TRACE_IRQS_IRETQ
    SWAPGS
    jmp restore_args

restore_args:
    RESTORE_ARGS 0,8,0  ;; 恢复参数

irq_return:
    INTERRUPT_RETURN  ;; INTERRUPT_RETURN=iretq 返回

其中的SAVE_ARSSAVE_REST用于保存如下结构:

struct pt_regs {
/* SAVE_REST_END */
    unsigned long r15;
    unsigned long r14;
    unsigned long r13;
    unsigned long r12;
    unsigned long bp;
    unsigned long bx;
/* arguments: non interrupts/non tracing syscalls only save upto here SAVE_ARGS_END*/
    unsigned long r11;
    unsigned long r10;
    unsigned long r9;
    unsigned long r8;
    unsigned long ax;
    unsigned long cx;
    unsigned long dx;
    unsigned long si;
    unsigned long di;
    unsigned long orig_ax;
/* end of arguments SAVE_ARGS_START */
/* cpu exception frame or undefined */
    unsigned long ip;
    unsigned long cs;
    unsigned long flags;
    unsigned long sp;
    unsigned long ss;
/* top of stack page */
};

快速系统调用

系统调用是高频的操作,通过中断进行系统调用无疑会有一些没必要的损耗,因此处理器添加了专用功能加速系统调用,在Intel下相关的有两对指令syscall/sysret与sysenter与sysexit,它们都用于从Ring3快速进入Ring0.

syscall/sysret

它只支持IA-32e的64bit模式,且激活该功能:

IF (CS.L1 ) or (IA32_EFER.LMA1) or (IA32_EFER.SCE1)
    (* Not in 64-Bit Mode or SYSCALL/SYSRET not enabled in IA32_EFER *)
THEN #UD; FI;

使用前需要先设置如下MSRs,IA32_STAR的47:32存放内核态的CS/SS(堆栈段描述符需要紧接着代码段描述符放置,即&SS=&CS+8),63:48存放用户态的CS/SS,IA32_LSTAR存放入口点的地址,IA32_FMASK存放RFLAGS的掩码:

在执行syscall时,硬件会把用户态的下一条指令的地址存入RCX,RFLAGS存入R11,并从MSRs中加载RIP/CS/SS,计算RFLAGS,不过注意64位并不会从GDT/LDT中加载CS/SS对应的段描述符,CS/SS中隐藏部分的数据是固定值(0x00:0xFFFFF...)。另外可见此处并没有存储RSP的值,因此若需要则由软件自行保存。 返回时使用sysret指令,它从RCX与R11恢复RFLAGS和RIP,注意尽管它本身只能在64Bit模式下执行,但它支持返回到兼容模式,此时寄存器只赋值低位,并设置段寄存器的对应标志。

sysenter/sysexit

这对指令在开启保护模式后即可使用(保护模式/虚拟8086模式/兼容模式/64Bit模式),且在切换时不改变位数,可通过如下方式判断是否处理器支持:

IF CPUID SEP bit is set
  THEN IF (Family = 6) and (Model < 3) and (Stepping < 3)
    THEN
      SYSENTER/SYSEXIT_Not_Supported; FI;
  ELSE
    SYSENTER/SYSEXIT_Supported; FI;
FI;

使用前也需要设置一些MSRs,如下表:

IA32_SYSENTER_EIP为系统调用的入口地址,IA32_SYSENTER_ESP保存内核栈的栈顶(实际上不一定是栈顶,只要内核代码能通过它获取到栈顶即可),IA32_SYSENTER_CS的15:0为内核态的CS,此时内核态与用户态的CS与SS描述符是连续存放的,该值不能为0:

KernelCS.Selector := IA32_SYSENTER_CS[15:0]
KernelSS.Selector := KernelCS.Selector + 8;

IF operand size is 64-bit (* Operating system provides CS; RPL forced to 3 *) 
  THEN UserCS.Selector := IA32_SYSENTER_CS[15:0] + 32;
  ELSE UserCS.Selector := IA32_SYSENTER_CS[15:0] + 16;
FI;
UserSS.Selector := UserCS.Selector + 8;

在执行sysenter时,会从MSRs加载对应值,并把FLAGS的VM与IF位清除,来确保处于保护模式并禁用中断,注意它不会自动保存任何值,若需要软件则由软件实现。在返回时,它和进入并不对称,它会从RCX和RDX分别加载RSP和RIP,并根据操作位数从IA32_SYSENTER_CS加载用户态的CS与SS。

注:这里的操作数大小是根据REX指令前缀来识别的,如REX.W置位表示64位操作数。

vsyscall/vdso

int80速度慢,而sysenter/syscall又不一定支持,似乎libc只能在兼容性与效率间做取舍,但实际操作系统知道后者是否可用,因此它向用户态导出了syscall(),libc直接调用此处的syscall就可知道,之前的实现是vsyscall,它作为一个地址固定的内存页被直接映射到用户空间:

root@bm:~# cat /proc/1/maps | grep vsyscall
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0                  [vsyscall]

该页里实现了3个函数:

#define VSYSCALL_ADDR_vgettimeofday   0xffffffffff600000
#define VSYSCALL_ADDR_vtime           0xffffffffff600400
#define VSYSCALL_ADDR_vgetcpu          0xffffffffff600800

显然这个固定的地址,且里面还有syscall指令用来做rop的gadget很胖,于是它就被废弃了,要用的话还是模拟的方式运行...

(gdb) x/10i 0xffffffffff600000
   0xffffffffff600000:  mov    $0x60,%rax
   0xffffffffff600007:  syscall 
   0xffffffffff600009:  retq   

vdso是更现代的方式,它会以动态库的形式被注入到用户进程的内存空间,且地址是随机的:

root@bm:~# cat /proc/self/maps | grep vdso
7ffca2272000-7ffca2273000 r-xp 00000000 00:00 0                          [vdso]
root@bm:~# cat /proc/1/maps | grep vdso
7fff46d9c000-7fff46d9d000 r-xp 00000000 00:00 0                          [vdso]

它在sysenter_setup中初始化:

int __init sysenter_setup(void)
{
    void *syscall_page = (void *)get_zeroed_page(GFP_ATOMIC);
    const void *vsyscall;
    size_t vsyscall_len;

    vdso32_pages[0] = virt_to_page(syscall_page);

#ifdef CONFIG_X86_32
    gate_vma_init();
#endif

    /* 如下根据CPUID判断是否支持如下指令,并指定对应的镜像
    X86_FEATURE_SYSCALL32   (3*32+14)
    X86_FEATURE_SYSENTER32  (3*32+15)
    */
    if (vdso32_syscall()) {
        vsyscall = &vdso32_syscall_start;
        vsyscall_len = &vdso32_syscall_end - &vdso32_syscall_start;
    } else if (vdso32_sysenter()){
        vsyscall = &vdso32_sysenter_start;
        vsyscall_len = &vdso32_sysenter_end - &vdso32_sysenter_start;
    } else {
        vsyscall = &vdso32_int80_start;
        vsyscall_len = &vdso32_int80_end - &vdso32_int80_start;
    }
    // 将要加载的镜像拷贝到syscall_page区域后,对该区域进行ELF解析,此时会把它加载到固定位置
    memcpy(syscall_page, vsyscall, vsyscall_len);
    relocate_vdso(syscall_page);

    return 0;
}

并在程序初始化load_elf_binary中被插入:

int arch_setup_additional_pages(struct linux_binprm *bprm, int uses_interp)
{
    if (vdso_enabled == VDSO_DISABLED)
        return 0;

    compat = (vdso_enabled == VDSO_COMPAT);

    map_compat_vdso(compat);

    // 获取要被装入的线性区
    if (compat)
        addr = VDSO_HIGH_BASE;
    else {
        addr = get_unmapped_area(NULL, 0, PAGE_SIZE, 0, 0);
    }

    current->mm->context.vdso = (void *)addr;

    if (compat_uses_vma || !compat) {
        // 安装线性区,将vdso32_pages映射到该区域
        ret = install_special_mapping(mm, addr, PAGE_SIZE,
                          VM_READ|VM_EXEC|
                          VM_MAYREAD|VM_MAYWRITE|VM_MAYEXEC|
                          VM_ALWAYSDUMP,
                          vdso32_pages);

    }
    // 设置返回地址
    current_thread_info()->sysenter_return = VDSO32_SYMBOL(addr, SYSENTER_RETURN);
}

其在支持syscall时如下:

__kernel_vsyscall:
    push    %ebp
    movl    %ecx, %ebp
    syscall
    movl    $__USER32_DS, %ecx
    movl    %ecx, %ss
    movl    %ebp, %ecx
    popl    %ebp
    ret

再看syscall_init,它设置了进入的入口,和中断方式一致,不再赘述:

void syscall_init(void)
{
    wrmsrl(MSR_STAR,  ((u64)__USER32_CS)<<48  | ((u64)__KERNEL_CS)<<32);
    wrmsrl(MSR_LSTAR, system_call);
    wrmsrl(MSR_CSTAR, ignore_sysret);

#ifdef CONFIG_IA32_EMULATION
    syscall32_cpu_init();
#endif

    wrmsrl(MSR_SYSCALL_MASK, X86_EFLAGS_TF|X86_EFLAGS_DF|X86_EFLAGS_IF|X86_EFLAGS_IOPL);
}

参考

  1. 从用户态到内核态的切换 -- JasonLe
  2. Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3 (3A, 3B, 3C & 3D): System Programming Guide
  3. Linux 内核系统调用 第三节 vsyscalls 和 vDSO -- 0xA0