Assembly ljmp指令在linux内核fork系统调用中做什么?
我正在学习linux内核源代码(旧版本0.11v)。 当我检查fork系统调用时,有一些asm代码用于上下文切换,如下所示:Assembly ljmp指令在linux内核fork系统调用中做什么?,assembly,linux-kernel,x86,gnu-assembler,att,Assembly,Linux Kernel,X86,Gnu Assembler,Att,我正在学习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 switche
/*
* 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
指令必须使用\u TSS(n),xx
。
我们不需要提供有意义的偏移量值,因为cpu将更改cpu的寄存器,包括新任务的eip
ljmp%0
如何像ljmp$section、$offset
那样工作,也不知道为什么这条指令使用%0
。%0
只是\uu tmp.a
的地址吗ljmp
指令时,CPU可能会将EIP寄存器保存到TSS以用于旧任务。旧任务的EIP值是“cmpl%%ecx,\u last\u task\u used\u math\n\t”
的地址,对吗 asm [volatile] ( AssemblerTemplate
: OutputOperands
[ : InputOperands
[ : Clobbers ] ] )
在这种情况下,\uu asm\uu
语句只包含一个AssemblerTemplate
和InputOperands
。输入操作数部分解释了%0
和%1
的含义,以及ecx
和edx
如何获取其值:
- 第一个输入操作数是
,因此“m”(*&&uu tmp.a)
成为%0
的m内存地址(老实说,我不知道这里为什么需要\uu tmp.a
)*&
- 第二个输入操作数是
,因此“m”(*&uu tmp.b)
成为%1
的m内存地址\uu tmp.b
- 第三个输入操作数是
,因此当此代码启动时,dX寄存器将包含“d”(\u TSS(n))
\u TSS(n)
- 第四个输入操作数是
,因此当此代码启动时,EcX寄存器将包含“c”((长)任务[n])
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
的参数指向\uu tmp
结构的开头。前四个字节(\u tmp.a
)包含偏移量,后面的两个字节(\u tmp.b
的下半部分)包含段
这个间接的ljmp\uu tmp.a
相当于ljmp[\uu tmp.b]:[\uu tmp.a]
,除了ljmp段:offset
只能接受立即参数。如果您想切换到任意TSS而不使用自修改代码(这是一个糟糕的想法),那么可以使用间接指令
还要注意,\uu tmp.a
从未初始化。我们可以假设\u TSS(n)
引用任务门(因为这是使用TSS进行上下文切换的方式),并且忽略“通过”任务门的跳转偏移量
旧的指令指针到哪里去了?
这段代码不在TSS中存储旧的EIP
(我在这一点上进行猜测,但我认为这一猜测是合理的。)
旧EIP存储在与旧任务对应的内核空间堆栈上
Linux 0.11为每个任务分配一个环0堆栈(即内核的堆栈)(参见
fork.c
中的copy\u进程函数,它初始化TSS)。在任务A期间发生中断时,旧的EIP保存在内核空间堆栈而不是用户空间堆栈上。如果内核决定切换到任务B,那么内核空间堆栈也会切换。当内核最终切换回任务A时,这个堆栈被切换回,通过iret
我们可以返回到任务A中的位置。读起来很糟糕,Linus的一些评论可能会很好ljmp%0
将跳转到内存地址%0中包含的48位地址。因此,它将ljmp
有效地发送到内存地址\uu tmp
中包含的地址。您将看到movw%%dx,%1
使用值\u TSS(n)
有效初始化\u tmp.b
_TSS(n)将是任务门的段描述符。您会注意到%0
(\uu tmp.a)未初始化。不需要这样做,因为当通过任务门ljmp时,会忽略偏移量(即_tmp.a表示的偏移量)。实际上,您可以执行ljmp来分段:offset _TSS(n):garbage.ljmp _TSS(n):当_TSS(n)表示任务门选择器时,垃圾将基于任务选择器切换任务,忽略偏移量(因此不需要设置为任何值),并在新任务上下文中ljmp之后继续执行指令。cmpl%%ecx,_上次使用的任务将在ljmp之后的新任务上下文中执行。我没有看过旧的内核源代码,但似乎_last_task_used_math是使用数学指令的上一个任务的taskid。如果它与当前的taskid不同,那么就避免使用clts
指令。还要注意的是,TSS任务切换在linux中已被放弃,因此它只是具有历史意义。是的@Jester,我只是对旧内核进行了评论,因为这似乎是OP感兴趣的。Linux自0.11以来已经走过了漫长的道路;)一个受欢迎的补充是解释数学协处理器与TS标志的关系