如何从用户空间访问系统调用?

inj*_*joy 17 linux system-calls

我读了LKD 1中的一些段落, 我无法理解下面的内容:

从用户空间访问系统调用

通常,C库提供对系统调用的支持.用户应用程序可以从标准头中提取函数原型并与C库链接以使用您的系统调用(或者库例程,而该库例程又使用您的系统调用).但是,如果您刚刚编写了系统调用,那么glibc已经支持它是值得怀疑的!

值得庆幸的是,Linux提供了一组用于包装系统调用访问的宏.它设置寄存器内容并发出陷阱指令.这些宏被命名,其中介于0和6之间.该数字对应于传递给系统调用的参数数量,因为宏需要知道预期的参数数量,从而推入寄存器.例如,考虑系统调用,定义为_syscalln()nopen()

long open(const char *filename, int flags, int mode)
Run Code Online (Sandbox Code Playgroud)

在没有显式库支持的情况下使用此系统调用的syscall宏将是

#define __NR_open 5
_syscall3(long, open, const char *, filename, int, flags, int, mode)
Run Code Online (Sandbox Code Playgroud)

然后,应用程序可以简单地调用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\n", stack_size);
        return 0;
}
Run Code Online (Sandbox Code Playgroud)

是什么应用程序可以简单的调用open()是什么意思?

此外,对于最后一段代码,声明在foo()哪里?我怎样才能使这段代码可编辑和可运行?我需要包含哪些头文件?

__________
1 Linux内核开发,作者:Robert Love.  wordpress.com上的PDF文件(转到第81页); Google图书搜索结果.

Bas*_*tch 18

首先应该了解linux内核的作用,并且应用程序通过系统调用与内核交互.

实际上,应用程序在内核提供的"虚拟机"上运行:它在用户空间中运行,并且只能(在最低机器级别)执行用户CPU模式允许的机器指令集(由指令增强)(例如SYSENTERINT 0x80...)用于进行系统调用.因此,从用户级应用程序的角度来看,系统调用是一种原子伪机器指令.

Linux的大会HOWTO解释了如何一个系统调用可以在组件(即机器指令)的水平来完成.

GNU库被提供对应于所述系统调用的C函数.因此,例如open函数是一个微小的粘合剂(即包装器),在数字的系统调用之上NR__open(它使系统调用然后更新errno).应用程序通常在libc中调用此类C函数而不是执行系统调用.

你可以使用其他一些libc.例如,MUSL libc是somhow"更简单",它的代码可能更容易阅读.它还将原始系统调用包装到相应的C函数中.

如果添加自己的系统调用,最好还实现类似的C函数(在您自己的库中).所以你应该有一个库的头文件.

另请参见intro(2)syscall(2)以及syscalls(2)手册页,以及VDSO在系统调用中的作用.

请注意,系统调用不是C函数.它们不使用调用堆栈(甚至可以在没有任何堆栈的情况下调用它们).系统调用基本上是一个数字,例如NR__openfrom <asm/unistd.h>,一个SYSENTER机器指令,其中包含有关哪些寄存器在syscall的参数之前保留的约定以及哪些寄存器在syscall的结果[s]之后保存(包括失败结果,errno在C库包装中设置)系统调用).系统调用的约定不是ABI规范中C函数的调用约定(例如x86-64 psABI).所以你需要一个C包装器.

  • 所以,你的意思是 `open` 函数不是 **real** 系统调用,而是 **real** 系统调用之上的包装函数。我对吗? (2认同)

Zar*_*trA 5

首先我想提供一些系统调用的定义。系统调用是从用户空间应用程序同步显式请求特定内核服务的过程。同步意味着系统调用的行为是通过执行指令序列预先确定的。中断是异步系统服务请求的一个例子,因为它们到达内核时完全独立于处理器上执行的代码。与系统调用相反的例外是对内核服务的同步但隐含的请求。

系统调用由四个阶段组成:

  1. 通过将处理器从用户模式切换到内核模式,将控制权传递给内核中的特定点,并通过将处理器切换回用户模式将其返回。
  2. 指定请求的内核服务的 id。
  3. 为请求的服务传递参数。
  4. 捕获服务的结果。

一般来说,所有这些动作都可以作为一个大库函数的一部分来实现,该函数在实际系统调用之前和/或之后进行许多辅助动作。在这种情况下,我们可以说系统调用嵌入在该函数中,但该函数通常不是系统调用。在另一种情况下,我们可以有一个小函数,它只执行这四个步骤,仅此而已。在这种情况下,我们可以说这个函数是一个系统调用。实际上,您可以通过手动实现上述所有四个阶段来实现系统调用本身。请注意,在这种情况下,您将被迫使用汇编程序,因为所有这些步骤都完全依赖于体系结构。

例如,Linux/i386 环境有下一个系统调用约定:

  1. 可以通过编号为 0x80 的软件中断(汇编指令 INT 0x80)或通过 SYSCALL 指令(AMD)或通过 SYSENTER 指令(Intel)将控制权从用户模式传递到内核模式
  2. 请求的系统服务的id由进入内核模式时存储在EAX寄存器中的整数值指定。内核服务 ID 必须以 _ NR的形式定义。您可以在路径上的 Linux 源代码树中找到所有系统服务 ID include\uapi\asm-generic\unistd.h
  3. 最多可以通过寄存器 EBX(1)、ECX(2)、EDX(3)、ESI(4)、EDI(5)、EBP(6) 传递 6 个参数。括号中的数字是参数的序号。
  4. 内核返回在 EAX 寄存器中执行的服务的状态。这个值通常被 glibc 用来设置 errno 变量。

在现代版本的 Linux 中,没有任何 _syscall 宏(据我所知)。相反,glibc 库,即 Linux 内核的主要接口库,提供了一个特殊的宏 - INTERNAL_SYSCALL,它扩展为由内联汇编指令填充的一小段代码。这段代码针对特定的硬件平台,实现了系统调用的所有阶段,因此,这个宏代表了系统调用本身。还有另一个宏 - INLINE_SYSCALL. 最后一个宏提供了类似 glibc 的错误处理,根据哪个系统调用失败将返回 -1 并将错误号存储在errno变量中。这两个宏都在sysdep.hglibc 包中定义。

您可以通过以下方式调用系统调用:

#include <sysdep.h>

#define __NR_<name> <id>

int my_syscall(void)
{
    return INLINE_SYSCALL(<name>, <argc>, <argv>);
}
Run Code Online (Sandbox Code Playgroud)

where<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"
}
Run Code Online (Sandbox Code Playgroud)

或另一个例子:

#include <sysdep.h>

#define __NR_fork 2 

int _fork(void)
{
    return INLINE_SYSCALL(fork, 0); // takes no parameters
}
Run Code Online (Sandbox Code Playgroud)