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

28

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

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

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

如何使用sysenter/syscall直接发出系统调用?

我找到了一些资料http://damocles.blogbus.com/tag/sysenter/,但仍然很难理解。

为了间接路由到sysenter,我们该怎么编写代码呢?


如何从用户空间访问系统调用? 如何通过内联汇编在sysenter中调用系统调用? Linux系统调用表或汇编语言中的快速参考表。 汇编和系统调用。 在汇编代码中,“int 0x80”是什么意思? - jww
2个回答

51
首先,您不能安全地使用GNU C基本的asm("");语法(没有输入/输出/破坏约束)来实现此目的。您需要使用扩展的asm来告诉编译器您修改了哪些寄存器。有关asm()语句中像"D"(1)这样的内容的详细信息,请参见GNU C手册中的内联汇编内联汇编标签wiki中的其他指南链接。
您还需要使用asm volatile,因为对于具有1个或多个输出操作数的扩展asm语句,这不是隐含的。
我将向您展示如何通过编写一个程序来执行系统调用,该程序使用write()系统调用将“Hello World!”写入标准输出。以下是没有实现实际系统调用的程序源代码:
#include <sys/types.h>

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

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

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

i386

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

如果您想了解更多相关内容,请查看此链接。其中包含内核和VDSO中应用的技术的简要概述。另请参阅(x86) Linux系统调用权威指南-某些系统调用如getpidclock_gettime非常简单,内核可以导出在用户空间运行的代码+数据,因此VDSO永远不需要进入内核,使其比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 0x80 API相对简单。系统调用的数量存储在eax寄存器中,而系统调用所需的所有参数分别存储在ebxecxedxesiediebp中。可以通过读取文件/usr/include/asm/unistd_32.h来获取系统调用号码。
函数的原型和描述在手册的第二部分中可用,在此示例中为write(2)
内核保存/恢复所有寄存器(除EAX外),因此我们可以将它们用作内联汇编的输入操作数。请参阅What are the calling conventions for UNIX & Linux system calls (and user-space functions) on i386 and x86-64

请记住,clobber列表还包括memory参数,这意味着在指令列表中列出的指令引用了内存(通过buf参数)。 (内联汇编的指针输入并不意味着指向的内存也是一个输入。请参见如何指示内联ASM参数指向的内存可能被使用?

amd64

在AMD64架构上看起来有所不同,它配备了一条称为SYSCALL的新指令。 它与原始的SYSENTER指令非常不同,并且从用户空间应用程序中使用起来要容易得多 - 实际上它真的很像普通的CALL,将旧的int 0x80调整为新的SYSCALL几乎微不足道。(除了使用RCX和R11而不是内核堆栈来保存用户空间RIP和RFLAGS,以便内核知道返回到哪里)。

在这种情况下,系统调用的数量仍然通过寄存器 rax 传递,但现在用于保存参数的寄存器几乎与函数调用约定相匹配:按顺序为 rdi rsi rdx r10 r8 r9 。( syscall 本身会破坏 rcx 因此使用 r10 代替 rcx ,让libc包装函数只需使用 mov r10,rcx / syscall 即可。)
// 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上查看编译结果)

Please note that the only necessary changes were to the register names and the instruction used for making the call. This is mainly due to the input/output lists provided by gcc's extended inline assembly syntax, which automatically provides the necessary move instructions required for executing the instruction list.
The matching constraint "0"(callnum) could be replaced with "a", since operand 0 (the output "=a"(ret)) only has one register to choose from, which we know will be EAX. Use whichever is clearer to you.
请注意,非Linux操作系统(如MacOS)使用不同的调用号,甚至在32位模式下也有不同的参数传递约定。

2
谢谢!即使是奇怪的程序员,使用sysenter直接编写代码调用系统调用似乎也非常不可能。我们实际上正在开发一个二进制文件(包括恶意软件)分析器,列出目标程序中的所有系统调用。这就是为什么我们想收集发出系统调用的所有方法。似乎我们可以忽略这种直接的sysenter方法。 - Infinite
1
为什么第一个输入参数是“0”,难道不应该是“a”,因为系统调用号进入eax/rax吗? - Calmarius
2
@Calmarius:这里的0意味着“第一个输出参数”。如果我没记错的话(这很久以前了),我用来编译此代码的gcc版本之所以不接受-我们认为是绝对正确的- "a"(__NR_write),似乎是因为某种原因。但是gcc 6.1.1没有这个问题,所以我想你可以使用它。 - Daniel Kamil Kozar
2
根据 http://lxr.free-electrons.com/source/arch/x86/kernel/entry_32.S?v=3.14#L37 ,需要在 clobbers 列表中指定 "cc"(因为 eflags 被保存)或 "edi""esi"(因为这些寄存器也被保存)。 - pts
@MichaelPetch: 对,但在这种情况下它并不是好的文档,因为Linux系统调用ABI保留了EFLAGS(以及edi/esi)。Daniel:您还可以使用虚拟内存输入(转换为结构体或数组)来避免“memory”破坏,以便告诉编译器它将从buf读取length字节,就像手册中的例子https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html:"m" ((const char ()[length]) buf) - Peter Cordes
显示剩余3条评论

5

显式寄存器变量

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

我认为这应该成为推荐的方法,因为:

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

main_reg.c

#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\n";
    my_exit(my_write(1, msg, sizeof(msg)) != sizeof(msg));
}

GitHub上游

编译并运行:

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

输出

hello world
0

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

main_constraint.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\n";
    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 在两个文件中都是相同的,但是 main_constraint.c 中的 _start 稍微有所不同:

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

fd设置为1,它等于系统调用号码中的1,而不是更直接地:

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

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

在Ubuntu 18.10、GCC 8.2.0中进行测试。


1
强制转换是不必要的。在内核中声明为“int”的系统调用参数将安全地忽略寄存器中的高垃圾。顺便说一句,寄存器asm是指定r8..r15中特定寄存器的唯一方法。 - Peter Cordes
@PeterCordes 哦,太棒了,提到了 r8 - r10 - Ciro Santilli OurBigBook.com
如果您使用的是TCC编译器,您可能需要避免使用显式寄存器变量——在我更改它之前,我的系统调用无法正常工作(但在使用GCC时可以正常工作)。 - PhilipRoman
1
此外,Linux系统调用不会破坏RFLAGS寄存器,因此在这里使用“cc”占位符是不合适的。(它已经隐含了,因此删除它不会提高性能。) - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接