如何从用户空间访问系统调用?
- 2024-10-21 09:14:00
- admin 原创
- 81
问题描述:
我读了 LKD 1中的一些段落
,但就是无法理解下面的内容:
从用户空间访问系统调用
通常,C 库提供对系统调用的支持。用户应用程序可以从标准头文件中引入函数原型,并与 C 库链接以使用您的系统调用(或反过来使用您的 syscall 调用的库例程)。但是,如果您刚刚编写了系统调用,则 glibc 是否已经支持它值得怀疑!
值得庆幸的是,Linux 提供了一组宏来包装对系统调用的访问。它设置寄存器内容并发出陷阱指令。这些宏名为,其中介于 0 到 6 之间。该数字对应于传递给系统调用的参数数量,因为宏需要知道预期的参数数量,并因此将其推送到寄存器中。例如,考虑系统调用,定义为
_syscalln()
n
open()
long open(const char *filename, int flags, int mode)
在没有明确库支持的情况下使用此系统调用的 syscall 宏将是
#define __NR_open 5 _syscall3(long, open, const char *, filename, int, flags, int, mode)
然后,应用程序只需调用即可
open()
。对于每个宏,有 2+2×n 个参数。第一个参数对应于系统调用的返回类型。第二个参数是系统调用的名称。接下来是按系统调用顺序排列的每个参数的类型和名称。定义
__NR_open
在 中<asm/unistd.h>
;它是系统调用号。_syscall3
宏使用内联汇编扩展为 C 函数;汇编执行上一节中讨论的步骤,将系统调用号和参数推送到正确的寄存器中,并发出软件中断以陷入内核。将此宏放置在应用程序中是使用open()
系统调用所需的全部内容。让我们编写宏来使用我们出色的新
foo()
系统调用,然后编写一些测试代码来展示我们的努力成果。#define __NR_foo 283 __syscall0(long, foo) int main () { long stack_size; stack_size = foo (); printf ("The kernel stack size is %ld ", stack_size); return 0; }
应用程序可以简单调用open()
是什么意思?
此外,对于最后一段代码,声明在哪里foo()
?我怎样才能使这段代码可编译和运行?我需要包含哪些头文件?
__
1 Linux 内核开发,作者:Robert Love。wordpress.com
上的 PDF 文件(转至第 81 页);Google Books 搜索结果。
解决方案 1:
您首先应该了解Linux 内核的作用,以及应用程序只能通过系统调用与内核交互。
实际上,应用程序在内核提供的“虚拟机”上运行:它在用户空间中运行,并且只能执行(在最低机器级别)用户 CPU 模式下允许的机器指令集,并通过用于进行系统调用的指令(例如SYSENTER
或INT 0x80
...)进行增强。因此,从用户级应用程序的角度来看,系统调用是一种原子伪机器指令。
Linux Assembly Howto解释了如何在汇编(即机器指令)级别完成系统调用。
GNU libc提供与系统调用相对应的 C 函数。例如,open函数是 number 系统调用上的一个微小粘合剂(即包装器)NR__open
(它先进行系统调用,然后进行更新errno
)。应用程序通常在 libc 中调用此类 C 函数,而不是执行系统调用。
您可以使用其他一些libc
。例如,MUSL libc在某种程度上“更简单”,其代码可能更易于阅读。它还将原始系统调用包装到相应的 C 函数中。
如果你添加了自己的系统调用,你最好也实现一个类似的 C 函数(在你自己库中)。所以你也应该有一个库的头文件。
另请参阅intro(2)和syscall(2)和syscalls(2)手册页,以及VDSO 在 syscalls 中的作用。
请注意,系统调用不是 C 函数。它们不使用调用堆栈(甚至可以在没有任何堆栈的情况下调用它们)。系统调用基本上是一个类似于NR__open
from 的数字<asm/unistd.h>
,它SYSENTER
是一条机器指令,其中约定了哪些寄存器在系统调用的参数之前保存,哪些寄存器在系统调用的结果(包括失败结果,在包装errno
系统调用的 C 库中设置)之后保存。系统调用的约定不是 ABI 规范中 C 函数的调用约定(例如x86-64 psABI)。所以你需要一个 C 包装器。
解决方案 2:
首先,我想提供一些系统调用的定义。系统调用是从用户空间应用程序同步显式请求特定内核服务的过程。同步意味着系统调用的行为是通过执行指令序列预先确定的。中断是异步系统服务请求的一个例子,因为它们完全独立于处理器上执行的代码到达内核。与系统调用相反,异常是同步但隐式的内核服务请求。
系统调用由四个阶段组成:
通过将处理器从用户模式切换到内核模式,将控制权传递给内核中的特定点,并通过将处理器切换回用户模式将其返回。
指定请求的内核服务的 id。
传递所请求服务的参数。
捕获服务的结果。
一般来说,所有这些操作都可以作为一个大型库函数的一部分来实现,该库函数在实际系统调用之前和/或之后执行许多辅助操作。在这种情况下,我们可以说系统调用嵌入在此函数中,但该函数通常不是系统调用。在另一种情况下,我们可以有一个只执行这四个步骤的小函数。在这种情况下,我们可以说这个函数是一个系统调用。实际上,您可以通过手动实现上述所有四个阶段来实现系统调用本身。请注意,在这种情况下,您将被迫使用汇编程序,因为所有这些步骤都完全依赖于体系结构。
例如,Linux/i386 环境具有以下系统调用约定:
将控制权从用户模式传递到内核模式可以通过编号为 0x80 的软件中断(汇编指令 INT 0x80)、SYSCALL 指令(AMD)或 SYSENTER 指令(Intel)来完成
请求的系统服务的 ID 由进入内核模式时存储在 EAX 寄存器中的整数值指定。内核服务 ID 必须以 _ NR形式定义。您可以在 Linux 源代码树的路径上找到所有系统服务 ID
include/uapiasm-generic/unistd.h
。通过寄存器 EBX(1)、ECX(2)、EDX(3)、ESI(4)、EDI(5)、EBP(6) 最多可以传递 6 个参数。括号中的数字是参数的顺序编号。
内核在 EAX 寄存器中返回所执行服务的状态。此值通常由 glibc 用来设置 errno 变量。
在现代版本的 Linux 中,没有任何 _syscall 宏(据我所知)。相反,glibc 库(Linux 内核的主要接口库)提供了一个特殊的宏 - INTERNAL_SYSCALL
,它扩展为由内联汇编指令填充的一小段代码。这段代码针对特定的硬件平台并实现系统调用的所有阶段,因此,这个宏代表系统调用本身。还有另一个宏 - INLINE_SYSCALL
。最后一个宏提供类似 glibc 的错误处理,根据该处理,系统调用失败时将返回 -1 并将错误号存储在变量中。这两个宏都是在glibc 包errno
中定义的。sysdep.h
您可以通过以下方式调用系统调用:
#include <sysdep.h>
#define __NR_<name> <id>
int my_syscall(void)
{
return INLINE_SYSCALL(<name>, <argc>, <argv>);
}
其中<name>
必须用系统调用名称字符串替换,<id>
- 用所需的系统服务编号 id 替换,<argc>
- 用实际参数数量(从 0 到 6)替换,<argv>
- 用逗号分隔的实际参数(如果存在参数,则以逗号开头)。
例如:
#include <sysdep.h>
#define __NR_exit 1
int _exit(int status)
{
return INLINE_SYSCALL(exit, 1, status); // takes 1 parameter "status"
}
或者另一个例子:
#include <sysdep.h>
#define __NR_fork 2
int _fork(void)
{
return INLINE_SYSCALL(fork, 0); // takes no parameters
}
解决方案 3:
最小可运行汇编示例
你好世界.asm:
section .rodata
hello_world db "hello world", 10
hello_world_len equ $ - hello_world
section .text
global _start
_start:
mov eax, 4 ; syscall number: write
mov ebx, 1 ; stdout
mov ecx, hello_world ; buffer
mov edx, hello_world_len
int 0x80 ; make the call
mov eax, 1 ; syscall number: exit
mov ebx, 0 ; exit status
int 0x80
编译并运行:
nasm -w+all -f elf32 -o hello_world.o hello_world.asm
ld -m elf_i386 -o hello_world hello_world.o
./hello_world
从代码中,很容易推断出:
eax
包含系统调用编号,例如4
用于写入。内核源代码中的完整 32 位列表位于:https: //github.com/torvalds/linux/blob/v4.9/arch/x86/entry/syscalls/syscall_32.tbl#L13ebx
,ecx
并edx
包含输入参数。这些应该可以从内核源代码中每个系统调用的签名中推断出来。另请参阅:x86-64 上的 UNIX 和 Linux 系统调用的调用约定是什么以及汇编语言中的 Linux 系统调用表或 cheetsheet返回值主要包含错误代码并存入 eax(为简单起见,未在此代码中显示)
int 0x80
进行调用,尽管现在有更好的方法:“int 0x80”或“syscall”哪个更好?
当然,汇编很快就会变得乏味,并且您很快就会想在可以使用时使用 glibc / POSIX 提供的 C 包装器,或者SYSCALL
在不能使用时使用宏。
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件