【问题标题】:What does the ljmp instruction do in the linux kernel fork system call?linux内核fork系统调用中的ljmp指令有什么作用?
【发布时间】:2016-02-20 09:40:37
【问题描述】:

我正在研究linux内核源码(旧版本0.11v)。 当我检查 fork 系统调用时,有一些用于上下文切换的 asm 代码,如下所示:

/*
 * switch_to(n) should switch tasks to task nr n, first
 * checking that n isn't the current task, in which case it does nothing.
 * This also clears the TS-flag if the task we switched to has used
 * tha math co-processor latest.
 */
#define switch_to(n) {\
struct {long a,b;} __tmp; \
__asm__("cmpl %%ecx,current\n\t" \
    "je 1f\n\t" \
    "movw %%dx,%1\n\t" \
    "xchgl %%ecx,current\n\t" \
    "ljmp *%0\n\t" \
    "cmpl %%ecx,last_task_used_math\n\t" \
    "jne 1f\n\t" \
    "clts\n" \
    "1:" \
    ::"m" (*&__tmp.a),"m" (*&__tmp.b), \
    "d" (_TSS(n)),"c" ((long) task[n])); \
}

我猜"ljmp %0\n\t" 将适用于更改 TSS 和 LDT。 我知道ljmp 指令需要两个参数,比如ljmp $section, $offset。 我认为ljmp 指令必须使用_TSS(n), xx。 我们不需要提供有意义的偏移值,因为 cpu 会更改 cpu 的寄存器,包括新任务的 eip。

  1. 我不知道ljmp %0 是如何像ljmp $section, $offset 一样工作的,以及为什么这条指令使用%0%0只是__tmp.a的地址吗?

  2. 在执行ljmp 指令时,CPU 可能会将 EIP 寄存器保存到旧任务的 TSS 中。旧任务的 EIP 值是"cmpl %%ecx,_last_task_used_math\n\t" 地址,我说的对吗?

【问题讨论】:

  • 这很难读,Linus 的一些 cmets 可能还不错。 ljmp %0 将跳转到内存地址 %0 中包含的 48 位地址。如此有效地将ljmp 指向内存地址__tmp 中包含的地址。您将观察到movw %%dx,%1 有效地将__tmp.b 初始化为_TSS(n)_TSS(n) 将是任务门的段描述符。您会注意到 %0 (__tmp.a) 未初始化。不需要,因为当您通过任务门 ljmp 时,偏移量(__tmp.a 表示)会被忽略。实际上,您执行 ljmp 以分段:偏移 _TSS(n):garbage。
  • ljmp _TSS(n):garbage 当_TSS(n)代表一个任务门选择器时会根据任务选择器切换任务,忽略偏移量(所以不需要设置任何东西) ,并在新任务上下文中继续执行 ljmp 之后的指令。
  • cmpl %%ecx,_last_task_used_math 将在 ljmp 之后的新任务的上下文中执行。我没有看过旧的内核源代码,但似乎 _last_task_used_math 是最后一个使用数学指令的任务的任务 ID。如果它与当前 taskid 不同,则避免使用 clts 指令。
  • 另请注意,TSS 任务切换在 linux 中已被放弃,因此仅具有历史意义。
  • 是的@Jester,我只是在评论那个旧内核,因为这似乎是 OP 感兴趣的内容。Linux 自 0.11 以来已经走过了漫长的道路;)

标签: assembly linux-kernel x86 gnu-assembler att


【解决方案1】:

这个语法到底是什么意思?

这个不可读的混乱是 GCC 的Extended ASM,它的一般格式为

 asm [volatile] ( AssemblerTemplate
                : OutputOperands
              [ : InputOperands
              [ : Clobbers ] ] )

在这种情况下,__asm__ 语句仅包含 AssemblerTemplateInputOperands。输入操作数部分解释了%0%1 的含义,以及ecxedx 如何获得它们的值:

  • 第一个输入操作数是"m" (*&__tmp.a),所以%0变成了__tmp.am内存地址(老实说,我不知道为什么这里需要*& )。
  • 第二个输入操作数是"m" (*&__tmp.b),所以%1变成了__tmp.bm内存地址。
  • 第三个输入操作数是"d" (_TSS(n)),所以当这段代码开始时,DX寄存器将包含_TSS(n)
  • 第四个输入操作数是"c" ((long) task[n]),因此当此代码开始时,ECX 寄存器将包含task[n]

清理后,代码可以解释如下

    cmpl %ecx, _current
    je 1f

    movw %dx, __tmp.b          ;; the address of __tmp.b
    xchgl %ecx, _current
    ljmp __tmp.a               ;; the address of __tmp.a

    cmpl %ecx, _last_task_used_math
    jne 1f
    clts
1:

ljmp %0 怎么可能工作?

请注意ljmp(也称为jmpf)指令有两种形式。你知道的那个(操作码EA)有两个直接参数:一个用于段,一个用于偏移。这里使用的(操作码FF /5)不同:段和地址参数不在代码流中,而是在内存中的某处,指令指向地址。

在这种情况下,ljmp 的参数指向 __tmp 结构的开头。前四个字节 (__tmp.a) 包含偏移量,后面的两个字节(__tmp.b 的下半部分)包含段。

这个间接的ljmp __tmp.a 将等价于ljmp [__tmp.b]:[__tmp.a],除了ljmp segment:offset 只能接受直接参数。如果您想在没有自修改代码的情况下切换到任意 TSS(这将是一个糟糕的想法),那么可以使用间接指令。

还要注意__tmp.a 永远不会初始化。我们可以假设_TSS(n) 指的是一个任务门(因为这是您使用 TSS 进行上下文切换的方式),并且忽略“通过”任务门的跳转偏移量。

旧指令指针去哪了?

这段代码不会将旧的 EIP 存储在 TSS 中。

(我在这点之后猜测,但我认为这个猜测是合理的。)

旧的 EIP 存储在与旧任务对应的内核空间堆栈中。

Linux 0.11 为每个任务分配一个 ring 0 堆栈(即内核堆栈)(参见 fork.c 中的 copy_process 函数,它初始化 TSS)。当任务 A 期间发生中断时,旧的 EIP 将保存在内核空间堆栈而不是用户空间堆栈中。如果内核决定切换到任务 B,内核空间堆栈也会被切换。当内核最终切换回任务 A 时,此堆栈被切换回,通过 iret 我们可以返回到任务 A 中的位置。

【讨论】:

  • 一个受欢迎的补充是解释数学协处理器如何与 TS 标志相关。
  • 感谢您将我的 cmets 形式化并将它们放入社区 wiki(赞)。我没有这样做的唯一原因是因为我没有去查看内核代码来确认到底发生了什么。至于协处理器,当有一个任务切换时,TS 标志被设置,以便下一个协处理器操作抛出一个可以被捕获的异常。当异常被捕获时,内核可以将协处理器状态保存在堆栈上。如果前一个任务和当前任务相同,则清除它,因为不需要保存协处理器状态并避免抛出异常
猜你喜欢
  • 1970-01-01
  • 2015-12-22
  • 1970-01-01
  • 1970-01-01
  • 2010-09-20
  • 1970-01-01
  • 1970-01-01
  • 2020-07-17
  • 2012-11-18
相关资源
最近更新 更多