系统调用

    进程 ——> 调用OS API;OS进程管理 ——> 调配进程。

    仅从用户进程角度,OS就像是一个被动响应的运行时库。Windows提供了一个系统调用界面作为外层,即Win32API;Linux的CRuntime库标准,Windows也支持。用户进程通过调用这些OS接口,可以部分操作系统内核。

    硬件] 设备驱动] 核心层] 管理层] 系统服务界面,中断入口(不公开)] 系统DLL(公开)] ——>API

    用户进程没有调用内核函数(特权指令)的权限,只有调用API,通过API函数内的“自陷中断”进入“系统态”调用系统函数。或者是“异常中断”出错进入OS运行。

  Win32API{
    __asm{
      push  ebp                     KiSystemService()
      mov  eax,  151  //系统函数号           {  中断,保护CPU环境,进入系统态
      lea    edx, 9[ebp] //用户区参数栈地址         copy栈参数块到系统内存
      int    0x2e  //中断id,进入IDT表对应函数 ——>    执行eax寄存器里的id调用函数
      pop  ebp                       返回结果,copy回用户区
      ret   10  //返回结果参数,9+1           关中断,基本是开头的逆操作                  }
    }

  } //如果用户参数少,可以直接在寄存器中传,用户指定fastcall方式
  
    除外部IO外,自陷和异常也是IDT表中的中断项(255个,外设中断预留100个左右),自陷中断有:
  int  0x3 (2c\ 2d)   KiTrap3() ,DEBUG调试用
    0x2e     KiSystemService() ,系统函数调用
    0x2b   内核可以调用用户空间的子程序,子程序执行完再返回内核,就用这个中断
    0x2a   获取精确CPU时间,轻型调用,普通中断开销大,无法取到精确的时间  

    通常的特权指令调用都通过KiSystemService() ,通过系统函数调用号id跳转到函数地址上。Windows核心和Liunx类似,有200多个核心函数,但MS又把视窗界面函数也移入内核中。

另外:不同的用户进程,有各自的系统堆栈空间,供其调用的内核函数使用(是彼此独立的)。

windows内核情景分析--系统调用 

Windows的地址空间分用户模式与内核模式,低2GB的部分叫用户模式,高2G的部分叫内核模式,位于用户空间的代码不能访问内核空间,位于内核空间的代码却可以访问用户空间

一个线程的运行状态分内核态与用户态,当指令位于用户空间时,就表示当前处于内核态,当指令位于内核空间时,就处于内核态.

一个线程由用户态进入内核态的途径有3种典型的方式:

1、 主动通过int 2e(软中断自陷方式)或sysenter指令(快速系统调用方式)调用系统服务函数,主动进入内核
2、 发生异常,被迫进入内核
3、 发生硬件中断,被迫进入内核

现在讨论第一种进入内核的方式:(又分为两种方式)
1、 通过老式的int 2e指令方式调用系统服务(因为老式cpu没提供sysenter指令)

如ReadFile函数调用系统服务函数NtReadFile
Kernel32.ReadFile()  //点号前面表示该函数的所在模块
{
//所有Win32 API通过NTDLL中的系统服务存根函数调用系统服务进入内核
NTDLL.NtReadFile();
}

NTDLL.NtReadFile()
{
   Mov eax,152   //我们要调用的系统服务函数号,也即SSDT表中的索引,记录在eax中
   If(cpu不支持sysenter指令)
   {
      Lea edx,[esp+4] //用户空间中的参数区基地址,记录在edx中
      Int 2e  //通过该自陷指令方式进入KiSystemService,‘调用’对应的系统服务
   }
   Else
   {
      Lea edx,[esp +4] //用户空间中的参数区基地址,记录在edx中
      Sysenter //通过sysenter方式进入KiFastCallEntry,‘调用’对应的系统服务
   }
   Ret 36 //不管是从int 2e方式还是sysenter方式,系统调用都会返回到此条指令处
}

Int 2e的内部实现原理:
该指令是一条自陷指令,执行该条指令后,cpu会自动将当前线程的当前栈切换为本线程的内核栈(栈分用户栈、内核栈),保存中断现场,也即那5个寄存器。然后从该cpu的中断描述符表(简称IDT)中找到这个2e中断号对应的函数(也即中断服务例程,简称ISR),jmp 到对应的isr处继续执行,此时这个ISR本身就处于内核空间了,当前线程就进入内核空间了

Int 2e指令可以把它理解为intel提供的一个内部函数,它内部所做的工作如下
Int 2e
{
   Cli  //cpu一中断,立马自动关中断
   Mov esp, TSS.内核栈地址 //切换为内核栈,TSS中记录了当前线程的内核栈地址
   Push SS
   Push esp
   Push eflags
   Push cs
Push eip  //这5项工作保存了中断现场【标志、ip、esp】
Jmp  IDT[中断号]  //跳转到对应本中断号的isr
}

明白了IDT,就可以看到0x2e号中断的isr为KiSystemService,顾名思义,这个中断号专用于提供系统服务。

https://www.cnblogs.com/jadeshu/articles/10663613.html


利用Windbg初步解析系统调用
https://cloud.tencent.com/developer/news/240246

Windbg初步解析系统调用

其实我以前没怎么用过,编程的时候很少调试,即便调试也是依赖编译器的调试。

最近要解析系统调用,解析的时候出了点问题,没有办法解析到NtReadFile系统调用中读的内容。这事儿很奇怪,其他的系统调用都可以解析到,这个就不行。

于是,我写了一个简单的读文件程序进行测试。代码如下

代码很简单,就不说了。64位debug模式下编译通过后,用windbg打开,

设置断点:bu ntdll!ZwReadFile

执行g命令,开始运行

连续按F10执行到syscall完成

R命令查看寄存器值

dq rsp+0x30得到buffer地址

da [buffer地址] 就可以看到buffer的内容了。如下所示,内容很简单,是“1 2 3 4 5 6 7 8 9 0”,和我在hello.txt中写入的一样。

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐