系统调用

来源:互联网 发布:windows徽标键与t 编辑:程序博客网 时间:2024/06/06 02:56
在Linux的用户空间,我们经常会调用系统调用,下面我们跟踪一下read系统调用,使用的Linux内核版本为Linux2.6.37。不同的Linux版本其中的实现略有不同。
在一些应用中我们可以看到下面的一些定义:

#define real_read(fd, buf, count ) (syscall(SYS_read, (fd), (buf), (count)))

   其实真正调用的还是系统函数syscall(SYS_read),也就是sys_read()函数中,在Linux2.6.37中的利用几个宏定义实现。

    Linux 系统调用(SCI,system call interface)的实现机制实际上是一个多路汇聚以及分解的过程,该汇聚点就是 0x80 中断这个入口点(X86 系统结构)。也就是说,所有系统调用都从用户空间中汇聚到 0x80 中断点,同时保存具体的系统调用号。当 0x80 中断处理程序运行时,将根据系统调用号对不同的系统调用分别处理(调用不同的内核函数处理)。

引起系统调用的两种途径

      (1)int $0×80 , 老式linux内核版本中引起系统调用的唯一方式

      (2)sysenter汇编指令

在Linux内核中使用下面的宏进行系统调用

SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
    struct file *file;
    ssize_t ret = -EBADF;
    int fput_needed;

    file = fget_light(fd, &fput_needed);
    if (file) {
        loff_t pos = file_pos_read(file);
        ret = vfs_read(file, buf, count, &pos);
        file_pos_write(file, pos);
        fput_light(file, fput_needed);
    }

    return ret;
}

其中SYSCALL_DEFINE3的宏定义如下:

#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)

##的意思就是宏中的字符直接替换,
如果name = read,那么在宏中__NR_##name就替换成了__NR_read了。 __NR_##name是系统调用号,##指的是两次宏展开.即用实际的系统调用名字代替"name",然后再把__NR_...展开.如name == ioctl,则为__NR_ioctl。

  

 

#ifdef CONFIG_FTRACE_SYSCALLS
#define SYSCALL_DEFINEx(x, sname, ...)                \
    static const char *types_##sname[] = {            \
        __SC_STR_TDECL##x(__VA_ARGS__)            \
    };                            \
    static const char *args_##sname[] = {            \
        __SC_STR_ADECL##x(__VA_ARGS__)            \
    };                            \
    SYSCALL_METADATA(sname, x);                \
    __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
#else
#define SYSCALL_DEFINEx(x, sname, ...)                \
    __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
#endif

不管是否定义CONFIG_FTRACE_SYSCALLS宏,最终都会执行 下面的这个宏定义:

__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

#ifdef CONFIG_HAVE_SYSCALL_WRAPPERS

#define SYSCALL_DEFINE(name) static inline long SYSC_##name

#define __SYSCALL_DEFINEx(x, name, ...)                    \
    asmlinkage long sys##name(__SC_DECL##x(__VA_ARGS__));        \
    static inline long SYSC##name(__SC_DECL##x(__VA_ARGS__));    \
    asmlinkage long SyS##name(__SC_LONG##x(__VA_ARGS__))        \
    {                                \
        __SC_TEST##x(__VA_ARGS__);                \
        return (long) SYSC##name(__SC_CAST##x(__VA_ARGS__));    \
    }                                \
    SYSCALL_ALIAS(sys##name, SyS##name);                \
    static inline long SYSC##name(__SC_DECL##x(__VA_ARGS__))

#else /* CONFIG_HAVE_SYSCALL_WRAPPERS */

#define SYSCALL_DEFINE(name) asmlinkage long sys_##name
#define __SYSCALL_DEFINEx(x, name, ...)                    \
    asmlinkage long sys##name(__SC_DECL##x(__VA_ARGS__))

#endif /* CONFIG_HAVE_SYSCALL_WRAPPERS */

最终会调用下面类型的宏定义:

asmlinkage long sys##name(__SC_DECL##x(__VA_ARGS__))
也就是我们前面提到的sys_read()系统函数。
asmlinkage通知编译器仅从栈中提取该函数的参数。所有的系统调用都需要这个限定词!这和我们上一篇文章quagga中提到的宏定义,有异曲同工之妙。

也就是宏定义中的下面代码:

struct file *file;
    ssize_t ret = -EBADF;
    int fput_needed;

    file = fget_light(fd, &fput_needed);
    if (file) {
        loff_t pos = file_pos_read(file);
        ret = vfs_read(file, buf, count, &pos);
        file_pos_write(file, pos);
        fput_light(file, fput_needed);
    }

    return ret;

代码解析:

  • fget_light() :根据 fd 指定的索引,从当前进程描述符中取出相应的 file 对象(见图3)。
  • 如果没找到指定的 file 对象,则返回错误
  • 如果找到了指定的 file 对象:
  • 调用 file_pos_read() 函数取出此次读写文件的当前位置。
  • 调用 vfs_read() 执行文件读取操作,而这个函数最终调用 file->f_op.read() 指向的函数,代码如下:

if (file->f_op->read)
ret = file->f_op->read(file, buf, count, pos);

  • 调用 file_pos_write() 更新文件的当前读写位置。
  • 调用 fput_light() 更新文件的引用计数。
  • 最后返回读取数据的字节数。

到此,虚拟文件系统层所做的处理就完成了,控制权交给了 ext2 文件系统层。

http://blogold.chinaunix.net/u3/104447/showart_2527011.html




使用 syscall/sysret 指令

mik
(mik@mouseos.com)

在这里,我想介绍一下 syscall 与 sysret 这对指令。关于这对指令请以 AMD 的手册为准,毕竟它们的 AMD 的产物。当然不能说 Intel 的不对,Intel 对 syscall/sysret 的完全兼容的。

可是,在 AMD 与 Intel 的 processor 上还是有区别的:

  1. 在 AMD 的 processor 上:syscall/sysret 指令在 long mode 和 protected mode(指的是 Legacy x86 和 compatibility mode)上都是有效的(valid)。
  2. 在 Intel processor 上:syscall/sysret 指令只能在 64-bit 模式上使用,compatibility 模式和 Legacy x86 模式上都是无效的。可是 sysret 指令虽然不能在 compatibility 模式下执行,但 sysret 却可以返回到 compaitibility 模式。这一点只能是认为了兼容 AMD 的 sysret 指令。

怎么办,这会不会出现兼容上的问题?这里有一个折衷的处理办法:

在 64 位环境里统一使用 syscall/sysret 指令,在 32 位环境里统一使用 sysenter/sysexit 指令

然而依旧会产生一些令人不愉快的顾虑:

没错,在 compatibility 模式下谁都不兼容谁:
    Intel 的 syscall/sysret 指令不能在 compatibility 模式下执行;AMD 的 sysenter/sysexit 指令也不能在 compatibility 模式下执行。

因此:在 compatibility 模式下必须切换到 64 位模式,然后使用 syscall/sysret 指令


1. syscall 指令的逻辑

下面是用 C 语言描述 syscall 指令执行的逻辑:

MSR_EFER EFER;
MSR_STAR STAR;
MSR_LSTAR LSTAR;
MSR_CSTAR CSTAR;
MSR_SFMASK SFMASK;

void syscall()
{
    if (EFER.SCE == 0)        /* system call extensions is disable */
        do_exception_UD();    /* #UD exception */
       

    if (EFER.LMA == 1) {      /* long mode is active */
        rcx = rip;            /* save rip for syscall return */
        r11 = rflags;         /* save rflags to r11 */

        /*
         * CS.L == 1 for 64-bit mode, rip from MSR_LSTAR
         * CS.L == 0 for compatibility, rip from MSR_CSTAR
         */

        rip = CS.attribute.L ? LSTAR : CSTAR;

        /*
         * processor set CS register 
         */ 
      
        CS.selector = STAR.SYSCALL_CS;       /* load selector from MSR_STAR.SYSCALL_CS */
        CS.selector.RPL = 0;                 /* RPL = 0 */
        CS.attribute.S = 1;                  /* user segment descriptor */
        CS.attribute.C_D = 1;                /* code segment */
        CS.attribute.L = 1;                  /* 64-bit */
        CS.attribute.D = 0;                  /* 64-bit */
        CS.attribute.DPL = 0;                /* CPL = 0 */                   
        CS.attribute.P = 1;                  /* present = 1 */
        CS.base = 0;
        CS.limit = 0xFFFFFFFF;

        /*
         * processor set SS register
         */

         SS.selector = STAR.SYSCALL_CS + 8;
         SS.attribute.S = 1;
         SS.attribute.C_D = 0;
         SS.attribute.P = 1;
         SS.attribute.DPL = 0;
         SS.base = 0;
         SS.limit = 0xFFFFFFFF;

         /* set rflags */
         rflags = rflags & ~ SFMASK;
         rflags.RF = 0;

         /* goto rip ... */


    } else {
        /* legacy mode */

        rcx = (unsigned long long)eip;            /* eip extend to 64 load into rcx */
        rip = (unsigned long long)STAR.EIP;       /* get eip from MSR_STAR.EIP */
       
        CS.selector = STAR.SYSCALL_CS;
        CS.selector.RPL = 0;
        CS.attribute.S = 1;                  /* user descriptor */
        CS.attribute.C_D = 1;                /* code segment */
        CS.attribute.D = 1;                  /* 32-bit */
        CS.attribute.C = 0;                  /* non-conforming */
        CS.attribute.R = 1;                  /* read/execute */
        CS.attribute.DPL = 0;                /* CPL = 0 */                   
        CS.attribute.P = 1;                  /* present = 1 */
        CS.attribute.G = 1;                  /* G = 1 */
        CS.base = 0;
        CS.limit = 0xFFFFFFFF;                     

        SS.selector = STAR.SYSCALL_CS + 8;
        SS.attribute.S = 1;                 /* user descriptor */
        SS.attribute.C_D = 0;               /* data segment */
        SS.attribute.D = 1;                 /* 32-bit esp */
        SS.attribute.E = 0;                 /* expand-up */
        SS.attribute.W = 1;                 /* read/write */
        SS.attribute.P = 1;                 /* present */
        SS.attribute.DPL = 0;               /* DPL = 0 */
        SS.attribute.G = 1;                 /* G = 1 */
        SS.base = 0;
        SS.limit = 0xFFFFFFFF;

        rflags.VM = 0;
        rflags.IF = 0;
        rflags.RF = 0;

        /* goto rip */
    }

}

syscall 指令促使 processor 进行一系列的强制行为:

  • 目标代码 DPL = 0,强制切换到 CPL = 0
  • CS 被强制为 non-conforming,read/execute 属性,当 long mode 下,目标代码为 64 位,当 legacy mode 下目标代码为 32 位。
  • SS 被强制为 expand-up,read/write 属性
  • base 都被 0
  • limit 都为 4G

在执行 syscall 指令前,processor 会检测 EFER 寄存器的 SCE 标志位,以确认是否开启 System Call Extension 功能,如果没有开启会产生 #UD 无效 opcode 码异常。

可以看出 syscall 指令只是加载 selector,但是并不进行实质的 descriptor 加载行为,强制设置 CS 和 SS 寄存器以满足系统服务例程(指 CPL=0 下的服务例程)的工作环境。


2. sysret 指令的逻辑

sysret 指令执行的情形有些稍复杂,我举些例子来说明。

2.1 从 64 位返回到 64 位

    bits 64

 
    ... ...

    pop rcx                  ; restore rcx for rip
    pop r11                  ; restore r11 for rflags

    db 0x48                  ; 64-bit operand size for sysret !!!
    dw 0x070f                ; sysret

别看上面的汇编代码怪异,实际上它是最正常的代码,使用手工编码的方式是基于:确保编译器能够编译出我们想要的机器指令。

sysret 指令的缺省操作数是 32 位的,因此这里使用 REX prefix 将 sysret 指令的操作数调整到 64 位,现在我们的代码就返回到 64-bit 模式。这是我们想要的结果:从 64-bit Level-0 代码返回到 64-bit Level-3 代码。

2.2 从 64 位返回到 compatibility 模式

    bits 64

 
    ... ...

    pop rcx                  ; restore rcx for rip
    pop r11                  ; restore r11 for rflags

    sysret                   ; 32-bit operand size

这里看上去很正常,其实不正常的汇编代码(不正常是指:不是我们想要的),我们让编译器来产生 sysret 指令机器码,那么编译器会产生 32 位操作数的机器指令。在这情形下,将会返回到 compatibility 模式,这样大多数情况下并不是我们想的结果。

有没有可能通过 sysret 返回到 legacy 模式?当然不可能。回到 legacy 模式要经过一系列的切换工作,这方面的工作,详见:http://www.mouseos.com/arch/exit_longmode.html

sysret 的逻辑如下:

void sysret()
{
    if (EFER.SCE == 0)          /* System Call Extension is disable */
        do_exception_UD();

    if (CR0.PE == 0 || CS.attribute.DPL != 0)  /* protected mode is disable or CPL != 0 */
        do_exception_GP();    

    if (CS.attribute.L == 1)       /* 64-bit mode */
    {   
        if (REX.W == 1)            /* 64-bit operand size */
        {
             /* 
              * return to 64-bit code !
              */
             CS.selector = STAR.SYSRET_CS + 16;     /* 64-bit code segment selector */
             CS.selector.RPL = 3;                   /* CPL = 3 */
             CS.attribute.L = 1;
             CS.attribute.D = 0;
             CS.attribute.P = 1;
             CS.attribute.DPL = 3;
             CS.base = 0;
             CS.limit = 0xFFFFFFFF;
           
             rip = rcx;                           /* restore rip for return */

        } else {
             /*
              * return to compatibility !
              */
             CS.selector = STAR.SYSRET_CS;        /* 32-bit code segment selector */
             CS.selector.RPL = 3;
             CS.attribute.L = 0;                  /* compatibility mode */
             CS.attribute.D = 1;                  /* 32-bit code */
             CS.attribute.P = 1;
             CS.attribute.C = 0;
             CS.attribute.R = 1;
             CS.attribute.DPL = 3;
             CS.base = 0;
             CS.limit = 0xFFFFFFFF; 

             rip = (unsigned long long)ecx;              
        }
        
        SS.selector = START.SYSRET_CS + 8;       /* SS selector for return */
        rflags = r11;                            /* restore rflags */

        /* goto rip */

    } else {                       /* compatibility or legacy mode */

         CS.selector = STAR.SYSRET_CS;        /* 32-bit code segment selector */
         CS.selector.RPL = 3
         CS.attribute.L = 0;                  /* compatibility mode */
         CS.attribute.D = 1;                  /* 32-bit code */
         CS.attribute.P = 1;
         CS.attribute.C = 0;
         CS.attribute.R = 1;
         CS.attribute.DPL = 3;
         CS.base = 0;
         CS.limit = 0xFFFFFFFF; 

         SS.selector = STAR.SYSRET_CS + 8;

         rflags.IF = 1;

         rip = (unsigned long long)ecx;    
    }

}

processor 同样要检查是否开启了 System Call Extension 功能,并且检查是否处于保护模式,当前的 CPL 是否为 0,和 syscall 指令一样,sysret 不做 descriptor 的加载工作,同样需要强制设置 CS 以满足用户环境(指的是 CPL=3 下的代码),可是:processor 并不设置 SS 寄存器,那么需要加载 data segment descriptor 进入 SS 寄存器


3. syscall/sysret 使用的寄存器

为了支持 syscall/sysret 指令,AMD 新增了4个 MSR 寄存器:

  • STAR
  • LSTAR
  • CSTAR
  • SFMASK

在 Intel 下 STSR 被称作 IA32_STAR,LSTAR 被称作 IA32_LSTAR, SFMASK 被称作 IA32_SFMASK, 虽然是冠以 IA32 体系,但是请相信它们是 64 位的。除前面所说的只能在 64 位环境执行,其它方面完全是兼容 AMD 的


4. STAR 寄存器

通过上图我们已经明白了 STAR 寄存器的用途:

  • 在 legacy x86 下提供 eip 值(仅在 legacy x86 模式下)
  • 为 syscall 指令提供目标代码的 CS 和 SS selector
  • 为 sysret 指令提供返回代码的 CS 和 SS selector

因此,STAR 寄存器分为三部分:

  • [31:00] - SYSCALL_EIP
  • [47:32] - SYSCALL_CS
  • [63:48] - SYSRET_CS

STAR 寄存器的地址是 C0000081h,我们可以使用 wrmsr 指令来写 STAR 寄存器

下面是来自我的 mouseos 0.01 版中的初始化 syscall 环境的 init_syscall() 代码:

init_syscall:
;-------------------------------------------------------
; Note: the sysret instruction: not change SS.RPL to 3
;       So: MSR_STAR.SYSRET_CS.RPL must be to set 3 !!!!
;-------------------------------------------------------

    mov edx, SYSCALL_CS | ((SYSRET_CS | 0x3) << 16)
    xor eax, eax
    mov ecx, MSR_STAR                      ; MSR_STAR's address
    wrmsr                                  ; write edx:eax into MSR_STAR register
 
    mov rax, sys_services_order * 5 + MICKEY_CODE_ENTRY
    mov rdx, rax
    shr rdx, 32
    mov ecx, MSR_LSTAR                     ; set MSR_LSTAR = sys_services
    wrmsr

    xor edx, edx
    xor eax, eax
    mov ecx, MSR_SFMASK                    ; set MSR_SFMASK = 0                     
    wrmsr
         
    ret

在 ECX 寄存器中提供 MSR_STAR 的地址,64 位的值由 EDX:EAX 提供,高 32 位放在 EDX 寄存器,低 32 位放在 EAX 寄存器。

init_syscall 代码中分别设置了 STAR 和 LSTAR 以及 SFMASK 寄存器,这里 SFMASK 被设为 0


4.1 设置 SYSCALL_CS 以及 SYSCALL_SS

你应该让 SYSCALL_CS 提供索引 DPL=0 的 Code Segment Descriptor 的 selector

注意:
    尽管 processor 会忽略你提供的 SYSCALL_CS.RPL 值,而强制 SYSCALL_CS.RPL = 0,但是你必须将 SYSCALL_CS.RPL 设为 0
    那是因为:SYSCALL_SS 由 SYSCALL_CS + 8 而来!

SYSCALL_SS = SYSCALL_CS + 8,这表示:目标代码的 SS descriptor 是 CS descriptor 的下一个 descriptor

下面同样是来自 mouseos 0.01 的代码,为 syscall 设置 descriptor

;--------------------------------------------------------------
; kernel_cs & kernel_ss for syscall into kernel code
;--------------------------------------------------------------
kernel_cs_desc dd 0                     ; 0x0b
                dd 0x00209800

kernel_ss_desc dd 0                     ; 0x0c
                dd 0x00009200

在这个代码中 SYSCALL_CS 设为 0x0b,那么 SYSCALL_SS 就是 0x0c,因此我们在设置 SYSCALL_CS 必须考虑到下一个应该是 SYSCALL_SS


4.2 设置 SYSRET_CS 以及 SYSRET_SS

同理,你也应该让 SYSRET_CS 提供索引 DPL=3 的 Code Segment Descriptor 的 selector,同样 SYSRET_SS = SYSRET_CS + 8,因此:你必须设置 SYSRET_CS.RPL = 3,以此来设置返回的 SYSRET_SS.RPL = 3。

注意,SYSRET_CS 提供的是 3 个 selector,分别是:

  1. 32-bit code 的 selector:用来返回到 compaitibility 模式代码,以及 legacy x86 模式代码
  2. SS selector:这个 selector 既是 32-bit 下的 selector,也是 64-bit 下的 selector,将会被加载到 SS 寄存器,descriptor 也会被加载。
  3. 64-bit code 的 selector:这个是用来返回到 64-bit 模式代码

那么:

  • SYSRET_CS:32-bit code segment descriptor selector(包括 legacy x86 的 16-bit 代码)
  • SYSRET_CS+8:stack segment descriptor selector
  • SYSRET_CS+16:64-bit code segment descriptor selector

这里很容易让人产生疑问:为什么 code segment descriptor selector 分 32-bit(包括 legacy 16-bit)和 64-bit 两个,SS selector 只有一个?

实际上,这个问题很简单,很容易明白其中的道理,只要我们明白 data segment descriptor 的结构就知道了。

上图是 legacy mode 下的 data segment descriptor 结构,下图是 long mode 下的 data segment descriptor 结构(实际上不包括 compaitibility 模式),可以看出 data segment descriiptor 在 legacy 模式下与 long mode 模式下都是一样的。只是在 64-bit 模式下,绝大部分是无效的。

同一个 data segment descriptor 由 processor 当前的状态来决定是属于 legacy mode 下的 data segment descriptor 还是 64-bit 下的 data segment desciptor,因此,只提供一个 data segment descriptor selector 来加载到 SS 寄存器。


4.3 SYSRET_EIP

这部分是为 legacy mode 下提供的,当 processor 处于 32-bit protected mode 下,像这样:

    bits 32

    ......

    syscall         ; 32-bit mode syscall! eip = MSR_STAR.SYSCALL_EIP

在 32 位代码下执行 syscall 指令,目标的 eip 将从 STAR 的 SYSCALL_EIP 部分提取!

当然使用前需设置 SYSCALL_EIP 值:

    bits 32


init_syscall32:
    ... ...

    mov eax, syscall32_entry                     ; 32-bit syscall entry
    mov edx, (user_cs << 16) | (kernel_cs)       ; SYSRET_CS and SYSCALL_CS selector
    mov ecx, 0C0000081h                          ; MSR_STAR address
    wrmsr                                        ; write MSR_STAR register

    ... ...


5. LSTAR 寄存器

LSTAR 寄存器为 64-bit 代码的提供目标的 rip 值。

5.1 设置 LSTAR 寄存器

下面代码示范了设置 LSTAR 寄存器:

    bits 64


init_syscall:

    ... ...

    mov rax, syscall64_entry                         ; 64-bit syscall entry
    mov rdx, rax
    shr rdx, 32
    mov ecx, 0C0000082h                              ; MSR_LSTAR address
    wrmsr

    ... ...

64 位的 rip 值需要被分两部分,低 32 位放在 eax 寄存器,高 32 位放在 edx 寄存器,EDX:EAX 形成 64 位的 rip 值。必须注意的是:这个 64 位地址值是 canonical 形式的值,否则 wrmsr 会产生 #GP 异常。这段初始化代码虽然在 64 位执行,当然你可以在 32 位代码下对 MSR_LSTAR 完成初始化工作,这是正确的

5.2 使用 LSTAR 寄存器

    bits 64


;---------------------------------------------
; Now: processor is run on 64-bit mode
;---------------------------------------------

sys_service_call:

    ... ...

    syscall           ; fast call system service routine, load rip from LSTAR

    ret

当 processor 运行在 64-bit 模式下,执行 syscall 指令时,目标代码的 rip 将从 LSTAR 中加载。

5.3 返回 64-bit

    bits 64


;---------------------------------------------
; Now: processor is run on 64-bit mode
;---------------------------------------------

sys_service_return:

    ... ...

    pop rcx           ; restore rip
    pop r11           ; restore rflags

    db 0x48           ; operand size is 64-bit for return to 64-bit code
    sysret            ; return user code

前面说过,我们需使用 REX prefix 进行调整 sysret 指令的操作数,以它能够正确返回到 64-bit 代码,否则将会返回到 compatibility 模式。如果有需要应该恢复 rcx 和 r11 寄存器值。取决你有没有改变 rcx 和 r11 的值。因为 rip 和 rflags 需要从 rcx 和 r11 提取。


6. CSTAR 寄存器

CSTAR 寄存器为 compatibility 模式下的代码提供 rip 值,当 processor 在 comatibility 模式下运行时,执行了 syscall 指令,此时 rip 值将从 MSR_STAR 寄存器中加载。

请记住:只能在 AMD 的 processor 使用 compaitibility 模式下的调用。正如下面的代码:

    bits 32


;---------------------------------------------
; Now: processor is run on compatibility mode
;---------------------------------------------

sys_service_entry:

    ... ...

    syscall                    ; fast call system service routine, load rip from MSR_CSTAR

    ret

执行 syscall 指令时,processor 处于 compaitibility 模式,那么 rip 将从 CSTAR 寄存器取得目标代码的 rip 值。实际上执行的结果是:会从 compatibility 模式切换到 64-bit 模式

那么,在系统的服务例程执行完毕后,使用 sysret 指令会从 64-bit 模式切换回 compatibility 模式继续执行,正如下面代码所示:

    bits 64


;---------------------------------------------
; Now: processor is run on 64-bit mode
;---------------------------------------------

sys_service_return_to_compatibility:

    ... ...

    pop rcx                    ; restore for return eip
    pop r11                    ; restore for return rflags

    sysret                     ; return to user's compatibility mode

这段代码在 64-bit 模式下执行,但是返回到 compaitibility 模式。

6.1 通用性的考虑

照顾通用性,为了在 Intel 和 AMD 的 processor 上都能够使用 fast call 功能,操作系统的设计者应该要避免在 comaptibility 模式下使用 syscall 指令。前面提到过,建议在 compatibility 模式下先切换到 64-bit 模式后,再执行 syscall 指令

正如下面的代码,在系统中提供切换的 stub 库功能:

6.1.1 切换到 64-bit

在 compatibility 模式下执行切换到 64-bit 模式,如下示例:

    bits 32


;----------------------------------------------
; Now: processor is run on compatibility mode
;----------------------------------------------


switch_to_64_entry:

    mov ecx, return_position                   ; save return position for compatibility mode

    jmp far sel64:sys_service_entry_stub       ; 64-bit stub function
                                               ; Now: processor switch to 64-bit mode from compatibility !
return_position:
    
    ret                                        ; return to calling

这段代码先保存返回值到 ecx 寄存器,然然通过远跳转(jmp far)切换到 64 位代码,此时 processor 的权限是不变的。 切换后 CPL 还是 3

6.1.2 执行 fast call

现在正处于 64-bit 代码下,CPL=3,下面代码进行 fast call:

    bits 64

;----------------------------------------------------
; Now: processor is run on 64-bit mode
;----------------------------------------------------


sys_service_entry_stub:

    mov eax, ecx                                ; return position

    push sel32                                  ; 32-bit code selector
    push rax                                    ; return position
    
    syscall                                     ; fast call into system service routine

    db 0x48                                     ; operand size is 64-bit for retf
    retf                                        ; return to compatibility, switch to 32-bit

为了返回原来的 compatibility 模式代码,我们可以使用 retf 指令切换回原来的 compatibility 模式,我们依次压入 32-bit selector 和返回地址,再执行我们需要的 syscall 指令。这样当使用 sysret 指令返回后,就可以执行 retf 指令切换回 compatibility 模式。


7. SFMASK 寄存器

在 long mode 下,当执行 syscall 指令时,当前的 rflags 寄存器值被保存在 r11 寄存器,processor 在执行 syscall 时,准备的目标执行环境中,rflags 将会根据 SFMASK 寄存器的值进行设置:如果 SFMASK 寄存器的某一位置为 1,那么 rflags 寄存器中相应的位将会被清 0置为 0 时,rflags 寄存器中相应位不变

它的逻辑 C 描述为:

rflags = rflags & (~sfmask);

下面代码显示了如何使用 SFMASK 寄存器:

init_syscall:

    ... ...

    xor eax, eax
    xor edx, edx

    bts eax, 14                     ; for rflags.NT = 0

    mov ecx, 0C0000084h             ; MSR_SFMASK address
    wrmsr

在这段初台化 SFMASK 寄存器代码中,将 SFMASK 的 bit 14 置为 1,这样的结果导致执行 syscall 指令后,rflags.NT 将会被清为 0(bit14 是 NT 标志)。

你应该在系统服务例程先保存 r11 值(原来的 rflags 寄存器值),以便 sysret 执行返回时可以恢复原来的 rflags 值。


版权 mik 所有,转载请注明出处


0 0