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

时间:2012-07-23 08:43:54

标签: linux system-calls

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

从用户空间访问系统调用

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

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

long open(const char *filename, int flags, int mode)

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

#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\n", stack_size);
        return 0;
}

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

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

__________
罗伯特·洛夫(Robert Love)的 1 Linux内核开发PDF file at wordpress.com(转到第81页); Google Books result

3 个答案:

答案 0 :(得分:18)

首先应该了解linux kernel的作用是什么,以及应用程序只与 通过system calls进行交互。

实际上,应用程序在内核提供的“虚拟机”上运行:它在user space中运行,并且只能(在最低的机器级别)user CPU mode中允许的一组机器指令3}}由用于进行系统调用的指令(例如SYSENTERINT 0x80 ...)进行扩充。因此,从用户级应用程序的角度来看,系统调用是一种原子伪机器指令。

Linux Assembly Howto解释了如何在汇编(即机器指令)级别完成系统调用。

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

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

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

另请参阅intro(2)syscall(2)syscalls(2)手册页以及VDSO in syscalls的角色。

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

答案 1 :(得分:4)

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

系统调用包括四个阶段:

  1. 通过将处理器从用户模式切换到内核模式,将控制权交给内核中的特定点,然后将切换处理器返回到用户模式。
  2. 指定所请求的内核服务的id。
  3. 传递所请求服务的参数。
  4. 捕获服务结果。
  5. 通常,所有这些操作都可以作为一个大库函数的一部分来实现,该函数在实际系统调用之前和/或之后进行许多辅助操作。在这种情况下,我们可以说系统调用嵌入在此函数中,但该函数通常不是系统调用。在另一种情况下,我们可以有一个微小的功能,只有这四个步骤而已。在这种情况下,我们可以说这个函数是一个系统调用。实际上,您可以通过手动实现上述所有四个阶段来实现系统调用。请注意,在这种情况下,您将被迫使用Assembler,因为所有这些步骤都完全取决于架构。

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

    1. 将控制从用户模式传递到内核模式可以通过编号为0x80的软件中断(汇编指令INT 0x80)或SYSCALL指令(AMD)或SYSENTER指令(英特尔)来完成
    2. 所请求的系统服务的Id由进入内核模式期间存储在EAX寄存器中的整数值指定。必须以_ NR 的形式定义内核服务ID。您可以在路径include\uapi\asm-generic\unistd.h上的Linux源代码树中找到所有系统服务ID。
    3. 最多可以通过寄存器EBX(1),ECX(2),EDX(3),ESI(4),EDI(5),EBP(6)传递6个参数。括号中的数字是参数的序号。
    4. 内核返回在EAX寄存器中执行的服务的状态。这个值通常由glibc用于设置errno变量。
    5. 在Linux的现代版本中,没有任何_syscall宏(据我所知)。相反,glibc库是Linux内核的主要接口库,提供了一个特殊的宏 - INTERNAL_SYSCALL,它扩展为由内联汇编程序指令填充的一小段代码。这段代码针对特定的硬件平台并实现系统调用的所有阶段,因此,此宏表示系统调用本身。还有另一个宏 - INLINE_SYSCALL。最后一个宏提供类似glibc的错误处理,根据该错误处理,将返回失败的系统调用-1,错误号将存储在errno变量中。这两个宏都在glibc包的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
      }
      

答案 2 :(得分:1)

最小可运行程序集示例

hello_world.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

从代码中可以很容易地推断出来:

当然,汇编会很快变得乏味,你很快就会想要尽可能使用glibc / POSIX提供的C包装器,或者当你不能使用SYSCALL宏时。