gpt4 book ai didi

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

转载 作者:IT王子 更新时间:2023-10-29 00:19:31 26 4
gpt4 key购买 nike

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

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

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

间接路由到sysenter。

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

我找到了一些 Material http://damocles.blogbus.com/tag/sysenter/ .但还是觉得很难弄明白。

最佳答案

首先,你不能安全地使用 GNU C Basic asm("");此语法 (没有输入/输出/clobber 约束)。您需要扩展 asm 来告诉编译器您修改的寄存器。见 inline asm in the GNU C manualinline-assembly tag wiki有关其他指南的链接,以了解有关诸如 "D"(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!\n";
my_write(1, hello, sizeof(hello));
return 0;
}

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

i386

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

如果您想了解更多相关信息,请查看 this link .它包含对在内核和 VDSO 中应用的技术的相当简要的概述。另见 The Definitive Guide to (x86) Linux System Calls - 一些系统调用,如 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注册,而系统调用所需的所有参数分别进入 ebx , ecx , edx , esi , edi , 和 ebp .系统调用号可以通过读取文件 /usr/include/asm/unistd_32.h获得.

功能的原型(prototype)和描述在手册的第二部分中可用,所以在这种情况下 write(2) .

内核保存/恢复所有寄存器(EAX 除外),因此我们可以将它们用作内联汇编的仅输入操作数。见 What are the calling conventions for UNIX & Linux system calls on i386 and x86-64

请记住,clobber 列表还包含 memory参数,这意味着指令列表中列出的指令引用内存(通过 buf 参数)。 (内联 asm 的指针输入并不意味着指向的内存也是输入。参见 How can I indicate that the memory *pointed* to by an inline ASM argument may be used?)

amd64

AMD64 架构上的情况看起来有所不同,它运行了一条名为 SYSCALL 的新指令。 .和原来的很不一样 SYSENTER指令,并且从用户态应用程序中使用肯定要容易得多 - 它真的很像一个普通的 CALL ,实际上,并适应旧的 int 0x80到新 SYSCALL非常简单。 (除了它使用 RCX 和 R11 而不是内核堆栈来保存用户空间 RIP 和 RFLAGS 以便内核知道从哪里返回)。

在这种情况下,系统调用的编号仍然传递到寄存器 rax 中。 ,但用于保存参数的寄存器现在几乎符合函数调用约定: rdi , rsi , rdx , r10 , r8r9以该顺序。 ( syscall 本身会破坏 rcx so r10 is used instead of 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 上编译)

请注意实际上唯一需要更改的是寄存器名称和用于进行调用的实际指令。这主要归功于 gcc 的扩展内联汇编语法提供的输入/输出列表,它自动提供执行指令列表所需的适当移动指令。
"0"(callnum)匹配约束可以写成 "a"因为操作数 0( "=a"(ret) 输出)只有一个寄存器可供选择;我们知道它会选择 EAX。使用你觉得更清楚的那个。

请注意,非 Linux 操作系统(如 MacOS)使用不同的电话号码。甚至还有不同的 32 位 arg 传递约定。

关于linux - 如何在内联汇编中通过 syscall 或 sysenter 调用系统调用?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/9506353/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com