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

ed9*_*9er 8 x86 assembly linux-kernel

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 */ \
Run Code Online (Sandbox Code Playgroud)

命名操作数具有内存约束[prev_sp] "=m" (prev->thread.sp). __switch_canary除非CONFIG_CC_STACKPROTECTOR被定义(然后它是一个加载和存储使用%ebx),被定义为什么都没有.

我理解它是如何工作的,如内核堆栈指针备份/恢复,以及如何push next->eipjmp __switch_to一个ret在功能,这实际上是一个真正的相匹配的"假"调用指令的结束指令ret的指令,并有效地使next->eip返回下一个线程的要点.

我不明白的是,为什么黑客?为什么不干脆call __switch_to,那么之后ret,jmpnext->eip,这是更清洁,方便读者.

Fra*_*kH. 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]
Run Code Online (Sandbox Code Playgroud)

即推送返回地址; 你最终会得到一个序列push/ jmp(== call)/ ret/ jmp如果你不想回到这个地方(并且这段代码没有),你可以通过"伪造"一个电话来节省代码分支,因为你只有做push/ jmp/ ret.代码在这里使尾部递归.

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

  • 但它不会有效地杀死回归预测堆栈吗? (2认同)