浅谈栈中埋了一个坑:当用户态线程system call时,在哪里可以获取到该线程的内核栈地址呢?

答案是tss。

本文内容部分源于x86体系下linux中的任务切换与TSS

什么是tss

tss(task status segment)就是一个段内存。tss中存储了相关信息,例如内核栈的地址。

tss的内存地址存放在tr寄存器中。

背景知识

x86的分段机制。

强烈推荐《系统虚拟化》2.2.3节对于分段机制的介绍。这里mark一下我的笔记:

  1. CS、DS、 SS等段寄存器,存放的是当前程序的各个段的段选择符。为了加速段描述符的访问,x86在段寄存器后增加了一个程序不可见的段描述符寄存器。

  2. 为了加速对GDT和LDT的访问,x86提供了GDTR寄存器和LDTR寄存器。

    • GDTR:包括一个 32 位的基地址(base) 和一个 16 位长度(limit)

    • LDTR: 结构同段寄存器(包括对程序不可见的段描述符寄存器)

  3. tr 寄存器是段寄存器。

tss 介绍

tss保存不同特权级别下任务所使用的寄存器,特别重要的是esp,因为比如中断后,涉及特权级切换时(一个任务切换),首先要切换栈,这个栈显然是内核栈,那么如何找到该栈的地址呢,这需要从tss段中得到,这样后续的执行才有所依托(在x86机器上,c语言的函数调用是通过栈实现的)。只要涉及地特权环到高特权环的任务切换,都需要找到高特权环对应的栈,因此需要esp2,esp1,esp0起码三个esp,然而linux只使用esp0。

Intel sdm vs linux kernel

intel

intel的建议:为每一个进程准备一个独立的tss段,进程切换的时候切换tr寄存器使之指向该进程对应的tss段,然后在任务切换时(比如涉及特权级切换的中断)使用该段保留所有的寄存器。

linux kernel

linux的做法:

  1. linux没有为每一个进程都准备一个tss段,而是每一个cpu使用一个tss段,tr寄存器保存该段。进程切换时,只更新唯一tss段中的esp0字段,esp0保存新进程的内核栈地址。
  2. linux的tss段中只使用esp0和iomap等字段,不用它来保存寄存器,在一个用户进程被中断进入ring0的时候,tss中取出esp0,然后切到esp0,其它的寄存器则保存在esp0指示的内核栈上,而不保存在tss中。
  3. 结果,linux中每一个cpu只有一个tss段,tr寄存器永远指向它。符合x86处理器的使用规范,但不遵循intel的建议,这样的后果是开销更小了,因为不必切换tr寄存器了。

参考资料:

  1. x86体系下linux中的任务切换与TSS
  2. 13-任务状态段(TSS)
  3. Why doesn’t Linux use the hardware context switch via the TSS?