一尘不染

Intel x86 vs x64系统调用

linux

我正在阅读有关x86和x64之间的汇编差异的信息。

在x86上,系统调用号码放在中eax,然后int 80h执行以生成软件中断。

但是在x64上,系统调用号码放在中rax,然后syscall执行。

有人告诉我,这syscall比生成软件中断更轻松,更快捷。

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


阅读 559

收藏
2020-06-07

共1个答案

一尘不染

一般部分

编辑:Linux无关部分已删除

虽然并非完全错误,但缩小问题范围int 0x80syscall简化了问题,因为sysenter至少存在第三种选择。

使用0x80和eax作为系统调用编号,ebx,ecx,edx,esi,edi和ebp传递参数只是实现系统调用的许多其他选择之一,但是这些寄存器是32位Linux
ABI选择的寄存器。

在仔细研究所涉及的技术之前,应该指出,它们都绕过了逃避每个进程运行的特权监狱的问题。

x86架构在此提供的选择的另一个选择是使用呼叫门(请参阅:http :
//en.wikipedia.org/wiki/Call_gate)

所有i386机器上存在的唯一其他可能性是使用软件中断,该中断允许ISR( 中断服务程序 或简称为 中断处理程序 )以与以前不同的特权级别运行。

(有趣的事实:某些i386操作系统使用无效指令异常进入系统调用内核,因为它实际上比int386 CPU上的指令快。请参见[OsDev syscall /sysret和sysenter /sysexit指令],以获取可能的摘要。系统调用机制。)

软件中断

触发中断后究竟会发生什么,取决于切换到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位计算机上都存在该解决方案,因此int 0x80,如果CPU不知道该方法,则仍然必须提供该方法。

奔腾II处理器的IA-32架构中引入了SYSENTER和SYSEXIT指令。这些指令在处理器上的可用性由CPUID指令返回给EDX寄存器的SYSENTER
/ SYSEXIT存在(SEP)功能标志指示。合格SEP标志的操作系统还必须合格处理器系列和型号,以确保实际存在SYSENTER / SYSEXIT指令

系统调用

最后一种可能性,syscall指令几乎允许与sysenter指令相同的功能。两者之所以存在,是因为一个(systenter)是由Intel引入的,而另一个(syscall)是AMD引入的。

特定于Linux

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

另请参见 《 Linux系统调用权威指南》

如上所述,该int 0x80方法是可以在任何i386 CPU上运行的3种选择的实现中的唯一方法,因此这是唯一始终可用于32位用户空间的方法。

syscall是唯一可始终用于64位用户空间的内核,并且是您应在64位代码中使用的唯一内核;
x86-64内核可以不使用构建CONFIG_IA32_EMULATION,并且int 0x80仍调用截断指针的32位ABI。到32位。)

为了允许在所有3个选项之间进行切换,每个运行的进程都可以访问一个特殊的共享对象,该共享对象可以访问为正在运行的系统选择的系统调用实现。这是linux-gate.so.1您使用ldd或类似方法时作为未解析库可能已经遇到的奇怪外观。

(arch / x86 / 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

因此堆栈布局如下所示(地址向下增长):

  • parameter-0
  • parameter-m
  • NULL
  • environment-0
  • ....
  • environment-n
  • NULL
  • auxilliary elf vector: AT_SYSINFO
  • auxilliary elf vector: 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)和size(edx
= 1)作为其参数,每个参数都传递到相应的寄存器中。

简而言之

使用(由AMD发明的)指令将(应该是)更快的实现与_任何_英特尔CPUint0x80上运行缓慢的系统调用进行比较(希望是将苹果与橙子进行比较)。
__syscall

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

2020-06-07