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

问题描述 投票:19回答:2

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

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

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

间接路由到sysenter。

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

我找到一些材料http://damocles.blogbus.com/tag/sysenter/。但仍然难以弄明白。

linux gcc x86 x86-64 system-calls
2个回答
32
投票

我将通过编写一个使用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!\n";
    my_write(1, hello, sizeof(hello));
    return 0;
}

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

澳洲东部时间

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

如果您想了解更多相关信息,请查看this link。它包含了内核和VDSO中应用的技术的相当简要的概述。

#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)
        : "cc", "edi", "esi", "memory"
    );
    return ret;
}

如您所见,使用int 0x80 API相对简单。系统调用的数量转到eax寄存器,而系统调用所需的所有参数分别分别为ebxecxedxesiediebp。可以通过读取文件/usr/include/asm/unistd_32.h获得系统调用号。手册的第2部分提供了函数的原型和描述,因此在本例中为write(2)。由于内核被允许几乎破坏任何寄存器,因此我将所有剩余的GPR放在clobber列表以及cc上,因为eflags寄存器也可能会改变。请记住,clobber列表还包含memory参数,这意味着指令列表中列出的指令引用内存(通过buf参数)。

AMD64

在AMD64架构上看起来非常不同,它采用了一种名为SYSCALL的新指令。它与原始的SYSENTER指令非常不同,并且绝对更容易使用用户态应用程序 - 它实际上类似于普通的CALL,实际上,将旧的int 0x80改编为新的SYSCALL几乎是微不足道的。

在这种情况下,系统调用的数量仍然在寄存器rax中传递,但用于保存参数的寄存器已经发生严重变化,因为现在它们应按以下顺序使用:rdirsirdxr10r8r9。允许内核破坏寄存器rcxr11的内容(它们用于保存SYSCALL的一些其他寄存器)。

#define __NR_write 1
ssize_t my_write(int fd, const void *buf, size_t size)
{
    ssize_t ret;
    asm volatile
    (
        "syscall"
        : "=a" (ret)
        : "0"(__NR_write), "D"(fd), "S"(buf), "d"(size)
        : "cc", "rcx", "r11", "memory"
    );
    return ret;
}

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


2
投票

显式寄存器变量

为了完整起见,我想提供一个使用GCC explicit register variables的示例。

该机制具有以下优点:

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

还要注意其他诸如ARM之类的拱门完全放弃了单字母助记符,并且注册变量是看起来的唯一方法,例如:参见:How to specify an individual register as constraint in ARM GCC inline assembly?

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)
        : "cc", "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)
        : "cc", "rcx", "r11", "memory"
    );
}

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

GitHub upstream

编译并运行:

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

产量

hello world
0

为了比较,以下类似于How to invoke a system call via sysenter in inline assembly?产生等效组件:

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)
        : "cc", "rcx", "r11", "memory"
    );
    return ret;
}

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

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

GitHub upstream

两者的拆卸:

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

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

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

有关调用约定的深入讨论,请参阅:What are the calling conventions for UNIX & Linux system calls on i386 and x86-64

在Ubuntu 18.10,GCC 8.2.0中测试。

© www.soinside.com 2019 - 2024. All rights reserved.