在Linux内核中的fork系统调用中,ljmp指令是用来做什么的?

8

我正在学习Linux内核源码(旧版本0.11v)。当我查看关于fork系统调用的信息时,发现有一些用于上下文切换的汇编代码如下:

/*
 * 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会改变寄存器,包括eip,以进行新任务。

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

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


2
这段代码很难读,如果Linus加上一些注释会更好。ljmp %0将跳转到存储在内存地址%0中的48位地址。因此,它将有效地跳转到存储在内存地址__tmp中的地址。您将观察到movw %%dx,%1有效地使用值_TSS(n)初始化了__tmp.b。*_TSS(n)将是任务门的段描述符。您会注意到%0__tmp.a)未初始化。由于通过任务门时忽略了偏移量(__tmp.a*表示),因此不需要初始化它。实际上,您将执行一个ljmp到segment:offset _TSS(n):garbage。 - Michael Petch
在ljmp之后,cmpl %%ecx,_last_task_used_math将在新任务的上下文中执行。我没有查看旧内核源代码,但似乎*_last_task_used_math*是最后一个使用数学指令的任务的任务ID。如果它与当前任务ID不同,则避免使用clts指令。 - Michael Petch
是的@Jester,我只是在评论那个旧内核,因为这似乎是OP感兴趣的。自0.11以来,Linux已经走了很长的路;) - Michael Petch
据我所见,Linux 0.11的源代码确实在这个宏定义之前有一条注释。如果是这样,OP应该在复制代码时将其一并复制。感谢您对任务门控的解释,这是我从未了解过的事情,也正是我无法解释的事情之一。 - user824425
@Rhymoid 抱歉,我更新了评论。 - bongsu
显示剩余10条评论
1个回答

4

这个语法是什么意思?

这个看起来不可读的代码混乱是 GCC 的 扩展汇编语法,它的一般格式为:

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

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

  • 第一个输入操作数是"m" (*&__tmp.a),所以%0成为__tmp.amemory地址(老实说,我不确定这里为什么需要*&)。
  • 第二个输入操作数是"m" (*&__tmp.b),所以%1成为__tmp.bmemory地址。
  • 第三个输入操作数是"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为每个任务(请参阅fork.c中的copy_process函数,该函数初始化TSS)分配一个环形0堆栈(即内核的堆栈)。当在任务A期间发生中断时,旧EIP保存在内核空间堆栈而不是用户空间堆栈上。如果内核决定切换到任务B,则也会切换内核空间堆栈。当内核最终切换回任务A时,此堆栈也会切换回来,并且通过iret我们可以返回到任务A中的原始位置。


一个很好的补充说明是关于数学协处理器如何与TS标志相关联的解释。 - user824425
1
感谢您将我的评论形式化并将其放入社区维基(赞)。我没有这样做的唯一原因是我没有查看内核代码以确认实际发生了什么。至于协处理器,在任务切换时,TS标志会被设置,以便下一个协处理器操作引发可以捕获的异常。当捕获异常时,内核可以将协处理器状态保存在堆栈上。如果前一个任务和当前任务相同,则清除它,因为没有必要保存协处理器状态并避免引发异常。 - Michael Petch

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接