「内核知识」Linux下的系统调用write

news2025/2/1 11:52:36

本文以x86_64平台为例,分析linux下的系统调用是如何被执行的。

假设目标系统调用是,其对应的内核源码为:

// fs/read_write.c
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
                size_t, count)
{
        return ksys_write(fd, buf, count);
}

这里主要看下SYSCALL_DEFINE3这个宏定义:

// include/linux/syscalls.h
#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)
...
#define SYSCALL_DEFINEx(x, sname, ...)                          \
        ...
        __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
【文章福利】小编推荐自己的Linux内核技术交流群:【 865977150】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!前100名进群领取,额外赠送一份价值 699的内核资料包(含视频教程、电子书、实战项目及代码)

资料直通车:最新Linux内核源码资料文档+视频资料

学习直通车:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈

该宏又引用了__SYSCALL_DEFINEx,继续看下:

// arch/x86/include/asm/syscall_wrapper.h
#define __SYSCALL_DEFINEx(x, name, ...)                                 \
        asmlinkage long __x64_sys##name(const struct pt_regs *regs);    \
        ...                                                             \
        static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__));     \
        static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\
        asmlinkage long __x64_sys##name(const struct pt_regs *regs)     \
        {                                                               \
                return __se_sys##name(SC_X86_64_REGS_TO_ARGS(x,__VA_ARGS__));\
        }                                                               \
        ...                                                             \
        static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__))      \
        {                                                               \
                long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));\
                ...                                                     \
                return ret;                                             \
        }                                                               \
        static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))

该宏的参数中,x为3,name为_write,...代表的__VA_ARGS__为unsigned int, fd, const char __user *, buf, size_t, count。

接着,在宏的定义中,先声明了三个函数,分别为__x64_sys_write、_se_sys_write、__do_sys_write,紧接着,定义了__x64_sys_write和_se_sys_write的实现,__x64_sys_write内调用_se_sys_write,_se_sys_write内调用__do_sys_write。

__do_sys_write只是一个方法头,它和最开始的write系统调用的方法体构成完整的方法。

由上可以看到,三个方法中,只有__x64_sys_write方法没有static,即只有它是外部可调用的,所以我们看下哪里引用了__x64_sys_write。

// arch/x86/entry/syscalls/syscall_64.tbl
#
# 64-bit system call numbers and entry vectors
#
# The format is:
# <number> <abi> <name> <entry point>
#
# The __x64_sys_*() stubs are created on-the-fly for sys_*() system calls
#
# The abi is "common", "64" or "x32" for this file.
#
0       common  read                    __x64_sys_read
1       common  write                   __x64_sys_write
...

我们会在一个非c文件中,找到了对__x64_sys_write方法的引用,但这个文件又是怎么被使用的呢?

根据arch/x86/entry/syscalls/Makefile我们可以知道,是有对应的shell脚本,根据上面的文件来生成c版的头文件,比如下面两个。

kernel内部使用的:

// arch/x86/include/generated/asm/syscalls_64.h
#ifdef CONFIG_X86
__SYSCALL_64(0, __x64_sys_read, )
#else /* CONFIG_UML */
__SYSCALL_64(0, sys_read, )
#endif
#ifdef CONFIG_X86
__SYSCALL_64(1, __x64_sys_write, )
#else /* CONFIG_UML */
__SYSCALL_64(1, sys_write, )
#endif
...

给用户使用的:

// arch/x86/include/generated/uapi/asm/unistd_64.h
#define __NR_read 0
#define __NR_write 1
...

那生成的这两个头文件又是给谁使用的呢?看下下面这个文件:

// arch/x86/entry/syscall_64.c
#define __SYSCALL_64(nr, sym, qual) [nr] = sym,

asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
        /*
         * Smells like a compiler bug -- it doesn't work
         * when the & below is removed.
         */
        [0 ... __NR_syscall_max] = &sys_ni_syscall,
#include <asm/syscalls_64.h>
};

该文件中定义了一个const的数组变量sys_call_table,数组下标为系统调用的编号,值为该编号对应的系统调用方法。

最开始整个数组都初始化为sys_ni_syscall,该方法内会返回错误码ENOSYS,表示对应的方法未实现。

接着用#include <asm/syscalls_64.h>的方式再初始化存在的系统调用。

该include的文件就是上面生成的arch/x86/include/generated/asm/syscalls_64.h,syscalls_64.h文件里调用__SYSCALL_64,为对应的系统下标赋值。

最后,sys_call_table[1] = __x64_sys_write。

到这里,我们基本可以猜测,肯定有个地方是根据系统调用的编号,到数组sys_call_table中找到对应方法,然后调用。

让我们来看下这段代码在哪里

// arch/x86/entry/common.c
__visible void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
        ...
        if (likely(nr < NR_syscalls)) {
                nr = array_index_nospec(nr, NR_syscalls);
                regs->ax = sys_call_table[nr](regs);
        }
        ...
}

上面的方法就是我们要找的方法。

我们再看下这个方法是在哪里被调用的。

// arch/x86/entry/entry_64.S
ENTRY(entry_SYSCALL_64)
        ...
        call    do_syscall_64           /* returns with IRQs disabled */
        ...

上面的就是对应的汇编代码了,这里为了简单,省略掉了该汇编方法的其他部分。

那这段汇编代码又是在哪里调用的呢?

// arch/x86/kernel/cpu/common.c
void syscall_init(void)
{
        ...
        wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);
        ...
}

在上面的方法中,我们可以看到,汇编代码entry_SYSCALL_64被写到了MSR_LSTAR表示的寄存器中。

该寄存器的作用就是,当我们执行syscall机器指令时,MSR_LSTAR寄存器中存放的对应方法就会被执行,即在user space,我们只要执行syscall机器指令,给它对应的系统调用编号和参数,kernel space里对应的系统调用就会被执行了。

有兴趣的可以分析并执行下下面的汇编代码,好好体会下整个系统调用的流程。

# ----------------------------------------------------------------------------------------
# Writes "Hello, World" to the console using only system calls. Runs on 64-bit Linux only.
# To assemble and run:
#
#     gcc -c hello.s && ld hello.o && ./a.out
#
# or
#
#     gcc -nostdlib hello.s && ./a.out
# ----------------------------------------------------------------------------------------

        .global _start

        .text
_start:
        # write(1, message, 13)
        mov     $1, %rax                # system call 1 is write
        mov     $1, %rdi                # file handle 1 is stdout
        mov     $message, %rsi          # address of string to output
        mov     $13, %rdx               # number of bytes
        syscall                         # invoke operating system to do the write

        # exit(0)
        mov     $60, %rax               # system call 60 is exit
        xor     %rdi, %rdi              # we want return code 0
        syscall                         # invoke operating system to exit
message:
        .ascii  "Hello, world\n"

到这里,系统调用对应的kernel space部分就已经分析完毕了,下篇文章我们结合对应的c源码,看下user space的部分是如何实现的。

简而言之就是通过一定的约定来实现指定系统调用编号和传递参数及返回值。

比如x86_64平台,在执行syscall机器码之前,系统调用的编号要先放到rax寄存器,参数要分别放到rdi、rsi、rdx、r10、r8、r9寄存器中,这样kernel中的代码就会从这些地方取值,然后继续执行逻辑,当kernel部分的逻辑完成之后,结果会再放到rax寄存器中,这样user space的部分就可以从rax寄存器中拿到返回值。

下面我们再来看下上篇文章最后的例子:

# ----------------------------------------------------------------------------------------
# Writes "Hello, World" to the console using only system calls. Runs on 64-bit Linux only.
# To assemble and run:
#
#     gcc -c hello.s && ld hello.o && ./a.out
#
# or
#
#     gcc -nostdlib hello.s && ./a.out
# ----------------------------------------------------------------------------------------

        .global _start

        .text
_start:
        # write(1, message, 13)
        mov     $1, %rax                # system call 1 is write
        mov     $1, %rdi                # file handle 1 is stdout
        mov     $message, %rsi          # address of string to output
        mov     $13, %rdx               # number of bytes
        syscall                         # invoke operating system to do the write

        # exit(0)
        mov     $60, %rax               # system call 60 is exit
        xor     %rdi, %rdi              # we want return code 0
        syscall                         # invoke operating system to exit
message:
        .ascii  "Hello, world\n"

现在就非常明白了吧,比如第一个write系统调用,因为其编号为1,所以先将1放到rax里,之后将标准输出文件描述符到到rdi里,再之后将message地址放到rsi里,再之后将message的长度13放到rdx里,最后调用syscall机器码,这样就会转到对应kernel space部分的代码。

从汇编角度我们已经讲明白了,那在c语言中我们又是如何调用呢?总不能在c中嵌入汇编代码吧?

其实本质上就是在c中嵌入汇编代码,只是不是我们来做,而是glibc来帮我做。

再来看个例子:

#include <unistd.h>

int main(int argc, char *argv[]) {
  write(STDOUT_FILENO, "Hello, World\n", 13);
  return 60;
}

这个例子就是上面汇编代码对应的c实现,编译执行之后也是会输出同样的内容。

注意,这里的write并不是kernel内部的系统调用write,而是glibc中的一个wrapper,这个wrapper里面再帮我们调用真正的系统调用write。

我们再来看下对应的glibc的代码:

// sysdeps/unix/sysv/linux/write.c
/* Write NBYTES of BUF to FD.  Return the number written, or -1.  */
ssize_t
__libc_write (int fd, const void *buf, size_t nbytes)
{
  return SYSCALL_CANCEL (write, fd, buf, nbytes);
}
...
weak_alias (__libc_write, write)
...

这里需要注意的是,write方法其实是__lib_write的一个weak alias,当我们调用write时,其实相当于我们在调用__lib_write。

继续看下SYSCALL_CANCEL宏:

// sysdeps/unix/sysdep.h
#define SYSCALL_CANCEL(...) \
  ({                                                                         \
    long int sc_ret;                                                         \
    if (SINGLE_THREAD_P)                                                     \
      sc_ret = INLINE_SYSCALL_CALL (__VA_ARGS__);                            \
    else                                                                     \
      {
        ...                                                                  \
      }                                                                      \
    sc_ret;                                                                  \
  })

这个宏里面又调用了INLINE_SYSCALL_CALL,INLINE_SYSCALL_CALL里又调用了很多其他的宏,这里就不一一展开了,有兴趣的朋友可以留言,我们再一起交流。

最终,会调用下面的宏。

// sysdeps/unix/sysv/linux/x86_64/sysdep.h
#define internal_syscall3(number, err, arg1, arg2, arg3)                \
({                                                                      \
    unsigned long int resultvar;                                        \
    TYPEFY (arg3, __arg3) = ARGIFY (arg3);                              \
    TYPEFY (arg2, __arg2) = ARGIFY (arg2);                              \
    TYPEFY (arg1, __arg1) = ARGIFY (arg1);                              \
    register TYPEFY (arg3, _a3) asm ("rdx") = __arg3;                   \
    register TYPEFY (arg2, _a2) asm ("rsi") = __arg2;                   \
    register TYPEFY (arg1, _a1) asm ("rdi") = __arg1;                   \
    asm volatile (                                                      \
    "syscall\n\t"                                                       \
    : "=a" (resultvar)                                                  \
    : "0" (number), "r" (_a1), "r" (_a2), "r" (_a3)                     \
    : "memory", REGISTERS_CLOBBERED_BY_SYSCALL);                        \
    (long int) resultvar;                                               \
})

是不是很熟悉,这就是我们上面手写的汇编代码啊。

到此,整个流程就全部通了。

我们在写c时(其他语言也一样),调用的其实是glibc里的wrapper,glibc里的wrapper再帮我们调用对应的系统调用,之后再将结果从rax中取出,返回给我们,这样我们使用起来就非常方便了

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/104402.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

通过国产化低代码平台搭建设备管理系统,助力中国航天企业信息化建设

编者按&#xff1a;掌握软件自主权&#xff0c;支持信创国产化。本文分析了国产化低代码平台的意义&#xff0c;并介绍了低低代码平台是如何为航天企业提高信息化建设水平的。 关键词&#xff1a;国产化&#xff0c;第三方对接能力&#xff0c;文件管理 以容器、微服务、DevOp…

Linux内核中的open方法

在linux下&#xff0c;假设我们想打开文件/dev/tty&#xff0c;我们可以使用系统调用open&#xff0c;比如&#xff1a; int fd open("/dev/tty", O_RDWR, 0); 本文将从源码角度看下&#xff0c;在linux内核中&#xff0c;open方法是如何打开文件的。 首先看下入…

【opencv】centos下opencv的编译(带opencv_contrib扩展包)

目录1.安装cmake2.opencv安装文件准备可选安装3.进行编译4.进行安装5.使用测试编译步骤&#xff1a;1.安装cmake 安装wget: 应该已安装gcc工具套组&#xff1a; yum install sudo yum install -y gcc gcc-c make automake yum install -y wget wget https://cmake.org/files…

【树莓派不吃灰】兄弟连篇④ Shell编程

目录1、Shell基础1.1 脚本执行方式1.1.1 echo1.1.2 脚本执行1.2 Bash基本功能1.2.1 history1.2.2 命令补全1.2.3 命令别名1.2.4 常用快捷键1.2.5 输出输入重定向1.2.6 多命令顺序执行与管道符1.2.7 通配符和特殊符号1.3 Bash变量1.3.1 用户自定义变量1.3.2 环境变量1.3.3 位置变…

Hudi入门到实战

简介 Apache Hudi&#xff08;Hadoop Upserts Delete and Incremental&#xff09;是下一代流数据湖平台。Apache Hudi将核心仓库和数据库功能直接引入数据湖。Hudi提供了表、事务、高效的upserts/delete、高级索引、流摄取服务、数据集群/压缩优化和并发&#xff0c;同时保持…

用于医学诊断的菁染料ICG-Sulfo-OSu,活性基团修饰ICG

英文名&#xff1a;ICG-Sulfo-EG4-OSu 激发波长: 780nm; 发射波长: 800nm 分子量: 1777.36 溶剂:DMSO 凯新生物描述&#xff1a; (ICG)是一种用于医学诊断的菁染料。它用于测定心输出量、肝功能和肝脏血流量&#xff0c;以及用于眼科血管造影。它的峰值光谱吸收接近800纳米…

【课程整理】随机系统期末整理

文章目录1 概率论部分 (1-4)概率空间随机变量概率分布随机变量的函数仍然是随机变量条件期望2 随机过程 (5-7)随机过程Martingale停时马尔科夫链3 参数估计 (8-10)参数估计问题充分统计量贝叶斯估计非随机估计部分思维导图如下&#xff0c;私信发送html完整版 1 概率论部分 (1-…

3.path路径模块

目录 1 路径问题 2 直接给绝对路径 3 用 __dirname 表示绝对路径 4 path模块常用方法 4.1 路径拼接 path.join() 4.2 从路径字符串中解析出文件名 path.basename() 4.3 获取路径中的扩展名 path.extname() 1 路径问题 当我们使用 ./ 或是 ../ 表示相对路径的时…

小林Coding阅读笔记:操作系统篇之硬件结构,中断问题

前言 参考/导流&#xff1a; 小林coding - 2.6 什么是软中断&#xff1f;学习意义 学习CPU与外设如何去提升处理效率的设计思想&#xff0c;异步机制的理解与借鉴掌握相关的Linux命令&#xff0c;帮助问题排查 相关说明 该篇博文是个人阅读的重要梳理&#xff0c;仅做简单参…

【修饰性PEG供应商】mPEG-DBCO_DBCO mPEG_甲氧基聚乙二醇环辛炔

【产品描述】 西安凯新生物科技有限公司是国内业PEG供应商&#xff0c;可以提供不同分子量的PEG衍生物&#xff0c;小分子PEG的循环节可以做到1-36个&#xff0c;高分子PEG分子量从1000-40000不等&#xff0c;可以修饰的基团有&#xff1a;氨基类&#xff0c;NHBOC类&#xff0…

多层板PCB设计中电源平面相对地平面为什么要进行内缩

大家是否观察过&#xff0c;有一些人绘制的PCB&#xff0c;在GND层和电源层会进行一定程度的内缩设计&#xff0c;那么大家有没有想过为什么要内缩呢。需要搞清楚这个问题&#xff0c;我们需要来先了解一个知识点&#xff0c;那就是“20H”原则&#xff1a; 20H原则主要是为了减…

MySQL的数据结构

阅读目录MySQL 数据结构用 btree 做的为什么不用红黑树叉树呢&#xff1f;什么是 B-Tree&#xff08;B-树&#xff09;&#xff1f;什么是 BTree&#xff1f;BTree 相对于 B-Tree 的几点不同MySQL 数据结构用 btree 做的 为什么不用红黑树叉树呢&#xff1f; 不用红黑树是因为…

计算机3D数学基础 旋转的三种表示方法 学习笔记

旋转的三种表示方法&#xff1a;1、矩阵 2、欧拉角 3、四元数 矩阵的缺点&#xff0c;记录旋转角度要记录9个数 欧拉角的heading就是绕着y(绿色轴)轴30 &#xff0c;容易理解 三种旋转方式的优缺点&#xff1a; 矩阵&#xff1a;缺点&#xff1a;1、上手难&#xff0c;记…

Android常用布局总结之(LinearLayout、TableLayout、GridLayout、RelativeLayout)

一、LinearLayout 线性布局 LinearLayout 是一个视图组&#xff0c;用于使所有子视图在单个方向&#xff08;垂直或水平&#xff09;保持对齐。您可以使用 android:orientation 属性指定布局方向。 android:orientation&#xff0c;指定布局方向&#xff0c;vertical-竖向布局…

在线点餐网站

开发工具(eclipse/idea/vscode等)&#xff1a; 数据库(sqlite/mysql/sqlserver等)&#xff1a; 功能模块(请用文字描述&#xff0c;至少200字)&#xff1a; 管理员&#xff1a; 1、管理门店介绍、联系我们 2、对公告类型、公告信息增删改查 3、对菜品类型、菜品信息增册改查 4…

【计算机考研408】2023考研408相关题目预测总结

目录数据结构选择1-时间复杂度选择2-栈或者队列选择3-二叉树、树、森林选择4-并查集选择5-红黑树选择6-图的概念选择7-图的应用选择8-B树&#xff08;B-树&#xff09;选择8-B树选择8-B树与B树的区别选择8-B树与B树的相关应用选择9-查找算法选择10、11-排序算法综合应用题41-算…

UG/NX二次开发Siemens官方NXOPEN实例解析—2.3 Selection_UIStyler

列文章目录 UG/NX二次开发Siemens官方NXOPEN实例解析—2.1 AssemblyViewer UG/NX二次开发Siemens官方NXOPEN实例解析—2.2 Selection UG/NX二次开发Siemens官方NXOPEN实例解析—2.3 Selection_UIStyler 列文章目录 文章目录 前言 一、知识点提取 二、案例需求分析 三、…

大数据期末总结

文章目录一、这学期分别接触了Linux&#xff0c;hadoop&#xff0c;hbase&#xff0c;hive1、Linux2、Hadoop3、hbase4、hive二、总结一、这学期分别接触了Linux&#xff0c;hadoop&#xff0c;hbase&#xff0c;hive 1、Linux Linux是一款安全性十分良好的操作系统。不仅有用…

前端监控与前端埋点方案

前端监控与前端埋点方案 https://blog.csdn.net/sinat_36521655/article/details/114650138 ​ 用户行为数据可以通过前端数据监控的方式获得&#xff0c;除此之外&#xff0c;前端还需要实现**性能监控和异常监控。**性能监控包括首屏加载时间、白屏时间、http请求时间和htt…

软件设计师

1.在项目初期的需求并不明确&#xff0c;需要不断同用户进行交流与沟通&#xff0c;分布获取功能要求&#xff0c;在这种情况要采用敏捷开发方法最适合&#xff0c;比如极限编程 2.设计模式包括&#xff1a;创建型&#xff0c;结构型&#xff0c;行为型三大类别。 创建型模式…