当前位置: 首页 > 科技观察

系统调用和函数调用有什么区别?

时间:2023-03-22 17:09:44 科技观察

大家新年快乐,我是小风哥,这是今年的第一篇技术文章,我们来说说系统调用和普通函数调用的区别。作为一个程序员,你一定写过无数的函数。假设有两个函数:voidfuncB()}voidfuncA()funcB();}函数之间可以互相调用,很简单也很开心。如果知道是代码和函数,就可以互相调用,不管用什么语言写。假设funcB是内核中的函数,funcA是自己写的函数,像这样://FunctioninLinuxkernelvoidfuncB()}//YourfunctionvoidfuncA()funcB();}那么funcA应该也可以调用funcB(如果funcB可以被外界调用的话)。可能有的同学会惊呼,我们可以写代码调用操作系统的功能,那我们是不是可以直接控制操作系统呢?太简单太简单了!如果我们写的代码可以直接调用所有的操作系??统函数,那么某种程度上确实可以说是可以控制操作系统,但是如果操作系统只允许你调用内核中有限的几个函数呢??怎么样,你(应用)是不是受限了。你又会问,操作系统是如何限制应用程序可以调用内核中哪些函数的呢?事实上,仅靠操作系统等软件是无法限制一个应用程序可以调用哪些内核函数和调用多少内核函数的,所以必须依靠硬件来进行这样的限制。这里的硬件指的是CPU。那么CPU是如何施加这个限制的呢?我们先看看普通的函数调用。函数调用对应的机器指令就是call指令,像这样:call0x400410call指令后的地址0x400410是被调用函数的第一条机器指令所在的内存地址。CPU在执行这条机器指令时,直接跳转到相应地址继续执行指令。从程序员的角度来看,它是一个函数调用。而如果我们程序的函数调用操作系统的函数,call指令是不允许的,可以是syscall机器指令(x86_64)。使用syscall指令调用操作系统函数时,对应函数的第一条指令地址是否放在syscall之后?显然不是,因为操作系统的系统代码和你的代码是分开编译运行的,你不知道操作系统的某个函数存放在内存的什么地方,也不应该让你知道,所以在调用的时候使用syscall操作系统的函数,我们只能加一个序号,比如序号0对应操作系统中的A函数,序号1对应操作系统中的B函数等,所以当使用syscall指令,我们只需要将序号写入rax寄存器即可,即CPU在执行syscall指令时,通过读取rax寄存器的值,就可以知道要调用操作系统中的哪个函数。可以看出,使用这种机制,操作系统限制了应用程序可以调用内核中的哪些函数。可能有同学会有疑惑,如果由于各种原因,call指令后面的地址“无意中”指向了内核函数的地址,那么CPU执行call指令时会发生什么情况呢?就像这样:call0x400410这里假设地址0x400410指向一个内核函数地址。很简单,CPU在执行这条指令的时候,会判断当前进程没有访问地址0x400410的权限,所以CPU在执行这条指令的时候会产生异常,直接杀死进程。下面是Linux如何在各种处理器上进行系统调用的列表。看,syscall和call在用法上还是有很大区别的。可以看到call是直接调用,也就是说直接调用应用层的函数调用,而syscall其实是间接调用。是的,就是我们在操作系统中调用函数的时候,其实是间接调用的。另外,CPU执行call指令和syscall指令的另一个区别在于模式的切换。CPU在执行普通功能的时候,其实是运行在用户态,用户态。在这种模式下,CPU不能执行某些特权指令,也就是说我们的程序实际上是受到限制的;而当CPU执行syscall时,开始执行操作系统的代码,就会切换到内核态,kernelmode。在这种模式下,CPU可以不受任何限制地执行任何特权指令。操作系统才是真正管理计算机的大老板。可见,在普通程序中进行函数调用时,是函数调用,而普通函数调用操作系统中的函数时,则称为系统调用。最后补充一点,普通函数调用使用的栈都位于进程的栈区。假设main函数调用了funcA函数,funcA调用了funcB函数,那么此时的进程内存布局是这样的:当进行系统调用时,CPU启动时,执行操作系统的代码时,是不再基于进程栈区而是会跳转到操作系统的特定内存区。该区域在内核中用作进程的栈区,因此也称为内核栈。内核中的每个进程都有自己的内核栈,所以我们可以看到一个进程其实有两个栈区,一个在用户态,一个在内核态。假设main函数调用funcA,funcA进行系统调用,调用内核中的funcB函数,funcB函数调用内核中的funcC函数,那么此时的内存布局是这样的:好了,本题是在这里,我希望你能理解OS有所帮助。