我正在研究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)。
ljmp %0
如何像ljmp $section, $offset
一样工作,以及为什么此指令使用%0
。 %0
只是__tmp.a
的地址吗? ljmp
指令时,"cmpl %%ecx,_last_task_used_math\n\t"
的地址是否正确? 最佳答案
这个语法甚至是什么意思?
这个难以理解的困惑是GCC的Extended ASM,其一般格式为
asm [volatile] ( AssemblerTemplate
: OutputOperands
[ : InputOperands
[ : Clobbers ] ] )
在这种情况下,
__asm__
语句仅包含AssemblerTemplate
和InputOperands
。输入操作数部分解释%0
和%1
的含义,以及ecx
和edx
如何获得其值:"m" (*&__tmp.a)
,因此%0
成为__tmp.a
的 m emory地址(老实说,我不确定为什么这里需要*&
)。 "m" (*&__tmp.b)
,因此%1
成为__tmp.b
的和 Emory地址。 "d" (_TSS(n))
,因此当此代码启动时, D X寄存器将包含_TSS(n)
。 "c" ((long) task[n])
,因此当此代码启动时,E C X寄存器将包含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
)指令有两种形式。您知道的一个(opcode EA
)需要两个立即参数:一个用于段,一个用于偏移量。这里使用的一个(操作码FF /5
)是不同的:segment和address参数不在代码流中,而是在内存中的某个地方,并且指令指向该地址。在这种情况下,
ljmp
的参数指向__tmp
结构的开头。前四个字节(__tmp.a
)包含偏移量,其后的两个字节(__tmp.b
的下半部分)包含段。间接
ljmp __tmp.a
等效于ljmp [__tmp.b]:[__tmp.a]
,除了ljmp segment:offset
只能接受立即参数。如果要切换到任意的TSS而无需自行修改代码(这是一个糟糕的主意),则可以使用间接指令。另请注意,
__tmp.a
从未初始化。我们可以假设_TSS(n)
指的是任务门(因为这是您使用TSS进行上下文切换的方式),并且“通过”任务门的跳转偏移量将被忽略。旧指令指针在哪里?
这段代码没有在TSS中存储旧的EIP。
(我在此之后进行猜测,但是我认为这种猜测是合理的。)
旧的EIP存储在与旧任务相对应的内核空间堆栈中。
Linux 0.11为每个任务分配一个ring 0堆栈(即内核的堆栈)(请参阅
copy_process
中的fork.c
函数,该函数初始化TSS)。当任务A期间发生中断时,旧的EIP将保存在内核空间堆栈中,而不是用户空间堆栈中。如果内核决定切换到任务B,则也将切换内核空间堆栈。当内核最终切换回任务A时,该堆栈又切换回去,并且通过iret
我们可以返回到任务A所在的位置。关于assembly - ljmp指令在linux内核fork系统调用中起什么作用?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/33783692/