首页 文章

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

提问于
浏览
16

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

从用户空间访问系统调用通常,C库提供对系统调用的支持 . 用户应用程序可以从标准头中提取函数原型并与C库链接以使用您的系统调用(或者库例程,而该例程又使用您的系统调用) . 但是,如果您刚刚编写了系统调用,那么glibc已经支持它是值得怀疑的!值得庆幸的是,Linux提供了一组用于包装对系统调用的访问的宏 . 它设置寄存器内容并发出陷阱指令 . 这些宏名为_syscalln(),其中n介于0和6之间 . 该数字对应于传递给系统调用的参数数量,因为宏需要知道预期的参数数量,从而推入寄存器 . 例如,考虑系统调用open(),定义为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(“内核堆栈大小为%ld \ n”,stack_size);
返回0;
}

the application can simply call open() 是什么意思?

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


1 Linux内核开发,作者:Robert Love . PDF file at wordpress.com(转到第81页); Google Books result .

3 回答

  • 4

    您首先应该了解linux kernel的作用是什么,并且应用程序仅通过system calls与内核交互 .

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

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

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

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

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

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

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

  • 18

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

    系统调用包括四个阶段:

    • 将控制权从用户模式切换到内核模式并将其返回到用户模式,将控制权交给内核中的特定点 .

    • 指定所请求的内核服务的id .

    • 传递所请求服务的参数 .

    • 捕获服务结果 .

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

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

    • 将控制从用户模式传递到内核模式可以通过编号为0x80的软件中断(汇编指令INT 0x80)或SYSCALL指令(AMD)或SYSENTER指令(英特尔)完成

    • 请求的系统服务的Id由进入内核模式期间存储在EAX寄存器中的整数值指定 . 必须以_NR形式定义内核服务标识 . 您可以在路径 include\uapi\asm-generic\unistd.h 上的Linux源代码树中找到所有系统服务ID .

    • 最多可以通过寄存器EBX(1),ECX(2),EDX(3),ESI(4),EDI(5),EBP(6)传递6个参数 . 括号中的数字是参数的序号 .

    • 内核返回在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
    }
    
  • 1

    Minimal runnable assembly example

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

相关问题