Linux内核源码解析 | system call part 1

system call part 1

1. 摘要

  • system call 章节主要内容简介
  • System call. What is it?
  • Implementation ofwrite system call

2. system call` 章节主要内容简介

在本章节,我们将看到与系统调用相关的许多不同方面的概念。
例如:

  • what’s happening when a system call occurs from userspace
  • Linux内核中几个系统调用处理程序的实现
  • VDSOvsyscall 等等概念

3. System call. What is it?

3.1 system call 定义

  • 系统调用内核提供的可供userspcae程序调用的服务

  • 换句话说,系统调用是userspace 程序调用内核空间 函数,用户空间程序调用该C内核空间函数来处理某些请求。

    Linux内核提供了系统调用功能的集合,每种体系结构都提供了自己的集合。例如 x86_64 提供322 system calls , x86 提供358 different system calls

3,2 一个调用 system call的 汇编小程序

3.2.1 代码 以及 如何运行该程序
//.data stores initialized data of our program (Hello world string and its length in our case).
.data   
msg:
    .ascii "Hello, world!\n"
    len = . - msg
//.text contains the code of our program. 
.text
    .global _start

_start:
    movq  $1, %rax //first part
    movq  $1, %rdi
    movq  $msg, %rsi
    movq  $len, %rdx
    syscall

    movq  $60, %rax //secont part
    xorq  %rdi, %rdi
    syscall

我们可以使用以下命令编译以上内容:

$ gcc -c test.S
$ ld -o test test.o

使用下列命令运行该程序

./test
Hello, world!
3.2.2 代码解释

对于代码段 .text 我们可以分为以下三个部分

  • 第一部分是在第一条系统调用指令syscall之前的代码
  • 第二部分将在第一和第二个系统调用指令syscall之间的代码
  • 第三部分为第二个系统调用指令syscall之后的代码
3.2.2.1 syscall指令的含义

syscall指令跳转到存储在 MSR_LSTAR Model specific register(Long system target address register)中的地址,即 entry_SYSCALL_64 function 处。内核负责提供一个自定义的函数来处理所有系统调用,并在系统启动时将此处理函数的地址写入MSR_LSTAR寄存器. 该自定义函数负责判定system call的类型,以及呼叫特定的 system call handler function.

在Linux 中 ,该自定义函数是entry_SYSCALL_64,它在arch / x86 / entry / entry_64.S中定义。在启动过程中,此syscall处理函数entry_SYSCALL_64的地址将在 arch / x86 / kernel / cpu / common.c中写入MSR_LSTAR寄存器。

wrmsrl(MSR_LSTAR, entry_SYSCALL_64);

SYSCALL invokes an OS system-call handler at privilege level 0. It does so by
loading RIP from the IA32_LSTAR MSR (after saving the address of the instruction
following SYSCALL into RCX). (The WRMSR instruction ensures that the
IA32_LSTAR MSR always contain a canonical address.)
SYSCALL loads the CS and SS selectors with values derived from bits 47:32 of the
IA32_STAR MSR. However, the CS and SS descriptor caches are not loaded from the
descriptors (in GDT or LDT) referenced by those selectors
Instead, the descriptor caches are loaded with fixed values. It is the respon-
sibility of OS software to ensure that the descriptors (in GDT or LDT) referenced
by those selector values correspond to the fixed values loaded into the descriptor
caches; the SYSCALL instruction does not ensure this correspondence.

3.2.2.2 entry_SYSCALL_64 函数 如何判断呼叫哪个 特定的 system call handler function(解释源程序)

实际上,它是从通用寄存器中获取此信息的。正如我们在系统调用表中看到的那样,每个系统调用都有一个唯一的号码。

在我们的示例中,即第一部分。第一个系统调用是write,它将数据写入给定文件。如我们所见,write系统调用的编号为1。在示例中,我们通过rax寄存器传递了该系统调用的编号1entry_SYSCALL_64函数可以通过查看该寄存器,获知哪一个system call 被 user space 程序呼叫。 程序中接下来的几个通用寄存器:%rdi,%rsi和%rdx分别保存了采用write系统调用的三个参数。

  • File descriptor (1 is stdout in our case)
  • Pointer to our string
  • Size of data

write system call 在 fs/read_write.c 中的定义类似于:

ssize_t write(int fd, const void *buf, size_t nbytes);

示例的第二部分与第一部分类似,但是我们调用了另一个系统调用。即exit system call。该系统调用有一个参数: 返回值,并处理程序退出的方式

通常,将systen call函数的参数放置在寄存器中或压入堆栈。正确的顺序是:

  • rdi
  • rsi
  • rdx
  • rcx
  • r8
  • r9
    而 system call 在系统调用表中的 编号 则存在rax寄存器中

注意:system call handler function 的前六个参数被放置在以上6个通用寄存器中。如果该函数有六个以上的参数,其余的参数将被放置在堆栈中。

3.2.3 总结:
  • User application contains code that fills general purpose register with the values (system call number and arguments of this system call);
  • Processor switches from the user mode to kernel mode and starts execution of the system call entry - entry_SYSCALL_64;
  • entry_SYSCALL_64 switches to the kernel stack and saves some general purpose registers, old stack and code segment, flags and etc… on the stack;
    +entry_SYSCALL_64 checks the system call number in the rax register, searches a system call handler in the sys_call_table and calls it, if the number of a system call is correct;
  • If a system call is not correct, jump on exit from system call;
  • After a system call handler will finish its work, restore general purpose registers, old stack, flags and return address and exit from the entry_SYSCALL_64 with the sysretq instruction.

4 write system call的实现

让我们一起来看看 write system call handler function 是如何实现
write system call handler function 源码定义在 fs/read_write.c fs/read_write.c

SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
        size_t, count)
{
    struct fd f = fdget_pos(fd);
    ssize_t ret = -EBADF;

    if (f.file) {
        loff_t pos = file_pos_read(f.file);
        ret = vfs_write(f.file, buf, count, &pos);
        if (ret >= 0)
            file_pos_write(f.file, pos);
        fdput_pos(f);
    }

    return ret;
}

4.1 SYSCALL_DEFINE3 的奥秘

  • SYSCALL_DEFINE3宏在include / linux / syscalls.h头文件中定义,并扩展为SYSCALL_METADATA__SYSCALL_DEFINEx两个宏。让我们来看看看这个宏 的定义
 // SYSCALL_DEFINE3(name, ...) 中的 ... 代表这个宏 可以接受任意数量的 参数,
 // __VA_ARGS__ 代表SYSCALL_DEFINEx 这个宏 可以从SYSCALL_DEFINE3接受任意数量的参数
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)

#define SYSCALL_DEFINEx(x, sname, ...)                \
        SYSCALL_METADATA(sname, x, __VA_ARGS__)       \
        __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
4.1.1 SYSCALL_METADATA
  • 第一个宏SYSCALL_METADATA的实现 取决于CONFIG_FTRACE_SYSCALLS 这个内核配置选项。从该选项的名称可以理解,它允许tracer追踪syscall 进入 和 退出 事件。如果启用了此内核配置选项,则SYSCALL_METADATA宏将执行在include / trace / syscall.h头文件中定义的syscall_metadata结构体的初始化,并包含不同的有用字段,例如系统调用的名称,系统中的系统调用的编号调用表,系统调用的参数数量,参数类型列表等
#define SYSCALL_METADATA(sname, nb, ...)                             \
   ...                                                              \
   ...                                                              \
   ...                                                              \
   struct syscall_metadata __used                                   \
             __syscall_meta_##sname = {                             \
                   .name           = "sys"#sname,                   \
                   .syscall_nr     = -1,                            \
                   .nb_args        = nb,                            \
                   .types          = nb ? types_##sname : NULL,     \
                   .args           = nb ? args_##sname : NULL,      \
                   .enter_event    = &event_enter_##sname,          \
                   .exit_event     = &event_exit_##sname,           \
                   .enter_fields   = LIST_HEAD_INIT(__syscall_meta_##sname.enter_fields), \
            };                                                                            \

   static struct syscall_metadata __used                           \
             __attribute__((section("__syscalls_metadata")))       \
            *__p_syscall_meta_##sname = &__syscall_meta_##sname;
  • 如果在内核配置过程中未启用CONFIG_FTRACE_SYSCALLS内核选项,则SYSCALL_METADATA宏将扩展为空字符串:
#define SYSCALL_METADATA(sname, nb, ...)
4.1.2 __SYSCALL_DEFINEx

该宏定义扩展为以下五个函数:

// sys##name 中 的 ## 代表 将 sys 字符串 与后面的变量 “name” 连接起来 ,当 __SYSCALL_DEFINEx(x, name, ...)   中的 name = write ,则 sys##name 变为 sys_wirte
#define __SYSCALL_DEFINEx(x, name, ...)                                 \
        asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))       \
                __attribute__((alias(__stringify(SyS##name))));         \                                                                            \
        static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__));  \
                                                                        \
        asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__));      \
                                                                        \
        asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__))       \
        {                                                               \
                long ret = SYSC##name(__MAP(x,__SC_CAST,__VA_ARGS__));  \
                __MAP(x,__SC_TEST,__VA_ARGS__);                         \
                __PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));       \
                return ret;                                             \
        }                                                               \
                                                                        \
        static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__))
  • The first sys##name is definition of the syscall handler function with the givenname- sys_system_call_name
  • __SC_DECL宏接受从__SYSCALL_DEFINEx 传过来的任意数量的 参数__VA_ARGS__并将其中的相应的参数名称和参数类型进行组合,因为该宏__SC_DECL定义无法确定参数类型。 __MAP宏将__SC_DECL宏应用于__VA_ARGS__参数。
  • 其他剩下的四个函数不重要
  • SYSCALL_DEFINE3宏的结果:
asmlinkage long sys_write(unsigned int fd, const char __user * buf, size_t count);

4.2 一起来看看 代码的 内容吧 :)

//writes data from a buffer declared by the user to a given device or a file.
//__user  后缀代表 该指针指向的内存地址是 在 userspace====》 不能直接在 kernel space dereference
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
        size_t, count)
{
    struct fd f = fdget_pos(fd);
    ssize_t ret = -EBADF;

    if (f.file) {
        loff_t pos = file_pos_read(f.file);
        ret = vfs_write(f.file, buf, count, &pos);
        if (ret >= 0)
            file_pos_write(f.file, pos);
        fdput_pos(f);
    }

    return ret;
}
  • 该函数 有三个参数
    • fd - file descriptor;
    • buf - buffer to write;
    • count - length of buffer to write.
  • fd结构类型 表示Linux内核中的文件描述符(file descriptor),我们将fdget_pos函数的调用结果放入。而在同一源代码文件中定义的fdget_pos函数仅扩展__to_fd函数的调用:
static inline struct fd fdget_pos(int fd)
{
        return __to_fd(__fdget_pos(fd));
}

The main purpose of the fdget_pos is to convert the given file descriptor which is just a number to the fd structure. Through the long chain of function calls, the fdget_pos function gets the file descriptor table of the current process, current->files, and tries to find a corresponding file descriptor number there.

  • 当获得给定 文件描述符编号 的 fd结构时,我们将对其进行检查 并如果该 fd 结构体不存在,返回-EBADF
  • 我们通过调用file_pos_read函数获得文件中的当前位置,该函数仅返回文件的f_pos字段:
static inline loff_t file_pos_read(struct file *file)
{
        return file->f_pos;
}
  • 接下来我们调用 vfs_write 函数。该函数从给定位置开始将给定缓冲区的内容写入给定的文件。
  • vfs_write完成工作之后,如果 写操作成功,则更新 “f_pos”,即file 的 offset
  • 最后,我们可以看到以下函fdput_pos(f);的调用:其解锁了f_pos_lock互斥锁,该互斥锁在共享文件描述符的线程进行并发写入期间保护文件位置

结束啦 :)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要深度解析Linux内核,首先需要获取相应的源码Linux源码是开放的,可以从官方网站或镜像站点下载。以下是下载和获取Linux内核源码的步骤: 1. 访问官方网站:进入Linux官方网站(https://www.kernel.org/),在页面上找到“Releases”或类似的链接,点击进入。 2. 选择版本:在“Releases”页面上,列出了各个版本的Linux内核。可以根据需要选择特定的版本,或者选择最新版本。 3. 下载源码包:在选择了特定版本后,页面会列出该版本的所有源码包。通常有两个版本可供下载:tarball(后缀为.tar.gz)和patch(后缀为.sign)。下载tarball版本源码包。 4. 解压源码包:下载完成后,使用解压工具(如tar命令)将源码包解压到指定的目录中。例如,使用以下命令解压源码包:tar zxvf linux-x.x.x.tar.gz 5. 进入源码目录:解压完成后,进入解压后的目录:cd linux-x.x.x 6. 开始探索源码源码目录中包含了Linux内核的各个子系统和模块。可以通过浏览源码文件、查看文档和参考资料等,进行系统的深入了解。 了解Linux内核源码的方式有很多,可以通过阅读相关的书籍、论文以及互联网上的博客和文章进行学习。深入理解Linux内核的关键在于逐步学习每个子系统的实现原理和源码逻辑,并通过实践和调试来加深对内核的理解。 总之,通过下载Linux内核源码,我们可以深入了解内核的实现细节和工作原理,为深度解析内核打下基础。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值