如何在内联汇编中通过 syscall 或 sysenter 调用系统调用?

2024-10-10 09:28:00
admin
原创
208
摘要:问题描述:如何在 x86 Linux 中直接使用 sysenter/syscall 实现系统调用?有人能帮忙吗?如果您还能展示 amd64 平台的代码就更好了。我知道在 x86 中我们可以使用__asm__( " movl $1, %eax &quot...

问题描述:

如何在 x86 Linux 中直接使用 sysenter/syscall 实现系统调用?有人能帮忙吗?如果您还能展示 amd64 平台的代码就更好了。

我知道在 x86 中我们可以使用

__asm__(
"               movl $1, %eax  
"
"               movl $0, %ebx 
"
"               call *%gs:0x10 
"
);

间接路由至 sysenter。

但是我们如何直接使用 sysenter/syscall 编写代码来发出系统调用呢?

我找到了一些资料http://damocles.blogbus.com/tag/sysenter/。但仍然觉得很难弄清楚。


解决方案 1:

首先,您不能安全地使用 GNU C Basicasm("");语法(没有输入/输出/破坏约束)。您需要扩展 asm 来告诉编译器您修改的寄存器。请参阅GNU C 手册中的内联 asm和内联汇编标签 wiki以获取指向其他指南的链接,以了解诸如"D"(1)作为asm()语句的一部分的含义的详细信息。

您还需要asm volatile,因为这对于具有 1 个或多个输出操作数的扩展语句来说不是隐含的asm


我将向您展示如何通过编写一个Hello World!使用write()系统调用写入标准输出的程序来执行系统调用。以下是没有实际系统调用实现的程序源代码:

#include <sys/types.h>

ssize_t my_write(int fd, const void *buf, size_t size);

int main(void)
{
    const char hello[] = "Hello world!
";
    my_write(1, hello, sizeof(hello));
    return 0;
}

您可以看到,我将自定义系统调用函数命名为,my_write以避免与writelibc 提供的“正常”函数发生名称冲突。本答案的其余部分包含my_writei386 和 amd64 的源代码。

i386

i386 Linux 中的系统调用是使用第 128 个中断向量实现的,例如通过调用int 0x80汇编代码,当然,需要事先设置相应的参数。也可以通过 执行相同操作SYSENTER,但实际执行此指令是通过虚拟映射到每个正在运行的进程的 VDSO 实现的。由于SYSENTER它从来都不是int 0x80API 的直接替代品,它从不由用户空间应用程序直接执行 - 相反,当应用程序需要访问某些内核代码时,它会调用 VDSO 中的虚拟映射例程(这就是call *%gs:0x10代码中的 的用途),其中包含支持该指令的所有代码SYSENTER。由于指令的实际工作方式,所以代码量相当大。

如果您想了解更多信息,请查看此链接。它包含对内核和 VDSO 中应用的技术的简要概述。另请参阅(x86) Linux 系统调用权威指南- 一些系统调用(如getpid和)非常简单,内核可以导出在用户空间中运行的代码 + 数据,因此 VDSO 永远不需要进入内核,从而使其速度比可能的clock_gettime速度快得多。sysenter


使用较慢的版本int $0x80来调用 32 位 ABI 要容易得多。

// i386 Linux
#include <asm/unistd.h>      // compile with -m32 for 32 bit call numbers
//#define __NR_write 4
ssize_t my_write(int fd, const void *buf, size_t size)
{
    ssize_t ret;
    asm volatile
    (
        "int $0x80"
        : "=a" (ret)
        : "0"(__NR_write), "b"(fd), "c"(buf), "d"(size)
        : "memory"    // the kernel dereferences pointer args
    );
    return ret;
}

可以看出,使用int 0x80API 相对简单。系统调用的编号存入寄存器eax,而系统调用所需的所有参数分别存入ebxecxedxesiediebp。系统调用号可以通过读取文件 来获取/usr/include/asm/unistd_32.h

该函数的原型和描述可以在手册的第 2 部分找到,因此在这种情况下write(2)

内核保存/恢复所有寄存器(EAX 除外),因此我们可以将它们用作内联汇编的仅输入操作数。请参阅i386 和 x86-64 上 UNIX 和 Linux 系统调用(和用户空间函数)的调用约定是什么

请记住,clobber 列表还包含memory参数,这意味着指令列表中列出的指令引用内存(通过buf参数)。 (输入到内联 asm 的指针并不意味着指向的内存也是输入。请参阅如何指示可以使用内联 ASM 参数指向的内存?)

amd64

在 AMD64 架构上,情况有所不同,它采用了一条名为 的新指令SYSCALL。它与原始SYSENTER指令有很大不同,并且绝对更容易从用户空间应用程序使用 -CALL实际上,它真的很像一个正常的 ,并且将旧指令改编int 0x80为新SYSCALL指令非常简单。(除了它使用 RCX 和 R11 而不是内核堆栈来保存用户空间 RIP 和 RFLAGS,以便内核知道返回到哪里)。

在这种情况下,系统调用的编号仍在寄存器中传递,但用于保存参数的rax寄存器现在几乎符合函数调用约定:rdi、、、、和按此顺序排列。(rsi本身会被破坏,所以使用 而不是rdx,让libc 包装器函数只使用/ 。)r10`r8r9syscallrcx` `r10rcxmov r10, rcxsyscall`

// x86-64 Linux
#include <asm/unistd.h>      // compile without -m32 for 64 bit call numbers
// #define __NR_write 1
ssize_t my_write(int fd, const void *buf, size_t size)
{
    ssize_t ret;
    asm volatile
    (
        "syscall"
        : "=a" (ret)
        //                 EDI      RSI       RDX
        : "0"(__NR_write), "D"(fd), "S"(buf), "d"(size)
        : "rcx", "r11", "memory"
    );
    return ret;
}

(参见Godbolt上的汇编)

请注意,实际上唯一需要更改的是寄存器名称和用于进行调用的实际指令。这主要归功于 gcc 的扩展内联汇编语法提供的输入/输出列表,它会自动提供执行指令列表所需的适当移动指令。

匹配约束"0"(callnum)可以写成,"a"因为操作数 0("=a"(ret)输出)只有一个寄存器可供选择;我们知道它会选择 EAX。使用您认为更清楚的任何一个。


请注意,非 Linux 操作系统(如 MacOS)使用不同的调用号。甚至 32 位的参数传递约定也不同。

解决方案 2:

显式寄存器变量

https://gcc.gnu.org/onlinedocs/gcc-8.2.0/gcc/Explicit-Register-Variables.html#Explicit-Reg-Vars

我相信这现在应该是优于寄存器约束的推荐方法,因为:

  • 它可以表示所有寄存器,包括和r8,用于系统调用参数:如何在 GCC 内联汇编中指定 Intel x86_64 寄存器 r8 到 r15 上的寄存器约束?r9`r10`

  • 对于除 x86 之外的其他 ISA(如 ARM),它是唯一的最佳选择,它们没有魔术寄存器约束名称:如何在 ARM GCC 内联汇编中将单个寄存器指定为约束?(除了使用临时寄存器 + clobbers + 和额外的 mov 指令)

  • 我认为这种语法比使用单字母助记符更具可读性,例如S -> rsi

寄存器变量例如在 glibc 2.29 中使用,参见:sysdeps/unix/sysv/linux/x86_64/sysdep.h

主寄存器

#define _XOPEN_SOURCE 700
#include <inttypes.h>
#include <sys/types.h>

ssize_t my_write(int fd, const void *buf, size_t size) {
    register int64_t rax __asm__ ("rax") = 1;
    register int rdi __asm__ ("rdi") = fd;
    register const void *rsi __asm__ ("rsi") = buf;
    register size_t rdx __asm__ ("rdx") = size;
    __asm__ __volatile__ (
        "syscall"
        : "+r" (rax)
        : "r" (rdi), "r" (rsi), "r" (rdx)
        : "rcx", "r11", "memory"
    );
    return rax;
}

void my_exit(int exit_status) {
    register int64_t rax __asm__ ("rax") = 60;
    register int rdi __asm__ ("rdi") = exit_status;
    __asm__ __volatile__ (
        "syscall"
        : "+r" (rax)
        : "r" (rdi)
        : "rcx", "r11", "memory"
    );
}

void _start(void) {
    char msg[] = "hello world
";
    my_exit(my_write(1, msg, sizeof(msg)) != sizeof(msg));
}

GitHub 上游。

编译并运行:

gcc -O3 -std=c99 -ggdb3 -ffreestanding -nostdlib -Wall -Werror \n  -pedantic -o main_reg.out main_reg.c
./main.out
echo $?

输出

hello world
0

为了进行比较,以下类似于如何在内联汇编中通过 syscall 或 sysenter 调用系统调用?产生等效的汇编:

主约束.c

#define _XOPEN_SOURCE 700
#include <inttypes.h>
#include <sys/types.h>

ssize_t my_write(int fd, const void *buf, size_t size) {
    ssize_t ret;
    __asm__ __volatile__ (
        "syscall"
        : "=a" (ret)
        : "0" (1), "D" (fd), "S" (buf), "d" (size)
        : "rcx", "r11", "memory"
    );
    return ret;
}

void my_exit(int exit_status) {
    ssize_t ret;
    __asm__ __volatile__ (
        "syscall"
        : "=a" (ret)
        : "0" (60), "D" (exit_status)
        : "rcx", "r11", "memory"
    );
}

void _start(void) {
    char msg[] = "hello world
";
    my_exit(my_write(1, msg, sizeof(msg)) != sizeof(msg));
}

GitHub 上游。

拆卸两者:

objdump -d main_reg.out

几乎完全相同,以下是其中main_reg.c之一:

Disassembly of section .text:

0000000000001000 <my_write>:
    1000:   b8 01 00 00 00          mov    $0x1,%eax
    1005:   0f 05                   syscall 
    1007:   c3                      retq   
    1008:   0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
    100f:   00 

0000000000001010 <my_exit>:
    1010:   b8 3c 00 00 00          mov    $0x3c,%eax
    1015:   0f 05                   syscall 
    1017:   c3                      retq   
    1018:   0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
    101f:   00 

0000000000001020 <_start>:
    1020:   c6 44 24 ff 00          movb   $0x0,-0x1(%rsp)
    1025:   bf 01 00 00 00          mov    $0x1,%edi
    102a:   48 8d 74 24 f3          lea    -0xd(%rsp),%rsi
    102f:   48 b8 68 65 6c 6c 6f    movabs $0x6f77206f6c6c6568,%rax
    1036:   20 77 6f 
    1039:   48 89 44 24 f3          mov    %rax,-0xd(%rsp)
    103e:   ba 0d 00 00 00          mov    $0xd,%edx
    1043:   b8 01 00 00 00          mov    $0x1,%eax
    1048:   c7 44 24 fb 72 6c 64    movl   $0xa646c72,-0x5(%rsp)
    104f:   0a 
    1050:   0f 05                   syscall 
    1052:   31 ff                   xor    %edi,%edi
    1054:   48 83 f8 0d             cmp    $0xd,%rax
    1058:   b8 3c 00 00 00          mov    $0x3c,%eax
    105d:   40 0f 95 c7             setne  %dil
    1061:   0f 05                   syscall 
    1063:   c3                      retq   

因此我们看到 GCC 按要求内联了那些微小的系统调用函数。

my_writemy_exit两者相同,但_start略有main_constraint.c不同:

0000000000001020 <_start>:
    1020:   c6 44 24 ff 00          movb   $0x0,-0x1(%rsp)
    1025:   48 8d 74 24 f3          lea    -0xd(%rsp),%rsi
    102a:   ba 0d 00 00 00          mov    $0xd,%edx
    102f:   48 b8 68 65 6c 6c 6f    movabs $0x6f77206f6c6c6568,%rax
    1036:   20 77 6f 
    1039:   48 89 44 24 f3          mov    %rax,-0xd(%rsp)
    103e:   b8 01 00 00 00          mov    $0x1,%eax
    1043:   c7 44 24 fb 72 6c 64    movl   $0xa646c72,-0x5(%rsp)
    104a:   0a 
    104b:   89 c7                   mov    %eax,%edi
    104d:   0f 05                   syscall 
    104f:   31 ff                   xor    %edi,%edi
    1051:   48 83 f8 0d             cmp    $0xd,%rax
    1055:   b8 3c 00 00 00          mov    $0x3c,%eax
    105a:   40 0f 95 c7             setne  %dil
    105e:   0f 05                   syscall 
    1060:   c3                      retq 

有趣的是,在这种情况下,GCC 通过选择找到了稍短的等效编码:

    104b:   89 c7                   mov    %eax,%edi

将 设置fd1,它等于1系统调用号中的 ,而不是更直接的:

    1025:   bf 01 00 00 00          mov    $0x1,%edi    

有关调用约定的深入讨论,另请参阅:i386 和 x86-64 上 UNIX 和 Linux 系统调用(以及用户空间函数)的调用约定是什么

在 Ubuntu 18.10、GCC 8.2.0 中测试。

相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   2079  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1459  
  建筑行业正处于数字化转型的关键时期,建筑产品生命周期管理(PLM)系统的实施对于提升项目效率、质量和协同性至关重要。特别是在 2025 年,基于建筑信息模型(BIM)的项目进度优化工具成为众多建筑企业关注的焦点。这些工具不仅能够整合项目全生命周期的数据,还能通过精准的分析和模拟,为项目进度管理提供强大支持。BIM 与建...
plm是什么软件   0  
  PLM系统开发的重要性与现状PLM(产品生命周期管理)系统在现代企业的产品研发、生产与管理过程中扮演着至关重要的角色。它贯穿产品从概念设计到退役的整个生命周期,整合了产品数据、流程以及人员等多方面的资源,极大地提高了企业的协同效率和创新能力。通过PLM系统,企业能够实现产品信息的集中管理与共享,不同部门之间可以实时获取...
国产plm软件   0  
  PLM(产品生命周期管理)系统在企业产品研发与管理过程中扮演着至关重要的角色。随着市场竞争的加剧和技术的飞速发展,企业对PLM系统的迭代周期优化需求日益迫切。2025年敏捷认证对项目管理提出了新的要求,其中燃尽图作为一种强大的可视化工具,在PLM系统迭代周期优化中有着广泛且重要的应用。深入探讨这些应用,对于提升企业的项...
plm系统主要干什么的   0  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

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

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

内置subversion和git源码管理

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

免费试用