-
2021-06-29 21:15:25
内核创建一个进程
一个进程的通过进程控制块创建属于自己的堆栈
堆栈包括 用户栈和内核栈
用户栈:指向用户内存空间,用于保存用户空间和子线程调用的参数、返回值和局部变量
内核栈: 指向内核地址空间,保留中断现场,特别是嵌套中断,被中断信息一次性压入栈中,恢复时,按照先进后出逆序弹出中断,保存操作系统相互调用的参数、返回值和局部变量
为什么不同共有:
1、如果用内核栈:大小有限,用户调用次数很多,中断16个优先级,系统栈是15个,用户程序调用次数很多,15次子程序调用后的参数返回值和局部变量就不能保存,用户程序也就不能正常运行了
2、如果用用户栈:系统程序需要在某种保护机制下,才能运行,因为用户栈在用户空间是无保护运行
更多相关内容 -
Linux 中的各种栈:进程栈 线程栈 内核栈 中断栈
2018-05-06 12:03:22---------------------------------------------------Linux 中的各种栈:进程栈 线程栈 内核栈 中断栈 -
内核栈的使用
2020-11-18 23:58:37回来后头脑中又浮现出这个问题,突然想到内核栈,觉得这个问题的原因应该出在内核栈上。下面简要介绍下内核的知识。 用户空间运行的程序可以从用户空间的栈上分配大量的空间来存放变量或数组、大的结构体。之所以... -
试验四-基于内核栈切换的进程切换.zip
2020-06-26 09:17:25试验四-基于内核栈切换的进程切换,回答问题: (1)为什么要加 4096; (2)为什么没有设置 tss 中的 ss0。 答: (1)由于Linux 0.11进程的内核栈和该进程的PCB在同一页内存上(一块4KB大小的内存),其中PCB位于... -
内核栈
2017-04-13 17:37:30为什么需要内核栈 进程在内核态运行时需要自己的堆栈信息, 因此linux内核为每个进程都提供了一个内核栈kernel stack, struct task_struct { // ... void *stack; // 指向内核栈的指针 // ... }; 内核态...为什么需要内核栈
- 进程在内核态运行时需要自己的堆栈信息, 因此linux内核为每个进程都提供了一个内核栈kernel stack,
struct task_struct { // ... void *stack; // 指向内核栈的指针 // ... };
内核态的进程访问处于内核数据段的栈,这个栈不同于用户态的进程所用的栈。
用户态进程所用的栈,是在进程线性地址空间中;
而内核栈是当进程从用户空间进入内核空间时,特权级发生变化,需要切换堆栈,那么内核空间中使用的就是这个内核栈。因为内核控制路径使用很少的栈空间,所以只需要几千个字节的内核态堆栈。
需要注意的是,内核态堆栈仅用于内核例程,Linux内核另外为中断提供了单独的硬中断栈和软中断栈
为什么需要thread_info
- 内核还需要存储每个进程的PCB信息, linux内核是支持不同体系的的, 但是不同的体系结构可能进程需要存储的信息不尽相同, 这就需要我们实现一种通用的方式, 我们将体系结构相关的部分和无关的部门进行分离
用一种通用的方式来描述进程, 这就是struct task_struct, 而thread_info就保存了特定体系结构的汇编代码段需要访问的那部分进程的数据,我们在thread_info中嵌入指向task_struct的指针, 则我们可以很方便的通过thread_info来查找task_struct
将两种结构融合在一起
linux将内核栈和进程控制块thread_info融合在一起, 组成一个联合体thread_union
通常内核栈和thread_info一同保存在一个联合体中, thread_info保存了线程所需的所有特定处理器的信息, 以及通用的task_struct的指针
内核数据结构描述
thread_union
对每个进程,Linux内核都把两个不同的数据结构紧凑的存放在一个单独为进程分配的内存区域中:
-
一个是内核态的进程堆栈stack
-
另一个是紧挨着进程描述符的小数据结构thread_info,叫做线程描述符。
这两个结构被紧凑的放在一个联合体中thread_union中,
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
这块区域32位上通常是8K=8192(占两个页框),64位上通常是16K,其实地址必须是8192的整数倍。
出于效率考虑,内核让这8K(或者16K)空间占据连续的两个页框并让第一个页框的起始地址是213的倍数。
下图中显示了在物理内存中存放两种数据结构的方式。线程描述符驻留与这个内存区的开始,而栈顶末端向下增长。 下图摘自ULK3,进程内核栈与进程描述符的关系如下图:
在这个图中,
- esp寄存器是CPU栈指针,用来存放栈顶单元的地址。在80x86系统中,栈起始于顶端,并朝着这个内存区开始的方向增长。从用户态刚切换到内核态以后,进程的内核栈总是空的。因此,esp寄存器指向这个栈的顶端。一旦数据写入堆栈,esp的值就递减。
同时我们可以看到,
-
thread_info和内核栈虽然共用了thread_union结构, 但是thread_info大小固定, 存储在联合体的开始部分, 而内核栈由高地址向低地址扩展, 当内核栈的栈顶到达thread_info的存储空间时, 则会发生栈溢出
-
系统的current指针指向了当前运行进程的thread_union(或者thread_info)的地址
-
进程task_struct中的stack指针指向了进程的thread_union(或者thread_info)的地址, 在早期的内核中这个指针用struct thread_info *thread_info来表示, 但是新的内核中用了一个更浅显的名字void *stack, 即内核栈
即,进程的thread_info存储在进程内核栈的最低端
task_struct中的内核栈stack
我们之前在描述task_struct时就提到了其stack指针指向的是内核栈的地址。
其被定义在include/linux/sched.h中
http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.5#L1391
形式如下
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
在早期的linux内核中进程描述符中是不包含内核栈的, 相反包含着指向
thread_info
的指针但是在2007年的一次更新(since 2.6.22)中加入了
stack
内核栈指针, 替代了原来的thread_info
的指针进程描述符task_struct结构中没有直接指向thread_info结构的指针,而是用一个void指针类型的成员表示,然后通过类型转换来访问thread_info结构。
stack指向了内核栈的地址(其实也就是thread_info和thread_union的地址),因为联合体中stack和thread_info都在起始地址, 因此可以很方便的转型
相关代码在include/linux/sched.h中
task_thread_info用于通过task_struct来查找其thread_info的信息, 只需要一次指针类型转换即可- 1
- 1
内核栈数据结构描述thread_info
thread_info是体系结构相关的,结构的定义在thread_info.h中,保存了进程所有依赖于体系结构的信息, 同时也保存了一个指向进程描述符task_struct的指针
架构 定义链接 x86 linux-4.5/arch/x86/include/asm/thread_info.h, line 55 arm linux-4.5arch/arm/include/asm/thread_info.h, line 49 arm64 linux/4.5/arch/arm64/include/asm/thread_info.h, line 47 函数接口
内核栈与thread_info的通用操作
原则上, 只要设置了预处理器常数
__HAVE_THREAD_FUNCTIONS
通知内核, 那么各个体系结构就可以随意在stack数组中存储数据。在这种情况下, 他们必须自行实现task_thread_info和task_stack_page, 这两个函数用于获取给定task_struct实例的线程信息和内核栈。
另外, 他们必须实现dup_task_struct中调用的函数setup_thread_stack, 以便确定stack成员的具体内存布局, 当前只有ia64等少数架构不依赖于内核的默认方法
下标给出了不同架构的task_thread_info和task_stack_page的实现
架构 定义链接 ia64 arch/ia64/include/asm/thread_info.h, line 53 通用 include/linux/sched.h, line 2812
在内核的某个特定组建使用了较多的栈空间时, 内核栈会溢出到thread_info部分, 因此内核提供了 kstack_end 函数来判断给出的地址是否位于栈的有效部分// 未定义__HAVE_THREAD_FUNCTIONS的时候使用内核的默认操作 #ifndef __HAVE_THREAD_FUNCTIONS // 通过进程的task_struct来获取进程的thread_info #define task_thread_info(task) ((struct thread_info *)(task)->stack) // 通过进程的task_struct来获取进程的内核栈 #define task_stack_page(task) ((task)->stack) // 初始化thread_info, 指定其存储结构的内存布局 static inline void setup_thread_stack(struct task_struct *p, struct task_struct *org) { *task_thread_info(p) = *task_thread_info(org); task_thread_info(p)->task = p; } /* * Return the address of the last usable long on the stack. * * When the stack grows down, this is just above the thread * info struct. Going any lower will corrupt the threadinfo. * * When the stack grows up, this is the highest address. * Beyond that position, we corrupt data on the next page. */ static inline unsigned long *end_of_stack(struct task_struct *p) { #ifdef CONFIG_STACK_GROWSUP return (unsigned long *)((unsigned long)task_thread_info(p) + THREAD_SIZE) - 1; #else return (unsigned long *)(task_thread_info(p) + 1); #endif } #endif
#ifndef __HAVE_ARCH_KSTACK_END static inline int kstack_end(void *addr) { /* Reliable end of stack detection: * Some APM bios versions misalign the stack */ return !(((unsigned long)addr+sizeof(void*)-1) & (THREAD_SIZE-sizeof(void*))); } #endif
前面我们在讲_do_fork创建进程的时候, 提到dup_task_struct会复制父进程的task_struct和thread_info实例的内容, 但是stack则与新的thread_info实例位于同一个内存, 这意味着父子进程的task_struct此时除了栈指针之外完全相同。
获取当前在CPU上正在运行进程的thread_info
所有的体系结构都必须实现两个current和current_thread_info的符号定义宏或者函数,
-
current_thread_info可获得当前执行进程的thread_info实例指针, 其地址可以根据内核指针来确定, 因为thread_info总是位于起始位置,
因为每个进程都有自己的内核栈, 因此进程到内核栈的映射是唯一的, 那么指向内核栈的指针通常保存在一个特别保留的寄存器中(多数情况下是esp)
-
current给出了当前进程进程描述符task_struct的地址,该地址往往通过current_thread_info来确定
current = current_thread_info()->task
因此我们的关键就是current_thread_info的实现了,即如何通过esp栈指针来获取当前在CPU上正在运行进程的thread_info结构。
早期的版本中,不需要对64位处理器的支持,所以,内核通过简单的屏蔽掉esp的低13位有效位就可以获得thread_info结构的基地址了。
我们在下面对比了,获取正在运行的进程的thread_info的实现方式
架构 版本 定义链接 实现方式 思路解析 x86 3.14 current_thread_info(void) return (struct thread_info *)(sp & ~(THREAD_SIZE - 1)); 屏蔽了esp的低十三位,最终得到的是thread_info的地址 x86 3.15 current_thread_info(void) ti = (void *)(this_cpu_read_stable(kernel_stack) + KERNEL_STACK_OFFSET - THREAD_SIZE); x86 4.1 current_thread_info(void) (struct thread_info *)(current_top_of_stack() - THREAD_SIZE); 早期版本
当前的栈指针(current_stack_pointer == sp)就是esp,
THREAD_SIZE为8K,二进制的表示为0000 0000 0000 0000 0010 0000 0000 0000。
~(THREAD_SIZE-1)的结果刚好为1111 1111 1111 1111 1110 0000 0000 0000,第十三位是全为零,也就是刚好屏蔽了esp的低十三位,最终得到的是thread_info的地址。
进程最常用的是进程描述符结构task_struct而不是thread_info结构的地址。为了获取当前CPU上运行进程的task_struct结构,内核提供了current宏,由于task_struct *task在thread_info的起始位置,该宏本质上等价于current_thread_info()->task,在include/asm-generic/current.h中定义:
- 1
- 2
- 1
- 2
这个定义是体系结构无关的,当然linux也为各个体系结构定义了更加方便或者快速的current
分配和销毁thread_info
进程通过alloc_thread_info_node函数分配它的内核栈,通过free_thread_info函数释放所分配的内核栈。
# if THREAD_SIZE >= PAGE_SIZE static struct thread_info *alloc_thread_info_node(struct task_struct *tsk, int node) { struct page *page = alloc_kmem_pages_node(node, THREADINFO_GFP, THREAD_SIZE_ORDER); return page ? page_address(page) : NULL; } static inline void free_thread_info(struct thread_info *ti) { free_kmem_pages((unsigned long)ti, THREAD_SIZE_ORDER); } # else static struct kmem_cache *thread_info_cache; static struct thread_info *alloc_thread_info_node(struct task_struct *tsk, int node) { return kmem_cache_alloc_node(thread_info_cache, THREADINFO_GFP, node); } static void free_thread_info(struct thread_info *ti) { kmem_cache_free(thread_info_cache, ti); }
其中,THREAD_SIZE_ORDER宏的定义请查看
架构 版本 定义链接 实现方式 思路解析 x86 4.5 arch/x86/include/asm/page_32_types.h, line 20 define THREAD_SIZE_ORDER 1 __get_free_pages函数分配2个页的内存(它的首地址是8192字节对齐的) x86_64 4.5 arch/x86/include/asm/page_64_types.h, line 10 define THREAD_SIZE_ORDER (2 + KASAN_STACK_ORDER)
-
基于内核栈切换的进程切换
2021-07-28 10:17:35修改fork(),由于是基于内核栈的切换,所以进程需要创建出能完成内核栈切换的样子。 修改PCB,即task_struct结构,增加相应的内容域,同时处理由于修改了task_struct所造成的影响。 用修改后的Linux写在开头
由于本人汇编基础基本为0,所以吐血花了将近10小时,整理了网上所有相关的知识点,彻底搞清楚了linux系统进程的问题。博客同样适用于汇编基础不好的同学,讲的非常清楚,请耐心观看,相信你也肯定大有收获
这里是个人写的完整源码哈工大操作系统课程及实验完结体会
实验目的
深入理解进程和进程切换的概念; 综合应用进程、CPU管理、PCB、LDT、内核栈、内核态等知识解决实际问题; 开始建立系统认识。
实验内容
本次实验包括如下内容:
- 编写汇编程序switch_to:
- 完成主体框架;
- 在主体框架下依次完成PCB切换、内核栈切换、LDT切换等;
- 修改fork(),由于是基于内核栈的切换,所以进程需要创建出能完成内核栈切换的样子。
- 修改PCB,即task_struct结构,增加相应的内容域,同时处理由于修改了task_struct所造成的影响。
- 用修改后的Linux 0.11仍然可以启动、可以正常使用。
总览
本实验可以分为两个主要部分:
一部分是对应switch_to五段论中的中间段——内核栈的切换,主要是对switch_to代码的修改。
(原来的switch_to是基于TSS切换和长跳转指令来切换到下一个线程);另一部分则在更改进程切换方式的情况下,如何修改fork()使其配合新的进程切换方式。
(因为fork()需要在新建一个子线程后,切换到子线程执行,而切换方式在上一部分改变了)本文旨在将实现的整个流程讲清楚,在switch_to的注释和内核栈视角下的五段论务必详细观看,在理解整个流程起的作用非常大。
寄存器介绍
-
eip,用来存储CPU要读取指令的地址,CPU通过EIP寄存器读取即将要执行的指令
-
ebp是基址指针寄存器;处理栈帧,即处理 ebp 寄存器
-
esp是堆栈指针寄存器;这里已经进入内核态,所示是内核栈指针
一、编写switch_to(),40%
新的switch_to()函数是系统调用函数,所以要将函数重写在汇编文件system_call.s。这个函数依次主要完成如下功能:
首先我们要明确以下几点:
- 由于是 C 语言调用汇编,所以需要首先在汇编中处理栈帧,即处理 ebp 寄存器。这个需要不错的汇编基础,可以参考这里,讲的很清晰。
- 接下来要取出表示下一个进程 PCB 的参数,并和 current 做一个比较,如果等于 current,则什么也不用做。不等于 current,就开始进程切换
- 进程切换
- 完成 PCB 的切换
- TSS 中的内核栈指针的重写
- 内核栈的切换
- LDT 的切换以及 PC 指针(即 CS:EIP)的切换
以下代码的p,pnext都是PCB的首地址,也就是sched.h里的task_struct就是PCB,如果看不明白偏移的话,可以看看task_struct是长什么样的。而且,查看task_struct可知,tss_struct在task_struct的最后一个位置。
每个进程需要有自己的 LDT,地址分离地址还是必须要有的,而进程切换必然要涉及到 LDT 的切换。
switch_to(pnext, LDT(next))先给出一个大体框架
switch_to: pushl %ebp !这两句作用是把c函数整个压入栈, movl %esp,%ebp !函数的参数和局部变量都是通过ebp的值加上一个偏移量来访问 pushl %ecx !接下来会用到这三个寄存器 pushl %ebx pushl %eax movl 8(%ebp),%ebx !取出从右向左第二个参数pnext,放入ebx寄存器中 cmpl %ebx,current !比较pnext和current,与之前的pnext初始化为current对应 je 1f !如果相等,直接跳过切换过程 ! 切换PCB ! ... ! TSS中的内核栈指针的重写 ! ... ! 切换内核栈 ! ... ! 切换LDT ! ... movl $0x17,%ecx mov %cx,%fs ! 和后面的 clts 配合来处理协处理器,由于和主题关系不大,此处不做论述 cmpl %eax,last_task_used_math jne 1f clts 1: popl %eax !这四个寄存器是一开始进入函数保存数据的寄存器以及通用寄存器 popl %ebx popl %ecx popl %ebp ret
下面逐条解释:
1.PCB的切换
PCB 的切换可以采用下面两条指令,由上面代码可知,其中ebx是从参数中取出来的下一个进程的 PCB 指针
// PCB的切换 movl %ebx,%eax !movl的用法:把32位的EAX寄存器值传送给32位的EBX寄存器值 xchgl %eax,current !xchgl的用法:交换eax和current的值 /*ebx是下一个进程的PCB首地址,current是当前进程PCB首地址*/
经过这两条指令以后
- eax 指向现在的当前进程
- ebx指向下一个进程
- 全局变量 current 也指向下一个进程
2.TSS 中的内核栈指针的重写
由于CPU原有的CPU机制,TSS结构仍需要保留,但在切换进程时可以让所有进程共用tss0。现在ebx为pnext,即PCB位置。加上4K后即为内核栈的位置。所以在tss0的ESP0偏移位置就保存了内核栈的指针。
重写可以用下面三条指令完成,其中宏
struct tss_struct *tss = &(init_task.task.tss);
也是定义了一个全局变量,和 current 类似,用来指向那一段 0 号进程的 TSS 内存。此时唯一的tss的目的就是:在中断处理时,能够找到当前进程的内核栈的位置。movl tss,%ecx !把tss存到ecx当中 addl $4096,%ebx !现在ebx为pnext,即PCB位置。加上4K后即为内核栈的位置 movl %ebx,ESP0(%ecx) !把内核栈的位置存入tss
定义 ESP0 = 4 是因为 TSS 中内核栈指针 esp0 就放在偏移为 4 的地方,看一看 tss 的结构体定义就明白了
加入tss的定义:
extern struct tss_struct* tss = &(init_task.task.tss);
3.内核栈的切换
切换内核栈就是将寄存器esp的值放在PCB中,再从下一个进程的PCB中取出保存的内核栈栈顶放在当前的esp中。
首先要在task_struck中添加kernelstack的声明:
// 在 include/linux/sched.h 中 struct task_struct { long state; long counter; long priority; long kernelstack; //第4个位置,偏移为12 //......
在init_task中添加kernelstack的初始值
由#define INIT_TASK { 0,15,15, 0,{{},},0,...
改为
#define INIT_TASK { 0,15,15,PAGE_SIZE+(long)&init_task, 0,{{},},0,...,
在system_call.s中定义一些常量:
ESP0 =4 KERNEL_STACK = 12 #添加实验指导需要的常量 state = 0 # these are offsets into the task-struct. counter = 4 priority = 8 kernelstack = 12 # 添加定义 signal = 16 sigaction = 20 blocked = (33*16+4) #由于添加了一个long,所以偏移量加4
然后加入切换内核栈指针的代码:
- eax 指向现在的当前进程
- ebx指向下一个进程
- 全局变量 current 也指向下一个进程
# KERNEL_STACK = 12 movl %esp,KERNEL_STACK(%eax) # 将当前栈顶指针存到当前PCB的kernelstack位置 movl 8(%ebp),%ebx #!取出从右向左第二个参数pnext,放入ebx寄存器中。再取一下ebx,因为前面修改过ebx的值 movl KERNEL_STACK(%ebx),%esp #将下一个进程的PCB中的kernelstack取出,赋给esp,完成切换
4.LDT的切换
movl 12(%ebp),%ecx #负责取出对应 LDT(next)的那个参数 lldt %cx #指令 lldt %cx 负责修改 LDTR 寄存器 #finish
5.PC 指针(即 CS:EIP)的切换
关于 PC 的切换,和前面论述的一致,依靠的就是 switch_to 的最后一句指令 ret。
这里还有一个地方需要格外注意,那就是 switch_to 代码中在切换完 LDT 后的两句,即:
! 切换 LDT 之后 movl $0x17,%ecx mov %cx,%fs
FS的作用。通过FS操作系统才能访问进程的用户态内存。这里LDT切换完成意味着切换到了新的用户态地址空间,所以需要重置FS。
二、修改fork.c——内核栈的复制加切换
准则:把进程的用户栈和内核栈通过内核栈中的 SS:ESP,CS:IP 关联在一起,方式:压栈。但由于 fork() 这个叉子的含义就是要让父子进程共用同一个代码、数据和堆栈,故为内核栈的复制加切换。
不难想象,对 fork() 的修改就是对子进程的内核栈的初始化,在 fork() 的核心实现 copy_process 中,
p = (struct task_struct *) get_free_page(); //创建PCB
用来完成申请一页内存作为子进程的 PCB,而 p 指针加上页面大小就是子进程的内核栈位置,所以语句long * krnstack; krnstack = (long *) (PAGE_SIZE + (long) p); //创建内核栈
就可以找到子进程的内核栈位置,接下来就是初始化 krnstack 中的内容了。
*(--krnstack) = ss & 0xffff; //用户态的位置 *(--krnstack) = esp; //创建用户栈(和父进程共用栈) *(--krnstack) = eflags; *(--krnstack) = cs & 0xffff; *(--krnstack) = eip; //父进程执行中断后的位置
这五条语句就完成了上图所示的那个重要的关联,因为其中 ss,esp 等内容都是 copy_proces() 函数的参数,这些参数来自调用 copy_proces() 的进程的内核栈中,就是父进程的内核栈中,所以上面给出的指令不就是将父进程内核栈中的前五个内容拷贝到子进程的内核栈中,图中所示的关联不也就是一个拷贝吗?*(--krnstack) = ds & 0xffff; *(--krnstack) = es & 0xffff; *(--krnstack) = fs & 0xffff; *(--krnstack) = gs & 0xffff; *(--krnstack) = esi; *(--krnstack) = edi; *(--krnstack) = edx; // 用户栈的消息 *(--krnstack) = (long) first_return_kernel; *(--krnstack) = ebp; *(--krnstack) = ecx; *(--krnstack) = ebx; *(--krnstack) = 0; p->kernelstack = krnstack;
接下来的工作就需要和 switch_to 接在一起考虑了,故事从哪里开始呢?回顾一下前面给出来的 switch_to,应该从 “切换内核栈” 完事的那个地方开始,现在到子进程的内核栈开始工作了,接下来做的四次弹栈以及 ret 处理使用的都是子进程内核栈中的东西,
1: popl %eax //这四个寄存器是一开始进入保存所用的寄存器以及通用寄存器 popl %ebx popl %ecx popl %ebp ret
为了能够顺利完成这些弹栈工作,子进程的内核栈中应该有这些内容,所以需要对 krnstack 进行初始化:
*(--krnstack) = ebp; *(--krnstack) = ecx; *(--krnstack) = ebx; // 这里的 0 最有意思。 *(--krnstack) = 0;
回答问题:
(1)子进程第一次执行时,eax=?为什么要等于这个数?哪里的工作让eax 等于这样一个数?
- eax =0,为了与父进程区分开 copy_process(),成功初始化进程copy_process后赋值eax得到
(2)这段代码中的 ebx和 ecx 来自哪里,是什么含义,为什么要通过这些代码将其写到子进程的内核栈中?
- 让eax=0 这段代码中的ebx和ecx来自copy_process()的形参,是段寄存器。fork函数决定,让父子的内核栈在初始化时完全一致
(3)这段代码中的 ebp 来自哪里,是什么含义,为什么要做这样的设置?可以不设置吗?为什么?
- ebp是用户栈地址,一定要设置,不设置子进程就没有用户栈了
1: popl %eax //这四个寄存器是一开始进入保存所用的寄存器以及通用寄存器 popl %ebx popl %ecx popl %ebp ret <——————————
现在到了 ret 指令了,这条指令要从内核栈中弹出一个 32 位数作为 EIP 跳去执行,所以需要弄一个函数地址(仍然是一段汇编程序,所以这个地址是这段汇编程序开始处的标号)并将其初始化到栈中。我们弄的一个名为 first_return_from_kernel 的汇编标号,然后可以用语句 *(–krnstack) = (long) first_return_from_kernel; 将这个地址初始化到子进程的内核栈中,现在执行 ret 以后就会跳转到 first_return_from_kernel 去执行了。
想一想 first_return_from_kernel 要完成什么工作?
PCB 切换完成、内核栈切换完成、LDT 切换完成,接下来应该那个“内核级线程切换五段论”中的最后一段切换了,即完成用户栈和用户代码的切换,依靠的核心指令就是 iret,当然在切换之前应该恢复一下执行现场,主要就是 eax,ebx,ecx,edx,esi,edi,gs,fs,es,ds 等寄存器的恢复。
下面给出了 first_return_from_kernel 的核心代码,当然 edx 等寄存器的值也应该先初始化到子进程内核栈,即 krnstack 中。
first_return_from_kernel: popl %edx popl %edi popl %esi popl %gs popl %fs popl %es popl %ds iret
最后别忘了将存放在 PCB 中的内核栈指针修改到初始化完成时内核栈的栈顶,即:
p->kernelstack = stack;
整体的栈空间内存图如下所示:
三、修改sched.h和sched.c
之前的进程控制块(pcb)中是没有保存内核栈信息的寄存器的,所以需要在sched.h中的task_struct(也就是pcb)中添加kernelstack,而宏 KERNEL_STACK 就是你加的那个位置,但kernelstack 千万不要放置在 task_struct 中的第一个位置,有结构体硬编码的要求,要放在其他位置,然后修改 kernal/system_call.s 中的那些硬编码就可以了。
task_struct 的定义:
// 在 include/linux/sched.c 中 struct task_struct { long state; long counter; long priority; long kernelstack; ... //......
基于堆栈的切换程序要做到承上启下:
- 承上:基于堆栈的切换,要用到当前进程(current指向)与目标进程的PCB,当前进程与目标进程的内核栈等
- Linux 0.11 进程的内核栈和该进程的 PCB 在同一页内存上(一块 4KB 大小的内存),其中 PCB 位于这页内存的低地址,栈位于这页内存的高地址
- 启下:要将next传递下去,虽然 TSS(next)不再需要了,但是 LDT(next)仍然是需要的。
修改 schedule() 函数(在 kernal/sched.c 中),代码如下:
if ((*p)->state == TASK_RUNNING && (*p)->counter > c) c = (*p)->counter, next = i, pnext = *p; //....... switch_to(pnext, _LDT(next)); //修改switch_to,实验中的LDT缺少了一个下划线,这里要给它补上,不然会报错 //同时在schedule中要添加一个全局变量 struct task_struct *pnext = &(init_task.task);
感谢
-
Linux内核栈溢出分析
2021-05-11 01:48:12Linux系统进程运行分为用户态和内核态,进入内核态之后使用的是内核栈,作为基本的安全机制,用户程序不能直接访问内核栈,所以尽管内核栈属于进程的地址空间,但与用户栈是分开的。内核栈需要方便快捷...8种机械键盘轴体对比
本人程序员,要买一个写代码的键盘,请问红轴和茶轴怎么选?
由于内核栈的大小是有限的,就会有发生溢出的可能,比如调用嵌套太多、参数太多都会导致内核栈的使用超出设定的大小。本文分析内核栈溢出。
Linux系统进程运行分为用户态和内核态,进入内核态之后使用的是内核栈,作为基本的安全机制,用户程序不能直接访问内核栈,所以尽管内核栈属于进程的地址空间,但与用户栈是分开的。内核栈需要方便快捷的访问用户态进程信息thread_info,这部分数据就压在内核栈的底部,大小默认为16Kb。如下图所示:
内核栈溢出的结果往往是系统崩溃,因为溢出会覆盖掉本不该触碰的数据,首当其冲的就是thread_info — 它就在内核栈的底部,内核栈是从高地址往低地址生长的,一旦溢出首先就破坏了thread_info,thread_info里存放着指向进程的指针等关键数据,迟早会被访问到,那时系统崩溃就是必然的事。
内核栈溢出导致的系统崩溃有时会被直接报出来,比如你可能会看到:...
Call Trace:
[] ? warn_slowpath_common+0x87/0xc0
BUG: unable to handle kernel NULL pointer dereference at 00000000000009e8
IP: [] print_context_stack+0xad/0x140
PGD 5fdb8ae067 PUD 5fdbee9067 PMD 0
Thread overran stack, or stack corrupted
Oops: 0000 [#1] SMP
...
但更多的情况是不直接报错,而是各种奇怪的panic。在分析vmcore的时候,它们的共同点是thread_info被破坏了。
以下是一个实例,注意在task_struct中stack字段直接指向内核栈底部也就是thread_info的位置,我们看到thread_info显然被破坏了:cpu的值大得离谱,而且指向task的指针与task_struct的实际地址不匹配:crash64> struct task_struct ffff8800374cb540
struct task_struct {
state = 2,
stack = 0xffff8800bae2a000,
...
crash64> thread_info 0xffff8800bae2a000
struct thread_info {
task = 0xffff8800458efba0,
exec_domain = 0xffffffff,
flags = 0,
status = 0,
cpu = 91904,
preempt_count = 0,
...
作为一种分析故障的手段,可以监控内核栈的大小和深度,方法如下:# mount -t debugfs nodev /sys/kernel/debug
# echo 1 > /proc/sys/kernel/stack_tracer_enabled
然后检查下列数值,可以看到迄今为止内核栈使用的峰值和对应的backtrace:# cat /sys/kernel/debug/tracing/stack_max_size
# cat /sys/kernel/debug/tracing/stack_trace
-
Linux内核栈和中断栈
2019-08-09 11:47:21内核栈 #define MIN_THREAD_SHIFT (14 + KASAN_THREAD_SHIFT) #define THREAD_SIZE (UL(1) << THREAD_SHIFT) union thread_union { #ifndef CONFIG_THREAD_INFO_IN_TASK struct thread_info ... -
Linux 2.6.32的内核栈和用户空间栈关系
2021-05-12 02:00:571.进程的堆栈内核在创建进程的时候,在创建task_struct的...当进程在内核空间时,cpu堆栈指针寄存器里面的内容是内核栈空间地址,使用内核栈。2.进程用户栈和内核栈的切换当进程因为中断或者系统调用而陷入内核态... -
Linux 内核栈溢出分析
2021-05-11 04:59:33Linux 系统进程运行分为 用户态 和 内核态,进入内核态之后使用的是内核栈,作为基本的安全机制,用户程序不能直接访问内核栈,所以尽管内核栈属于进程的地址空间,但与用户栈是分开的。内核栈需要... -
内核态与用户态、内核空间与用户空间、内核栈与用户栈
2022-05-27 20:33:501.内核态与用户态、内核空间与用户空间、内核栈与用户栈 1.1 内核态与用户态 内核态和用户态是指两种运行状态,这两种状态的设置是为了区分不同运行权限 CPU的两种运行状态 操作系统的运行级别 进程的运行状态 以CPU... -
进程内核栈、用户栈
2018-05-03 16:01:381.进程的堆栈 内核在创建进程的时候,在创建task_struct的同事,会为进程创建...当进程在内核空间时,cpu堆栈指针寄存器里面的内容是内核栈空间地址,使用内核栈。 2.进程用户栈和内核栈的切换 当进程因为中... -
linux用户栈和内核栈解析
2020-04-20 21:25:55每个进程都有自己的堆栈,内核在创建一个新的进程时,在创建进程控制块task_struct的同时,也为进程创建 自己堆栈。一个进程 有2个堆栈,用户堆栈和系统堆栈;用户堆栈的空间指向用户地址空间,内核堆栈的空间 ... -
用户栈和内核栈的区别
2021-03-28 23:19:31用户栈和内核栈的区别 -
内核栈和用户栈
2019-03-13 15:50:14每个进程有两个栈:用户栈、内核栈 用户栈在用户地址空间中,内核栈在内核地址空间中。 用户栈 用户栈不难理解,用户栈是用户空间中的一块区域,用于保存用户进程的子程序间相互调用的参数、返回值以及局部变量... -
哈工大操作系统实验四——基于内核栈切换的进程切换(极其详细)
2020-12-12 23:59:22目录总览第一部分、switch_to相关一、改写switch_to二、配合switch_to...to四、PCB结构如下第二部分、fork()相关一、为什么要修改fork()二、内核栈视角下的五段论2.1中断进入阶段(第一阶段)2.2 调用schedule引起PCB切 -
轻量级进程 +SS寄存器和ESP寄存器+怎么理解linux内核栈?+用户态/内核态、用户栈/内核栈
2020-06-28 17:49:15SS寄存器:包含当前程序栈的段 CS:代码段寄存器 DS:数据段寄存器; SS:堆栈段寄存器。 CS:存放当前正在运行的程序代码所在段的段基值。 DS:存放数据段的段基值。 SS:存放堆栈段的段基值。 cs代码... -
linux 进程内核栈
2020-11-26 12:50:19每个task的栈分成用户栈和内核栈两部分,进程内核栈在kernel中的定义是: union thread_union { struct thread_info thread_info; unsigned long stack[THREAD_SIZE/sizeof(long)]; }; 每个ta -
【哈工大李治军】操作系统课程笔记5:多线程、用户线程和内核线程 +【实验 5】基于内核栈切换的进程切换
2022-04-20 19:05:301、用户级线程 在多进程任务中,引出了一个问题: 能否只切换任务,而不切换映射表? 从而引出了线程这个概念。我们能否只是让执行指令发生变化,而内存不发生变化?...(2)线程栈 当线程之间使用yield()的时 -
进程的用户栈和内核栈
2020-04-10 10:37:00这是因为,当进程在用户态运行时,使用的是用户栈,当进程陷入到内核态时,内核栈保存进程在内核态运行的相关信心,但是一旦进程返回到用户态后,内核栈中保存的信息无效,会全部恢复,因此每次进程从用户态陷入内核... -
操作系统实验五 基于内核栈切换的进程切换(哈工大李治军)
2021-09-12 21:22:05实验5 基于内核栈切换的进程切换 实验目的 深入理解进程和进程切换的概念; 综合应用进程、CPU 管理、PCB、LDT、内核栈、内核态等知识解决实际问题; 开始建立系统认识。 实验内容 现在的 Linux 0.11 采用 TSS 和... -
Linux内核栈调试
2021-11-21 15:15:23Linux内核栈调试dump_stack现象原因分析oops信息寄存器信息应用层信息调用栈关系objdump反汇编内核编译反汇编命令分析 Linux内核驱动调试经常遇到定位困难的问题,除了在代码之间添加log(printk/dev_info等)... -
【操作系统】实验楼实验五——基于内核栈的进程切换
2022-03-17 00:22:53在内核栈指针重写指令中有宏定义ESP0,所以在上面需要提前定义好 ESP0 = 4,(是因为TSS中内核栈指针ESP0放在偏移为4的地方) 修改以后的fork()要使得父子进程共享同一块内存空间、堆栈和数据代码块。 首先需要将... -
哈工大操作系统实验4---基于内核栈切换的进程切换
2021-12-02 18:42:38前置知识 关于栈桢 关于栈栈帧详解https://blog.csdn.net/ylyuanlu/article/details/18947951 ...然后就要进入到A进程的内核态去执行相应的中断服务例程,在这期间突然不小心发生一个意外,程序的执行由于缺