目录
1. 系统调用简介 2. Linux系统调用实现方式的演进 3. 通过INT 0x80中断方式进入系统调用 4. 通过sysenter指令方式直接进入系统调用 5. sysenter/sysexit编程示例
1. 系统调用简介
由操作系统实现提供的所有系统调用所构成的集合即程序接口或应用编程接口(Application Programming Interface,API)。是应用程序同系统之间的接口
\linux-3.15.5\arch\x86\kernel\entry_32.S
syscall_call: /* 调用系统函数 sys_call_table也定义在是一张由指向实现各种系统调用的内核函数的函数指针组成的表: linux-2.6.32.63\arch\x86\kernel\syscall_table_32.S ENTRY(sys_call_table) .long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */ .long sys_exit .long ptregs_fork .long sys_read .long sys_write .long sys_open /* 5 */ .long sys_close .long sys_waitpid .long sys_creat .long sys_link .long sys_unlink /* 10 */ .long ptregs_execve .long sys_chdir .long sys_time .long sys_mknod .long sys_chmod /* 15 */ .long sys_lchown16 .long sys_ni_syscall /* old break syscall holder */ .long sys_stat .long sys_lseek .long sys_getpid /* 20 */ .long sys_mount .long sys_oldumount .long sys_setuid16 .long sys_getuid16 .long sys_stime /* 25 */ .long sys_ptrace .long sys_alarm .long sys_fstat .long sys_pause .long sys_utime /* 30 */ .long sys_ni_syscall /* old stty syscall holder */ .long sys_ni_syscall /* old gtty syscall holder */ .long sys_access .long sys_nice .long sys_ni_syscall /* 35 - old ftime syscall holder */ .long sys_sync .long sys_kill .long sys_rename .long sys_mkdir .long sys_rmdir /* 40 */ .long sys_dup .long sys_pipe .long sys_times .long sys_ni_syscall /* old prof syscall holder */ .long sys_brk /* 45 */ .long sys_setgid16 .long sys_getgid16 .long sys_signal .long sys_geteuid16 .long sys_getegid16 /* 50 */ .long sys_acct .long sys_umount /* recycled never used phys() */ .long sys_ni_syscall /* old lock syscall holder */ .long sys_ioctl .long sys_fcntl /* 55 */ .long sys_ni_syscall /* old mpx syscall holder */ .long sys_setpgid .long sys_ni_syscall /* old ulimit syscall holder */ .long sys_olduname .long sys_umask /* 60 */ .long sys_chroot .long sys_ustat .long sys_dup2 .long sys_getppid .long sys_getpgrp /* 65 */ .long sys_setsid .long sys_sigaction .long sys_sgetmask .long sys_ssetmask .long sys_setreuid16 /* 70 */ .long sys_setregid16 .long sys_sigsuspend .long sys_sigpending .long sys_sethostname .long sys_setrlimit /* 75 */ .long sys_old_getrlimit .long sys_getrusage .long sys_gettimeofday .long sys_settimeofday .long sys_getgroups16 /* 80 */ .long sys_setgroups16 .long old_select .long sys_symlink .long sys_lstat .long sys_readlink /* 85 */ .long sys_uselib .long sys_swapon .long sys_reboot .long sys_old_readdir .long old_mmap /* 90 */ .long sys_munmap .long sys_truncate .long sys_ftruncate .long sys_fchmod .long sys_fchown16 /* 95 */ .long sys_getpriority .long sys_setpriority .long sys_ni_syscall /* old profil syscall holder */ .long sys_statfs .long sys_fstatfs /* 100 */ .long sys_ioperm .long sys_socketcall .long sys_syslog .long sys_setitimer .long sys_getitimer /* 105 */ .long sys_newstat .long sys_newlstat .long sys_newfstat .long sys_uname .long ptregs_iopl /* 110 */ .long sys_vhangup .long sys_ni_syscall /* old "idle" system call */ .long ptregs_vm86old .long sys_wait4 .long sys_swapoff /* 115 */ .long sys_sysinfo .long sys_ipc .long sys_fsync .long ptregs_sigreturn .long ptregs_clone /* 120 */ .long sys_setdomainname .long sys_newuname .long sys_modify_ldt .long sys_adjtimex .long sys_mprotect /* 125 */ .long sys_sigprocmask .long sys_ni_syscall /* old "create_module" */ .long sys_init_module .long sys_delete_module .long sys_ni_syscall /* 130: old "get_kernel_syms" */ .long sys_quotactl .long sys_getpgid .long sys_fchdir .long sys_bdflush .long sys_sysfs /* 135 */ .long sys_personality .long sys_ni_syscall /* reserved for afs_syscall */ .long sys_setfsuid16 .long sys_setfsgid16 .long sys_llseek /* 140 */ .long sys_getdents .long sys_select .long sys_flock .long sys_msync .long sys_readv /* 145 */ .long sys_writev .long sys_getsid .long sys_fdatasync .long sys_sysctl .long sys_mlock /* 150 */ .long sys_munlock .long sys_mlockall .long sys_munlockall .long sys_sched_setparam .long sys_sched_getparam /* 155 */ .long sys_sched_setscheduler .long sys_sched_getscheduler .long sys_sched_yield .long sys_sched_get_priority_max .long sys_sched_get_priority_min /* 160 */ .long sys_sched_rr_get_interval .long sys_nanosleep .long sys_mremap .long sys_setresuid16 .long sys_getresuid16 /* 165 */ .long ptregs_vm86 .long sys_ni_syscall /* Old sys_query_module */ .long sys_poll .long sys_nfsservctl .long sys_setresgid16 /* 170 */ .long sys_getresgid16 .long sys_prctl .long ptregs_rt_sigreturn .long sys_rt_sigaction .long sys_rt_sigprocmask /* 175 */ .long sys_rt_sigpending .long sys_rt_sigtimedwait .long sys_rt_sigqueueinfo .long sys_rt_sigsuspend .long sys_pread64 /* 180 */ .long sys_pwrite64 .long sys_chown16 .long sys_getcwd .long sys_capget .long sys_capset /* 185 */ .long ptregs_sigaltstack .long sys_sendfile .long sys_ni_syscall /* reserved for streams1 */ .long sys_ni_syscall /* reserved for streams2 */ .long ptregs_vfork /* 190 */ .long sys_getrlimit .long sys_mmap_pgoff .long sys_truncate64 .long sys_ftruncate64 .long sys_stat64 /* 195 */ .long sys_lstat64 .long sys_fstat64 .long sys_lchown .long sys_getuid .long sys_getgid /* 200 */ .long sys_geteuid .long sys_getegid .long sys_setreuid .long sys_setregid .long sys_getgroups /* 205 */ .long sys_setgroups .long sys_fchown .long sys_setresuid .long sys_getresuid .long sys_setresgid /* 210 */ .long sys_getresgid .long sys_chown .long sys_setuid .long sys_setgid .long sys_setfsuid /* 215 */ .long sys_setfsgid .long sys_pivot_root .long sys_mincore .long sys_madvise .long sys_getdents64 /* 220 */ .long sys_fcntl64 .long sys_ni_syscall /* reserved for TUX */ .long sys_ni_syscall .long sys_gettid .long sys_readahead /* 225 */ .long sys_setxattr .long sys_lsetxattr .long sys_fsetxattr .long sys_getxattr .long sys_lgetxattr /* 230 */ .long sys_fgetxattr .long sys_listxattr .long sys_llistxattr .long sys_flistxattr .long sys_removexattr /* 235 */ .long sys_lremovexattr .long sys_fremovexattr .long sys_tkill .long sys_sendfile64 .long sys_futex /* 240 */ .long sys_sched_setaffinity .long sys_sched_getaffinity .long sys_set_thread_area .long sys_get_thread_area .long sys_io_setup /* 245 */ .long sys_io_destroy .long sys_io_getevents .long sys_io_submit .long sys_io_cancel .long sys_fadvise64 /* 250 */ .long sys_ni_syscall .long sys_exit_group .long sys_lookup_dcookie .long sys_epoll_create .long sys_epoll_ctl /* 255 */ .long sys_epoll_wait .long sys_remap_file_pages .long sys_set_tid_address .long sys_timer_create .long sys_timer_settime /* 260 */ .long sys_timer_gettime .long sys_timer_getoverrun .long sys_timer_delete .long sys_clock_settime .long sys_clock_gettime /* 265 */ .long sys_clock_getres .long sys_clock_nanosleep .long sys_statfs64 .long sys_fstatfs64 .long sys_tgkill /* 270 */ .long sys_utimes .long sys_fadvise64_64 .long sys_ni_syscall /* sys_vserver */ .long sys_mbind .long sys_get_mempolicy .long sys_set_mempolicy .long sys_mq_open .long sys_mq_unlink .long sys_mq_timedsend .long sys_mq_timedreceive /* 280 */ .long sys_mq_notify .long sys_mq_getsetattr .long sys_kexec_load .long sys_waitid .long sys_ni_syscall /* 285 */ /* available */ .long sys_add_key .long sys_request_key .long sys_keyctl .long sys_ioprio_set .long sys_ioprio_get /* 290 */ .long sys_inotify_init .long sys_inotify_add_watch .long sys_inotify_rm_watch .long sys_migrate_pages .long sys_openat /* 295 */ .long sys_mkdirat .long sys_mknodat .long sys_fchownat .long sys_futimesat .long sys_fstatat64 /* 300 */ .long sys_unlinkat .long sys_renameat .long sys_linkat .long sys_symlinkat .long sys_readlinkat /* 305 */ .long sys_fchmodat .long sys_faccessat .long sys_pselect6 .long sys_ppoll .long sys_unshare /* 310 */ .long sys_set_robust_list .long sys_get_robust_list .long sys_splice .long sys_sync_file_range .long sys_tee /* 315 */ .long sys_vmsplice .long sys_move_pages .long sys_getcpu .long sys_epoll_pwait .long sys_utimensat /* 320 */ .long sys_signalfd .long sys_timerfd_create .long sys_eventfd .long sys_fallocate .long sys_timerfd_settime /* 325 */ .long sys_timerfd_gettime .long sys_signalfd4 .long sys_eventfd2 .long sys_epoll_create1 .long sys_dup3 /* 330 */ .long sys_pipe2 .long sys_inotify_init1 .long sys_preadv .long sys_pwritev .long sys_rt_tgsigqueueinfo /* 335 */ .long sys_perf_event_open
在entry_32.S中列出了Linux操作系统所支持的所有系统调用
2. Linux系统调用实现方式的演进
1. 通过INT 0x80中断方式进入系统调用 在 2.6以前的 Linux 2.4 内核中,用户态 Ring3 代码请求内核态 Ring0 代码完成某些功能是通过系统调用完成的,而系统调用的是通过软中断指令(int 0x80) 实现的。在 x86 保护模式中,处理 INT 中断指令时 1) CPU 首先从中断描述表 IDT 取出对应的门描述符 2) 判断门描述符的种类 3) 检查门描述符的级别 DPL 和 INT 指令调用者的级别 CPL,当 CPL<=DPL 也就是说 INT 调用者级别高于描述符指定级别时,才能成功调用 4) 根据描述符的内容,进行压栈、跳转、权限级别提升 5) 内核代码执行完毕之后,调用 IRET 指令返回,IRET 指令恢复用户栈,并跳转会低级别的代码 /* 在发生系统调用,由 Ring3 进入 Ring0 的这个过程浪费了不少的 CPU 周期,例如,系统调用必然需要由 Ring3 进入 Ring0,权限提升之前和之后的级别是固定的,CPL 肯定是 3,而 INT 80 的 DPL 肯定也是 3,这样 CPU 检查门描述符的 DPL 和调用者的 CPL 就是完全没必要。正是由于如此,Intel x86 CPU 从 PII 300(Family 6,Model 3,Stepping 3)之后,开始支持新的系统调用指令 sysenter/sysexit */ 2. 通过sysenter指令方式直接进入系统调用 sysenter 指令用于由 Ring3 进入 Ring0,SYSEXIT 指令用于由 Ring0 返回 Ring3。由于没有特权级别检查的处理,也没有压栈的操作,所以执行速度比 INT n/IRET 快了不少。 sysenter和sysexit都是CPU原生支持的指令集
0x1: 不同系统调用方法的性能比较
Relevant Link:
http://www.ibm.com/developerworks/cn/linux/kernel/l-k26ncpu/
3. 通过INT 0x80中断方式进入系统调用
通过80中断(软中断)进入系统调用的方式是Linux 2.6之前的做法,关于这块的内容请参阅另一篇文章
http://www.cnblogs.com/LittleHann/p/3871630.html
4. 通过sysenter指令方式直接进入系统调用
0x1: sysenter/sysexit机制简介
1. sysenter 指令 1) 用于特权级 3 的用户代码调用特权级 0 的系统内核代码 2) sysenter 指令可以在 3,2,1 这三个特权级别调用(Linux 中只用到了特权级 3) 2. SYSEXIT 指令 1) 用于特权级 0 的系统代码返回用户空间中 2) SYSEXIT 指令只能从特权级 0 调用
0x2: sysenter/sysexit和int n/iret的区别
1. sysenter/sysexit 1) 目标 Ring 0 代码段必须是平坦模式(Flat Mode)的 4GB 的可读可执行的非一致代码段 2) 目标 RING 0 堆栈段必须是平坦模式(Flat Mode)的 4GB 的可读可写向上扩展的栈段 3) sysenter/sysexit 指令并不成对,sysenter 指令并不会把 SYSEXIT 所需的返回地址压栈,sysexit 返回的地址并不一定是 sysenter 指令的下一个指令地址。调用 sysenter/sysexit 指令地址的跳转是通过设置一组特殊寄存器实现的,这些寄存器可以通过 wrmsr 指令来设置。这些寄存器包括: 3.1) SYSENTER_CS_MSR: 用于指定要执行的 Ring 0 代码的代码段选择符,由它还能得出目标 Ring 0 所用堆栈段的段选择符 3.2) SYSENTER_EIP_MSR: 用于指定要执行的 Ring 0 代码的起始地址 3.3) SYSENTER_ESP_MSR: 用于指定要执行的Ring 0代码所使用的栈指针 2. int n/iret 1) int n/iret是成对出现的,iret 返回的地址并一定是 int n 指令的下一个指令地址
需要明白的是,不管是以前的INT 0x80中断方式进入系统调用,还是使用sysenter方式进入系统调用,对于系统调用来说,最终都是通过"sys_call_table"来根据调用号寻址,跳转到对应的系统调用处理例程里面的,所以我们对sys_call_table进行hijack replace hook不管在linux 2.4还是2.6以后都是有效的
0x3: sysenter执行流程
在 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 代码
0x3: sysexit执行流程
在 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 的代码
Relevant Link:
http://www.ibm.com/developerworks/cn/linux/kernel/l-k26ncpu/
http://chenyufei.info/blog/2007-05-12/post-070512-221011-78/
http://articles.manugarg.com/systemcallinlinux2_6.html
5. sysenter/sysexit编程示例
#include <stdio.h> int pid; int main() { __asm__( "movl $20, %eax \n" "call *%gs:0x10 \n" /* offset 0x10 is not fixed across the systems */ "movl %eax, pid \n" ); printf("pid is %d\n", pid); return 0; }
使用 syscall/sysret 指令
在这里,我想介绍一下 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 位的,因此这里使用 REXprefix 将 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 descriptorselector 分 32-bit(包括 legacy 16-bit)和 64-bit 两个,SS selector 只有一个?
实际上,这个问题很简单,很容易明白其中的道理,只要我们明白 datasegment descriptor 的结构就知道了。
上图是 legacy mode 下的 data segment descriptor 结构,下图是 long mode 下的 data segment descriptor 结构(实际上不包括 compaitibility 模式),可以看出 datasegment 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.3SYSRET_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 值。
Windows2000使用INT 2e进中断,而XP使用sysenter指令进中断,主要是为了快速进入内核,调用内核函数。sysenter和sysexit是一对指令,又叫“快速系统调用”。 INT 2e中的INT是中断指令,而2e是中断号。操作系统中有一个中断向量表(IDT),保存则中断和异常向量。 操作系统有一个中断机制,是为了解放CPU。早期的IO操作,是靠CPU的轮询机制完成的,与IO设备之间的数据传输,每次传送间隙都是由CPU发送“开始发送”和“结束发送”命令完成的。这样使CPU的任务繁重,且关键是CPU与IO设备速度不匹配,CPU会等待慢速的IO传送完成,造成CPU资源浪费。后来出现了中断机制,即CPU发送开始传输命令后,就可以去忙其它的活了,待IO数据传送完毕,给CPU发送“传输完成”命令即可。但中断机制任然满足不了当今的大量计算、海量存储的需求,为了解决问题,出现了IO通道的设计。IO通道对每个IO设备(硬盘、鼠标键盘、游戏杆、网卡)增设一个类似于CPU功能的芯片,分担CPU繁重的任务。CPU只需与IO通道发送接收很少的数据即可完成IO任务。 而Linux从XX版本的内核开始使用中断改进以前靠CPU调度的机制,也就是epoll的实现。 回到中断向量表(IDT),中断又分为软中断和硬中断,即可有硬件引发也可由软件指令引发(也就是INT指令)。 操作系统提供很多API给应用程序调用,而很多Ring3(应用层)的API是通过调用内核的API完成的。而应用层的堆栈和系统层的是不一样的(也就是SS、ESP),而且执行入口也不一样(也就是CS、IP)。所以要通知CPU切换寄存器。这里就用中断机制实现了。 win2000的INT 2e方式是通过压入应用层堆栈(SS、ESP)、应用层返回地址(CS、IP),而后从TSS(任务状态段)取内核层的堆栈(内核API入口)设置为当前堆栈。内核层的API函数入口地址,首先从IDT从获得2E中断入口(对应KiSystemSerivce函数)并然后执行,入口会根据传入的应用层API编号(靠EAX传递)以及堆栈位置(EDX传递)调用SSDT表中的相应函数。而如果应用层需有界面则会根据API编号调用SSDT shadow表中的响应函数。调用完毕,从内核态返回应用态靠iret指令返回,然后装入TSS保存的应用层堆栈中的SS、ESP、CS、IP。 以上过程是否很繁琐且费时,操作系统有那么多API调用,不累死它才怪呢。 所以winXP以后使用sysenter来替代INT 2e的调用,但同时也兼容INT 2e。sysenter会把预设好的SYSENTER_CS_MSR、SYSENTER_EIP_MSR、SYSENTER_SS_MSR、SYSENTER_ESP_MSR四个寄存器内容传递内核态的API入口、堆栈给CS、EIP、SS、ESP。这样不像通过INT指令进入熊空间那样执行那么多操作,效率将提高很多。而执行sysexit指令回到用户态也是借助以上四个寄存器完成的,只不过偏移不相同。 sysenter实际对应KiFastSystemCall,sysexit对应KiFastSystemCallRet。KiFastSystemCall和KiFastSystemCallRet是通过PspLookupKernelUserEntryPoints()函数获取的。