20

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

从用户空间访问系统调用

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

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

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()哪里?我怎样才能使这段代码可编译和可运行?我需要包含哪些头文件?

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

4

3 回答 3

24

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

实际上,应用程序在内核提供的“虚拟机”上运行:它在用户空间中运行,并且只能执行(在最低机器级别)在用户 CPU 模式下允许的由指令增强的机器指令集(例如SYSENTERINT 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__openfrom的数字<asm/unistd.h>,它是一条SYSENTER机器指令,约定哪些寄存器在系统调用的参数之前保存,哪些在系统调用的结果[s] 之后保存(包括失败结果,errno在 C 库包装中设置系统调用)。系统调用的约定不是 ABI 规范中 C 函数的调用约定(例如x86-64 psABI)。所以你需要一个 C 包装器。

于 2012-07-23T09:27:58.873 回答
5

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

系统调用包括四个阶段:

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

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

例如,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>);
}

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"
}

或另一个例子:

#include <sysdep.h>

#define __NR_fork 2 

int _fork(void)
{
    return INLINE_SYSCALL(fork, 0); // takes no parameters
}
于 2013-12-01T23:11:47.287 回答
2

最小可运行程序集示例

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当您不能使用宏时。

于 2017-10-02T21:18:29.327 回答