Linux系统调用指南

2023-11-11

Linux系统调用指南

文章是转载,但是我在后面的案例加了不少注解并debug了,如有疑问,留言交流 。(其实我也不懂)
原文链接: blog.packagecloud.io
https://zcfy.cc/article/the-definitive-guide-to-linux-system-calls-670.html?t=new
这篇blog解释linux程序如何调用linux内核函数。
这篇文章概述不同的几个做系统调用的方法,如何自己写系统调用(包含例子),系统调用的内核入口,内核出口,glibc封装器,bugs等等。

什么是系统调用

当你运行的程序调用了open, fork, read, write等等,你就做了系统调用 。
系统调用就是程序如何进入内核执行任务。程序使用系统调用执行一系列的操作诸如:创建进程,网络和文件IO等等。
你可以在man page for syscalls(2)里面看到系统调用的列表。 用户程序做系统调用有不同的方法,CPU架构不同做系统调用的底层指令也不同。

作为应用开发者,你不需要经常思考系统调用如何正确执行。你只需要把头文件引入,然后像普通功能一样调用。

glibc作为装饰器,抽象组装你传递的参数然后进入内核的细节。
在我们详细研究系统调用如何实现之前,需要定义一些后面将会出现的条款和核心概念。

前提信息

Hardware and software

硬件和软件

本文做如下假设:

  • 你使用的是Intel或者AMD的32位或者64位CPU。本文讨论的方法可能对其他系统也有用,但是例子中的代码包含一些CPU专用代码。

  • 你对3.13.0版本的Linux内核感兴趣。其他版本内核是相似的,但是代码准确的行数,代码的组织和文件路径是不一样的。建议从GitHub上链接3.13.0版本内核源码树。

  • 你对glibc或者由glibc得到的libc实现感兴趣。 本文所指的x86-64是基于x86架构的64位Intel和AMDCPU。

User programs, the kernel, and CPU privilege levels

用户程序,内核,CPU权限等级

  • 用户程序(比如编辑器,终端,ssh守护程序等等)需要和linux内核交互,所以有些用户程序无法自己执行的行为可以调用内核执行。比如,如果用户程序需要做IO操作(open, read, write等等)或者修改自己地址空间(mmap, sbrk等等),必须触发内核运行来完成这些操作行为。

是什么阻止用户程序自己执行这些操作?

原来是x86-64的CPU有一个权限等级概念。权限等级是个复杂的题目适合单独一片博客来阐述。在这片博客中,我们简单地把权限等级概念解释为:

1. 权限等级意味着访问控制。当前权限等级决定了那些CPU指令和IO操作可以执行。

2. 内核运行在最高权限等级,叫做“Ring 0”。用户程序运行在较低等级,叫做“Ring 3”。

用户程序为了要执行某些高权限操作,必须修改权限等级(从“Ring 3”到“Ring 0”),所以由内核执行。

这里有一些方法可以改变权限等级,触发内核执行操作。

先介绍一个内核调用的普通方法:中断

Interrupts

中断
你可以认为中断时由硬件或者软件产生的事件。
一个硬件中断时由硬件设备产生的通知内核有特殊事件发生了。这种中断较常见的例子是网卡收到包产生的中断。

**一个软件中断是执行某条代码的时候产生的。**在x86-64系统中,执行int指令可以产生一个软件中断。

中断一般有一个分配的中断号。有些中断号有特殊意义。

你可以想象CPU存储器中有一个数组。数组中的每一个条目都指向一个中断号。每个条目都包含一个函数的入口地址,当某个操作产生中断的时候,CPU可以通过入口地址执行这个函数。

Intel CPU指南里面这张图展示了数组中各个条目的布局:
在这里插入图片描述
Screenshot of Interrupt Descriptor Table entry diagram for x86_64 CPUs

如果你仔细看这个图,会发现2bit字段DPL(Descriptor Privilege Level)。这个字段的值决定了CPU执行程序的权限等级。
这就CPU是如何知道需要执行哪个地址的指令以及这个指令的权限等级(当一个特殊类型事件发生的时候)。

实际上x86-64系统有很多种方法可以处理中断。如果你对这方面感兴趣可以读8259 Programmable Interrupt Controller, Advanced Interrupt Controllers, 和 IO Advanced Interrupt Controllers.

处理硬件/软件中断还要处理一些其它复杂的事情,比如中断号冲突和重映射。

讨论系统调用的时候我们不需要关心这些细节。

Model Specific Registers (MSRs)
特殊模块寄存器

  • 特殊模块寄存器(MSRs)是以提供CPU的某些控制功能为目的的寄存器。CPU文档列出了这些MSRs地址。

你可以分别使用rdmsr 和 wrmsr来读写MSRs。

也有命令行工具可以读写MSRs,但是不推荐因为改变MSRs值是危险的(特别是当操作系统正在运行的时候),除非你真的很小心。

如果你不介意系统的崩溃或者数据的不可逆失效风险,可以安装msr-tools然后加载msr内核模块来读写MSRs。 (干活前,打个快照!!!)

% sudo apt-get install msr-tools
% sudo modprobe msr 
% sudo rdmsr  

稍后我们将会看到一些系统调用使用MSRs。

Calling system calls with assembly is a bad idea

用汇编做系统调用是坏主意

自己写汇编代码执行系统调用不是个好办法。
其中的一个原因是在有些系统调用前/调用后,glibc要执行一些额外的代码。
下面的例子我们会使用 exit 系统调用。使用 atexit 注册函数,当程序调用 exit 时就会执行你注册的函数。

那些代码是通过glibc调用的而不是内核。所以,如果你写汇编语言像下面那样执行exit,你注册的函数不会被执行因为绕过了glibc。
然而,用汇编语言做系统调用有利于学习经验。

传统系统调用

有两个需要预先准备的知识:

  1. 我们可以通过生成软中断触发内核调用。

  2. 我们可以用汇编指令int生成软中断。

结合这两个概念让我们看Linux传统系统调用接口。
用户空间程序可以取到Linux内核软中断号,这样就可以进入内核和执行系统调用。
Linux内核给128(0x80)中断注册了名为 ia32_syscall 的中断执行程序。让我们看看具体做这件事的代码。

内核3.13.0,arch/x86/kernel/traps.c源码中的trap_init函数

void __init trap_init(void)  {       
    /* ..... other code ... */          
    set_system_intr_gate(IA32_SYSCALL_VECTOR, ia32_syscall);  
IA32_SYSCALL_VECTOR在arch/x86/include/asm/irq_vectors.h中定义的,值是0x80。

但是,虽然用户空间程序可以发出内核系统存储的软中断信号来触发内核,内核如何知道需要执行哪个系统调用?

用户空间程序会把系统调用号放在eax寄存器中,系统调用的参数放在其它通用寄存器中。

arch/x86/ia32/ia32entry.S注释中有它的说明。

* Emulated IA32 system calls via int 0x80.  
* 
* Arguments:  
* %eax System call number.  
* %ebx Arg1  
* %ecx Arg2  
* %edx Arg3  
* %esi Arg4  
* %edi Arg5 
* %ebp Arg6     [note: not saved in the stack frame, should not be touched] 
*  

我们已经知道如何做系统调用和参数存在哪里,现在我们通过写内联汇编做一个系统调用。

Using legacy system calls with your own assembly

用汇编做传统系统调用

你可以写一小段内联汇编做传统系统调用。虽然以学习的观点来看这很有趣,但我还是建议读者永远不要手动写汇编函数做系统调用。

在这个例子中,我们试着做exit系统调用,这个调用有一个参数:退出状态。

首先,我们先找到exit的系统调用号。Linux内核包含一个文件,这个文件在一个表格中列出了各个系统调用。这个文件在构建阶段被不同的脚本加工然后生成可以被用户程序使用的头文件。

让我们看看这个在 arch/x86/syscalls/syscall_32.tbl 发现的表格:

1  i386 exit sys_exit

exit系统调用号是1。根据上面的接口描述,我们只需要把系统调用号放到eax寄存器,第一个参数(推出状态)放到ebx寄存器。

这里是一段含有内联汇编代码的C语言程序。我们把退出状态设置成“42”:

(这段代码可以被简化,但我想这样写可以让那些不知道GCC内联汇编的人理解和参考。)

int  
main(int argc, char *argv[])  
{   
    unsigned int syscall_nr = 1; 
    int exit_status = 42; 

    asm ("movl %0, %%eax\n" 
    "movl %1, %%ebx\n"  
    "int $0x80"     
    : /* output parameters, we aren't outputting anything, no none */ 
    /* (none) */     
    : /* input parameters mapped to %0 and %1, repsectively */ 
    "m" (syscall_nr), "m" (exit_status)     
    : /* registers that we are "clobbering", unneeded since we are calling exit */      
    "eax", "ebx");  
}  

下一步,编译,执行,然后检查退出状态:

$ gcc -o test test.c  
$ ./test  
$ echo $? 
42

成功了!我们使用传统系统调用方法通过发出软中断来执行exit。

Kernel-side: int $0x80 entry point

内核内部:int $0x80入口

我们已经看了用户空间程序如何触发系统调用,接下来看看内核如何使用系统调用号执行系统调用代码。

上一章我们提到,内核注册了一个系统调用执行函数叫做 ia32_syscall 。

这个函数是在 arch/x86/ia32/ia32entry.S 中用汇编实现的,

ia32_do_call:         
    IA32_ARG_FIXUP         
    call *ia32_sys_call_table(,%rax,8) # xxx: rip relative  

IA32_ARG_FIXUP 是一个宏,它重新排列了传统参数好让当前系统调用层恰当理解。

ia32_sys_call_table 标示符引用了 arch/x86/ia32/syscall_ia32.c 中定义的表格。注意代码结尾处的#include行。

const sys_call_ptr_t ia32_sys_call_table[__NR_ia32_syscall_max+1] = { 
    /*         
    * Smells like a compiler bug -- it doesn't work         
    * when the & below is removed.          
    */         
    [0 ... __NR_ia32_syscall_max] = &compat_ni_syscall,  
#include <asm/syscalls_32.h>  
}; 

回忆我们之前看到的在arch/x86/syscalls/syscall_32.tbl中定义的表格。
在这里插入图片描述
编译期有几个脚本会取这个表格并且生成syscalls_32.h文件。生成的头文件由有效的C语言组成,就是上面用#include插入的代码,它把根据系统调用号得到的函数地址索引写进ia32_sys_call_table。

这就是你如何通过传统系统调用进入内核的。

Returning from a legacy system call with iret

用iret从传统系统调用返回

我们已经看到用软中断如何进入内核,但是内核是如何返回用户空间的,并且内核结束执行之后如何丢弃权限等级的?

我们可以在这个文档(注意:大PDF)Intel Software Developer’s Manual 看到一副有用的图解说明了当权限级别改变的时候程序栈是如何安排的。

如图:
在这里插入图片描述
Screenshot of the Stack Usage on Transfers to Interrupt and Exception-Handling Routines

当用户程序触发软中断,程序转移到内核函数ia32_syscall的时候权限级别发生改变。结果就是当进入ia32_syscall 的时候程序栈就会想上面图例一样。

这就意味着返回地址,译成权限等级等的CPU标志,还有很多在ia32_syscall执行前都被保存在程序栈中。

所以,为了恢复执行,内核只需要把程序栈中的值拷贝回寄存器,这样程序又恢复到了用户空间。

好的,你该怎么做?
只有很少的方法可以做到,但是最简单的方法是用iret指令。

在Intel指令集手册的解释是:iret指令把返回地址和存贮的寄存器值从栈中压出。

随着实地址模式中断的返回,IRET指令从栈中分别弹出返回指令指针,返回代码段选择器,EFLAGS镜像到EIP, CS, 和 EFLAGS 寄存器,然后恢复执行被中断的程序或进程。

在内核里面找到这段代码有点困难,因为它隐藏在一些宏代码之下,并且处理信号和 ptrace 退出跟踪需要特别小心。

最终在内核中挖出的汇编语言宏代码揭露了iret如何从系统调用返回用户程序。

从 arch/x86/kernel/entry_64.S中的irq_return :

irq_return:   
INTERRUPT_RETURN  
INTERRUPT_RETURN在arch/x86/include/asm/irqflags.h中定义为iretq。

你现在知道了传统系统调用时如何工作了。

快速系统调用

传统方法看起来非常合理,但是现在有新的方法触发系统调用,不需要包含软中断并且比使用软中断要快得多。

两个快速方法都是由两个指令组成。一个进入内核,一个离开内核。两个方法都在Intel CPU文档中“快速系统调用”里介绍。

不幸的是,当CPU在32位或64位模式的时候,在哪个方法有效的问题上,Intel和AMD的实现是不一致的。

为了最大的兼容Intel和AMD的CPU:

在32位系统使用: sysenter 和 sysexit.
在64位系统使用: syscall 和 sysret.
32-bit fast system calls
32位快速系统调用

sysenter/sysexit

使用sysenter做系统调用比传统通断方法更复杂并且在用户空间和内核之间要做更多适配(通过glibc)。

我们一步一步的做并挑出其中的细节。首先我们看看Intel指令集参考文档(注意大文件PDF)对sysenter的介绍和怎么使用。

我们看一看:

执行SYSENTER指令前软件必须通过把值写入下面的MSRs中来指定权限等级0的代码段和代码入口,并且指定权限等级0的堆栈段和堆栈指针。

  • IA32_SYSENTER_CS (MSR address 174H) — MSR的低16位是权限等级0代码段的段选择器。这个值也用来决定权限等级0堆栈段的段选择器(见Operation章)。这个值不能指示一个空选择器。

  • IA32_SYSENTER_EIP (MSR address 176H) — MSR的这个值加载到RIP(这样,这个值就指向了被选择的操作程序或常规程序的第一条指令的地方)。在保护模式,只有31:0位会被加载。

  • IA32_SYSENTER_ESP (MSR address 175H) — MSR的这个值加载到RSP(这样,这个值包含了权限等级0栈的栈指针)。这个值不能表示一个不按规则的地址。在保护模式,只有31:0位会被加载。

换句话说:为了让内核收到 sysenter 系统调用,内核必须设置3个特殊模块寄存器(MSRs)。我们最需要关注的MSR是IA32_SYSENTER_EIP(含有0x176地址)。当用户程序执行sysenter指令,这个MSR就是内核指定的将要执行的程序的地址

我们可以在内核中arch/x86/vdso/vdso32-setup.c找到写MSR的代码:

void enable_sep_cpu(void)  
{ 
    /* ... other code ... */           
    wrmsr(MSR_IA32_SYSENTER_EIP, (unsigned long) ia32_sysenter_target, 0);  
MSR_IA32_SYSENTER_EIP在arch/x86/include/uapi/asm/msr-index.h中赋值为0x00000176。

就像传统软中断系统调用,这是用 sysenter 做系统调用的惯例。

arch/x86/ia32/ia32entry.S中的注释做了说明:

* 32bit SYSENTER instruction entry.  
*  
* Arguments:  
* %eax System call number.  
* %ebx Arg1  
* %ecx Arg2  
* %edx Arg3  
* %esi Arg4  
* %edi Arg5  
* %ebp user stack 
* 0(%ebp) Arg6      

回忆传统系统调用方法对返回被中断的用户空间程序有个一机制:iret指令。

理解让sysenter正确执行的逻辑是复杂的,因为不像软中断,sysenter没有保存返回地址。

确切的讲,在执行 sysenter 指令之前,内核存储地址和其它数据是如何实现的。(你将会在下面的Bugs那章看到它确实实现了)。

为了避免未来的变化,用户程序原打算调用__kernel_vsyscall函数,这个函数是内核实现的,但是在进程开始的时候映射到了各个用户进程。

这有点旧了;这段代码来自内核,却在用户空间执行。

__kernel_vsyscall 被证明是虚拟动态共享对象(vDSO)的一部分vDSO是用来让程序在用户空间执行内核代码。

接下来我们将会深度的检查vDSO是什么,有什么功能,和如何工作的。

我们开始检测__kernel_vsyscall的内部构件。

__kernel_vsyscall internals
__kernel_vsyscall 内部构件

__kernel_vsyscall 函数封装了sysenter调用惯例,可以在 arch/x86/vdso/vdso32/sysenter.S中看到:

__kernel_vsyscall:  
.LSTART_vsyscall:         
    push %ecx  
.Lpush_ecx:        
    push %edx  
.Lpush_edx:         
    push %ebp  
.Lenter_kernel:        
    movl %esp,%ebp         
    sysenter  

__kernel_vsyscall 是动态共享对象(也被叫做共享库)的一部分。用户程序在运行时如何找到动态共享函数的地址的?

__kernel_vsyscall 函数地址写在 ELF 辅助向量,这个向量在用户程序或者库(特别是glibc)可以找到和使用的地方。

有一些方法可以找到ELF辅助向量:

  1. 使用 getauxval ,参数是AT_SYSINFO
  2. 迭代环境变量,在内存中解析。

第一种方法最简单,但是 glibc 的2.16版本之后才有。下面例子的代码对第二种方法做了解释。

就像我们上面看到的代码,__kernel_vsyscall 在 sysenter 调用前做了一些记账。

所以,我们手动用 sysenter 进入内核需要做的全部事情是:

找到 AT_SYSINFOELF辅助向量,找到 __kernel_vsyscall 地址。
就像传统系统调用一样把系统调用号和参数放倒寄存器中。
调用 __kernel_vsyscall 函数。
你永远也不应该自己写 sysenter 封装函数因为内核使用 sysenter 进入和退出系统调用的??传统会变化,你的代码会被中断。

你应该一直用 __kernel_vsyscall 来执行 sysenter 系统调用。

好的,让我们这么做。

Using sysenter system calls with your own assembly

写汇编使用sysenter系统调用

就像之前我们的传统系统调用例子,我们将会执行退出状态是 42 的 exit 。

exit系统调用号是1。根据上面的接口描述,我们只需要把系统调用号传入eax寄存器,把第一个参数(退出状态码)传入ebx。

(这段代码可以被简化,但我想这样写可以让那些不知道GCC内联汇编的人理解和参考。)

#include <stdlib.h>  
#include <elf.h>    

int  main(int argc, char* argv[], char* envp[])  
{   
    unsigned int syscall_nr = 1;   
    int exit_status = 42;  
    Elf32_auxv_t *auxv;     
  
    /*typedef struct
{
  uint32_t a_type;		/* Entry type */
  union
    {
      uint32_t a_val;		/* Integer value */
      /* We use to have pointer elements added here.  We cannot do that,
	 though, since it does not work when using 32-bit definitions
	 on 64-bit platforms and vice versa.  */
    } a_un;
} Elf32_auxv_t;
*/
    /* auxilliary vectors are located after the end of the environment    
    * variables    
    *    
    * check this helpful diagram: https://static.lwn.net/images/2012/auxvec.png   
    */   

    while(*envp++ != NULL);     
    /* envp is now pointed at the auxilliary vectors, since we've iterated    
    * through the environment variables.    
    */   
   // #define AT_SYSINFO	32
   // #define AT_NULL		0
    for (auxv = (Elf32_auxv_t *)envp; auxv->a_type != AT_NULL; auxv++)   
    {     
        if( auxv->a_type == AT_SYSINFO) 
        {       
            break;     
        }   
    }     

    /* NOTE: in glibc 2.16 and higher you can replace the above code with    
    * a call to getauxval(3):  getauxval(AT_SYSINFO)   
    */    

    asm( 
        "movl %0,  %%eax    \n"   
        "movl %1, %%ebx    \n"   
        "call *%2          \n"      
        : /* output parameters, we aren't outputting anything, no none */         
        /* (none) */      
        : /* input parameters mapped to %0 and %1, repsectively */   
        "m" (syscall_nr), "m" (exit_status), "m" (auxv->a_un.a_val)      
        : /* registers that we are "clobbering", unneeded since we are calling exit */        
        "eax", "ebx");  
    }

下一步,编译,执行,然后检查退出状态:

Dump of assembler code for function main:
   0x0804843b <+0>:	lea    ecx,[esp+0x4]
   0x0804843f <+4>:	and    esp,0xfffffff0
   0x08048442 <+7>:	push   DWORD PTR [ecx-0x4]
   0x08048445 <+10>:	push   ebp
   0x08048446 <+11>:	mov    ebp,esp
   0x08048448 <+13>:	push   ebx
   0x08048449 <+14>:	push   ecx
   0x0804844a <+15>:	sub    esp,0x20
   0x0804844d <+18>:	mov    eax,ecx
   0x0804844f <+20>:	mov    edx,DWORD PTR [eax+0x4]
   0x08048452 <+23>:	mov    DWORD PTR [ebp-0x1c],edx
   0x08048455 <+26>:	mov    eax,DWORD PTR [eax+0x8]
   0x08048458 <+29>:	mov    DWORD PTR [ebp-0x20],eax
   0x0804845b <+32>:	mov    eax,gs:0x14
   0x08048461 <+38>:	mov    DWORD PTR [ebp-0xc],eax
   0x08048464 <+41>:	xor    eax,eax
   0x08048466 <+43>:	mov    DWORD PTR [ebp-0x18],0x1
   0x0804846d <+50>:	mov    DWORD PTR [ebp-0x14],0x2a
   0x08048474 <+57>:	nop
   0x08048475 <+58>:	mov    eax,DWORD PTR [ebp-0x20]
   0x08048478 <+61>:	lea    edx,[eax+0x4]
   0x0804847b <+64>:	mov    DWORD PTR [ebp-0x20],edx
   0x0804847e <+67>:	mov    eax,DWORD PTR [eax]
   0x08048480 <+69>:	test   eax,eax
   0x08048482 <+71>:	jne    0x8048475 <main+58>
   0x08048484 <+73>:	mov    eax,DWORD PTR [ebp-0x20]
   0x08048487 <+76>:	mov    DWORD PTR [ebp-0x10],eax
   0x0804848a <+79>:	jmp    0x804849a <main+95>
   0x0804848c <+81>:	mov    eax,DWORD PTR [ebp-0x10]
   0x0804848f <+84>:	mov    eax,DWORD PTR [eax]
   0x08048491 <+86>:	cmp    eax,0x20
   0x08048494 <+89>:	je     0x80484a5 <main+106>
   0x08048496 <+91>:	add    DWORD PTR [ebp-0x10],0x8
   0x0804849a <+95>:	mov    eax,DWORD PTR [ebp-0x10]
   0x0804849d <+98>:	mov    eax,DWORD PTR [eax]
   0x0804849f <+100>:	test   eax,eax
   0x080484a1 <+102>:	jne    0x804848c <main+81>
   0x080484a3 <+104>:	jmp    0x80484a6 <main+107>
   0x080484a5 <+106>:	nop
   0x080484a6 <+107>:	mov    edx,DWORD PTR [ebp-0x10]
   0x080484a9 <+110>:	mov    eax,DWORD PTR [ebp-0x18]
   0x080484ac <+113>:	mov    ebx,DWORD PTR [ebp-0x14]
   0x080484af <+116>:	call   DWORD PTR [edx+0x4]
   0x080484b2 <+119>:	mov    eax,0x0
   0x080484b7 <+124>:	mov    ebx,DWORD PTR [ebp-0xc]
   0x080484ba <+127>:	xor    ebx,DWORD PTR gs:0x14
   0x080484c1 <+134>:	je     0x80484c8 <main+141>
   0x080484c3 <+136>:	call   0x8048310 <__stack_chk_fail@plt>
   0x080484c8 <+141>:	add    esp,0x20
   0x080484cb <+144>:	pop    ecx
   0x080484cc <+145>:	pop    ebx
   0x080484cd <+146>:	pop    ebp
   0x080484ce <+147>:	lea    esp,[ecx-0x4]
   0x080484d1 <+150>:	ret    
End of assembler dump.

debug

 ► 0x80484af <main+116>    call   dword ptr [edx + 4] <0xf7fd8be0>
 
   0x80484b2 <main+119>    mov    eax, 0
   0x80484b7 <main+124>    mov    ebx, dword ptr [ebp - 0xc]
   0x80484ba <main+127>    xor    ebx, dword ptr gs:[0x14]
   0x80484c1 <main+134>    je     main+141 <0x80484c8>
 

pwndbg> x/10i 0xf7fd8be0
   0xf7fd8be0 <__kernel_vsyscall>:	push   ecx
   0xf7fd8be1 <__kernel_vsyscall+1>:	push   edx
pwndbg> p/x  auxv->a_un.a_val  实际上就是 __kernel_vsyscall 地址,get it!!!
$2 = 0xf7fd8be0
pwndbg> x/2xw $edx+4
0xffffd180:	0xf7fd8be0	0x00000021
$ gcc -m32 -o demo demo.c  
$  $ echo $?  
42

成功了!我们在没有生成软中断的情况下使用传统的 sysenter 方法做了 exit 系统调用。怎么理解这里的 sysenter 呢? 看下面的代码自明 。

pwndbg> x/12i 0xf7fd8be0
   0xf7fd8be0 <__kernel_vsyscall>:	push   ecx
   0xf7fd8be1 <__kernel_vsyscall+1>:	push   edx
   0xf7fd8be2 <__kernel_vsyscall+2>:	push   ebp
   0xf7fd8be3 <__kernel_vsyscall+3>:	mov    ebp,esp
   0xf7fd8be5 <__kernel_vsyscall+5>:	sysenter 
   0xf7fd8be7 <__kernel_vsyscall+7>:	int    0x80
   0xf7fd8be9 <__kernel_vsyscall+9>:	pop    ebp
   0xf7fd8bea <__kernel_vsyscall+10>:	pop    edx
   0xf7fd8beb <__kernel_vsyscall+11>:	pop    ecx
   0xf7fd8bec <__kernel_vsyscall+12>:	ret
check:
   0xf7fd8000 0xf7fd9000 r-xp     1000 0      [vdso]

Kernel-side: sysenter entry point

内核侧:sysenter入口

好的,我们已经看到用户空间程序是如何用**__kernel_vsyscall** 做 sysenter来触发系统调用的,让我们看看内核如何使用系统调用号执行系统调用代码的。

回忆上一章内核注册了ia32_sysenter_target系统调用执行函数。

这个功能是arch/x86/ia32/ia32entry.S里面用汇编实现的。让我们看看为了执行系统调用eax寄存器中的值是在哪里被使用的。

sysenter_dispatch:         
    call    *ia32_sys_call_table(,%rax,8)  

和我们看到了和传统系统调用模式一样的代码:一个叫ia32_sys_call_table的表格,里面有系统调用号。

在所有需要的记录存储以后,传统系统调用模式和sysenter系统调用模式使用了相同的机制和系统调用表格来分发系统调用。

相关 int $0x80 入口章来学习 ia32_sys_call_table 是在哪定义的和如何构造的。

这就是你如何通过sysenter系统调用进入内核。

Returning from a sysenter system call with sysexit

用sysexit从sysenter调用中返回

内核可以用sysexit恢复用户程序执行。

使用这个这个指令不像iret那么直接。调用着需要把返回的地址放入rdx寄存器,把程序栈指针放入rcx寄存器

就是说你的软件必须计算程序恢复执行的地址,保存这个值,在执行sysexit之前恢复这个值 。

我们可以看到这么做的代码:arch/x86/ia32/ia32entry.S:

sysexit_from_sys_call:        
    andl   $~TS_COMPAT,TI_status+THREAD_INFO(%rsp,RIP-ARGOFFSET)         
    /* clear IF, that popfq doesn't enable interrupts early */         
    andl  $~0x200,EFLAGS-R11(%rsp)         
    movl    RIP-R11(%rsp),%edx              /* User %eip */         
    CFI_REGISTER rip,rdx         
    RESTORE_ARGS 0,24,0,0,0,0         
    xorq    %r8,%r8         
    xorq    %r9,%r9         
    xorq    %r10,%r10         
    xorq    %r11,%r11         
    popfq_cfi         
    /*CFI_RESTORE rflags*/         
    popq_cfi %rcx                          /* User %esp */         
    CFI_REGISTER rsp,rcx         
    TRACE_IRQS_ON         
    ENABLE_INTERRUPTS_SYSEXIT32  
ENABLE_INTERRUPTS_SYSEXIT32是arch/x86/include/asm/irqflags.h 里定义的宏,包含sysexit指令。

现在你知道了32位快速系统调用如何工作的了。

64-bit fast system calls

64位快速系统调用

旅程的下一步是64位快速系统调用。这些系统调用分别使用syscall sysret指令进入和返回。

  • syscall/sysret
    Intel指令集参考文档( 大PDF文件)解释了syscall指令如何工作。

SYSCALL触发一个权限等级0的操作系统系统调用执行程序。它通过从IA32_LSTAR MSR加载RIP 来实现(把 SYSCALL 后面指令的地址保存到RCX之后)。

换句话说:为了让内核收到接下来的系统调用,必须把系统调用发生时候执行的代码地址储存在IA32_LSTAR` MSR 中。

我们可以在arch/x86/kernel/cpu/common.c里面看到这段代码:

void syscall_init(void)  
{         
    /* ... other code ... */         
    wrmsrl(MSR_LSTAR, system_call);  
MSR_LSTAR值在arch/x86/include/uapi/asm/msr-index.h中定义为0xc0000082。

就像传统软中断系统调用,有一个惯例是用syscall做系统调用。

用户空间程序需要把系统调用号存入rax寄存器syscall的参数存入其它通用寄存器中。

x86-64 ABI文档第A.2.1章写到:

  1. 用户程序使用%rdi, %rsi, %rdx, %rcx, %r8 和 %r9寄存器传递参数序列,内核接口使用%rdi, %rsi, %rdx, %r10, %r8 和 %r9寄存器。 2. syscall指令完成系统调用,内核会破坏%rcx 和 %r11寄存器值。 3. 系统调用号用%rax寄存器传递。
  2. 系统调用最多6个参数,不能用直接用栈传递。
  3. syscall的返回结果保存在%rax寄存器中。-4095到-1的值表示错误,他是错误号。
  4. 只能传递整数或者内存值到内核。

arch/x86/kernel/entry_64.S中的注释也做了说明。

现在我们知道如何做系统调用,如何传递参数,下面我们通过写内联汇编程序实现一个。

Using syscall system calls with your own assembly

自己写汇编做 syscall系统调用

继续前面的例子,我们用内联汇编的C程序执行exit系统调用,退出状态码为42 。

首先,我们需要找到 exit 系统调用号。在这个例子里我们需要在arch/x86/syscalls/syscall_64.tbl里找:

60 common exit sys_exit

exit 系统调用号是60。根据上面的接口描述,我们只需要把60传入rax寄存器,把第一个参数(退出状态码)传入rdi寄存器。

这里是一段有内联汇编的C代码实现。就像上面的例子,这个例子不是最简单实现,是为了方便说明:

int  
main(int argc, char *argv[])  {   
    unsigned long syscall_nr = 60; 
    long exit_status = 42;    
    asm ("movq %0, %%rax\n" 
        "movq %1, %%rdi\n" 
        "syscall"     
        : /* output parameters, we aren't outputting anything, no none */       
        /* (none) */    
        : /* input parameters mapped to %0 and %1, repsectively */      
        "m" (syscall_nr), "m" (exit_status)     
        : /* registers that we are "clobbering", unneeded since we are calling exit */       
        "rax", "rdi");  
}  

下面,编译,执行,然后检查退出状态码:

$ gcc -o test test.c  
$ ./test  
$ echo $?  
42

成功了!我们用syscall执行了exit系统调用。我们没有生成软中断并且(如果我们做计时)它执行的更快。

Kernel-side: syscall entry point

内核侧:系统调用入口

现在我们已经看了如何从用户空间触发系统调用,让我们看看内核如何用系统调用号执行系统调用代码。

回忆上一章我们看到了system_call函数的地址被写入了LSTAR MSR。

让我们看看这个函数的代码和它使用rax值如何准确传递执行到系统调用。arch/x86/kernel/entry_64.S:

`call *sys_call_table(,%rax,8)  # XXX:    rip relative`  

非常像传统系统调用方法,sys_call_table是定义在C文件中的表格,这个C文件是脚本生成的,在C代码中用#include引入。

arch/x86/kernel/syscall_64.c,底部的#include:

asmlinkage const sys_call_ptr_t 
sys_call_table[__NR_syscall_max+1] = {        
    /* 
    * Smells like a compiler bug -- it doesn't work 
    * when the & below is removed.          
    */        
    [0 ... __NR_syscall_max] = &sys_ni_syscall,  
#include <asm/syscalls_64.h> 
}; 
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Linux系统调用指南 的相关文章

  • 如何使用 GOPATH 的 Samba 服务器位置?

    我正在尝试将 GOPATH 设置为共享网络文件夹 当我进入 export GOPATH smb path to shared folder I get go GOPATH entry is relative must be absolute
  • 如何在 Bash 中给定超时后终止子进程?

    我有一个 bash 脚本 它启动一个子进程 该进程时不时地崩溃 实际上是挂起 而且没有明显的原因 闭源 所以我对此无能为力 因此 我希望能够在给定的时间内启动此进程 如果在给定的时间内没有成功返回 则将其终止 有没有simple and r
  • Linux 上的用户空间能否实现本机代码的抢占式多任务处理?

    我想知道是否可以在 Linux 用户空间的单个进程中实现本机代码的抢占式多任务处理 也就是说 从外部暂停一些正在运行的本机代码 保存上下文 交换到不同的上下文 然后恢复执行 所有这些都由用户空间精心安排 但使用可能进入内核的调用 我认为这可
  • php exec 返回的结果比直接进入命令行要少

    我有一个 exec 命令 它的行为与通过 Penguinet 给 linux 的相同命令不同 res exec cd mnt mydirectory zcat log file gz echo res 当将命令直接放入命令行时 我在日志文件
  • waitpid() 的作用是什么?

    有什么用waitpid 它通常用于等待特定进程完成 或者如果您使用特殊标志则更改状态 基于其进程 ID 也称为pid 它还可用于等待一组子进程中的任何一个 无论是来自特定进程组的子进程还是当前进程的任何子进程 See here http l
  • 进程退出后 POSIX 名称信号量不会释放

    我正在尝试使用 POSIX 命名信号量进行跨进程同步 我注意到进程死亡或退出后 信号量仍然被系统打开 在进程 打开它 死亡或退出后是否有办法使其关闭 释放 早期的讨论在这里 当将信号量递减至零的进程崩溃时 如何恢复信号量 https sta
  • 如果在等待“read -s”时中断,在子进程中运行 bash 会破坏 tty 的标准输出吗?

    正如 Bakuriu 在评论中指出的那样 这基本上与BASH 输入期间按 Ctrl C 会中断当前终端 https stackoverflow com questions 31808863 bash ctrlc during input b
  • Windows CE 与嵌入式 Linux [关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 现在我确信我们都清楚 Linux 与 Windows 桌面的相对优点 然而 我对嵌入式开发世界的了解却少得多 我主要对行业解决方案感兴
  • 如何允许应用程序声明“https”方案 URI? (即如何从 https URL 打开桌面应用程序?)

    目前我正在尝试为 OAuth 2 0 授权流程创建一个客户端 实际上是一个本机应用程序 并且在规范中就在这儿 https www rfc editor org rfc rfc8252 section 7 2据说有 3 种方法来处理重定向 U
  • 无法在 Perl 中找到 DBI.pm 模块

    我使用的是 CentOS 并且已经安装了 Perl 5 20 并且默认情况下存在 Perl 5 10 我正在使用 Perl 5 20 版本来执行 Perl 代码 我尝试使用 DBI 模块并收到此错误 root localhost perl
  • Python 脚本作为 Linux 服务/守护进程

    Hallo 我试图让 python 脚本作为服务 守护进程 在 ubuntu linux 上运行 网络上存在多种解决方案 例如 http pypi python org pypi python daemon http pypi python
  • 使用 plistBuddy 获取值数组

    var keychain access groups declare a val usr libexec PlistBuddy c Print var sample plist echo val echo val 0 Ouput Array
  • 后台分叉无法正常工作[重复]

    这个问题在这里已经有答案了 我运行这个程序 在前景和背景中 int main int pid printf App Start pid d n getpid while 1 pid fork if pid 0 printf Child n
  • Raspberry 交叉编译 - 执行程序以“分段错误”结束

    我有一个自己编写的程序 我想从我的 x86 机器上为 Raspberry Pi 构建它 我正在使用 eclipse 生成的 makefile 并且无法更改此内容 我已经阅读了 CC for raspi 的教程 Hackaday 链接 htt
  • 捕获数据包后会发生什么?

    我一直在阅读关于网卡捕获数据包后会发生什么的内容 我读得越多 我就越困惑 首先 我读过传统上 在网卡捕获数据包后 它会被复制到内核空间中的一个内存块 然后复制到用户空间 供随后处理数据包数据的任何应用程序使用 然后我读到了 DMA 其中 N
  • X11 模式对话框

    如何使用 Xlib 在 X11 中创建模式对话框 模态对话框是一个位于应用程序其他窗口之上的窗口 就像瞬态窗口一样 并且拒绝将焦点给予应用程序的其他窗口 在 Windows 中 当试图从模态窗口夺取焦点时 模态也会通过闪 烁模态窗口的标题栏
  • 如何将后台作业的输出分配给 bash 变量?

    我想在 bash 中运行后台作业并将其结果分配给一个变量 我不喜欢使用临时文件 并且希望同时运行多个类似的后台任务 root root var echo hello world root root echo var hello world
  • 配置:错误:无法运行C编译的程序

    我正在尝试使用 Debian Wheezy 操作系统在我的 Raspberry Pi 上安装不同的软件 当我运行尝试配置软件时 我尝试安装我得到此输出 checking for C compiler default output file
  • linux下写入后崩溃

    如果我使用 write 将一些数据写入磁盘上的文件会发生什么 但我的应用程序在刷新之前崩溃了 如果没有系统故障 是否可以保证我的数据最终会刷新到磁盘 如果您正在使用write 并不是fwrite or std ostream write 那
  • 找出 Linux 上的默认语言

    有没有办法从C语言中找出Linux系统的默认语言 有 POSIX API 可以实现这个功能吗 例如 我想要一个人类可读格式的字符串 即德语系统上的 German 或 Deutsch 法语系统上的 French 或 Francais 等 有类

随机推荐

  • Java项目——文档搜索引擎

    文章目录 1 项目概述 2 准备阶段 2 1 项目创建 2 2 准备静态页面 3 搜索逻辑 4 分词 5 处理 HTML 文件 5 1 枚举文件夹中所有文件 5 2 预处理文件 5 2 1 获取标题 5 2 2 获取 URL 5 2 3 获
  • [VUE] 过滤器函数

    VUE 过滤器可以用在两个地方 双花括号插值和 v bind 表达式 代码如下 message capitalize div div 你可以在一个组件的选项中定义本地的过滤器 filters capitalize function valu
  • Apple Magic Mouse 卡顿的问题

    更新时间 2022 06 30 17 58 37 发现在公司使用就会很卡顿 在家里使用就很流畅 感觉还是公司信号被干扰了 更新时间 2022年06月13日 尝试过下面所以的方法 以及怀疑是键盘蓝牙干扰 把键盘关掉 最后的结论 都没什么卵用
  • opencv读写和保存中文路径图片及base64与图片互转

    文章目录 1 opencv读取中文路径图片 2 opencv保存中文路径图片 3 图片转base64 4 base64转图片 有几点要注意 cv2 imread filename flags cv2 imwrite filename img
  • 交叉编译器的安装方法

    首先简单介绍一下 所谓的搭建交叉编译环境 即安装 配置交叉编译工具链 在该环境下编译出嵌入式Linux系统所需的操作系统 应用程序等 然后再上传到目标机上 交叉编译工具链是为了编译 链接 处理和调试跨平台体系结构的程序代码 对于交叉开发的工
  • STL 常用函数

    STL 常用函数 本文参考自 C STL常用函数总结 总结学习用 sort 函数 排序函数 sort 起始地址 末尾地址 cmp 其中cmp是可以自己定义的函数名 sort a a 5 sort vec begin vec end bool
  • ajax内置对象有什么,用js内置对象XMLHttpRequest 来用ajax

    步骤 用XMLHTTPRequest来进行ajax异步数据交交互 主要有几个步骤 1 创建XMLHTTPRequest对象 最复杂的一步 if window XMLHttpRequest code for IE7 Firefox Chrom
  • Apache Beam程序向导4

    今天在集群上实验Beam On Spark的时候 遇到一个坑爹的问题 这个问题总结起来是一个java lang NoClassDefFoundError 错误 具体错误如下图1所示 图1 错误提示 该错误提示SparkStreamingCo
  • cesium中定位方法使用

    cesium中定位到位置 在cesium中viewer flyTo和Camera flyTo的区别挺大 我们通常会用camera来定位 但当需要加上一个倾斜角的时候 可能定位的结果就和预想的区别很大 需求 矩形的中心点位置 110 0 35
  • CSDN竞赛第35期题解

    CSDN竞赛第35期题解 1 题目名称 交换后的or 给定两组长度为n的二进制串 请问有多少种方法在第一个串中交换两个不同位置上的数字 使得这两个二进制串 或 的 结果发生改变 int n cin gt gt n string a b ci
  • Python GUI 设计(三)---Widget组件详解

    1 1 Canvas画布组件 Tkinter模块中的Canvas组件主要用于绘制图形 文字 设计动画等甚至也可以将其他小部件放在画布上 比如视频 它的语法格式如下 Canvas 父窗口 options 第一个参数是父窗口 表示这个画布建立在
  • Linux操作系统~必考面试题⑥

    文件管理命令 1 cat 命令 cat 命令用于连接文件并打印到标准输出设备上 cat 主要有三大功能 1 一次显示整个文件 cat filename 2 从键盘创建一个文件 cat gt filename 3 将几个文件合并为一个文件 c
  • 链表-哈希表 详解

    链表 链表是由一系列节点组成的元素集合 每个节点包含两部分 数据域item和指向一下个节点的指针next 通过节点之间相互连接 最终串联成一个链表 链式存储结构就是 两个相邻的元素在内存中可能不是相邻的 每一个元素都有一个指针域 指针域一般
  • odoo权限管理详解

    前言 odoo作为ERP框架 必然有不同角色的用户使用这同一系统 对于系统上面的数据 应该对不同角色设置不同的查阅修改权限 odoo框架自带了了比较完善的权限控制机制 这篇博客的实践基于odoo13 其他版本可能略有差别 A 按odoo使用
  • 文举论金:黄金原油全面走势分析策略指导。

    市场没有绝对 涨跌没有定势 所以 对市场行情的涨跌平衡判断就是你的制胜法宝 欲望 有句意大利谚语 让金钱成为我们忠心耿耿的仆人 否则 它就会成为一个专横跋扈的主人 空头 多头都能赚钱 唯有贪心不能赚 是你掌控欲望还是欲望掌控你 古人云 不积
  • MVCC 实现原理

    这里是CS大白话专场 让枯燥的学习变得有趣 没有对象不要怕 我们new一个出来 每天对ta说不尽情话 好记性不如烂键盘 自己总结不如收藏别人 在讲解 MVCC 之前先来看一下 MySQL 中事务的四种隔离级别 读未提交 一个事务可以读到另一
  • ChatGPT生成内容很难脱离标准化,不建议用来写留学文书

    ChatGPT无疑是23年留学届的热门话题 也成为了不少留学生再也离不开的万能工具 从总结文献 润色论文 给教授写email似乎无所不能 各大高校对于学生使用ChatGPT的态度也有所不同 例如 哈佛大学教育代理院长 Anne Harrin
  • Unity游戏编程-——迷宫巡逻兵

    文章目录 游戏设计要求 程序设计要求 基本思路分析 模式基础 架构设计 关键模块 遇到的问题 资源地址 游戏设计要求 创建一个地图和若干巡逻兵 使用动画 每个巡逻兵走一个3 5个边的凸多边型 位置数据是相对地址 即每次确定下一个目标位置 用
  • 字节跳动(飞书)产品测试实习生一面

    下面面试问题的顺序记不清了 所以没按面试官问的顺序写 1 性能测试 2 黑盒和白盒 3 用过飞书吗 知道飞书的产品流程吗 4 谈谈你简历上写的项目 提到购物车功能 仔细讲讲 5 学过软件工程管理 说说整个软件的项目管理流程 6 看有服役的经
  • Linux系统调用指南

    Linux系统调用指南 文章是转载 但是我在后面的案例加了不少注解并debug了 如有疑问 留言交流 其实我也不懂 原文链接 blog packagecloud io https zcfy cc article the definitive