Linux如何确定要通过syscall执行的另一个进程的地址?像这个例子一样?
mov rax, 59
mov rdi, progName
syscall
澄清一下,我的问题似乎有点混乱,我要问的是syscall如何工作,与传递的寄存器或参数无关。当调用另一个进程时,如何知道跳转,返回等。
答案 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
是返回地址。
还对CS
和SS
段进行了调整,因此您的内核代码将能够在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.S中call *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二进制文件中定义。实际上,只需要确定一个地址并将其保存在任务当前的swapgs
指针中,然后“返回”用户空间即可。正常的需求分页机制将处理其余的工作:如果尚未加载代码,它将生成#PF页面错误异常,并且内核将在此时加载必要的代码。尽管在大多数情况下,加载程序已经将软件的某些部分作为优化程序进行加载,以避免出现初始页面错误。
(页面上未映射的#PF会导致内核向您的进程传递SIGSEGV segfault信号,但内核会静默处理“有效”页面错误。)
所有新进程通常都加载到相同的虚拟地址(忽略PIE + ASLR)。这是可能的,因为我们使用MMU(内存管理单元)。该协处理器在虚拟地址空间和物理地址空间之间转换内存地址。
(编者注:MMU并不是真正的协处理器;在现代CPU中,虚拟内存逻辑与L1指令/数据高速缓存紧密地集成在每个内核中。不过,某些古老的CPU确实使用了外部MMU芯片。 )
因此,现在我们知道所有进程都具有相同的虚拟地址(在Linux下,RIP
选择的默认地址为0x400000)。为了确定真实的物理地址,我们使用MMU。内核如何确定该物理地址?好吧,它具有内存分配功能。这么简单。
它调用“ malloc()”类型的函数,该函数搜索当前不使用的内存块并在该位置创建(也称为加载)进程。如果当前没有可用的内存块,则内核会检查是否有某些内容从内存中交换出来。如果失败,则流程创建失败。
在创建进程的情况下,它将开始分配相当大的内存块。分配1Mb或2Mb缓冲区以启动新进程并不常见。这样可以使处理速度更快。
此外,如果该进程已经在运行并且您再次启动它,那么可以重新使用已经在运行的实例使用的大量内存。在那种情况下,内核不会分配/加载那些部分。它将使用MMU共享那些可以在流程的两个实例中通用的页面(即,在大多数情况下,由于它是只读的,因此可以共享流程的代码部分,可以在某些时候共享数据的某些部分)它也被标记为只读;如果未标记为只读,则即使尚未修改数据,仍可以共享数据-在这种情况下,它被标记为写入时复制。)< / p>