Linux - 系统调用(syscall)

2023-11-25 20:45
文章标签 linux 系统 调用 syscall

本文主要是介绍Linux - 系统调用(syscall),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

说明

  • 基于riscv64 soc + linux_5.10.4平台,通过新增一个系统调用深入了解下系统调用实现原理。

简介

  • Linux 软件运行环境分为用户空间和内核空间,默认情况下,用户进程无法访问内核,既不能访问内核所在的内存空间,也不能调用内核中的函数。
  • 为了给应用层提供系统支持,Linux提供了一组系统调用接口,用户可以通过调用它们访问linux内核的数据和函数。
  • Linux系统调用实现原理是固定,不同平台(arm64,riscv)只是切换至内核态的汇编指令不同,大致原理如下:
  1. 程序将系统调用参数填充到对应的平台通用寄存器。
  2. 调用平台特定的汇编指令,触发同步异常,切换至内核态运行。
  3. 内核初始化时已设置异常向量表,应用层触发同步异常后,CPU会跳到异常向量表对应的异常处理执行(通常是一段平台相关的汇编代码)。
  4. 异常处理代码会检查系统调用号是否超出,未超出,再根据定义的系统调用表(sys_call_table)找到相应的系统调用函数入口地址,执行后,再通过汇编指令返回应用层。
  • 新增系统调用,实现步骤,如下:
  1. 修改系统调用表(syscall_table),新增一项。
  2. 系统调用声明。
  3. 系统调用实现。

修改系统调用表

  • 系统调用表(syscall_table)定义如下:
// file: arch/riscv/kernel/syscall_table.c
#undef __SYSCALL
#define __SYSCALL(nr, call)     [nr] = (call),const void *sys_call_table[__NR_syscalls] = {[0 ... __NR_syscalls - 1] = sys_ni_syscall,
#include <asm/unistd.h> //通过unistd.h导入实际定义
};
  • asm/unistd.h最终定义,如下:
//file: include/uapi/asm-generic/unistd.h 
...
#define __NR_openat2 437     //系统调用openat2 编号
__SYSCALL(__NR_openat2, sys_openat2) //系统调用openat2 syscall_table项定义
#define __NR_pidfd_getfd 438
__SYSCALL(__NR_pidfd_getfd, sys_pidfd_getfd)
#define __NR_faccessat2 439
__SYSCALL(__NR_faccessat2, sys_faccessat2)
#define __NR_process_madvise 440
__SYSCALL(__NR_process_madvise, sys_process_madvise)#undef __NR_syscalls
#define __NR_syscalls 441 //系统调用表 项个数
...
  • 新增一项系统调用(mytest)
diff --git a/include/uapi/asm-generic/unistd.h b/include/uapi/asm-generic/unistd.h
index 15279e8d8..7df066dc5 100644
--- a/include/uapi/asm-generic/unistd.h
+++ b/include/uapi/asm-generic/unistd.h
@@ -860,8 +860,11 @@ __SYSCALL(__NR_faccessat2, sys_faccessat2)#define __NR_process_madvise 440__SYSCALL(__NR_process_madvise, sys_process_madvise)+#define __NR_mytest 441
+__SYSCALL(__NR_mytest, sys_mytest)
+#undef __NR_syscalls
-#define __NR_syscalls 441
+#define __NR_syscalls 442

系统调用声明

  • 新增系统调用需要先声明,否则内核编译时会报错(找不到新增系统调用声明)。
//file: include/linux/syscalls.h
....
asmlinkage long sys_madvise(unsigned long start, size_t len, int behavior);
asmlinkage long sys_process_madvise(int pidfd, const struct iovec __user *vec,size_t vlen, int behavior, unsigned int flags);
....
+asmlinkage long sys_mytest(int id); //新增系统调用声明

系统调用实现

  • 实现系统调用时,不能像实现普通函数一样,需要使用SYSCALL_DEFINE宏,如:系统调用madvise,定义时使用SYSCALL_DEFINE3宏,宏展开后就是sys_madvise。
//file: mm/madvise.c
SYSCALL_DEFINE3(madvise, unsigned long, start, size_t, len_in, int, behavior)
{                       return do_madvise(current->mm, start, len_in, behavior);
}int do_madvise(struct mm_struct *mm, unsigned long start, size_t len_in, int behavior)
{        ....//实际功能实现....
}
  • SYSCALL_DEFINE 宏定义
//file: include/linux/syscalls.h
#ifndef SYSCALL_DEFINE0         
#define SYSCALL_DEFINE0(sname)                                  \SYSCALL_METADATA(_##sname, 0);                          \asmlinkage long sys_##sname(void);                      \ALLOW_ERROR_INJECTION(sys_##sname, ERRNO);              \asmlinkage long sys_##sname(void)
#endif /* SYSCALL_DEFINE0 */#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_DEFINE_MAXARGS  6#define SYSCALL_DEFINEx(x, sname, ...)                          \SYSCALL_METADATA(sname, x, __VA_ARGS__)                 \__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)#define __PROTECT(...) asmlinkage_protect(__VA_ARGS__)
  1. 系统调用最多支持6个参数,1个参数使用SYSCALL_DEFINE1,2个参数使用SYSCALL_DEFINE2,以此类推。
  • 新系统调用
// file: mm/madvise.c ,随便找了一个文件保存代码
SYSCALL_DEFINE1(mytest, int, id)
{return id; //测试将id返回
}

应用层测试

  • 编译并运行新内核后,可运行应用层程序验证。
int main(void) 
{int id = 0;id = syscall(441, 100);printf("result : %d\n", id);return 0;
}~# ./mytest 
result : 100

标准C库

  • 程序中调用的syscall来自标准C库,根据源码可知:应用层系统调用接口是封装的syscall。
  • 当前使用的标准C库(musl)syscall源码如下:
//file: musl-1.2.1/arch/riscv64/syscall_arch.h
...
#define __asm_syscall(...) \__asm__ __volatile__ ("ecall\n\t" \: "=r"(a0) : __VA_ARGS__ : "memory"); \return a0; \static inline long __syscall0(long n)
{register long a7 __asm__("a7") = n;register long a0 __asm__("a0");__asm_syscall("r"(a7))
}
...
static inline long __syscall6(long n, long a, long b, long c, long d, long e, long f)
{register long a7 __asm__("a7") = n;register long a0 __asm__("a0") = a;register long a1 __asm__("a1") = b;register long a2 __asm__("a2") = c;register long a3 __asm__("a3") = d;register long a4 __asm__("a4") = e;register long a5 __asm__("a5") = f;__asm_syscall("r"(a7), "0"(a0), "r"(a1), "r"(a2), "r"(a3), "r"(a4), "r"(a5))
}
  • 可知:
  1. riscv64最终使用汇编指令ecall,触发同步异常,切换至内核态执行。
  2. 使用通用寄存器a7 存储系统调用编号
  3. 和内核定义一致,syscall支持0 ~ 6个参数(__syscall0 ~ __syscall6 ),使用寄存器a0 ~ a5传递参数。
  • ARM64实现原理也是一样,不同的只是触发异常的指令(svc)以及通用寄存器的使用,如下:
#define __asm_syscall(...) do { \__asm__ __volatile__ ( "svc 0" \: "=r"(x0) : __VA_ARGS__ : "memory", "cc"); \return x0; \} while (0)static inline long __syscall0(long n)
{register long x8 __asm__("x8") = n;register long x0 __asm__("x0");__asm_syscall("r"(x8));
}
...
static inline long __syscall6(long n, long a, long b, long c, long d, long e, long f)
{register long x8 __asm__("x8") = n;register long x0 __asm__("x0") = a;register long x1 __asm__("x1") = b;register long x2 __asm__("x2") = c;register long x3 __asm__("x3") = d;register long x4 __asm__("x4") = e;register long x5 __asm__("x5") = f;__asm_syscall("r"(x8), "0"(x0), "r"(x1), "r"(x2), "r"(x3), "r"(x4), "r"(x5));
}

总结

  1. 系统调用是安全的,执行时,应用层没有访问内核空间。
  2. 系统调用执行时,应用层暂停,切换至内核空间执行。
  3. 系统调用执行时,是通过平台相关的特定汇编指令触发同步异常,riscv64是使用ecall,aarch64是使用svc 0,Intel CPU由中断0x80实现。
  • CPU会跳转到对应的异常处理,源码如下:
//file: arch/riscv/kernel/entry.S
....
ENTRY(handle_exception) //对应的异常处理....
check_syscall_nr:/* Check to make sure we don't jump to a bogus syscall number. */li t0, __NR_syscallsla s0, sys_ni_syscall/** Syscall number held in a7.* If syscall number is above allowed value, redirect to ni_syscall.*/bgeu a7, t0, 3f
#ifdef CONFIG_COMPATREG_L s0, PT_STATUS(sp)srli s0, s0, SR_UXL_SHIFTandi s0, s0, (SR_UXL >> SR_UXL_SHIFT)li t0, (SR_UXL_32 >> SR_UXL_SHIFT)sub t0, s0, t0bnez t0, 1f/* Call compat_syscall */la s0, compat_sys_call_tablej 2f
1:
#endif/* Call syscall */la s0, sys_call_table
2:slli t0, a7, RISCV_LGPTRadd s0, s0, t0REG_L s0, 0(s0)
3:jalr s0ret_from_syscall:
....
  1. 内核态调用对应的系统调用函数,执行完后,会退出内核态切换至用户态,如上 ret_from_syscall。
  • 此过程 aarch64平台是由eret汇编指令实现,和arm trustzone机制 bl31切换至非安全world(REE)以及切换至安全world(bl32)实现流程是一样的,riscv64 平台,具体指令暂不明。

这篇关于Linux - 系统调用(syscall)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/424583

相关文章

Linux命令之firewalld的用法

《Linux命令之firewalld的用法》:本文主要介绍Linux命令之firewalld的用法,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录linux命令之firewalld1、程序包2、启动firewalld3、配置文件4、firewalld规则定义的九大

Linux之计划任务和调度命令at/cron详解

《Linux之计划任务和调度命令at/cron详解》:本文主要介绍Linux之计划任务和调度命令at/cron的使用,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录linux计划任务和调度命令at/cron一、计划任务二、命令{at}介绍三、命令语法及功能 :at

Linux下如何使用C++获取硬件信息

《Linux下如何使用C++获取硬件信息》这篇文章主要为大家详细介绍了如何使用C++实现获取CPU,主板,磁盘,BIOS信息等硬件信息,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下... 目录方法获取CPU信息:读取"/proc/cpuinfo"文件获取磁盘信息:读取"/proc/diskstats"文

Linux内核参数配置与验证详细指南

《Linux内核参数配置与验证详细指南》在Linux系统运维和性能优化中,内核参数(sysctl)的配置至关重要,本文主要来聊聊如何配置与验证这些Linux内核参数,希望对大家有一定的帮助... 目录1. 引言2. 内核参数的作用3. 如何设置内核参数3.1 临时设置(重启失效)3.2 永久设置(重启仍生效

kali linux 无法登录root的问题及解决方法

《kalilinux无法登录root的问题及解决方法》:本文主要介绍kalilinux无法登录root的问题及解决方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,... 目录kali linux 无法登录root1、问题描述1.1、本地登录root1.2、ssh远程登录root2、

Linux ls命令操作详解

《Linuxls命令操作详解》通过ls命令,我们可以查看指定目录下的文件和子目录,并结合不同的选项获取详细的文件信息,如权限、大小、修改时间等,:本文主要介绍Linuxls命令详解,需要的朋友可... 目录1. 命令简介2. 命令的基本语法和用法2.1 语法格式2.2 使用示例2.2.1 列出当前目录下的文

利用Python快速搭建Markdown笔记发布系统

《利用Python快速搭建Markdown笔记发布系统》这篇文章主要为大家详细介绍了使用Python生态的成熟工具,在30分钟内搭建一个支持Markdown渲染、分类标签、全文搜索的私有化知识发布系统... 目录引言:为什么要自建知识博客一、技术选型:极简主义开发栈二、系统架构设计三、核心代码实现(分步解析

Java调用C++动态库超详细步骤讲解(附源码)

《Java调用C++动态库超详细步骤讲解(附源码)》C语言因其高效和接近硬件的特性,时常会被用在性能要求较高或者需要直接操作硬件的场合,:本文主要介绍Java调用C++动态库的相关资料,文中通过代... 目录一、直接调用C++库第一步:动态库生成(vs2017+qt5.12.10)第二步:Java调用C++

Linux中的计划任务(crontab)使用方式

《Linux中的计划任务(crontab)使用方式》:本文主要介绍Linux中的计划任务(crontab)使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、前言1、linux的起源与发展2、什么是计划任务(crontab)二、crontab基础1、cro

Linux换行符的使用方法详解

《Linux换行符的使用方法详解》本文介绍了Linux中常用的换行符LF及其在文件中的表示,展示了如何使用sed命令替换换行符,并列举了与换行符处理相关的Linux命令,通过代码讲解的非常详细,需要的... 目录简介检测文件中的换行符使用 cat -A 查看换行符使用 od -c 检查字符换行符格式转换将