Intel x86 vs x64系统调用

bec*_*cks 26 linux x86 assembly x86-64 interrupt

我正在阅读x86和x64之间的汇编差异.

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

但是在x64上,系统调用号被放入rax,然后syscall被执行.

我被告知,syscall它比生成软件中断更轻,更快.

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

mik*_*yra 30

一般部分

编辑:删除了Linux无关的部分

虽然并非完全错误,但与sysenter相比,缩小范围int 0x80syscall过度简化问题,至少有第三种选择.

使用0x80和eax作为sysenter数字,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内核使用的是int指令.

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

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

使用这种解决方案的一个缺点是它并不存在于所有32位机器上,因此sysenter在CPU不知道它的情况下仍然必须提供该方法.

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

系统调用

最后一种可能性,int 0x80指令,几乎允许与syscall指令相同的功能.两者的存在是由于一个(sysenter)由英特尔引入而另一个(systenter)由AMD引入.

特定于Linux

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

如上所述,该syscall方法是3个所选实现中唯一的一个,可以在任何i386 CPU上运行,因此这是唯一一个始终可用的方法.

为了允许在所有3个选项之间切换,每个进程运行都可以访问一个特殊的共享对象,该对象可以访问为正在运行的系统选择的系统调用实现.这是int 0x80您在使用syscall等时可能遇到的未解析库的奇怪外观.

(拱/ 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;                                           
    }   
Run Code Online (Sandbox Code Playgroud)

要使用它,您只需要在eax中加载所有寄存器系统调用号,在ebx,ecx,edx,esi,edi中加载与CONFIG_IA32_EMULATION系统调用实现和int 0x80主程序一样的参数.

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

每个进程的个人地址一旦启动就会传递给它.

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

除此之外,第三块所谓的精灵辅助矢量也按照前面提到的那样传递.正确的位置被编码在携带类型标识符的其中一个中ldd.

所以堆栈布局如下所示:

  • 参数-0
  • ...
  • 参数-m的
  • 空值
  • 环境-0
  • ....
  • 环境-N
  • 空值
  • ...
  • 辅助精灵矢量: int 0x80
  • ...
  • 辅助精灵矢量: call

用法示例

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

#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;
}
Run Code Online (Sandbox Code Playgroud)

正如您将看到AT_SYSINFO我的系统上的以下片段:

#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
Run Code Online (Sandbox Code Playgroud)

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

长话短说

比较任何英特尔CPU AT_SYSINFO上所谓的慢速运行的系统调用与使用(真正由AMD真正发明)指令(希望)快得多的实现,将苹果与橙子进行比较.AT_NULL

恕我直言:很可能是AT_SYSINFO指令而不是/usr/include/asm/unistd_32.h应该在这里进行测试.

  • OP已包含'linux'标记,因此有关DOS使用的'int 21h'的讨论可能无关紧要。此外,问题根本不是关于如何在不同的操作系统中实现系统调用的不同方式,而是关于Linux中的“ int 80h”和“ syscall”以及它们在x86 / x86-64上的区别。 (2认同)

Mat*_*son 17

当你调用内核(进行系统调用)时,有三件事需要发生:1.系统从"用户模式"变为"内核模式"(环0).2.堆栈从"用户模式"切换到"内核模式".3.跳转到内核的适当部分.

显然,一旦进入内核,内核代码将需要知道你真正想要内核做什么,因此在EAX中添加一些东西,而在其他寄存器中往往会有更多东西,因为有些东西比如"你要打开的文件的名称" "或"缓冲区将文件中的数据读入"等等"

不同的处理器有不同的方式来实现上述三个步骤.在x86中,有几种选择,但最受欢迎的两种是int 0xnnsyscall(也有sysenter)

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

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

int 0x80在32位模式下使用的变体将根据中断描述符表中的值决定做什么,这意味着从内存中读取.在那里它找到新的CS和EIP/RIP值.新的CS寄存器确定新的"振铃"电平 - 在这种情况下为Ring0.然后,它将使用新的CS值查看任务状态段(基于TR寄存器)以找出哪个堆栈指针(ESP/RSP和SS),然后最终跳转到新地址.由于这是一种不太直接且更通用的解决方案,因此速度也较慢.旧的EIP/RIP和CS与旧的SS和ESP/RSP值一起存储在新堆栈中.

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

因此,总而言之,它更快,因为它意味着以这种方式工作.

对于32位代码,是的,你绝对可以使用int 0x80.对于64位代码,我认为它不起作用,但我不确定.我确信这样做没什么意义 - 你为什么要让它变慢?您无论如何都需要更改代码,所以我在使用syscall64位代码时看不到任何意义.

  • 这个答案肯定是回答“为什么int 0x80为什么比sysenter / syscall慢”的答案更准确。+1 (2认同)