0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

系统调用是如何实现的?

Linux阅码场 来源:Linuxer 作者:Linuxer 2021-02-20 16:46 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

这张图画了挺久的,主要是想让大家可以从全局角度,看下linux内核中系统调用的实现。

4156d6e4-71ad-11eb-8b86-12bb97331649.png

在讲具体的细节之前,我们先根据上图,从整体上看一下系统调用的实现。

系统调用的实现基础,其实就是两条汇编指令,分别是syscall和sysret。

syscall使执行逻辑从用户态切换到内核态,在进入到内核态之后,cpu会从 MSR_LSTAR 寄存器中,获取处理系统调用内核代码的起始地址,即上面的 entry_SYSCALL_64。

在执行 entry_SYSCALL_64 函数时,内核代码会根据约定,先从rax寄存器中获取想要执行的系统调用的编号,然后根据该编号从sys_call_table数组中找到对应的系统调用函数。

接着,从 rdi, rsi, rdx, r10, r8, r9 寄存器中获取该系统调用函数所需的参数,然后调用该函数,把这些参数传入其中。

在系统调用函数执行完毕之后,执行结果会被放到rax寄存器中。

最后,执行sysret汇编指令,从内核态切换回用户态,用户程序继续执行。

如果用户程序需要该系统调用的返回结果,则从rax中获取。

总体流程就是这样,相对来说,还是比较简单的,主要就是先去理解syscall和sysret这两条汇编指令,在理解这两条汇编指令的基础上,再去看内核源码,就会容易很多。

有关syscall和sysret指令的详细介绍,请参考Intel 64 and IA-32 Architectures Software Developer’s Manual。

有了上面对系统调用的整理理解,我们接下来看下其具体的实现细节。

以write系统调用为例,其对应的内核源码为:

419dd24c-71ad-11eb-8b86-12bb97331649.png

在内核中,所有的系统调用函数都是通过 SYSCALL_DEFINE 等宏定义的,比如上面的write函数,使用的是 SYSCALL_DEFINE3。

将该宏展开后,我们可以得到如下的函数定义:

41e59078-71ad-11eb-8b86-12bb97331649.png

由上可见,SYSCALL_DEFINE3宏展开后为三个函数,其中只有__x64_sys_write是外部可访问的,其它两个都有被static修饰,不能被外部访问,所以注册到上文中提到的sys_call_table数组里的函数,应该就是这个函数。

那该函数是怎么注册到这个数组的呢?

我们先不说答案,先来看下sys_call_table数组的定义:

4213a346-71ad-11eb-8b86-12bb97331649.png

由上可见,该数组各元素的默认值都是 __x64_sys_ni_syscall:

425994a0-71ad-11eb-8b86-12bb97331649.png

该函数也非常简单,就是直接返回错误码-ENOSYS,表示系统调用非法。

sys_call_table数组定义的地方好像只设置了默认值,并没有设置真正的系统调用函数。

我们再看看其他地方,看是否有代码会注册真正的系统调用函数到sys_call_table数组里。

可惜,并没有。

这就奇怪了,那各系统调用函数到底是在哪里注册的呢?

我们再回头仔细看下sys_call_table数组的定义,它在设置完默认值之后,后面还include了一个名为asm/syscalls_64.h的头文件,这个位置include头文件还是比较奇怪的,我们看下它里面是什么内容。

但是,这个文件居然不存在。

那我们只能初步怀疑这个头文件是编译时生成的,带着这个疑问,我们去搜索相关内容,确实发现了一些线索:

4298af50-71ad-11eb-8b86-12bb97331649.png

这个文件确实是编译时生成的,上面的makefile中使用了syscalltbl.sh脚本和syscall_64.tbl模板文件来生成这个syscalls_64.h头文件。

我们来看下syscall_64.tbl模板文件的内容:

42bb097e-71ad-11eb-8b86-12bb97331649.png

这里确实定义了write系统调用,且标明了它的编号是1。

我们再来看下生成的syscalls_64.h头文件:

431eb82a-71ad-11eb-8b86-12bb97331649.png

这里面定义了很多好像宏调用一样的东西。

__SYSCALL_COMMON,这个不就是sys_call_table数组定义那里define的那个宏嘛。

再去上面看下__SYSCALL_COMMON这个宏定义,它的作用是将sym表示的函数赋值到sys_call_table数组的nr下标处。

所以对于__SYSCALL_COMMON(1, sys_write)来说,它就是注册__x64_sys_write函数到sys_call_table数组下标为1的槽位处。

而这个__x64_sys_write函数,正是我们上面猜测的,SYSCALL_DEFINE3定义的write系统调用,展开之后的一个外部可访问的函数。

这样就豁然开朗了,原来真正的系统调用函数的注册,是通过先定义__SYSCALL_COMMON宏,再include那个根据syscall_64.tbl模板生成的syscalls_64.h头文件来完成的,非常巧妙。

系统调用函数注册到sys_call_table数组的过程,到这里已经非常清楚了。

下面我们继续来看下哪里在使用这个数组:

4348f482-71ad-11eb-8b86-12bb97331649.png

do_syscall_64在使用,方式是先通过nr在sys_call_table数组中找到对应的系统调用函数,然后再调用该函数,将regs传入其中。

这个流程和我们上面预估的一样,且传入的regs参数类型,和我们上面注册的系统调用函数所需的类型也一样。

那也就是说,regs参数的字段里,是带着各系统调用函数所需的参数的,SYSCALL_DEFINE等宏展开出来的一系列函数,会从这些字段中提取出真正的参数,然后对其进行类型转换,最后这些参数被传入到最终的系统调用函数中。

对于上面的write系统调用宏展开后的那些函数,__x64_sys_write会先从regs中提取出di, si, dx字段作为真正参数,然后__se_sys_write会将这些参数转成正确的类型,最后__do_sys_write函数被调用,转换后的这些参数被传入其中。

在系统调用函数执行完毕后,其结果会被赋值到了regs的ax字段里。

由上可见,系统调用函数的参数及返回值的传递,都是通过regs来完成的。

但文章开始的时候不是说,系统调用的参数及返回值的传递,是通过寄存器来完成的吗,这里怎么是通过struct pt_regs的字段呢?

先别急,先来看下struct pt_regs的定义:

43838782-71ad-11eb-8b86-12bb97331649.png

你有没有发现,这里面的字段名都是寄存器的名字。

那是不是说,在执行系统调用的代码里,有逻辑把各寄存器里的值放到了这个结构体的对应字段里,在结束系统调用时,这些字段里的值又被赋值到各个对应的寄存器里呢?

离真相越来越近。

我们继续看使用了do_syscall_64的地方:

43e020fa-71ad-11eb-8b86-12bb97331649.png

上图中的entry_SYSCALL_64方法,就是系统调用流程中最重要的一个方法了,为了便于理解,我对该方法做了很多修改,并添加了很多注释。

这里需要注意的是100行到121行这段逻辑,它将各寄存器的值压入到栈中,以此来构建struct pt_regs对象。

这就能构建出一个struct pt_regs对象了?

是的。

我们回上面看下struct pt_regs的定义,看其字段名字及顺序是不是和这里的压栈顺序正好相反。

我们再想下,当我们要构建一个struct pt_regs对象时,我们要为其在内存中分配一块空间,然后用一个地址来指向这段空间,这个地址就是该struct pt_regs对象的指针,这里需要注意的是,这个指针里存放的地址,是这段内存空间的最小地址。

再看上面的压栈过程,每一次压栈操作我们都可以认为是在分配内存空间并赋值,当r15被最终压入到栈中后,整个内存空间分配完毕,且数据也初始化完毕,此时,rsp指向的栈顶地址,就是这段内存空间的最小地址,因为压栈过程中,栈顶的地址是一直在变小的。

综上可知,在压栈完毕后,rsp里的地址就是一个struct pt_regs对象的地址,即该对象的指针。

在构建完struct pt_regs对象后,123行将rax中存放的系统调用编号赋值到了rdx里,124行将rsp里存放的struct pt_regs对象的地址,即该对象的指针,赋值到了rsi中,接着后面执行了call指令,来调用do_syscall_64方法。

调用do_syscall_64方法之前,对rdi和rsi的赋值,是为了遵守c calling convention,因为在该calling convention中约定,在调用c方法时,第一个参数要放到rdi里,第二个参数要放到rsi里。

我们再去上面看下do_syscall_64方法的定义,参数类型及顺序是不是和我们这里说的是完全一样的。

在调用完do_syscall_64方法后,系统调用的整个流程基本上就快结束了,上图中的129行到133行做的都是一些寄存器恢复的工作,比如从栈中弹出对应的值到rax,rip,rsp等等。

这里需要注意的是,栈中rax的值是在上面do_syscall_64方法里设置的,其存放的是系统调用的最终结果。

另外,在栈中弹出的rip和rsp的值,分别是用户态程序的后续指令地址及其堆栈地址。

最后执行sysret,从内核态切换回用户态,继续执行syscall后面逻辑。

到这里,完整的系统调用处理流程就已经差不多说完了,不过这里还差一小步,就是syscall指令在进入到内核态之后,是如何找到entry_SYSCALL_64方法的:

445e0cae-71ad-11eb-8b86-12bb97331649.png

它其实是注册到了MSR_LSTAR寄存器里了,syscall指令在进入到内核态之后,会直接从这个寄存器里拿系统调用处理函数的地址,并开始执行。

系统调用内核态的逻辑处理就是这些。

下面我们用一个例子来演示下用户态部分:

44aa1ba8-71ad-11eb-8b86-12bb97331649.png

编译并执行:

44ec4ee2-71ad-11eb-8b86-12bb97331649.png

我们用syscall来执行write系统调用,写的字符串为Hi ,syscall执行完毕后,我们直接使用ret指令将write的返回结果当作程序的退出码返回。

所以在上图中,输出了Hi,且程序的退出码是3。

如果对上面的汇编不太理解,可以把它想像成下面这个样子:

455a1bfc-71ad-11eb-8b86-12bb97331649.png

在这里,我们使用的是glibc中的write方法来执行该系统调用,其实该方法就是对syscall指令做的一层封装,本质上使用的还是我们上面的汇编代码。

这个例子到这里就结束了。

有没有觉得不太尽兴?

我们分析了这么多的代码,最终就用了这么个小例子就结束了,不行,我们要再做点什么。

要不我们来自己写个系统调用?

说干就干。

我们先在write系统调用下面定义一个我们自己的系统调用:

458e3fcc-71ad-11eb-8b86-12bb97331649.png

该方法很简单,就是将参数加10,然后返回。

再把这个系统调用在syscall_64.tbl里注册一下,编号为442:

45ce44f0-71ad-11eb-8b86-12bb97331649.png

编译内核,等待执行。

我们再把上面写的那个hi程序改下并编译好:

4630f078-71ad-11eb-8b86-12bb97331649.png

然后在虚拟机中启动新编译的linux内核,并执行上面的程序:

466ec3f8-71ad-11eb-8b86-12bb97331649.png

看结果,正好就是20。

搞定,收工。

原文标题:精致全景图 | 系统调用是如何实现的

文章出处:【微信公众号:Linuxer】欢迎添加关注!文章转载请注明出处。

责任编辑:haq

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 寄存器
    +关注

    关注

    31

    文章

    5589

    浏览量

    129059
  • 系统调用
    +关注

    关注

    0

    文章

    28

    浏览量

    8611

原文标题:精致全景图 | 系统调用是如何实现的

文章出处:【微信号:LinuxDev,微信公众号:Linux阅码场】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    系统调用和API有什么区别呢?

    其实你已经明白了,操作系统本身也是一堆代码,它本身也有很多能力可以供我们使用,操作系统就像前面举例中的发动机、餐厅、游戏或者一个代码的功能模块一样,常说的系统调用system call
    发表于 12-03 06:52

    连载|开发工具,易安卓让系统功能调用像写应用逻辑一样简单

    通过自研系统封装库,易安卓将复杂的系统控制能力以接口形式开放给开发者,让系统功能调用像写应用逻辑一样简单。
    的头像 发表于 11-27 11:40 87次阅读
    连载|开发工具,易安卓让<b class='flag-5'>系统</b>功能<b class='flag-5'>调用</b>像写应用逻辑一样简单

    Jumia API 调用:覆盖非洲市场的实操指南

    一、调用前的四大核心准备(适配 Jumia 地区特性)​ Jumia API 的调用准备需围绕 “地区差异化” 展开,这是区别于其他电商 API 的关键前提。​ 1. 开发者账号与 API Key
    的头像 发表于 11-25 17:12 359次阅读

    探索操作系统底层的关键接口

      在linux中,将程序的运行空间分为内核空间与用户空间(内核态和用户态),在逻辑上它们之间是相互隔离的,因此用户程序不能访问内核数据,也无法使用内核函数。当用户进程必须访问内核或使用某个内核函数时,就得使用系统调用(System Call)。在Linux中,
    的头像 发表于 11-08 12:42 581次阅读

    深入了解系统调用API:探索操作系统底层的关键接口

    ,也无法使用内核函数。当用户进程必须访问内核或使用某个内核函数时,就得使用系统调用(System Call)。在Linux中,系统调用是用户空间访问内核空间的唯一途径。 什么是
    的头像 发表于 11-03 09:20 437次阅读

    Python调用API教程

    两个不同系统之间的信息交互。在这篇文章中,我们将详细介绍Python调用API的方法和技巧。 一、用Requests库发送HTTP请求 使用Python调用API的第一步是发送HTTP请求,通常
    的头像 发表于 11-03 09:15 325次阅读

    GCC编译器,怎么才能实现c文件中未被调用的函数,不会被编译呢?

    GCC编译器,怎么才能实现c文件中未被调用的函数,不会被编译?有什么编译选项可以设置吗? 移植代码,有些函数没被调用的函数想留在代码里,但不想被编译,编译的话报错报警告啥的太多了,而且编译起来也慢。 谢谢!
    发表于 09-28 12:25

    RK3568驱动指南|驱动基础进阶篇-进阶7 向系统中添加一个系统调用

    RK3568驱动指南|驱动基础进阶篇-进阶7 向系统中添加一个系统调用
    的头像 发表于 05-21 14:15 551次阅读
    RK3568驱动指南|驱动基础进阶篇-进阶7 向<b class='flag-5'>系统</b>中添加一个<b class='flag-5'>系统</b><b class='flag-5'>调用</b>

    研发排查问题的利器:一款方法调用栈跟踪工具

    作者:京东物流 郭忠强 导语 本文从日常值班问题排查痛点出发,分析方法复用的调用链路和上下文业务逻辑,通过思考分析,借助栈帧开发了一个方法调用栈的链式跟踪工具,便于展示一次请求的方法串行调用
    的头像 发表于 05-06 17:24 3011次阅读
    研发排查问题的利器:一款方法<b class='flag-5'>调用</b>栈跟踪工具

    verilog模块的调用、任务和函数

    在做模块划分时,通常会出现这种情形,某个大的模块中包含了一个或多个功能子模块,verilog是通过模块调用或称为模块实例化的方式来实现这些子模块与高层模块的连接的.
    的头像 发表于 05-03 10:29 1286次阅读
    verilog模块的<b class='flag-5'>调用</b>、任务和函数

    在Vivado调用MIG产生DDR3的问题解析

    下面是调用的DDR3模块的,模块的倒数第二行是,模块的时钟输入,时钟源来自PLL产生的系统时钟的倍频。
    的头像 发表于 05-03 10:21 1216次阅读
    在Vivado<b class='flag-5'>调用</b>MIG产生DDR3的问题解析

    请问STM32cubeProgrammer是否有提供API用于设计定制化的升级软件?

    升级的应用。其实也可以理解为这款应用是对STM32cubeProgrammer的一个模仿,只不过GUI界面是定制为我们公司的。 所以我想问一下STM32cubeProgrammer是否有提供一些API,能够让我们设计的应用能够调用这些API从而
    发表于 03-07 07:27

    NFS网络文件系统深度解析

    Procedure Call Protocol 远程过程调用实现RPC采用C/S模式,客户机请求程序调用进程发送一个有进程参数的调用信息到服务进程,然后等待应答信息。
    的头像 发表于 03-01 14:15 1168次阅读

    ShiMetaOS | 怎样调用ShiMeta信息发布软件快速打造数字标牌产品

    发布软件无缝集成于ShiMetaOS系统中,无需额外授权即可使用,极大地降低了使用成本,为用户提供了一款高性价比的解决方案。二、怎样调用ShiMetaOS的信息发
    的头像 发表于 02-05 17:10 1048次阅读
    ShiMetaOS  | 怎样<b class='flag-5'>调用</b>ShiMeta信息发布软件快速打造数字标牌产品

    开源大师兄怎么刷鸿蒙系统的,那些传感器配件还能调用

    开源大师兄怎么刷鸿蒙系统的,那些传感器配件还能调用
    发表于 01-03 14:32