任务段

📅 2026/8/1 17:14:18 👁️ 阅读次数 📝 编程学习
任务段

任务段

任务和CPU的关系

CPU本身并没有线程 进程的概念,线程进程的实现是由操作系统实现的,一个程序执行,对于CPU而言,就是一个任务。
CPU里面的TSS存在,目的是让一个CPU可以执行多个任务。

什么是任务段

这里有一个问题 我们在使用调用门或者中断门提权时, SS 和 ESP会保存在 0环堆栈, 这里涉及到了换栈,在前面的学习我们简单的知道了,ESP0 SS0,其实来自TSS,那TSS是啥呢?老实说我先前并没有在意
TSS(Task State Segment),翻译过来也就是 任务状态段(任务段)

任务段的作用

TSS让一个CPU执行多个任务
TSS存在的意义,为了替换所有的寄存器
打个比方A任务在进行,现在切到B任务,此时TSS会保存A任务当前的寄存器状态,从B任务切回A任务,保存B寄存器状态,恢复A寄存器

除此之外我们最熟悉的作用,应该就是提权换栈
在调用门或者中断门提权需要换栈时, ESP0 和 SS0 来自于TSS

TSS的结构

首先我们需要明确TSS本质上就是一块内存,大小为104字节
可以保存通用寄存器、段描述符、提权对应的ESPSS
o_2109250300299-1
windows操作系统主要使用 ESP0SS0

TSS结构中一些需要注意的成员

CR3字段存储的是该任务所使用的页目录表(Page Directory)的物理基地址。


提一个问题,我们通过TSS段描述符,进行了任务切换,替换了全部的寄存器,当我们要返回原本的任务应该怎么返回?
这里涉及应该关键的成员Previous Task Link
返回到之前的任务,也就意味着要把之前任务的寄存器值重新替换回来,所以我们需要之前的TSS段描述符,Previous Task Link是上一个任务段描述符的选择子

CPU如何找到TSS

我们知道的段寄存器有 ES、GS、SS、FS、DS、TR、IDTR
其中TR段寄存器,就是用来确定TSS位置的
o_2109250300369-2

大致流程: TR段描述符在GDT表确定TSS段描述符,TSS段描述符扩展后加载到TR寄存器的后80位,根据Base 和 Limit 就可以确定TSS位置。

TSS段描述符结构

重点区分 系统段 + TYPE
o_2109250300409-3

构建TSS段描述符

XX00E9XX XXXX0068

TR寄存器读写

1.将TSS段描述符加载到TR寄存器

指令:LTR(特权指令)
说明:
LTR指令去装载,仅仅是改变TR寄存器的值(96位),并没有真正改变TSSLTR指令只能在系统层使用,加载后TSS段描述符会状态位会发生改变。

2.读TR寄存器

指令:STR
说明:如果用STR去读的话,只读了TR的16位,即选择子。

3.修改TR寄存器

在0环可以通过LTR指令去修改TR寄存器。
在3环可以通过CALL FAR或者JMP FAR指令来修改。

用JMP去访问一个代码段时候,改变的是CSEIP
JMP 0x48:0x123456 如果0x48是代码段
执行后: CS->0x48 ESP->0x123456

用JMP去访问一个任务段的时候
如果0x48是TSS段描述符,先修改TR寄存器,在用TR.Base指向的TSS中的值修改当前的寄存器。

4.JMP FAR 和 CALL FAR 访问任务段的区别。

这里的访问主要涉及到EFLAG寄存器中的NT
JMP FAR NT = 0 (不保存前一个任务的TSS段选择子 到 Previous Task Link )
以下取自白皮书说明(3-501)
Note that when you execute at task switch with a JMP instruction, the nested task flag (NT) is not set in the EFLAGS
register and the new TSS’s previous task link field is not loaded with the old task’s TSS selector. A return to the
previous task can thus not be carried out by executing the IRET instruction. Switching tasks with the JMP instruc
tion differs in this regard from the CALL instruction which does set the NT flag and save the previous task link infor
mation, allowing a return to the calling task with an IRET instruction
翻译如下
请注意,当您使用 JMP 指令在任务切换时执行操作时,EFLAGs 寄存器中不会设置嵌套任务标志(NT),并且新 TSS 的先前任务链接字段也不会加载旧任务的 TSS 选择器。因此,通过执行 IRET 指令无法返回到上一个任务。使用 JMP 指令进行任务切换在这方面与 CALL 指令不同,CALL 指令会设置 NT 标志并保存先前的任务链接信息,从而允许使用 IRET 指令返回到调用任务。

如果此时使用iretd返回,程序是会触发蓝屏的,切换回之前的任务,寄存器的值不能得到正确的修改,因为此时 Previous Task Link = 0,所以回按照中断返回。

那么我使用jmp far 该怎么返回呢?
可以保存一开始任务的选择子,需要返回的时候,利用JMP FAR 指令返回。



CALL FAR NT = 1 (保存前一个TSS段选择子 到 revious Task Link)
可以使用iretd返回,此时不是中断返回,会去根据(PTL)保存的前一个段选择子去找任务段描述符,从而确定TSS内存的位置,然后更改寄存器的值。

实验

1.找出GDT表所有的TSS段描述符

PixPin_2026-07-30_10-22-46
PixPin_2026-07-30_10-29-51

2.自己构造任务段通过CALL(长调用)实现任务切换

代码示例

#include "stdafx.h"
#include <Windows.h>DWORD dwOK;
DWORD dwESP;
DWORD dwCS;void __declspec(naked)  test()
{dwOK=1;__asm{//int 3;mov eax,esp;mov dwESP,eax;mov word ptr [dwCS],cs;iretd;}
}int main(int argc,char * argv[])
{char stack[100]={0};    //自己构造一个堆栈使用DWORD cr3=0;DWORD test_Address = 0;printf("请输入test_Address:\n");scanf("%x",&test_Address);//下一步构造TSS,标有*说明必填有效值DWORD tss[26]={0x0,        //link0x0,        //esp00x0,        //ss00x0,        //esp10x0,        //ss10x0,        //esp20x0,        //ss20,        //*(DWORD)test_Address,        //eip *0,        //eflags0,        //eax0,        //ecx0,        //edx0,        //ebx((DWORD)stack) + 100,        //esp *0,        //ebp0,        //esi0,        //edi0x23,        //es *0x08,        //cs *0x10,        //ss *0x23,        //ds *0x30,        //fs *0,        //gs0,        //idt0x20ac0000        //IO权限位图,VISTA之后不再用了,从其他结构体拷贝出来};printf("tss_Address: %x\n",(DWORD)tss);printf("请输入CR3:\n");scanf("%x",&cr3);    //通过WinDbg指令进行获取:!process 0 0tss[7] = cr3;char buffer[6] = {0, 0, 0, 0, 0x4B, 0};//构造任务段__asm{call fword ptr [buffer];}printf("切换成功,获取的值:dwESP=%d\tdwCS=%d\n",dwESP,dwCS);return 0;
}

踩坑说明:
CR3字段存储的是该任务所使用的页目录表(Page Directory)的物理基地址。
CR3这里我们需要使用当前程序的CR3值(程序运行后,windbg用 !process 0 0查看)
我们不能使用其他进程的CR3,否则会出现蓝屏错误 或者 虚拟机错误(不能触发蓝屏)

在tss结构中,成员esp的值,我们需要注意,esp也就是换栈的栈顶指针,我们构建堆栈,一定要分清楚构建堆栈的地址在哪里,这个地址决定了我们的栈顶指针ESP

此处构建 任务段
PixPin_2026-07-31_20-10-51

构建任务段描述符
TSS的地址
这里有一个问题这里的tss数组真正的地址在哪里?局部变量是在堆栈里面创建的,在里的地址其实是堆栈里的地址,而不是在反汇编里面看到的地址
fd
这个0x00401096并不算这个数组真正的地址!!!
我们可以通过

printf("%x\n",tss);

PixPin_2026-07-31_20-43-48
获取真正的tss地址。
TSS段描述符应该构建为 0000E912 feac0068
eq ffffffff8003f048 0000E912feac0068


获取CR3(查看当前进程)
PixPin_2026-08-01_13-25-15

实现了任务切换的模拟
PixPin_2026-08-01_13-25-52

总结

在任务段这一块的学习,遇到了很多问题,课程中留了很多问题,有一些坑你必须踩,因为你不知道,而且去查别人的实验,也会发现解释的不够详细,会出现很多问题,花了不少时间,好在结果不差,解决了。

参考资料

TR寄存器,TSS描述符,TSS段 https://www.cnblogs.com/revercc/p/14468330.html

保护模式篇——段和门小结 https://www.cnblogs.com/wingsummer/p/15353642.html