程序锅

  • 首页
  • 分类
  • 标签
  • 归档
  • 关于

  • 搜索
基础知识 Etcd LeetCode 计算机体系结构 Kubernetes Containerd Docker 容器 云原生 Serverless 项目开发维护 ELF 深入理解程序 Tmux Vim Linux Kernel Linux numpy matplotlib 机器学习 MQTT 网络基础 Thrift RPC OS 操作系统 Clang 研途 数据结构和算法 Java 编程语言 Golang Python 个人网站搭建 Nginx 计算机通用技术 Git

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

发表于 2021-09-15 | 分类于 Linux Kernel | 0 | 阅读次数 2764

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. 巨人的肩膀

  1. 极客时间专栏《趣谈Linux操作系统》
  2. https://blog.csdn.net/gatieme/article/details/51383272
卷死我
dawnguo 微信支付

微信支付

dawnguo 支付宝

支付宝

  • 本文作者: dawnguo
  • 本文链接: /archives/105
  • 版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处!
# Linux # Linux Kernel
Linux Kernel | Linux task_struct 结构体概述
Linux Kernel | Linux 信号机制介绍
  • 文章目录
  • 站点概览
dawnguo

dawnguo

215 日志
24 分类
37 标签
RSS
Creative Commons
© 2018 — 2025 程序锅
0%