前言
syscall
和int 80
是中断指令,Linux
通过对这两个指令的封装为开发者们提供的一种用户态切换至内核态
的方法,因为在处理器中用户态
是没有权限
向更高的权限空间切换
的,以x86
为例,它只允许高权限向低权限切换或同等权限切换
,不允许低权限向高权限切换。但是处理器保留了一个机制
,就是当产生中断
时(无论是任何中断)都会让处理器切换权限
并跳转至中断处理函数
里,而中断处理函数又由内核注册
所以就完成了用户态到内核态的切换
。
Tips
本文以X86架构为例
中断与普通函数的区别
中断与普通的函数调用是有区别的,一般最大的区别是返回值上面,一般普通函数调用是存在返回值的,相比之下函数调用
的开销一般比中断函数
要大,普通函数在调用时遵循的是函数调用约定(Calling Convention)
,这些约定定义了哪些寄存器需要由调用者(Caller)
处理,哪些寄存器需要由被调用者(Callee)
处理,例如调用者需要使用参数寄存器来传递参数
然后通过call发起调用,而被调用者需要接收这些参数并执行代码体
同时还需要使用返回寄存器来存储返回值和跳转回之前的代码段并进行现场恢复
,这些约定都是由ISO或处理器厂商
定义的。
一般的情况下函数调用使用的寄存器是通用寄存器
,并且函数调用不涉及到切换堆栈
,只涉及到堆栈增长
:
函数调用通常使用下面这些寄存器:
通用寄存器:EAX
, EBX
, ECX
, EDX
, ESI
, EDI
, EBP
, ESP
等。
浮点寄存器:ST0
-ST7
(在使用浮点运算时)。
SIMD寄存器:XMM0
-XMM7
(在使用SSE
指令集时)。
而中断是为了快速响应
而涉及的,它没有参数
也没有返回值
,也不会有堆栈切换,它切换到内核态之后由内核态做堆栈现场保护
和堆栈切换
,在返回时也由内核来恢复堆栈
。
同时它们的调用也不一样,函数调用使用的是call
指令,中断是int
,并且中断通常是由硬件自动触发
的,返回的指令也有所不同,函数调用是RET
,中断是IRET
。
并且RET指令是不会清理堆栈
的也不会恢复堆栈寄存器
,而IRET
指令执行之后CPU
会自动清理堆栈
和恢复堆栈寄存器(ESP/SS)
。
int 80
0x80属于处理器的一个中断号
,它是IDT表
的第0x80
号中断,int
是处理器中触发中断
的一个指令,当产生中断时CPU会以0x80为索引
到IDT表中取到中断函数
的入口地址
并进行跳转
,在跳转时CPU会进行权限切换,在x86
里内核态的权限为ring 0
,用户态为最低级别的ring 3
,为方便内核处理中断CPU
会在产生中断时先将当前上下文进行保存
,然后切换到中断函数
里去执行,由于x86架构权限设计的限制,所以想要切换到内核态
只能由中断来进入,所以Linux依靠这一点为用户态提供了调用内核
功能的能力,因为用户态是不能访问实际物理设备的所以只能通过内核来访问,例如我们平时所使用的read
、write
内部其实都是使用了int 80
来调用内核态帮我们完成工作。
以下是printf
调用write
输出到屏幕的汇编代码,write
内部会调用sys_write
:
sys_write(unsigned int fd, const char * buf, size_t count)
[section .data]
strHello db “Hello, world!”,0Ah
STRLEN equ $ - strHello
[section .text]
global _start
_start:
mov edx,STRLEN ;将count保存到eda寄存器
mov ecx,strHello ;将buf保存到ecx寄存器
mov ebx,1 ;将fd参数保存到ebx寄存器,这里fd=1,对应的是stdout
mov eax,4 ;调用sys_write, 系统调用号为4
int 0x80 ;产生中断
当执行int 0x80之后就会进入到IDT索引为0x80的中断函数里,这个中断函数会读取eax寄存器里的值然后去调用sys_call_table数组里存放的函数,可以在linux/kernel/system_call.s代码里找到调用过程:
movl $0x10,%edx # set up ds, es to kernel space
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx # fs points to local data space
mov %dx,%fs
call _sys_call_table(,%eax,4) # %eax contains the index (NR_write)
pushl %eax
这里linux
设计的非常精明,_sys_call_table
里的元素是按系统调用顺序来排列的,这样就可以把eax
作为偏移来调用,4是偏移量
,作为乘数
,在32位操作系统
里地址是以4字节
做偏移
的,下面是sys_call_table
的定义。
fn_ptr sys_call_table[] = {sys_setup, sys_exit, sys_fork, sys_read, sys_write,sys_open, sys_close, sys_waitpid, sys_creat, sys_link,sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod,sys_chmod, sys_chown, sys_break, sys_stat, sys_lseek,sys_getpid, sys_mount, sys_umount, sys_setuid, sys_getuid,sys_stime, sys_ptrace, sys_alarm, sys_fstat, sys_pause,sys_utime, sys_stty, sys_gtty, sys_access, sys_nice,sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof,sys_brk, sys_setgid, sys_getgid, sys_signal, sys_geteuid,sys_getegid, sys_acct, sys_phys, sys_lock, sys_ioctl,sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit, sys_uname,sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,sys_setreuid, sys_setregid
};
syscall
既然已经知道了int 80
的意义,那么syscall
与它一样,都是产生中断
的方式,它是处理器为提升系统调用效率
而开发的新的指令,目前linux
已经使用syscall
来替代int 80
了,syscall的系统调用地址由IA32_LSTAR
寄存器记录,这个寄存器里会存放系统调用的入口函数地址,当执行syscall
指令时CPU会去读取IA32_LSTAR
寄存器里的地址并跳转,一个完整的syscall由三个寄存器组成:IA32_LSTAR
、IA32_STAR
、IA32_FMASK
, 它们的作用分别如下:
IA32_LSTAR: 配置内核态代码段选择子。
**IA32_STAR: ** 配置用户态代码段选择子。
**IA32_FMASK: ** 指定系统调用过程中需要屏蔽的标志位。
IA32_LSTAR
与IA32_STAR
分别对应高32位
和低32位
,可以把它俩看成一个寄存器
,IA32_STAR存储的是返回用户态时的地址
,当执行syscall
时会跳转IA32_LSTAR的地址
,当处理完成时需要调用sysret
来返回用户态,则跳转的就是IA32_STAR的地址
,IA32_FMASK
用来控制在执行系统调用
时需要屏蔽的特权标志位
,确保系统调用
执行时的状态是安全
的,例如屏蔽中断(IF)
、陷阱中断(TF)
、方向标志位(DF)
,屏蔽这些标志位以保证系统能够正确稳定的执行系统调用。
Tips
中断: 计算机系统中的机制,用于处理异步事件或外部信号。它允许计算机在处理当前任务时,立即响应高优先级的事件,从而提高系统的响应性和效率,中断分为硬件中断和软件中断,硬件中断是由硬件产生中断信号给CPU由CPU转入IDT表中处理,软件中断是由软件调用CPU特定指令来产生中断让CPU进入中断函数进行处理
陷阱中断: 跟踪标志位主要作用是控制处理器进入单步操作方式。 当陷阱标志位被设置为1时,处理器会以单步执行的方式运行指令,即处理器在每条指令执行结束后,都会产生一个编号为1的内部中断,这种内部中断被称为单步中断
方向标志位: 用于控制字符串操作指令的处理方向。这个标志位用于决定在执行字符串操作指令(如 MOVS, CMPS, SCAS, LODS, STOS)时,处理的数据是向内存地址增加还是减少
syscall与int 80的区别在哪里?
最大的区别就是效率上,以x86
为例,intel
推出syscall
就是为了替代int 80
中断效率的,int 80
是通用中断,它产生中断的流程与硬件中断一致,在进行中断切换时会进行现场保护其次会去查找IDT
,IDT
是存放在内存当中的,CPU
首先需要去IDTR
里将IDT
地址读取出来,然后在根据索引到IDT
表里找到对应入口地址
然后在进行跳转
,同时还需要保存所有寄存器的值如 EAX, EBX, ECX, EDX, ESI, EDI, EBP
等,而syscall
是特殊设计,它只保存必要的恢复寄存器,例如EIP
,返回地址是存储在IA32_STAR
里的,它所需要的操作极少,同时在切换堆栈时也会进行堆栈保存,但是它是从MSR
寄存器中获取内核堆栈,而通用中断则是从TSS
中获取,TSS存在于内存当中
,TSS的基地址存放在TR
寄存器中,产生中断时需要先从TR寄存器获取到TSS任务状态段然后去里面查找内核
的堆栈在进行切换。
至于堆栈的保护这些一般是由操作系统来使用汇编代码保存,CPU的设计时只会规定寄存器的使用,但它不是一定的,是灵活的,操作系统可以自由决定使用什么寄存器或内存来保存那些想要保存的数据信息。