前言

记录20250424-20250425通过gdb调试学到的东西

参考博客:

https://arttnba3.cn/2021/02/21/OS-0X00-LINUX-KERNEL-PART-I/

https://arttnba3.cn/2021/02/21/OS-0X01-LINUX-KERNEL-PART-II/

https://arttnba3.cn/2021/03/03/PWN-0X00-LINUX-KERNEL-PWN-PART-I/

内核态与用户态切换

昨天学习了一下内核态用户态是如何切换的,今天通过gdb调试与询问gpt大概知晓了之中的过程。

前置知识

MSR寄存器

MSR (Model Specific Registers) 是一类专门为 CPU 架构或微架构设计的寄存器,主要用于存储 CPU 特定的控制、状态和配置信息。它们与处理器的核心功能和硬件特性密切相关。MSR 寄存器在不同的处理器型号和微架构之间可能会有所不同,因此它们被称为 “Model Specific”(特定型号的寄存器)。

简而言之,MSR寄存器中记录了关于CPU的一些信息。

gs_base

MSR寄存器中有MSR_GS_BASE和MSR_KERNEL_GS_BASE,我们在之后见到的swapgs交换的就是这两个值。

经过我的gdb调试发现,gs寄存器一直是0,只有这两个寄存器发生了变化。

我们在汇编代码中经常发现的比如gs:[0x……] 这里的gs应该指的是MSR_GS_BASE,并不是gs寄存器。

方便起见,以下称为gs_base。

利用gs_base我们可以做到一些事情,比如索引到per_cpu区域。

MSR_STAR

这也是MSR寄存器中的一员,这个寄存器中间有几位存储的数据是 SYSCALL CS and SS (大概就是保存的用户态的CS和SS寄存器值)

MSR_LSTAR

syscall的时候,会把这个寄存器的值加载到RIP中,也就是说它保存了切换到内核态时rip的值。

task_struct与内核栈

是每个进程(或线程)对应的结构体,记录了它的所有信息,比如:

  • pid、父子关系
  • 虚拟内存空间
  • 打开的文件
  • 内核栈
  • 所属调度器、优先级
  • 所在 CPU 等

简单来说,这是一个在内核中记录了进程信息的结构体。这个结构体中有一个成员叫内核栈。

是的,我现在才知道,内核栈并不是一个内核只有一个的,内核栈是一个进程一个的,也就是说内核没有自己栈,它在运行是使用的栈都是不同进程自己的内核栈。

这样也就是说,不同进程的内核栈互不影响,不会因为某一个进程的内核栈崩溃而导致整个内核崩溃。

per-CPU与TSS

TSS

TSS(Task State Segment,任务状态段)是 x86 架构 提供的一种机制,最初是为“硬件任务切换”设计的,但在现代 Linux 中,它的主要作用已经简化,特别是:为进入内核态时提供栈顶指针(RSP0

也就是说,TSS本来可能很有用,但是现在他的作用就是为切换到内核态时提供内核栈信息。

我们已经知道,内核栈是每个进程都有的,TSS也正是在进程切换时保存了切换后进程的内核栈。

per-CPU

percpu 结构体是 Linux 内核中一种非常关键的机制,它为每个 CPU 提供私有变量副本,以提高并发性能并避免锁争用。

简单来说,就是存储了当前cpu的一些信息,自然,这里面存储了TSS。

per-CPU存储在内核空间中,还记得gs_base吗,就是利用它来访问per-CPU空间的,也因为TSS在这里面,TSS保存了内核栈,因此可以利用gs_base索引到内核栈,这也就是为什么要有swapgs这条指令,根本目的就是为了获得内核栈信息。

syscall发生了什么?

  1. 把MSR_LSTAR寄存器中的值加载到RIP寄存器,并把当前程序运行的下一条指令(即syscall指令的下一条指令)保存在RCX寄存器中

  2. 把当前的RFLAGS寄存器的值保存在R11寄存器,并使用MSR_FMASK寄存器的值mask当前RFLAGS的值。一般通过这种方式关闭中断,保证进入系统调用后,CPU的中断时关闭的

  3. 把MSR_STAR寄存器的SYSCALL CS and SS分别加载到CPU的CS和SS段寄存器,同时更新CS和SS的不可见部分。

抄的博客

https://blog.csdn.net/chengwenyang/article/details/117794217

由于csdn的vip机制导致作为免费用户的我只能看到开头的内容(哭)

从syscall结束到进入自己编写的内核模块中间发生了什么?

还记得昨天讲的吗?回顾一下:(抄的大佬博客)

  • 切换 GS 段寄存器:通过 swapgs 切换 GS 段寄存器,将 GS 寄存器值和一个特定位置的值进行交换,目的是保存 GS 值,同时将该位置的值作为内核执行时的 GS 值使用
  • 保存用户态栈帧信息:将当前栈顶(用户空间栈顶)记录在 CPU 独占变量区域里(由 GS 寄存器所指定的percpu 段),将 CPU 独占区域里记录的内核栈顶放入 rsp/esp
  • 保存用户态寄存器信息: 通过 push 保存各寄存器值到栈上,以便后续“着陆”回用户态
  • 通过汇编指令判断是否为32位
  • 控制权转交内核,执行相应的操作

首先第一步就是swapgs,切换gs_base与kernel_gs_base的值。

接下来就是这两句

mov    qword ptr gs:[0x140d8], rsp
mov rsp, qword ptr gs:[0x500c]

这是从gdb显示的汇编代码截取的,基本来说第一步就是把当前的rsp,也就是用户态的栈信息给到percpu中的TSS保存起来。第二句就是把TSS中的内核栈给到现在的rsp。

之后就是push各种寄存器到栈上,其中有几个特殊的寄存器,cs ss ip sp rflags

push   irq_stack_union+43            <43>
push qword ptr gs:[0x140d8]
push r11
push irq_stack_union+51 <51>
push rcx

顺序是这样的:ss sp rflags cs ip

其中第一句汇编指令看字节码是这样的 0x6a 0x2b

反汇编就是 push 0x2b,这刚好是用户态的ss值。

第四句汇编指令看汇编代码是这样的0x6a 0x33

反汇编就是 push 0x33,这也刚好是用户态的cs值。

第二句话qword ptr gs:[0x140d8] 结合上文来看很容易知道这就是用户态的rsp值。

第三句话r11根据syscall的具体操作也知道这就是rflags。

第五句话rcx也能知道这就是rip。

而内核态cs ss的值分别是0x10和0x18,可以认为这两个寄存器只会有这两种取值:

用户态 内核态
cs 0x33 0x10
ss 0x2b 0x18

push完这五个重要的寄存器之后就会开始push其他的寄存器

0xffffffff81a00034 <__do_softirq+52>:        push   rax
0xffffffff81a00035 <__do_softirq+53>: push rdi
0xffffffff81a00036 <__do_softirq+54>: push rsi
0xffffffff81a00037 <__do_softirq+55>: push rdx
0xffffffff81a00038 <__do_softirq+56>: push rcx
0xffffffff81a00039 <__do_softirq+57>: push 0xffffffffffffffda
0xffffffff81a0003b <__do_softirq+59>: push r8
0xffffffff81a0003d <__do_softirq+61>: xor r8,r8
0xffffffff81a00040 <__do_softirq+64>: push r9
0xffffffff81a00042 <__do_softirq+66>: xor r9,r9
0xffffffff81a00045 <__do_softirq+69>: push r10
0xffffffff81a00047 <__do_softirq+71>: xor r10,r10
0xffffffff81a0004a <__do_softirq+74>: push r11
0xffffffff81a0004c <__do_softirq+76>: xor r11,r11
0xffffffff81a0004f <__do_softirq+79>: push rbx
0xffffffff81a00050 <__do_softirq+80>: xor ebx,ebx
0xffffffff81a00052 <__do_softirq+82>: push rbp
0xffffffff81a00053 <__do_softirq+83>: xor ebp,ebp
0xffffffff81a00055 <__do_softirq+85>: push r12
0xffffffff81a00057 <__do_softirq+87>: xor r12,r12
0xffffffff81a0005a <__do_softirq+90>: push r13
0xffffffff81a0005c <__do_softirq+92>: xor r13,r13
0xffffffff81a0005f <__do_softirq+95>: push r14
0xffffffff81a00061 <__do_softirq+97>: xor r14,r14
0xffffffff81a00064 <__do_softirq+100>: push r15
0xffffffff81a00066 <__do_softirq+102>: xor r15,r15
0xffffffff81a00069 <__do_softirq+105>: mov rdi,rsp
0xffffffff81a0006c <__do_softirq+108>: call 0xffffffff810013a0 <vvar_fault+99>

之后就会通过一系列内核函数最终进入真正执行的函数。

从内核模块返回到sysretq之前发生了什么?

首先是经过前面一系列的内核函数。

然后pop一堆寄存器(和之前push一堆寄存器相对应)

之后的汇编代码是这样的

mov    rdi,rsp
mov rsp,QWORD PTR gs:0x5004
push QWORD PTR [rdi+0x28]
push QWORD PTR [rdi]
push rax
jmp 0xffffffff81a00142 <__do_softirq+322>
pop    rax
pop rdi
pop rsp
swapgs
sysretq

第一段汇编中的第二句并不是用户态栈,仍然是内核态的栈,具体是什么目前还没有了解,看这段汇编代码的话就是提供了一个给这几句push pop执行的栈。

真正的用户态栈是 QWORD PTR [rdi+0x28],其中rdi是之前的rsp。

还记得我们之前进入内核模块时push了5个寄存器中就有用户态的rsp,这里最后pop rsp的rsp也正是当初push进去的rsp。

swapgs不再赘述,和之前的swapgs一样。

sysretq 发生了什么?

  1. 把RCX寄存器中的值加载到RIP寄存器
  2. 把R11寄存器中的值加载到rflags寄存器
  3. 把MSR_STAR寄存器中的SYSRET CS and SS分别加载到CS和SS段寄存器。

可以看到和之前的syscall的操作基本是对应着的

也就是说,我们之前push了5个重要的寄存器ss cs ip sp rflags在这里只用到了sp,其余的没有用到,而是用的rcx r11 msr寄存器。

iretq发生了什么?

iretq相对于sysretq来说,用上了之前push的5个寄存器。

简单来说,iretq就是从当前栈开始按照顺序恢复这5个寄存器

顺序就是

rip
cs
rflags
rsp
ss

正好和push入栈的顺序相反。