在探索“ Go Language Call Time.now()之前,是否有系统调用?”我们首先查看系统的电话是什么。
系统调用是指运行操作系统内核的服务,要求操作系统内核请求操作系统内核请求。它是什么服务?这些服务是指操作系统核心管理的服务,例如流程管理,存储,内存,网络等。以文件为例,用户程序需要调用和两个系统调用。在C语言中,它要么使用LIBC库实现(底层也是系统调用),要么直接调用系统。
为什么必须使用Linux系统通过系统调用来访问特殊资源?您不能在用户空间中完成呼叫访问功能吗?此设计之所以考虑系统隔离,提高系统安全性和容错性并避免恶意攻击。操作系统将CPU访问资源的安全水平分为4水平。这些级别称为特权级别,也称为CPU环。在任何时候,CPU都在特定的特权级别下运行,该级别确定了要做什么和不能做什么。这些级别可以一一将其视为一个环,这是最高的特权戒指。外部是RING1,RING2,最后是最低的特权环。当系统调用时,应用程序将从应用程序空间进入内核空间。目前,特权级别将从RING3提高到RING0,并且应用代码还将跳到相关的系统调用代码以执行。
早些时候,系统调用是通过软中断实现的。因为软中断实现方法需要扫描中断描述表以查找系统调用相应的入口地址,并且性能很差。为此,Linux系统引入了适当的系统调用指令以完成系统调用。64位系统下的相关指令是SYSCALL/SYSRET指令,我们需要知道的是,当系统调用时,用户需要切换内核,这将导致某些性能损失。
在回顾了系统调用的概念之后,我们将使用strace命令查看以下代码中是否使用以下代码。
执行以下命令,首先构建二进制可执行文件测试,然后使用Strace在测试过程中查看所有系统调用,以查看是否使用任何系统调用时间:
结果,我们发现在致电time.now()时,我们没有使用与时间相关的任何系统调用。我们可以得出结论,呼叫time.now()。上面介绍的系统呼叫概念,因为采集时间需要读取系统时钟信息。它属于RING0特权,需要使用系统来调用。
接下来,让我们分析time.now()的实现以查看调用它时发生了什么?
有两种分析源代码的方法。首先是直接检查源代码。在查看源代码的过程中,源代码内容很大,并且有汇编代码和多系统支持。代码编辑器不支持良好的支持提示和跳跃。我们需要使用全局搜索相关的关键字来查找功能或变量位置。第二是使用GDB或DLV调试工具来通过中断来跟踪执行过程的源代码这两种方法通常混合在一起。这段时间我们将使用GDB进行分析。作者的系统环境如下:
首先,我们启动GDB,然后设置断点并在主要函数上运行程序:
接下来,我们在时间上设置断点。
从上图,我们可以看到time.now()源代码位于time/time.go file.time.time.now()function中的第1121行中.NEXT我们查看现在()函数的实现:
从上图,您可以看到,当我们查看now()函数时,它会跳至time_now()。这是由于汇编指令,该指令用于链接当前源文件中的私有函数或变量链接到指定的方法或变量进行编译时。例如,这意味着将time_now链接到时间。。去。
接下来,我们查看time_now.now的实现。从下面的图,您可以看到Walltime位于文件的23号中,并调用WallTime1函数。WallTime1由汇编程序实现,源代码为位于第209位。
接下来,让我们看一下装配代码。我们仅关心功能部分,特别是在SYS_LINUX_AMD64.S文件中的209至210之间的汇编代码。核心部分已标记为箭头:
在上图中,装配代码主要完成两个功能。首先,将Goroutine堆栈切换到G0堆栈。
根据GMP模型,由M执行的堆栈可能是系统堆栈(IE G0堆栈)或Signal Stack,或者用户线程堆栈(即Gorountine stack)可能会返回的G0要执行G0,GSIGNAL或GSIGNAL或与M关联的Goroutine,并且返回始终是与Goroutine相关的Goroutine,然后我们可以比较当前M的当前M执行当前M。堆栈类型判断的系统堆栈是基于此的。
第二个函数是调用变量指向获得当前秒和毫秒的函数。这也是时间的核心。
从以上,您可以看到time.now()最终调用该变量指向的函数,而函数输入地址为。为什么使用变量指向函数地址,而不是在正常情况下通过函数符号获得地址?我们首先推测功能地址没有固定,它将随着不同的应用程序而更改,它需要在运行时动态获取地址。
接下来,让我们看一下入口地址的汇编代码作为一个函数:
我们可以看到与地址相对应的函数名称。
GDB一直进行调试,最后我们发现我们必须了解该变量如何分配给clock_getTime函数入口地址。
我们知道,当GO应用程序启动时,GO将在运行时完成全局变量(例如NCPU,G0和SCHET)的初始化。这也不例外。他们在执行主函数之前已经完成了初始化。因此,当我们使用Watch命令观察变量更改时,我们必须在启动应用程序时开始。
观察变量更改时,我们可以看到该函数会更改其值。它分配了变量的值,即函数clock_getTime的入口地址。
应该注意的是,当您在GDB中访问此变量时,运行时和VDSocloclockettimesym之间的点号(。)以及汇编中的点号(·)不同。
接下来,我们使用bt命令,我们可以看到整个函数堆栈帧,我们可以根据Tuzu打开代码编辑器:
此时,我们使用GDB分析来跟踪time.now()。我们使用代码编辑器查看位于文件中的vdsoparsembols函数。该文件的开头有这样的句子:.. combin combin with函数名称,您可以知道,vdsoparsesymbols用于完成VDSO的符号分析。这引入了VDSO的概念。
VDSO是虚拟动态共享对象的缩写。中文名称是一个虚拟动态共享对象,它是Linux内核公开用户空间内核函数的一种机制。VDSO实现方法是直接映射某些直接涉及内核中安全性的系统调用空间,然后用户代码不再使用系统调用,也可以完成相关功能。在避免系统调用时,需要在用户空间之间切换到内核空间,VDSO机制可以减少性能消耗。VDSO。支持系统呼叫等,等人等人等。
我们可以找到该过程的内存映射以找到VDSO模块:
可以从上面找到VDSO地址来自。
为了安全性并防止更换恶意程序,VDSO的起始地址尚未固定,并且每个二进制应用程序的VDSO都不同。我们可以使用以下命令来测试和验证。您可以看到执行VDSO的开始地址不同:
接下来,我们尝试将VDSO的信息保存在存储器中,以查看它的格式?有两种方法可以在此处介绍。
使用GDB的第一个转储命令在过程的过程中保存VDSO零件。首先,我们在应用程序过程内存中使用VDSO的启动地址,然后使用dump memory命令保存相应开始地址的内存数据到vdso.so文件。
第二种方法是编写代码以自己实施,请单击以查看完整的源代码。
在获得上面介绍的VDSO文件后,我们可以使用命令查看文件类型,以及命令查看其信息。
我们从上图再次看到了它。
从上面的介绍中,当我们知道以GO语言调用Call.ming()时,没有系统调用,因为它使用VDSO技术将系统cally_getTime映射到应用程序空间。系统呼叫。
上面的简介还提到,VDSO的入口地址尚未固定,那么GO语言如何找到此入口地址并找到函数地址?
GO语言可以通过阅读辅助向量信息来获得VDSO启动地址,然后阅读VDSO信息以解析地址。AuxiliaryVectors是内核精灵二进制加载程序提供的一些信息的集合,包括可执行输入地址,诸如可执行信息,线程,线程UID,VDSO条目地址和其他信息。
辅助向量包含一系列钥匙值对,每个键对应于一个值。与VDSO条目地址相对应的密钥是AT_SYSINFO_EHDR.,您可以检查系统以调用getAuxval手册。运行时如下,并且不会重复具体细节:
文本末尾有一个思考的问题:call.sleep()以go语言的方式会发生系统调用吗?