assembly 系统调用如何知道跳转到哪里?[closed]

lyfkaqu1  于 2022-11-30  发布在  其他
关注(0)|答案(1)|浏览(136)

已关闭。此问题需要更多focused。当前不接受答案。
**想要改进此问题吗?**更新问题,使其仅关注editing this post的一个问题。

三年前就关门了。
Improve this question
Linux如何通过系统调用确定另一个进程的地址?就像这个例子一样?

mov rax, 59 
mov rdi, progName
syscall

看起来我的问题有点混乱,澄清一下,我想问的是系统调用是如何工作的,独立于寄存器或传递的参数。当调用另一个进程时,它如何知道跳转到哪里,返回到哪里等等。

ubof19bj

ubof19bj1#

系统调用

syscall指令实际上只是一条INTEL/AMD CPU指令。

IF (CS.L ≠ 1 ) or (IA32_EFER.LMA ≠ 1) or (IA32_EFER.SCE ≠ 1)
(* Not in 64-Bit Mode or SYSCALL/SYSRET not enabled in IA32_EFER *)
    THEN #UD;
FI;
RCX ← RIP; (* Will contain address of next instruction *)
RIP ← IA32_LSTAR;
R11 ← RFLAGS;
RFLAGS ← RFLAGS AND NOT(IA32_FMASK);
CS.Selector ← IA32_STAR[47:32] AND FFFCH (* Operating system provides CS; RPL forced to 0 *)
(* Set rest of CS to a fixed value *)
CS.Base ← 0;
        (* Flat segment *)
CS.Limit ← FFFFFH;
        (* With 4-KByte granularity, implies a 4-GByte limit *)
CS.Type ← 11;
        (* Execute/read code, accessed *)
CS.S ← 1;
CS.DPL ← 0;
CS.P ← 1;
CS.L ← 1;
        (* Entry is to 64-bit mode *)
CS.D ← 0;
        (* Required if CS.L = 1 *)
CS.G ← 1;
        (* 4-KByte granularity *)
CPL ← 0;
SS.Selector ← IA32_STAR[47:32] + 8;
        (* SS just above CS *)
(* Set rest of SS to a fixed value *)
SS.Base ← 0;
        (* Flat segment *)
SS.Limit ← FFFFFH;
        (* With 4-KByte granularity, implies a 4-GByte limit *)
SS.Type ← 3;
        (* Read/write data, accessed *)
SS.S ← 1;
SS.DPL ← 0;
SS.P ← 1;
SS.B ← 1;
        (* 32-bit stack segment *)
SS.G ← 1;
        (* 4-KByte granularity *)

最重要的部分是保存和管理RIP寄存器的两条指令:

RCX ← RIP
RIP ← IA32_LSTAR

换句话说,IA32_LSTAR(一个寄存器)中保存的地址必须有代码,RCX是返回地址。
CSSS段也进行了调整,以便内核代码能够进一步在CPU级别0(特权级别)上运行。
如果您没有执行syscall的权限或指令不存在,则可能会发生#UD

如何解释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在实践中更加复杂,因为有一些x86漏洞(如Meltdown和L1 TF)的变通办法,通过更改页表,在用户空间运行时,大部分内核内存不会被Map。(来自AT&T语法)在Linux 4.12 arch/x86/entry/entry_64.S(在添加Spectre/Meltdown缓解措施之前)中从ENTRY(entry_SYSCALL_64)转换为call *sys_call_table(, %rax, 8)What happens if you use the 32-bit int 0x80 Linux ABI in 64-bit code?提供了关于系统调用调度的内核端的更多详细信息。
快?
该指令被称为 fast。这是因为在过去,必须使用INT3这样的指令。中断使用内核堆栈,它将许多寄存器压入堆栈,并使用相当慢的RTE来退出异常状态并返回中断后的地址。这通常要慢得多。
有了syscall,你也许可以避免大部分的开销。但是,在你所要求的方面,这并没有真正的帮助。
swapgs是另一条与syscall一起使用的指令。它为内核提供了一种访问自己的数据和堆栈的方法。您应该查看Intel/AMD文档中有关这些指令的详细信息。

新流程?

Linux系统有一个所谓的任务表,每个进程和进程内的每个线程实际上都被称为任务。
当你创建一个新的进程时,Linux会创建一个任务。为了使它工作,它会运行一些代码,这些代码会做如下事情:

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

当然,这是超级简化的。
起始地址是在ELF二进制文件中定义的。它实际上只需要确定一个地址,并将其保存在任务的当前RIP指针中,然后“返回”到用户空间。正常的请求分页机制将处理其余的工作:如果代码尚未加载,它将生成#PF页面错误异常,内核将在此时加载必要的代码。尽管在大多数情况下,加载程序已经加载了软件的某个部分,作为避免初始页面错误的优化。
(未Map的页面上的#PF将导致内核向进程发送SIGSEGV segfault信号,但“有效”页面错误由内核以静默方式处理。)
所有的新进程通常都加载到同一个虚拟地址(忽略PIE + ASLR)。这是可能的,因为我们使用了MMU(内存管理单元)。协处理器在虚拟地址空间和物理地址空间之间转换内存地址。
(编者注:MMU实际上不是协处理器;在现代CPU中,虚拟内存逻辑沿着L1指令/数据缓存一起紧密集成到每个内核中。不过,一些古老的CPU确实使用了外部MMU芯片。)

确定地址?

所以,现在我们知道所有进程都有相同的虚拟地址(0x 400000在Linux下是ld的默认值)。为了确定真实的的物理地址,我们使用MMU。内核如何确定物理地址?它有一个内存分配函数。就这么简单。
它会调用一个malloc()函数,该函数会搜索当前未使用的内存块,并在该位置创建(也称为加载)进程。如果当前没有可用的内存块,内核会检查是否有内存块被换出。如果换出失败,则进程的创建失败。
在创建一个进程的情况下,它会分配相当大的内存块来启动。分配1 Mb或2 Mb的缓冲区来启动一个新进程是很常见的。这会使事情进行得更快。

同样,如果进程已经在运行,并且您再次启动它,则可以重用已经运行的示例所使用的大量内存。在这种情况下,内核不会分配/加载这些部分。它将使用MMU来共享那些可以为进程的两个示例所共用的页面(即,在大多数情况下,进程的代码部分可以被共享,因为它是只读的,当数据的某个部分也被标记为只读时,它可以被共享;如果未将数据标记为只读,则在数据尚未修改的情况下仍然可以共享数据--在本例中,它被标记为 copy on write。)

相关问题