syscall如何知道要跳转到哪里?

时间:2019-07-02 14:04:33

标签: linux assembly x86-64 nasm system-calls

Linux如何确定要通过syscall执行的另一个进程的地址?像这个例子一样?

mov rax, 59 
mov rdi, progName
syscall

澄清一下,我的问题似乎有点混乱,我要问的是syscall如何工作,与传递的寄存器或参数无关。当调用另一个进程时,如何知道跳转,返回等。

1 个答案:

答案 0 :(得分:2)

系统调用

syscall指令实际上只是一个INTEL / AMD CPU指令。这是简介:

IF (CS.L ≠ 1 ) or (IA32_EFER.LMA ≠ 1) or (IA32_EFER.SCE ≠ 1)
  THEN #UD;
FI;
RCX ← RIP;
RIP ← IA32_LSTAR;
R11 ← RFLAGS;
RFLAGS ← RFLAGS AND NOT(IA32_FMASK);
CS.Selector ← IA32_STAR[47:32] AND FFFCH
CS.Base ← 0;
CS.Limit ← FFFFFH;
CS.Type ← 11;
CS.S ← 1;
CS.DPL ← 0;
CS.P ← 1;
CS.L ← 1;
CS.D ← 0;
CS.G ← 1;
CPL ← 0;
SS.Selector ← IA32_STAR[47:32] + 8;
SS.Base ← 0;
SS.Limit ← FFFFFH;
SS.Type ← 3;
SS.S ← 1;
SS.DPL ← 0;
SS.P ← 1;
SS.B ← 1;
SS.G ← 1;

最重要的部分是保存和设置RIP寄存器的两条指令。

RCX ← RIP
RIP ← IA32_LSTAR

换句话说,IA32_LSTAR(寄存器)中保存的地址必须有代码,而RCX是返回地址。

还对CSSS段进行了调整,因此您的内核代码将能够在CPU级别0(特权级别)上进一步运行。

如果您无权执行#UD或该指令不存在,则syscall可能会发生。

RAX的解释如何?

这只是内核函数指针表的索引。首先,内核进行边界检查(如果RAX > __NR_syscall_max,则返回-ENOSYS),然后分派给(C语法)sys_call_table[rax](rdi, rsi, rdx, r10, r8, r9);

; Intel-syntax translation of Linux 4.12 syscall entry point
       ...                 ; save user-space registers etc.
    call   [sys_call_table + rax * 8]       ; dispatch to sys_execve() or whatever kernel C function

;;; execve probably won't return via this path, but most other calls will
       ...                 ; restore registers except RAX return value, and return to user-space

现代Linux在实践中更加复杂,因为通过更改页表来解决诸如Meltdown和L1TF之类的x86漏洞,因此在运行用户空间时不会映射大多数内核内存。上面的代码是Linux 4.12 arch/x86/entry/entry_64.Scall *sys_call_table(, %rax, 8)的{​​{1}}的字面翻译(来自AT&T语法)(在添加Spectre / Meltdown缓解措施之前)。也相关:What happens if you use the 32-bit int 0x80 Linux ABI in 64-bit code?提供了有关系统调用分派的内核方面的更多详细信息。

快速?

该指令被称为 fast 。这是因为在过去,人们将不得不使用诸如ENTRY(entry_SYSCALL_64)之类的指令。中断利用内核堆栈,它将堆栈中的许多寄存器压入堆栈,并使用速度较慢的INT3退出异常状态并在中断后立即返回地址。通常这要慢得多。

使用RTE,您可以避免大部分开销。但是,按照您的要求,这并没有帮助。

syscall一起使用的另一条指令是syscall。这为内核提供了一种访问其自身数据和堆栈的方法。您应该查看有关这些说明的英特尔/ AMD文档,以了解更多详细信息。

新流程?

Linux系统具有所谓的任务表。每个进程和一个进程中的每个线程实际上都称为任务。

在创建新进程时,Linux将创建一个任务。为此,它将运行执行以下操作的代码:

  • 确保可执行文件存在
  • 设置新任务(包括从该可执行文件中解析ELF程序标头,以在新创建的虚拟地址空间中创建内存映射。)
  • 分配堆栈缓冲区
  • 加载可执行文件的前几个块(作为按需分页的优化),为要映射的虚拟页面分配一些物理页面。
  • 在任务中设置起始地址(可执行文件中的ELF入口点)
  • 将任务标记为就绪(也可以运行)

这当然是超级简化。

起始地址在您的ELF二进制文件中定义。实际上,只需要确定一个地址并将其保存在任务当前的swapgs指针中,然后“返回”用户空间即可。正常的需求分页机制将处理其余的工作:如果尚未加载代码,它将生成#PF页面错误异常,并且内核将在此时加载必要的代码。尽管在大多数情况下,加载程序已经将软件的某些部分作为优化程序进行加载,以避免出现初始页面错误。

(页面上未映射的#PF会导致内核向您的进程传递SIGSEGV segfault信号,但内核会静默处理“有效”页面错误。)

所有新进程通常都加载到相同的虚拟地址(忽略PIE + ASLR)。这是可能的,因为我们使用MMU(内存管理单元)。该协处理器在虚拟地址空间和物理地址空间之间转换内存地址。

(编者注:MMU并不是真正的协处理器;在现代CPU中,虚拟内存逻辑与L1指令/数据高速缓存紧密地集成在每个内核中。不过,某些古老的CPU确实使用了外部MMU芯片。 )

确定地址?

因此,现在我们知道所有进程都具有相同的虚拟地址(在Linux下,RIP选择的默认地址为0x400000)。为了确定真实的物理地址,我们使用MMU。内核如何确定该物理地址?好吧,它具有内存分配功能。这么简单。

它调用“ malloc()”类型的函数,该函数搜索当前不使用的内存块并在该位置创建(也称为加载)进程。如果当前没有可用的内存块,则内核会检查是否有某些内容从内存中交换出来。如果失败,则流程创建失败。

在创建进程的情况下,它将开始分配相当大的内存块。分配1Mb或2Mb缓冲区以启动新进程并不常见。这样可以使处理速度更快。

此外,如果该进程已经在运行并且您再次启动它,那么可以重新使用已经在运行的实例使用的大量内存。在那种情况下,内核不会分配/加载那些部分。它将使用MMU共享那些可以在流程的两个实例中通用的页面(即,在大多数情况下,由于它是只读的,因此可以共享流程的代码部分,可以在某些时候共享数据的某些部分)它也被标记为只读;如果未标记为只读,则即使尚未修改数据,仍可以共享数据-在这种情况下,它被标记为写入时复制。)< / p>