Linux将内核程序和基于之上的用户程序分开处理,分别运行在用户态和核心态。以32位x86架构为例,虚拟空间共4G,高地址的1G为系统程序运行的核心栈,低地址的3G空间为用户程序运行的用户栈。
如果一个用户程序需要调用底层的系统接口,比如printf, malloc等等诸如libc里面的系统调用函数,那么就需要牵涉到用户态与核心态的两个栈切换问题。所有的系统调用函数都是运行在核心态。
在系统调用时,由于用户态和核心态是运行两个独立栈上面,所以我们不能仅仅简单的传递函数指针,因为对于核心态空间用户态是不可见的,所以系统调用函数指针对于用户态不可见;另外一个问题是参数传递,由于两个栈之前独立运行的,所以不能用普通的压栈出栈的形式进行参数传递。
所以我们需要分别就1) 系统调用函数的名称转换;2) 系统调用函数的参数传递;两个方面来讨论。
每一个系统调用函数在内核当中都有对应的句柄处理函数,一般以sys_开头,比如sys_restart_syscall, sys_getpid等等,这些句柄函数作为一个“系统调用表”以汇编语言文件形式存在,位于./arch/x86/kernel/syscall_table_32.S,部分内容如下:
1 ENTRY(sys_call_table)每一个系统调用的函数对应着内核里的具体实现,每一个系统函数都有一个对应的数字对应,这个数字事实上是系统调用函数指针的偏移。
当我们调用一个系统函数时,运行时库通过查找这个表来决定对应的函数代码,然后存入到寄存器中,然后当切换到到核心态后,内核同样也是根绝这个函数代码来查找到对应的系统函数名称,从而找到对应的代码入口地址。系统调用切换过程如图所示:
参数的传递
由于当我们调用一个系统函数时候大多数都需要传递参数,但是这些参数传递并不能直接压栈传递,因为用户态和核心态运行在两个独立的栈上面!为了解决这个问题,系统调用函数借助寄存器来传递参数,由于寄存器数量的限制,所以规定参数不超过六个(也有些系统是规定不超过五个,这个和平台相关)。如果参数超过六个,我们可以用其他方法,比如packing方法将有些参数压缩,放在一个结构体里从而传递结构体的指针等方法来进行传递。下面的函数就是x86运行时库用来进行用户态与核心态进行切换时候的函数,(需要注意的是不同的平台的这个系统调用切换函数是不一样的):
http://cristi.indefero.net/p/uClibc-cristi/source/tree/9a2837c77c664d32a1fc9860cb193f25e0f3f37e/libc/sysdeps/linux/i386/syscall.S
但是这里有一点不明白的是为什么寄存器保存是四个而不是六个或更多?
Note:EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP等是x86 汇编语言中CPU上的通用寄存器的名称,32位的寄存器。这些32位寄存器有多种用途,但每一个都有特定的用处。
EAX 是"累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器。同时在函数调用的时候往往保存的是返回值的结果,如果有多个返回值,那么保存的是返回值结果的指针。
EBX 是"基地址"(base)寄存器, 在内存寻址时存放基地址。
ECX 是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器。
EDX 则总是被用来放整数除法产生的余数。
ESI/EDI分别叫做"源/目标索引寄存器"(source/destination index),因为在很多字符串操作指令中, DS:ESI指向源串,而ES:EDI指向目标串.
EBP是"基址指针"(BASE POINTER)。