当前位置: 首页 > 面试题库 >

Intel x86 vs x64系统调用

邹宏峻
2023-03-14
问题内容

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

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

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

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

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


问题答案:

一般部分

编辑: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应该在这里进行测试。



 类似资料:
  • 本章描述 Linux 内核中的系统调用概念。 系统调用概念简介 - 介绍 Linux 内核中的系统调用概念 Linux 内核如何处理系统调用 - 介绍 Linux 内核如何处理来自于用户空间应用的系统调用。 vsyscall and vDSO - 介绍 vsyscall 和 vDSO 概念。 Linux 内核如何运行程序 - 介绍一个程序的启动过程。 open 系统调用的实现 - 介绍 open

  • 系统调用sendfile Sendfile是Linux实现的系统调用,可以通过避免文件在内核态和用户态的拷贝来优化文件传输的效率。 其中大名鼎鼎的分布式消息队列服务Kafka就使用sendfile来优化效率,具体用法可参见其官方文档。 优化策略 在普通进程中,要从磁盘拷贝数据到网络,其实是需要通过系统调用,进程也会反复在用户态和内核态切换,频繁的数据传输在此有效率问题。因此我们必须意识到Linux

  • 系统调用 我们要想启动一个进程,需要操作系统的调用(system call)。实际上操作系统和普通进程是运行在不同空间上的,操作系统进程运行在内核态(todo: kernel space),开发者运行的进程运行在用户态(todo: user space),这样有效规避了用户程序破坏系统的可能。 如果用户态进程想执行内核态的操作,只能通过系统调用了。Linux提供了超多系统调用函数,我们关注与进程相

  • 11.3.1 默认的调用规范 通常, FreeBSD 的内核使用 C 语言的调用规范。 此外, 虽然我们使用 int 80h 来访问内核, 但是我们常常通过调用一个函数来执行 int 80h, 而不是直接访问。 这个规范是非常方便的, 比 Microsoft® 的 MS-DOS® 上使用的规范更加优越。 为什么呢? 因为 UNIX® 的规范允许任何语言所写的程序访问内核。 汇编语言也可以这样做,

  • curl 和 curl -I 可以被轻松地应用于 web 调试中,它们的好兄弟 wget 也是如此,或者也可以试试更潮的 httpie。 获取 CPU 和硬盘的使用状态,通常使用使用 top(htop 更佳),iostat 和 iotop。而 iostat -mxz 15 可以让你获悉 CPU 和每个硬盘分区的基本信息和性能表现。 使用 netstat 和 ss 查看网络连接的细节。 dstat

  • 问题内容: 我正在开发一个需要与Video4Linux抽象交互的应用程序。该应用程序使用mono框架以C#开发。 我面临的问题是我无法P /调用系统调用。或者,更准确地说,我可以P /调用它,但是它崩溃严重。 extern声明如下: 到目前为止,一切都很好。 使用的实际例程如下: 以上所有代码似乎都不错。该类用于按照标头规范计算I / O请求代码(基本上,它遵循处声明的宏)。 该参数是一个结构,声

  • 当想知道一个进程在做什么事情的时候,可以通过strace命令跟踪一个进程的所有系统调用。 1、运行 php start.php status 能看到workerman相关进程的信息 如下: Hello admin ---------------------------------------GLOBAL STATUS-----------------------------------------

  • 我刚刚开始研究系统调用。我想知道当进行系统调用时是什么导致了开销。 例如,如果我们考虑getpid(),当系统调用getpid()时,我的猜测是,如果控件当前位于子进程中,则必须进行上下文切换才能进入父进程以获取pid。这会导致间接费用吗? 此外,当调用getpid()时,会有一些元数据跨用户空间边界传输,并进入和退出内核。那么,用户空间和内核之间的不断切换也会导致一些开销吗?