1. 函数栈
1.1. 用户态函数栈
在进程的内存空间里面,栈是一个从高地址到低地址,往下增长的结构,也就是上面是栈底,下面是栈顶,入栈和出栈的操作都是从下面的栈顶开始的。
1.1.1. 32 位操作系统的情况
CPU 里,ESP(Extended Stack Pointer)是栈顶指针寄存器,入栈操作 Push 和出栈操作 Pop 指令,会自动调整 ESP 的值。另外有一个寄存器 EBP(Extended Base Pointer),是栈基地址指针寄存器,指向当前栈帧的最底部。
当 A 调用 B 时,A 的栈帧里面包含了 A 函数的局部变量,调用 B 的时候要传给 B 的参数,然后是 B 返回 A 执行的地址。B 的栈帧将会先保存 A 栈帧的栈底位置,也就是 EBP 的值(在 B 函数里面获取 A 传进来的参数就是用这个指针获取的),之后是 B 的局部变量。
当 B 返回的时候,返回值会保存在 EAX 寄存器中,从栈中弹出返回地址,然后指令从这个返回地址开始继续执行 A,同时将参数也从栈中弹出。
1.1.2. 64 位操作系统的情况下
rax 用于保存函数调用的返回结果;栈顶指针寄存器变成了 rsp,指向栈顶位置,堆栈的 Pop 和 Push 操作会自动调整 rsp;栈基指针寄存器变成了 rbp,指向当前栈帧的起始位置。
改变比较多的是参数传递。rdi、rsi、rdx、rcx、r8、r9 这 6 个寄存器,用于传递存储函数调用时的 6 个参数。如果参数个数超过 6 个的时候,就需要将参数放到栈里面。
然而,前 6 个参数有时候需要进行寻址,但是如果在寄存器里面,是没有地址的,因而还是会放到栈里面,只不过这 6 个参数放到栈里面的操作是被调用函数做的,也就是将参数放到被调用函数的栈帧中。
1.2. 内核态函数栈
可以看这篇推文中对内核栈的讲解 https://blog.csdn.net/gatieme/article/details/51383272
在程序的执行过程中,一旦调用了系统调用,那么就需要进入内核继续执行。跟在用户态下函数执行的过程类似,进程陷入到内核态执行时也有一个栈,我们称其为内核栈。
// include\linux\sched.h
struct thread_info thread_info;
void *stack;
Linux 给每个 task 都分配了内核栈。在 x86 32 系统上,内核栈的大小是 8K。在 x86 64 系统上,内核栈的大小一般是 16K,并且要求起始地址必须是 8192 的整数倍。
// arch\x86\include\asm\page_32_types.h
#define THREAD_SIZE_ORDER 1
#define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER)
// arch\x86\include\asm\page_64_types.h
#ifdef CONFIG_KASAN
#ifdef CONFIG_KASAN_EXTRA
#define KASAN_STACK_ORDER 2
#else
#define KASAN_STACK_ORDER 1
#endif
#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 和 pt_regs 等数据结构,也就是说 THREAD_SIZE 的长度是指包含了 thread_info 和 pt_regs 长度之后的,如下图所示。
这段空间的最低位置,是一个 thread_info 数据结构,这个数据结构是对 task_struct 的补充。需要这个数据结构主要是因为 task_struct 通用,而 linux 需要考虑到不同体系结构,而不同体系结构会有一套自己需要保存的东西。所以往往与体系结构相关的内容都会被保存到 thread_info,也就说 thread_info 这个数据结构由不同的体系结构自己定义,可以查看 arch 目录下各体系结构对 thread_info 这个结构体的定义。
在内核代码中有一个 union,就是将 task_struct、thread_info 以及 stack 放到一起的。当然这个具体放不放在一起,得看宏定义的情况。
// include\linux\sched.h union thread_union { #ifndef CONFIG_ARCH_TASK_STRUCT_ON_STACK struct task_struct task; #endif #ifndef CONFIG_THREAD_INFO_IN_TASK struct thread_info thread_info; #endif unsigned long stack[THREAD_SIZE/sizeof(long)]; };
在内核栈的最高地址端,存放的是结构体 pt_regs。这个结构体也是跟体系结构相关的,x86 32 位和 64 位的结构体的定义就是不一样的,如下所示。
在系统调用的时候,从用户态切换到内核态的时候,首先要做的第一件事情就是将用户态运行过程中的 CPU 上下文保存起来,其实主要就是保存在这个结构的寄存器变量里。这样当从内核系统调用返回的时候,就可以从进程在刚才的地方继续运行下去。而在系统调用的过程中,压栈的顺序和 struct pt_regs 中寄存器定义的顺序是一样的。
#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
1.2.1. 通过 task_struct 找内核栈
下面的代码展示了如何通过 task_struct 找到内核栈。首先是通过函数 task_stack_page() 获取到 satck 的开始地址。之后这个位置加上 THREAD_SIZE - TOP_OF_KERNEL_STACK_PADDING,那么就变成了 pt_regs 结束的地址。最后,这个地址再减去 1,也就是减去 pt_regs 的大小。那么就变成了内核栈栈底真正开始的地址了(请结合上图来看)。
// arch\x86\include\asm\processor.h
#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; \
})
// include\linux\sched\task_stack.h
static inline void *task_stack_page(const struct task_struct *task)
{
return task->stack;
}
这里有个 TOP_OF_KERNEL_STACK_PADDING,定义如下所示。也就说,在 32 位机器上是 8 或者 16,而其他机器上是 0。这个主要是因为将 pt_regs 压栈会有两种情况,
-
一种是从用户态切换到内核态来说,因为涉及到权限的切换,所以会保存 SS、ESP 寄存器的值,这两个寄存器共占用 8 个 byte。
-
另一个是不涉及到权限的变换(用的都是一个栈),也就是不需要将这两个寄存器的值压栈了。那么,这样子在一次性取 pt_regs 整个结构体值的时候就会出现两种情况,一种是把 SS、ESP 压入之后的,那么另外一种是没有压入的。对于没压入的情况,假如直接取整个结构体的值的话可能会出错,因为额外需要的 8 个字节是使用接下去的内容。所以这边先预留 8 个字节,可以保证安全(当压入的当中没有 SS 或者 ESP,那么后头就是填充的 8 个字节了)。
这种情况,在 64 位上得到了修正,因为都变成定长的了。
#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
综上,我们就通过 task_struct 得到了内核栈的地址了。
1.2.2. 获取每个 CPU 上运行的 task_struct
-
之前是怎么获取正在 CPU 上运行的 task_struct 的
主要是通过 thread_info 来获取。
- 首先我们需要获取 current_thread_info(),那么会采用如下的方式。 current_top_of_stack() 获取到的位置就是内核栈的最高位置,减去 THREAD_SIZE,就到了 thread_info 的起始地址。
// 可在 linux 4.1-arch/x86/include/asm/thread_info.h 中找到 static inline struct thread_info *current_thread_info(void) { return (struct thread_info *)(current_top_of_stack() - THREAD_SIZE); }
- 之后,我们通过 current_thread_info()->task 来获取 task_struct 就可以获取到了。
// 可在 linux 4.1-arch/x86/include/asm/thread_info.h 中找到 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 */ };
-
现在是怎么获取正在 CPU 上运行的 task_struct 的
Per CPU 变量是内核中一种重要的同步机制,也就说每个 CPU 都有一个变量的副本,这样多个 CPU 各自操作自己的副本,互不干涉。
每个 CPU 运行的 task_struct 就是 Per CPU 变量,而负责声明是的是 DECLARE_PER_CPU(struct task_struct *, current_task)
这条语句。而 DECLARE_PER_CPU() 会在系统刚刚初始化的时候将,current_task 都指向 init_task。
// arch\x86\include\asm\current.h
struct task_struct;
DECLARE_PER_CPU(struct task_struct *, current_task);
// arch\x86\kernel\cpu\common.c
DEFINE_PER_CPU(struct task_struct *, current_task) = &init_task;
当某个 CPU 上的进程进行切换的时候,current_task 被修改为将要切换到的目标进程。比如,进程切换函数 __switch_to()
就会改变 current_task,从而保证 current_task 始终指向 CPU 当前正在运行的 task_struct。
_visible __notrace_funcgraph struct task_struct *
__switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{
......
this_cpu_write(current_task, next_p);
......
return prev_p;
}
因此,当我们想要获取 CPU 上运行的 task_struct 的时候,我们只需要使用 current 变量即可获取。
// arch\x86\include\asm\current.h
static __always_inline struct task_struct *get_current(void)
{
return this_cpu_read_stable(current_task);
}
#define current get_current()
// arch\x86\include\asm\percpu.h
#define this_cpu_read_stable(var) percpu_stable_op("mov", var)
通过上述的方式也可以直接获取 thread_info,但是需要注意的是,这个时候需要有 CONFIG_THREAD_INFO_IN_TASK 宏定义,也就说 thread_info 这个结构需要在 task_struct 中。我们查看 task_struct 的话发现也有同样的宏定义,并且 thread_info 这个结构体必须是 task_struct 的第一个成员变量。而 current 存的是当前正在运行进程的 task_struct 的首地址,那么这个地址同时也是 thread_info 这个结构体的首地址。
// include\linux\thread_info.h #ifdef CONFIG_THREAD_INFO_IN_TASK #include <asm/current.h> #define current_thread_info() ((struct thread_info *)current) #endif // include\linux\sched.h struct task_struct { #ifdef CONFIG_THREAD_INFO_IN_TASK /* * For reasons of header soup (see current_thread_info()), this * must be the first element of task_struct. */ struct thread_info thread_info; #endif ...... } // arch\x86\include\asm\thread_info.h struct thread_info { unsigned long flags; /* low level flags */ u32 status; /* thread synchronous flags */ };
下面总结一下 32 位和 64 位的用户态/内核态下栈的不同之处,左边是 32 位的,右边是 64 位的。
- 在用户态下,32 位和 64 的传递参数的方式稍有不同,32 位的就是用函数栈,64 位的前 6 个参数用寄存器,其他的用函数栈。
- 在内核态,32 位和 64 位都使用内核栈,格式也稍有不同,主要集中在 pt_regs 结构上。
- 在内核态,32 位和 64 位的内核栈和 task_struct 的关联关系不同。32 位主要靠 thread_info,64 位主要靠 Per-CPU 变量。
1.3. 巨人的肩膀
- 极客时间专栏《趣谈Linux操作系统》
- https://blog.csdn.net/gatieme/article/details/51383272