系统调用如何将用户态的执行和内核态的执行串起来
两个重要的成员变量
struct thread_info thread_info;
void *stack;
用户态函数栈
方式:程序的执行往往是一个函数调用另一个函数,通过栈来进行,其汇编语言的代码就是指令跳转,通过 JMP + 参数 + 返回地址 调用函数
栈: 栈是一个从高地址到低地址,往下增长的结构,也就是上面是栈底,下面是栈顶,入栈和出栈的操作都是从下面的栈顶开始
32位栈结构
- 栈帧包含 前一个帧的 EBP + 局部变量 + N个参数 + 返回地址
- ESP(Extended Stack Pointer)是栈顶指针寄存器
入栈操作 Push 和出栈操作 Pop 指令,会自动调整 ESP 的值 - EBP(Extended Base Pointer)是栈基地址指针寄存器,指向当前栈帧的最底部
- 返回值保存在 EAX 中
- A 调用 B
A 的栈帧:包含 A 函数的局部变量、调用 B 的时候要传给它的参数、返回 A 时的返回地址
B 的栈帧:A 栈帧的栈底位置(EBP)用于获取传入B的参数、B 的局部变量等等 - B 返回
返回值会保存在 EAX 寄存器中
从栈中弹出返回地址,将指令跳转回去,参数也从栈中弹出,然后继续执行 A
64位栈结构
- rax 用于保存函数调用的返回结果
- rsp栈顶指针寄存器,指向栈顶位置
堆栈的 Pop 和 Push 操作会自动调整 rsp - rbp栈基指针寄存器,,指向当前栈帧的起始位置
- 参数传递
rdi、rsi、rdx、rcx、r8、r9 这 6 个寄存器,用于传递存储函数调用时的 6 个参数。如果超过 6 的时候,还是需要放到栈里面
内核态函数栈
成员变量 stack,内核栈
Linux 为每个 task 分配了内核栈, 32位(8K), 64位(16K)
32 位系统
arch/x86/include/asm/page_32_types.h
一个 PAGE_SIZE 是 4K,左移一位就是乘以 2,也就是 8K
#define THREAD_SIZE_ORDER 1
#define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER)
64 位系统
arch/x86/include/asm/page_64_types.h
PAGE_SIZE 的基础上左移两位,也即 16K,并且要求起始地址必须是 8192 的整数倍
#ifdef CONFIG_KASAN
#define KASAN_STACK_ORDER 1
#else
#define KASAN_STACK_ORDER 0
#endif
#define THREAD_SIZE_ORDER (2 + KASAN_STACK_ORDER)
#define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER)
内核栈结构
最低位置是thread_info 结构,这个结构是对 task_struct 结构的补充,task_struct 结构庞大且通用,但不同的体系结构就需要保存不同的东西,所以与体系结构有关的,都放在 thread_info 里面。
thread_union
include/linux/sched.h
有个union将 thread_info 和 stack 放在一起
union thread_union {
#ifndef CONFIG_THREAD_INFO_IN_TASK
struct thread_info thread_info;
#endif
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
thread_info保存了线程所需的所有特定处理器的信息, 以及通用的task_struct的指针(注 linux-4.13.16版本x86的struct thread_info只有unsigned long flags; )
物理内存中存放两种数据结构的方式
图片来自 https://blog.csdn.net/gatieme/article/details/51577479
- 0x015bfff - 0x015b000 = 0x1000 = 4096 = 4k
- 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存储在进程内核栈的最低端
pt_regs
内核栈的最高地址端,存放的是另一个结构 pt_regs
32 位和 64 位的定义不一样。
#ifdef __i386__
struct pt_regs {
unsigned long bx;
unsigned long cx;
unsigned long dx;
unsigned long si;
unsigned long di;
unsigned long bp;
unsigned long ax;
unsigned long ds;
unsigned long es;
unsigned long fs;
unsigned long gs;
unsigned long orig_ax;
unsigned long ip;
unsigned long cs;
unsigned long flags;
unsigned long sp;
unsigned long ss;
};
#else
struct pt_regs {
unsigned long r15;
unsigned long r14;
unsigned long r13;
unsigned long r12;
unsigned long bp;
unsigned long bx;
unsigned long r11;
unsigned long r10;
unsigned long r9;
unsigned long r8;
unsigned long ax;
unsigned long cx;
unsigned long dx;
unsigned long si;
unsigned long di;
unsigned long orig_ax;
unsigned long ip;
unsigned long cs;
unsigned long flags;
unsigned long sp;
unsigned long ss;
/* top of stack page */
};
#endif
当系统调用从用户态到内核态的时候,首先要做的第一件事情,就是将用户态运行过程中的 CPU 上下文保存起来,其实主要就是保存在这个结构的寄存器变量里。这样当从内核系统调用返回的时候,才能让进程在刚才的地方接着运行下去。
参见 06 系统调用 https://blog.csdn.net/leacock1991/article/details/106773065,压栈的值的顺序和 struct pt_regs 中寄存器定义的顺序是一样的
通过 task_struct 找内核栈
直接由 task_struct 内的 stack 直接得到指向 thread_info 的指针
\include\linux\sched\task_stack.h
static inline void *task_stack_page(const struct task_struct *task)
{
return task->stack;
}
通过 task_struct 得到相应的 pt_regs
\arch\x86\include\asm\processor.h
/*
* TOP_OF_KERNEL_STACK_PADDING reserves 8 bytes on top of the ring0 stack.
* This is necessary to guarantee that the entire "struct pt_regs"
* is accessible even if the CPU haven't stored the SS/ESP registers
* on the stack (interrupt gate does not save these registers
* when switching to the same priv ring).
* Therefore beware: accessing the ss/esp fields of the
* "struct pt_regs" is possible, but they may contain the
* completely wrong values.
*/
#define task_pt_regs(task) \
({ \
unsigned long __ptr = (unsigned long)task_stack_page(task); \
__ptr += THREAD_SIZE - TOP_OF_KERNEL_STACK_PADDING; \
((struct pt_regs *)__ptr) - 1; \
})
1、 从 task_struct 找到内核栈的最低位置(低地址), (unsigned long)task_stack_page(task);
2、 加上 THREAD_SIZE(高地址) 再减去TOP_OF_KERNEL_STACK_PADDING 就到了最后的位置
- 栈是一个从高地址到低地址,往下增长的结构
- TOP_OF_KERNEL_STACK_PADDING
- arch\x86\include\asm\
#ifdef CONFIG_X86_32
# ifdef CONFIG_VM86
# define TOP_OF_KERNEL_STACK_PADDING 16
# else
# define TOP_OF_KERNEL_STACK_PADDING 8
# endif
#else
# define TOP_OF_KERNEL_STACK_PADDING 0
#endif
32 位机器上是 8,其他是 0,CPU 用 ring 来区分权限,从而 Linux 可以区分内核态和用户态,涉及权限的改变,会压栈保存 SS、ESP 寄存器的,这两个寄存器共占用 8 个 byte
3、 强转为struct pt_regs再减一,就相当于减少了一个 pt_regs 的位置,就到了这个结构的首地址
通过内核栈找 task_struct
比较早的内核版本
通过 thread_info 这个结构获取
https://elixir.bootlin.com/linux/v4.5.7/source/arch/x86/include/asm/thread_info.h#L55
struct thread_info {
struct task_struct *task; /* main task structure */
__u32 flags; /* low level flags */
__u32 status; /* thread synchronous flags */
__u32 cpu; /* current CPU */
mm_segment_t addr_limit;
unsigned int sig_on_uaccess_error:1;
unsigned int uaccess_err:1; /* uaccess failed */
};
成员变量 task 指向 task_struct,常用 current_thread_info() 来获取 thread_info 进而通过变量task获取 task_struct。(v4.5.7版本实现如下,更早的不同版本实现会不一样)
https://elixir.bootlin.com/linux/v4.5.7/source/arch/x86/include/asm/thread_info.h#L164
static inline struct thread_info *current_thread_info(void)
{
return (struct thread_info *)(current_top_of_stack() - THREAD_SIZE);
}
current_top_of_stack() 的位置就是内核栈的最高位置(高地址),减去 THREAD_SIZE,就到了 thread_info 的起始地址(低地址)
不同内核版本实现不同
屏蔽了esp的低十三位,最终得到的是thread_info的地址
当前的栈指针(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的地址。
linux-4.13.16
thread_info变为
include/linux/thread_info.h
struct thread_info {
unsigned long flags; /* low level flags */
};
怎么获取当前运行中的 task_struct 呢?current_thread_info 有了新的实现方式。
current_thread_info 新的实现方式
include/linux/thread_info.h
#include <asm/current.h>
#define current_thread_info() ((struct thread_info *)current)
#endif
current 又是什么
arch/x86/include/asm/current.h
struct task_struct;
DECLARE_PER_CPU(struct task_struct *, current_task);
static __always_inline struct task_struct *get_current(void)
{
return this_cpu_read_stable(current_task);
}
#define current get_current
新的机制里面,每个 CPU 运行的 task_struct 不通过 thread_info 获取了,而是直接放在 Per CPU 变量里面了。
Per CPU 变量
多核情况下,CPU 是同时运行的,但是它们共同使用其他的硬件资源的时候,我们需要解决多个 CPU 之间的同步问题。
Per CPU 变量是内核中一种重要的同步机制,Per CPU 变量就是为每个 CPU 构造一个变量的副本,这样多个 CPU 各自操作自己的副本,互不干涉。
当前进程的变量 current_task 就被声明为 Per CPU 变量。
arch/x86/include/asm/current.h
DECLARE_PER_CPU(struct task_struct *, current_task);
DECLARE_PER_CPU(struct task_struct *, current_task);变量定义
arch/x86/kernel/cpu/common.c
DEFINE_PER_CPU(struct task_struct *, current_task) = &init_task;
系统刚刚初始化的时候,current_task 都指向 init_task,当某个 CPU 上的进程进行切换的时候,current_task 被修改为将要切换到的目标进程
要获取当前的运行中的 task_struct 的时候,需要调用 this_cpu_read_stable 进行读取。
/arch/x86/include/asm/percpu.h
#define this_cpu_read_stable(var) percpu_stable_op("mov", var)
总结
在用户态,32 位和 64 的传递参数的方式稍有不同,32 位的就是用函数栈,64 位的前 6 个参数用寄存器,其他的用函数栈。
在内核态,32 位和 64 位都使用内核栈,格式也稍有不同,主要集中在 pt_regs 结构上。
在内核态,32 位和 64 位的内核栈和 task_struct 的关联关系不同。32 位主要靠 thread_info,64 位主要靠 Per-CPU 变量。
参考资料:
趣谈Linux操作系统(极客时间)链接:
http://gk.link/a/10iXZ
Linux进程内核栈与thread_info结构详解–Linux进程的管理与调度(九):
https://blog.csdn.net/gatieme/article/details/51577479
欢迎大家来一起交流学习