将jmp指令用于非常量TSS段

时间:2016-11-09 16:11:55

标签: assembly x86 operating-system kernel gas

JMP instruction referece.

根据文档,我们可以jmp执行常量远段:

jmp 0x18:00

此处,0x18是GDT全局描述符表中的有效段选择器。

jmp可以与包含有效GDT条目的段寄存器一起使用,即代码/数据段描述符:

mov es, 0x18
jmp es:0x0

这里,0x18是TSS(任务状态段)描述符,当跳转到时,CPU执行任务切换,自动将其状态保存到当前TSS中,然后填充新TSS中保存的状态

但是,TSS是系统段描述符,因此无法加载到任何段寄存器中(如英特尔文档所建议)。那么,如何在运行时使用动态分配的TSS跳转到任务?

我能想到的唯一方法是使用iret指令,但我觉得它像是一个黑客,因为我需要修改链接字段,然后在EFLAGS中设置NT位以执行反向链接任务切换。

3 个答案:

答案 0 :(得分:6)

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

答案 1 :(得分:4)

您不仅无法使用TSS选择器加载ES,指令jmp es:0x0也无效。没有指令将段寄存器移动到另一个段寄存器(例如,ES到CS)。还没有指令可以从通用寄存器加载CS。正如Margaret Bloom的回答所示,您需要使用带有内存操作数的JMP指令加载CS,特别是将远指针作为内存操作数的指令,这样您就可以获得设置CS的远程跳转指令。

就实现这一点而言,将这个far指针放在任务结构中是有意义的,任务结构是放置任务的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));
}

代码假定一个“任务结构”,其中一个far指针包含为任务分配的TSS选择器(忽略偏移部分)。

从技术上讲,您也可以使用LTR指令后跟JMP指令跳转到任务。这会在不执行任务切换的情况下更改任务,因此不会影响寄存器(TR,CS:EIP和您明确更改的任何其他寄存器除外)。例如:

mov  esi, [new_task]
ltr  [esi + TASK_FAR_JMP_PTR + 4]
jmp  [esi + TASK_TSS + TSS_EIP]

这只有在新任务在0号环上运行并且刚刚启动或在不需要恢复寄存器的已知点停止时才可行。特别是这可以启动初始内核任务(或单个TSS操作系统中的唯一任务)。

请注意,大多数操作系统仅对所有任务使用一个TSS,因此不要使用CPU提供的任务切换机制。对于64位操作系统,这是必需的,因为在长模式下不支持任务切换。

答案 2 :(得分:-1)

这里建议的答案是正确的,但缺少一部分:建议的语法不会产生长跳跃。我做了Margaret Bloom 建议,但它没有工作。我的代码肯定有问题,因为我知道她给了我正确的答案,因为其他人也提出同样的建议。看看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  $0xa0
    30af:       6a 00                   push   $0x0
    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 esp有效地址的ModR / M字节。这意味着,需要一个SiB字节,即偏移量。 (参考:表2-2。带ModR / M字节的32位寻址形式)
  • 0x24SiB字节代表ESP,但没有任何缩放(值为none),实际上,保持相同。 (参考:表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  $0xa0
    308f:       6a 00                   push   $0x0
    3091:       ff 2c 24                ljmp   *(%esp)
    3094:       90                      nop
    3095:       5d                      pop    %ebp
    3096:       c3                      ret  

在上面,生成了ljmp

  • 0xffjmp的操作码,相同。 ljmp只是GAS(GNU汇编程序)用来生成FF /5操作码的特定语法。
  • 0x2c[--][--](无位移)的ModR / M字节,但在表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

感谢大家的建议。没有它,我无法对此进行调查。