为什么switch_to使用push + jmp + ret来改变EIP,而不是直接改变jmp?

时间:2013-02-22 08:41:22

标签: assembly linux-kernel x86

linux/arch/x86/include/asm/switch_to.h中,有宏switch_to的定义,真正的线程切换奇迹读取的关键线就是这样(直到Linux 4.7改变时):

asm volatile("pushfl\n\t"       /* save    flags */ \
              pushl %%ebp\n\t"      /* save    EBP   */ \
              "movl %%esp,%[prev_sp]\n\t"   /* save    ESP   */ \
              "movl %[next_sp],%%esp\n\t"   /* restore ESP   */ \
              "movl $1f,%[prev_ip]\n\t" /* save    EIP   */ \
              "pushl %[next_ip]\n\t"    /* restore EIP   */ \
              __switch_canary                   \
              "jmp __switch_to\n"   /* regparm call  */ \
              "1:\t"                        \
              "popl %%ebp\n\t"      /* restore EBP   */ \
              "popfl\n"         /* restore flags */ \

命名的操作数具有内存约束,如[prev_sp] "=m" (prev->thread.sp)。除非__switch_canary已定义(然后使用CONFIG_CC_STACKPROTECTOR加载和存储),否则%ebx被定义为空。

我理解它是如何工作的,比如内核堆栈指针备份/恢复,以及push next->eipjmp __switch_to在函数末尾带有ret指令的方式,实际上是与真实ret指令匹配的“伪”调用指令,有效地使next->eip成为下一个线程的返回点。

我不明白的是,为什么黑客?为什么不只是call __switch_to,然后是retjmpnext->eip,这更干净,读者友好。

1 个答案:

答案 0 :(得分:6)

这样做有两个原因。

一个是允许[next_ip]的操作数/寄存器分配的完全灵活性。如果您希望能够在jmp %[next_ip] 之后执行call __switch_to ,则必须将%[next_ip]分配给非易失性寄存器(即,通过ABI定义,在进行函数调用时保留其值)。

这引入了编译器优化能力的限制,context_switch()('调用者' - 使用switch_to())的结果代码可能不尽如人意。但为了什么好处?

嗯 - 这就是第二个原因,真的,没有,因为call __switch_to相当于:

pushl 1f
jmp __switch_to
1: jmp %[next_ip]

即。它推送返回地址;您最终会得到一个序列push / jmp== call)/ ret / jmp,如果您不想返回这个地方(并且此代码没有),您通过“伪造”一个电话来保存代码分支,因为您只需要执行push / jmp / ret。代码在这里使尾递归

是的,这是一个小优化,但避免分支减少延迟和延迟对于上下文切换至关重要。