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

系统调用,让世界旋转!

时间:2023-03-22 17:02:43 科技观察

真的不想拆开给你看,用户应用其实是个缸里的穷脑:它和外界的每一次通信都必须借助内核通过系统调用来完成.结束。对于保存文件、写入终端或打开TCP连接的应用程序,内核必须参与。该应用程序被内核高度怀疑:它被认为是漏洞百出,甚至是满脑子恶念。这些系统调用是从应用程序到内核的函数调用。出于安全原因,他们使用特定的机制,实际上你只是调用内核的API。术语“系统调用”是指调用内核提供的特定功能(例如系统调用open())或调用途径。您也可以将其缩短为:syscall。本文解释了系统调用、系统调用与调用库的区别以及用于监视OS/AP接口的工具。在操作系统的帮助下准确了解您的应用程序发生了什么,可以将一个不可能的问题变成一个快速而有趣的难题。所以,这是一个正在运行的应用程序的图片,一个用户进程:它有一个私有的虚拟地址空间——它自己的内存沙箱。整个系统都在它的地址空间中(也就是上面比喻中的“瓮”),程序的二进制文件加上它使用的库都映射到内存中。内核本身也被映射为地址空间的一部分。下面是我们程序pid的代码,通过getpid(2)直接获取其进程id:#include#include#includeintmain(){pid_tp=getpid();printf("%d\n",p);}pid.c下载在Linux中,一个进程在它诞生时并不知道它的PID。要知道它的PID,它必须询问内核,所以这个询问请求也是一个系统调用:它的第一步是从调用C库的getpid()开始,它是系统调用的包装器。当您调用某些函数时,例如open(2)、read(2)等,您就是在调用这些包装器。事实上,对于大多数编程语言来说,这方面的native方法最终都是在libc中完成的。封装为这些基本的操作系统API提供了便利,从而保持了内核的清洁。所有内核代码都在特权模式下运行,一行错误的内核代码可能会导致致命的后果。任何可以在用户模式下完成的事情都应该在用户模式下完成。由库提供友好的方法和所需的参数处理,例如printf(3)。让我们将其与WebAPI进行比较。核心封装方式可以类比为尽可能简单的构建一个HTTP接口来提供服务,然后提供特定语言的库和辅助方法。或者可能有一些缓存,这就是libc的getpid()所做的:***调用时,它实际上执行一个系统调用,然后,它缓存PID,这样可以避免系统在后续调用中。调用开销。一旦封装,它做的第一件事就是进入超空间:内核。这种翻译机制因处理器架构设计而异。在Intel处理器中,参数和系统调用编号被加载到寄存器中,然后运行一条指令将CPU置于特权模式并立即将控制权转移到内核中的全局系统调用条目。如果您对细节感兴趣,DavidDrysdale在LWN上有两篇非常好的文章(一篇,两篇)。然后内核使用这个系统调用号作为sys_call_table的索引,它是指向每个系统调用实现的函数指针数组。这里调用了sys_getpid:在Linux中,大部分系统调用都是作为架构无关的C函数实现的,有时候这样做是微不足道的,但是通过内核的优秀设计,将系统调用机制严格隔离。它们是处理普通数据结构的普通代码。好吧,除了完全偏执的参数验证。一旦他们的工作完成,他们就会正常返回,然后特定于体系结构的代码接管并返回到用户模式,包装器继续进行一些后处理。在我们的例子中,getpid(2)现在缓存内核返回的PID。如果内核返回错误,另一个包装器可以设置全局errno变量。这些细节可以让您了解GNU是如何处理事情的。如果你想要本地调用,glibc提供了syscall(2)函数,它可以在不包装的情况下进行系统调用。您也可以使用它来制作自己的包装纸。这对于C库来说既不神奇也不特别。这个系统调用的设计意义是深远的。我们从非常有用的strace(1)开始,它可用于监视Linux进程的系统调用(在Mac上,请参阅dtruss(1m)和神奇的dtrace;在Windows上,请参阅sysinternals)。这是对pid程序的跟踪:~/code/x86-os$strace./pidexecve("./pid",["./pid"],[/*20vars*/])=0brk(0)=0x9aa0000access("/etc/ld.so.nohwcap",F_OK)=-1ENOENT(没有这样的文件或目录)mmap2(NULL,8192,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0)=0xb7767000access("/etc/ld.so.preload",R_OK)=-1ENOENT(没有那个文件或目录)open("/etc/ld.so.cache",O_RDONLY|O_CLOEXEC)=3fstat64(3,{st_mode=S_IFREG|0644,st_size=18056,...})=0mmap2(NULL,18056,PROT_READ,MAP_PRIVATE,3,0)=0xb7762000close(3)=0[...snip...]getpid()=14678fstat64(1,{st_mode=S_IFCHR|0600,st_rdev=makedev(136,1),...})=0mmap2(NULL,4096,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0)=0xb7766000write(1,"14678\n",614678)=6exit_group(6)=?输出的每行显示一个系统调用、它的参数和返回值。如果你循环运行1000次getpid(2),你会发现始终只有一个getpid()系统调用,因为它的PID已经被缓存了。我们还可以看到printf(3)在格式化输出字符串后调用write(2)。strace可以启动一个新进程或附加到一个已经运行的进程。您可以从不同程序的系统调用中学到很多东西。例如,sshd守护进程一天都在干什么?~/code/x86-os$psax|grepsshd12218?Ss0:00/usr/sbin/sshd-D~/code/x86-os$sudostrace-p12218Process12218attached-中断退出选择(7,[34],NULL,NULL,NULL[...没有任何反应...没什么好玩的,它只是在等待使用select(2)的连接如果我们等待足够长的时间,我们可能会看到生成新的密钥等等,但是让我们再次附加,告诉strace跟随叉子(-f),并且通过SSH连接]~/code/x86-os$sudostrace-p12218-f[在SSH登录期间发生了很多调用,只显示了一些][pid14692]read(3,"-----BEGINRSA私钥-----\n"...,1024)=1024[pid14692]open("/usr/share/ssh/blacklist.RSA-2048",O_RDONLY|O_LARGEFILE)=-1ENOENT(没有这样的文件或目录)[pid14692]打开(“/etc/ssh/blacklist.RSA-2048”,O_RDONLY|O_LARGEFILE)=-1ENOENT(没有这样的文件或目录)[pid14692]打开(“/etc/ssh/ssh_host_dsa_key",O_RDONLY|O_LARGEFILE)=3[pid14692]open("/etc/protocols",O_RDONLY|O_CLOEXEC)=4[pid14692]read(4,"#Internet(IP)protocols\n#\n#Up"...,4096)=2933[pid14692]open("/etc/hosts.allow",O_RDONLY)=4[pid14692]open("/lib/i386-linux-gnu/libnss_dns.so.2",O_RDONLY|O_CLOEXEC)=4[pid14692]stat64("/etc/pam.d",{st_mode=S_IFDIR|0755,st_size=4096,...})=0[pid14692]打开("/etc/pam.d/common-password",O_RDONLY|O_LARGEFILE)=8[pid14692]open("/etc/pam.d/other",O_RDONLY|O_LARGEFILE)=4理解SSH调用是一个棘手的问题,但如果你理解了它,你就可以学会跟踪它会是能够查看应用程序打开了哪个文件很有用(“此配置来自何处?”)。如果你有一个错误的进程,你能跟踪它并查看它对系统调用做了什么吗?当某些应用程序意外退出而没有提供适当的错误消息时,您可以检查它是否存在系统调用失败。您还可以使用过滤器查看每次调用的次数等:~/code/x86-os$strace-T-etrace=recvcurl-silentwww.google.com。>/dev/nullrecv(3,"HTTP/1.1200OK\r\nDate:Wed,05N"...,16384,0)=4164<0.000007>recv(3,"fla{color:#36c}a:visited{color:"..,16384,0)=2776<0.000005>recv(3,"adient(top,#4d90fe,#4787ed);filt"...,16384,0)=4164<0.000007>recv(3,"gbar.up.spd(b,d,1,!0);break;case"...,16384,0)=2776<0.000006>recv(3,"$),a.i.G(!0)),window.gbar.up.sl("...,16384,0)=1388<0.000004>recv(3,"margin:0;padding:5px8px06px;v"...,16384,0)=1388<0.000007>recv(3,"){window.setTimeout(function(){v"...,16384,0)=1484<0.000006>我鼓励您在操作系统中试用这些工具。好好利用它们会让你觉得自己拥有超能力。然而,足够有用的东西往往需要我们深入研究它的设计。我们可以看到,那些用户空间中的应用被严格限制在自己的虚拟地址空间内,运行在Ring3(非特权模式)。一般来说,只涉及计算和内存访问的任务不需要系统调用。例如,像strlen(3)和memcpy(3)这样的C库函数不需要内核做任何事情。这些都是应用程序内部发生的事情。C库函数的手册页部分(即括号中的2和3)也提供了线索。第2部分用于系统调用包装器,而第3部分包含其他C库函数。然而,正如我们在printf(3)中看到的那样,库函数最终可能会进行一个或多个系统调用。如果你很好奇,这里有一份完整的Linux系统调用列表(还有Filippo的列表)和Windows。它们分别有大约310和460个系统调用。查看这些系统调用很有趣,因为它们代表了软件在现代计算机上可以做什么。此外,你还可能在这里找到与进程间通信和性能相关的“宝藏”。这是一个“不懂Unix的人注定要重新发明一个蹩脚的Unix”的地方。(LCTT译注:原文“不懂Unix的人注定要重新发明它,可怜。”这句话是HenrySpencer的名言,反映了Unix的设计哲学。它的一些思想和文化是技术发展所必需的。结果,它看起来很糟糕但无法超越。)与CPU周期相比,许多系统调用需要很长时间才能执行任务,例如从硬盘读取数据。在这种情况下,调用进程会休眠直到基础工作完成。由于CPU运行速度非常快,一般程序由于I/O的限制,生命周期的大部分时间都处于休眠状态,等待系统调用返回。相反,如果您跟踪一项计算密集型任务,您通常会发现不涉及任何系统调用。在这种情况下,top(1)将显示大量CPU使用率。系统调用中的开销可能是个问题。例如,固态驱动器比硬盘驱动器快得多,但操作系统开销可能比I/O操作本身更昂贵。执行大量读写的程序可能是操作系统开销的瓶颈。向量化I/O对此有所帮助。因此,完成了文件的内存映射,使得程序只能通过访问内存来读取或写入磁盘文件。类似的映射存在于视频卡等地方。最终,云计算的经济性可能导致内核消除或用户模式/内核模式切换的最小化。最后,系统调用还有利于系统安全。一是不管一个二进制程序如何来历不明,你都可以通过观察它的系统调用来检查它的行为。这种方法可用于检测恶意程序。例如,我们可以记录未知程序的系统调用策略,对其异常行为进行告警,或者为程序调用指定白名单,提高漏洞利用的难度。在这方面,我们有很多研究,也有很多工具,但是还没有“杀手级”的解决方案。这是系统调用。很抱歉发了这么长的帖子,希望对你有用。随着时间的推移,我会写更多(短)文章,您也可以在RSS和Twitter上关注我。这篇文章献给光荣的米内罗竞技俱乐部。