赵建清+原创作品转载请注明出处+《Linux内核分析》MOOC课程http://mooc.study.163.com/learn/USTC-1000029000
概述
系统调用是Linux内核提供的基础服务入口,通过使用这一机制,应用程序可以使用内核的一些专门功能。在分析系统调用之前,以下三点需要了解:
1.系统调用将CPU从用户态切换到核心态,以便访问受保护的内核内存。
2.系统调用的组成是固定的,每个系统调用在内核中都由一个唯一的数字来标识。
3.系统调用的参数传递方式与普通C函数的方式有所不同。
工作过程
下面以系统调用dup()为例,说明在应用程序如何使用Linux的系统调用。dup()复制一个打开的文件描述符,并返回一个新描述符,二者都指向同一个打开的文件句柄。系统会保证新描述符一定是编号低最低的未使用文件描述符。
使用库函数API调用dup()的程序:
使用C语言嵌入汇编代码来实现调用dup():
第一条汇编语句将立即数复制到寄存器edi,用作dup()的参数,表示要复制的描述符;
第二条汇编语句将立即数41复制到寄存器eax,41是dup()在内核中的系统调用号;
第三条汇编语句执行中断指令int,从用户态切换到内核态,发起系统调用;
第四条汇编语句将寄存器eax中保存的系统调用返回值复制fd,表示复制的新描述符。
上面两个小程序的功能是等价的,复制得到的新描述符值都是3,因为文件描述符0、1和2分别用作标准输入,标准输出和标准出错。
下图是通过库函数API使用系统调用dup()时的事件发生序列:
从应用程序到执行系统调用要经历诸多步骤,下面是x86-32架构的具体过程:
1.应用程序通过C语言函数库中提供的API发起系统调用。
2.对系统调用中断处理例程来说,API从堆栈中取得传入的参数,发起系统调用之前,参数被复制到CPU寄存器中。
3.库函数API将系统调用号复制到寄存器eax中。
4.执行中断指令int,引发CPU从用户态切换到内核态,并执行系统中断0x80所执行的终端向量所指向的代码。
5.为了响应中断0x80,内核会调用system_call()例程来处理此次中断,包括在内核栈中保存寄存器值,检查参数有效性并查找对应的中断服务例程,执行完成后将结构状态返回给system_call()。
6.如果系统调用不成功,库函数API会使用该值设置全局变量errno,然后从API返回到应用程序中。
使用系统调用的传统方法是通过汇编指令int。向量128(十六进制0x80)对应于内核的入口。在内核初始化时调用的函数trap_init(),用下面的方式建立对应于向量128的终端描述符表项:
set_system_intr_gate(SYSCALL_VECTOR, &system_call);
其中SYSCALL_VECTOR是定义在arch/x86/include/asm/irq_vectors.h中的一个宏:
#define SYSCALL_VECTOR 0x80
执行int $0x80指令后,CPU切换到内核态并从地址system_call处开始执行指令,system_call()函数的具体分析留给《Linux系统调用的工作机制(下)》。
与普通函数类似,系统调用同城也需要输入/输出参数。发起系统调用时系统处于用户态,执行系统调用时系统已进入内核态,同时操作两个栈较复杂,因此内核利用CPU寄存器传递参数。使用系统调用前参数被写入CPU寄存器(上面调用dup()时传递参数使用寄存器edi),执行系统调用时内核再把寄存器中的参数复制到内核堆栈中。
除了上文提到的使用int指令,应用程序还有一种方式可以调用系统调用。Intel Pentium II微处理器引入sysenter指令,从Linux 2.6开始内核也支持这条指令。内核从系统调用返回,使CPU从内核态切换回用户态,也有两种方式:
1.执行iret汇编语言指令。
2.执行sysexit汇编语言指令,和sysenter指令对应,同时从Intel Pentium II引入。
总结
从服务提供者的角度来看Linux系统,可以将内核视为一个综合性的库,它包含了各种可面向用户应用程序提供的功能。系统调用是应用程序与该库之间的接口,因此了解系统调用的工作方式无论对于研究内核还是系统编程都是颇有益处的。
4.执行中断指令int,引发CPU从用户态切换到内核态,并执行系统中断0x80所执行的终端向量所指向的代码。
5.为了响应中断0x80,内核会调用system_call()例程来处理此次中断,包括在内核栈中保存寄存器值,检查参数有效性并查找对应的中断服务例程,执行完成后将结构状态返回给system_call()。
6.如果系统调用不成功,库函数API会使用该值设置全局变量errno,然后从API返回到应用程序中。
使用系统调用的传统方法是通过汇编指令int。向量128(十六进制0x80)对应于内核的入口。在内核初始化时调用的函数trap_init(),用下面的方式建立对应于向量128的终端描述符表项:
set_system_intr_gate(SYSCALL_VECTOR, &system_call);
其中SYSCALL_VECTOR是定义在arch/x86/include/asm/irq_vectors.h中的一个宏:
#define SYSCALL_VECTOR 0x80
执行int $0x80指令后,CPU切换到内核态并从地址system_call处开始执行指令,system_call()函数的具体分析留给《Linux系统调用的工作机制(下)》。
与普通函数类似,系统调用同城也需要输入/输出参数。发起系统调用时系统处于用户态,执行系统调用时系统已进入内核态,同时操作两个栈较复杂,因此内核利用CPU寄存器传递参数。使用系统调用前参数被写入CPU寄存器(上面调用dup()时传递参数使用寄存器edi),执行系统调用时内核再把寄存器中的参数复制到内核堆栈中。
除了上文提到的使用int指令,应用程序还有一种方式可以调用系统调用。Intel Pentium II微处理器引入sysenter指令,从Linux 2.6开始内核也支持这条指令。内核从系统调用返回,使CPU从内核态切换回用户态,也有两种方式:
1.执行iret汇编语言指令。
2.执行sysexit汇编语言指令,和sysenter指令对应,同时从Intel Pentium II引入。
总结
从服务提供者的角度来看Linux系统,可以将内核视为一个综合性的库,它包含了各种可面向用户应用程序提供的功能。系统调用是应用程序与该库之间的接口,因此了解系统调用的工作方式无论对于研究内核还是系统编程都是颇有益处的。