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

2024-10-21 09:14:00
admin
原创
81
摘要:问题描述:我读了 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在不能使用时使用宏。

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

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

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

内置subversion和git源码管理

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

免费试用