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

2024-10-21 09:14:00
admin
原创
242
摘要:问题描述:我读了 LKD 1中的一些段落,但就是无法理解下面的内容:从用户空间访问系统调用通常,C 库提供对系统调用的支持。用户应用程序可以从标准头文件中引入函数原型,并与 C 库链接以使用您的系统调用(或反过来使用您的 syscall 调用的库例程)。但是,如果您刚刚编写了系统调用,则 glibc 是否已经...

问题描述:

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

从用户空间访问系统调用

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

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

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 模式下允许的机器指令集,并通过用于进行系统调用的指令(例如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是一条机器指令,其中约定了哪些寄存器在系统调用的参数之前保存,哪些寄存器在系统调用的结果(包括失败结果,在包装errno系统调用的 C 库中设置)之后保存。系统调用的约定不是 ABI 规范中 C 函数的调用约定(例如x86-64 psABI)。所以你需要一个 C 包装器。

解决方案 2:

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

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

  1. 通过将处理器从用户模式切换到内核模式,将控制权传递给内核中的特定点,并通过将处理器切换回用户模式将其返回。

  2. 指定请求的内核服务的 id。

  3. 传递所请求服务的参数。

  4. 捕获服务的结果。

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

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

  1. 将控制权从用户模式传递到内核模式可以通过编号为 0x80 的软件中断(汇编指令 INT 0x80)、SYSCALL 指令(AMD)或 SYSENTER 指令(Intel)来完成

  2. 请求的系统服务的 ID 由进入内核模式时存储在 EAX 寄存器中的整数值指定。内核服务 ID 必须以 _ NR形式定义。您可以在 Linux 源代码树的路径上找到所有系统服务 ID include/uapiasm-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 并将错误号存储在变量中。这两个宏都是在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#L13

  • ebxecxedx包含输入参数。这些应该可以从内核源代码中每个系统调用的签名中推断出来。另请参阅:x86-64 上的 UNIX 和 Linux 系统调用的调用约定是什么以及汇编语言中的 Linux 系统调用表或 cheetsheet

  • 返回值主要包含错误代码并存入 eax(为简单起见,未在此代码中显示)

  • int 0x80进行调用,尽管现在有更好的方法:“int 0x80”或“syscall”哪个更好?

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

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1324  
  IPD研发管理体系作为一种先进的研发管理理念和方法,对于打造优质产品体验起着至关重要的作用。它涵盖了从产品规划、研发、上市到生命周期管理的全流程,通过整合资源、优化流程、加强团队协作等方式,确保产品能够精准满足用户需求,提升用户满意度和忠诚度。IPD研发管理体系的核心原则IPD研发管理体系以市场驱动为核心原则。这意味着...
IPD集成产品开发   8  
  IPD(Integrated Product Development)产品开发流程作为一种先进的产品开发管理模式,在众多企业中得到广泛应用。它强调跨部门团队协作、并行工程以及基于市场的产品开发理念,旨在提高产品开发效率、缩短产品上市时间、提升产品质量。而成本控制在产品开发过程中至关重要,关乎企业的利润空间和市场竞争力。...
华为IPD流程   6  
  IPD(Integrated Product Development)产品开发流程作为一种先进的产品开发管理模式,在众多企业中得到了广泛应用。它从多个维度对产品开发过程进行优化和整合,为企业创新提供了强大的支撑。通过实施IPD产品开发流程,企业能够更加高效地将创意转化为具有市场竞争力的产品,从而在激烈的市场竞争中占据优...
华为IPD流程管理   10  
  华为作为全球知名的科技企业,其产品质量在市场上有口皆碑。华为IPD产品开发流程在确保产品质量方面发挥了至关重要的作用。IPD(Integrated Product Development)即集成产品开发,是一套先进的、成熟的产品开发管理思想、模式和方法。它打破了传统产品开发中各部门之间的壁垒,强调跨部门团队协作,从产品...
IPD集成产品开发流程   9  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用