目录

Linux Kernel | Linux 函数栈(内核态和用户态)

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,同时将参数也从栈中弹出。

https://img.dawnguo.cn/Linux/aec865abccf0308155f4138cc905972e.jpg

1.1.2. 64 位操作系统的情况下

rax 用于保存函数调用的返回结果;栈顶指针寄存器变成了 rsp,指向栈顶位置,堆栈的 Pop 和 Push 操作会自动调整 rsp;栈基指针寄存器变成了 rbp,指向当前栈帧的起始位置。

改变比较多的是参数传递。rdi、rsi、rdx、rcx、r8、r9 这 6 个寄存器,用于传递存储函数调用时的 6 个参数。如果参数个数超过 6 个的时候,就需要将参数放到栈里面。

然而,前 6 个参数有时候需要进行寻址,但是如果在寄存器里面,是没有地址的,因而还是会放到栈里面,只不过这 6 个参数放到栈里面的操作是被调用函数做的,也就是将参数放到被调用函数的栈帧中。

https://img.dawnguo.cn/Linux/770b0036a8b2695463cd95869f5adec0.jpg

1.2. 内核态函数栈

可以看这篇推文中对内核栈的讲解 https://blog.csdn.net/gatieme/article/details/51383272

在程序的执行过程中,一旦调用了系统调用,那么就需要进入内核继续执行。跟在用户态下函数执行的过程类似,进程陷入到内核态执行时也有一个栈,我们称其为内核栈。

1
2
3
4
// include\linux\sched.h

struct thread_info    thread_info;
void  *stack;

Linux 给每个 task 都分配了内核栈。在 x86 32 系统上,内核栈的大小是 8K。在 x86 64 系统上,内核栈的大小一般是 16K,并且要求起始地址必须是 8192 的整数倍。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 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 长度之后的,如下图所示。

https://img.dawnguo.cn/Linux/image-20201103104824641.png

这段空间的最低位置,是一个 thread_info 数据结构,这个数据结构是对 task_struct 的补充。需要这个数据结构主要是因为 task_struct 通用,而 linux 需要考虑到不同体系结构,而不同体系结构会有一套自己需要保存的东西。所以往往与体系结构相关的内容都会被保存到 thread_info,也就说 thread_info 这个数据结构由不同的体系结构自己定义,可以查看 arch 目录下各体系结构对 thread_info 这个结构体的定义。

在内核代码中有一个 union,就是将 task_struct、thread_info 以及 stack 放到一起的。当然这个具体放不放在一起,得看宏定义的情况。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 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 中寄存器定义的顺序是一样的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

#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 的大小。那么就变成了内核栈栈底真正开始的地址了(请结合上图来看)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 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 位上得到了修正,因为都变成定长的了。

1
2
3
4
5
6
7
8
9
#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 的起始地址。
    1
    2
    3
    4
    5
    
    // 可在 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 就可以获取到了。
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    // 可在 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。

1
2
3
4
5
6
7
// 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。

1
2
3
4
5
6
7
8
_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 变量即可获取。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 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 这个结构体的首地址。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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 变量。

https://img.dawnguo.cn/Linux/82ba663aad4f6bd946d48424196e515c.jpeg

1.3. 巨人的肩膀

  1. 极客时间专栏《趣谈Linux操作系统》
  2. https://blog.csdn.net/gatieme/article/details/51383272