-
-
[原创]7.滴水中级班(内核驱动)——代码跨段跳转流程
-
发表于: 2025-9-20 22:41 530
-
代码跨段本质就是修改CS段寄存器。
CS寄存器的作用:CS寄存器存储了当前正在执行的代码段的段选择子(在保护模式下指向GDT或LDT中的段描述符),CPU根据CS:IP(或CS:EIP)的值决定下一条指令的位置。
代码跨段的情形:当程序需要跳转到另一个代码段执行时(比如JMP FAR、CALL FAR、RETF、INT等指令),CPU不仅要修改EIP(指令指针),还必须同时修改CS寄存器,这样才能真正切换到新的代码段,执行那里的指令。跨段的过程:CPU首先检查新的段选择子,查找段描述符,进行权限检查(DPL、CPL、RPL等),通过后加载新的段描述符到CS寄存器,然后更新EIP,最后在CS:EIP指向的地址开始执行新代码。CS的特殊性:不能直接用MOV等指令修改CS,因为CS的改变意味着指令流的改变,必须有专门的指令(如JMP FAR)同时修改CS和EIP。实际意义:代码跨段不仅仅是指令地址的跳转,更是CPU对内存段权限、保护机制的重新检查与切换,这一切的核心就是“修改CS段寄存器”。
举例说明:假设当前在代码段A执行,需要跳转到代码段B,CPU会做以下事情:根据指令(如JMP FAR 0x30:0x12345678)中的段选择子(0x30)查找到段B的段描述符。做权限检查(DPL、CPL、RPL等)。通过后,将段B的段描述符加载到CS寄存器,同时用偏移地址(0x12345678)更新EIP。CPU接下来就会从CS:EIP(即段B的0x12345678处)开始执行。这种从段A到段B的跳转,本质就是改变了CS寄存器的内容,让CPU切换到新的代码段。
补充说明:段内跳转:只用JMP、CALL、RET等指令修改EIP,不修改CS,属于段内跳转,不属于跨段。跨段与权限:跨段跳转还涉及特权级检查,是否能跨段取决于当前权限(CPL)和目标段权限(DPL)。不可随意修改CS:CS寄存器的修改必须严格遵循CPU的保护机制,否则会导致异常或崩溃。
总结:“代码跨段本质就是修改CS段寄存器”这句话抓住了x86架构下代码跨段执行的核心机制——通过修改CS寄存器,切换到新的代码段,再配合EIP的更新,让CPU在另一个内存段中继续执行指令。这种机制不仅实现了代码的跨段执行,也是CPU内存保护、权限管理的基础。


同时修改CS和EIP的指令如下:

只改变EIP的指令比如这里的JMP、CALL、JCC、RET,不仅会修改EIP,还会修改堆栈,修改EIP并不会影响CS。
修改EIP(如JMP、CALL、JCC、RET、RETF等指令):本身只是让CPU从新地址继续取指令。但某些情况下,堆栈也会被修改,这主要是由具体指令的功能决定的,而不是EIP修改本身的副作用。
JMP、JCC:都是直接跳转指令,只修改EIP(或CS+EIP),不会修改堆栈。它们是“跳”过去执行,不保存“返回”信息。CALL:除了修改EIP(或CS+EIP)外,还会把返回地址(即CALL的下一条指令地址)压栈,供RET/RETF指令使用,实现“调用-返回”机制。这就是为什么“CALL会修改堆栈”。RET:从堆栈中弹出返回地址到EIP,恢复栈指针。这个过程也要“修改”堆栈,因为它会弹出压入的数据。RETF:从堆栈中弹出段地址和偏移地址,同时修改CS和EIP,进一步修改栈指针。总结:JMP、JCC不涉及栈;CALL、RET、RETF会修改栈,这是它们指令特性的一部分,而非EIP修改本身的后果。
为什么“修改EIP”不会影响CS?
在x86体系结构中,EIP(或IP)和CS是两个独立的寄存器,EIP只记录当前指令偏移,CS记录当前代码段。它们的修改是分别控制的:段内跳转(near jump/call/ret):只修改EIP(IP),CS不变。CPU继续在当前代码段执行,只是指令地址变了。段间跳转(far jump/call/ret):需要同时修改CS和EIP(IP),这样CPU才会切换到另一个代码段。为什么区分:x86的“分段”内存模型,让程序可以分成多个“段”,每个段有独立的安全、权限、界限等属性。CS的改变意味着代码段的切换,是一个更“危险”的操作,需要额外的保护机制(如权限检查、描述符加载等)。因此,只有专门的指令(如JMP FAR、CALL FAR、RETF等)才能同时修改CS和EIP,而普通的JMP、CALL、RET只改EIP,CS不受影响。
参考表:常见指令对EIP、CS、堆栈的影响
指令 | 修改EIP | 修改CS | 修改堆栈 | 说明 |
JMP | 是 | 否 | 否 | 直接跳转,不保存返回地址 |
JCC | 是 | 否 | 否 | 条件跳转,同JMP |
CALL | 是 | 否 | 是 | 保存返回地址,用于子程序调用 |
RET | 是 | 否 | 是 | 从栈弹出返回地址 |
JMP FAR | 是 | 是 | 否 | 跨段跳转 |
CALL FAR | 是 | 是 | 是 | 跨段调用,保存CS+EIP |
RETF | 是 | 是 | 是 | 跨段返回,弹出CS+EIP |
结论:修改EIP本身不会自动修改堆栈,是否修改堆栈取决于具体指令(CALL、RET、RETF会,JMP、JCC不会)。EIP和CS是独立控制的,普通指令只改EIP不改CS,只有跨段指令才会同时修改CS和EIP。CS的改变意味着代码段的切换,这是x86分段内存和保护机制的核心之一。理解这些细节,对逆向、内核、底层开发都至关重要。


JMP FAR可以叫做长跳转,不同教科书上叫法不一样。Index=4的时候,只有四种情况的段描述符才可以跳转,并且这四种情况里面除了第一个代码段,其余三个都是系统段的内容。这里先关注代码段,并且需要记住的是——通过这个索引查到的这个段描述符必须是一个代码段,数据段不可以。
一、“JMP FAR”与“长跳转”的关系
JMP FAR:这是x86汇编中“跨段跳转”的指令(如jmp far ptr seg:offset),它同时修改CS和EIP(或IP),让CPU切换到另一个代码段继续执行。
长跳转:中文教材有时称JMP FAR为“长跳转”(和“短跳转”、“近跳转”对应,后两者只修改EIP/IP,属于段内跳转)。英文习惯称为“far jump”或“intersegment jump”。叫法不同,本质一致。
二、“Index=4的时候,只有四种情况的段描述符才可以跳转,并且这四种情况里面除了第一个代码段,其余三个都是系统段的内容”
这一段讲的是段选择子(selector)的构造和段描述符表(GDT/LDT)的使用规范。
背景知识
段选择子:16位,格式为 | Index(13位) | TI(1位,GDT/LDT) | RPL(2位,请求特权级) |
Index:即“索引”,决定在GDT或LDT中查找第几个描述符。
GDT/LDT:全局/局部描述符表,每个表项(描述符)定义了一个段的各种属性(基址、界限、特权级、类型等)。
描述符类型:每个描述符的“Type”字段决定了它是代码段、数据段、系统段(如TSS、调用门、任务门等)。
特殊情境界定
Index=4:指段选择子中的“Index”值为4(即二进制100)。
TI=0:假设使用GDT(全局描述符表)。
RPL:暂时忽略,不影响“能否跳转”的讨论。
为什么只有四种段描述符可以跳转?
在x86保护模式下,只有特定类型的段描述符才能通过JMP FAR跳转。具体规则如下:
代码段描述符(Type字段为“可执行”):可以跳转,这是正常函数调用的目标。
系统段描述符(Type字段为“TSS”、“任务门”、“调用门”、“中断门”、“陷阱门”等):有些特殊用途,例如任务切换、调用门/中断门提权等,但这些不是常规的代码段,而是CPU用于特权操作、异常处理、任务切换的特殊机制。
数据段描述符:不可跳转,因为数据段不可执行。
为什么说“Index=4的时候,只有四种情况”?
这里的“四种”是指描述符的Type字段为可跳转的类型(具体取决于CPU架构和操作系统设计,但常见划分如下):
代码段(可执行):普通代码跳转的目标。
TSS(任务状态段):用于任务切换(任务门)。
调用门(Call Gate):用于特权级切换(如从用户态进内核态)。
任务门:用于任务切换(另一种格式)。
这些四种类型在Intel SDM中有明确定义(具体名称和编码可能随手册版本变化,但逻辑一致)。
除了代码段,其他三个都是系统段(属于CPU内部管理,不是普通代码)。
三、“这里先关注代码段,并且需要记住的是——通过这个索引查到的这个段描述符必须是一个代码段,数据段不可以。”
关键点:JMP FAR的目标段(即段选择子所指向的段描述符)必须是一个代码段(Type字段标记为“可执行”),不能是数据段。
为什么不能是数据段:数据段的描述符Type标记为“不可执行”,如果强行JMP FAR到一个数据段,CPU会触发#GP(一般保护性异常),因为数据段不允许CPU从中取指令执行。
系统段(如TSS、调用门等):虽然可以跳转,但不是常规的代码执行流程,属于CPU特殊机制。
四、完整逻辑链(以index=4为例)
假设你执行了jmp far ptr 0x20:0x12345678,其中0x20是段选择子(Index=4, TI=0, RPL=0):
CPU从GDT表中查到第4项(index=4)的描述符。
该描述符必须是代码段(可执行),否则CPU报异常。
如果它是其他类型(如数据段),报错。
如果它是系统段(如TSS、调用门等),CPU会按系统段规则处理(如任务切换、特权切换),但这不是普通代码跳转的流程。
五、逆向/内核安全视角的延伸
GDT/LDT表的篡改:如果攻击者能篡改GDT表,将数据段的Type改为代码段,就能让JMP FAR跳转到数据段执行(ROP攻击、代码注入的早期手段)。
提权/权限检查:JMP FAR(和CALL FAR)涉及CPL、DPL、RPL的检查,是操作系统权限隔离的核心机制之一。
系统段利用:通过伪造调用门、任务门等,可以实现提权、进程切换等高级攻击,这是内核漏洞利用的经典场景。
六、总结
JMP FAR是跨段跳转,必须同时修改CS和EIP。
段选择子的Index决定查哪个描述符,只有代码段(Type=可执行)才能作为JMP FAR的目标。
数据段(Type=不可执行)不能跳转,强行跳转会触发CPU异常。
系统段(TSS、调用门、任务门等)可以跳转,但属于CPU特殊机制,不是常规代码执行流程。
教科书叫法不同:中文常称“长跳转”,英文称“far jump”或“intersegment jump”,本质一致。
判断一个段描述符是否为代码段,是x86保护模式下非常重要的底层知识,涉及CPU对内存权限和代码执行的控制。下面是核心判断方法和相关细节:
一、段描述符的基本结构
在保护模式下,每个段描述符(8字节,通常位于GDT或LDT中)描述了段的基地址、界限、权限、类型等属性。 “类型”属性(Type字段)决定了这个段是代码段、数据段还是系统段。
二、判断方法
第一步:判断S位
S位(Descriptor Type):位于段描述符的“属性字节”(第5字节)的Bit 4(从0开始计数)。
S=1:表示这是一个代码段或数据段(统称“非系统段”)。
S=0:表示这是一个系统段(如TSS、LDT、各种门等)。
重要结论:只有S=1的段描述符才可能是代码段或数据段,S=0的段描述符不可能是代码段。
第二步:判断Type字段(高4位)
Type字段:位于属性字节(第5字节)的低4位(Bit 0~3)。
Bit 3(Executable, 可执行位): 如果Bit 3=1,表示这个段是可执行的,即代码段。 如果Bit 3=0,表示这个段是不可执行的,即数据段(或栈段,栈段是数据段的一种)。
Bit 2(Conforming, 一致位)、Bit 1(Readable, 可读位)、Bit 0(Accessed, 访问位): 用于更细致的权限区分(如一致代码段、非一致代码段、可读代码段等),但判断是不是代码段,只要看Bit 3是否为1。
三、快速识别法
看第5字节(属性字节): 可以简单用十六进制表示法,第5字节(属性字节)> 0x8(即Bit 3=1),就是代码段;≤ 0x8,就是数据段(或栈段)。
S位和Type位是连续的,实际编程中可以一起解析(如属性字节的二进制第4位是S,第3位是Type的Bit 3)。
四、示例
假设一个段描述符的第5字节为0x9A(二进制10011010):
S=1(Bit 4=1):非系统段(代码或数据段)
Type=9(二进制1001,Bit 3=1):可执行(代码段)
结论:这是代码段
再看0x72(二进制01110010):
S=1(Bit 4=1):非系统段
Type=2(二进制0010,Bit 3=0):不可执行(数据段)
结论:这是数据段
五、汇编/逆向中的实际查询
用调试器(如WinDbg、OllyDbg)查看GDT/LDT:每个表项(段描述符)的第5字节就是关键属性字节。
编程解析:在代码中定义结构体,提取S位和Type位(Bit 3)即可判断。
不能仅靠段基址/段限长判断:必须看段描述符的属性字节。
六、总结
判断步骤 | 方法 | 代码段特征 | 数据段特征 |
S位 | 段描述符第5字节Bit 4 | S=1 | S=1 |
Type的Bit 3 | 段描述符第5字节Bit 3(可执行位) | Bit 3=1(可执行) | Bit 3=0(不可执行) |
十六进制速判 | 第5字节 > 0x8 | 是代码段 | 否 |
一句话: 判断一个段描述符是否为代码段,先看其属性字节的S位(Bit 4)是否为1(非系统段),再看其Type位的Bit 3是否为1(可执行);两个条件都满足,就是代码段。
核心:1.S=1且Type的Bit 3=1,就是代码段。2.不能通过段基址、段限长判断,必须解析段描述符的属性字节。
快速识别:属性字节(第5字节)> 0x8就是代码段。

代码段的描述符是需要权限检查的,权限检查的时候要区分一致代码段还是非一致代码段。这里举个例子来说明什么是一致代码段和非一致代码段:比如操作系统提供了一段代码,这段代码提供一些通用的功能,这些功能并不会对系统的内核数据构成破坏,我们希望这些功能可以让应用层直接来访问的,如果有这样的要求,就可以用一致代码段来修饰这段数据,否则,如果不希望应用层直接访问的数据,就可以用非一致代码段。这也就是为什么对于一致代码段,允许CPL的权限比DPL低(也就是数值上大于),一致代码段还有另外一个名字称为共享段,共享段指的就是给低权限的人用的、并不会对其他构成别的影响,否则,内核数据我们不希望用低权限来访问,那么我们就用非一致代码段来修饰,这时CPL必须等于DPL,这也就是为什么代码段会分为一致的和非一致的,因为目的不一样。

指令含义
JMP 0x20:0x004183D 这是一个“远跳转”(JMP FAR),在x86保护模式下表示让CPU跳转到新的段(CS=0x20)和新的偏移地址(EIP=0x004183D)开始执行代码。
CPU执行流程
解析操作数
- 指令中的
0x20是段选择子(selector),代表段寄存器CS要设置为0x20。 - 指令中的
0x004183D是偏移地址,要设置到EIP。
段描述符查找
- CPU通过GDT或LDT表,用Index字段(0x20 >> 3 = 4)找到第4个段描述符项。
- 检查该描述符的类型(Type/S位),必须是“代码段”(可执行)。
权限检查
- CPU会检查特权级(CPL、DPL、RPL)等安全字段,只有权限允许才可以跳转。
- 如果段描述符不是代码段(比如数据段或系统段),则会引发异常,跳转失败。
加载新CS和EIP
- CPU将查到的段描述符内容加载到CS寄存器,将0x004183D加载到EIP寄存器。
开始执行新位置的指令
- CPU从新的CS:EIP位置取指令,继续执行。
关键点解读
JMP FAR本质:不仅修改EIP,还要修改CS实现代码段的切换。这是“跨段跳转”。
段类型必须为代码段:通过段选择子查找的描述符,必须是“Type为可执行”(代码段),否则CPU不会跳转,数据段等不允许取指令执行。
异常处理:如跳转到非代码段,会触发#GP(General Protection Fault)异常。
安全与权限:段描述符内还记录权限,普通应用程序不能随意跳到高权级代码段。

只有第一步、第二步、第三步都完成了,从第四步开始才是真正开始修改CS寄存器,通过上面的权限检查后,CPU会将段描述符加载到CS段寄存器中。加载完后,CPU会将新的Base加上Offset的值写进去,注意这里的Offset就是上面冒号后面的那四个字节,把这两个值加完的值放到EIP中,然后执行EIP,这样一来,这个代码才真正开始执行。
可见,简简单单一行JMP指令,CPU却分了五步去执行它,所以Intel的指令真是非常复杂。这节课要求能把这五步记住。
五步总结:
1.拆分段选择子(Segment Selector)
- 将JMP指令中的段选择子拆成三个部分:
- RPL(请求特权级,2位)
- TI(标志位,决定查GDT还是LDT)
- Index(13位,决定查GDT/LDT的第几个描述符)
2.查GDT或LDT表得到段描述符
- 根据上一部得到的TI和Index,定位到全局描述符表(GDT)或本地描述符表(LDT)中的描述符项。
- 读取段描述符,获得段的基地址、界限、权限、类型等详细信息。
3.权限检查
- 审核段描述符的DPL(描述符特权级)、CPL(当前特权级)、RPL(请求特权级)等,确保跳转目标段允许当前代码访问。
- 若权限不符,CPU拒绝跳转,触发保护异常。
- 还会确认段类型必须是“代码段”,且是否为一致代码段,执行相应的权限安全规则。
4.加载段描述符到CS寄存器
- 经过权限检查,CPU正式将该段描述符加载到CS寄存器,更新CS的内容(段选择子)。
- 同时更新CS的缓存寄存器(基地址、高28位权限界限等),准备执行该段代码。
5.将 CS的基地址 + 指令偏移(Offset)写入 EIP,执行跳转
- 将偏移地址(冒号后面地址)加上CS段的基地址,写入EIP。
- CPU从CS:EIP指向的新地址开始取指令执行。
好记的记忆方法:
方法一:用“拆查检装跳”五字口诀
拆:拆分段选择子(RPL、TI、Index)
查:查GDT/LDT表,得到段描述符
检:权限检查(DPL、CPL、RPL)
装:加载段描述符到CS
跳:跳转执行,EIP=基地址+偏移
用这五个字,顺序记忆流程简单又高效。
方法二:用生活比喻记忆
想象CPU执行jmp far是“去另一个城市(代码段)执行任务”:
拆:先拿出地图指针(拆选择子,定位哪个城市)
查:查城市名录表(查GDT/LDT拿到城市具体信息)
检:看你有没有通行证(权限检查)
装:拿到通行证后,办理入城手续(加载段描述符到CS)
跳:最后走向任务目的地开始工作(计算新地址,更新EIP,执行新段代码)
方法三:图形联想
想象CPU像一个搬家机器人:
先拆开包装盒(选择子拆分)
找到搬家地址(查表找段描述符)
查验身份证(权限检查)
装入新家钥匙(加载段描述符到CS)
进入新家开始生活(EIP跳转执行)

最后再总结一下一致代码段和非一致代码段的问题。如上图所示。