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

20

我在LKD1中读了一些段落,但是我就是无法理解下面的内容:

从用户空间访问系统调用

通常,C库提供对系统调用的支持。用户应用程序可以从标准头文件中获取函数原型,并链接C库以使用您的系统调用(或反过来使用您的系统调用的库例程)。但是,如果您刚刚编写了系统调用,那么glibc是否已经支持它仍然存疑!

幸运的是,Linux提供了一组宏来包装对系统调用的访问。它设置寄存器内容并发出陷阱指令。这些宏命名为_syscalln(),其中n介于零和六之间。该数字对应传递到系统调用中的参数数量,因为宏需要知道要期望多少个参数,并相应地推入寄存器。例如,考虑定义为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;
}

“the application can simply call open()”的意思是应用程序可以简单地调用open()函数。

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

__________
1 《Linux内核开发》,作者Robert Love。WordPress.com上的PDF文件(请前往第81页);Google图书结果


顺便问一下,你为什么想要添加一个新的系统调用?这通常被认为是不好的做法,而且你可能会很难将其包含在官方的Linux内核中! - Basile Starynkevitch
3个回答

24
您首先应该了解Linux内核的作用,以及应用程序只能通过系统调用与内核进行交互。
实际上,应用程序在内核提供的“虚拟机器”上运行:它在用户空间中运行,并且只能执行(在最低的机器级别上)由用户CPU模式允许的一组机器指令,这些指令增强了指令(例如SYSENTERINT 0x80...),用于进行系统调用。因此,从用户级应用程序的角度来看,系统调用是一个原子伪机器指令。 Linux汇编入门教程解释了如何在汇编(即机器指令)级别上进行系统调用。 GNU libc 提供了对应于系统调用的 C 函数。例如,open 函数是系统调用编号为 NR__open 的一个小包装器(即一个封装),它会进行系统调用,然后更新 errno。应用程序通常在 libc 中调用这样的 C 函数,而不是进行系统调用。 您也可以使用其他的 libc。例如,MUSL libc 有点“简单”,其代码可能更容易阅读。它还将原始系统调用封装成相应的 C 函数。 如果您添加自己的系统调用,最好也实现类似的 C 函数(在您自己的库中)。因此,您应该为您的库编写头文件。
请参阅intro(2)syscall(2)syscalls(2)手册页面,以及VDSO在系统调用中的作用
请注意,syscalls不是C函数。它们不使用调用堆栈(甚至可以在没有任何堆栈的情况下被调用)。系统调用基本上是一个数字,例如来自<asm/unistd.h>中的NR__open,一个带有约定的SYSENTER机器指令,约定了哪些寄存器在系统调用之前保存参数,哪些寄存器在系统调用之后保存结果(包括失败结果,以设置C库中包装系统调用的errno)。系统调用的约定不是ABI规范中C函数的调用约定(例如x86-64 psABI)。因此,您需要一个C语言包装器。

2
那么,你的意思是 open 函数不是一个真正的系统调用,而是在真正的系统调用之上的包装函数。我说得对吗? - injoy
是的,系统调用不是 C 函数,需要将系统调用包装在 C 函数中,以便在失败时显著设置 errno - Basile Starynkevitch
谢谢提供信息,但是你最后提供的链接已经失效了(x86-64 ABI)。 - WinEunuuchs2Unix

5
首先,我想提供一些系统调用的定义。系统调用是用户空间应用程序显式同步请求特定内核服务的过程。同步意味着系统调用的行为由执行指令序列预先确定。中断是异步系统服务请求的一个例子,因为它们完全独立于处理器上执行的代码而到达内核。与系统调用相反,异常是隐式请求内核服务的同步请求。
系统调用包括四个阶段:
1. 将控制传递到内核中的特定点,并通过将处理器从用户模式切换到内核模式并返回到用户模式来返回控制。 2. 指定所请求内核服务的ID。 3. 传递所请求服务的参数。 4. 捕获服务结果。
总的来说,所有这些操作都可以作为一个大型库函数的一部分实现,在实际系统调用之前或之后执行许多辅助操作。在这种情况下,我们可以说系统调用嵌入在此函数中,但该函数本身不是系统调用。在另一种情况下,我们可能只有一个小函数,只执行这四个步骤而没有其他操作。在这种情况下,我们可以说这个函数是一个系统调用。实际上,您可以通过手动实现上述四个阶段来实现系统调用。请注意,这种情况下,您将被迫使用汇编语言,因为所有这些步骤都完全依赖于体系结构。
例如,Linux/i386环境具有以下系统调用约定:
1. 通过0x80号软件中断(汇编指令INT 0x80)或SYSCALL指令(AMD)或SYSENTER指令(Intel)从用户模式切换到内核模式。 2. 在进入内核模式时,所请求的系统服务ID由EAX寄存器中存储的整数值指定。内核服务ID必须以_NR的形式定义。您可以在Linux源树上的路径include\uapi\asm-generic\unistd.h中找到所有系统服务ID。 3. 最多可以通过寄存器EBX(1)、ECX(2)、EDX(3)、ESI(4)、EDI(5)、EBP(6)传递6个参数。括号中的数字是参数的顺序号。 4. 内核将执行服务的状态返回到EAX寄存器中。glibc通常使用此值设置errno变量。
在现代Linux版本中,没有任何_syscall宏(据我所知)。相反,Linux内核的主要接口库glibc提供了一个特殊的宏——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
}

3

最小可运行汇编示例

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宏。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接