对非常量 TSS 段使用 jmp 指令
Use jmp instruction to a non-constant TSS segment
根据文档,我们可以对常量远段执行jmp
:
jmp 0x18:00
这里,0x18
是GDT中有效的段选择符,全局描述符Table。
jmp
可以与包含有效 GDT 条目的段寄存器一起使用,即 code/data 段描述符:
mov es, 0x18
jmp es:0x0
这里,0x18
是一个TSS(Task State Segment)描述符,当跳转到的时候,CPU执行一个任务切换,自动将其状态保存到当前TSS中,然后填充状态保存在新的 TSS 中。
但是,TSS 是一个系统段描述符,因此不能加载到任何段寄存器中(如 Intel 文档所建议的)。那么如何才能在运行时使用动态分配的 TSS 跳转到任务?
我能想到的唯一方法是使用iret
指令,但我觉得它很hack,因为我需要修改link字段,然后设置NT位在 EFLAGS 中执行返回 link 任务切换。
push WORD <TSS_selector>
push DWORD 0
jmp FAR [esp]
假定 32 位代码和可用堆栈。
这将使调用线程中的堆栈不平衡且未对齐,您可能需要使用专用内存位置:
mov WORD [tss_pointer + 4], <TSS_selector>
jmp FAR [tss_pointer]
tss_pointer dd 0, dw 0
不仅不能用TSS选择器加载ES,指令jmp es:0x0
也是无效的。没有将段寄存器移动到另一个段寄存器的指令(例如 ES 到 CS)。也没有从通用寄存器加载 CS 的指令。正如 Margaret Bloom 的回答所示,您需要使用 JMP 指令加载 CS,该指令采用内存操作数,特别是采用远指针作为内存操作数的指令,因此您可以获得设置 CS 的远跳转指令。
就实现这一点而言,将此远指针放在您的任务结构中是有意义的,该结构是您放置任务的 TSS 和其他任务特定信息的地方。例如,要切换任务,您可以使用如下代码:
struct task {
struct {
unsigned offset;
unsigned short selector;
} far_jmp_ptr;
struct tss tss;
// ...
};
void
switch_tasks(struct task *new_task) {
asm("jmp FAR PTR %0" : : "m" (new_task->far_jmp_ptr));
}
代码假定 "task structure" 带有远指针,其中包含为任务分配的 TSS 选择器(忽略偏移部分)。
从技术上讲,您还可以通过使用 LTR 指令后跟 JMP 指令来跳转到任务。这会在不执行任务切换的情况下更改任务,因此没有寄存器(除了 TR、CS:EIP 和您明确更改的任何其他寄存器)受到影响。例如:
mov esi, [new_task]
ltr [esi + TASK_FAR_JMP_PTR + 4]
jmp [esi + TASK_TSS + TSS_EIP]
只有当新任务 运行 在 ring 0 并且刚刚开始或在不需要恢复其寄存器的已知点停止时,这才实用。特别是这就是您启动初始内核任务(或单个 TSS 操作系统中的唯一任务)的方式。
请注意,大多数操作系统只为所有任务使用一个 TSS,因此不要使用 CPU 提供的任务切换机制。对于 64 位操作系统,这是必需的,因为长模式不支持任务切换。
此处建议的答案是正确的,但缺少一块:建议的语法不会生成长跳转。我做了
建议,但没有用。我的代码一定有问题,因为我知道她给了我正确的答案,因为其他人也提出了同样的建议。查看 GDB,当我应用上述语法时:
asm("pushw 0xa0");
asm("pushd 0x0");
asm("jmp far [esp]");
(以上语法为内联汇编,GCC风格)
查看GDB,jmp far
生成为:
0x30a9 <task1_start+1> mov ebp,esp
0x30ab <task1_start+3> pushw 0xa0
0x30af <task1_start+7> push 0x0
0x30b1 <task1_start+9> jmp DWORD PTR [esp+0xff06]
显然,[esp + 0xff06]
对我来说并不遥远。这是一个接近跳跃,与 esp
有偏移。更明显的是,从 objdump
:
的输出
000030a8 <task1_start>:
30a8: 55 push %ebp
30a9: 89 e5 mov %esp,%ebp
30ab: 66 68 a0 00 pushw [=12=]xa0
30af: 6a 00 push [=12=]x0
30b1: ff a4 24 06 ff 00 00 jmp *0xff06(%esp)
30b8: 90 nop
30b9: 5d pop %ebp
30ba: c3 ret
注意 0x30ab
处的操作码,它对应于 jmp
指令。查看英特尔手册,该操作码用于近距离跳跃:
0xff
代表jmp
指令。
0xa4
是[--][--] + disp32
的ModR/M字节esp
的有效地址。这意味着,需要一个 SiB 字节,即偏移量。 (参考:Table 2-2. ModR/M 字节的 32 位寻址形式)
0x24
是 SiB
字节代表 ESP,但没有任何缩放(值为 none
),有效地保持不变。 (参考:Table 2-3. SIB 字节的 32 位寻址形式)。
上面生成的jmp
对应的是FF /4
操作码(参考:jmp instruction),意思是近跳转,因为生成的ModR/M字节是0xa4
.远跳的正确操作码是 FF /5
.
很明显,我必须为汇编器做一些事情来生成跳远。所以,事实证明,使用 ljmp
指令而不是像这样的 jmp far
语法很容易修复:
ljmp [esp]
之后,我们得到了正确生成的代码:
00003088 <task1_start>:
3088: 55 push %ebp
3089: 89 e5 mov %esp,%ebp
308b: 66 68 a0 00 pushw [=14=]xa0
308f: 6a 00 push [=14=]x0
3091: ff 2c 24 ljmp *(%esp)
3094: 90 nop
3095: 5d pop %ebp
3096: c3 ret
上面生成了ljmp
:
0xff
是jmp
的操作码,一样。 ljmp
只是 GAS(GNU 汇编器)用来生成 FF /5
操作码的一种特殊语法。
0x2c
是 [--][--]
的 ModR/M 字节(无位移),但在 table 2-2 中的 5
列。这意味着,这个操作码确实是 FF /5
.
0x24
同近跳,也就是不缩放
这是 GDB 看到的实际代码:
0x308b <task1_start+3> pushw 0xa0
0x308f <task1_start+7> push 0x0
0x3091 <task1_start+9> jmp FWORD PTR [esp]
现在,FWORD
是新东西,但至少它不再添加随机位移了。事实上,任务被正确切换到 0xa0
.
谢谢大家的建议。没有它,我永远无法调查清楚。
根据文档,我们可以对常量远段执行jmp
:
jmp 0x18:00
这里,0x18
是GDT中有效的段选择符,全局描述符Table。
jmp
可以与包含有效 GDT 条目的段寄存器一起使用,即 code/data 段描述符:
mov es, 0x18
jmp es:0x0
这里,0x18
是一个TSS(Task State Segment)描述符,当跳转到的时候,CPU执行一个任务切换,自动将其状态保存到当前TSS中,然后填充状态保存在新的 TSS 中。
但是,TSS 是一个系统段描述符,因此不能加载到任何段寄存器中(如 Intel 文档所建议的)。那么如何才能在运行时使用动态分配的 TSS 跳转到任务?
我能想到的唯一方法是使用iret
指令,但我觉得它很hack,因为我需要修改link字段,然后设置NT位在 EFLAGS 中执行返回 link 任务切换。
push WORD <TSS_selector>
push DWORD 0
jmp FAR [esp]
假定 32 位代码和可用堆栈。
这将使调用线程中的堆栈不平衡且未对齐,您可能需要使用专用内存位置:
mov WORD [tss_pointer + 4], <TSS_selector>
jmp FAR [tss_pointer]
tss_pointer dd 0, dw 0
不仅不能用TSS选择器加载ES,指令jmp es:0x0
也是无效的。没有将段寄存器移动到另一个段寄存器的指令(例如 ES 到 CS)。也没有从通用寄存器加载 CS 的指令。正如 Margaret Bloom 的回答所示,您需要使用 JMP 指令加载 CS,该指令采用内存操作数,特别是采用远指针作为内存操作数的指令,因此您可以获得设置 CS 的远跳转指令。
就实现这一点而言,将此远指针放在您的任务结构中是有意义的,该结构是您放置任务的 TSS 和其他任务特定信息的地方。例如,要切换任务,您可以使用如下代码:
struct task {
struct {
unsigned offset;
unsigned short selector;
} far_jmp_ptr;
struct tss tss;
// ...
};
void
switch_tasks(struct task *new_task) {
asm("jmp FAR PTR %0" : : "m" (new_task->far_jmp_ptr));
}
代码假定 "task structure" 带有远指针,其中包含为任务分配的 TSS 选择器(忽略偏移部分)。
从技术上讲,您还可以通过使用 LTR 指令后跟 JMP 指令来跳转到任务。这会在不执行任务切换的情况下更改任务,因此没有寄存器(除了 TR、CS:EIP 和您明确更改的任何其他寄存器)受到影响。例如:
mov esi, [new_task]
ltr [esi + TASK_FAR_JMP_PTR + 4]
jmp [esi + TASK_TSS + TSS_EIP]
只有当新任务 运行 在 ring 0 并且刚刚开始或在不需要恢复其寄存器的已知点停止时,这才实用。特别是这就是您启动初始内核任务(或单个 TSS 操作系统中的唯一任务)的方式。
请注意,大多数操作系统只为所有任务使用一个 TSS,因此不要使用 CPU 提供的任务切换机制。对于 64 位操作系统,这是必需的,因为长模式不支持任务切换。
此处建议的答案是正确的,但缺少一块:建议的语法不会生成长跳转。我做了
asm("pushw 0xa0");
asm("pushd 0x0");
asm("jmp far [esp]");
(以上语法为内联汇编,GCC风格)
查看GDB,jmp far
生成为:
0x30a9 <task1_start+1> mov ebp,esp
0x30ab <task1_start+3> pushw 0xa0
0x30af <task1_start+7> push 0x0
0x30b1 <task1_start+9> jmp DWORD PTR [esp+0xff06]
显然,[esp + 0xff06]
对我来说并不遥远。这是一个接近跳跃,与 esp
有偏移。更明显的是,从 objdump
:
000030a8 <task1_start>:
30a8: 55 push %ebp
30a9: 89 e5 mov %esp,%ebp
30ab: 66 68 a0 00 pushw [=12=]xa0
30af: 6a 00 push [=12=]x0
30b1: ff a4 24 06 ff 00 00 jmp *0xff06(%esp)
30b8: 90 nop
30b9: 5d pop %ebp
30ba: c3 ret
注意 0x30ab
处的操作码,它对应于 jmp
指令。查看英特尔手册,该操作码用于近距离跳跃:
0xff
代表jmp
指令。0xa4
是[--][--] + disp32
的ModR/M字节esp
的有效地址。这意味着,需要一个 SiB 字节,即偏移量。 (参考:Table 2-2. ModR/M 字节的 32 位寻址形式)0x24
是SiB
字节代表 ESP,但没有任何缩放(值为none
),有效地保持不变。 (参考:Table 2-3. SIB 字节的 32 位寻址形式)。
上面生成的jmp
对应的是FF /4
操作码(参考:jmp instruction),意思是近跳转,因为生成的ModR/M字节是0xa4
.远跳的正确操作码是 FF /5
.
很明显,我必须为汇编器做一些事情来生成跳远。所以,事实证明,使用 ljmp
指令而不是像这样的 jmp far
语法很容易修复:
ljmp [esp]
之后,我们得到了正确生成的代码:
00003088 <task1_start>:
3088: 55 push %ebp
3089: 89 e5 mov %esp,%ebp
308b: 66 68 a0 00 pushw [=14=]xa0
308f: 6a 00 push [=14=]x0
3091: ff 2c 24 ljmp *(%esp)
3094: 90 nop
3095: 5d pop %ebp
3096: c3 ret
上面生成了ljmp
:
0xff
是jmp
的操作码,一样。ljmp
只是 GAS(GNU 汇编器)用来生成FF /5
操作码的一种特殊语法。0x2c
是[--][--]
的 ModR/M 字节(无位移),但在 table 2-2 中的5
列。这意味着,这个操作码确实是FF /5
.0x24
同近跳,也就是不缩放
这是 GDB 看到的实际代码:
0x308b <task1_start+3> pushw 0xa0
0x308f <task1_start+7> push 0x0
0x3091 <task1_start+9> jmp FWORD PTR [esp]
现在,FWORD
是新东西,但至少它不再添加随机位移了。事实上,任务被正确切换到 0xa0
.
谢谢大家的建议。没有它,我永远无法调查清楚。