编程开源技术交流,分享技术与知识

网站首页 > 开源技术 正文

CPU眼里的:系统调用(系统调用性能)

wxchong 2024-08-22 23:58:32 开源技术 11 ℃ 0 评论

Hello World程序会调用库函数,库函数又会调用什么呢?谁帮我们打印出来了:Hello World?


01

提出问题

“系统调用”是现代大型操作系统的核心功能,那你知道什么是系统调用吗?它跟“函数调用”有什么区别呢?为什么有人说:普通程序员跟大神之间就差了一个:系统调用?如何在 2 分钟内,手写一个可以运行的:系统调用?

这里,我们将以Linux操作系统为例,用CPU 的视角,开启今天的“系统调用”之旅。


02

代码分析

其实,当你写第一个程序:hello world 的时候:

int main()
{
    printf( "HiWorld\n" );
}

你已经在作“系统调用”了。虽然,hello world 是公认最简单的程序,但从 CPU 眼里,可一点都不简单!

因为,这个从天而降的 printf 函数,不仅仅帮你实现了:字符打印;还依次穿透了整个计算机系统的:应用层、操作系统层、驱动层 和 硬件层,如图所示。

显然,这都不是普通函数能做到的!真正的幕后英雄是:操作系统,是它调动了驱动和硬件,才完成了字符的显示。我们不过是通过 printf 函数,召唤操作系统完成特定的工作而已,而这个召唤过程,就是“系统调用”。

为了便于描述,我们把 printf 直接替換成:它必須要作的系统调用write,如图所示。

其中,参数1是标准输出设备(stdout)的文件描述符,它的值是:1;参数2是字符串“HiWorld\n”所在内存的首地址;参数3是字符串“HiWorld\n”的长度:8 个字节。

它所对应的CPU指令如图的左侧所示。好了,让我们化身成世界最慢的CPU,看看系统调用的具体过程。

假设:上边的内存颗粒,存放着:hello world 程序;下边的内存颗粒,存放着AMD64 的 Linux 操作系统。

先看第1条CPU指令,它把参数1的值:1,传递给寄存器 rdi;第2条CPU指令,把参数3的值:8,传递给寄存器 rdx,如图所示。

没想到吧?系统调用的参数传递方法,跟普通函数是完全一致的!有兴趣的同学,可以回看一下“CPU眼里的参数传递”

随后的 3 条CPU指令,用来把字符串写入“堆栈”内存,并把内存地址传递给寄存器 rsi。假设这段内存,就是当前的堆栈(为了方便展示:“堆栈”的堆叠结构,下面是高端地址,上面是低端地址),如图所示。

每个内存块的字节长度为:8 个字节;红色水位线,表示“堆栈”栈顶的内存地址,也就是:CPU寄存器 rsp 的值,如图所示。

好了,先把字符串“\ndlroWiH”对应的 8 字节无符号数0x0a646c726f576948,写入到寄存器:rbx。每个字节,正好对应着1个字符的 ASCII 码,至于为什么倒着写?可以参看“CPU眼里的:大端、小端”

随后的push指令,把 rbx 的值压入“堆栈”,栈顶也随之上升,这样,就把字符串写入到堆栈内存了;而这个字符串所在的内存地址,正好就是此时寄存器 rsp 的值:0x8000 0008。

最后,mov 指令把寄存器rsp的值,传递给:寄存器 rsi,这样,参数2(字符串“HiWorld\n”的内存首地址)的传递工作,也完成了。

好了,万事俱备,可以进行系统调用了,如图所示。

mov 指令,把 1 传递给寄存器 rax,表示我们要作:1号系统调用;随后的 syscall 指令,会产生CPU 异常,迫使 CPU 切换到操作系统内核,进行异常处理,如图所示。

操作系统,早就为所有的系统调用准备好了:一张表格,一行都对应着一个系统调用。因为,此时寄存器rax的值是1,所以操作系统会执行第1行的系统调用,调用所需的参数,分别在寄存器 rdi、rsi 和 rdx 里面。

剩下的事情,就由操作系统代劳了:把字符串输出到显示屏上。至此,整个系统调用结束!

但无论多么精妙的讲述,都无法替代一次上手操作,我们可以把代码粘贴到一台 64 位的 Ubuntu 电脑上,当然,也可以在虚拟机 或 Windows自带的Linux子系统(WSL)上操作:

先安装一下编译器 nasm,作一下编译,现在就可以运行了,如图所示。

不出所料,字符串被打印了出来。当然,我们还可以修改代码,打印一些有趣的东西。代码(syscall.s)已经上传至:GitHub(https://github.com/idea4good/AbuCoding)可以按照上面的步骤,亲手操作一下。


03

总结

1. “系统调用”跟“函数调用”一样,都可以通过寄存器来传递参数,但会用 syscall指令触发CPU异常,从而让操作系统,接管后面的功能实现。

2. 系统调用会引发:CPU状态切换,CPU在用户态准备参数,然后切换到内核态完成功能。

3. 系统调用能够有效的隔离应用程序和操作系统核心,提高整个系统的安全性。

4. 系统调用的实现,会因为CPU指令集的不同而不同。几乎所有重要的库函数,都需要通过系统调用来实现,看看图中的 Linux 的“系统调用”表,是不是觉得非常眼熟呢?

最后,不是所有的操作系统都支持系统调用,许多单片机操作系统,就不支持系统调用,程序员在调用操作系统的高级功能(例如:创建线程、sleep)时,大多是在作普通API函数的调用。


04

热点问题

Q1:实例程序,相当于一个脱离了C运行库的hello world程序,是这样吗?

A1:是的,因为脱离了第三方库的支持,这也可能是世界上最小、最简单的hello world程序。当然,printf这个库函数,并不仅仅是把字符串打印出来那么简单,其实它包含了更丰富的功能,例如:格式化字符串、不同进制(2进制、8进制、16进制)之间的数制转换等。


Q2:系统调用,存在的意义是什么?

A2:系统调用的一个重大意义是:隔离了用户空间和内核空间。以本章节的实例来说,在系统调用前,都是在用户内存空间,运行用户的代码;在syscall指令之后,CPU会跳转到内核内存空间,运行操作系统预设好的代码。

内核态的代码(例如:操作系统、驱动程序代码),一旦运行出错,往往就是系统崩溃或蓝屏,因此它们往往是经过了千锤百炼,基本确保了正确性、安全性的可靠代码。

相比之下,程序员编写的在用户态下运行的程序,出错的可能性更大,但由于这种良好的隔离,不会让用户态的错误影响到内核态。所以,即使用户的程序崩溃,也不会影响到整个计算机系统(例如:操作系统、驱动程序)的继续运行。


Q3:为什么把系统调用设计成这个样子?貌似不能随意改动,万一设计出来,不合理怎么办?

A3:形成一个固定的规则非常重要,这样才便于各种库函数的开发,如果系统调用的接口、编号不断变化,那对应的函数库(例如:C标准库、多线程库),也需要不断的调整!这非常不利于软件的标准化、工业化。

至于可能的设计缺陷,一方面要在开发的早期,及时发现、及时解决;另一方面可以由库函数来做适当修正,如你所见,系统调用的功能是非常基本的,更丰富的功能往往是由库函数来完善的。保持系统调用的简单性,本身就是良好的设计。如你所见,无论Windows还是Linux,它们的系统调用都非常相似,这也足以说明当前设计方法的可行性。


05

更多知识

如果喜欢阿布这种解读方式,希望更加系统学习这些编程知识的话,也可以考虑看看由阿布亲自编写,并由多位微软大佬联袂推荐的新书《CPU眼里的C/C++》

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表