起因和缘起
本文献给一位非常努力的穿皮鞋的同事。
有一天,因为一个已经忘却了的原因就没有在公司食堂吃午饭,当然是出去饭店来了一顿更好的咯…餐后,突然就是天昏地暗暴雨倾盆,当我们意识到这场雨一时半会儿停不下来的时候,我们就打了同事的电话,看看能不能帮忙送几把伞过来…
穿着皮鞋跑步总是不会慢的!
过了大概十分钟的样子,同事穿着皮鞋蹚着到小腿肚子深的水过来了…那双皮鞋因此进水了,那双皮鞋因为进水而胖了,那双皮鞋可能也就没有办法继续穿了,那双皮鞋的牌子可能是意尔康,自那以后,同事就不再穿皮鞋了。
这件事已经过去很久,作文而忆起,不禁唏嘘…
问题
近日,曾经因为我们在公司外吃完午饭下暴雨被困而特意来送雨伞从而把皮鞋弄湿后再也不穿皮鞋的 (定语有点长)同事咨询了一个问题,涉及到Linux内核进程调度源码,问题是这样的:
Linux在进程调度时,调用了一个switch_to宏:
/* frame pointer must be last for get_wchan */
#define SAVE_CONTEXT "pushf ; pushq %%rbp ; movq %%rsi,%%rbp\n\t"
#define RESTORE_CONTEXT "movq %%rbp,%%rsi ; popq %%rbp ; popf\t"
#define __EXTRA_CLOBBER \
, "rcx", "rbx", "rdx", "r8", "r9", "r10", "r11", \
"r12", "r13", "r14", "r15"
/* Save restore flags to clear handle leaking NT */
#define switch_to(prev, next, last) \
asm volatile(SAVE_CONTEXT \
"movq %%rsp,%P[threadrsp](%[prev])\n\t" /* save RSP */ \
"movq %P[threadrsp](%[next]),%%rsp\n\t" /* restore RSP */ \
"call __switch_to\n\t" \
"movq "__percpu_arg([current_task])",%%rsi\n\t" \
__switch_canary \
"movq %P[thread_info](%%rsi),%%r8\n\t" \
"movq %%rax,%%rdi\n\t" \
"testl %[_tif_fork],%P[ti_flags](%%r8)\n\t" \
"jnz ret_from_fork\n\t" \
RESTORE_CONTEXT \
: "=a" (last) \
__switch_canary_oparam \
: [next] "S" (next), [prev] "D" (prev), \
[threadrsp] "i" (offsetof(struct task_struct, thread.sp)), \
[ti_flags] "i" (offsetof(struct thread_info, flags)), \
[_tif_fork] "i" (_TIF_FORK), \
[thread_info] "i" (offsetof(struct task_struct, stack)), \
[current_task] "m" (current_task) \
__switch_canary_iparam \
: "memory", "cc" __EXTRA_CLOBBER)
该宏的SAVE_CONTEXT中,为什么要保存rsi寄存器?rsi有什么用?
为了理解这个,理解inline汇编比理解Linux内核的调度细节更加重要。可以说, “为什么保存rsi?” 这个问题和Linux内核调度机制的关系并不大。
为此需要先理解inline汇编的原理和用法。
本文不是专门的inline汇编教程文档,所以这里给出一个链接,如果还不懂inline汇编的可以直接锚过去:
GCC-Inline-Assembly-HOWTO: http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html
后面的篇幅我尽量只说自己的想法。
问题的解释
gcc可以把C源文件编译成汇编码,背后依靠的是超级复杂的编译原理,详情参考圣经龙书。
那么在C文件中自己写一段汇编告诉gcc, “不用gcc你劳神来编译了,按我写的来就行! ,会怎样?当然是很好啊,但是会有问题。
一个C文件混杂这C源码和汇编码,gcc如何保证它使用的寄存器和C文件中程序员自己直接写的汇编使用的寄存器不会相互冲突影响,比如下面:
int i = 3, j = 6, k;
add $0x3, %%rbx
k = i + j;
这段代码让gcc如何是好?
这是比较简单的情况,gcc扫描一遍就知道汇编使用了rbx,然后它小心的避开和rbx冲突即可,但是如果嵌入的汇编逻辑非常复杂,这就意味着gcc不光要理解C语言,还要理解汇编语言,换言之,它同时要做一个汇编语言的汇编器。
所以说,更好的做法是,gcc只关注C代码,至于程序员自己直接写的汇编,不去管它的逻辑,直接inline到它该在的位置即可,为了避免冲突,需要编写这段汇编的程序员告诉gcc,自己动用了哪些寄存器,一切归结到一套接口而不是实现。
嗯,这就是 inline汇编!(确切说应该是扩展inline汇编)
inline汇编的接口和调用如下:
asm $修饰关键字 ( "实际汇编码"
: 输出列表(寄存器,内存…)
: 输入列表(寄存器,内存…)
: 会产生的副作用clobber列表(影响的寄存器,内存,标志寄存器…)
)
如果你希望gcc在inline这段汇编码之后,不再信任之前的某个寄存器的值依然有效,那就将这个寄存器加入到clobber列表即可。
那么是不是说,只要不在clobber列表中的寄存器,gcc就会信任它呢?严格来讲是的。
可以说 inline汇编的input和output就是gcc的结界!gcc不理解也不试图理解结界中发生了什么,它也做不到。 gcc能深入到inline汇编的最深的边界就是input列表和output列表!
我们知道,input列表和output列表都可以包含寄存器,很显然,对于output寄存器,gcc非常明确地知道发生了什么-- 将一个值送到了某个寄存器或者某个地址 ,那么此后离开了inline汇编,gcc依然可以使用这个最后被inline