Intel x86与x64系统调用

我正在阅读有关x86和x64之间的组装差异。

在x86上,系统调用号被放在eax ,然后执行int 80h来产生一个软件中断

但在x64上,系统调用号被置于rax ,然后执行syscall

我被告知syscall比生成软件中断更轻更快。

为什么它在x64比x86更快,我可以使用int 80h在x64上进行系统调用?

Solutions Collecting From Web of "Intel x86与x64系统调用"

一般部分

编辑:Linux不相关的部分删除

虽然不是完全错误的缩小到int 0x80syscall过于简单的问题与sysenter至少有第三个选项。

使用0x80和eax作为syscall号码,ebx,ecx,edx,esi,edi和ebp来传递参数只是实现系统调用的许多其他选择之一。

在仔细研究所涉及的技术之前,应该说明,他们都围绕着每一个进程运行的特权监狱逃跑的问题。

x86架构提供的另一种选择是使用调用门(参见http://en.wikipedia.org/wiki/Call_gate

所有i386机器上唯一可能存在的其他可能性是使用软件中断,它允许ISR以不同于以前的特权级别运行。

软件中断

一旦中断被触发,到底发生了什么事取决于切换到ISR是否需要更改权限:

(英特尔®64和IA-32架构软件开发人员手册)

6.4.1中断或异常处理程序的调用和返回操作

如果处理程序过程的代码段与当前正在执行的程序或任务具有相同的特权级别,则处理程序过程使用当前栈; 如果处理程序在特权级别执行,则处理程序切换到处理程序的特权级别的堆栈。

….

如果堆叠交换机发生,处理器将执行以下操作:

  1. 临时保存(内部)SS,ESP,EFLAGS,CS和> EIP寄存器的当前内容。

  2. 将来自TSS的新堆栈(也就是被调用的特权级别的堆栈)的段选择器和堆栈指针加载到SS和ESP寄存器中,并切换到新堆栈。

  3. 将临时保存的中断程序堆栈的SS,ESP,EFLAGS,CS和EIP值推送到新堆栈。

  4. 推送新堆栈上的错误代码(如果适用)。

  5. 将新代码段的段选择器和新指令指针(来自中断门或陷阱门)分别加载到CS和EIP寄存器中。

  6. 如果调用通过中断门,则清除EFLAGS寄存器中的IF标志。

  7. 在新的特权级开始处理程序的执行。

感叹这似乎是很多事情要做,甚至一旦我们完成了它并没有太多好转:

(摘自上面提到的同一来源:英特尔®64和IA-32架构软件开发人员手册)

当执行从中断或异常处理程序从不同于中断过程的权限级别返回时,处理器执行这些操作:

  1. 执行特权检查。

  2. 在中断或异常之前,将CS和EIP寄存器恢复为其值。

  3. 恢复EFLAGS寄存器。

  4. 在中断或异常之前,将SS和ESP寄存器恢复为其值,导致堆栈切换回被中断的程序堆栈。

  5. 恢复中断程序的执行。

SYSENTER

在32位平台上的另一个选项在你的问题中没有提到,但是Linux内核使用的是sysenter指令。

(英特尔®64和IA-32架构软件开发人员手册第2卷(2A,2B和2C):指令集参考,AZ)

描述执行对0级系统过程或例程的快速调用。 SYSENTER是SYSEXIT的配套指令。 该指令经过优化,可以为从特权级别3运行的用户代码到在特权级别0运行的操作系统或执行程序提供系统调用的最高性能。

使用这种解决方案的一个缺点是,它并不是在所有的32位机器上都存在,所以在CPU不知道的情况下仍然需要提供int 0x80方法。

SYSENTER和SYSEXIT指令被引入Pentium II处理器的IA-32架构中。 这些指令在处理器上的可用性通过由CPUID指令返回到EDX寄存器的SYSENTER / SYSEXIT存在(SEP)特征标志来指示。 符合SEP标志的操作系统还必须符合处理器系列和型号的要求,以确保SYSENTER / SYSEXIT指令实际存在

系统调用

syscall指令的最后一种可能性与sysenter指令相同。 两者的存在是由于一个( systenter )是由Intel引入的,而另一个( syscall )是由AMD引入的。

特定的Linux

在Linux内核中,可以选择上述三种可能性中的任何一种来实现系统调用。

如上所述, int 0x80方法是3个选择的实现中唯一可以在任何i386 CPU上运行的方法,所以这是唯一一个始终可用的方法。

为了允许在所有3个选择之间进行切换,每个进程运行都可以访问一个特殊的共享对象,以访问为运行系统选择的系统调用实现。 这是看起来很奇怪的linux-gate.so.1 ,在使用ldd之类的时候,你可能已经遇到了linux-gate.so.1解析的库。

(拱/ 86 / VDSO / vdso32-setup.c中)

  if (vdso32_syscall()) { vsyscall = &vdso32_syscall_start; vsyscall_len = &vdso32_syscall_end - &vdso32_syscall_start; } else if (vdso32_sysenter()){ vsyscall = &vdso32_sysenter_start; vsyscall_len = &vdso32_sysenter_end - &vdso32_sysenter_start; } else { vsyscall = &vdso32_int80_start; vsyscall_len = &vdso32_int80_end - &vdso32_int80_start; } 

为了充分利用它,你需要做的就是在eax中加载你所有的寄存器系统调用号,ebx,ecx,edx,esi,edi中的参数和int 0x80系统调用实现一样,并call主程序。

不幸的是,并不是那么容易,为了最小化固定预定义地址的安全风险, vdso在进程中可见的位置是随机的,因此您必须首先找出正确的位置。

一旦启动,每个进程的这个地址就被传递给它。

如果您不知道,在Linux中启动时,每个进程都会获取指向一旦启动的参数的指针,并指向在其堆栈上传递的环境变量的描述 – 每个进程都以NULL结尾。

除了这些之外,所谓的精灵辅助向量的第三个块被传递到前面提到的那些之后。 正确的位置编码在其中的一个携带类型标识符AT_SYSINFO

所以栈布局看起来像这样:

  • 参数-0
  • 参数-m的
  • 空值
  • 环境-0
  • ….
  • 环境-N
  • 空值
  • 辅助精灵矢量: AT_SYSINFO
  • 辅助精灵矢量: AT_NULL

用法示例

要找到正确的地址,必须首先跳过所有参数和所有环境指针,然后开始扫描AT_SYSINFO ,如下例所示:

 #include <stdio.h> #include <elf.h> void putc_1 (char c) { __asm__ ("movl $0x04, %%eax\n" "movl $0x01, %%ebx\n" "movl $0x01, %%edx\n" "int $0x80" :: "c" (&c) : "eax", "ebx", "edx"); } void putc_2 (char c, void *addr) { __asm__ ("movl $0x04, %%eax\n" "movl $0x01, %%ebx\n" "movl $0x01, %%edx\n" "call *%%esi" :: "c" (&c), "S" (addr) : "eax", "ebx", "edx"); } int main (int argc, char *argv[]) { /* using int 0x80 */ putc_1 ('1'); /* rather nasty search for jump address */ argv += argc + 1; /* skip args */ while (*argv != NULL) /* skip env */ ++argv; Elf32_auxv_t *aux = (Elf32_auxv_t*) ++argv; /* aux vector start */ while (aux->a_type != AT_SYSINFO) { if (aux->a_type == AT_NULL) return 1; ++aux; } putc_2 ('2', (void*) aux->a_un.a_val); return 0; } 

正如你将看到我的系统上的/usr/include/asm/unistd_32.h的下面的代码片段所示:

 #define __NR_restart_syscall 0 #define __NR_exit 1 #define __NR_fork 2 #define __NR_read 3 #define __NR_write 4 #define __NR_open 5 #define __NR_close 6 

我使用的系统调用是在eax寄存器中传递的编号为4(写入)的系统调用。 以filedescriptor(ebx = 1),数据指针(ecx =&c)和大小(edx = 1)作为参数,每个都传入相应的寄存器。

长话短说

比较一个据称缓慢运行int 0x80系统调用任何英特尔CPU与(希望)更快的实现使用(真正由AMD发明的) syscall指令比较苹果橙子。

恕我直言:最有可能的sysenter指令,而不是int 0x80应该在这里测试。

调用内核时需要做三件事情(进行系统调用):1.系统从“用户模式”进入“内核模式”(响铃0)。 2.堆栈从“用户模式”切换到“内核模式”。 3.跳转到内核的合适部分。

很显然,一旦进入内核,内核代码就需要知道你真正想要内核做什么,因此把东西放到EAX中,而且在其他寄存器中往往会放些更多的东西,因为有些东西就像“你想打开的文件的名字“或者”从文件中读取数据的缓冲区“等。

不同的处理器有不同的方法来实现上述三个步骤。 在x86中,有几个选择,但最流行的两个是int 0xnnsyscall (还有sysenter

x86-64体系结构中引入了syscall指令,作为进入系统调用的更快方式。 它有一组寄存器(使用x86 MSR机制),它们包含我们希望跳转到的EIP / RIP的地址,加载到CS和SS中用于执行Ring3到Ring0转换的值以及堆栈指针值。 它还将返回地址存储在ECX / RCX中。 [请阅读指令手册,了解本说明的所有细节 – 这不完全是微不足道的! 由于处理器知道这将切换到Ring0,它可以直接做正确的事情。

当使用SYSRET指令返回时,值从寄存器中的预定值恢复,因此处理器只需设置几个寄存器即可。 处理器知道它会从Ring0变成Ring3,所以可以快速做正确的事情。

在32位模式下使用的int 0x80变量将根据中断描述符表中的值决定如何处理,这意味着从内存中读取数据。 在那里它找到了新的CS和EIP / RIP值。 新的CS寄存器决定了新的“振铃”电平 – 在这种情况下是Ring0。 然后,它将使用新的CS值查看任务状态段(基于TR寄存器)以找出哪个堆栈指针(ESP / RSP和SS),然后最后跳转到新地址。 由于这是一个不那么直接和更通用的解决方案,所以也比较慢。 旧的EIP / RIP和CS被存储在新堆栈上,以及SS和ESP / RSP的旧值。

当使用IRET指令返回时,处理器从堆栈中读取返回地址和堆栈指针值,并从堆栈中加载新的堆栈段和代码段值。 同样,这个过程是通用的,并且需要相当多的内存读取。 由于它是通用的,处理器也必须检查“我们是否正在从Ring0改变模式到Ring3,如果改变了这些东西”。

所以总的来说,速度更快,因为它本来就是要这样工作的。

对于32位代码,是的,你肯定可以使用int 0x80 。 对于64位代码,我不认为它可行,但我不确定。 我确定这样做没有什么意义 – 为什么你想让它变慢? 无论如何,您将需要更改代码,所以我在64位代码片段中使用int 0x80时看不到任何意义。