linux内核学习入门2
前言
记录20250424-20250425通过gdb调试学到的东西
参考博客:
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发生了什么?
把MSR_LSTAR寄存器中的值加载到RIP寄存器,并把当前程序运行的下一条指令(即syscall指令的下一条指令)保存在RCX寄存器中
把当前的RFLAGS寄存器的值保存在R11寄存器,并使用MSR_FMASK寄存器的值mask当前RFLAGS的值。一般通过这种方式关闭中断,保证进入系统调用后,CPU的中断时关闭的
把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 |
这是从gdb显示的汇编代码截取的,基本来说第一步就是把当前的rsp,也就是用户态的栈信息给到percpu中的TSS保存起来。第二句就是把TSS中的内核栈给到现在的rsp。
之后就是push各种寄存器到栈上,其中有几个特殊的寄存器,cs ss ip sp rflags
push irq_stack_union+43 <43> |
顺序是这样的: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 |
之后就会通过一系列内核函数最终进入真正执行的函数。
从内核模块返回到sysretq之前发生了什么?
首先是经过前面一系列的内核函数。
然后pop一堆寄存器(和之前push一堆寄存器相对应)
之后的汇编代码是这样的
mov rdi,rsp |
pop rax |
第一段汇编中的第二句并不是用户态栈,仍然是内核态的栈,具体是什么目前还没有了解,看这段汇编代码的话就是提供了一个给这几句push pop执行的栈。
真正的用户态栈是 QWORD PTR [rdi+0x28],其中rdi是之前的rsp。
还记得我们之前进入内核模块时push了5个寄存器中就有用户态的rsp,这里最后pop rsp的rsp也正是当初push进去的rsp。
swapgs不再赘述,和之前的swapgs一样。
sysretq 发生了什么?
- 把RCX寄存器中的值加载到RIP寄存器
- 把R11寄存器中的值加载到rflags寄存器
- 把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 |
正好和push入栈的顺序相反。





