系统调用
1、系统调用号
asmlinkage
asmlinkage:asmlinkage 是 Linux 内核中用于定义系统调用的函数的修饰符。asmlinkage 表示该函数是内核函数,不能被用户空间程序调用。只从栈中获取参数,返回结果也只通过栈返回。
在 Linux 系统中,每个系统调用都有一个唯一的系统调用号,用户空间程序通过这些系统调用号来请求内核执行特定的系统调用。不同的 Linux 内核版本和架构(如 x86、ARM 等)的系统调用号可能会有所不同。以下为你分别介绍常见架构下的部分系统调用及其系统调用号:
x86_64 架构
在 x86_64 架构下,系统调用通过 syscall
指令发起,下面是一些常见系统调用及其系统调用号:
系统调用名 | 系统调用号 | 功能描述 |
---|---|---|
read |
0 | 从文件描述符读取数据 |
write |
1 | 向文件描述符写入数据 |
open |
2 | 打开文件或设备 |
close |
3 | 关闭文件描述符 |
stat |
4 | 获取文件状态信息 |
fstat |
5 | 通过文件描述符获取文件状态信息 |
lstat |
6 | 获取符号链接的状态信息 |
poll |
7 | 等待文件描述符上的 I/O 事件 |
lseek |
8 | 移动文件读写指针 |
mmap |
9 | 内存映射文件或设备 |
munmap |
11 | 解除内存映射 |
brk |
12 | 调整数据段的大小 |
rt_sigaction |
13 | 设置信号处理函数 |
rt_sigprocmask |
14 | 阻塞或解除阻塞信号 |
rt_sigreturn |
15 | 从信号处理程序返回 |
ioctl |
16 | 对设备文件执行特定操作 |
pread64 |
17 | 从指定偏移量处读取数据 |
pwrite64 |
18 | 向指定偏移量处写入数据 |
readv |
19 | 分散读 |
writev |
20 | 集中写 |
access |
21 | 检查文件的访问权限 |
pipe |
22 | 创建管道 |
select |
23 | 等待文件描述符上的 I/O 事件 |
sched_yield |
24 | 主动让出 CPU |
ARM 架构
ARM 架构下系统调用的实现和系统调用号也有其特点,以下是部分常见系统调用号:
系统调用名 | 系统调用号 | 功能描述 |
---|---|---|
read |
3 | 从文件描述符读取数据 |
write |
4 | 向文件描述符写入数据 |
open |
5 | 打开文件或设备 |
close |
6 | 关闭文件描述符 |
stat |
106 | 获取文件状态信息 |
fstat |
107 | 通过文件描述符获取文件状态信息 |
lstat |
108 | 获取符号链接的状态信息 |
你可以通过以下几种方式查看系统调用号:
- 查阅内核源码:Linux 内核源码中的
include/uapi/asm-generic/unistd.h
(对于通用部分)以及arch/<架构>/include/uapi/asm/unistd.h
(对于特定架构)文件中定义了系统调用号。 - 使用
man
手册:部分man
手册页会提及系统调用号,例如man 2 syscalls
。 - 使用
syscall(2)
手册:man 2 syscall
可以提供有关系统调用号的一些信息。
系统调用号传递给内核
在 ARM Linux 系统中,当用户空间程序发起系统调用并陷入内核之前,需要将系统调用号以及相关参数传递给内核,主要通过以下方式实现:
1. 寄存器传递
在 ARM 架构里,通常使用寄存器来传递系统调用号和参数。具体的寄存器分配规则由 ABI(Application Binary Interface)规定,常见的做法如下:
- 系统调用号传递:一般会把系统调用号存于特定的寄存器中,像 ARM EABI(Embedded Application Binary Interface)规定使用
R7
寄存器来存放系统调用号。例如,当用户程序要发起read
系统调用时,会先把read
对应的系统调用号加载到R7
寄存器。 - 参数传递:系统调用所需的参数会按顺序存于其他寄存器,常见的是
R0 - R6
寄存器。例如,read
系统调用需要文件描述符、缓冲区地址和读取字节数这三个参数,那么这些参数会依次存于R0
、R1
和R2
寄存器。
2. 代码示例(以汇编代码模拟系统调用)
以下是一个简单的 ARM 汇编代码示例,展示了如何通过 svc
指令发起 write
系统调用,同时传递系统调用号和参数:
1 | @ 数据段 |
代码解释
- 系统调用号设置:
mov r7, #4
将write
系统调用号 4 存于R7
寄存器;mov r7, #1
把exit
系统调用号 1 存于R7
寄存器。 - 参数设置:
mov r0, #1
将标准输出的文件描述符 1 存于R0
寄存器;ldr r1, =message
把要输出的字符串地址加载到R1
寄存器;mov r2, #len
把字符串长度存于R2
寄存器;mov r0, #0
将退出状态码 0 存于R0
寄存器。 - 触发系统调用:
svc 0
指令触发异常,使处理器从用户模式切换到内核模式,内核依据R7
寄存器中的系统调用号调用相应的系统调用处理函数,并使用R0 - R6
寄存器中的参数执行具体操作。
3. 高级语言中的封装
在高级语言(如 C 语言)里,系统调用一般通过库函数进行封装。这些库函数会在内部完成系统调用号和参数的设置,然后触发 svc
指令。例如,在 C 语言中使用 write
函数:
1 |
|
在编译和执行这段代码时,write
库函数会把 write
系统调用号和参数按 ABI 规定存于相应寄存器,再触发 svc
指令来发起系统调用。
swi 指令
在早期的 ARM 架构中,确实使用 SWI
(Software Interrupt,软件中断)指令来触发系统调用,不过后来逐渐被 SVC
(Supervisor Call)指令所取代。下面为你详细介绍它们在传递系统调用号进入内核时的相关情况:
使用 SWI
指令触发系统调用
1. 基本原理
SWI
指令是一种软中断指令,当执行该指令时,处理器会从用户模式切换到管理模式(Supervisor Mode),并跳转到异常向量表中 SWI
异常对应的入口地址,从而进入内核执行相应的异常处理程序。
2. 系统调用号传递
在使用 SWI
指令时,系统调用号通常被编码在 SWI
指令的立即数中。例如,SWI
指令的格式可能如下:
1 | SWI <imm24> |
这里的 <imm24>
是一个 24 位的立即数,系统调用号就包含在这个立即数中。当执行 SWI
指令时,内核可以从这个立即数中提取出系统调用号,进而确定要执行的具体系统调用。
3. 代码示例(汇编)
1 | @ 数据段 |
在这个示例中,swi #4
触发了 write
系统调用,swi #1
触发了 exit
系统调用,系统调用号被直接编码在 SWI
指令的立即数中。
从 SWI
到 SVC
的转变
随着 ARM 架构的发展,为了提高指令集的安全性和兼容性,引入了 SVC
指令来替代 SWI
指令。SVC
指令在功能上与 SWI
指令类似,也是用于触发系统调用,但它的设计更加清晰和安全。在使用 SVC
指令时,系统调用号通常通过寄存器(如 R7
)传递,而不是编码在指令的立即数中。
代码示例(使用 SVC
指令)
1 | @ 数据段 |
在这个示例中,系统调用号通过 R7
寄存器传递,svc 0
指令触发系统调用进入内核。
综上所述,早期 ARM 架构使用 SWI
指令触发系统调用,系统调用号编码在指令立即数中;后来的架构采用 SVC
指令,系统调用号通过寄存器传递。
2、内核将数据接收,内核向用户空间返回数据
在 Linux 系统中,内核与用户空间的数据交互是一个关键操作,下面将详细介绍内核接收来自用户空间的数据以及将内核空间的数据传递给用户空间的相关内容。
内核接收来自用户空间的数据
1. 系统调用中的参数传递
当用户空间程序发起系统调用时,会将数据通过寄存器或栈传递给内核。在 ARM 架构中,常见的是使用寄存器传递参数,例如使用 R0 - R6
传递系统调用的参数。内核在处理系统调用时,会从这些寄存器中获取用户传递的数据。
2. 使用 copy_from_user
函数
在系统调用处理函数中,如果需要将用户空间的大块数据复制到内核空间,通常会使用 copy_from_user
函数。该函数会检查用户空间地址的有效性,并将数据从用户空间复制到内核空间。
函数原型:
1 | unsigned long copy_from_user(void *to, const void __user *from, unsigned long n); |
to
:指向内核空间的目标缓冲区。from
:指向用户空间的源缓冲区。n
:要复制的字节数。
返回值:如果复制成功,返回 0;如果出现错误,返回未复制的字节数。
示例代码:
1 |
|
内核将数据传递给用户空间
1. 使用 copy_to_user
函数
与 copy_from_user
函数相对应,copy_to_user
函数用于将内核空间的数据复制到用户空间。该函数同样会检查用户空间地址的有效性,并将数据从内核空间复制到用户空间。
函数原型:
1 | unsigned long copy_to_user(void __user *to, const void *from, unsigned long n); |
to
:指向用户空间的目标缓冲区。from
:指向内核空间的源缓冲区。n
:要复制的字节数。
返回值:如果复制成功,返回 0;如果出现错误,返回未复制的字节数。
示例代码:
1 |
|
2. 注意事项
- 地址有效性检查:在使用
copy_from_user
和copy_to_user
函数时,内核会自动检查用户空间地址的有效性,以防止用户程序传递无效的地址。 - 权限问题:内核需要确保用户程序具有访问目标缓冲区的权限,避免数据泄露或越界访问。
- 错误处理:在复制数据时,可能会出现各种错误,如内存不足、地址无效等,因此需要进行适当的错误处理。
通过上述方法,内核可以安全、有效地与用户空间进行数据交互。
当系统调用返回的时候,控制权在system_call()函数中,返回给用户空间。
3、我们的实现一个系统调用
系统调用号实现自己的比较简单,这里不做说明。可以百度到
对于想在内核中获取信息传递到应用层。可以使用一个设备节点,对节点进行读写操作。